From 4b8cd18f5dbebb162a233c2e58525762a8912426 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 9 Oct 2017 19:43:06 +0800 Subject: [PATCH 001/520] Merge with EMQ X project --- .gitignore | 4 +- .travis.yml | 1 + LICENSE-MPL-RabbitMQ | 455 +++++++++++++++++ Makefile | 31 +- {doc => docs}/.placeholder | 0 {doc => docs}/MQTT-SN_spec_v1.2.pdf | Bin {doc => docs}/README | 0 {doc => docs}/mqtt-v3.1.1.pdf | Bin erlang.mk | 2 +- etc/{emq.conf => emqx.conf} | 59 ++- include/{emqttd.hrl => emqx.hrl} | 2 +- include/{emqttd_cli.hrl => emqx_cli.hrl} | 0 ...{emqttd_internal.hrl => emqx_internal.hrl} | 22 +- .../{emqttd_protocol.hrl => emqx_mqtt.hrl} | 0 include/emqx_rest.hrl | 17 + include/{emqttd_trie.hrl => emqx_trie.hrl} | 0 priv/{emq.schema => emqx.schema} | 362 ++++++++------ src/emqttd.app.src | 12 - src/emqttd.erl | 187 ------- src/emqttd_app.erl | 242 --------- src/emqttd_sup.erl | 55 -- src/emqx.erl | 279 +++++++++++ ...ss_control.erl => emqx_access_control.erl} | 8 +- ...d_access_rule.erl => emqx_access_rule.erl} | 15 +- ...acl_internal.erl => emqx_acl_internal.erl} | 15 +- src/{emqttd_acl_mod.erl => emqx_acl_mod.erl} | 4 +- src/{emqttd_alarm.erl => emqx_alarm.erl} | 24 +- src/emqx_app.erl | 85 ++++ ...{emqttd_auth_mod.erl => emqx_auth_mod.erl} | 10 +- src/{emqttd_base62.erl => emqx_base62.erl} | 2 +- src/{emqttd_boot.erl => emqx_boot.erl} | 2 +- src/{emqttd_bridge.erl => emqx_bridge.erl} | 36 +- ...ttd_bridge_sup.erl => emqx_bridge_sup.erl} | 11 +- ...ge_sup_sup.erl => emqx_bridge_sup_sup.erl} | 14 +- src/{emqttd_broker.erl => emqx_broker.erl} | 26 +- src/{emqttd_cli.erl => emqx_cli.erl} | 91 ++-- ...ttd_cli_config.erl => emqx_cli_config.erl} | 25 +- src/{emqttd_client.erl => emqx_client.erl} | 68 +-- src/{emqttd_cm.erl => emqx_cm.erl} | 8 +- src/{emqttd_cm_sup.erl => emqx_cm_sup.erl} | 10 +- src/{emqttd_config.erl => emqx_config.erl} | 13 +- src/{emqttd_ctl.erl => emqx_ctl.erl} | 18 +- src/{emqttd_gc.erl => emqx_gc.erl} | 4 +- src/{emqttd_gen_mod.erl => emqx_gen_mod.erl} | 6 +- src/{emqttd_guid.erl => emqx_guid.erl} | 6 +- src/{emqttd_hooks.erl => emqx_hooks.erl} | 2 +- src/{emqttd_http.erl => emqx_http.erl} | 24 +- ...{emqttd_inflight.erl => emqx_inflight.erl} | 2 +- ...mqttd_keepalive.erl => emqx_keepalive.erl} | 2 +- ...qtt_backend.erl => emqx_lager_backend.erl} | 8 +- src/{emqttd_message.erl => emqx_message.erl} | 10 +- src/{emqttd_metrics.erl => emqx_metrics.erl} | 21 +- src/{emqttd_mgmt.erl => emqx_mgmt.erl} | 99 ++-- src/{emqttd_misc.erl => emqx_misc.erl} | 2 +- src/{emqttd_mod_sup.erl => emqx_mod_sup.erl} | 4 +- src/{emqttd_mqueue.erl => emqx_mqueue.erl} | 8 +- src/{emqttd_net.erl => emqx_net.erl} | 4 +- src/{emqttd_packet.erl => emqx_packet.erl} | 30 +- src/{emqttd_parser.erl => emqx_parser.erl} | 6 +- src/{emqttd_plugins.erl => emqx_plugins.erl} | 16 +- src/{emqttd_pmon.erl => emqx_pmon.erl} | 2 +- ...{emqttd_pool_sup.erl => emqx_pool_sup.erl} | 4 +- src/{emqttd_pooler.erl => emqx_pooler.erl} | 8 +- src/{priority_queue.erl => emqx_pqueue.erl} | 41 +- ...{emqttd_protocol.erl => emqx_protocol.erl} | 138 ++--- src/{emqttd_pubsub.erl => emqx_pubsub.erl} | 42 +- ...ttd_pubsub_sup.erl => emqx_pubsub_sup.erl} | 8 +- ...{emqttd_rest_api.erl => emqx_rest_api.erl} | 105 ++-- src/{emqttd_router.erl => emqx_router.erl} | 18 +- src/emqx_rpc.erl | 17 + ...ttd_serializer.erl => emqx_serializer.erl} | 30 +- src/{emqttd_server.erl => emqx_server.erl} | 70 +-- src/{emqttd_session.erl => emqx_session.erl} | 149 +++--- ...d_session_sup.erl => emqx_session_sup.erl} | 7 +- src/{emqttd_sm.erl => emqx_sm.erl} | 24 +- ...mqttd_sm_helper.erl => emqx_sm_helper.erl} | 6 +- src/{emqttd_sm_sup.erl => emqx_sm_sup.erl} | 12 +- src/emqx_ssl.erl | 259 ++++++++++ src/{emqttd_stats.erl => emqx_stats.erl} | 20 +- src/emqx_sup.erl | 83 ++++ src/{emqttd_sysmon.erl => emqx_sysmon.erl} | 21 +- ...ttd_sysmon_sup.erl => emqx_sysmon_sup.erl} | 10 +- src/{emqttd_time.erl => emqx_time.erl} | 2 +- src/{emqttd_topic.erl => emqx_topic.erl} | 15 +- src/{emqttd_trace.erl => emqx_trace.erl} | 5 +- ...mqttd_trace_sup.erl => emqx_trace_sup.erl} | 6 +- src/{emqttd_trie.erl => emqx_trie.erl} | 10 +- src/{emqttd_vm.erl => emqx_vm.erl} | 2 +- src/{emqttd_ws.erl => emqx_ws.erl} | 18 +- ...mqttd_ws_client.erl => emqx_ws_client.erl} | 66 +-- ..._client_sup.erl => emqx_ws_client_sup.erl} | 8 +- test/emqttd_inflight_SUITE.erl | 51 -- test/{emqttd_SUITE.erl => emqx_SUITE.erl} | 470 +++++++++++------- test/emqx_SUITE_data/acl.conf | 29 ++ test/emqx_SUITE_data/slave.config | 60 +++ ...access_SUITE.erl => emqx_access_SUITE.erl} | 48 +- test/emqx_access_SUITE_data/acl.conf | 16 + ...acl_test_mod.erl => emqx_acl_test_mod.erl} | 3 +- ...d.erl => emqx_auth_anonymous_test_mod.erl} | 4 +- ..._dashboard.erl => emqx_auth_dashboard.erl} | 5 +- ...config_SUITE.erl => emqx_config_SUITE.erl} | 0 test/emqx_inflight_SUITE.erl | 63 +++ ...mqttd_lib_SUITE.erl => emqx_lib_SUITE.erl} | 68 ++- ...d_mock_client.erl => emqx_mock_client.erl} | 36 +- ...mqttd_mod_SUITE.erl => emqx_mod_SUITE.erl} | 0 ...mqueue_SUITE.erl => emqx_mqueue_SUITE.erl} | 80 +-- ...mqttd_net_SUITE.erl => emqx_net_SUITE.erl} | 6 +- ...ocol_SUITE.erl => emqx_protocol_SUITE.erl} | 130 ++--- ...d_topic_SUITE.erl => emqx_topic_SUITE.erl} | 11 +- ...ttd_trie_SUITE.erl => emqx_trie_SUITE.erl} | 14 +- ...{emqttd_vm_SUITE.erl => emqx_vm_SUITE.erl} | 38 +- test/ws_client.erl | 77 +++ 112 files changed, 3063 insertions(+), 1821 deletions(-) create mode 100644 LICENSE-MPL-RabbitMQ rename {doc => docs}/.placeholder (100%) rename {doc => docs}/MQTT-SN_spec_v1.2.pdf (100%) rename {doc => docs}/README (100%) rename {doc => docs}/mqtt-v3.1.1.pdf (100%) rename etc/{emq.conf => emqx.conf} (93%) rename include/{emqttd.hrl => emqx.hrl} (99%) rename include/{emqttd_cli.hrl => emqx_cli.hrl} (100%) rename include/{emqttd_internal.hrl => emqx_internal.hrl} (68%) rename include/{emqttd_protocol.hrl => emqx_mqtt.hrl} (100%) create mode 100644 include/emqx_rest.hrl rename include/{emqttd_trie.hrl => emqx_trie.hrl} (100%) rename priv/{emq.schema => emqx.schema} (74%) delete mode 100644 src/emqttd.app.src delete mode 100644 src/emqttd.erl delete mode 100644 src/emqttd_app.erl delete mode 100644 src/emqttd_sup.erl create mode 100644 src/emqx.erl rename src/{emqttd_access_control.erl => emqx_access_control.erl} (97%) rename src/{emqttd_access_rule.erl => emqx_access_rule.erl} (94%) rename src/{emqttd_acl_internal.erl => emqx_acl_internal.erl} (92%) rename src/{emqttd_acl_mod.erl => emqx_acl_mod.erl} (96%) rename src/{emqttd_alarm.erl => emqx_alarm.erl} (87%) create mode 100644 src/emqx_app.erl rename src/{emqttd_auth_mod.erl => emqx_auth_mod.erl} (94%) rename src/{emqttd_base62.erl => emqx_base62.erl} (98%) rename src/{emqttd_boot.erl => emqx_boot.erl} (99%) rename src/{emqttd_bridge.erl => emqx_bridge.erl} (87%) rename src/{emqttd_bridge_sup.erl => emqx_bridge_sup.erl} (77%) rename src/{emqttd_bridge_sup_sup.erl => emqx_bridge_sup_sup.erl} (86%) rename src/{emqttd_broker.erl => emqx_broker.erl} (89%) rename src/{emqttd_cli.erl => emqx_cli.erl} (88%) rename src/{emqttd_cli_config.erl => emqx_cli_config.erl} (97%) rename src/{emqttd_client.erl => emqx_client.erl} (87%) rename src/{emqttd_cm.erl => emqx_cm.erl} (97%) rename src/{emqttd_cm_sup.erl => emqx_cm_sup.erl} (85%) rename src/{emqttd_config.erl => emqx_config.erl} (93%) rename src/{emqttd_ctl.erl => emqx_ctl.erl} (91%) rename src/{emqttd_gc.erl => emqx_gc.erl} (96%) rename src/{emqttd_gen_mod.erl => emqx_gen_mod.erl} (92%) rename src/{emqttd_guid.erl => emqx_guid.erl} (97%) rename src/{emqttd_hooks.erl => emqx_hooks.erl} (99%) rename src/{emqttd_http.erl => emqx_http.erl} (93%) rename src/{emqttd_inflight.erl => emqx_inflight.erl} (99%) rename src/{emqttd_keepalive.erl => emqx_keepalive.erl} (98%) rename src/{lager_emqtt_backend.erl => emqx_lager_backend.erl} (91%) rename src/{emqttd_message.erl => emqx_message.erl} (97%) rename src/{emqttd_metrics.erl => emqx_metrics.erl} (94%) rename src/{emqttd_mgmt.erl => emqx_mgmt.erl} (85%) rename src/{emqttd_misc.erl => emqx_misc.erl} (99%) rename src/{emqttd_mod_sup.erl => emqx_mod_sup.erl} (97%) rename src/{emqttd_mqueue.erl => emqx_mqueue.erl} (98%) rename src/{emqttd_net.erl => emqx_net.erl} (99%) rename src/{emqttd_packet.erl => emqx_packet.erl} (88%) rename src/{emqttd_parser.erl => emqx_parser.erl} (99%) rename src/{emqttd_plugins.erl => emqx_plugins.erl} (96%) rename src/{emqttd_pmon.erl => emqx_pmon.erl} (98%) rename src/{emqttd_pool_sup.erl => emqx_pool_sup.erl} (94%) rename src/{emqttd_pooler.erl => emqx_pooler.erl} (94%) rename src/{priority_queue.erl => emqx_pqueue.erl} (90%) rename src/{emqttd_protocol.erl => emqx_protocol.erl} (80%) rename src/{emqttd_pubsub.erl => emqx_pubsub.erl} (86%) rename src/{emqttd_pubsub_sup.erl => emqx_pubsub_sup.erl} (94%) rename src/{emqttd_rest_api.erl => emqx_rest_api.erl} (86%) rename src/{emqttd_router.erl => emqx_router.erl} (94%) create mode 100644 src/emqx_rpc.erl rename src/{emqttd_serializer.erl => emqx_serializer.erl} (96%) rename src/{emqttd_server.erl => emqx_server.erl} (82%) rename src/{emqttd_session.erl => emqx_session.erl} (86%) rename src/{emqttd_session_sup.erl => emqx_session_sup.erl} (91%) rename src/{emqttd_sm.erl => emqx_sm.erl} (94%) rename src/{emqttd_sm_helper.erl => emqx_sm_helper.erl} (97%) rename src/{emqttd_sm_sup.erl => emqx_sm_sup.erl} (84%) create mode 100644 src/emqx_ssl.erl rename src/{emqttd_stats.erl => emqx_stats.erl} (91%) create mode 100644 src/emqx_sup.erl rename src/{emqttd_sysmon.erl => emqx_sysmon.erl} (91%) rename src/{emqttd_sysmon_sup.erl => emqx_sysmon_sup.erl} (82%) rename src/{emqttd_time.erl => emqx_time.erl} (98%) rename src/{emqttd_topic.erl => emqx_topic.erl} (93%) rename src/{emqttd_trace.erl => emqx_trace.erl} (97%) rename src/{emqttd_trace_sup.erl => emqx_trace_sup.erl} (88%) rename src/{emqttd_trie.erl => emqx_trie.erl} (95%) rename src/{emqttd_vm.erl => emqx_vm.erl} (99%) rename src/{emqttd_ws.erl => emqx_ws.erl} (88%) rename src/{emqttd_ws_client.erl => emqx_ws_client.erl} (83%) rename src/{emqttd_ws_client_sup.erl => emqx_ws_client_sup.erl} (86%) delete mode 100644 test/emqttd_inflight_SUITE.erl rename test/{emqttd_SUITE.erl => emqx_SUITE.erl} (58%) create mode 100644 test/emqx_SUITE_data/acl.conf create mode 100644 test/emqx_SUITE_data/slave.config rename test/{emqttd_access_SUITE.erl => emqx_access_SUITE.erl} (85%) create mode 100644 test/emqx_access_SUITE_data/acl.conf rename test/{emqttd_acl_test_mod.erl => emqx_acl_test_mod.erl} (97%) rename test/{emqttd_auth_anonymous_test_mod.erl => emqx_auth_anonymous_test_mod.erl} (92%) rename test/{emqttd_auth_dashboard.erl => emqx_auth_dashboard.erl} (92%) rename test/{emqttd_config_SUITE.erl => emqx_config_SUITE.erl} (100%) create mode 100644 test/emqx_inflight_SUITE.erl rename test/{emqttd_lib_SUITE.erl => emqx_lib_SUITE.erl} (71%) rename test/{emqttd_mock_client.erl => emqx_mock_client.erl} (51%) rename test/{emqttd_mod_SUITE.erl => emqx_mod_SUITE.erl} (100%) rename test/{emqttd_mqueue_SUITE.erl => emqx_mqueue_SUITE.erl} (74%) rename test/{emqttd_net_SUITE.erl => emqx_net_SUITE.erl} (89%) rename test/{emqttd_protocol_SUITE.erl => emqx_protocol_SUITE.erl} (78%) rename test/{emqttd_topic_SUITE.erl => emqx_topic_SUITE.erl} (96%) rename test/{emqttd_trie_SUITE.erl => emqx_trie_SUITE.erl} (92%) rename test/{emqttd_vm_SUITE.erl => emqx_vm_SUITE.erl} (87%) create mode 100644 test/ws_client.erl diff --git a/.gitignore b/.gitignore index 0aa159fd7..d3fc9149b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,13 +17,13 @@ log/ *.so .erlang.mk/ cover/ -emqttd.d +emqx.d eunit.coverdata test/ct.cover.spec logs ct.coverdata .idea/ -emqttd.iml +emqx.iml _rel/ data/ _build diff --git a/.travis.yml b/.travis.yml index 2266ed9e3..6c41760ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: erlang otp_release: - 20.0 + - 20.1 script: - make diff --git a/LICENSE-MPL-RabbitMQ b/LICENSE-MPL-RabbitMQ new file mode 100644 index 000000000..f1ba9a5ca --- /dev/null +++ b/LICENSE-MPL-RabbitMQ @@ -0,0 +1,455 @@ + MOZILLA PUBLIC LICENSE + Version 1.1 + + --------------- + +1. Definitions. + + 1.0.1. "Commercial Use" means distribution or otherwise making the + Covered Code available to a third party. + + 1.1. "Contributor" means each entity that creates or contributes to + the creation of Modifications. + + 1.2. "Contributor Version" means the combination of the Original + Code, prior Modifications used by a Contributor, and the Modifications + made by that particular Contributor. + + 1.3. "Covered Code" means the Original Code or Modifications or the + combination of the Original Code and Modifications, in each case + including portions thereof. + + 1.4. "Electronic Distribution Mechanism" means a mechanism generally + accepted in the software development community for the electronic + transfer of data. + + 1.5. "Executable" means Covered Code in any form other than Source + Code. + + 1.6. "Initial Developer" means the individual or entity identified + as the Initial Developer in the Source Code notice required by Exhibit + A. + + 1.7. "Larger Work" means a work which combines Covered Code or + portions thereof with code not governed by the terms of this License. + + 1.8. "License" means this document. + + 1.8.1. "Licensable" means having the right to grant, to the maximum + extent possible, whether at the time of the initial grant or + subsequently acquired, any and all of the rights conveyed herein. + + 1.9. "Modifications" means any addition to or deletion from the + substance or structure of either the Original Code or any previous + Modifications. When Covered Code is released as a series of files, a + Modification is: + A. Any addition to or deletion from the contents of a file + containing Original Code or previous Modifications. + + B. Any new file that contains any part of the Original Code or + previous Modifications. + + 1.10. "Original Code" means Source Code of computer software code + which is described in the Source Code notice required by Exhibit A as + Original Code, and which, at the time of its release under this + License is not already Covered Code governed by this License. + + 1.10.1. "Patent Claims" means any patent claim(s), now owned or + hereafter acquired, including without limitation, method, process, + and apparatus claims, in any patent Licensable by grantor. + + 1.11. "Source Code" means the preferred form of the Covered Code for + making modifications to it, including all modules it contains, plus + any associated interface definition files, scripts used to control + compilation and installation of an Executable, or source code + differential comparisons against either the Original Code or another + well known, available Covered Code of the Contributor's choice. The + Source Code can be in a compressed or archival form, provided the + appropriate decompression or de-archiving software is widely available + for no charge. + + 1.12. "You" (or "Your") means an individual or a legal entity + exercising rights under, and complying with all of the terms of, this + License or a future version of this License issued under Section 6.1. + For legal entities, "You" includes any entity which controls, is + controlled by, or is under common control with You. For purposes of + this definition, "control" means (a) the power, direct or indirect, + to cause the direction or management of such entity, whether by + contract or otherwise, or (b) ownership of more than fifty percent + (50%) of the outstanding shares or beneficial ownership of such + entity. + +2. Source Code License. + + 2.1. The Initial Developer Grant. + The Initial Developer hereby grants You a world-wide, royalty-free, + non-exclusive license, subject to third party intellectual property + claims: + (a) under intellectual property rights (other than patent or + trademark) Licensable by Initial Developer to use, reproduce, + modify, display, perform, sublicense and distribute the Original + Code (or portions thereof) with or without Modifications, and/or + as part of a Larger Work; and + + (b) under Patents Claims infringed by the making, using or + selling of Original Code, to make, have made, use, practice, + sell, and offer for sale, and/or otherwise dispose of the + Original Code (or portions thereof). + + (c) the licenses granted in this Section 2.1(a) and (b) are + effective on the date Initial Developer first distributes + Original Code under the terms of this License. + + (d) Notwithstanding Section 2.1(b) above, no patent license is + granted: 1) for code that You delete from the Original Code; 2) + separate from the Original Code; or 3) for infringements caused + by: i) the modification of the Original Code or ii) the + combination of the Original Code with other software or devices. + + 2.2. Contributor Grant. + Subject to third party intellectual property claims, each Contributor + hereby grants You a world-wide, royalty-free, non-exclusive license + + (a) under intellectual property rights (other than patent or + trademark) Licensable by Contributor, to use, reproduce, modify, + display, perform, sublicense and distribute the Modifications + created by such Contributor (or portions thereof) either on an + unmodified basis, with other Modifications, as Covered Code + and/or as part of a Larger Work; and + + (b) under Patent Claims infringed by the making, using, or + selling of Modifications made by that Contributor either alone + and/or in combination with its Contributor Version (or portions + of such combination), to make, use, sell, offer for sale, have + made, and/or otherwise dispose of: 1) Modifications made by that + Contributor (or portions thereof); and 2) the combination of + Modifications made by that Contributor with its Contributor + Version (or portions of such combination). + + (c) the licenses granted in Sections 2.2(a) and 2.2(b) are + effective on the date Contributor first makes Commercial Use of + the Covered Code. + + (d) Notwithstanding Section 2.2(b) above, no patent license is + granted: 1) for any code that Contributor has deleted from the + Contributor Version; 2) separate from the Contributor Version; + 3) for infringements caused by: i) third party modifications of + Contributor Version or ii) the combination of Modifications made + by that Contributor with other software (except as part of the + Contributor Version) or other devices; or 4) under Patent Claims + infringed by Covered Code in the absence of Modifications made by + that Contributor. + +3. Distribution Obligations. + + 3.1. Application of License. + The Modifications which You create or to which You contribute are + governed by the terms of this License, including without limitation + Section 2.2. The Source Code version of Covered Code may be + distributed only under the terms of this License or a future version + of this License released under Section 6.1, and You must include a + copy of this License with every copy of the Source Code You + distribute. You may not offer or impose any terms on any Source Code + version that alters or restricts the applicable version of this + License or the recipients' rights hereunder. However, You may include + an additional document offering the additional rights described in + Section 3.5. + + 3.2. Availability of Source Code. + Any Modification which You create or to which You contribute must be + made available in Source Code form under the terms of this License + either on the same media as an Executable version or via an accepted + Electronic Distribution Mechanism to anyone to whom you made an + Executable version available; and if made available via Electronic + Distribution Mechanism, must remain available for at least twelve (12) + months after the date it initially became available, or at least six + (6) months after a subsequent version of that particular Modification + has been made available to such recipients. You are responsible for + ensuring that the Source Code version remains available even if the + Electronic Distribution Mechanism is maintained by a third party. + + 3.3. Description of Modifications. + You must cause all Covered Code to which You contribute to contain a + file documenting the changes You made to create that Covered Code and + the date of any change. You must include a prominent statement that + the Modification is derived, directly or indirectly, from Original + Code provided by the Initial Developer and including the name of the + Initial Developer in (a) the Source Code, and (b) in any notice in an + Executable version or related documentation in which You describe the + origin or ownership of the Covered Code. + + 3.4. Intellectual Property Matters + (a) Third Party Claims. + If Contributor has knowledge that a license under a third party's + intellectual property rights is required to exercise the rights + granted by such Contributor under Sections 2.1 or 2.2, + Contributor must include a text file with the Source Code + distribution titled "LEGAL" which describes the claim and the + party making the claim in sufficient detail that a recipient will + know whom to contact. If Contributor obtains such knowledge after + the Modification is made available as described in Section 3.2, + Contributor shall promptly modify the LEGAL file in all copies + Contributor makes available thereafter and shall take other steps + (such as notifying appropriate mailing lists or newsgroups) + reasonably calculated to inform those who received the Covered + Code that new knowledge has been obtained. + + (b) Contributor APIs. + If Contributor's Modifications include an application programming + interface and Contributor has knowledge of patent licenses which + are reasonably necessary to implement that API, Contributor must + also include this information in the LEGAL file. + + (c) Representations. + Contributor represents that, except as disclosed pursuant to + Section 3.4(a) above, Contributor believes that Contributor's + Modifications are Contributor's original creation(s) and/or + Contributor has sufficient rights to grant the rights conveyed by + this License. + + 3.5. Required Notices. + You must duplicate the notice in Exhibit A in each file of the Source + Code. If it is not possible to put such notice in a particular Source + Code file due to its structure, then You must include such notice in a + location (such as a relevant directory) where a user would be likely + to look for such a notice. If You created one or more Modification(s) + You may add your name as a Contributor to the notice described in + Exhibit A. You must also duplicate this License in any documentation + for the Source Code where You describe recipients' rights or ownership + rights relating to Covered Code. You may choose to offer, and to + charge a fee for, warranty, support, indemnity or liability + obligations to one or more recipients of Covered Code. However, You + may do so only on Your own behalf, and not on behalf of the Initial + Developer or any Contributor. You must make it absolutely clear than + any such warranty, support, indemnity or liability obligation is + offered by You alone, and You hereby agree to indemnify the Initial + Developer and every Contributor for any liability incurred by the + Initial Developer or such Contributor as a result of warranty, + support, indemnity or liability terms You offer. + + 3.6. Distribution of Executable Versions. + You may distribute Covered Code in Executable form only if the + requirements of Section 3.1-3.5 have been met for that Covered Code, + and if You include a notice stating that the Source Code version of + the Covered Code is available under the terms of this License, + including a description of how and where You have fulfilled the + obligations of Section 3.2. The notice must be conspicuously included + in any notice in an Executable version, related documentation or + collateral in which You describe recipients' rights relating to the + Covered Code. You may distribute the Executable version of Covered + Code or ownership rights under a license of Your choice, which may + contain terms different from this License, provided that You are in + compliance with the terms of this License and that the license for the + Executable version does not attempt to limit or alter the recipient's + rights in the Source Code version from the rights set forth in this + License. If You distribute the Executable version under a different + license You must make it absolutely clear that any terms which differ + from this License are offered by You alone, not by the Initial + Developer or any Contributor. You hereby agree to indemnify the + Initial Developer and every Contributor for any liability incurred by + the Initial Developer or such Contributor as a result of any such + terms You offer. + + 3.7. Larger Works. + You may create a Larger Work by combining Covered Code with other code + not governed by the terms of this License and distribute the Larger + Work as a single product. In such a case, You must make sure the + requirements of this License are fulfilled for the Covered Code. + +4. Inability to Comply Due to Statute or Regulation. + + If it is impossible for You to comply with any of the terms of this + License with respect to some or all of the Covered Code due to + statute, judicial order, or regulation then You must: (a) comply with + the terms of this License to the maximum extent possible; and (b) + describe the limitations and the code they affect. Such description + must be included in the LEGAL file described in Section 3.4 and must + be included with all distributions of the Source Code. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Application of this License. + + This License applies to code to which the Initial Developer has + attached the notice in Exhibit A and to related Covered Code. + +6. Versions of the License. + + 6.1. New Versions. + Netscape Communications Corporation ("Netscape") may publish revised + and/or new versions of the License from time to time. Each version + will be given a distinguishing version number. + + 6.2. Effect of New Versions. + Once Covered Code has been published under a particular version of the + License, You may always continue to use it under the terms of that + version. You may also choose to use such Covered Code under the terms + of any subsequent version of the License published by Netscape. No one + other than Netscape has the right to modify the terms applicable to + Covered Code created under this License. + + 6.3. Derivative Works. + If You create or use a modified version of this License (which you may + only do in order to apply it to code which is not already Covered Code + governed by this License), You must (a) rename Your license so that + the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", + "MPL", "NPL" or any confusingly similar phrase do not appear in your + license (except to note that your license differs from this License) + and (b) otherwise make it clear that Your version of the license + contains terms which differ from the Mozilla Public License and + Netscape Public License. (Filling in the name of the Initial + Developer, Original Code or Contributor in the notice described in + Exhibit A shall not of themselves be deemed to be modifications of + this License.) + +7. DISCLAIMER OF WARRANTY. + + COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, + WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, + WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF + DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. + THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE + IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, + YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE + COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER + OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF + ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + +8. TERMINATION. + + 8.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to cure + such breach within 30 days of becoming aware of the breach. All + sublicenses to the Covered Code which are properly granted shall + survive any termination of this License. Provisions which, by their + nature, must remain in effect beyond the termination of this License + shall survive. + + 8.2. If You initiate litigation by asserting a patent infringement + claim (excluding declatory judgment actions) against Initial Developer + or a Contributor (the Initial Developer or Contributor against whom + You file such action is referred to as "Participant") alleging that: + + (a) such Participant's Contributor Version directly or indirectly + infringes any patent, then any and all rights granted by such + Participant to You under Sections 2.1 and/or 2.2 of this License + shall, upon 60 days notice from Participant terminate prospectively, + unless if within 60 days after receipt of notice You either: (i) + agree in writing to pay Participant a mutually agreeable reasonable + royalty for Your past and future use of Modifications made by such + Participant, or (ii) withdraw Your litigation claim with respect to + the Contributor Version against such Participant. If within 60 days + of notice, a reasonable royalty and payment arrangement are not + mutually agreed upon in writing by the parties or the litigation claim + is not withdrawn, the rights granted by Participant to You under + Sections 2.1 and/or 2.2 automatically terminate at the expiration of + the 60 day notice period specified above. + + (b) any software, hardware, or device, other than such Participant's + Contributor Version, directly or indirectly infringes any patent, then + any rights granted to You by such Participant under Sections 2.1(b) + and 2.2(b) are revoked effective as of the date You first made, used, + sold, distributed, or had made, Modifications made by that + Participant. + + 8.3. If You assert a patent infringement claim against Participant + alleging that such Participant's Contributor Version directly or + indirectly infringes any patent where such claim is resolved (such as + by license or settlement) prior to the initiation of patent + infringement litigation, then the reasonable value of the licenses + granted by such Participant under Sections 2.1 or 2.2 shall be taken + into account in determining the amount or value of any payment or + license. + + 8.4. In the event of termination under Sections 8.1 or 8.2 above, + all end user license agreements (excluding distributors and resellers) + which have been validly granted by You or any distributor hereunder + prior to termination shall survive termination. + +9. LIMITATION OF LIABILITY. + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT + (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL + DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, + OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR + ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY + CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, + WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER + COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN + INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF + LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY + RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW + PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE + EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO + THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + +10. U.S. GOVERNMENT END USERS. + + The Covered Code is a "commercial item," as that term is defined in + 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer + software" and "commercial computer software documentation," as such + terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 + C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), + all U.S. Government End Users acquire Covered Code with only those + rights set forth herein. + +11. MISCELLANEOUS. + + This License represents the complete agreement concerning subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. This License shall be governed by + California law provisions (except to the extent applicable law, if + any, provides otherwise), excluding its conflict-of-law provisions. + With respect to disputes in which at least one party is a citizen of, + or an entity chartered or registered to do business in the United + States of America, any litigation relating to this License shall be + subject to the jurisdiction of the Federal Courts of the Northern + District of California, with venue lying in Santa Clara County, + California, with the losing party responsible for costs, including + without limitation, court costs and reasonable attorneys' fees and + expenses. The application of the United Nations Convention on + Contracts for the International Sale of Goods is expressly excluded. + Any law or regulation which provides that the language of a contract + shall be construed against the drafter shall not apply to this + License. + +12. RESPONSIBILITY FOR CLAIMS. + + As between Initial Developer and the Contributors, each party is + responsible for claims and damages arising, directly or indirectly, + out of its utilization of rights under this License and You agree to + work with Initial Developer and Contributors to distribute such + responsibility on an equitable basis. Nothing herein is intended or + shall be deemed to constitute any admission of liability. + +13. MULTIPLE-LICENSED CODE. + + Initial Developer may designate portions of the Covered Code as + "Multiple-Licensed". "Multiple-Licensed" means that the Initial + Developer permits you to utilize portions of the Covered Code under + Your choice of the NPL or the alternative licenses, if any, specified + by the Initial Developer in the file described in Exhibit A. + +EXHIBIT A -Mozilla Public License. + + ``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. + + The Initial Developer of the Original Code is Pivotal Software, Inc. + Copyright (c) 2007-2016 Pivotal Software, Inc. All rights reserved.'' + + [NOTE: The text of this Exhibit A may differ slightly from the text of + the notices in the Source Code files of the Original Code. You should + use the text of this Exhibit A rather than the text found in the + Original Code Source Code for Your Modifications.] diff --git a/Makefile b/Makefile index 3ea8fff3d..21eacc064 100644 --- a/Makefile +++ b/Makefile @@ -1,32 +1,34 @@ -PROJECT = emqttd -PROJECT_DESCRIPTION = Erlang MQTT Broker -PROJECT_VERSION = 2.3 +PROJECT = emqx +PROJECT_DESCRIPTION = EMQ X Broker +PROJECT_VERSION = 2.4 -DEPS = goldrush gproc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx +NO_AUTOPATCH = gen_rpc cuttlefish + +DEPS = goldrush gproc gen_rpc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx dep_goldrush = git https://github.com/basho/goldrush 0.1.9 dep_gproc = git https://github.com/uwiger/gproc +dep_gen_rpc = git https://github.com/priestjim/gen_rpc dep_getopt = git https://github.com/jcomellas/getopt v0.8.2 dep_lager = git https://github.com/basho/lager master +dep_lager_syslog = git https://github.com/basho/lager_syslog +dep_jsx = git https://github.com/talentdeficit/jsx dep_esockd = git https://github.com/emqtt/esockd master dep_ekka = git https://github.com/emqtt/ekka master dep_mochiweb = git https://github.com/emqtt/mochiweb master dep_pbkdf2 = git https://github.com/emqtt/pbkdf2 2.0.1 -dep_lager_syslog = git https://github.com/basho/lager_syslog dep_bcrypt = git https://github.com/smarkets/erlang-bcrypt master dep_clique = git https://github.com/emqtt/clique -dep_jsx = git https://github.com/talentdeficit/jsx ERLC_OPTS += +debug_info ERLC_OPTS += +'{parse_transform, lager_transform}' -NO_AUTOPATCH = cuttlefish - BUILD_DEPS = cuttlefish dep_cuttlefish = git https://github.com/emqtt/cuttlefish -TEST_DEPS = emqttc +TEST_DEPS = websocket_client emqttc dep_emqttc = git https://github.com/emqtt/emqttc +dep_websocket_client = git https://github.com/jeremyong/websocket_client TEST_ERLC_OPTS += +debug_info TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' @@ -34,11 +36,10 @@ TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose # EUNIT_ERL_OPTS = -CT_SUITES = emqttd emqttd_access emqttd_lib emqttd_inflight emqttd_mod \ - emqttd_net emqttd_mqueue emqttd_protocol emqttd_topic \ - emqttd_trie emqttd_vm emqttd_config +CT_SUITES = emqx emqx_lib emqx_topic emqx_trie emqx_mqueue emqx_inflight \ +emqx_vm emqx_net emqx_protocol emqx_access emqx_config -CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqttd_ct@127.0.0.1 +CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqxct@127.0.0.1 COVER = true @@ -49,8 +50,6 @@ DIALYZER_OPTS := --verbose --statistics -Werror_handling \ include erlang.mk -app:: rebar.config - app.config:: - ./deps/cuttlefish/cuttlefish -l info -e etc/ -c etc/emq.conf -i priv/emq.schema -d data/ + ./deps/cuttlefish/cuttlefish -l info -e etc/ -c etc/emqx.conf -i priv/emqx.schema -d data/ diff --git a/doc/.placeholder b/docs/.placeholder similarity index 100% rename from doc/.placeholder rename to docs/.placeholder diff --git a/doc/MQTT-SN_spec_v1.2.pdf b/docs/MQTT-SN_spec_v1.2.pdf similarity index 100% rename from doc/MQTT-SN_spec_v1.2.pdf rename to docs/MQTT-SN_spec_v1.2.pdf diff --git a/doc/README b/docs/README similarity index 100% rename from doc/README rename to docs/README diff --git a/doc/mqtt-v3.1.1.pdf b/docs/mqtt-v3.1.1.pdf similarity index 100% rename from doc/mqtt-v3.1.1.pdf rename to docs/mqtt-v3.1.1.pdf diff --git a/erlang.mk b/erlang.mk index e348d4493..f38d22653 100644 --- a/erlang.mk +++ b/erlang.mk @@ -2174,7 +2174,7 @@ help:: CT_RUN = ct_run \ -no_auto_compile \ -noinput \ - -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \ + -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(DEPS_DIR)/gen_rpc/_build/dev/lib/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \ -dir $(TEST_DIR) \ -logdir $(CURDIR)/logs diff --git a/etc/emq.conf b/etc/emqx.conf similarity index 93% rename from etc/emq.conf rename to etc/emqx.conf index 1c0859921..bcf92d751 100644 --- a/etc/emq.conf +++ b/etc/emqx.conf @@ -1,5 +1,5 @@ ##=================================================================== -## EMQ Configuration R2.3 +## EMQ X Configuration R2.4 ##=================================================================== ##-------------------------------------------------------------------- @@ -7,7 +7,7 @@ ##-------------------------------------------------------------------- ## Cluster name -cluster.name = emqcl +cluster.name = emqxcl ## Cluster discovery strategy: manual | static | mcast | dns | etcd | k8s cluster.discovery = manual @@ -70,10 +70,10 @@ cluster.autoclean = 5m ##-------------------------------------------------------------------- ## Node name -node.name = emq@127.0.0.1 +node.name = emqx@127.0.0.1 ## Cookie for distributed node -node.cookie = emqsecretcookie +node.cookie = emqxsecretcookie ## SMP support: enable, auto, disable node.smp = auto @@ -93,7 +93,7 @@ node.async_threads = 32 node.process_limit = 256000 ## Sets the maximum number of simultaneously existing ports for this system -node.max_ports = 65536 +node.max_ports = 256000 ## Set the distribution buffer busy limit (dist_buf_busy_limit) node.dist_buffer_size = 32MB @@ -113,7 +113,40 @@ node.dist_net_ticktime = 60 ## Distributed node port range node.dist_listen_min = 6369 -node.dist_listen_max = 6379 +node.dist_listen_max = 6369 + +##-------------------------------------------------------------------- +## RPC Args +##-------------------------------------------------------------------- + +## TCP server port. +rpc.tcp_server_port = 5369 + +## Default TCP port for outgoing connections +rpc.tcp_client_port = 5369 + +## Client connect timeout +rpc.connect_timeout = 5000 + +## Client and Server send timeout +rpc.send_timeout = 5000 + +## Authentication timeout +rpc.authentication_timeout = 5000 + +## Default receive timeout for call() functions +rpc.call_receive_timeout = 15000 + +## Socket keepalive configuration +rpc.socket_keepalive_idle = 7200 + +## Seconds between probes +rpc.socket_keepalive_interval = 75 + +## Probes lost to close the connection +rpc.socket_keepalive_count = 9 + +## TODO: sndbuf, rcvbuf and buffer ##-------------------------------------------------------------------- ## Log @@ -311,6 +344,9 @@ listener.tcp.external.acceptors = 16 ## Maximum number of concurrent clients listener.tcp.external.max_clients = 102400 +## TODO: +## listener.tcp.external.zone = external + #listener.tcp.external.mountpoint = external/ ## Rate Limit. Format is 'burst,rate', Unit is KB/Sec @@ -347,6 +383,8 @@ listener.tcp.internal.acceptors = 16 ## Maximum number of concurrent clients listener.tcp.internal.max_clients = 102400 +#listener.tcp.internal.zone = internal + #listener.tcp.external.mountpoint = internal/ ## Rate Limit. Format is 'burst,rate', Unit is KB/Sec @@ -375,7 +413,10 @@ listener.ssl.external = 8883 listener.ssl.external.acceptors = 16 ## Maximum number of concurrent clients -listener.ssl.external.max_clients = 1024 +listener.ssl.external.max_clients = 102400 + +## Authentication Zone +## listener.ssl.external.zone = external ## listener.ssl.external.mountpoint = inbound/ @@ -478,6 +519,8 @@ listener.ws.external.acceptors = 4 listener.ws.external.max_clients = 64 +## listener.ws.external.zone = external + listener.ws.external.access.1 = allow all ## TCP Options @@ -500,6 +543,8 @@ listener.wss.external.acceptors = 4 listener.wss.external.max_clients = 64 +## listener.wss.external.zone = external + listener.wss.external.access.1 = allow all ## SSL Options diff --git a/include/emqttd.hrl b/include/emqx.hrl similarity index 99% rename from include/emqttd.hrl rename to include/emqx.hrl index 508712512..7254805be 100644 --- a/include/emqttd.hrl +++ b/include/emqx.hrl @@ -24,7 +24,7 @@ -define(PROTOCOL_VERSION, "MQTT/5.0"). --define(ERTS_MINIMUM, "8.0"). +-define(ERTS_MINIMUM, "9.0"). %%-------------------------------------------------------------------- %% Sys/Queue/Share Topics' Prefix diff --git a/include/emqttd_cli.hrl b/include/emqx_cli.hrl similarity index 100% rename from include/emqttd_cli.hrl rename to include/emqx_cli.hrl diff --git a/include/emqttd_internal.hrl b/include/emqx_internal.hrl similarity index 68% rename from include/emqttd_internal.hrl rename to include/emqx_internal.hrl index 343be68e4..3b3378d0c 100644 --- a/include/emqttd_internal.hrl +++ b/include/emqx_internal.hrl @@ -35,19 +35,19 @@ -define(UNEXPECTED_REQ(Req, State), (begin - lager:error("Unexpected Request: ~p", [Req]), + lager:error("[~s] Unexpected Request: ~p", [?MODULE, Req]), {reply, {error, unexpected_request}, State} end)). -define(UNEXPECTED_MSG(Msg, State), (begin - lager:error("Unexpected Message: ~p", [Msg]), + lager:error("[~s] Unexpected Message: ~p", [?MODULE, Msg]), {noreply, State} end)). -define(UNEXPECTED_INFO(Info, State), (begin - lager:error("Unexpected Info: ~p", [Info]), + lager:error("[~s] Unexpected Info: ~p", [?MODULE, Info]), {noreply, State} end)). @@ -59,19 +59,3 @@ -define(FULLSWEEP_OPTS, [{fullsweep_after, 10}]). --define(SUCCESS, 0). %% Success --define(ERROR1, 101). %% badrpc --define(ERROR2, 102). %% Unknown error --define(ERROR3, 103). %% Username or password error --define(ERROR4, 104). %% Empty username or password --define(ERROR5, 105). %% User does not exist --define(ERROR6, 106). %% Admin can not be deleted --define(ERROR7, 107). %% Missing request parameter --define(ERROR8, 108). %% Request parameter type error --define(ERROR9, 109). %% Request parameter is not a json --define(ERROR10, 110). %% Plugin has been loaded --define(ERROR11, 111). %% Plugin has been loaded --define(ERROR12, 112). %% Client not online --define(ERROR13, 113). %% User already exist --define(ERROR14, 114). %% OldPassword error - diff --git a/include/emqttd_protocol.hrl b/include/emqx_mqtt.hrl similarity index 100% rename from include/emqttd_protocol.hrl rename to include/emqx_mqtt.hrl diff --git a/include/emqx_rest.hrl b/include/emqx_rest.hrl new file mode 100644 index 000000000..c1c6b219d --- /dev/null +++ b/include/emqx_rest.hrl @@ -0,0 +1,17 @@ + +-define(SUCCESS, 0). %% Success +-define(ERROR1, 101). %% badrpc +-define(ERROR2, 102). %% Unknown error +-define(ERROR3, 103). %% Username or password error +-define(ERROR4, 104). %% Empty username or password +-define(ERROR5, 105). %% User does not exist +-define(ERROR6, 106). %% Admin can not be deleted +-define(ERROR7, 107). %% Missing request parameter +-define(ERROR8, 108). %% Request parameter type error +-define(ERROR9, 109). %% Request parameter is not a json +-define(ERROR10, 110). %% Plugin has been loaded +-define(ERROR11, 111). %% Plugin has been loaded +-define(ERROR12, 112). %% Client not online +-define(ERROR13, 113). %% User already exist +-define(ERROR14, 114). %% OldPassword error + diff --git a/include/emqttd_trie.hrl b/include/emqx_trie.hrl similarity index 100% rename from include/emqttd_trie.hrl rename to include/emqx_trie.hrl diff --git a/priv/emq.schema b/priv/emqx.schema similarity index 74% rename from priv/emq.schema rename to priv/emqx.schema index ce4baf36b..82f940a36 100644 --- a/priv/emq.schema +++ b/priv/emqx.schema @@ -1,5 +1,5 @@ %%-*- mode: erlang -*- -%% EMQ R2.3 config mapping +%% EMQ X config mapping %%-------------------------------------------------------------------- %% Cluster @@ -7,7 +7,7 @@ %% @doc Cluster name {mapping, "cluster.name", "ekka.cluster_name", [ - {default, emqcl}, + {default, emqxcl}, {datatype, atom} ]}. @@ -165,12 +165,12 @@ end}. %% @doc Erlang node name {mapping, "node.name", "vm_args.-name", [ - {default, "emq@127.0.0.1"} + {default, "emqx@127.0.0.1"} ]}. %% @doc Secret cookie for distributed erlang node {mapping, "node.cookie", "vm_args.-setcookie", [ - {default, "emqsecretcookie"} + {default, "emqxsecretcookie"} ]}. %% @doc SMP Support @@ -302,6 +302,64 @@ end}. hidden ]}. +%%-------------------------------------------------------------------- +%% RPC Args +%%-------------------------------------------------------------------- + +%% RPC server port. +{mapping, "rpc.tcp_server_port", "gen_rpc.tcp_server_port", [ + {default, 5369}, + {datatype, integer} +]}. + +%% Default TCP port for outgoing connections +{mapping, "rpc.tcp_client_port", "gen_rpc.tcp_client_port", [ + {default, 5369}, + {datatype, integer} +]}. + +%% Client connect timeout +{mapping, "rpc.connect_timeout", "gen_rpc.connect_timeout", [ + {default, 5000}, + {datatype, integer} +]}. + +%% Client and Server send timeout +{mapping, "rpc.send_timeout", "gen_rpc.send_timeout", [ + {default, 5000}, + {datatype, integer} +]}. + +%% Authentication timeout +{mapping, "rpc.authentication_timeout", "gen_rpc.authentication_timeout", [ + {default, 5000}, + {datatype, integer} +]}. + +%% Default receive timeout for call() functions +{mapping, "rpc.call_receive_timeout", "gen_rpc.call_receive_timeout", [ + {default, 15000}, + {datatype, integer} +]}. + +%% Socket keepalive configuration +{mapping, "rpc.socket_keepalive_idle", "gen_rpc.socket_keepalive_idle", [ + {default, 7200}, + {datatype, integer} +]}. + +%% Seconds between probes +{mapping, "rpc.socket_keepalive_interval", "gen_rpc.socket_keepalive_interval", [ + {default, 75}, + {datatype, integer} +]}. + +%% Probes lost to close the connection +{mapping, "rpc.socket_keepalive_count", "gen_rpc.socket_keepalive_count", [ + {default, 9}, + {datatype, integer} +]}. + %%-------------------------------------------------------------------- %% Log %%-------------------------------------------------------------------- @@ -331,13 +389,17 @@ end}. {datatype, file} ]}. +{mapping, "log.info.file", "lager.handlers", [ + {datatype, file} +]}. + {mapping, "log.syslog", "lager.handlers", [ {default, off}, {datatype, flag} ]}. {mapping, "log.syslog.identity", "lager.handlers", [ - {default, "emqttd"}, + {default, "emqx"}, {datatype, string} ]}. @@ -366,7 +428,7 @@ end}. {translation, "lager.handlers", fun(Conf) -> - ErrorHandler = case cuttlefish:conf_get("log.error.file", Conf) of + ErrorHandler = case cuttlefish:conf_get("log.error.file", Conf, undefined) of undefined -> []; ErrorFilename -> [{lager_file_backend, [{file, ErrorFilename}, {level, error}, @@ -374,11 +436,19 @@ end}. {date, "$D0"}, {count, 5}]}] end, + InfoHandler = case cuttlefish:conf_get("log.info.file", Conf, undefined) of + undefined -> []; + InfoFilename -> [{lager_file_backend, [{file, InfoFilename}, + {level, info}, + {size, 10485760}, + {date, "$D0"}, + {count, 5}]}] + end, ConsoleLogLevel = cuttlefish:conf_get("log.console.level", Conf), ConsoleLogFile = cuttlefish:conf_get("log.console.file", Conf), - ConsoleHandler = {lager_console_backend, ConsoleLogLevel}, + ConsoleHandler = {lager_console_backend, [{level, ConsoleLogLevel}]}, ConsoleFileHandler = {lager_file_backend, [{file, ConsoleLogFile}, {level, ConsoleLogLevel}, {size, 10485760}, @@ -400,7 +470,7 @@ end}. cuttlefish:conf_get("log.syslog.level", Conf)]}] end, - ConsoleHandlers ++ ErrorHandler ++ SyslogHandler + ConsoleHandlers ++ ErrorHandler ++ InfoHandler ++SyslogHandler end }. @@ -435,25 +505,25 @@ end}. %%-------------------------------------------------------------------- %% @doc Allow Anonymous -{mapping, "mqtt.allow_anonymous", "emqttd.allow_anonymous", [ +{mapping, "mqtt.allow_anonymous", "emqx.allow_anonymous", [ {default, false}, {datatype, {enum, [true, false]}} ]}. %% @doc ACL nomatch -{mapping, "mqtt.acl_nomatch", "emqttd.acl_nomatch", [ +{mapping, "mqtt.acl_nomatch", "emqx.acl_nomatch", [ {default, allow}, {datatype, {enum, [allow, deny]}} ]}. %% @doc Default ACL File -{mapping, "mqtt.acl_file", "emqttd.acl_file", [ +{mapping, "mqtt.acl_file", "emqx.acl_file", [ {datatype, string}, hidden ]}. %% @doc Cache ACL for PUBLISH -{mapping, "mqtt.cache_acl", "emqttd.cache_acl", [ +{mapping, "mqtt.cache_acl", "emqx.cache_acl", [ {default, true}, {datatype, {enum, [true, false]}} ]}. @@ -463,13 +533,13 @@ end}. %%-------------------------------------------------------------------- %% @doc Set the Max ClientId Length Allowed. -{mapping, "mqtt.max_clientid_len", "emqttd.protocol", [ +{mapping, "mqtt.max_clientid_len", "emqx.protocol", [ {default, 1024}, {datatype, integer} ]}. %% @doc Max Packet Size Allowed, 64K by default. -{mapping, "mqtt.max_packet_size", "emqttd.protocol", [ +{mapping, "mqtt.max_packet_size", "emqx.protocol", [ {default, "64KB"}, {datatype, bytesize} ]}. @@ -480,13 +550,13 @@ end}. {datatype, float} ]}. -{translation, "emqttd.protocol", fun(Conf) -> +{translation, "emqx.protocol", fun(Conf) -> [{max_clientid_len, cuttlefish:conf_get("mqtt.max_clientid_len", Conf)}, {max_packet_size, cuttlefish:conf_get("mqtt.max_packet_size", Conf)}, {keepalive_backoff, cuttlefish:conf_get("mqtt.keepalive_backoff", Conf)}] end}. -{mapping, "mqtt.websocket_protocol_header", "emqttd.websocket_protocol_header", [ +{mapping, "mqtt.websocket_protocol_header", "emqx.websocket_protocol_header", [ {default, on}, {datatype, flag} ]}. @@ -496,7 +566,7 @@ end}. %%-------------------------------------------------------------------- %% @doc Force the client to GC: integer -{mapping, "mqtt.conn.force_gc_count", "emqttd.conn_force_gc_count", [ +{mapping, "mqtt.conn.force_gc_count", "emqx.conn_force_gc_count", [ {datatype, integer} ]}. @@ -505,24 +575,24 @@ end}. %%-------------------------------------------------------------------- %% @doc Max Publish Rate of Message -{mapping, "mqtt.client.max_publish_rate", "emqttd.client", [ +{mapping, "mqtt.client.max_publish_rate", "emqx.client", [ {default, 0}, {datatype, integer} ]}. %% @doc Client Idle Timeout. -{mapping, "mqtt.client.idle_timeout", "emqttd.client", [ +{mapping, "mqtt.client.idle_timeout", "emqx.client", [ {default, "30s"}, {datatype, {duration, ms}} ]}. %% @doc Enable Stats of Client. -{mapping, "mqtt.client.enable_stats", "emqttd.client", [ +{mapping, "mqtt.client.enable_stats", "emqx.client", [ {default, off}, {datatype, flag} ]}. -{translation, "emqttd.client", fun(Conf) -> +{translation, "emqx.client", fun(Conf) -> [{max_publish_rate, cuttlefish:conf_get("mqtt.client.max_publish_rate", Conf)}, {client_idle_timeout, cuttlefish:conf_get("mqtt.client.idle_timeout", Conf)}, {client_enable_stats, cuttlefish:conf_get("mqtt.client.enable_stats", Conf)}] @@ -533,61 +603,61 @@ end}. %%-------------------------------------------------------------------- %% @doc Max Number of Subscriptions Allowed -{mapping, "mqtt.session.max_subscriptions", "emqttd.session", [ +{mapping, "mqtt.session.max_subscriptions", "emqx.session", [ {default, 0}, {datatype, integer} ]}. %% @doc Upgrade QoS? -{mapping, "mqtt.session.upgrade_qos", "emqttd.session", [ +{mapping, "mqtt.session.upgrade_qos", "emqx.session", [ {default, off}, {datatype, flag} ]}. %% @doc Max number of QoS 1 and 2 messages that can be “inflight” at one time. %% 0 means no limit -{mapping, "mqtt.session.max_inflight", "emqttd.session", [ +{mapping, "mqtt.session.max_inflight", "emqx.session", [ {default, 100}, {datatype, integer} ]}. %% @doc Retry interval for redelivering QoS1/2 messages. -{mapping, "mqtt.session.retry_interval", "emqttd.session", [ +{mapping, "mqtt.session.retry_interval", "emqx.session", [ {default, "20s"}, {datatype, {duration, ms}} ]}. %% @doc Max Packets that Awaiting PUBREL, 0 means no limit -{mapping, "mqtt.session.max_awaiting_rel", "emqttd.session", [ +{mapping, "mqtt.session.max_awaiting_rel", "emqx.session", [ {default, 0}, {datatype, integer} ]}. %% @doc Awaiting PUBREL Timeout -{mapping, "mqtt.session.await_rel_timeout", "emqttd.session", [ +{mapping, "mqtt.session.await_rel_timeout", "emqx.session", [ {default, "20s"}, {datatype, {duration, ms}} ]}. %% @doc Enable Stats -{mapping, "mqtt.session.enable_stats", "emqttd.session", [ +{mapping, "mqtt.session.enable_stats", "emqx.session", [ {default, off}, {datatype, flag} ]}. %% @doc Session Expiry Interval -{mapping, "mqtt.session.expiry_interval", "emqttd.session", [ +{mapping, "mqtt.session.expiry_interval", "emqx.session", [ {default, "2h"}, {datatype, {duration, ms}} ]}. %% @doc Ignore message from self publish -{mapping, "mqtt.session.ignore_loop_deliver", "emqttd.session", [ +{mapping, "mqtt.session.ignore_loop_deliver", "emqx.session", [ {default, false}, {datatype, {enum, [true, false]}} ]}. -{translation, "emqttd.session", fun(Conf) -> +{translation, "emqx.session", fun(Conf) -> [{max_subscriptions, cuttlefish:conf_get("mqtt.session.max_subscriptions", Conf)}, {upgrade_qos, cuttlefish:conf_get("mqtt.session.upgrade_qos", Conf)}, {max_inflight, cuttlefish:conf_get("mqtt.session.max_inflight", Conf)}, @@ -604,42 +674,42 @@ end}. %%-------------------------------------------------------------------- %% @doc Type: simple | priority -{mapping, "mqtt.mqueue.type", "emqttd.mqueue", [ +{mapping, "mqtt.mqueue.type", "emqx.mqueue", [ {default, simple}, {datatype, atom} ]}. %% @doc Topic Priority: 0~255, Default is 0 -{mapping, "mqtt.mqueue.priority", "emqttd.mqueue", [ +{mapping, "mqtt.mqueue.priority", "emqx.mqueue", [ {default, ""}, {datatype, string} ]}. %% @doc Max queue length. Enqueued messages when persistent client disconnected, or inflight window is full. 0 means no limit. -{mapping, "mqtt.mqueue.max_length", "emqttd.mqueue", [ +{mapping, "mqtt.mqueue.max_length", "emqx.mqueue", [ {default, 0}, {datatype, integer} ]}. %% @doc Low-water mark of queued messages -{mapping, "mqtt.mqueue.low_watermark", "emqttd.mqueue", [ +{mapping, "mqtt.mqueue.low_watermark", "emqx.mqueue", [ {default, "20%"}, {datatype, {percent, float}} ]}. %% @doc High-water mark of queued messages -{mapping, "mqtt.mqueue.high_watermark", "emqttd.mqueue", [ +{mapping, "mqtt.mqueue.high_watermark", "emqx.mqueue", [ {default, "60%"}, {datatype, {percent, float}} ]}. %% @doc Queue Qos0 messages? -{mapping, "mqtt.mqueue.store_qos0", "emqttd.mqueue", [ +{mapping, "mqtt.mqueue.store_qos0", "emqx.mqueue", [ {default, true}, {datatype, {enum, [true, false]}} ]}. -{translation, "emqttd.mqueue", fun(Conf) -> +{translation, "emqx.mqueue", fun(Conf) -> Opts = [{type, cuttlefish:conf_get("mqtt.mqueue.type", Conf, simple)}, {max_length, cuttlefish:conf_get("mqtt.mqueue.max_length", Conf)}, {low_watermark, cuttlefish:conf_get("mqtt.mqueue.low_watermark", Conf)}, @@ -658,7 +728,7 @@ end}. %% MQTT Broker %%-------------------------------------------------------------------- -{mapping, "mqtt.broker.sys_interval", "emqttd.broker_sys_interval", [ +{mapping, "mqtt.broker.sys_interval", "emqx.broker_sys_interval", [ {default, 60}, {datatype, integer} ]}. @@ -667,22 +737,22 @@ end}. %% MQTT PubSub %%-------------------------------------------------------------------- -{mapping, "mqtt.pubsub.pool_size", "emqttd.pubsub", [ +{mapping, "mqtt.pubsub.pool_size", "emqx.pubsub", [ {default, 8}, {datatype, integer} ]}. -{mapping, "mqtt.pubsub.by_clientid", "emqttd.pubsub", [ +{mapping, "mqtt.pubsub.by_clientid", "emqx.pubsub", [ {default, true}, {datatype, {enum, [true, false]}} ]}. -{mapping, "mqtt.pubsub.async", "emqttd.pubsub", [ +{mapping, "mqtt.pubsub.async", "emqx.pubsub", [ {default, true}, {datatype, {enum, [true, false]}} ]}. -{translation, "emqttd.pubsub", fun(Conf) -> +{translation, "emqx.pubsub", fun(Conf) -> [{pool_size, cuttlefish:conf_get("mqtt.pubsub.pool_size", Conf)}, {by_clientid, cuttlefish:conf_get("mqtt.pubsub.by_clientid", Conf)}, {async, cuttlefish:conf_get("mqtt.pubsub.async", Conf)}] @@ -692,17 +762,17 @@ end}. %% MQTT Bridge %%-------------------------------------------------------------------- -{mapping, "mqtt.bridge.max_queue_len", "emqttd.bridge", [ +{mapping, "mqtt.bridge.max_queue_len", "emqx.bridge", [ {default, 10000}, {datatype, integer} ]}. -{mapping, "mqtt.bridge.ping_down_interval", "emqttd.bridge", [ +{mapping, "mqtt.bridge.ping_down_interval", "emqx.bridge", [ {default, 1}, {datatype, integer} ]}. -{translation, "emqttd.bridge", fun(Conf) -> +{translation, "emqx.bridge", fun(Conf) -> [{max_queue_len, cuttlefish:conf_get("mqtt.bridge.max_queue_len", Conf)}, {ping_down_interval, cuttlefish:conf_get("mqtt.bridge.ping_down_interval", Conf)}] end}. @@ -711,11 +781,11 @@ end}. %% MQTT Plugins %%------------------------------------------------------------------- -{mapping, "mqtt.plugins.etc_dir", "emqttd.plugins_etc_dir", [ +{mapping, "mqtt.plugins.etc_dir", "emqx.plugins_etc_dir", [ {datatype, string} ]}. -{mapping, "mqtt.plugins.loaded_file", "emqttd.plugins_loaded_file", [ +{mapping, "mqtt.plugins.loaded_file", "emqx.plugins_loaded_file", [ {datatype, string} ]}. @@ -726,73 +796,73 @@ end}. %%-------------------------------------------------------------------- %% TCP Listeners -{mapping, "listener.tcp.$name", "emqttd.listeners", [ +{mapping, "listener.tcp.$name", "emqx.listeners", [ {datatype, [integer, ip]} ]}. -{mapping, "listener.tcp.$name.acceptors", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.acceptors", "emqx.listeners", [ {default, 8}, {datatype, integer} ]}. -{mapping, "listener.tcp.$name.max_clients", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.max_clients", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. -{mapping, "listener.tcp.$name.zone", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.zone", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.tcp.$name.mountpoint", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.mountpoint", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.tcp.$name.rate_limit", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.rate_limit", "emqx.listeners", [ {default, undefined}, {datatype, string} ]}. -{mapping, "listener.tcp.$name.access.$id", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.access.$id", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.tcp.$name.proxy_protocol", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.proxy_protocol", "emqx.listeners", [ %%{default, off}, {datatype, flag} ]}. -{mapping, "listener.tcp.$name.proxy_protocol_timeout", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.proxy_protocol_timeout", "emqx.listeners", [ %%{default, "5s"}, {datatype, {duration, ms}} ]}. -{mapping, "listener.tcp.$name.backlog", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.backlog", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. -{mapping, "listener.tcp.$name.recbuf", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.recbuf", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.tcp.$name.sndbuf", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.sndbuf", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.tcp.$name.buffer", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.buffer", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.tcp.$name.tune_buffer", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.tune_buffer", "emqx.listeners", [ {datatype, flag}, hidden ]}. -{mapping, "listener.tcp.$name.nodelay", "emqttd.listeners", [ +{mapping, "listener.tcp.$name.nodelay", "emqx.listeners", [ {datatype, {enum, [true, false]}}, hidden ]}. @@ -800,186 +870,186 @@ end}. %%-------------------------------------------------------------------- %% SSL Listeners -{mapping, "listener.ssl.$name", "emqttd.listeners", [ +{mapping, "listener.ssl.$name", "emqx.listeners", [ {datatype, [integer, ip]} ]}. -{mapping, "listener.ssl.$name.acceptors", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.acceptors", "emqx.listeners", [ {default, 8}, {datatype, integer} ]}. -{mapping, "listener.ssl.$name.max_clients", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.max_clients", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. -{mapping, "listener.ssl.$name.zone", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.zone", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.ssl.$name.mountpoint", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.mountpoint", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.ssl.$name.rate_limit", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.rate_limit", "emqx.listeners", [ {default, undefined}, {datatype, string} ]}. -{mapping, "listener.ssl.$name.access.$id", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.access.$id", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.ssl.$name.proxy_protocol", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.proxy_protocol", "emqx.listeners", [ %%{default, off}, {datatype, flag} ]}. -{mapping, "listener.ssl.$name.proxy_protocol_timeout", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.proxy_protocol_timeout", "emqx.listeners", [ %%{default, "5s"}, {datatype, {duration, ms}} ]}. -{mapping, "listener.ssl.$name.backlog", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.backlog", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. -{mapping, "listener.ssl.$name.recbuf", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.recbuf", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.ssl.$name.sndbuf", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.sndbuf", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.ssl.$name.buffer", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.buffer", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.ssl.$name.tune_buffer", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.tune_buffer", "emqx.listeners", [ {datatype, flag}, hidden ]}. -{mapping, "listener.ssl.$name.nodelay", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.nodelay", "emqx.listeners", [ {datatype, {enum, [true, false]}}, hidden ]}. -{mapping, "listener.ssl.$name.tls_versions", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.tls_versions", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.ssl.$name.ciphers", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.ciphers", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.ssl.$name.handshake_timeout", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.handshake_timeout", "emqx.listeners", [ {default, "15s"}, {datatype, {duration, ms}} ]}. -{mapping, "listener.ssl.$name.dhfile", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.dhfile", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.ssl.$name.keyfile", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.keyfile", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.ssl.$name.certfile", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.certfile", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.ssl.$name.cacertfile", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.cacertfile", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.ssl.$name.verify", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.verify", "emqx.listeners", [ {datatype, atom} ]}. -{mapping, "listener.ssl.$name.fail_if_no_peer_cert", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.fail_if_no_peer_cert", "emqx.listeners", [ {datatype, {enum, [true, false]}} ]}. -{mapping, "listener.ssl.$name.secure_renegotiate", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.secure_renegotiate", "emqx.listeners", [ {datatype, flag} ]}. -{mapping, "listener.ssl.$name.reuse_sessions", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.reuse_sessions", "emqx.listeners", [ {default, on}, {datatype, flag} ]}. -{mapping, "listener.ssl.$name.honor_cipher_order", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.honor_cipher_order", "emqx.listeners", [ {datatype, flag} ]}. -{mapping, "listener.ssl.$name.peer_cert_as_username", "emqttd.listeners", [ +{mapping, "listener.ssl.$name.peer_cert_as_username", "emqx.listeners", [ {datatype, {enum, [cn, dn]}} ]}. %%-------------------------------------------------------------------- %% MQTT/WebSocket Listeners -{mapping, "listener.ws.$name", "emqttd.listeners", [ +{mapping, "listener.ws.$name", "emqx.listeners", [ {datatype, [integer, ip]} ]}. -{mapping, "listener.ws.$name.acceptors", "emqttd.listeners", [ +{mapping, "listener.ws.$name.acceptors", "emqx.listeners", [ {default, 8}, {datatype, integer} ]}. -{mapping, "listener.ws.$name.max_clients", "emqttd.listeners", [ +{mapping, "listener.ws.$name.max_clients", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. -{mapping, "listener.ws.$name.rate_limit", "emqttd.listeners", [ +{mapping, "listener.ws.$name.rate_limit", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.ws.$name.zone", "emqttd.listeners", [ +{mapping, "listener.ws.$name.zone", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.ws.$name.access.$id", "emqttd.listeners", [ +{mapping, "listener.ws.$name.access.$id", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.ws.$name.backlog", "emqttd.listeners", [ +{mapping, "listener.ws.$name.backlog", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. -{mapping, "listener.ws.$name.recbuf", "emqttd.listeners", [ +{mapping, "listener.ws.$name.recbuf", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.ws.$name.sndbuf", "emqttd.listeners", [ +{mapping, "listener.ws.$name.sndbuf", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.ws.$name.buffer", "emqttd.listeners", [ +{mapping, "listener.ws.$name.buffer", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.ws.$name.tune_buffer", "emqttd.listeners", [ +{mapping, "listener.ws.$name.tune_buffer", "emqx.listeners", [ {datatype, flag}, hidden ]}. -{mapping, "listener.ws.$name.nodelay", "emqttd.listeners", [ +{mapping, "listener.ws.$name.nodelay", "emqx.listeners", [ {datatype, {enum, [true, false]}}, hidden ]}. @@ -987,92 +1057,92 @@ end}. %%-------------------------------------------------------------------- %% MQTT/WebSocket/SSL Listeners -{mapping, "listener.wss.$name", "emqttd.listeners", [ +{mapping, "listener.wss.$name", "emqx.listeners", [ {datatype, [integer, ip]} ]}. -{mapping, "listener.wss.$name.acceptors", "emqttd.listeners", [ +{mapping, "listener.wss.$name.acceptors", "emqx.listeners", [ {default, 8}, {datatype, integer} ]}. -{mapping, "listener.wss.$name.max_clients", "emqttd.listeners", [ +{mapping, "listener.wss.$name.max_clients", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. -{mapping, "listener.wss.$name.zone", "emqttd.listeners", [ +{mapping, "listener.wss.$name.zone", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.wss.$name.mountpoint", "emqttd.listeners", [ +{mapping, "listener.wss.$name.mountpoint", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.wss.$name.rate_limit", "emqttd.listeners", [ +{mapping, "listener.wss.$name.rate_limit", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.wss.$name.access.$id", "emqttd.listeners", [ +{mapping, "listener.wss.$name.access.$id", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.wss.$name.backlog", "emqttd.listeners", [ +{mapping, "listener.wss.$name.backlog", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. -{mapping, "listener.wss.$name.recbuf", "emqttd.listeners", [ +{mapping, "listener.wss.$name.recbuf", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.wss.$name.sndbuf", "emqttd.listeners", [ +{mapping, "listener.wss.$name.sndbuf", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.wss.$name.buffer", "emqttd.listeners", [ +{mapping, "listener.wss.$name.buffer", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.wss.$name.tune_buffer", "emqttd.listeners", [ +{mapping, "listener.wss.$name.tune_buffer", "emqx.listeners", [ {datatype, flag}, hidden ]}. -{mapping, "listener.wss.$name.nodelay", "emqttd.listeners", [ +{mapping, "listener.wss.$name.nodelay", "emqx.listeners", [ {datatype, {enum, [true, false]}}, hidden ]}. -{mapping, "listener.wss.$name.handshake_timeout", "emqttd.listeners", [ +{mapping, "listener.wss.$name.handshake_timeout", "emqx.listeners", [ {default, "15s"}, {datatype, {duration, ms}} ]}. -{mapping, "listener.wss.$name.keyfile", "emqttd.listeners", [ +{mapping, "listener.wss.$name.keyfile", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.wss.$name.certfile", "emqttd.listeners", [ +{mapping, "listener.wss.$name.certfile", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.wss.$name.cacertfile", "emqttd.listeners", [ +{mapping, "listener.wss.$name.cacertfile", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.wss.$name.verify", "emqttd.listeners", [ +{mapping, "listener.wss.$name.verify", "emqx.listeners", [ {datatype, atom} ]}. -{mapping, "listener.wss.$name.fail_if_no_peer_cert", "emqttd.listeners", [ +{mapping, "listener.wss.$name.fail_if_no_peer_cert", "emqx.listeners", [ {datatype, {enum, [true, false]}} ]}. -{translation, "emqttd.listeners", fun(Conf) -> +{translation, "emqx.listeners", fun(Conf) -> Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, @@ -1188,79 +1258,79 @@ end}. %%-------------------------------------------------------------------- %% MQTT REST API Listeners -{mapping, "listener.api.$name", "emqttd.listeners", [ +{mapping, "listener.api.$name", "emqx.listeners", [ {datatype, [integer, ip]} ]}. -{mapping, "listener.api.$name.acceptors", "emqttd.listeners", [ +{mapping, "listener.api.$name.acceptors", "emqx.listeners", [ {default, 8}, {datatype, integer} ]}. -{mapping, "listener.api.$name.max_clients", "emqttd.listeners", [ +{mapping, "listener.api.$name.max_clients", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. -{mapping, "listener.api.$name.rate_limit", "emqttd.listeners", [ +{mapping, "listener.api.$name.rate_limit", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.api.$name.access.$id", "emqttd.listeners", [ +{mapping, "listener.api.$name.access.$id", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.api.$name.backlog", "emqttd.listeners", [ +{mapping, "listener.api.$name.backlog", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. -{mapping, "listener.api.$name.recbuf", "emqttd.listeners", [ +{mapping, "listener.api.$name.recbuf", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.api.$name.sndbuf", "emqttd.listeners", [ +{mapping, "listener.api.$name.sndbuf", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.api.$name.buffer", "emqttd.listeners", [ +{mapping, "listener.api.$name.buffer", "emqx.listeners", [ {datatype, bytesize}, hidden ]}. -{mapping, "listener.api.$name.tune_buffer", "emqttd.listeners", [ +{mapping, "listener.api.$name.tune_buffer", "emqx.listeners", [ {datatype, flag}, hidden ]}. -{mapping, "listener.api.$name.nodelay", "emqttd.listeners", [ +{mapping, "listener.api.$name.nodelay", "emqx.listeners", [ {datatype, {enum, [true, false]}}, hidden ]}. -{mapping, "listener.api.$name.handshake_timeout", "emqttd.listeners", [ +{mapping, "listener.api.$name.handshake_timeout", "emqx.listeners", [ {datatype, {duration, ms}} ]}. -{mapping, "listener.api.$name.keyfile", "emqttd.listeners", [ +{mapping, "listener.api.$name.keyfile", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.api.$name.certfile", "emqttd.listeners", [ +{mapping, "listener.api.$name.certfile", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.api.$name.cacertfile", "emqttd.listeners", [ +{mapping, "listener.api.$name.cacertfile", "emqx.listeners", [ {datatype, string} ]}. -{mapping, "listener.api.$name.verify", "emqttd.listeners", [ +{mapping, "listener.api.$name.verify", "emqx.listeners", [ {datatype, atom} ]}. -{mapping, "listener.api.$name.fail_if_no_peer_cert", "emqttd.listeners", [ +{mapping, "listener.api.$name.fail_if_no_peer_cert", "emqx.listeners", [ {datatype, {enum, [true, false]}} ]}. @@ -1270,36 +1340,36 @@ end}. %% @doc Long GC, don't monitor in production mode for: %% https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 -{mapping, "sysmon.long_gc", "emqttd.sysmon", [ +{mapping, "sysmon.long_gc", "emqx.sysmon", [ {default, false}, {datatype, {enum, [true, false]}} ]}. %% @doc Long Schedule(ms) -{mapping, "sysmon.long_schedule", "emqttd.sysmon", [ +{mapping, "sysmon.long_schedule", "emqx.sysmon", [ {default, 1000}, {datatype, integer} ]}. %% @doc Large Heap -{mapping, "sysmon.large_heap", "emqttd.sysmon", [ +{mapping, "sysmon.large_heap", "emqx.sysmon", [ {default, "8MB"}, {datatype, bytesize} ]}. %% @doc Monitor Busy Port -{mapping, "sysmon.busy_port", "emqttd.sysmon", [ +{mapping, "sysmon.busy_port", "emqx.sysmon", [ {default, false}, {datatype, {enum, [true, false]}} ]}. %% @doc Monitor Busy Dist Port -{mapping, "sysmon.busy_dist_port", "emqttd.sysmon", [ +{mapping, "sysmon.busy_dist_port", "emqx.sysmon", [ {default, true}, {datatype, {enum, [true, false]}} ]}. -{translation, "emqttd.sysmon", fun(Conf) -> +{translation, "emqx.sysmon", fun(Conf) -> [{long_gc, cuttlefish:conf_get("sysmon.long_gc", Conf)}, {long_schedule, cuttlefish:conf_get("sysmon.long_schedule", Conf)}, {large_heap, cuttlefish:conf_get("sysmon.large_heap", Conf)}, diff --git a/src/emqttd.app.src b/src/emqttd.app.src deleted file mode 100644 index 0b85fe0f0..000000000 --- a/src/emqttd.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application,emqttd, - [{description,"Erlang MQTT Broker"}, - {vsn,"2.3"}, - {modules,[]}, - {registered,[emqttd_sup]}, - {applications,[kernel,stdlib,gproc,lager,esockd,mochiweb, - lager_syslog,pbkdf2,bcrypt]}, - {env,[]}, - {mod,{emqttd_app,[]}}, - {maintainers,["Feng Lee "]}, - {licenses,["Apache-2.0"]}, - {links,[{"Github","https://github.com/emqtt/emqttd"}]}]}. diff --git a/src/emqttd.erl b/src/emqttd.erl deleted file mode 100644 index d4cdd8437..000000000 --- a/src/emqttd.erl +++ /dev/null @@ -1,187 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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 EMQ Main Module. - --module(emqttd). - --author("Feng Lee "). - --include("emqttd.hrl"). - --include("emqttd_protocol.hrl"). - --export([start/0, env/1, env/2, is_running/1, stop/0]). - -%% PubSub API --export([subscribe/1, subscribe/2, subscribe/3, publish/1, - unsubscribe/1, unsubscribe/2]). - -%% PubSub Management API --export([setqos/3, topics/0, subscriptions/1, subscribers/1, - is_subscribed/2, subscriber_down/1]). - -%% Hooks API --export([hook/4, hook/3, unhook/2, run_hooks/2, run_hooks/3]). - -%% Debug API --export([dump/0]). - -%% Shutdown and reboot --export([shutdown/0, shutdown/1, reboot/0]). - --type(subscriber() :: pid() | binary()). - --type(suboption() :: local | {qos, non_neg_integer()} | {share, {'$queue' | binary()}}). - --type(pubsub_error() :: {error, {already_subscribed, binary()} - | {subscription_not_found, binary()}}). - --export_type([subscriber/0, suboption/0, pubsub_error/0]). - --define(APP, ?MODULE). - -%%-------------------------------------------------------------------- -%% Bootstrap, environment, configuration, is_running... -%%-------------------------------------------------------------------- - -%% @doc Start emqttd application. --spec(start() -> ok | {error, any()}). -start() -> application:start(?APP). - -%% @doc Stop emqttd application. --spec(stop() -> ok | {error, any()}). -stop() -> application:stop(?APP). - -%% @doc Environment --spec(env(Key:: atom()) -> {ok, any()} | undefined). -env(Key) -> application:get_env(?APP, Key). - -%% @doc Get environment --spec(env(Key:: atom(), Default:: any()) -> undefined | any()). -env(Key, Default) -> application:get_env(?APP, Key, Default). - -%% @doc Is running? --spec(is_running(node()) -> boolean()). -is_running(Node) -> - case rpc:call(Node, erlang, whereis, [?APP]) of - {badrpc, _} -> false; - undefined -> false; - Pid when is_pid(Pid) -> true - end. - -%%-------------------------------------------------------------------- -%% PubSub APIs -%%-------------------------------------------------------------------- - -%% @doc Subscribe --spec(subscribe(iodata()) -> ok | {error, any()}). -subscribe(Topic) -> - subscribe(Topic, self()). - --spec(subscribe(iodata(), subscriber()) -> ok | {error, any()}). -subscribe(Topic, Subscriber) -> - subscribe(Topic, Subscriber, []). - --spec(subscribe(iodata(), subscriber(), [suboption()]) -> ok | pubsub_error()). -subscribe(Topic, Subscriber, Options) -> - emqttd_server:subscribe(iolist_to_binary(Topic), Subscriber, Options). - -%% @doc Publish MQTT Message --spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore). -publish(Msg) -> - emqttd_server:publish(Msg). - -%% @doc Unsubscribe --spec(unsubscribe(iodata()) -> ok | pubsub_error()). -unsubscribe(Topic) -> - unsubscribe(Topic, self()). - --spec(unsubscribe(iodata(), subscriber()) -> ok | pubsub_error()). -unsubscribe(Topic, Subscriber) -> - emqttd_server:unsubscribe(iolist_to_binary(Topic), Subscriber). - --spec(setqos(binary(), subscriber(), mqtt_qos()) -> ok). -setqos(Topic, Subscriber, Qos) -> - emqttd_server:setqos(iolist_to_binary(Topic), Subscriber, Qos). - --spec(topics() -> [binary()]). -topics() -> emqttd_router:topics(). - --spec(subscribers(iodata()) -> list(subscriber())). -subscribers(Topic) -> - emqttd_server:subscribers(iolist_to_binary(Topic)). - --spec(subscriptions(subscriber()) -> [{binary(), binary(), list(suboption())}]). -subscriptions(Subscriber) -> - emqttd_server:subscriptions(Subscriber). - --spec(is_subscribed(iodata(), subscriber()) -> boolean()). -is_subscribed(Topic, Subscriber) -> - emqttd_server:is_subscribed(iolist_to_binary(Topic), Subscriber). - --spec(subscriber_down(subscriber()) -> ok). -subscriber_down(Subscriber) -> - emqttd_server:subscriber_down(Subscriber). - -%%-------------------------------------------------------------------- -%% Hooks API -%%-------------------------------------------------------------------- - --spec(hook(atom(), function() | {emqttd_hooks:hooktag(), function()}, list(any())) - -> ok | {error, any()}). -hook(Hook, TagFunction, InitArgs) -> - emqttd_hooks:add(Hook, TagFunction, InitArgs). - --spec(hook(atom(), function() | {emqttd_hooks:hooktag(), function()}, list(any()), integer()) - -> ok | {error, any()}). -hook(Hook, TagFunction, InitArgs, Priority) -> - emqttd_hooks:add(Hook, TagFunction, InitArgs, Priority). - --spec(unhook(atom(), function() | {emqttd_hooks:hooktag(), function()}) - -> ok | {error, any()}). -unhook(Hook, TagFunction) -> - emqttd_hooks:delete(Hook, TagFunction). - --spec(run_hooks(atom(), list(any())) -> ok | stop). -run_hooks(Hook, Args) -> - emqttd_hooks:run(Hook, Args). - --spec(run_hooks(atom(), list(any()), any()) -> {ok | stop, any()}). -run_hooks(Hook, Args, Acc) -> - emqttd_hooks:run(Hook, Args, Acc). - -%%-------------------------------------------------------------------- -%% Shutdown and reboot -%%-------------------------------------------------------------------- - -shutdown() -> - shutdown(normal). - -shutdown(Reason) -> - lager:error("EMQ shutdown for ~s", [Reason]), - emqttd_plugins:unload(), - lists:foreach(fun application:stop/1, [emqttd, ekka, mochiweb, esockd, gproc]). - -reboot() -> - lists:foreach(fun application:start/1, [gproc, esockd, mochiweb, ekka, emqttd]). - -%%-------------------------------------------------------------------- -%% Debug -%%-------------------------------------------------------------------- - -dump() -> lists:append([emqttd_server:dump(), emqttd_router:dump()]). - diff --git a/src/emqttd_app.erl b/src/emqttd_app.erl deleted file mode 100644 index 1e99cb951..000000000 --- a/src/emqttd_app.erl +++ /dev/null @@ -1,242 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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(emqttd_app). - --behaviour(application). - --author("Feng Lee "). - --include("emqttd_cli.hrl"). - --include("emqttd_protocol.hrl"). - -%% Application callbacks --export([start/2, stop/1]). - --export([start_listener/1, stop_listener/1, restart_listener/1]). - --type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}). - --define(APP, emqttd). - -%%-------------------------------------------------------------------- -%% Application Callbacks -%%-------------------------------------------------------------------- - -start(_Type, _Args) -> - print_banner(), - ekka:start(), - {ok, Sup} = emqttd_sup:start_link(), - start_servers(Sup), - emqttd_cli:load(), - register_acl_mod(), - start_autocluster(), - register(emqttd, self()), - print_vsn(), - {ok, Sup}. - --spec(stop(State :: term()) -> term()). -stop(_State) -> - catch stop_listeners(). - -%%-------------------------------------------------------------------- -%% Print Banner -%%-------------------------------------------------------------------- - -print_banner() -> - ?PRINT("starting ~s on node '~s'~n", [?APP, node()]). - -print_vsn() -> - {ok, Vsn} = application:get_key(vsn), - ?PRINT("~s ~s is running now~n", [?APP, Vsn]). - -%%-------------------------------------------------------------------- -%% Start Servers -%%-------------------------------------------------------------------- - -start_servers(Sup) -> - Servers = [{"emqttd ctl", emqttd_ctl}, - {"emqttd hook", emqttd_hooks}, - {"emqttd router", emqttd_router}, - {"emqttd pubsub", {supervisor, emqttd_pubsub_sup}}, - {"emqttd stats", emqttd_stats}, - {"emqttd metrics", emqttd_metrics}, - {"emqttd pooler", {supervisor, emqttd_pooler}}, - {"emqttd trace", {supervisor, emqttd_trace_sup}}, - {"emqttd client manager", {supervisor, emqttd_cm_sup}}, - {"emqttd session manager", {supervisor, emqttd_sm_sup}}, - {"emqttd session supervisor", {supervisor, emqttd_session_sup}}, - {"emqttd wsclient supervisor", {supervisor, emqttd_ws_client_sup}}, - {"emqttd broker", emqttd_broker}, - {"emqttd alarm", emqttd_alarm}, - {"emqttd mod supervisor", emqttd_mod_sup}, - {"emqttd bridge supervisor", {supervisor, emqttd_bridge_sup_sup}}, - {"emqttd access control", emqttd_access_control}, - {"emqttd system monitor", {supervisor, emqttd_sysmon_sup}}], - [start_server(Sup, Server) || Server <- Servers]. - -start_server(_Sup, {Name, F}) when is_function(F) -> - ?PRINT("~s is starting...", [Name]), - F(), - ?PRINT_MSG("[ok]~n"); - -start_server(Sup, {Name, Server}) -> - ?PRINT("~s is starting...", [Name]), - start_child(Sup, Server), - ?PRINT_MSG("[ok]~n"); - -start_server(Sup, {Name, Server, Opts}) -> - ?PRINT("~s is starting...", [ Name]), - start_child(Sup, Server, Opts), - ?PRINT_MSG("[ok]~n"). - -start_child(Sup, {supervisor, Module}) -> - supervisor:start_child(Sup, supervisor_spec(Module)); - -start_child(Sup, Module) when is_atom(Module) -> - {ok, _ChiId} = supervisor:start_child(Sup, worker_spec(Module)). - -start_child(Sup, {supervisor, Module}, Opts) -> - supervisor:start_child(Sup, supervisor_spec(Module, Opts)); - -start_child(Sup, Module, Opts) when is_atom(Module) -> - supervisor:start_child(Sup, worker_spec(Module, Opts)). - -supervisor_spec(Module) when is_atom(Module) -> - supervisor_spec(Module, start_link, []). - -supervisor_spec(Module, Opts) -> - supervisor_spec(Module, start_link, [Opts]). - -supervisor_spec(M, F, A) -> - {M, {M, F, A}, permanent, infinity, supervisor, [M]}. - -worker_spec(Module) when is_atom(Module) -> - worker_spec(Module, start_link, []). - -worker_spec(Module, Opts) when is_atom(Module) -> - worker_spec(Module, start_link, [Opts]). - -worker_spec(M, F, A) -> - {M, {M, F, A}, permanent, 10000, worker, [M]}. - -%%-------------------------------------------------------------------- -%% Register default ACL File -%%-------------------------------------------------------------------- - -register_acl_mod() -> - case emqttd:env(acl_file) of - {ok, File} -> emqttd_access_control:register_mod(acl, emqttd_acl_internal, [File]); - undefined -> ok - end. - -%%-------------------------------------------------------------------- -%% Autocluster -%%-------------------------------------------------------------------- - -start_autocluster() -> - ekka:callback(prepare, fun emqttd:shutdown/1), - ekka:callback(reboot, fun emqttd:reboot/0), - ekka:autocluster(?APP, fun after_autocluster/0). - -after_autocluster() -> - emqttd_plugins:init(), - emqttd_plugins:load(), - start_listeners(). - -%%-------------------------------------------------------------------- -%% Start Listeners -%%-------------------------------------------------------------------- - -%% @doc Start Listeners of the broker. --spec(start_listeners() -> any()). -start_listeners() -> lists:foreach(fun start_listener/1, emqttd:env(listeners, [])). - -%% Start mqtt listener --spec(start_listener(listener()) -> any()). -start_listener({tcp, ListenOn, Opts}) -> - start_listener('mqtt:tcp', ListenOn, Opts); - -%% Start mqtt(SSL) listener -start_listener({ssl, ListenOn, Opts}) -> - start_listener('mqtt:ssl', ListenOn, Opts); - -%% Start http listener -start_listener({Proto, ListenOn, Opts}) when Proto == http; Proto == ws -> - mochiweb:start_http('mqtt:ws', ListenOn, Opts, {emqttd_ws, handle_request, []}); - -%% Start https listener -start_listener({Proto, ListenOn, Opts}) when Proto == https; Proto == wss -> - mochiweb:start_http('mqtt:wss', ListenOn, Opts, {emqttd_ws, handle_request, []}); - -start_listener({Proto, ListenOn, Opts}) when Proto == api -> - mochiweb:start_http('mqtt:api', ListenOn, Opts, emqttd_http:http_handler()). - -start_listener(Proto, ListenOn, Opts) -> - Env = lists:append(emqttd:env(client, []), emqttd:env(protocol, [])), - MFArgs = {emqttd_client, start_link, [Env]}, - {ok, _} = esockd:open(Proto, ListenOn, merge_sockopts(Opts), MFArgs). - -merge_sockopts(Options) -> - SockOpts = emqttd_misc:merge_opts( - ?MQTT_SOCKOPTS, proplists:get_value(sockopts, Options, [])), - emqttd_misc:merge_opts(Options, [{sockopts, SockOpts}]). - -%%-------------------------------------------------------------------- -%% Stop Listeners -%%-------------------------------------------------------------------- - -%% @doc Stop Listeners -stop_listeners() -> lists:foreach(fun stop_listener/1, emqttd:env(listeners, [])). - -%% @private -stop_listener({tcp, ListenOn, _Opts}) -> - esockd:close('mqtt:tcp', ListenOn); -stop_listener({ssl, ListenOn, _Opts}) -> - esockd:close('mqtt:ssl', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> - mochiweb:stop_http('mqtt:ws', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> - mochiweb:stop_http('mqtt:wss', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == api -> - mochiweb:stop_http('mqtt:api', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) -> - esockd:close(Proto, ListenOn). - -%% @doc Restart Listeners -restart_listener({tcp, ListenOn, _Opts}) -> - esockd:reopen('mqtt:tcp', ListenOn); -restart_listener({ssl, ListenOn, _Opts}) -> - esockd:reopen('mqtt:ssl', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> - mochiweb:restart_http('mqtt:ws', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> - mochiweb:restart_http('mqtt:wss', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == api -> - mochiweb:restart_http('mqtt:api', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) -> - esockd:reopen(Proto, ListenOn). - - --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). -merge_sockopts_test_() -> - Opts = [{acceptors, 16}, {max_clients, 512}], - ?_assert(merge_sockopts(Opts) == [{sockopts, ?MQTT_SOCKOPTS} | Opts]). - --endif. - diff --git a/src/emqttd_sup.erl b/src/emqttd_sup.erl deleted file mode 100644 index e38d20d65..000000000 --- a/src/emqttd_sup.erl +++ /dev/null @@ -1,55 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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(emqttd_sup). - --behaviour(supervisor). - --author("Feng Lee "). - --include("emqttd.hrl"). - -%% API --export([start_link/0, start_child/1, start_child/2]). - -%% Supervisor callbacks --export([init/1]). - -%% Helper macro for declaring children of supervisor --define(CHILD(Mod, Type), {Mod, {Mod, start_link, []}, - permanent, 5000, Type, [Mod]}). - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -start_child(ChildSpec) when is_tuple(ChildSpec) -> - supervisor:start_child(?MODULE, ChildSpec). - --spec(start_child(Mod::atom(), Type :: worker | supervisor) -> {ok, pid()}). -start_child(Mod, Type) when is_atom(Mod) and is_atom(Type) -> - supervisor:start_child(?MODULE, ?CHILD(Mod, Type)). - -%%-------------------------------------------------------------------- -%% Supervisor callbacks -%%-------------------------------------------------------------------- - -init([]) -> - {ok, {{one_for_all, 0, 1}, []}}. - diff --git a/src/emqx.erl b/src/emqx.erl new file mode 100644 index 000000000..66c368cf3 --- /dev/null +++ b/src/emqx.erl @@ -0,0 +1,279 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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 EMQ X Main Module. + +-module(emqx). + +-author("Feng Lee "). + +-include("emqx.hrl"). + +-include("emqx_mqtt.hrl"). + +%% Start/Stop Application +-export([start/0, env/1, env/2, is_running/1, stop/0]). + +%% Start/Stop Listeners +-export([start_listeners/0, start_listener/1, listeners/0, + stop_listeners/0, stop_listener/1, + restart_listeners/0, restart_listener/1]). + +%% PubSub API +-export([subscribe/1, subscribe/2, subscribe/3, publish/1, + unsubscribe/1, unsubscribe/2]). + +%% PubSub Management API +-export([setqos/3, topics/0, subscriptions/1, subscribers/1, + is_subscribed/2, subscriber_down/1]). + +%% Hooks API +-export([hook/4, hook/3, unhook/2, run_hooks/2, run_hooks/3]). + +%% Debug API +-export([dump/0]). + +%% Shutdown and reboot +-export([shutdown/0, shutdown/1, reboot/0]). + +-type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}). + +-type(subscriber() :: pid() | binary()). + +-type(suboption() :: local | {qos, non_neg_integer()} | {share, {'$queue' | binary()}}). + +-export_type([subscriber/0, suboption/0]). + +-define(APP, ?MODULE). + +%%-------------------------------------------------------------------- +%% Bootstrap, environment, configuration, is_running... +%%-------------------------------------------------------------------- + +%% @doc Start emqx application. +-spec(start() -> ok | {error, any()}). +start() -> application:start(?APP). + +%% @doc Stop emqx application. +-spec(stop() -> ok | {error, any()}). +stop() -> application:stop(?APP). + +%% @doc Get Environment +-spec(env(Key:: atom()) -> {ok, any()} | undefined). +env(Key) -> application:get_env(?APP, Key). + +%% @doc Get environment with default +-spec(env(Key:: atom(), Default:: any()) -> undefined | any()). +env(Key, Default) -> application:get_env(?APP, Key, Default). + +%% @doc Is running? +-spec(is_running(node()) -> boolean()). +is_running(Node) -> + case rpc:call(Node, erlang, whereis, [?APP]) of + {badrpc, _} -> false; + undefined -> false; + Pid when is_pid(Pid) -> true + end. + +%%-------------------------------------------------------------------- +%% Start/Stop Listeners +%%-------------------------------------------------------------------- + +%% @doc Start Listeners. +-spec(start_listeners() -> ok). +start_listeners() -> lists:foreach(fun start_listener/1, env(listeners, [])). + +%% Start mqtt listener +-spec(start_listener(listener()) -> {ok, pid()} | {error, any()}). +start_listener({tcp, ListenOn, Opts}) -> + start_listener('mqtt:tcp', ListenOn, Opts); + +%% Start mqtt(SSL) listener +start_listener({ssl, ListenOn, Opts}) -> + start_listener('mqtt:ssl', ListenOn, Opts); + +%% Start http listener +start_listener({Proto, ListenOn, Opts}) when Proto == http; Proto == ws -> + {ok, _} = mochiweb:start_http('mqtt:ws', ListenOn, Opts, {emqx_ws, handle_request, []}); + +%% Start https listener +start_listener({Proto, ListenOn, Opts}) when Proto == https; Proto == wss -> + {ok, _} = mochiweb:start_http('mqtt:wss', ListenOn, Opts, {emqx_ws, handle_request, []}); + +start_listener({Proto, ListenOn, Opts}) when Proto == api -> + {ok, _} = mochiweb:start_http('mqtt:api', ListenOn, Opts, emqx_http:http_handler()). + +start_listener(Proto, ListenOn, Opts) -> + Env = lists:append(emqx:env(client, []), emqx:env(protocol, [])), + MFArgs = {emqx_client, start_link, [Env]}, + {ok, _} = esockd:open(Proto, ListenOn, merge_sockopts(Opts), MFArgs). + +listeners() -> + [Listener || Listener = {{Proto, _}, _Pid} <- esockd:listeners(), is_mqtt(Proto)]. + +is_mqtt('mqtt:tcp') -> true; +is_mqtt('mqtt:ssl') -> true; +is_mqtt('mqtt:ws') -> true; +is_mqtt('mqtt:wss') -> true; +is_mqtt(_Proto) -> false. + +%% @doc Stop Listeners +-spec(stop_listeners() -> ok). +stop_listeners() -> lists:foreach(fun stop_listener/1, env(listeners, [])). + +-spec(stop_listener(listener()) -> ok | {error, any()}). +stop_listener({tcp, ListenOn, _Opts}) -> + esockd:close('mqtt:tcp', ListenOn); +stop_listener({ssl, ListenOn, _Opts}) -> + esockd:close('mqtt:ssl', ListenOn); +stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> + mochiweb:stop_http('mqtt:ws', ListenOn); +stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> + mochiweb:stop_http('mqtt:wss', ListenOn); +stop_listener({Proto, ListenOn, _Opts}) when Proto == api -> + mochiweb:stop_http('mqtt:api', ListenOn); +stop_listener({Proto, ListenOn, _Opts}) -> + esockd:close(Proto, ListenOn). + +%% @doc Restart Listeners +-spec(restart_listeners() -> ok). +restart_listeners() -> lists:foreach(fun restart_listener/1, env(listeners, [])). + +-spec(restart_listener(listener()) -> any()). +restart_listener({tcp, ListenOn, _Opts}) -> + esockd:reopen('mqtt:tcp', ListenOn); +restart_listener({ssl, ListenOn, _Opts}) -> + esockd:reopen('mqtt:ssl', ListenOn); +restart_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> + mochiweb:restart_http('mqtt:ws', ListenOn); +restart_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> + mochiweb:restart_http('mqtt:wss', ListenOn); +restart_listener({Proto, ListenOn, _Opts}) when Proto == api -> + mochiweb:restart_http('mqtt:api', ListenOn); +restart_listener({Proto, ListenOn, _Opts}) -> + esockd:reopen(Proto, ListenOn). + +merge_sockopts(Options) -> + SockOpts = emqx_misc:merge_opts( + ?MQTT_SOCKOPTS, proplists:get_value(sockopts, Options, [])), + emqx_misc:merge_opts(Options, [{sockopts, SockOpts}]). + +%%-------------------------------------------------------------------- +%% PubSub APIs +%%-------------------------------------------------------------------- + +%% @doc Subscribe +-spec(subscribe(iodata()) -> ok | {error, any()}). +subscribe(Topic) -> + subscribe(Topic, self()). + +-spec(subscribe(iodata(), subscriber()) -> ok | {error, any()}). +subscribe(Topic, Subscriber) -> + subscribe(Topic, Subscriber, []). + +-spec(subscribe(iodata(), subscriber(), [suboption()]) -> ok | {error, term()}). +subscribe(Topic, Subscriber, Options) -> + emqx_server:subscribe(iolist_to_binary(Topic), Subscriber, Options). + +%% @doc Publish MQTT Message +-spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore). +publish(Msg) -> + emqx_server:publish(Msg). + +%% @doc Unsubscribe +-spec(unsubscribe(iodata()) -> ok | {error, term()}). +unsubscribe(Topic) -> + unsubscribe(Topic, self()). + +-spec(unsubscribe(iodata(), subscriber()) -> ok | {error, term()}). +unsubscribe(Topic, Subscriber) -> + emqx_server:unsubscribe(iolist_to_binary(Topic), Subscriber). + +%%-------------------------------------------------------------------- +%% PubSub Management API +%%-------------------------------------------------------------------- + +-spec(setqos(binary(), subscriber(), mqtt_qos()) -> ok). +setqos(Topic, Subscriber, Qos) -> + emqx_server:setqos(iolist_to_binary(Topic), Subscriber, Qos). + +-spec(topics() -> [binary()]). +topics() -> emqx_router:topics(). + +-spec(subscribers(iodata()) -> list(subscriber())). +subscribers(Topic) -> + emqx_server:subscribers(iolist_to_binary(Topic)). + +-spec(subscriptions(subscriber()) -> [{binary(), binary(), list(suboption())}]). +subscriptions(Subscriber) -> + emqx_server:subscriptions(Subscriber). + +-spec(is_subscribed(iodata(), subscriber()) -> boolean()). +is_subscribed(Topic, Subscriber) -> + emqx_server:is_subscribed(iolist_to_binary(Topic), Subscriber). + +-spec(subscriber_down(subscriber()) -> ok). +subscriber_down(Subscriber) -> + emqx_server:subscriber_down(Subscriber). + +%%-------------------------------------------------------------------- +%% Hooks API +%%-------------------------------------------------------------------- + +-spec(hook(atom(), function() | {emqx_hooks:hooktag(), function()}, list(any())) + -> ok | {error, any()}). +hook(Hook, TagFunction, InitArgs) -> + emqx_hooks:add(Hook, TagFunction, InitArgs). + +-spec(hook(atom(), function() | {emqx_hooks:hooktag(), function()}, list(any()), integer()) + -> ok | {error, any()}). +hook(Hook, TagFunction, InitArgs, Priority) -> + emqx_hooks:add(Hook, TagFunction, InitArgs, Priority). + +-spec(unhook(atom(), function() | {emqx_hooks:hooktag(), function()}) + -> ok | {error, any()}). +unhook(Hook, TagFunction) -> + emqx_hooks:delete(Hook, TagFunction). + +-spec(run_hooks(atom(), list(any())) -> ok | stop). +run_hooks(Hook, Args) -> + emqx_hooks:run(Hook, Args). + +-spec(run_hooks(atom(), list(any()), any()) -> {ok | stop, any()}). +run_hooks(Hook, Args, Acc) -> + emqx_hooks:run(Hook, Args, Acc). + +%%-------------------------------------------------------------------- +%% Shutdown and reboot +%%-------------------------------------------------------------------- + +shutdown() -> + shutdown(normal). + +shutdown(Reason) -> + lager:error("EMQ shutdown for ~s", [Reason]), + emqx_plugins:unload(), + lists:foreach(fun application:stop/1, [emqx, ekka, mochiweb, esockd, gproc]). + +reboot() -> + lists:foreach(fun application:start/1, [gproc, esockd, mochiweb, ekka, emqx]). + +%%-------------------------------------------------------------------- +%% Debug +%%-------------------------------------------------------------------- + +dump() -> lists:append([emqx_server:dump(), emqx_router:dump()]). + diff --git a/src/emqttd_access_control.erl b/src/emqx_access_control.erl similarity index 97% rename from src/emqttd_access_control.erl rename to src/emqx_access_control.erl index 6cfcc03cf..2515689fa 100644 --- a/src/emqttd_access_control.erl +++ b/src/emqx_access_control.erl @@ -14,13 +14,13 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_access_control). +-module(emqx_access_control). -behaviour(gen_server). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). %% API Function Exports -export([start_link/0, auth/2, check_acl/3, reload_acl/0, lookup_mods/1, @@ -52,7 +52,7 @@ start_link() -> auth(Client, Password) when is_record(Client, mqtt_client) -> auth(Client, Password, lookup_mods(auth)). auth(_Client, _Password, []) -> - case emqttd:env(allow_anonymous, false) of + case emqx:env(allow_anonymous, false) of true -> ok; false -> {error, "No auth module to check!"} end; @@ -74,7 +74,7 @@ check_acl(Client, PubSub, Topic) when ?PS(PubSub) -> check_acl(Client, PubSub, Topic, lookup_mods(acl)). check_acl(_Client, _PubSub, _Topic, []) -> - emqttd:env(acl_nomatch, allow); + emqx:env(acl_nomatch, allow); check_acl(Client, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) -> case Mod:check_acl({Client, PubSub, Topic}, State) of allow -> allow; diff --git a/src/emqttd_access_rule.erl b/src/emqx_access_rule.erl similarity index 94% rename from src/emqttd_access_rule.erl rename to src/emqx_access_rule.erl index 73718fd3a..294ba06ab 100644 --- a/src/emqttd_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -14,12 +14,11 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_access_rule). +-module(emqx_access_rule). -author("Feng Lee "). --include("emqttd.hrl"). - +-include("emqx.hrl"). -type(who() :: all | binary() | {ipaddr, esockd_cidr:cidr_string()} | @@ -69,9 +68,9 @@ compile(who, {'or', Conds}) when is_list(Conds) -> {'or', [compile(who, Cond) || Cond <- Conds]}; compile(topic, {eq, Topic}) -> - {eq, emqttd_topic:words(bin(Topic))}; + {eq, emqx_topic:words(bin(Topic))}; compile(topic, Topic) -> - Words = emqttd_topic:words(bin(Topic)), + Words = emqx_topic:words(bin(Topic)), case 'pattern?'(Words) of true -> {pattern, Words}; false -> Words @@ -126,12 +125,12 @@ match_topics(_Client, _Topic, []) -> false; match_topics(Client, Topic, [{pattern, PatternFilter}|Filters]) -> TopicFilter = feed_var(Client, PatternFilter), - case match_topic(emqttd_topic:words(Topic), TopicFilter) of + case match_topic(emqx_topic:words(Topic), TopicFilter) of true -> true; false -> match_topics(Client, Topic, Filters) end; match_topics(Client, Topic, [TopicFilter|Filters]) -> - case match_topic(emqttd_topic:words(Topic), TopicFilter) of + case match_topic(emqx_topic:words(Topic), TopicFilter) of true -> true; false -> match_topics(Client, Topic, Filters) end. @@ -139,7 +138,7 @@ match_topics(Client, Topic, [TopicFilter|Filters]) -> match_topic(Topic, {eq, TopicFilter}) -> Topic =:= TopicFilter; match_topic(Topic, TopicFilter) -> - emqttd_topic:match(Topic, TopicFilter). + emqx_topic:match(Topic, TopicFilter). feed_var(Client, Pattern) -> feed_var(Client, Pattern, []). diff --git a/src/emqttd_acl_internal.erl b/src/emqx_acl_internal.erl similarity index 92% rename from src/emqttd_acl_internal.erl rename to src/emqx_acl_internal.erl index 5305985c4..3b146eb38 100644 --- a/src/emqttd_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -14,14 +14,15 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_acl_internal). +-module(emqx_acl_internal). --behaviour(emqttd_acl_mod). +-behaviour(emqx_acl_mod). -author("Feng Lee "). --include("emqttd.hrl"). --include("emqttd_cli.hrl"). +-include("emqx.hrl"). + +-include("emqx_cli.hrl"). -export([all_rules/0]). @@ -37,7 +38,7 @@ %%-------------------------------------------------------------------- %% @doc Read all rules --spec(all_rules() -> list(emqttd_access_rule:rule())). +-spec(all_rules() -> list(emqx_access_rule:rule())). all_rules() -> case ets:lookup(?ACL_RULE_TAB, all_rules) of [] -> []; @@ -58,7 +59,7 @@ init([File]) -> load_rules_from_file(#state{config = AclFile}) -> {ok, Terms} = file:consult(AclFile), - Rules = [emqttd_access_rule:compile(Term) || Term <- Terms], + Rules = [emqx_access_rule:compile(Term) || Term <- Terms], lists:foreach(fun(PubSub) -> ets:insert(?ACL_RULE_TAB, {PubSub, lists:filter(fun(Rule) -> filter(PubSub, Rule) end, Rules)}) @@ -103,7 +104,7 @@ match(_Client, _Topic, []) -> nomatch; match(Client, Topic, [Rule|Rules]) -> - case emqttd_access_rule:match(Client, Topic, Rule) of + case emqx_access_rule:match(Client, Topic, Rule) of nomatch -> match(Client, Topic, Rules); {matched, AllowDeny} -> {matched, AllowDeny} end. diff --git a/src/emqttd_acl_mod.erl b/src/emqx_acl_mod.erl similarity index 96% rename from src/emqttd_acl_mod.erl rename to src/emqx_acl_mod.erl index 12e949afe..6ce338209 100644 --- a/src/emqttd_acl_mod.erl +++ b/src/emqx_acl_mod.erl @@ -14,11 +14,11 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_acl_mod). +-module(emqx_acl_mod). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). %%-------------------------------------------------------------------- %% ACL behavihour diff --git a/src/emqttd_alarm.erl b/src/emqx_alarm.erl similarity index 87% rename from src/emqttd_alarm.erl rename to src/emqx_alarm.erl index 1467797c7..e0c99dc00 100644 --- a/src/emqttd_alarm.erl +++ b/src/emqx_alarm.erl @@ -14,13 +14,13 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_alarm). +-module(emqx_alarm). -author("Feng Lee "). -behaviour(gen_event). --include("emqttd.hrl"). +-include("emqx.hrl"). -define(ALARM_MGR, ?MODULE). @@ -82,7 +82,7 @@ delete_alarm_handler(Module) when is_atom(Module) -> %%-------------------------------------------------------------------- init(_) -> {ok, []}. - + handle_event({set_alarm, Alarm = #mqtt_alarm{id = AlarmId, severity = Severity, title = Title, @@ -92,13 +92,13 @@ handle_event({set_alarm, Alarm = #mqtt_alarm{id = AlarmId, {severity, Severity}, {title, iolist_to_binary(Title)}, {summary, iolist_to_binary(Summary)}, - {ts, emqttd_time:now_secs(TS)}]), - emqttd:publish(alarm_msg(alert, AlarmId, Json)), + {ts, emqx_time:now_secs(TS)}]), + emqx:publish(alarm_msg(alert, AlarmId, Json)), {ok, [Alarm#mqtt_alarm{timestamp = TS} | Alarms]}; handle_event({clear_alarm, AlarmId}, Alarms) -> - Json = mochijson2:encode([{id, AlarmId}, {ts, emqttd_time:now_secs()}]), - emqttd:publish(alarm_msg(clear, AlarmId, Json)), + Json = mochijson2:encode([{id, AlarmId}, {ts, emqx_time:now_secs()}]), + emqx:publish(alarm_msg(clear, AlarmId, Json)), {ok, lists:keydelete(AlarmId, 2, Alarms), hibernate}; handle_event(_, Alarms)-> @@ -127,14 +127,12 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- alarm_msg(Type, AlarmId, Json) -> - Msg = emqttd_message:make(alarm, - topic(Type, AlarmId), - iolist_to_binary(Json)), - emqttd_message:set_flag(sys, Msg). + Msg = emqx_message:make(alarm, topic(Type, AlarmId), iolist_to_binary(Json)), + emqx_message:set_flag(sys, Msg). topic(alert, AlarmId) -> - emqttd_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>); + emqx_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>); topic(clear, AlarmId) -> - emqttd_topic:systop(<<"alarms/", AlarmId/binary, "/clear">>). + emqx_topic:systop(<<"alarms/", AlarmId/binary, "/clear">>). diff --git a/src/emqx_app.erl b/src/emqx_app.erl new file mode 100644 index 000000000..a3066f7de --- /dev/null +++ b/src/emqx_app.erl @@ -0,0 +1,85 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_app). + +-behaviour(application). + +-author("Feng Lee "). + +-include("emqx_cli.hrl"). + +-include("emqx_mqtt.hrl"). + +%% Application callbacks +-export([start/2, stop/1]). + +-define(APP, emqx). + +%%-------------------------------------------------------------------- +%% Application Callbacks +%%-------------------------------------------------------------------- + +start(_Type, _Args) -> + print_banner(), + ekka:start(), + {ok, Sup} = emqx_sup:start_link(), + ok = emqx_cli:load(), + ok = register_acl_mod(), + start_autocluster(), + register(emqx, self()), + print_vsn(), + {ok, Sup}. + +-spec(stop(State :: term()) -> term()). +stop(_State) -> + catch emqx:stop_listeners(). + +%%-------------------------------------------------------------------- +%% Print Banner +%%-------------------------------------------------------------------- + +print_banner() -> + ?PRINT("Starting ~s on node ~s~n", [?APP, node()]). + +print_vsn() -> + {ok, Vsn} = application:get_key(vsn), + ?PRINT("~s ~s is running now!~n", [?APP, Vsn]). + +%%-------------------------------------------------------------------- +%% Register default ACL File +%%-------------------------------------------------------------------- + +register_acl_mod() -> + case emqx:env(acl_file) of + {ok, File} -> emqx_access_control:register_mod(acl, emqx_acl_internal, [File]); + undefined -> ok + end. + +%%-------------------------------------------------------------------- +%% Autocluster +%%-------------------------------------------------------------------- + +start_autocluster() -> + ekka:callback(prepare, fun emqx:shutdown/1), + ekka:callback(reboot, fun emqx:reboot/0), + ekka:autocluster(?APP, fun after_autocluster/0). + +after_autocluster() -> + emqx_plugins:init(), + emqx_plugins:load(), + emqx:start_listeners(). + diff --git a/src/emqttd_auth_mod.erl b/src/emqx_auth_mod.erl similarity index 94% rename from src/emqttd_auth_mod.erl rename to src/emqx_auth_mod.erl index c94578c46..ca6219c90 100644 --- a/src/emqttd_auth_mod.erl +++ b/src/emqx_auth_mod.erl @@ -14,11 +14,11 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_auth_mod). +-module(emqx_auth_mod). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). -export([passwd_hash/2]). @@ -35,7 +35,7 @@ -callback(check(Client :: mqtt_client(), Password :: binary(), State :: any()) - -> ok | | {ok, boolean()} | ignore | {error, string()}). + -> ok | {ok, boolean()} | ignore | {error, string()}). -callback(description() -> string()). @@ -63,12 +63,12 @@ passwd_hash(sha256, Password) -> passwd_hash(pbkdf2, {Salt, Password, Macfun, Iterations, Dklen}) -> case pbkdf2:pbkdf2(Macfun, Password, Salt, Iterations, Dklen) of {ok, Hexstring} -> pbkdf2:to_hex(Hexstring); - {error, Error} -> lager:error("PasswdHash with pbkdf2 error:~p", [Error]), error + {error, Error} -> lager:error("PasswdHash with pbkdf2 error:~p", [Error]), <<>> end; passwd_hash(bcrypt, {Salt, Password}) -> case bcrypt:hashpw(Password, Salt) of {ok, HashPassword} -> list_to_binary(HashPassword); - {error, Error}-> lager:error("PasswdHash with bcrypt error:~p", [Error]), error + {error, Error}-> lager:error("PasswdHash with bcrypt error:~p", [Error]), <<>> end. hexstring(<>) -> diff --git a/src/emqttd_base62.erl b/src/emqx_base62.erl similarity index 98% rename from src/emqttd_base62.erl rename to src/emqx_base62.erl index 481488fb9..58c1b7b40 100644 --- a/src/emqttd_base62.erl +++ b/src/emqx_base62.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_base62). +-module(emqx_base62). -author("Feng Lee "). diff --git a/src/emqttd_boot.erl b/src/emqx_boot.erl similarity index 99% rename from src/emqttd_boot.erl rename to src/emqx_boot.erl index d7a6d311e..fdd2e1a71 100644 --- a/src/emqttd_boot.erl +++ b/src/emqx_boot.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_boot). +-module(emqx_boot). -author("Feng Lee "). diff --git a/src/emqttd_bridge.erl b/src/emqx_bridge.erl similarity index 87% rename from src/emqttd_bridge.erl rename to src/emqx_bridge.erl index 49b5a95d0..5bdebf519 100644 --- a/src/emqttd_bridge.erl +++ b/src/emqx_bridge.erl @@ -14,17 +14,17 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_bridge). - --behaviour(gen_server2). +-module(emqx_bridge). -author("Feng Lee "). --include("emqttd.hrl"). +-behaviour(gen_server2). --include("emqttd_protocol.hrl"). +-include("emqx.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_mqtt.hrl"). + +-include("emqx_internal.hrl"). %% API Function Exports -export([start_link/5]). @@ -37,14 +37,16 @@ -record(state, {pool, id, node, subtopic, + qos = ?QOS_0, topic_suffix = <<>>, topic_prefix = <<>>, - mqueue :: emqttd_mqueue:mqueue(), + mqueue :: emqx_mqueue:mqueue(), max_queue_len = 10000, ping_down_interval = ?PING_DOWN_INTERVAL, status = up}). --type(option() :: {topic_suffix, binary()} | +-type(option() :: {qos, mqtt_qos()} | + {topic_suffix, binary()} | {topic_prefix, binary()} | {max_queue_len, pos_integer()} | {ping_down_interval, pos_integer()}). @@ -72,11 +74,11 @@ init([Pool, Id, Node, Topic, Options]) -> true -> true = erlang:monitor_node(Node, true), Share = iolist_to_binary(["$bridge:", atom_to_list(Node), ":", Topic]), - emqttd:subscribe(Topic, self(), [local, {share, Share}, {qos, ?QOS_0}]), + emqx_server:subscribe(Topic, self(), [local, {share, Share}, {qos, ?QOS_0}]), State = parse_opts(Options, #state{node = Node, subtopic = Topic}), - MQueue = emqttd_mqueue:new(qname(Node, Topic), - [{max_len, State#state.max_queue_len}], - emqttd_alarm:alarm_fun()), + MQueue = emqx_mqueue:new(qname(Node, Topic), + [{max_len, State#state.max_queue_len}], + emqx_alarm:alarm_fun()), {ok, State#state{pool = Pool, id = Id, mqueue = MQueue}, hibernate, {backoff, 1000, 1000, 10000}}; false -> @@ -85,6 +87,8 @@ init([Pool, Id, Node, Topic, Options]) -> parse_opts([], State) -> State; +parse_opts([{qos, Qos} | Opts], State) -> + parse_opts(Opts, State#state{qos = Qos}); parse_opts([{topic_suffix, Suffix} | Opts], State) -> parse_opts(Opts, State#state{topic_suffix= Suffix}); parse_opts([{topic_prefix, Prefix} | Opts], State) -> @@ -108,10 +112,10 @@ handle_cast(Msg, State) -> ?UNEXPECTED_MSG(Msg, State). handle_info({dispatch, _Topic, Msg}, State = #state{mqueue = MQ, status = down}) -> - {noreply, State#state{mqueue = emqttd_mqueue:in(Msg, MQ)}}; + {noreply, State#state{mqueue = emqx_mqueue:in(Msg, MQ)}}; handle_info({dispatch, _Topic, Msg}, State = #state{node = Node, status = up}) -> - rpc:cast(Node, emqttd, publish, [transform(Msg, State)]), + emqx_rpc:cast(Node, emqx, publish, [transform(Msg, State)]), {noreply, State, hibernate}; handle_info({nodedown, Node}, State = #state{node = Node, ping_down_interval = Interval}) -> @@ -121,7 +125,7 @@ handle_info({nodedown, Node}, State = #state{node = Node, ping_down_interval = I handle_info({nodeup, Node}, State = #state{node = Node}) -> %% TODO: Really fast?? - case emqttd:is_running(Node) of + case emqx:is_running(Node) of true -> lager:warning("Bridge Node Up: ~p", [Node]), {noreply, dequeue(State#state{status = up})}; @@ -160,7 +164,7 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- dequeue(State = #state{mqueue = MQ}) -> - case emqttd_mqueue:out(MQ) of + case emqx_mqueue:out(MQ) of {empty, MQ1} -> State#state{mqueue = MQ1}; {{value, Msg}, MQ1} -> diff --git a/src/emqttd_bridge_sup.erl b/src/emqx_bridge_sup.erl similarity index 77% rename from src/emqttd_bridge_sup.erl rename to src/emqx_bridge_sup.erl index d28b2274c..dcfb09a6c 100644 --- a/src/emqttd_bridge_sup.erl +++ b/src/emqx_bridge_sup.erl @@ -14,7 +14,9 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_bridge_sup). +-module(emqx_bridge_sup). + +-author("Feng Lee "). -export([start_link/3]). @@ -23,8 +25,9 @@ %%-------------------------------------------------------------------- %% @doc Start bridge pool supervisor --spec(start_link(atom(), binary(), [emqttd_bridge:option()]) -> {ok, pid()} | {error, any()}). +-spec(start_link(atom(), binary(), [emqx_bridge:option()]) -> + {ok, pid()} | {error, any()}). start_link(Node, Topic, Options) -> - MFA = {emqttd_bridge, start_link, [Node, Topic, Options]}, - emqttd_pool_sup:start_link({bridge, Node, Topic}, random, MFA). + MFA = {emqx_bridge, start_link, [Node, Topic, Options]}, + emqx_pool_sup:start_link({bridge, Node, Topic}, random, MFA). diff --git a/src/emqttd_bridge_sup_sup.erl b/src/emqx_bridge_sup_sup.erl similarity index 86% rename from src/emqttd_bridge_sup_sup.erl rename to src/emqx_bridge_sup_sup.erl index 1d47bd003..d3f5d6bc5 100644 --- a/src/emqttd_bridge_sup_sup.erl +++ b/src/emqx_bridge_sup_sup.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_bridge_sup_sup). +-module(emqx_bridge_sup_sup). -behavior(supervisor). @@ -44,12 +44,12 @@ bridges() -> start_bridge(Node, Topic) when is_atom(Node) andalso is_binary(Topic) -> start_bridge(Node, Topic, []). --spec(start_bridge(atom(), binary(), [emqttd_bridge:option()]) -> {ok, pid()} | {error, any()}). +-spec(start_bridge(atom(), binary(), [emqx_bridge:option()]) -> {ok, pid()} | {error, any()}). start_bridge(Node, _Topic, _Options) when Node =:= node() -> {error, bridge_to_self}; start_bridge(Node, Topic, Options) when is_atom(Node) andalso is_binary(Topic) -> - {ok, BridgeEnv} = emqttd:env(bridge), - Options1 = emqttd_misc:merge_opts(BridgeEnv, Options), + {ok, BridgeEnv} = emqx:env(bridge), + Options1 = emqx_misc:merge_opts(BridgeEnv, Options), supervisor:start_child(?MODULE, bridge_spec(Node, Topic, Options1)). %% @doc Stop a bridge @@ -66,10 +66,10 @@ stop_bridge(Node, Topic) when is_atom(Node) andalso is_binary(Topic) -> %%-------------------------------------------------------------------- init([]) -> - {ok, {{one_for_one, 10, 100}, []}}. + {ok, {{one_for_one, 10, 3600}, []}}. bridge_spec(Node, Topic, Options) -> {?CHILD_ID(Node, Topic), - {emqttd_bridge_sup, start_link, [Node, Topic, Options]}, - permanent, infinity, supervisor, [emqttd_bridge_sup]}. + {emqx_bridge_sup, start_link, [Node, Topic, Options]}, + permanent, infinity, supervisor, [emqx_bridge_sup]}. diff --git a/src/emqttd_broker.erl b/src/emqx_broker.erl similarity index 89% rename from src/emqttd_broker.erl rename to src/emqx_broker.erl index 9f939a45e..dd59c2f11 100644 --- a/src/emqttd_broker.erl +++ b/src/emqx_broker.erl @@ -14,15 +14,15 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_broker). +-module(emqx_broker). -behaviour(gen_server). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). %% API Function Exports -export([start_link/0]). @@ -42,7 +42,7 @@ -record(state, {started_at, sys_interval, heartbeat, ticker, version, sysdescr}). --define(APP, emqttd). +-define(APP, emqx). -define(SERVER, ?MODULE). @@ -60,7 +60,7 @@ %% API %%-------------------------------------------------------------------- -%% @doc Start emqttd broker +%% @doc Start the broker -spec(start_link() -> {ok, pid()} | ignore | {error, any()}). start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). @@ -107,7 +107,7 @@ datetime() -> %% @doc Start a tick timer start_tick(Msg) -> - start_tick(timer:seconds(emqttd:env(broker_sys_interval, 60)), Msg). + start_tick(timer:seconds(emqx:env(broker_sys_interval, 60)), Msg). start_tick(0, _Msg) -> undefined; @@ -125,7 +125,7 @@ stop_tick(TRef) -> %%-------------------------------------------------------------------- init([]) -> - emqttd_time:seed(), + emqx_time:seed(), ets:new(?BROKER_TAB, [set, public, named_table]), % Tick {ok, #state{started_at = os:timestamp(), @@ -172,16 +172,16 @@ code_change(_OldVsn, State, _Extra) -> retain(brokers) -> Payload = list_to_binary(string:join([atom_to_list(N) || N <- ekka_mnesia:running_nodes()], ",")), - Msg = emqttd_message:make(broker, <<"$SYS/brokers">>, Payload), - emqttd:publish(emqttd_message:set_flag(sys, emqttd_message:set_flag(retain, Msg))). + Msg = emqx_message:make(broker, <<"$SYS/brokers">>, Payload), + emqx:publish(emqx_message:set_flag(sys, emqx_message:set_flag(retain, Msg))). retain(Topic, Payload) when is_binary(Payload) -> - Msg = emqttd_message:make(broker, emqttd_topic:systop(Topic), Payload), - emqttd:publish(emqttd_message:set_flag(sys, emqttd_message:set_flag(retain, Msg))). + Msg = emqx_message:make(broker, emqx_topic:systop(Topic), Payload), + emqx:publish(emqx_message:set_flag(sys, emqx_message:set_flag(retain, Msg))). publish(Topic, Payload) when is_binary(Payload) -> - Msg = emqttd_message:make(broker, emqttd_topic:systop(Topic), Payload), - emqttd:publish(emqttd_message:set_flag(sys, Msg)). + Msg = emqx_message:make(broker, emqx_topic:systop(Topic), Payload), + emqx:publish(emqx_message:set_flag(sys, Msg)). uptime(#state{started_at = Ts}) -> Secs = timer:now_diff(os:timestamp(), Ts) div 1000000, diff --git a/src/emqttd_cli.erl b/src/emqx_cli.erl similarity index 88% rename from src/emqttd_cli.erl rename to src/emqx_cli.erl index a4029d46f..37f9a904a 100644 --- a/src/emqttd_cli.erl +++ b/src/emqx_cli.erl @@ -14,15 +14,15 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_cli). +-module(emqx_cli). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_cli.hrl"). +-include("emqx_mqtt.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_cli.hrl"). -import(lists, [foreach/2]). @@ -44,12 +44,13 @@ -define(MAX_LIMIT, 10000). --define(APP, emqttd). +-define(APP, emqx). +-spec(load() -> ok). load() -> Cmds = [Fun || {Fun, _} <- ?MODULE:module_info(exports), is_cmd(Fun)], - [emqttd_ctl:register_cmd(Cmd, {?MODULE, Cmd}, []) || Cmd <- Cmds], - emqttd_cli_config:register_config(). + lists:foreach(fun(Cmd) -> emqx_ctl:register_cmd(Cmd, {?MODULE, Cmd}, []) end, Cmds), + emqx_cli_config:register_config(). is_cmd(Fun) -> not lists:member(Fun, [init, load, module_info]). @@ -66,9 +67,9 @@ status([]) -> ?PRINT("Node ~p is ~p~n", [node(), InternalStatus]), case lists:keysearch(?APP, 1, application:which_applications()) of false -> - ?PRINT_MSG("emqttd is not running~n"); + ?PRINT("~s is not running~n", [?APP]); {value, {?APP, _Desc, Vsn}} -> - ?PRINT("emqttd ~s is running~n", [Vsn]) + ?PRINT("~s ~s is running~n", [?APP, Vsn]) end; status(_) -> ?PRINT_CMD("status", "Show broker status"). @@ -79,21 +80,21 @@ status(_) -> broker([]) -> Funs = [sysdescr, version, uptime, datetime], foreach(fun(Fun) -> - ?PRINT("~-10s: ~s~n", [Fun, emqttd_broker:Fun()]) + ?PRINT("~-10s: ~s~n", [Fun, emqx_broker:Fun()]) end, Funs); broker(["stats"]) -> foreach(fun({Stat, Val}) -> ?PRINT("~-20s: ~w~n", [Stat, Val]) - end, emqttd_stats:getstats()); + end, emqx_stats:getstats()); broker(["metrics"]) -> foreach(fun({Metric, Val}) -> ?PRINT("~-24s: ~w~n", [Metric, Val]) - end, lists:sort(emqttd_metrics:all())); + end, lists:sort(emqx_metrics:all())); broker(["pubsub"]) -> - Pubsubs = supervisor:which_children(emqttd_pubsub_sup:pubsub_pool()), + Pubsubs = supervisor:which_children(emqx_pubsub_sup:pubsub_pool()), foreach(fun({{_, Id}, Pid, _, _}) -> ProcInfo = erlang:process_info(Pid, ?PROC_INFOKEYS), ?PRINT("pubsub: ~w~n", [Id]), @@ -154,9 +155,9 @@ cluster(_) -> %%-------------------------------------------------------------------- %% @doc Users usage -users(Args) -> emq_auth_username:cli(Args). +users(Args) -> emqx_auth_username:cli(Args). -acl(["reload"]) -> emqttd_access_control:reload_acl(); +acl(["reload"]) -> emqx_access_control:reload_acl(); acl(_) -> ?USAGE([{"acl reload", "reload etc/acl.conf"}]). %%-------------------------------------------------------------------- @@ -169,7 +170,7 @@ clients(["show", ClientId]) -> if_client(ClientId, fun print/1); clients(["kick", ClientId]) -> - if_client(ClientId, fun(#mqtt_client{client_pid = Pid}) -> emqttd_client:kick(Pid) end); + if_client(ClientId, fun(#mqtt_client{client_pid = Pid}) -> emqx_client:kick(Pid) end); clients(_) -> ?USAGE([{"clients list", "List all clients"}, @@ -177,7 +178,7 @@ clients(_) -> {"clients kick ", "Kick out a client"}]). if_client(ClientId, Fun) -> - case emqttd_cm:lookup(bin(ClientId)) of + case emqx_cm:lookup(bin(ClientId)) of undefined -> ?PRINT_MSG("Not Found.~n"); Client -> Fun(Client) end. @@ -214,7 +215,7 @@ sessions(_) -> %% @doc Routes Command routes(["list"]) -> - Routes = emqttd_router:dump(), + Routes = emqx_router:dump(), foreach(fun print/1, Routes); routes(["show", Topic]) -> @@ -228,7 +229,7 @@ routes(_) -> %% @doc Topics Command topics(["list"]) -> - lists:foreach(fun(Topic) -> ?PRINT("~s~n", [Topic]) end, emqttd:topics()); + lists:foreach(fun(Topic) -> ?PRINT("~s~n", [Topic]) end, emqx:topics()); topics(["show", Topic]) -> print(mnesia:dirty_read(mqtt_route, bin(Topic))); @@ -251,26 +252,25 @@ subscriptions(["show", ClientId]) -> subscriptions(["add", ClientId, Topic, QoS]) -> Add = fun(IntQos) -> - case emqttd:subscribe(bin(Topic), bin(ClientId), [{qos, IntQos}]) of + case emqx:subscribe(bin(Topic), bin(ClientId), [{qos, IntQos}]) of ok -> ?PRINT_MSG("ok~n"); + {error, already_existed} -> + ?PRINT_MSG("Error: already existed~n"); {error, Reason} -> ?PRINT("Error: ~p~n", [Reason]) end end, if_valid_qos(QoS, Add); - - subscriptions(["del", ClientId]) -> - Ok = emqttd:subscriber_down(bin(ClientId)), + Ok = emqx_server:subscriber_down(bin(ClientId)), ?PRINT("~p~n", [Ok]); subscriptions(["del", ClientId, Topic]) -> - Ok = emqttd:unsubscribe(bin(Topic), bin(ClientId)), + Ok = emqx:unsubscribe(bin(Topic), bin(ClientId)), ?PRINT("~p~n", [Ok]); - subscriptions(_) -> ?USAGE([{"subscriptions list", "List all subscriptions"}, {"subscriptions show ", "Show subscriptions of a client"}, @@ -278,7 +278,7 @@ subscriptions(_) -> {"subscriptions del ", "Delete static subscriptions manually"}, {"subscriptions del ", "Delete a static subscription manually"}]). -% if_could_print(Tab, Fun) -> +%if_could_print(Tab, Fun) -> % case mnesia:table_info(Tab, size) of % Size when Size >= ?MAX_LIMIT -> % ?PRINT("Could not list, too many ~ss: ~p~n", [Tab, Size]); @@ -296,10 +296,10 @@ if_valid_qos(QoS, Fun) -> end. plugins(["list"]) -> - foreach(fun print/1, emqttd_plugins:list()); + foreach(fun print/1, emqx_plugins:list()); plugins(["load", Name]) -> - case emqttd_plugins:load(list_to_atom(Name)) of + case emqx_plugins:load(list_to_atom(Name)) of {ok, StartedApps} -> ?PRINT("Start apps: ~p~nPlugin ~s loaded successfully.~n", [StartedApps, Name]); {error, Reason} -> @@ -307,7 +307,7 @@ plugins(["load", Name]) -> end; plugins(["unload", Name]) -> - case emqttd_plugins:unload(list_to_atom(Name)) of + case emqx_plugins:unload(list_to_atom(Name)) of ok -> ?PRINT("Plugin ~s unloaded successfully.~n", [Name]); {error, Reason} -> @@ -325,31 +325,32 @@ plugins(_) -> bridges(["list"]) -> foreach(fun({Node, Topic, _Pid}) -> ?PRINT("bridge: ~s--~s-->~s~n", [node(), Topic, Node]) - end, emqttd_bridge_sup_sup:bridges()); + end, emqx_bridge_sup_sup:bridges()); bridges(["options"]) -> ?PRINT_MSG("Options:~n"), + ?PRINT_MSG(" qos = 0 | 1 | 2~n"), ?PRINT_MSG(" prefix = string~n"), ?PRINT_MSG(" suffix = string~n"), ?PRINT_MSG(" queue = integer~n"), ?PRINT_MSG("Example:~n"), - ?PRINT_MSG(" prefix=abc/,suffix=/yxz,queue=1000~n"); + ?PRINT_MSG(" qos=2,prefix=abc/,suffix=/yxz,queue=1000~n"); bridges(["start", SNode, Topic]) -> - case emqttd_bridge_sup_sup:start_bridge(list_to_atom(SNode), list_to_binary(Topic)) of + case emqx_bridge_sup_sup:start_bridge(list_to_atom(SNode), list_to_binary(Topic)) of {ok, _} -> ?PRINT_MSG("bridge is started.~n"); {error, Error} -> ?PRINT("error: ~p~n", [Error]) end; bridges(["start", SNode, Topic, OptStr]) -> Opts = parse_opts(bridge, OptStr), - case emqttd_bridge_sup_sup:start_bridge(list_to_atom(SNode), list_to_binary(Topic), Opts) of + case emqx_bridge_sup_sup:start_bridge(list_to_atom(SNode), list_to_binary(Topic), Opts) of {ok, _} -> ?PRINT_MSG("bridge is started.~n"); {error, Error} -> ?PRINT("error: ~p~n", [Error]) end; bridges(["stop", SNode, Topic]) -> - case emqttd_bridge_sup_sup:stop_bridge(list_to_atom(SNode), list_to_binary(Topic)) of + case emqx_bridge_sup_sup:stop_bridge(list_to_atom(SNode), list_to_binary(Topic)) of ok -> ?PRINT_MSG("bridge is stopped.~n"); {error, Error} -> ?PRINT("error: ~p~n", [Error]) end; @@ -365,6 +366,8 @@ parse_opts(Cmd, OptStr) -> Tokens = string:tokens(OptStr, ","), [parse_opt(Cmd, list_to_atom(Opt), Val) || [Opt, Val] <- [string:tokens(S, "=") || S <- Tokens]]. +parse_opt(bridge, qos, Qos) -> + {qos, list_to_integer(Qos)}; parse_opt(bridge, suffix, Suffix) -> {topic_suffix, bin(Suffix)}; parse_opt(bridge, prefix, Prefix) -> @@ -384,7 +387,7 @@ vm(["all"]) -> [vm([Name]) || Name <- ["load", "memory", "process", "io", "ports"]]; vm(["load"]) -> - [?PRINT("cpu/~-20s: ~s~n", [L, V]) || {L, V} <- emqttd_vm:loads()]; + [?PRINT("cpu/~-20s: ~s~n", [L, V]) || {L, V} <- emqx_vm:loads()]; vm(["memory"]) -> [?PRINT("memory/~-17s: ~w~n", [Cat, Val]) || {Cat, Val} <- erlang:memory()]; @@ -428,7 +431,7 @@ mnesia(_) -> trace(["list"]) -> foreach(fun({{Who, Name}, LogFile}) -> ?PRINT("trace ~s ~s -> ~s~n", [Who, Name, LogFile]) - end, emqttd_trace:all_traces()); + end, emqx_trace:all_traces()); trace(["client", ClientId, "off"]) -> trace_off(client, ClientId); @@ -450,7 +453,7 @@ trace(_) -> {"trace topic off", "Stop tracing a Topic"}]). trace_on(Who, Name, LogFile) -> - case emqttd_trace:start_trace({Who, iolist_to_binary(Name)}, LogFile) of + case emqx_trace:start_trace({Who, iolist_to_binary(Name)}, LogFile) of ok -> ?PRINT("trace ~s ~s successfully.~n", [Who, Name]); {error, Error} -> @@ -458,7 +461,7 @@ trace_on(Who, Name, LogFile) -> end. trace_off(Who, Name) -> - case emqttd_trace:stop_trace({Who, iolist_to_binary(Name)}) of + case emqx_trace:stop_trace({Who, iolist_to_binary(Name)}) of ok -> ?PRINT("stop tracing ~s ~s successfully.~n", [Who, Name]); {error, Error} -> @@ -485,7 +488,7 @@ listeners(["restart", Proto, ListenOn]) -> [Port] -> list_to_integer(Port); [IP, Port] -> {IP, list_to_integer(Port)} end, - case emqttd_app:restart_listener({list_to_atom(Proto), ListenOn1, []}) of + case emqx:restart_listener({list_to_atom(Proto), ListenOn1, []}) of {ok, _Pid} -> io:format("Restart ~s listener on ~s successfully.~n", [Proto, ListenOn]); {error, Error} -> @@ -497,7 +500,7 @@ listeners(["stop", Proto, ListenOn]) -> [Port] -> list_to_integer(Port); [IP, Port] -> {IP, list_to_integer(Port)} end, - case emqttd_app:stop_listener({list_to_atom(Proto), ListenOn1, []}) of + case emqx:stop_listener({list_to_atom(Proto), ListenOn1, []}) of ok -> io:format("Stop ~s listener on ~s successfully.~n", [Proto, ListenOn]); {error, Error} -> @@ -548,8 +551,8 @@ print(#mqtt_plugin{name = Name, version = Ver, descr = Descr, active = Active}) print(#mqtt_client{client_id = ClientId, clean_sess = CleanSess, username = Username, peername = Peername, connected_at = ConnectedAt}) -> ?PRINT("Client(~s, clean_sess=~s, username=~s, peername=~s, connected_at=~p)~n", - [ClientId, CleanSess, Username, emqttd_net:format(Peername), - emqttd_time:now_secs(ConnectedAt)]); + [ClientId, CleanSess, Username, emqx_net:format(Peername), + emqx_time:now_secs(ConnectedAt)]); %% print(#mqtt_topic{topic = Topic, flags = Flags}) -> %% ?PRINT("~s: ~s~n", [Topic, string:join([atom_to_list(F) || F <- Flags], ",")]); @@ -563,7 +566,7 @@ print({Topic, Node}) -> ?PRINT("~s -> ~s~n", [Topic, Node]); print({ClientId, _ClientPid, _Persistent, SessInfo}) -> - Data = lists:append(SessInfo, emqttd_stats:get_session_stats(ClientId)), + Data = lists:append(SessInfo, emqx_stats:get_session_stats(ClientId)), InfoKeys = [clean_sess, subscriptions, max_inflight, @@ -589,7 +592,7 @@ print(subscription, {Sub, Topic}) -> ?PRINT("~s -> ~s~n", [Sub, Topic]). format(created_at, Val) -> - emqttd_time:now_secs(Val); + emqx_time:now_secs(Val); format(_, Val) -> Val. diff --git a/src/emqttd_cli_config.erl b/src/emqx_cli_config.erl similarity index 97% rename from src/emqttd_cli_config.erl rename to src/emqx_cli_config.erl index 1ce0de49c..a2167d2c6 100644 --- a/src/emqttd_cli_config.erl +++ b/src/emqx_cli_config.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module (emqttd_cli_config). +-module (emqx_cli_config). -export ([register_config_cli/0, register_config/0, @@ -26,8 +26,9 @@ read_config/1, write_config/2]). --define(APP, emqttd). --define(TAB, emqttd_config). +-define(APP, emqx). + +-define(TAB, emqx_config). register_config() -> application:start(clique), @@ -40,7 +41,7 @@ create_config_tab() -> case ets:info(?TAB, name) of undefined -> ets:new(?TAB, [named_table, public]), - {ok, PluginsEtcDir} = emqttd:env(plugins_etc_dir), + {ok, PluginsEtcDir} = emqx:env(plugins_etc_dir), Files = filelib:wildcard("*.conf", PluginsEtcDir), lists:foreach(fun(File) -> [FileName | _] = string:tokens(File, "."), @@ -173,7 +174,7 @@ protocol_config_callback(_App, websocket_protocol_header, Value) -> application:set_env(?APP, websocket_protocol_header, Value), " successfully\n"; protocol_config_callback(App, Key, Value) -> - {ok, Env} = emqttd:env(App), + {ok, Env} = emqx:env(App), application:set_env(?APP, App, lists:keyreplace(Key, 1, Env, {Key, Value})), " successfully\n". @@ -215,15 +216,15 @@ client_config_callback([_, AppStr, KeyStr], Value) -> client_config_callback(l2a(AppStr), l2a(KeyStr), Value). client_config_callback(App, idle_timeout, Value) -> - {ok, Env} = emqttd:env(App), + {ok, Env} = emqx:env(App), application:set_env(?APP, App, lists:keyreplace(client_idle_timeout, 1, Env, {client_idle_timeout, Value})), " successfully\n"; client_config_callback(App, enable_stats, Value) -> - {ok, Env} = emqttd:env(App), + {ok, Env} = emqx:env(App), application:set_env(?APP, App, lists:keyreplace(client_enable_stats, 1, Env, {client_enable_stats, Value})), " successfully\n"; client_config_callback(App, Key, Value) -> - {ok, Env} = emqttd:env(App), + {ok, Env} = emqx:env(App), application:set_env(?APP, App, lists:keyreplace(Key, 1, Env, {Key, Value})), " successfully\n". @@ -262,7 +263,7 @@ register_session_config() -> session_config_callback([_, AppStr, KeyStr], Value) -> session_config_callback(l2a(AppStr), l2a(KeyStr), Value). session_config_callback(App, Key, Value) -> - {ok, Env} = emqttd:env(App), + {ok, Env} = emqx:env(App), application:set_env(?APP, App, lists:keyreplace(Key, 1, Env, {Key, Value})), " successfully\n". @@ -298,15 +299,15 @@ queue_config_callback([_, AppStr, KeyStr], Value) -> queue_config_callback(l2a(AppStr), l2a(KeyStr), Value). queue_config_callback(App, low_watermark, Value) -> - {ok, Env} = emqttd:env(App), + {ok, Env} = emqx:env(App), application:set_env(?APP, App, lists:keyreplace(low_watermark, 1, Env, {low_watermark, Value})), " successfully\n"; queue_config_callback(App, high_watermark, Value) -> - {ok, Env} = emqttd:env(App), + {ok, Env} = emqx:env(App), application:set_env(?APP, App, lists:keyreplace(high_watermark, 1, Env, {high_watermark, Value})), " successfully\n"; queue_config_callback(App, Key, Value) -> - {ok, Env} = emqttd:env(App), + {ok, Env} = emqx:env(App), application:set_env(?APP, App, lists:keyreplace(Key, 1, Env, {Key, Value})), " successfully\n". diff --git a/src/emqttd_client.erl b/src/emqx_client.erl similarity index 87% rename from src/emqttd_client.erl rename to src/emqx_client.erl index 6631b4566..5a16d1897 100644 --- a/src/emqttd_client.erl +++ b/src/emqx_client.erl @@ -16,17 +16,17 @@ %% @doc MQTT/TCP Connection. --module(emqttd_client). +-module(emqx_client). -behaviour(gen_server2). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). -import(proplists, [get_value/2, get_value/3]). @@ -114,11 +114,11 @@ do_init(Conn, Env, Peername) -> SendFun = send_fun(Conn, Peername), RateLimit = get_value(rate_limit, Conn:opts()), PacketSize = get_value(max_packet_size, Env, ?MAX_PACKET_SIZE), - Parser = emqttd_parser:initial_state(PacketSize), - ProtoState = emqttd_protocol:init(Conn, Peername, SendFun, Env), + Parser = emqx_parser:initial_state(PacketSize), + ProtoState = emqx_protocol:init(Conn, Peername, SendFun, Env), EnableStats = get_value(client_enable_stats, Env, false), IdleTimout = get_value(client_idle_timeout, Env, 30000), - ForceGcCount = emqttd_gc:conn_max_gc_count(), + ForceGcCount = emqx_gc:conn_max_gc_count(), State = run_socket(#client_state{connection = Conn, peername = Peername, await_recv = false, @@ -136,9 +136,9 @@ do_init(Conn, Env, Peername) -> send_fun(Conn, Peername) -> Self = self(), fun(Packet) -> - Data = emqttd_serializer:serialize(Packet), + Data = emqx_serializer:serialize(Packet), ?LOG(debug, "SEND ~p", [Data], #client_state{peername = Peername}), - emqttd_metrics:inc('bytes/sent', iolist_size(Data)), + emqx_metrics:inc('bytes/sent', iolist_size(Data)), try Conn:async_send(Data) of true -> ok catch @@ -153,17 +153,17 @@ prioritise_info(Msg, _Len, _State) -> case Msg of {redeliver, _} -> 5; _ -> 0 end. handle_pre_hibernate(State) -> - {hibernate, emqttd_gc:reset_conn_gc_count(#client_state.force_gc_count, emit_stats(State))}. + {hibernate, emqx_gc:reset_conn_gc_count(#client_state.force_gc_count, emit_stats(State))}. handle_call(info, From, State = #client_state{proto_state = ProtoState}) -> - ProtoInfo = emqttd_protocol:info(ProtoState), + ProtoInfo = emqx_protocol:info(ProtoState), ClientInfo = ?record_to_proplist(client_state, State, ?INFO_KEYS), {reply, Stats, _, _} = handle_call(stats, From, State), reply(lists:append([ClientInfo, ProtoInfo, Stats]), State); handle_call(stats, _From, State = #client_state{proto_state = ProtoState}) -> - reply(lists:append([emqttd_misc:proc_stats(), - emqttd_protocol:stats(ProtoState), + reply(lists:append([emqx_misc:proc_stats(), + emqx_protocol:stats(ProtoState), sock_stats(State)]), State); handle_call(kick, _From, State) -> @@ -176,7 +176,7 @@ handle_call(get_rate_limit, _From, State = #client_state{rate_limit = Rl}) -> reply(Rl, State); handle_call(session, _From, State = #client_state{proto_state = ProtoState}) -> - reply(emqttd_protocol:session(ProtoState), State); + reply(emqx_protocol:session(ProtoState), State); handle_call({clean_acl_cache, Topic}, _From, State) -> erase({acl, publish, Topic}), @@ -191,13 +191,13 @@ handle_cast(Msg, State) -> handle_info({subscribe, TopicTable}, State) -> with_proto( fun(ProtoState) -> - emqttd_protocol:subscribe(TopicTable, ProtoState) + emqx_protocol:subscribe(TopicTable, ProtoState) end, State); handle_info({unsubscribe, Topics}, State) -> with_proto( fun(ProtoState) -> - emqttd_protocol:unsubscribe(Topics, ProtoState) + emqx_protocol:unsubscribe(Topics, ProtoState) end, State); %% Asynchronous SUBACK @@ -205,19 +205,23 @@ handle_info({suback, PacketId, GrantedQos}, State) -> with_proto( fun(ProtoState) -> Packet = ?SUBACK_PACKET(PacketId, GrantedQos), - emqttd_protocol:send(Packet, ProtoState) + emqx_protocol:send(Packet, ProtoState) end, State); +%% Fastlane +handle_info({dispatch, _Topic, Message}, State) -> + handle_info({deliver, Message#mqtt_message{qos = ?QOS_0}}, State); + handle_info({deliver, Message}, State) -> with_proto( fun(ProtoState) -> - emqttd_protocol:send(Message, ProtoState) + emqx_protocol:send(Message, ProtoState) end, State); handle_info({redeliver, {?PUBREL, PacketId}}, State) -> with_proto( fun(ProtoState) -> - emqttd_protocol:pubrel(PacketId, ProtoState) + emqx_protocol:pubrel(PacketId, ProtoState) end, State); handle_info(emit_stats, State) -> @@ -240,7 +244,7 @@ handle_info(activate_sock, State) -> handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) -> Size = iolist_size(Data), ?LOG(debug, "RECV ~p", [Data], State), - emqttd_metrics:inc('bytes/received', Size), + emqx_metrics:inc('bytes/received', Size), received(Data, rate_limit(Size, State#client_state{await_recv = false})); handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) -> @@ -260,7 +264,7 @@ handle_info({keepalive, start, Interval}, State = #client_state{connection = Con {error, Error} -> {error, Error} end end, - case emqttd_keepalive:start(StatFun, Interval, {keepalive, check}) of + case emqx_keepalive:start(StatFun, Interval, {keepalive, check}) of {ok, KeepAlive} -> {noreply, State#client_state{keepalive = KeepAlive}, hibernate}; {error, Error} -> @@ -269,7 +273,7 @@ handle_info({keepalive, start, Interval}, State = #client_state{connection = Con end; handle_info({keepalive, check}, State = #client_state{keepalive = KeepAlive}) -> - case emqttd_keepalive:check(KeepAlive) of + case emqx_keepalive:check(KeepAlive) of {ok, KeepAlive1} -> {noreply, State#client_state{keepalive = KeepAlive1}, hibernate}; {error, timeout} -> @@ -289,14 +293,14 @@ terminate(Reason, State = #client_state{connection = Conn, ?LOG(debug, "Terminated for ~p", [Reason], State), Conn:fast_close(), - emqttd_keepalive:cancel(KeepAlive), + emqx_keepalive:cancel(KeepAlive), case {ProtoState, Reason} of {undefined, _} -> ok; {_, {shutdown, Error}} -> - emqttd_protocol:shutdown(Error, ProtoState); + emqx_protocol:shutdown(Error, ProtoState); {_, Reason} -> - emqttd_protocol:shutdown(Reason, ProtoState) + emqx_protocol:shutdown(Reason, ProtoState) end. code_change(_OldVsn, State, _Extra) -> @@ -314,14 +318,14 @@ received(Bytes, State = #client_state{parser = Parser, packet_size = PacketSize, proto_state = ProtoState, idle_timeout = IdleTimeout}) -> - case catch emqttd_parser:parse(Bytes, Parser) of + case catch emqx_parser:parse(Bytes, Parser) of {more, NewParser} -> {noreply, run_socket(State#client_state{parser = NewParser}), IdleTimeout}; {ok, Packet, Rest} -> - emqttd_metrics:received(Packet), - case emqttd_protocol:received(Packet, ProtoState) of + emqx_metrics:received(Packet), + case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> - received(Rest, State#client_state{parser = emqttd_parser:initial_state(PacketSize), + received(Rest, State#client_state{parser = emqx_parser:initial_state(PacketSize), proto_state = ProtoState1}); {error, Error} -> ?LOG(error, "Protocol error - ~p", [Error], State), @@ -365,7 +369,7 @@ with_proto(Fun, State = #client_state{proto_state = ProtoState}) -> {noreply, State#client_state{proto_state = ProtoState1}, hibernate}. emit_stats(State = #client_state{proto_state = ProtoState}) -> - emit_stats(emqttd_protocol:clientid(ProtoState), State). + emit_stats(emqx_protocol:clientid(ProtoState), State). emit_stats(_ClientId, State = #client_state{enable_stats = false}) -> State; @@ -373,7 +377,7 @@ emit_stats(undefined, State) -> State; emit_stats(ClientId, State) -> {reply, Stats, _, _} = handle_call(stats, undefined, State), - emqttd_stats:set_client_stats(ClientId, Stats), + emqx_stats:set_client_stats(ClientId, Stats), State. sock_stats(#client_state{connection = Conn}) -> @@ -390,5 +394,5 @@ stop(Reason, State) -> gc(State = #client_state{connection = Conn}) -> Cb = fun() -> Conn:gc(), emit_stats(State) end, - emqttd_gc:maybe_force_gc(#client_state.force_gc_count, State, Cb). + emqx_gc:maybe_force_gc(#client_state.force_gc_count, State, Cb). diff --git a/src/emqttd_cm.erl b/src/emqx_cm.erl similarity index 97% rename from src/emqttd_cm.erl rename to src/emqx_cm.erl index 2e57ebe5b..f56ce2929 100644 --- a/src/emqttd_cm.erl +++ b/src/emqx_cm.erl @@ -16,15 +16,15 @@ %% @doc MQTT Client Manager --module(emqttd_cm). +-module(emqx_cm). -behaviour(gen_server2). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). %% API Exports -export([start_link/3]). @@ -123,7 +123,7 @@ handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> {ok, {ClientId, DownPid}} -> case lookup_proc(ClientId) of DownPid -> - emqttd_stats:del_client_stats(ClientId), + emqx_stats:del_client_stats(ClientId), ets:delete(mqtt_client, ClientId); _ -> ignore diff --git a/src/emqttd_cm_sup.erl b/src/emqx_cm_sup.erl similarity index 85% rename from src/emqttd_cm_sup.erl rename to src/emqx_cm_sup.erl index fc01ea649..704b0e1fd 100644 --- a/src/emqttd_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -16,13 +16,13 @@ %% @doc Client Manager Supervisor. --module(emqttd_cm_sup). +-module(emqx_cm_sup). -behaviour(supervisor). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). %% API -export([start_link/0]). @@ -30,7 +30,7 @@ %% Supervisor callbacks -export([init/1]). --define(CM, emqttd_cm). +-define(CM, emqx_cm). -define(TAB, mqtt_client). @@ -42,8 +42,8 @@ init([]) -> create_client_tab(), %% CM Pool Sup - MFA = {?CM, start_link, [emqttd_stats:statsfun('clients/count', 'clients/max')]}, - PoolSup = emqttd_pool_sup:spec([?CM, hash, erlang:system_info(schedulers), MFA]), + MFA = {?CM, start_link, [emqx_stats:statsfun('clients/count', 'clients/max')]}, + PoolSup = emqx_pool_sup:spec([?CM, hash, erlang:system_info(schedulers), MFA]), {ok, {{one_for_all, 10, 3600}, [PoolSup]}}. diff --git a/src/emqttd_config.erl b/src/emqx_config.erl similarity index 93% rename from src/emqttd_config.erl rename to src/emqx_config.erl index deaaa77d1..c36cdc8c3 100644 --- a/src/emqttd_config.erl +++ b/src/emqx_config.erl @@ -23,7 +23,7 @@ %% 3. Store in data/app.config? %% --module(emqttd_config). +-module(emqx_config). -export([read/1, write/2, dump/2, reload/1, get/2, get/3, set/3]). @@ -57,7 +57,7 @@ write(App, Terms) -> Schema = cuttlefish_schema:files([Path]), case cuttlefish_generator:map(Schema, Configs) of [{App, Configs1}] -> - emqttd_cli_config:write_config(App, Configs), + emqx_cli_config:write_config(App, Configs), lists:foreach(fun({Key, Val}) -> application:set_env(App, Key, Val) end, Configs1); _ -> error @@ -70,25 +70,25 @@ dump(_App, _Terms) -> -spec(set(atom(), list(), list()) -> ok). set(App, Par, Val) -> - emqttd_cli_config:run(["config", + emqx_cli_config:run(["config", "set", lists:concat([Par, "=", Val]), lists:concat(["--app=", App])]). -spec(get(atom(), list()) -> undefined | {ok, term()}). get(App, Par) -> - case emqttd_cli_config:get_cfg(App, Par) of + case emqx_cli_config:get_cfg(App, Par) of undefined -> undefined; Val -> {ok, Val} end. -spec(get(atom(), list(), atom()) -> term()). get(App, Par, Def) -> - emqttd_cli_config:get_cfg(App, Par, Def). + emqx_cli_config:get_cfg(App, Par, Def). read_(App) -> - Configs = emqttd_cli_config:read_config(App), + Configs = emqx_cli_config:read_config(App), Path = lists:concat([code:priv_dir(App), "/", App, ".schema"]), case filelib:is_file(Path) of false -> @@ -112,3 +112,4 @@ read_(App) -> end, [], Configs), RequiredCfg ++ OptionalCfg end. + diff --git a/src/emqttd_ctl.erl b/src/emqx_ctl.erl similarity index 91% rename from src/emqttd_ctl.erl rename to src/emqx_ctl.erl index 77769e3c8..565c08ff1 100644 --- a/src/emqttd_ctl.erl +++ b/src/emqx_ctl.erl @@ -14,15 +14,15 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_ctl). +-module(emqx_ctl). -behaviour(gen_server). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_cli.hrl"). +-include("emqx_cli.hrl"). -define(SERVER, ?MODULE). @@ -69,13 +69,13 @@ run([]) -> usage(), ok; run(["help"]) -> usage(), ok; run(["set"] = CmdS) when length(CmdS) =:= 1 -> - emqttd_cli_config:set_usage(), ok; + emqx_cli_config:set_usage(), ok; run(["set" | _] = CmdS) -> - emqttd_cli_config:run(["config" | CmdS]), ok; + emqx_cli_config:run(["config" | CmdS]), ok; run(["show" | _] = CmdS) -> - emqttd_cli_config:run(["config" | CmdS]), ok; + emqx_cli_config:run(["config" | CmdS]), ok; run([CmdS|Args]) -> case lookup(list_to_atom(CmdS)) of @@ -161,14 +161,14 @@ next_seq(State = #state{seq = Seq}) -> register_cmd_test_() -> {setup, fun() -> - {ok, InitState} = emqttd_ctl:init([]), + {ok, InitState} = emqx_ctl:init([]), InitState end, fun(State) -> - ok = emqttd_ctl:terminate(shutdown, State) + ok = emqx_ctl:terminate(shutdown, State) end, fun(State = #state{seq = Seq}) -> - emqttd_ctl:handle_cast({register_cmd, test0, {?MODULE, test0}, []}, State), + emqx_ctl:handle_cast({register_cmd, test0, {?MODULE, test0}, []}, State), [?_assertMatch([{{0,test0},{?MODULE, test0}, []}], ets:lookup(?CMD_TAB, {Seq,test0}))] end }. diff --git a/src/emqttd_gc.erl b/src/emqx_gc.erl similarity index 96% rename from src/emqttd_gc.erl rename to src/emqx_gc.erl index 75545a77f..a0b702db0 100644 --- a/src/emqttd_gc.erl +++ b/src/emqx_gc.erl @@ -16,7 +16,7 @@ %% GC Utility functions. --module(emqttd_gc). +-module(emqx_gc). -author("Feng Lee "). @@ -25,7 +25,7 @@ -spec(conn_max_gc_count() -> integer()). conn_max_gc_count() -> - case emqttd:env(conn_force_gc_count) of + case emqx:env(conn_force_gc_count) of {ok, I} when I > 0 -> I + rand:uniform(I); {ok, I} when I =< 0 -> undefined; undefined -> undefined diff --git a/src/emqttd_gen_mod.erl b/src/emqx_gen_mod.erl similarity index 92% rename from src/emqttd_gen_mod.erl rename to src/emqx_gen_mod.erl index 824b65f4c..24c0036b8 100644 --- a/src/emqttd_gen_mod.erl +++ b/src/emqx_gen_mod.erl @@ -14,14 +14,10 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc emqttd gen_mod behaviour - --module(emqttd_gen_mod). +-module(emqx_gen_mod). -author("Feng Lee "). --include("emqttd.hrl"). - -ifdef(use_specs). -callback(load(Opts :: any()) -> ok | {error, any()}). diff --git a/src/emqttd_guid.erl b/src/emqx_guid.erl similarity index 97% rename from src/emqttd_guid.erl rename to src/emqx_guid.erl index 24199fa01..163fa277f 100644 --- a/src/emqttd_guid.erl +++ b/src/emqx_guid.erl @@ -28,7 +28,7 @@ %% %% @end --module(emqttd_guid). +-module(emqx_guid). -export([gen/0, new/0, timestamp/1, to_hexstr/1, from_hexstr/1, to_base62/1, from_base62/1]). @@ -128,8 +128,8 @@ from_hexstr(S) -> I = list_to_integer(binary_to_list(S), 16), <>. to_base62(<>) -> - emqttd_base62:encode(I). + emqx_base62:encode(I). from_base62(S) -> - I = emqttd_base62:decode(S), <>. + I = emqx_base62:decode(S), <>. diff --git a/src/emqttd_hooks.erl b/src/emqx_hooks.erl similarity index 99% rename from src/emqttd_hooks.erl rename to src/emqx_hooks.erl index 693a67ff7..da152675e 100644 --- a/src/emqttd_hooks.erl +++ b/src/emqx_hooks.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_hooks). +-module(emqx_hooks). -behaviour(gen_server). diff --git a/src/emqttd_http.erl b/src/emqx_http.erl similarity index 93% rename from src/emqttd_http.erl rename to src/emqx_http.erl index f62329ada..109c1e6ee 100644 --- a/src/emqttd_http.erl +++ b/src/emqx_http.erl @@ -16,19 +16,21 @@ %% @doc HTTP publish API and websocket client. --module(emqttd_http). +-module(emqx_http). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). -import(proplists, [get_value/2, get_value/3]). -export([http_handler/0, handle_request/2, http_api/0, inner_handle_request/2]). --include("emqttd_internal.hrl"). +-include("emqx_rest.hrl"). + +-include("emqx_internal.hrl"). -record(state, {dispatch}). @@ -38,7 +40,7 @@ http_handler() -> {?MODULE, handle_request, [State]}. http_api() -> - Attr = emqttd_rest_api:module_info(attributes), + Attr = emqx_rest_api:module_info(attributes), [{Regexp, Method, Function, Args} || {http_api, [{Regexp, Method, Function, Args}]} <- Attr]. %%-------------------------------------------------------------------- @@ -51,7 +53,7 @@ handle_request(Req, State) -> handle_request("/status", Req, Req:get(method)); "/" -> handle_request("/", Req, Req:get(method)); - "/api/v2/auth" -> + "/api/v2/auth" -> %%TODO: Security Issue! handle_request(Path, Req, State); _ -> if_authorized(Req, fun() -> handle_request(Path, Req, State) end) @@ -67,11 +69,11 @@ handle_request("/api/v2/" ++ Url, Req, #state{dispatch = Dispatch}) -> handle_request("/status", Req, Method) when Method =:= 'HEAD'; Method =:= 'GET' -> {InternalStatus, _ProvidedStatus} = init:get_status(), - AppStatus = case lists:keysearch(emqttd, 1, application:which_applications()) of + AppStatus = case lists:keysearch(emqx, 1, application:which_applications()) of false -> not_running; {value, _Val} -> running end, - Status = io_lib:format("Node ~s is ~s~nemqttd is ~s", + Status = io_lib:format("Node ~s is ~s~nemqx is ~s", [node(), InternalStatus, AppStatus]), Req:ok({"text/plain", iolist_to_binary(Status)}); @@ -95,8 +97,8 @@ dispatcher(APIs) -> {true, true} -> {match, [MatchList]} = re:run(Url, Regexp, [global, {capture, all_but_first, list}]), Args = lists:append([[Method, Params], MatchList]), - lager:debug("Mod:~p, Fun:~p, Args:~p", [emqttd_rest_api, Function, Args]), - case catch apply(emqttd_rest_api, Function, Args) of + lager:debug("Mod:~p, Fun:~p, Args:~p", [emqx_rest_api, Function, Args]), + case catch apply(emqx_rest_api, Function, Args) of {ok, Data} -> respond(Req, 200, [{code, ?SUCCESS}, {result, Data}]); {error, Error} -> @@ -132,7 +134,7 @@ authorized(Req) -> false; "Basic " ++ BasicAuth -> {Username, Password} = user_passwd(BasicAuth), - case emqttd_mgmt:check_user(Username, Password) of + case emq_mgmt:check_user(Username, Password) of ok -> true; {error, Reason} -> diff --git a/src/emqttd_inflight.erl b/src/emqx_inflight.erl similarity index 99% rename from src/emqttd_inflight.erl rename to src/emqx_inflight.erl index bb9af390b..146780253 100644 --- a/src/emqttd_inflight.erl +++ b/src/emqx_inflight.erl @@ -16,7 +16,7 @@ %% @doc Inflight Window that wraps the gb_trees. --module(emqttd_inflight). +-module(emqx_inflight). -author("Feng Lee "). diff --git a/src/emqttd_keepalive.erl b/src/emqx_keepalive.erl similarity index 98% rename from src/emqttd_keepalive.erl rename to src/emqx_keepalive.erl index 5d26ea8a6..44dac454c 100644 --- a/src/emqttd_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -16,7 +16,7 @@ %% @doc Client Keepalive --module(emqttd_keepalive). +-module(emqx_keepalive). -author("Feng Lee "). diff --git a/src/lager_emqtt_backend.erl b/src/emqx_lager_backend.erl similarity index 91% rename from src/lager_emqtt_backend.erl rename to src/emqx_lager_backend.erl index 69c1aece4..45e3891d6 100644 --- a/src/lager_emqtt_backend.erl +++ b/src/emqx_lager_backend.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(lager_emqtt_backend). +-module(emqx_lager_backend). -author("Feng Lee "). @@ -79,10 +79,10 @@ publish_log(Message, State = #state{formatter = Formatter, format_config = FormatConfig}) -> Severity = lager_msg:severity(Message), Payload = Formatter:format(Message, FormatConfig), - Msg = emqttd_message:make(log, topic(Severity), iolist_to_binary(Payload)), - emqttd:publish(emqttd_message:set_flag(sys, Msg)), + Msg = emqx_message:make(log, topic(Severity), iolist_to_binary(Payload)), + emqx:publish(emqx_message:set_flag(sys, Msg)), {ok, State}. topic(Severity) -> - emqttd_topic:systop(list_to_binary(lists:concat(['logs/', Severity]))). + emqx_topic:systop(list_to_binary(lists:concat(['logs/', Severity]))). diff --git a/src/emqttd_message.erl b/src/emqx_message.erl similarity index 97% rename from src/emqttd_message.erl rename to src/emqx_message.erl index 4c3bea0d8..858a73000 100644 --- a/src/emqttd_message.erl +++ b/src/emqx_message.erl @@ -16,13 +16,13 @@ %% @doc MQTT Message Functions --module(emqttd_message). +-module(emqx_message). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). -export([make/3, make/4, from_packet/1, from_packet/2, from_packet/3, to_packet/1]). @@ -91,9 +91,9 @@ from_packet(Username, ClientId, Packet) -> Msg = from_packet(Packet), Msg#mqtt_message{from = {ClientId, Username}}. -msgid() -> emqttd_guid:gen(). +msgid() -> emqx_guid:gen(). -%% @doc Message to packet +%% @doc Message to Packet -spec(to_packet(mqtt_message()) -> mqtt_packet()). to_packet(#mqtt_message{pktid = PkgId, qos = Qos, diff --git a/src/emqttd_metrics.erl b/src/emqx_metrics.erl similarity index 94% rename from src/emqttd_metrics.erl rename to src/emqx_metrics.erl index c21d274a0..032969234 100644 --- a/src/emqttd_metrics.erl +++ b/src/emqx_metrics.erl @@ -14,15 +14,15 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_metrics). +-module(emqx_metrics). -behaviour(gen_server). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). -define(SERVER, ?MODULE). @@ -89,7 +89,8 @@ {counter, 'messages/qos2/sent'}, % QoS2 Messages sent {counter, 'messages/qos2/dropped'}, % QoS2 Messages dropped {gauge, 'messages/retained'}, % Messagea retained - {counter, 'messages/dropped'} % Messages dropped + {counter, 'messages/dropped'}, % Messages dropped + {counter, 'messages/forward'} % Messages forward ]). %%-------------------------------------------------------------------- @@ -242,7 +243,7 @@ key(counter, Metric) -> %%-------------------------------------------------------------------- init([]) -> - emqttd_time:seed(), + emqx_time:seed(), Metrics = ?SYSTOP_BYTES ++ ?SYSTOP_PACKETS ++ ?SYSTOP_MESSAGES, % Create metrics table ets:new(?METRIC_TAB, [set, public, named_table, {write_concurrency, true}]), @@ -251,7 +252,7 @@ init([]) -> % $SYS Topics for metrics % [ok = emqttd:create(topic, metric_topic(Topic)) || {_, Topic} <- Metrics], % Tick to publish metrics - {ok, #state{tick = emqttd_broker:start_tick(tick)}, hibernate}. + {ok, #state{tick = emqx_broker:start_tick(tick)}, hibernate}. handle_call(_Req, _From, State) -> {reply, error, State}. @@ -268,7 +269,7 @@ handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, #state{tick = TRef}) -> - emqttd_broker:stop_tick(TRef). + emqx_broker:stop_tick(TRef). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -278,8 +279,8 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- publish(Metric, Val) -> - Msg = emqttd_message:make(metrics, metric_topic(Metric), bin(Val)), - emqttd:publish(emqttd_message:set_flag(sys, Msg)). + Msg = emqx_message:make(metrics, metric_topic(Metric), bin(Val)), + emqx:publish(emqx_message:set_flag(sys, Msg)). create_metric({gauge, Name}) -> ets:insert(?METRIC_TAB, {{Name, 0}, 0}); @@ -289,7 +290,7 @@ create_metric({counter, Name}) -> ets:insert(?METRIC_TAB, [{{Name, I}, 0} || I <- Schedulers]). metric_topic(Metric) -> - emqttd_topic:systop(list_to_binary(lists:concat(['metrics/', Metric]))). + emqx_topic:systop(list_to_binary(lists:concat(['metrics/', Metric]))). bin(I) when is_integer(I) -> list_to_binary(integer_to_list(I)). diff --git a/src/emqttd_mgmt.erl b/src/emqx_mgmt.erl similarity index 85% rename from src/emqttd_mgmt.erl rename to src/emqx_mgmt.erl index 2c671f3bd..3d1141018 100644 --- a/src/emqttd_mgmt.erl +++ b/src/emqx_mgmt.erl @@ -14,15 +14,17 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_mgmt). +-module(emqx_mgmt). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). + +-include("emqx_rest.hrl"). -include_lib("stdlib/include/qlc.hrl"). @@ -61,7 +63,7 @@ brokers() -> [{Node, broker(Node)} || Node <- ekka_mnesia:running_nodes()]. broker(Node) when Node =:= node() -> - emqttd_broker:info(); + emqx_broker:info(); broker(Node) -> rpc_call(Node, broker, [Node]). @@ -69,7 +71,7 @@ metrics() -> [{Node, metrics(Node)} || Node <- ekka_mnesia:running_nodes()]. metrics(Node) when Node =:= node() -> - emqttd_metrics:all(); + emqx_metrics:all(); metrics(Node) -> rpc_call(Node, metrics, [Node]). @@ -77,7 +79,7 @@ stats() -> [{Node, stats(Node)} || Node <- ekka_mnesia:running_nodes()]. stats(Node) when Node =:= node() -> - emqttd_stats:getstats(); + emqx_stats:getstats(); stats(Node) -> rpc_call(Node, stats, [Node]). @@ -85,7 +87,7 @@ plugins() -> [{Node, plugins(Node)} || Node <- ekka_mnesia:running_nodes()]. plugins(Node) when Node =:= node() -> - emqttd_plugins:list(Node); + emqx_plugins:list(Node); plugins(Node) -> rpc_call(Node, plugins, [Node]). @@ -111,8 +113,8 @@ nodes_info() -> [node_info(Node) || Node <- Running] ++ DownNodes. node_info(Node) when Node =:= node() -> - CpuInfo = [{K, list_to_binary(V)} || {K, V} <- emqttd_vm:loads()], - Memory = emqttd_vm:get_memory(), + CpuInfo = [{K, list_to_binary(V)} || {K, V} <- emqx_vm:loads()], + Memory = emqx_vm:get_memory(), OtpRel = "R" ++ erlang:system_info(otp_release) ++ "/" ++ erlang:system_info(version), [{name, node()}, {otp_release, list_to_binary(OtpRel)}, @@ -133,17 +135,17 @@ stop_node(Node) -> %% plugins %%-------------------------------------------------------- plugin_list(Node) when Node =:= node() -> - emqttd_plugins:list(); + emqx_plugins:list(); plugin_list(Node) -> rpc_call(Node, plugin_list, [Node]). plugin_load(Node, PluginName) when Node =:= node() -> - emqttd_plugins:load(PluginName); + emqx_plugins:load(PluginName); plugin_load(Node, PluginName) -> rpc_call(Node, plugin_load, [Node, PluginName]). plugin_unload(Node, PluginName) when Node =:= node() -> - emqttd_plugins:unload(PluginName); + emqx_plugins:unload(PluginName); plugin_unload(Node, PluginName) -> rpc_call(Node, plugin_unload, [Node, PluginName]). @@ -189,7 +191,7 @@ route(Key) -> route_list(Key, 1, 20). %% alarm %%-------------------------------------------------------- alarm_list() -> - emqttd_alarm:get_alarms(). + emqx_alarm:get_alarms(). query_table(Qh, PageNo, PageSize, TotalNum) -> Cursor = qlc:cursor(Qh), @@ -220,8 +222,8 @@ lookup_table(LookupFun, _PageNo, _PageSize) -> publish({ClientId, Topic, Payload, Qos, Retain}) -> case validate(topic, Topic) of true -> - Msg = emqttd_message:make(ClientId, Qos, Topic, Payload), - emqttd:publish(Msg#mqtt_message{retain = Retain}), + Msg = emqx_message:make(ClientId, Qos, Topic, Payload), + emqx:publish(Msg#mqtt_message{retain = Retain}), ok; false -> {error, format_error(Topic, "validate topic: ${0} fail")} @@ -230,11 +232,11 @@ publish({ClientId, Topic, Payload, Qos, Retain}) -> subscribe({ClientId, Topic, Qos}) -> case validate(topic, Topic) of true -> - case emqttd_sm:lookup_session(ClientId) of + case emqx_sm:lookup_session(ClientId) of undefined -> {error, format_error(ClientId, "Clientid: ${0} not found")}; #mqtt_session{sess_pid = SessPid} -> - emqttd_session:subscribe(SessPid, [{Topic, [{qos, Qos}]}]), + emqx_session:subscribe(SessPid, [{Topic, [{qos, Qos}]}]), ok end; false -> @@ -244,16 +246,53 @@ subscribe({ClientId, Topic, Qos}) -> unsubscribe({ClientId, Topic}) -> case validate(topic, Topic) of true -> - case emqttd_sm:lookup_session(ClientId) of + case emqx_sm:lookup_session(ClientId) of undefined -> {error, format_error(ClientId, "Clientid: ${0} not found")}; #mqtt_session{sess_pid = SessPid} -> - emqttd_session:unsubscribe(SessPid, [{Topic, []}]), + emqx_session:unsubscribe(SessPid, [{Topic, []}]), ok end; false -> {error, format_error(Topic, "validate topic: ${0} fail")} end. + +% publish(Messages) -> +% lists:foldl( +% fun({ClientId, Topic, Payload, Qos, Retain}, {Success, Failed}) -> +% case validate(topic, Topic) of +% true -> +% Msg = emqx_message:make(ClientId, Qos, Topic, Payload), +% emqx:publish(Msg#mqtt_message{retain = Retain}), +% {[[{topic, Topic}]| Success], Failed}; +% false -> +% {Success, [[{topic, Topic}]| Failed]} +% end +% end, {[], []}, Messages). + +% subscribers(Subscribers) -> +% lists:foldl( +% fun({ClientId, Topic, Qos}, {Success, Failed}) -> +% case emqx_sm:lookup_session(ClientId) of +% undefined -> +% {Success, [[{client_id, ClientId}]|Failed]}; +% #mqtt_session{sess_pid = SessPid} -> +% emqx_session:subscribe(SessPid, [{Topic, [{qos, Qos}]}]), +% {[[{client_id, ClientId}]| Success], Failed} +% end +% end,{[], []}, Subscribers). + +% unsubscribers(UnSubscribers)-> +% lists:foldl( +% fun({ClientId, Topic}, {Success, Failed}) -> +% case emqx_sm:lookup_session(ClientId) of +% undefined -> +% {Success, [[{client_id, ClientId}]|Failed]}; +% #mqtt_session{sess_pid = SessPid} -> +% emqx_session:unsubscriber(SessPid, [{Topic, []}]), +% {[[{client_id, ClientId}]| Success], Failed} +% end +% end, {[], []}, UnSubscribers). %%-------------------------------------------------------------------- %% manager API @@ -263,9 +302,9 @@ kick_client(ClientId) -> lists:any(fun(Item) -> Item =:= ok end, Result). kick_client(Node, ClientId) when Node =:= node() -> - case emqttd_cm:lookup(ClientId) of + case emqx_cm:lookup(ClientId) of undefined -> error; - #mqtt_client{client_pid = Pid}-> emqttd_client:kick(Pid) + #mqtt_client{client_pid = Pid}-> emqx_client:kick(Pid) end; kick_client(Node, ClientId) -> rpc_call(Node, kick_client, [Node, ClientId]). @@ -276,9 +315,9 @@ clean_acl_cache(ClientId, Topic) -> lists:any(fun(Item) -> Item =:= ok end, Result). clean_acl_cache(Node, ClientId, Topic) when Node =:= node() -> - case emqttd_cm:lookup(ClientId) of + case emqx_cm:lookup(ClientId) of undefined -> error; - #mqtt_client{client_pid = Pid}-> emqttd_client:clean_acl_cache(Pid, Topic) + #mqtt_client{client_pid = Pid}-> emqx_client:clean_acl_cache(Pid, Topic) end; clean_acl_cache(Node, ClientId, Topic) -> rpc_call(Node, clean_acl_cache, [Node, ClientId, Topic]). @@ -287,14 +326,14 @@ clean_acl_cache(Node, ClientId, Topic) -> %% Config ENV %%-------------------------------------------------------------------- modify_config(App, Terms) -> - emqttd_config:write(App, Terms). + emqx_config:write(App, Terms). modify_config(App, Key, Value) -> Result = [modify_config(Node, App, Key, Value) || Node <- ekka_mnesia:running_nodes()], lists:any(fun(Item) -> Item =:= ok end, Result). modify_config(Node, App, Key, Value) when Node =:= node() -> - emqttd_config:set(App, Key, Value); + emqx_config:set(App, Key, Value); modify_config(Node, App, Key, Value) -> rpc_call(Node, modify_config, [Node, App, Key, Value]). @@ -302,17 +341,17 @@ get_configs() -> [{Node, get_config(Node)} || Node <- ekka_mnesia:running_nodes()]. get_config(Node) when Node =:= node()-> - emqttd_cli_config:all_cfgs(); + emqx_cli_config:all_cfgs(); get_config(Node) -> rpc_call(Node, get_config, [Node]). get_plugin_config(PluginName) -> - emqttd_config:read(PluginName). + emqx_config:read(PluginName). get_plugin_config(Node, PluginName) -> rpc_call(Node, get_plugin_config, [PluginName]). modify_plugin_config(PluginName, Terms) -> - emqttd_config:write(PluginName, Terms). + emqx_config:write(PluginName, Terms). modify_plugin_config(Node, PluginName, Terms) -> rpc_call(Node, modify_plugin_config, [PluginName, Terms]). @@ -428,7 +467,7 @@ validate(qos, Qos) -> (Qos >= ?QOS_0) and (Qos =< ?QOS_2); validate(topic, Topic) -> - emqttd_topic:validate({name, Topic}). + emqx_topic:validate({name, Topic}). client_list(ClientId, PageNo, PageSize) when ?EMPTY_KEY(ClientId) -> TotalNum = ets:info(mqtt_client, size), diff --git a/src/emqttd_misc.erl b/src/emqx_misc.erl similarity index 99% rename from src/emqttd_misc.erl rename to src/emqx_misc.erl index e60d27d4f..444b0de61 100644 --- a/src/emqttd_misc.erl +++ b/src/emqx_misc.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_misc). +-module(emqx_misc). -author("Feng Lee "). diff --git a/src/emqttd_mod_sup.erl b/src/emqx_mod_sup.erl similarity index 97% rename from src/emqttd_mod_sup.erl rename to src/emqx_mod_sup.erl index 586d5af1a..a3f4f19ff 100644 --- a/src/emqttd_mod_sup.erl +++ b/src/emqx_mod_sup.erl @@ -14,11 +14,11 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_mod_sup). +-module(emqx_mod_sup). -behaviour(supervisor). --include("emqttd.hrl"). +-include("emqx.hrl"). %% API -export([start_link/0, start_child/1, start_child/2, stop_child/1]). diff --git a/src/emqttd_mqueue.erl b/src/emqx_mqueue.erl similarity index 98% rename from src/emqttd_mqueue.erl rename to src/emqx_mqueue.erl index 08e620a37..82cdc30b1 100644 --- a/src/emqttd_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -41,13 +41,13 @@ %% %% @end --module(emqttd_mqueue). +-module(emqx_mqueue). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). -import(proplists, [get_value/3]). @@ -58,7 +58,7 @@ -define(HIGH_WM, 0.6). --define(PQUEUE, priority_queue). +-define(PQUEUE, emqx_pqueue). -type(priority() :: {iolist(), pos_integer()}). diff --git a/src/emqttd_net.erl b/src/emqx_net.erl similarity index 99% rename from src/emqttd_net.erl rename to src/emqx_net.erl index 1f246a315..a25e830e0 100644 --- a/src/emqttd_net.erl +++ b/src/emqx_net.erl @@ -14,7 +14,9 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_net). +-module(emqx_net). + +-author("Feng Lee "). -include_lib("kernel/include/inet.hrl"). diff --git a/src/emqttd_packet.erl b/src/emqx_packet.erl similarity index 88% rename from src/emqttd_packet.erl rename to src/emqx_packet.erl index 6349e58b1..ed690b7dd 100644 --- a/src/emqttd_packet.erl +++ b/src/emqx_packet.erl @@ -14,13 +14,13 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_packet). +-module(emqx_packet). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). %% API -export([protocol_name/1, type_name/1, connack_name/1]). @@ -70,18 +70,18 @@ format_variable(Variable, Payload) -> io_lib:format("~s, Payload=~p", [format_variable(Variable), Payload]). format_variable(#mqtt_packet_connect{ - proto_ver = ProtoVer, - proto_name = ProtoName, - will_retain = WillRetain, - will_qos = WillQoS, - will_flag = WillFlag, - clean_sess = CleanSess, - keep_alive = KeepAlive, - client_id = ClientId, - will_topic = WillTopic, - will_msg = WillMsg, - username = Username, - password = Password}) -> + proto_ver = ProtoVer, + proto_name = ProtoName, + will_retain = WillRetain, + will_qos = WillQoS, + will_flag = WillFlag, + clean_sess = CleanSess, + keep_alive = KeepAlive, + client_id = ClientId, + will_topic = WillTopic, + will_msg = WillMsg, + username = Username, + password = Password}) -> Format = "ClientId=~s, ProtoName=~s, ProtoVsn=~p, CleanSess=~s, KeepAlive=~p, Username=~s, Password=~s", Args = [ClientId, ProtoName, ProtoVer, CleanSess, KeepAlive, Username, format_password(Password)], {Format1, Args1} = if diff --git a/src/emqttd_parser.erl b/src/emqx_parser.erl similarity index 99% rename from src/emqttd_parser.erl rename to src/emqx_parser.erl index dde9ae4dc..ba1624af1 100644 --- a/src/emqttd_parser.erl +++ b/src/emqx_parser.erl @@ -15,13 +15,13 @@ %%-------------------------------------------------------------------- %% @doc MQTT Packet Parser --module(emqttd_parser). +-module(emqx_parser). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). %% API -export([initial_state/0, initial_state/1, parse/2]). diff --git a/src/emqttd_plugins.erl b/src/emqx_plugins.erl similarity index 96% rename from src/emqttd_plugins.erl rename to src/emqx_plugins.erl index 73f184a3a..ad5523e54 100644 --- a/src/emqttd_plugins.erl +++ b/src/emqx_plugins.erl @@ -14,11 +14,11 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_plugins). +-module(emqx_plugins). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). -export([init/0]). @@ -31,7 +31,7 @@ %% @doc Init plugins' config -spec(init() -> ok). init() -> - case emqttd:env(plugins_etc_dir) of + case emqx:env(plugins_etc_dir) of {ok, PluginsEtc} -> CfgFiles = [filename:join(PluginsEtc, File) || File <- filelib:wildcard("*.config", PluginsEtc)], @@ -49,7 +49,7 @@ init_config(CfgFile) -> %% @doc Load all plugins when the broker started. -spec(load() -> list() | {error, any()}). load() -> - case emqttd:env(plugins_loaded_file) of + case emqx:env(plugins_loaded_file) of {ok, File} -> ensure_file(File), with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end); @@ -82,7 +82,7 @@ load_plugins(Names, Persistent) -> %% @doc Unload all plugins before broker stopped. -spec(unload() -> list() | {error, any()}). unload() -> - case emqttd:env(plugins_loaded_file) of + case emqx:env(plugins_loaded_file) of {ok, File} -> with_loaded_file(File, fun stop_plugins/1); undefined -> @@ -96,7 +96,7 @@ stop_plugins(Names) -> %% @doc List all available plugins -spec(list() -> [mqtt_plugin()]). list() -> - case emqttd:env(plugins_etc_dir) of + case emqx:env(plugins_etc_dir) of {ok, PluginsEtc} -> CfgFiles = filelib:wildcard("*.{conf,config}", PluginsEtc), Plugins = [plugin(CfgFile) || CfgFile <- CfgFiles], @@ -251,7 +251,7 @@ plugin_unloaded(Name, true) -> end. read_loaded() -> - case emqttd:env(plugins_loaded_file) of + case emqx:env(plugins_loaded_file) of {ok, File} -> read_loaded(File); undefined -> {error, not_found} end. @@ -259,7 +259,7 @@ read_loaded() -> read_loaded(File) -> file:consult(File). write_loaded(AppNames) -> - {ok, File} = emqttd:env(plugins_loaded_file), + {ok, File} = emqx:env(plugins_loaded_file), case file:open(File, [binary, write]) of {ok, Fd} -> lists:foreach(fun(Name) -> diff --git a/src/emqttd_pmon.erl b/src/emqx_pmon.erl similarity index 98% rename from src/emqttd_pmon.erl rename to src/emqx_pmon.erl index ebe691ad4..5efdd115e 100644 --- a/src/emqttd_pmon.erl +++ b/src/emqx_pmon.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_pmon). +-module(emqx_pmon). -author("Feng Lee "). diff --git a/src/emqttd_pool_sup.erl b/src/emqx_pool_sup.erl similarity index 94% rename from src/emqttd_pool_sup.erl rename to src/emqx_pool_sup.erl index fa1d6aace..afed06e87 100644 --- a/src/emqttd_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -15,7 +15,7 @@ %%-------------------------------------------------------------------- %% @doc Common Pool Supervisor --module(emqttd_pool_sup). +-module(emqx_pool_sup). -author("Feng Lee "). @@ -41,7 +41,7 @@ start_link(Pool, Type, MFA) -> Schedulers = erlang:system_info(schedulers), start_link(Pool, Type, Schedulers, MFA). --spec(start_link(atom(), atom(), pos_integer(), mfa()) -> {ok, pid()} | {error, any()}). +-spec(start_link(atom() | tuple(), atom(), pos_integer(), mfa()) -> {ok, pid()} | {error, any()}). start_link(Pool, Type, Size, MFA) -> supervisor:start_link(?MODULE, [Pool, Type, Size, MFA]). diff --git a/src/emqttd_pooler.erl b/src/emqx_pooler.erl similarity index 94% rename from src/emqttd_pooler.erl rename to src/emqx_pooler.erl index 1b73d138c..c60a22e27 100644 --- a/src/emqttd_pooler.erl +++ b/src/emqx_pooler.erl @@ -14,11 +14,13 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_pooler). +-module(emqx_pooler). + +-author("Feng Lee "). -behaviour(gen_server). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). %% Start the pool supervisor -export([start_link/0]). @@ -34,7 +36,7 @@ %% @doc Start Pooler Supervisor. start_link() -> - emqttd_pool_sup:start_link(pooler, random, {?MODULE, start_link, []}). + emqx_pool_sup:start_link(pooler, random, {?MODULE, start_link, []}). %%-------------------------------------------------------------------- %% API diff --git a/src/priority_queue.erl b/src/emqx_pqueue.erl similarity index 90% rename from src/priority_queue.erl rename to src/emqx_pqueue.erl index 7147b0bb4..f9392d5f6 100644 --- a/src/priority_queue.erl +++ b/src/emqx_pqueue.erl @@ -37,46 +37,27 @@ %% calls into the same function knowing that ordinary queues represent %% a base case. --module(priority_queue). +-module(emqx_pqueue). -export([new/0, is_queue/1, is_empty/1, len/1, plen/2, to_list/1, from_list/1, in/2, in/3, out/1, out/2, out_p/1, join/2, filter/2, fold/3, highest/1]). %%---------------------------------------------------------------------------- --ifdef(use_specs). - --type(q() :: pqueue()). -type(priority() :: integer() | 'infinity'). -type(squeue() :: {queue, [any()], [any()], non_neg_integer()}). -type(pqueue() :: squeue() | {pqueue, [{priority(), squeue()}]}). +-type(q() :: pqueue()). -export_type([q/0]). --spec(new/0 :: () -> pqueue()). --spec(is_queue/1 :: (any()) -> boolean()). --spec(is_empty/1 :: (pqueue()) -> boolean()). --spec(len/1 :: (pqueue()) -> non_neg_integer()). --spec(plen/2 :: (priority(), pqueue()) -> non_neg_integer()). --spec(to_list/1 :: (pqueue()) -> [{priority(), any()}]). --spec(from_list/1 :: ([{priority(), any()}]) -> pqueue()). --spec(in/2 :: (any(), pqueue()) -> pqueue()). --spec(in/3 :: (any(), priority(), pqueue()) -> pqueue()). --spec(out/1 :: (pqueue()) -> {empty | {value, any()}, pqueue()}). --spec(out_p/1 :: (pqueue()) -> {empty | {value, any(), priority()}, pqueue()}). --spec(join/2 :: (pqueue(), pqueue()) -> pqueue()). --spec(filter/2 :: (fun ((any()) -> boolean()), pqueue()) -> pqueue()). --spec(fold/3 :: - (fun ((any(), priority(), A) -> A), A, pqueue()) -> A). --spec(highest/1 :: (pqueue()) -> priority() | 'empty'). - --endif. - %%---------------------------------------------------------------------------- +-spec(new() -> pqueue()). new() -> {queue, [], [], 0}. +-spec(is_queue(any()) -> boolean()). is_queue({queue, R, F, L}) when is_list(R), is_list(F), is_integer(L) -> true; is_queue({pqueue, Queues}) when is_list(Queues) -> @@ -86,16 +67,19 @@ is_queue({pqueue, Queues}) when is_list(Queues) -> is_queue(_) -> false. +-spec(is_empty(pqueue()) -> boolean()). is_empty({queue, [], [], 0}) -> true; is_empty(_) -> false. +-spec(len(pqueue()) -> non_neg_integer()). len({queue, _R, _F, L}) -> L; len({pqueue, Queues}) -> lists:sum([len(Q) || {_, Q} <- Queues]). +-spec(plen(priority(), pqueue()) -> non_neg_integer()). plen(0, {queue, _R, _F, L}) -> L; plen(_, {queue, _R, _F, _}) -> @@ -106,18 +90,22 @@ plen(P, {pqueue, Queues}) -> false -> 0 end. +-spec(to_list(pqueue()) -> [{priority(), any()}]). to_list({queue, In, Out, _Len}) when is_list(In), is_list(Out) -> [{0, V} || V <- Out ++ lists:reverse(In, [])]; to_list({pqueue, Queues}) -> [{maybe_negate_priority(P), V} || {P, Q} <- Queues, {0, V} <- to_list(Q)]. +-spec(from_list([{priority(), any()}]) -> pqueue()). from_list(L) -> lists:foldl(fun ({P, E}, Q) -> in(E, P, Q) end, new(), L). +-spec(in(any(), pqueue()) -> pqueue()). in(Item, Q) -> in(Item, 0, Q). +-spec(in(any(), priority(), pqueue()) -> pqueue()). in(X, 0, {queue, [_] = In, [], 1}) -> {queue, [X], In, 2}; in(X, 0, {queue, In, Out, Len}) when is_list(In), is_list(Out) -> @@ -143,6 +131,7 @@ in(X, Priority, {pqueue, Queues}) -> end end}. +-spec(out(pqueue()) -> {empty | {value, any()}, pqueue()}). out({queue, [], [], 0} = Q) -> {empty, Q}; out({queue, [V], [], 1}) -> @@ -166,6 +155,7 @@ out({pqueue, [{P, Q} | Queues]}) -> end, {R, NewQ}. +-spec(out_p(pqueue()) -> {empty | {value, any(), priority()}, pqueue()}). out_p({queue, _, _, _} = Q) -> add_p(out(Q), 0); out_p({pqueue, [{P, _} | _]} = Q) -> add_p(out(Q), maybe_negate_priority(P)). @@ -196,6 +186,7 @@ add_p(R, P) -> case R of {{value, V}, Q} -> {{value, V, P}, Q} end. +-spec(join(pqueue(), pqueue()) -> pqueue()). join(A, {queue, [], [], 0}) -> A; join({queue, [], [], 0}, B) -> @@ -234,6 +225,7 @@ merge([{PA, A}|As], Bs = [{PB, _}|_], Acc) when PA < PB orelse PA == infinity -> merge(As = [{_, _}|_], [{PB, B}|Bs], Acc) -> merge(As, Bs, [ {PB, B} | Acc ]). +-spec(filter(fun ((any()) -> boolean()), pqueue()) -> pqueue()). filter(Pred, Q) -> fold(fun(V, P, Acc) -> case Pred(V) of true -> in(V, P, Acc); @@ -241,11 +233,13 @@ filter(Pred, Q) -> fold(fun(V, P, Acc) -> end end, new(), Q). +-spec(fold(fun ((any(), priority(), A) -> A), A, pqueue()) -> A). fold(Fun, Init, Q) -> case out_p(Q) of {empty, _Q} -> Init; {{value, V, P}, Q1} -> fold(Fun, Fun(V, P, Init), Q1) end. +-spec(highest(pqueue()) -> priority() | 'empty'). highest({queue, [], [], 0}) -> empty; highest({queue, _, _, _}) -> 0; highest({pqueue, [{P, _} | _]}) -> maybe_negate_priority(P). @@ -257,3 +251,4 @@ r2f([X,Y|R], L) -> {queue, [X,Y], lists:reverse(R, []), L}. maybe_negate_priority(infinity) -> infinity; maybe_negate_priority(P) -> -P. + diff --git a/src/emqttd_protocol.erl b/src/emqx_protocol.erl similarity index 80% rename from src/emqttd_protocol.erl rename to src/emqx_protocol.erl index 0129faedd..b03213310 100644 --- a/src/emqttd_protocol.erl +++ b/src/emqx_protocol.erl @@ -14,15 +14,15 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_protocol). +-module(emqx_protocol). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). -import(proplists, [get_value/2, get_value/3]). @@ -42,14 +42,15 @@ %% ws_initial_headers: Headers from first HTTP request for WebSocket Client. -record(proto_state, {peername, sendfun, connected = false, client_id, client_pid, clean_sess, proto_ver, proto_name, username, is_superuser, - will_msg, keepalive, keepalive_backoff, max_clientid_len, - session, stats_data, mountpoint, ws_initial_headers, - connected_at}). + will_msg, keepalive, max_clientid_len, session, stats_data, + keepalive_backoff, peercert_username, ws_initial_headers, + mountpoint, connected_at}). -type(proto_state() :: #proto_state{}). -define(INFO_KEYS, [client_id, username, clean_sess, proto_ver, proto_name, - keepalive, will_msg, ws_initial_headers, mountpoint, connected_at]). + keepalive, will_msg, ws_initial_headers, mountpoint, + peercert_username, connected_at]). -define(STATS_KEYS, [recv_pkt, recv_msg, send_pkt, send_msg]). @@ -68,6 +69,7 @@ init(Peername, SendFun, Opts) -> max_clientid_len = MaxLen, is_superuser = false, client_pid = self(), + peercert_username = false, ws_initial_headers = WsInitialHeaders, keepalive_backoff = Backoff, stats_data = #proto_stats{enable_stats = EnableStats}}. @@ -79,9 +81,28 @@ enrich_opt([], _Conn, State) -> State; enrich_opt([{mountpoint, MountPoint} | ConnOpts], Conn, State) -> enrich_opt(ConnOpts, Conn, State#proto_state{mountpoint = MountPoint}); +enrich_opt([{peer_cert_as_username, N} | ConnOpts], Conn, State) -> + case Conn:type() of + ssl -> enrich_opt(ConnOpts, Conn, State#proto_state{ + peercert_username = peercert_username(N, Conn:peercert())}); + _ -> enrich_opt(ConnOpts, Conn, State) + end; enrich_opt([_ | ConnOpts], Conn, State) -> enrich_opt(ConnOpts, Conn, State). +peercert_username(cn, Cert) -> + case emqx_ssl:peer_cert_common_name(Cert) of + not_found -> undefined; + CN -> iolist_to_binary(CN) + end; +peercert_username(dn, Cert) -> + iolist_to_binary(emqx_ssl:peer_cert_subject(Cert)). + +repl_username_with_peercert(State = #proto_state{peercert_username = false}) -> + State; +repl_username_with_peercert(State = #proto_state{peercert_username = PeerCert}) -> + State#proto_state{username = PeerCert}. + info(ProtoState) -> ?record_to_proplist(proto_state, ProtoState, ?INFO_KEYS). @@ -150,9 +171,9 @@ subscribe(RawTopicTable, ProtoState = #proto_state{client_id = ClientId, username = Username, session = Session}) -> TopicTable = parse_topic_table(RawTopicTable), - case emqttd_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of + case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of {ok, TopicTable1} -> - emqttd_session:subscribe(Session, TopicTable1); + emqx_session:subscribe(Session, TopicTable1); {stop, _} -> ok end, @@ -161,9 +182,9 @@ subscribe(RawTopicTable, ProtoState = #proto_state{client_id = ClientId, unsubscribe(RawTopics, ProtoState = #proto_state{client_id = ClientId, username = Username, session = Session}) -> - case emqttd_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of + case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of {ok, TopicTable} -> - emqttd_session:unsubscribe(Session, TopicTable); + emqx_session:unsubscribe(Session, TopicTable); {stop, _} -> ok end, @@ -182,14 +203,15 @@ process(?CONNECT_PACKET(Var), State0) -> keep_alive = KeepAlive, client_id = ClientId} = Var, - State1 = State0#proto_state{proto_ver = ProtoVer, - proto_name = ProtoName, - username = Username, - client_id = ClientId, - clean_sess = CleanSess, - keepalive = KeepAlive, - will_msg = willmsg(Var, State0), - connected_at = os:timestamp()}, + State1 = repl_username_with_peercert( + State0#proto_state{proto_ver = ProtoVer, + proto_name = ProtoName, + username = Username, + client_id = ClientId, + clean_sess = CleanSess, + keepalive = KeepAlive, + will_msg = willmsg(Var, State0), + connected_at = os:timestamp()}), {ReturnCode1, SessPresent, State3} = case validate_connect(Var, State1) of @@ -200,10 +222,10 @@ process(?CONNECT_PACKET(Var), State0) -> State2 = maybe_set_clientid(State1), %% Start session - case emqttd_sm:start_session(CleanSess, {clientid(State2), Username}) of + case emqx_sm:start_session(CleanSess, {clientid(State2), Username}) of {ok, Session, SP} -> %% Register the client - emqttd_cm:reg(client(State2)), + emqx_cm:reg(client(State2)), %% Start keepalive start_keepalive(KeepAlive, State2), %% Emit Stats @@ -221,7 +243,7 @@ process(?CONNECT_PACKET(Var), State0) -> {ReturnCode, false, State1} end, %% Run hooks - emqttd_hooks:run('client.connected', [ReturnCode1], client(State3)), + emqx_hooks:run('client.connected', [ReturnCode1], client(State3)), %% Send connack send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), State3), %% stop if authentication failure @@ -235,19 +257,19 @@ process(Packet = ?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload), State = #pro {ok, State}; process(?PUBACK_PACKET(?PUBACK, PacketId), State = #proto_state{session = Session}) -> - emqttd_session:puback(Session, PacketId), + emqx_session:puback(Session, PacketId), {ok, State}; process(?PUBACK_PACKET(?PUBREC, PacketId), State = #proto_state{session = Session}) -> - emqttd_session:pubrec(Session, PacketId), + emqx_session:pubrec(Session, PacketId), send(?PUBREL_PACKET(PacketId), State); process(?PUBACK_PACKET(?PUBREL, PacketId), State = #proto_state{session = Session}) -> - emqttd_session:pubrel(Session, PacketId), + emqx_session:pubrel(Session, PacketId), send(?PUBACK_PACKET(?PUBCOMP, PacketId), State); process(?PUBACK_PACKET(?PUBCOMP, PacketId), State = #proto_state{session = Session})-> - emqttd_session:pubcomp(Session, PacketId), {ok, State}; + emqx_session:pubcomp(Session, PacketId), {ok, State}; %% Protect from empty topic table process(?SUBSCRIBE_PACKET(PacketId, []), State) -> @@ -270,9 +292,9 @@ process(?SUBSCRIBE_PACKET(PacketId, RawTopicTable), ?LOG(error, "Cannot SUBSCRIBE ~p for ACL Deny", [TopicTable], State), send(?SUBACK_PACKET(PacketId, [16#80 || _ <- TopicTable]), State); false -> - case emqttd_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of + case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of {ok, TopicTable1} -> - emqttd_session:subscribe(Session, PacketId, mount(MountPoint, TopicTable1)), + emqx_session:subscribe(Session, PacketId, mount(MountPoint, TopicTable1)), {ok, State}; {stop, _} -> {ok, State} @@ -288,9 +310,9 @@ process(?UNSUBSCRIBE_PACKET(PacketId, RawTopics), username = Username, mountpoint = MountPoint, session = Session}) -> - case emqttd_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of + case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of {ok, TopicTable} -> - emqttd_session:unsubscribe(Session, mount(MountPoint, TopicTable)); + emqx_session:unsubscribe(Session, mount(MountPoint, TopicTable)); {stop, _} -> ok end, @@ -308,8 +330,8 @@ publish(Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), username = Username, mountpoint = MountPoint, session = Session}) -> - Msg = emqttd_message:from_packet(Username, ClientId, Packet), - emqttd_session:publish(Session, mount(MountPoint, Msg)); + Msg = emqx_message:from_packet(Username, ClientId, Packet), + emqx_session:publish(Session, mount(MountPoint, Msg)); publish(Packet = ?PUBLISH_PACKET(?QOS_1, _PacketId), State) -> with_puback(?PUBACK, Packet, State); @@ -322,8 +344,8 @@ with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), username = Username, mountpoint = MountPoint, session = Session}) -> - Msg = emqttd_message:from_packet(Username, ClientId, Packet), - case emqttd_session:publish(Session, mount(MountPoint, Msg)) of + Msg = emqx_message:from_packet(Username, ClientId, Packet), + case emqx_session:publish(Session, mount(MountPoint, Msg)) of ok -> send(?PUBACK_PACKET(Type, PacketId), State); {error, Error} -> @@ -335,22 +357,22 @@ send(Msg, State = #proto_state{client_id = ClientId, username = Username, mountpoint = MountPoint}) when is_record(Msg, mqtt_message) -> - emqttd_hooks:run('message.delivered', [ClientId, Username], Msg), - send(emqttd_message:to_packet(unmount(MountPoint, Msg)), State); + emqx_hooks:run('message.delivered', [ClientId, Username], Msg), + send(emqx_message:to_packet(unmount(MountPoint, Msg)), State); send(Packet = ?PACKET(Type), State = #proto_state{sendfun = SendFun, stats_data = Stats}) -> trace(send, Packet, State), - emqttd_metrics:sent(Packet), + emqx_metrics:sent(Packet), SendFun(Packet), Stats1 = inc_stats(send, Type, Stats), {ok, State#proto_state{stats_data = Stats1}}. trace(recv, Packet, ProtoState) -> - ?LOG(debug, "RECV ~s", [emqttd_packet:format(Packet)], ProtoState); + ?LOG(info, "RECV ~s", [emqx_packet:format(Packet)], ProtoState); trace(send, Packet, ProtoState) -> - ?LOG(debug, "SEND ~s", [emqttd_packet:format(Packet)], ProtoState). + ?LOG(info, "SEND ~s", [emqx_packet:format(Packet)], ProtoState). inc_stats(_Direct, _Type, Stats = #proto_stats{enable_stats = false}) -> Stats; @@ -381,20 +403,20 @@ shutdown(_Error, #proto_state{client_id = undefined}) -> shutdown(conflict, #proto_state{client_id = _ClientId}) -> %% let it down - %% emqttd_cm:unreg(ClientId); + %% emqx_cm:unreg(ClientId); ignore; shutdown(Error, State = #proto_state{will_msg = WillMsg}) -> - ?LOG(debug, "Shutdown for ~p", [Error], State), + ?LOG(info, "Shutdown for ~p", [Error], State), Client = client(State), send_willmsg(Client, WillMsg), - emqttd_hooks:run('client.disconnected', [Error], Client), + emqx_hooks:run('client.disconnected', [Error], Client), %% let it down - %% emqttd_cm:unreg(ClientId). + %% emqx_cm:unreg(ClientId). ok. willmsg(Packet, #proto_state{mountpoint = MountPoint}) when is_record(Packet, mqtt_packet_connect) -> - case emqttd_message:from_packet(Packet) of + case emqx_message:from_packet(Packet) of undefined -> undefined; Msg -> mount(MountPoint, Msg) end. @@ -402,8 +424,8 @@ willmsg(Packet, #proto_state{mountpoint = MountPoint}) when is_record(Packet, mq %% Generate a client if if nulll maybe_set_clientid(State = #proto_state{client_id = NullId}) when NullId =:= undefined orelse NullId =:= <<>> -> - {_, NPid, _} = emqttd_guid:new(), - ClientId = iolist_to_binary(["emqttd_", integer_to_list(NPid)]), + {_, NPid, _} = emqx_guid:new(), + ClientId = iolist_to_binary(["emqx_", integer_to_list(NPid)]), State#proto_state{client_id = ClientId}; maybe_set_clientid(State) -> @@ -412,7 +434,7 @@ maybe_set_clientid(State) -> send_willmsg(_Client, undefined) -> ignore; send_willmsg(#mqtt_client{client_id = ClientId, username = Username}, WillMsg) -> - emqttd:publish(WillMsg#mqtt_message{from = {ClientId, Username}}). + emqx_server:publish(WillMsg#mqtt_message{from = {ClientId, Username}}). start_keepalive(0, _State) -> ignore; @@ -463,7 +485,7 @@ validate_clientid(#mqtt_packet_connect{proto_ver = ProtoVer, false. validate_packet(?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload)) -> - case emqttd_topic:validate({name, Topic}) of + case emqx_topic:validate({name, Topic}) of true -> ok; false -> {error, badtopic} end; @@ -483,7 +505,7 @@ validate_topics(_Type, []) -> validate_topics(Type, TopicTable = [{_Topic, _Qos}|_]) when Type =:= name orelse Type =:= filter -> Valid = fun(Topic, Qos) -> - emqttd_topic:validate({Type, Topic}) and validate_qos(Qos) + emqx_topic:validate({Type, Topic}) and validate_qos(Qos) end, case [Topic || {Topic, Qos} <- TopicTable, not Valid(Topic, Qos)] of [] -> ok; @@ -491,7 +513,7 @@ validate_topics(Type, TopicTable = [{_Topic, _Qos}|_]) end; validate_topics(Type, Topics = [Topic0|_]) when is_binary(Topic0) -> - case [Topic || Topic <- Topics, not emqttd_topic:validate({Type, Topic})] of + case [Topic || Topic <- Topics, not emqx_topic:validate({Type, Topic})] of [] -> ok; _ -> {error, badtopic} end. @@ -505,15 +527,15 @@ validate_qos(_) -> parse_topic_table(TopicTable) -> lists:map(fun({Topic0, Qos}) -> - {Topic, Opts} = emqttd_topic:parse(Topic0), + {Topic, Opts} = emqx_topic:parse(Topic0), {Topic, [{qos, Qos}|Opts]} end, TopicTable). parse_topics(Topics) -> - [emqttd_topic:parse(Topic) || Topic <- Topics]. + [emqx_topic:parse(Topic) || Topic <- Topics]. authenticate(Client, Password) -> - case emqttd_access_control:auth(Client, Password) of + case emqx_access_control:auth(Client, Password) of ok -> {ok, false}; {ok, IsSuper} -> {ok, IsSuper}; {error, Error} -> {error, Error} @@ -521,20 +543,20 @@ authenticate(Client, Password) -> %% PUBLISH ACL is cached in process dictionary. check_acl(publish, Topic, Client) -> - IfCache = emqttd:env(cache_acl, true), + IfCache = emqx:env(cache_acl, true), case {IfCache, get({acl, publish, Topic})} of {true, undefined} -> - AllowDeny = emqttd_access_control:check_acl(Client, publish, Topic), + AllowDeny = emqx_access_control:check_acl(Client, publish, Topic), put({acl, publish, Topic}, AllowDeny), AllowDeny; {true, AllowDeny} -> AllowDeny; {false, _} -> - emqttd_access_control:check_acl(Client, publish, Topic) + emqx_access_control:check_acl(Client, publish, Topic) end; check_acl(subscribe, Topic, Client) -> - emqttd_access_control:check_acl(Client, subscribe, Topic). + emqx_access_control:check_acl(Client, subscribe, Topic). sp(true) -> 1; sp(false) -> 0. diff --git a/src/emqttd_pubsub.erl b/src/emqx_pubsub.erl similarity index 86% rename from src/emqttd_pubsub.erl rename to src/emqx_pubsub.erl index d976618cd..541007975 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqx_pubsub.erl @@ -14,15 +14,15 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_pubsub). +-module(emqx_pubsub). -behaviour(gen_server2). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). -export([start_link/3]). @@ -55,22 +55,23 @@ start_link(Pool, Id, Env) -> %%-------------------------------------------------------------------- %% @doc Subscribe a Topic --spec(subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok). +-spec(subscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> ok). subscribe(Topic, Subscriber, Options) -> call(pick(Topic), {subscribe, Topic, Subscriber, Options}). --spec(async_subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok). +-spec(async_subscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> ok). async_subscribe(Topic, Subscriber, Options) -> cast(pick(Topic), {subscribe, Topic, Subscriber, Options}). -%% @doc Publish MQTT Message to Topic +%% @doc Publish Message to a Topic -spec(publish(binary(), any()) -> {ok, mqtt_delivery()} | ignore). publish(Topic, Msg) -> - route(lists:append(emqttd_router:match(Topic), - emqttd_router:match_local(Topic)), delivery(Msg)). + route(lists:append(emqx_router:match(Topic), + emqx_router:match_local(Topic)), delivery(Msg)). -route([], #mqtt_delivery{message = #mqtt_message{topic = Topic}}) -> - dropped(Topic), ignore; +route([], #mqtt_delivery{message = Msg}) -> + emqx_hooks:run('message.offline', [undefined, Msg]), + dropped(Msg#mqtt_message.topic), ignore; %% Dispatch on the local node route([#mqtt_route{topic = To, node = Node}], @@ -90,13 +91,14 @@ delivery(Msg) -> #mqtt_delivery{sender = self(), message = Msg, flows = []}. %% @doc Forward message to another node... forward(Node, To, Delivery) -> - rpc:cast(Node, ?PUBSUB, dispatch, [To, Delivery]), {ok, Delivery}. + emqx_rpc:cast(Node, ?PUBSUB, dispatch, [To, Delivery]), {ok, Delivery}. %% @doc Dispatch Message to Subscribers -spec(dispatch(binary(), mqtt_delivery()) -> mqtt_delivery()). dispatch(Topic, Delivery = #mqtt_delivery{message = Msg, flows = Flows}) -> case subscribers(Topic) of [] -> + emqx_hooks:run('message.offline', [undefined, Msg]), dropped(Topic), {ok, Delivery}; [Sub] -> %% optimize? dispatch(Sub, Topic, Msg), @@ -110,7 +112,7 @@ dispatch(Topic, Delivery = #mqtt_delivery{message = Msg, flows = Flows}) -> dispatch(Pid, Topic, Msg) when is_pid(Pid) -> Pid ! {dispatch, Topic, Msg}; dispatch(SubId, Topic, Msg) when is_binary(SubId) -> - emqttd_sm:dispatch(SubId, Topic, Msg); + emqx_sm:dispatch(SubId, Topic, Msg); dispatch({_Share, [Sub]}, Topic, Msg) -> dispatch(Sub, Topic, Msg); dispatch({_Share, []}, _Topic, _Msg) -> @@ -138,14 +140,14 @@ group_by_share(Subscribers) -> dropped(<<"$SYS/", _/binary>>) -> ok; dropped(_Topic) -> - emqttd_metrics:inc('messages/dropped'). + emqx_metrics:inc('messages/dropped'). %% @doc Unsubscribe --spec(unsubscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok). +-spec(unsubscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> ok). unsubscribe(Topic, Subscriber, Options) -> call(pick(Topic), {unsubscribe, Topic, Subscriber, Options}). --spec(async_unsubscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok). +-spec(async_unsubscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> ok). async_unsubscribe(Topic, Subscriber, Options) -> cast(pick(Topic), {unsubscribe, Topic, Subscriber, Options}). @@ -210,11 +212,11 @@ add_subscriber(Topic, Subscriber, Options) -> end. add_subscriber_(Share, Topic, Subscriber) -> - (not ets:member(mqtt_subscriber, Topic)) andalso emqttd_router:add_route(Topic), + (not ets:member(mqtt_subscriber, Topic)) andalso emqx_router:add_route(Topic), ets:insert(mqtt_subscriber, {Topic, shared(Share, Subscriber)}). add_local_subscriber_(Share, Topic, Subscriber) -> - (not ets:member(mqtt_subscriber, {local, Topic})) andalso emqttd_router:add_local_route(Topic), + (not ets:member(mqtt_subscriber, {local, Topic})) andalso emqx_router:add_local_route(Topic), ets:insert(mqtt_subscriber, {{local, Topic}, shared(Share, Subscriber)}). del_subscriber(Topic, Subscriber, Options) -> @@ -226,11 +228,11 @@ del_subscriber(Topic, Subscriber, Options) -> del_subscriber_(Share, Topic, Subscriber) -> ets:delete_object(mqtt_subscriber, {Topic, shared(Share, Subscriber)}), - (not ets:member(mqtt_subscriber, Topic)) andalso emqttd_router:del_route(Topic). + (not ets:member(mqtt_subscriber, Topic)) andalso emqx_router:del_route(Topic). del_local_subscriber_(Share, Topic, Subscriber) -> ets:delete_object(mqtt_subscriber, {{local, Topic}, shared(Share, Subscriber)}), - (not ets:member(mqtt_subscriber, {local, Topic})) andalso emqttd_router:del_local_route(Topic). + (not ets:member(mqtt_subscriber, {local, Topic})) andalso emqx_router:del_local_route(Topic). shared(undefined, Subscriber) -> Subscriber; @@ -238,6 +240,6 @@ shared(Share, Subscriber) -> {Share, Subscriber}. setstats(State) -> - emqttd_stats:setstats('subscribers/count', 'subscribers/max', ets:info(mqtt_subscriber, size)), + emqx_stats:setstats('subscribers/count', 'subscribers/max', ets:info(mqtt_subscriber, size)), State. diff --git a/src/emqttd_pubsub_sup.erl b/src/emqx_pubsub_sup.erl similarity index 94% rename from src/emqttd_pubsub_sup.erl rename to src/emqx_pubsub_sup.erl index 09d08d110..c203ed5c3 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqx_pubsub_sup.erl @@ -15,7 +15,7 @@ %%-------------------------------------------------------------------- %% @doc PubSub Supervisor. --module(emqttd_pubsub_sup). +-module(emqx_pubsub_sup). -author("Feng Lee "). @@ -44,7 +44,7 @@ pubsub_pool() -> %%-------------------------------------------------------------------- init([]) -> - {ok, Env} = emqttd:env(pubsub), + {ok, Env} = emqx:env(pubsub), %% Create ETS Tables [create_tab(Tab) || Tab <- [mqtt_subproperty, mqtt_subscriber, mqtt_subscription]], {ok, { {one_for_all, 10, 3600}, [pool_sup(pubsub, Env), pool_sup(server, Env)]} }. @@ -59,9 +59,9 @@ pool_size(Env) -> pool_sup(Name, Env) -> Pool = list_to_atom(atom_to_list(Name) ++ "_pool"), - Mod = list_to_atom("emqttd_" ++ atom_to_list(Name)), + Mod = list_to_atom("emqx_" ++ atom_to_list(Name)), MFA = {Mod, start_link, [Env]}, - emqttd_pool_sup:spec(Pool, [Name, hash, pool_size(Env), MFA]). + emqx_pool_sup:spec(Pool, [Name, hash, pool_size(Env), MFA]). %%-------------------------------------------------------------------- %% Create PubSub Tables diff --git a/src/emqttd_rest_api.erl b/src/emqx_rest_api.erl similarity index 86% rename from src/emqttd_rest_api.erl rename to src/emqx_rest_api.erl index baa056b81..f405f1ce5 100644 --- a/src/emqttd_rest_api.erl +++ b/src/emqx_rest_api.erl @@ -13,11 +13,12 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module (emqttd_rest_api). --include("emqttd.hrl"). +-module (emqx_rest_api). --include("emqttd_internal.hrl"). +-include("emqx.hrl"). + +-include("emqx_rest.hrl"). -http_api({"^nodes/(.+?)/alarms/?$", 'GET', alarm_list, []}). @@ -94,7 +95,7 @@ %% alarm %%-------------------------------------------------------------------------- alarm_list('GET', _Req, _Node) -> - Alarms = emqttd_mgmt:alarm_list(), + Alarms = emqx_mgmt:alarm_list(), {ok, lists:map(fun alarm_row/1, Alarms)}. alarm_row(#mqtt_alarm{id = AlarmId, @@ -112,12 +113,12 @@ alarm_row(#mqtt_alarm{id = AlarmId, %% client %%-------------------------------------------------------------------------- client('GET', _Params, Key) -> - Data = emqttd_mgmt:client(l2b(Key)), + Data = emqx_mgmt:client(l2b(Key)), {ok, [{objects, [client_row(Row) || Row <- Data]}]}. client_list('GET', Params, Node) -> {PageNo, PageSize} = page_params(Params), - Data = emqttd_mgmt:client_list(l2a(Node), undefined, PageNo, PageSize), + Data = emqx_mgmt:client_list(l2a(Node), undefined, PageNo, PageSize), Rows = get_value(result, Data), TotalPage = get_value(totalPage, Data), TotalNum = get_value(totalNum, Data), @@ -129,11 +130,11 @@ client_list('GET', Params, Node) -> client_list('GET', Params, Node, Key) -> {PageNo, PageSize} = page_params(Params), - Data = emqttd_mgmt:client_list(l2a(Node), l2b(Key), PageNo, PageSize), + Data = emqx_mgmt:client_list(l2a(Node), l2b(Key), PageNo, PageSize), {ok, [{objects, [client_row(Row) || Row <- Data]}]}. kick_client('DELETE', _Params, Key) -> - case emqttd_mgmt:kick_client(l2b(Key)) of + case emqx_mgmt:kick_client(l2b(Key)) of true -> {ok, []}; false -> {error, [{code, ?ERROR12}]} end. @@ -141,7 +142,7 @@ kick_client('DELETE', _Params, Key) -> clean_acl_cache('PUT', Params, Key0) -> Topic = get_value(<<"topic">>, Params), [Key | _] = string:tokens(Key0, "/"), - case emqttd_mgmt:clean_acl_cache(l2b(Key), Topic) of + case emqx_mgmt:clean_acl_cache(l2b(Key), Topic) of true -> {ok, []}; false -> {error, [{code, ?ERROR12}]} end. @@ -166,12 +167,12 @@ client_row(#mqtt_client{client_id = ClientId, %% route %%-------------------------------------------------------------------------- route('GET', _Params, Key) -> - Data = emqttd_mgmt:route(l2b(Key)), + Data = emqx_mgmt:route(l2b(Key)), {ok, [{objects, [route_row(Row) || Row <- Data]}]}. route_list('GET', Params) -> {PageNo, PageSize} = page_params(Params), - Data = emqttd_mgmt:route_list(undefined, PageNo, PageSize), + Data = emqx_mgmt:route_list(undefined, PageNo, PageSize), Rows = get_value(result, Data), TotalPage = get_value(totalPage, Data), TotalNum = get_value(totalNum, Data), @@ -191,12 +192,12 @@ route_row({Topic, Node}) -> %% session %%-------------------------------------------------------------------------- session('GET', _Params, Key) -> - Data = emqttd_mgmt:session(l2b(Key)), + Data = emqx_mgmt:session(l2b(Key)), {ok, [{objects, [session_row(Row) || Row <- Data]}]}. session_list('GET', Params, Node) -> {PageNo, PageSize} = page_params(Params), - Data = emqttd_mgmt:session_list(l2a(Node), undefined, PageNo, PageSize), + Data = emqx_mgmt:session_list(l2a(Node), undefined, PageNo, PageSize), Rows = get_value(result, Data), TotalPage = get_value(totalPage, Data), TotalNum = get_value(totalNum, Data), @@ -208,7 +209,7 @@ session_list('GET', Params, Node) -> session_list('GET', Params, Node, ClientId) -> {PageNo, PageSize} = page_params(Params), - Data = emqttd_mgmt:session_list(l2a(Node), l2b(ClientId), PageNo, PageSize), + Data = emqx_mgmt:session_list(l2a(Node), l2b(ClientId), PageNo, PageSize), {ok, [{objects, [session_row(Row) || Row <- Data]}]}. session_row({ClientId, _Pid, _Persistent, Session}) -> @@ -220,12 +221,12 @@ session_row({ClientId, _Pid, _Persistent, Session}) -> %% subscription %%-------------------------------------------------------------------------- subscription('GET', _Params, Key) -> - Data = emqttd_mgmt:subscription(l2b(Key)), + Data = emqx_mgmt:subscription(l2b(Key)), {ok, [{objects, [subscription_row(Row) || Row <- Data]}]}. subscription_list('GET', Params, Node) -> {PageNo, PageSize} = page_params(Params), - Data = emqttd_mgmt:subscription_list(l2a(Node), undefined, PageNo, PageSize), + Data = emqx_mgmt:subscription_list(l2a(Node), undefined, PageNo, PageSize), Rows = get_value(result, Data), TotalPage = get_value(totalPage, Data), TotalNum = get_value(totalNum, Data), @@ -237,7 +238,7 @@ subscription_list('GET', Params, Node) -> subscription_list('GET', Params, Node, Key) -> {PageNo, PageSize} = page_params(Params), - Data = emqttd_mgmt:subscription_list(l2a(Node), l2b(Key), PageNo, PageSize), + Data = emqx_mgmt:subscription_list(l2a(Node), l2b(Key), PageNo, PageSize), {ok, [{objects, [subscription_row(Row) || Row <- Data]}]}. subscription_row({{Topic, ClientId}, Option}) when is_pid(ClientId) -> @@ -250,43 +251,43 @@ subscription_row({{Topic, ClientId}, Option}) -> %% management/monitoring %%-------------------------------------------------------------------------- nodes('GET', _Params) -> - Data = emqttd_mgmt:nodes_info(), + Data = emqx_mgmt:nodes_info(), {ok, Data}. node('GET', _Params, Node) -> - Data = emqttd_mgmt:node_info(l2a(Node)), + Data = emqx_mgmt:node_info(l2a(Node)), {ok, Data}. brokers('GET', _Params) -> - Data = emqttd_mgmt:brokers(), + Data = emqx_mgmt:brokers(), {ok, [format_broker(Node, Broker) || {Node, Broker} <- Data]}. broker('GET', _Params, Node) -> - Data = emqttd_mgmt:broker(l2a(Node)), + Data = emqx_mgmt:broker(l2a(Node)), {ok, format_broker(Data)}. listeners('GET', _Params) -> - Data = emqttd_mgmt:listeners(), - {ok, [[{Node, format_listeners(Listeners, [])} || {Node, Listeners} <- Data]]}. + Data = emqx_mgmt:listeners(), + {ok, [{Node, format_listeners(Listeners, [])} || {Node, Listeners} <- Data]}. listener('GET', _Params, Node) -> - Data = emqttd_mgmt:listener(l2a(Node)), + Data = emqx_mgmt:listener(l2a(Node)), {ok, [format_listener(Listeners) || Listeners <- Data]}. metrics('GET', _Params) -> - Data = emqttd_mgmt:metrics(), - {ok, [Data]}. + Data = emqx_mgmt:metrics(), + {ok, Data}. metric('GET', _Params, Node) -> - Data = emqttd_mgmt:metrics(l2a(Node)), + Data = emqx_mgmt:metrics(l2a(Node)), {ok, Data}. stats('GET', _Params) -> - Data = emqttd_mgmt:stats(), + Data = emqx_mgmt:stats(), {ok, [Data]}. stat('GET', _Params, Node) -> - Data = emqttd_mgmt:stats(l2a(Node)), + Data = emqx_mgmt:stats(l2a(Node)), {ok, Data}. format_broker(Node, Broker) -> @@ -321,12 +322,12 @@ format_listener({Protocol, ListenOn, Info}) -> %% mqtt %%-------------------------------------------------------------------------- publish('POST', Params) -> - Topic = get_value(<<"topic">>, Params), + Topic = get_value(<<"topic">>, Params), ClientId = get_value(<<"client_id">>, Params, http), - Payload = get_value(<<"payload">>, Params, <<>>), - Qos = get_value(<<"qos">>, Params, 0), - Retain = get_value(<<"retain">>, Params, false), - case emqttd_mgmt:publish({ClientId, Topic, Payload, Qos, Retain}) of + Payload = get_value(<<"payload">>, Params, <<>>), + Qos = get_value(<<"qos">>, Params, 0), + Retain = get_value(<<"retain">>, Params, false), + case emqx_mgmt:publish({ClientId, Topic, Payload, Qos, Retain}) of ok -> {ok, []}; {error, Error} -> @@ -337,7 +338,7 @@ subscribe('POST', Params) -> ClientId = get_value(<<"client_id">>, Params), Topic = get_value(<<"topic">>, Params), Qos = get_value(<<"qos">>, Params, 0), - case emqttd_mgmt:subscribe({ClientId, Topic, Qos}) of + case emqx_mgmt:subscribe({ClientId, Topic, Qos}) of ok -> {ok, []}; {error, Error} -> @@ -347,7 +348,7 @@ subscribe('POST', Params) -> unsubscribe('POST', Params) -> ClientId = get_value(<<"client_id">>, Params), Topic = get_value(<<"topic">>, Params), - case emqttd_mgmt:unsubscribe({ClientId, Topic})of + case emqx_mgmt:unsubscribe({ClientId, Topic})of ok -> {ok, []}; {error, Error} -> @@ -358,16 +359,16 @@ unsubscribe('POST', Params) -> %% plugins %%-------------------------------------------------------------------------- plugin_list('GET', _Params, Node) -> - Plugins = lists:map(fun plugin/1, emqttd_mgmt:plugin_list(l2a(Node))), + Plugins = lists:map(fun plugin/1, emqx_mgmt:plugin_list(l2a(Node))), {ok, Plugins}. enabled('PUT', Params, Node, PluginName) -> Active = get_value(<<"active">>, Params), case Active of true -> - return(emqttd_mgmt:plugin_load(l2a(Node), l2a(PluginName))); + return(emqx_mgmt:plugin_load(l2a(Node), l2a(PluginName))); false -> - return(emqttd_mgmt:plugin_unload(l2a(Node), l2a(PluginName))) + return(emqx_mgmt:plugin_unload(l2a(Node), l2a(PluginName))) end. return(Result) -> @@ -397,7 +398,7 @@ plugin(#mqtt_plugin{name = Name, version = Ver, descr = Descr, modify_config('PUT', Params, App) -> Key = get_value(<<"key">>, Params, <<"">>), Value = get_value(<<"value">>, Params, <<"">>), - case emqttd_mgmt:modify_config(l2a(App), b2l(Key), b2l(Value)) of + case emqx_mgmt:modify_config(l2a(App), b2l(Key), b2l(Value)) of true -> {ok, []}; false -> {error, [{code, ?ERROR2}]} end. @@ -405,26 +406,26 @@ modify_config('PUT', Params, App) -> modify_config('PUT', Params, Node, App) -> Key = get_value(<<"key">>, Params, <<"">>), Value = get_value(<<"value">>, Params, <<"">>), - case emqttd_mgmt:modify_config(l2a(Node), l2a(App), b2l(Key), b2l(Value)) of + case emqx_mgmt:modify_config(l2a(Node), l2a(App), b2l(Key), b2l(Value)) of ok -> {ok, []}; _ -> {error, [{code, ?ERROR2}]} end. config_list('GET', _Params) -> - Data = emqttd_mgmt:get_configs(), + Data = emqx_mgmt:get_configs(), {ok, [{Node, format_config(Config, [])} || {Node, Config} <- Data]}. config_list('GET', _Params, Node) -> - Data = emqttd_mgmt:get_config(l2a(Node)), + Data = emqx_mgmt:get_config(l2a(Node)), {ok, [format_config(Config) || Config <- lists:reverse(Data)]}. plugin_config_list('GET', _Params, Node, App) -> - {ok, Data} = emqttd_mgmt:get_plugin_config(l2a(Node), l2a(App)), + {ok, Data} = emqx_mgmt:get_plugin_config(l2a(Node), l2a(App)), {ok, [format_plugin_config(Config) || Config <- lists:reverse(Data)]}. modify_plugin_config('PUT', Params, Node, App) -> PluginName = l2a(App), - case emqttd_mgmt:modify_plugin_config(l2a(Node), PluginName, Params) of + case emqx_mgmt:modify_plugin_config(l2a(Node), PluginName, Params) of ok -> Plugins = emqttd_plugins:list(), {_, _, _, _, Status} = lists:keyfind(PluginName, 2, Plugins), @@ -465,7 +466,7 @@ format_plugin_config({Key, Value, Desc, Required}) -> auth('POST', Params) -> Username = get_value(<<"username">>, Params), Password = get_value(<<"password">>, Params), - case emqttd_mgmt:check_user(Username, Password) of + case emqx_mgmt:check_user(Username, Password) of ok -> {ok, []}; {error, Reason} -> @@ -476,26 +477,26 @@ users('POST', Params) -> Username = get_value(<<"username">>, Params), Password = get_value(<<"password">>, Params), Tag = get_value(<<"tags">>, Params), - code(emqttd_mgmt:add_user(Username, Password, Tag)); + code(emqx_mgmt:add_user(Username, Password, Tag)); users('GET', _Params) -> - {ok, [Admin || Admin <- emqttd_mgmt:user_list()]}. + {ok, [Admin || Admin <- emqx_mgmt:user_list()]}. users('GET', _Params, Username) -> - {ok, emqttd_mgmt:lookup_user(list_to_binary(Username))}; + {ok, emqx_mgmt:lookup_user(list_to_binary(Username))}; users('PUT', Params, Username) -> - code(emqttd_mgmt:update_user(list_to_binary(Username), Params)); + code(emqx_mgmt:update_user(list_to_binary(Username), Params)); users('DELETE', _Params, "admin") -> {error, [{code, ?ERROR6}, {message, <<"admin cannot be deleted">>}]}; users('DELETE', _Params, Username) -> - code(emqttd_mgmt:remove_user(list_to_binary(Username))). + code(emqx_mgmt:remove_user(list_to_binary(Username))). change_pwd('PUT', Params, Username) -> OldPwd = get_value(<<"old_pwd">>, Params), NewPwd = get_value(<<"new_pwd">>, Params), - code(emqttd_mgmt:change_password(list_to_binary(Username), OldPwd, NewPwd)). + code(emqx_mgmt:change_password(list_to_binary(Username), OldPwd, NewPwd)). code(ok) -> {ok, []}; code(error) -> {error, [{code, ?ERROR2}]}; diff --git a/src/emqttd_router.erl b/src/emqx_router.erl similarity index 94% rename from src/emqttd_router.erl rename to src/emqx_router.erl index b3dd8b4ad..b16d8af1f 100644 --- a/src/emqttd_router.erl +++ b/src/emqx_router.erl @@ -14,13 +14,13 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_router). +-module(emqx_router). -author("Feng Lee "). -behaviour(gen_server). --include("emqttd.hrl"). +-include("emqx.hrl"). %% Mnesia Bootstrap -export([mnesia/1]). @@ -89,7 +89,7 @@ local_topics() -> %% @doc Match Routes. -spec(match(Topic:: binary()) -> [mqtt_route()]). match(Topic) when is_binary(Topic) -> - Matched = mnesia:async_dirty(fun emqttd_trie:match/1, [Topic]), + Matched = mnesia:async_dirty(fun emqx_trie:match/1, [Topic]), %% Optimize: route table will be replicated to all nodes. lists:append([ets:lookup(mqtt_route, To) || To <- [Topic | Matched]]). @@ -123,8 +123,8 @@ add_routes(Routes) -> add_route_(Route = #mqtt_route{topic = Topic}) -> case mnesia:wread({mqtt_route, Topic}) of [] -> - case emqttd_topic:wildcard(Topic) of - true -> emqttd_trie:insert(Topic); + case emqx_topic:wildcard(Topic) of + true -> emqx_trie:insert(Topic); false -> ok end, mnesia:write(Route), @@ -163,8 +163,8 @@ del_route_(Route = #mqtt_route{topic = Topic}) -> [Route] -> %% Remove route and trie mnesia:delete_object(Route), - case emqttd_topic:wildcard(Topic) of - true -> emqttd_trie:delete(Topic); + case emqx_topic:wildcard(Topic) of + true -> emqx_trie:delete(Topic); false -> ok end, mnesia:delete({mqtt_topic, Topic}); @@ -206,7 +206,7 @@ del_local_route(Topic) -> match_local(Name) -> [#mqtt_route{topic = {local, Filter}, node = Node} || {Filter, Node} <- ets:tab2list(mqtt_local_route), - emqttd_topic:match(Name, Filter)]. + emqx_topic:match(Name, Filter)]. dump() -> [{route, ets:tab2list(mqtt_route)}, {local_route, ets:tab2list(mqtt_local_route)}]. @@ -281,5 +281,5 @@ clean_routes_(Node) -> mnesia:transaction(Clean). update_stats_() -> - emqttd_stats:setstats('routes/count', 'routes/max', mnesia:table_info(mqtt_route, size)). + emqx_stats:setstats('routes/count', 'routes/max', mnesia:table_info(mqtt_route, size)). diff --git a/src/emqx_rpc.erl b/src/emqx_rpc.erl new file mode 100644 index 000000000..17525c065 --- /dev/null +++ b/src/emqx_rpc.erl @@ -0,0 +1,17 @@ +%% +%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. All Rights Reserved. +%% +%% @doc EMQ X Distributed RPC. +%% + +-module(emqx_rpc). + +-author("Feng Lee "). + +-export([cast/4]). + +%% @doc Wraps gen_rpc first. +cast(Node, Mod, Fun, Args) -> + emqx_metrics:inc('messages/forward'), + gen_rpc:cast({Node, erlang:system_info(scheduler_id)}, Mod, Fun, Args). + diff --git a/src/emqttd_serializer.erl b/src/emqx_serializer.erl similarity index 96% rename from src/emqttd_serializer.erl rename to src/emqx_serializer.erl index 079cfbb3c..ef6aaf15c 100644 --- a/src/emqttd_serializer.erl +++ b/src/emqx_serializer.erl @@ -15,13 +15,13 @@ %%-------------------------------------------------------------------- %% @doc MQTT Packet Serializer --module(emqttd_serializer). +-module(emqx_serializer). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). %% API -export([serialize/1]). @@ -46,18 +46,18 @@ serialize_header(#mqtt_packet_header{type = Type, [<>, serialize_len(Len), VariableBin, PayloadBin]. -serialize_variable(?CONNECT, #mqtt_packet_connect{client_id = ClientId, - proto_ver = ProtoVer, - proto_name = ProtoName, - will_retain = WillRetain, - will_qos = WillQos, - will_flag = WillFlag, - clean_sess = CleanSess, - keep_alive = KeepAlive, - will_topic = WillTopic, - will_msg = WillMsg, - username = Username, - password = Password}, undefined) -> +serialize_variable(?CONNECT, #mqtt_packet_connect{client_id = ClientId, + proto_ver = ProtoVer, + proto_name = ProtoName, + will_retain = WillRetain, + will_qos = WillQos, + will_flag = WillFlag, + clean_sess = CleanSess, + keep_alive = KeepAlive, + will_topic = WillTopic, + will_msg = WillMsg, + username = Username, + password = Password}, undefined) -> VariableBin = <<(byte_size(ProtoName)):16/big-unsigned-integer, ProtoName/binary, ProtoVer:8, diff --git a/src/emqttd_server.erl b/src/emqx_server.erl similarity index 82% rename from src/emqttd_server.erl rename to src/emqx_server.erl index 69d18e1e4..0ffc99eb6 100644 --- a/src/emqttd_server.erl +++ b/src/emqx_server.erl @@ -14,17 +14,17 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_server). +-module(emqx_server). -behaviour(gen_server2). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). -export([start_link/3]). @@ -47,9 +47,9 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {pool, id, env, submon :: emqttd_pmon:pmon()}). +-record(state, { pool, id, env, submon :: emqx_pmon:pmon() }). -%% @doc Start server +%% @doc Start a Server -spec(start_link(atom(), pos_integer(), list()) -> {ok, pid()} | ignore | {error, any()}). start_link(Pool, Id, Env) -> gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). @@ -59,16 +59,16 @@ start_link(Pool, Id, Env) -> %%-------------------------------------------------------------------- %% @doc Subscribe a Topic --spec(subscribe(binary()) -> ok | emqttd:pubsub_error()). +-spec(subscribe(binary()) -> ok | {error, any()}). subscribe(Topic) when is_binary(Topic) -> subscribe(Topic, self()). --spec(subscribe(binary(), emqttd:subscriber()) -> ok | emqttd:pubsub_error()). +-spec(subscribe(binary(), emqx:subscriber()) -> ok | {error, any()}). subscribe(Topic, Subscriber) when is_binary(Topic) -> subscribe(Topic, Subscriber, []). --spec(subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> - ok | emqttd:pubsub_error()). +-spec(subscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> + ok | {error, any()}). subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> call(pick(Subscriber), {subscribe, Topic, Subscriber, Options}). @@ -77,23 +77,23 @@ subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> async_subscribe(Topic) when is_binary(Topic) -> async_subscribe(Topic, self()). --spec(async_subscribe(binary(), emqttd:subscriber()) -> ok). +-spec(async_subscribe(binary(), emqx:subscriber()) -> ok). async_subscribe(Topic, Subscriber) when is_binary(Topic) -> async_subscribe(Topic, Subscriber, []). --spec(async_subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok). +-spec(async_subscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> ok). async_subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> cast(pick(Subscriber), {subscribe, Topic, Subscriber, Options}). -%% @doc Publish message to Topic. +%% @doc Publish a message -spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore). publish(Msg = #mqtt_message{from = From}) -> trace(publish, From, Msg), - case emqttd_hooks:run('message.publish', [], Msg) of + case emqx_hooks:run('message.publish', [], Msg) of {ok, Msg1 = #mqtt_message{topic = Topic}} -> - emqttd_pubsub:publish(Topic, Msg1); + emqx_pubsub:publish(Topic, Msg1); {stop, Msg1} -> - lager:warning("Stop publishing: ~s", [emqttd_message:format(Msg1)]), + lager:warning("Stop publishing: ~s", [emqx_message:format(Msg1)]), ignore end. @@ -102,19 +102,19 @@ trace(publish, From, _Msg) when is_atom(From) -> %% Dont' trace '$SYS' publish ignore; trace(publish, {ClientId, Username}, #mqtt_message{topic = Topic, payload = Payload}) -> - lager:debug([{client, ClientId}, {topic, Topic}], - "~s/~s PUBLISH to ~s: ~p", [ClientId, Username, Topic, Payload]); -trace(publish, From, #mqtt_message{topic = Topic, payload = Payload}) -> - lager:debug([{client, From}, {topic, Topic}], - "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). + lager:info([{client, ClientId}, {topic, Topic}], + "~s/~s PUBLISH to ~s: ~p", [ClientId, Username, Topic, Payload]); +trace(publish, From, #mqtt_message{topic = Topic, payload = Payload}) when is_binary(From); is_list(From) -> + lager:info([{client, From}, {topic, Topic}], + "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). %% @doc Unsubscribe --spec(unsubscribe(binary()) -> ok | emqttd:pubsub_error()). +-spec(unsubscribe(binary()) -> ok | {error, any()}). unsubscribe(Topic) when is_binary(Topic) -> unsubscribe(Topic, self()). %% @doc Unsubscribe --spec(unsubscribe(binary(), emqttd:subscriber()) -> ok | emqttd:pubsub_error()). +-spec(unsubscribe(binary(), emqx:subscriber()) -> ok | {error, any()}). unsubscribe(Topic, Subscriber) when is_binary(Topic) -> call(pick(Subscriber), {unsubscribe, Topic, Subscriber}). @@ -123,14 +123,14 @@ unsubscribe(Topic, Subscriber) when is_binary(Topic) -> async_unsubscribe(Topic) when is_binary(Topic) -> async_unsubscribe(Topic, self()). --spec(async_unsubscribe(binary(), emqttd:subscriber()) -> ok). +-spec(async_unsubscribe(binary(), emqx:subscriber()) -> ok). async_unsubscribe(Topic, Subscriber) when is_binary(Topic) -> cast(pick(Subscriber), {unsubscribe, Topic, Subscriber}). setqos(Topic, Subscriber, Qos) when is_binary(Topic) -> call(pick(Subscriber), {setqos, Topic, Subscriber, Qos}). --spec(subscriptions(emqttd:subscriber()) -> [{binary(), binary(), list(emqttd:suboption())}]). +-spec(subscriptions(emqx:subscriber()) -> [{binary(), list(emqx:suboption())}]). subscriptions(Subscriber) -> lists:map(fun({_, {_Share, Topic}}) -> subscription(Topic, Subscriber); @@ -142,13 +142,13 @@ subscription(Topic, Subscriber) -> {Topic, Subscriber, ets:lookup_element(mqtt_subproperty, {Topic, Subscriber}, 2)}. subscribers(Topic) -> - emqttd_pubsub:subscribers(Topic). + emqx_pubsub:subscribers(Topic). --spec(is_subscribed(binary(), emqttd:subscriber()) -> boolean()). +-spec(is_subscribed(binary(), emqx:subscriber()) -> boolean()). is_subscribed(Topic, Subscriber) when is_binary(Topic) -> ets:member(mqtt_subproperty, {Topic, Subscriber}). --spec(subscriber_down(emqttd:subscriber()) -> ok). +-spec(subscriber_down(emqx:subscriber()) -> ok). subscriber_down(Subscriber) -> cast(pick(Subscriber), {subscriber_down, Subscriber}). @@ -170,7 +170,7 @@ dump() -> init([Pool, Id, Env]) -> ?GPROC_POOL(join, Pool, Id), - {ok, #state{pool = Pool, id = Id, env = Env, submon = emqttd_pmon:new()}}. + {ok, #state{pool = Pool, id = Id, env = Env, submon = emqx_pmon:new()}}. handle_call({subscribe, Topic, Subscriber, Options}, _From, State) -> case do_subscribe_(Topic, Subscriber, Options, State) of @@ -237,7 +237,7 @@ code_change(_OldVsn, State, _Extra) -> do_subscribe_(Topic, Subscriber, Options, State) -> case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of [] -> - emqttd_pubsub:async_subscribe(Topic, Subscriber, Options), + emqx_pubsub:async_subscribe(Topic, Subscriber, Options), Share = proplists:get_value(share, Options), add_subscription_(Share, Subscriber, Topic), ets:insert(mqtt_subproperty, {{Topic, Subscriber}, Options}), @@ -259,7 +259,7 @@ monitor_subpid(_SubPid, State) -> do_unsubscribe_(Topic, Subscriber, State) -> case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of [{_, Options}] -> - emqttd_pubsub:async_unsubscribe(Topic, Subscriber, Options), + emqx_pubsub:async_unsubscribe(Topic, Subscriber, Options), Share = proplists:get_value(share, Options), del_subscription_(Share, Subscriber, Topic), ets:delete(mqtt_subproperty, {Topic, Subscriber}), @@ -294,13 +294,13 @@ subscriber_down_(Share, Subscriber, Topic) -> [] -> %% TODO:....??? Options = if Share == undefined -> []; true -> [{share, Share}] end, - emqttd_pubsub:async_unsubscribe(Topic, Subscriber, Options); + emqx_pubsub:async_unsubscribe(Topic, Subscriber, Options); [{_, Options}] -> - emqttd_pubsub:async_unsubscribe(Topic, Subscriber, Options), + emqx_pubsub:async_unsubscribe(Topic, Subscriber, Options), ets:delete(mqtt_subproperty, {Topic, Subscriber}) end. setstats(State) -> - emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', - ets:info(mqtt_subscription, size)), State. + emqx_stats:setstats('subscriptions/count', 'subscriptions/max', + ets:info(mqtt_subscription, size)), State. diff --git a/src/emqttd_session.erl b/src/emqx_session.erl similarity index 86% rename from src/emqttd_session.erl rename to src/emqx_session.erl index e8e694530..c46f50b97 100644 --- a/src/emqttd_session.erl +++ b/src/emqx_session.erl @@ -43,19 +43,19 @@ %% @end %% --module(emqttd_session). +-module(emqx_session). -behaviour(gen_server2). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). --import(emqttd_misc, [start_timer/2]). +-import(emqx_misc, [start_timer/2]). -import(proplists, [get_value/2, get_value/3]). @@ -77,7 +77,7 @@ -export([prioritise_call/4, prioritise_cast/3, prioritise_info/3, handle_pre_hibernate/1]). --define(MQueue, emqttd_mqueue). +-define(MQueue, emqx_mqueue). -record(state, { @@ -111,7 +111,7 @@ upgrade_qos = false :: boolean(), %% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked. - inflight :: emqttd_inflight:inflight(), + inflight :: emqx_inflight:inflight(), %% Max Inflight Size max_inflight = 32 :: non_neg_integer(), @@ -152,9 +152,10 @@ %% Force GC Count force_gc_count :: undefined | integer(), - created_at :: erlang:timestamp(), + %% Ignore loop deliver? + ignore_loop_deliver = false :: boolean(), - ignore_loop_deliver = false :: boolean() + created_at :: erlang:timestamp() }). -define(TIMEOUT, 60000). @@ -181,11 +182,11 @@ start_link(CleanSess, {ClientId, Username}, ClientPid) -> %%-------------------------------------------------------------------- %% @doc Subscribe topics --spec(subscribe(pid(), [{binary(), [emqttd_topic:option()]}]) -> ok). -subscribe(Session, TopicTable) -> %%TODO: the ack function??... +-spec(subscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok). +subscribe(Session, TopicTable) ->%%TODO: the ack function??... gen_server2:cast(Session, {subscribe, self(), TopicTable, fun(_) -> ok end}). --spec(subscribe(pid(), mqtt_packet_id(), [{binary(), [emqttd_topic:option()]}]) -> ok). +-spec(subscribe(pid(), mqtt_packet_id(), [{binary(), [emqx_topic:option()]}]) -> ok). subscribe(Session, PacketId, TopicTable) -> %%TODO: the ack function??... From = self(), AckFun = fun(GrantedQos) -> From ! {suback, PacketId, GrantedQos} end, @@ -195,11 +196,11 @@ subscribe(Session, PacketId, TopicTable) -> %%TODO: the ack function??... -spec(publish(pid(), mqtt_message()) -> ok | {error, any()}). publish(_Session, Msg = #mqtt_message{qos = ?QOS_0}) -> %% Publish QoS0 Directly - emqttd_server:publish(Msg), ok; + emqx_server:publish(Msg), ok; publish(_Session, Msg = #mqtt_message{qos = ?QOS_1}) -> %% Publish QoS1 message directly for client will PubAck automatically - emqttd_server:publish(Msg), ok; + emqx_server:publish(Msg), ok; publish(Session, Msg = #mqtt_message{qos = ?QOS_2}) -> %% Publish QoS2 to Session @@ -223,7 +224,7 @@ pubcomp(Session, PacketId) -> gen_server2:cast(Session, {pubcomp, PacketId}). %% @doc Unsubscribe the topics --spec(unsubscribe(pid(), [{binary(), [emqttd_topic:option()]}]) -> ok). +-spec(unsubscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok). unsubscribe(Session, TopicTable) -> gen_server2:cast(Session, {unsubscribe, self(), TopicTable}). @@ -255,7 +256,7 @@ stats(#state{max_subscriptions = MaxSubscriptions, mqueue = MQueue, max_awaiting_rel = MaxAwaitingRel, awaiting_rel = AwaitingRel}) -> - lists:append(emqttd_misc:proc_stats(), + lists:append(emqx_misc:proc_stats(), [{max_subscriptions, MaxSubscriptions}, {subscriptions, maps:size(Subscriptions)}, {max_inflight, MaxInflight}, @@ -281,13 +282,13 @@ init([CleanSess, {ClientId, Username}, ClientPid]) -> process_flag(trap_exit, true), true = link(ClientPid), init_stats([deliver_msg, enqueue_msg]), - {ok, Env} = emqttd:env(session), - {ok, QEnv} = emqttd:env(mqueue), + {ok, Env} = emqx:env(session), + {ok, QEnv} = emqx:env(mqueue), MaxInflight = get_value(max_inflight, Env, 0), EnableStats = get_value(enable_stats, Env, false), + ForceGcCount = emqx_gc:conn_max_gc_count(), IgnoreLoopDeliver = get_value(ignore_loop_deliver, Env, false), - ForceGcCount = emqttd_gc:conn_max_gc_count(), - MQueue = ?MQueue:new(ClientId, QEnv, emqttd_alarm:alarm_fun()), + MQueue = ?MQueue:new(ClientId, QEnv, emqx_alarm:alarm_fun()), State = #state{clean_sess = CleanSess, binding = binding(ClientPid), client_id = ClientId, @@ -297,7 +298,7 @@ init([CleanSess, {ClientId, Username}, ClientPid]) -> max_subscriptions = get_value(max_subscriptions, Env, 0), upgrade_qos = get_value(upgrade_qos, Env, false), max_inflight = MaxInflight, - inflight = emqttd_inflight:new(MaxInflight), + inflight = emqx_inflight:new(MaxInflight), mqueue = MQueue, retry_interval = get_value(retry_interval, Env), awaiting_rel = #{}, @@ -306,10 +307,10 @@ init([CleanSess, {ClientId, Username}, ClientPid]) -> expiry_interval = get_value(expiry_interval, Env), enable_stats = EnableStats, force_gc_count = ForceGcCount, - created_at = os:timestamp(), - ignore_loop_deliver = IgnoreLoopDeliver}, - emqttd_sm:register_session(ClientId, CleanSess, info(State)), - emqttd_hooks:run('session.created', [ClientId, Username]), + ignore_loop_deliver = IgnoreLoopDeliver, + created_at = os:timestamp()}, + emqx_sm:register_session(ClientId, CleanSess, info(State)), + emqx_hooks:run('session.created', [ClientId, Username]), {ok, emit_stats(State), hibernate, {backoff, 1000, 1000, 10000}}. init_stats(Keys) -> @@ -343,7 +344,7 @@ prioritise_info(Msg, _Len, _State) -> end. handle_pre_hibernate(State) -> - {hibernate, emqttd_gc:reset_conn_gc_count(#state.force_gc_count, emit_stats(State))}. + {hibernate, emqx_gc:reset_conn_gc_count(#state.force_gc_count, emit_stats(State))}. handle_call({publish, Msg = #mqtt_message{qos = ?QOS_2, pktid = PacketId}}, _From, State = #state{awaiting_rel = AwaitingRel, @@ -358,7 +359,7 @@ handle_call({publish, Msg = #mqtt_message{qos = ?QOS_2, pktid = PacketId}}, _Fro reply(ok, State1#state{awaiting_rel = maps:put(PacketId, Msg, AwaitingRel)}); true -> ?LOG(warning, "Dropped Qos2 Message for too many awaiting_rel: ~p", [Msg], State), - emqttd_metrics:inc('messages/qos2/dropped'), + emqx_metrics:inc('messages/qos2/dropped'), reply({error, dropped}, State) end; @@ -374,27 +375,31 @@ handle_call(state, _From, State) -> handle_call(Req, _From, State) -> ?UNEXPECTED_REQ(Req, State). -handle_cast({subscribe, _From, TopicTable, AckFun}, +handle_cast({subscribe, From, TopicTable, AckFun}, State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> - ?LOG(debug, "Subscribe ~p", [TopicTable], State), + ?LOG(info, "Subscribe ~p", [TopicTable], State), {GrantedQos, Subscriptions1} = lists:foldl(fun({Topic, Opts}, {QosAcc, SubMap}) -> - NewQos = proplists:get_value(qos, Opts), + Fastlane = lists:member(fastlane, Opts), + NewQos = if Fastlane == true -> ?QOS_0; true -> get_value(qos, Opts) end, SubMap1 = case maps:find(Topic, SubMap) of {ok, NewQos} -> ?LOG(warning, "Duplicated subscribe: ~s, qos = ~w", [Topic, NewQos], State), SubMap; {ok, OldQos} -> - emqttd:setqos(Topic, ClientId, NewQos), + emqx_server:setqos(Topic, ClientId, NewQos), ?LOG(warning, "Duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, NewQos], State), maps:put(Topic, NewQos, SubMap); error -> - emqttd:subscribe(Topic, ClientId, Opts), - emqttd_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), + case Fastlane of + true -> emqx:subscribe(Topic, From, Opts); + false -> emqx:subscribe(Topic, ClientId, Opts) + end, + emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), maps:put(Topic, NewQos, SubMap) end, {[NewQos|QosAcc], SubMap1} @@ -402,17 +407,21 @@ handle_cast({subscribe, _From, TopicTable, AckFun}, AckFun(lists:reverse(GrantedQos)), hibernate(emit_stats(State#state{subscriptions = Subscriptions1})); -handle_cast({unsubscribe, _From, TopicTable}, +handle_cast({unsubscribe, From, TopicTable}, State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> - ?LOG(debug, "Unsubscribe ~p", [TopicTable], State), + ?LOG(info, "Unsubscribe ~p", [TopicTable], State), Subscriptions1 = lists:foldl(fun({Topic, Opts}, SubMap) -> + Fastlane = lists:member(fastlane, Opts), case maps:find(Topic, SubMap) of {ok, _Qos} -> - emqttd:unsubscribe(Topic, ClientId), - emqttd_hooks:run('session.unsubscribed', [ClientId, Username], {Topic, Opts}), + case Fastlane of + true -> emqx:unsubscribe(Topic, From); + false -> emqx:unsubscribe(Topic, ClientId) + end, + emqx_hooks:run('session.unsubscribed', [ClientId, Username], {Topic, Opts}), maps:remove(Topic, SubMap); error -> SubMap @@ -429,7 +438,7 @@ handle_cast({puback, PacketId}, State = #state{inflight = Inflight}) -> false -> ?LOG(warning, "PUBACK ~p missed inflight: ~p", [PacketId, Inflight:window()], State), - emqttd_metrics:inc('packets/puback/missed'), + emqx_metrics:inc('packets/puback/missed'), State end, hibernate}; @@ -442,7 +451,7 @@ handle_cast({pubrec, PacketId}, State = #state{inflight = Inflight}) -> false -> ?LOG(warning, "PUBREC ~p missed inflight: ~p", [PacketId, Inflight:window()], State), - emqttd_metrics:inc('packets/pubrec/missed'), + emqx_metrics:inc('packets/pubrec/missed'), State end, hibernate}; @@ -451,11 +460,12 @@ handle_cast({pubrel, PacketId}, State = #state{awaiting_rel = AwaitingRel}) -> {noreply, case maps:take(PacketId, AwaitingRel) of {Msg, AwaitingRel1} -> - spawn(emqttd_server, publish, [Msg]), %%:) + %% TODO: woker pool to publish the qos2 messages? + spawn(emqx_server, publish, [Msg]), %%:) gc(State#state{awaiting_rel = AwaitingRel1}); error -> ?LOG(warning, "Cannot find PUBREL: ~p", [PacketId], State), - emqttd_metrics:inc('packets/pubrel/missed'), + emqx_metrics:inc('packets/pubrel/missed'), State end, hibernate}; @@ -468,7 +478,7 @@ handle_cast({pubcomp, PacketId}, State = #state{inflight = Inflight}) -> false -> ?LOG(warning, "The PUBCOMP ~p is not inflight: ~p", [PacketId, Inflight:window()], State), - emqttd_metrics:inc('packets/pubcomp/missed'), + emqx_metrics:inc('packets/pubcomp/missed'), State end, hibernate}; @@ -481,10 +491,10 @@ handle_cast({resume, ClientId, ClientPid}, await_rel_timer = AwaitTimer, expiry_timer = ExpireTimer}) -> - ?LOG(debug, "Resumed by ~p", [ClientPid], State), + ?LOG(info, "Resumed by ~p", [ClientPid], State), %% Cancel Timers - lists:foreach(fun emqttd_misc:cancel_timer/1, + lists:foreach(fun emqx_misc:cancel_timer/1, [RetryTimer, AwaitTimer, ExpireTimer]), case kick(ClientId, OldClientPid, ClientPid) of @@ -507,7 +517,7 @@ handle_cast({resume, ClientId, ClientPid}, if CleanSess =:= true -> ?LOG(error, "CleanSess changed to false.", [], State1), - emqttd_sm:register_session(ClientId, false, info(State1)); + emqx_sm:register_session(ClientId, false, info(State1)); CleanSess =:= false -> ok end, @@ -528,17 +538,14 @@ handle_cast({destroy, ClientId}, handle_cast(Msg, State) -> ?UNEXPECTED_MSG(Msg, State). -%% Dispatch message from self publish -handle_info({dispatch, Topic, Msg = #mqtt_message{from = {ClientId, _}}}, - State = #state{client_id = ClientId, - ignore_loop_deliver = IgnoreLoopDeliver}) when is_record(Msg, mqtt_message) -> - case IgnoreLoopDeliver of - true -> {noreply, State, hibernate}; - false -> {noreply, gc(dispatch(tune_qos(Topic, Msg, State), State)), hibernate} - end; +%% Ignore Messages delivered by self +handle_info({dispatch, _Topic, #mqtt_message{from = {ClientId, _}}}, + State = #state{client_id = ClientId, ignore_loop_deliver = true}) -> + hibernate(State); + %% Dispatch Message handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, mqtt_message) -> - {noreply, gc(dispatch(tune_qos(Topic, Msg, State), State)), hibernate}; + hibernate(gc(dispatch(tune_qos(Topic, Msg, State), State))); %% Do nothing if the client has been disconnected. handle_info({timeout, _Timer, retry_delivery}, State = #state{client_pid = undefined}) -> @@ -551,7 +558,7 @@ handle_info({timeout, _Timer, check_awaiting_rel}, State) -> hibernate(expire_awaiting_rel(emit_stats(State#state{await_rel_timer = undefined}))); handle_info({timeout, _Timer, expired}, State) -> - ?LOG(debug, "Expired, shutdown now.", [], State), + ?LOG(info, "Expired, shutdown now.", [], State), shutdown(expired, State); handle_info({'EXIT', ClientPid, _Reason}, @@ -562,7 +569,7 @@ handle_info({'EXIT', ClientPid, Reason}, State = #state{clean_sess = false, client_pid = ClientPid, expiry_interval = Interval}) -> - ?LOG(debug, "Client ~p EXIT for ~p", [ClientPid, Reason], State), + ?LOG(info, "Client ~p EXIT for ~p", [ClientPid, Reason], State), ExpireTimer = start_timer(Interval, expired), State1 = State#state{client_pid = undefined, expiry_timer = ExpireTimer}, hibernate(emit_stats(State1)); @@ -581,10 +588,10 @@ handle_info(Info, Session) -> ?UNEXPECTED_INFO(Info, Session). terminate(Reason, #state{client_id = ClientId, username = Username}) -> - emqttd_stats:del_session_stats(ClientId), - emqttd_hooks:run('session.terminated', [ClientId, Username, Reason]), - emqttd_server:subscriber_down(ClientId), - emqttd_sm:unregister_session(ClientId). + emqx_stats:del_session_stats(ClientId), + emqx_hooks:run('session.terminated', [ClientId, Username, Reason]), + emqx_server:subscriber_down(ClientId), + emqx_sm:unregister_session(ClientId). code_change(_OldVsn, Session, _Extra) -> {ok, Session}. @@ -687,8 +694,11 @@ is_awaiting_full(#state{awaiting_rel = AwaitingRel, max_awaiting_rel = MaxLen}) %%-------------------------------------------------------------------- %% Enqueue message if the client has been disconnected -dispatch(Msg, State = #state{client_pid = undefined}) -> - enqueue_msg(Msg, State); +dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) -> + case emqx_hooks:run('message.offline', [ClientId, Msg]) of + ok -> enqueue_msg(Msg, State); + stop -> State + end; %% Deliver qos0 message directly to client dispatch(Msg = #mqtt_message{qos = ?QOS0}, State) -> @@ -717,9 +727,10 @@ enqueue_msg(Msg, State = #state{mqueue = Q}) -> redeliver(Msg = #mqtt_message{qos = QoS}, State) -> deliver(Msg#mqtt_message{dup = if QoS =:= ?QOS2 -> false; true -> true end}, State). -deliver(Msg, #state{client_pid = Pid}) -> - inc_stats(deliver_msg), - Pid ! {deliver, Msg}. +deliver(Msg, #state{client_pid = Pid, binding = local}) -> + inc_stats(deliver_msg), Pid ! {deliver, Msg}; +deliver(Msg, #state{client_pid = Pid, binding = remote}) -> + inc_stats(deliver_msg), emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, Msg}]). %%-------------------------------------------------------------------- %% Awaiting ACK for QoS1/QoS2 Messages @@ -738,7 +749,7 @@ acked(puback, PacketId, State = #state{client_id = ClientId, inflight = Inflight}) -> case Inflight:lookup(PacketId) of {publish, Msg, _Ts} -> - emqttd_hooks:run('message.acked', [ClientId, Username], Msg), + emqx_hooks:run('message.acked', [ClientId, Username], Msg), State#state{inflight = Inflight:delete(PacketId)}; _ -> ?LOG(warning, "Duplicated PUBACK Packet: ~p", [PacketId], State), @@ -750,7 +761,7 @@ acked(pubrec, PacketId, State = #state{client_id = ClientId, inflight = Inflight}) -> case Inflight:lookup(PacketId) of {publish, Msg, _Ts} -> - emqttd_hooks:run('message.acked', [ClientId, Username], Msg), + emqx_hooks:run('message.acked', [ClientId, Username], Msg), State#state{inflight = Inflight:update(PacketId, {pubrel, PacketId, os:timestamp()})}; {pubrel, PacketId, _Ts} -> ?LOG(warning, "Duplicated PUBREC Packet: ~p", [PacketId], State), @@ -817,7 +828,7 @@ next_msg_id(State = #state{next_msg_id = Id}) -> emit_stats(State = #state{enable_stats = false}) -> State; emit_stats(State = #state{client_id = ClientId}) -> - emqttd_stats:set_session_stats(ClientId, stats(State)), + emqx_stats:set_session_stats(ClientId, stats(State)), State. inc_stats(Key) -> put(Key, get(Key) + 1). @@ -836,5 +847,5 @@ shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. gc(State) -> - emqttd_gc:maybe_force_gc(#state.force_gc_count, State). + emqx_gc:maybe_force_gc(#state.force_gc_count, State). diff --git a/src/emqttd_session_sup.erl b/src/emqx_session_sup.erl similarity index 91% rename from src/emqttd_session_sup.erl rename to src/emqx_session_sup.erl index bd9b34f02..8784ca396 100644 --- a/src/emqttd_session_sup.erl +++ b/src/emqx_session_sup.erl @@ -15,7 +15,7 @@ %%-------------------------------------------------------------------- %% @doc Session Supervisor. --module(emqttd_session_sup). +-module(emqx_session_sup). -author("Feng Lee "). @@ -41,5 +41,6 @@ start_session(CleanSess, {ClientId, Username}, ClientPid) -> init([]) -> {ok, {{simple_one_for_one, 0, 1}, - [{session, {emqttd_session, start_link, []}, - temporary, 5000, worker, [emqttd_session]}]}}. + [{session, {emqx_session, start_link, []}, + temporary, 5000, worker, [emqx_session]}]}}. + diff --git a/src/emqttd_sm.erl b/src/emqx_sm.erl similarity index 94% rename from src/emqttd_sm.erl rename to src/emqx_sm.erl index dacb0b6a1..1b652d600 100644 --- a/src/emqttd_sm.erl +++ b/src/emqx_sm.erl @@ -14,15 +14,15 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_sm). +-module(emqx_sm). -author("Feng Lee "). -behaviour(gen_server2). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). %% Mnesia Callbacks -export([mnesia/1]). @@ -116,7 +116,9 @@ dispatch(ClientId, Topic, Msg) -> try ets:lookup_element(mqtt_local_session, ClientId, 2) of Pid -> Pid ! {dispatch, Topic, Msg} catch - error:badarg -> ok %%FIXME Later. + error:badarg -> + emqx_hooks:run('message.offline', [ClientId, Msg]), + ok %%TODO: How?? end. call(SM, Req) -> @@ -187,7 +189,7 @@ handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> [] -> ok; [Sess = #mqtt_session{sess_pid = DownPid}] -> - emqttd_stats:del_session_stats(ClientId), + emqx_stats:del_session_stats(ClientId), mnesia:delete_object(mqtt_session, Sess, write); [_Sess] -> ok @@ -223,7 +225,7 @@ create_session({CleanSess, {ClientId, Username}, ClientPid}, State) -> end. create_session(CleanSess, {ClientId, Username}, ClientPid) -> - case emqttd_session_sup:start_session(CleanSess, {ClientId, Username}, ClientPid) of + case emqx_session_sup:start_session(CleanSess, {ClientId, Username}, ClientPid) of {ok, SessPid} -> Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid, clean_sess = CleanSess}, case insert_session(Session) of @@ -255,7 +257,7 @@ resume_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid} case is_process_alive(SessPid) of true -> - emqttd_session:resume(SessPid, ClientId, ClientPid), + emqx_session:resume(SessPid, ClientId, ClientPid), {ok, SessPid}; false -> ?LOG(error, "Cannot resume ~p which seems already dead!", [SessPid], Session), @@ -265,7 +267,7 @@ resume_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid} %% Remote node resume_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid}, ClientPid) -> Node = node(SessPid), - case rpc:call(Node, emqttd_session, resume, [SessPid, ClientId, ClientPid]) of + case rpc:call(Node, emqx_session, resume, [SessPid, ClientId, ClientPid]) of ok -> {ok, SessPid}; {badrpc, nodedown} -> @@ -278,16 +280,16 @@ resume_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid} end. %% Local node -destroy_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid}) +destroy_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid}) when node(SessPid) =:= node() -> - emqttd_session:destroy(SessPid, ClientId), + emqx_session:destroy(SessPid, ClientId), remove_session(Session); %% Remote node destroy_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid}) -> Node = node(SessPid), - case rpc:call(Node, emqttd_session, destroy, [SessPid, ClientId]) of + case rpc:call(Node, emqx_session, destroy, [SessPid, ClientId]) of ok -> remove_session(Session); {badrpc, nodedown} -> diff --git a/src/emqttd_sm_helper.erl b/src/emqx_sm_helper.erl similarity index 97% rename from src/emqttd_sm_helper.erl rename to src/emqx_sm_helper.erl index 2e8e9a749..4d0e7a9cb 100644 --- a/src/emqttd_sm_helper.erl +++ b/src/emqx_sm_helper.erl @@ -15,15 +15,15 @@ %%-------------------------------------------------------------------- %% @doc Session Helper. --module(emqttd_sm_helper). +-module(emqx_sm_helper). -author("Feng Lee "). -behaviour(gen_server). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). diff --git a/src/emqttd_sm_sup.erl b/src/emqx_sm_sup.erl similarity index 84% rename from src/emqttd_sm_sup.erl rename to src/emqx_sm_sup.erl index efabadb96..ad5c9732b 100644 --- a/src/emqttd_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -16,17 +16,17 @@ %% @doc Session Manager Supervisor. --module(emqttd_sm_sup). +-module(emqx_sm_sup). -behaviour(supervisor). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --define(SM, emqttd_sm). +-define(SM, emqx_sm). --define(HELPER, emqttd_sm_helper). +-define(HELPER, emqx_sm_helper). %% API -export([start_link/0]). @@ -42,13 +42,13 @@ init([]) -> ets:new(mqtt_local_session, [public, ordered_set, named_table, {write_concurrency, true}]), %% Helper - StatsFun = emqttd_stats:statsfun('sessions/count', 'sessions/max'), + StatsFun = emqx_stats:statsfun('sessions/count', 'sessions/max'), Helper = {?HELPER, {?HELPER, start_link, [StatsFun]}, permanent, 5000, worker, [?HELPER]}, %% SM Pool Sup MFA = {?SM, start_link, []}, - PoolSup = emqttd_pool_sup:spec([?SM, hash, erlang:system_info(schedulers), MFA]), + PoolSup = emqx_pool_sup:spec([?SM, hash, erlang:system_info(schedulers), MFA]), {ok, {{one_for_all, 10, 3600}, [Helper, PoolSup]}}. diff --git a/src/emqx_ssl.erl b/src/emqx_ssl.erl new file mode 100644 index 000000000..8d91202cd --- /dev/null +++ b/src/emqx_ssl.erl @@ -0,0 +1,259 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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 SSL Utility Functions. This module is copied from rabbit_ssl.erl +%% + +-module(emqx_ssl). + +-include_lib("public_key/include/public_key.hrl"). + +-type(certificate() :: binary()). + +-export([peer_cert_issuer/1, peer_cert_subject/1, peer_cert_common_name/1, + peer_cert_subject_items/2, peer_cert_validity/1]). + +%% Return a string describing the certificate's issuer. +-spec(peer_cert_issuer(certificate()) -> string()). +peer_cert_issuer(Cert) -> + cert_info(fun(#'OTPCertificate' { + tbsCertificate = #'OTPTBSCertificate' { + issuer = Issuer }}) -> + format_rdn_sequence(Issuer) + end, Cert). + +%% Return a string describing the certificate's subject, as per RFC4514. +-spec(peer_cert_subject(certificate()) -> string()). +peer_cert_subject(Cert) -> + cert_info(fun(#'OTPCertificate' { + tbsCertificate = #'OTPTBSCertificate' { + subject = Subject }}) -> + format_rdn_sequence(Subject) + end, Cert). + +-spec(peer_cert_common_name(certificate()) -> string() | 'not_found'). +peer_cert_common_name(Cert) -> + case peer_cert_subject_items(Cert, ?'id-at-commonName') of + not_found -> not_found; + CNs -> string:join(CNs, ",") + end. + +%% Return the parts of the certificate's subject. +-spec(peer_cert_subject_items(certificate(), tuple()) -> [string()] | 'undefined'). +peer_cert_subject_items(Cert, Type) -> + cert_info(fun(#'OTPCertificate' { + tbsCertificate = #'OTPTBSCertificate' { + subject = Subject }}) -> + find_by_type(Type, Subject) + end, Cert). + +%% Return a string describing the certificate's validity. +-spec(peer_cert_validity(certificate()) -> string()). +peer_cert_validity(Cert) -> + cert_info(fun(#'OTPCertificate' { + tbsCertificate = #'OTPTBSCertificate' { + validity = {'Validity', Start, End} }}) -> + format("~s - ~s", [format_asn1_value(Start), + format_asn1_value(End)]) + end, Cert). + +cert_info(F, {ok, Cert}) -> + F(case public_key:pkix_decode_cert(Cert, otp) of + {ok, DecCert} -> DecCert; %%pre R14B + DecCert -> DecCert %%R14B onwards + end). + +find_by_type(Type, {rdnSequence, RDNs}) -> + case [V || #'AttributeTypeAndValue'{type = T, value = V} + <- lists:flatten(RDNs), + T == Type] of + [] -> not_found; + L -> [format_asn1_value(V) || V <- L] + end. + +%%-------------------------------------------------------------------------- +%% Formatting functions. +%%-------------------------------------------------------------------------- + +%% Format and rdnSequence as a RFC4514 subject string. +format_rdn_sequence({rdnSequence, Seq}) -> + string:join(lists:reverse([format_complex_rdn(RDN) || RDN <- Seq]), ","). + +%% Format an RDN set. +format_complex_rdn(RDNs) -> + string:join([format_rdn(RDN) || RDN <- RDNs], "+"). + +%% Format an RDN. If the type name is unknown, use the dotted decimal +%% representation. See RFC4514, section 2.3. +format_rdn(#'AttributeTypeAndValue'{type = T, value = V}) -> + FV = escape_rdn_value(format_asn1_value(V)), + Fmts = [{?'id-at-surname' , "SN"}, + {?'id-at-givenName' , "GIVENNAME"}, + {?'id-at-initials' , "INITIALS"}, + {?'id-at-generationQualifier' , "GENERATIONQUALIFIER"}, + {?'id-at-commonName' , "CN"}, + {?'id-at-localityName' , "L"}, + {?'id-at-stateOrProvinceName' , "ST"}, + {?'id-at-organizationName' , "O"}, + {?'id-at-organizationalUnitName' , "OU"}, + {?'id-at-title' , "TITLE"}, + {?'id-at-countryName' , "C"}, + {?'id-at-serialNumber' , "SERIALNUMBER"}, + {?'id-at-pseudonym' , "PSEUDONYM"}, + {?'id-domainComponent' , "DC"}, + {?'id-emailAddress' , "EMAILADDRESS"}, + {?'street-address' , "STREET"}, + {{0,9,2342,19200300,100,1,1} , "UID"}], %% Not in public_key.hrl + case proplists:lookup(T, Fmts) of + {_, Fmt} -> + format(Fmt ++ "=~s", [FV]); + none when is_tuple(T) -> + TypeL = [format("~w", [X]) || X <- tuple_to_list(T)], + format("~s=~s", [string:join(TypeL, "."), FV]); + none -> + format("~p=~s", [T, FV]) + end. + +%% Escape a string as per RFC4514. +escape_rdn_value(V) -> + escape_rdn_value(V, start). + +escape_rdn_value([], _) -> + []; +escape_rdn_value([C | S], start) when C =:= $ ; C =:= $# -> + [$\\, C | escape_rdn_value(S, middle)]; +escape_rdn_value(S, start) -> + escape_rdn_value(S, middle); +escape_rdn_value([$ ], middle) -> + [$\\, $ ]; +escape_rdn_value([C | S], middle) when C =:= $"; C =:= $+; C =:= $,; C =:= $;; + C =:= $<; C =:= $>; C =:= $\\ -> + [$\\, C | escape_rdn_value(S, middle)]; +escape_rdn_value([C | S], middle) when C < 32 ; C >= 126 -> + %% Of ASCII characters only U+0000 needs escaping, but for display + %% purposes it's handy to escape all non-printable chars. All non-ASCII + %% characters get converted to UTF-8 sequences and then escaped. We've + %% already got a UTF-8 sequence here, so just escape it. + rabbit_misc:format("\\~2.16.0B", [C]) ++ escape_rdn_value(S, middle); +escape_rdn_value([C | S], middle) -> + [C | escape_rdn_value(S, middle)]. + +%% Get the string representation of an OTPCertificate field. +format_asn1_value({ST, S}) when ST =:= teletexString; ST =:= printableString; + ST =:= universalString; ST =:= utf8String; + ST =:= bmpString -> + format_directory_string(ST, S); +format_asn1_value({utcTime, [Y1, Y2, M1, M2, D1, D2, H1, H2, + Min1, Min2, S1, S2, $Z]}) -> + format("20~c~c-~c~c-~c~cT~c~c:~c~c:~c~cZ", + [Y1, Y2, M1, M2, D1, D2, H1, H2, Min1, Min2, S1, S2]); +%% We appear to get an untagged value back for an ia5string +%% (e.g. domainComponent). +format_asn1_value(V) when is_list(V) -> + V; +format_asn1_value(V) when is_binary(V) -> + %% OTP does not decode some values when combined with an unknown + %% type. That's probably wrong, so as a last ditch effort let's + %% try manually decoding. 'DirectoryString' is semi-arbitrary - + %% but it is the type which covers the various string types we + %% handle below. + try + {ST, S} = public_key:der_decode('DirectoryString', V), + format_directory_string(ST, S) + catch _:_ -> + format("~p", [V]) + end; +format_asn1_value(V) -> + format("~p", [V]). + +%% DirectoryString { INTEGER : maxSize } ::= CHOICE { +%% teletexString TeletexString (SIZE (1..maxSize)), +%% printableString PrintableString (SIZE (1..maxSize)), +%% bmpString BMPString (SIZE (1..maxSize)), +%% universalString UniversalString (SIZE (1..maxSize)), +%% uTF8String UTF8String (SIZE (1..maxSize)) } +%% +%% Precise definitions of printable / teletexString are hard to come +%% by. This is what I reconstructed: +%% +%% printableString: +%% "intended to represent the limited character sets available to +%% mainframe input terminals" +%% A-Z a-z 0-9 ' ( ) + , - . / : = ? [space] +%% http://msdn.microsoft.com/en-us/library/bb540814(v=vs.85).aspx +%% +%% teletexString: +%% "a sizable volume of software in the world treats TeletexString +%% (T61String) as a simple 8-bit string with mostly Windows Latin 1 +%% (superset of iso-8859-1) encoding" +%% http://www.mail-archive.com/asn1@asn1.org/msg00460.html +%% +%% (However according to that link X.680 actually defines +%% TeletexString in some much more involved and crazy way. I suggest +%% we treat it as ISO-8859-1 since Erlang does not support Windows +%% Latin 1). +%% +%% bmpString: +%% UCS-2 according to RFC 3641. Hence cannot represent Unicode +%% characters above 65535 (outside the "Basic Multilingual Plane"). +%% +%% universalString: +%% UCS-4 according to RFC 3641. +%% +%% utf8String: +%% UTF-8 according to RFC 3641. +%% +%% Within Rabbit we assume UTF-8 encoding. Since printableString is a +%% subset of ASCII it is also a subset of UTF-8. The others need +%% converting. Fortunately since the Erlang SSL library does the +%% decoding for us (albeit into a weird format, see below), we just +%% need to handle encoding into UTF-8. Note also that utf8Strings come +%% back as binary. +%% +%% Note for testing: the default Ubuntu configuration for openssl will +%% only create printableString or teletexString types no matter what +%% you do. Edit string_mask in the [req] section of +%% /etc/ssl/openssl.cnf to change this (see comments there). You +%% probably also need to set utf8 = yes to get it to accept UTF-8 on +%% the command line. Also note I could not get openssl to generate a +%% universalString. + +format_directory_string(printableString, S) -> S; +format_directory_string(teletexString, S) -> utf8_list_from(S); +format_directory_string(bmpString, S) -> utf8_list_from(S); +format_directory_string(universalString, S) -> utf8_list_from(S); +format_directory_string(utf8String, S) -> binary_to_list(S). + +utf8_list_from(S) -> + binary_to_list( + unicode:characters_to_binary(flatten_ssl_list(S), utf32, utf8)). + +%% The Erlang SSL implementation invents its own representation for +%% non-ascii strings - looking like [97,{0,0,3,187}] (that's LATIN +%% SMALL LETTER A followed by GREEK SMALL LETTER LAMDA). We convert +%% this into a list of unicode characters, which we can tell +%% unicode:characters_to_binary is utf32. + +flatten_ssl_list(L) -> [flatten_ssl_list_item(I) || I <- L]. + +flatten_ssl_list_item({A, B, C, D}) -> + A * (1 bsl 24) + B * (1 bsl 16) + C * (1 bsl 8) + D; +flatten_ssl_list_item(N) when is_number (N) -> + N. + +format(Fmt, Args) -> + lists:flatten(io_lib:format(Fmt, Args)). + diff --git a/src/emqttd_stats.erl b/src/emqx_stats.erl similarity index 91% rename from src/emqttd_stats.erl rename to src/emqx_stats.erl index 73a1471ac..b9ce291e5 100644 --- a/src/emqttd_stats.erl +++ b/src/emqx_stats.erl @@ -14,13 +14,13 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_stats). +-module(emqx_stats). -behaviour(gen_server). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). -export([start_link/0, stop/0]). @@ -87,7 +87,7 @@ stop() -> -spec(set_client_stats(binary(), stats()) -> true). set_client_stats(ClientId, Stats) -> - ets:insert(?CLIENT_STATS_TAB, {ClientId, [{'$ts', emqttd_time:now_secs()}|Stats]}). + ets:insert(?CLIENT_STATS_TAB, {ClientId, [{'$ts', emqx_time:now_secs()}|Stats]}). -spec(get_client_stats(binary()) -> stats()). get_client_stats(ClientId) -> @@ -102,7 +102,7 @@ del_client_stats(ClientId) -> -spec(set_session_stats(binary(), stats()) -> true). set_session_stats(ClientId, Stats) -> - ets:insert(?SESSION_STATS_TAB, {ClientId, [{'$ts', emqttd_time:now_secs()}|Stats]}). + ets:insert(?SESSION_STATS_TAB, {ClientId, [{'$ts', emqx_time:now_secs()}|Stats]}). -spec(get_session_stats(binary()) -> stats()). get_session_stats(ClientId) -> @@ -152,7 +152,7 @@ setstats(Stat, MaxStat, Val) -> %%-------------------------------------------------------------------- init([]) -> - emqttd_time:seed(), + emqx_time:seed(), lists:foreach( fun(Tab) -> Tab = ets:new(Tab, [set, public, named_table, {write_concurrency, true}]) @@ -160,7 +160,7 @@ init([]) -> Topics = ?SYSTOP_CLIENTS ++ ?SYSTOP_SESSIONS ++ ?SYSTOP_PUBSUB ++ ?SYSTOP_RETAINED, ets:insert(?STATS_TAB, [{Topic, 0} || Topic <- Topics]), % Tick to publish stats - {ok, #state{tick = emqttd_broker:start_tick(tick)}, hibernate}. + {ok, #state{tick = emqx_broker:start_tick(tick)}, hibernate}. handle_call(stop, _From, State) -> {stop, normal, ok, State}; @@ -191,7 +191,7 @@ handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, #state{tick = TRef}) -> - emqttd_broker:stop_tick(TRef). + emqx_broker:stop_tick(TRef). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -201,11 +201,11 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- publish(Stat, Val) -> - Msg = emqttd_message:make(stats, stats_topic(Stat), bin(Val)), - emqttd:publish(emqttd_message:set_flag(sys, Msg)). + Msg = emqx_message:make(stats, stats_topic(Stat), bin(Val)), + emqx:publish(emqx_message:set_flag(sys, Msg)). stats_topic(Stat) -> - emqttd_topic:systop(list_to_binary(lists:concat(['stats/', Stat]))). + emqx_topic:systop(list_to_binary(lists:concat(['stats/', Stat]))). bin(I) when is_integer(I) -> list_to_binary(integer_to_list(I)). diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl new file mode 100644 index 000000000..63c52e4ef --- /dev/null +++ b/src/emqx_sup.erl @@ -0,0 +1,83 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_sup). + +-behaviour(supervisor). + +-author("Feng Lee "). + +-export([start_link/0, start_child/1, start_child/2, stop_child/1]). + +%% Supervisor callbacks +-export([init/1]). + +-type(startchild_ret() :: {ok, supervisor:child()} + | {ok, supervisor:child(), term()} + | {error, term()}). + +-define(SUPERVISOR, ?MODULE). + +-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). + +start_link() -> + supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). + +-spec(start_child(atom(), worker | supervisor) -> startchild_ret()). +start_child(Mod, Type) when Type == worker orelse Type == supervisor -> + start_child(?CHILD(Mod, Type)). + +-spec(start_child(supervisor:child_spec()) -> startchild_ret()). +start_child(ChildSpec) when is_tuple(ChildSpec) -> + supervisor:start_child(?SUPERVISOR, ChildSpec). + +-spec(start_child(Mod::atom(), Type :: worker | supervisor) -> {ok, pid()}). +start_child(Mod, Type) when is_atom(Mod) and is_atom(Type) -> + supervisor:start_child(?MODULE, ?CHILD(Mod, Type)). + +-spec(stop_child(supervisor:child_id()) -> ok | {error, any()}). +stop_child(ChildId) -> + case supervisor:terminate_child(?SUPERVISOR, ChildId) of + ok -> supervisor:delete_child(?SUPERVISOR, ChildId); + Error -> Error + end. + +%%-------------------------------------------------------------------- +%% Supervisor callbacks +%%-------------------------------------------------------------------- + +init([]) -> + {ok, {{one_for_all, 0, 1}, + [?CHILD(emqx_ctl, worker), + ?CHILD(emqx_hooks, worker), + ?CHILD(emqx_router, worker), + ?CHILD(emqx_pubsub_sup, supervisor), + ?CHILD(emqx_stats, worker), + ?CHILD(emqx_metrics, worker), + ?CHILD(emqx_pooler, supervisor), + ?CHILD(emqx_trace_sup, supervisor), + ?CHILD(emqx_cm_sup, supervisor), + ?CHILD(emqx_sm_sup, supervisor), + ?CHILD(emqx_session_sup, supervisor), + ?CHILD(emqx_ws_client_sup, supervisor), + ?CHILD(emqx_broker, worker), + ?CHILD(emqx_alarm, worker), + ?CHILD(emqx_mod_sup, supervisor), + ?CHILD(emqx_bridge_sup_sup, supervisor), + ?CHILD(emqx_access_control, worker), + ?CHILD(emqx_sysmon_sup, supervisor)] + }}. + diff --git a/src/emqttd_sysmon.erl b/src/emqx_sysmon.erl similarity index 91% rename from src/emqttd_sysmon.erl rename to src/emqx_sysmon.erl index c94c1df54..77103c9d6 100644 --- a/src/emqttd_sysmon.erl +++ b/src/emqx_sysmon.erl @@ -14,14 +14,13 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc VM System Monitor --module(emqttd_sysmon). +-module(emqx_sysmon). -author("Feng Lee "). -behavior(gen_server). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). -export([start_link/1]). @@ -143,26 +142,24 @@ code_change(_OldVsn, State, _Extra) -> suppress(Key, SuccFun, State = #state{events = Events}) -> case lists:member(Key, Events) of - true -> - {noreply, State}; - false -> - SuccFun(), - {noreply, State#state{events = [Key|Events]}} + true -> {noreply, State}; + false -> SuccFun(), + {noreply, State#state{events = [Key|Events]}} end. procinfo(Pid) -> - case {emqttd_vm:get_process_info(Pid), emqttd_vm:get_process_gc(Pid)} of + case {emqx_vm:get_process_info(Pid), emqx_vm:get_process_gc(Pid)} of {undefined, _} -> undefined; {_, undefined} -> undefined; {Info, GcInfo} -> Info ++ GcInfo end. publish(Sysmon, WarnMsg) -> - Msg = emqttd_message:make(sysmon, topic(Sysmon), iolist_to_binary(WarnMsg)), - emqttd:publish(emqttd_message:set_flag(sys, Msg)). + Msg = emqx_message:make(sysmon, topic(Sysmon), iolist_to_binary(WarnMsg)), + emqx:publish(emqx_message:set_flag(sys, Msg)). topic(Sysmon) -> - emqttd_topic:systop(list_to_binary(lists:concat(['sysmon/', Sysmon]))). + emqx_topic:systop(list_to_binary(lists:concat(['sysmon/', Sysmon]))). %% start_tracelog(undefined) -> %% {ok, undefined}; diff --git a/src/emqttd_sysmon_sup.erl b/src/emqx_sysmon_sup.erl similarity index 82% rename from src/emqttd_sysmon_sup.erl rename to src/emqx_sysmon_sup.erl index 99e7a628d..6db24a30e 100644 --- a/src/emqttd_sysmon_sup.erl +++ b/src/emqx_sysmon_sup.erl @@ -14,7 +14,9 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_sysmon_sup). +-module(emqx_sysmon_sup). + +-author("Feng Lee "). -behaviour(supervisor). @@ -28,8 +30,8 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - {ok, Env} = emqttd:env(sysmon), - Sysmon = {sysmon, {emqttd_sysmon, start_link, [Env]}, - permanent, 5000, worker, [emqttd_sysmon]}, + {ok, Env} = emqx:env(sysmon), + Sysmon = {sysmon, {emqx_sysmon, start_link, [Env]}, + permanent, 5000, worker, [emqx_sysmon]}, {ok, {{one_for_one, 10, 100}, [Sysmon]}}. diff --git a/src/emqttd_time.erl b/src/emqx_time.erl similarity index 98% rename from src/emqttd_time.erl rename to src/emqx_time.erl index 7e5940438..f6cb02854 100644 --- a/src/emqttd_time.erl +++ b/src/emqx_time.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_time). +-module(emqx_time). -author("Feng Lee "). diff --git a/src/emqttd_topic.erl b/src/emqx_topic.erl similarity index 93% rename from src/emqttd_topic.erl rename to src/emqx_topic.erl index 458a41f7d..3d4c2a741 100644 --- a/src/emqttd_topic.erl +++ b/src/emqx_topic.erl @@ -14,13 +14,13 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_topic). +-module(emqx_topic). -author("Feng Lee "). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). -import(lists, [reverse/1]). @@ -186,6 +186,11 @@ parse(<<"$local/", Topic1/binary>>, Options) -> parse(Topic1, [local | Options]) end); +parse(<<"$fastlane/", Topic1/binary>>, Options) -> + if_not_contain(fastlane, Options, fun() -> + parse(Topic1, [fastlane | Options]) + end); + parse(<<"$queue/", Topic1/binary>>, Options) -> if_not_contain(share, Options,fun() -> parse(Topic1, [{share, '$queue'} | Options]) @@ -200,8 +205,8 @@ parse(<<"$share/", Topic1/binary>>, Options) -> parse(Topic, Options) -> {Topic, Options}. -if_not_contain(local, Options, Fun) -> - ?IF(lists:member(local, Options), error(invalid_topic), Fun()); +if_not_contain(Key, Options, Fun) when Key == local; Key == fastlane -> + ?IF(lists:member(Key, Options), error(invalid_topic), Fun()); if_not_contain(share, Options, Fun) -> ?IF(lists:keyfind(share, 1, Options), error(invalid_topic), Fun()). diff --git a/src/emqttd_trace.erl b/src/emqx_trace.erl similarity index 97% rename from src/emqttd_trace.erl rename to src/emqx_trace.erl index 4d46d9165..7b09b55a8 100644 --- a/src/emqttd_trace.erl +++ b/src/emqx_trace.erl @@ -14,14 +14,13 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @docTrace MQTT packets/messages by ClientID or Topic. --module(emqttd_trace). +-module(emqx_trace). -behaviour(gen_server). -author("Feng Lee "). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). %% API Function Exports -export([start_link/0]). diff --git a/src/emqttd_trace_sup.erl b/src/emqx_trace_sup.erl similarity index 88% rename from src/emqttd_trace_sup.erl rename to src/emqx_trace_sup.erl index 728e6818e..31f5ba7d8 100644 --- a/src/emqttd_trace_sup.erl +++ b/src/emqx_trace_sup.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_trace_sup). +-module(emqx_trace_sup). -author("Feng Lee "). @@ -30,7 +30,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - Trace = {trace, {emqttd_trace, start_link, []}, - permanent, 5000, worker, [emqttd_trace]}, + Trace = {trace, {emqx_trace, start_link, []}, + permanent, 5000, worker, [emqx_trace]}, {ok, {{one_for_one, 10, 3600}, [Trace]}}. diff --git a/src/emqttd_trie.erl b/src/emqx_trie.erl similarity index 95% rename from src/emqttd_trie.erl rename to src/emqx_trie.erl index 5b36e6e04..4ad525a90 100644 --- a/src/emqttd_trie.erl +++ b/src/emqx_trie.erl @@ -18,11 +18,11 @@ %% [Trie](http://en.wikipedia.org/wiki/Trie) %% @end --module(emqttd_trie). +-module(emqx_trie). -author("Feng Lee "). --include("emqttd_trie.hrl"). +-include("emqx_trie.hrl"). %% Mnesia Callbacks -export([mnesia/1]). @@ -71,7 +71,7 @@ insert(Topic) when is_binary(Topic) -> write_trie_node(TrieNode#trie_node{topic=Topic}); [] -> % Add trie path - lists:foreach(fun add_path/1, emqttd_topic:triples(Topic)), + lists:foreach(fun add_path/1, emqx_topic:triples(Topic)), % Add last node write_trie_node(#trie_node{node_id=Topic, topic=Topic}) end. @@ -79,7 +79,7 @@ insert(Topic) when is_binary(Topic) -> %% @doc Find trie nodes that match topic -spec(match(Topic :: binary()) -> list(MatchedTopic :: binary())). match(Topic) when is_binary(Topic) -> - TrieNodes = match_node(root, emqttd_topic:words(Topic)), + TrieNodes = match_node(root, emqx_topic:words(Topic)), [Name || #trie_node{topic=Name} <- TrieNodes, Name =/= undefined]. %% @doc Lookup a Trie Node @@ -93,7 +93,7 @@ delete(Topic) when is_binary(Topic) -> case mnesia:read(mqtt_trie_node, Topic) of [#trie_node{edge_count=0}] -> mnesia:delete({mqtt_trie_node, Topic}), - delete_path(lists:reverse(emqttd_topic:triples(Topic))); + delete_path(lists:reverse(emqx_topic:triples(Topic))); [TrieNode] -> write_trie_node(TrieNode#trie_node{topic = undefined}); [] -> diff --git a/src/emqttd_vm.erl b/src/emqx_vm.erl similarity index 99% rename from src/emqttd_vm.erl rename to src/emqx_vm.erl index 16fae60ae..a4b9a67fc 100644 --- a/src/emqttd_vm.erl +++ b/src/emqx_vm.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_vm). +-module(emqx_vm). -export([schedulers/0]). diff --git a/src/emqttd_ws.erl b/src/emqx_ws.erl similarity index 88% rename from src/emqttd_ws.erl rename to src/emqx_ws.erl index c7d0b2119..1e2870a42 100644 --- a/src/emqttd_ws.erl +++ b/src/emqx_ws.erl @@ -14,11 +14,11 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_ws). +-module(emqx_ws). -author("Feng Lee "). --include("emqttd_protocol.hrl"). +-include("emqx_protocol.hrl"). -import(proplists, [get_value/3]). @@ -44,12 +44,12 @@ handle_request('GET', "/mqtt", Req) -> Proto = check_protocol_header(Req), case {is_websocket(Upgrade), Proto} of {true, "mqtt" ++ _Vsn} -> - {ok, ProtoEnv} = emqttd:env(protocol), + {ok, ProtoEnv} = emqx:env(protocol), PacketSize = get_value(max_packet_size, ProtoEnv, ?MAX_PACKET_SIZE), - Parser = emqttd_parser:initial_state(PacketSize), + Parser = emqx_parser:initial_state(PacketSize), %% Upgrade WebSocket. {ReentryWs, ReplyChannel} = mochiweb_websocket:upgrade_connection(Req, fun ?MODULE:ws_loop/3), - {ok, ClientPid} = emqttd_ws_client_sup:start_client(self(), Req, ReplyChannel), + {ok, ClientPid} = emqx_ws_client_sup:start_client(self(), Req, ReplyChannel), ReentryWs(#wsocket_state{peername = Req:get(peername), parser = Parser, max_packet_size = PacketSize, client_pid = ClientPid}); {false, _} -> @@ -68,7 +68,7 @@ is_websocket(Upgrade) -> Upgrade =/= undefined andalso string:to_lower(Upgrade) =:= "websocket". check_protocol_header(Req) -> - case emqttd:env(websocket_protocol_header, false) of + case emqx:env(websocket_protocol_header, false) of true -> get_protocol_header(Req); false -> "mqtt-v3.1.1" end. @@ -90,8 +90,8 @@ ws_loop([<<>>], State, _ReplyChannel) -> State; ws_loop(Data, State = #wsocket_state{client_pid = ClientPid, parser = Parser}, ReplyChannel) -> ?WSLOG(debug, "RECV ~p", [Data], State), - emqttd_metrics:inc('bytes/received', iolist_size(Data)), - case catch emqttd_parser:parse(iolist_to_binary(Data), Parser) of + emqx_metrics:inc('bytes/received', iolist_size(Data)), + case catch emqx_parser:parse(iolist_to_binary(Data), Parser) of {more, NewParser} -> State#wsocket_state{parser = NewParser}; {ok, Packet, Rest} -> @@ -107,5 +107,5 @@ ws_loop(Data, State = #wsocket_state{client_pid = ClientPid, parser = Parser}, R end. reset_parser(State = #wsocket_state{max_packet_size = PacketSize}) -> - State#wsocket_state{parser = emqttd_parser:initial_state(PacketSize)}. + State#wsocket_state{parser = emqx_parser:initial_state(PacketSize)}. diff --git a/src/emqttd_ws_client.erl b/src/emqx_ws_client.erl similarity index 83% rename from src/emqttd_ws_client.erl rename to src/emqx_ws_client.erl index a583611a4..e1861684b 100644 --- a/src/emqttd_ws_client.erl +++ b/src/emqx_ws_client.erl @@ -16,19 +16,19 @@ %% @doc MQTT WebSocket Connection. --module(emqttd_ws_client). +-module(emqx_ws_client). -behaviour(gen_server2). -author("Feng Lee "). --include("emqttd.hrl"). +-include("emqx.hrl"). --include("emqttd_protocol.hrl"). +-include("emqx_mqtt.hrl"). --include("emqttd_internal.hrl"). +-include("emqx_internal.hrl"). --import(proplists, [get_value/3]). +-import(proplists, [get_value/2, get_value/3]). %% API Exports -export([start_link/4]). @@ -96,11 +96,11 @@ init([Env, WsPid, Req, ReplyChannel]) -> Headers = mochiweb_headers:to_list( mochiweb_request:get(headers, Req)), Conn = Req:get(connection), - ProtoState = emqttd_protocol:init(Conn, Peername, send_fun(ReplyChannel), - [{ws_initial_headers, Headers} | Env]), + ProtoState = emqx_protocol:init(Conn, Peername, send_fun(ReplyChannel), + [{ws_initial_headers, Headers} | Env]), IdleTimeout = get_value(client_idle_timeout, Env, 30000), EnableStats = get_value(client_enable_stats, Env, false), - ForceGcCount = emqttd_gc:conn_max_gc_count(), + ForceGcCount = emqx_gc:conn_max_gc_count(), {ok, #wsclient_state{connection = Conn, ws_pid = WsPid, peername = Peername, @@ -117,24 +117,24 @@ prioritise_info(Msg, _Len, _State) -> handle_pre_hibernate(State = #wsclient_state{ws_pid = WsPid}) -> erlang:garbage_collect(WsPid), - {hibernate, emqttd_gc:reset_conn_gc_count(#wsclient_state.force_gc_count, emit_stats(State))}. + {hibernate, emqx_gc:reset_conn_gc_count(#wsclient_state.force_gc_count, emit_stats(State))}. handle_call(info, From, State = #wsclient_state{peername = Peername, proto_state = ProtoState}) -> - Info = [{websocket, true}, {peername, Peername} | emqttd_protocol:info(ProtoState)], + Info = [{websocket, true}, {peername, Peername} | emqx_protocol:info(ProtoState)], {reply, Stats, _, _} = handle_call(stats, From, State), reply(lists:append(Info, Stats), State); handle_call(stats, _From, State = #wsclient_state{proto_state = ProtoState}) -> - reply(lists:append([emqttd_misc:proc_stats(), + reply(lists:append([emqx_misc:proc_stats(), wsock_stats(State), - emqttd_protocol:stats(ProtoState)]), State); + emqx_protocol:stats(ProtoState)]), State); handle_call(kick, _From, State) -> {stop, {shutdown, kick}, ok, State}; handle_call(session, _From, State = #wsclient_state{proto_state = ProtoState}) -> - reply(emqttd_protocol:session(ProtoState), State); + reply(emqx_protocol:session(ProtoState), State); handle_call({clean_acl_cache, Topic}, _From, State) -> erase({acl, publish, Topic}), @@ -145,8 +145,8 @@ handle_call(Req, _From, State) -> reply({error, unexpected_request}, State). handle_cast({received, Packet}, State = #wsclient_state{proto_state = ProtoState}) -> - emqttd_metrics:received(Packet), - case emqttd_protocol:received(Packet, ProtoState) of + emqx_metrics:received(Packet), + case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> {noreply, gc(State#wsclient_state{proto_state = ProtoState1}), hibernate}; {error, Error} -> @@ -165,32 +165,36 @@ handle_cast(Msg, State) -> handle_info({subscribe, TopicTable}, State) -> with_proto( fun(ProtoState) -> - emqttd_protocol:subscribe(TopicTable, ProtoState) + emqx_protocol:subscribe(TopicTable, ProtoState) end, State); handle_info({unsubscribe, Topics}, State) -> with_proto( fun(ProtoState) -> - emqttd_protocol:unsubscribe(Topics, ProtoState) + emqx_protocol:unsubscribe(Topics, ProtoState) end, State); handle_info({suback, PacketId, GrantedQos}, State) -> with_proto( fun(ProtoState) -> Packet = ?SUBACK_PACKET(PacketId, GrantedQos), - emqttd_protocol:send(Packet, ProtoState) + emqx_protocol:send(Packet, ProtoState) end, State); +%% Fastlane +handle_info({dispatch, _Topic, Message}, State) -> + handle_info({deliver, Message#mqtt_message{qos = ?QOS_0}}, State); + handle_info({deliver, Message}, State) -> with_proto( fun(ProtoState) -> - emqttd_protocol:send(Message, ProtoState) + emqx_protocol:send(Message, ProtoState) end, gc(State)); handle_info({redeliver, {?PUBREL, PacketId}}, State) -> with_proto( fun(ProtoState) -> - emqttd_protocol:pubrel(PacketId, ProtoState) + emqx_protocol:pubrel(PacketId, ProtoState) end, State); handle_info(emit_stats, State) -> @@ -205,7 +209,7 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> handle_info({keepalive, start, Interval}, State = #wsclient_state{connection = Conn}) -> ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State), - case emqttd_keepalive:start(stat_fun(Conn), Interval, {keepalive, check}) of + case emqx_keepalive:start(stat_fun(Conn), Interval, {keepalive, check}) of {ok, KeepAlive} -> {noreply, State#wsclient_state{keepalive = KeepAlive}, hibernate}; {error, Error} -> @@ -214,7 +218,7 @@ handle_info({keepalive, start, Interval}, State = #wsclient_state{connection = C end; handle_info({keepalive, check}, State = #wsclient_state{keepalive = KeepAlive}) -> - case emqttd_keepalive:check(KeepAlive) of + case emqx_keepalive:check(KeepAlive) of {ok, KeepAlive1} -> {noreply, emit_stats(State#wsclient_state{keepalive = KeepAlive1}), hibernate}; {error, timeout} -> @@ -234,7 +238,7 @@ handle_info({'EXIT', WsPid, Reason}, State = #wsclient_state{ws_pid = WsPid}) -> %% The session process exited unexpectedly. handle_info({'EXIT', Pid, Reason}, State = #wsclient_state{proto_state = ProtoState}) -> - case emqttd_protocol:session(ProtoState) of + case emqx_protocol:session(ProtoState) of Pid -> stop(Reason, State); _ -> ?WSLOG(error, "Unexpected EXIT: ~p, Reason: ~p", [Pid, Reason], State), {noreply, State, hibernate} @@ -245,12 +249,12 @@ handle_info(Info, State) -> {noreply, State, hibernate}. terminate(Reason, #wsclient_state{proto_state = ProtoState, keepalive = KeepAlive}) -> - emqttd_keepalive:cancel(KeepAlive), + emqx_keepalive:cancel(KeepAlive), case Reason of {shutdown, Error} -> - emqttd_protocol:shutdown(Error, ProtoState); + emqx_protocol:shutdown(Error, ProtoState); _ -> - emqttd_protocol:shutdown(Reason, ProtoState) + emqx_protocol:shutdown(Reason, ProtoState) end. code_change(_OldVsn, State, _Extra) -> @@ -262,8 +266,8 @@ code_change(_OldVsn, State, _Extra) -> send_fun(ReplyChannel) -> fun(Packet) -> - Data = emqttd_serializer:serialize(Packet), - emqttd_metrics:inc('bytes/sent', iolist_size(Data)), + Data = emqx_serializer:serialize(Packet), + emqx_metrics:inc('bytes/sent', iolist_size(Data)), ReplyChannel({binary, Data}) end. @@ -276,7 +280,7 @@ stat_fun(Conn) -> end. emit_stats(State = #wsclient_state{proto_state = ProtoState}) -> - emit_stats(emqttd_protocol:clientid(ProtoState), State). + emit_stats(emqx_protocol:clientid(ProtoState), State). emit_stats(_ClientId, State = #wsclient_state{enable_stats = false}) -> State; @@ -284,7 +288,7 @@ emit_stats(undefined, State) -> State; emit_stats(ClientId, State) -> {reply, Stats, _, _} = handle_call(stats, undefined, State), - emqttd_stats:set_client_stats(ClientId, Stats), + emqx_stats:set_client_stats(ClientId, Stats), State. wsock_stats(#wsclient_state{connection = Conn}) -> @@ -308,5 +312,5 @@ stop(Reason, State) -> gc(State) -> Cb = fun() -> emit_stats(State) end, - emqttd_gc:maybe_force_gc(#wsclient_state.force_gc_count, State, Cb). + emqx_gc:maybe_force_gc(#wsclient_state.force_gc_count, State, Cb). diff --git a/src/emqttd_ws_client_sup.erl b/src/emqx_ws_client_sup.erl similarity index 86% rename from src/emqttd_ws_client_sup.erl rename to src/emqx_ws_client_sup.erl index 21f683eaa..9fdbbcfc7 100644 --- a/src/emqttd_ws_client_sup.erl +++ b/src/emqx_ws_client_sup.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_ws_client_sup). +-module(emqx_ws_client_sup). -author("Feng Lee "). @@ -39,8 +39,8 @@ start_client(WsPid, Req, ReplyChannel) -> %%-------------------------------------------------------------------- init([]) -> - Env = lists:append(emqttd:env(client, []), emqttd:env(protocol, [])), + Env = lists:append(emqx:env(client, []), emqx:env(protocol, [])), {ok, {{simple_one_for_one, 0, 1}, - [{ws_client, {emqttd_ws_client, start_link, [Env]}, - temporary, 5000, worker, [emqttd_ws_client]}]}}. + [{ws_client, {emqx_ws_client, start_link, [Env]}, + temporary, 5000, worker, [emqx_ws_client]}]}}. diff --git a/test/emqttd_inflight_SUITE.erl b/test/emqttd_inflight_SUITE.erl deleted file mode 100644 index de5391f1a..000000000 --- a/test/emqttd_inflight_SUITE.erl +++ /dev/null @@ -1,51 +0,0 @@ -%% -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% - --module(emqttd_inflight_SUITE). - --author("Feng Lee "). - --include_lib("eunit/include/eunit.hrl"). - -%% CT --compile(export_all). - -all() -> [t_contain, t_lookup, t_insert, t_update, t_delete, t_window, - t_is_full, t_is_empty]. - -t_contain(_) -> - Inflight = emqttd_inflight:new(0), - ?assertNot(Inflight:contain(k)), - Inflight1 = Inflight:insert(k, v), - ?assert(Inflight1:contain(k)). - -t_lookup(_) -> - Inflight = (emqttd_inflight:new(0)):insert(k, v), - ?assertEqual(v, Inflight:lookup(k)). - -t_insert(_) -> - Inflight = ((emqttd_inflight:new(0)):insert(k1, v1)):insert(k2, v2), - ?assertEqual(v2, Inflight:lookup(k2)). - -t_update(_) -> - Inflight = ((emqttd_inflight:new(0)):insert(k, v1)):update(k, v2), - ?assertEqual(v2, Inflight:lookup(k)). - -t_delete(_) -> - Inflight = ((emqttd_inflight:new(0)):insert(k, v1)):delete(k), - ?assert(Inflight:is_empty()). - -t_window(_) -> - ?assertEqual([], (emqttd_inflight:new(10)):window()), - Inflight = ((emqttd_inflight:new(0)):insert(1, 1)):insert(2, 2), - ?assertEqual([1, 2], Inflight:window()). - -t_is_full(_) -> - Inflight = ((emqttd_inflight:new(1)):insert(k, v1)), - ?assert(Inflight:is_full()). - -t_is_empty(_) -> - Inflight = ((emqttd_inflight:new(1)):insert(k, v1)), - ?assertNot(Inflight:is_empty()). - diff --git a/test/emqttd_SUITE.erl b/test/emqx_SUITE.erl similarity index 58% rename from test/emqttd_SUITE.erl rename to test/emqx_SUITE.erl index 195820248..c803244ab 100644 --- a/test/emqttd_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -14,18 +14,20 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_SUITE). +-module(emqx_SUITE). -compile(export_all). --include("emqttd.hrl"). +-include("emqx.hrl"). + +-include("emqx_mqtt.hrl"). + +-define(APP, emqx). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(APP, emqttd). - -define(CONTENT_TYPE, "application/json"). -define(MQTT_SSL_TWOWAY, [{cacertfile, "certs/cacert.pem"}, @@ -67,15 +69,14 @@ all() -> {group, http}, {group, alarms}, {group, cli}, - {group, get_api}, + {group, rest_api}, {group, cleanSession}]. groups() -> [{protocol, [sequence], [mqtt_connect, - mqtt_ssl_twoway, - mqtt_ssl_oneway - ]}, + mqtt_ssl_oneway, + mqtt_ssl_twoway]}, {pubsub, [sequence], [subscribe_unsubscribe, publish, pubsub, @@ -121,18 +122,22 @@ groups() -> conflict_listeners ]}, cli_vm]}, - {cleanSession, [sequence], - [cleanSession_validate]}, - {get_api, [sequence], [get_api_lists]}]. + {cleanSession, [sequence], + [cleanSession_validate, + cleanSession_validate1] + }, + {rest_api, [sequence], [get_api_lists]} + ]. init_per_suite(Config) -> NewConfig = generate_config(), lists:foreach(fun set_app_env/1, NewConfig), - application:ensure_all_started(?APP), + Apps = application:ensure_all_started(?APP), + ct:log("Apps:~p", [Apps]), Config. end_per_suite(_Config) -> - emqttd:shutdown(). + emqx:shutdown(). %%-------------------------------------------------------------------- %% Protocol Test @@ -153,12 +158,12 @@ connect_broker_(Packet, RecvSize) -> Data. mqtt_ssl_oneway(_) -> - emqttd:stop(), + emqx:stop(), change_opts(ssl_oneway), - emqttd:start(), - timer:sleep(6000), + emqx:start(), {ok, SslOneWay} = emqttc:start_link([{host, "localhost"}, {port, 8883}, + {logger, debug}, {client_id, <<"ssloneway">>}, ssl]), timer:sleep(100), emqttc:subscribe(SslOneWay, <<"topic">>, qos1), @@ -174,13 +179,12 @@ mqtt_ssl_oneway(_) -> emqttc:disconnect(SslOneWay), emqttc:disconnect(Pub). -mqtt_ssl_twoway(_) -> - emqttd:stop(), +mqtt_ssl_twoway(_Config) -> + emqx:stop(), change_opts(ssl_twoway), - emqttd:start(), - timer:sleep(6000), - ClientSSl = [{Key, local_path(["etc", File])} || - {Key, File} <- ?MQTT_SSL_CLIENT], + emqx:start(), + timer:sleep(3000), + ClientSSl = [{Key, local_path(["etc", File])} || {Key, File} <- ?MQTT_SSL_CLIENT], {ok, SslTwoWay} = emqttc:start_link([{host, "localhost"}, {port, 8883}, {client_id, <<"ssltwoway">>}, @@ -202,82 +206,82 @@ mqtt_ssl_twoway(_) -> %%-------------------------------------------------------------------- subscribe_unsubscribe(_) -> - ok = emqttd:subscribe(<<"topic">>, <<"clientId">>), - ok = emqttd:subscribe(<<"topic/1">>, <<"clientId">>, [{qos, 1}]), - ok = emqttd:subscribe(<<"topic/2">>, <<"clientId">>, [{qos, 2}]), - ok = emqttd:unsubscribe(<<"topic">>, <<"clientId">>), - ok = emqttd:unsubscribe(<<"topic/1">>, <<"clientId">>), - ok = emqttd:unsubscribe(<<"topic/2">>, <<"clientId">>). + ok = emqx:subscribe(<<"topic">>, <<"clientId">>), + ok = emqx:subscribe(<<"topic/1">>, <<"clientId">>, [{qos, 1}]), + ok = emqx:subscribe(<<"topic/2">>, <<"clientId">>, [{qos, 2}]), + ok = emqx:unsubscribe(<<"topic">>, <<"clientId">>), + ok = emqx:unsubscribe(<<"topic/1">>, <<"clientId">>), + ok = emqx:unsubscribe(<<"topic/2">>, <<"clientId">>). publish(_) -> - Msg = emqttd_message:make(ct, <<"test/pubsub">>, <<"hello">>), - ok = emqttd:subscribe(<<"test/+">>), + Msg = emqx_message:make(ct, <<"test/pubsub">>, <<"hello">>), + ok = emqx:subscribe(<<"test/+">>), timer:sleep(10), - emqttd:publish(Msg), + emqx:publish(Msg), ?assert(receive {dispatch, <<"test/+">>, Msg} -> true after 5 -> false end). pubsub(_) -> Self = self(), - ok = emqttd:subscribe(<<"a/b/c">>, Self, [{qos, 1}]), - ?assertMatch({error, _}, emqttd:subscribe(<<"a/b/c">>, Self, [{qos, 2}])), + ok = emqx:subscribe(<<"a/b/c">>, Self, [{qos, 1}]), + ?assertMatch({error, _}, emqx:subscribe(<<"a/b/c">>, Self, [{qos, 2}])), timer:sleep(10), [{Self, <<"a/b/c">>}] = ets:lookup(mqtt_subscription, Self), [{<<"a/b/c">>, Self}] = ets:lookup(mqtt_subscriber, <<"a/b/c">>), - emqttd:publish(emqttd_message:make(ct, <<"a/b/c">>, <<"hello">>)), + emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), ?assert(receive {dispatch, <<"a/b/c">>, _} -> true after 2 -> false end), spawn(fun() -> - emqttd:subscribe(<<"a/b/c">>), - emqttd:subscribe(<<"c/d/e">>), + emqx:subscribe(<<"a/b/c">>), + emqx:subscribe(<<"c/d/e">>), timer:sleep(10), - emqttd:unsubscribe(<<"a/b/c">>) + emqx:unsubscribe(<<"a/b/c">>) end), timer:sleep(20), - emqttd:unsubscribe(<<"a/b/c">>). + emqx:unsubscribe(<<"a/b/c">>). t_local_subscribe(_) -> - emqttd:subscribe("$local/topic0"), - emqttd:subscribe("$local/topic1", <<"x">>), - emqttd:subscribe("$local/topic2", <<"x">>, [{qos, 2}]), + emqx:subscribe("$local/topic0"), + emqx:subscribe("$local/topic1", <<"x">>), + emqx:subscribe("$local/topic2", <<"x">>, [{qos, 2}]), timer:sleep(10), - ?assertEqual([self()], emqttd:subscribers("$local/topic0")), - ?assertEqual([<<"x">>], emqttd:subscribers("$local/topic1")), - ?assertEqual([{<<"$local/topic1">>,<<"x">>,[]},{<<"$local/topic2">>,<<"x">>,[{qos,2}]}], emqttd:subscriptions(<<"x">>)), + ?assertEqual([self()], emqx:subscribers("$local/topic0")), + ?assertEqual([<<"x">>], emqx:subscribers("$local/topic1")), + ?assertEqual([{<<"$local/topic1">>,<<"x">>,[]},{<<"$local/topic2">>,<<"x">>,[{qos,2}]}], emqx:subscriptions(<<"x">>)), - ?assertEqual(ok, emqttd:unsubscribe("$local/topic0")), - ?assertMatch({error, {subscription_not_found, _}}, emqttd:unsubscribe("$local/topic0")), - ?assertEqual(ok, emqttd:unsubscribe("$local/topic1", <<"x">>)), - ?assertEqual(ok, emqttd:unsubscribe("$local/topic2", <<"x">>)), - ?assertEqual([], emqttd:subscribers("topic1")), - ?assertEqual([], emqttd:subscriptions(<<"x">>)). + ?assertEqual(ok, emqx:unsubscribe("$local/topic0")), + ?assertMatch({error, {subscription_not_found, _}}, emqx:unsubscribe("$local/topic0")), + ?assertEqual(ok, emqx:unsubscribe("$local/topic1", <<"x">>)), + ?assertEqual(ok, emqx:unsubscribe("$local/topic2", <<"x">>)), + ?assertEqual([], emqx:subscribers("topic1")), + ?assertEqual([], emqx:subscriptions(<<"x">>)). t_shared_subscribe(_) -> - emqttd:subscribe("$local/$share/group1/topic1"), - emqttd:subscribe("$share/group2/topic2"), - emqttd:subscribe("$queue/topic3"), + emqx:subscribe("$local/$share/group1/topic1"), + emqx:subscribe("$share/group2/topic2"), + emqx:subscribe("$queue/topic3"), timer:sleep(10), - ?assertEqual([self()], emqttd:subscribers(<<"$local/$share/group1/topic1">>)), + ?assertEqual([self()], emqx:subscribers(<<"$local/$share/group1/topic1">>)), ?assertEqual([{<<"$local/$share/group1/topic1">>, self(), []}, {<<"$queue/topic3">>, self(), []}, {<<"$share/group2/topic2">>, self(), []}], - lists:sort(emqttd:subscriptions(self()))), - emqttd:unsubscribe("$local/$share/group1/topic1"), - emqttd:unsubscribe("$share/group2/topic2"), - emqttd:unsubscribe("$queue/topic3"), - ?assertEqual([], lists:sort(emqttd:subscriptions(self()))). + lists:sort(emqx:subscriptions(self()))), + emqx:unsubscribe("$local/$share/group1/topic1"), + emqx:unsubscribe("$share/group2/topic2"), + emqx:unsubscribe("$queue/topic3"), + ?assertEqual([], lists:sort(emqx:subscriptions(self()))). 'pubsub#'(_) -> - emqttd:subscribe(<<"a/#">>), + emqx:subscribe(<<"a/#">>), timer:sleep(10), - emqttd:publish(emqttd_message:make(ct, <<"a/b/c">>, <<"hello">>)), + emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), ?assert(receive {dispatch, <<"a/#">>, _} -> true after 2 -> false end), - emqttd:unsubscribe(<<"a/#">>). + emqx:unsubscribe(<<"a/#">>). 'pubsub+'(_) -> - emqttd:subscribe(<<"a/+/+">>), + emqx:subscribe(<<"a/+/+">>), timer:sleep(10), - emqttd:publish(emqttd_message:make(ct, <<"a/b/c">>, <<"hello">>)), + emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), ?assert(receive {dispatch, <<"a/+/+">>, _} -> true after 1 -> false end), - emqttd:unsubscribe(<<"a/+/+">>). + emqx:unsubscribe(<<"a/+/+">>). loop_recv(Topic, Timeout) -> loop_recv(Topic, Timeout, []). @@ -296,42 +300,42 @@ loop_recv(Topic, Timeout, Acc) -> router_add_del(_) -> %% Add - emqttd_router:add_route(<<"#">>), - emqttd_router:add_route(<<"a/b/c">>), - emqttd_router:add_route(<<"+/#">>, node()), + emqx_router:add_route(<<"#">>), + emqx_router:add_route(<<"a/b/c">>), + emqx_router:add_route(<<"+/#">>, node()), Routes = [R1, R2 | _] = [ #mqtt_route{topic = <<"#">>, node = node()}, #mqtt_route{topic = <<"+/#">>, node = node()}, #mqtt_route{topic = <<"a/b/c">>, node = node()}], - Routes = lists:sort(emqttd_router:match(<<"a/b/c">>)), + Routes = lists:sort(emqx_router:match(<<"a/b/c">>)), %% Batch Add - emqttd_router:add_routes(Routes), - Routes = lists:sort(emqttd_router:match(<<"a/b/c">>)), + emqx_router:add_routes(Routes), + Routes = lists:sort(emqx_router:match(<<"a/b/c">>)), %% Del - emqttd_router:del_route(<<"a/b/c">>), - [R1, R2] = lists:sort(emqttd_router:match(<<"a/b/c">>)), - {atomic, []} = mnesia:transaction(fun emqttd_trie:lookup/1, [<<"a/b/c">>]), + emqx_router:del_route(<<"a/b/c">>), + [R1, R2] = lists:sort(emqx_router:match(<<"a/b/c">>)), + {atomic, []} = mnesia:transaction(fun emqx_trie:lookup/1, [<<"a/b/c">>]), %% Batch Del R3 = #mqtt_route{topic = <<"#">>, node = 'a@127.0.0.1'}, - emqttd_router:add_route(R3), - emqttd_router:del_routes([R1, R2]), - emqttd_router:del_route(R3), - [] = lists:sort(emqttd_router:match(<<"a/b/c">>)). + emqx_router:add_route(R3), + emqx_router:del_routes([R1, R2]), + emqx_router:del_route(R3), + [] = lists:sort(emqx_router:match(<<"a/b/c">>)). router_print(_) -> Routes = [#mqtt_route{topic = <<"a/b/c">>, node = node()}, #mqtt_route{topic = <<"#">>, node = node()}, #mqtt_route{topic = <<"+/#">>, node = node()}], - emqttd_router:add_routes(Routes), - emqttd_router:print(<<"a/b/c">>). + emqx_router:add_routes(Routes), + emqx_router:print(<<"a/b/c">>). router_unused(_) -> - gen_server:call(emqttd_router, bad_call), - gen_server:cast(emqttd_router, bad_msg), - emqttd_router ! bad_info. + gen_server:call(emqx_router, bad_call), + gen_server:cast(emqx_router, bad_msg), + emqx_router ! bad_info. recv_loop(Msgs) -> receive @@ -346,17 +350,17 @@ recv_loop(Msgs) -> %%-------------------------------------------------------------------- start_session(_) -> - {ok, ClientPid} = emqttd_mock_client:start_link(<<"clientId">>), - {ok, SessPid} = emqttd_mock_client:start_session(ClientPid), - Message = emqttd_message:make(<<"clientId">>, 2, <<"topic">>, <<"hello">>), + {ok, ClientPid} = emqx_mock_client:start_link(<<"clientId">>), + {ok, SessPid} = emqx_mock_client:start_session(ClientPid), + Message = emqx_message:make(<<"clientId">>, 2, <<"topic">>, <<"hello">>), Message1 = Message#mqtt_message{pktid = 1}, - emqttd_session:publish(SessPid, Message1), - emqttd_session:pubrel(SessPid, 1), - emqttd_session:subscribe(SessPid, [{<<"topic/session">>, [{qos, 2}]}]), - Message2 = emqttd_message:make(<<"clientId">>, 1, <<"topic/session">>, <<"test">>), - emqttd_session:publish(SessPid, Message2), - emqttd_session:unsubscribe(SessPid, [{<<"topic/session">>, []}]), - emqttd_mock_client:stop(ClientPid). + emqx_session:publish(SessPid, Message1), + emqx_session:pubrel(SessPid, 1), + emqx_session:subscribe(SessPid, [{<<"topic/session">>, [{qos, 2}]}]), + Message2 = emqx_message:make(<<"clientId">>, 1, <<"topic/session">>, <<"test">>), + emqx_session:publish(SessPid, Message2), + emqx_session:unsubscribe(SessPid, [{<<"topic/session">>, []}]), + emqx_mock_client:stop(ClientPid). %%-------------------------------------------------------------------- %% Broker Group @@ -368,55 +372,55 @@ hook_unhook(_) -> %% Metric Group %%-------------------------------------------------------------------- inc_dec_metric(_) -> - emqttd_metrics:inc(gauge, 'messages/retained', 10), - emqttd_metrics:dec(gauge, 'messages/retained', 10). + emqx_metrics:inc(gauge, 'messages/retained', 10), + emqx_metrics:dec(gauge, 'messages/retained', 10). %%-------------------------------------------------------------------- %% Stats Group %%-------------------------------------------------------------------- set_get_stat(_) -> - emqttd_stats:setstat('retained/max', 99), - 99 = emqttd_stats:getstat('retained/max'). + emqx_stats:setstat('retained/max', 99), + 99 = emqx_stats:getstat('retained/max'). %%-------------------------------------------------------------------- %% Hook Test %%-------------------------------------------------------------------- add_delete_hook(_) -> - ok = emqttd:hook(test_hook, fun ?MODULE:hook_fun1/1, []), - ok = emqttd:hook(test_hook, {tag, fun ?MODULE:hook_fun2/1}, []), - {error, already_hooked} = emqttd:hook(test_hook, {tag, fun ?MODULE:hook_fun2/1}, []), + ok = emqx:hook(test_hook, fun ?MODULE:hook_fun1/1, []), + ok = emqx:hook(test_hook, {tag, fun ?MODULE:hook_fun2/1}, []), + {error, already_hooked} = emqx:hook(test_hook, {tag, fun ?MODULE:hook_fun2/1}, []), Callbacks = [{callback, undefined, fun ?MODULE:hook_fun1/1, [], 0}, {callback, tag, fun ?MODULE:hook_fun2/1, [], 0}], - Callbacks = emqttd_hooks:lookup(test_hook), - ok = emqttd:unhook(test_hook, fun ?MODULE:hook_fun1/1), - ct:print("Callbacks: ~p~n", [emqttd_hooks:lookup(test_hook)]), - ok = emqttd:unhook(test_hook, {tag, fun ?MODULE:hook_fun2/1}), - {error, not_found} = emqttd:unhook(test_hook1, {tag, fun ?MODULE:hook_fun2/1}), - [] = emqttd_hooks:lookup(test_hook), + Callbacks = emqx_hooks:lookup(test_hook), + ok = emqx:unhook(test_hook, fun ?MODULE:hook_fun1/1), + ct:print("Callbacks: ~p~n", [emqx_hooks:lookup(test_hook)]), + ok = emqx:unhook(test_hook, {tag, fun ?MODULE:hook_fun2/1}), + {error, not_found} = emqx:unhook(test_hook1, {tag, fun ?MODULE:hook_fun2/1}), + [] = emqx_hooks:lookup(test_hook), - ok = emqttd:hook(emqttd_hook, fun ?MODULE:hook_fun1/1, [], 9), - ok = emqttd:hook(emqttd_hook, {"tag", fun ?MODULE:hook_fun2/1}, [], 8), + ok = emqx:hook(emqx_hook, fun ?MODULE:hook_fun1/1, [], 9), + ok = emqx:hook(emqx_hook, {"tag", fun ?MODULE:hook_fun2/1}, [], 8), Callbacks2 = [{callback, "tag", fun ?MODULE:hook_fun2/1, [], 8}, {callback, undefined, fun ?MODULE:hook_fun1/1, [], 9}], - Callbacks2 = emqttd_hooks:lookup(emqttd_hook), - ok = emqttd:unhook(emqttd_hook, fun ?MODULE:hook_fun1/1), - ok = emqttd:unhook(emqttd_hook, {"tag", fun ?MODULE:hook_fun2/1}), - [] = emqttd_hooks:lookup(emqttd_hook). + Callbacks2 = emqx_hooks:lookup(emqx_hook), + ok = emqx:unhook(emqx_hook, fun ?MODULE:hook_fun1/1), + ok = emqx:unhook(emqx_hook, {"tag", fun ?MODULE:hook_fun2/1}), + [] = emqx_hooks:lookup(emqx_hook). run_hooks(_) -> - ok = emqttd:hook(foldl_hook, fun ?MODULE:hook_fun3/4, [init]), - ok = emqttd:hook(foldl_hook, {tag, fun ?MODULE:hook_fun3/4}, [init]), - ok = emqttd:hook(foldl_hook, fun ?MODULE:hook_fun4/4, [init]), - ok = emqttd:hook(foldl_hook, fun ?MODULE:hook_fun5/4, [init]), - {stop, [r3, r2]} = emqttd:run_hooks(foldl_hook, [arg1, arg2], []), - {ok, []} = emqttd:run_hooks(unknown_hook, [], []), + ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun3/4, [init]), + ok = emqx:hook(foldl_hook, {tag, fun ?MODULE:hook_fun3/4}, [init]), + ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun4/4, [init]), + ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun5/4, [init]), + {stop, [r3, r2]} = emqx:run_hooks(foldl_hook, [arg1, arg2], []), + {ok, []} = emqx:run_hooks(unknown_hook, [], []), - ok = emqttd:hook(foreach_hook, fun ?MODULE:hook_fun6/2, [initArg]), - ok = emqttd:hook(foreach_hook, {tag, fun ?MODULE:hook_fun6/2}, [initArg]), - ok = emqttd:hook(foreach_hook, fun ?MODULE:hook_fun7/2, [initArg]), - ok = emqttd:hook(foreach_hook, fun ?MODULE:hook_fun8/2, [initArg]), - stop = emqttd:run_hooks(foreach_hook, [arg]). + ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun6/2, [initArg]), + ok = emqx:hook(foreach_hook, {tag, fun ?MODULE:hook_fun6/2}, [initArg]), + ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun7/2, [initArg]), + ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun8/2, [initArg]), + stop = emqx:run_hooks(foreach_hook, [arg]). hook_fun1([]) -> ok. hook_fun2([]) -> {ok, []}. @@ -440,7 +444,7 @@ request_status(_) -> false -> not_running; {value, _Val} -> running end, - Status = iolist_to_binary(io_lib:format("Node ~s is ~s~nemqttd is ~s", + Status = iolist_to_binary(io_lib:format("Node ~s is ~s~nemqx is ~s", [node(), InternalStatus, AppStatus])), Url = "http://127.0.0.1:8080/status", {ok, {{"HTTP/1.1", 200, "OK"}, _, Return}} = @@ -462,7 +466,7 @@ request_publish(_) -> UnSubParams = "{\"topic\" : \"a\/b\/c\", \"client_id\" :\"random\"}", ?assert(connect_emqttd_pubsub_(post, "api/v2/mqtt/unsubscribe", UnSubParams, auth_header_("", ""))). -connect_emqttd_pubsub_(Method, Api, Params, Auth) -> +connect_emqx_publish_(Method, Api, Params, Auth) -> Url = "http://127.0.0.1:8080/" ++ Api, case httpc:request(Method, {Url, [Auth], ?CONTENT_TYPE, Params}, [], []) of {error, socket_closed_remotely} -> @@ -479,33 +483,34 @@ auth_header_(User, Pass) -> Encoded = base64:encode_to_string(lists:append([User,":",Pass])), {"Authorization","Basic " ++ Encoded}. +%%TODO: ... websocket_test(_) -> Conn = esockd_connection:new(esockd_transport, nil, []), Req = mochiweb_request:new(Conn, 'GET', "/mqtt", {1, 1}, mochiweb_headers:make([{"Sec-WebSocket-Key","Xn3fdKyc3qEXPuj2A3O+ZA=="}])), - ct:log("Req:~p", [Req]), - emqttd_http:handle_request(Req). + ct:log("Req:~p", [Req]). + %%emqx_http:handle_request(Req). set_alarms(_) -> AlarmTest = #mqtt_alarm{id = <<"1">>, severity = error, title="alarm title", summary="alarm summary"}, - emqttd_alarm:set_alarm(AlarmTest), - Alarms = emqttd_alarm:get_alarms(), + emqx_alarm:set_alarm(AlarmTest), + Alarms = emqx_alarm:get_alarms(), ?assertEqual(1, length(Alarms)), - emqttd_alarm:clear_alarm(<<"1">>), - [] = emqttd_alarm:get_alarms(). + emqx_alarm:clear_alarm(<<"1">>), + [] = emqx_alarm:get_alarms(). %%-------------------------------------------------------------------- %% Cli group %%-------------------------------------------------------------------- ctl_register_cmd(_) -> - emqttd_ctl:register_cmd(test_cmd, {?MODULE, test_cmd}), + emqx_ctl:register_cmd(test_cmd, {?MODULE, test_cmd}), erlang:yield(), timer:sleep(5), - [{?MODULE, test_cmd}] = emqttd_ctl:lookup(test_cmd), - emqttd_ctl:run(["test_cmd", "arg1", "arg2"]), - emqttd_ctl:unregister_cmd(test_cmd). + [{?MODULE, test_cmd}] = emqx_ctl:lookup(test_cmd), + emqx_ctl:run(["test_cmd", "arg1", "arg2"]), + emqx_ctl:unregister_cmd(test_cmd). test_cmd(["arg1", "arg2"]) -> ct:print("test_cmd is called"); @@ -514,55 +519,55 @@ test_cmd([]) -> io:format("test command"). cli_status(_) -> - emqttd_cli:status([]). + emqx_cli:status([]). cli_broker(_) -> - emqttd_cli:broker([]), - emqttd_cli:broker(["stats"]), - emqttd_cli:broker(["metrics"]), - emqttd_cli:broker(["pubsub"]). + emqx_cli:broker([]), + emqx_cli:broker(["stats"]), + emqx_cli:broker(["metrics"]), + emqx_cli:broker(["pubsub"]). cli_clients(_) -> - emqttd_cli:clients(["list"]), - emqttd_cli:clients(["show", "clientId"]), - emqttd_cli:clients(["kick", "clientId"]). + emqx_cli:clients(["list"]), + emqx_cli:clients(["show", "clientId"]), + emqx_cli:clients(["kick", "clientId"]). cli_sessions(_) -> - emqttd_cli:sessions(["list"]), - emqttd_cli:sessions(["list", "persistent"]), - emqttd_cli:sessions(["list", "transient"]), - emqttd_cli:sessions(["show", "clientId"]). + emqx_cli:sessions(["list"]), + emqx_cli:sessions(["list", "persistent"]), + emqx_cli:sessions(["list", "transient"]), + emqx_cli:sessions(["show", "clientId"]). cli_routes(_) -> - emqttd:subscribe(<<"topic/route">>), - emqttd_cli:routes(["list"]), - emqttd_cli:routes(["show", "topic/route"]), - emqttd:unsubscribe(<<"topic/route">>). + emqx:subscribe(<<"topic/route">>), + emqx_cli:routes(["list"]), + emqx_cli:routes(["show", "topic/route"]), + emqx:unsubscribe(<<"topic/route">>). cli_topics(_) -> - emqttd:subscribe(<<"topic">>), - emqttd_cli:topics(["list"]), - emqttd_cli:topics(["show", "topic"]), - emqttd:unsubscribe(<<"topic">>). + emqx:subscribe(<<"topic">>), + emqx_cli:topics(["list"]), + emqx_cli:topics(["show", "topic"]), + emqx:unsubscribe(<<"topic">>). cli_subscriptions(_) -> - emqttd_cli:subscriptions(["list"]), - emqttd_cli:subscriptions(["show", "clientId"]), - emqttd_cli:subscriptions(["add", "clientId", "topic", "2"]), - emqttd_cli:subscriptions(["del", "clientId", "topic"]). + emqx_cli:subscriptions(["list"]), + emqx_cli:subscriptions(["show", "clientId"]), + emqx_cli:subscriptions(["add", "clientId", "topic", "2"]), + emqx_cli:subscriptions(["del", "clientId", "topic"]). cli_plugins(_) -> - emqttd_cli:plugins(["list"]), - emqttd_cli:plugins(["load", "emqttd_plugin_template"]), - emqttd_cli:plugins(["unload", "emqttd_plugin_template"]). + emqx_cli:plugins(["list"]), + emqx_cli:plugins(["load", "emqx_plugin_template"]), + emqx_cli:plugins(["unload", "emqx_plugin_template"]). cli_bridges(_) -> - emqttd_cli:bridges(["list"]), - emqttd_cli:bridges(["start", "a@127.0.0.1", "topic"]), - emqttd_cli:bridges(["stop", "a@127.0.0.1", "topic"]). + emqx_cli:bridges(["list"]), + emqx_cli:bridges(["start", "a@127.0.0.1", "topic"]), + emqx_cli:bridges(["stop", "a@127.0.0.1", "topic"]). cli_listeners(_) -> - emqttd_cli:listeners([]). + emqx_cli:listeners([]). conflict_listeners(_) -> F = @@ -595,8 +600,8 @@ conflict_listeners(_) -> emqttc:disconnect(C2). cli_vm(_) -> - emqttd_cli:vm([]), - emqttd_cli:vm(["ports"]). + emqx_cli:vm([]), + emqx_cli:vm(["ports"]). cleanSession_validate(_) -> {ok, C1} = emqttc:start_link([{host, "localhost"}, @@ -605,6 +610,7 @@ cleanSession_validate(_) -> {clean_sess, false}]), timer:sleep(10), emqttc:subscribe(C1, <<"topic">>, qos0), + ok = emqx_cli:sessions(["list", "persistent"]), emqttc:disconnect(C1), {ok, Pub} = emqttc:start_link([{host, "localhost"}, {port, 1883}, @@ -617,15 +623,136 @@ cleanSession_validate(_) -> {client_id, <<"c1">>}, {clean_sess, false}]), timer:sleep(100), - Metrics = emqttd_metrics:all(), + Metrics = emqx_metrics:all(), + ct:log("Metrics:~p~n", [Metrics]), ?assertEqual(1, proplists:get_value('messages/qos0/sent', Metrics)), ?assertEqual(1, proplists:get_value('messages/qos0/received', Metrics)), emqttc:disconnect(Pub), emqttc:disconnect(C11). +cleanSession_validate1(_) -> + {ok, C1} = emqttc:start_link([{host, "localhost"}, + {port, 1883}, + {client_id, <<"c1">>}, + {clean_sess, true}]), + timer:sleep(10), + emqttc:subscribe(C1, <<"topic">>, qos1), + ok = emqx_cli:sessions(["list", "transient"]), + emqttc:disconnect(C1), + {ok, Pub} = emqttc:start_link([{host, "localhost"}, + {port, 1883}, + {client_id, <<"pub">>}]), + + emqttc:publish(Pub, <<"topic">>, <<"m1">>, [{qos, 1}]), + timer:sleep(10), + {ok, C11} = emqttc:start_link([{host, "localhost"}, + {port, 1883}, + {client_id, <<"c1">>}, + {clean_sess, false}]), + timer:sleep(100), + Metrics = emqx_metrics:all(), + ?assertEqual(0, proplists:get_value('messages/qos1/sent', Metrics)), + ?assertEqual(1, proplists:get_value('messages/qos1/received', Metrics)), + emqttc:disconnect(Pub), + emqttc:disconnect(C11). + get_api_lists(_Config) -> lists:foreach(fun request/1, ?GET_API). +request_publish(_) -> + emqttc:start_link([{host, "localhost"}, + {port, 1883}, + {client_id, <<"random">>}, + {clean_sess, false}]), + SubParams = "{\"qos\":1, \"topic\" : \"a\/b\/c\", \"client_id\" :\"random\"}", + ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/subscribe", SubParams, auth_header_("", ""))), + ok = emqx:subscribe(<<"a/b/c">>, self(), [{qos, 1}]), + Params = "{\"qos\":1, \"retain\":false, \"topic\" : \"a\/b\/c\", \"messages\" :\"hello\"}", + ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/publish", Params, auth_header_("", ""))), + ?assert(receive {dispatch, <<"a/b/c">>, _} -> true after 2 -> false end), + + UnSubParams = "{\"topic\" : \"a\/b\/c\", \"client_id\" :\"random\"}", + ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/unsubscribe", UnSubParams, auth_header_("", ""))). + +connect_emqx_pubsub_(Method, Api, Params, Auth) -> + Url = "http://127.0.0.1:8080/" ++ Api, + case httpc:request(Method, {Url, [Auth], ?CONTENT_TYPE, Params}, [], []) of + {error, socket_closed_remotely} -> + false; + {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> + true; + {ok, {{"HTTP/1.1", 400, _}, _, []}} -> + false; + {ok, {{"HTTP/1.1", 404, _}, _, []}} -> + false + end. + +request(Path) -> + http_get(get, Path). + +http_get(Method, Path) -> + req(Method, Path, []). + +http_put(Method, Path, Params) -> + req(Method, Path, format_for_upload(Params)). + +http_post(Method, Path, Params) -> + req(Method, Path, format_for_upload(Params)). + +req(Method, Path, Body) -> + Url = ?URL ++ Path, + Headers = auth_header_("", ""), + case httpc:request(Method, {Url, [Headers]}, [], []) of + {error, R} -> + ct:log("R:~p~n", [R]), + false; + {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> + true; + {ok, {{"HTTP/1.1", 400, _}, _, []}} -> + false; + {ok, {{"HTTP/1.1", 404, _}, _, []}} -> + false + end. + +format_for_upload(none) -> + <<"">>; +format_for_upload(List) -> + iolist_to_binary(mochijson2:encode(List)). + +ensure_ok(ok) -> ok; +ensure_ok({error, {already_started, _}}) -> ok. + +host() -> ct:print("!!!! Node: ~p~n", [node()]), [_, Host] = string:tokens(atom_to_list(node()), "@"), Host. + +wait_running(Node) -> + wait_running(Node, 30000). + +wait_running(Node, Timeout) when Timeout < 0 -> + throw({wait_timeout, Node}); + +wait_running(Node, Timeout) -> + case rpc:call(Node, emqx, is_running, [Node]) of + true -> ok; + false -> timer:sleep(100), + wait_running(Node, Timeout - 100) + end. + +slave(emqx, Node) -> + {ok, Slave} = slave:start(host(), Node, "-config ../../test/emqx_SUITE_data/slave.config " ++ ensure_slave()), + ct:log("Slave:~p~n", [Slave]), + rpc:call(Slave, application, ensure_all_started, [emqx]), + Slave; + +slave(node, Node) -> + {ok, N} = slave:start(host(), Node, ensure_slave()), + N. + +ensure_slave() -> + EbinDir = local_path(["ebin"]), + DepsDir = local_path(["deps", "*", "ebin"]), + RpcDir = local_path(["deps", "gen_rpc", "_build", "dev", "lib", "*", "ebin"]), + "-pa " ++ EbinDir ++ " -pa " ++ DepsDir ++ " -pa " ++ RpcDir. + change_opts(SslType) -> {ok, Listeners} = application:get_env(?APP, listeners), NewListeners = @@ -658,8 +785,8 @@ change_opts(SslType) -> application:set_env(?APP, listeners, NewListeners). generate_config() -> - Schema = cuttlefish_schema:files([local_path(["priv", "emq.schema"])]), - Conf = conf_parse:file([local_path(["etc", "emq.conf"])]), + Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), + Conf = conf_parse:file([local_path(["etc", "emqx.conf"])]), cuttlefish_generator:map(Schema, Conf). get_base_dir(Module) -> @@ -710,3 +837,4 @@ format_for_upload(none) -> <<"">>; format_for_upload(List) -> iolist_to_binary(mochijson2:encode(List)). + diff --git a/test/emqx_SUITE_data/acl.conf b/test/emqx_SUITE_data/acl.conf new file mode 100644 index 000000000..3cb3b8c52 --- /dev/null +++ b/test/emqx_SUITE_data/acl.conf @@ -0,0 +1,29 @@ +%%-------------------------------------------------------------------- +%% +%% [ACL](https://github.com/emqtt/emqttd/wiki/ACL) +%% +%% -type who() :: all | binary() | +%% {ipaddr, esockd_access:cidr()} | +%% {client, binary()} | +%% {user, binary()}. +%% +%% -type access() :: subscribe | publish | pubsub. +%% +%% -type topic() :: binary(). +%% +%% -type rule() :: {allow, all} | +%% {allow, who(), access(), list(topic())} | +%% {deny, all} | +%% {deny, who(), access(), list(topic())}. +%% +%%-------------------------------------------------------------------- + +{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. + +{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. + +{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. + +{allow, all}. + + diff --git a/test/emqx_SUITE_data/slave.config b/test/emqx_SUITE_data/slave.config new file mode 100644 index 000000000..dd3a5f32b --- /dev/null +++ b/test/emqx_SUITE_data/slave.config @@ -0,0 +1,60 @@ +[{emqx, + [{plugins_loaded_file,"loaded_plugins"}, + {plugins_etc_dir,"plugins/"}, + {broker_sys_interval,60}, + {cache_acl,true}, + {allow_anonymous,true}, + {license_file,"../../etc/emqx.lic"}, + {protocol,[{max_clientid_len,1024},{max_packet_size,65536}]}, + {client, + [{max_publish_rate,5},{idle_timeout,30000},{enable_stats,60000}]}, + {session, + [{max_subscriptions,0}, + {upgrade_qos,false}, + {max_inflight,32}, + {retry_interval,20000}, + {max_awaiting_rel,100}, + {await_rel_timeout,20000}, + {enable_stats,60000}, + {expiry_interval,7200000}]}, + {mqueue, + [{priority,[]}, + {type,simple}, + {max_length,infinity}, + {low_watermark,0.2}, + {high_watermark,0.6}, + {store_qos0,true}]}, + {pubsub,[{pool_size,8},{by_clientid,true},{async,true}]}, + {bridge,[{max_queue_len,10000},{ping_down_interval,1}]}, + {listeners, []}, + {sysmon, + [{long_gc,false}, + {long_schedule,240}, + {large_heap,8388608}, + {busy_port,false}, + {busy_dist_port,true}]}]}, + {sasl,[{sasl_error_logger,false}]}, + {lager, + [{error_logger_hwm,1000}, + {error_logger_redirect,true}, + {log_dir,"{{ platform_log_dir }}"}, + {handlers, + [{lager_console_backend,error}, + {lager_file_backend, + [{file,"{{ platform_log_dir }}/error.log"}, + {level,error}, + {size,10485760}, + {date,"$D0"}, + {count,5}]}, + {lager_syslog_backend,["emq",local0,error]}]}, + {crash_log,"{{ platform_log_dir }}/crash.log"}]}, + {gen_rpc, + [{socket_keepalive_count,2}, + {socket_keepalive_interval,5}, + {socket_keepalive_idle,5}, + {call_receive_timeout,15000}, + {authentication_timeout,5000}, + {send_timeout,5000}, + {connect_timeout,5000}, + {tcp_client_port,5369}, + {tcp_server_port,7369}]}]. diff --git a/test/emqttd_access_SUITE.erl b/test/emqx_access_SUITE.erl similarity index 85% rename from test/emqttd_access_SUITE.erl rename to test/emqx_access_SUITE.erl index 762ae6f40..d3eef923e 100644 --- a/test/emqttd_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -14,15 +14,15 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_access_SUITE). +-module(emqx_access_SUITE). -compile(export_all). --include("emqttd.hrl"). +-include("emqx.hrl"). --define(AC, emqttd_access_control). +-define(AC, emqx_access_control). --import(emqttd_access_rule, [compile/1, match/3]). +-import(emqx_access_rule, [compile/1, match/3]). all() -> [{group, access_control}, @@ -39,7 +39,7 @@ groups() -> match_rule]}]. init_per_group(access_control, Config) -> - application:load(emqttd), + application:load(emqx), prepare_config(), Config; @@ -59,8 +59,8 @@ prepare_config() -> Config = [{auth, anonymous, []}, {acl, internal, [{config, "access_SUITE_acl.conf"}, {nomatch, allow}]}], - write_config("access_SUITE_emqttd.conf", Config), - application:set_env(emqttd, conf, "access_SUITE_emqttd.conf"). + write_config("access_SUITE_emqx.conf", Config), + application:set_env(emqx, conf, "access_SUITE_emqx.conf"). write_config(Filename, Terms) -> file:write_file(Filename, [io_lib:format("~tp.~n", [Term]) || Term <- Terms]). @@ -87,31 +87,31 @@ end_per_testcase(_TestCase, _Config) -> ok. %%-------------------------------------------------------------------- -%% emqttd_access_control +%% emqx_access_control %%-------------------------------------------------------------------- reload_acl(_) -> - [] = ?AC:reload_acl(). + [ok] = ?AC:reload_acl(). register_mod(_) -> - ok = ?AC:register_mod(acl, emqttd_acl_test_mod, []), - {error, already_existed} = ?AC:register_mod(acl, emqttd_acl_test_mod, []), - [{emqttd_acl_test_mod, _, 0}] = ?AC:lookup_mods(acl), - ok = ?AC:register_mod(auth, emqttd_auth_anonymous_test_mod,[]), - ok = ?AC:register_mod(auth, emqttd_auth_dashboard, [], 99), - [{emqttd_auth_dashboard, _, 99}, - {emqttd_auth_anonymous_test_mod, _, 0}] = ?AC:lookup_mods(auth). + ok = ?AC:register_mod(acl, emqx_acl_test_mod, []), + {error, already_existed} = ?AC:register_mod(acl, emqx_acl_test_mod, []), + {emqx_acl_test_mod, _, 0} = hd(?AC:lookup_mods(acl)), + ok = ?AC:register_mod(auth, emqx_auth_anonymous_test_mod,[]), + ok = ?AC:register_mod(auth, emqx_auth_dashboard, [], 99), + [{emqx_auth_dashboard, _, 99}, + {emqx_auth_anonymous_test_mod, _, 0}] = ?AC:lookup_mods(auth). unregister_mod(_) -> - ok = ?AC:register_mod(acl, emqttd_acl_test_mod, []), - [{emqttd_acl_test_mod, _, 0}] = ?AC:lookup_mods(acl), - ok = ?AC:unregister_mod(acl, emqttd_acl_test_mod), + ok = ?AC:register_mod(acl, emqx_acl_test_mod, []), + {emqx_acl_test_mod, _, 0} = hd(?AC:lookup_mods(acl)), + ok = ?AC:unregister_mod(acl, emqx_acl_test_mod), timer:sleep(5), - [] = ?AC:lookup_mods(acl), - ok = ?AC:register_mod(auth, emqttd_auth_anonymous_test_mod,[]), - [{emqttd_auth_anonymous_test_mod, _, 0}] = ?AC:lookup_mods(auth), + {emqx_acl_internal, _, 0}= hd(?AC:lookup_mods(acl)), + ok = ?AC:register_mod(auth, emqx_auth_anonymous_test_mod,[]), + [{emqx_auth_anonymous_test_mod, _, 0}] = ?AC:lookup_mods(auth), - ok = ?AC:unregister_mod(auth, emqttd_auth_anonymous_test_mod), + ok = ?AC:unregister_mod(auth, emqx_auth_anonymous_test_mod), timer:sleep(5), [] = ?AC:lookup_mods(auth). @@ -126,7 +126,7 @@ check_acl(_) -> allow = ?AC:check_acl(User2, subscribe, <<"a/b/c">>). %%-------------------------------------------------------------------- -%% emqttd_access_rule +%% emqx_access_rule %%-------------------------------------------------------------------- compile_rule(_) -> diff --git a/test/emqx_access_SUITE_data/acl.conf b/test/emqx_access_SUITE_data/acl.conf new file mode 100644 index 000000000..03416f002 --- /dev/null +++ b/test/emqx_access_SUITE_data/acl.conf @@ -0,0 +1,16 @@ +{allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]}. + +{allow, {user, "testuser"}, subscribe, ["a/b/c", "d/e/f/#"]}. + +{allow, {user, "admin"}, pubsub, ["a/b/c", "d/e/f/#"]}. + +{allow, {client, "testClient"}, subscribe, ["testTopics/testClient"]}. + +{allow, all, subscribe, ["clients/%c"]}. + +{allow, all, pubsub, ["users/%u/#"]}. + +{deny, all, subscribe, ["$SYS/#", "#"]}. + +{deny, all}. + diff --git a/test/emqttd_acl_test_mod.erl b/test/emqx_acl_test_mod.erl similarity index 97% rename from test/emqttd_acl_test_mod.erl rename to test/emqx_acl_test_mod.erl index 08f1f9c94..4b9c14b14 100644 --- a/test/emqttd_acl_test_mod.erl +++ b/test/emqx_acl_test_mod.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_acl_test_mod). +-module(emqx_acl_test_mod). %% ACL callbacks -export([init/1, check_acl/2, reload_acl/1, description/0]). @@ -30,3 +30,4 @@ reload_acl(_State) -> description() -> "Test ACL Mod". + diff --git a/test/emqttd_auth_anonymous_test_mod.erl b/test/emqx_auth_anonymous_test_mod.erl similarity index 92% rename from test/emqttd_auth_anonymous_test_mod.erl rename to test/emqx_auth_anonymous_test_mod.erl index be6a14bf8..9fb52f419 100644 --- a/test/emqttd_auth_anonymous_test_mod.erl +++ b/test/emqx_auth_anonymous_test_mod.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_auth_anonymous_test_mod). +-module(emqx_auth_anonymous_test_mod). %% ACL callbacks -export([init/1, check/3, description/0]). @@ -26,4 +26,4 @@ check(_Client, _Password, _Opts) -> allow. description() -> - "Test emqttd_auth_anonymous Mod". + "Test emqx_auth_anonymous Mod". diff --git a/test/emqttd_auth_dashboard.erl b/test/emqx_auth_dashboard.erl similarity index 92% rename from test/emqttd_auth_dashboard.erl rename to test/emqx_auth_dashboard.erl index 49f54c377..eae612dd3 100644 --- a/test/emqttd_auth_dashboard.erl +++ b/test/emqx_auth_dashboard.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_auth_dashboard). +-module(emqx_auth_dashboard). %% Auth callbacks -export([init/1, check/3, description/0]). @@ -26,4 +26,5 @@ check(_Client, _Password, _Opts) -> allow. description() -> - "Test emqttd_auth_dashboard Mod". + "Test Auth Mod". + diff --git a/test/emqttd_config_SUITE.erl b/test/emqx_config_SUITE.erl similarity index 100% rename from test/emqttd_config_SUITE.erl rename to test/emqx_config_SUITE.erl diff --git a/test/emqx_inflight_SUITE.erl b/test/emqx_inflight_SUITE.erl new file mode 100644 index 000000000..c391d08ea --- /dev/null +++ b/test/emqx_inflight_SUITE.erl @@ -0,0 +1,63 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_inflight_SUITE). + +-author("Feng Lee "). + +-include_lib("eunit/include/eunit.hrl"). + +%% CT +-compile(export_all). + +all() -> [t_contain, t_lookup, t_insert, t_update, t_delete, t_window, + t_is_full, t_is_empty]. + +t_contain(_) -> + Inflight = emqx_inflight:new(0), + ?assertNot(Inflight:contain(k)), + Inflight1 = Inflight:insert(k, v), + ?assert(Inflight1:contain(k)). + +t_lookup(_) -> + Inflight = (emqx_inflight:new(0)):insert(k, v), + ?assertEqual(v, Inflight:lookup(k)). + +t_insert(_) -> + Inflight = ((emqx_inflight:new(0)):insert(k1, v1)):insert(k2, v2), + ?assertEqual(v2, Inflight:lookup(k2)). + +t_update(_) -> + Inflight = ((emqx_inflight:new(0)):insert(k, v1)):update(k, v2), + ?assertEqual(v2, Inflight:lookup(k)). + +t_delete(_) -> + Inflight = ((emqx_inflight:new(0)):insert(k, v1)):delete(k), + ?assert(Inflight:is_empty()). + +t_window(_) -> + ?assertEqual([], (emqx_inflight:new(10)):window()), + Inflight = ((emqx_inflight:new(0)):insert(1, 1)):insert(2, 2), + ?assertEqual([1, 2], Inflight:window()). + +t_is_full(_) -> + Inflight = ((emqx_inflight:new(1)):insert(k, v1)), + ?assert(Inflight:is_full()). + +t_is_empty(_) -> + Inflight = ((emqx_inflight:new(1)):insert(k, v1)), + ?assertNot(Inflight:is_empty()). + diff --git a/test/emqttd_lib_SUITE.erl b/test/emqx_lib_SUITE.erl similarity index 71% rename from test/emqttd_lib_SUITE.erl rename to test/emqx_lib_SUITE.erl index a808fbcc8..e39b8178d 100644 --- a/test/emqttd_lib_SUITE.erl +++ b/test/emqx_lib_SUITE.erl @@ -14,7 +14,9 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_lib_SUITE). +-module(emqx_lib_SUITE). + +-author("Feng Lee "). -include_lib("eunit/include/eunit.hrl"). @@ -28,13 +30,13 @@ {nodelay, true} ]). --define(PQ, priority_queue). +-define(PQ, emqx_pqueue). --define(BASE62, emqttd_base62). +-define(BASE62, emqx_base62). all() -> [{group, guid}, {group, opts}, {group, ?PQ}, {group, time}, - {group, base62}]. + {group, node}, {group, base62}]. groups() -> [{guid, [], [guid_gen, guid_hexstr, guid_base62]}, @@ -42,40 +44,41 @@ groups() -> {?PQ, [], [priority_queue_plen, priority_queue_out2]}, {time, [], [time_now_to_]}, + {node, [], [node_is_aliving, node_parse_name]}, {base62, [], [base62_encode]}]. %%-------------------------------------------------------------------- -%% emqttd_guid +%% emqx_guid %%-------------------------------------------------------------------- guid_gen(_) -> - Guid1 = emqttd_guid:gen(), - Guid2 = emqttd_guid:gen(), + Guid1 = emqx_guid:gen(), + Guid2 = emqx_guid:gen(), <<_:128>> = Guid1, true = (Guid2 >= Guid1), - {Ts1, _, 0} = emqttd_guid:new(), - Ts2 = emqttd_guid:timestamp(emqttd_guid:gen()), + {Ts1, _, 0} = emqx_guid:new(), + Ts2 = emqx_guid:timestamp(emqx_guid:gen()), true = Ts2 > Ts1. guid_hexstr(_) -> - Guid = emqttd_guid:gen(), - ?assertEqual(Guid, emqttd_guid:from_hexstr(emqttd_guid:to_hexstr(Guid))). + Guid = emqx_guid:gen(), + ?assertEqual(Guid, emqx_guid:from_hexstr(emqx_guid:to_hexstr(Guid))). guid_base62(_) -> - Guid = emqttd_guid:gen(), - ?assertEqual(Guid, emqttd_guid:from_base62(emqttd_guid:to_base62(Guid))). + Guid = emqx_guid:gen(), + ?assertEqual(Guid, emqx_guid:from_base62(emqx_guid:to_base62(Guid))). %%-------------------------------------------------------------------- -%% emqttd_opts +%% emqx_opts %%-------------------------------------------------------------------- opts_merge(_) -> - Opts = emqttd_misc:merge_opts(?SOCKOPTS, [raw, - binary, - {backlog, 1024}, - {nodelay, false}, - {max_clients, 1024}, - {acceptors, 16}]), + 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, @@ -135,13 +138,26 @@ priority_queue_out2(_) -> {empty, _Q7} = ?PQ:out(Q6). %%-------------------------------------------------------------------- -%% emqttd_time +%% emqx_time %%-------------------------------------------------------------------- time_now_to_(_) -> - emqttd_time:seed(), - emqttd_time:now_secs(), - emqttd_time:now_ms(). + emqx_time:seed(), + emqx_time:now_secs(), + emqx_time:now_ms(). + +%%-------------------------------------------------------------------- +%% emqx_node +%%-------------------------------------------------------------------- + +node_is_aliving(_) -> + io:format("Node: ~p~n", [node()]), + true = emqx_node:is_aliving(node()), + false = emqx_node:is_aliving('x@127.0.0.1'). + +node_parse_name(_) -> + 'a@127.0.0.1' = emqx_node:parse_name("a@127.0.0.1"), + 'b@127.0.0.1' = emqx_node:parse_name("b"). %%-------------------------------------------------------------------- %% base62 encode decode @@ -152,8 +168,8 @@ base62_encode(_) -> 100 = ?BASE62:decode(?BASE62:encode(100)), 9999 = ?BASE62:decode(?BASE62:encode(9999)), 65535 = ?BASE62:decode(?BASE62:encode(65535)), - <> = emqttd_guid:gen(), - <> = emqttd_guid:gen(), + <> = emqx_guid:gen(), + <> = emqx_guid:gen(), X = ?BASE62:decode(?BASE62:encode(X)), Y = ?BASE62:decode(?BASE62:encode(Y)). diff --git a/test/emqttd_mock_client.erl b/test/emqx_mock_client.erl similarity index 51% rename from test/emqttd_mock_client.erl rename to test/emqx_mock_client.erl index f4d26fa30..2b18c348f 100644 --- a/test/emqttd_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -1,27 +1,30 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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(emqttd_mock_client). +-module(emqx_mock_client). -behaviour(gen_server). -%% ------------------------------------------------------------------ -%% API Function Exports -%% ------------------------------------------------------------------ - -export([start_link/1, start_session/1, stop/1]). -%% ------------------------------------------------------------------ -%% gen_server Function Exports -%% ------------------------------------------------------------------ - -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, {clientid, session}). -%% ------------------------------------------------------------------ -%% API Function Definitions -%% ------------------------------------------------------------------ - start_link(ClientId) -> gen_server:start_link(?MODULE, [ClientId], []). @@ -31,15 +34,11 @@ start_session(CPid) -> stop(CPid) -> gen_server:call(CPid, stop). -%% ------------------------------------------------------------------ -%% gen_server Function Definitions -%% ------------------------------------------------------------------ - init([ClientId]) -> {ok, #state{clientid = ClientId}}. handle_call(start_session, _From, State = #state{clientid = ClientId}) -> - {ok, SessPid, _} = emqttd_sm:start_session(true, {ClientId, undefined}), + {ok, SessPid, _} = emqx_sm:start_session(true, {ClientId, undefined}), {reply, {ok, SessPid}, State#state{session = SessPid}}; handle_call(stop, _From, State) -> @@ -60,4 +59,3 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. - diff --git a/test/emqttd_mod_SUITE.erl b/test/emqx_mod_SUITE.erl similarity index 100% rename from test/emqttd_mod_SUITE.erl rename to test/emqx_mod_SUITE.erl diff --git a/test/emqttd_mqueue_SUITE.erl b/test/emqx_mqueue_SUITE.erl similarity index 74% rename from test/emqttd_mqueue_SUITE.erl rename to test/emqx_mqueue_SUITE.erl index 93ccc9833..db4bbbc73 100644 --- a/test/emqttd_mqueue_SUITE.erl +++ b/test/emqx_mqueue_SUITE.erl @@ -14,13 +14,17 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_mqueue_SUITE). +-module(emqx_mqueue_SUITE). + +-author("Feng Lee "). -compile(export_all). --include("emqttd.hrl"). +-include("emqx.hrl"). --define(Q, emqttd_mqueue). +-include_lib("eunit/include/eunit.hrl"). + +-define(Q, emqx_mqueue). all() -> [t_in, t_in_qos0, t_out, t_simple_mqueue, t_priority_mqueue, t_priority_mqueue2, t_infinity_priority_mqueue, @@ -30,24 +34,24 @@ t_in(_) -> Opts = [{max_length, 5}, {store_qos0, true}], Q = ?Q:new(<<"testQ">>, Opts, alarm_fun()), - true = ?Q:is_empty(Q), + ?assert(?Q:is_empty(Q)), Q1 = ?Q:in(#mqtt_message{}, Q), - 1 = ?Q:len(Q1), + ?assertEqual(1, ?Q:len(Q1)), Q2 = ?Q:in(#mqtt_message{qos = 1}, Q1), - 2 = ?Q:len(Q2), + ?assertEqual(2, ?Q:len(Q2)), Q3 = ?Q:in(#mqtt_message{qos = 2}, Q2), Q4 = ?Q:in(#mqtt_message{}, Q3), Q5 = ?Q:in(#mqtt_message{}, Q4), - 5 = ?Q:len(Q5). + ?assertEqual(5, ?Q:len(Q5)). t_in_qos0(_) -> Opts = [{max_length, 5}, {store_qos0, false}], Q = ?Q:new(<<"testQ">>, Opts, alarm_fun()), Q1 = ?Q:in(#mqtt_message{}, Q), - true = ?Q:is_empty(Q1), + ?assert(?Q:is_empty(Q1)), Q2 = ?Q:in(#mqtt_message{qos = 0}, Q1), - true = ?Q:is_empty(Q2). + ?assert(?Q:is_empty(Q2)). t_out(_) -> Opts = [{max_length, 5}, @@ -56,8 +60,8 @@ t_out(_) -> {empty, Q} = ?Q:out(Q), Q1 = ?Q:in(#mqtt_message{}, Q), {Value, Q2} = ?Q:out(Q1), - 0 = ?Q:len(Q2), - {value, #mqtt_message{}} = Value. + ?assertEqual(0, ?Q:len(Q2)), + ?assertEqual({value, #mqtt_message{}}, Value). t_simple_mqueue(_) -> Opts = [{type, simple}, @@ -66,18 +70,18 @@ t_simple_mqueue(_) -> {high_watermark, 0.6}, {store_qos0, false}], Q = ?Q:new("simple_queue", Opts, alarm_fun()), - simple = ?Q:type(Q), - 3 = ?Q:max_len(Q), - <<"simple_queue">> = ?Q:name(Q), - true = ?Q:is_empty(Q), + ?assertEqual(simple, ?Q:type(Q)), + ?assertEqual(3, ?Q:max_len(Q)), + ?assertEqual(<<"simple_queue">>, ?Q:name(Q)), + ?assert(?Q:is_empty(Q)), Q1 = ?Q:in(#mqtt_message{qos = 1, payload = <<"1">>}, Q), Q2 = ?Q:in(#mqtt_message{qos = 1, payload = <<"2">>}, Q1), Q3 = ?Q:in(#mqtt_message{qos = 1, payload = <<"3">>}, Q2), Q4 = ?Q:in(#mqtt_message{qos = 1, payload = <<"4">>}, Q3), - 3 = ?Q:len(Q4), + ?assertEqual(3, ?Q:len(Q4)), {{value, Msg}, Q5} = ?Q:out(Q4), - <<"2">> = Msg#mqtt_message.payload, - [{len, 2}, {max_len, 3}, {dropped, 1}] = ?Q:stats(Q5). + ?assertEqual(<<"2">>, Msg#mqtt_message.payload), + ?assertEqual([{len, 2}, {max_len, 3}, {dropped, 1}], ?Q:stats(Q5)). t_infinity_simple_mqueue(_) -> Opts = [{type, simple}, @@ -86,15 +90,15 @@ t_infinity_simple_mqueue(_) -> {high_watermark, 0.6}, {store_qos0, false}], Q = ?Q:new("infinity_simple_queue", Opts, alarm_fun()), - true = ?Q:is_empty(Q), - 0 = ?Q:max_len(Q), + ?assert(?Q:is_empty(Q)), + ?assertEqual(0, ?Q:max_len(Q)), Qx = lists:foldl(fun(I, AccQ) -> ?Q:in(#mqtt_message{qos = 1, payload = iolist_to_binary([I])}, AccQ) end, Q, lists:seq(1, 255)), - 255 = ?Q:len(Qx), - [{len, 255}, {max_len, 0}, {dropped, 0}] = ?Q:stats(Qx), + ?assertEqual(255, ?Q:len(Qx)), + ?assertEqual([{len, 255}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)), {{value, V}, _Qy} = ?Q:out(Qx), - <<1>> = V#mqtt_message.payload. + ?assertEqual(<<1>>, V#mqtt_message.payload). t_priority_mqueue(_) -> Opts = [{type, priority}, @@ -104,23 +108,23 @@ t_priority_mqueue(_) -> {high_watermark, 0.6}, {store_qos0, false}], Q = ?Q:new("priority_queue", Opts, alarm_fun()), - priority = ?Q:type(Q), - 3 = ?Q:max_len(Q), - <<"priority_queue">> = ?Q:name(Q), + ?assertEqual(priority, ?Q:type(Q)), + ?assertEqual(3, ?Q:max_len(Q)), + ?assertEqual(<<"priority_queue">>, ?Q:name(Q)), - true = ?Q:is_empty(Q), + ?assert(?Q:is_empty(Q)), Q1 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t1">>}, Q), Q2 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t">>}, Q1), Q3 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t2">>}, Q2), - 3 = ?Q:len(Q3), + ?assertEqual(3, ?Q:len(Q3)), Q4 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t1">>}, Q3), - 4 = ?Q:len(Q4), + ?assertEqual(4, ?Q:len(Q4)), Q5 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t1">>}, Q4), - 5 = ?Q:len(Q5), + ?assertEqual(5, ?Q:len(Q5)), Q6 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t1">>}, Q5), - 5 = ?Q:len(Q6), + ?assertEqual(5, ?Q:len(Q6)), {{value, Msg}, _Q7} = ?Q:out(Q6), - <<"t">> = Msg#mqtt_message.topic. + ?assertEqual(<<"t">>, Msg#mqtt_message.topic). t_infinity_priority_mqueue(_) -> Opts = [{type, priority}, @@ -128,14 +132,14 @@ t_infinity_priority_mqueue(_) -> {max_length, 0}, {store_qos0, false}], Q = ?Q:new("infinity_priority_queue", Opts, alarm_fun()), - 0 = ?Q:max_len(Q), + ?assertEqual(0, ?Q:max_len(Q)), Qx = lists:foldl(fun(I, AccQ) -> AccQ1 = ?Q:in(#mqtt_message{topic = <<"t1">>, qos = 1, payload = iolist_to_binary([I])}, AccQ), ?Q:in(#mqtt_message{topic = <<"t">>, qos = 1, payload = iolist_to_binary([I])}, AccQ1) end, Q, lists:seq(1, 255)), - 510 = ?Q:len(Qx), - [{len, 510}, {max_len, 0}, {dropped, 0}] = ?Q:stats(Qx). + ?assertEqual(510, ?Q:len(Qx)), + ?assertEqual([{len, 510}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)). t_priority_mqueue2(_) -> Opts = [{type, priority}, @@ -144,14 +148,14 @@ t_priority_mqueue2(_) -> {high_watermark, 0.6}, {store_qos0, false}], Q = ?Q:new("priority_queue2_test", Opts, alarm_fun()), - 2 = ?Q:max_len(Q), + ?assertEqual(2, ?Q:max_len(Q)), Q1 = ?Q:in(#mqtt_message{topic = <<"x">>, qos = 1, payload = <<1>>}, Q), Q2 = ?Q:in(#mqtt_message{topic = <<"x">>, qos = 1, payload = <<2>>}, Q1), Q3 = ?Q:in(#mqtt_message{topic = <<"y">>, qos = 1, payload = <<3>>}, Q2), Q4 = ?Q:in(#mqtt_message{topic = <<"y">>, qos = 1, payload = <<4>>}, Q3), - 4 = ?Q:len(Q4), + ?assertEqual(4, ?Q:len(Q4)), {{value, _Val}, Q5} = ?Q:out(Q4), - 3 = ?Q:len(Q5). + ?assertEqual(3, ?Q:len(Q5)). alarm_fun() -> fun(_, _) -> alarm_fun() end. diff --git a/test/emqttd_net_SUITE.erl b/test/emqx_net_SUITE.erl similarity index 89% rename from test/emqttd_net_SUITE.erl rename to test/emqx_net_SUITE.erl index 78abb50c9..dd3ea0015 100644 --- a/test/emqttd_net_SUITE.erl +++ b/test/emqx_net_SUITE.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_net_SUITE). +-module(emqx_net_SUITE). %% CT -compile(export_all). @@ -28,13 +28,13 @@ groups() -> [{keepalive, [], [t_keepalive]}]. %%-------------------------------------------------------------------- t_keepalive(_) -> - {ok, KA} = emqttd_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), + {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), [resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). keepalive_recv(KA, Acc) -> receive {keepalive, timeout} -> - case emqttd_keepalive:check(KA) of + case emqx_keepalive:check(KA) of {ok, KA1} -> keepalive_recv(KA1, [resumed | Acc]); {error, timeout} -> [timeout | Acc] end diff --git a/test/emqttd_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl similarity index 78% rename from test/emqttd_protocol_SUITE.erl rename to test/emqx_protocol_SUITE.erl index 21428f0c7..4cf21b73a 100644 --- a/test/emqttd_protocol_SUITE.erl +++ b/test/emqx_protocol_SUITE.erl @@ -14,17 +14,17 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_protocol_SUITE). +-module(emqx_protocol_SUITE). -compile(export_all). --import(emqttd_serializer, [serialize/1]). +-include("emqx.hrl"). --include("emqttd.hrl"). +-include("emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). --include("emqttd_protocol.hrl"). +-import(emqx_serializer, [serialize/1]). all() -> [{group, parser}, @@ -73,7 +73,7 @@ groups() -> %%-------------------------------------------------------------------- parse_connect(_) -> - Parser = emqttd_parser:initial_state(), + Parser = emqx_parser:initial_state(), %% CONNECT(Q0, R0, D0, ClientId=mosqpub/10451-iMac.loca, ProtoName=MQIsdp, ProtoVsn=3, CleanSess=true, KeepAlive=60, Username=undefined, Password=undefined) V31ConnBin = <<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>>, {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT, @@ -84,7 +84,7 @@ parse_connect(_) -> proto_name = <<"MQIsdp">>, client_id = <<"mosqpub/10451-iMac.loca">>, clean_sess = true, - keep_alive = 60}}, <<>>} = emqttd_parser:parse(V31ConnBin, Parser), + keep_alive = 60}}, <<>>} = emqx_parser:parse(V31ConnBin, Parser), %% CONNECT(Q0, R0, D0, ClientId=mosqpub/10451-iMac.loca, ProtoName=MQTT, ProtoVsn=4, CleanSess=true, KeepAlive=60, Username=undefined, Password=undefined) V311ConnBin = <<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>>, {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT, @@ -95,7 +95,7 @@ parse_connect(_) -> proto_name = <<"MQTT">>, client_id = <<"mosqpub/10451-iMac.loca">>, clean_sess = true, - keep_alive = 60 } }, <<>>} = emqttd_parser:parse(V311ConnBin, Parser), + keep_alive = 60 } }, <<>>} = emqx_parser:parse(V311ConnBin, Parser), %% CONNECT(Qos=0, Retain=false, Dup=false, ClientId="", ProtoName=MQTT, ProtoVsn=4, CleanSess=true, KeepAlive=60) V311ConnWithoutClientId = <<16,12,0,4,77,81,84,84,4,2,0,60,0,0>>, @@ -107,7 +107,7 @@ parse_connect(_) -> proto_name = <<"MQTT">>, client_id = <<>>, clean_sess = true, - keep_alive = 60 } }, <<>>} = emqttd_parser:parse(V311ConnWithoutClientId, Parser), + keep_alive = 60 } }, <<>>} = emqx_parser:parse(V311ConnWithoutClientId, Parser), %%CONNECT(Q0, R0, D0, ClientId=mosqpub/10452-iMac.loca, ProtoName=MQIsdp, ProtoVsn=3, CleanSess=true, KeepAlive=60, %% Username=test, Password=******, Will(Qos=1, Retain=false, Topic=/will, Msg=willmsg)) ConnBinWithWill = <<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,112,117,98,108,105,99>>, @@ -126,18 +126,18 @@ parse_connect(_) -> will_topic = <<"/will">>, will_msg = <<"willmsg">>, username = <<"test">>, - password = <<"public">>}}, <<>>} = emqttd_parser:parse(ConnBinWithWill, Parser), + password = <<"public">>}}, <<>>} = emqx_parser:parse(ConnBinWithWill, Parser), ok. parse_bridge(_) -> - Parser = emqttd_parser:initial_state(), + Parser = emqx_parser:initial_state(), Data = <<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, 67,58,50,57,58,50,66,58,55,55,58,53,50,47,115,116,97,116,101,0,1,48>>, %% CONNECT(Q0, R0, D0, ClientId=C_00:0C:29:2B:77:52, ProtoName=MQIsdp, ProtoVsn=131, CleanSess=false, KeepAlive=60, %% Username=undefined, Password=undefined, Will(Q1, R1, Topic=$SYS/broker/connection/C_00:0C:29:2B:77:52/state, Msg=0)) - {ok, #mqtt_packet{variable = Variable}, <<>>} = emqttd_parser:parse(Data, Parser), + {ok, #mqtt_packet{variable = Variable}, <<>>} = emqx_parser:parse(Data, Parser), #mqtt_packet_connect{client_id = <<"C_00:0C:29:2B:77:52">>, proto_ver = 16#03, proto_name = <<"MQIsdp">>, @@ -150,7 +150,7 @@ parse_bridge(_) -> will_msg = <<"0">>} = Variable. parse_publish(_) -> - Parser = emqttd_parser:initial_state(), + Parser = emqx_parser:initial_state(), %%PUBLISH(Qos=1, Retain=false, Dup=false, TopicName=a/b/c, PacketId=1, Payload=<<"hahah">>) PubBin = <<50,14,0,5,97,47,98,47,99,0,1,104,97,104,97,104>>, {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, @@ -159,7 +159,7 @@ parse_publish(_) -> retain = false}, variable = #mqtt_packet_publish{topic_name = <<"a/b/c">>, packet_id = 1}, - payload = <<"hahah">> }, <<>>} = emqttd_parser:parse(PubBin, Parser), + payload = <<"hahah">> }, <<>>} = emqx_parser:parse(PubBin, Parser), %PUBLISH(Qos=0, Retain=false, Dup=false, TopicName=xxx/yyy, PacketId=undefined, Payload=<<"hello">>) %DISCONNECT(Qos=0, Retain=false, Dup=false) @@ -170,43 +170,43 @@ parse_publish(_) -> retain = false}, variable = #mqtt_packet_publish{topic_name = <<"xxx/yyy">>, packet_id = undefined}, - payload = <<"hello">> }, <<224,0>>} = emqttd_parser:parse(PubBin1, Parser), + payload = <<"hello">> }, <<224,0>>} = emqx_parser:parse(PubBin1, Parser), {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT, dup = false, qos = 0, - retain = false}}, <<>>} = emqttd_parser:parse(<<224, 0>>, Parser). + retain = false}}, <<>>} = emqx_parser:parse(<<224, 0>>, Parser). parse_puback(_) -> - Parser = emqttd_parser:initial_state(), + Parser = emqx_parser:initial_state(), %%PUBACK(Qos=0, Retain=false, Dup=false, PacketId=1) {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK, dup = false, qos = 0, - retain = false}}, <<>>} = emqttd_parser:parse(<<64,2,0,1>>, Parser). + retain = false}}, <<>>} = emqx_parser:parse(<<64,2,0,1>>, Parser). parse_pubrec(_) -> - Parser = emqttd_parser:initial_state(), + Parser = emqx_parser:initial_state(), %%PUBREC(Qos=0, Retain=false, Dup=false, PacketId=1) {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC, dup = false, qos = 0, - retain = false}}, <<>>} = emqttd_parser:parse(<<5:4,0:4,2,0,1>>, Parser). + retain = false}}, <<>>} = emqx_parser:parse(<<5:4,0:4,2,0,1>>, Parser). parse_pubrel(_) -> - Parser = emqttd_parser:initial_state(), + Parser = emqx_parser:initial_state(), {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, dup = false, qos = 1, - retain = false}}, <<>>} = emqttd_parser:parse(<<6:4,2:4,2,0,1>>, Parser). + retain = false}}, <<>>} = emqx_parser:parse(<<6:4,2:4,2,0,1>>, Parser). parse_pubcomp(_) -> - Parser = emqttd_parser:initial_state(), + Parser = emqx_parser:initial_state(), {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP, dup = false, qos = 0, - retain = false}}, <<>>} = emqttd_parser:parse(<<7:4,0:4,2,0,1>>, Parser). + retain = false}}, <<>>} = emqx_parser:parse(<<7:4,0:4,2,0,1>>, Parser). parse_subscribe(_) -> - Parser = emqttd_parser:initial_state(), + Parser = emqx_parser:initial_state(), %% SUBSCRIBE(Q1, R0, D0, PacketId=2, TopicTable=[{<<"TopicA">>,2}]) {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, dup = false, @@ -214,10 +214,10 @@ parse_subscribe(_) -> retain = false}, variable = #mqtt_packet_subscribe{packet_id = 2, topic_table = [{<<"TopicA">>,2}]} }, <<>>} - = emqttd_parser:parse(<<130,11,0,2,0,6,84,111,112,105,99,65,2>>, Parser). + = emqx_parser:parse(<<130,11,0,2,0,6,84,111,112,105,99,65,2>>, Parser). parse_unsubscribe(_) -> - Parser = emqttd_parser:initial_state(), + Parser = emqx_parser:initial_state(), %% UNSUBSCRIBE(Q1, R0, D0, PacketId=2, TopicTable=[<<"TopicA">>]) {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, dup = false, @@ -225,24 +225,24 @@ parse_unsubscribe(_) -> retain = false}, variable = #mqtt_packet_unsubscribe{packet_id = 2, topics = [<<"TopicA">>]}}, <<>>} - = emqttd_parser:parse(<<162,10,0,2,0,6,84,111,112,105,99,65>>, Parser). + = emqx_parser:parse(<<162,10,0,2,0,6,84,111,112,105,99,65>>, Parser). parse_pingreq(_) -> - Parser = emqttd_parser:initial_state(), + Parser = emqx_parser:initial_state(), {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PINGREQ, dup = false, qos = 0, retain = false}}, <<>>} - = emqttd_parser:parse(<>, Parser). + = emqx_parser:parse(<>, Parser). parse_disconnect(_) -> - Parser = emqttd_parser:initial_state(), + Parser = emqx_parser:initial_state(), %DISCONNECT(Qos=0, Retain=false, Dup=false) Bin = <<224, 0>>, {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT, dup = false, qos = 0, - retain = false}}, <<>>} = emqttd_parser:parse(Bin, Parser). + retain = false}}, <<>>} = emqx_parser:parse(Bin, Parser). %%-------------------------------------------------------------------- %% Serialize Cases @@ -305,72 +305,72 @@ long_payload() -> %%-------------------------------------------------------------------- packet_proto_name(_) -> - ?assertEqual(<<"MQIsdp">>, emqttd_packet:protocol_name(3)), - ?assertEqual(<<"MQTT">>, emqttd_packet:protocol_name(4)). + ?assertEqual(<<"MQIsdp">>, emqx_packet:protocol_name(3)), + ?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(4)). packet_type_name(_) -> - ?assertEqual('CONNECT', emqttd_packet:type_name(?CONNECT)), - ?assertEqual('UNSUBSCRIBE', emqttd_packet:type_name(?UNSUBSCRIBE)). + ?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT)), + ?assertEqual('UNSUBSCRIBE', emqx_packet:type_name(?UNSUBSCRIBE)). packet_connack_name(_) -> - ?assertEqual('CONNACK_ACCEPT', emqttd_packet:connack_name(?CONNACK_ACCEPT)), - ?assertEqual('CONNACK_PROTO_VER', emqttd_packet:connack_name(?CONNACK_PROTO_VER)), - ?assertEqual('CONNACK_INVALID_ID', emqttd_packet:connack_name(?CONNACK_INVALID_ID)), - ?assertEqual('CONNACK_SERVER', emqttd_packet:connack_name(?CONNACK_SERVER)), - ?assertEqual('CONNACK_CREDENTIALS', emqttd_packet:connack_name(?CONNACK_CREDENTIALS)), - ?assertEqual('CONNACK_AUTH', emqttd_packet:connack_name(?CONNACK_AUTH)). + ?assertEqual('CONNACK_ACCEPT', emqx_packet:connack_name(?CONNACK_ACCEPT)), + ?assertEqual('CONNACK_PROTO_VER', emqx_packet:connack_name(?CONNACK_PROTO_VER)), + ?assertEqual('CONNACK_INVALID_ID', emqx_packet:connack_name(?CONNACK_INVALID_ID)), + ?assertEqual('CONNACK_SERVER', emqx_packet:connack_name(?CONNACK_SERVER)), + ?assertEqual('CONNACK_CREDENTIALS', emqx_packet:connack_name(?CONNACK_CREDENTIALS)), + ?assertEqual('CONNACK_AUTH', emqx_packet:connack_name(?CONNACK_AUTH)). packet_format(_) -> - io:format("~s", [emqttd_packet:format(?CONNECT_PACKET(#mqtt_packet_connect{}))]), - io:format("~s", [emqttd_packet:format(?CONNACK_PACKET(?CONNACK_SERVER))]), - io:format("~s", [emqttd_packet:format(?PUBLISH_PACKET(?QOS_1, 1))]), - io:format("~s", [emqttd_packet:format(?PUBLISH_PACKET(?QOS_2, <<"topic">>, 10, <<"payload">>))]), - io:format("~s", [emqttd_packet:format(?PUBACK_PACKET(?PUBACK, 98))]), - io:format("~s", [emqttd_packet:format(?PUBREL_PACKET(99))]), - io:format("~s", [emqttd_packet:format(?SUBSCRIBE_PACKET(15, [{<<"topic">>, ?QOS0}, {<<"topic1">>, ?QOS1}]))]), - io:format("~s", [emqttd_packet:format(?SUBACK_PACKET(40, [?QOS0, ?QOS1]))]), - io:format("~s", [emqttd_packet:format(?UNSUBSCRIBE_PACKET(89, [<<"t">>, <<"t2">>]))]), - io:format("~s", [emqttd_packet:format(?UNSUBACK_PACKET(90))]). + 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))]), + io:format("~s", [emqx_packet:format(?PUBLISH_PACKET(?QOS_2, <<"topic">>, 10, <<"payload">>))]), + io:format("~s", [emqx_packet:format(?PUBACK_PACKET(?PUBACK, 98))]), + io:format("~s", [emqx_packet:format(?PUBREL_PACKET(99))]), + io:format("~s", [emqx_packet:format(?SUBSCRIBE_PACKET(15, [{<<"topic">>, ?QOS0}, {<<"topic1">>, ?QOS1}]))]), + io:format("~s", [emqx_packet:format(?SUBACK_PACKET(40, [?QOS0, ?QOS1]))]), + io:format("~s", [emqx_packet:format(?UNSUBSCRIBE_PACKET(89, [<<"t">>, <<"t2">>]))]), + io:format("~s", [emqx_packet:format(?UNSUBACK_PACKET(90))]). %%-------------------------------------------------------------------- %% Message Cases %%-------------------------------------------------------------------- message_make(_) -> - Msg = emqttd_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), + Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), ?assertEqual(0, Msg#mqtt_message.qos), - Msg1 = emqttd_message:make(<<"clientid">>, qos2, <<"topic">>, <<"payload">>), + Msg1 = emqx_message:make(<<"clientid">>, qos2, <<"topic">>, <<"payload">>), ?assert(is_binary(Msg1#mqtt_message.id)), ?assertEqual(2, Msg1#mqtt_message.qos). message_from_packet(_) -> - Msg = emqttd_message:from_packet(?PUBLISH_PACKET(1, <<"topic">>, 10, <<"payload">>)), + Msg = emqx_message:from_packet(?PUBLISH_PACKET(1, <<"topic">>, 10, <<"payload">>)), ?assertEqual(1, Msg#mqtt_message.qos), ?assertEqual(10, Msg#mqtt_message.pktid), ?assertEqual(<<"topic">>, Msg#mqtt_message.topic), - WillMsg = emqttd_message:from_packet(#mqtt_packet_connect{will_flag = true, + WillMsg = emqx_message:from_packet(#mqtt_packet_connect{will_flag = true, will_topic = <<"WillTopic">>, will_msg = <<"WillMsg">>}), ?assertEqual(<<"WillTopic">>, WillMsg#mqtt_message.topic), ?assertEqual(<<"WillMsg">>, WillMsg#mqtt_message.payload), - Msg2 = emqttd_message:from_packet(<<"username">>, <<"clientid">>, + Msg2 = emqx_message:from_packet(<<"username">>, <<"clientid">>, ?PUBLISH_PACKET(1, <<"topic">>, 20, <<"payload">>)), ?assertEqual({<<"clientid">>, <<"username">>}, Msg2#mqtt_message.from), - io:format("~s", [emqttd_message:format(Msg2)]). + io:format("~s", [emqx_message:format(Msg2)]). message_flag(_) -> Pkt = ?PUBLISH_PACKET(1, <<"t">>, 2, <<"payload">>), - Msg2 = emqttd_message:from_packet(<<"clientid">>, Pkt), - Msg3 = emqttd_message:set_flag(retain, Msg2), - Msg4 = emqttd_message:set_flag(dup, Msg3), + Msg2 = emqx_message:from_packet(<<"clientid">>, Pkt), + Msg3 = emqx_message:set_flag(retain, Msg2), + Msg4 = emqx_message:set_flag(dup, Msg3), ?assert(Msg4#mqtt_message.dup), ?assert(Msg4#mqtt_message.retain), - Msg5 = emqttd_message:set_flag(Msg4), - Msg6 = emqttd_message:unset_flag(dup, Msg5), - Msg7 = emqttd_message:unset_flag(retain, Msg6), + Msg5 = emqx_message:set_flag(Msg4), + Msg6 = emqx_message:unset_flag(dup, Msg5), + Msg7 = emqx_message:unset_flag(retain, Msg6), ?assertNot(Msg7#mqtt_message.dup), ?assertNot(Msg7#mqtt_message.retain), - emqttd_message:unset_flag(Msg7), - emqttd_message:to_packet(Msg7). + emqx_message:unset_flag(Msg7), + emqx_message:to_packet(Msg7). diff --git a/test/emqttd_topic_SUITE.erl b/test/emqx_topic_SUITE.erl similarity index 96% rename from test/emqttd_topic_SUITE.erl rename to test/emqx_topic_SUITE.erl index b1ea4d8ed..ffdc65c1a 100644 --- a/test/emqttd_topic_SUITE.erl +++ b/test/emqx_topic_SUITE.erl @@ -14,15 +14,17 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_topic_SUITE). +-module(emqx_topic_SUITE). + +-author("Feng Lee "). -include_lib("eunit/include/eunit.hrl"). %% CT -compile(export_all). --import(emqttd_topic, [wildcard/1, match/2, validate/1, triples/1, join/1, - words/1, systop/1, feed_var/3, parse/1, parse/2]). +-import(emqx_topic, [wildcard/1, match/2, validate/1, triples/1, join/1, + words/1, systop/1, feed_var/3, parse/1, parse/2]). -define(N, 10000). @@ -185,5 +187,6 @@ t_parse(_) -> ?assertEqual({<<"topic">>, [{share, <<"group">>}]}, parse(<<"$share/group/topic">>)), ?assertEqual({<<"topic">>, [local]}, parse(<<"$local/topic">>)), ?assertEqual({<<"topic">>, [{share, '$queue'}, local]}, parse(<<"$local/$queue/topic">>)), - ?assertEqual({<<"/a/b/c">>, [{share, <<"group">>}, local]}, parse(<<"$local/$share/group//a/b/c">>)). + ?assertEqual({<<"/a/b/c">>, [{share, <<"group">>}, local]}, parse(<<"$local/$share/group//a/b/c">>)), + ?assertEqual({<<"topic">>, [fastlane]}, parse(<<"$fastlane/topic">>)). diff --git a/test/emqttd_trie_SUITE.erl b/test/emqx_trie_SUITE.erl similarity index 92% rename from test/emqttd_trie_SUITE.erl rename to test/emqx_trie_SUITE.erl index a81a132f5..6562575a7 100644 --- a/test/emqttd_trie_SUITE.erl +++ b/test/emqx_trie_SUITE.erl @@ -14,13 +14,15 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_trie_SUITE). +-module(emqx_trie_SUITE). + +-author("Feng Lee "). -compile(export_all). --include("emqttd_trie.hrl"). +-include("emqx_trie.hrl"). --define(TRIE, emqttd_trie). +-define(TRIE, emqx_trie). -include_lib("eunit/include/eunit.hrl"). @@ -81,10 +83,10 @@ t_match2(_) -> t_match3(_) -> Topics = [<<"d/#">>, <<"a/b/c">>, <<"a/b/+">>, <<"a/#">>, <<"#">>, <<"$SYS/#">>], - mnesia:transaction(fun() -> [emqttd_trie:insert(Topic) || Topic <- Topics] end), - Matched = mnesia:async_dirty(fun emqttd_trie:match/1, [<<"a/b/c">>]), + mnesia:transaction(fun() -> [emqx_trie:insert(Topic) || Topic <- Topics] end), + Matched = mnesia:async_dirty(fun emqx_trie:match/1, [<<"a/b/c">>]), ?assertEqual(4, length(Matched)), - SysMatched = mnesia:async_dirty(fun emqttd_trie:match/1, [<<"$SYS/a/b/c">>]), + SysMatched = mnesia:async_dirty(fun emqx_trie:match/1, [<<"$SYS/a/b/c">>]), ?assertEqual([<<"$SYS/#">>], SysMatched). t_delete(_) -> diff --git a/test/emqttd_vm_SUITE.erl b/test/emqx_vm_SUITE.erl similarity index 87% rename from test/emqttd_vm_SUITE.erl rename to test/emqx_vm_SUITE.erl index ef0ac2946..3263b85e7 100644 --- a/test/emqttd_vm_SUITE.erl +++ b/test/emqx_vm_SUITE.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_vm_SUITE). +-module(emqx_vm_SUITE). -compile(export_all). @@ -102,73 +102,73 @@ all() -> get_process_limit]. load(_Config) -> - Loads = emqttd_vm:loads(), + Loads = emqx_vm:loads(), [{load1, _}, {load5, _}, {load15, _}] = Loads. systeminfo(_Config) -> - Keys = [Key || {Key, _} <- emqttd_vm:get_system_info()], + Keys = [Key || {Key, _} <- emqx_vm:get_system_info()], ?SYSTEM_INFO = Keys. mem_info(_Config) -> application:ensure_all_started(os_mon), - MemInfo = emqttd_vm:mem_info(), + MemInfo = emqx_vm:mem_info(), [{total_memory, _}, {used_memory, _}]= MemInfo, application:stop(os_mon). process_list(_Config) -> Pid = self(), - ProcessInfo = emqttd_vm:get_process_list(), + ProcessInfo = emqx_vm:get_process_list(), true = lists:member({pid, Pid}, lists:concat(ProcessInfo)). process_info(_Config) -> - ProcessInfos = emqttd_vm:get_process_info(), + ProcessInfos = emqx_vm:get_process_info(), ProcessInfo = lists:last(ProcessInfos), Keys = [K || {K, _V}<- ProcessInfo], ?PROCESS_INFO = Keys. process_gc(_Config) -> - ProcessGcs = emqttd_vm:get_process_gc(), + ProcessGcs = emqx_vm:get_process_gc(), ProcessGc = lists:last(ProcessGcs), Keys = [K || {K, _V}<- ProcessGc], ?PROCESS_GC = Keys. get_ets_list(_Config) -> ets:new(test, [named_table]), - Ets = emqttd_vm:get_ets_list(), + Ets = emqx_vm:get_ets_list(), true = lists:member(test, Ets). get_ets_info(_Config) -> ets:new(test, [named_table]), - [] = emqttd_vm:get_ets_info(test1), - EtsInfo = emqttd_vm:get_ets_info(test), + [] = emqx_vm:get_ets_info(test1), + EtsInfo = emqx_vm:get_ets_info(test), test = proplists:get_value(name, EtsInfo). get_ets_object(_Config) -> ets:new(test, [named_table]), ets:insert(test, {k, v}), - [{k, v}] = emqttd_vm:get_ets_object(test). + [{k, v}] = emqx_vm:get_ets_object(test). get_port_types(_Config) -> - emqttd_vm:get_port_types(). + emqx_vm:get_port_types(). get_port_info(_Config) -> - emqttd_vm:get_port_info(). + emqx_vm:get_port_info(). scheduler_usage(_Config) -> - emqttd_vm:scheduler_usage(5000). + emqx_vm:scheduler_usage(5000). get_memory(_Config) -> - emqttd_vm:get_memory(). + emqx_vm:get_memory(). microsecs(_Config) -> - emqttd_vm:microsecs(). + emqx_vm:microsecs(). schedulers(_Config) -> - emqttd_vm:schedulers(). + emqx_vm:schedulers(). get_process_group_leader_info(_Config) -> - emqttd_vm:get_process_group_leader_info(self()). + emqx_vm:get_process_group_leader_info(self()). get_process_limit(_Config) -> - emqttd_vm:get_process_limit(). + emqx_vm:get_process_limit(). diff --git a/test/ws_client.erl b/test/ws_client.erl new file mode 100644 index 000000000..f049e3256 --- /dev/null +++ b/test/ws_client.erl @@ -0,0 +1,77 @@ +-module(ws_client). + +-behaviour(websocket_client_handler). + +-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}) -> + lager:info("Client received frame~p", [Frame]), + {ok, State#state{buffer = [Frame|Buffer]}}; +websocket_handle(Frame, _, State = #state{waiting = From}) -> + lager: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. From 735211fd023d46702f6a5fd9c18ae955bcb072ac Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 9 Oct 2017 23:29:05 +0800 Subject: [PATCH 002/520] Merge with EMQ X project --- src/emqx_sup.erl | 9 +++------ src/emqx_ws.erl | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index 63c52e4ef..cdcf03255 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -36,17 +36,14 @@ start_link() -> supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). --spec(start_child(atom(), worker | supervisor) -> startchild_ret()). -start_child(Mod, Type) when Type == worker orelse Type == supervisor -> - start_child(?CHILD(Mod, Type)). -spec(start_child(supervisor:child_spec()) -> startchild_ret()). start_child(ChildSpec) when is_tuple(ChildSpec) -> supervisor:start_child(?SUPERVISOR, ChildSpec). --spec(start_child(Mod::atom(), Type :: worker | supervisor) -> {ok, pid()}). -start_child(Mod, Type) when is_atom(Mod) and is_atom(Type) -> - supervisor:start_child(?MODULE, ?CHILD(Mod, Type)). +-spec(start_child(atom(), worker | supervisor) -> startchild_ret()). +start_child(Mod, Type) when Type == worker orelse Type == supervisor -> + start_child(?CHILD(Mod, Type)). -spec(stop_child(supervisor:child_id()) -> ok | {error, any()}). stop_child(ChildId) -> diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl index 1e2870a42..4f5e5b4b3 100644 --- a/src/emqx_ws.erl +++ b/src/emqx_ws.erl @@ -18,7 +18,7 @@ -author("Feng Lee "). --include("emqx_protocol.hrl"). +-include("emqx_mqtt.hrl"). -import(proplists, [get_value/3]). From 2354a3dd5d3fa03a0a17ad57700f7c424175d53c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 10 Oct 2017 15:50:40 +0800 Subject: [PATCH 003/520] Fix the building errors --- etc/emqx.conf | 8 +- src/emqx.app.src | 11 + src/{emqx_mgmt.erl => emqx_mgmt.erl.bk} | 0 ...emqx_rest_api.erl => emqx_rest_api.erl.bk} | 0 src/priority_queue.erl | 259 ++++++++++++++++++ 5 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 src/emqx.app.src rename src/{emqx_mgmt.erl => emqx_mgmt.erl.bk} (100%) rename src/{emqx_rest_api.erl => emqx_rest_api.erl.bk} (100%) create mode 100644 src/priority_queue.erl diff --git a/etc/emqx.conf b/etc/emqx.conf index bcf92d751..e24f3fab5 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -563,13 +563,13 @@ listener.wss.external.certfile = {{ platform_etc_dir }}/certs/cert.pem ##-------------------------------------------------------------------- ## HTTP Management API Listener -listener.api.mgmt = 127.0.0.1:8080 +## listener.api.mgmt = 127.0.0.1:8080 -listener.api.mgmt.acceptors = 4 +## listener.api.mgmt.acceptors = 4 -listener.api.mgmt.max_clients = 64 +## listener.api.mgmt.max_clients = 64 -listener.api.mgmt.access.1 = allow all +## listener.api.mgmt.access.1 = allow all ##------------------------------------------------------------------- ## System Monitor diff --git a/src/emqx.app.src b/src/emqx.app.src new file mode 100644 index 000000000..eece8df33 --- /dev/null +++ b/src/emqx.app.src @@ -0,0 +1,11 @@ +{application,emqx, + [{description,"EMQ X Broker"}, + {vsn,"2.4"}, + {modules,[]}, + {registered,[emqx_sup]}, + {applications,[kernel,stdlib,gproc,gen_rpc,lager,esockd,mochiweb, lager_syslog,pbkdf2,bcrypt,clique,jsx]}, + {env,[]}, + {mod,{emqx_app,[]}}, + {maintainers,["Feng Lee "]}, + {licenses,["Apache-2.0"]}, + {links,[{"Github","https://github.com/emqtt/emqttd"}]}]}. diff --git a/src/emqx_mgmt.erl b/src/emqx_mgmt.erl.bk similarity index 100% rename from src/emqx_mgmt.erl rename to src/emqx_mgmt.erl.bk diff --git a/src/emqx_rest_api.erl b/src/emqx_rest_api.erl.bk similarity index 100% rename from src/emqx_rest_api.erl rename to src/emqx_rest_api.erl.bk diff --git a/src/priority_queue.erl b/src/priority_queue.erl new file mode 100644 index 000000000..7147b0bb4 --- /dev/null +++ b/src/priority_queue.erl @@ -0,0 +1,259 @@ +%% 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. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2007-2015 Pivotal Software, Inc. All rights reserved. +%% + +%% Priority queues have essentially the same interface as ordinary +%% queues, except that a) there is an in/3 that takes a priority, and +%% b) we have only implemented the core API we need. +%% +%% Priorities should be integers - the higher the value the higher the +%% priority - but we don't actually check that. +%% +%% in/2 inserts items with priority 0. +%% +%% We optimise the case where a priority queue is being used just like +%% an ordinary queue. When that is the case we represent the priority +%% queue as an ordinary queue. We could just call into the 'queue' +%% module for that, but for efficiency we implement the relevant +%% functions directly in here, thus saving on inter-module calls and +%% eliminating a level of boxing. +%% +%% When the queue contains items with non-zero priorities, it is +%% represented as a sorted kv list with the inverted Priority as the +%% key and an ordinary queue as the value. Here again we use our own +%% ordinary queue implemention for efficiency, often making recursive +%% calls into the same function knowing that ordinary queues represent +%% a base case. + +-module(priority_queue). + +-export([new/0, is_queue/1, is_empty/1, len/1, plen/2, to_list/1, from_list/1, + in/2, in/3, out/1, out/2, out_p/1, join/2, filter/2, fold/3, highest/1]). + +%%---------------------------------------------------------------------------- + +-ifdef(use_specs). + +-type(q() :: pqueue()). +-type(priority() :: integer() | 'infinity'). +-type(squeue() :: {queue, [any()], [any()], non_neg_integer()}). +-type(pqueue() :: squeue() | {pqueue, [{priority(), squeue()}]}). + +-export_type([q/0]). + +-spec(new/0 :: () -> pqueue()). +-spec(is_queue/1 :: (any()) -> boolean()). +-spec(is_empty/1 :: (pqueue()) -> boolean()). +-spec(len/1 :: (pqueue()) -> non_neg_integer()). +-spec(plen/2 :: (priority(), pqueue()) -> non_neg_integer()). +-spec(to_list/1 :: (pqueue()) -> [{priority(), any()}]). +-spec(from_list/1 :: ([{priority(), any()}]) -> pqueue()). +-spec(in/2 :: (any(), pqueue()) -> pqueue()). +-spec(in/3 :: (any(), priority(), pqueue()) -> pqueue()). +-spec(out/1 :: (pqueue()) -> {empty | {value, any()}, pqueue()}). +-spec(out_p/1 :: (pqueue()) -> {empty | {value, any(), priority()}, pqueue()}). +-spec(join/2 :: (pqueue(), pqueue()) -> pqueue()). +-spec(filter/2 :: (fun ((any()) -> boolean()), pqueue()) -> pqueue()). +-spec(fold/3 :: + (fun ((any(), priority(), A) -> A), A, pqueue()) -> A). +-spec(highest/1 :: (pqueue()) -> priority() | 'empty'). + +-endif. + +%%---------------------------------------------------------------------------- + +new() -> + {queue, [], [], 0}. + +is_queue({queue, R, F, L}) when is_list(R), is_list(F), is_integer(L) -> + true; +is_queue({pqueue, Queues}) when is_list(Queues) -> + lists:all(fun ({infinity, Q}) -> is_queue(Q); + ({P, Q}) -> is_integer(P) andalso is_queue(Q) + end, Queues); +is_queue(_) -> + false. + +is_empty({queue, [], [], 0}) -> + true; +is_empty(_) -> + false. + +len({queue, _R, _F, L}) -> + L; +len({pqueue, Queues}) -> + lists:sum([len(Q) || {_, Q} <- Queues]). + +plen(0, {queue, _R, _F, L}) -> + L; +plen(_, {queue, _R, _F, _}) -> + 0; +plen(P, {pqueue, Queues}) -> + case lists:keysearch(maybe_negate_priority(P), 1, Queues) of + {value, {_, Q}} -> len(Q); + false -> 0 + end. + +to_list({queue, In, Out, _Len}) when is_list(In), is_list(Out) -> + [{0, V} || V <- Out ++ lists:reverse(In, [])]; +to_list({pqueue, Queues}) -> + [{maybe_negate_priority(P), V} || {P, Q} <- Queues, + {0, V} <- to_list(Q)]. + +from_list(L) -> + lists:foldl(fun ({P, E}, Q) -> in(E, P, Q) end, new(), L). + +in(Item, Q) -> + in(Item, 0, Q). + +in(X, 0, {queue, [_] = In, [], 1}) -> + {queue, [X], In, 2}; +in(X, 0, {queue, In, Out, Len}) when is_list(In), is_list(Out) -> + {queue, [X|In], Out, Len + 1}; +in(X, Priority, _Q = {queue, [], [], 0}) -> + in(X, Priority, {pqueue, []}); +in(X, Priority, Q = {queue, _, _, _}) -> + in(X, Priority, {pqueue, [{0, Q}]}); +in(X, Priority, {pqueue, Queues}) -> + P = maybe_negate_priority(Priority), + {pqueue, case lists:keysearch(P, 1, Queues) of + {value, {_, Q}} -> + lists:keyreplace(P, 1, Queues, {P, in(X, Q)}); + false when P == infinity -> + [{P, {queue, [X], [], 1}} | Queues]; + false -> + case Queues of + [{infinity, InfQueue} | Queues1] -> + [{infinity, InfQueue} | + lists:keysort(1, [{P, {queue, [X], [], 1}} | Queues1])]; + _ -> + lists:keysort(1, [{P, {queue, [X], [], 1}} | Queues]) + end + end}. + +out({queue, [], [], 0} = Q) -> + {empty, Q}; +out({queue, [V], [], 1}) -> + {{value, V}, {queue, [], [], 0}}; +out({queue, [Y|In], [], Len}) -> + [V|Out] = lists:reverse(In, []), + {{value, V}, {queue, [Y], Out, Len - 1}}; +out({queue, In, [V], Len}) when is_list(In) -> + {{value,V}, r2f(In, Len - 1)}; +out({queue, In,[V|Out], Len}) when is_list(In) -> + {{value, V}, {queue, In, Out, Len - 1}}; +out({pqueue, [{P, Q} | Queues]}) -> + {R, Q1} = out(Q), + NewQ = case is_empty(Q1) of + true -> case Queues of + [] -> {queue, [], [], 0}; + [{0, OnlyQ}] -> OnlyQ; + [_|_] -> {pqueue, Queues} + end; + false -> {pqueue, [{P, Q1} | Queues]} + end, + {R, NewQ}. + +out_p({queue, _, _, _} = Q) -> add_p(out(Q), 0); +out_p({pqueue, [{P, _} | _]} = Q) -> add_p(out(Q), maybe_negate_priority(P)). + +out(0, {queue, _, _, _} = Q) -> + out(Q); +out(Priority, {queue, _, _, _}) -> + erlang:error(badarg, [Priority]); +out(Priority, {pqueue, Queues}) -> + P = maybe_negate_priority(Priority), + case lists:keysearch(P, 1, Queues) of + {value, {_, Q}} -> + {R, Q1} = out(Q), + Queues1 = case is_empty(Q1) of + true -> lists:keydelete(P, 1, Queues); + false -> lists:keyreplace(P, 1, Queues, {P, Q1}) + end, + {R, case Queues1 of + [] -> {queue, [], [], 0}; + [{0, OnlyQ}] -> OnlyQ; + [_|_] -> {pqueue, Queues1} + end}; + false -> + {empty, {pqueue, Queues}} + end. + +add_p(R, P) -> case R of + {empty, Q} -> {empty, Q}; + {{value, V}, Q} -> {{value, V, P}, Q} + end. + +join(A, {queue, [], [], 0}) -> + A; +join({queue, [], [], 0}, B) -> + B; +join({queue, AIn, AOut, ALen}, {queue, BIn, BOut, BLen}) -> + {queue, BIn, AOut ++ lists:reverse(AIn, BOut), ALen + BLen}; +join(A = {queue, _, _, _}, {pqueue, BPQ}) -> + {Pre, Post} = + lists:splitwith(fun ({P, _}) -> P < 0 orelse P == infinity end, BPQ), + Post1 = case Post of + [] -> [ {0, A} ]; + [ {0, ZeroQueue} | Rest ] -> [ {0, join(A, ZeroQueue)} | Rest ]; + _ -> [ {0, A} | Post ] + end, + {pqueue, Pre ++ Post1}; +join({pqueue, APQ}, B = {queue, _, _, _}) -> + {Pre, Post} = + lists:splitwith(fun ({P, _}) -> P < 0 orelse P == infinity end, APQ), + Post1 = case Post of + [] -> [ {0, B} ]; + [ {0, ZeroQueue} | Rest ] -> [ {0, join(ZeroQueue, B)} | Rest ]; + _ -> [ {0, B} | Post ] + end, + {pqueue, Pre ++ Post1}; +join({pqueue, APQ}, {pqueue, BPQ}) -> + {pqueue, merge(APQ, BPQ, [])}. + +merge([], BPQ, Acc) -> + lists:reverse(Acc, BPQ); +merge(APQ, [], Acc) -> + lists:reverse(Acc, APQ); +merge([{P, A}|As], [{P, B}|Bs], Acc) -> + merge(As, Bs, [ {P, join(A, B)} | Acc ]); +merge([{PA, A}|As], Bs = [{PB, _}|_], Acc) when PA < PB orelse PA == infinity -> + merge(As, Bs, [ {PA, A} | Acc ]); +merge(As = [{_, _}|_], [{PB, B}|Bs], Acc) -> + merge(As, Bs, [ {PB, B} | Acc ]). + +filter(Pred, Q) -> fold(fun(V, P, Acc) -> + case Pred(V) of + true -> in(V, P, Acc); + false -> Acc + end + end, new(), Q). + +fold(Fun, Init, Q) -> case out_p(Q) of + {empty, _Q} -> Init; + {{value, V, P}, Q1} -> fold(Fun, Fun(V, P, Init), Q1) + end. + +highest({queue, [], [], 0}) -> empty; +highest({queue, _, _, _}) -> 0; +highest({pqueue, [{P, _} | _]}) -> maybe_negate_priority(P). + +r2f([], 0) -> {queue, [], [], 0}; +r2f([_] = R, 1) -> {queue, [], R, 1}; +r2f([X,Y], 2) -> {queue, [X], [Y], 2}; +r2f([X,Y|R], L) -> {queue, [X,Y], lists:reverse(R, []), L}. + +maybe_negate_priority(infinity) -> infinity; +maybe_negate_priority(P) -> -P. From 49d91cf6944b8c102275a5b374d626ec49fb39d7 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 31 Oct 2017 15:19:53 +0800 Subject: [PATCH 004/520] Rename 'message.offline' hook to 'message.dropped' --- src/emqx_pubsub.erl | 4 ++-- src/emqx_session.erl | 2 +- src/emqx_sm.erl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/emqx_pubsub.erl b/src/emqx_pubsub.erl index 541007975..f67c3c8e4 100644 --- a/src/emqx_pubsub.erl +++ b/src/emqx_pubsub.erl @@ -70,7 +70,7 @@ publish(Topic, Msg) -> emqx_router:match_local(Topic)), delivery(Msg)). route([], #mqtt_delivery{message = Msg}) -> - emqx_hooks:run('message.offline', [undefined, Msg]), + emqx_hooks:run('message.dropped', [undefined, Msg]), dropped(Msg#mqtt_message.topic), ignore; %% Dispatch on the local node @@ -98,7 +98,7 @@ forward(Node, To, Delivery) -> dispatch(Topic, Delivery = #mqtt_delivery{message = Msg, flows = Flows}) -> case subscribers(Topic) of [] -> - emqx_hooks:run('message.offline', [undefined, Msg]), + emqx_hooks:run('message.dropped', [undefined, Msg]), dropped(Topic), {ok, Delivery}; [Sub] -> %% optimize? dispatch(Sub, Topic, Msg), diff --git a/src/emqx_session.erl b/src/emqx_session.erl index c46f50b97..1a9bfb65e 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -695,7 +695,7 @@ is_awaiting_full(#state{awaiting_rel = AwaitingRel, max_awaiting_rel = MaxLen}) %% Enqueue message if the client has been disconnected dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) -> - case emqx_hooks:run('message.offline', [ClientId, Msg]) of + case emqx_hooks:run('message.dropped', [ClientId, Msg]) of ok -> enqueue_msg(Msg, State); stop -> State end; diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 1b652d600..8c9b1c8eb 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -117,7 +117,7 @@ dispatch(ClientId, Topic, Msg) -> Pid -> Pid ! {dispatch, Topic, Msg} catch error:badarg -> - emqx_hooks:run('message.offline', [ClientId, Msg]), + emqx_hooks:run('message.dropped', [ClientId, Msg]), ok %%TODO: How?? end. From 845c5eddc1c4069adfb04eeeca8ded9622edfcf2 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 21 Nov 2017 22:41:02 +0800 Subject: [PATCH 005/520] Merge with emqx --- src/emqx_rest_api.erl.bk | 551 --------------------------------------- 1 file changed, 551 deletions(-) delete mode 100644 src/emqx_rest_api.erl.bk diff --git a/src/emqx_rest_api.erl.bk b/src/emqx_rest_api.erl.bk deleted file mode 100644 index f04a7c538..000000000 --- a/src/emqx_rest_api.erl.bk +++ /dev/null @@ -1,551 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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_rest_api). - --include("emqx.hrl"). - --include("emqx_rest.hrl"). - --http_api({"^nodes/(.+?)/alarms/?$", 'GET', alarm_list, []}). - --http_api({"^nodes/(.+?)/clients/?$", 'GET', client_list, []}). --http_api({"^nodes/(.+?)/clients/(.+?)/?$", 'GET',client_list, []}). --http_api({"^clients/(.+?)/?$", 'GET', client, []}). --http_api({"^clients/(.+?)/?$", 'DELETE', kick_client, []}). --http_api({"^clients/(.+?)/clean_acl_cache?$", 'PUT', clean_acl_cache, [{<<"topic">>, binary}]}). - --http_api({"^routes?$", 'GET', route_list, []}). --http_api({"^routes/(.+?)/?$", 'GET', route, []}). - --http_api({"^nodes/(.+?)/sessions/?$", 'GET', session_list, []}). --http_api({"^nodes/(.+?)/sessions/(.+?)/?$", 'GET', session_list, []}). --http_api({"^sessions/(.+?)/?$", 'GET', session, []}). - --http_api({"^nodes/(.+?)/subscriptions/?$", 'GET', subscription_list, []}). --http_api({"^nodes/(.+?)/subscriptions/(.+?)/?$", 'GET', subscription_list, []}). --http_api({"^subscriptions/(.+?)/?$", 'GET', subscription, []}). - --http_api({"^mqtt/publish?$", 'POST', publish, [{<<"topic">>, binary}]}). --http_api({"^mqtt/subscribe?$", 'POST', subscribe, [{<<"client_id">>, binary},{<<"topic">>, binary}]}). --http_api({"^mqtt/unsubscribe?$", 'POST', unsubscribe, [{<<"client_id">>, binary},{<<"topic">>, binary}]}). - --http_api({"^management/nodes/?$", 'GET', brokers, []}). --http_api({"^management/nodes/(.+?)/?$", 'GET', broker, []}). --http_api({"^monitoring/nodes/?$", 'GET', nodes, []}). --http_api({"^monitoring/nodes/(.+?)/?$", 'GET', node, []}). --http_api({"^monitoring/listeners/?$", 'GET', listeners, []}). --http_api({"^monitoring/listeners/(.+?)/?$", 'GET', listener, []}). --http_api({"^monitoring/metrics/?$", 'GET', metrics, []}). --http_api({"^monitoring/metrics/(.+?)/?$", 'GET', metric, []}). --http_api({"^monitoring/stats/?$", 'GET', stats, []}). --http_api({"^monitoring/stats/(.+?)/?$", 'GET', stat, []}). - --http_api({"^nodes/(.+?)/plugins/?$", 'GET', plugin_list, []}). --http_api({"^nodes/(.+?)/plugins/(.+?)/?$", 'PUT', enabled, [{<<"active">>, bool}]}). - --http_api({"^configs/(.+?)/?$", 'PUT', modify_config, [{<<"key">>, binary}, {<<"value">>, binary}]}). --http_api({"^configs/?$", 'GET', config_list, []}). --http_api({"^nodes/(.+?)/configs/(.+?)/?$", 'PUT', modify_config, [{<<"key">>, binary}, {<<"value">>, binary}]}). --http_api({"^nodes/(.+?)/configs/?$", 'GET', config_list, []}). --http_api({"^nodes/(.+?)/plugin_configs/(.+?)/?$", 'GET', plugin_config_list, []}). --http_api({"^nodes/(.+?)/plugin_configs/(.+?)/?$", 'PUT', modify_plugin_config, []}). - --http_api({"^users/?$", 'GET', users, []}). --http_api({"^users/?$", 'POST', users, [{<<"username">>, binary}, - {<<"password">>, binary}, - {<<"tags">>, binary}]}). --http_api({"^users/(.+?)/?$", 'GET', users, []}). --http_api({"^users/(.+?)/?$", 'PUT', users, [{<<"tags">>, binary}]}). --http_api({"^users/(.+?)/?$", 'DELETE', users, []}). - --http_api({"^auth/?$", 'POST', auth, [{<<"username">>, binary}, {<<"password">>, binary}]}). --http_api({"^change_pwd/(.+?)/?$", 'PUT', change_pwd, [{<<"old_pwd">>, binary}, - {<<"new_pwd">>, binary}]}). - --import(proplists, [get_value/2, get_value/3]). - --export([alarm_list/3]). --export([client/3, client_list/3, client_list/4, kick_client/3, clean_acl_cache/3]). --export([route/3, route_list/2]). --export([session/3, session_list/3, session_list/4]). --export([subscription/3, subscription_list/3, subscription_list/4]). --export([nodes/2, node/3, brokers/2, broker/3, listeners/2, listener/3, metrics/2, metric/3, stats/2, stat/3]). --export([publish/2, subscribe/2, unsubscribe/2]). --export([plugin_list/3, enabled/4]). --export([modify_config/3, modify_config/4, config_list/2, config_list/3, - plugin_config_list/4, modify_plugin_config/4]). - --export([users/2,users/3, auth/2, change_pwd/3]). - -%%-------------------------------------------------------------------------- -%% alarm -%%-------------------------------------------------------------------------- -alarm_list('GET', _Req, _Node) -> - Alarms = emqx_mgmt:alarm_list(), - {ok, lists:map(fun alarm_row/1, Alarms)}. - -alarm_row(#mqtt_alarm{id = AlarmId, - severity = Severity, - title = Title, - summary = Summary, - timestamp = Timestamp}) -> - [{id, AlarmId}, - {severity, Severity}, - {title, l2b(Title)}, - {summary, l2b(Summary)}, - {occurred_at, l2b(strftime(Timestamp))}]. - -%%-------------------------------------------------------------------------- -%% client -%%-------------------------------------------------------------------------- -client('GET', _Params, Key) -> - Data = emqx_mgmt:client(l2b(Key)), - {ok, [{objects, [client_row(Row) || Row <- Data]}]}. - -client_list('GET', Params, Node) -> - {PageNo, PageSize} = page_params(Params), - Data = emqx_mgmt:client_list(l2a(Node), undefined, PageNo, PageSize), - Rows = get_value(result, Data), - TotalPage = get_value(totalPage, Data), - TotalNum = get_value(totalNum, Data), - {ok, [{current_page, PageNo}, - {page_size, PageSize}, - {total_num, TotalNum}, - {total_page, TotalPage}, - {objects, [client_row(Row) || Row <- Rows]}]}. - -client_list('GET', Params, Node, Key) -> - {PageNo, PageSize} = page_params(Params), - Data = emqx_mgmt:client_list(l2a(Node), l2b(Key), PageNo, PageSize), - {ok, [{objects, [client_row(Row) || Row <- Data]}]}. - -kick_client('DELETE', _Params, Key) -> - case emqx_mgmt:kick_client(l2b(Key)) of - true -> {ok, []}; - false -> {error, [{code, ?ERROR12}]} - end. - -clean_acl_cache('PUT', Params, Key0) -> - Topic = get_value(<<"topic">>, Params), - [Key | _] = string:tokens(Key0, "/"), - case emqx_mgmt:clean_acl_cache(l2b(Key), Topic) of - true -> {ok, []}; - false -> {error, [{code, ?ERROR12}]} - end. - -client_row(#mqtt_client{client_id = ClientId, - peername = {IpAddr, Port}, - username = Username, - clean_sess = CleanSess, - proto_ver = ProtoVer, - keepalive = KeepAlvie, - connected_at = ConnectedAt}) -> - [{client_id, ClientId}, - {username, Username}, - {ipaddress, l2b(ntoa(IpAddr))}, - {port, Port}, - {clean_sess, CleanSess}, - {proto_ver, ProtoVer}, - {keepalive, KeepAlvie}, - {connected_at, l2b(strftime(ConnectedAt))}]. - -%%-------------------------------------------------------------------------- -%% route -%%-------------------------------------------------------------------------- -route('GET', _Params, Key) -> - Data = emqx_mgmt:route(l2b(Key)), - {ok, [{objects, [route_row(Row) || Row <- Data]}]}. - -route_list('GET', Params) -> - {PageNo, PageSize} = page_params(Params), - Data = emqx_mgmt:route_list(undefined, PageNo, PageSize), - Rows = get_value(result, Data), - TotalPage = get_value(totalPage, Data), - TotalNum = get_value(totalNum, Data), - {ok, [{current_page, PageNo}, - {page_size, PageSize}, - {total_num, TotalNum}, - {total_page, TotalPage}, - {objects, [route_row(Row) || Row <- Rows]}]}. - -route_row(Route) when is_record(Route, mqtt_route) -> - [{topic, Route#mqtt_route.topic}, {node, Route#mqtt_route.node}]; - -route_row({Topic, Node}) -> - [{topic, Topic}, {node, Node}]. - -%%-------------------------------------------------------------------------- -%% session -%%-------------------------------------------------------------------------- -session('GET', _Params, Key) -> - Data = emqx_mgmt:session(l2b(Key)), - {ok, [{objects, [session_row(Row) || Row <- Data]}]}. - -session_list('GET', Params, Node) -> - {PageNo, PageSize} = page_params(Params), - Data = emqx_mgmt:session_list(l2a(Node), undefined, PageNo, PageSize), - Rows = get_value(result, Data), - TotalPage = get_value(totalPage, Data), - TotalNum = get_value(totalNum, Data), - {ok, [{current_page, PageNo}, - {page_size, PageSize}, - {total_num, TotalNum}, - {total_page, TotalPage}, - {objects, [session_row(Row) || Row <- Rows]}]}. - -session_list('GET', Params, Node, ClientId) -> - {PageNo, PageSize} = page_params(Params), - Data = emqx_mgmt:session_list(l2a(Node), l2b(ClientId), PageNo, PageSize), - {ok, [{objects, [session_row(Row) || Row <- Data]}]}. - -session_row({ClientId, _Pid, _Persistent, Session}) -> - Data = lists:append(Session, emqttd_stats:get_session_stats(ClientId)), - InfoKeys = [clean_sess, subscriptions, max_inflight, inflight_len, mqueue_len, - mqueue_dropped, awaiting_rel_len, deliver_msg,enqueue_msg, created_at], - [{client_id, ClientId} | [{Key, format(Key, get_value(Key, Data))} || Key <- InfoKeys]]. - -%%-------------------------------------------------------------------------- -%% subscription -%%-------------------------------------------------------------------------- -subscription('GET', _Params, Key) -> - Data = emqx_mgmt:subscription(l2b(Key)), - {ok, [{objects, [subscription_row(Row) || Row <- Data]}]}. - -subscription_list('GET', Params, Node) -> - {PageNo, PageSize} = page_params(Params), - Data = emqx_mgmt:subscription_list(l2a(Node), undefined, PageNo, PageSize), - Rows = get_value(result, Data), - TotalPage = get_value(totalPage, Data), - TotalNum = get_value(totalNum, Data), - {ok, [{current_page, PageNo}, - {page_size, PageSize}, - {total_num, TotalNum}, - {total_page, TotalPage}, - {objects, [subscription_row(Row) || Row <- Rows]}]}. - -subscription_list('GET', Params, Node, Key) -> - {PageNo, PageSize} = page_params(Params), - Data = emqx_mgmt:subscription_list(l2a(Node), l2b(Key), PageNo, PageSize), - {ok, [{objects, [subscription_row(Row) || Row <- Data]}]}. - -subscription_row({{Topic, SubPid}, Options}) when is_pid(SubPid) -> - subscription_row({{Topic, {undefined, SubPid}}, Options}); -subscription_row({{Topic, {SubId, SubPid}}, Options}) -> - Qos = proplists:get_value(qos, Options), - ClientId = case SubId of - undefined -> list_to_binary(pid_to_list(SubPid)); - SubId -> SubId - end, - [{client_id, ClientId}, {topic, Topic}, {qos, Qos}]. - -%%-------------------------------------------------------------------------- -%% management/monitoring -%%-------------------------------------------------------------------------- -nodes('GET', _Params) -> - Data = emqx_mgmt:nodes_info(), - {ok, Data}. - -node('GET', _Params, Node) -> - Data = emqx_mgmt:node_info(l2a(Node)), - {ok, Data}. - -brokers('GET', _Params) -> - Data = emqx_mgmt:brokers(), - {ok, [format_broker(Node, Broker) || {Node, Broker} <- Data]}. - -broker('GET', _Params, Node) -> - Data = emqx_mgmt:broker(l2a(Node)), - {ok, format_broker(Data)}. - -listeners('GET', _Params) -> - Data = emqx_mgmt:listeners(), - {ok, [{Node, format_listeners(Listeners, [])} || {Node, Listeners} <- Data]}. - -listener('GET', _Params, Node) -> - Data = emqx_mgmt:listener(l2a(Node)), - {ok, [format_listener(Listeners) || Listeners <- Data]}. - -metrics('GET', _Params) -> - Data = emqx_mgmt:metrics(), - {ok, Data}. - -metric('GET', _Params, Node) -> - Data = emqx_mgmt:metrics(l2a(Node)), - {ok, Data}. - -stats('GET', _Params) -> - Data = emqx_mgmt:stats(), - {ok, [Data]}. - -stat('GET', _Params, Node) -> - Data = emqx_mgmt:stats(l2a(Node)), - {ok, Data}. - -format_broker(Node, Broker) -> - OtpRel = "R" ++ erlang:system_info(otp_release) ++ "/" ++ erlang:system_info(version), - [{name, Node}, - {version, bin(get_value(version, Broker))}, - {sysdescr, bin(get_value(sysdescr, Broker))}, - {uptime, bin(get_value(uptime, Broker))}, - {datetime, bin(get_value(datetime, Broker))}, - {otp_release, l2b(OtpRel)}, - {node_status, 'Running'}]. - -format_broker(Broker) -> - OtpRel = "R" ++ erlang:system_info(otp_release) ++ "/" ++ erlang:system_info(version), - [{version, bin(get_value(version, Broker))}, - {sysdescr, bin(get_value(sysdescr, Broker))}, - {uptime, bin(get_value(uptime, Broker))}, - {datetime, bin(get_value(datetime, Broker))}, - {otp_release, l2b(OtpRel)}, - {node_status, 'Running'}]. - -format_listeners([], Acc) -> - Acc; -format_listeners([{Protocol, ListenOn, Info}| Listeners], Acc) -> - format_listeners(Listeners, [format_listener({Protocol, ListenOn, Info}) | Acc]). - -format_listener({Protocol, ListenOn, Info}) -> - Listen = l2b(esockd:to_string(ListenOn)), - lists:append([{protocol, Protocol}, {listen, Listen}], Info). - -%%-------------------------------------------------------------------------- -%% mqtt -%%-------------------------------------------------------------------------- -publish('POST', Params) -> - Topic = get_value(<<"topic">>, Params), - ClientId = get_value(<<"client_id">>, Params, http), - Payload = get_value(<<"payload">>, Params, <<>>), - Qos = get_value(<<"qos">>, Params, 0), - Retain = get_value(<<"retain">>, Params, false), - case emqx_mgmt:publish({ClientId, Topic, Payload, Qos, Retain}) of - ok -> - {ok, []}; - {error, Error} -> - {error, [{code, ?ERROR2}, {message, Error}]} - end. - -subscribe('POST', Params) -> - ClientId = get_value(<<"client_id">>, Params), - Topic = get_value(<<"topic">>, Params), - Qos = get_value(<<"qos">>, Params, 0), - case emqx_mgmt:subscribe({ClientId, Topic, Qos}) of - ok -> - {ok, []}; - {error, Error} -> - {error, [{code, ?ERROR2}, {message, Error}]} - end. - -unsubscribe('POST', Params) -> - ClientId = get_value(<<"client_id">>, Params), - Topic = get_value(<<"topic">>, Params), - case emqx_mgmt:unsubscribe({ClientId, Topic})of - ok -> - {ok, []}; - {error, Error} -> - {error, [{code, ?ERROR2}, {message, Error}]} - end. - -%%-------------------------------------------------------------------------- -%% plugins -%%-------------------------------------------------------------------------- -plugin_list('GET', _Params, Node) -> - Plugins = lists:map(fun plugin/1, emqx_mgmt:plugin_list(l2a(Node))), - {ok, Plugins}. - -enabled('PUT', Params, Node, PluginName) -> - Active = get_value(<<"active">>, Params), - case Active of - true -> - return(emqx_mgmt:plugin_load(l2a(Node), l2a(PluginName))); - false -> - return(emqx_mgmt:plugin_unload(l2a(Node), l2a(PluginName))) - end. - -return(Result) -> - case Result of - ok -> - {ok, []}; - {ok, _} -> - {ok, []}; - {error, already_started} -> - {error, [{code, ?ERROR10}, {message, <<"already_started">>}]}; - {error, not_started} -> - {error, [{code, ?ERROR11}, {message, <<"not_started">>}]}; - Error -> - lager:error("error:~p", [Error]), - {error, [{code, ?ERROR2}, {message, <<"unknown">>}]} - end. -plugin(#mqtt_plugin{name = Name, version = Ver, descr = Descr, - active = Active}) -> - [{name, Name}, - {version, iolist_to_binary(Ver)}, - {description, iolist_to_binary(Descr)}, - {active, Active}]. - -%%-------------------------------------------------------------------------- -%% modify config -%%-------------------------------------------------------------------------- -modify_config('PUT', Params, App) -> - Key = get_value(<<"key">>, Params, <<"">>), - Value = get_value(<<"value">>, Params, <<"">>), - case emqx_mgmt:modify_config(l2a(App), b2l(Key), b2l(Value)) of - true -> {ok, []}; - false -> {error, [{code, ?ERROR2}]} - end. - -modify_config('PUT', Params, Node, App) -> - Key = get_value(<<"key">>, Params, <<"">>), - Value = get_value(<<"value">>, Params, <<"">>), - case emqx_mgmt:modify_config(l2a(Node), l2a(App), b2l(Key), b2l(Value)) of - ok -> {ok, []}; - _ -> {error, [{code, ?ERROR2}]} - end. - -config_list('GET', _Params) -> - Data = emqx_mgmt:get_configs(), - {ok, [{Node, format_config(Config, [])} || {Node, Config} <- Data]}. - -config_list('GET', _Params, Node) -> - Data = emqx_mgmt:get_config(l2a(Node)), - {ok, [format_config(Config) || Config <- lists:reverse(Data)]}. - -plugin_config_list('GET', _Params, Node, App) -> - {ok, Data} = emqx_mgmt:get_plugin_config(l2a(Node), l2a(App)), - {ok, [format_plugin_config(Config) || Config <- lists:reverse(Data)]}. - -modify_plugin_config('PUT', Params, Node, App) -> - PluginName = l2a(App), - case emqx_mgmt:modify_plugin_config(l2a(Node), PluginName, Params) of - ok -> - Plugins = emqttd_plugins:list(), - {_, _, _, _, Status} = lists:keyfind(PluginName, 2, Plugins), - case Status of - true -> - emqttd_plugins:unload(PluginName), - timer:sleep(500), - emqttd_plugins:load(PluginName), - {ok, []}; - false -> - {ok, []} - end; - _ -> - {error, [{code, ?ERROR2}]} - end. - - -format_config([], Acc) -> - Acc; -format_config([{Key, Value, Datatpye, App}| Configs], Acc) -> - format_config(Configs, [format_config({Key, Value, Datatpye, App}) | Acc]). - -format_config({Key, Value, Datatpye, App}) -> - [{<<"key">>, l2b(Key)}, - {<<"value">>, l2b(Value)}, - {<<"datatpye">>, l2b(Datatpye)}, - {<<"app">>, App}]. - -format_plugin_config({Key, Value, Desc, Required}) -> - [{<<"key">>, l2b(Key)}, - {<<"value">>, l2b(Value)}, - {<<"desc">>, l2b(Desc)}, - {<<"required">>, Required}]. - -%%-------------------------------------------------------------------------- -%% Admin -%%-------------------------------------------------------------------------- -auth('POST', Params) -> - Username = get_value(<<"username">>, Params), - Password = get_value(<<"password">>, Params), - case emqx_mgmt:check_user(Username, Password) of - ok -> - {ok, []}; - {error, Reason} -> - {error, [{code, ?ERROR3}, {message, list_to_binary(Reason)}]} - end. - -users('POST', Params) -> - Username = get_value(<<"username">>, Params), - Password = get_value(<<"password">>, Params), - Tag = get_value(<<"tags">>, Params), - code(emqx_mgmt:add_user(Username, Password, Tag)); - -users('GET', _Params) -> - {ok, [Admin || Admin <- emqx_mgmt:user_list()]}. - -users('GET', _Params, Username) -> - {ok, emqx_mgmt:lookup_user(list_to_binary(Username))}; - -users('PUT', Params, Username) -> - code(emqx_mgmt:update_user(list_to_binary(Username), Params)); - -users('DELETE', _Params, "admin") -> - {error, [{code, ?ERROR6}, {message, <<"admin cannot be deleted">>}]}; -users('DELETE', _Params, Username) -> - code(emqx_mgmt:remove_user(list_to_binary(Username))). - -change_pwd('PUT', Params, Username) -> - OldPwd = get_value(<<"old_pwd">>, Params), - NewPwd = get_value(<<"new_pwd">>, Params), - code(emqx_mgmt:change_password(list_to_binary(Username), OldPwd, NewPwd)). - -code(ok) -> {ok, []}; -code(error) -> {error, [{code, ?ERROR2}]}; -code({error, Error}) -> {error, Error}. -%%-------------------------------------------------------------------------- -%% Inner function -%%-------------------------------------------------------------------------- -format(created_at, Val) -> - l2b(strftime(Val)); -format(_, Val) -> - Val. - -ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> - inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}); -ntoa(IP) -> - inet_parse:ntoa(IP). - -%%-------------------------------------------------------------------- -%% Strftime -%%-------------------------------------------------------------------- -strftime({MegaSecs, Secs, _MicroSecs}) -> - strftime(datetime(MegaSecs * 1000000 + Secs)); - -strftime({{Y,M,D}, {H,MM,S}}) -> - lists:flatten( - io_lib:format( - "~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])). - -datetime(Timestamp) when is_integer(Timestamp) -> - Universal = calendar:gregorian_seconds_to_datetime(Timestamp + - calendar:datetime_to_gregorian_seconds({{1970,1,1}, {0,0,0}})), - calendar:universal_time_to_local_time(Universal). - -bin(S) when is_list(S) -> l2b(S); -bin(A) when is_atom(A) -> bin(atom_to_list(A)); -bin(B) when is_binary(B) -> B; -bin(undefined) -> <<>>. -int(L) -> list_to_integer(L). -l2a(L) -> list_to_atom(L). -l2b(L) -> list_to_binary(L). -b2l(B) -> binary_to_list(B). - - -page_params(Params) -> - PageNo = int(get_value("curr_page", Params, "1")), - PageSize = int(get_value("page_size", Params, "20")), - {PageNo, PageSize}. From 38c33e9c8b03052055e238dbbe46c403fe523344 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 21 Nov 2017 22:41:05 +0800 Subject: [PATCH 006/520] Merge with emqx --- Makefile | 1 - src/emqttd.erl | 181 ------------------------------------------- src/emqx.app.src | 6 +- src/emqx_metrics.erl | 2 - src/emqx_pubsub.erl | 2 +- src/emqx_router.erl | 8 +- src/emqx_server.erl | 6 +- src/emqx_sm.erl | 2 +- 8 files changed, 12 insertions(+), 196 deletions(-) delete mode 100644 src/emqttd.erl diff --git a/Makefile b/Makefile index d9d04c517..d4cd2009f 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,6 @@ TEST_ERLC_OPTS += +debug_info TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose -# EUNIT_ERL_OPTS = CT_SUITES = emqx emqx_mod emqx_lib emqx_topic emqx_trie emqx_mqueue emqx_inflight \ emqx_vm emqx_net emqx_protocol emqx_access emqx_config emqx_router diff --git a/src/emqttd.erl b/src/emqttd.erl deleted file mode 100644 index 65739952f..000000000 --- a/src/emqttd.erl +++ /dev/null @@ -1,181 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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 EMQ Main Module. - --module(emqttd). - --author("Feng Lee "). - --include("emqttd.hrl"). - --include("emqttd_protocol.hrl"). - --export([start/0, env/1, env/2, is_running/1, stop/0]). - -%% PubSub API --export([subscribe/1, subscribe/2, subscribe/3, publish/1, - unsubscribe/1, unsubscribe/2]). - -%% PubSub Management API --export([setqos/3, topics/0, subscriptions/1, subscribers/1, subscribed/2]). - -%% Hooks API --export([hook/4, hook/3, unhook/2, run_hooks/2, run_hooks/3]). - -%% Debug API --export([dump/0]). - -%% Shutdown and reboot --export([shutdown/0, shutdown/1, reboot/0]). - --type(subid() :: binary()). - --type(subscriber() :: pid() | subid() | {subid(), pid()}). - --type(suboption() :: local | {qos, non_neg_integer()} | {share, {'$queue' | binary()}}). - --export_type([subscriber/0, suboption/0]). - --define(APP, ?MODULE). - -%%-------------------------------------------------------------------- -%% Bootstrap, environment, configuration, is_running... -%%-------------------------------------------------------------------- - -%% @doc Start emqttd application. --spec(start() -> ok | {error, term()}). -start() -> application:start(?APP). - -%% @doc Stop emqttd application. --spec(stop() -> ok | {error, term()}). -stop() -> application:stop(?APP). - -%% @doc Environment --spec(env(Key :: atom()) -> {ok, any()} | undefined). -env(Key) -> application:get_env(?APP, Key). - -%% @doc Get environment --spec(env(Key :: atom(), Default :: any()) -> undefined | any()). -env(Key, Default) -> application:get_env(?APP, Key, Default). - -%% @doc Is running? --spec(is_running(node()) -> boolean()). -is_running(Node) -> - case rpc:call(Node, erlang, whereis, [?APP]) of - {badrpc, _} -> false; - undefined -> false; - Pid when is_pid(Pid) -> true - end. - -%%-------------------------------------------------------------------- -%% PubSub APIs -%%-------------------------------------------------------------------- - -%% @doc Subscribe --spec(subscribe(iodata()) -> ok | {error, term()}). -subscribe(Topic) -> - emqttd_server:subscribe(iolist_to_binary(Topic)). - --spec(subscribe(iodata(), subscriber()) -> ok | {error, term()}). -subscribe(Topic, Subscriber) -> - emqttd_server:subscribe(iolist_to_binary(Topic), Subscriber). - --spec(subscribe(iodata(), subscriber(), [suboption()]) -> ok | {error, term()}). -subscribe(Topic, Subscriber, Options) -> - emqttd_server:subscribe(iolist_to_binary(Topic), Subscriber, Options). - -%% @doc Publish MQTT Message --spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore). -publish(Msg) -> - emqttd_server:publish(Msg). - -%% @doc Unsubscribe --spec(unsubscribe(iodata()) -> ok | {error, term()}). -unsubscribe(Topic) -> - emqttd_server:unsubscribe(iolist_to_binary(Topic)). - --spec(unsubscribe(iodata(), subscriber()) -> ok | {error, term()}). -unsubscribe(Topic, Subscriber) -> - emqttd_server:unsubscribe(iolist_to_binary(Topic), Subscriber). - --spec(setqos(binary(), subscriber(), mqtt_qos()) -> ok). -setqos(Topic, Subscriber, Qos) -> - emqttd_server:setqos(iolist_to_binary(Topic), Subscriber, Qos). - --spec(topics() -> [binary()]). -topics() -> emqttd_router:topics(). - --spec(subscribers(iodata()) -> list(subscriber())). -subscribers(Topic) -> - emqttd_server:subscribers(iolist_to_binary(Topic)). - --spec(subscriptions(subscriber()) -> [{emqttd:subscriber(), binary(), list(emqttd:suboption())}]). -subscriptions(Subscriber) -> - emqttd_server:subscriptions(Subscriber). - --spec(subscribed(iodata(), subscriber()) -> boolean()). -subscribed(Topic, Subscriber) -> - emqttd_server:subscribed(iolist_to_binary(Topic), Subscriber). - -%%-------------------------------------------------------------------- -%% Hooks API -%%-------------------------------------------------------------------- - --spec(hook(atom(), function() | {emqttd_hooks:hooktag(), function()}, list(any())) - -> ok | {error, term()}). -hook(Hook, TagFunction, InitArgs) -> - emqttd_hooks:add(Hook, TagFunction, InitArgs). - --spec(hook(atom(), function() | {emqttd_hooks:hooktag(), function()}, list(any()), integer()) - -> ok | {error, term()}). -hook(Hook, TagFunction, InitArgs, Priority) -> - emqttd_hooks:add(Hook, TagFunction, InitArgs, Priority). - --spec(unhook(atom(), function() | {emqttd_hooks:hooktag(), function()}) - -> ok | {error, term()}). -unhook(Hook, TagFunction) -> - emqttd_hooks:delete(Hook, TagFunction). - --spec(run_hooks(atom(), list(any())) -> ok | stop). -run_hooks(Hook, Args) -> - emqttd_hooks:run(Hook, Args). - --spec(run_hooks(atom(), list(any()), any()) -> {ok | stop, any()}). -run_hooks(Hook, Args, Acc) -> - emqttd_hooks:run(Hook, Args, Acc). - -%%-------------------------------------------------------------------- -%% Shutdown and reboot -%%-------------------------------------------------------------------- - -shutdown() -> - shutdown(normal). - -shutdown(Reason) -> - lager:error("EMQ shutdown for ~s", [Reason]), - emqttd_plugins:unload(), - lists:foreach(fun application:stop/1, [emqttd, ekka, mochiweb, esockd, gproc]). - -reboot() -> - lists:foreach(fun application:start/1, [gproc, esockd, mochiweb, ekka, emqttd]). - -%%-------------------------------------------------------------------- -%% Debug -%%-------------------------------------------------------------------- - -dump() -> lists:append([emqttd_server:dump(), emqttd_router:dump()]). - diff --git a/src/emqx.app.src b/src/emqx.app.src index eece8df33..43c0afb21 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -1,11 +1,11 @@ {application,emqx, [{description,"EMQ X Broker"}, - {vsn,"2.4"}, + {vsn,"2.3.0"}, {modules,[]}, {registered,[emqx_sup]}, - {applications,[kernel,stdlib,gproc,gen_rpc,lager,esockd,mochiweb, lager_syslog,pbkdf2,bcrypt,clique,jsx]}, + {applications,[kernel,stdlib,gproc,gen_rpc,lager,esockd,mochiweb,lager_syslog,pbkdf2,bcrypt,clique,jsx]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, {licenses,["Apache-2.0"]}, - {links,[{"Github","https://github.com/emqtt/emqttd"}]}]}. + {links,[{"Github","https://github.com/emqx/emqx"}]}]}. diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 166173d18..f8cb7b60b 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -249,8 +249,6 @@ init([]) -> ets:new(?METRIC_TAB, [set, public, named_table, {write_concurrency, true}]), % Init metrics [create_metric(Metric) || Metric <- Metrics], - % $SYS Topics for metrics - % [ok = emqttd:create(topic, metric_topic(Topic)) || {_, Topic} <- Metrics], % Tick to publish metrics {ok, #state{tick = emqx_broker:start_tick(tick)}, hibernate}. diff --git a/src/emqx_pubsub.erl b/src/emqx_pubsub.erl index 06f1ea3bc..7885dd811 100644 --- a/src/emqx_pubsub.erl +++ b/src/emqx_pubsub.erl @@ -55,7 +55,7 @@ start_link(Pool, Id, Env) -> %%-------------------------------------------------------------------- %% @doc Subscribe to a Topic --spec(subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok). +-spec(subscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> ok). subscribe(Topic, Subscriber, Options) -> call(pick(Topic), {subscribe, Topic, Subscriber, Options}). diff --git a/src/emqx_router.erl b/src/emqx_router.erl index ac356f7de..d50654033 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -112,7 +112,7 @@ print(Topic) -> add_route(Topic) when is_binary(Topic) -> add_route(#mqtt_route{topic = Topic, node = node()}); add_route(Route = #mqtt_route{topic = Topic}) -> - case emqttd_topic:wildcard(Topic) of + case emqx_topic:wildcard(Topic) of true -> case mnesia:is_transaction() of true -> add_trie_route(Route); false -> trans(fun add_trie_route/1, [Route]) @@ -125,7 +125,7 @@ add_direct_route(Route) -> add_trie_route(Route = #mqtt_route{topic = Topic}) -> case mnesia:wread({mqtt_route, Topic}) of - [] -> emqttd_trie:insert(Topic); + [] -> emqx_trie:insert(Topic); _ -> ok end, mnesia:write(Route). @@ -135,7 +135,7 @@ add_trie_route(Route = #mqtt_route{topic = Topic}) -> del_route(Topic) when is_binary(Topic) -> del_route(#mqtt_route{topic = Topic, node = node()}); del_route(Route = #mqtt_route{topic = Topic}) -> - case emqttd_topic:wildcard(Topic) of + case emqx_topic:wildcard(Topic) of true -> case mnesia:is_transaction() of true -> del_trie_route(Route); false -> trans(fun del_trie_route/1, [Route]) @@ -150,7 +150,7 @@ del_trie_route(Route = #mqtt_route{topic = Topic}) -> case mnesia:wread({mqtt_route, Topic}) of [Route] -> %% Remove route and trie mnesia:delete_object(Route), - emqttd_trie:delete(Topic); + emqx_trie:delete(Topic); [_|_] -> %% Remove route only mnesia:delete_object(Route); [] -> ok diff --git a/src/emqx_server.erl b/src/emqx_server.erl index 12886656d..726750881 100644 --- a/src/emqx_server.erl +++ b/src/emqx_server.erl @@ -62,11 +62,11 @@ start_link(Pool, Id, Env) -> subscribe(Topic) when is_binary(Topic) -> subscribe(Topic, self()). --spec(subscribe(binary(), emqttd:subscriber()) -> ok | {error, term()}). +-spec(subscribe(binary(), emqx:subscriber()) -> ok | {error, term()}). subscribe(Topic, Subscriber) when is_binary(Topic) -> subscribe(Topic, Subscriber, []). --spec(subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> +-spec(subscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> ok | {error, term()}). subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> call(pick(Subscriber), {subscribe, Topic, with_subpid(Subscriber), Options}). @@ -158,7 +158,7 @@ with_subproperty(Subscriptions) when is_list(Subscriptions) -> subscribers(Topic) when is_binary(Topic) -> emqx_pubsub:subscribers(Topic). --spec(subscribed(binary(), emqttd:subscriber()) -> boolean()). +-spec(subscribed(binary(), emqx:subscriber()) -> boolean()). subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> ets:member(mqtt_subproperty, {Topic, SubPid}); subscribed(Topic, SubId) when is_binary(Topic), is_binary(SubId) -> diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 78e53da70..3322aed1f 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -107,7 +107,7 @@ unregister_session(ClientId) -> unregister_session(ClientId, Pid) -> case ets:lookup(mqtt_local_session, ClientId) of [LocalSess = {_, Pid, _, _}] -> - emqttd_stats:del_session_stats(ClientId), + emqx_stats:del_session_stats(ClientId), ets:delete_object(mqtt_local_session, LocalSess); _ -> false From 223f3d4da5e8bb37c2b18153c805a1f9d77f9490 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 22 Nov 2017 16:44:00 +0800 Subject: [PATCH 007/520] Remove RPC configurations --- Makefile | 5 ++--- src/emqx_rpc.erl | 3 +-- src/emqx_server.erl | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index d4cd2009f..c3718ad6a 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,12 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker PROJECT_VERSION = 2.3.0 -NO_AUTOPATCH = gen_rpc cuttlefish +NO_AUTOPATCH = cuttlefish -DEPS = goldrush gproc gen_rpc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx +DEPS = goldrush gproc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx dep_goldrush = git https://github.com/basho/goldrush 0.1.9 dep_gproc = git https://github.com/uwiger/gproc -dep_gen_rpc = git https://github.com/priestjim/gen_rpc dep_getopt = git https://github.com/jcomellas/getopt v0.8.2 dep_lager = git https://github.com/basho/lager master dep_lager_syslog = git https://github.com/basho/lager_syslog diff --git a/src/emqx_rpc.erl b/src/emqx_rpc.erl index a39b1c784..a137b543c 100644 --- a/src/emqx_rpc.erl +++ b/src/emqx_rpc.erl @@ -26,6 +26,5 @@ %% @doc Wraps gen_rpc first. cast(Node, Mod, Fun, Args) -> - emqx_metrics:inc('messages/forward'), - gen_rpc:cast({Node, erlang:system_info(scheduler_id)}, Mod, Fun, Args). + emqx_metrics:inc('messages/forward'), rpc:cast(Node, Mod, Fun, Args). diff --git a/src/emqx_server.erl b/src/emqx_server.erl index 726750881..26521fa5c 100644 --- a/src/emqx_server.erl +++ b/src/emqx_server.erl @@ -137,7 +137,7 @@ with_subpid(SubId) when is_binary(SubId) -> with_subpid({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> {SubId, SubPid}. --spec(subscriptions(subscriber()) -> [{subscriber(), binary(), list(suboption())}]). +-spec(subscriptions(emqx:subscriber()) -> [{emqx:subscriber(), binary(), list(emqx:suboption())}]). subscriptions(SubPid) when is_pid(SubPid) -> with_subproperty(ets:lookup(mqtt_subscription, SubPid)); From 99ed0e46b618109d91028c17cd578d1de3ad7676 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 22 Nov 2017 17:07:11 +0800 Subject: [PATCH 008/520] Remove RPC args --- etc/emqx.conf | 33 --------------------------- priv/emqx.schema | 58 ------------------------------------------------ 2 files changed, 91 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 3cdf21103..a29ff071a 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -115,39 +115,6 @@ node.dist_net_ticktime = 60 node.dist_listen_min = 6369 node.dist_listen_max = 6369 -##-------------------------------------------------------------------- -## RPC Args -##-------------------------------------------------------------------- - -## TCP server port. -rpc.tcp_server_port = 5369 - -## Default TCP port for outgoing connections -rpc.tcp_client_port = 5369 - -## Client connect timeout -rpc.connect_timeout = 5000 - -## Client and Server send timeout -rpc.send_timeout = 5000 - -## Authentication timeout -rpc.authentication_timeout = 5000 - -## Default receive timeout for call() functions -rpc.call_receive_timeout = 15000 - -## Socket keepalive configuration -rpc.socket_keepalive_idle = 7200 - -## Seconds between probes -rpc.socket_keepalive_interval = 75 - -## Probes lost to close the connection -rpc.socket_keepalive_count = 9 - -## TODO: sndbuf, rcvbuf and buffer - ##-------------------------------------------------------------------- ## Log ##-------------------------------------------------------------------- diff --git a/priv/emqx.schema b/priv/emqx.schema index 508b98121..ac5871926 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -302,64 +302,6 @@ end}. hidden ]}. -%%-------------------------------------------------------------------- -%% RPC Args -%%-------------------------------------------------------------------- - -%% RPC server port. -{mapping, "rpc.tcp_server_port", "gen_rpc.tcp_server_port", [ - {default, 5369}, - {datatype, integer} -]}. - -%% Default TCP port for outgoing connections -{mapping, "rpc.tcp_client_port", "gen_rpc.tcp_client_port", [ - {default, 5369}, - {datatype, integer} -]}. - -%% Client connect timeout -{mapping, "rpc.connect_timeout", "gen_rpc.connect_timeout", [ - {default, 5000}, - {datatype, integer} -]}. - -%% Client and Server send timeout -{mapping, "rpc.send_timeout", "gen_rpc.send_timeout", [ - {default, 5000}, - {datatype, integer} -]}. - -%% Authentication timeout -{mapping, "rpc.authentication_timeout", "gen_rpc.authentication_timeout", [ - {default, 5000}, - {datatype, integer} -]}. - -%% Default receive timeout for call() functions -{mapping, "rpc.call_receive_timeout", "gen_rpc.call_receive_timeout", [ - {default, 15000}, - {datatype, integer} -]}. - -%% Socket keepalive configuration -{mapping, "rpc.socket_keepalive_idle", "gen_rpc.socket_keepalive_idle", [ - {default, 7200}, - {datatype, integer} -]}. - -%% Seconds between probes -{mapping, "rpc.socket_keepalive_interval", "gen_rpc.socket_keepalive_interval", [ - {default, 75}, - {datatype, integer} -]}. - -%% Probes lost to close the connection -{mapping, "rpc.socket_keepalive_count", "gen_rpc.socket_keepalive_count", [ - {default, 9}, - {datatype, integer} -]}. - %%-------------------------------------------------------------------- %% Log %%-------------------------------------------------------------------- From aaf19787af6503990dedc02e866903c5648eb6a6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 22 Nov 2017 18:11:21 +0800 Subject: [PATCH 009/520] Remove the duplicated InfoHandler --- priv/emqx.schema | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index ac5871926..e37950b12 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1,5 +1,5 @@ %%-*- mode: erlang -*- -%% EMQ X config mapping +%% EMQ X R2.3 config mapping %%-------------------------------------------------------------------- %% Cluster @@ -417,14 +417,6 @@ end}. {date, "$D0"}, {count, cuttlefish:conf_get("log.info.count", Conf)}]}] end, - InfoHandler = case cuttlefish:conf_get("log.info.file", Conf, undefined) of - undefined -> []; - InfoFilename -> [{lager_file_backend, [{file, InfoFilename}, - {level, info}, - {size, 10485760}, - {date, "$D0"}, - {count, 5}]}] - end, ConsoleLogLevel = cuttlefish:conf_get("log.console.level", Conf), ConsoleLogFile = cuttlefish:conf_get("log.console.file", Conf), From 24f05adb1a3a5aec8a37aab5bcaf0815d94b4945 Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 6 Dec 2017 10:19:46 +0800 Subject: [PATCH 010/520] Fix compile fail --- src/emqx_sm_sup.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index da2f30b07..f5ce97e1f 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -47,7 +47,7 @@ init([]) -> permanent, 5000, worker, [?HELPER]}, %% SM Pool Sup - MFA = {emqttd_sm, start_link, []}, - PoolSup = emqx_pool_sup:spec([emqttd_sm, hash, erlang:system_info(schedulers), MFA]), + MFA = {?SM, start_link, []}, + PoolSup = emqttd_pool_sup:spec([?SM, hash, erlang:system_info(schedulers), MFA]), {ok, {{one_for_all, 10, 3600}, [Helper, PoolSup]}}. From ddf29656519e90530a04cddcb34c49ca8b93e379 Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 6 Dec 2017 10:26:19 +0800 Subject: [PATCH 011/520] Rm emqttd.app.src --- src/emqttd.app.src | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/emqttd.app.src diff --git a/src/emqttd.app.src b/src/emqttd.app.src deleted file mode 100644 index 67af8854e..000000000 --- a/src/emqttd.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application,emqttd, - [{description,"Erlang MQTT Broker"}, - {vsn,"2.3.1"}, - {modules,[]}, - {registered,[emqttd_sup]}, - {applications,[kernel,stdlib,gproc,lager,esockd,mochiweb, - lager_syslog,pbkdf2,bcrypt]}, - {env,[]}, - {mod,{emqttd_app,[]}}, - {maintainers,["Feng Lee "]}, - {licenses,["Apache-2.0"]}, - {links,[{"Github","https://github.com/emqtt/emqttd"}]}]}. From bcf345efbe1e12ecb06b2a4dd9255638d8910577 Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 6 Dec 2017 10:27:55 +0800 Subject: [PATCH 012/520] Fix compile fail --- src/emqx_sm_sup.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index f5ce97e1f..27517d06b 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -48,6 +48,6 @@ init([]) -> %% SM Pool Sup MFA = {?SM, start_link, []}, - PoolSup = emqttd_pool_sup:spec([?SM, hash, erlang:system_info(schedulers), MFA]), + PoolSup = emqx_pool_sup:spec([?SM, hash, erlang:system_info(schedulers), MFA]), {ok, {{one_for_all, 10, 3600}, [Helper, PoolSup]}}. From 6e5134a8b2983f2184c7281a7ecc7535c6a428b2 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 7 Dec 2017 16:24:48 +0800 Subject: [PATCH 013/520] Merge with the enterprise edition --- Makefile | 2 +- src/emqx.erl | 10 +- src/emqx_app.erl | 1 - src/emqx_cli.erl | 613 ------------------ src/emqx_cli_config.erl | 363 ----------- src/emqx_ctl.erl | 9 - src/emqx_http.erl | 236 ------- src/emqx_mgmt.erl.bk | 544 ---------------- src/emqx_protocol.erl | 20 +- src/emqx_router.erl | 14 +- test/emqttd_cli_SUITE.erl | 52 -- test/emqx_SUITE.erl | 152 ++--- test/emqx_config_SUITE.erl | 149 ----- test/emqx_mod_SUITE.erl | 4 +- ...router_SUITE.erl => emqx_router_SUITE.erl} | 18 +- 15 files changed, 88 insertions(+), 2099 deletions(-) delete mode 100644 src/emqx_cli.erl delete mode 100644 src/emqx_cli_config.erl delete mode 100644 src/emqx_http.erl delete mode 100644 src/emqx_mgmt.erl.bk delete mode 100644 test/emqttd_cli_SUITE.erl delete mode 100644 test/emqx_config_SUITE.erl rename test/{emqttd_router_SUITE.erl => emqx_router_SUITE.erl} (93%) diff --git a/Makefile b/Makefile index e0a06a673..674fa4924 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose CT_SUITES = emqx emqx_mod emqx_lib emqx_topic emqx_trie emqx_mqueue emqx_inflight \ - emqx_vm emqx_net emqx_protocol emqx_access emqx_config emqx_router + emqx_vm emqx_net emqx_protocol emqx_access emqx_router CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqxct@127.0.0.1 diff --git a/src/emqx.erl b/src/emqx.erl index bdb88ed9a..07e133cf7 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -112,10 +112,10 @@ start_listener({Proto, ListenOn, Opts}) when Proto == http; Proto == ws -> %% Start https listener start_listener({Proto, ListenOn, Opts}) when Proto == https; Proto == wss -> - {ok, _} = mochiweb:start_http('mqtt:wss', ListenOn, Opts, {emqx_ws, handle_request, []}); + {ok, _} = mochiweb:start_http('mqtt:wss', ListenOn, Opts, {emqx_ws, handle_request, []}). -start_listener({Proto, ListenOn, Opts}) when Proto == api -> - {ok, _} = mochiweb:start_http('mqtt:api', ListenOn, Opts, emqx_http:http_handler()). +% start_listener({Proto, ListenOn, Opts}) when Proto == api -> +% {ok, _} = mochiweb:start_http('mqtt:api', ListenOn, Opts, emqx_http:http_handler()). start_listener(Proto, ListenOn, Opts) -> Env = lists:append(emqx:env(client, []), emqx:env(protocol, [])), @@ -144,8 +144,8 @@ stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> mochiweb:stop_http('mqtt:ws', ListenOn); stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> mochiweb:stop_http('mqtt:wss', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == api -> - mochiweb:stop_http('mqtt:api', ListenOn); +% stop_listener({Proto, ListenOn, _Opts}) when Proto == api -> +% mochiweb:stop_http('mqtt:api', ListenOn); stop_listener({Proto, ListenOn, _Opts}) -> esockd:close(Proto, ListenOn). diff --git a/src/emqx_app.erl b/src/emqx_app.erl index a3066f7de..8b31f669f 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -37,7 +37,6 @@ start(_Type, _Args) -> print_banner(), ekka:start(), {ok, Sup} = emqx_sup:start_link(), - ok = emqx_cli:load(), ok = register_acl_mod(), start_autocluster(), register(emqx, self()), diff --git a/src/emqx_cli.erl b/src/emqx_cli.erl deleted file mode 100644 index 14c1498b4..000000000 --- a/src/emqx_cli.erl +++ /dev/null @@ -1,613 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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_cli). - --author("Feng Lee "). - --include("emqx.hrl"). - --include("emqx_mqtt.hrl"). - --include("emqx_cli.hrl"). - --import(lists, [foreach/2]). - --import(proplists, [get_value/2]). - --export([load/0]). - --export([status/1, broker/1, cluster/1, users/1, clients/1, sessions/1, - routes/1, topics/1, subscriptions/1, plugins/1, bridges/1, - listeners/1, vm/1, mnesia/1, trace/1, acl/1]). - --define(PROC_INFOKEYS, [status, - memory, - message_queue_len, - total_heap_size, - heap_size, - stack_size, - reductions]). - --define(MAX_LIMIT, 10000). - --define(APP, emqx). - --spec(load() -> ok). -load() -> - Cmds = [Fun || {Fun, _} <- ?MODULE:module_info(exports), is_cmd(Fun)], - lists:foreach(fun(Cmd) -> emqx_ctl:register_cmd(Cmd, {?MODULE, Cmd}, []) end, Cmds), - emqx_cli_config:register_config(). - -is_cmd(Fun) -> - not lists:member(Fun, [init, load, module_info]). - -%%-------------------------------------------------------------------- -%% Commands -%%-------------------------------------------------------------------- - -%%-------------------------------------------------------------------- -%% @doc Node status - -status([]) -> - {InternalStatus, _ProvidedStatus} = init:get_status(), - ?PRINT("Node ~p is ~p~n", [node(), InternalStatus]), - case lists:keysearch(?APP, 1, application:which_applications()) of - false -> - ?PRINT("~s is not running~n", [?APP]); - {value, {?APP, _Desc, Vsn}} -> - ?PRINT("~s ~s is running~n", [?APP, Vsn]) - end; -status(_) -> - ?PRINT_CMD("status", "Show broker status"). - -%%-------------------------------------------------------------------- -%% @doc Query broker - -broker([]) -> - Funs = [sysdescr, version, uptime, datetime], - foreach(fun(Fun) -> - ?PRINT("~-10s: ~s~n", [Fun, emqx_broker:Fun()]) - end, Funs); - -broker(["stats"]) -> - foreach(fun({Stat, Val}) -> - ?PRINT("~-20s: ~w~n", [Stat, Val]) - end, emqx_stats:getstats()); - -broker(["metrics"]) -> - foreach(fun({Metric, Val}) -> - ?PRINT("~-24s: ~w~n", [Metric, Val]) - end, lists:sort(emqx_metrics:all())); - -broker(["pubsub"]) -> - Pubsubs = supervisor:which_children(emqx_pubsub_sup:pubsub_pool()), - foreach(fun({{_, Id}, Pid, _, _}) -> - ProcInfo = erlang:process_info(Pid, ?PROC_INFOKEYS), - ?PRINT("pubsub: ~w~n", [Id]), - foreach(fun({Key, Val}) -> - ?PRINT(" ~-18s: ~w~n", [Key, Val]) - end, ProcInfo) - end, lists:reverse(Pubsubs)); - -broker(_) -> - ?USAGE([{"broker", "Show broker version, uptime and description"}, - {"broker pubsub", "Show process_info of pubsub"}, - {"broker stats", "Show broker statistics of clients, topics, subscribers"}, - {"broker metrics", "Show broker metrics"}]). - -%%-------------------------------------------------------------------- -%% @doc Cluster with other nodes - -cluster(["join", SNode]) -> - case ekka:join(ekka_node:parse_name(SNode)) of - ok -> - ?PRINT_MSG("Join the cluster successfully.~n"), - cluster(["status"]); - ignore -> - ?PRINT_MSG("Ignore.~n"); - {error, Error} -> - ?PRINT("Failed to join the cluster: ~p~n", [Error]) - end; - -cluster(["leave"]) -> - case ekka:leave() of - ok -> - ?PRINT_MSG("Leave the cluster successfully.~n"), - cluster(["status"]); - {error, Error} -> - ?PRINT("Failed to leave the cluster: ~p~n", [Error]) - end; - -cluster(["force-leave", SNode]) -> - case ekka:force_leave(ekka_node:parse_name(SNode)) of - ok -> - ?PRINT_MSG("Remove the node from cluster successfully.~n"), - cluster(["status"]); - ignore -> - ?PRINT_MSG("Ignore.~n"); - {error, Error} -> - ?PRINT("Failed to remove the node from cluster: ~p~n", [Error]) - end; - -cluster(["status"]) -> - ?PRINT("Cluster status: ~p~n", [ekka_cluster:status()]); - -cluster(_) -> - ?USAGE([{"cluster join ", "Join the cluster"}, - {"cluster leave", "Leave the cluster"}, - {"cluster force-leave ","Force the node leave from cluster"}, - {"cluster status", "Cluster status"}]). - -%%-------------------------------------------------------------------- -%% @doc Users usage - -users(Args) -> emqx_auth_username:cli(Args). - -acl(["reload"]) -> emqx_access_control:reload_acl(); -acl(_) -> ?USAGE([{"acl reload", "reload etc/acl.conf"}]). - -%%-------------------------------------------------------------------- -%% @doc Query clients - -clients(["list"]) -> - dump(mqtt_client); - -clients(["show", ClientId]) -> - if_client(ClientId, fun print/1); - -clients(["kick", ClientId]) -> - if_client(ClientId, fun(#mqtt_client{client_pid = Pid}) -> emqx_client:kick(Pid) end); - -clients(_) -> - ?USAGE([{"clients list", "List all clients"}, - {"clients show ", "Show a client"}, - {"clients kick ", "Kick out a client"}]). - -if_client(ClientId, Fun) -> - case emqx_cm:lookup(bin(ClientId)) of - undefined -> ?PRINT_MSG("Not Found.~n"); - Client -> Fun(Client) - end. - -%%-------------------------------------------------------------------- -%% @doc Sessions Command - -sessions(["list"]) -> - dump(mqtt_local_session); - -%% performance issue? - -sessions(["list", "persistent"]) -> - lists:foreach(fun print/1, ets:match_object(mqtt_local_session, {'_', '_', false, '_'})); - -%% performance issue? - -sessions(["list", "transient"]) -> - lists:foreach(fun print/1, ets:match_object(mqtt_local_session, {'_', '_', true, '_'})); - -sessions(["show", ClientId]) -> - case ets:lookup(mqtt_local_session, bin(ClientId)) of - [] -> ?PRINT_MSG("Not Found.~n"); - [SessInfo] -> print(SessInfo) - end; - -sessions(_) -> - ?USAGE([{"sessions list", "List all sessions"}, - {"sessions list persistent", "List all persistent sessions"}, - {"sessions list transient", "List all transient sessions"}, - {"sessions show ", "Show a session"}]). - -%%-------------------------------------------------------------------- -%% @doc Routes Command - -routes(["list"]) -> - Routes = emqx_router:dump(), - foreach(fun print/1, Routes); - -routes(["show", Topic]) -> - Routes = lists:append(ets:lookup(mqtt_route, bin(Topic)), - ets:lookup(mqtt_local_route, bin(Topic))), - foreach(fun print/1, Routes); - -routes(_) -> - ?USAGE([{"routes list", "List all routes"}, - {"routes show ", "Show a route"}]). - -%%-------------------------------------------------------------------- -%% @doc Topics Command - -topics(["list"]) -> - lists:foreach(fun(Topic) -> ?PRINT("~s~n", [Topic]) end, emqx:topics()); - -topics(["show", Topic]) -> - print(mnesia:dirty_read(mqtt_route, bin(Topic))); - -topics(_) -> - ?USAGE([{"topics list", "List all topics"}, - {"topics show ", "Show a topic"}]). - -subscriptions(["list"]) -> - lists:foreach(fun(Subscription) -> - print(subscription, Subscription) - end, ets:tab2list(mqtt_subscription)); - -subscriptions(["show", ClientId]) -> - case emqx:subscriptions(bin(ClientId)) of - [] -> ?PRINT_MSG("Not Found.~n"); - Subscriptions -> - [print(subscription, Sub) || Sub <- Subscriptions] - end; - -subscriptions(["add", ClientId, Topic, QoS]) -> - if_valid_qos(QoS, fun(IntQos) -> - case emqx_sm:lookup_session(bin(ClientId)) of - undefined -> - ?PRINT_MSG("Error: Session not found!"); - #mqtt_session{sess_pid = SessPid} -> - {Topic1, Options} = emqx_topic:parse(bin(Topic)), - emqx_session:subscribe(SessPid, [{Topic1, [{qos, IntQos}|Options]}]), - ?PRINT_MSG("ok~n") - end - end); - -subscriptions(["del", ClientId, Topic]) -> - case emqx_sm:lookup_session(bin(ClientId)) of - undefined -> - ?PRINT_MSG("Error: Session not found!"); - #mqtt_session{sess_pid = SessPid} -> - emqx_session:unsubscribe(SessPid, [emqx_topic:parse(bin(Topic))]), - ?PRINT_MSG("ok~n") - end; - -subscriptions(_) -> - ?USAGE([{"subscriptions list", "List all subscriptions"}, - {"subscriptions show ", "Show subscriptions of a client"}, - {"subscriptions add ", "Add a static subscription manually"}, - {"subscriptions del ", "Delete a static subscription manually"}]). - -%if_could_print(Tab, Fun) -> -% case mnesia:table_info(Tab, size) of -% Size when Size >= ?MAX_LIMIT -> -% ?PRINT("Could not list, too many ~ss: ~p~n", [Tab, Size]); -% _Size -> -% Keys = mnesia:dirty_all_keys(Tab), -% foreach(fun(Key) -> Fun(ets:lookup(Tab, Key)) end, Keys) -% end. - -if_valid_qos(QoS, Fun) -> - try list_to_integer(QoS) of - Int when ?IS_QOS(Int) -> Fun(Int); - _ -> ?PRINT_MSG("QoS should be 0, 1, 2~n") - catch _:_ -> - ?PRINT_MSG("QoS should be 0, 1, 2~n") - end. - -plugins(["list"]) -> - foreach(fun print/1, emqx_plugins:list()); - -plugins(["load", Name]) -> - case emqx_plugins:load(list_to_atom(Name)) of - {ok, StartedApps} -> - ?PRINT("Start apps: ~p~nPlugin ~s loaded successfully.~n", [StartedApps, Name]); - {error, Reason} -> - ?PRINT("load plugin error: ~p~n", [Reason]) - end; - -plugins(["unload", Name]) -> - case emqx_plugins:unload(list_to_atom(Name)) of - ok -> - ?PRINT("Plugin ~s unloaded successfully.~n", [Name]); - {error, Reason} -> - ?PRINT("unload plugin error: ~p~n", [Reason]) - end; - -plugins(_) -> - ?USAGE([{"plugins list", "Show loaded plugins"}, - {"plugins load ", "Load plugin"}, - {"plugins unload ", "Unload plugin"}]). - -%%-------------------------------------------------------------------- -%% @doc Bridges command - -bridges(["list"]) -> - foreach(fun({Node, Topic, _Pid}) -> - ?PRINT("bridge: ~s--~s-->~s~n", [node(), Topic, Node]) - end, emqx_bridge_sup_sup:bridges()); - -bridges(["options"]) -> - ?PRINT_MSG("Options:~n"), - ?PRINT_MSG(" qos = 0 | 1 | 2~n"), - ?PRINT_MSG(" prefix = string~n"), - ?PRINT_MSG(" suffix = string~n"), - ?PRINT_MSG(" queue = integer~n"), - ?PRINT_MSG("Example:~n"), - ?PRINT_MSG(" qos=2,prefix=abc/,suffix=/yxz,queue=1000~n"); - -bridges(["start", SNode, Topic]) -> - case emqx_bridge_sup_sup:start_bridge(list_to_atom(SNode), list_to_binary(Topic)) of - {ok, _} -> ?PRINT_MSG("bridge is started.~n"); - {error, Error} -> ?PRINT("error: ~p~n", [Error]) - end; - -bridges(["start", SNode, Topic, OptStr]) -> - Opts = parse_opts(bridge, OptStr), - case emqx_bridge_sup_sup:start_bridge(list_to_atom(SNode), list_to_binary(Topic), Opts) of - {ok, _} -> ?PRINT_MSG("bridge is started.~n"); - {error, Error} -> ?PRINT("error: ~p~n", [Error]) - end; - -bridges(["stop", SNode, Topic]) -> - case emqx_bridge_sup_sup:stop_bridge(list_to_atom(SNode), list_to_binary(Topic)) of - ok -> ?PRINT_MSG("bridge is stopped.~n"); - {error, Error} -> ?PRINT("error: ~p~n", [Error]) - end; - -bridges(_) -> - ?USAGE([{"bridges list", "List bridges"}, - {"bridges options", "Bridge options"}, - {"bridges start ", "Start a bridge"}, - {"bridges start ", "Start a bridge with options"}, - {"bridges stop ", "Stop a bridge"}]). - -parse_opts(Cmd, OptStr) -> - Tokens = string:tokens(OptStr, ","), - [parse_opt(Cmd, list_to_atom(Opt), Val) - || [Opt, Val] <- [string:tokens(S, "=") || S <- Tokens]]. -parse_opt(bridge, qos, Qos) -> - {qos, list_to_integer(Qos)}; -parse_opt(bridge, suffix, Suffix) -> - {topic_suffix, bin(Suffix)}; -parse_opt(bridge, prefix, Prefix) -> - {topic_prefix, bin(Prefix)}; -parse_opt(bridge, queue, Len) -> - {max_queue_len, list_to_integer(Len)}; -parse_opt(_Cmd, Opt, _Val) -> - ?PRINT("Bad Option: ~s~n", [Opt]). - -%%-------------------------------------------------------------------- -%% @doc vm command - -vm([]) -> - vm(["all"]); - -vm(["all"]) -> - [vm([Name]) || Name <- ["load", "memory", "process", "io", "ports"]]; - -vm(["load"]) -> - [?PRINT("cpu/~-20s: ~s~n", [L, V]) || {L, V} <- emqx_vm:loads()]; - -vm(["memory"]) -> - [?PRINT("memory/~-17s: ~w~n", [Cat, Val]) || {Cat, Val} <- erlang:memory()]; - -vm(["process"]) -> - foreach(fun({Name, Key}) -> - ?PRINT("process/~-16s: ~w~n", [Name, erlang:system_info(Key)]) - end, [{limit, process_limit}, {count, process_count}]); - -vm(["io"]) -> - IoInfo = erlang:system_info(check_io), - foreach(fun(Key) -> - ?PRINT("io/~-21s: ~w~n", [Key, get_value(Key, IoInfo)]) - end, [max_fds, active_fds]); - -vm(["ports"]) -> - foreach(fun({Name, Key}) -> - ?PRINT("ports/~-16s: ~w~n", [Name, erlang:system_info(Key)]) - end, [{count, port_count}, {limit, port_limit}]); - -vm(_) -> - ?USAGE([{"vm all", "Show info of Erlang VM"}, - {"vm load", "Show load of Erlang VM"}, - {"vm memory", "Show memory of Erlang VM"}, - {"vm process", "Show process of Erlang VM"}, - {"vm io", "Show IO of Erlang VM"}, - {"vm ports", "Show Ports of Erlang VM"}]). - -%%-------------------------------------------------------------------- -%% @doc mnesia Command - -mnesia([]) -> - mnesia:system_info(); - -mnesia(_) -> - ?PRINT_CMD("mnesia", "Mnesia system info"). - -%%-------------------------------------------------------------------- -%% @doc Trace Command - -trace(["list"]) -> - foreach(fun({{Who, Name}, LogFile}) -> - ?PRINT("trace ~s ~s -> ~s~n", [Who, Name, LogFile]) - end, emqx_trace:all_traces()); - -trace(["client", ClientId, "off"]) -> - trace_off(client, ClientId); - -trace(["client", ClientId, LogFile]) -> - trace_on(client, ClientId, LogFile); - -trace(["topic", Topic, "off"]) -> - trace_off(topic, Topic); - -trace(["topic", Topic, LogFile]) -> - trace_on(topic, Topic, LogFile); - -trace(_) -> - ?USAGE([{"trace list", "List all traces"}, - {"trace client ","Trace a client"}, - {"trace client off", "Stop tracing a client"}, - {"trace topic ", "Trace a topic"}, - {"trace topic off", "Stop tracing a Topic"}]). - -trace_on(Who, Name, LogFile) -> - case emqx_trace:start_trace({Who, iolist_to_binary(Name)}, LogFile) of - ok -> - ?PRINT("trace ~s ~s successfully.~n", [Who, Name]); - {error, Error} -> - ?PRINT("trace ~s ~s error: ~p~n", [Who, Name, Error]) - end. - -trace_off(Who, Name) -> - case emqx_trace:stop_trace({Who, iolist_to_binary(Name)}) of - ok -> - ?PRINT("stop tracing ~s ~s successfully.~n", [Who, Name]); - {error, Error} -> - ?PRINT("stop tracing ~s ~s error: ~p.~n", [Who, Name, Error]) - end. - -%%-------------------------------------------------------------------- -%% @doc Listeners Command - -listeners([]) -> - foreach(fun({{Protocol, ListenOn}, Pid}) -> - Info = [{acceptors, esockd:get_acceptors(Pid)}, - {max_clients, esockd:get_max_clients(Pid)}, - {current_clients,esockd:get_current_clients(Pid)}, - {shutdown_count, esockd:get_shutdown_count(Pid)}], - ?PRINT("listener on ~s:~s~n", [Protocol, esockd:to_string(ListenOn)]), - foreach(fun({Key, Val}) -> - ?PRINT(" ~-16s: ~w~n", [Key, Val]) - end, Info) - end, esockd:listeners()); - -listeners(["restart", Proto, ListenOn]) -> - ListenOn1 = case string:tokens(ListenOn, ":") of - [Port] -> list_to_integer(Port); - [IP, Port] -> {IP, list_to_integer(Port)} - end, - case emqx:restart_listener({list_to_atom(Proto), ListenOn1, []}) of - {ok, _Pid} -> - io:format("Restart ~s listener on ~s successfully.~n", [Proto, ListenOn]); - {error, Error} -> - io:format("Failed to restart ~s listener on ~s, error:~p~n", [Proto, ListenOn, Error]) - end; - -listeners(["stop", Proto, ListenOn]) -> - ListenOn1 = case string:tokens(ListenOn, ":") of - [Port] -> list_to_integer(Port); - [IP, Port] -> {IP, list_to_integer(Port)} - end, - case emqx:stop_listener({list_to_atom(Proto), ListenOn1, []}) of - ok -> - io:format("Stop ~s listener on ~s successfully.~n", [Proto, ListenOn]); - {error, Error} -> - io:format("Failed to stop ~s listener on ~s, error:~p~n", [Proto, ListenOn, Error]) - end; - -listeners(_) -> - ?USAGE([{"listeners", "List listeners"}, - {"listeners restart ", "Restart a listener"}, - {"listeners stop ", "Stop a listener"}]). - -%%-------------------------------------------------------------------- -%% Dump ETS -%%-------------------------------------------------------------------- - -dump(Table) -> - dump(Table, ets:first(Table)). - -dump(_Table, '$end_of_table') -> - ok; - -dump(Table, Key) -> - case ets:lookup(Table, Key) of - [Record] -> print(Record); - [] -> ok - end, - dump(Table, ets:next(Table, Key)). - -print([]) -> - ok; - -print(Routes = [#mqtt_route{topic = Topic} | _]) -> - Nodes = [atom_to_list(Node) || #mqtt_route{node = Node} <- Routes], - ?PRINT("~s -> ~s~n", [Topic, string:join(Nodes, ",")]); - -%% print(Subscriptions = [#mqtt_subscription{subid = ClientId} | _]) -> -%% TopicTable = [io_lib:format("~s:~w", [Topic, Qos]) -%% || #mqtt_subscription{topic = Topic, qos = Qos} <- Subscriptions], -%% ?PRINT("~s -> ~s~n", [ClientId, string:join(TopicTable, ",")]); - -%% print(Topics = [#mqtt_topic{}|_]) -> -%% foreach(fun print/1, Topics); - -print(#mqtt_plugin{name = Name, version = Ver, descr = Descr, active = Active}) -> - ?PRINT("Plugin(~s, version=~s, description=~s, active=~s)~n", - [Name, Ver, Descr, Active]); - -print(#mqtt_client{client_id = ClientId, clean_sess = CleanSess, username = Username, - peername = Peername, connected_at = ConnectedAt}) -> - ?PRINT("Client(~s, clean_sess=~s, username=~s, peername=~s, connected_at=~p)~n", - [ClientId, CleanSess, Username, emqx_net:format(Peername), - emqx_time:now_secs(ConnectedAt)]); - -%% print(#mqtt_topic{topic = Topic, flags = Flags}) -> -%% ?PRINT("~s: ~s~n", [Topic, string:join([atom_to_list(F) || F <- Flags], ",")]); -print({route, Routes}) -> - foreach(fun print/1, Routes); -print({local_route, Routes}) -> - foreach(fun print/1, Routes); -print(#mqtt_route{topic = Topic, node = Node}) -> - ?PRINT("~s -> ~s~n", [Topic, Node]); -print({Topic, Node}) -> - ?PRINT("~s -> ~s~n", [Topic, Node]); - -print({ClientId, _ClientPid, _Persistent, SessInfo}) -> - Data = lists:append(SessInfo, emqx_stats:get_session_stats(ClientId)), - InfoKeys = [clean_sess, - subscriptions, - max_inflight, - inflight_len, - mqueue_len, - mqueue_dropped, - awaiting_rel_len, - deliver_msg, - enqueue_msg, - created_at], - ?PRINT("Session(~s, clean_sess=~s, subscriptions=~w, max_inflight=~w, inflight=~w, " - "mqueue_len=~w, mqueue_dropped=~w, awaiting_rel=~w, " - "deliver_msg=~w, enqueue_msg=~w, created_at=~w)~n", - [ClientId | [format(Key, get_value(Key, Data)) || Key <- InfoKeys]]). - -print(subscription, {Sub, {share, _Share, Topic}}) when is_pid(Sub) -> - ?PRINT("~p -> ~s~n", [Sub, Topic]); -print(subscription, {Sub, Topic}) when is_pid(Sub) -> - ?PRINT("~p -> ~s~n", [Sub, Topic]); -print(subscription, {{SubId, SubPid}, {share, _Share, Topic}}) - when is_binary(SubId), is_pid(SubPid) -> - ?PRINT("~s~p -> ~s~n", [SubId, SubPid, Topic]); -print(subscription, {{SubId, SubPid}, Topic}) - when is_binary(SubId), is_pid(SubPid) -> - ?PRINT("~s~p -> ~s~n", [SubId, SubPid, Topic]); -print(subscription, {Sub, Topic, Props}) -> - print(subscription, {Sub, Topic}), - lists:foreach(fun({K, V}) when is_binary(V) -> - ?PRINT(" ~-8s: ~s~n", [K, V]); - ({K, V}) -> - ?PRINT(" ~-8s: ~w~n", [K, V]); - (K) -> - ?PRINT(" ~-8s: true~n", [K]) - end, Props). - -format(created_at, Val) -> - emqx_time:now_secs(Val); - -format(_, Val) -> - Val. - -bin(S) -> iolist_to_binary(S). - diff --git a/src/emqx_cli_config.erl b/src/emqx_cli_config.erl deleted file mode 100644 index a2167d2c6..000000000 --- a/src/emqx_cli_config.erl +++ /dev/null @@ -1,363 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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_cli_config). - --export ([register_config_cli/0, - register_config/0, - run/1, - set_usage/0, - all_cfgs/0, - get_cfg/2, - get_cfg/3, - read_config/1, - write_config/2]). - --define(APP, emqx). - --define(TAB, emqx_config). - -register_config() -> - application:start(clique), - F = fun() -> ekka_mnesia:running_nodes() end, - clique:register_node_finder(F), - register_config_cli(), - create_config_tab(). - -create_config_tab() -> - case ets:info(?TAB, name) of - undefined -> - ets:new(?TAB, [named_table, public]), - {ok, PluginsEtcDir} = emqx:env(plugins_etc_dir), - Files = filelib:wildcard("*.conf", PluginsEtcDir), - lists:foreach(fun(File) -> - [FileName | _] = string:tokens(File, "."), - Configs = cuttlefish_conf:file(lists:concat([PluginsEtcDir, File])), - ets:insert(?TAB, {list_to_atom(FileName), Configs}) - end, Files); - _ -> - ok - end. - -read_config(App) -> - case ets:lookup(?TAB, App) of - [] -> []; - [{_, Value}] -> Value - end. - -write_config(App, Terms) -> - ets:insert(?TAB, {App, Terms}). - -run(Cmd) -> - clique:run(Cmd). - -register_config_cli() -> - ok = clique_config:load_schema([code:priv_dir(?APP)], ?APP), - register_protocol_formatter(), - register_client_formatter(), - register_session_formatter(), - register_queue_formatter(), - register_lager_formatter(), - - register_auth_config(), - register_protocol_config(), - register_connection_config(), - register_client_config(), - register_session_config(), - register_queue_config(), - register_broker_config(), - register_lager_config(). - -set_usage() -> - io:format("~-40s# ~-20s# ~-20s ~p~n", ["key", "value", "datatype", "app"]), - io:format("------------------------------------------------------------------------------------------------~n"), - lists:foreach(fun({Key, Val, Datatype, App}) -> - io:format("~-40s# ~-20s# ~-20s ~p~n", [Key, Val, Datatype, App]) - end, all_cfgs()), - io:format("------------------------------------------------------------------------------------------------~n"), - io:format("Usage: set key=value --app=appname~n"). - -all_cfgs() -> - {Mappings, Mappings1} = lists:foldl( - fun({Key, {_, Map, _}}, {Acc, Acc1}) -> - Map1 = lists:map(fun(M) -> {cuttlefish_mapping:variable(M), Key} end, Map), - {Acc ++ Map, Acc1 ++ Map1} - end, {[], []}, ets:tab2list(clique_schema)), - lists:foldl(fun({Key, _}, Acc) -> - case lists:keyfind(cuttlefish_variable:tokenize(Key), 2, Mappings) of - false -> Acc; - Map -> - Datatype = format_datatype(cuttlefish_mapping:datatype(Map)), - App = proplists:get_value(cuttlefish_variable:tokenize(Key), Mappings1), - [{_, [Val0]}] = clique_config:show([Key], [{app, App}]), - Val = any_to_string(proplists:get_value(Key, Val0)), - [{Key, Val, Datatype, App} | Acc] - end - end, [],lists:sort(ets:tab2list(clique_config))). - -get_cfg(App, Key) -> - get_cfg(App, Key, undefined). - -get_cfg(App, Key, Def) -> - [{_, [Val0]}] = clique_config:show([Key], [{app, App}]), - proplists:get_value(Key, Val0, Def). - -format_datatype(Value) -> - format_datatype(Value, ""). - -format_datatype([Head], Acc) when is_tuple(Head) -> - [Head1 | _] = erlang:tuple_to_list(Head), - lists:concat([Acc, Head1]); -format_datatype([Head], Acc) -> - lists:concat([Acc, Head]); -format_datatype([Head | Tail], Acc) when is_tuple(Head)-> - [Head1 | _] = erlang:tuple_to_list(Head), - format_datatype(Tail, Acc ++ lists:concat([Head1, ", "])); -format_datatype([Head | Tail], Acc) -> - format_datatype(Tail, Acc ++ lists:concat([Head, ", "])). - -%%-------------------------------------------------------------------- -%% Auth/Acl -%%-------------------------------------------------------------------- - -register_auth_config() -> - ConfigKeys = ["mqtt.allow_anonymous", - "mqtt.acl_nomatch", - "mqtt.acl_file", - "mqtt.cache_acl"], - [clique:register_config(Key , fun auth_config_callback/2) || Key <- ConfigKeys], - ok = register_config_whitelist(ConfigKeys). - -auth_config_callback([_, KeyStr], Value) -> - application:set_env(?APP, l2a(KeyStr), Value), " successfully\n". - -%%-------------------------------------------------------------------- -%% MQTT Protocol -%%-------------------------------------------------------------------- - -register_protocol_formatter() -> - ConfigKeys = ["max_clientid_len", - "max_packet_size", - "websocket_protocol_header", - "keepalive_backoff"], - [clique:register_formatter(["mqtt", Key], fun protocol_formatter_callback/2) || Key <- ConfigKeys]. - -protocol_formatter_callback([_, "websocket_protocol_header"], Params) -> - Params; -protocol_formatter_callback([_, Key], Params) -> - proplists:get_value(l2a(Key), Params). - -register_protocol_config() -> - ConfigKeys = ["mqtt.max_clientid_len", - "mqtt.max_packet_size", - "mqtt.websocket_protocol_header", - "mqtt.keepalive_backoff"], - [clique:register_config(Key , fun protocol_config_callback/2) || Key <- ConfigKeys], - ok = register_config_whitelist(ConfigKeys). - -protocol_config_callback([_AppStr, KeyStr], Value) -> - protocol_config_callback(protocol, l2a(KeyStr), Value). -protocol_config_callback(_App, websocket_protocol_header, Value) -> - application:set_env(?APP, websocket_protocol_header, Value), - " successfully\n"; -protocol_config_callback(App, Key, Value) -> - {ok, Env} = emqx:env(App), - application:set_env(?APP, App, lists:keyreplace(Key, 1, Env, {Key, Value})), - " successfully\n". - -%%-------------------------------------------------------------------- -%% MQTT Connection -%%-------------------------------------------------------------------- - -register_connection_config() -> - ConfigKeys = ["mqtt.conn.force_gc_count"], - [clique:register_config(Key , fun connection_config_callback/2) || Key <- ConfigKeys], - ok = register_config_whitelist(ConfigKeys). - -connection_config_callback([_, KeyStr0, KeyStr1], Value) -> - KeyStr = lists:concat([KeyStr0, "_", KeyStr1]), - application:set_env(?APP, l2a(KeyStr), Value), - " successfully\n". - -%%-------------------------------------------------------------------- -%% MQTT Client -%%-------------------------------------------------------------------- - -register_client_formatter() -> - ConfigKeys = ["max_publish_rate", - "idle_timeout", - "enable_stats"], - [clique:register_formatter(["mqtt", "client", Key], fun client_formatter_callback/2) || Key <- ConfigKeys]. - -client_formatter_callback([_, _, Key], Params) -> - proplists:get_value(list_to_atom(Key), Params). - -register_client_config() -> - ConfigKeys = ["mqtt.client.max_publish_rate", - "mqtt.client.idle_timeout", - "mqtt.client.enable_stats"], - [clique:register_config(Key , fun client_config_callback/2) || Key <- ConfigKeys], - ok = register_config_whitelist(ConfigKeys). - -client_config_callback([_, AppStr, KeyStr], Value) -> - client_config_callback(l2a(AppStr), l2a(KeyStr), Value). - -client_config_callback(App, idle_timeout, Value) -> - {ok, Env} = emqx:env(App), - application:set_env(?APP, App, lists:keyreplace(client_idle_timeout, 1, Env, {client_idle_timeout, Value})), - " successfully\n"; -client_config_callback(App, enable_stats, Value) -> - {ok, Env} = emqx:env(App), - application:set_env(?APP, App, lists:keyreplace(client_enable_stats, 1, Env, {client_enable_stats, Value})), - " successfully\n"; -client_config_callback(App, Key, Value) -> - {ok, Env} = emqx:env(App), - application:set_env(?APP, App, lists:keyreplace(Key, 1, Env, {Key, Value})), - " successfully\n". - -%%-------------------------------------------------------------------- -%% session -%%-------------------------------------------------------------------- - -register_session_formatter() -> - ConfigKeys = ["max_subscriptions", - "upgrade_qos", - "max_inflight", - "retry_interval", - "max_awaiting_rel", - "await_rel_timeout", - "enable_stats", - "expiry_interval", - "ignore_loop_deliver"], - [clique:register_formatter(["mqtt", "session", Key], fun session_formatter_callback/2) || Key <- ConfigKeys]. - -session_formatter_callback([_, _, Key], Params) -> - proplists:get_value(list_to_atom(Key), Params). - -register_session_config() -> - ConfigKeys = ["mqtt.session.max_subscriptions", - "mqtt.session.upgrade_qos", - "mqtt.session.max_inflight", - "mqtt.session.retry_interval", - "mqtt.session.max_awaiting_rel", - "mqtt.session.await_rel_timeout", - "mqtt.session.enable_stats", - "mqtt.session.expiry_interval", - "mqtt.session.ignore_loop_deliver"], - [clique:register_config(Key , fun session_config_callback/2) || Key <- ConfigKeys], - ok = register_config_whitelist(ConfigKeys). - -session_config_callback([_, AppStr, KeyStr], Value) -> - session_config_callback(l2a(AppStr), l2a(KeyStr), Value). -session_config_callback(App, Key, Value) -> - {ok, Env} = emqx:env(App), - application:set_env(?APP, App, lists:keyreplace(Key, 1, Env, {Key, Value})), - " successfully\n". - -l2a(List) -> list_to_atom(List). - -%%-------------------------------------------------------------------- -%% MQTT MQueue -%%-------------------------------------------------------------------- - -register_queue_formatter() -> - ConfigKeys = ["type", - "priority", - "max_length", - "low_watermark", - "high_watermark", - "store_qos0"], - [clique:register_formatter(["mqtt", "mqueue", Key], fun queue_formatter_callback/2) || Key <- ConfigKeys]. - -queue_formatter_callback([_, _, Key], Params) -> - proplists:get_value(list_to_atom(Key), Params). - -register_queue_config() -> - ConfigKeys = ["mqtt.mqueue.type", - "mqtt.mqueue.priority", - "mqtt.mqueue.max_length", - "mqtt.mqueue.low_watermark", - "mqtt.mqueue.high_watermark", - "mqtt.mqueue.store_qos0"], - [clique:register_config(Key , fun queue_config_callback/2) || Key <- ConfigKeys], - ok = register_config_whitelist(ConfigKeys). - -queue_config_callback([_, AppStr, KeyStr], Value) -> - queue_config_callback(l2a(AppStr), l2a(KeyStr), Value). - -queue_config_callback(App, low_watermark, Value) -> - {ok, Env} = emqx:env(App), - application:set_env(?APP, App, lists:keyreplace(low_watermark, 1, Env, {low_watermark, Value})), - " successfully\n"; -queue_config_callback(App, high_watermark, Value) -> - {ok, Env} = emqx:env(App), - application:set_env(?APP, App, lists:keyreplace(high_watermark, 1, Env, {high_watermark, Value})), - " successfully\n"; -queue_config_callback(App, Key, Value) -> - {ok, Env} = emqx:env(App), - application:set_env(?APP, App, lists:keyreplace(Key, 1, Env, {Key, Value})), - " successfully\n". - -%%-------------------------------------------------------------------- -%% MQTT Broker -%%-------------------------------------------------------------------- - -register_broker_config() -> - ConfigKeys = ["mqtt.broker.sys_interval"], - [clique:register_config(Key , fun broker_config_callback/2) || Key <- ConfigKeys], - ok = register_config_whitelist(ConfigKeys). - -broker_config_callback([_, KeyStr0, KeyStr1], Value) -> - KeyStr = lists:concat([KeyStr0, "_", KeyStr1]), - application:set_env(?APP, l2a(KeyStr), Value), - " successfully\n". - -%%-------------------------------------------------------------------- -%% MQTT Lager -%%-------------------------------------------------------------------- - -register_lager_formatter() -> - ConfigKeys = ["level"], - [clique:register_formatter(["log", "console", Key], fun lager_formatter_callback/2) || Key <- ConfigKeys]. - -lager_formatter_callback(_, Params) -> - proplists:get_value(lager_console_backend, Params). - -register_lager_config() -> - ConfigKeys = ["log.console.level"], - [clique:register_config(Key , fun lager_config_callback/2) || Key <- ConfigKeys], - ok = register_config_whitelist(ConfigKeys). - -lager_config_callback(_, Value) -> - lager:set_loglevel(lager_console_backend, Value), - " successfully\n". - -register_config_whitelist(ConfigKeys) -> - clique:register_config_whitelist(ConfigKeys, ?APP). - -%%-------------------------------------------------------------------- -%% Inner Function -%%-------------------------------------------------------------------- -any_to_string(I) when is_integer(I) -> - integer_to_list(I); -any_to_string(F) when is_float(F)-> - float_to_list(F,[{decimals, 4}]); -any_to_string(A) when is_atom(A) -> - atom_to_list(A); -any_to_string(B) when is_binary(B) -> - binary_to_list(B); -any_to_string(L) when is_list(L) -> - L. diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index 565c08ff1..8f3ba57d3 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -68,15 +68,6 @@ run([]) -> usage(), ok; run(["help"]) -> usage(), ok; -run(["set"] = CmdS) when length(CmdS) =:= 1 -> - emqx_cli_config:set_usage(), ok; - -run(["set" | _] = CmdS) -> - emqx_cli_config:run(["config" | CmdS]), ok; - -run(["show" | _] = CmdS) -> - emqx_cli_config:run(["config" | CmdS]), ok; - run([CmdS|Args]) -> case lookup(list_to_atom(CmdS)) of [{Mod, Fun}] -> diff --git a/src/emqx_http.erl b/src/emqx_http.erl deleted file mode 100644 index efc24e8fc..000000000 --- a/src/emqx_http.erl +++ /dev/null @@ -1,236 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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 HTTP publish API and websocket client. - --module(emqx_http). - --author("Feng Lee "). - --include("emqx.hrl"). - --include("emqx_mqtt.hrl"). - --import(proplists, [get_value/2, get_value/3]). - --export([http_handler/0, handle_request/2, http_api/0, inner_handle_request/2]). - --include("emqx_rest.hrl"). - --include("emqx_internal.hrl"). - --record(state, {dispatch}). - -http_handler() -> - APIs = http_api(), - State = #state{dispatch = dispatcher(APIs)}, - {?MODULE, handle_request, [State]}. - -http_api() -> - Attr = emqx_rest_api:module_info(attributes), - [{Regexp, Method, Function, Args} || {http_api, [{Regexp, Method, Function, Args}]} <- Attr]. - -%%-------------------------------------------------------------------- -%% Handle HTTP Request -%%-------------------------------------------------------------------- -handle_request(Req, State) -> - Path = Req:get(path), - case Path of - "/status" -> - handle_request("/status", Req, Req:get(method)); - "/" -> - handle_request("/", Req, Req:get(method)); - "/api/v2/auth" -> %%TODO: Security Issue! - handle_request(Path, Req, State); - _ -> - if_authorized(Req, fun() -> handle_request(Path, Req, State) end) - end. - -inner_handle_request(Req, State) -> - Path = Req:get(path), - case Path of - "/api/v2/auth" -> handle_request(Path, Req, State); - _ -> if_authorized(Req, fun() -> handle_request(Path, Req, State) end) - end. - -handle_request("/api/v2/" ++ Url, Req, #state{dispatch = Dispatch}) -> - Dispatch(Req, Url); - -handle_request("/status", Req, Method) when Method =:= 'HEAD'; Method =:= 'GET' -> - {InternalStatus, _ProvidedStatus} = init:get_status(), - AppStatus = case lists:keysearch(emqx, 1, application:which_applications()) of - false -> not_running; - {value, _Val} -> running - end, - Status = io_lib:format("Node ~s is ~s~nemqx is ~s", - [node(), InternalStatus, AppStatus]), - Req:ok({"text/plain", iolist_to_binary(Status)}); - -handle_request("/", Req, Method) when Method =:= 'HEAD'; Method =:= 'GET' -> - respond(Req, 200, api_list()); - -handle_request(_, Req, #state{}) -> - respond(Req, 404, []). - -dispatcher(APIs) -> - fun(Req, Url) -> - Method = Req:get(method), - case filter(APIs, Url, Method) of - [{Regexp, _Method, Function, FilterArgs}] -> - case params(Req) of - {error, Error1} -> - respond(Req, 200, Error1); - Params -> - case {check_params(Params, FilterArgs), - check_params_type(Params, FilterArgs)} of - {true, true} -> - {match, [MatchList]} = re:run(Url, Regexp, [global, {capture, all_but_first, list}]), - Args = lists:append([[Method, Params], MatchList]), - lager:debug("Mod:~p, Fun:~p, Args:~p", [emqx_rest_api, Function, Args]), - case catch apply(emqx_rest_api, Function, Args) of - {ok, Data} -> - respond(Req, 200, [{code, ?SUCCESS}, {result, Data}]); - {error, Error} -> - respond(Req, 200, Error); - {'EXIT', Reason} -> - lager:error("Execute API '~s' Error: ~p", [Url, Reason]), - respond(Req, 404, []) - end; - {false, _} -> - respond(Req, 200, [{code, ?ERROR7}, {message, <<"params error">>}]); - {_, false} -> - respond(Req, 200, [{code, ?ERROR8}, {message, <<"params type error">>}]) - end - end; - _ -> - lager:error("No match Url:~p", [Url]), - respond(Req, 404, []) - end - end. - -% %%-------------------------------------------------------------------- -% %% Basic Authorization -% %%-------------------------------------------------------------------- -if_authorized(Req, Fun) -> - case authorized(Req) of - true -> Fun(); - false -> respond(Req, 401, []) - end. - -authorized(Req) -> - case Req:get_header_value("Authorization") of - undefined -> - false; - "Basic " ++ BasicAuth -> - {Username, Password} = user_passwd(BasicAuth), - case emq_mgmt:check_user(Username, Password) of - ok -> - true; - {error, Reason} -> - lager:error("HTTP Auth failure: username=~s, reason=~p", [Username, Reason]), - false - end - end. - -user_passwd(BasicAuth) -> - list_to_tuple(binary:split(base64:decode(BasicAuth), <<":">>)). - -respond(Req, 401, Data) -> - Req:respond({401, [{"WWW-Authenticate", "Basic Realm=\"emqx control center\""}], Data}); -respond(Req, 404, Data) -> - Req:respond({404, [{"Content-Type", "text/plain"}], Data}); -respond(Req, 200, Data) -> - Req:respond({200, [{"Content-Type", "application/json"}], to_json(Data)}); -respond(Req, Code, Data) -> - Req:respond({Code, [{"Content-Type", "text/plain"}], Data}). - -filter(APIs, Url, Method) -> - lists:filter(fun({Regexp, Method1, _Function, _Args}) -> - case re:run(Url, Regexp, [global, {capture, all_but_first, list}]) of - {match, _} -> Method =:= Method1; - _ -> false - end - end, APIs). - -params(Req) -> - Method = Req:get(method), - case Method of - 'GET' -> - mochiweb_request:parse_qs(Req); - _ -> - case Req:recv_body() of - <<>> -> []; - undefined -> []; - Body -> - case jsx:is_json(Body) of - true -> jsx:decode(Body); - false -> - lager:error("Body:~p", [Body]), - {error, [{code, ?ERROR9}, {message, <<"Body not json">>}]} - end - end - end. - -check_params(_Params, Args) when Args =:= [] -> - true; -check_params(Params, Args)-> - not lists:any(fun({Item, _Type}) -> undefined =:= proplists:get_value(Item, Params) end, Args). - -check_params_type(_Params, Args) when Args =:= [] -> - true; -check_params_type(Params, Args) -> - not lists:any(fun({Item, Type}) -> - Val = proplists:get_value(Item, Params), - case Type of - int -> not is_integer(Val); - binary -> not is_binary(Val); - bool -> not is_boolean(Val) - end - end, Args). - -to_json([]) -> <<"[]">>; -to_json(Data) -> iolist_to_binary(mochijson2:encode(Data)). - -api_list() -> - [{paths, [<<"api/v2/management/nodes">>, - <<"api/v2/management/nodes/{node_name}">>, - <<"api/v2/monitoring/nodes">>, - <<"api/v2/monitoring/nodes/{node_name}">>, - <<"api/v2/monitoring/listeners">>, - <<"api/v2/monitoring/listeners/{node_name}">>, - <<"api/v2/monitoring/metrics/">>, - <<"api/v2/monitoring/metrics/{node_name}">>, - <<"api/v2/monitoring/stats">>, - <<"api/v2/monitoring/stats/{node_name}">>, - <<"api/v2/nodes/{node_name}/clients">>, - <<"api/v2/nodes/{node_name}/clients/{clientid}">>, - <<"api/v2/clients/{clientid}">>, - <<"api/v2/clients/{clientid}/clean_acl_cache">>, - <<"api/v2/nodes/{node_name}/sessions">>, - <<"api/v2/nodes/{node_name}/sessions/{clientid}">>, - <<"api/v2/sessions/{clientid}">>, - <<"api/v2/nodes/{node_name}/subscriptions">>, - <<"api/v2/nodes/{node_name}/subscriptions/{clientid}">>, - <<"api/v2/subscriptions/{clientid}">>, - <<"api/v2/routes">>, - <<"api/v2/routes/{topic}">>, - <<"api/v2/mqtt/publish">>, - <<"api/v2/mqtt/subscribe">>, - <<"api/v2/mqtt/unsubscribe">>, - <<"api/v2/nodes/{node_name}/plugins">>, - <<"api/v2/nodes/{node_name}/plugins/{plugin_name}">>, - <<"api/v2/configs/{app}">>, - <<"api/v2/nodes/{node_name}/configs/{app}">>]}]. diff --git a/src/emqx_mgmt.erl.bk b/src/emqx_mgmt.erl.bk deleted file mode 100644 index 14345c019..000000000 --- a/src/emqx_mgmt.erl.bk +++ /dev/null @@ -1,544 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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_mgmt). - --author("Feng Lee "). - --include("emqx.hrl"). - --include("emqx_mqtt.hrl"). - --include("emqx_internal.hrl"). - --include("emqx_rest.hrl"). - --include_lib("stdlib/include/qlc.hrl"). - --record(mqtt_admin, {username, password, tags}). - --define(EMPTY_KEY(Key), ((Key == undefined) orelse (Key == <<>>))). - --import(proplists, [get_value/2]). - --export([brokers/0, broker/1, metrics/0, metrics/1, stats/1, stats/0, - plugins/0, plugins/1, listeners/0, listener/1, nodes_info/0, node_info/1]). - --export([plugin_list/1, plugin_unload/2, plugin_load/2]). - --export([client_list/4, session_list/4, route_list/3, subscription_list/4, alarm_list/0]). - --export([client/1, session/1, route/1, subscription/1]). - --export([query_table/4, lookup_table/3]). - --export([publish/1, subscribe/1, unsubscribe/1]). - --export([kick_client/1, clean_acl_cache/2]). - --export([modify_config/2, modify_config/3, modify_config/4, get_configs/0, get_config/1, - get_plugin_config/1, get_plugin_config/2, modify_plugin_config/2, modify_plugin_config/3]). - --export([add_user/3, check_user/2, user_list/0, lookup_user/1, - update_user/2, change_password/3, remove_user/1]). - --define(KB, 1024). --define(MB, (1024*1024)). --define(GB, (1024*1024*1024)). - -brokers() -> - [{Node, broker(Node)} || Node <- ekka_mnesia:running_nodes()]. - -broker(Node) when Node =:= node() -> - emqx_broker:info(); -broker(Node) -> - rpc_call(Node, broker, [Node]). - -metrics() -> - [{Node, metrics(Node)} || Node <- ekka_mnesia:running_nodes()]. - -metrics(Node) when Node =:= node() -> - emqx_metrics:all(); -metrics(Node) -> - rpc_call(Node, metrics, [Node]). - -stats() -> - [{Node, stats(Node)} || Node <- ekka_mnesia:running_nodes()]. - -stats(Node) when Node =:= node() -> - emqx_stats:getstats(); -stats(Node) -> - rpc_call(Node, stats, [Node]). - -plugins() -> - [{Node, plugins(Node)} || Node <- ekka_mnesia:running_nodes()]. - -plugins(Node) when Node =:= node() -> - emqx_plugins:list(Node); -plugins(Node) -> - rpc_call(Node, plugins, [Node]). - -listeners() -> - [{Node, listener(Node)} || Node <- ekka_mnesia:running_nodes()]. - -listener(Node) when Node =:= node() -> - lists:map(fun({{Protocol, ListenOn}, Pid}) -> - Info = [{acceptors, esockd:get_acceptors(Pid)}, - {max_clients, esockd:get_max_clients(Pid)}, - {current_clients,esockd:get_current_clients(Pid)}, - {shutdown_count, esockd:get_shutdown_count(Pid)}], - {Protocol, ListenOn, Info} - end, esockd:listeners()); - -listener(Node) -> - rpc_call(Node, listener, [Node]). - -nodes_info() -> - Running = mnesia:system_info(running_db_nodes), - Stopped = mnesia:system_info(db_nodes) -- Running, - DownNodes = lists:map(fun stop_node/1, Stopped), - [node_info(Node) || Node <- Running] ++ DownNodes. - -node_info(Node) when Node =:= node() -> - CpuInfo = [{K, list_to_binary(V)} || {K, V} <- emqx_vm:loads()], - Memory = emqx_vm:get_memory(), - OtpRel = "R" ++ erlang:system_info(otp_release) ++ "/" ++ erlang:system_info(version), - [{name, node()}, - {otp_release, list_to_binary(OtpRel)}, - {memory_total, kmg(get_value(allocated, Memory))}, - {memory_used, kmg(get_value(used, Memory))}, - {process_available, erlang:system_info(process_limit)}, - {process_used, erlang:system_info(process_count)}, - {max_fds, get_value(max_fds, erlang:system_info(check_io))}, - {clients, ets:info(mqtt_client, size)}, - {node_status, 'Running'} | CpuInfo]; - -node_info(Node) -> - rpc_call(Node, node_info, [Node]). - -stop_node(Node) -> - [{name, Node}, {node_status, 'Stopped'}]. -%%-------------------------------------------------------- -%% plugins -%%-------------------------------------------------------- -plugin_list(Node) when Node =:= node() -> - emqx_plugins:list(); -plugin_list(Node) -> - rpc_call(Node, plugin_list, [Node]). - -plugin_load(Node, PluginName) when Node =:= node() -> - emqx_plugins:load(PluginName); -plugin_load(Node, PluginName) -> - rpc_call(Node, plugin_load, [Node, PluginName]). - -plugin_unload(Node, PluginName) when Node =:= node() -> - emqx_plugins:unload(PluginName); -plugin_unload(Node, PluginName) -> - rpc_call(Node, plugin_unload, [Node, PluginName]). - -%%-------------------------------------------------------- -%% client -%%-------------------------------------------------------- -client_list(Node, Key, PageNo, PageSize) when Node =:= node() -> - client_list(Key, PageNo, PageSize); -client_list(Node, Key, PageNo, PageSize) -> - rpc_call(Node, client_list, [Node, Key, PageNo, PageSize]). - -client(ClientId) -> - lists:flatten([client_list(Node, ClientId, 1, 20) || Node <- ekka_mnesia:running_nodes()]). - -%%-------------------------------------------------------- -%% session -%%-------------------------------------------------------- -session_list(Node, Key, PageNo, PageSize) when Node =:= node() -> - session_list(Key, PageNo, PageSize); -session_list(Node, Key, PageNo, PageSize) -> - rpc_call(Node, session_list, [Node, Key, PageNo, PageSize]). - -session(ClientId) -> - lists:flatten([session_list(Node, ClientId, 1, 20) || Node <- ekka_mnesia:running_nodes()]). - -%%-------------------------------------------------------- -%% subscription -%%-------------------------------------------------------- -subscription_list(Node, Key, PageNo, PageSize) when Node =:= node() -> - subscription_list(Key, PageNo, PageSize); -subscription_list(Node, Key, PageNo, PageSize) -> - rpc_call(Node, subscription_list, [Node, Key, PageNo, PageSize]). - -subscription(Key) -> - lists:flatten([subscription_list(Node, Key, 1, 20) || Node <- ekka_mnesia:running_nodes()]). - -%%-------------------------------------------------------- -%% Routes -%%-------------------------------------------------------- -route(Key) -> route_list(Key, 1, 20). - -%%-------------------------------------------------------- -%% alarm -%%-------------------------------------------------------- -alarm_list() -> - emqx_alarm:get_alarms(). - -query_table(Qh, PageNo, PageSize, TotalNum) -> - Cursor = qlc:cursor(Qh), - case PageNo > 1 of - true -> qlc:next_answers(Cursor, (PageNo - 1) * PageSize); - false -> ok - end, - Rows = qlc:next_answers(Cursor, PageSize), - qlc:delete_cursor(Cursor), - [{totalNum, TotalNum}, - {totalPage, total_page(TotalNum, PageSize)}, - {result, Rows}]. - -total_page(TotalNum, PageSize) -> - case TotalNum rem PageSize of - 0 -> TotalNum div PageSize; - _ -> (TotalNum div PageSize) + 1 - end. - -%%TODO: refactor later... -lookup_table(LookupFun, _PageNo, _PageSize) -> - Rows = LookupFun(), - Rows. - -%%-------------------------------------------------------------------- -%% mqtt -%%-------------------------------------------------------------------- -publish({ClientId, Topic, Payload, Qos, Retain}) -> - case validate(topic, Topic) of - true -> - Msg = emqx_message:make(ClientId, Qos, Topic, Payload), - emqx:publish(Msg#mqtt_message{retain = Retain}), - ok; - false -> - {error, format_error(Topic, "validate topic: ${0} fail")} - end. - -subscribe({ClientId, Topic, Qos}) -> - case validate(topic, Topic) of - true -> - case emqx_sm:lookup_session(ClientId) of - undefined -> - {error, format_error(ClientId, "Clientid: ${0} not found")}; - #mqtt_session{sess_pid = SessPid} -> - emqx_session:subscribe(SessPid, [{Topic, [{qos, Qos}]}]), - ok - end; - false -> - {error, format_error(Topic, "validate topic: ${0} fail")} - end. - -unsubscribe({ClientId, Topic}) -> - case validate(topic, Topic) of - true -> - case emqx_sm:lookup_session(ClientId) of - undefined -> - {error, format_error(ClientId, "Clientid: ${0} not found")}; - #mqtt_session{sess_pid = SessPid} -> - emqx_session:unsubscribe(SessPid, [{Topic, []}]), - ok - end; - false -> - {error, format_error(Topic, "validate topic: ${0} fail")} - end. - -% publish(Messages) -> -% lists:foldl( -% fun({ClientId, Topic, Payload, Qos, Retain}, {Success, Failed}) -> -% case validate(topic, Topic) of -% true -> -% Msg = emqx_message:make(ClientId, Qos, Topic, Payload), -% emqx:publish(Msg#mqtt_message{retain = Retain}), -% {[[{topic, Topic}]| Success], Failed}; -% false -> -% {Success, [[{topic, Topic}]| Failed]} -% end -% end, {[], []}, Messages). - -% subscribers(Subscribers) -> -% lists:foldl( -% fun({ClientId, Topic, Qos}, {Success, Failed}) -> -% case emqx_sm:lookup_session(ClientId) of -% undefined -> -% {Success, [[{client_id, ClientId}]|Failed]}; -% #mqtt_session{sess_pid = SessPid} -> -% emqx_session:subscribe(SessPid, [{Topic, [{qos, Qos}]}]), -% {[[{client_id, ClientId}]| Success], Failed} -% end -% end,{[], []}, Subscribers). - -% unsubscribers(UnSubscribers)-> -% lists:foldl( -% fun({ClientId, Topic}, {Success, Failed}) -> -% case emqx_sm:lookup_session(ClientId) of -% undefined -> -% {Success, [[{client_id, ClientId}]|Failed]}; -% #mqtt_session{sess_pid = SessPid} -> -% emqx_session:unsubscriber(SessPid, [{Topic, []}]), -% {[[{client_id, ClientId}]| Success], Failed} -% end -% end, {[], []}, UnSubscribers). - -%%-------------------------------------------------------------------- -%% manager API -%%-------------------------------------------------------------------- -kick_client(ClientId) -> - Result = [kick_client(Node, ClientId) || Node <- ekka_mnesia:running_nodes()], - lists:any(fun(Item) -> Item =:= ok end, Result). - -kick_client(Node, ClientId) when Node =:= node() -> - case emqx_cm:lookup(ClientId) of - undefined -> error; - #mqtt_client{client_pid = Pid}-> emqx_client:kick(Pid) - end; -kick_client(Node, ClientId) -> - rpc_call(Node, kick_client, [Node, ClientId]). - - -clean_acl_cache(ClientId, Topic) -> - Result = [clean_acl_cache(Node, ClientId, Topic) || Node <- ekka_mnesia:running_nodes()], - lists:any(fun(Item) -> Item =:= ok end, Result). - -clean_acl_cache(Node, ClientId, Topic) when Node =:= node() -> - case emqx_cm:lookup(ClientId) of - undefined -> error; - #mqtt_client{client_pid = Pid}-> emqx_client:clean_acl_cache(Pid, Topic) - end; -clean_acl_cache(Node, ClientId, Topic) -> - rpc_call(Node, clean_acl_cache, [Node, ClientId, Topic]). - -%%-------------------------------------------------------------------- -%% Config ENV -%%-------------------------------------------------------------------- -modify_config(App, Terms) -> - emqx_config:write(App, Terms). - -modify_config(App, Key, Value) -> - Result = [modify_config(Node, App, Key, Value) || Node <- ekka_mnesia:running_nodes()], - lists:any(fun(Item) -> Item =:= ok end, Result). - -modify_config(Node, App, Key, Value) when Node =:= node() -> - emqx_config:set(App, Key, Value); -modify_config(Node, App, Key, Value) -> - rpc_call(Node, modify_config, [Node, App, Key, Value]). - -get_configs() -> - [{Node, get_config(Node)} || Node <- ekka_mnesia:running_nodes()]. - -get_config(Node) when Node =:= node()-> - emqx_cli_config:all_cfgs(); -get_config(Node) -> - rpc_call(Node, get_config, [Node]). - -get_plugin_config(PluginName) -> - emqx_config:read(PluginName). -get_plugin_config(Node, PluginName) -> - rpc_call(Node, get_plugin_config, [PluginName]). - -modify_plugin_config(PluginName, Terms) -> - emqx_config:write(PluginName, Terms). -modify_plugin_config(Node, PluginName, Terms) -> - rpc_call(Node, modify_plugin_config, [PluginName, Terms]). - -%%-------------------------------------------------------------------- -%% manager user API -%%-------------------------------------------------------------------- -check_user(undefined, _) -> - {error, "Username undefined"}; -check_user(_, undefined) -> - {error, "Password undefined"}; -check_user(Username, Password) -> - case mnesia:dirty_read(mqtt_admin, Username) of - [#mqtt_admin{password = <>}] -> - case Hash =:= md5_hash(Salt, Password) of - true -> ok; - false -> {error, "Password error"} - end; - [] -> - {error, "User not found"} - end. - -add_user(Username, Password, Tag) -> - Admin = #mqtt_admin{username = Username, - password = hash(Password), - tags = Tag}, - return(mnesia:transaction(fun add_user_/1, [Admin])). - -add_user_(Admin = #mqtt_admin{username = Username}) -> - case mnesia:wread({mqtt_admin, Username}) of - [] -> mnesia:write(Admin); - [_] -> {error, [{code, ?ERROR13}, {message, <<"User already exist">>}]} - end. - -user_list() -> - [row(Admin) || Admin <- ets:tab2list(mqtt_admin)]. - -lookup_user(Username) -> - Admin = mnesia:dirty_read(mqtt_admin, Username), - row(Admin). - -update_user(Username, Params) -> - case mnesia:dirty_read({mqtt_admin, Username}) of - [] -> - {error, [{code, ?ERROR5}, {message, <<"User not found">>}]}; - [User] -> - Admin = case proplists:get_value(<<"tags">>, Params) of - undefined -> User; - Tag -> User#mqtt_admin{tags = Tag} - end, - return(mnesia:transaction(fun() -> mnesia:write(Admin) end)) - end. - -remove_user(Username) -> - Trans = fun() -> - case lookup_user(Username) of - [] -> {error, [{code, ?ERROR5}, {message, <<"User not found">>}]}; - _ -> mnesia:delete({mqtt_admin, Username}) - end - end, - return(mnesia:transaction(Trans)). - -change_password(Username, OldPwd, NewPwd) -> - Trans = fun() -> - case mnesia:wread({mqtt_admin, Username}) of - [Admin = #mqtt_admin{password = <>}] -> - case Hash =:= md5_hash(Salt, OldPwd) of - true -> - mnesia:write(Admin#mqtt_admin{password = hash(NewPwd)}); - false -> - {error, [{code, ?ERROR14}, {message, <<"OldPassword error">>}]} - end; - [] -> - {error, [{code, ?ERROR5}, {message, <<"User not found">>}]} - end - end, - return(mnesia:transaction(Trans)). - -return({atomic, ok}) -> - ok; -return({atomic, Error}) -> - Error; -return({aborted, Reason}) -> - lager:error("Mnesia Transaction error:~p~n", [Reason]), - error. - -row(#mqtt_admin{username = Username, tags = Tags}) -> - [{username, Username}, {tags, Tags}]; -row([#mqtt_admin{username = Username, tags = Tags}]) -> - [{username, Username}, {tags, Tags}]; -row([]) ->[]. -%%-------------------------------------------------------------------- -%% Internel Functions. -%%-------------------------------------------------------------------- - -rpc_call(Node, Fun, Args) -> - case rpc:call(Node, ?MODULE, Fun, Args) of - {badrpc, Reason} -> {error, Reason}; - Res -> Res - end. - -kmg(Byte) when Byte > ?GB -> - float(Byte / ?GB, "G"); -kmg(Byte) when Byte > ?MB -> - float(Byte / ?MB, "M"); -kmg(Byte) when Byte > ?KB -> - float(Byte / ?MB, "K"); -kmg(Byte) -> - Byte. -float(F, S) -> - iolist_to_binary(io_lib:format("~.2f~s", [F, S])). - -validate(qos, Qos) -> - (Qos >= ?QOS_0) and (Qos =< ?QOS_2); - -validate(topic, Topic) -> - emqx_topic:validate({name, Topic}). - -client_list(ClientId, PageNo, PageSize) when ?EMPTY_KEY(ClientId) -> - TotalNum = ets:info(mqtt_client, size), - Qh = qlc:q([R || R <- ets:table(mqtt_client)]), - query_table(Qh, PageNo, PageSize, TotalNum); - -client_list(ClientId, PageNo, PageSize) -> - Fun = fun() -> ets:lookup(mqtt_client, ClientId) end, - lookup_table(Fun, PageNo, PageSize). - -session_list(ClientId, PageNo, PageSize) when ?EMPTY_KEY(ClientId) -> - TotalNum = lists:sum([ets:info(Tab, size) || Tab <- [mqtt_local_session]]), - Qh = qlc:append([qlc:q([E || E <- ets:table(Tab)]) || Tab <- [mqtt_local_session]]), - query_table(Qh, PageNo, PageSize, TotalNum); - -session_list(ClientId, PageNo, PageSize) -> - MP = {ClientId, '_', '_', '_'}, - Fun = fun() -> lists:append([ets:match_object(Tab, MP) || Tab <- [mqtt_local_session]]) end, - lookup_table(Fun, PageNo, PageSize). - -subscription_list(Key, PageNo, PageSize) when ?EMPTY_KEY(Key) -> - TotalNum = ets:info(mqtt_subproperty, size), - Qh = qlc:q([E || E <- ets:table(mqtt_subproperty)]), - query_table(Qh, PageNo, PageSize, TotalNum); - -subscription_list(Key, PageNo, PageSize) -> - Fun = fun() -> ets:match_object(mqtt_subproperty, {{'_', {Key, '_'}}, '_'}) end, - lookup_table(Fun, PageNo, PageSize). - -route_list(Topic, PageNo, PageSize) when ?EMPTY_KEY(Topic) -> - Tables = [mqtt_route], - TotalNum = lists:sum([ets:info(Tab, size) || Tab <- [mqtt_route, mqtt_local_route]]), - Qh = qlc:append([qlc:q([E || E <- ets:table(Tab)]) || Tab <- Tables]), - Data = query_table(Qh, PageNo, PageSize, TotalNum), - Route = get_value(result, Data), - LocalRoute = local_route_list(Topic, PageNo, PageSize), - lists:keyreplace(result, 1, Data, {result, lists:append(Route, LocalRoute)}); - -route_list(Topic, PageNo, PageSize) -> - Tables = [mqtt_route], - Fun = fun() -> lists:append([ets:lookup(Tab, Topic) || Tab <- Tables]) end, - Route = lookup_table(Fun, PageNo, PageSize), - LocalRoute = local_route_list(Topic, PageNo, PageSize), - lists:append(Route, LocalRoute). - -local_route_list(Topic, PageNo, PageSize) when ?EMPTY_KEY(Topic) -> - TotalNum = lists:sum([ets:info(Tab, size) || Tab <- [mqtt_local_route]]), - Qh = qlc:append([qlc:q([E || E <- ets:table(Tab)]) || Tab <- [mqtt_local_route]]), - Data = query_table(Qh, PageNo, PageSize, TotalNum), - lists:map(fun({Topic1, Node}) -> {<<"$local/", Topic1/binary>>, Node} end, get_value(result, Data)); - -local_route_list(Topic, PageNo, PageSize) -> - Fun = fun() -> lists:append([ets:lookup(Tab, Topic) || Tab <- [mqtt_local_route]]) end, - Data = lookup_table(Fun, PageNo, PageSize), - lists:map(fun({Topic1, Node}) -> {<<"$local/", Topic1/binary>>, Node} end, Data). - - -format_error(Val, Msg) -> - re:replace(Msg, <<"\\$\\{[^}]+\\}">>, Val, [global, {return, binary}]). - -hash(Password) -> - SaltBin = salt(), - <>. - -md5_hash(SaltBin, Password) -> - erlang:md5(<>). - -salt() -> - seed(), - Salt = rand:uniform(16#ffffffff), - <>. - -seed() -> - rand:seed(exsplus, erlang:timestamp()). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index e50d7355a..093fc02f7 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -204,15 +204,16 @@ process(?CONNECT_PACKET(Var), State0) -> client_id = ClientId, is_bridge = IsBridge} = Var, - State1 = State0#proto_state{proto_ver = ProtoVer, - proto_name = ProtoName, - username = Username, - client_id = ClientId, - clean_sess = CleanSess, - keepalive = KeepAlive, - will_msg = willmsg(Var, State0), - is_bridge = IsBridge, - connected_at = os:timestamp()}, + State1 = repl_username_with_peercert( + State0#proto_state{proto_ver = ProtoVer, + proto_name = ProtoName, + username = Username, + client_id = ClientId, + clean_sess = CleanSess, + keepalive = KeepAlive, + will_msg = willmsg(Var, State0), + is_bridge = IsBridge, + connected_at = os:timestamp()}), {ReturnCode1, SessPresent, State3} = case validate_connect(Var, State1) of @@ -407,6 +408,7 @@ shutdown(mnesia_conflict, _State) -> %% let it down %% emqx_cm:unreg(ClientId); ignore; + shutdown(Error, State = #proto_state{will_msg = WillMsg}) -> ?LOG(info, "Shutdown for ~p", [Error], State), Client = client(State), diff --git a/src/emqx_router.erl b/src/emqx_router.erl index c376325aa..4ab3b49b2 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -34,7 +34,10 @@ -export([start/0, stop/0]). %% Route APIs --export([add_route/1, del_route/1, match/1, print/1, has_route/1]). +-export([add_route/1, get_routes/1, del_route/1, has_route/1]). + +%% Match and print +-export([match/1, print/1]). %% Local Route API -export([get_local_routes/0, add_local_route/1, match_local/1, @@ -130,6 +133,11 @@ add_trie_route(Route = #mqtt_route{topic = Topic}) -> end, mnesia:write(Route). +%% @doc Lookup Routes +-spec(get_routes(binary()) -> [mqtt_route()]). +get_routes(Topic) -> + ets:lookup(mqtt_route, Topic). + %% @doc Delete Route -spec(del_route(binary() | mqtt_route()) -> ok | {error, Reason :: term()}). del_route(Topic) when is_binary(Topic) -> @@ -284,7 +292,5 @@ clean_routes_(Node) -> mnesia:transaction(Clean). update_stats_() -> - Size = mnesia:table_info(mqtt_route, size), - emqx_stats:setstats('routes/count', 'routes/max', Size), - emqx_stats:setstats('topics/count', 'topics/max', Size). + emqx_stats:setstats('routes/count', 'routes/max', mnesia:table_info(mqtt_route, size)). diff --git a/test/emqttd_cli_SUITE.erl b/test/emqttd_cli_SUITE.erl deleted file mode 100644 index 273518b7f..000000000 --- a/test/emqttd_cli_SUITE.erl +++ /dev/null @@ -1,52 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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(emqttd_cli_SUITE). - --compile(export_all). - --include("emqttd.hrl"). - --include_lib("eunit/include/eunit.hrl"). - -all() -> - [{group, subscriptions}]. - -groups() -> - [{subscriptions, [sequence], - [t_subsciptions_list, - t_subsciptions_show, - t_subsciptions_add, - t_subsciptions_del]}]. - -init_per_suite(Config) -> - Config. - -end_per_suite(_Config) -> - todo. - -t_subsciptions_list(_) -> - todo. - -t_subsciptions_show(_) -> - todo. - -t_subsciptions_add(_) -> - todo. - -t_subsciptions_del(_) -> - todo. - diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index 2bd961f30..472069464 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -68,7 +68,6 @@ all() -> {group, http}, {group, alarms}, {group, cli}, - {group, rest_api}, {group, cleanSession}]. groups() -> @@ -121,9 +120,7 @@ groups() -> {cleanSession, [sequence], [cleanSession_validate, cleanSession_validate1] - }, - {rest_api, [sequence], - [get_api_lists]} + } ]. init_per_suite(Config) -> @@ -413,14 +410,59 @@ request_publish(_) -> {client_id, <<"random">>}, {clean_sess, false}]), SubParams = "{\"qos\":1, \"topic\" : \"a\/b\/c\", \"client_id\" :\"random\"}", - ?assert(connect_emqttd_pubsub_(post, "api/v2/mqtt/subscribe", SubParams, auth_header_("admin", "public"))), - ok = emqttd:subscribe(<<"a/b/c">>, self(), [{qos, 1}]), + ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/subscribe", SubParams, auth_header_("admin", "public"))), + ok = emqx:subscribe(<<"a/b/c">>, self(), [{qos, 1}]), Params = "{\"qos\":1, \"retain\":false, \"topic\" : \"a\/b\/c\", \"messages\" :\"hello\"}", - ?assert(connect_emqttd_pubsub_(post, "api/v2/mqtt/publish", Params, auth_header_("admin", "public"))), + ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/publish", Params, auth_header_("admin", "public"))), ?assert(receive {dispatch, <<"a/b/c">>, _} -> true after 2 -> false end), UnSubParams = "{\"topic\" : \"a\/b\/c\", \"client_id\" :\"random\"}", - ?assert(connect_emqttd_pubsub_(post, "api/v2/mqtt/unsubscribe", UnSubParams, auth_header_("admin", "public"))). + ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/unsubscribe", UnSubParams, auth_header_("admin", "public"))). + +connect_emqx_pubsub_(Method, Api, Params, Auth) -> + Url = "http://127.0.0.1:8080/" ++ Api, + case httpc:request(Method, {Url, [Auth], ?CONTENT_TYPE, Params}, [], []) of + {error, socket_closed_remotely} -> + false; + {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> + true; + {ok, {{"HTTP/1.1", 400, _}, _, []}} -> + false; + {ok, {{"HTTP/1.1", 404, _}, _, []}} -> + false + end. + +request(Path) -> + http_get(get, Path). + +http_get(Method, Path) -> + req(Method, Path, []). + +http_put(Method, Path, Params) -> + req(Method, Path, format_for_upload(Params)). + +http_post(Method, Path, Params) -> + req(Method, Path, format_for_upload(Params)). + +req(Method, Path, Body) -> + Url = ?URL ++ Path, + Headers = auth_header_("", ""), + case httpc:request(Method, {Url, [Headers]}, [], []) of + {error, R} -> + ct:log("R:~p~n", [R]), + false; + {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> + true; + {ok, {{"HTTP/1.1", 400, _}, _, []}} -> + false; + {ok, {{"HTTP/1.1", 404, _}, _, []}} -> + false + end. + +format_for_upload(none) -> + <<"">>; +format_for_upload(List) -> + iolist_to_binary(mochijson2:encode(List)). connect_emqx_publish_(Method, Api, Params, Auth) -> Url = "http://127.0.0.1:8080/" ++ Api, @@ -615,69 +657,6 @@ cleanSession_validate1(_) -> emqttc:disconnect(Pub), emqttc:disconnect(C11). -get_api_lists(_Config) -> - lists:foreach(fun request/1, ?GET_API). - -request_publish(_) -> - emqttc:start_link([{host, "localhost"}, - {port, 1883}, - {client_id, <<"random">>}, - {clean_sess, false}]), - SubParams = "{\"qos\":1, \"topic\" : \"a\/b\/c\", \"client_id\" :\"random\"}", - ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/subscribe", SubParams, auth_header_("", ""))), - ok = emqx:subscribe(<<"a/b/c">>, self(), [{qos, 1}]), - Params = "{\"qos\":1, \"retain\":false, \"topic\" : \"a\/b\/c\", \"messages\" :\"hello\"}", - ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/publish", Params, auth_header_("", ""))), - ?assert(receive {dispatch, <<"a/b/c">>, _} -> true after 2 -> false end), - - UnSubParams = "{\"topic\" : \"a\/b\/c\", \"client_id\" :\"random\"}", - ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/unsubscribe", UnSubParams, auth_header_("", ""))). - -connect_emqx_pubsub_(Method, Api, Params, Auth) -> - Url = "http://127.0.0.1:8080/" ++ Api, - case httpc:request(Method, {Url, [Auth], ?CONTENT_TYPE, Params}, [], []) of - {error, socket_closed_remotely} -> - false; - {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> - true; - {ok, {{"HTTP/1.1", 400, _}, _, []}} -> - false; - {ok, {{"HTTP/1.1", 404, _}, _, []}} -> - false - end. - -request(Path) -> - http_get(get, Path). - -http_get(Method, Path) -> - req(Method, Path, []). - -http_put(Method, Path, Params) -> - req(Method, Path, format_for_upload(Params)). - -http_post(Method, Path, Params) -> - req(Method, Path, format_for_upload(Params)). - -req(Method, Path, Body) -> - Url = ?URL ++ Path, - Headers = auth_header_("", ""), - case httpc:request(Method, {Url, [Headers]}, [], []) of - {error, R} -> - ct:log("R:~p~n", [R]), - false; - {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> - true; - {ok, {{"HTTP/1.1", 400, _}, _, []}} -> - false; - {ok, {{"HTTP/1.1", 404, _}, _, []}} -> - false - end. - -format_for_upload(none) -> - <<"">>; -format_for_upload(List) -> - iolist_to_binary(mochijson2:encode(List)). - ensure_ok(ok) -> ok; ensure_ok({error, {already_started, _}}) -> ok. @@ -766,34 +745,3 @@ set_app_env({App, Lists}) -> application:set_env(App, Par, Var) end, Lists). -request(Path) -> - http_get(get, Path). - -http_get(Method, Path) -> - req(Method, Path, []). - -http_put(Method, Path, Params) -> - req(Method, Path, format_for_upload(Params)). - -http_post(Method, Path, Params) -> - req(Method, Path, format_for_upload(Params)). - -req(Method, Path, Body) -> - Url = ?URL ++ Path, - Headers = auth_header_("admin", "public"), - case httpc:request(Method, {Url, [Headers]}, [], []) of - {error, socket_closed_remotely} -> - false; - {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> - true; - {ok, {{"HTTP/1.1", 400, _}, _, []}} -> - false; - {ok, {{"HTTP/1.1", 404, _}, _, []}} -> - false - end. - -format_for_upload(none) -> - <<"">>; -format_for_upload(List) -> - iolist_to_binary(mochijson2:encode(List)). - diff --git a/test/emqx_config_SUITE.erl b/test/emqx_config_SUITE.erl deleted file mode 100644 index 04c957b75..000000000 --- a/test/emqx_config_SUITE.erl +++ /dev/null @@ -1,149 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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(emqttd_config_SUITE). - --compile(export_all). - --include("emqttd.hrl"). - --include_lib("eunit/include/eunit.hrl"). - --include_lib("common_test/include/ct.hrl"). - -all() -> - [{group, emq_config}]. - -groups() -> - [{emq_config, [sequence], - [run_protocol_cmd, - run_client_cmd, - run_session_cmd, - run_queue_cmd, - run_auth_cmd, - run_lager_cmd, - run_connection_cmd, - run_broker_config] - }]. - -init_per_suite(Config) -> - Config. - -end_per_suite(Config) -> - Config. - -run_protocol_cmd(_Config) -> - SetConfigKeys = [{"max_clientid_len=2048", int}, - {"max_packet_size=1024", int}, - % {"websocket_protocol_header=off", atom}, - {"keepalive_backoff=0.5", float}], - lists:foreach(fun set_cmd/1, SetConfigKeys), - R = lists:sort(lists:map(fun env_value/1, SetConfigKeys)), - {ok, E} = application:get_env(emqttd, protocol), - ?assertEqual(R, lists:sort(E)), - emqttd_cli_config:run(["config", "set", "mqtt.websocket_protocol_header=off", "--app=emqttd"]), - {ok, E1} = application:get_env(emqttd, websocket_protocol_header), - ?assertEqual(false, E1). - -run_client_cmd(_Config) -> - SetConfigKeys = [{"max_publish_rate=100", int}, - {"idle_timeout=60s", date}, - {"enable_stats=on", atom}], - lists:foreach(fun(Key) -> set_cmd("client", Key) end, SetConfigKeys), - R = lists:sort(lists:map(fun(Key) -> env_value("client", Key) end, SetConfigKeys)), - {ok, E} = application:get_env(emqttd, client), - ?assertEqual(R, lists:sort(E)). - -run_session_cmd(_Config) -> - SetConfigKeys = [{"max_subscriptions=5", int}, - {"upgrade_qos=on", atom}, - {"max_inflight=64", int}, - {"retry_interval=60s", date}, - {"max_awaiting_rel=200", int}, - {"await_rel_timeout=60s",date}, - {"enable_stats=on", atom}, - {"expiry_interval=60s", date}, - {"ignore_loop_deliver=true", atom}], - lists:foreach(fun(Key) -> set_cmd("session", Key) end, SetConfigKeys), - R = lists:sort(lists:map(fun env_value/1, SetConfigKeys)), - {ok, E} = application:get_env(emqttd, session), - ?assertEqual(R, lists:sort(E)). - -run_queue_cmd(_Config) -> - SetConfigKeys = [{"type=priority", atom}, - {"priority=hah", string}, - {"max_length=2000", int}, - {"low_watermark=40%",percent}, - {"high_watermark=80%", percent}, - {"store_qos0=false", atom}], - lists:foreach(fun(Key) -> set_cmd("mqueue", Key) end, SetConfigKeys), - R = lists:sort(lists:map(fun env_value/1, SetConfigKeys)), - {ok, E} = application:get_env(emqttd, mqueue), - ?assertEqual(R, lists:sort(E)). - -run_auth_cmd(_Config) -> - SetConfigKeys = [{"allow_anonymous=true", atom}, - {"acl_nomatch=deny", atom}, - {"acl_file=etc/test.acl", string}, - {"cache_acl=false", atom}], - lists:foreach(fun set_cmd/1, SetConfigKeys), - {ok, true} = application:get_env(emqttd, allow_anonymous), - {ok, deny} = application:get_env(emqttd, acl_nomatch), - {ok, "etc/test.acl"} = application:get_env(emqttd, acl_file), - {ok, false} = application:get_env(emqttd, cache_acl). - -run_lager_cmd(_Config) -> - emqttd_cli_config:run(["config", "set", "log.console.level=info", "--app=emqttd"]), - ok. - -run_connection_cmd(_Config) -> - emqttd_cli_config:run(["config", "set", "mqtt.conn.force_gc_count=1000", "--app=emqttd"]), - {ok, E} = application:get_env(emqttd, conn_force_gc_count), - ?assertEqual(1000, E). - -run_broker_config(_Config) -> - emqttd_cli_config:run(["config", "set", "mqtt.broker.sys_interval=10", "--app=emqttd"]), - {ok, E} = application:get_env(emqttd, broker_sys_interval), - ?assertEqual(10, E). - -env_value("client", {Key, Type}) -> - case string:split(Key, "=") of - ["max_publish_rate", V] -> - {list_to_atom("max_publish_rate"), format(Type, V)}; - [K, V] -> - {list_to_atom(string:join(["client", K], "_")), format(Type, V)} - end. - -env_value({Key, Type}) -> - [K, V] = string:split(Key, "="), - {list_to_atom(K), format(Type, V)}. - -format(string, S) -> S; -format(atom, "on") -> true; -format(atom, "off") -> false; -format(atom, A) -> list_to_atom(A); -format(float, F) -> list_to_float(F); -format(percent, P) -> - {match, [N]} = re:run(P, "^([0-9]+)%$", [{capture, all_but_first, list}]), - list_to_integer(N) / 100; -format(int, I) -> list_to_integer(I); -format(date, _I) -> 60000. - -set_cmd({Key, _Type}) -> - emqttd_cli_config:run(["config", "set", string:join(["mqtt", Key], "."), "--app=emqttd"]). - -set_cmd(Pre, {Key, _Type}) -> - emqttd_cli_config:run(["config", "set", string:join(["mqtt", Pre, Key], "."), "--app=emqttd"]). diff --git a/test/emqx_mod_SUITE.erl b/test/emqx_mod_SUITE.erl index 1fcf455d0..846b14467 100644 --- a/test/emqx_mod_SUITE.erl +++ b/test/emqx_mod_SUITE.erl @@ -14,11 +14,11 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_mod_SUITE). +-module(emqx_mod_SUITE). -compile(export_all). --include("emqttd.hrl"). +-include("emqx.hrl"). all() -> [mod_subscription_rep]. diff --git a/test/emqttd_router_SUITE.erl b/test/emqx_router_SUITE.erl similarity index 93% rename from test/emqttd_router_SUITE.erl rename to test/emqx_router_SUITE.erl index 415550ec3..51db6c506 100644 --- a/test/emqttd_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -14,15 +14,15 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd_router_SUITE). +-module(emqx_router_SUITE). -compile(export_all). --include("emqttd.hrl"). +-include("emqx.hrl"). -include_lib("eunit/include/eunit.hrl"). --define(R, emqttd_router). +-define(R, emqx_router). all() -> [{group, route}, @@ -44,11 +44,11 @@ groups() -> init_per_suite(Config) -> ekka:start(), ekka_mnesia:ensure_started(), - {ok, _R} = emqttd_router:start(), + {ok, _R} = emqx_router:start(), Config. end_per_suite(_Config) -> - emqttd_router:stop(), + emqx_router:stop(), ekka:stop(), ekka_mnesia:ensure_stopped(), ekka_mnesia:delete_schema(). @@ -148,7 +148,7 @@ router_add_del(_) -> %% Del ?R:del_route(<<"a/b/c">>), [R1, R2] = lists:sort(?R:match(<<"a/b/c">>)), - {atomic, []} = mnesia:transaction(fun emqttd_trie:lookup/1, [<<"a/b/c">>]), + {atomic, []} = mnesia:transaction(fun emqx_trie:lookup/1, [<<"a/b/c">>]), %% Batch Del R3 = #mqtt_route{topic = <<"#">>, node = 'a@127.0.0.1'}, @@ -169,6 +169,6 @@ t_print(_) -> ?R:del_route(<<"#">>). router_unused(_) -> - gen_server:call(emqttd_router, bad_call), - gen_server:cast(emqttd_router, bad_msg), - emqttd_router ! bad_info. + gen_server:call(emqx_router, bad_call), + gen_server:cast(emqx_router, bad_msg), + emqx_router ! bad_info. From a904dfd4f2b367c84e6419f7cc14758a6afe02b8 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 7 Dec 2017 16:26:32 +0800 Subject: [PATCH 014/520] Comment the ERTS_MINIMUM macro --- include/emqx.hrl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/emqx.hrl b/include/emqx.hrl index 7254805be..2fbd49887 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -24,7 +24,7 @@ -define(PROTOCOL_VERSION, "MQTT/5.0"). --define(ERTS_MINIMUM, "9.0"). +%%-define(ERTS_MINIMUM, "9.0"). %%-------------------------------------------------------------------- %% Sys/Queue/Share Topics' Prefix From c8d6e9a01223e8df889d40f4640499ddfe1149e4 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 7 Dec 2017 16:37:46 +0800 Subject: [PATCH 015/520] Update test cases --- test/emqx_SUITE.erl | 97 +++++++++++++++++++------------------- test/emqx_lib_SUITE.erl | 8 ++-- test/emqx_router_SUITE.erl | 4 -- 3 files changed, 52 insertions(+), 57 deletions(-) diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index 472069464..5cf671125 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -404,65 +404,19 @@ request_status(_) -> ?assertEqual(binary_to_list(Status), Return). request_publish(_) -> - application:start(emq_dashboard), emqttc:start_link([{host, "localhost"}, {port, 1883}, {client_id, <<"random">>}, {clean_sess, false}]), SubParams = "{\"qos\":1, \"topic\" : \"a\/b\/c\", \"client_id\" :\"random\"}", - ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/subscribe", SubParams, auth_header_("admin", "public"))), + ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/subscribe", SubParams, auth_header_("", ""))), ok = emqx:subscribe(<<"a/b/c">>, self(), [{qos, 1}]), Params = "{\"qos\":1, \"retain\":false, \"topic\" : \"a\/b\/c\", \"messages\" :\"hello\"}", - ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/publish", Params, auth_header_("admin", "public"))), + ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/publish", Params, auth_header_("", ""))), ?assert(receive {dispatch, <<"a/b/c">>, _} -> true after 2 -> false end), UnSubParams = "{\"topic\" : \"a\/b\/c\", \"client_id\" :\"random\"}", - ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/unsubscribe", UnSubParams, auth_header_("admin", "public"))). - -connect_emqx_pubsub_(Method, Api, Params, Auth) -> - Url = "http://127.0.0.1:8080/" ++ Api, - case httpc:request(Method, {Url, [Auth], ?CONTENT_TYPE, Params}, [], []) of - {error, socket_closed_remotely} -> - false; - {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> - true; - {ok, {{"HTTP/1.1", 400, _}, _, []}} -> - false; - {ok, {{"HTTP/1.1", 404, _}, _, []}} -> - false - end. - -request(Path) -> - http_get(get, Path). - -http_get(Method, Path) -> - req(Method, Path, []). - -http_put(Method, Path, Params) -> - req(Method, Path, format_for_upload(Params)). - -http_post(Method, Path, Params) -> - req(Method, Path, format_for_upload(Params)). - -req(Method, Path, Body) -> - Url = ?URL ++ Path, - Headers = auth_header_("", ""), - case httpc:request(Method, {Url, [Headers]}, [], []) of - {error, R} -> - ct:log("R:~p~n", [R]), - false; - {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> - true; - {ok, {{"HTTP/1.1", 400, _}, _, []}} -> - false; - {ok, {{"HTTP/1.1", 404, _}, _, []}} -> - false - end. - -format_for_upload(none) -> - <<"">>; -format_for_upload(List) -> - iolist_to_binary(mochijson2:encode(List)). + ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/unsubscribe", UnSubParams, auth_header_("", ""))). connect_emqx_publish_(Method, Api, Params, Auth) -> Url = "http://127.0.0.1:8080/" ++ Api, @@ -657,6 +611,51 @@ cleanSession_validate1(_) -> emqttc:disconnect(Pub), emqttc:disconnect(C11). +connect_emqx_pubsub_(Method, Api, Params, Auth) -> + Url = "http://127.0.0.1:8080/" ++ Api, + case httpc:request(Method, {Url, [Auth], ?CONTENT_TYPE, Params}, [], []) of + {error, socket_closed_remotely} -> + false; + {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> + true; + {ok, {{"HTTP/1.1", 400, _}, _, []}} -> + false; + {ok, {{"HTTP/1.1", 404, _}, _, []}} -> + false + end. + +request(Path) -> + http_get(get, Path). + +http_get(Method, Path) -> + req(Method, Path, []). + +http_put(Method, Path, Params) -> + req(Method, Path, format_for_upload(Params)). + +http_post(Method, Path, Params) -> + req(Method, Path, format_for_upload(Params)). + +req(Method, Path, Body) -> + Url = ?URL ++ Path, + Headers = auth_header_("", ""), + case httpc:request(Method, {Url, [Headers]}, [], []) of + {error, R} -> + ct:log("R:~p~n", [R]), + false; + {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> + true; + {ok, {{"HTTP/1.1", 400, _}, _, []}} -> + false; + {ok, {{"HTTP/1.1", 404, _}, _, []}} -> + false + end. + +format_for_upload(none) -> + <<"">>; +format_for_upload(List) -> + iolist_to_binary(mochijson2:encode(List)). + ensure_ok(ok) -> ok; ensure_ok({error, {already_started, _}}) -> ok. diff --git a/test/emqx_lib_SUITE.erl b/test/emqx_lib_SUITE.erl index e39b8178d..4f6bc8996 100644 --- a/test/emqx_lib_SUITE.erl +++ b/test/emqx_lib_SUITE.erl @@ -152,12 +152,12 @@ time_now_to_(_) -> node_is_aliving(_) -> io:format("Node: ~p~n", [node()]), - true = emqx_node:is_aliving(node()), - false = emqx_node:is_aliving('x@127.0.0.1'). + true = ekka_node:is_aliving(node()), + false = ekka_node:is_aliving('x@127.0.0.1'). node_parse_name(_) -> - 'a@127.0.0.1' = emqx_node:parse_name("a@127.0.0.1"), - 'b@127.0.0.1' = emqx_node:parse_name("b"). + '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 diff --git a/test/emqx_router_SUITE.erl b/test/emqx_router_SUITE.erl index 51db6c506..72ce4e5f0 100644 --- a/test/emqx_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -126,10 +126,6 @@ clear_tables() -> ?R:clean_local_routes(), lists:foreach(fun mnesia:clear_table/1, [mqtt_route, mqtt_trie, mqtt_trie_node]). -%%-------------------------------------------------------------------- -%% Router Test -%%-------------------------------------------------------------------- - router_add_del(_) -> %% Add ?R:add_route(<<"#">>), From 6f64eb469c804ad0e5e74d7b0131fc8803e388bd Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 7 Dec 2017 17:58:24 +0800 Subject: [PATCH 016/520] Remove emqttd_ssl.erl --- src/emqttd_ssl.erl | 259 --------------------------------------------- 1 file changed, 259 deletions(-) delete mode 100644 src/emqttd_ssl.erl diff --git a/src/emqttd_ssl.erl b/src/emqttd_ssl.erl deleted file mode 100644 index c4126e820..000000000 --- a/src/emqttd_ssl.erl +++ /dev/null @@ -1,259 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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 SSL Utility Functions. This module is copied from rabbit_ssl.erl -%% - --module(emqttd_ssl). - --include_lib("public_key/include/public_key.hrl"). - --type(certificate() :: binary()). - --export([peer_cert_issuer/1, peer_cert_subject/1, peer_cert_common_name/1, - peer_cert_subject_items/2, peer_cert_validity/1]). - -%% Return a string describing the certificate's issuer. --spec(peer_cert_issuer(certificate()) -> string()). -peer_cert_issuer(Cert) -> - cert_info(fun(#'OTPCertificate' { - tbsCertificate = #'OTPTBSCertificate' { - issuer = Issuer }}) -> - format_rdn_sequence(Issuer) - end, Cert). - -%% Return a string describing the certificate's subject, as per RFC4514. --spec(peer_cert_subject(certificate()) -> string()). -peer_cert_subject(Cert) -> - cert_info(fun(#'OTPCertificate' { - tbsCertificate = #'OTPTBSCertificate' { - subject = Subject }}) -> - format_rdn_sequence(Subject) - end, Cert). - --spec(peer_cert_common_name(certificate()) -> string() | 'not_found'). -peer_cert_common_name(Cert) -> - case peer_cert_subject_items(Cert, ?'id-at-commonName') of - not_found -> not_found; - CNs -> string:join(CNs, ",") - end. - -%% Return the parts of the certificate's subject. --spec(peer_cert_subject_items(certificate(), tuple()) -> [string()] | 'undefined'). -peer_cert_subject_items(Cert, Type) -> - cert_info(fun(#'OTPCertificate' { - tbsCertificate = #'OTPTBSCertificate' { - subject = Subject }}) -> - find_by_type(Type, Subject) - end, Cert). - -%% Return a string describing the certificate's validity. --spec(peer_cert_validity(certificate()) -> string()). -peer_cert_validity(Cert) -> - cert_info(fun(#'OTPCertificate' { - tbsCertificate = #'OTPTBSCertificate' { - validity = {'Validity', Start, End} }}) -> - format("~s - ~s", [format_asn1_value(Start), - format_asn1_value(End)]) - end, Cert). - -cert_info(F, {ok, Cert}) -> - F(case public_key:pkix_decode_cert(Cert, otp) of - {ok, DecCert} -> DecCert; %%pre R14B - DecCert -> DecCert %%R14B onwards - end). - -find_by_type(Type, {rdnSequence, RDNs}) -> - case [V || #'AttributeTypeAndValue'{type = T, value = V} - <- lists:flatten(RDNs), - T == Type] of - [] -> not_found; - L -> [format_asn1_value(V) || V <- L] - end. - -%%-------------------------------------------------------------------------- -%% Formatting functions. -%%-------------------------------------------------------------------------- - -%% Format and rdnSequence as a RFC4514 subject string. -format_rdn_sequence({rdnSequence, Seq}) -> - string:join(lists:reverse([format_complex_rdn(RDN) || RDN <- Seq]), ","). - -%% Format an RDN set. -format_complex_rdn(RDNs) -> - string:join([format_rdn(RDN) || RDN <- RDNs], "+"). - -%% Format an RDN. If the type name is unknown, use the dotted decimal -%% representation. See RFC4514, section 2.3. -format_rdn(#'AttributeTypeAndValue'{type = T, value = V}) -> - FV = escape_rdn_value(format_asn1_value(V)), - Fmts = [{?'id-at-surname' , "SN"}, - {?'id-at-givenName' , "GIVENNAME"}, - {?'id-at-initials' , "INITIALS"}, - {?'id-at-generationQualifier' , "GENERATIONQUALIFIER"}, - {?'id-at-commonName' , "CN"}, - {?'id-at-localityName' , "L"}, - {?'id-at-stateOrProvinceName' , "ST"}, - {?'id-at-organizationName' , "O"}, - {?'id-at-organizationalUnitName' , "OU"}, - {?'id-at-title' , "TITLE"}, - {?'id-at-countryName' , "C"}, - {?'id-at-serialNumber' , "SERIALNUMBER"}, - {?'id-at-pseudonym' , "PSEUDONYM"}, - {?'id-domainComponent' , "DC"}, - {?'id-emailAddress' , "EMAILADDRESS"}, - {?'street-address' , "STREET"}, - {{0,9,2342,19200300,100,1,1} , "UID"}], %% Not in public_key.hrl - case proplists:lookup(T, Fmts) of - {_, Fmt} -> - format(Fmt ++ "=~s", [FV]); - none when is_tuple(T) -> - TypeL = [format("~w", [X]) || X <- tuple_to_list(T)], - format("~s=~s", [string:join(TypeL, "."), FV]); - none -> - format("~p=~s", [T, FV]) - end. - -%% Escape a string as per RFC4514. -escape_rdn_value(V) -> - escape_rdn_value(V, start). - -escape_rdn_value([], _) -> - []; -escape_rdn_value([C | S], start) when C =:= $ ; C =:= $# -> - [$\\, C | escape_rdn_value(S, middle)]; -escape_rdn_value(S, start) -> - escape_rdn_value(S, middle); -escape_rdn_value([$ ], middle) -> - [$\\, $ ]; -escape_rdn_value([C | S], middle) when C =:= $"; C =:= $+; C =:= $,; C =:= $;; - C =:= $<; C =:= $>; C =:= $\\ -> - [$\\, C | escape_rdn_value(S, middle)]; -escape_rdn_value([C | S], middle) when C < 32 ; C >= 126 -> - %% Of ASCII characters only U+0000 needs escaping, but for display - %% purposes it's handy to escape all non-printable chars. All non-ASCII - %% characters get converted to UTF-8 sequences and then escaped. We've - %% already got a UTF-8 sequence here, so just escape it. - rabbit_misc:format("\\~2.16.0B", [C]) ++ escape_rdn_value(S, middle); -escape_rdn_value([C | S], middle) -> - [C | escape_rdn_value(S, middle)]. - -%% Get the string representation of an OTPCertificate field. -format_asn1_value({ST, S}) when ST =:= teletexString; ST =:= printableString; - ST =:= universalString; ST =:= utf8String; - ST =:= bmpString -> - format_directory_string(ST, S); -format_asn1_value({utcTime, [Y1, Y2, M1, M2, D1, D2, H1, H2, - Min1, Min2, S1, S2, $Z]}) -> - format("20~c~c-~c~c-~c~cT~c~c:~c~c:~c~cZ", - [Y1, Y2, M1, M2, D1, D2, H1, H2, Min1, Min2, S1, S2]); -%% We appear to get an untagged value back for an ia5string -%% (e.g. domainComponent). -format_asn1_value(V) when is_list(V) -> - V; -format_asn1_value(V) when is_binary(V) -> - %% OTP does not decode some values when combined with an unknown - %% type. That's probably wrong, so as a last ditch effort let's - %% try manually decoding. 'DirectoryString' is semi-arbitrary - - %% but it is the type which covers the various string types we - %% handle below. - try - {ST, S} = public_key:der_decode('DirectoryString', V), - format_directory_string(ST, S) - catch _:_ -> - format("~p", [V]) - end; -format_asn1_value(V) -> - format("~p", [V]). - -%% DirectoryString { INTEGER : maxSize } ::= CHOICE { -%% teletexString TeletexString (SIZE (1..maxSize)), -%% printableString PrintableString (SIZE (1..maxSize)), -%% bmpString BMPString (SIZE (1..maxSize)), -%% universalString UniversalString (SIZE (1..maxSize)), -%% uTF8String UTF8String (SIZE (1..maxSize)) } -%% -%% Precise definitions of printable / teletexString are hard to come -%% by. This is what I reconstructed: -%% -%% printableString: -%% "intended to represent the limited character sets available to -%% mainframe input terminals" -%% A-Z a-z 0-9 ' ( ) + , - . / : = ? [space] -%% http://msdn.microsoft.com/en-us/library/bb540814(v=vs.85).aspx -%% -%% teletexString: -%% "a sizable volume of software in the world treats TeletexString -%% (T61String) as a simple 8-bit string with mostly Windows Latin 1 -%% (superset of iso-8859-1) encoding" -%% http://www.mail-archive.com/asn1@asn1.org/msg00460.html -%% -%% (However according to that link X.680 actually defines -%% TeletexString in some much more involved and crazy way. I suggest -%% we treat it as ISO-8859-1 since Erlang does not support Windows -%% Latin 1). -%% -%% bmpString: -%% UCS-2 according to RFC 3641. Hence cannot represent Unicode -%% characters above 65535 (outside the "Basic Multilingual Plane"). -%% -%% universalString: -%% UCS-4 according to RFC 3641. -%% -%% utf8String: -%% UTF-8 according to RFC 3641. -%% -%% Within Rabbit we assume UTF-8 encoding. Since printableString is a -%% subset of ASCII it is also a subset of UTF-8. The others need -%% converting. Fortunately since the Erlang SSL library does the -%% decoding for us (albeit into a weird format, see below), we just -%% need to handle encoding into UTF-8. Note also that utf8Strings come -%% back as binary. -%% -%% Note for testing: the default Ubuntu configuration for openssl will -%% only create printableString or teletexString types no matter what -%% you do. Edit string_mask in the [req] section of -%% /etc/ssl/openssl.cnf to change this (see comments there). You -%% probably also need to set utf8 = yes to get it to accept UTF-8 on -%% the command line. Also note I could not get openssl to generate a -%% universalString. - -format_directory_string(printableString, S) -> S; -format_directory_string(teletexString, S) -> utf8_list_from(S); -format_directory_string(bmpString, S) -> utf8_list_from(S); -format_directory_string(universalString, S) -> utf8_list_from(S); -format_directory_string(utf8String, S) -> binary_to_list(S). - -utf8_list_from(S) -> - binary_to_list( - unicode:characters_to_binary(flatten_ssl_list(S), utf32, utf8)). - -%% The Erlang SSL implementation invents its own representation for -%% non-ascii strings - looking like [97,{0,0,3,187}] (that's LATIN -%% SMALL LETTER A followed by GREEK SMALL LETTER LAMDA). We convert -%% this into a list of unicode characters, which we can tell -%% unicode:characters_to_binary is utf32. - -flatten_ssl_list(L) -> [flatten_ssl_list_item(I) || I <- L]. - -flatten_ssl_list_item({A, B, C, D}) -> - A * (1 bsl 24) + B * (1 bsl 16) + C * (1 bsl 8) + D; -flatten_ssl_list_item(N) when is_number (N) -> - N. - -format(Fmt, Args) -> - lists:flatten(io_lib:format(Fmt, Args)). - From fcb2ec842718c05a06d59c14b4444a53b4491df2 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 24 Feb 2018 15:56:32 +0800 Subject: [PATCH 017/520] Merge the latest enterprise branch --- .gitignore | 1 + Makefile | 11 +- README.md | 2 +- etc/emqx.conf | 13 +- include/emqx.hrl | 4 +- priv/emqx.schema | 9 + src/emqx_plugins.erl | 65 +++- src/emqx_protocol.erl | 36 +- src/emqx_session.erl | 2 +- src/emqx_ws.erl | 3 +- test/emqx_SUITE.erl | 670 +++----------------------------- test/emqx_access_SUITE.erl | 2 + test/emqx_broker_SUITE.erl | 238 ++++++++++++ test/emqx_ct_broker_helpers.erl | 102 +++++ test/emqx_mod_SUITE.erl | 9 - test/rfc6455_client.erl | 252 ++++++++++++ 16 files changed, 765 insertions(+), 654 deletions(-) create mode 100644 test/emqx_broker_SUITE.erl create mode 100644 test/emqx_ct_broker_helpers.erl create mode 100644 test/rfc6455_client.erl diff --git a/.gitignore b/.gitignore index 1259471d2..d1b8a289e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ _build rebar3.crashdump .DS_Store rebar.config +emqx.iml diff --git a/Makefile b/Makefile index 4da68accf..7084cadaa 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,19 @@ +.PHONY: plugins tests + PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker PROJECT_VERSION = 3.0 -NO_AUTOPATCH = cuttlefish +NO_AUTOPATCH = gen_rpc cuttlefish DEPS = goldrush gproc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx dep_goldrush = git https://github.com/basho/goldrush 0.1.9 dep_gproc = git https://github.com/uwiger/gproc +dep_jsx = git https://github.com/talentdeficit/jsx dep_getopt = git https://github.com/jcomellas/getopt v0.8.2 dep_lager = git https://github.com/basho/lager master dep_lager_syslog = git https://github.com/basho/lager_syslog -dep_jsx = git https://github.com/talentdeficit/jsx dep_esockd = git https://github.com/emqtt/esockd v5.2.1 dep_ekka = git https://github.com/emqtt/ekka v0.2.2 dep_mochiweb = git https://github.com/emqtt/mochiweb v4.2.2 @@ -25,16 +27,15 @@ ERLC_OPTS += +'{parse_transform, lager_transform}' BUILD_DEPS = cuttlefish dep_cuttlefish = git https://github.com/emqtt/cuttlefish -TEST_DEPS = emqttc emq_dashboard +TEST_DEPS = emqttc dep_emqttc = git https://github.com/emqtt/emqttc -dep_emq_dashboard = git https://github.com/emqtt/emq_dashboard develop TEST_ERLC_OPTS += +debug_info TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose -CT_SUITES = emqx emqx_mod emqx_lib emqx_topic emqx_trie emqx_mqueue emqx_inflight \ +CT_SUITES = emqx emqx_broker emqx_mod emqx_lib emqx_topic emqx_trie emqx_mqueue emqx_inflight \ emqx_vm emqx_net emqx_protocol emqx_access emqx_router CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqxct@127.0.0.1 diff --git a/README.md b/README.md index ecb5c569f..eceeee5de 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# *EMQ* - Erlang MQTT Broker +# *EMQ X* - EMQ X Broker [![Build Status](https://travis-ci.org/emqtt/emqttd.svg?branch=master)](https://travis-ci.org/emqtt/emqttd) diff --git a/etc/emqx.conf b/etc/emqx.conf index b15206df9..fe69bec20 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -400,6 +400,11 @@ mqtt.max_packet_size = 64KB ## Value: on | off mqtt.websocket_protocol_header = on +## Check Websocket Upgrade Header. +## +## Value: on | off +mqtt.websocket_check_upgrade_header = on + ## The backoff for MQTT keepalive timeout. ## EMQ will kick a MQTT connection out until 'Keepalive * backoff * 2' timeout. ## @@ -578,6 +583,9 @@ mqtt.plugins.etc_dir ={{ platform_etc_dir }}/plugins/ ## Value: File mqtt.plugins.loaded_file = {{ platform_data_dir }}/loaded_plugins +## File to store loaded plugin names. +mqtt.plugins.expand_plugins_dir = {{ platform_plugins_dir }}/ + ##-------------------------------------------------------------------- ## MQTT Listeners ##-------------------------------------------------------------------- @@ -611,6 +619,7 @@ listener.tcp.external.max_clients = 102400 ## Mountpoint of the MQTT/TCP Listener. All the topics of this ## listener will be prefixed with the mount point if this option ## is enabled. +## Notice that EMQ X supports wildcard mount:%c clientid, %u username ## ## Value: String ## listener.tcp.external.mountpoint = external/ @@ -830,7 +839,7 @@ listener.ssl.external.acceptors = 16 ## Maximum number of concurrent MQTT/SSL connections. ## ## Value: Number -listener.ssl.external.max_clients = 1024 +listener.ssl.external.max_clients = 102400 ## TODO: Zone of the external MQTT/SSL listener belonged to. ## @@ -1314,7 +1323,7 @@ listener.wss.external.certfile = {{ platform_etc_dir }}/certs/cert.pem ## TCP backlog for the WebSocket/SSL connection. ## -## See listener.tcp..backlog +## See: listener.tcp..backlog ## ## Value: Number >= 0 listener.wss.external.backlog = 1024 diff --git a/include/emqx.hrl b/include/emqx.hrl index 4752adc86..7476446f1 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -85,7 +85,9 @@ will_topic :: undefined | binary(), ws_initial_headers :: list({ws_header_key(), ws_header_val()}), mountpoint :: undefined | binary(), - connected_at :: erlang:timestamp() + connected_at :: erlang:timestamp(), + %%TODO: Headers + headers = [] :: list() }). -type(mqtt_client() :: #mqtt_client{}). diff --git a/priv/emqx.schema b/priv/emqx.schema index 27ccabba1..00f4735f8 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -538,6 +538,11 @@ end}. {datatype, flag} ]}. +{mapping, "mqtt.websocket_check_upgrade_header", "emqx.websocket_check_upgrade_header", [ + {default, on}, + {datatype, flag} +]}. + %%-------------------------------------------------------------------- %% MQTT Connection %%-------------------------------------------------------------------- @@ -760,6 +765,10 @@ end}. {datatype, string} ]}. +{mapping, "mqtt.plugins.expand_plugins_dir", "emqx.expand_plugins_dir", [ + {datatype, string} +]}. + %%-------------------------------------------------------------------- %% MQTT Listeners %%-------------------------------------------------------------------- diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index ba3ca569b..a9b3c5c25 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -28,6 +28,8 @@ -export([list/0]). +-export([load_expand_plugin/1]). + %% @doc Init plugins' config -spec(init() -> ok). init() -> @@ -49,6 +51,7 @@ init_config(CfgFile) -> %% @doc Load all plugins when the broker started. -spec(load() -> list() | {error, term()}). load() -> + load_expand_plugins(), case emqx:env(plugins_loaded_file) of {ok, File} -> ensure_file(File), @@ -58,6 +61,66 @@ load() -> ignore end. +load_expand_plugins() -> + case emqx:env(expand_plugins_dir) of + {ok, Dir} -> + PluginsDir = filelib:wildcard("*", Dir), + lists:foreach(fun(PluginDir) -> + case filelib:is_dir(Dir ++ PluginDir) of + true -> load_expand_plugin(Dir ++ PluginDir); + false -> ok + end + end, PluginsDir); + _ -> ok + end. + +load_expand_plugin(PluginDir) -> + init_expand_plugin_config(PluginDir), + Ebin = PluginDir ++ "/ebin", + code:add_patha(Ebin), + Modules = filelib:wildcard(Ebin ++ "/*.beam"), + lists:foreach(fun(Mod) -> + Module = list_to_atom(filename:basename(Mod, ".beam")), + code:load_file(Module) + end, Modules), + case filelib:wildcard(Ebin ++ "/*.app") of + [App|_] -> application:load(list_to_atom(filename:basename(App, ".app"))); + _ -> lager:error("load application fail"), {error, load_app_fail} + end. + +init_expand_plugin_config(PluginDir) -> + Priv = PluginDir ++ "/priv", + Etc = PluginDir ++ "/etc", + Schema = filelib:wildcard(Priv ++ "/*.schema"), + Conf = case filelib:wildcard(Etc ++ "/*.conf") of + [] -> []; + [Conf1] -> cuttlefish_conf:file(Conf1) + end, + AppsEnv = cuttlefish_generator:map(cuttlefish_schema:files(Schema), Conf), + lists:foreach(fun({AppName, Envs}) -> + [application:set_env(AppName, Par, Val) || {Par, Val} <- Envs] + end, AppsEnv). + +get_expand_plugin_config() -> + case emqx:env(expand_plugins_dir) of + {ok, Dir} -> + PluginsDir = filelib:wildcard("*", Dir), + lists:foldl(fun(PluginDir, Acc) -> + case filelib:is_dir(Dir ++ PluginDir) of + true -> + Etc = Dir ++ PluginDir ++ "/etc", + case filelib:wildcard("*.{conf,config}", Etc) of + [] -> Acc; + [Conf] -> [Conf | Acc] + end; + false -> + Acc + end + end, [], PluginsDir); + _ -> ok + end. + + ensure_file(File) -> case filelib:is_file(File) of false -> write_loaded([]); true -> ok end. @@ -98,7 +161,7 @@ stop_plugins(Names) -> list() -> case emqx:env(plugins_etc_dir) of {ok, PluginsEtc} -> - CfgFiles = filelib:wildcard("*.{conf,config}", PluginsEtc), + CfgFiles = filelib:wildcard("*.{conf,config}", PluginsEtc) ++ get_expand_plugin_config(), Plugins = [plugin(CfgFile) || CfgFile <- CfgFiles], StartedApps = names(started_app), lists:map(fun(Plugin = #mqtt_plugin{name = Name}) -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 9856ec744..7e5cd2402 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -35,6 +35,10 @@ -export([process/2]). +-ifdef(TEST). +-compile(export_all). +-endif. + -record(proto_stats, {enable_stats = false, recv_pkt = 0, recv_msg = 0, send_pkt = 0, send_msg = 0}). @@ -289,7 +293,7 @@ process(?SUBSCRIBE_PACKET(PacketId, RawTopicTable), false -> case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of {ok, TopicTable1} -> - emqx_session:subscribe(Session, PacketId, mount(MountPoint, TopicTable1)), + emqx_session:subscribe(Session, PacketId, mount(replvar(MountPoint, State), TopicTable1)), {ok, State}; {stop, _} -> {ok, State} @@ -307,7 +311,7 @@ process(?UNSUBSCRIBE_PACKET(PacketId, RawTopics), session = Session}) -> case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of {ok, TopicTable} -> - emqx_session:unsubscribe(Session, mount(MountPoint, TopicTable)); + emqx_session:unsubscribe(Session, mount(replvar(MountPoint, State), TopicTable)); {stop, _} -> ok end, @@ -321,12 +325,12 @@ process(?PACKET(?DISCONNECT), State) -> {stop, normal, State#proto_state{will_msg = undefined}}. publish(Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), - #proto_state{client_id = ClientId, - username = Username, - mountpoint = MountPoint, - session = Session}) -> + State = #proto_state{client_id = ClientId, + username = Username, + mountpoint = MountPoint, + session = Session}) -> Msg = emqx_message:from_packet(Username, ClientId, Packet), - emqx_session:publish(Session, mount(MountPoint, Msg)); + emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg)); publish(Packet = ?PUBLISH_PACKET(?QOS_1, _PacketId), State) -> with_puback(?PUBACK, Packet, State); @@ -340,7 +344,7 @@ with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), mountpoint = MountPoint, session = Session}) -> Msg = emqx_message:from_packet(Username, ClientId, Packet), - case emqx_session:publish(Session, mount(MountPoint, Msg)) of + case emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg)) of ok -> send(?PUBACK_PACKET(Type, PacketId), State); {error, Error} -> @@ -415,10 +419,10 @@ shutdown(Error, State = #proto_state{will_msg = WillMsg}) -> %% emqx_cm:unreg(ClientId). ok. -willmsg(Packet, #proto_state{mountpoint = MountPoint}) when is_record(Packet, mqtt_packet_connect) -> +willmsg(Packet, State = #proto_state{mountpoint = MountPoint}) when is_record(Packet, mqtt_packet_connect) -> case emqx_message:from_packet(Packet) of undefined -> undefined; - Msg -> mount(MountPoint, Msg) + Msg -> mount(replvar(MountPoint, State), Msg) end. %% Generate a client if if nulll @@ -577,6 +581,18 @@ clean_retain(_IsBridge, Msg) -> %% Mount Point %%-------------------------------------------------------------------- +replvar(undefined, _State) -> + undefined; +replvar(MountPoint, #proto_state{client_id = ClientId, username = Username}) -> + lists:foldl(fun feed_var/2, MountPoint, [{<<"%c">>, ClientId}, {<<"%u">>, Username}]). + +feed_var({<<"%c">>, ClientId}, MountPoint) -> + emqx_topic:feed_var(<<"%c">>, ClientId, MountPoint); +feed_var({<<"%u">>, undefined}, MountPoint) -> + MountPoint; +feed_var({<<"%u">>, Username}, MountPoint) -> + emqx_topic:feed_var(<<"%u">>, Username, MountPoint). + mount(undefined, Any) -> Any; mount(MountPoint, Msg = #mqtt_message{topic = Topic}) -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 1c7ae1f52..8c72cd499 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -667,7 +667,7 @@ expire_awaiting_rel([{PacketId, Msg = #mqtt_message{timestamp = TS}} | Msgs], case (timer:now_diff(Now, TS) div 1000) of Diff when Diff >= Timeout -> ?LOG(warning, "Dropped Qos2 Message for await_rel_timeout: ~p", [Msg], State), - emqttd_metrics:inc('messages/qos2/dropped'), + emqx_metrics:inc('messages/qos2/dropped'), expire_awaiting_rel(Msgs, Now, State#state{awaiting_rel = maps:remove(PacketId, AwaitingRel)}); Diff -> State#state{await_rel_timer = start_timer(Timeout - Diff, check_awaiting_rel)} diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl index 355ea1b92..ff9ea3a5b 100644 --- a/src/emqx_ws.erl +++ b/src/emqx_ws.erl @@ -74,7 +74,8 @@ handle_request(Method, Path, Req) -> Req:not_found(). is_websocket(Upgrade) -> - Upgrade =/= undefined andalso string:to_lower(Upgrade) =:= "websocket". + (not emqx:env(websocket_check_upgrade_header, true)) orelse + (Upgrade =/= undefined andalso string:to_lower(Upgrade) =:= "websocket"). check_protocol_header(Req) -> case emqx:env(websocket_protocol_header, false) of diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index 71d6ae234..b47b23942 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -18,9 +18,7 @@ -compile(export_all). --include("emqx.hrl"). - --include("emqx_mqtt.hrl"). +-include_lib("emqttc/include/emqttc_packet.hrl"). -define(APP, emqx). @@ -28,115 +26,37 @@ -include_lib("common_test/include/ct.hrl"). --define(CONTENT_TYPE, "application/json"). - --define(MQTT_SSL_TWOWAY, [{cacertfile, "certs/cacert.pem"}, - {verify, verify_peer}, - {fail_if_no_peer_cert, true}]). - --define(MQTT_SSL_CLIENT, [{keyfile, "certs/client-key.pem"}, - {cacertfile, "certs/cacert.pem"}, - {certfile, "certs/client-cert.pem"}]). - --define(URL, "http://localhost:8080/api/v2/"). - --define(APPL_JSON, "application/json"). - --define(PRINT(PATH), lists:flatten(io_lib:format(PATH, [atom_to_list(node())]))). - --define(GET_API, ["management/nodes", - ?PRINT("management/nodes/~s"), - "monitoring/nodes", - ?PRINT("monitoring/nodes/~s"), - "monitoring/listeners", - ?PRINT("monitoring/listeners/~s"), - "monitoring/metrics", - ?PRINT("monitoring/metrics/~s"), - "monitoring/stats", - ?PRINT("monitoring/stats/~s"), - ?PRINT("nodes/~s/clients"), - "routes"]). - +-define(CLIENT, ?CONNECT_PACKET(#mqtt_packet_connect{ + client_id = <<"mqtt_client">>, + username = <<"admin">>, + password = <<"public">>})). all() -> - [{group, protocol}, - {group, pubsub}, - {group, session}, - {group, broker}, - {group, metrics}, - {group, stats}, - {group, hook}, - {group, http}, - {group, alarms}, - {group, cli}, + [{group, connect}, {group, cleanSession}]. groups() -> - [{protocol, [sequence], + [{connect, [non_parallel_tests], [mqtt_connect, - mqtt_ssl_oneway, - mqtt_ssl_twoway]}, - {pubsub, [sequence], - [subscribe_unsubscribe, - publish, pubsub, - t_local_subscribe, - t_shared_subscribe, - 'pubsub#', 'pubsub+']}, - {session, [sequence], - [start_session]}, - {broker, [sequence], - [hook_unhook]}, - {metrics, [sequence], - [inc_dec_metric]}, - {stats, [sequence], - [set_get_stat]}, - {hook, [sequence], - [add_delete_hook, - run_hooks]}, - {http, [sequence], - [request_status, - request_publish, - get_api_lists - % websocket_test - ]}, - {alarms, [sequence], - [set_alarms] - }, - {cli, [sequence], - [ctl_register_cmd, - cli_status, - cli_broker, - cli_clients, - cli_sessions, - cli_routes, - cli_topics, - cli_subscriptions, - cli_bridges, - cli_plugins, - {listeners, [sequence], - [cli_listeners, - conflict_listeners - ]}, - cli_vm]}, + mqtt_connect_with_tcp, + mqtt_connect_with_ssl_oneway, + mqtt_connect_with_ssl_twoway, + mqtt_connect_with_ws]}, {cleanSession, [sequence], - [cleanSession_validate, - cleanSession_validate1] + [cleanSession_validate] } ]. init_per_suite(Config) -> - NewConfig = generate_config(), - lists:foreach(fun set_app_env/1, NewConfig), - Apps = application:ensure_all_started(?APP), - ct:log("Apps:~p", [Apps]), + emqx_ct_broker_helpers:run_setup_steps(), + % ct:log("Apps:~p", [Apps]), Config. end_per_suite(_Config) -> - emqx:shutdown(). + emqx_ct_broker_helpers:run_teardown_steps(). %%-------------------------------------------------------------------- %% Protocol Test %%-------------------------------------------------------------------- - mqtt_connect(_) -> %% Issue #599 %% Empty clientId and clean_session = false @@ -151,10 +71,21 @@ connect_broker_(Packet, RecvSize) -> gen_tcp:close(Sock), Data. -mqtt_ssl_oneway(_) -> +mqtt_connect_with_tcp(_) -> + %% Issue #599 + %% Empty clientId and clean_session = false + {ok, Sock} = gen_tcp:connect({127,0,0,1}, 1883, [binary, {packet, raw}, {active, false}]), + Packet = raw_send_serialise(?CLIENT), + gen_tcp:send(Sock, Packet), + {ok, Data} = gen_tcp:recv(Sock, 0), + {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(Data), + gen_tcp:close(Sock). + +mqtt_connect_with_ssl_oneway(_) -> emqx:stop(), - change_opts(ssl_oneway), + emqx_ct_broker_helpers:change_opts(ssl_oneway), emqx:start(), + timer:sleep(5000), {ok, SslOneWay} = emqttc:start_link([{host, "localhost"}, {port, 8883}, {logger, debug}, @@ -173,12 +104,12 @@ mqtt_ssl_oneway(_) -> emqttc:disconnect(SslOneWay), emqttc:disconnect(Pub). -mqtt_ssl_twoway(_Config) -> +mqtt_connect_with_ssl_twoway(_Config) -> emqx:stop(), - change_opts(ssl_twoway), + emqx_ct_broker_helpers:change_opts(ssl_twoway), emqx:start(), timer:sleep(3000), - ClientSSl = [{Key, local_path(["etc", File])} || {Key, File} <- ?MQTT_SSL_CLIENT], + ClientSSl = emqx_ct_broker_helpers:client_ssl(), {ok, SslTwoWay} = emqttc:start_link([{host, "localhost"}, {port, 8883}, {client_id, <<"ssltwoway">>}, @@ -195,369 +126,16 @@ mqtt_ssl_twoway(_Config) -> emqttc:disconnect(SslTwoWay), emqttc:disconnect(Sub). -%%-------------------------------------------------------------------- -%% 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}]), - ok = emqx:unsubscribe(<<"topic">>, <<"clientId">>), - ok = emqx:unsubscribe(<<"topic/1">>, <<"clientId">>), - ok = emqx:unsubscribe(<<"topic/2">>, <<"clientId">>). - -publish(_) -> - Msg = emqx_message:make(ct, <<"test/pubsub">>, <<"hello">>), - ok = emqx:subscribe(<<"test/+">>), - timer:sleep(10), - emqx:publish(Msg), - ?assert(receive {dispatch, <<"test/+">>, Msg} -> true after 5 -> false end). - -pubsub(_) -> - Self = self(), - ok = emqx:subscribe(<<"a/b/c">>, Self, [{qos, 1}]), - ?assertMatch({error, _}, emqx:subscribe(<<"a/b/c">>, Self, [{qos, 2}])), - timer:sleep(10), - [{Self, <<"a/b/c">>}] = ets:lookup(mqtt_subscription, Self), - [{<<"a/b/c">>, Self}] = ets:lookup(mqtt_subscriber, <<"a/b/c">>), - emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), - ?assert(receive {dispatch, <<"a/b/c">>, _} -> true after 2 -> false end), - spawn(fun() -> - emqx:subscribe(<<"a/b/c">>), - emqx:subscribe(<<"c/d/e">>), - timer:sleep(10), - emqx:unsubscribe(<<"a/b/c">>) - end), - timer:sleep(20), - emqx:unsubscribe(<<"a/b/c">>). - -t_local_subscribe(_) -> - ok = emqx:subscribe("$local/topic0"), - ok = emqx:subscribe("$local/topic1", <<"x">>), - ok = emqx:subscribe("$local/topic2", <<"x">>, [{qos, 2}]), - timer:sleep(10), - ?assertEqual([self()], emqx:subscribers("$local/topic0")), - ?assertEqual([{<<"x">>, self()}], emqx:subscribers("$local/topic1")), - ?assertEqual([{{<<"x">>, self()}, <<"$local/topic1">>, []}, - {{<<"x">>, self()}, <<"$local/topic2">>, [{qos,2}]}], - emqx:subscriptions(<<"x">>)), - ?assertEqual(ok, emqx:unsubscribe("$local/topic0")), - ?assertMatch({error, {subscription_not_found, _}}, emqx:unsubscribe("$local/topic0")), - ?assertEqual(ok, emqx:unsubscribe("$local/topic1", <<"x">>)), - ?assertEqual(ok, emqx:unsubscribe("$local/topic2", <<"x">>)), - ?assertEqual([], emqx:subscribers("topic1")), - ?assertEqual([], emqx:subscriptions(<<"x">>)). - -t_shared_subscribe(_) -> - emqx:subscribe("$local/$share/group1/topic1"), - emqx:subscribe("$share/group2/topic2"), - emqx:subscribe("$queue/topic3"), - timer:sleep(10), - ?assertEqual([self()], emqx:subscribers(<<"$local/$share/group1/topic1">>)), - ?assertEqual([{self(), <<"$local/$share/group1/topic1">>, []}, - {self(), <<"$queue/topic3">>, []}, - {self(), <<"$share/group2/topic2">>, []}], - lists:sort(emqx:subscriptions(self()))), - emqx:unsubscribe("$local/$share/group1/topic1"), - emqx:unsubscribe("$share/group2/topic2"), - emqx:unsubscribe("$queue/topic3"), - ?assertEqual([], lists:sort(emqx:subscriptions(self()))). - -'pubsub#'(_) -> - emqx:subscribe(<<"a/#">>), - timer:sleep(10), - emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), - ?assert(receive {dispatch, <<"a/#">>, _} -> true after 2 -> false end), - emqx: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 1 -> false end), - emqx:unsubscribe(<<"a/+/+">>). - -loop_recv(Topic, Timeout) -> - loop_recv(Topic, Timeout, []). - -loop_recv(Topic, Timeout, Acc) -> - receive - {dispatch, Topic, Msg} -> - loop_recv(Topic, Timeout, [Msg|Acc]) - after - Timeout -> {ok, Acc} - end. - -recv_loop(Msgs) -> - receive - {dispatch, _Topic, Msg} -> - recv_loop([Msg|Msgs]) - after - 100 -> lists:reverse(Msgs) - end. - -%%-------------------------------------------------------------------- -%% Session Group -%%-------------------------------------------------------------------- - -start_session(_) -> - {ok, ClientPid} = emqx_mock_client:start_link(<<"clientId">>), - {ok, SessPid} = emqx_mock_client:start_session(ClientPid), - Message = emqx_message:make(<<"clientId">>, 2, <<"topic">>, <<"hello">>), - Message1 = Message#mqtt_message{pktid = 1}, - emqx_session:publish(SessPid, Message1), - emqx_session:pubrel(SessPid, 1), - emqx_session:subscribe(SessPid, [{<<"topic/session">>, [{qos, 2}]}]), - Message2 = emqx_message:make(<<"clientId">>, 1, <<"topic/session">>, <<"test">>), - emqx_session:publish(SessPid, Message2), - emqx_session:unsubscribe(SessPid, [{<<"topic/session">>, []}]), - emqx_mock_client:stop(ClientPid). - -%%-------------------------------------------------------------------- -%% Broker Group -%%-------------------------------------------------------------------- -hook_unhook(_) -> +mqtt_connect_with_ws(_Config) -> + WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()), + {ok, _} = rfc6455_client:open(WS), + Packet = raw_send_serialise(?CLIENT), + ok = rfc6455_client:send_binary(WS, Packet), + {binary, P} = rfc6455_client:recv(WS), + {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(P), + {close, _} = rfc6455_client:close(WS), ok. -%%-------------------------------------------------------------------- -%% Metric Group -%%-------------------------------------------------------------------- -inc_dec_metric(_) -> - emqx_metrics:inc(gauge, 'messages/retained', 10), - emqx_metrics:dec(gauge, 'messages/retained', 10). - -%%-------------------------------------------------------------------- -%% Stats Group -%%-------------------------------------------------------------------- -set_get_stat(_) -> - emqx_stats:setstat('retained/max', 99), - 99 = emqx_stats:getstat('retained/max'). - -%%-------------------------------------------------------------------- -%% Hook Test -%%-------------------------------------------------------------------- - -add_delete_hook(_) -> - ok = emqx:hook(test_hook, fun ?MODULE:hook_fun1/1, []), - ok = emqx:hook(test_hook, {tag, fun ?MODULE:hook_fun2/1}, []), - {error, already_hooked} = emqx:hook(test_hook, {tag, fun ?MODULE:hook_fun2/1}, []), - Callbacks = [{callback, undefined, fun ?MODULE:hook_fun1/1, [], 0}, - {callback, tag, fun ?MODULE:hook_fun2/1, [], 0}], - Callbacks = emqx_hooks:lookup(test_hook), - ok = emqx:unhook(test_hook, fun ?MODULE:hook_fun1/1), - ct:print("Callbacks: ~p~n", [emqx_hooks:lookup(test_hook)]), - ok = emqx:unhook(test_hook, {tag, fun ?MODULE:hook_fun2/1}), - {error, not_found} = emqx:unhook(test_hook1, {tag, fun ?MODULE:hook_fun2/1}), - [] = emqx_hooks:lookup(test_hook), - - ok = emqx:hook(emqx_hook, fun ?MODULE:hook_fun1/1, [], 9), - ok = emqx:hook(emqx_hook, {"tag", fun ?MODULE:hook_fun2/1}, [], 8), - Callbacks2 = [{callback, "tag", fun ?MODULE:hook_fun2/1, [], 8}, - {callback, undefined, fun ?MODULE:hook_fun1/1, [], 9}], - Callbacks2 = emqx_hooks:lookup(emqx_hook), - ok = emqx:unhook(emqx_hook, fun ?MODULE:hook_fun1/1), - ok = emqx:unhook(emqx_hook, {"tag", fun ?MODULE:hook_fun2/1}), - [] = emqx_hooks:lookup(emqx_hook). - -run_hooks(_) -> - ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun3/4, [init]), - ok = emqx:hook(foldl_hook, {tag, fun ?MODULE:hook_fun3/4}, [init]), - ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun4/4, [init]), - ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun5/4, [init]), - {stop, [r3, r2]} = emqx:run_hooks(foldl_hook, [arg1, arg2], []), - {ok, []} = emqx:run_hooks(unknown_hook, [], []), - - ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun6/2, [initArg]), - ok = emqx:hook(foreach_hook, {tag, fun ?MODULE:hook_fun6/2}, [initArg]), - ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun7/2, [initArg]), - ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun8/2, [initArg]), - stop = emqx:run_hooks(foreach_hook, [arg]). - -hook_fun1([]) -> ok. -hook_fun2([]) -> {ok, []}. - -hook_fun3(arg1, arg2, _Acc, init) -> ok. -hook_fun4(arg1, arg2, Acc, init) -> {ok, [r2 | Acc]}. -hook_fun5(arg1, arg2, Acc, init) -> {stop, [r3 | Acc]}. - -hook_fun6(arg, initArg) -> ok. -hook_fun7(arg, initArg) -> any. -hook_fun8(arg, initArg) -> stop. - -%%-------------------------------------------------------------------- -%% HTTP Request Test -%%-------------------------------------------------------------------- - -request_status(_) -> - {InternalStatus, _ProvidedStatus} = init:get_status(), - AppStatus = - case lists:keysearch(?APP, 1, application:which_applications()) of - false -> not_running; - {value, _Val} -> running - end, - Status = iolist_to_binary(io_lib:format("Node ~s is ~s~nemqx is ~s", - [node(), InternalStatus, AppStatus])), - Url = "http://127.0.0.1:8080/status", - {ok, {{"HTTP/1.1", 200, "OK"}, _, Return}} = - httpc:request(get, {Url, []}, [], []), - ?assertEqual(binary_to_list(Status), Return). - -request_publish(_) -> - emqttc:start_link([{host, "localhost"}, - {port, 1883}, - {client_id, <<"random">>}, - {clean_sess, false}]), - SubParams = "{\"qos\":1, \"topic\" : \"a\/b\/c\", \"client_id\" :\"random\"}", - ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/subscribe", SubParams, auth_header_("", ""))), - ok = emqx:subscribe(<<"a/b/c">>, self(), [{qos, 1}]), - Params = "{\"qos\":1, \"retain\":false, \"topic\" : \"a\/b\/c\", \"messages\" :\"hello\"}", - ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/publish", Params, auth_header_("", ""))), - ?assert(receive {dispatch, <<"a/b/c">>, _} -> true after 2 -> false end), - - UnSubParams = "{\"topic\" : \"a\/b\/c\", \"client_id\" :\"random\"}", - ?assert(connect_emqx_pubsub_(post, "api/v2/mqtt/unsubscribe", UnSubParams, auth_header_("", ""))). - -connect_emqx_publish_(Method, Api, Params, Auth) -> - Url = "http://127.0.0.1:8080/" ++ Api, - case httpc:request(Method, {Url, [Auth], ?CONTENT_TYPE, Params}, [], []) of - {error, socket_closed_remotely} -> - false; - {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> - true; - {ok, {{"HTTP/1.1", 400, _}, _, []}} -> - false; - {ok, {{"HTTP/1.1", 404, _}, _, []}} -> - false - end. - -auth_header_(User, Pass) -> - Encoded = base64:encode_to_string(lists:append([User,":",Pass])), - {"Authorization","Basic " ++ Encoded}. - -get_api_lists(_Config) -> - lists:foreach(fun request/1, ?GET_API). - -websocket_test(_) -> - Conn = esockd_connection:new(esockd_transport, nil, []), - Req = mochiweb_request:new(Conn, 'GET', "/mqtt", {1, 1}, - mochiweb_headers:make([{"Sec-WebSocket-Key","Xn3fdKyc3qEXPuj2A3O+ZA=="}])), - - ct:log("Req:~p", [Req]). - %%emqx_http:handle_request(Req). - -set_alarms(_) -> - AlarmTest = #mqtt_alarm{id = <<"1">>, severity = error, title="alarm title", summary="alarm summary"}, - emqx_alarm:set_alarm(AlarmTest), - Alarms = emqx_alarm:get_alarms(), - ?assertEqual(1, length(Alarms)), - emqx_alarm:clear_alarm(<<"1">>), - [] = emqx_alarm:get_alarms(). - -%%-------------------------------------------------------------------- -%% Cli group -%%-------------------------------------------------------------------- - -ctl_register_cmd(_) -> - emqx_ctl:register_cmd(test_cmd, {?MODULE, test_cmd}), - erlang:yield(), - timer:sleep(5), - [{?MODULE, test_cmd}] = emqx_ctl:lookup(test_cmd), - emqx_ctl:run(["test_cmd", "arg1", "arg2"]), - emqx_ctl:unregister_cmd(test_cmd). - -test_cmd(["arg1", "arg2"]) -> - ct:print("test_cmd is called"); - -test_cmd([]) -> - io:format("test command"). - -cli_status(_) -> - emqx_cli:status([]). - -cli_broker(_) -> - emqx_cli:broker([]), - emqx_cli:broker(["stats"]), - emqx_cli:broker(["metrics"]), - emqx_cli:broker(["pubsub"]). - -cli_clients(_) -> - emqx_cli:clients(["list"]), - emqx_cli:clients(["show", "clientId"]), - emqx_cli:clients(["kick", "clientId"]). - -cli_sessions(_) -> - emqx_cli:sessions(["list"]), - emqx_cli:sessions(["list", "persistent"]), - emqx_cli:sessions(["list", "transient"]), - emqx_cli:sessions(["show", "clientId"]). - -cli_routes(_) -> - emqx:subscribe(<<"topic/route">>), - emqx_cli:routes(["list"]), - emqx_cli:routes(["show", "topic/route"]), - emqx:unsubscribe(<<"topic/route">>). - -cli_topics(_) -> - emqx:subscribe(<<"topic">>), - emqx_cli:topics(["list"]), - emqx_cli:topics(["show", "topic"]), - emqx:unsubscribe(<<"topic">>). - -cli_subscriptions(_) -> - emqx_cli:subscriptions(["list"]), - emqx_cli:subscriptions(["show", "clientId"]), - emqx_cli:subscriptions(["add", "clientId", "topic", "2"]), - emqx_cli:subscriptions(["del", "clientId", "topic"]). - -cli_plugins(_) -> - emqx_cli:plugins(["list"]), - emqx_cli:plugins(["load", "emqx_plugin_template"]), - emqx_cli:plugins(["unload", "emqx_plugin_template"]). - -cli_bridges(_) -> - emqx_cli:bridges(["list"]), - emqx_cli:bridges(["start", "a@127.0.0.1", "topic"]), - emqx_cli:bridges(["stop", "a@127.0.0.1", "topic"]). - -cli_listeners(_) -> - emqx_cli:listeners([]). - -conflict_listeners(_) -> - F = - fun() -> - process_flag(trap_exit, true), - emqttc:start_link([{host, "localhost"}, - {port, 1883}, - {client_id, <<"c1">>}, - {clean_sess, false}]) - end, - spawn_link(F), - - {ok, C2} = emqttc:start_link([{host, "localhost"}, - {port, 1883}, - {client_id, <<"c1">>}, - {clean_sess, false}]), - timer:sleep(100), - - Listeners = - lists:map(fun({{Protocol, ListenOn}, Pid}) -> - Key = atom_to_list(Protocol) ++ ":" ++ esockd:to_string(ListenOn), - {Key, [{acceptors, esockd:get_acceptors(Pid)}, - {max_clients, esockd:get_max_clients(Pid)}, - {current_clients, esockd:get_current_clients(Pid)}, - {shutdown_count, esockd:get_shutdown_count(Pid)}]} - end, esockd:listeners()), - L = proplists:get_value("mqtt:tcp:0.0.0.0:1883", Listeners), - ?assertEqual(1, proplists:get_value(current_clients, L)), - ?assertEqual(1, proplists:get_value(conflict, proplists:get_value(shutdown_count, L))), - timer:sleep(100), - emqttc:disconnect(C2). - -cli_vm(_) -> - emqx_cli:vm([]), - emqx_cli:vm(["ports"]). - cleanSession_validate(_) -> {ok, C1} = emqttc:start_link([{host, "localhost"}, {port, 1883}, @@ -565,7 +143,6 @@ cleanSession_validate(_) -> {clean_sess, false}]), timer:sleep(10), emqttc:subscribe(C1, <<"topic">>, qos0), - ok = emqx_cli:sessions(["list", "persistent"]), emqttc:disconnect(C1), {ok, Pub} = emqttc:start_link([{host, "localhost"}, {port, 1883}, @@ -578,169 +155,16 @@ cleanSession_validate(_) -> {client_id, <<"c1">>}, {clean_sess, false}]), timer:sleep(100), - Metrics = emqx_metrics:all(), - ct:log("Metrics:~p~n", [Metrics]), - ?assertEqual(1, proplists:get_value('messages/qos0/sent', Metrics)), - ?assertEqual(1, proplists:get_value('messages/qos0/received', Metrics)), + receive {publish, _Topic, M1} -> + ?assertEqual(<<"m1">>, M1) + after 1000 -> false + end, emqttc:disconnect(Pub), emqttc:disconnect(C11). -cleanSession_validate1(_) -> - {ok, C1} = emqttc:start_link([{host, "localhost"}, - {port, 1883}, - {client_id, <<"c1">>}, - {clean_sess, true}]), - timer:sleep(10), - emqttc:subscribe(C1, <<"topic">>, qos1), - ok = emqx_cli:sessions(["list", "transient"]), - emqttc:disconnect(C1), - {ok, Pub} = emqttc:start_link([{host, "localhost"}, - {port, 1883}, - {client_id, <<"pub">>}]), +raw_send_serialise(Packet) -> + emqttc_serialiser:serialise(Packet). - emqttc:publish(Pub, <<"topic">>, <<"m1">>, [{qos, 1}]), - timer:sleep(10), - {ok, C11} = emqttc:start_link([{host, "localhost"}, - {port, 1883}, - {client_id, <<"c1">>}, - {clean_sess, false}]), - timer:sleep(100), - Metrics = emqx_metrics:all(), - ?assertEqual(0, proplists:get_value('messages/qos1/sent', Metrics)), - ?assertEqual(1, proplists:get_value('messages/qos1/received', Metrics)), - emqttc:disconnect(Pub), - emqttc:disconnect(C11). - -connect_emqx_pubsub_(Method, Api, Params, Auth) -> - Url = "http://127.0.0.1:8080/" ++ Api, - case httpc:request(Method, {Url, [Auth], ?CONTENT_TYPE, Params}, [], []) of - {error, socket_closed_remotely} -> - false; - {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> - true; - {ok, {{"HTTP/1.1", 400, _}, _, []}} -> - false; - {ok, {{"HTTP/1.1", 404, _}, _, []}} -> - false - end. - -request(Path) -> - http_get(get, Path). - -http_get(Method, Path) -> - req(Method, Path, []). - -http_put(Method, Path, Params) -> - req(Method, Path, format_for_upload(Params)). - -http_post(Method, Path, Params) -> - req(Method, Path, format_for_upload(Params)). - -req(Method, Path, Body) -> - Url = ?URL ++ Path, - Headers = auth_header_("", ""), - case httpc:request(Method, {Url, [Headers]}, [], []) of - {error, R} -> - ct:log("R:~p~n", [R]), - false; - {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> - true; - {ok, {{"HTTP/1.1", 400, _}, _, []}} -> - false; - {ok, {{"HTTP/1.1", 404, _}, _, []}} -> - false - end. - -format_for_upload(none) -> - <<"">>; -format_for_upload(List) -> - iolist_to_binary(mochijson2:encode(List)). - -ensure_ok(ok) -> ok; -ensure_ok({error, {already_started, _}}) -> ok. - -host() -> ct:print("!!!! Node: ~p~n", [node()]), [_, Host] = string:tokens(atom_to_list(node()), "@"), Host. - -wait_running(Node) -> - wait_running(Node, 30000). - -wait_running(Node, Timeout) when Timeout < 0 -> - throw({wait_timeout, Node}); - -wait_running(Node, Timeout) -> - case rpc:call(Node, emqx, is_running, [Node]) of - true -> ok; - false -> timer:sleep(100), - wait_running(Node, Timeout - 100) - end. - -slave(emqx, Node) -> - {ok, Slave} = slave:start(host(), Node, "-config ../../test/emqx_SUITE_data/slave.config " ++ ensure_slave()), - ct:log("Slave:~p~n", [Slave]), - rpc:call(Slave, application, ensure_all_started, [emqx]), - Slave; - -slave(node, Node) -> - {ok, N} = slave:start(host(), Node, ensure_slave()), - N. - -ensure_slave() -> - EbinDir = local_path(["ebin"]), - DepsDir = local_path(["deps", "*", "ebin"]), - RpcDir = local_path(["deps", "gen_rpc", "_build", "dev", "lib", "*", "ebin"]), - "-pa " ++ EbinDir ++ " -pa " ++ DepsDir ++ " -pa " ++ RpcDir. - -change_opts(SslType) -> - {ok, Listeners} = application:get_env(?APP, listeners), - NewListeners = - lists:foldl(fun({Protocol, Port, Opts} = Listener, Acc) -> - case Protocol of - ssl -> - SslOpts = proplists:get_value(sslopts, Opts), - Keyfile = local_path(["etc/certs", "key.pem"]), - Certfile = local_path(["etc/certs", "cert.pem"]), - TupleList1 = lists:keyreplace(keyfile, 1, SslOpts, {keyfile, Keyfile}), - TupleList2 = lists:keyreplace(certfile, 1, TupleList1, {certfile, Certfile}), - TupleList3 = - case SslType of - ssl_twoway-> - CAfile = local_path(["etc", proplists:get_value(cacertfile, ?MQTT_SSL_TWOWAY)]), - MutSslList = lists:keyreplace(cacertfile, 1, ?MQTT_SSL_TWOWAY, {cacertfile, CAfile}), - lists:merge(TupleList2, MutSslList); - _ -> - lists:filter(fun ({cacertfile, _}) -> false; - ({verify, _}) -> false; - ({fail_if_no_peer_cert, _}) -> false; - (_) -> true - end, TupleList2) - end, - [{Protocol, Port, lists:keyreplace(sslopts, 1, Opts, {sslopts, TupleList3})} | Acc]; - _ -> - [Listener | Acc] - end - end, [], Listeners), - application:set_env(?APP, listeners, NewListeners). - -generate_config() -> - Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), - Conf = conf_parse:file([local_path(["etc", "emqx.conf"])]), - cuttlefish_generator:map(Schema, Conf). - -get_base_dir(Module) -> - {file, Here} = code:is_loaded(Module), - filename:dirname(filename:dirname(Here)). - -get_base_dir() -> - get_base_dir(?MODULE). - -local_path(Components, Module) -> - filename:join([get_base_dir(Module) | Components]). - -local_path(Components) -> - local_path(Components, ?MODULE). - -set_app_env({App, Lists}) -> - lists:foreach(fun({Par, Var}) -> - application:set_env(App, Par, Var) - end, Lists). +raw_recv_pase(P) -> + emqttc_parser:parse(P, emqttc_parser:new()). diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index c311d4061..ac79daa3c 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -20,6 +20,8 @@ -include("emqx.hrl"). +-include_lib("common_test/include/ct.hrl"). + -define(AC, emqx_access_control). -import(emqx_access_rule, [compile/1, match/3]). diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl new file mode 100644 index 000000000..e0ec205f7 --- /dev/null +++ b/test/emqx_broker_SUITE.erl @@ -0,0 +1,238 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_broker_SUITE). + +-compile(export_all). + +-define(APP, emqx). + +-include_lib("eunit/include/eunit.hrl"). + +-include_lib("common_test/include/ct.hrl"). + +-include("emqx.hrl"). + +all() -> + [ + {group, pubsub}, + {group, session}, + {group, broker}, + {group, metrics}, + {group, stats}, + {group, hook}, + {group, alarms}]. + +groups() -> + [ + {pubsub, [sequence], [subscribe_unsubscribe, + publish, pubsub, + t_local_subscribe, + t_shared_subscribe, + 'pubsub#', 'pubsub+']}, + {session, [sequence], [start_session]}, + {broker, [sequence], [hook_unhook]}, + {metrics, [sequence], [inc_dec_metric]}, + {stats, [sequence], [set_get_stat]}, + {hook, [sequence], [add_delete_hook, run_hooks]}, + {alarms, [sequence], [set_alarms]} + ]. + +init_per_suite(Config) -> + emqx_ct_broker_helpers:run_setup_steps(), + Config. + +end_per_suite(Config) -> + emqx_ct_broker_helpers:run_teardown_steps(). + +%%-------------------------------------------------------------------- +%% 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}]), + ok = emqx:unsubscribe(<<"topic">>, <<"clientId">>), + ok = emqx:unsubscribe(<<"topic/1">>, <<"clientId">>), + ok = emqx:unsubscribe(<<"topic/2">>, <<"clientId">>). + +publish(_) -> + Msg = emqx_message:make(ct, <<"test/pubsub">>, <<"hello">>), + ok = emqx:subscribe(<<"test/+">>), + timer:sleep(10), + emqx:publish(Msg), + ?assert(receive {dispatch, <<"test/+">>, Msg} -> true after 5 -> false end). + +pubsub(_) -> + Self = self(), + ok = emqx:subscribe(<<"a/b/c">>, Self, [{qos, 1}]), + ?assertMatch({error, _}, emqx:subscribe(<<"a/b/c">>, Self, [{qos, 2}])), + timer:sleep(10), + [{Self, <<"a/b/c">>}] = ets:lookup(mqtt_subscription, Self), + [{<<"a/b/c">>, Self}] = ets:lookup(mqtt_subscriber, <<"a/b/c">>), + emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), + ?assert(receive {dispatch, <<"a/b/c">>, _} -> true after 2 -> false end), + spawn(fun() -> + emqx:subscribe(<<"a/b/c">>), + emqx:subscribe(<<"c/d/e">>), + timer:sleep(10), + emqx:unsubscribe(<<"a/b/c">>) + end), + timer:sleep(20), + emqx:unsubscribe(<<"a/b/c">>). + +t_local_subscribe(_) -> + ok = emqx:subscribe("$local/topic0"), + ok = emqx:subscribe("$local/topic1", <<"x">>), + ok = emqx:subscribe("$local/topic2", <<"x">>, [{qos, 2}]), + timer:sleep(10), + ?assertEqual([self()], emqx:subscribers("$local/topic0")), + ?assertEqual([{<<"x">>, self()}], emqx:subscribers("$local/topic1")), + ?assertEqual([{{<<"x">>, self()}, <<"$local/topic1">>, []}, + {{<<"x">>, self()}, <<"$local/topic2">>, [{qos,2}]}], + emqx:subscriptions(<<"x">>)), + ?assertEqual(ok, emqx:unsubscribe("$local/topic0")), + ?assertMatch({error, {subscription_not_found, _}}, emqx:unsubscribe("$local/topic0")), + ?assertEqual(ok, emqx:unsubscribe("$local/topic1", <<"x">>)), + ?assertEqual(ok, emqx:unsubscribe("$local/topic2", <<"x">>)), + ?assertEqual([], emqx:subscribers("topic1")), + ?assertEqual([], emqx:subscriptions(<<"x">>)). + +t_shared_subscribe(_) -> + emqx:subscribe("$local/$share/group1/topic1"), + emqx:subscribe("$share/group2/topic2"), + emqx:subscribe("$queue/topic3"), + timer:sleep(10), + ?assertEqual([self()], emqx:subscribers(<<"$local/$share/group1/topic1">>)), + ?assertEqual([{self(), <<"$local/$share/group1/topic1">>, []}, + {self(), <<"$queue/topic3">>, []}, + {self(), <<"$share/group2/topic2">>, []}], + lists:sort(emqx:subscriptions(self()))), + emqx:unsubscribe("$local/$share/group1/topic1"), + emqx:unsubscribe("$share/group2/topic2"), + emqx:unsubscribe("$queue/topic3"), + ?assertEqual([], lists:sort(emqx:subscriptions(self()))). + +'pubsub#'(_) -> + emqx:subscribe(<<"a/#">>), + timer:sleep(10), + emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), + ?assert(receive {dispatch, <<"a/#">>, _} -> true after 2 -> false end), + emqx: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 1 -> false end), + emqx:unsubscribe(<<"a/+/+">>). + +%%-------------------------------------------------------------------- +%% Session Group +%%-------------------------------------------------------------------- +start_session(_) -> + {ok, ClientPid} = emqx_mock_client:start_link(<<"clientId">>), + {ok, SessPid} = emqx_mock_client:start_session(ClientPid), + Message = emqx_message:make(<<"clientId">>, 2, <<"topic">>, <<"hello">>), + Message1 = Message#mqtt_message{pktid = 1}, + emqx_session:publish(SessPid, Message1), + emqx_session:pubrel(SessPid, 1), + emqx_session:subscribe(SessPid, [{<<"topic/session">>, [{qos, 2}]}]), + Message2 = emqx_message:make(<<"clientId">>, 1, <<"topic/session">>, <<"test">>), + emqx_session:publish(SessPid, Message2), + emqx_session:unsubscribe(SessPid, [{<<"topic/session">>, []}]), + emqx_mock_client:stop(ClientPid). + +%%-------------------------------------------------------------------- +%% Broker Group +%%-------------------------------------------------------------------- +hook_unhook(_) -> + ok. + +%%-------------------------------------------------------------------- +%% Metric Group +%%-------------------------------------------------------------------- +inc_dec_metric(_) -> + emqx_metrics:inc(gauge, 'messages/retained', 10), + emqx_metrics:dec(gauge, 'messages/retained', 10). + +%%-------------------------------------------------------------------- +%% Stats Group +%%-------------------------------------------------------------------- +set_get_stat(_) -> + emqx_stats:setstat('retained/max', 99), + 99 = emqx_stats:getstat('retained/max'). + +%%-------------------------------------------------------------------- +%% Hook Test +%%-------------------------------------------------------------------- + +add_delete_hook(_) -> + ok = emqx:hook(test_hook, fun ?MODULE:hook_fun1/1, []), + ok = emqx:hook(test_hook, {tag, fun ?MODULE:hook_fun2/1}, []), + {error, already_hooked} = emqx:hook(test_hook, {tag, fun ?MODULE:hook_fun2/1}, []), + Callbacks = [{callback, undefined, fun ?MODULE:hook_fun1/1, [], 0}, + {callback, tag, fun ?MODULE:hook_fun2/1, [], 0}], + Callbacks = emqx_hooks:lookup(test_hook), + ok = emqx:unhook(test_hook, fun ?MODULE:hook_fun1/1), + ct:print("Callbacks: ~p~n", [emqx_hooks:lookup(test_hook)]), + ok = emqx:unhook(test_hook, {tag, fun ?MODULE:hook_fun2/1}), + {error, not_found} = emqx:unhook(test_hook1, {tag, fun ?MODULE:hook_fun2/1}), + [] = emqx_hooks:lookup(test_hook), + + ok = emqx:hook(emqx_hook, fun ?MODULE:hook_fun1/1, [], 9), + ok = emqx:hook(emqx_hook, {"tag", fun ?MODULE:hook_fun2/1}, [], 8), + Callbacks2 = [{callback, "tag", fun ?MODULE:hook_fun2/1, [], 8}, + {callback, undefined, fun ?MODULE:hook_fun1/1, [], 9}], + Callbacks2 = emqx_hooks:lookup(emqx_hook), + ok = emqx:unhook(emqx_hook, fun ?MODULE:hook_fun1/1), + ok = emqx:unhook(emqx_hook, {"tag", fun ?MODULE:hook_fun2/1}), + [] = emqx_hooks:lookup(emqx_hook). + +run_hooks(_) -> + ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun3/4, [init]), + ok = emqx:hook(foldl_hook, {tag, fun ?MODULE:hook_fun3/4}, [init]), + ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun4/4, [init]), + ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun5/4, [init]), + {stop, [r3, r2]} = emqx:run_hooks(foldl_hook, [arg1, arg2], []), + {ok, []} = emqx:run_hooks(unknown_hook, [], []), + + ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun6/2, [initArg]), + ok = emqx:hook(foreach_hook, {tag, fun ?MODULE:hook_fun6/2}, [initArg]), + ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun7/2, [initArg]), + ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun8/2, [initArg]), + stop = emqx:run_hooks(foreach_hook, [arg]). + +hook_fun1([]) -> ok. +hook_fun2([]) -> {ok, []}. + +hook_fun3(arg1, arg2, _Acc, init) -> ok. +hook_fun4(arg1, arg2, Acc, init) -> {ok, [r2 | Acc]}. +hook_fun5(arg1, arg2, Acc, init) -> {stop, [r3 | Acc]}. + +hook_fun6(arg, initArg) -> ok. +hook_fun7(arg, initArg) -> any. +hook_fun8(arg, initArg) -> stop. + +set_alarms(_) -> + AlarmTest = #mqtt_alarm{id = <<"1">>, severity = error, title="alarm title", summary="alarm summary"}, + emqx_alarm:set_alarm(AlarmTest), + Alarms = emqx_alarm:get_alarms(), + ?assertEqual(1, length(Alarms)), + emqx_alarm:clear_alarm(<<"1">>), + [] = emqx_alarm:get_alarms(). + + diff --git a/test/emqx_ct_broker_helpers.erl b/test/emqx_ct_broker_helpers.erl new file mode 100644 index 000000000..3dfe84fcc --- /dev/null +++ b/test/emqx_ct_broker_helpers.erl @@ -0,0 +1,102 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_ct_broker_helpers). + +-compile(export_all). + +-define(APP, emqx). + +-define(MQTT_SSL_TWOWAY, [{cacertfile, "certs/cacert.pem"}, + {verify, verify_peer}, + {fail_if_no_peer_cert, true}]). + +-define(MQTT_SSL_CLIENT, [{keyfile, "certs/client-key.pem"}, + {cacertfile, "certs/cacert.pem"}, + {certfile, "certs/client-cert.pem"}]). + + +run_setup_steps() -> + NewConfig = generate_config(), + lists:foreach(fun set_app_env/1, NewConfig), + application:ensure_all_started(?APP). + +run_teardown_steps() -> + ?APP:shutdown(). + +generate_config() -> + Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), + Conf = conf_parse:file([local_path(["etc", "emqx.conf"])]), + cuttlefish_generator:map(Schema, Conf). + +get_base_dir(Module) -> + {file, Here} = code:is_loaded(Module), + filename:dirname(filename:dirname(Here)). + +get_base_dir() -> + get_base_dir(?MODULE). + +local_path(Components, Module) -> + filename:join([get_base_dir(Module) | Components]). + +local_path(Components) -> + local_path(Components, ?MODULE). + +set_app_env({App, Lists}) -> + lists:foreach(fun({acl_file, _Var}) -> + application:set_env(App, acl_file, local_path(["etc", "acl.conf"])); + ({license_file, _Var}) -> + application:set_env(App, license_file, local_path(["etc", "emqx.lic"])); + ({plugins_loaded_file, _Var}) -> + application:set_env(App, plugins_loaded_file, local_path(["test", "emqx_SUITE_data","loaded_plugins"])); + ({Par, Var}) -> + application:set_env(App, Par, Var) + end, Lists). + +change_opts(SslType) -> + {ok, Listeners} = application:get_env(?APP, listeners), + NewListeners = + lists:foldl(fun({Protocol, Port, Opts} = Listener, Acc) -> + case Protocol of + ssl -> + SslOpts = proplists:get_value(sslopts, Opts), + Keyfile = local_path(["etc/certs", "key.pem"]), + Certfile = local_path(["etc/certs", "cert.pem"]), + TupleList1 = lists:keyreplace(keyfile, 1, SslOpts, {keyfile, Keyfile}), + TupleList2 = lists:keyreplace(certfile, 1, TupleList1, {certfile, Certfile}), + TupleList3 = + case SslType of + ssl_twoway-> + CAfile = local_path(["etc", proplists:get_value(cacertfile, ?MQTT_SSL_TWOWAY)]), + MutSslList = lists:keyreplace(cacertfile, 1, ?MQTT_SSL_TWOWAY, {cacertfile, CAfile}), + lists:merge(TupleList2, MutSslList); + _ -> + lists:filter(fun ({cacertfile, _}) -> false; + ({verify, _}) -> false; + ({fail_if_no_peer_cert, _}) -> false; + (_) -> true + end, TupleList2) + end, + [{Protocol, Port, lists:keyreplace(sslopts, 1, Opts, {sslopts, TupleList3})} | Acc]; + _ -> + [Listener | Acc] + end + end, [], Listeners), + application:set_env(?APP, listeners, NewListeners). + +client_ssl() -> + [{Key, local_path(["etc", File])} || {Key, File} <- ?MQTT_SSL_CLIENT]. + diff --git a/test/emqx_mod_SUITE.erl b/test/emqx_mod_SUITE.erl index d1c340a0b..e87bddecc 100644 --- a/test/emqx_mod_SUITE.erl +++ b/test/emqx_mod_SUITE.erl @@ -23,13 +23,4 @@ all() -> [mod_subscription_rep]. mod_subscription_rep(_) -> ok. -%% <<"topic/clientId">> = emqttd_mod_subscription:rep( -%% <<"$c">>, <<"clientId">>, <<"topic/$c">>), -%% <<"topic/username">> = emqttd_mod_subscription:rep( -%% <<"$u">>, <<"username">>, <<"topic/$u">>), -%% <<"topic/username/clientId">> = emqttd_mod_subscription:rep( -%% <<"$c">>, <<"clientId">>, emqttd_mod_subscription:rep( -%% <<"$u">>, <<"username">>, <<"topic/$u/$c">>)). - - diff --git a/test/rfc6455_client.erl b/test/rfc6455_client.erl new file mode 100644 index 000000000..4696f7ab3 --- /dev/null +++ b/test/rfc6455_client.erl @@ -0,0 +1,252 @@ +%% 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. From c396bf48889d85deb95d9e601ed47562793d0afb Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 24 Feb 2018 16:01:27 +0800 Subject: [PATCH 018/520] Add TODO --- TODO | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 TODO diff --git a/TODO b/TODO new file mode 100644 index 000000000..f748eab3f --- /dev/null +++ b/TODO @@ -0,0 +1,2 @@ +1. Update the README.md +2. Update the documentation From d5893ba2be0581501614c62547d3c826a6ef25e5 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 26 Feb 2018 13:24:29 +0800 Subject: [PATCH 019/520] Replace gen_server2 with gen_server for we cannot trace the size of drain queue --- src/emqx_bridge.erl | 6 +- src/emqx_client.erl | 44 +- src/emqx_cm.erl | 20 +- src/emqx_pubsub.erl | 18 +- src/emqx_server.erl | 17 +- src/emqx_session.erl | 89 +-- src/emqx_sm.erl | 20 +- src/emqx_ws_client.erl | 26 +- src/gen_server2.erl | 1361 ---------------------------------------- src/priority_queue.erl | 259 -------- 10 files changed, 84 insertions(+), 1776 deletions(-) delete mode 100644 src/gen_server2.erl delete mode 100644 src/priority_queue.erl diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index 8ad6bf4b2..fc6f33535 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -16,9 +16,7 @@ -module(emqx_bridge). --author("Feng Lee "). - --behaviour(gen_server2). +-behaviour(gen_server). -include("emqx.hrl"). @@ -61,7 +59,7 @@ -spec(start_link(any(), pos_integer(), atom(), binary(), [option()]) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id, Node, Topic, Options) -> - gen_server2:start_link(?MODULE, [Pool, Id, Node, Topic, Options], []). + gen_server:start_link(?MODULE, [Pool, Id, Node, Topic, Options], []). %%-------------------------------------------------------------------- %% gen_server callbacks diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 520cd8878..0a3d7d8ce 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -18,9 +18,7 @@ -module(emqx_client). --behaviour(gen_server2). - --author("Feng Lee "). +-behaviour(gen_server). -include("emqx.hrl"). @@ -48,8 +46,8 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]). -%% gen_server2 Callbacks --export([prioritise_call/4, prioritise_info/3, handle_pre_hibernate/1]). +%% TODO: How to emit stats? +-export([handle_pre_hibernate/1]). %% Client State %% Unused fields: connname, peerhost, peerport @@ -69,19 +67,19 @@ start_link(Conn, Env) -> {ok, proc_lib:spawn_link(?MODULE, init, [[Conn, Env]])}. info(CPid) -> - gen_server2:call(CPid, info). + gen_server:call(CPid, info). stats(CPid) -> - gen_server2:call(CPid, stats). + gen_server:call(CPid, stats). kick(CPid) -> - gen_server2:call(CPid, kick). + gen_server:call(CPid, kick). set_rate_limit(Cpid, Rl) -> - gen_server2:call(Cpid, {set_rate_limit, Rl}). + gen_server:call(Cpid, {set_rate_limit, Rl}). get_rate_limit(Cpid) -> - gen_server2:call(Cpid, get_rate_limit). + gen_server:call(Cpid, get_rate_limit). subscribe(CPid, TopicTable) -> CPid ! {subscribe, TopicTable}. @@ -90,10 +88,10 @@ unsubscribe(CPid, Topics) -> CPid ! {unsubscribe, Topics}. session(CPid) -> - gen_server2:call(CPid, session, infinity). + gen_server:call(CPid, session, infinity). clean_acl_cache(CPid, Topic) -> - gen_server2:call(CPid, {clean_acl_cache, Topic}). + gen_server:call(CPid, {clean_acl_cache, Topic}). %%-------------------------------------------------------------------- %% gen_server Callbacks @@ -130,8 +128,8 @@ do_init(Conn, Env, Peername) -> enable_stats = EnableStats, idle_timeout = IdleTimout, force_gc_count = ForceGcCount}), - gen_server2:enter_loop(?MODULE, [], State, self(), IdleTimout, - {backoff, 2000, 2000, 20000}). + gen_server:enter_loop(?MODULE, [{hibernate_after, 10000}], + State, self(), IdleTimout). send_fun(Conn, Peername) -> Self = self(), @@ -148,12 +146,6 @@ send_fun(Conn, Peername) -> end end. -prioritise_call(Msg, _From, _Len, _State) -> - case Msg of info -> 10; stats -> 10; state -> 10; _ -> 5 end. - -prioritise_info(Msg, _Len, _State) -> - case Msg of {redeliver, _} -> 5; _ -> 0 end. - handle_pre_hibernate(State) -> {hibernate, emqx_gc:reset_conn_gc_count(#client_state.force_gc_count, emit_stats(State))}. @@ -241,7 +233,7 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> shutdown(conflict, State); handle_info(activate_sock, State) -> - {noreply, run_socket(State#client_state{conn_state = running}), hibernate}; + {noreply, run_socket(State#client_state{conn_state = running})}; handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) -> Size = iolist_size(Data), @@ -253,7 +245,7 @@ handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) -> shutdown(Reason, State); handle_info({inet_reply, _Sock, ok}, State) -> - {noreply, gc(State), hibernate}; %% Tune GC + {noreply, gc(State)}; %% Tune GC handle_info({inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); @@ -268,7 +260,7 @@ handle_info({keepalive, start, Interval}, State = #client_state{connection = Con end, case emqx_keepalive:start(StatFun, Interval, {keepalive, check}) of {ok, KeepAlive} -> - {noreply, State#client_state{keepalive = KeepAlive}, hibernate}; + {noreply, State#client_state{keepalive = KeepAlive}}; {error, Error} -> ?LOG(warning, "Keepalive error - ~p", [Error], State), shutdown(Error, State) @@ -277,7 +269,7 @@ handle_info({keepalive, start, Interval}, State = #client_state{connection = Con handle_info({keepalive, check}, State = #client_state{keepalive = KeepAlive}) -> case emqx_keepalive:check(KeepAlive) of {ok, KeepAlive1} -> - {noreply, State#client_state{keepalive = KeepAlive1}, hibernate}; + {noreply, State#client_state{keepalive = KeepAlive1}}; {error, timeout} -> ?LOG(debug, "Keepalive timeout", [], State), shutdown(keepalive_timeout, State); @@ -314,7 +306,7 @@ code_change(_OldVsn, State, _Extra) -> %% Receive and Parse TCP Data received(<<>>, State) -> - {noreply, gc(State), hibernate}; + {noreply, gc(State)}; received(Bytes, State = #client_state{parser = Parser, packet_size = PacketSize, @@ -368,7 +360,7 @@ run_socket(State = #client_state{connection = Conn}) -> with_proto(Fun, State = #client_state{proto_state = ProtoState}) -> {ok, ProtoState1} = Fun(ProtoState), - {noreply, State#client_state{proto_state = ProtoState1}, hibernate}. + {noreply, State#client_state{proto_state = ProtoState1}}. emit_stats(State = #client_state{proto_state = ProtoState}) -> emit_stats(emqx_protocol:clientid(ProtoState), State). diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 2cdbbefe0..f0b502170 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -18,7 +18,7 @@ -module(emqx_cm). --behaviour(gen_server2). +-behaviour(gen_server). -author("Feng Lee "). @@ -35,9 +35,6 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% gen_server2 priorities --export([prioritise_call/4, prioritise_cast/3, prioritise_info/3]). - -record(state, {pool, id, statsfun, monitors}). -define(POOL, ?MODULE). @@ -49,7 +46,7 @@ %% @doc Start Client Manager -spec(start_link(atom(), pos_integer(), fun()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id, StatsFun) -> - gen_server2:start_link(?MODULE, [Pool, Id, StatsFun], []). + gen_server:start_link(?MODULE, [Pool, Id, StatsFun], []). %% @doc Lookup Client by ClientId -spec(lookup(binary()) -> mqtt_client() | undefined). @@ -67,12 +64,12 @@ lookup_proc(ClientId) when is_binary(ClientId) -> %% @doc Register ClientId with Pid. -spec(reg(mqtt_client()) -> ok). reg(Client = #mqtt_client{client_id = ClientId}) -> - gen_server2:call(pick(ClientId), {reg, Client}, 120000). + gen_server:call(pick(ClientId), {reg, Client}, 120000). %% @doc Unregister clientId with pid. -spec(unreg(binary()) -> ok). unreg(ClientId) when is_binary(ClientId) -> - gen_server2:cast(pick(ClientId), {unreg, ClientId, self()}). + gen_server:cast(pick(ClientId), {unreg, ClientId, self()}). pick(ClientId) -> gproc_pool:pick_worker(?POOL, ClientId). @@ -84,15 +81,6 @@ init([Pool, Id, StatsFun]) -> ?GPROC_POOL(join, Pool, Id), {ok, #state{pool = Pool, id = Id, statsfun = StatsFun, monitors = dict:new()}}. -prioritise_call(Req, _From, _Len, _State) -> - case Req of {reg, _Client} -> 2; _ -> 1 end. - -prioritise_cast(Msg, _Len, _State) -> - case Msg of {unreg, _ClientId, _Pid} -> 9; _ -> 1 end. - -prioritise_info(_Msg, _Len, _State) -> - 3. - handle_call({reg, Client = #mqtt_client{client_id = ClientId, client_pid = Pid}}, _From, State) -> case lookup_proc(ClientId) of diff --git a/src/emqx_pubsub.erl b/src/emqx_pubsub.erl index 873cd1c2b..03647fa3c 100644 --- a/src/emqx_pubsub.erl +++ b/src/emqx_pubsub.erl @@ -16,9 +16,7 @@ -module(emqx_pubsub). --behaviour(gen_server2). - --author("Feng Lee "). +-behaviour(gen_server). -include("emqx.hrl"). @@ -48,7 +46,8 @@ -spec(start_link(atom(), pos_integer(), list()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id, Env) -> - gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). + gen_server:start_link({local, ?PROC_NAME(?MODULE, Id)}, + ?MODULE, [Pool, Id, Env], [{hibernate_after, 10000}]). %%-------------------------------------------------------------------- %% PubSub API @@ -152,10 +151,10 @@ async_unsubscribe(Topic, Subscriber, Options) -> cast(pick(Topic), {unsubscribe, Topic, Subscriber, Options}). call(PubSub, Req) when is_pid(PubSub) -> - gen_server2:call(PubSub, Req, infinity). + gen_server:call(PubSub, Req, infinity). cast(PubSub, Msg) when is_pid(PubSub) -> - gen_server2:cast(PubSub, Msg). + gen_server:cast(PubSub, Msg). pick(Topic) -> gproc_pool:pick_worker(pubsub, Topic). @@ -166,8 +165,7 @@ pick(Topic) -> init([Pool, Id, Env]) -> ?GPROC_POOL(join, Pool, Id), - {ok, #state{pool = Pool, id = Id, env = Env}, - hibernate, {backoff, 2000, 2000, 20000}}. + {ok, #state{pool = Pool, id = Id, env = Env}, hibernate}. handle_call({subscribe, Topic, Subscriber, Options}, _From, State) -> add_subscriber(Topic, Subscriber, Options), @@ -247,8 +245,8 @@ setstats(State) -> State. reply(Reply, State) -> - {reply, Reply, State, hibernate}. + {reply, Reply, State}. noreply(State) -> - {noreply, State, hibernate}. + {noreply, State}. diff --git a/src/emqx_server.erl b/src/emqx_server.erl index 1abb0d24b..47aafad49 100644 --- a/src/emqx_server.erl +++ b/src/emqx_server.erl @@ -16,9 +16,7 @@ -module(emqx_server). --behaviour(gen_server2). - --author("Feng Lee "). +-behaviour(gen_server). -include("emqx.hrl"). @@ -51,7 +49,8 @@ %% @doc Start the server -spec(start_link(atom(), pos_integer(), list()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id, Env) -> - gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). + gen_server:start_link({local, ?PROC_NAME(?MODULE, Id)}, + ?MODULE, [Pool, Id, Env], [{hibernate_after, 10000}]). %%-------------------------------------------------------------------- %% PubSub API @@ -167,10 +166,10 @@ subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), is_binary(SubId), is_p ets:member(mqtt_subproperty, {Topic, {SubId, SubPid}}). call(Server, Req) -> - gen_server2:call(Server, Req, infinity). + gen_server:call(Server, Req, infinity). cast(Server, Msg) when is_pid(Server) -> - gen_server2:cast(Server, Msg). + gen_server:cast(Server, Msg). pick(SubPid) when is_pid(SubPid) -> gproc_pool:pick_worker(server, SubPid); @@ -190,7 +189,7 @@ init([Pool, Id, Env]) -> ?GPROC_POOL(join, Pool, Id), State = #state{pool = Pool, id = Id, env = Env, subids = #{}, submon = emqx_pmon:new()}, - {ok, State, hibernate, {backoff, 2000, 2000, 20000}}. + {ok, State, hibernate}. handle_call({subscribe, Topic, Subscriber, Options}, _From, State) -> case do_subscribe(Topic, Subscriber, Options, State) of @@ -321,8 +320,8 @@ setstats(State) -> ets:info(mqtt_subscription, size)), State. reply(Reply, State) -> - {reply, Reply, State, hibernate}. + {reply, Reply, State}. noreply(State) -> - {noreply, State, hibernate}. + {noreply, State}. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 8c72cd499..44cd44e00 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -45,7 +45,7 @@ -module(emqx_session). --behaviour(gen_server2). +-behaviour(gen_server). -author("Feng Lee "). @@ -73,9 +73,8 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% gen_server2 Message Priorities --export([prioritise_call/4, prioritise_cast/3, prioritise_info/3, - handle_pre_hibernate/1]). +%% TODO: gen_server Message Priorities +-export([handle_pre_hibernate/1]). -define(MQueue, emqx_mqueue). @@ -175,7 +174,8 @@ %% @doc Start a Session -spec(start_link(boolean(), {mqtt_client_id(), mqtt_username()}, pid()) -> {ok, pid()} | {error, term()}). start_link(CleanSess, {ClientId, Username}, ClientPid) -> - gen_server2:start_link(?MODULE, [CleanSess, {ClientId, Username}, ClientPid], []). + gen_server:start_link(?MODULE, [CleanSess, {ClientId, Username}, ClientPid], + [{hibernate_after, 10000}]). %%-------------------------------------------------------------------- %% PubSub API @@ -183,14 +183,14 @@ start_link(CleanSess, {ClientId, Username}, ClientPid) -> %% @doc Subscribe topics -spec(subscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok). -subscribe(Session, TopicTable) ->%%TODO: the ack function??... - gen_server2:cast(Session, {subscribe, self(), TopicTable, fun(_) -> ok end}). +subscribe(Session, TopicTable) -> %%TODO: the ack function??... + gen_server:cast(Session, {subscribe, self(), TopicTable, fun(_) -> ok end}). -spec(subscribe(pid(), mqtt_packet_id(), [{binary(), [emqx_topic:option()]}]) -> ok). subscribe(Session, PacketId, TopicTable) -> %%TODO: the ack function??... From = self(), AckFun = fun(GrantedQos) -> From ! {suback, PacketId, GrantedQos} end, - gen_server2:cast(Session, {subscribe, From, TopicTable, AckFun}). + gen_server:cast(Session, {subscribe, From, TopicTable, AckFun}). %% @doc Publish Message -spec(publish(pid(), mqtt_message()) -> ok | {error, term()}). @@ -204,50 +204,50 @@ publish(_Session, Msg = #mqtt_message{qos = ?QOS_1}) -> publish(Session, Msg = #mqtt_message{qos = ?QOS_2}) -> %% Publish QoS2 to Session - gen_server2:call(Session, {publish, Msg}, ?TIMEOUT). + gen_server:call(Session, {publish, Msg}, ?TIMEOUT). %% @doc PubAck Message -spec(puback(pid(), mqtt_packet_id()) -> ok). puback(Session, PacketId) -> - gen_server2:cast(Session, {puback, PacketId}). + gen_server:cast(Session, {puback, PacketId}). -spec(pubrec(pid(), mqtt_packet_id()) -> ok). pubrec(Session, PacketId) -> - gen_server2:cast(Session, {pubrec, PacketId}). + gen_server:cast(Session, {pubrec, PacketId}). -spec(pubrel(pid(), mqtt_packet_id()) -> ok). pubrel(Session, PacketId) -> - gen_server2:cast(Session, {pubrel, PacketId}). + gen_server:cast(Session, {pubrel, PacketId}). -spec(pubcomp(pid(), mqtt_packet_id()) -> ok). pubcomp(Session, PacketId) -> - gen_server2:cast(Session, {pubcomp, PacketId}). + gen_server:cast(Session, {pubcomp, PacketId}). %% @doc Unsubscribe the topics -spec(unsubscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok). unsubscribe(Session, TopicTable) -> - gen_server2:cast(Session, {unsubscribe, self(), TopicTable}). + gen_server:cast(Session, {unsubscribe, self(), TopicTable}). %% @doc Resume the session -spec(resume(pid(), mqtt_client_id(), pid()) -> ok). resume(Session, ClientId, ClientPid) -> - gen_server2:cast(Session, {resume, ClientId, ClientPid}). + gen_server:cast(Session, {resume, ClientId, ClientPid}). %% @doc Get session state state(Session) when is_pid(Session) -> - gen_server2:call(Session, state). + gen_server:call(Session, state). %% @doc Get session info -spec(info(pid() | #state{}) -> list(tuple())). info(Session) when is_pid(Session) -> - gen_server2:call(Session, info); + gen_server:call(Session, info); info(State) when is_record(State, state) -> ?record_to_proplist(state, State, ?INFO_KEYS). -spec(stats(pid() | #state{}) -> list({atom(), non_neg_integer()})). stats(Session) when is_pid(Session) -> - gen_server2:call(Session, stats); + gen_server:call(Session, stats); stats(#state{max_subscriptions = MaxSubscriptions, subscriptions = Subscriptions, @@ -272,7 +272,7 @@ stats(#state{max_subscriptions = MaxSubscriptions, %% @doc Destroy the session -spec(destroy(pid(), mqtt_client_id()) -> ok). destroy(Session, ClientId) -> - gen_server2:cast(Session, {destroy, ClientId}). + gen_server:cast(Session, {destroy, ClientId}). %%-------------------------------------------------------------------- %% gen_server Callbacks @@ -311,7 +311,7 @@ init([CleanSess, {ClientId, Username}, ClientPid]) -> created_at = os:timestamp()}, emqx_sm:register_session(ClientId, CleanSess, info(State)), emqx_hooks:run('session.created', [ClientId, Username]), - {ok, emit_stats(State), hibernate, {backoff, 1000, 1000, 10000}}. + {ok, emit_stats(State), hibernate}. init_stats(Keys) -> lists:foreach(fun(K) -> put(K, 0) end, Keys). @@ -319,30 +319,6 @@ init_stats(Keys) -> binding(ClientPid) -> case node(ClientPid) =:= node() of true -> local; false -> remote end. -prioritise_call(Msg, _From, _Len, _State) -> - case Msg of info -> 10; stats -> 10; state -> 10; _ -> 5 end. - -prioritise_cast(Msg, _Len, _State) -> - case Msg of - {destroy, _} -> 10; - {resume, _, _} -> 9; - {pubrel, _} -> 8; - {pubcomp, _} -> 8; - {pubrec, _} -> 8; - {puback, _} -> 7; - {unsubscribe, _, _} -> 6; - {subscribe, _, _} -> 5; - _ -> 0 - end. - -prioritise_info(Msg, _Len, _State) -> - case Msg of - {'EXIT', _, _} -> 10; - {timeout, _, _} -> 5; - {dispatch, _, _} -> 1; - _ -> 0 - end. - handle_pre_hibernate(State) -> {hibernate, emqx_gc:reset_conn_gc_count(#state.force_gc_count, emit_stats(State))}. @@ -406,7 +382,7 @@ handle_cast({subscribe, From, TopicTable, AckFun}, {[NewQos|QosAcc], SubMap1} end, {[], Subscriptions}, TopicTable), AckFun(lists:reverse(GrantedQos)), - hibernate(emit_stats(State#state{subscriptions = Subscriptions1})); + {noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate}; handle_cast({unsubscribe, From, TopicTable}, State = #state{client_id = ClientId, @@ -428,7 +404,7 @@ handle_cast({unsubscribe, From, TopicTable}, SubMap end end, Subscriptions, TopicTable), - hibernate(emit_stats(State#state{subscriptions = Subscriptions1})); + {noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate}; %% PUBACK: handle_cast({puback, PacketId}, State = #state{inflight = Inflight}) -> @@ -525,7 +501,7 @@ handle_cast({resume, ClientId, ClientPid}, end, %% Replay delivery and Dequeue pending messages - hibernate(emit_stats(dequeue(retry_delivery(true, State1)))); + {noreply, emit_stats(dequeue(retry_delivery(true, State1)))}; handle_cast({destroy, ClientId}, State = #state{client_id = ClientId, client_pid = undefined}) -> @@ -543,21 +519,21 @@ handle_cast(Msg, State) -> %% Ignore Messages delivered by self handle_info({dispatch, _Topic, #mqtt_message{from = {ClientId, _}}}, State = #state{client_id = ClientId, ignore_loop_deliver = true}) -> - hibernate(State); + {noreply, State}; %% Dispatch Message handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, mqtt_message) -> - hibernate(gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))); + {noreply, gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))}; %% Do nothing if the client has been disconnected. handle_info({timeout, _Timer, retry_delivery}, State = #state{client_pid = undefined}) -> - hibernate(emit_stats(State#state{retry_timer = undefined})); + {noreply, emit_stats(State#state{retry_timer = undefined})}; handle_info({timeout, _Timer, retry_delivery}, State) -> - hibernate(emit_stats(retry_delivery(false, State#state{retry_timer = undefined}))); + {noreply, emit_stats(retry_delivery(false, State#state{retry_timer = undefined}))}; handle_info({timeout, _Timer, check_awaiting_rel}, State) -> - hibernate(expire_awaiting_rel(emit_stats(State#state{await_rel_timer = undefined}))); + {noreply, expire_awaiting_rel(emit_stats(State#state{await_rel_timer = undefined}))}; handle_info({timeout, _Timer, expired}, State) -> ?LOG(info, "Expired, shutdown now.", [], State), @@ -574,17 +550,17 @@ handle_info({'EXIT', ClientPid, Reason}, ?LOG(info, "Client ~p EXIT for ~p", [ClientPid, Reason], State), ExpireTimer = start_timer(Interval, expired), State1 = State#state{client_pid = undefined, expiry_timer = ExpireTimer}, - hibernate(emit_stats(State1)); + {noreply, emit_stats(State1), hibernate}; handle_info({'EXIT', Pid, _Reason}, State = #state{old_client_pid = Pid}) -> %%ignore - hibernate(State); + {noreply, State, hibernate}; handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = ClientPid}) -> ?LOG(error, "Unexpected EXIT: client_pid=~p, exit_pid=~p, reason=~p", [ClientPid, Pid, Reason], State), - hibernate(State); + {noreply, State, hibernate}; handle_info(Info, Session) -> ?UNEXPECTED_INFO(Info, Session). @@ -857,9 +833,6 @@ inc_stats(Key) -> put(Key, get(Key) + 1). reply(Reply, State) -> {reply, Reply, State, hibernate}. -hibernate(State) -> - {noreply, State, hibernate}. - shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index aca1f77f1..d75f68984 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -16,9 +16,7 @@ -module(emqx_sm). --author("Feng Lee "). - --behaviour(gen_server2). +-behaviour(gen_server). -include("emqx.hrl"). @@ -44,9 +42,6 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% gen_server2 priorities --export([prioritise_call/4, prioritise_cast/3, prioritise_info/3]). - -record(state, {pool, id, monitors}). -define(POOL, ?MODULE). @@ -78,7 +73,7 @@ mnesia(copy) -> %% @doc Start a session manager -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> - gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id], []). + gen_server:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id], []). %% @doc Start a session -spec(start_session(boolean(), {binary(), binary() | undefined}) -> {ok, pid(), boolean()} | {error, term()}). @@ -123,7 +118,7 @@ dispatch(ClientId, Topic, Msg) -> end. call(SM, Req) -> - gen_server2:call(SM, Req, ?TIMEOUT). %%infinity). + gen_server:call(SM, Req, ?TIMEOUT). %%infinity). %% @doc for debug. local_sessions() -> @@ -137,15 +132,6 @@ init([Pool, Id]) -> ?GPROC_POOL(join, Pool, Id), {ok, #state{pool = Pool, id = Id, monitors = dict:new()}}. -prioritise_call(_Msg, _From, _Len, _State) -> - 1. - -prioritise_cast(_Msg, _Len, _State) -> - 0. - -prioritise_info(_Msg, _Len, _State) -> - 2. - %% Persistent Session handle_call({start_session, false, {ClientId, Username}, ClientPid}, _From, State) -> case lookup_session(ClientId) of diff --git a/src/emqx_ws_client.erl b/src/emqx_ws_client.erl index bac0f48d3..b5f8d7b51 100644 --- a/src/emqx_ws_client.erl +++ b/src/emqx_ws_client.erl @@ -18,7 +18,7 @@ -module(emqx_ws_client). --behaviour(gen_server2). +-behaviour(gen_server). -author("Feng Lee "). @@ -46,8 +46,8 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% gen_server2 Callbacks --export([prioritise_call/4, prioritise_info/3, handle_pre_hibernate/1]). +%% TODO: remove ... +-export([handle_pre_hibernate/1]). %% WebSocket Client State -record(wsclient_state, {ws_pid, peername, connection, proto_state, keepalive, @@ -61,17 +61,17 @@ %% @doc Start WebSocket Client. start_link(Env, WsPid, Req, ReplyChannel) -> - gen_server2:start_link(?MODULE, [Env, WsPid, Req, ReplyChannel], - [{spawn_opt, ?FULLSWEEP_OPTS}]). %% Tune GC. + gen_server:start_link(?MODULE, [Env, WsPid, Req, ReplyChannel], + [[{hibernate_after, 10000}]]). info(CPid) -> - gen_server2:call(CPid, info). + gen_server:call(CPid, info). stats(CPid) -> - gen_server2:call(CPid, stats). + gen_server:call(CPid, stats). kick(CPid) -> - gen_server2:call(CPid, kick). + gen_server:call(CPid, kick). subscribe(CPid, TopicTable) -> CPid ! {subscribe, TopicTable}. @@ -80,10 +80,10 @@ unsubscribe(CPid, Topics) -> CPid ! {unsubscribe, Topics}. session(CPid) -> - gen_server2:call(CPid, session). + gen_server:call(CPid, session). clean_acl_cache(CPid, Topic) -> - gen_server2:call(CPid, {clean_acl_cache, Topic}). + gen_server:call(CPid, {clean_acl_cache, Topic}). %%-------------------------------------------------------------------- %% gen_server Callbacks @@ -116,12 +116,6 @@ init([Env, WsPid, Req, ReplyChannel]) -> exit({shutdown, Reason}) end. -prioritise_call(Msg, _From, _Len, _State) -> - case Msg of info -> 10; stats -> 10; state -> 10; _ -> 5 end. - -prioritise_info(Msg, _Len, _State) -> - case Msg of {redeliver, _} -> 5; _ -> 0 end. - handle_pre_hibernate(State = #wsclient_state{ws_pid = WsPid}) -> erlang:garbage_collect(WsPid), {hibernate, emqx_gc:reset_conn_gc_count(#wsclient_state.force_gc_count, emit_stats(State))}. diff --git a/src/gen_server2.erl b/src/gen_server2.erl deleted file mode 100644 index d5127f8d6..000000000 --- a/src/gen_server2.erl +++ /dev/null @@ -1,1361 +0,0 @@ -%% This file is a copy of gen_server.erl from the R13B-1 Erlang/OTP -%% distribution, with the following modifications: -%% -%% 1) the module name is gen_server2 -%% -%% 2) more efficient handling of selective receives in callbacks -%% gen_server2 processes drain their message queue into an internal -%% buffer before invoking any callback module functions. Messages are -%% dequeued from the buffer for processing. Thus the effective message -%% queue of a gen_server2 process is the concatenation of the internal -%% buffer and the real message queue. -%% As a result of the draining, any selective receive invoked inside a -%% callback is less likely to have to scan a large message queue. -%% -%% 3) gen_server2:cast is guaranteed to be order-preserving -%% The original code could reorder messages when communicating with a -%% process on a remote node that was not currently connected. -%% -%% 4) The callback module can optionally implement prioritise_call/4, -%% prioritise_cast/3 and prioritise_info/3. These functions take -%% Message, From, Length and State or just Message, Length and State -%% (where Length is the current number of messages waiting to be -%% processed) and return a single integer representing the priority -%% attached to the message, or 'drop' to ignore it (for -%% prioritise_cast/3 and prioritise_info/3 only). Messages with -%% higher priorities are processed before requests with lower -%% priorities. The default priority is 0. -%% -%% 5) The callback module can optionally implement -%% handle_pre_hibernate/1 and handle_post_hibernate/1. These will be -%% called immediately prior to and post hibernation, respectively. If -%% handle_pre_hibernate returns {hibernate, NewState} then the process -%% will hibernate. If the module does not implement -%% handle_pre_hibernate/1 then the default action is to hibernate. -%% -%% 6) init can return a 4th arg, {backoff, InitialTimeout, -%% MinimumTimeout, DesiredHibernatePeriod} (all in milliseconds, -%% 'infinity' does not make sense here). Then, on all callbacks which -%% can return a timeout (including init), timeout can be -%% 'hibernate'. When this is the case, the current timeout value will -%% be used (initially, the InitialTimeout supplied from init). After -%% this timeout has occurred, hibernation will occur as normal. Upon -%% awaking, a new current timeout value will be calculated. -%% -%% The purpose is that the gen_server2 takes care of adjusting the -%% current timeout value such that the process will increase the -%% timeout value repeatedly if it is unable to sleep for the -%% DesiredHibernatePeriod. If it is able to sleep for the -%% DesiredHibernatePeriod it will decrease the current timeout down to -%% the MinimumTimeout, so that the process is put to sleep sooner (and -%% hopefully stays asleep for longer). In short, should a process -%% using this receive a burst of messages, it should not hibernate -%% between those messages, but as the messages become less frequent, -%% the process will not only hibernate, it will do so sooner after -%% each message. -%% -%% When using this backoff mechanism, normal timeout values (i.e. not -%% 'hibernate') can still be used, and if they are used then the -%% handle_info(timeout, State) will be called as normal. In this case, -%% returning 'hibernate' from handle_info(timeout, State) will not -%% hibernate the process immediately, as it would if backoff wasn't -%% being used. Instead it'll wait for the current timeout as described -%% above. -%% -%% 7) The callback module can return from any of the handle_* -%% functions, a {become, Module, State} triple, or a {become, Module, -%% State, Timeout} quadruple. This allows the gen_server to -%% dynamically change the callback module. The State is the new state -%% which will be passed into any of the callback functions in the new -%% module. Note there is no form also encompassing a reply, thus if -%% you wish to reply in handle_call/3 and change the callback module, -%% you need to use gen_server2:reply/2 to issue the reply -%% manually. The init function can similarly return a 5th argument, -%% Module, in order to dynamically decide the callback module on init. -%% -%% 8) The callback module can optionally implement -%% format_message_queue/2 which is the equivalent of format_status/2 -%% but where the second argument is specifically the priority_queue -%% which contains the prioritised message_queue. -%% -%% 9) The function with_state/2 can be used to debug a process with -%% heavyweight state (without needing to copy the entire state out of -%% process as sys:get_status/1 would). Pass through a function which -%% can be invoked on the state, get back the result. The state is not -%% modified. -%% -%% 10) an mcall/1 function has been added for performing multiple -%% call/3 in parallel. Unlike multi_call, which sends the same request -%% to same-named processes residing on a supplied list of nodes, it -%% operates on name/request pairs, where name is anything accepted by -%% call/3, i.e. a pid, global name, local name, or local name on a -%% particular node. -%% - -%% All modifications are (C) 2009-2013 GoPivotal, Inc. - -%% ``The contents of this file are subject to the Erlang Public License, -%% Version 1.1, (the "License"); you may not use this file except in -%% compliance with the License. You should have received a copy of the -%% Erlang Public License along with this software. If not, it can be -%% retrieved via the world wide web at http://www.erlang.org/. -%% -%% 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 Initial Developer of the Original Code is Ericsson Utvecklings AB. -%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings -%% AB. All Rights Reserved.'' -%% -%% $Id$ -%% --module(gen_server2). - -%%% --------------------------------------------------- -%%% -%%% The idea behind THIS server is that the user module -%%% provides (different) functions to handle different -%%% kind of inputs. -%%% If the Parent process terminates the Module:terminate/2 -%%% function is called. -%%% -%%% The user module should export: -%%% -%%% init(Args) -%%% ==> {ok, State} -%%% {ok, State, Timeout} -%%% {ok, State, Timeout, Backoff} -%%% {ok, State, Timeout, Backoff, Module} -%%% ignore -%%% {stop, Reason} -%%% -%%% handle_call(Msg, {From, Tag}, State) -%%% -%%% ==> {reply, Reply, State} -%%% {reply, Reply, State, Timeout} -%%% {noreply, State} -%%% {noreply, State, Timeout} -%%% {stop, Reason, Reply, State} -%%% Reason = normal | shutdown | Term terminate(State) is called -%%% -%%% handle_cast(Msg, State) -%%% -%%% ==> {noreply, State} -%%% {noreply, State, Timeout} -%%% {stop, Reason, State} -%%% Reason = normal | shutdown | Term terminate(State) is called -%%% -%%% handle_info(Info, State) Info is e.g. {'EXIT', P, R}, {nodedown, N}, ... -%%% -%%% ==> {noreply, State} -%%% {noreply, State, Timeout} -%%% {stop, Reason, State} -%%% Reason = normal | shutdown | Term, terminate(State) is called -%%% -%%% terminate(Reason, State) Let the user module clean up -%%% Reason = normal | shutdown | {shutdown, Term} | Term -%%% always called when server terminates -%%% -%%% ==> ok | Term -%%% -%%% handle_pre_hibernate(State) -%%% -%%% ==> {hibernate, State} -%%% {stop, Reason, State} -%%% Reason = normal | shutdown | Term, terminate(State) is called -%%% -%%% handle_post_hibernate(State) -%%% -%%% ==> {noreply, State} -%%% {stop, Reason, State} -%%% Reason = normal | shutdown | Term, terminate(State) is called -%%% -%%% The work flow (of the server) can be described as follows: -%%% -%%% User module Generic -%%% ----------- ------- -%%% start -----> start -%%% init <----- . -%%% -%%% loop -%%% handle_call <----- . -%%% -----> reply -%%% -%%% handle_cast <----- . -%%% -%%% handle_info <----- . -%%% -%%% terminate <----- . -%%% -%%% -----> reply -%%% -%%% -%%% --------------------------------------------------- - -%% API --export([start/3, start/4, - start_link/3, start_link/4, - call/2, call/3, - cast/2, reply/2, - abcast/2, abcast/3, - multi_call/2, multi_call/3, multi_call/4, - mcall/1, - with_state/2, - enter_loop/3, enter_loop/4, enter_loop/5, enter_loop/6, wake_hib/1]). - -%% System exports --export([system_continue/3, - system_terminate/4, - system_code_change/4, - format_status/2]). - -%% Internal exports --export([init_it/6]). - --import(error_logger, [format/2]). - -%% State record --record(gs2_state, {parent, name, state, mod, time, - timeout_state, queue, debug, prioritisers}). - --ifdef(use_specs). - -%%%========================================================================= -%%% Specs. These exist only to shut up dialyzer's warnings -%%%========================================================================= - --type(gs2_state() :: #gs2_state{}). - --spec(handle_common_termination/3 :: - (any(), atom(), gs2_state()) -> no_return()). --spec(hibernate/1 :: (gs2_state()) -> no_return()). --spec(pre_hibernate/1 :: (gs2_state()) -> no_return()). --spec(system_terminate/4 :: (_, _, _, gs2_state()) -> no_return()). - --type(millis() :: non_neg_integer()). - -%%%========================================================================= -%%% API -%%%========================================================================= - --callback init(Args :: term()) -> - {ok, State :: term()} | - {ok, State :: term(), timeout() | hibernate} | - {ok, State :: term(), timeout() | hibernate, - {backoff, millis(), millis(), millis()}} | - {ok, State :: term(), timeout() | hibernate, - {backoff, millis(), millis(), millis()}, atom()} | - ignore | - {stop, Reason :: term()}. --callback handle_call(Request :: term(), From :: {pid(), Tag :: term()}, - State :: term()) -> - {reply, Reply :: term(), NewState :: term()} | - {reply, Reply :: term(), NewState :: term(), timeout() | hibernate} | - {noreply, NewState :: term()} | - {noreply, NewState :: term(), timeout() | hibernate} | - {stop, Reason :: term(), - Reply :: term(), NewState :: term()}. --callback handle_cast(Request :: term(), State :: term()) -> - {noreply, NewState :: term()} | - {noreply, NewState :: term(), timeout() | hibernate} | - {stop, Reason :: term(), NewState :: term()}. --callback handle_info(Info :: term(), State :: term()) -> - {noreply, NewState :: term()} | - {noreply, NewState :: term(), timeout() | hibernate} | - {stop, Reason :: term(), NewState :: term()}. --callback terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), - State :: term()) -> - ok | term(). --callback code_change(OldVsn :: (term() | {down, term()}), State :: term(), - Extra :: term()) -> - {ok, NewState :: term()} | {error, Reason :: term()}. - -%% It's not possible to define "optional" -callbacks, so putting specs -%% for handle_pre_hibernate/1 and handle_post_hibernate/1 will result -%% in warnings (the same applied for the behaviour_info before). - --else. - --export([behaviour_info/1]). - -behaviour_info(callbacks) -> - [{init,1},{handle_call,3},{handle_cast,2},{handle_info,2}, - {terminate,2},{code_change,3}]; -behaviour_info(_Other) -> - undefined. - --endif. - -%%% ----------------------------------------------------------------- -%%% Starts a generic server. -%%% start(Mod, Args, Options) -%%% start(Name, Mod, Args, Options) -%%% start_link(Mod, Args, Options) -%%% start_link(Name, Mod, Args, Options) where: -%%% Name ::= {local, atom()} | {global, atom()} -%%% Mod ::= atom(), callback module implementing the 'real' server -%%% Args ::= term(), init arguments (to Mod:init/1) -%%% Options ::= [{timeout, Timeout} | {debug, [Flag]}] -%%% Flag ::= trace | log | {logfile, File} | statistics | debug -%%% (debug == log && statistics) -%%% Returns: {ok, Pid} | -%%% {error, {already_started, Pid}} | -%%% {error, Reason} -%%% ----------------------------------------------------------------- -start(Mod, Args, Options) -> - gen:start(?MODULE, nolink, Mod, Args, Options). - -start(Name, Mod, Args, Options) -> - gen:start(?MODULE, nolink, Name, Mod, Args, Options). - -start_link(Mod, Args, Options) -> - gen:start(?MODULE, link, Mod, Args, Options). - -start_link(Name, Mod, Args, Options) -> - gen:start(?MODULE, link, Name, Mod, Args, Options). - - -%% ----------------------------------------------------------------- -%% Make a call to a generic server. -%% If the server is located at another node, that node will -%% be monitored. -%% If the client is trapping exits and is linked server termination -%% is handled here (? Shall we do that here (or rely on timeouts) ?). -%% ----------------------------------------------------------------- -call(Name, Request) -> - case catch gen:call(Name, '$gen_call', Request) of - {ok,Res} -> - Res; - {'EXIT',Reason} -> - exit({Reason, {?MODULE, call, [Name, Request]}}) - end. - -call(Name, Request, Timeout) -> - case catch gen:call(Name, '$gen_call', Request, Timeout) of - {ok,Res} -> - Res; - {'EXIT',Reason} -> - exit({Reason, {?MODULE, call, [Name, Request, Timeout]}}) - end. - -%% ----------------------------------------------------------------- -%% Make a cast to a generic server. -%% ----------------------------------------------------------------- -cast({global,Name}, Request) -> - catch global:send(Name, cast_msg(Request)), - ok; -cast({Name,Node}=Dest, Request) when is_atom(Name), is_atom(Node) -> - do_cast(Dest, Request); -cast(Dest, Request) when is_atom(Dest) -> - do_cast(Dest, Request); -cast(Dest, Request) when is_pid(Dest) -> - do_cast(Dest, Request). - -do_cast(Dest, Request) -> - do_send(Dest, cast_msg(Request)), - ok. - -cast_msg(Request) -> {'$gen_cast',Request}. - -%% ----------------------------------------------------------------- -%% Send a reply to the client. -%% ----------------------------------------------------------------- -reply({To, Tag}, Reply) -> - catch To ! {Tag, Reply}. - -%% ----------------------------------------------------------------- -%% Asyncronous broadcast, returns nothing, it's just send'n pray -%% ----------------------------------------------------------------- -abcast(Name, Request) when is_atom(Name) -> - do_abcast([node() | nodes()], Name, cast_msg(Request)). - -abcast(Nodes, Name, Request) when is_list(Nodes), is_atom(Name) -> - do_abcast(Nodes, Name, cast_msg(Request)). - -do_abcast([Node|Nodes], Name, Msg) when is_atom(Node) -> - do_send({Name,Node},Msg), - do_abcast(Nodes, Name, Msg); -do_abcast([], _,_) -> abcast. - -%%% ----------------------------------------------------------------- -%%% Make a call to servers at several nodes. -%%% Returns: {[Replies],[BadNodes]} -%%% A Timeout can be given -%%% -%%% A middleman process is used in case late answers arrives after -%%% the timeout. If they would be allowed to glog the callers message -%%% queue, it would probably become confused. Late answers will -%%% now arrive to the terminated middleman and so be discarded. -%%% ----------------------------------------------------------------- -multi_call(Name, Req) - when is_atom(Name) -> - do_multi_call([node() | nodes()], Name, Req, infinity). - -multi_call(Nodes, Name, Req) - when is_list(Nodes), is_atom(Name) -> - do_multi_call(Nodes, Name, Req, infinity). - -multi_call(Nodes, Name, Req, infinity) -> - do_multi_call(Nodes, Name, Req, infinity); -multi_call(Nodes, Name, Req, Timeout) - when is_list(Nodes), is_atom(Name), is_integer(Timeout), Timeout >= 0 -> - do_multi_call(Nodes, Name, Req, Timeout). - -%%% ----------------------------------------------------------------- -%%% Make multiple calls to multiple servers, given pairs of servers -%%% and messages. -%%% Returns: {[{Dest, Reply}], [{Dest, Error}]} -%%% -%%% Dest can be pid() | RegName :: atom() | -%%% {Name :: atom(), Node :: atom()} | {global, Name :: atom()} -%%% -%%% A middleman process is used to avoid clogging up the callers -%%% message queue. -%%% ----------------------------------------------------------------- -mcall(CallSpecs) -> - Tag = make_ref(), - {_, MRef} = spawn_monitor( - fun() -> - Refs = lists:foldl( - fun ({Dest, _Request}=S, Dict) -> - dict:store(do_mcall(S), Dest, Dict) - end, dict:new(), CallSpecs), - collect_replies(Tag, Refs, [], []) - end), - receive - {'DOWN', MRef, _, _, {Tag, Result}} -> Result; - {'DOWN', MRef, _, _, Reason} -> exit(Reason) - end. - -do_mcall({{global,Name}=Dest, Request}) -> - %% whereis_name is simply an ets lookup, and is precisely what - %% global:send/2 does, yet we need a Ref to put in the call to the - %% server, so invoking whereis_name makes a lot more sense here. - case global:whereis_name(Name) of - Pid when is_pid(Pid) -> - MRef = erlang:monitor(process, Pid), - catch msend(Pid, MRef, Request), - MRef; - undefined -> - Ref = make_ref(), - self() ! {'DOWN', Ref, process, Dest, noproc}, - Ref - end; -do_mcall({{Name,Node}=Dest, Request}) when is_atom(Name), is_atom(Node) -> - {_Node, MRef} = start_monitor(Node, Name), %% NB: we don't handle R6 - catch msend(Dest, MRef, Request), - MRef; -do_mcall({Dest, Request}) when is_atom(Dest); is_pid(Dest) -> - MRef = erlang:monitor(process, Dest), - catch msend(Dest, MRef, Request), - MRef. - -msend(Dest, MRef, Request) -> - erlang:send(Dest, {'$gen_call', {self(), MRef}, Request}, [noconnect]). - -collect_replies(Tag, Refs, Replies, Errors) -> - case dict:size(Refs) of - 0 -> exit({Tag, {Replies, Errors}}); - _ -> receive - {MRef, Reply} -> - {Refs1, Replies1} = handle_call_result(MRef, Reply, - Refs, Replies), - collect_replies(Tag, Refs1, Replies1, Errors); - {'DOWN', MRef, _, _, Reason} -> - Reason1 = case Reason of - noconnection -> nodedown; - _ -> Reason - end, - {Refs1, Errors1} = handle_call_result(MRef, Reason1, - Refs, Errors), - collect_replies(Tag, Refs1, Replies, Errors1) - end - end. - -handle_call_result(MRef, Result, Refs, AccList) -> - %% we avoid the mailbox scanning cost of a call to erlang:demonitor/{1,2} - %% here, so we must cope with MRefs that we've already seen and erased - case dict:find(MRef, Refs) of - {ok, Pid} -> {dict:erase(MRef, Refs), [{Pid, Result}|AccList]}; - _ -> {Refs, AccList} - end. - -%% ----------------------------------------------------------------- -%% Apply a function to a generic server's state. -%% ----------------------------------------------------------------- -with_state(Name, Fun) -> - case catch gen:call(Name, '$with_state', Fun, infinity) of - {ok,Res} -> - Res; - {'EXIT',Reason} -> - exit({Reason, {?MODULE, with_state, [Name, Fun]}}) - end. - -%%----------------------------------------------------------------- -%% enter_loop(Mod, Options, State, , , ) ->_ -%% -%% Description: Makes an existing process into a gen_server. -%% The calling process will enter the gen_server receive -%% loop and become a gen_server process. -%% The process *must* have been started using one of the -%% start functions in proc_lib, see proc_lib(3). -%% The user is responsible for any initialization of the -%% process, including registering a name for it. -%%----------------------------------------------------------------- -enter_loop(Mod, Options, State) -> - enter_loop(Mod, Options, State, self(), infinity, undefined). - -enter_loop(Mod, Options, State, Backoff = {backoff, _, _ , _}) -> - enter_loop(Mod, Options, State, self(), infinity, Backoff); - -enter_loop(Mod, Options, State, ServerName = {_, _}) -> - enter_loop(Mod, Options, State, ServerName, infinity, undefined); - -enter_loop(Mod, Options, State, Timeout) -> - enter_loop(Mod, Options, State, self(), Timeout, undefined). - -enter_loop(Mod, Options, State, ServerName, Backoff = {backoff, _, _, _}) -> - enter_loop(Mod, Options, State, ServerName, infinity, Backoff); - -enter_loop(Mod, Options, State, ServerName, Timeout) -> - enter_loop(Mod, Options, State, ServerName, Timeout, undefined). - -enter_loop(Mod, Options, State, ServerName, Timeout, Backoff) -> - Name = get_proc_name(ServerName), - Parent = get_parent(), - Debug = debug_options(Name, Options), - Queue = priority_queue:new(), - Backoff1 = extend_backoff(Backoff), - loop(find_prioritisers( - #gs2_state { parent = Parent, name = Name, state = State, - mod = Mod, time = Timeout, timeout_state = Backoff1, - queue = Queue, debug = Debug })). - -%%%======================================================================== -%%% Gen-callback functions -%%%======================================================================== - -%%% --------------------------------------------------- -%%% Initiate the new process. -%%% Register the name using the Rfunc function -%%% Calls the Mod:init/Args function. -%%% Finally an acknowledge is sent to Parent and the main -%%% loop is entered. -%%% --------------------------------------------------- -init_it(Starter, self, Name, Mod, Args, Options) -> - init_it(Starter, self(), Name, Mod, Args, Options); -init_it(Starter, Parent, Name0, Mod, Args, Options) -> - Name = name(Name0), - Debug = debug_options(Name, Options), - Queue = priority_queue:new(), - GS2State = find_prioritisers( - #gs2_state { parent = Parent, - name = Name, - mod = Mod, - queue = Queue, - debug = Debug }), - case catch Mod:init(Args) of - {ok, State} -> - proc_lib:init_ack(Starter, {ok, self()}), - loop(GS2State #gs2_state { state = State, - time = infinity, - timeout_state = undefined }); - {ok, State, Timeout} -> - proc_lib:init_ack(Starter, {ok, self()}), - loop(GS2State #gs2_state { state = State, - time = Timeout, - timeout_state = undefined }); - {ok, State, Timeout, Backoff = {backoff, _, _, _}} -> - Backoff1 = extend_backoff(Backoff), - proc_lib:init_ack(Starter, {ok, self()}), - loop(GS2State #gs2_state { state = State, - time = Timeout, - timeout_state = Backoff1 }); - {ok, State, Timeout, Backoff = {backoff, _, _, _}, Mod1} -> - Backoff1 = extend_backoff(Backoff), - proc_lib:init_ack(Starter, {ok, self()}), - loop(find_prioritisers( - GS2State #gs2_state { mod = Mod1, - state = State, - time = Timeout, - timeout_state = Backoff1 })); - {stop, Reason} -> - %% For consistency, we must make sure that the - %% registered name (if any) is unregistered before - %% the parent process is notified about the failure. - %% (Otherwise, the parent process could get - %% an 'already_started' error if it immediately - %% tried starting the process again.) - unregister_name(Name0), - proc_lib:init_ack(Starter, {error, Reason}), - exit(Reason); - ignore -> - unregister_name(Name0), - proc_lib:init_ack(Starter, ignore), - exit(normal); - {'EXIT', Reason} -> - unregister_name(Name0), - proc_lib:init_ack(Starter, {error, Reason}), - exit(Reason); - Else -> - Error = {bad_return_value, Else}, - proc_lib:init_ack(Starter, {error, Error}), - exit(Error) - end. - -name({local,Name}) -> Name; -name({global,Name}) -> Name; -%% name(Pid) when is_pid(Pid) -> Pid; -%% when R12 goes away, drop the line beneath and uncomment the line above -name(Name) -> Name. - -unregister_name({local,Name}) -> - _ = (catch unregister(Name)); -unregister_name({global,Name}) -> - _ = global:unregister_name(Name); -unregister_name(Pid) when is_pid(Pid) -> - Pid; -%% Under R12 let's just ignore it, as we have a single term as Name. -%% On R13 it will never get here, as we get tuple with 'local/global' atom. -unregister_name(_Name) -> ok. - -extend_backoff(undefined) -> - undefined; -extend_backoff({backoff, InitialTimeout, MinimumTimeout, DesiredHibPeriod}) -> - {backoff, InitialTimeout, MinimumTimeout, DesiredHibPeriod, rand:seed(exsplus)}. - -%%%======================================================================== -%%% Internal functions -%%%======================================================================== -%%% --------------------------------------------------- -%%% The MAIN loop. -%%% --------------------------------------------------- -loop(GS2State = #gs2_state { time = hibernate, - timeout_state = undefined, - queue = Queue }) -> - case priority_queue:is_empty(Queue) of - true -> - pre_hibernate(GS2State); - false -> - process_next_msg(GS2State) - end; - -loop(GS2State) -> - process_next_msg(drain(GS2State)). - -drain(GS2State) -> - receive - Input -> drain(in(Input, GS2State)) - after 0 -> GS2State - end. - -process_next_msg(GS2State = #gs2_state { time = Time, - timeout_state = TimeoutState, - queue = Queue }) -> - case priority_queue:out(Queue) of - {{value, Msg}, Queue1} -> - process_msg(Msg, GS2State #gs2_state { queue = Queue1 }); - {empty, Queue1} -> - {Time1, HibOnTimeout} - = case {Time, TimeoutState} of - {hibernate, {backoff, Current, _Min, _Desired, _RSt}} -> - {Current, true}; - {hibernate, _} -> - %% wake_hib/7 will set Time to hibernate. If - %% we were woken and didn't receive a msg - %% then we will get here and need a sensible - %% value for Time1, otherwise we crash. - %% R13B1 always waits infinitely when waking - %% from hibernation, so that's what we do - %% here too. - {infinity, false}; - _ -> {Time, false} - end, - receive - Input -> - %% Time could be 'hibernate' here, so *don't* call loop - process_next_msg( - drain(in(Input, GS2State #gs2_state { queue = Queue1 }))) - after Time1 -> - case HibOnTimeout of - true -> - pre_hibernate( - GS2State #gs2_state { queue = Queue1 }); - false -> - process_msg(timeout, - GS2State #gs2_state { queue = Queue1 }) - end - end - end. - -wake_hib(GS2State = #gs2_state { timeout_state = TS }) -> - TimeoutState1 = case TS of - undefined -> - undefined; - {SleptAt, TimeoutState} -> - adjust_timeout_state(SleptAt, - erlang:monotonic_time(), - TimeoutState) - end, - post_hibernate( - drain(GS2State #gs2_state { timeout_state = TimeoutState1 })). - -hibernate(GS2State = #gs2_state { timeout_state = TimeoutState }) -> - TS = case TimeoutState of - undefined -> undefined; - {backoff, _, _, _, _} -> {erlang:monotonic_time(), - TimeoutState} - end, - proc_lib:hibernate(?MODULE, wake_hib, - [GS2State #gs2_state { timeout_state = TS }]). - -pre_hibernate(GS2State = #gs2_state { state = State, - mod = Mod }) -> - case erlang:function_exported(Mod, handle_pre_hibernate, 1) of - true -> - case catch Mod:handle_pre_hibernate(State) of - {hibernate, NState} -> - hibernate(GS2State #gs2_state { state = NState } ); - Reply -> - handle_common_termination(Reply, pre_hibernate, GS2State) - end; - false -> - hibernate(GS2State) - end. - -post_hibernate(GS2State = #gs2_state { state = State, - mod = Mod }) -> - case erlang:function_exported(Mod, handle_post_hibernate, 1) of - true -> - case catch Mod:handle_post_hibernate(State) of - {noreply, NState} -> - process_next_msg(GS2State #gs2_state { state = NState, - time = infinity }); - {noreply, NState, Time} -> - process_next_msg(GS2State #gs2_state { state = NState, - time = Time }); - Reply -> - handle_common_termination(Reply, post_hibernate, GS2State) - end; - false -> - %% use hibernate here, not infinity. This matches - %% R13B. The key is that we should be able to get through - %% to process_msg calling sys:handle_system_msg with Time - %% still set to hibernate, iff that msg is the very msg - %% that woke us up (or the first msg we receive after - %% waking up). - process_next_msg(GS2State #gs2_state { time = hibernate }) - end. - -adjust_timeout_state(SleptAt, AwokeAt, {backoff, CurrentTO, MinimumTO, - DesiredHibPeriod, RandomState}) -> - NapLengthMicros = erlang:convert_time_unit(AwokeAt - SleptAt, - native, micro_seconds), - CurrentMicros = CurrentTO * 1000, - MinimumMicros = MinimumTO * 1000, - DesiredHibMicros = DesiredHibPeriod * 1000, - GapBetweenMessagesMicros = NapLengthMicros + CurrentMicros, - Base = - %% If enough time has passed between the last two messages then we - %% should consider sleeping sooner. Otherwise stay awake longer. - case GapBetweenMessagesMicros > (MinimumMicros + DesiredHibMicros) of - true -> lists:max([MinimumTO, CurrentTO div 2]); - false -> CurrentTO - end, - {Extra, RandomState1} = rand:uniform_s(Base, RandomState), - CurrentTO1 = Base + Extra, - {backoff, CurrentTO1, MinimumTO, DesiredHibPeriod, RandomState1}. - -in({'$gen_cast', Msg} = Input, - GS2State = #gs2_state { prioritisers = {_, F, _} }) -> - in(Input, F(Msg, GS2State), GS2State); -in({'$gen_call', From, Msg} = Input, - GS2State = #gs2_state { prioritisers = {F, _, _} }) -> - in(Input, F(Msg, From, GS2State), GS2State); -in({'$with_state', _From, _Fun} = Input, GS2State) -> - in(Input, 0, GS2State); -in({'EXIT', Parent, _R} = Input, GS2State = #gs2_state { parent = Parent }) -> - in(Input, infinity, GS2State); -in({system, _From, _Req} = Input, GS2State) -> - in(Input, infinity, GS2State); -in(Input, GS2State = #gs2_state { prioritisers = {_, _, F} }) -> - in(Input, F(Input, GS2State), GS2State). - -in(_Input, drop, GS2State) -> - GS2State; - -in(Input, Priority, GS2State = #gs2_state { queue = Queue }) -> - GS2State # gs2_state { queue = priority_queue:in(Input, Priority, Queue) }. - -process_msg({system, From, Req}, - GS2State = #gs2_state { parent = Parent, debug = Debug }) -> - %% gen_server puts Hib on the end as the 7th arg, but that version - %% of the fun seems not to be documented so leaving out for now. - sys:handle_system_msg(Req, From, Parent, ?MODULE, Debug, GS2State); -process_msg({'$with_state', From, Fun}, - GS2State = #gs2_state{state = State}) -> - reply(From, catch Fun(State)), - loop(GS2State); -process_msg({'EXIT', Parent, Reason} = Msg, - GS2State = #gs2_state { parent = Parent }) -> - terminate(Reason, Msg, GS2State); -process_msg(Msg, GS2State = #gs2_state { debug = [] }) -> - handle_msg(Msg, GS2State); -process_msg(Msg, GS2State = #gs2_state { name = Name, debug = Debug }) -> - Debug1 = sys:handle_debug(Debug, fun print_event/3, Name, {in, Msg}), - handle_msg(Msg, GS2State #gs2_state { debug = Debug1 }). - -%%% --------------------------------------------------- -%%% Send/recive functions -%%% --------------------------------------------------- -do_send(Dest, Msg) -> - catch erlang:send(Dest, Msg). - -do_multi_call(Nodes, Name, Req, infinity) -> - Tag = make_ref(), - Monitors = send_nodes(Nodes, Name, Tag, Req), - rec_nodes(Tag, Monitors, Name, undefined); -do_multi_call(Nodes, Name, Req, Timeout) -> - Tag = make_ref(), - Caller = self(), - Receiver = - spawn( - fun () -> - %% Middleman process. Should be unsensitive to regular - %% exit signals. The sychronization is needed in case - %% the receiver would exit before the caller started - %% the monitor. - process_flag(trap_exit, true), - Mref = erlang:monitor(process, Caller), - receive - {Caller,Tag} -> - Monitors = send_nodes(Nodes, Name, Tag, Req), - TimerId = erlang:start_timer(Timeout, self(), ok), - Result = rec_nodes(Tag, Monitors, Name, TimerId), - exit({self(),Tag,Result}); - {'DOWN',Mref,_,_,_} -> - %% Caller died before sending us the go-ahead. - %% Give up silently. - exit(normal) - end - end), - Mref = erlang:monitor(process, Receiver), - Receiver ! {self(),Tag}, - receive - {'DOWN',Mref,_,_,{Receiver,Tag,Result}} -> - Result; - {'DOWN',Mref,_,_,Reason} -> - %% The middleman code failed. Or someone did - %% exit(_, kill) on the middleman process => Reason==killed - exit(Reason) - end. - -send_nodes(Nodes, Name, Tag, Req) -> - send_nodes(Nodes, Name, Tag, Req, []). - -send_nodes([Node|Tail], Name, Tag, Req, Monitors) - when is_atom(Node) -> - Monitor = start_monitor(Node, Name), - %% Handle non-existing names in rec_nodes. - catch {Name, Node} ! {'$gen_call', {self(), {Tag, Node}}, Req}, - send_nodes(Tail, Name, Tag, Req, [Monitor | Monitors]); -send_nodes([_Node|Tail], Name, Tag, Req, Monitors) -> - %% Skip non-atom Node - send_nodes(Tail, Name, Tag, Req, Monitors); -send_nodes([], _Name, _Tag, _Req, Monitors) -> - Monitors. - -%% Against old nodes: -%% If no reply has been delivered within 2 secs. (per node) check that -%% the server really exists and wait for ever for the answer. -%% -%% Against contemporary nodes: -%% Wait for reply, server 'DOWN', or timeout from TimerId. - -rec_nodes(Tag, Nodes, Name, TimerId) -> - rec_nodes(Tag, Nodes, Name, [], [], 2000, TimerId). - -rec_nodes(Tag, [{N,R}|Tail], Name, Badnodes, Replies, Time, TimerId ) -> - receive - {'DOWN', R, _, _, _} -> - rec_nodes(Tag, Tail, Name, [N|Badnodes], Replies, Time, TimerId); - {{Tag, N}, Reply} -> %% Tag is bound !!! - unmonitor(R), - rec_nodes(Tag, Tail, Name, Badnodes, - [{N,Reply}|Replies], Time, TimerId); - {timeout, TimerId, _} -> - unmonitor(R), - %% Collect all replies that already have arrived - rec_nodes_rest(Tag, Tail, Name, [N|Badnodes], Replies) - end; -rec_nodes(Tag, [N|Tail], Name, Badnodes, Replies, Time, TimerId) -> - %% R6 node - receive - {nodedown, N} -> - monitor_node(N, false), - rec_nodes(Tag, Tail, Name, [N|Badnodes], Replies, 2000, TimerId); - {{Tag, N}, Reply} -> %% Tag is bound !!! - receive {nodedown, N} -> ok after 0 -> ok end, - monitor_node(N, false), - rec_nodes(Tag, Tail, Name, Badnodes, - [{N,Reply}|Replies], 2000, TimerId); - {timeout, TimerId, _} -> - receive {nodedown, N} -> ok after 0 -> ok end, - monitor_node(N, false), - %% Collect all replies that already have arrived - rec_nodes_rest(Tag, Tail, Name, [N | Badnodes], Replies) - after Time -> - case rpc:call(N, erlang, whereis, [Name]) of - Pid when is_pid(Pid) -> % It exists try again. - rec_nodes(Tag, [N|Tail], Name, Badnodes, - Replies, infinity, TimerId); - _ -> % badnode - receive {nodedown, N} -> ok after 0 -> ok end, - monitor_node(N, false), - rec_nodes(Tag, Tail, Name, [N|Badnodes], - Replies, 2000, TimerId) - end - end; -rec_nodes(_, [], _, Badnodes, Replies, _, TimerId) -> - case catch erlang:cancel_timer(TimerId) of - false -> % It has already sent it's message - receive - {timeout, TimerId, _} -> ok - after 0 -> - ok - end; - _ -> % Timer was cancelled, or TimerId was 'undefined' - ok - end, - {Replies, Badnodes}. - -%% Collect all replies that already have arrived -rec_nodes_rest(Tag, [{N,R}|Tail], Name, Badnodes, Replies) -> - receive - {'DOWN', R, _, _, _} -> - rec_nodes_rest(Tag, Tail, Name, [N|Badnodes], Replies); - {{Tag, N}, Reply} -> %% Tag is bound !!! - unmonitor(R), - rec_nodes_rest(Tag, Tail, Name, Badnodes, [{N,Reply}|Replies]) - after 0 -> - unmonitor(R), - rec_nodes_rest(Tag, Tail, Name, [N|Badnodes], Replies) - end; -rec_nodes_rest(Tag, [N|Tail], Name, Badnodes, Replies) -> - %% R6 node - receive - {nodedown, N} -> - monitor_node(N, false), - rec_nodes_rest(Tag, Tail, Name, [N|Badnodes], Replies); - {{Tag, N}, Reply} -> %% Tag is bound !!! - receive {nodedown, N} -> ok after 0 -> ok end, - monitor_node(N, false), - rec_nodes_rest(Tag, Tail, Name, Badnodes, [{N,Reply}|Replies]) - after 0 -> - receive {nodedown, N} -> ok after 0 -> ok end, - monitor_node(N, false), - rec_nodes_rest(Tag, Tail, Name, [N|Badnodes], Replies) - end; -rec_nodes_rest(_Tag, [], _Name, Badnodes, Replies) -> - {Replies, Badnodes}. - - -%%% --------------------------------------------------- -%%% Monitor functions -%%% --------------------------------------------------- - -start_monitor(Node, Name) when is_atom(Node), is_atom(Name) -> - if node() =:= nonode@nohost, Node =/= nonode@nohost -> - Ref = make_ref(), - self() ! {'DOWN', Ref, process, {Name, Node}, noconnection}, - {Node, Ref}; - true -> - case catch erlang:monitor(process, {Name, Node}) of - {'EXIT', _} -> - %% Remote node is R6 - monitor_node(Node, true), - Node; - Ref when is_reference(Ref) -> - {Node, Ref} - end - end. - -%% Cancels a monitor started with Ref=erlang:monitor(_, _). -unmonitor(Ref) when is_reference(Ref) -> - erlang:demonitor(Ref), - receive - {'DOWN', Ref, _, _, _} -> - true - after 0 -> - true - end. - -%%% --------------------------------------------------- -%%% Message handling functions -%%% --------------------------------------------------- - -dispatch({'$gen_cast', Msg}, Mod, State) -> - Mod:handle_cast(Msg, State); -dispatch(Info, Mod, State) -> - Mod:handle_info(Info, State). - -common_reply(_Name, From, Reply, _NState, [] = _Debug) -> - reply(From, Reply), - []; -common_reply(Name, {To, _Tag} = From, Reply, NState, Debug) -> - reply(From, Reply), - sys:handle_debug(Debug, fun print_event/3, Name, {out, Reply, To, NState}). - -common_noreply(_Name, _NState, [] = _Debug) -> - []; -common_noreply(Name, NState, Debug) -> - sys:handle_debug(Debug, fun print_event/3, Name, {noreply, NState}). - -common_become(_Name, _Mod, _NState, [] = _Debug) -> - []; -common_become(Name, Mod, NState, Debug) -> - sys:handle_debug(Debug, fun print_event/3, Name, {become, Mod, NState}). - -handle_msg({'$gen_call', From, Msg}, GS2State = #gs2_state { mod = Mod, - state = State, - name = Name, - debug = Debug }) -> - case catch Mod:handle_call(Msg, From, State) of - {reply, Reply, NState} -> - Debug1 = common_reply(Name, From, Reply, NState, Debug), - loop(GS2State #gs2_state { state = NState, - time = infinity, - debug = Debug1 }); - {reply, Reply, NState, Time1} -> - Debug1 = common_reply(Name, From, Reply, NState, Debug), - loop(GS2State #gs2_state { state = NState, - time = Time1, - debug = Debug1}); - {stop, Reason, Reply, NState} -> - {'EXIT', R} = - (catch terminate(Reason, Msg, - GS2State #gs2_state { state = NState })), - common_reply(Name, From, Reply, NState, Debug), - exit(R); - Other -> - handle_common_reply(Other, Msg, GS2State) - end; -handle_msg(Msg, GS2State = #gs2_state { mod = Mod, state = State }) -> - Reply = (catch dispatch(Msg, Mod, State)), - handle_common_reply(Reply, Msg, GS2State). - -handle_common_reply(Reply, Msg, GS2State = #gs2_state { name = Name, - debug = Debug}) -> - case Reply of - {noreply, NState} -> - Debug1 = common_noreply(Name, NState, Debug), - loop(GS2State #gs2_state {state = NState, - time = infinity, - debug = Debug1}); - {noreply, NState, Time1} -> - Debug1 = common_noreply(Name, NState, Debug), - loop(GS2State #gs2_state {state = NState, - time = Time1, - debug = Debug1}); - {become, Mod, NState} -> - Debug1 = common_become(Name, Mod, NState, Debug), - loop(find_prioritisers( - GS2State #gs2_state { mod = Mod, - state = NState, - time = infinity, - debug = Debug1 })); - {become, Mod, NState, Time1} -> - Debug1 = common_become(Name, Mod, NState, Debug), - loop(find_prioritisers( - GS2State #gs2_state { mod = Mod, - state = NState, - time = Time1, - debug = Debug1 })); - _ -> - handle_common_termination(Reply, Msg, GS2State) - end. - -handle_common_termination(Reply, Msg, GS2State) -> - case Reply of - {stop, Reason, NState} -> - terminate(Reason, Msg, GS2State #gs2_state { state = NState }); - {'EXIT', What} -> - terminate(What, Msg, GS2State); - _ -> - terminate({bad_return_value, Reply}, Msg, GS2State) - end. - -%%----------------------------------------------------------------- -%% Callback functions for system messages handling. -%%----------------------------------------------------------------- -system_continue(Parent, Debug, GS2State) -> - loop(GS2State #gs2_state { parent = Parent, debug = Debug }). - -system_terminate(Reason, _Parent, Debug, GS2State) -> - terminate(Reason, [], GS2State #gs2_state { debug = Debug }). - -system_code_change(GS2State = #gs2_state { mod = Mod, - state = State }, - _Module, OldVsn, Extra) -> - case catch Mod:code_change(OldVsn, State, Extra) of - {ok, NewState} -> - NewGS2State = find_prioritisers( - GS2State #gs2_state { state = NewState }), - {ok, [NewGS2State]}; - Else -> - Else - end. - -%%----------------------------------------------------------------- -%% Format debug messages. Print them as the call-back module sees -%% them, not as the real erlang messages. Use trace for that. -%%----------------------------------------------------------------- -print_event(Dev, {in, Msg}, Name) -> - case Msg of - {'$gen_call', {From, _Tag}, Call} -> - io:format(Dev, "*DBG* ~p got call ~p from ~w~n", - [Name, Call, From]); - {'$gen_cast', Cast} -> - io:format(Dev, "*DBG* ~p got cast ~p~n", - [Name, Cast]); - _ -> - io:format(Dev, "*DBG* ~p got ~p~n", [Name, Msg]) - end; -print_event(Dev, {out, Msg, To, State}, Name) -> - io:format(Dev, "*DBG* ~p sent ~p to ~w, new state ~w~n", - [Name, Msg, To, State]); -print_event(Dev, {noreply, State}, Name) -> - io:format(Dev, "*DBG* ~p new state ~w~n", [Name, State]); -print_event(Dev, Event, Name) -> - io:format(Dev, "*DBG* ~p dbg ~p~n", [Name, Event]). - - -%%% --------------------------------------------------- -%%% Terminate the server. -%%% --------------------------------------------------- - -terminate(Reason, Msg, #gs2_state { name = Name, - mod = Mod, - state = State, - debug = Debug }) -> - case catch Mod:terminate(Reason, State) of - {'EXIT', R} -> - error_info(R, Reason, Name, Msg, State, Debug), - exit(R); - _ -> - case Reason of - normal -> - exit(normal); - shutdown -> - exit(shutdown); - {shutdown,_}=Shutdown -> - exit(Shutdown); - _ -> - error_info(Reason, undefined, Name, Msg, State, Debug), - exit(Reason) - end - end. - -error_info(_Reason, _RootCause, application_controller, _Msg, _State, _Debug) -> - %% OTP-5811 Don't send an error report if it's the system process - %% application_controller which is terminating - let init take care - %% of it instead - ok; -error_info(Reason, RootCause, Name, Msg, State, Debug) -> - Reason1 = error_reason(Reason), - Fmt = - "** Generic server ~p terminating~n" - "** Last message in was ~p~n" - "** When Server state == ~p~n" - "** Reason for termination == ~n** ~p~n", - case RootCause of - undefined -> format(Fmt, [Name, Msg, State, Reason1]); - _ -> format(Fmt ++ "** In 'terminate' callback " - "with reason ==~n** ~p~n", - [Name, Msg, State, Reason1, - error_reason(RootCause)]) - end, - sys:print_log(Debug), - ok. - -error_reason({undef,[{M,F,A}|MFAs]} = Reason) -> - case code:is_loaded(M) of - false -> {'module could not be loaded',[{M,F,A}|MFAs]}; - _ -> case erlang:function_exported(M, F, length(A)) of - true -> Reason; - false -> {'function not exported',[{M,F,A}|MFAs]} - end - end; -error_reason(Reason) -> - Reason. - -%%% --------------------------------------------------- -%%% Misc. functions. -%%% --------------------------------------------------- - -opt(Op, [{Op, Value}|_]) -> - {ok, Value}; -opt(Op, [_|Options]) -> - opt(Op, Options); -opt(_, []) -> - false. - -debug_options(Name, Opts) -> - case opt(debug, Opts) of - {ok, Options} -> dbg_options(Name, Options); - _ -> dbg_options(Name, []) - end. - -dbg_options(Name, []) -> - Opts = - case init:get_argument(generic_debug) of - error -> - []; - _ -> - [log, statistics] - end, - dbg_opts(Name, Opts); -dbg_options(Name, Opts) -> - dbg_opts(Name, Opts). - -dbg_opts(Name, Opts) -> - case catch sys:debug_options(Opts) of - {'EXIT',_} -> - format("~p: ignoring erroneous debug options - ~p~n", - [Name, Opts]), - []; - Dbg -> - Dbg - end. - -get_proc_name(Pid) when is_pid(Pid) -> - Pid; -get_proc_name({local, Name}) -> - case process_info(self(), registered_name) of - {registered_name, Name} -> - Name; - {registered_name, _Name} -> - exit(process_not_registered); - [] -> - exit(process_not_registered) - end; -get_proc_name({global, Name}) -> - case whereis_name(Name) of - undefined -> - exit(process_not_registered_globally); - Pid when Pid =:= self() -> - Name; - _Pid -> - exit(process_not_registered_globally) - end. - -get_parent() -> - case get('$ancestors') of - [Parent | _] when is_pid(Parent)-> - Parent; - [Parent | _] when is_atom(Parent)-> - name_to_pid(Parent); - _ -> - exit(process_was_not_started_by_proc_lib) - end. - -name_to_pid(Name) -> - case whereis(Name) of - undefined -> - case whereis_name(Name) of - undefined -> - exit(could_not_find_registerd_name); - Pid -> - Pid - end; - Pid -> - Pid - end. - -whereis_name(Name) -> - case ets:lookup(global_names, Name) of - [{_Name, Pid, _Method, _RPid, _Ref}] -> - if node(Pid) == node() -> - case is_process_alive(Pid) of - true -> Pid; - false -> undefined - end; - true -> - Pid - end; - [] -> undefined - end. - -find_prioritisers(GS2State = #gs2_state { mod = Mod }) -> - PCall = function_exported_or_default(Mod, 'prioritise_call', 4, - fun (_Msg, _From, _State) -> 0 end), - PCast = function_exported_or_default(Mod, 'prioritise_cast', 3, - fun (_Msg, _State) -> 0 end), - PInfo = function_exported_or_default(Mod, 'prioritise_info', 3, - fun (_Msg, _State) -> 0 end), - GS2State #gs2_state { prioritisers = {PCall, PCast, PInfo} }. - -function_exported_or_default(Mod, Fun, Arity, Default) -> - case erlang:function_exported(Mod, Fun, Arity) of - true -> case Arity of - 3 -> fun (Msg, GS2State = #gs2_state { queue = Queue, - state = State }) -> - Length = priority_queue:len(Queue), - case catch Mod:Fun(Msg, Length, State) of - drop -> - drop; - Res when is_integer(Res) -> - Res; - Err -> - handle_common_termination(Err, Msg, GS2State) - end - end; - 4 -> fun (Msg, From, GS2State = #gs2_state { queue = Queue, - state = State }) -> - Length = priority_queue:len(Queue), - case catch Mod:Fun(Msg, From, Length, State) of - Res when is_integer(Res) -> - Res; - Err -> - handle_common_termination(Err, Msg, GS2State) - end - end - end; - false -> Default - end. - -%%----------------------------------------------------------------- -%% Status information -%%----------------------------------------------------------------- -format_status(Opt, StatusData) -> - [PDict, SysState, Parent, Debug, - #gs2_state{name = Name, state = State, mod = Mod, queue = Queue}] = - StatusData, - NameTag = if is_pid(Name) -> - pid_to_list(Name); - is_atom(Name) -> - Name - end, - Header = lists:concat(["Status for generic server ", NameTag]), - Log = sys:get_debug(log, Debug, []), - Specfic = callback(Mod, format_status, [Opt, [PDict, State]], - fun () -> [{data, [{"State", State}]}] end), - Messages = callback(Mod, format_message_queue, [Opt, Queue], - fun () -> priority_queue:to_list(Queue) end), - [{header, Header}, - {data, [{"Status", SysState}, - {"Parent", Parent}, - {"Logged events", Log}, - {"Queued messages", Messages}]} | - Specfic]. - -callback(Mod, FunName, Args, DefaultThunk) -> - case erlang:function_exported(Mod, FunName, length(Args)) of - true -> case catch apply(Mod, FunName, Args) of - {'EXIT', _} -> DefaultThunk(); - Success -> Success - end; - false -> DefaultThunk() - end. diff --git a/src/priority_queue.erl b/src/priority_queue.erl deleted file mode 100644 index 7147b0bb4..000000000 --- a/src/priority_queue.erl +++ /dev/null @@ -1,259 +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. -%% -%% The Initial Developer of the Original Code is GoPivotal, Inc. -%% Copyright (c) 2007-2015 Pivotal Software, Inc. All rights reserved. -%% - -%% Priority queues have essentially the same interface as ordinary -%% queues, except that a) there is an in/3 that takes a priority, and -%% b) we have only implemented the core API we need. -%% -%% Priorities should be integers - the higher the value the higher the -%% priority - but we don't actually check that. -%% -%% in/2 inserts items with priority 0. -%% -%% We optimise the case where a priority queue is being used just like -%% an ordinary queue. When that is the case we represent the priority -%% queue as an ordinary queue. We could just call into the 'queue' -%% module for that, but for efficiency we implement the relevant -%% functions directly in here, thus saving on inter-module calls and -%% eliminating a level of boxing. -%% -%% When the queue contains items with non-zero priorities, it is -%% represented as a sorted kv list with the inverted Priority as the -%% key and an ordinary queue as the value. Here again we use our own -%% ordinary queue implemention for efficiency, often making recursive -%% calls into the same function knowing that ordinary queues represent -%% a base case. - --module(priority_queue). - --export([new/0, is_queue/1, is_empty/1, len/1, plen/2, to_list/1, from_list/1, - in/2, in/3, out/1, out/2, out_p/1, join/2, filter/2, fold/3, highest/1]). - -%%---------------------------------------------------------------------------- - --ifdef(use_specs). - --type(q() :: pqueue()). --type(priority() :: integer() | 'infinity'). --type(squeue() :: {queue, [any()], [any()], non_neg_integer()}). --type(pqueue() :: squeue() | {pqueue, [{priority(), squeue()}]}). - --export_type([q/0]). - --spec(new/0 :: () -> pqueue()). --spec(is_queue/1 :: (any()) -> boolean()). --spec(is_empty/1 :: (pqueue()) -> boolean()). --spec(len/1 :: (pqueue()) -> non_neg_integer()). --spec(plen/2 :: (priority(), pqueue()) -> non_neg_integer()). --spec(to_list/1 :: (pqueue()) -> [{priority(), any()}]). --spec(from_list/1 :: ([{priority(), any()}]) -> pqueue()). --spec(in/2 :: (any(), pqueue()) -> pqueue()). --spec(in/3 :: (any(), priority(), pqueue()) -> pqueue()). --spec(out/1 :: (pqueue()) -> {empty | {value, any()}, pqueue()}). --spec(out_p/1 :: (pqueue()) -> {empty | {value, any(), priority()}, pqueue()}). --spec(join/2 :: (pqueue(), pqueue()) -> pqueue()). --spec(filter/2 :: (fun ((any()) -> boolean()), pqueue()) -> pqueue()). --spec(fold/3 :: - (fun ((any(), priority(), A) -> A), A, pqueue()) -> A). --spec(highest/1 :: (pqueue()) -> priority() | 'empty'). - --endif. - -%%---------------------------------------------------------------------------- - -new() -> - {queue, [], [], 0}. - -is_queue({queue, R, F, L}) when is_list(R), is_list(F), is_integer(L) -> - true; -is_queue({pqueue, Queues}) when is_list(Queues) -> - lists:all(fun ({infinity, Q}) -> is_queue(Q); - ({P, Q}) -> is_integer(P) andalso is_queue(Q) - end, Queues); -is_queue(_) -> - false. - -is_empty({queue, [], [], 0}) -> - true; -is_empty(_) -> - false. - -len({queue, _R, _F, L}) -> - L; -len({pqueue, Queues}) -> - lists:sum([len(Q) || {_, Q} <- Queues]). - -plen(0, {queue, _R, _F, L}) -> - L; -plen(_, {queue, _R, _F, _}) -> - 0; -plen(P, {pqueue, Queues}) -> - case lists:keysearch(maybe_negate_priority(P), 1, Queues) of - {value, {_, Q}} -> len(Q); - false -> 0 - end. - -to_list({queue, In, Out, _Len}) when is_list(In), is_list(Out) -> - [{0, V} || V <- Out ++ lists:reverse(In, [])]; -to_list({pqueue, Queues}) -> - [{maybe_negate_priority(P), V} || {P, Q} <- Queues, - {0, V} <- to_list(Q)]. - -from_list(L) -> - lists:foldl(fun ({P, E}, Q) -> in(E, P, Q) end, new(), L). - -in(Item, Q) -> - in(Item, 0, Q). - -in(X, 0, {queue, [_] = In, [], 1}) -> - {queue, [X], In, 2}; -in(X, 0, {queue, In, Out, Len}) when is_list(In), is_list(Out) -> - {queue, [X|In], Out, Len + 1}; -in(X, Priority, _Q = {queue, [], [], 0}) -> - in(X, Priority, {pqueue, []}); -in(X, Priority, Q = {queue, _, _, _}) -> - in(X, Priority, {pqueue, [{0, Q}]}); -in(X, Priority, {pqueue, Queues}) -> - P = maybe_negate_priority(Priority), - {pqueue, case lists:keysearch(P, 1, Queues) of - {value, {_, Q}} -> - lists:keyreplace(P, 1, Queues, {P, in(X, Q)}); - false when P == infinity -> - [{P, {queue, [X], [], 1}} | Queues]; - false -> - case Queues of - [{infinity, InfQueue} | Queues1] -> - [{infinity, InfQueue} | - lists:keysort(1, [{P, {queue, [X], [], 1}} | Queues1])]; - _ -> - lists:keysort(1, [{P, {queue, [X], [], 1}} | Queues]) - end - end}. - -out({queue, [], [], 0} = Q) -> - {empty, Q}; -out({queue, [V], [], 1}) -> - {{value, V}, {queue, [], [], 0}}; -out({queue, [Y|In], [], Len}) -> - [V|Out] = lists:reverse(In, []), - {{value, V}, {queue, [Y], Out, Len - 1}}; -out({queue, In, [V], Len}) when is_list(In) -> - {{value,V}, r2f(In, Len - 1)}; -out({queue, In,[V|Out], Len}) when is_list(In) -> - {{value, V}, {queue, In, Out, Len - 1}}; -out({pqueue, [{P, Q} | Queues]}) -> - {R, Q1} = out(Q), - NewQ = case is_empty(Q1) of - true -> case Queues of - [] -> {queue, [], [], 0}; - [{0, OnlyQ}] -> OnlyQ; - [_|_] -> {pqueue, Queues} - end; - false -> {pqueue, [{P, Q1} | Queues]} - end, - {R, NewQ}. - -out_p({queue, _, _, _} = Q) -> add_p(out(Q), 0); -out_p({pqueue, [{P, _} | _]} = Q) -> add_p(out(Q), maybe_negate_priority(P)). - -out(0, {queue, _, _, _} = Q) -> - out(Q); -out(Priority, {queue, _, _, _}) -> - erlang:error(badarg, [Priority]); -out(Priority, {pqueue, Queues}) -> - P = maybe_negate_priority(Priority), - case lists:keysearch(P, 1, Queues) of - {value, {_, Q}} -> - {R, Q1} = out(Q), - Queues1 = case is_empty(Q1) of - true -> lists:keydelete(P, 1, Queues); - false -> lists:keyreplace(P, 1, Queues, {P, Q1}) - end, - {R, case Queues1 of - [] -> {queue, [], [], 0}; - [{0, OnlyQ}] -> OnlyQ; - [_|_] -> {pqueue, Queues1} - end}; - false -> - {empty, {pqueue, Queues}} - end. - -add_p(R, P) -> case R of - {empty, Q} -> {empty, Q}; - {{value, V}, Q} -> {{value, V, P}, Q} - end. - -join(A, {queue, [], [], 0}) -> - A; -join({queue, [], [], 0}, B) -> - B; -join({queue, AIn, AOut, ALen}, {queue, BIn, BOut, BLen}) -> - {queue, BIn, AOut ++ lists:reverse(AIn, BOut), ALen + BLen}; -join(A = {queue, _, _, _}, {pqueue, BPQ}) -> - {Pre, Post} = - lists:splitwith(fun ({P, _}) -> P < 0 orelse P == infinity end, BPQ), - Post1 = case Post of - [] -> [ {0, A} ]; - [ {0, ZeroQueue} | Rest ] -> [ {0, join(A, ZeroQueue)} | Rest ]; - _ -> [ {0, A} | Post ] - end, - {pqueue, Pre ++ Post1}; -join({pqueue, APQ}, B = {queue, _, _, _}) -> - {Pre, Post} = - lists:splitwith(fun ({P, _}) -> P < 0 orelse P == infinity end, APQ), - Post1 = case Post of - [] -> [ {0, B} ]; - [ {0, ZeroQueue} | Rest ] -> [ {0, join(ZeroQueue, B)} | Rest ]; - _ -> [ {0, B} | Post ] - end, - {pqueue, Pre ++ Post1}; -join({pqueue, APQ}, {pqueue, BPQ}) -> - {pqueue, merge(APQ, BPQ, [])}. - -merge([], BPQ, Acc) -> - lists:reverse(Acc, BPQ); -merge(APQ, [], Acc) -> - lists:reverse(Acc, APQ); -merge([{P, A}|As], [{P, B}|Bs], Acc) -> - merge(As, Bs, [ {P, join(A, B)} | Acc ]); -merge([{PA, A}|As], Bs = [{PB, _}|_], Acc) when PA < PB orelse PA == infinity -> - merge(As, Bs, [ {PA, A} | Acc ]); -merge(As = [{_, _}|_], [{PB, B}|Bs], Acc) -> - merge(As, Bs, [ {PB, B} | Acc ]). - -filter(Pred, Q) -> fold(fun(V, P, Acc) -> - case Pred(V) of - true -> in(V, P, Acc); - false -> Acc - end - end, new(), Q). - -fold(Fun, Init, Q) -> case out_p(Q) of - {empty, _Q} -> Init; - {{value, V, P}, Q1} -> fold(Fun, Fun(V, P, Init), Q1) - end. - -highest({queue, [], [], 0}) -> empty; -highest({queue, _, _, _}) -> 0; -highest({pqueue, [{P, _} | _]}) -> maybe_negate_priority(P). - -r2f([], 0) -> {queue, [], [], 0}; -r2f([_] = R, 1) -> {queue, [], R, 1}; -r2f([X,Y], 2) -> {queue, [X], [Y], 2}; -r2f([X,Y|R], L) -> {queue, [X,Y], lists:reverse(R, []), L}. - -maybe_negate_priority(infinity) -> infinity; -maybe_negate_priority(P) -> -P. From f4fd6efe1605245be80019480412592d5dc18e5e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 26 Feb 2018 23:29:53 +0800 Subject: [PATCH 020/520] Merge the emqx-common, emqx-router libraries --- erlang.mk | 1203 ++++++++++++++++++++++----------- etc/emqx.conf | 49 +- include/emqx.hrl | 9 +- include/emqx_common.hrl | 13 + include/emqx_trie.hrl | 4 +- priv/emqx.schema | 75 +- src/emqx_app.erl | 2 + src/emqx_base62.erl | 4 +- src/emqx_boot.erl | 2 - src/emqx_gc.erl | 2 - src/emqx_keepalive.erl | 6 +- src/emqx_misc.erl | 4 +- src/emqx_mod_presence.erl | 73 ++ src/emqx_mod_rewrite.erl | 91 +++ src/emqx_mod_subscription.erl | 61 ++ src/emqx_modules.erl | 33 + src/emqx_net.erl | 2 - src/emqx_pmon.erl | 4 +- src/emqx_router.erl | 100 ++- src/emqx_router_sup.erl | 38 ++ src/emqx_time.erl | 4 +- src/emqx_topic.erl | 14 +- src/emqx_trie.erl | 38 +- test/emqx_base62_SUITE.erl | 35 + test/emqx_guid_SUITE.erl | 41 ++ test/emqx_inflight_SUITE.erl | 2 - test/emqx_keepalive_SUITE.erl | 43 ++ test/emqx_misc_SUITE.erl | 46 ++ test/emqx_pqueue_SUITE.erl | 69 ++ test/emqx_router_SUITE.erl | 43 +- test/emqx_time_SUITE.erl | 28 + test/emqx_trie_SUITE.erl | 2 +- 32 files changed, 1621 insertions(+), 519 deletions(-) create mode 100644 include/emqx_common.hrl create mode 100644 src/emqx_mod_presence.erl create mode 100644 src/emqx_mod_rewrite.erl create mode 100644 src/emqx_mod_subscription.erl create mode 100644 src/emqx_modules.erl create mode 100644 src/emqx_router_sup.erl create mode 100644 test/emqx_base62_SUITE.erl create mode 100644 test/emqx_guid_SUITE.erl create mode 100644 test/emqx_keepalive_SUITE.erl create mode 100644 test/emqx_misc_SUITE.erl create mode 100644 test/emqx_pqueue_SUITE.erl create mode 100644 test/emqx_time_SUITE.erl diff --git a/erlang.mk b/erlang.mk index f38d22653..adf9d98b8 100644 --- a/erlang.mk +++ b/erlang.mk @@ -1,4 +1,4 @@ -# Copyright (c) 2013-2015, Loïc Hoguin +# Copyright (c) 2013-2016, Loïc Hoguin # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -12,11 +12,23 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -.PHONY: all app apps deps search rel docs install-docs check tests clean distclean help erlang-mk +.PHONY: all app apps deps search rel relup docs install-docs check tests clean distclean help erlang-mk ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST))) +export ERLANG_MK_FILENAME -ERLANG_MK_VERSION = 2.0.0-pre.2-130-gc6fe5ea +ERLANG_MK_VERSION = 2017.08.28-22-gf545564 +ERLANG_MK_WITHOUT = + +# Make 3.81 and 3.82 are deprecated. + +ifeq ($(MAKE_VERSION),3.81) +$(warning Please upgrade to GNU Make 4 or later: https://erlang.mk/guide/installation.html) +endif + +ifeq ($(MAKE_VERSION),3.82) +$(warning Please upgrade to GNU Make 4 or later: https://erlang.mk/guide/installation.html) +endif # Core configuration. @@ -25,6 +37,7 @@ PROJECT := $(strip $(PROJECT)) PROJECT_VERSION ?= rolling PROJECT_MOD ?= $(PROJECT)_app +PROJECT_ENV ?= [] # Verbosity. @@ -85,6 +98,8 @@ all:: deps app rel rel:: $(verbose) : +relup:: deps app + check:: tests clean:: clean-crashdump @@ -102,7 +117,7 @@ distclean-tmp: help:: $(verbose) printf "%s\n" \ "erlang.mk (version $(ERLANG_MK_VERSION)) is distributed under the terms of the ISC License." \ - "Copyright (c) 2013-2015 Loïc Hoguin " \ + "Copyright (c) 2013-2016 Loïc Hoguin " \ "" \ "Usage: [V=1] $(MAKE) [target]..." \ "" \ @@ -110,6 +125,8 @@ help:: " all Run deps, app and rel targets in that order" \ " app Compile the project" \ " deps Fetch dependencies (if needed) and compile them" \ + " fetch-deps Fetch dependencies recursively (if needed) without compiling them" \ + " list-deps List dependencies recursively on stdout" \ " search q=... Search for a package in the built-in index" \ " rel Build a release for this project, if applicable" \ " docs Build the documentation for this project" \ @@ -148,30 +165,7 @@ else core_native_path = $1 endif -ifeq ($(shell which wget 2>/dev/null | wc -l), 1) -define core_http_get - wget --no-check-certificate -O $(1) $(2)|| rm $(1) -endef -else -define core_http_get.erl - ssl:start(), - inets:start(), - case httpc:request(get, {"$(2)", []}, [{autoredirect, true}], []) of - {ok, {{_, 200, _}, _, Body}} -> - case file:write_file("$(1)", Body) of - ok -> ok; - {error, R1} -> halt(R1) - end; - {error, R2} -> - halt(R2) - end, - halt(0). -endef - -define core_http_get - $(call erlang,$(call core_http_get.erl,$(call core_native_path,$1),$2)) -endef -endif +core_http_get = curl -Lf$(if $(filter-out 0,$(V)),,s)o $(call core_native_path,$1) $2 core_eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))) @@ -191,17 +185,102 @@ ERLANG_MK_COMMIT ?= ERLANG_MK_BUILD_CONFIG ?= build.config ERLANG_MK_BUILD_DIR ?= .erlang.mk.build +erlang-mk: WITHOUT ?= $(ERLANG_MK_WITHOUT) erlang-mk: git clone $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR) ifdef ERLANG_MK_COMMIT cd $(ERLANG_MK_BUILD_DIR) && git checkout $(ERLANG_MK_COMMIT) endif if [ -f $(ERLANG_MK_BUILD_CONFIG) ]; then cp $(ERLANG_MK_BUILD_CONFIG) $(ERLANG_MK_BUILD_DIR)/build.config; fi - $(MAKE) -C $(ERLANG_MK_BUILD_DIR) + $(MAKE) -C $(ERLANG_MK_BUILD_DIR) WITHOUT='$(strip $(WITHOUT))' cp $(ERLANG_MK_BUILD_DIR)/erlang.mk ./erlang.mk rm -rf $(ERLANG_MK_BUILD_DIR) -# Copyright (c) 2015, Loïc Hoguin +# The erlang.mk package index is bundled in the default erlang.mk build. +# Search for the string "copyright" to skip to the rest of the code. + +# Copyright (c) 2015-2017, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: distclean-kerl + +KERL_INSTALL_DIR ?= $(HOME)/erlang + +ifeq ($(strip $(KERL)),) +KERL := $(ERLANG_MK_TMP)/kerl/kerl +endif + +export KERL + +KERL_GIT ?= https://github.com/kerl/kerl +KERL_COMMIT ?= master + +KERL_MAKEFLAGS ?= + +OTP_GIT ?= https://github.com/erlang/otp + +define kerl_otp_target +ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(1)),) +$(KERL_INSTALL_DIR)/$(1): $(KERL) + MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $(1) $(1) + $(KERL) install $(1) $(KERL_INSTALL_DIR)/$(1) +endif +endef + +define kerl_hipe_target +ifeq ($(wildcard $(KERL_INSTALL_DIR)/$1-native),) +$(KERL_INSTALL_DIR)/$1-native: $(KERL) + KERL_CONFIGURE_OPTIONS=--enable-native-libs \ + MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $1 $1-native + $(KERL) install $1-native $(KERL_INSTALL_DIR)/$1-native +endif +endef + +$(KERL): + $(verbose) mkdir -p $(ERLANG_MK_TMP) + $(gen_verbose) git clone --depth 1 $(KERL_GIT) $(ERLANG_MK_TMP)/kerl + $(verbose) cd $(ERLANG_MK_TMP)/kerl && git checkout $(KERL_COMMIT) + $(verbose) chmod +x $(KERL) + +distclean:: distclean-kerl + +distclean-kerl: + $(gen_verbose) rm -rf $(KERL) + +# Allow users to select which version of Erlang/OTP to use for a project. + +ERLANG_OTP ?= +ERLANG_HIPE ?= + +# Use kerl to enforce a specific Erlang/OTP version for a project. +ifneq ($(strip $(ERLANG_OTP)),) +export PATH := $(KERL_INSTALL_DIR)/$(ERLANG_OTP)/bin:$(PATH) +SHELL := env PATH=$(PATH) $(SHELL) +$(eval $(call kerl_otp_target,$(ERLANG_OTP))) + +# Build Erlang/OTP only if it doesn't already exist. +ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(ERLANG_OTP))$(BUILD_ERLANG_OTP),) +$(info Building Erlang/OTP $(ERLANG_OTP)... Please wait...) +$(shell $(MAKE) $(KERL_INSTALL_DIR)/$(ERLANG_OTP) ERLANG_OTP=$(ERLANG_OTP) BUILD_ERLANG_OTP=1 >&2) +endif + +else +# Same for a HiPE enabled VM. +ifneq ($(strip $(ERLANG_HIPE)),) +export PATH := $(KERL_INSTALL_DIR)/$(ERLANG_HIPE)-native/bin:$(PATH) +SHELL := env PATH=$(PATH) $(SHELL) +$(eval $(call kerl_hipe_target,$(ERLANG_HIPE))) + +# Build Erlang/OTP only if it doesn't already exist. +ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(ERLANG_HIPE))$(BUILD_ERLANG_OTP),) +$(info Building HiPE-enabled Erlang/OTP $(ERLANG_OTP)... Please wait...) +$(shell $(MAKE) $(KERL_INSTALL_DIR)/$(ERLANG_HIPE) ERLANG_HIPE=$(ERLANG_HIPE) BUILD_ERLANG_OTP=1 >&2) +endif + +endif +endif + +# Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: search @@ -228,10 +307,10 @@ else $(foreach p,$(PACKAGES),$(call pkg_print,$(p))) endif -# Copyright (c) 2013-2015, Loïc Hoguin +# Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. -.PHONY: distclean-deps +.PHONY: distclean-deps clean-tmp-deps.log # Configuration. @@ -251,11 +330,32 @@ export DEPS_DIR REBAR_DEPS_DIR = $(DEPS_DIR) export REBAR_DEPS_DIR +# External "early" plugins (see core/plugins.mk for regular plugins). +# They both use the core_dep_plugin macro. + +define core_dep_plugin +ifeq ($(2),$(PROJECT)) +-include $$(patsubst $(PROJECT)/%,%,$(1)) +else +-include $(DEPS_DIR)/$(1) + +$(DEPS_DIR)/$(1): $(DEPS_DIR)/$(2) ; +endif +endef + +DEP_EARLY_PLUGINS ?= + +$(foreach p,$(DEP_EARLY_PLUGINS),\ + $(eval $(if $(findstring /,$p),\ + $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\ + $(call core_dep_plugin,$p/early-plugins.mk,$p)))) + dep_name = $(if $(dep_$(1)),$(1),$(if $(pkg_$(1)_name),$(pkg_$(1)_name),$(1))) dep_repo = $(patsubst git://github.com/%,https://github.com/%, \ $(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_repo))) dep_commit = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 3,$(dep_$(1))),$(pkg_$(1)_commit))) +LOCAL_DEPS_DIRS = $(foreach a,$(LOCAL_DEPS),$(if $(wildcard $(APPS_DIR)/$(a)),$(APPS_DIR)/$(a))) ALL_APPS_DIRS = $(if $(wildcard $(APPS_DIR)/),$(filter-out $(APPS_DIR),$(shell find $(APPS_DIR) -maxdepth 1 -type d))) ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(BUILD_DEPS) $(DEPS)),$(call dep_name,$(dep)))) @@ -278,10 +378,7 @@ dep_verbose = $(dep_verbose_$(V)) # Core targets. -ifdef IS_APP -apps:: -else -apps:: $(ALL_APPS_DIRS) +apps:: $(ALL_APPS_DIRS) clean-tmp-deps.log ifeq ($(IS_APP)$(IS_DEP),) $(verbose) rm -f $(ERLANG_MK_TMP)/apps.log endif @@ -289,36 +386,42 @@ endif # Create ebin directory for all apps to make sure Erlang recognizes them # as proper OTP applications when using -include_lib. This is a temporary # fix, a proper fix would be to compile apps/* in the right order. - $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ - mkdir -p $$dep/ebin || exit $$?; \ +ifndef IS_APP + $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \ + mkdir -p $$dep/ebin; \ done - $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ +endif +# at the toplevel: if LOCAL_DEPS is defined with at least one local app, only +# compile that list of apps. otherwise, compile everything. +# within an app: compile all LOCAL_DEPS that are (uncompiled) local apps + $(verbose) set -e; for dep in $(if $(LOCAL_DEPS_DIRS)$(IS_APP),$(LOCAL_DEPS_DIRS),$(ALL_APPS_DIRS)) ; do \ if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/apps.log; then \ :; \ else \ echo $$dep >> $(ERLANG_MK_TMP)/apps.log; \ - $(MAKE) -C $$dep IS_APP=1 || exit $$?; \ + $(MAKE) -C $$dep IS_APP=1; \ fi \ done + +clean-tmp-deps.log: +ifeq ($(IS_APP)$(IS_DEP),) + $(verbose) rm -f $(ERLANG_MK_TMP)/deps.log endif ifneq ($(SKIP_DEPS),) deps:: else -deps:: $(ALL_DEPS_DIRS) apps -ifeq ($(IS_APP)$(IS_DEP),) - $(verbose) rm -f $(ERLANG_MK_TMP)/deps.log -endif +deps:: $(ALL_DEPS_DIRS) apps clean-tmp-deps.log $(verbose) mkdir -p $(ERLANG_MK_TMP) - $(verbose) for dep in $(ALL_DEPS_DIRS) ; do \ + $(verbose) set -e; for dep in $(ALL_DEPS_DIRS) ; do \ if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/deps.log; then \ :; \ else \ echo $$dep >> $(ERLANG_MK_TMP)/deps.log; \ if [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ]; then \ - $(MAKE) -C $$dep IS_DEP=1 || exit $$?; \ + $(MAKE) -C $$dep IS_DEP=1; \ else \ - echo "Error: No Makefile to build dependency $$dep."; \ + echo "Error: No Makefile to build dependency $$dep." >&2; \ exit 2; \ fi \ fi \ @@ -332,17 +435,18 @@ endif # in practice only Makefile is needed so far. define dep_autopatch if [ -f $(DEPS_DIR)/$(1)/erlang.mk ]; then \ + rm -rf $(DEPS_DIR)/$1/ebin/; \ $(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \ $(call dep_autopatch_erlang_mk,$(1)); \ elif [ -f $(DEPS_DIR)/$(1)/Makefile ]; then \ - if [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$(1)/Makefile` ]; then \ + if [ -f $(DEPS_DIR)/$1/rebar.lock ]; then \ + $(call dep_autopatch2,$1); \ + elif [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$(1)/Makefile` ]; then \ $(call dep_autopatch2,$(1)); \ - elif [ 0 != `grep -ci rebar $(DEPS_DIR)/$(1)/Makefile` ]; then \ + elif [ 0 != `grep -ci "^[^#].*rebar" $(DEPS_DIR)/$(1)/Makefile` ]; then \ $(call dep_autopatch2,$(1)); \ - elif [ -n "`find $(DEPS_DIR)/$(1)/ -type f -name \*.mk -not -name erlang.mk -exec grep -i rebar '{}' \;`" ]; then \ + elif [ -n "`find $(DEPS_DIR)/$(1)/ -type f -name \*.mk -not -name erlang.mk -exec grep -i "^[^#].*rebar" '{}' \;`" ]; then \ $(call dep_autopatch2,$(1)); \ - else \ - $(call erlang,$(call dep_autopatch_app.erl,$(1))); \ fi \ else \ if [ ! -d $(DEPS_DIR)/$(1)/src/ ]; then \ @@ -354,11 +458,14 @@ define dep_autopatch endef define dep_autopatch2 + ! test -f $(DEPS_DIR)/$1/ebin/$1.app || \ + mv -n $(DEPS_DIR)/$1/ebin/$1.app $(DEPS_DIR)/$1/src/$1.app.src; \ + rm -f $(DEPS_DIR)/$1/ebin/$1.app; \ if [ -f $(DEPS_DIR)/$1/src/$1.app.src.script ]; then \ $(call erlang,$(call dep_autopatch_appsrc_script.erl,$(1))); \ fi; \ $(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \ - if [ -f $(DEPS_DIR)/$(1)/rebar -o -f $(DEPS_DIR)/$(1)/rebar.config -o -f $(DEPS_DIR)/$(1)/rebar.config.script ]; then \ + if [ -f $(DEPS_DIR)/$(1)/rebar -o -f $(DEPS_DIR)/$(1)/rebar.config -o -f $(DEPS_DIR)/$(1)/rebar.config.script -o -f $(DEPS_DIR)/$1/rebar.lock ]; then \ $(call dep_autopatch_fetch_rebar); \ $(call dep_autopatch_rebar,$(1)); \ else \ @@ -370,11 +477,15 @@ define dep_autopatch_noop printf "noop:\n" > $(DEPS_DIR)/$(1)/Makefile endef -# Overwrite erlang.mk with the current file by default. +# Replace "include erlang.mk" with a line that will load the parent Erlang.mk +# if given. Do it for all 3 possible Makefile file names. ifeq ($(NO_AUTOPATCH_ERLANG_MK),) define dep_autopatch_erlang_mk - echo "include $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(DEPS_DIR)/app)/erlang.mk" \ - > $(DEPS_DIR)/$1/erlang.mk + for f in Makefile makefile GNUmakefile; do \ + if [ -f $(DEPS_DIR)/$1/$$f ]; then \ + sed -i.bak s/'include *erlang.mk'/'include $$(if $$(ERLANG_MK_FILENAME),$$(ERLANG_MK_FILENAME),erlang.mk)'/ $(DEPS_DIR)/$1/$$f; \ + fi \ + done endef else define dep_autopatch_erlang_mk @@ -393,7 +504,7 @@ define dep_autopatch_fetch_rebar if [ ! -d $(ERLANG_MK_TMP)/rebar ]; then \ git clone -q -n -- https://github.com/rebar/rebar $(ERLANG_MK_TMP)/rebar; \ cd $(ERLANG_MK_TMP)/rebar; \ - git checkout -q 791db716b5a3a7671e0b351f95ddf24b848ee173; \ + git checkout -q 576e12171ab8d69b048b827b92aa65d067deea01; \ $(MAKE); \ cd -; \ fi @@ -410,6 +521,7 @@ endef define dep_autopatch_rebar.erl application:load(rebar), application:set_env(rebar, log_level, debug), + rmemo:start(), Conf1 = case file:consult("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config)") of {ok, Conf0} -> Conf0; _ -> [] @@ -438,6 +550,10 @@ define dep_autopatch_rebar.erl Write("C_SRC_TYPE = rebar\n"), Write("DRV_CFLAGS = -fPIC\nexport DRV_CFLAGS\n"), Write(["ERLANG_ARCH = ", rebar_utils:wordsize(), "\nexport ERLANG_ARCH\n"]), + ToList = fun + (V) when is_atom(V) -> atom_to_list(V); + (V) when is_list(V) -> "'\\"" ++ V ++ "\\"'" + end, fun() -> Write("ERLC_OPTS = +debug_info\nexport ERLC_OPTS\n"), case lists:keyfind(erl_opts, 1, Conf) of @@ -445,16 +561,18 @@ define dep_autopatch_rebar.erl {_, ErlOpts} -> lists:foreach(fun ({d, D}) -> - Write("ERLC_OPTS += -D" ++ atom_to_list(D) ++ "=1\n"); + Write("ERLC_OPTS += -D" ++ ToList(D) ++ "=1\n"); + ({d, DKey, DVal}) -> + Write("ERLC_OPTS += -D" ++ ToList(DKey) ++ "=" ++ ToList(DVal) ++ "\n"); ({i, I}) -> Write(["ERLC_OPTS += -I ", I, "\n"]); ({platform_define, Regex, D}) -> case rebar_utils:is_arch(Regex) of - true -> Write("ERLC_OPTS += -D" ++ atom_to_list(D) ++ "=1\n"); + true -> Write("ERLC_OPTS += -D" ++ ToList(D) ++ "=1\n"); false -> ok end; ({parse_transform, PT}) -> - Write("ERLC_OPTS += +'{parse_transform, " ++ atom_to_list(PT) ++ "}'\n"); + Write("ERLC_OPTS += +'{parse_transform, " ++ ToList(PT) ++ "}'\n"); (_) -> ok end, ErlOpts) end, @@ -563,9 +681,9 @@ define dep_autopatch_rebar.erl [] -> ok; _ -> Write("\npre-app::\n\t$$\(MAKE) -f c_src/Makefile.erlang.mk\n"), - PortSpecWrite(io_lib:format("ERL_CFLAGS = -finline-functions -Wall -fPIC -I \\"~s/erts-~s/include\\" -I \\"~s\\"\n", + PortSpecWrite(io_lib:format("ERL_CFLAGS ?= -finline-functions -Wall -fPIC -I \\"~s/erts-~s/include\\" -I \\"~s\\"\n", [code:root_dir(), erlang:system_info(version), code:lib_dir(erl_interface, include)])), - PortSpecWrite(io_lib:format("ERL_LDFLAGS = -L \\"~s\\" -lerl_interface -lei\n", + PortSpecWrite(io_lib:format("ERL_LDFLAGS ?= -L \\"~s\\" -lerl_interface -lei\n", [code:lib_dir(erl_interface, lib)])), [PortSpecWrite(["\n", E, "\n"]) || E <- OsEnv], FilterEnv = fun(Env) -> @@ -604,7 +722,7 @@ define dep_autopatch_rebar.erl "%.o: %.C\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", "%.o: %.cc\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", "%.o: %.cpp\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", - [[Output, ": ", K, " = ", ShellToMk(V), "\n"] || {K, V} <- lists:reverse(MergeEnv(FilterEnv(Env)))], + [[Output, ": ", K, " += ", ShellToMk(V), "\n"] || {K, V} <- lists:reverse(MergeEnv(FilterEnv(Env)))], Output, ": $$\(foreach ext,.c .C .cc .cpp,", "$$\(patsubst %$$\(ext),%.o,$$\(filter %$$\(ext),$$\(wildcard", Input, "))))\n", "\t$$\(CC) -o $$\@ $$\? $$\(LDFLAGS) $$\(ERL_LDFLAGS) $$\(DRV_LDFLAGS) $$\(EXE_LDFLAGS)", @@ -616,7 +734,7 @@ define dep_autopatch_rebar.erl end, [PortSpec(S) || S <- PortSpecs] end, - Write("\ninclude $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(DEPS_DIR)/app)/erlang.mk"), + Write("\ninclude $$\(if $$\(ERLANG_MK_FILENAME),$$\(ERLANG_MK_FILENAME),erlang.mk)"), RunPlugin = fun(Plugin, Step) -> case erlang:function_exported(Plugin, Step, 2) of false -> ok; @@ -664,27 +782,14 @@ define dep_autopatch_rebar.erl halt() endef -define dep_autopatch_app.erl - UpdateModules = fun(App) -> - case filelib:is_regular(App) of - false -> ok; - true -> - {ok, [{application, '$(1)', L0}]} = file:consult(App), - Mods = filelib:fold_files("$(call core_native_path,$(DEPS_DIR)/$1/src)", "\\\\.erl$$", true, - fun (F, Acc) -> [list_to_atom(filename:rootname(filename:basename(F)))|Acc] end, []), - L = lists:keystore(modules, 1, L0, {modules, Mods}), - ok = file:write_file(App, io_lib:format("~p.~n", [{application, '$(1)', L}])) - end - end, - UpdateModules("$(call core_native_path,$(DEPS_DIR)/$1/ebin/$1.app)"), - halt() -endef - define dep_autopatch_appsrc_script.erl AppSrc = "$(call core_native_path,$(DEPS_DIR)/$1/src/$1.app.src)", AppSrcScript = AppSrc ++ ".script", - Bindings = erl_eval:new_bindings(), - {ok, Conf} = file:script(AppSrcScript, Bindings), + {ok, Conf0} = file:consult(AppSrc), + Bindings0 = erl_eval:new_bindings(), + Bindings1 = erl_eval:add_binding('CONFIG', Conf0, Bindings0), + Bindings = erl_eval:add_binding('SCRIPT', AppSrcScript, Bindings1), + {ok, [Conf]} = file:script(AppSrcScript, Bindings), ok = file:write_file(AppSrc, io_lib:format("~p.~n", [Conf])), halt() endef @@ -697,7 +802,11 @@ define dep_autopatch_appsrc.erl true -> {ok, [{application, $(1), L0}]} = file:consult(AppSrcIn), L1 = lists:keystore(modules, 1, L0, {modules, []}), - L2 = case lists:keyfind(vsn, 1, L1) of {_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, "git"}); _ -> L1 end, + L2 = case lists:keyfind(vsn, 1, L1) of + {_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, "git"}); + {_, {cmd, _}} -> lists:keyreplace(vsn, 1, L1, {vsn, "cmd"}); + _ -> L1 + end, L3 = case lists:keyfind(registered, 1, L2) of false -> [{registered, []}|L2]; _ -> L2 end, ok = file:write_file(AppSrcOut, io_lib:format("~p.~n", [{application, $(1), L3}])), case AppSrcOut of AppSrcIn -> ok; _ -> ok = file:delete(AppSrcIn) end @@ -727,21 +836,16 @@ define dep_fetch_cp cp -R $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); endef -define dep_fetch_hex.erl - ssl:start(), - inets:start(), - {ok, {{_, 200, _}, _, Body}} = httpc:request(get, - {"https://s3.amazonaws.com/s3.hex.pm/tarballs/$(1)-$(2).tar", []}, - [], [{body_format, binary}]), - {ok, Files} = erl_tar:extract({binary, Body}, [memory]), - {_, Source} = lists:keyfind("contents.tar.gz", 1, Files), - ok = erl_tar:extract({binary, Source}, [{cwd, "$(call core_native_path,$(DEPS_DIR)/$1)"}, compressed]), - halt() +define dep_fetch_ln + ln -s $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); endef # Hex only has a package version. No need to look in the Erlang.mk packages. define dep_fetch_hex - $(call erlang,$(call dep_fetch_hex.erl,$(1),$(strip $(word 2,$(dep_$(1)))))); + mkdir -p $(ERLANG_MK_TMP)/hex $(DEPS_DIR)/$1; \ + $(call core_http_get,$(ERLANG_MK_TMP)/hex/$1.tar,\ + https://s3.amazonaws.com/s3.hex.pm/tarballs/$1-$(strip $(word 2,$(dep_$1))).tar); \ + tar -xOf $(ERLANG_MK_TMP)/hex/$1.tar contents.tar.gz | tar -C $(DEPS_DIR)/$1 -xzf -; endef define dep_fetch_fail @@ -771,7 +875,7 @@ $(DEPS_DIR)/$(call dep_name,$1): $(eval DEP_NAME := $(call dep_name,$1)) $(eval DEP_STR := $(if $(filter-out $1,$(DEP_NAME)),$1,"$1 ($(DEP_NAME))")) $(verbose) if test -d $(APPS_DIR)/$(DEP_NAME); then \ - echo "Error: Dependency" $(DEP_STR) "conflicts with application found in $(APPS_DIR)/$(DEP_NAME)."; \ + echo "Error: Dependency" $(DEP_STR) "conflicts with application found in $(APPS_DIR)/$(DEP_NAME)." >&2; \ exit 17; \ fi $(verbose) mkdir -p $(DEPS_DIR) @@ -813,15 +917,15 @@ ifndef IS_APP clean:: clean-apps clean-apps: - $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ - $(MAKE) -C $$dep clean IS_APP=1 || exit $$?; \ + $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \ + $(MAKE) -C $$dep clean IS_APP=1; \ done distclean:: distclean-apps distclean-apps: - $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ - $(MAKE) -C $$dep distclean IS_APP=1 || exit $$?; \ + $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \ + $(MAKE) -C $$dep distclean IS_APP=1; \ done endif @@ -832,77 +936,16 @@ distclean-deps: $(gen_verbose) rm -rf $(DEPS_DIR) endif -# External plugins. +# Forward-declare variables used in core/deps-tools.mk. This is required +# in case plugins use them. -DEP_PLUGINS ?= +ERLANG_MK_RECURSIVE_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-deps-list.log +ERLANG_MK_RECURSIVE_DOC_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-doc-deps-list.log +ERLANG_MK_RECURSIVE_REL_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-rel-deps-list.log +ERLANG_MK_RECURSIVE_TEST_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-test-deps-list.log +ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-shell-deps-list.log -define core_dep_plugin --include $(DEPS_DIR)/$(1) - -$(DEPS_DIR)/$(1): $(DEPS_DIR)/$(2) ; -endef - -$(foreach p,$(DEP_PLUGINS),\ - $(eval $(if $(findstring /,$p),\ - $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\ - $(call core_dep_plugin,$p/plugins.mk,$p)))) - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -# Configuration. - -DTL_FULL_PATH ?= -DTL_PATH ?= templates/ -DTL_SUFFIX ?= _dtl -DTL_OPTS ?= - -# Verbosity. - -dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); -dtl_verbose = $(dtl_verbose_$(V)) - -# Core targets. - -DTL_FILES = $(sort $(call core_find,$(DTL_PATH),*.dtl)) - -ifneq ($(DTL_FILES),) - -ifdef DTL_FULL_PATH -BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(subst /,_,$(DTL_FILES:$(DTL_PATH)%=%)))) -else -BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(notdir $(DTL_FILES)))) -endif - -# Rebuild templates when the Makefile changes. -$(DTL_FILES): $(MAKEFILE_LIST) - @touch $@ - -define erlydtl_compile.erl - [begin - Module0 = case "$(strip $(DTL_FULL_PATH))" of - "" -> - filename:basename(F, ".dtl"); - _ -> - "$(DTL_PATH)" ++ F2 = filename:rootname(F, ".dtl"), - re:replace(F2, "/", "_", [{return, list}, global]) - end, - Module = list_to_atom(string:to_lower(Module0) ++ "$(DTL_SUFFIX)"), - case erlydtl:compile(F, Module, [$(DTL_OPTS)] ++ [{out_dir, "ebin/"}, return_errors, {doc_root, "templates"}]) of - ok -> ok; - {ok, _} -> ok - end - end || F <- string:tokens("$(1)", " ")], - halt(). -endef - -ebin/$(PROJECT).app:: $(DTL_FILES) | ebin/ - $(if $(strip $?),\ - $(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$?),-pa ebin/ $(DEPS_DIR)/erlydtl/ebin/)) - -endif - -# Copyright (c) 2015, Loïc Hoguin +# Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. # Verbosity. @@ -921,10 +964,9 @@ endef define compile_proto.erl [begin - Dir = filename:dirname(filename:dirname(F)), protobuffs_compile:generate_source(F, - [{output_include_dir, Dir ++ "/include"}, - {output_src_dir, Dir ++ "/ebin"}]) + [{output_include_dir, "./include"}, + {output_src_dir, "./ebin"}]) end || F <- string:tokens("$(1)", " ")], halt(). endef @@ -934,7 +976,7 @@ ebin/$(PROJECT).app:: $(sort $(call core_find,src/,*.proto)) $(if $(strip $?),$(call compile_proto,$?)) endif -# Copyright (c) 2013-2015, Loïc Hoguin +# Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: clean-app @@ -948,6 +990,8 @@ COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST))) ERLC_EXCLUDE ?= ERLC_EXCLUDE_PATHS = $(addprefix src/,$(addsuffix .erl,$(ERLC_EXCLUDE))) +ERLC_ASN1_OPTS ?= + ERLC_MIB_OPTS ?= COMPILE_MIB_FIRST ?= COMPILE_MIB_FIRST_PATHS = $(addprefix mibs/,$(addsuffix .mib,$(COMPILE_MIB_FIRST))) @@ -997,25 +1041,27 @@ endif ifeq ($(wildcard src/$(PROJECT_MOD).erl),) define app_file -{application, $(PROJECT), [ +{application, '$(PROJECT)', [ {description, "$(PROJECT_DESCRIPTION)"}, {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), {id$(comma)$(space)"$(1)"}$(comma)) {modules, [$(call comma_list,$(2))]}, {registered, []}, - {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS))]} + {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(foreach dep,$(DEPS),$(call dep_name,$(dep))))]}, + {env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),) ]}. endef else define app_file -{application, $(PROJECT), [ +{application, '$(PROJECT)', [ {description, "$(PROJECT_DESCRIPTION)"}, {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), {id$(comma)$(space)"$(1)"}$(comma)) {modules, [$(call comma_list,$(2))]}, {registered, [$(call comma_list,$(PROJECT)_sup $(PROJECT_REGISTERED))]}, - {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS))]}, - {mod, {$(PROJECT_MOD), []}} + {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(foreach dep,$(DEPS),$(call dep_name,$(dep))))]}, + {mod, {$(PROJECT_MOD), []}}, + {env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),) ]}. endef endif @@ -1025,8 +1071,10 @@ app-build: ebin/$(PROJECT).app # Source files. -ERL_FILES = $(sort $(call core_find,src/,*.erl)) -CORE_FILES = $(sort $(call core_find,src/,*.core)) +ALL_SRC_FILES := $(sort $(call core_find,src/,*)) + +ERL_FILES := $(filter %.erl,$(ALL_SRC_FILES)) +CORE_FILES := $(filter %.core,$(ALL_SRC_FILES)) # ASN.1 files. @@ -1036,7 +1084,7 @@ ERL_FILES += $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES)))) define compile_asn1 $(verbose) mkdir -p include/ - $(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(1) + $(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(ERLC_ASN1_OPTS) $(1) $(verbose) mv asn1/*.erl src/ $(verbose) mv asn1/*.hrl include/ $(verbose) mv asn1/*.asn1db include/ @@ -1059,16 +1107,16 @@ endif # Leex and Yecc files. -XRL_FILES = $(sort $(call core_find,src/,*.xrl)) +XRL_FILES := $(filter %.xrl,$(ALL_SRC_FILES)) XRL_ERL_FILES = $(addprefix src/,$(patsubst %.xrl,%.erl,$(notdir $(XRL_FILES)))) ERL_FILES += $(XRL_ERL_FILES) -YRL_FILES = $(sort $(call core_find,src/,*.yrl)) +YRL_FILES := $(filter %.yrl,$(ALL_SRC_FILES)) YRL_ERL_FILES = $(addprefix src/,$(patsubst %.yrl,%.erl,$(notdir $(YRL_FILES)))) ERL_FILES += $(YRL_ERL_FILES) $(PROJECT).d:: $(XRL_FILES) $(YRL_FILES) - $(if $(strip $?),$(xyrl_verbose) erlc -v -o src/ $?) + $(if $(strip $?),$(xyrl_verbose) erlc -v -o src/ $(YRL_ERLC_OPTS) $?) # Erlang and Core Erlang files. @@ -1118,7 +1166,12 @@ define makedep.erl (F, Mod, include_lib, "$1/include/" ++ Hrl) -> AddHd(F, Mod, "include/" ++ Hrl); (F, Mod, include_lib, Hrl) -> AddHd(F, Mod, "include/" ++ Hrl); (F, Mod, import, {Imp, _}) -> - case filelib:is_file("src/" ++ atom_to_list(Imp) ++ ".erl") of + IsFile = + case lists:keyfind(Imp, 1, Modules) of + false -> false; + {_, FilePath} -> filelib:is_file(FilePath) + end, + case IsFile of false -> ok; true -> Add(Mod, Imp) end; @@ -1142,23 +1195,41 @@ define makedep.erl end || F <- ErlFiles], Depend = sofs:to_external(sofs:relation_to_family(sofs:relation(ets:tab2list(E)))), CompileFirst = [X || X <- lists:reverse(digraph_utils:topsort(G)), [] =/= digraph:in_neighbours(G, X)], + TargetPath = fun(Target) -> + case lists:keyfind(Target, 1, Modules) of + false -> ""; + {_, DepFile} -> + DirSubname = tl(string:tokens(filename:dirname(DepFile), "/")), + string:join(DirSubname ++ [atom_to_list(Target)], "/") + end + end, ok = file:write_file("$(1)", [ [[F, "::", [[" ", D] || D <- Deps], "; @touch \$$@\n"] || {F, Deps} <- Depend], - "\nCOMPILE_FIRST +=", [[" ", atom_to_list(CF)] || CF <- CompileFirst], "\n" + "\nCOMPILE_FIRST +=", [[" ", TargetPath(CF)] || CF <- CompileFirst], "\n" ]), halt() endef ifeq ($(if $(NO_MAKEDEP),$(wildcard $(PROJECT).d),),) -$(PROJECT).d:: $(ERL_FILES) $(call core_find,include/,*.hrl) +$(PROJECT).d:: $(ERL_FILES) $(call core_find,include/,*.hrl) $(MAKEFILE_LIST) $(makedep_verbose) $(call erlang,$(call makedep.erl,$@)) endif +ifneq ($(words $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES)),0) # Rebuild everything when the Makefile changes. -$(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(MAKEFILE_LIST) - @touch $@ +$(ERLANG_MK_TMP)/last-makefile-change: $(MAKEFILE_LIST) + $(verbose) mkdir -p $(ERLANG_MK_TMP) + $(verbose) if test -f $@; then \ + touch $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES); \ + touch -c $(PROJECT).d; \ + fi + $(verbose) touch $@ --include $(PROJECT).d +$(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(ERLANG_MK_TMP)/last-makefile-change +ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change +endif + +include $(wildcard $(PROJECT).d) ebin/$(PROJECT).app:: ebin/ @@ -1177,7 +1248,7 @@ ebin/$(PROJECT).app:: $(ERL_FILES) $(CORE_FILES) $(wildcard src/$(PROJECT).app.s $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \ $(filter-out $(ERLC_EXCLUDE_PATHS),$(ERL_FILES) $(CORE_FILES) $(BEAM_FILES))))))) ifeq ($(wildcard src/$(PROJECT).app.src),) - $(app_verbose) printf "$(subst $(newline),\n,$(subst ",\",$(call app_file,$(GITDESCRIBE),$(MODULES))))" \ + $(app_verbose) printf '$(subst %,%%,$(subst $(newline),\n,$(subst ','\'',$(call app_file,$(GITDESCRIBE),$(MODULES)))))' \ > ebin/$(PROJECT).app else $(verbose) if [ -z "$$(grep -e '^[^%]*{\s*modules\s*,' src/$(PROJECT).app.src)" ]; then \ @@ -1201,6 +1272,7 @@ clean-app: endif +# Copyright (c) 2016, Loïc Hoguin # Copyright (c) 2015, Viktor Söderqvist # This file is part of erlang.mk and subject to the terms of the ISC License. @@ -1218,10 +1290,10 @@ ifneq ($(SKIP_DEPS),) doc-deps: else doc-deps: $(ALL_DOC_DEPS_DIRS) - $(verbose) for dep in $(ALL_DOC_DEPS_DIRS) ; do $(MAKE) -C $$dep; done + $(verbose) set -e; for dep in $(ALL_DOC_DEPS_DIRS) ; do $(MAKE) -C $$dep; done endif -# Copyright (c) 2015, Loïc Hoguin +# Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: rel-deps @@ -1238,10 +1310,10 @@ ifneq ($(SKIP_DEPS),) rel-deps: else rel-deps: $(ALL_REL_DEPS_DIRS) - $(verbose) for dep in $(ALL_REL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done + $(verbose) set -e; for dep in $(ALL_REL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done endif -# Copyright (c) 2015, Loïc Hoguin +# Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: test-deps test-dir test-build clean-test-dir @@ -1263,7 +1335,7 @@ ifneq ($(SKIP_DEPS),) test-deps: else test-deps: $(ALL_TEST_DEPS_DIRS) - $(verbose) for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep IS_DEP=1; done + $(verbose) set -e; for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep IS_DEP=1; done endif ifneq ($(wildcard $(TEST_DIR)),) @@ -1296,7 +1368,7 @@ ifneq ($(wildcard $(TEST_DIR)/*.beam),) endif endif -# Copyright (c) 2015, Loïc Hoguin +# Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: rebar.config @@ -1332,54 +1404,90 @@ $(eval export _compat_rebar_config) rebar.config: $(gen_verbose) echo "$${_compat_rebar_config}" > rebar.config -# Copyright (c) 2015, Loïc Hoguin +# Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. -.PHONY: asciidoc asciidoc-guide asciidoc-manual install-asciidoc distclean-asciidoc +ifeq ($(filter asciideck,$(DEPS) $(DOC_DEPS)),asciideck) -MAN_INSTALL_PATH ?= /usr/local/share/man -MAN_SECTIONS ?= 3 7 +.PHONY: asciidoc asciidoc-guide asciidoc-manual install-asciidoc distclean-asciidoc-guide distclean-asciidoc-manual + +# Core targets. docs:: asciidoc +distclean:: distclean-asciidoc-guide distclean-asciidoc-manual + +# Plugin-specific targets. + asciidoc: asciidoc-guide asciidoc-manual +# User guide. + ifeq ($(wildcard doc/src/guide/book.asciidoc),) asciidoc-guide: else -asciidoc-guide: distclean-asciidoc doc-deps +asciidoc-guide: distclean-asciidoc-guide doc-deps a2x -v -f pdf doc/src/guide/book.asciidoc && mv doc/src/guide/book.pdf doc/guide.pdf a2x -v -f chunked doc/src/guide/book.asciidoc && mv doc/src/guide/book.chunked/ doc/html/ + +distclean-asciidoc-guide: + $(gen_verbose) rm -rf doc/html/ doc/guide.pdf endif -ifeq ($(wildcard doc/src/manual/*.asciidoc),) +# Man pages. + +ASCIIDOC_MANUAL_FILES := $(wildcard doc/src/manual/*.asciidoc) + +ifeq ($(ASCIIDOC_MANUAL_FILES),) asciidoc-manual: else -asciidoc-manual: distclean-asciidoc doc-deps - for f in doc/src/manual/*.asciidoc ; do \ - a2x -v -f manpage $$f ; \ - done - for s in $(MAN_SECTIONS); do \ - mkdir -p doc/man$$s/ ; \ - mv doc/src/manual/*.$$s doc/man$$s/ ; \ - gzip doc/man$$s/*.$$s ; \ - done + +# Configuration. + +MAN_INSTALL_PATH ?= /usr/local/share/man +MAN_SECTIONS ?= 3 7 +MAN_PROJECT ?= $(shell echo $(PROJECT) | sed 's/^./\U&\E/') +MAN_VERSION ?= $(PROJECT_VERSION) + +# Plugin-specific targets. + +define asciidoc2man.erl +try + [begin + io:format(" ADOC ~s~n", [F]), + ok = asciideck:to_manpage(asciideck:parse_file(F), #{ + compress => gzip, + outdir => filename:dirname(F), + extra2 => "$(MAN_PROJECT) $(MAN_VERSION)", + extra3 => "$(MAN_PROJECT) Function Reference" + }) + end || F <- [$(shell echo $(addprefix $(comma)\",$(addsuffix \",$1)) | sed 's/^.//')]], + halt(0) +catch C:E -> + io:format("Exception ~p:~p~nStacktrace: ~p~n", [C, E, erlang:get_stacktrace()]), + halt(1) +end. +endef + +asciidoc-manual:: doc-deps + +asciidoc-manual:: $(ASCIIDOC_MANUAL_FILES) + $(call erlang,$(call asciidoc2man.erl,$?)) + $(foreach s,$(MAN_SECTIONS),mkdir -p doc/man$s/ && mv doc/src/manual/*.$s.gz doc/man$s/;) install-docs:: install-asciidoc install-asciidoc: asciidoc-manual - for s in $(MAN_SECTIONS); do \ - mkdir -p $(MAN_INSTALL_PATH)/man$$s/ ; \ - install -g `id -u` -o `id -g` -m 0644 doc/man$$s/*.gz $(MAN_INSTALL_PATH)/man$$s/ ; \ - done + $(foreach s,$(MAN_SECTIONS),\ + mkdir -p $(MAN_INSTALL_PATH)/man$s/ && \ + install -g `id -g` -o `id -u` -m 0644 doc/man$s/*.gz $(MAN_INSTALL_PATH)/man$s/;) + +distclean-asciidoc-manual: + $(gen_verbose) rm -rf $(addprefix doc/man,$(MAN_SECTIONS)) +endif endif -distclean:: distclean-asciidoc - -distclean-asciidoc: - $(gen_verbose) rm -rf doc/html/ doc/guide.pdf doc/man3/ doc/man7/ - -# Copyright (c) 2014-2015, Loïc Hoguin +# Copyright (c) 2014-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: bootstrap bootstrap-lib bootstrap-rel new list-templates @@ -1436,7 +1544,7 @@ ifdef SP define bs_Makefile PROJECT = $p PROJECT_DESCRIPTION = New project -PROJECT_VERSION = 0.0.1 +PROJECT_VERSION = 0.1.0 # Whitespace to be used when creating files from templates. SP = $(SP) @@ -1446,7 +1554,7 @@ else define bs_Makefile PROJECT = $p PROJECT_DESCRIPTION = New project -PROJECT_VERSION = 0.0.1 +PROJECT_VERSION = 0.1.0 endef endif @@ -1454,7 +1562,7 @@ endif define bs_apps_Makefile PROJECT = $p PROJECT_DESCRIPTION = New project -PROJECT_VERSION = 0.0.1 +PROJECT_VERSION = 0.1.0 include $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(APPS_DIR)/app)/erlang.mk endef @@ -1474,7 +1582,7 @@ stop(_State) -> endef define bs_relx_config -{release, {$p_release, "1"}, [$p]}. +{release, {$p_release, "1"}, [$p, sasl, runtime_tools]}. {extended_start_script, true}. {sys_config, "rel/sys.config"}. {vm_args, "rel/vm.args"}. @@ -1825,9 +1933,6 @@ endif ifndef t $(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP]) endif -ifndef tpl_$(t) - $(error Unknown template) -endif ifndef n $(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP]) endif @@ -1840,7 +1945,7 @@ endif list-templates: $(verbose) echo Available templates: $(sort $(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES)))) -# Copyright (c) 2014-2015, Loïc Hoguin +# Copyright (c) 2014-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: clean-c_src distclean-c_src-env @@ -2075,57 +2180,56 @@ else $(call render_template,bs_erl_nif,src/$n.erl) endif -# Copyright (c) 2015, Loïc Hoguin +# Copyright (c) 2015-2017, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. -.PHONY: ci ci-setup distclean-kerl +.PHONY: ci ci-prepare ci-setup -KERL ?= $(CURDIR)/kerl -export KERL - -KERL_URL ?= https://raw.githubusercontent.com/yrashk/kerl/master/kerl - -OTP_GIT ?= https://github.com/erlang/otp - -CI_INSTALL_DIR ?= $(HOME)/erlang CI_OTP ?= +CI_HIPE ?= +CI_ERLLVM ?= -ifeq ($(strip $(CI_OTP)),) +ifeq ($(CI_VM),native) +ERLC_OPTS += +native +TEST_ERLC_OPTS += +native +else ifeq ($(CI_VM),erllvm) +ERLC_OPTS += +native +'{hipe, [to_llvm]}' +TEST_ERLC_OPTS += +native +'{hipe, [to_llvm]}' +endif + +ifeq ($(strip $(CI_OTP) $(CI_HIPE) $(CI_ERLLVM)),) ci:: else -ci:: $(addprefix ci-,$(CI_OTP)) -ci-prepare: $(addprefix $(CI_INSTALL_DIR)/,$(CI_OTP)) +ci:: $(addprefix ci-,$(CI_OTP) $(addsuffix -native,$(CI_HIPE)) $(addsuffix -erllvm,$(CI_ERLLVM))) + +ci-prepare: $(addprefix $(KERL_INSTALL_DIR)/,$(CI_OTP) $(addsuffix -native,$(CI_HIPE))) ci-setup:: +ci-extra:: + ci_verbose_0 = @echo " CI " $(1); ci_verbose = $(ci_verbose_$(V)) define ci_target -ci-$(1): $(CI_INSTALL_DIR)/$(1) +ci-$1: $(KERL_INSTALL_DIR)/$2 + $(verbose) $(MAKE) --no-print-directory clean $(ci_verbose) \ - PATH="$(CI_INSTALL_DIR)/$(1)/bin:$(PATH)" \ - CI_OTP_RELEASE="$(1)" \ - CT_OPTS="-label $(1)" \ - $(MAKE) clean ci-setup tests + PATH="$(KERL_INSTALL_DIR)/$2/bin:$(PATH)" \ + CI_OTP_RELEASE="$1" \ + CT_OPTS="-label $1" \ + CI_VM="$3" \ + $(MAKE) ci-setup tests + $(verbose) $(MAKE) --no-print-directory ci-extra endef -$(foreach otp,$(CI_OTP),$(eval $(call ci_target,$(otp)))) +$(foreach otp,$(CI_OTP),$(eval $(call ci_target,$(otp),$(otp),otp))) +$(foreach otp,$(CI_HIPE),$(eval $(call ci_target,$(otp)-native,$(otp)-native,native))) +$(foreach otp,$(CI_ERLLVM),$(eval $(call ci_target,$(otp)-erllvm,$(otp)-native,erllvm))) -define ci_otp_target -ifeq ($(wildcard $(CI_INSTALL_DIR)/$(1)),) -$(CI_INSTALL_DIR)/$(1): $(KERL) - $(KERL) build git $(OTP_GIT) $(1) $(1) - $(KERL) install $(1) $(CI_INSTALL_DIR)/$(1) -endif -endef - -$(foreach otp,$(CI_OTP),$(eval $(call ci_otp_target,$(otp)))) - -$(KERL): - $(gen_verbose) $(call core_http_get,$(KERL),$(KERL_URL)) - $(verbose) chmod +x $(KERL) +$(foreach otp,$(CI_OTP),$(eval $(call kerl_otp_target,$(otp)))) +$(foreach otp,$(sort $(CI_HIPE) $(CI_ERLLLVM)),$(eval $(call kerl_hipe_target,$(otp)))) help:: $(verbose) printf "%s\n" "" \ @@ -2135,13 +2239,9 @@ help:: "The CI_OTP variable must be defined with the Erlang versions" \ "that must be tested. For example: CI_OTP = OTP-17.3.4 OTP-17.5.3" -distclean:: distclean-kerl - -distclean-kerl: - $(gen_verbose) rm -rf $(KERL) endif -# Copyright (c) 2013-2015, Loïc Hoguin +# Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: ct apps-ct distclean-ct @@ -2149,11 +2249,14 @@ endif # Configuration. CT_OPTS ?= + ifneq ($(wildcard $(TEST_DIR)),) - CT_SUITES ?= $(sort $(subst _SUITE.erl,,$(notdir $(call core_find,$(TEST_DIR)/,*_SUITE.erl)))) -else - CT_SUITES ?= +ifndef CT_SUITES +CT_SUITES := $(sort $(subst _SUITE.erl,,$(notdir $(call core_find,$(TEST_DIR)/,*_SUITE.erl)))) endif +endif +CT_SUITES ?= +CT_LOGS_DIR ?= $(CURDIR)/logs # Core targets. @@ -2174,17 +2277,20 @@ help:: CT_RUN = ct_run \ -no_auto_compile \ -noinput \ - -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(DEPS_DIR)/gen_rpc/_build/dev/lib/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \ + -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \ -dir $(TEST_DIR) \ - -logdir $(CURDIR)/logs + -logdir $(CT_LOGS_DIR) ifeq ($(CT_SUITES),) ct: $(if $(IS_APP),,apps-ct) else +# We do not run tests if we are in an apps/* with no test directory. +ifneq ($(IS_APP)$(wildcard $(TEST_DIR)),1) ct: test-build $(if $(IS_APP),,apps-ct) - $(verbose) mkdir -p $(CURDIR)/logs/ + $(verbose) mkdir -p $(CT_LOGS_DIR) $(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS) endif +endif ifneq ($(ALL_APPS_DIRS),) define ct_app_target @@ -2210,16 +2316,16 @@ endif define ct_suite_target ct-$(1): test-build - $(verbose) mkdir -p $(CURDIR)/logs/ + $(verbose) mkdir -p $(CT_LOGS_DIR) $(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(1)) $(CT_EXTRA) $(CT_OPTS) endef $(foreach test,$(CT_SUITES),$(eval $(call ct_suite_target,$(test)))) distclean-ct: - $(gen_verbose) rm -rf $(CURDIR)/logs/ + $(gen_verbose) rm -rf $(CT_LOGS_DIR) -# Copyright (c) 2013-2015, Loïc Hoguin +# Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: plt distclean-plt dialyze @@ -2248,19 +2354,25 @@ help:: # Plugin-specific targets. define filter_opts.erl - Opts = binary:split(<<"$1">>, <<"-">>, [global]), - Filtered = lists:reverse(lists:foldl(fun - (O = <<"pa ", _/bits>>, Acc) -> [O|Acc]; - (O = <<"D ", _/bits>>, Acc) -> [O|Acc]; - (O = <<"I ", _/bits>>, Acc) -> [O|Acc]; - (_, Acc) -> Acc - end, [], Opts)), - io:format("~s~n", [[["-", O] || O <- Filtered]]), + Opts = init:get_plain_arguments(), + {Filtered, _} = lists:foldl(fun + (O, {Os, true}) -> {[O|Os], false}; + (O = "-D", {Os, _}) -> {[O|Os], true}; + (O = [\\$$-, \\$$D, _ | _], {Os, _}) -> {[O|Os], false}; + (O = "-I", {Os, _}) -> {[O|Os], true}; + (O = [\\$$-, \\$$I, _ | _], {Os, _}) -> {[O|Os], false}; + (O = "-pa", {Os, _}) -> {[O|Os], true}; + (_, Acc) -> Acc + end, {[], false}, Opts), + io:format("~s~n", [string:join(lists:reverse(Filtered), " ")]), halt(). endef $(DIALYZER_PLT): deps app - $(verbose) dialyzer --build_plt --apps erts kernel stdlib $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS) + $(eval DEPS_LOG := $(shell test -f $(ERLANG_MK_TMP)/deps.log && \ + while read p; do test -d $$p/ebin && echo $$p/ebin; done <$(ERLANG_MK_TMP)/deps.log)) + $(verbose) dialyzer --build_plt --apps erts kernel stdlib \ + $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS_LOG) plt: $(DIALYZER_PLT) @@ -2272,9 +2384,9 @@ dialyze: else dialyze: $(DIALYZER_PLT) endif - $(verbose) dialyzer --no_native `$(call erlang,$(call filter_opts.erl,$(ERLC_OPTS)))` $(DIALYZER_DIRS) $(DIALYZER_OPTS) + $(verbose) dialyzer --no_native `$(ERL) -eval "$(subst $(newline),,$(subst ",\",$(call filter_opts.erl)))" -extra $(ERLC_OPTS)` $(DIALYZER_DIRS) $(DIALYZER_OPTS) -# Copyright (c) 2013-2015, Loïc Hoguin +# Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: distclean-edoc edoc @@ -2282,10 +2394,21 @@ endif # Configuration. EDOC_OPTS ?= +EDOC_SRC_DIRS ?= +EDOC_OUTPUT ?= doc + +define edoc.erl + SrcPaths = lists:foldl(fun(P, Acc) -> + filelib:wildcard(atom_to_list(P) ++ "/{src,c_src}") ++ Acc + end, [], [$(call comma_list,$(patsubst %,'%',$(EDOC_SRC_DIRS)))]), + DefaultOpts = [{dir, "$(EDOC_OUTPUT)"}, {source_path, SrcPaths}, {subpackages, false}], + edoc:application($(1), ".", [$(2)] ++ DefaultOpts), + halt(0). +endef # Core targets. -ifneq ($(wildcard doc/overview.edoc),) +ifneq ($(strip $(EDOC_SRC_DIRS)$(wildcard doc/overview.edoc)),) docs:: edoc endif @@ -2294,30 +2417,91 @@ distclean:: distclean-edoc # Plugin-specific targets. edoc: distclean-edoc doc-deps - $(gen_verbose) $(ERL) -eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), halt().' + $(gen_verbose) $(call erlang,$(call edoc.erl,$(PROJECT),$(EDOC_OPTS))) distclean-edoc: - $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info + $(gen_verbose) rm -f $(EDOC_OUTPUT)/*.css $(EDOC_OUTPUT)/*.html $(EDOC_OUTPUT)/*.png $(EDOC_OUTPUT)/edoc-info -# Copyright (c) 2014 Dave Cottlehuber +# Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. -.PHONY: distclean-escript escript +# Configuration. + +DTL_FULL_PATH ?= +DTL_PATH ?= templates/ +DTL_SUFFIX ?= _dtl +DTL_OPTS ?= + +# Verbosity. + +dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); +dtl_verbose = $(dtl_verbose_$(V)) + +# Core targets. + +DTL_PATH := $(abspath $(DTL_PATH)) +DTL_FILES := $(sort $(call core_find,$(DTL_PATH),*.dtl)) + +ifneq ($(DTL_FILES),) + +DTL_NAMES = $(addsuffix $(DTL_SUFFIX),$(DTL_FILES:$(DTL_PATH)/%.dtl=%)) +DTL_MODULES = $(if $(DTL_FULL_PATH),$(subst /,_,$(DTL_NAMES)),$(notdir $(DTL_NAMES))) +BEAM_FILES += $(addsuffix .beam,$(addprefix ebin/,$(DTL_MODULES))) + +ifneq ($(words $(DTL_FILES)),0) +# Rebuild templates when the Makefile changes. +$(ERLANG_MK_TMP)/last-makefile-change-erlydtl: $(MAKEFILE_LIST) + @mkdir -p $(ERLANG_MK_TMP) + @if test -f $@; then \ + touch $(DTL_FILES); \ + fi + @touch $@ + +ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change-erlydtl +endif + +define erlydtl_compile.erl + [begin + Module0 = case "$(strip $(DTL_FULL_PATH))" of + "" -> + filename:basename(F, ".dtl"); + _ -> + "$(DTL_PATH)/" ++ F2 = filename:rootname(F, ".dtl"), + re:replace(F2, "/", "_", [{return, list}, global]) + end, + Module = list_to_atom(string:to_lower(Module0) ++ "$(DTL_SUFFIX)"), + case erlydtl:compile(F, Module, [$(DTL_OPTS)] ++ [{out_dir, "ebin/"}, return_errors]) of + ok -> ok; + {ok, _} -> ok + end + end || F <- string:tokens("$(1)", " ")], + halt(). +endef + +ebin/$(PROJECT).app:: $(DTL_FILES) | ebin/ + $(if $(strip $?),\ + $(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$(call core_native_path,$?)),\ + -pa ebin/ $(DEPS_DIR)/erlydtl/ebin/)) + +endif + +# Copyright (c) 2016, Loïc Hoguin +# Copyright (c) 2014, Dave Cottlehuber +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: distclean-escript escript escript-zip # Configuration. ESCRIPT_NAME ?= $(PROJECT) ESCRIPT_FILE ?= $(ESCRIPT_NAME) -ESCRIPT_COMMENT ?= This is an -*- erlang -*- file - -ESCRIPT_BEAMS ?= "ebin/*", "deps/*/ebin/*" -ESCRIPT_SYS_CONFIG ?= "rel/sys.config" -ESCRIPT_EMU_ARGS ?= -pa . \ - -sasl errlog_type error \ - -escript main $(ESCRIPT_NAME) ESCRIPT_SHEBANG ?= /usr/bin/env escript -ESCRIPT_STATIC ?= "deps/*/priv/**", "priv/**" +ESCRIPT_COMMENT ?= This is an -*- erlang -*- file +ESCRIPT_EMU_ARGS ?= -escript main $(ESCRIPT_NAME) + +ESCRIPT_ZIP ?= 7z a -tzip -mx=9 -mtc=off $(if $(filter-out 0,$(V)),,> /dev/null) +ESCRIPT_ZIP_FILE ?= $(ERLANG_MK_TMP)/escript.zip # Core targets. @@ -2330,44 +2514,28 @@ help:: # Plugin-specific targets. -# Based on https://github.com/synrc/mad/blob/master/src/mad_bundle.erl -# Copyright (c) 2013 Maxim Sokhatsky, Synrc Research Center -# Modified MIT License, https://github.com/synrc/mad/blob/master/LICENSE : -# Software may only be used for the great good and the true happiness of all -# sentient beings. +escript-zip:: deps app + $(verbose) mkdir -p $(dir $(ESCRIPT_ZIP)) + $(verbose) rm -f $(ESCRIPT_ZIP_FILE) + $(gen_verbose) cd .. && $(ESCRIPT_ZIP) $(ESCRIPT_ZIP_FILE) $(PROJECT)/ebin/* +ifneq ($(DEPS),) + $(verbose) cd $(DEPS_DIR) && $(ESCRIPT_ZIP) $(ESCRIPT_ZIP_FILE) \ + `cat $(ERLANG_MK_TMP)/deps.log | sed 's/^$(subst /,\/,$(DEPS_DIR))\///' | sed 's/$$/\/ebin\/\*/'` +endif -define ESCRIPT_RAW -'Read = fun(F) -> {ok, B} = file:read_file(filename:absname(F)), B end,'\ -'Files = fun(L) -> A = lists:concat([filelib:wildcard(X)||X<- L ]),'\ -' [F || F <- A, not filelib:is_dir(F) ] end,'\ -'Squash = fun(L) -> [{filename:basename(F), Read(F) } || F <- L ] end,'\ -'Zip = fun(A, L) -> {ok,{_,Z}} = zip:create(A, L, [{compress,all},memory]), Z end,'\ -'Ez = fun(Escript) ->'\ -' Static = Files([$(ESCRIPT_STATIC)]),'\ -' Beams = Squash(Files([$(ESCRIPT_BEAMS), $(ESCRIPT_SYS_CONFIG)])),'\ -' Archive = Beams ++ [{ "static.gz", Zip("static.gz", Static)}],'\ -' escript:create(Escript, [ $(ESCRIPT_OPTIONS)'\ -' {archive, Archive, [memory]},'\ -' {shebang, "$(ESCRIPT_SHEBANG)"},'\ -' {comment, "$(ESCRIPT_COMMENT)"},'\ -' {emu_args, " $(ESCRIPT_EMU_ARGS)"}'\ -' ]),'\ -' file:change_mode(Escript, 8#755)'\ -'end,'\ -'Ez("$(ESCRIPT_FILE)"),'\ -'halt().' -endef - -ESCRIPT_COMMAND = $(subst ' ',,$(ESCRIPT_RAW)) - -escript:: distclean-escript deps app - $(gen_verbose) $(ERL) -eval $(ESCRIPT_COMMAND) +escript:: escript-zip + $(gen_verbose) printf "%s\n" \ + "#!$(ESCRIPT_SHEBANG)" \ + "%% $(ESCRIPT_COMMENT)" \ + "%%! $(ESCRIPT_EMU_ARGS)" > $(ESCRIPT_FILE) + $(verbose) cat $(ESCRIPT_ZIP_FILE) >> $(ESCRIPT_FILE) + $(verbose) chmod +x $(ESCRIPT_FILE) distclean-escript: - $(gen_verbose) rm -f $(ESCRIPT_NAME) + $(gen_verbose) rm -f $(ESCRIPT_FILE) +# Copyright (c) 2015-2016, Loïc Hoguin # Copyright (c) 2014, Enrique Fernandez -# Copyright (c) 2015, Loïc Hoguin # This file is contributed to erlang.mk and subject to the terms of the ISC License. .PHONY: eunit apps-eunit @@ -2404,7 +2572,7 @@ define eunit.erl case "$(COVER)" of "" -> ok; _ -> - cover:export("eunit.coverdata") + cover:export("$(COVER_DATA_DIR)/eunit.coverdata") end, halt() endef @@ -2413,10 +2581,10 @@ EUNIT_ERL_OPTS += -pa $(TEST_DIR) $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(CURDIR ifdef t ifeq (,$(findstring :,$(t))) -eunit: test-build +eunit: test-build cover-data-dir $(gen_verbose) $(call erlang,$(call eunit.erl,['$(t)']),$(EUNIT_ERL_OPTS)) else -eunit: test-build +eunit: test-build cover-data-dir $(gen_verbose) $(call erlang,$(call eunit.erl,fun $(t)/0),$(EUNIT_ERL_OPTS)) endif else @@ -2426,28 +2594,90 @@ EUNIT_TEST_MODS = $(notdir $(basename $(call core_find,$(TEST_DIR)/,*.erl))) EUNIT_MODS = $(foreach mod,$(EUNIT_EBIN_MODS) $(filter-out \ $(patsubst %,%_tests,$(EUNIT_EBIN_MODS)),$(EUNIT_TEST_MODS)),'$(mod)') -eunit: test-build $(if $(IS_APP),,apps-eunit) +eunit: test-build $(if $(IS_APP),,apps-eunit) cover-data-dir $(gen_verbose) $(call erlang,$(call eunit.erl,[$(call comma_list,$(EUNIT_MODS))]),$(EUNIT_ERL_OPTS)) ifneq ($(ALL_APPS_DIRS),) apps-eunit: - $(verbose) for app in $(ALL_APPS_DIRS); do $(MAKE) -C $$app eunit IS_APP=1; done + $(verbose) eunit_retcode=0 ; for app in $(ALL_APPS_DIRS); do $(MAKE) -C $$app eunit IS_APP=1; \ + [ $$? -ne 0 ] && eunit_retcode=1 ; done ; \ + exit $$eunit_retcode endif endif -# Copyright (c) 2013-2015, Loïc Hoguin +# Copyright (c) 2015-2017, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. -.PHONY: relx-rel distclean-relx-rel distclean-relx run +ifeq ($(filter proper,$(DEPS) $(TEST_DEPS)),proper) +.PHONY: proper + +# Targets. + +tests:: proper + +define proper_check.erl + code:add_pathsa(["$(call core_native_path,$(CURDIR)/ebin)", "$(call core_native_path,$(DEPS_DIR)/*/ebin)"]), + Module = fun(M) -> + [true] =:= lists:usort([ + case atom_to_list(F) of + "prop_" ++ _ -> + io:format("Testing ~p:~p/0~n", [M, F]), + proper:quickcheck(M:F(), nocolors); + _ -> + true + end + || {F, 0} <- M:module_info(exports)]) + end, + try + case $(1) of + all -> [true] =:= lists:usort([Module(M) || M <- [$(call comma_list,$(3))]]); + module -> Module($(2)); + function -> proper:quickcheck($(2), nocolors) + end + of + true -> halt(0); + _ -> halt(1) + catch error:undef -> + io:format("Undefined property or module?~n~p~n", [erlang:get_stacktrace()]), + halt(0) + end. +endef + +ifdef t +ifeq (,$(findstring :,$(t))) +proper: test-build + $(verbose) $(call erlang,$(call proper_check.erl,module,$(t))) +else +proper: test-build + $(verbose) echo Testing $(t)/0 + $(verbose) $(call erlang,$(call proper_check.erl,function,$(t)())) +endif +else +proper: test-build + $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename $(wildcard ebin/*.beam)))))) + $(gen_verbose) $(call erlang,$(call proper_check.erl,all,undefined,$(MODULES))) +endif +endif + +# Copyright (c) 2013-2016, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: relx-rel relx-relup distclean-relx-rel run # Configuration. -RELX ?= $(CURDIR)/relx +RELX ?= $(ERLANG_MK_TMP)/relx RELX_CONFIG ?= $(CURDIR)/relx.config -RELX_URL ?= https://github.com/erlware/relx/releases/download/v3.19.0/relx +RELX_URL ?= https://github.com/erlware/relx/releases/download/v3.23.0/relx RELX_OPTS ?= RELX_OUTPUT_DIR ?= _rel +RELX_REL_EXT ?= +RELX_TAR ?= 1 + +ifdef SFX + RELX_TAR = 1 +endif ifeq ($(firstword $(RELX_OPTS)),-o) RELX_OUTPUT_DIR = $(word 2,$(RELX_OPTS)) @@ -2460,10 +2690,12 @@ endif ifeq ($(IS_DEP),) ifneq ($(wildcard $(RELX_CONFIG)),) rel:: relx-rel + +relup:: relx-relup endif endif -distclean:: distclean-relx-rel distclean-relx +distclean:: distclean-relx-rel # Plugin-specific targets. @@ -2472,31 +2704,43 @@ $(RELX): $(verbose) chmod +x $(RELX) relx-rel: $(RELX) rel-deps app - $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) + $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) release $(if $(filter 1,$(RELX_TAR)),tar) + +relx-relup: $(RELX) rel-deps app + $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) release relup $(if $(filter 1,$(RELX_TAR)),tar) distclean-relx-rel: $(gen_verbose) rm -rf $(RELX_OUTPUT_DIR) -distclean-relx: - $(gen_verbose) rm -rf $(RELX) - # Run target. ifeq ($(wildcard $(RELX_CONFIG)),) -run: +run:: else define get_relx_release.erl - {ok, Config} = file:consult("$(RELX_CONFIG)"), - {release, {Name, _}, _} = lists:keyfind(release, 1, Config), - io:format("~s", [Name]), + {ok, Config} = file:consult("$(call core_native_path,$(RELX_CONFIG))"), + {release, {Name, Vsn0}, _} = lists:keyfind(release, 1, Config), + Vsn = case Vsn0 of + {cmd, Cmd} -> os:cmd(Cmd); + semver -> ""; + {semver, _} -> ""; + VsnStr -> Vsn0 + end, + io:format("~s ~s", [Name, Vsn]), halt(0). endef -RELX_RELEASE = `$(call erlang,$(get_relx_release.erl))` +RELX_REL := $(shell $(call erlang,$(get_relx_release.erl))) +RELX_REL_NAME := $(word 1,$(RELX_REL)) +RELX_REL_VSN := $(word 2,$(RELX_REL)) -run: all - $(verbose) $(RELX_OUTPUT_DIR)/$(RELX_RELEASE)/bin/$(RELX_RELEASE) console +ifeq ($(PLATFORM),msys2) +RELX_REL_EXT := .cmd +endif + +run:: all + $(verbose) $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/bin/$(RELX_REL_NAME)$(RELX_REL_EXT) console help:: $(verbose) printf "%s\n" "" \ @@ -2505,8 +2749,8 @@ help:: endif +# Copyright (c) 2015-2016, Loïc Hoguin # Copyright (c) 2014, M Robert Martin -# Copyright (c) 2015, Loïc Hoguin # This file is contributed to erlang.mk and subject to the terms of the ISC License. .PHONY: shell @@ -2531,12 +2775,26 @@ help:: $(foreach dep,$(SHELL_DEPS),$(eval $(call dep_target,$(dep)))) build-shell-deps: $(ALL_SHELL_DEPS_DIRS) - $(verbose) for dep in $(ALL_SHELL_DEPS_DIRS) ; do $(MAKE) -C $$dep ; done + $(verbose) set -e; for dep in $(ALL_SHELL_DEPS_DIRS) ; do $(MAKE) -C $$dep ; done shell: build-shell-deps $(gen_verbose) $(SHELL_ERL) -pa $(SHELL_PATHS) $(SHELL_OPTS) -# Copyright (c) 2015, Loïc Hoguin +# Copyright (c) 2017, Jean-Sébastien Pédron +# This file is contributed to erlang.mk and subject to the terms of the ISC License. + +.PHONY: show-ERL_LIBS show-ERLC_OPTS show-TEST_ERLC_OPTS + +show-ERL_LIBS: + @echo $(ERL_LIBS) + +show-ERLC_OPTS: + @$(foreach opt,$(ERLC_OPTS) -pa ebin -I include,echo "$(opt)";) + +show-TEST_ERLC_OPTS: + @$(foreach opt,$(TEST_ERLC_OPTS) -pa ebin -I include,echo "$(opt)";) + +# Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. ifeq ($(filter triq,$(DEPS) $(TEST_DEPS)),triq) @@ -2547,7 +2805,10 @@ ifeq ($(filter triq,$(DEPS) $(TEST_DEPS)),triq) tests:: triq define triq_check.erl - code:add_pathsa(["$(CURDIR)/ebin", "$(DEPS_DIR)/*/ebin"]), + code:add_pathsa([ + "$(call core_native_path,$(CURDIR)/ebin)", + "$(call core_native_path,$(DEPS_DIR)/*/ebin)", + "$(call core_native_path,$(TEST_DIR))"]), try case $(1) of all -> [true] =:= lists:usort([triq:check(M) || M <- [$(call comma_list,$(3))]]); @@ -2558,7 +2819,7 @@ define triq_check.erl true -> halt(0); _ -> halt(1) catch error:undef -> - io:format("Undefined property or module~n"), + io:format("Undefined property or module?~n~p~n", [erlang:get_stacktrace()]), halt(0) end. endef @@ -2574,11 +2835,13 @@ triq: test-build endif else triq: test-build - $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename $(wildcard ebin/*.beam)))))) + $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \ + $(wildcard ebin/*.beam) $(call core_find,$(TEST_DIR)/,*.beam)))))) $(gen_verbose) $(call erlang,$(call triq_check.erl,all,undefined,$(MODULES))) endif endif +# Copyright (c) 2016, Loïc Hoguin # Copyright (c) 2015, Erlang Solutions Ltd. # This file is part of erlang.mk and subject to the terms of the ISC License. @@ -2587,22 +2850,22 @@ endif # Configuration. ifeq ($(XREF_CONFIG),) - XREF_ARGS := + XREFR_ARGS := else - XREF_ARGS := -c $(XREF_CONFIG) + XREFR_ARGS := -c $(XREF_CONFIG) endif XREFR ?= $(CURDIR)/xrefr export XREFR -XREFR_URL ?= https://github.com/inaka/xref_runner/releases/download/0.2.2/xrefr +XREFR_URL ?= https://github.com/inaka/xref_runner/releases/download/1.1.0/xrefr # Core targets. help:: - $(verbose) printf "%s\n" "" \ - "Xref targets:" \ - " xref Run Xrefr using $XREF_CONFIG as config file if defined" + $(verbose) printf '%s\n' '' \ + 'Xref targets:' \ + ' xref Run Xrefr using $$XREF_CONFIG as config file if defined' distclean:: distclean-xref @@ -2618,29 +2881,29 @@ xref: deps app $(XREFR) distclean-xref: $(gen_verbose) rm -rf $(XREFR) -# Copyright 2015, Viktor Söderqvist +# Copyright (c) 2016, Loïc Hoguin +# Copyright (c) 2015, Viktor Söderqvist # This file is part of erlang.mk and subject to the terms of the ISC License. -COVER_REPORT_DIR = cover +COVER_REPORT_DIR ?= cover +COVER_DATA_DIR ?= $(CURDIR) # Hook in coverage to ct ifdef COVER ifdef CT_RUN -# All modules in 'ebin' -COVER_MODS = $(notdir $(basename $(call core_ls,ebin/*.beam))) - +ifneq ($(wildcard $(TEST_DIR)),) test-build:: $(TEST_DIR)/ct.cover.spec -$(TEST_DIR)/ct.cover.spec: - $(verbose) echo Cover mods: $(COVER_MODS) +$(TEST_DIR)/ct.cover.spec: cover-data-dir $(gen_verbose) printf "%s\n" \ - '{incl_mods,[$(subst $(space),$(comma),$(COVER_MODS))]}.' \ - '{export,"$(CURDIR)/ct.coverdata"}.' > $@ + "{incl_app, '$(PROJECT)', details}." \ + '{export,"$(abspath $(COVER_DATA_DIR))/ct.coverdata"}.' > $@ CT_RUN += -cover $(TEST_DIR)/ct.cover.spec endif endif +endif # Core targets @@ -2649,6 +2912,13 @@ ifneq ($(COVER_REPORT_DIR),) tests:: $(verbose) $(MAKE) --no-print-directory cover-report endif + +cover-data-dir: | $(COVER_DATA_DIR) + +$(COVER_DATA_DIR): + $(verbose) mkdir -p $(COVER_DATA_DIR) +else +cover-data-dir: endif clean:: coverdata-clean @@ -2662,7 +2932,7 @@ help:: "Cover targets:" \ " cover-report Generate a HTML coverage report from previously collected" \ " cover data." \ - " all.coverdata Merge {eunit,ct}.coverdata into one coverdata file." \ + " all.coverdata Merge all coverdata files into all.coverdata." \ "" \ "If COVER=1 is set, coverage data is generated by the targets eunit and ct. The" \ "target tests additionally generates a HTML coverage report from the combined" \ @@ -2671,17 +2941,20 @@ help:: # Plugin specific targets -COVERDATA = $(filter-out all.coverdata,$(wildcard *.coverdata)) +COVERDATA = $(filter-out $(COVER_DATA_DIR)/all.coverdata,$(wildcard $(COVER_DATA_DIR)/*.coverdata)) .PHONY: coverdata-clean coverdata-clean: - $(gen_verbose) rm -f *.coverdata ct.cover.spec + $(gen_verbose) rm -f $(COVER_DATA_DIR)/*.coverdata $(TEST_DIR)/ct.cover.spec # Merge all coverdata files into one. -all.coverdata: $(COVERDATA) - $(gen_verbose) $(ERL) -eval ' \ - $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) \ - cover:export("$@"), halt(0).' +define cover_export.erl + $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) + cover:export("$(COVER_DATA_DIR)/$@"), halt(0). +endef + +all.coverdata: $(COVERDATA) cover-data-dir + $(gen_verbose) $(call erlang,$(cover_export.erl)) # These are only defined if COVER_REPORT_DIR is non-empty. Set COVER_REPORT_DIR to # empty if you want the coverdata files but not the HTML report. @@ -2691,6 +2964,7 @@ ifneq ($(COVER_REPORT_DIR),) cover-report-clean: $(gen_verbose) rm -rf $(COVER_REPORT_DIR) + $(if $(shell ls -A $(COVER_DATA_DIR)/),,$(verbose) rmdir $(COVER_DATA_DIR)) ifeq ($(COVERDATA),) cover-report: @@ -2699,7 +2973,7 @@ else # Modules which include eunit.hrl always contain one line without coverage # because eunit defines test/0 which is never called. We compensate for this. EUNIT_HRL_MODS = $(subst $(space),$(comma),$(shell \ - grep -e '^\s*-include.*include/eunit\.hrl"' src/*.erl \ + grep -H -e '^\s*-include.*include/eunit\.hrl"' src/*.erl \ | sed "s/^src\/\(.*\)\.erl:.*/'\1'/" | uniq)) define cover_report.erl @@ -2734,8 +3008,171 @@ define cover_report.erl endef cover-report: - $(gen_verbose) mkdir -p $(COVER_REPORT_DIR) + $(verbose) mkdir -p $(COVER_REPORT_DIR) $(gen_verbose) $(call erlang,$(cover_report.erl)) endif endif # ifneq ($(COVER_REPORT_DIR),) + +# Copyright (c) 2016, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: sfx + +ifdef RELX_REL +ifdef SFX + +# Configuration. + +SFX_ARCHIVE ?= $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/$(RELX_REL_NAME)-$(RELX_REL_VSN).tar.gz +SFX_OUTPUT_FILE ?= $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME).run + +# Core targets. + +rel:: sfx + +# Plugin-specific targets. + +define sfx_stub +#!/bin/sh + +TMPDIR=`mktemp -d` +ARCHIVE=`awk '/^__ARCHIVE_BELOW__$$/ {print NR + 1; exit 0;}' $$0` +FILENAME=$$(basename $$0) +REL=$${FILENAME%.*} + +tail -n+$$ARCHIVE $$0 | tar -xzf - -C $$TMPDIR + +$$TMPDIR/bin/$$REL console +RET=$$? + +rm -rf $$TMPDIR + +exit $$RET + +__ARCHIVE_BELOW__ +endef + +sfx: + $(call render_template,sfx_stub,$(SFX_OUTPUT_FILE)) + $(gen_verbose) cat $(SFX_ARCHIVE) >> $(SFX_OUTPUT_FILE) + $(verbose) chmod +x $(SFX_OUTPUT_FILE) + +endif +endif + +# Copyright (c) 2013-2017, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +# External plugins. + +DEP_PLUGINS ?= + +$(foreach p,$(DEP_PLUGINS),\ + $(eval $(if $(findstring /,$p),\ + $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\ + $(call core_dep_plugin,$p/plugins.mk,$p)))) + +# Copyright (c) 2013-2015, Loïc Hoguin +# Copyright (c) 2015-2016, Jean-Sébastien Pédron +# This file is part of erlang.mk and subject to the terms of the ISC License. + +# Fetch dependencies recursively (without building them). + +.PHONY: fetch-deps fetch-doc-deps fetch-rel-deps fetch-test-deps \ + fetch-shell-deps + +.PHONY: $(ERLANG_MK_RECURSIVE_DEPS_LIST) \ + $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) \ + $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) \ + $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) \ + $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST) + +fetch-deps: $(ERLANG_MK_RECURSIVE_DEPS_LIST) +fetch-doc-deps: $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) +fetch-rel-deps: $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) +fetch-test-deps: $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) +fetch-shell-deps: $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST) + +ifneq ($(SKIP_DEPS),) +$(ERLANG_MK_RECURSIVE_DEPS_LIST) \ +$(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) \ +$(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) \ +$(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) \ +$(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST): + $(verbose) :> $@ +else +# By default, we fetch "normal" dependencies. They are also included no +# matter the type of requested dependencies. +# +# $(ALL_DEPS_DIRS) includes $(BUILD_DEPS). + +$(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_DEPS_DIRS) +$(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST): $(ALL_DEPS_DIRS) $(ALL_DOC_DEPS_DIRS) +$(ERLANG_MK_RECURSIVE_REL_DEPS_LIST): $(ALL_DEPS_DIRS) $(ALL_REL_DEPS_DIRS) +$(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST): $(ALL_DEPS_DIRS) $(ALL_TEST_DEPS_DIRS) +$(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST): $(ALL_DEPS_DIRS) $(ALL_SHELL_DEPS_DIRS) + +# Allow to use fetch-deps and $(DEP_TYPES) to fetch multiple types of +# dependencies with a single target. +ifneq ($(filter doc,$(DEP_TYPES)),) +$(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_DOC_DEPS_DIRS) +endif +ifneq ($(filter rel,$(DEP_TYPES)),) +$(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_REL_DEPS_DIRS) +endif +ifneq ($(filter test,$(DEP_TYPES)),) +$(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_TEST_DEPS_DIRS) +endif +ifneq ($(filter shell,$(DEP_TYPES)),) +$(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_SHELL_DEPS_DIRS) +endif + +ERLANG_MK_RECURSIVE_TMP_LIST := $(abspath $(ERLANG_MK_TMP)/recursive-tmp-deps.log) + +$(ERLANG_MK_RECURSIVE_DEPS_LIST) \ +$(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) \ +$(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) \ +$(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) \ +$(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST): +ifeq ($(IS_APP)$(IS_DEP),) + $(verbose) mkdir -p $(ERLANG_MK_TMP) + $(verbose) rm -f $(ERLANG_MK_RECURSIVE_TMP_LIST) +endif +ifndef IS_APP + $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \ + $(MAKE) -C $$dep $@ \ + IS_APP=1 \ + ERLANG_MK_RECURSIVE_TMP_LIST=$(ERLANG_MK_RECURSIVE_TMP_LIST); \ + done +endif + $(verbose) set -e; for dep in $^ ; do \ + if ! grep -qs ^$$dep$$ $(ERLANG_MK_RECURSIVE_TMP_LIST); then \ + echo $$dep >> $(ERLANG_MK_RECURSIVE_TMP_LIST); \ + if grep -qs -E "^[[:blank:]]*include[[:blank:]]+(erlang\.mk|.*/erlang\.mk|.*ERLANG_MK_FILENAME.*)$$" \ + $$dep/GNUmakefile $$dep/makefile $$dep/Makefile; then \ + $(MAKE) -C $$dep fetch-deps \ + IS_DEP=1 \ + ERLANG_MK_RECURSIVE_TMP_LIST=$(ERLANG_MK_RECURSIVE_TMP_LIST); \ + fi \ + fi \ + done +ifeq ($(IS_APP)$(IS_DEP),) + $(verbose) sort < $(ERLANG_MK_RECURSIVE_TMP_LIST) | uniq > $@ + $(verbose) rm $(ERLANG_MK_RECURSIVE_TMP_LIST) +endif +endif # ifneq ($(SKIP_DEPS),) + +# List dependencies recursively. + +.PHONY: list-deps list-doc-deps list-rel-deps list-test-deps \ + list-shell-deps + +list-deps: $(ERLANG_MK_RECURSIVE_DEPS_LIST) +list-doc-deps: $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) +list-rel-deps: $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) +list-test-deps: $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) +list-shell-deps: $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST) + +list-deps list-doc-deps list-rel-deps list-test-deps list-shell-deps: + $(verbose) cat $^ diff --git a/etc/emqx.conf b/etc/emqx.conf index beb8995e1..78aa90215 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -578,7 +578,7 @@ mqtt.bridge.max_queue_len = 10000 mqtt.bridge.ping_down_interval = 1s ##------------------------------------------------------------------- -## MQTT Plugins +## Plugins ##------------------------------------------------------------------- ## The etc dir for plugins' config. @@ -595,7 +595,52 @@ mqtt.plugins.loaded_file = {{ platform_data_dir }}/loaded_plugins mqtt.plugins.expand_plugins_dir = {{ platform_plugins_dir }}/ ##-------------------------------------------------------------------- -## MQTT Listeners +## Modules +##-------------------------------------------------------------------- + +##-------------------------------------------------------------------- +## Presence Module + +## Enable Presence Module. +## +## Value: on | off +module.presence = on + +## Sets the QoS for presence MQTT message. +## +## Value: 0 | 1 | 2 +module.presence.qos = 1 + +##-------------------------------------------------------------------- +## Subscription Module + +## Enable Subscription Module. +## +## Value: on | off +module.subscription = off + +## Subscribe the Topics automatically when client connected. +## module.subscription.1.topic = $client/%c +## Qos of the subscription: 0 | 1 | 2 +## module.subscription.1.qos = 1 + +## module.subscription.2.topic = $user/%u +## module.subscription.2.qos = 1 + +##-------------------------------------------------------------------- +## Rewrite Module + +## Enable Rewrite Module. +## +## Value: on | off +module.rewrite = off + +## {rewrite, Topic, Re, Dest} +## module.rewrite.rule.1 = x/# ^x/y/(.+)$ z/y/$1 +## module.rewrite.rule.2 = y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2 + +##-------------------------------------------------------------------- +## Listeners ##-------------------------------------------------------------------- ##-------------------------------------------------------------------- diff --git a/include/emqx.hrl b/include/emqx.hrl index 7476446f1..1d80f009e 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -156,15 +156,12 @@ -type(mqtt_delivery() :: #mqtt_delivery{}). %%-------------------------------------------------------------------- -%% MQTT Route +%% Route %%-------------------------------------------------------------------- --record(mqtt_route, - { topic :: binary(), - node :: node() - }). +-record(route, { topic :: binary(), node :: node() }). --type(mqtt_route() :: #mqtt_route{}). +-type(route() :: #route{}). %%-------------------------------------------------------------------- %% MQTT Alarm diff --git a/include/emqx_common.hrl b/include/emqx_common.hrl new file mode 100644 index 000000000..f5ec9fbb2 --- /dev/null +++ b/include/emqx_common.hrl @@ -0,0 +1,13 @@ + +-define(record_to_map(Def, Rec), + maps:from_list(?record_to_proplist(Def, Rec))). + +-define(record_to_map(Def, Rec, Fields), + maps:from_list(?record_to_proplist(Def, Rec, Fields))). + +-define(record_to_proplist(Def, Rec), + lists:zip(record_info(fields, Def), tl(tuple_to_list(Rec)))). + +-define(record_to_proplist(Def, Rec, Fields), + [{K, V} || {K, V} <- ?record_to_proplist(Def, Rec), lists:member(K, Fields)]). + diff --git a/include/emqx_trie.hrl b/include/emqx_trie.hrl index ffd2acebc..ce57b4438 100644 --- a/include/emqx_trie.hrl +++ b/include/emqx_trie.hrl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ { node_id :: trie_node_id(), edge_count = 0 :: non_neg_integer(), topic :: binary() | undefined, - flags :: [retained | static] + flags :: list(atom()) }). -record(trie_edge, diff --git a/priv/emqx.schema b/priv/emqx.schema index dd539bffe..8c0a35397 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -762,7 +762,7 @@ end}. end}. %%------------------------------------------------------------------- -%% MQTT Plugins +%% Plugins %%------------------------------------------------------------------- {mapping, "mqtt.plugins.etc_dir", "emqx.plugins_etc_dir", [ @@ -778,7 +778,78 @@ end}. ]}. %%-------------------------------------------------------------------- -%% MQTT Listeners +%% Modules +%%-------------------------------------------------------------------- + +{mapping, "module.presence", "emqx.modules", [ + {default, off}, + {datatype, flag} +]}. + +{mapping, "module.presence.qos", "emqx.modules", [ + {default, 1}, + {datatype, integer}, + {validators, ["range:0-2"]} +]}. + +{mapping, "module.subscription", "emqx.modules", [ + {default, off}, + {datatype, flag} +]}. + +{mapping, "module.subscription.$id.topic", "emqx.modules", [ + {datatype, string} +]}. + +{mapping, "module.subscription.$id.qos", "emqx.modules", [ + {default, 1}, + {datatype, integer}, + {validators, ["range:0-2"]} +]}. + +{mapping, "module.rewrite", "emqx.modules", [ + {default, off}, + {datatype, flag} +]}. + +{mapping, "module.rewrite.rule.$id", "emqx.modules", [ + {datatype, string} +]}. + +{translation, "emqx.modules", fun(Conf) -> + Subscriptions = fun() -> + List = cuttlefish_variable:filter_by_prefix("module.subscription", Conf), + QosList = [Qos || {_, Qos} <- lists:sort([{I, Qos} || {[_,"subscription", I,"qos"], Qos} <- List])], + TopicList = [iolist_to_binary(Topic) || {_, Topic} <- + lists:sort([{I, Topic} || {[_,"subscription", I, "topic"], Topic} <- List])], + lists:zip(TopicList, QosList) + end, + Rewrites = fun() -> + Rules = cuttlefish_variable:filter_by_prefix("module.rewrite.rule", Conf), + lists:map(fun({[_, "rewrite", "rule", I], Rule}) -> + [Topic, Re, Dest] = string:tokens(Rule, " "), + {rewrite, list_to_binary(Topic), list_to_binary(Re), list_to_binary(Dest)} + end, Rules) + end, + lists:append([ + case cuttlefish:conf_get("module.presence", Conf) of %% Presence + true -> [{emqx_mod_presence, [{qos, cuttlefish:conf_get("module.presence.qos", Conf, 1)}]}]; + false -> [] + end, + case cuttlefish:conf_get("module.subscription", Conf) of %% Subscription + true -> [{emqx_mod_subscription, Subscriptions()}]; + false -> [] + end, + case cuttlefish:conf_get("module.rewrite", Conf) of %% Rewrite + true -> [{emqx_mod_rewrite, Rewrites()}]; + false -> [] + end + ]) +end}. + + +%%-------------------------------------------------------------------- +%% Listeners %%-------------------------------------------------------------------- %%-------------------------------------------------------------------- diff --git a/src/emqx_app.erl b/src/emqx_app.erl index 8b31f669f..ab83e870b 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -38,6 +38,7 @@ start(_Type, _Args) -> ekka:start(), {ok, Sup} = emqx_sup:start_link(), ok = register_acl_mod(), + emqx_modules:load(), start_autocluster(), register(emqx, self()), print_vsn(), @@ -45,6 +46,7 @@ start(_Type, _Args) -> -spec(stop(State :: term()) -> term()). stop(_State) -> + emqx_modules:unload(), catch emqx:stop_listeners(). %%-------------------------------------------------------------------- diff --git a/src/emqx_base62.erl b/src/emqx_base62.erl index f1fb44e7a..a3c00a979 100644 --- a/src/emqx_base62.erl +++ b/src/emqx_base62.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ -module(emqx_base62). --author("Feng Lee "). - -export([encode/1, decode/1]). %% @doc Encode an integer to base62 string diff --git a/src/emqx_boot.erl b/src/emqx_boot.erl index cea7f577a..6a2d76b8c 100644 --- a/src/emqx_boot.erl +++ b/src/emqx_boot.erl @@ -16,8 +16,6 @@ -module(emqx_boot). --author("Feng Lee "). - -export([apply_module_attributes/1, all_module_attributes/1]). %% only {F, Args}... diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index a252e4876..4bcfb4bc8 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -18,8 +18,6 @@ -module(emqx_gc). --author("Feng Lee "). - -export([conn_max_gc_count/0, reset_conn_gc_count/2, maybe_force_gc/2, maybe_force_gc/3]). diff --git a/src/emqx_keepalive.erl b/src/emqx_keepalive.erl index a3c1b44c1..7cfc4f48d 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -14,12 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc Client Keepalive - -module(emqx_keepalive). --author("Feng Lee "). - -export([start/3, check/1, cancel/1]). -record(keepalive, {statfun, statval, tsec, tmsg, tref, repeat = 0}). diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index 713e449d6..54a5bbd01 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ -module(emqx_misc). --author("Feng Lee "). - -export([merge_opts/2, start_timer/2, start_timer/3, cancel_timer/1, proc_stats/0, proc_stats/1]). diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl new file mode 100644 index 000000000..347213e31 --- /dev/null +++ b/src/emqx_mod_presence.erl @@ -0,0 +1,73 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_presence). + +-behaviour(emqx_gen_mod). + +-include("emqx.hrl"). + +-export([load/1, unload/1]). + +-export([on_client_connected/3, on_client_disconnected/3]). + +load(Env) -> + emqx:hook('client.connected', fun ?MODULE:on_client_connected/3, [Env]), + emqx:hook('client.disconnected', fun ?MODULE:on_client_disconnected/3, [Env]). + +on_client_connected(ConnAck, Client = #mqtt_client{client_id = ClientId, + username = Username, + peername = {IpAddr, _}, + clean_sess = CleanSess, + proto_ver = ProtoVer}, Env) -> + Payload = mochijson2:encode([{clientid, ClientId}, + {username, Username}, + {ipaddress, iolist_to_binary(emqx_net:ntoa(IpAddr))}, + {clean_sess, CleanSess}, + {protocol, ProtoVer}, + {connack, ConnAck}, + {ts, emqx_time:now_secs()}]), + Msg = message(qos(Env), topic(connected, ClientId), Payload), + emqx:publish(emqx_message:set_flag(sys, Msg)), + {ok, Client}. + +on_client_disconnected(Reason, #mqtt_client{client_id = ClientId, + username = Username}, Env) -> + Payload = mochijson2:encode([{clientid, ClientId}, + {username, Username}, + {reason, reason(Reason)}, + {ts, emqx_time:now_secs()}]), + Msg = message(qos(Env), topic(disconnected, ClientId), Payload), + emqx:publish(emqx_message:set_flag(sys, Msg)), ok. + +unload(_Env) -> + emqx:unhook('client.connected', fun ?MODULE:on_client_connected/3), + emqx:unhook('client.disconnected', fun ?MODULE:on_client_disconnected/3). + +message(Qos, Topic, Payload) -> + emqx_message:make(presence, Qos, Topic, iolist_to_binary(Payload)). + +topic(connected, ClientId) -> + emqx_topic:systop(list_to_binary(["clients/", ClientId, "/connected"])); +topic(disconnected, ClientId) -> + emqx_topic:systop(list_to_binary(["clients/", ClientId, "/disconnected"])). + +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 new file mode 100644 index 000000000..ca8d02551 --- /dev/null +++ b/src/emqx_mod_rewrite.erl @@ -0,0 +1,91 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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). + +-include_lib("emqx.hrl"). + +-export([load/1, unload/1]). + +-export([rewrite_subscribe/4, rewrite_unsubscribe/4, rewrite_publish/2]). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +load(Rules0) -> + Rules = compile(Rules0), + emqx:hook('client.subscribe', fun ?MODULE:rewrite_subscribe/4, [Rules]), + emqx:hook('client.unsubscribe',fun ?MODULE:rewrite_unsubscribe/4, [Rules]), + emqx:hook('message.publish', fun ?MODULE:rewrite_publish/2, [Rules]). + +rewrite_subscribe(_ClientId, _Username, TopicTable, Rules) -> + lager:info("Rewrite subscribe: ~p", [TopicTable]), + {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. + +rewrite_unsubscribe(_ClientId, _Username, TopicTable, Rules) -> + lager:info("Rewrite unsubscribe: ~p", [TopicTable]), + {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. + +rewrite_publish(Message=#mqtt_message{topic = Topic}, Rules) -> + %%TODO: this will not work if the client is always online. + RewriteTopic = + case get({rewrite, Topic}) of + undefined -> + DestTopic = match_rule(Topic, Rules), + put({rewrite, Topic}, DestTopic), DestTopic; + DestTopic -> + DestTopic + end, + {ok, Message#mqtt_message{topic = RewriteTopic}}. + +unload(_) -> + emqx:unhook('client.subscribe', fun ?MODULE:rewrite_subscribe/4), + emqx:unhook('client.unsubscribe',fun ?MODULE:rewrite_unsubscribe/4), + emqx:unhook('message.publish', fun ?MODULE:rewrite_publish/2). + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +match_rule(Topic, []) -> + Topic; + +match_rule(Topic, [{rewrite, Filter, MP, Dest} | Rules]) -> + case emqx_topic:match(Topic, Filter) of + true -> match_regx(Topic, MP, Dest); + false -> match_rule(Topic, Rules) + end. + +match_regx(Topic, MP, Dest) -> + case re:run(Topic, MP, [{capture, all_but_first, list}]) of + {match, Captured} -> + Vars = lists:zip(["\\$" ++ integer_to_list(I) + || I <- lists:seq(1, length(Captured))], Captured), + iolist_to_binary(lists:foldl( + fun({Var, Val}, Acc) -> + re:replace(Acc, Var, Val, [global]) + end, Dest, Vars)); + nomatch -> + Topic + end. + +compile(Rules) -> + lists:map(fun({rewrite, Topic, Re, Dest}) -> + {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 new file mode 100644 index 000000000..cae25842a --- /dev/null +++ b/src/emqx_mod_subscription.erl @@ -0,0 +1,61 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_mod_subscription). + +-behaviour(emqx_gen_mod). + +-include_lib("emqx.hrl"). + +-include_lib("emqx_mqtt.hrl"). + +-export([load/1, on_client_connected/3, unload/1]). + +-define(TAB, ?MODULE). + +%%-------------------------------------------------------------------- +%% Load/Unload Hook +%%-------------------------------------------------------------------- + +load(Topics) -> + emqx:hook('client.connected', fun ?MODULE:on_client_connected/3, [Topics]). + +on_client_connected(?CONNACK_ACCEPT, Client = #mqtt_client{client_id = ClientId, + client_pid = ClientPid, + username = Username}, Topics) -> + + Replace = fun(Topic) -> rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) end, + TopicTable = [{Replace(Topic), Qos} || {Topic, Qos} <- Topics], + ClientPid ! {subscribe, TopicTable}, + {ok, Client}; + +on_client_connected(_ConnAck, _Client, _State) -> + ok. + +unload(_) -> + emqx:unhook('client.connected', fun ?MODULE:on_client_connected/3). + +%%-------------------------------------------------------------------- +%% Internal Functions +%%-------------------------------------------------------------------- + +rep(<<"%c">>, ClientId, Topic) -> + emqx_topic:feed_var(<<"%c">>, ClientId, Topic); +rep(<<"%u">>, undefined, Topic) -> + Topic; +rep(<<"%u">>, Username, Topic) -> + emqx_topic:feed_var(<<"%u">>, Username, Topic). + diff --git a/src/emqx_modules.erl b/src/emqx_modules.erl new file mode 100644 index 000000000..3f51dbcdb --- /dev/null +++ b/src/emqx_modules.erl @@ -0,0 +1,33 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_modules). + +-export([load/0, unload/0]). + +load() -> + lists:foreach( + fun({Mod, Env}) -> + ok = Mod:load(Env), + io:format("Load ~s module successfully.~n", [Mod]) + end, emqx:env(modules, [])). + +unload() -> + lists:foreach( + fun({Mod, Env}) -> + Mod:unload(Env) end, + emqx:env(modules, [])). + diff --git a/src/emqx_net.erl b/src/emqx_net.erl index a2f38012e..0e666dfb1 100644 --- a/src/emqx_net.erl +++ b/src/emqx_net.erl @@ -16,8 +16,6 @@ -module(emqx_net). --author("Feng Lee "). - -include_lib("kernel/include/inet.hrl"). -export([tcp_name/3, tcp_host/1, getopts/2, setopts/2, getaddr/2, diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index 8c8857fea..e65f34c6c 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ -module(emqx_pmon). --author("Feng Lee "). - -export([new/0, monitor/2, demonitor/2, erase/2]). -type(pmon() :: {?MODULE, map()}). diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 81631c022..c07d9759d 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -28,11 +28,14 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). --export([start_link/0, topics/0, local_topics/0]). +-export([start_link/1]). %% For eunit tests -export([start/0, stop/0]). +%% Topics +-export([topics/0, local_topics/0]). + %% Route APIs -export([add_route/1, get_routes/1, del_route/1, has_route/1]). @@ -49,7 +52,7 @@ -export([dump/0]). --record(state, {stats_timer}). +-record(state, {stats_fun, stats_timer}). -define(ROUTER, ?MODULE). @@ -60,21 +63,21 @@ %%-------------------------------------------------------------------- mnesia(boot) -> - ok = ekka_mnesia:create_table(mqtt_route, [ + ok = ekka_mnesia:create_table(route, [ {type, bag}, {ram_copies, [node()]}, - {record_name, mqtt_route}, - {attributes, record_info(fields, mqtt_route)}]); + {record_name, route}, + {attributes, record_info(fields, route)}]); mnesia(copy) -> - ok = ekka_mnesia:copy_table(mqtt_route, ram_copies). + ok = ekka_mnesia:copy_table(route, ram_copies). %%-------------------------------------------------------------------- %% Start the Router %%-------------------------------------------------------------------- -start_link() -> - gen_server:start_link({local, ?ROUTER}, ?MODULE, [], []). +start_link(StatsFun) -> + gen_server:start_link({local, ?ROUTER}, ?MODULE, [StatsFun], []). %%-------------------------------------------------------------------- %% Topics @@ -82,39 +85,39 @@ start_link() -> -spec(topics() -> list(binary())). topics() -> - mnesia:dirty_all_keys(mqtt_route). + mnesia:dirty_all_keys(route). -spec(local_topics() -> list(binary())). local_topics() -> - ets:select(mqtt_local_route, [{{'$1', '_'}, [], ['$1']}]). + ets:select(local_route, [{{'$1', '_'}, [], ['$1']}]). %%-------------------------------------------------------------------- %% Match API %%-------------------------------------------------------------------- %% @doc Match Routes. --spec(match(Topic:: binary()) -> [mqtt_route()]). +-spec(match(Topic:: binary()) -> [route()]). match(Topic) when is_binary(Topic) -> %% Optimize: ets??? Matched = mnesia:ets(fun emqx_trie:match/1, [Topic]), %% Optimize: route table will be replicated to all nodes. - lists:append([ets:lookup(mqtt_route, To) || To <- [Topic | Matched]]). + lists:append([ets:lookup(route, To) || To <- [Topic | Matched]]). %% @doc Print Routes. -spec(print(Topic :: binary()) -> [ok]). print(Topic) -> [io:format("~s -> ~s~n", [To, Node]) || - #mqtt_route{topic = To, node = Node} <- match(Topic)]. + #route{topic = To, node = Node} <- match(Topic)]. %%-------------------------------------------------------------------- %% Route Management API %%-------------------------------------------------------------------- %% @doc Add Route. --spec(add_route(binary() | mqtt_route()) -> ok | {error, Reason :: term()}). +-spec(add_route(binary() | route()) -> ok | {error, Reason :: term()}). add_route(Topic) when is_binary(Topic) -> - add_route(#mqtt_route{topic = Topic, node = node()}); -add_route(Route = #mqtt_route{topic = Topic}) -> + add_route(#route{topic = Topic, node = node()}); +add_route(Route = #route{topic = Topic}) -> case emqx_topic:wildcard(Topic) of true -> case mnesia:is_transaction() of true -> add_trie_route(Route); @@ -126,23 +129,23 @@ add_route(Route = #mqtt_route{topic = Topic}) -> add_direct_route(Route) -> mnesia:async_dirty(fun mnesia:write/1, [Route]). -add_trie_route(Route = #mqtt_route{topic = Topic}) -> - case mnesia:wread({mqtt_route, Topic}) of +add_trie_route(Route = #route{topic = Topic}) -> + case mnesia:wread({route, Topic}) of [] -> emqx_trie:insert(Topic); _ -> ok end, mnesia:write(Route). %% @doc Lookup Routes --spec(get_routes(binary()) -> [mqtt_route()]). +-spec(get_routes(binary()) -> [route()]). get_routes(Topic) -> - ets:lookup(mqtt_route, Topic). + ets:lookup(route, Topic). %% @doc Delete Route --spec(del_route(binary() | mqtt_route()) -> ok | {error, Reason :: term()}). +-spec(del_route(binary() | route()) -> ok | {error, Reason :: term()}). del_route(Topic) when is_binary(Topic) -> - del_route(#mqtt_route{topic = Topic, node = node()}); -del_route(Route = #mqtt_route{topic = Topic}) -> + del_route(#route{topic = Topic, node = node()}); +del_route(Route = #route{topic = Topic}) -> case emqx_topic:wildcard(Topic) of true -> case mnesia:is_transaction() of true -> del_trie_route(Route); @@ -154,8 +157,8 @@ del_route(Route = #mqtt_route{topic = Topic}) -> del_direct_route(Route) -> mnesia:async_dirty(fun mnesia:delete_object/1, [Route]). -del_trie_route(Route = #mqtt_route{topic = Topic}) -> - case mnesia:wread({mqtt_route, Topic}) of +del_trie_route(Route = #route{topic = Topic}) -> + case mnesia:wread({route, Topic}) of [Route] -> %% Remove route and trie mnesia:delete_object(Route), emqx_trie:delete(Topic); @@ -167,7 +170,7 @@ del_trie_route(Route = #mqtt_route{topic = Topic}) -> %% @doc Has route? -spec(has_route(binary()) -> boolean()). has_route(Topic) when is_binary(Topic) -> - ets:member(mqtt_route, Topic). + ets:member(route, Topic). %% @private -spec(trans(function(), list(any())) -> ok | {error, term()}). @@ -183,7 +186,7 @@ trans(Fun, Args) -> -spec(get_local_routes() -> list({binary(), node()})). get_local_routes() -> - ets:tab2list(mqtt_local_route). + ets:tab2list(local_route). -spec(add_local_route(binary()) -> ok). add_local_route(Topic) -> @@ -195,15 +198,15 @@ del_local_route(Topic) -> -spec(match_local(binary()) -> [mqtt_route()]). match_local(Name) -> - case ets:info(mqtt_local_route, size) of + case ets:info(local_route, size) of 0 -> []; _ -> ets:foldl( fun({Filter, Node}, Matched) -> case emqx_topic:match(Name, Filter) of - true -> [#mqtt_route{topic = {local, Filter}, node = Node} | Matched]; + true -> [#route{topic = {local, Filter}, node = Node} | Matched]; false -> Matched end - end, [], mqtt_local_route) + end, [], local_route) end. -spec(clean_local_routes() -> ok). @@ -211,7 +214,7 @@ clean_local_routes() -> gen_server:call(?ROUTER, clean_local_routes). dump() -> - [{route, ets:tab2list(mqtt_route)}, {local_route, ets:tab2list(mqtt_local_route)}]. + [{route, ets:tab2list(route)}, {local_route, ets:tab2list(local_route)}]. %% For unit test. start() -> @@ -224,23 +227,23 @@ stop() -> %% gen_server Callbacks %%-------------------------------------------------------------------- -init([]) -> +init([StatsFun]) -> ekka:monitor(membership), - ets:new(mqtt_local_route, [set, named_table, protected]), + ets:new(local_route, [set, named_table, protected]), {ok, TRef} = timer:send_interval(timer:seconds(1), stats), - {ok, #state{stats_timer = TRef}}. + {ok, #state{stats_fun = StatsFun, stats_timer = TRef}}. handle_call({add_local_route, Topic}, _From, State) -> %% why node()...? - ets:insert(mqtt_local_route, {Topic, node()}), + ets:insert(local_route, {Topic, node()}), {reply, ok, State}; handle_call({del_local_route, Topic}, _From, State) -> - ets:delete(mqtt_local_route, Topic), + ets:delete(local_route, Topic), {reply, ok, State}; handle_call(clean_local_routes, _From, State) -> - ets:delete_all_objects(mqtt_local_route), + ets:delete_all_objects(local_route), {reply, ok, State}; handle_call(stop, _From, State) -> @@ -253,19 +256,15 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info({membership, {mnesia, down, Node}}, State) -> - global:trans({?LOCK, self()}, - fun() -> - clean_routes_(Node), - update_stats_() - end), - {noreply, State, hibernate}; + global:trans({?LOCK, self()}, fun() -> clean_routes_(Node) end), + handle_info(stats, State); handle_info({membership, _Event}, State) -> %% ignore {noreply, State}; -handle_info(stats, State) -> - update_stats_(), +handle_info(stats, State = #state{stats_fun = StatsFun}) -> + StatsFun(mnesia:table_info(route, size)), {noreply, State, hibernate}; handle_info(_Info, State) -> @@ -282,15 +281,12 @@ code_change(_OldVsn, State, _Extra) -> %% Internal Functions %%-------------------------------------------------------------------- -%% Clean Routes on Node +%% Clean routes on the down node. clean_routes_(Node) -> - Pattern = #mqtt_route{_ = '_', node = Node}, + Pattern = #route{_ = '_', node = Node}, Clean = fun() -> - [mnesia:delete_object(mqtt_route, R, write) || - R <- mnesia:match_object(mqtt_route, Pattern, write)] + [mnesia:delete_object(route, R, write) || + R <- mnesia:match_object(route, Pattern, write)] end, mnesia:transaction(Clean). -update_stats_() -> - emqx_stats:setstats('routes/count', 'routes/max', mnesia:table_info(mqtt_route, size)). - diff --git a/src/emqx_router_sup.erl b/src/emqx_router_sup.erl new file mode 100644 index 000000000..ddcbfe097 --- /dev/null +++ b/src/emqx_router_sup.erl @@ -0,0 +1,38 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% +%% 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_router_sup). + +-behaviour(supervisor). + +-export([start_link/0]). + +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + StatsFun = emqx_stats:statsfun('routes/count', 'routes/max'), + SupFlags = #{strategy => one_for_all, intensity => 1, period => 5}, + Router = #{id => emqx_router, + start => {emqx_router, start_link, [StatsFun]}, + restart => permanent, + shutdown => 30000, + type => worker, + modules => [emqx_router]}, + {ok, {SupFlags, [Router]}}. + diff --git a/src/emqx_time.erl b/src/emqx_time.erl index 4622c43db..d954c8204 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ -module(emqx_time). --author("Feng Lee "). - -export([seed/0, now_secs/0, now_secs/1, now_ms/0, now_ms/1, ts_from_ms/1]). seed() -> diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index a4f9fc4d8..7f177e8d0 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -16,12 +16,8 @@ -module(emqx_topic). --author("Feng Lee "). - -include("emqx_mqtt.hrl"). --include("emqx_internal.hrl"). - -import(lists, [reverse/1]). -export([match/2, validate/1, triples/1, words/1, wildcard/1]). @@ -206,8 +202,14 @@ parse(Topic, Options) -> {Topic, Options}. if_not_contain(Key, Options, Fun) when Key == local; Key == fastlane -> - ?IF(lists:member(Key, Options), error(invalid_topic), Fun()); + case lists:member(Key, Options) of + true -> error(invalid_topic); + false -> Fun() + end; if_not_contain(share, Options, Fun) -> - ?IF(lists:keyfind(share, 1, Options), error(invalid_topic), Fun()). + case lists:keyfind(share, 1, Options) of + true -> error(invalid_topic); + false -> Fun() + end. diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index 1b090a75f..f99bbb725 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -41,21 +41,21 @@ -spec(mnesia(boot | copy) -> ok). mnesia(boot) -> %% Trie Table - ok = ekka_mnesia:create_table(mqtt_trie, [ + ok = ekka_mnesia:create_table(trie, [ {ram_copies, [node()]}, {record_name, trie}, {attributes, record_info(fields, trie)}]), %% Trie Node Table - ok = ekka_mnesia:create_table(mqtt_trie_node, [ + ok = ekka_mnesia:create_table(trie_node, [ {ram_copies, [node()]}, {record_name, trie_node}, {attributes, record_info(fields, trie_node)}]); mnesia(copy) -> %% Copy Trie Table - ok = ekka_mnesia:copy_table(mqtt_trie), + ok = ekka_mnesia:copy_table(trie), %% Copy Trie Node Table - ok = ekka_mnesia:copy_table(mqtt_trie_node). + ok = ekka_mnesia:copy_table(trie_node). %%-------------------------------------------------------------------- %% Trie API @@ -64,7 +64,7 @@ mnesia(copy) -> %% @doc Insert topic to trie -spec(insert(Topic :: binary()) -> ok). insert(Topic) when is_binary(Topic) -> - case mnesia:read(mqtt_trie_node, Topic) of + case mnesia:read(trie_node, Topic) of [#trie_node{topic = Topic}] -> ok; [TrieNode = #trie_node{topic = undefined}] -> @@ -85,14 +85,14 @@ match(Topic) when is_binary(Topic) -> %% @doc Lookup a Trie Node -spec(lookup(NodeId :: binary()) -> [#trie_node{}]). lookup(NodeId) -> - mnesia:read(mqtt_trie_node, NodeId). + mnesia:read(trie_node, NodeId). %% @doc Delete topic from trie -spec(delete(Topic :: binary()) -> ok). delete(Topic) when is_binary(Topic) -> - case mnesia:read(mqtt_trie_node, Topic) of + case mnesia:read(trie_node, Topic) of [#trie_node{edge_count = 0}] -> - mnesia:delete({mqtt_trie_node, Topic}), + mnesia:delete({trie_node, Topic}), delete_path(lists:reverse(emqx_topic:triples(Topic))); [TrieNode] -> write_trie_node(TrieNode#trie_node{topic = undefined}); @@ -108,9 +108,9 @@ delete(Topic) when is_binary(Topic) -> %% @doc Add path to trie tree. add_path({Node, Word, Child}) -> Edge = #trie_edge{node_id = Node, word = Word}, - case mnesia:read(mqtt_trie_node, Node) of + case mnesia:read(trie_node, Node) of [TrieNode = #trie_node{edge_count = Count}] -> - case mnesia:wread({mqtt_trie, Edge}) of + case mnesia:wread({trie, Edge}) of [] -> write_trie_node(TrieNode#trie_node{edge_count = Count+1}), write_trie(#trie{edge = Edge, node_id = Child}); @@ -131,11 +131,11 @@ match_node(NodeId, Words) -> match_node(NodeId, Words, []). match_node(NodeId, [], ResAcc) -> - mnesia:read(mqtt_trie_node, NodeId) ++ 'match_#'(NodeId, ResAcc); + mnesia:read(trie_node, NodeId) ++ 'match_#'(NodeId, ResAcc); match_node(NodeId, [W|Words], ResAcc) -> lists:foldl(fun(WArg, Acc) -> - case mnesia:read(mqtt_trie, #trie_edge{node_id = NodeId, word = WArg}) of + case mnesia:read(trie, #trie_edge{node_id = NodeId, word = WArg}) of [#trie{node_id = ChildId}] -> match_node(ChildId, Words, Acc); [] -> Acc end @@ -144,9 +144,9 @@ match_node(NodeId, [W|Words], ResAcc) -> %% @private %% @doc Match node with '#'. 'match_#'(NodeId, ResAcc) -> - case mnesia:read(mqtt_trie, #trie_edge{node_id = NodeId, word = '#'}) of + case mnesia:read(trie, #trie_edge{node_id = NodeId, word = '#'}) of [#trie{node_id = ChildId}] -> - mnesia:read(mqtt_trie_node, ChildId) ++ ResAcc; + mnesia:read(trie_node, ChildId) ++ ResAcc; [] -> ResAcc end. @@ -156,10 +156,10 @@ match_node(NodeId, [W|Words], ResAcc) -> delete_path([]) -> ok; delete_path([{NodeId, Word, _} | RestPath]) -> - mnesia:delete({mqtt_trie, #trie_edge{node_id = NodeId, word = Word}}), - case mnesia:read(mqtt_trie_node, NodeId) of + mnesia:delete({trie, #trie_edge{node_id = NodeId, word = Word}}), + case mnesia:read(trie_node, NodeId) of [#trie_node{edge_count = 1, topic = undefined}] -> - mnesia:delete({mqtt_trie_node, NodeId}), + mnesia:delete({trie_node, NodeId}), delete_path(RestPath); [TrieNode = #trie_node{edge_count = 1, topic = _}] -> write_trie_node(TrieNode#trie_node{edge_count = 0}); @@ -171,9 +171,9 @@ delete_path([{NodeId, Word, _} | RestPath]) -> %% @private write_trie(Trie) -> - mnesia:write(mqtt_trie, Trie, write). + mnesia:write(trie, Trie, write). %% @private write_trie_node(TrieNode) -> - mnesia:write(mqtt_trie_node, TrieNode, write). + mnesia:write(trie_node, TrieNode, write). diff --git a/test/emqx_base62_SUITE.erl b/test/emqx_base62_SUITE.erl new file mode 100644 index 000000000..6baf724d1 --- /dev/null +++ b/test/emqx_base62_SUITE.erl @@ -0,0 +1,35 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_base62_SUITE). + +-include_lib("eunit/include/eunit.hrl"). + +-define(BASE62, emqx_base62). + +-compile(export_all). + +all() -> [t_base62_encode]. + +t_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)), + Y = ?BASE62:decode(?BASE62:encode(Y)). diff --git a/test/emqx_guid_SUITE.erl b/test/emqx_guid_SUITE.erl new file mode 100644 index 000000000..a6978bac8 --- /dev/null +++ b/test/emqx_guid_SUITE.erl @@ -0,0 +1,41 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% +%% 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_guid_SUITE). + +-include_lib("eunit/include/eunit.hrl"). + +-compile(export_all). + +all() -> [t_guid_gen, t_guid_hexstr, t_guid_base62]. + +t_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. + +t_guid_hexstr(_) -> + Guid = emqx_guid:gen(), + ?assertEqual(Guid, emqx_guid:from_hexstr(emqx_guid:to_hexstr(Guid))). + +t_guid_base62(_) -> + Guid = emqx_guid:gen(), + ?assertEqual(Guid, emqx_guid:from_base62(emqx_guid:to_base62(Guid))). + diff --git a/test/emqx_inflight_SUITE.erl b/test/emqx_inflight_SUITE.erl index c391d08ea..903d5c7b5 100644 --- a/test/emqx_inflight_SUITE.erl +++ b/test/emqx_inflight_SUITE.erl @@ -16,8 +16,6 @@ -module(emqx_inflight_SUITE). --author("Feng Lee "). - -include_lib("eunit/include/eunit.hrl"). %% CT diff --git a/test/emqx_keepalive_SUITE.erl b/test/emqx_keepalive_SUITE.erl new file mode 100644 index 000000000..270d78830 --- /dev/null +++ b/test/emqx_keepalive_SUITE.erl @@ -0,0 +1,43 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% +%% 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_keepalive_SUITE). + +-compile(export_all). + +all() -> [{group, keepalive}]. + +groups() -> [{keepalive, [], [t_keepalive]}]. + +%%-------------------------------------------------------------------- +%% Keepalive +%%-------------------------------------------------------------------- + +t_keepalive(_) -> + {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), + [resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). + +keepalive_recv(KA, Acc) -> + receive + {keepalive, timeout} -> + case emqx_keepalive:check(KA) of + {ok, KA1} -> keepalive_recv(KA1, [resumed | Acc]); + {error, timeout} -> [timeout | Acc] + end + after 4000 -> + Acc + end. + diff --git a/test/emqx_misc_SUITE.erl b/test/emqx_misc_SUITE.erl new file mode 100644 index 000000000..e1283a9d4 --- /dev/null +++ b/test/emqx_misc_SUITE.erl @@ -0,0 +1,46 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% +%% 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_misc_SUITE). + +-include_lib("eunit/include/eunit.hrl"). + +-compile(export_all). + +-define(SOCKOPTS, [binary, + {packet, raw}, + {reuseaddr, true}, + {backlog, 512}, + {nodelay, true}]). + +all() -> [t_merge_opts]. + +t_merge_opts(_) -> + Opts = emqx_misc:merge_opts(?SOCKOPTS, [raw, + binary, + {backlog, 1024}, + {nodelay, false}, + {max_clients, 1024}, + {acceptors, 16}]), + ?assertEqual(1024, proplists:get_value(backlog, Opts)), + ?assertEqual(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). diff --git a/test/emqx_pqueue_SUITE.erl b/test/emqx_pqueue_SUITE.erl new file mode 100644 index 000000000..c798bf100 --- /dev/null +++ b/test/emqx_pqueue_SUITE.erl @@ -0,0 +1,69 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% +%% 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_pqueue_SUITE). + +-include_lib("eunit/include/eunit.hrl"). + +-compile(export_all). + +-define(PQ, emqx_pqueue). + +all() -> [t_priority_queue_plen, t_priority_queue_out2]. + +t_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). + +t_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). diff --git a/test/emqx_router_SUITE.erl b/test/emqx_router_SUITE.erl index d48f643e1..c46ae27c0 100644 --- a/test/emqx_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -16,12 +16,12 @@ -module(emqx_router_SUITE). --compile(export_all). - -include("emqx.hrl"). -include_lib("eunit/include/eunit.hrl"). +-compile(export_all). + -define(R, emqx_router). all() -> @@ -35,7 +35,7 @@ groups() -> t_match_route, t_print, t_has_route, - router_unused]}, + t_unused]}, {local_route, [sequence], [t_get_local_topics, t_add_del_local_route, @@ -44,7 +44,7 @@ groups() -> init_per_suite(Config) -> ekka:start(), ekka_mnesia:ensure_started(), - {ok, _R} = emqx_router:start(), + {ok, _} = emqx_router_sup:start_link(), Config. end_per_suite(_Config) -> @@ -81,10 +81,10 @@ t_match_route(_) -> ?R:add_route(<<"a/+/c">>), ?R:add_route(<<"a/b/#">>), ?R:add_route(<<"#">>), - ?assertEqual([#mqtt_route{topic = <<"#">>, node = Node}, - #mqtt_route{topic = <<"a/+/c">>, node = Node}, - #mqtt_route{topic = <<"a/b/#">>, node = Node}, - #mqtt_route{topic = <<"a/b/c">>, node = Node}], + ?assertEqual([#route{topic = <<"#">>, node = Node}, + #route{topic = <<"a/+/c">>, node = Node}, + #route{topic = <<"a/b/#">>, node = Node}, + #route{topic = <<"a/b/c">>, node = Node}], lists:sort(?R:match(<<"a/b/c">>))). t_has_route(_) -> @@ -119,12 +119,12 @@ t_match_local_route(_) -> ?R:add_local_route(<<"a/+/c">>), ?R:add_local_route(<<"a/b/#">>), ?R:add_local_route(<<"#">>), - Matched = [Topic || #mqtt_route{topic = {local, Topic}} <- ?R:match_local(<<"a/b/c">>)], + Matched = [Topic || #route{topic = {local, Topic}} <- ?R:match_local(<<"a/b/c">>)], ?assertEqual([<<"#">>, <<"a/+/c">>, <<"a/b/#">>, <<"a/b/c">>], lists:sort(Matched)). clear_tables() -> ?R:clean_local_routes(), - lists:foreach(fun mnesia:clear_table/1, [mqtt_route, mqtt_trie, mqtt_trie_node]). + lists:foreach(fun mnesia:clear_table/1, [route, trie, trie_node]). router_add_del(_) -> %% Add @@ -132,9 +132,9 @@ router_add_del(_) -> ?R:add_route(<<"a/b/c">>), ?R:add_route(<<"+/#">>), Routes = [R1, R2 | _] = [ - #mqtt_route{topic = <<"#">>, node = node()}, - #mqtt_route{topic = <<"+/#">>, node = node()}, - #mqtt_route{topic = <<"a/b/c">>, node = node()}], + #route{topic = <<"#">>, node = node()}, + #route{topic = <<"+/#">>, node = node()}, + #route{topic = <<"a/b/c">>, node = node()}], Routes = lists:sort(?R:match(<<"a/b/c">>)), %% Batch Add @@ -147,7 +147,7 @@ router_add_del(_) -> {atomic, []} = mnesia:transaction(fun emqx_trie:lookup/1, [<<"a/b/c">>]), %% Batch Del - R3 = #mqtt_route{topic = <<"#">>, node = 'a@127.0.0.1'}, + R3 = #route{topic = <<"#">>, node = 'a@127.0.0.1'}, ?R:add_route(R3), ?R:del_route(R1), ?R:del_route(R2), @@ -155,16 +155,17 @@ router_add_del(_) -> [] = lists:sort(?R:match(<<"a/b/c">>)). t_print(_) -> - Routes = [#mqtt_route{topic = <<"a/b/c">>, node = node()}, - #mqtt_route{topic = <<"#">>, node = node()}, - #mqtt_route{topic = <<"+/#">>, node = node()}], + Routes = [#route{topic = <<"a/b/c">>, node = node()}, + #route{topic = <<"#">>, node = node()}, + #route{topic = <<"+/#">>, node = node()}], lists:foreach(fun(R) -> ?R:add_route(R) end, Routes), ?R:print(<<"a/b/c">>), ?R:del_route(<<"+/#">>), ?R:del_route(<<"a/b/c">>), ?R:del_route(<<"#">>). -router_unused(_) -> - gen_server:call(emqx_router, bad_call), - gen_server:cast(emqx_router, bad_msg), - emqx_router ! bad_info. +t_unused(_) -> + gen_server:call(?R, bad_call), + gen_server:cast(?R, bad_msg), + ?R ! bad_info. + diff --git a/test/emqx_time_SUITE.erl b/test/emqx_time_SUITE.erl new file mode 100644 index 000000000..9a0514e74 --- /dev/null +++ b/test/emqx_time_SUITE.erl @@ -0,0 +1,28 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% +%% 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_time_SUITE). + +-include_lib("eunit/include/eunit.hrl"). + +-compile(export_all). + +all() -> [t_time_now_to]. + +t_time_now_to(_) -> + emqx_time:seed(), + emqx_time:now_secs(), + emqx_time:now_ms(). diff --git a/test/emqx_trie_SUITE.erl b/test/emqx_trie_SUITE.erl index fd7fe4e38..69f251372 100644 --- a/test/emqx_trie_SUITE.erl +++ b/test/emqx_trie_SUITE.erl @@ -133,5 +133,5 @@ t_delete3(_) -> end). clear_tables() -> - lists:foreach(fun mnesia:clear_table/1, [mqtt_trie, mqtt_trie_node]). + lists:foreach(fun mnesia:clear_table/1, [trie, trie_node]). From a8aeb5ac175188fcf58634cd64e0289705c66b03 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 27 Feb 2018 09:13:14 +0800 Subject: [PATCH 021/520] Rename the 'mqtt_route' record to 'route' --- erlang.mk | 1199 ++++++++++++++----------------------------- src/emqx_pubsub.erl | 4 +- src/emqx_router.erl | 2 +- 3 files changed, 384 insertions(+), 821 deletions(-) diff --git a/erlang.mk b/erlang.mk index adf9d98b8..e348d4493 100644 --- a/erlang.mk +++ b/erlang.mk @@ -1,4 +1,4 @@ -# Copyright (c) 2013-2016, Loïc Hoguin +# Copyright (c) 2013-2015, Loïc Hoguin # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -12,23 +12,11 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -.PHONY: all app apps deps search rel relup docs install-docs check tests clean distclean help erlang-mk +.PHONY: all app apps deps search rel docs install-docs check tests clean distclean help erlang-mk ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST))) -export ERLANG_MK_FILENAME -ERLANG_MK_VERSION = 2017.08.28-22-gf545564 -ERLANG_MK_WITHOUT = - -# Make 3.81 and 3.82 are deprecated. - -ifeq ($(MAKE_VERSION),3.81) -$(warning Please upgrade to GNU Make 4 or later: https://erlang.mk/guide/installation.html) -endif - -ifeq ($(MAKE_VERSION),3.82) -$(warning Please upgrade to GNU Make 4 or later: https://erlang.mk/guide/installation.html) -endif +ERLANG_MK_VERSION = 2.0.0-pre.2-130-gc6fe5ea # Core configuration. @@ -37,7 +25,6 @@ PROJECT := $(strip $(PROJECT)) PROJECT_VERSION ?= rolling PROJECT_MOD ?= $(PROJECT)_app -PROJECT_ENV ?= [] # Verbosity. @@ -98,8 +85,6 @@ all:: deps app rel rel:: $(verbose) : -relup:: deps app - check:: tests clean:: clean-crashdump @@ -117,7 +102,7 @@ distclean-tmp: help:: $(verbose) printf "%s\n" \ "erlang.mk (version $(ERLANG_MK_VERSION)) is distributed under the terms of the ISC License." \ - "Copyright (c) 2013-2016 Loïc Hoguin " \ + "Copyright (c) 2013-2015 Loïc Hoguin " \ "" \ "Usage: [V=1] $(MAKE) [target]..." \ "" \ @@ -125,8 +110,6 @@ help:: " all Run deps, app and rel targets in that order" \ " app Compile the project" \ " deps Fetch dependencies (if needed) and compile them" \ - " fetch-deps Fetch dependencies recursively (if needed) without compiling them" \ - " list-deps List dependencies recursively on stdout" \ " search q=... Search for a package in the built-in index" \ " rel Build a release for this project, if applicable" \ " docs Build the documentation for this project" \ @@ -165,7 +148,30 @@ else core_native_path = $1 endif -core_http_get = curl -Lf$(if $(filter-out 0,$(V)),,s)o $(call core_native_path,$1) $2 +ifeq ($(shell which wget 2>/dev/null | wc -l), 1) +define core_http_get + wget --no-check-certificate -O $(1) $(2)|| rm $(1) +endef +else +define core_http_get.erl + ssl:start(), + inets:start(), + case httpc:request(get, {"$(2)", []}, [{autoredirect, true}], []) of + {ok, {{_, 200, _}, _, Body}} -> + case file:write_file("$(1)", Body) of + ok -> ok; + {error, R1} -> halt(R1) + end; + {error, R2} -> + halt(R2) + end, + halt(0). +endef + +define core_http_get + $(call erlang,$(call core_http_get.erl,$(call core_native_path,$1),$2)) +endef +endif core_eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))) @@ -185,102 +191,17 @@ ERLANG_MK_COMMIT ?= ERLANG_MK_BUILD_CONFIG ?= build.config ERLANG_MK_BUILD_DIR ?= .erlang.mk.build -erlang-mk: WITHOUT ?= $(ERLANG_MK_WITHOUT) erlang-mk: git clone $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR) ifdef ERLANG_MK_COMMIT cd $(ERLANG_MK_BUILD_DIR) && git checkout $(ERLANG_MK_COMMIT) endif if [ -f $(ERLANG_MK_BUILD_CONFIG) ]; then cp $(ERLANG_MK_BUILD_CONFIG) $(ERLANG_MK_BUILD_DIR)/build.config; fi - $(MAKE) -C $(ERLANG_MK_BUILD_DIR) WITHOUT='$(strip $(WITHOUT))' + $(MAKE) -C $(ERLANG_MK_BUILD_DIR) cp $(ERLANG_MK_BUILD_DIR)/erlang.mk ./erlang.mk rm -rf $(ERLANG_MK_BUILD_DIR) -# The erlang.mk package index is bundled in the default erlang.mk build. -# Search for the string "copyright" to skip to the rest of the code. - -# Copyright (c) 2015-2017, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: distclean-kerl - -KERL_INSTALL_DIR ?= $(HOME)/erlang - -ifeq ($(strip $(KERL)),) -KERL := $(ERLANG_MK_TMP)/kerl/kerl -endif - -export KERL - -KERL_GIT ?= https://github.com/kerl/kerl -KERL_COMMIT ?= master - -KERL_MAKEFLAGS ?= - -OTP_GIT ?= https://github.com/erlang/otp - -define kerl_otp_target -ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(1)),) -$(KERL_INSTALL_DIR)/$(1): $(KERL) - MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $(1) $(1) - $(KERL) install $(1) $(KERL_INSTALL_DIR)/$(1) -endif -endef - -define kerl_hipe_target -ifeq ($(wildcard $(KERL_INSTALL_DIR)/$1-native),) -$(KERL_INSTALL_DIR)/$1-native: $(KERL) - KERL_CONFIGURE_OPTIONS=--enable-native-libs \ - MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $1 $1-native - $(KERL) install $1-native $(KERL_INSTALL_DIR)/$1-native -endif -endef - -$(KERL): - $(verbose) mkdir -p $(ERLANG_MK_TMP) - $(gen_verbose) git clone --depth 1 $(KERL_GIT) $(ERLANG_MK_TMP)/kerl - $(verbose) cd $(ERLANG_MK_TMP)/kerl && git checkout $(KERL_COMMIT) - $(verbose) chmod +x $(KERL) - -distclean:: distclean-kerl - -distclean-kerl: - $(gen_verbose) rm -rf $(KERL) - -# Allow users to select which version of Erlang/OTP to use for a project. - -ERLANG_OTP ?= -ERLANG_HIPE ?= - -# Use kerl to enforce a specific Erlang/OTP version for a project. -ifneq ($(strip $(ERLANG_OTP)),) -export PATH := $(KERL_INSTALL_DIR)/$(ERLANG_OTP)/bin:$(PATH) -SHELL := env PATH=$(PATH) $(SHELL) -$(eval $(call kerl_otp_target,$(ERLANG_OTP))) - -# Build Erlang/OTP only if it doesn't already exist. -ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(ERLANG_OTP))$(BUILD_ERLANG_OTP),) -$(info Building Erlang/OTP $(ERLANG_OTP)... Please wait...) -$(shell $(MAKE) $(KERL_INSTALL_DIR)/$(ERLANG_OTP) ERLANG_OTP=$(ERLANG_OTP) BUILD_ERLANG_OTP=1 >&2) -endif - -else -# Same for a HiPE enabled VM. -ifneq ($(strip $(ERLANG_HIPE)),) -export PATH := $(KERL_INSTALL_DIR)/$(ERLANG_HIPE)-native/bin:$(PATH) -SHELL := env PATH=$(PATH) $(SHELL) -$(eval $(call kerl_hipe_target,$(ERLANG_HIPE))) - -# Build Erlang/OTP only if it doesn't already exist. -ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(ERLANG_HIPE))$(BUILD_ERLANG_OTP),) -$(info Building HiPE-enabled Erlang/OTP $(ERLANG_OTP)... Please wait...) -$(shell $(MAKE) $(KERL_INSTALL_DIR)/$(ERLANG_HIPE) ERLANG_HIPE=$(ERLANG_HIPE) BUILD_ERLANG_OTP=1 >&2) -endif - -endif -endif - -# Copyright (c) 2015-2016, Loïc Hoguin +# Copyright (c) 2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: search @@ -307,10 +228,10 @@ else $(foreach p,$(PACKAGES),$(call pkg_print,$(p))) endif -# Copyright (c) 2013-2016, Loïc Hoguin +# Copyright (c) 2013-2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. -.PHONY: distclean-deps clean-tmp-deps.log +.PHONY: distclean-deps # Configuration. @@ -330,32 +251,11 @@ export DEPS_DIR REBAR_DEPS_DIR = $(DEPS_DIR) export REBAR_DEPS_DIR -# External "early" plugins (see core/plugins.mk for regular plugins). -# They both use the core_dep_plugin macro. - -define core_dep_plugin -ifeq ($(2),$(PROJECT)) --include $$(patsubst $(PROJECT)/%,%,$(1)) -else --include $(DEPS_DIR)/$(1) - -$(DEPS_DIR)/$(1): $(DEPS_DIR)/$(2) ; -endif -endef - -DEP_EARLY_PLUGINS ?= - -$(foreach p,$(DEP_EARLY_PLUGINS),\ - $(eval $(if $(findstring /,$p),\ - $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\ - $(call core_dep_plugin,$p/early-plugins.mk,$p)))) - dep_name = $(if $(dep_$(1)),$(1),$(if $(pkg_$(1)_name),$(pkg_$(1)_name),$(1))) dep_repo = $(patsubst git://github.com/%,https://github.com/%, \ $(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_repo))) dep_commit = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 3,$(dep_$(1))),$(pkg_$(1)_commit))) -LOCAL_DEPS_DIRS = $(foreach a,$(LOCAL_DEPS),$(if $(wildcard $(APPS_DIR)/$(a)),$(APPS_DIR)/$(a))) ALL_APPS_DIRS = $(if $(wildcard $(APPS_DIR)/),$(filter-out $(APPS_DIR),$(shell find $(APPS_DIR) -maxdepth 1 -type d))) ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(BUILD_DEPS) $(DEPS)),$(call dep_name,$(dep)))) @@ -378,7 +278,10 @@ dep_verbose = $(dep_verbose_$(V)) # Core targets. -apps:: $(ALL_APPS_DIRS) clean-tmp-deps.log +ifdef IS_APP +apps:: +else +apps:: $(ALL_APPS_DIRS) ifeq ($(IS_APP)$(IS_DEP),) $(verbose) rm -f $(ERLANG_MK_TMP)/apps.log endif @@ -386,42 +289,36 @@ endif # Create ebin directory for all apps to make sure Erlang recognizes them # as proper OTP applications when using -include_lib. This is a temporary # fix, a proper fix would be to compile apps/* in the right order. -ifndef IS_APP - $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \ - mkdir -p $$dep/ebin; \ + $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ + mkdir -p $$dep/ebin || exit $$?; \ done -endif -# at the toplevel: if LOCAL_DEPS is defined with at least one local app, only -# compile that list of apps. otherwise, compile everything. -# within an app: compile all LOCAL_DEPS that are (uncompiled) local apps - $(verbose) set -e; for dep in $(if $(LOCAL_DEPS_DIRS)$(IS_APP),$(LOCAL_DEPS_DIRS),$(ALL_APPS_DIRS)) ; do \ + $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/apps.log; then \ :; \ else \ echo $$dep >> $(ERLANG_MK_TMP)/apps.log; \ - $(MAKE) -C $$dep IS_APP=1; \ + $(MAKE) -C $$dep IS_APP=1 || exit $$?; \ fi \ done - -clean-tmp-deps.log: -ifeq ($(IS_APP)$(IS_DEP),) - $(verbose) rm -f $(ERLANG_MK_TMP)/deps.log endif ifneq ($(SKIP_DEPS),) deps:: else -deps:: $(ALL_DEPS_DIRS) apps clean-tmp-deps.log +deps:: $(ALL_DEPS_DIRS) apps +ifeq ($(IS_APP)$(IS_DEP),) + $(verbose) rm -f $(ERLANG_MK_TMP)/deps.log +endif $(verbose) mkdir -p $(ERLANG_MK_TMP) - $(verbose) set -e; for dep in $(ALL_DEPS_DIRS) ; do \ + $(verbose) for dep in $(ALL_DEPS_DIRS) ; do \ if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/deps.log; then \ :; \ else \ echo $$dep >> $(ERLANG_MK_TMP)/deps.log; \ if [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ]; then \ - $(MAKE) -C $$dep IS_DEP=1; \ + $(MAKE) -C $$dep IS_DEP=1 || exit $$?; \ else \ - echo "Error: No Makefile to build dependency $$dep." >&2; \ + echo "Error: No Makefile to build dependency $$dep."; \ exit 2; \ fi \ fi \ @@ -435,18 +332,17 @@ endif # in practice only Makefile is needed so far. define dep_autopatch if [ -f $(DEPS_DIR)/$(1)/erlang.mk ]; then \ - rm -rf $(DEPS_DIR)/$1/ebin/; \ $(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \ $(call dep_autopatch_erlang_mk,$(1)); \ elif [ -f $(DEPS_DIR)/$(1)/Makefile ]; then \ - if [ -f $(DEPS_DIR)/$1/rebar.lock ]; then \ - $(call dep_autopatch2,$1); \ - elif [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$(1)/Makefile` ]; then \ + if [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$(1)/Makefile` ]; then \ $(call dep_autopatch2,$(1)); \ - elif [ 0 != `grep -ci "^[^#].*rebar" $(DEPS_DIR)/$(1)/Makefile` ]; then \ + elif [ 0 != `grep -ci rebar $(DEPS_DIR)/$(1)/Makefile` ]; then \ $(call dep_autopatch2,$(1)); \ - elif [ -n "`find $(DEPS_DIR)/$(1)/ -type f -name \*.mk -not -name erlang.mk -exec grep -i "^[^#].*rebar" '{}' \;`" ]; then \ + elif [ -n "`find $(DEPS_DIR)/$(1)/ -type f -name \*.mk -not -name erlang.mk -exec grep -i rebar '{}' \;`" ]; then \ $(call dep_autopatch2,$(1)); \ + else \ + $(call erlang,$(call dep_autopatch_app.erl,$(1))); \ fi \ else \ if [ ! -d $(DEPS_DIR)/$(1)/src/ ]; then \ @@ -458,14 +354,11 @@ define dep_autopatch endef define dep_autopatch2 - ! test -f $(DEPS_DIR)/$1/ebin/$1.app || \ - mv -n $(DEPS_DIR)/$1/ebin/$1.app $(DEPS_DIR)/$1/src/$1.app.src; \ - rm -f $(DEPS_DIR)/$1/ebin/$1.app; \ if [ -f $(DEPS_DIR)/$1/src/$1.app.src.script ]; then \ $(call erlang,$(call dep_autopatch_appsrc_script.erl,$(1))); \ fi; \ $(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \ - if [ -f $(DEPS_DIR)/$(1)/rebar -o -f $(DEPS_DIR)/$(1)/rebar.config -o -f $(DEPS_DIR)/$(1)/rebar.config.script -o -f $(DEPS_DIR)/$1/rebar.lock ]; then \ + if [ -f $(DEPS_DIR)/$(1)/rebar -o -f $(DEPS_DIR)/$(1)/rebar.config -o -f $(DEPS_DIR)/$(1)/rebar.config.script ]; then \ $(call dep_autopatch_fetch_rebar); \ $(call dep_autopatch_rebar,$(1)); \ else \ @@ -477,15 +370,11 @@ define dep_autopatch_noop printf "noop:\n" > $(DEPS_DIR)/$(1)/Makefile endef -# Replace "include erlang.mk" with a line that will load the parent Erlang.mk -# if given. Do it for all 3 possible Makefile file names. +# Overwrite erlang.mk with the current file by default. ifeq ($(NO_AUTOPATCH_ERLANG_MK),) define dep_autopatch_erlang_mk - for f in Makefile makefile GNUmakefile; do \ - if [ -f $(DEPS_DIR)/$1/$$f ]; then \ - sed -i.bak s/'include *erlang.mk'/'include $$(if $$(ERLANG_MK_FILENAME),$$(ERLANG_MK_FILENAME),erlang.mk)'/ $(DEPS_DIR)/$1/$$f; \ - fi \ - done + echo "include $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(DEPS_DIR)/app)/erlang.mk" \ + > $(DEPS_DIR)/$1/erlang.mk endef else define dep_autopatch_erlang_mk @@ -504,7 +393,7 @@ define dep_autopatch_fetch_rebar if [ ! -d $(ERLANG_MK_TMP)/rebar ]; then \ git clone -q -n -- https://github.com/rebar/rebar $(ERLANG_MK_TMP)/rebar; \ cd $(ERLANG_MK_TMP)/rebar; \ - git checkout -q 576e12171ab8d69b048b827b92aa65d067deea01; \ + git checkout -q 791db716b5a3a7671e0b351f95ddf24b848ee173; \ $(MAKE); \ cd -; \ fi @@ -521,7 +410,6 @@ endef define dep_autopatch_rebar.erl application:load(rebar), application:set_env(rebar, log_level, debug), - rmemo:start(), Conf1 = case file:consult("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config)") of {ok, Conf0} -> Conf0; _ -> [] @@ -550,10 +438,6 @@ define dep_autopatch_rebar.erl Write("C_SRC_TYPE = rebar\n"), Write("DRV_CFLAGS = -fPIC\nexport DRV_CFLAGS\n"), Write(["ERLANG_ARCH = ", rebar_utils:wordsize(), "\nexport ERLANG_ARCH\n"]), - ToList = fun - (V) when is_atom(V) -> atom_to_list(V); - (V) when is_list(V) -> "'\\"" ++ V ++ "\\"'" - end, fun() -> Write("ERLC_OPTS = +debug_info\nexport ERLC_OPTS\n"), case lists:keyfind(erl_opts, 1, Conf) of @@ -561,18 +445,16 @@ define dep_autopatch_rebar.erl {_, ErlOpts} -> lists:foreach(fun ({d, D}) -> - Write("ERLC_OPTS += -D" ++ ToList(D) ++ "=1\n"); - ({d, DKey, DVal}) -> - Write("ERLC_OPTS += -D" ++ ToList(DKey) ++ "=" ++ ToList(DVal) ++ "\n"); + Write("ERLC_OPTS += -D" ++ atom_to_list(D) ++ "=1\n"); ({i, I}) -> Write(["ERLC_OPTS += -I ", I, "\n"]); ({platform_define, Regex, D}) -> case rebar_utils:is_arch(Regex) of - true -> Write("ERLC_OPTS += -D" ++ ToList(D) ++ "=1\n"); + true -> Write("ERLC_OPTS += -D" ++ atom_to_list(D) ++ "=1\n"); false -> ok end; ({parse_transform, PT}) -> - Write("ERLC_OPTS += +'{parse_transform, " ++ ToList(PT) ++ "}'\n"); + Write("ERLC_OPTS += +'{parse_transform, " ++ atom_to_list(PT) ++ "}'\n"); (_) -> ok end, ErlOpts) end, @@ -681,9 +563,9 @@ define dep_autopatch_rebar.erl [] -> ok; _ -> Write("\npre-app::\n\t$$\(MAKE) -f c_src/Makefile.erlang.mk\n"), - PortSpecWrite(io_lib:format("ERL_CFLAGS ?= -finline-functions -Wall -fPIC -I \\"~s/erts-~s/include\\" -I \\"~s\\"\n", + PortSpecWrite(io_lib:format("ERL_CFLAGS = -finline-functions -Wall -fPIC -I \\"~s/erts-~s/include\\" -I \\"~s\\"\n", [code:root_dir(), erlang:system_info(version), code:lib_dir(erl_interface, include)])), - PortSpecWrite(io_lib:format("ERL_LDFLAGS ?= -L \\"~s\\" -lerl_interface -lei\n", + PortSpecWrite(io_lib:format("ERL_LDFLAGS = -L \\"~s\\" -lerl_interface -lei\n", [code:lib_dir(erl_interface, lib)])), [PortSpecWrite(["\n", E, "\n"]) || E <- OsEnv], FilterEnv = fun(Env) -> @@ -722,7 +604,7 @@ define dep_autopatch_rebar.erl "%.o: %.C\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", "%.o: %.cc\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", "%.o: %.cpp\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", - [[Output, ": ", K, " += ", ShellToMk(V), "\n"] || {K, V} <- lists:reverse(MergeEnv(FilterEnv(Env)))], + [[Output, ": ", K, " = ", ShellToMk(V), "\n"] || {K, V} <- lists:reverse(MergeEnv(FilterEnv(Env)))], Output, ": $$\(foreach ext,.c .C .cc .cpp,", "$$\(patsubst %$$\(ext),%.o,$$\(filter %$$\(ext),$$\(wildcard", Input, "))))\n", "\t$$\(CC) -o $$\@ $$\? $$\(LDFLAGS) $$\(ERL_LDFLAGS) $$\(DRV_LDFLAGS) $$\(EXE_LDFLAGS)", @@ -734,7 +616,7 @@ define dep_autopatch_rebar.erl end, [PortSpec(S) || S <- PortSpecs] end, - Write("\ninclude $$\(if $$\(ERLANG_MK_FILENAME),$$\(ERLANG_MK_FILENAME),erlang.mk)"), + Write("\ninclude $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(DEPS_DIR)/app)/erlang.mk"), RunPlugin = fun(Plugin, Step) -> case erlang:function_exported(Plugin, Step, 2) of false -> ok; @@ -782,14 +664,27 @@ define dep_autopatch_rebar.erl halt() endef +define dep_autopatch_app.erl + UpdateModules = fun(App) -> + case filelib:is_regular(App) of + false -> ok; + true -> + {ok, [{application, '$(1)', L0}]} = file:consult(App), + Mods = filelib:fold_files("$(call core_native_path,$(DEPS_DIR)/$1/src)", "\\\\.erl$$", true, + fun (F, Acc) -> [list_to_atom(filename:rootname(filename:basename(F)))|Acc] end, []), + L = lists:keystore(modules, 1, L0, {modules, Mods}), + ok = file:write_file(App, io_lib:format("~p.~n", [{application, '$(1)', L}])) + end + end, + UpdateModules("$(call core_native_path,$(DEPS_DIR)/$1/ebin/$1.app)"), + halt() +endef + define dep_autopatch_appsrc_script.erl AppSrc = "$(call core_native_path,$(DEPS_DIR)/$1/src/$1.app.src)", AppSrcScript = AppSrc ++ ".script", - {ok, Conf0} = file:consult(AppSrc), - Bindings0 = erl_eval:new_bindings(), - Bindings1 = erl_eval:add_binding('CONFIG', Conf0, Bindings0), - Bindings = erl_eval:add_binding('SCRIPT', AppSrcScript, Bindings1), - {ok, [Conf]} = file:script(AppSrcScript, Bindings), + Bindings = erl_eval:new_bindings(), + {ok, Conf} = file:script(AppSrcScript, Bindings), ok = file:write_file(AppSrc, io_lib:format("~p.~n", [Conf])), halt() endef @@ -802,11 +697,7 @@ define dep_autopatch_appsrc.erl true -> {ok, [{application, $(1), L0}]} = file:consult(AppSrcIn), L1 = lists:keystore(modules, 1, L0, {modules, []}), - L2 = case lists:keyfind(vsn, 1, L1) of - {_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, "git"}); - {_, {cmd, _}} -> lists:keyreplace(vsn, 1, L1, {vsn, "cmd"}); - _ -> L1 - end, + L2 = case lists:keyfind(vsn, 1, L1) of {_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, "git"}); _ -> L1 end, L3 = case lists:keyfind(registered, 1, L2) of false -> [{registered, []}|L2]; _ -> L2 end, ok = file:write_file(AppSrcOut, io_lib:format("~p.~n", [{application, $(1), L3}])), case AppSrcOut of AppSrcIn -> ok; _ -> ok = file:delete(AppSrcIn) end @@ -836,16 +727,21 @@ define dep_fetch_cp cp -R $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); endef -define dep_fetch_ln - ln -s $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); +define dep_fetch_hex.erl + ssl:start(), + inets:start(), + {ok, {{_, 200, _}, _, Body}} = httpc:request(get, + {"https://s3.amazonaws.com/s3.hex.pm/tarballs/$(1)-$(2).tar", []}, + [], [{body_format, binary}]), + {ok, Files} = erl_tar:extract({binary, Body}, [memory]), + {_, Source} = lists:keyfind("contents.tar.gz", 1, Files), + ok = erl_tar:extract({binary, Source}, [{cwd, "$(call core_native_path,$(DEPS_DIR)/$1)"}, compressed]), + halt() endef # Hex only has a package version. No need to look in the Erlang.mk packages. define dep_fetch_hex - mkdir -p $(ERLANG_MK_TMP)/hex $(DEPS_DIR)/$1; \ - $(call core_http_get,$(ERLANG_MK_TMP)/hex/$1.tar,\ - https://s3.amazonaws.com/s3.hex.pm/tarballs/$1-$(strip $(word 2,$(dep_$1))).tar); \ - tar -xOf $(ERLANG_MK_TMP)/hex/$1.tar contents.tar.gz | tar -C $(DEPS_DIR)/$1 -xzf -; + $(call erlang,$(call dep_fetch_hex.erl,$(1),$(strip $(word 2,$(dep_$(1)))))); endef define dep_fetch_fail @@ -875,7 +771,7 @@ $(DEPS_DIR)/$(call dep_name,$1): $(eval DEP_NAME := $(call dep_name,$1)) $(eval DEP_STR := $(if $(filter-out $1,$(DEP_NAME)),$1,"$1 ($(DEP_NAME))")) $(verbose) if test -d $(APPS_DIR)/$(DEP_NAME); then \ - echo "Error: Dependency" $(DEP_STR) "conflicts with application found in $(APPS_DIR)/$(DEP_NAME)." >&2; \ + echo "Error: Dependency" $(DEP_STR) "conflicts with application found in $(APPS_DIR)/$(DEP_NAME)."; \ exit 17; \ fi $(verbose) mkdir -p $(DEPS_DIR) @@ -917,15 +813,15 @@ ifndef IS_APP clean:: clean-apps clean-apps: - $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \ - $(MAKE) -C $$dep clean IS_APP=1; \ + $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ + $(MAKE) -C $$dep clean IS_APP=1 || exit $$?; \ done distclean:: distclean-apps distclean-apps: - $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \ - $(MAKE) -C $$dep distclean IS_APP=1; \ + $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ + $(MAKE) -C $$dep distclean IS_APP=1 || exit $$?; \ done endif @@ -936,16 +832,77 @@ distclean-deps: $(gen_verbose) rm -rf $(DEPS_DIR) endif -# Forward-declare variables used in core/deps-tools.mk. This is required -# in case plugins use them. +# External plugins. -ERLANG_MK_RECURSIVE_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-deps-list.log -ERLANG_MK_RECURSIVE_DOC_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-doc-deps-list.log -ERLANG_MK_RECURSIVE_REL_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-rel-deps-list.log -ERLANG_MK_RECURSIVE_TEST_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-test-deps-list.log -ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-shell-deps-list.log +DEP_PLUGINS ?= -# Copyright (c) 2015-2016, Loïc Hoguin +define core_dep_plugin +-include $(DEPS_DIR)/$(1) + +$(DEPS_DIR)/$(1): $(DEPS_DIR)/$(2) ; +endef + +$(foreach p,$(DEP_PLUGINS),\ + $(eval $(if $(findstring /,$p),\ + $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\ + $(call core_dep_plugin,$p/plugins.mk,$p)))) + +# Copyright (c) 2013-2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +# Configuration. + +DTL_FULL_PATH ?= +DTL_PATH ?= templates/ +DTL_SUFFIX ?= _dtl +DTL_OPTS ?= + +# Verbosity. + +dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); +dtl_verbose = $(dtl_verbose_$(V)) + +# Core targets. + +DTL_FILES = $(sort $(call core_find,$(DTL_PATH),*.dtl)) + +ifneq ($(DTL_FILES),) + +ifdef DTL_FULL_PATH +BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(subst /,_,$(DTL_FILES:$(DTL_PATH)%=%)))) +else +BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(notdir $(DTL_FILES)))) +endif + +# Rebuild templates when the Makefile changes. +$(DTL_FILES): $(MAKEFILE_LIST) + @touch $@ + +define erlydtl_compile.erl + [begin + Module0 = case "$(strip $(DTL_FULL_PATH))" of + "" -> + filename:basename(F, ".dtl"); + _ -> + "$(DTL_PATH)" ++ F2 = filename:rootname(F, ".dtl"), + re:replace(F2, "/", "_", [{return, list}, global]) + end, + Module = list_to_atom(string:to_lower(Module0) ++ "$(DTL_SUFFIX)"), + case erlydtl:compile(F, Module, [$(DTL_OPTS)] ++ [{out_dir, "ebin/"}, return_errors, {doc_root, "templates"}]) of + ok -> ok; + {ok, _} -> ok + end + end || F <- string:tokens("$(1)", " ")], + halt(). +endef + +ebin/$(PROJECT).app:: $(DTL_FILES) | ebin/ + $(if $(strip $?),\ + $(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$?),-pa ebin/ $(DEPS_DIR)/erlydtl/ebin/)) + +endif + +# Copyright (c) 2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. # Verbosity. @@ -964,9 +921,10 @@ endef define compile_proto.erl [begin + Dir = filename:dirname(filename:dirname(F)), protobuffs_compile:generate_source(F, - [{output_include_dir, "./include"}, - {output_src_dir, "./ebin"}]) + [{output_include_dir, Dir ++ "/include"}, + {output_src_dir, Dir ++ "/ebin"}]) end || F <- string:tokens("$(1)", " ")], halt(). endef @@ -976,7 +934,7 @@ ebin/$(PROJECT).app:: $(sort $(call core_find,src/,*.proto)) $(if $(strip $?),$(call compile_proto,$?)) endif -# Copyright (c) 2013-2016, Loïc Hoguin +# Copyright (c) 2013-2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: clean-app @@ -990,8 +948,6 @@ COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST))) ERLC_EXCLUDE ?= ERLC_EXCLUDE_PATHS = $(addprefix src/,$(addsuffix .erl,$(ERLC_EXCLUDE))) -ERLC_ASN1_OPTS ?= - ERLC_MIB_OPTS ?= COMPILE_MIB_FIRST ?= COMPILE_MIB_FIRST_PATHS = $(addprefix mibs/,$(addsuffix .mib,$(COMPILE_MIB_FIRST))) @@ -1041,27 +997,25 @@ endif ifeq ($(wildcard src/$(PROJECT_MOD).erl),) define app_file -{application, '$(PROJECT)', [ +{application, $(PROJECT), [ {description, "$(PROJECT_DESCRIPTION)"}, {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), {id$(comma)$(space)"$(1)"}$(comma)) {modules, [$(call comma_list,$(2))]}, {registered, []}, - {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(foreach dep,$(DEPS),$(call dep_name,$(dep))))]}, - {env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),) + {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS))]} ]}. endef else define app_file -{application, '$(PROJECT)', [ +{application, $(PROJECT), [ {description, "$(PROJECT_DESCRIPTION)"}, {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), {id$(comma)$(space)"$(1)"}$(comma)) {modules, [$(call comma_list,$(2))]}, {registered, [$(call comma_list,$(PROJECT)_sup $(PROJECT_REGISTERED))]}, - {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(foreach dep,$(DEPS),$(call dep_name,$(dep))))]}, - {mod, {$(PROJECT_MOD), []}}, - {env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),) + {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS))]}, + {mod, {$(PROJECT_MOD), []}} ]}. endef endif @@ -1071,10 +1025,8 @@ app-build: ebin/$(PROJECT).app # Source files. -ALL_SRC_FILES := $(sort $(call core_find,src/,*)) - -ERL_FILES := $(filter %.erl,$(ALL_SRC_FILES)) -CORE_FILES := $(filter %.core,$(ALL_SRC_FILES)) +ERL_FILES = $(sort $(call core_find,src/,*.erl)) +CORE_FILES = $(sort $(call core_find,src/,*.core)) # ASN.1 files. @@ -1084,7 +1036,7 @@ ERL_FILES += $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES)))) define compile_asn1 $(verbose) mkdir -p include/ - $(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(ERLC_ASN1_OPTS) $(1) + $(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(1) $(verbose) mv asn1/*.erl src/ $(verbose) mv asn1/*.hrl include/ $(verbose) mv asn1/*.asn1db include/ @@ -1107,16 +1059,16 @@ endif # Leex and Yecc files. -XRL_FILES := $(filter %.xrl,$(ALL_SRC_FILES)) +XRL_FILES = $(sort $(call core_find,src/,*.xrl)) XRL_ERL_FILES = $(addprefix src/,$(patsubst %.xrl,%.erl,$(notdir $(XRL_FILES)))) ERL_FILES += $(XRL_ERL_FILES) -YRL_FILES := $(filter %.yrl,$(ALL_SRC_FILES)) +YRL_FILES = $(sort $(call core_find,src/,*.yrl)) YRL_ERL_FILES = $(addprefix src/,$(patsubst %.yrl,%.erl,$(notdir $(YRL_FILES)))) ERL_FILES += $(YRL_ERL_FILES) $(PROJECT).d:: $(XRL_FILES) $(YRL_FILES) - $(if $(strip $?),$(xyrl_verbose) erlc -v -o src/ $(YRL_ERLC_OPTS) $?) + $(if $(strip $?),$(xyrl_verbose) erlc -v -o src/ $?) # Erlang and Core Erlang files. @@ -1166,12 +1118,7 @@ define makedep.erl (F, Mod, include_lib, "$1/include/" ++ Hrl) -> AddHd(F, Mod, "include/" ++ Hrl); (F, Mod, include_lib, Hrl) -> AddHd(F, Mod, "include/" ++ Hrl); (F, Mod, import, {Imp, _}) -> - IsFile = - case lists:keyfind(Imp, 1, Modules) of - false -> false; - {_, FilePath} -> filelib:is_file(FilePath) - end, - case IsFile of + case filelib:is_file("src/" ++ atom_to_list(Imp) ++ ".erl") of false -> ok; true -> Add(Mod, Imp) end; @@ -1195,41 +1142,23 @@ define makedep.erl end || F <- ErlFiles], Depend = sofs:to_external(sofs:relation_to_family(sofs:relation(ets:tab2list(E)))), CompileFirst = [X || X <- lists:reverse(digraph_utils:topsort(G)), [] =/= digraph:in_neighbours(G, X)], - TargetPath = fun(Target) -> - case lists:keyfind(Target, 1, Modules) of - false -> ""; - {_, DepFile} -> - DirSubname = tl(string:tokens(filename:dirname(DepFile), "/")), - string:join(DirSubname ++ [atom_to_list(Target)], "/") - end - end, ok = file:write_file("$(1)", [ [[F, "::", [[" ", D] || D <- Deps], "; @touch \$$@\n"] || {F, Deps} <- Depend], - "\nCOMPILE_FIRST +=", [[" ", TargetPath(CF)] || CF <- CompileFirst], "\n" + "\nCOMPILE_FIRST +=", [[" ", atom_to_list(CF)] || CF <- CompileFirst], "\n" ]), halt() endef ifeq ($(if $(NO_MAKEDEP),$(wildcard $(PROJECT).d),),) -$(PROJECT).d:: $(ERL_FILES) $(call core_find,include/,*.hrl) $(MAKEFILE_LIST) +$(PROJECT).d:: $(ERL_FILES) $(call core_find,include/,*.hrl) $(makedep_verbose) $(call erlang,$(call makedep.erl,$@)) endif -ifneq ($(words $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES)),0) # Rebuild everything when the Makefile changes. -$(ERLANG_MK_TMP)/last-makefile-change: $(MAKEFILE_LIST) - $(verbose) mkdir -p $(ERLANG_MK_TMP) - $(verbose) if test -f $@; then \ - touch $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES); \ - touch -c $(PROJECT).d; \ - fi - $(verbose) touch $@ +$(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(MAKEFILE_LIST) + @touch $@ -$(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(ERLANG_MK_TMP)/last-makefile-change -ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change -endif - -include $(wildcard $(PROJECT).d) +-include $(PROJECT).d ebin/$(PROJECT).app:: ebin/ @@ -1248,7 +1177,7 @@ ebin/$(PROJECT).app:: $(ERL_FILES) $(CORE_FILES) $(wildcard src/$(PROJECT).app.s $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \ $(filter-out $(ERLC_EXCLUDE_PATHS),$(ERL_FILES) $(CORE_FILES) $(BEAM_FILES))))))) ifeq ($(wildcard src/$(PROJECT).app.src),) - $(app_verbose) printf '$(subst %,%%,$(subst $(newline),\n,$(subst ','\'',$(call app_file,$(GITDESCRIBE),$(MODULES)))))' \ + $(app_verbose) printf "$(subst $(newline),\n,$(subst ",\",$(call app_file,$(GITDESCRIBE),$(MODULES))))" \ > ebin/$(PROJECT).app else $(verbose) if [ -z "$$(grep -e '^[^%]*{\s*modules\s*,' src/$(PROJECT).app.src)" ]; then \ @@ -1272,7 +1201,6 @@ clean-app: endif -# Copyright (c) 2016, Loïc Hoguin # Copyright (c) 2015, Viktor Söderqvist # This file is part of erlang.mk and subject to the terms of the ISC License. @@ -1290,10 +1218,10 @@ ifneq ($(SKIP_DEPS),) doc-deps: else doc-deps: $(ALL_DOC_DEPS_DIRS) - $(verbose) set -e; for dep in $(ALL_DOC_DEPS_DIRS) ; do $(MAKE) -C $$dep; done + $(verbose) for dep in $(ALL_DOC_DEPS_DIRS) ; do $(MAKE) -C $$dep; done endif -# Copyright (c) 2015-2016, Loïc Hoguin +# Copyright (c) 2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: rel-deps @@ -1310,10 +1238,10 @@ ifneq ($(SKIP_DEPS),) rel-deps: else rel-deps: $(ALL_REL_DEPS_DIRS) - $(verbose) set -e; for dep in $(ALL_REL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done + $(verbose) for dep in $(ALL_REL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done endif -# Copyright (c) 2015-2016, Loïc Hoguin +# Copyright (c) 2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: test-deps test-dir test-build clean-test-dir @@ -1335,7 +1263,7 @@ ifneq ($(SKIP_DEPS),) test-deps: else test-deps: $(ALL_TEST_DEPS_DIRS) - $(verbose) set -e; for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep IS_DEP=1; done + $(verbose) for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep IS_DEP=1; done endif ifneq ($(wildcard $(TEST_DIR)),) @@ -1368,7 +1296,7 @@ ifneq ($(wildcard $(TEST_DIR)/*.beam),) endif endif -# Copyright (c) 2015-2016, Loïc Hoguin +# Copyright (c) 2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: rebar.config @@ -1404,90 +1332,54 @@ $(eval export _compat_rebar_config) rebar.config: $(gen_verbose) echo "$${_compat_rebar_config}" > rebar.config -# Copyright (c) 2015-2016, Loïc Hoguin +# Copyright (c) 2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. -ifeq ($(filter asciideck,$(DEPS) $(DOC_DEPS)),asciideck) +.PHONY: asciidoc asciidoc-guide asciidoc-manual install-asciidoc distclean-asciidoc -.PHONY: asciidoc asciidoc-guide asciidoc-manual install-asciidoc distclean-asciidoc-guide distclean-asciidoc-manual - -# Core targets. +MAN_INSTALL_PATH ?= /usr/local/share/man +MAN_SECTIONS ?= 3 7 docs:: asciidoc -distclean:: distclean-asciidoc-guide distclean-asciidoc-manual - -# Plugin-specific targets. - asciidoc: asciidoc-guide asciidoc-manual -# User guide. - ifeq ($(wildcard doc/src/guide/book.asciidoc),) asciidoc-guide: else -asciidoc-guide: distclean-asciidoc-guide doc-deps +asciidoc-guide: distclean-asciidoc doc-deps a2x -v -f pdf doc/src/guide/book.asciidoc && mv doc/src/guide/book.pdf doc/guide.pdf a2x -v -f chunked doc/src/guide/book.asciidoc && mv doc/src/guide/book.chunked/ doc/html/ - -distclean-asciidoc-guide: - $(gen_verbose) rm -rf doc/html/ doc/guide.pdf endif -# Man pages. - -ASCIIDOC_MANUAL_FILES := $(wildcard doc/src/manual/*.asciidoc) - -ifeq ($(ASCIIDOC_MANUAL_FILES),) +ifeq ($(wildcard doc/src/manual/*.asciidoc),) asciidoc-manual: else - -# Configuration. - -MAN_INSTALL_PATH ?= /usr/local/share/man -MAN_SECTIONS ?= 3 7 -MAN_PROJECT ?= $(shell echo $(PROJECT) | sed 's/^./\U&\E/') -MAN_VERSION ?= $(PROJECT_VERSION) - -# Plugin-specific targets. - -define asciidoc2man.erl -try - [begin - io:format(" ADOC ~s~n", [F]), - ok = asciideck:to_manpage(asciideck:parse_file(F), #{ - compress => gzip, - outdir => filename:dirname(F), - extra2 => "$(MAN_PROJECT) $(MAN_VERSION)", - extra3 => "$(MAN_PROJECT) Function Reference" - }) - end || F <- [$(shell echo $(addprefix $(comma)\",$(addsuffix \",$1)) | sed 's/^.//')]], - halt(0) -catch C:E -> - io:format("Exception ~p:~p~nStacktrace: ~p~n", [C, E, erlang:get_stacktrace()]), - halt(1) -end. -endef - -asciidoc-manual:: doc-deps - -asciidoc-manual:: $(ASCIIDOC_MANUAL_FILES) - $(call erlang,$(call asciidoc2man.erl,$?)) - $(foreach s,$(MAN_SECTIONS),mkdir -p doc/man$s/ && mv doc/src/manual/*.$s.gz doc/man$s/;) +asciidoc-manual: distclean-asciidoc doc-deps + for f in doc/src/manual/*.asciidoc ; do \ + a2x -v -f manpage $$f ; \ + done + for s in $(MAN_SECTIONS); do \ + mkdir -p doc/man$$s/ ; \ + mv doc/src/manual/*.$$s doc/man$$s/ ; \ + gzip doc/man$$s/*.$$s ; \ + done install-docs:: install-asciidoc install-asciidoc: asciidoc-manual - $(foreach s,$(MAN_SECTIONS),\ - mkdir -p $(MAN_INSTALL_PATH)/man$s/ && \ - install -g `id -g` -o `id -u` -m 0644 doc/man$s/*.gz $(MAN_INSTALL_PATH)/man$s/;) - -distclean-asciidoc-manual: - $(gen_verbose) rm -rf $(addprefix doc/man,$(MAN_SECTIONS)) -endif + for s in $(MAN_SECTIONS); do \ + mkdir -p $(MAN_INSTALL_PATH)/man$$s/ ; \ + install -g `id -u` -o `id -g` -m 0644 doc/man$$s/*.gz $(MAN_INSTALL_PATH)/man$$s/ ; \ + done endif -# Copyright (c) 2014-2016, Loïc Hoguin +distclean:: distclean-asciidoc + +distclean-asciidoc: + $(gen_verbose) rm -rf doc/html/ doc/guide.pdf doc/man3/ doc/man7/ + +# Copyright (c) 2014-2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: bootstrap bootstrap-lib bootstrap-rel new list-templates @@ -1544,7 +1436,7 @@ ifdef SP define bs_Makefile PROJECT = $p PROJECT_DESCRIPTION = New project -PROJECT_VERSION = 0.1.0 +PROJECT_VERSION = 0.0.1 # Whitespace to be used when creating files from templates. SP = $(SP) @@ -1554,7 +1446,7 @@ else define bs_Makefile PROJECT = $p PROJECT_DESCRIPTION = New project -PROJECT_VERSION = 0.1.0 +PROJECT_VERSION = 0.0.1 endef endif @@ -1562,7 +1454,7 @@ endif define bs_apps_Makefile PROJECT = $p PROJECT_DESCRIPTION = New project -PROJECT_VERSION = 0.1.0 +PROJECT_VERSION = 0.0.1 include $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(APPS_DIR)/app)/erlang.mk endef @@ -1582,7 +1474,7 @@ stop(_State) -> endef define bs_relx_config -{release, {$p_release, "1"}, [$p, sasl, runtime_tools]}. +{release, {$p_release, "1"}, [$p]}. {extended_start_script, true}. {sys_config, "rel/sys.config"}. {vm_args, "rel/vm.args"}. @@ -1933,6 +1825,9 @@ endif ifndef t $(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP]) endif +ifndef tpl_$(t) + $(error Unknown template) +endif ifndef n $(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP]) endif @@ -1945,7 +1840,7 @@ endif list-templates: $(verbose) echo Available templates: $(sort $(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES)))) -# Copyright (c) 2014-2016, Loïc Hoguin +# Copyright (c) 2014-2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: clean-c_src distclean-c_src-env @@ -2180,56 +2075,57 @@ else $(call render_template,bs_erl_nif,src/$n.erl) endif -# Copyright (c) 2015-2017, Loïc Hoguin +# Copyright (c) 2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. -.PHONY: ci ci-prepare ci-setup +.PHONY: ci ci-setup distclean-kerl +KERL ?= $(CURDIR)/kerl +export KERL + +KERL_URL ?= https://raw.githubusercontent.com/yrashk/kerl/master/kerl + +OTP_GIT ?= https://github.com/erlang/otp + +CI_INSTALL_DIR ?= $(HOME)/erlang CI_OTP ?= -CI_HIPE ?= -CI_ERLLVM ?= -ifeq ($(CI_VM),native) -ERLC_OPTS += +native -TEST_ERLC_OPTS += +native -else ifeq ($(CI_VM),erllvm) -ERLC_OPTS += +native +'{hipe, [to_llvm]}' -TEST_ERLC_OPTS += +native +'{hipe, [to_llvm]}' -endif - -ifeq ($(strip $(CI_OTP) $(CI_HIPE) $(CI_ERLLVM)),) +ifeq ($(strip $(CI_OTP)),) ci:: else +ci:: $(addprefix ci-,$(CI_OTP)) -ci:: $(addprefix ci-,$(CI_OTP) $(addsuffix -native,$(CI_HIPE)) $(addsuffix -erllvm,$(CI_ERLLVM))) - -ci-prepare: $(addprefix $(KERL_INSTALL_DIR)/,$(CI_OTP) $(addsuffix -native,$(CI_HIPE))) +ci-prepare: $(addprefix $(CI_INSTALL_DIR)/,$(CI_OTP)) ci-setup:: -ci-extra:: - ci_verbose_0 = @echo " CI " $(1); ci_verbose = $(ci_verbose_$(V)) define ci_target -ci-$1: $(KERL_INSTALL_DIR)/$2 - $(verbose) $(MAKE) --no-print-directory clean +ci-$(1): $(CI_INSTALL_DIR)/$(1) $(ci_verbose) \ - PATH="$(KERL_INSTALL_DIR)/$2/bin:$(PATH)" \ - CI_OTP_RELEASE="$1" \ - CT_OPTS="-label $1" \ - CI_VM="$3" \ - $(MAKE) ci-setup tests - $(verbose) $(MAKE) --no-print-directory ci-extra + PATH="$(CI_INSTALL_DIR)/$(1)/bin:$(PATH)" \ + CI_OTP_RELEASE="$(1)" \ + CT_OPTS="-label $(1)" \ + $(MAKE) clean ci-setup tests endef -$(foreach otp,$(CI_OTP),$(eval $(call ci_target,$(otp),$(otp),otp))) -$(foreach otp,$(CI_HIPE),$(eval $(call ci_target,$(otp)-native,$(otp)-native,native))) -$(foreach otp,$(CI_ERLLVM),$(eval $(call ci_target,$(otp)-erllvm,$(otp)-native,erllvm))) +$(foreach otp,$(CI_OTP),$(eval $(call ci_target,$(otp)))) -$(foreach otp,$(CI_OTP),$(eval $(call kerl_otp_target,$(otp)))) -$(foreach otp,$(sort $(CI_HIPE) $(CI_ERLLLVM)),$(eval $(call kerl_hipe_target,$(otp)))) +define ci_otp_target +ifeq ($(wildcard $(CI_INSTALL_DIR)/$(1)),) +$(CI_INSTALL_DIR)/$(1): $(KERL) + $(KERL) build git $(OTP_GIT) $(1) $(1) + $(KERL) install $(1) $(CI_INSTALL_DIR)/$(1) +endif +endef + +$(foreach otp,$(CI_OTP),$(eval $(call ci_otp_target,$(otp)))) + +$(KERL): + $(gen_verbose) $(call core_http_get,$(KERL),$(KERL_URL)) + $(verbose) chmod +x $(KERL) help:: $(verbose) printf "%s\n" "" \ @@ -2239,9 +2135,13 @@ help:: "The CI_OTP variable must be defined with the Erlang versions" \ "that must be tested. For example: CI_OTP = OTP-17.3.4 OTP-17.5.3" +distclean:: distclean-kerl + +distclean-kerl: + $(gen_verbose) rm -rf $(KERL) endif -# Copyright (c) 2013-2016, Loïc Hoguin +# Copyright (c) 2013-2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: ct apps-ct distclean-ct @@ -2249,14 +2149,11 @@ endif # Configuration. CT_OPTS ?= - ifneq ($(wildcard $(TEST_DIR)),) -ifndef CT_SUITES -CT_SUITES := $(sort $(subst _SUITE.erl,,$(notdir $(call core_find,$(TEST_DIR)/,*_SUITE.erl)))) + CT_SUITES ?= $(sort $(subst _SUITE.erl,,$(notdir $(call core_find,$(TEST_DIR)/,*_SUITE.erl)))) +else + CT_SUITES ?= endif -endif -CT_SUITES ?= -CT_LOGS_DIR ?= $(CURDIR)/logs # Core targets. @@ -2279,18 +2176,15 @@ CT_RUN = ct_run \ -noinput \ -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \ -dir $(TEST_DIR) \ - -logdir $(CT_LOGS_DIR) + -logdir $(CURDIR)/logs ifeq ($(CT_SUITES),) ct: $(if $(IS_APP),,apps-ct) else -# We do not run tests if we are in an apps/* with no test directory. -ifneq ($(IS_APP)$(wildcard $(TEST_DIR)),1) ct: test-build $(if $(IS_APP),,apps-ct) - $(verbose) mkdir -p $(CT_LOGS_DIR) + $(verbose) mkdir -p $(CURDIR)/logs/ $(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS) endif -endif ifneq ($(ALL_APPS_DIRS),) define ct_app_target @@ -2316,16 +2210,16 @@ endif define ct_suite_target ct-$(1): test-build - $(verbose) mkdir -p $(CT_LOGS_DIR) + $(verbose) mkdir -p $(CURDIR)/logs/ $(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(1)) $(CT_EXTRA) $(CT_OPTS) endef $(foreach test,$(CT_SUITES),$(eval $(call ct_suite_target,$(test)))) distclean-ct: - $(gen_verbose) rm -rf $(CT_LOGS_DIR) + $(gen_verbose) rm -rf $(CURDIR)/logs/ -# Copyright (c) 2013-2016, Loïc Hoguin +# Copyright (c) 2013-2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: plt distclean-plt dialyze @@ -2354,25 +2248,19 @@ help:: # Plugin-specific targets. define filter_opts.erl - Opts = init:get_plain_arguments(), - {Filtered, _} = lists:foldl(fun - (O, {Os, true}) -> {[O|Os], false}; - (O = "-D", {Os, _}) -> {[O|Os], true}; - (O = [\\$$-, \\$$D, _ | _], {Os, _}) -> {[O|Os], false}; - (O = "-I", {Os, _}) -> {[O|Os], true}; - (O = [\\$$-, \\$$I, _ | _], {Os, _}) -> {[O|Os], false}; - (O = "-pa", {Os, _}) -> {[O|Os], true}; - (_, Acc) -> Acc - end, {[], false}, Opts), - io:format("~s~n", [string:join(lists:reverse(Filtered), " ")]), + Opts = binary:split(<<"$1">>, <<"-">>, [global]), + Filtered = lists:reverse(lists:foldl(fun + (O = <<"pa ", _/bits>>, Acc) -> [O|Acc]; + (O = <<"D ", _/bits>>, Acc) -> [O|Acc]; + (O = <<"I ", _/bits>>, Acc) -> [O|Acc]; + (_, Acc) -> Acc + end, [], Opts)), + io:format("~s~n", [[["-", O] || O <- Filtered]]), halt(). endef $(DIALYZER_PLT): deps app - $(eval DEPS_LOG := $(shell test -f $(ERLANG_MK_TMP)/deps.log && \ - while read p; do test -d $$p/ebin && echo $$p/ebin; done <$(ERLANG_MK_TMP)/deps.log)) - $(verbose) dialyzer --build_plt --apps erts kernel stdlib \ - $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS_LOG) + $(verbose) dialyzer --build_plt --apps erts kernel stdlib $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS) plt: $(DIALYZER_PLT) @@ -2384,9 +2272,9 @@ dialyze: else dialyze: $(DIALYZER_PLT) endif - $(verbose) dialyzer --no_native `$(ERL) -eval "$(subst $(newline),,$(subst ",\",$(call filter_opts.erl)))" -extra $(ERLC_OPTS)` $(DIALYZER_DIRS) $(DIALYZER_OPTS) + $(verbose) dialyzer --no_native `$(call erlang,$(call filter_opts.erl,$(ERLC_OPTS)))` $(DIALYZER_DIRS) $(DIALYZER_OPTS) -# Copyright (c) 2013-2016, Loïc Hoguin +# Copyright (c) 2013-2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: distclean-edoc edoc @@ -2394,21 +2282,10 @@ endif # Configuration. EDOC_OPTS ?= -EDOC_SRC_DIRS ?= -EDOC_OUTPUT ?= doc - -define edoc.erl - SrcPaths = lists:foldl(fun(P, Acc) -> - filelib:wildcard(atom_to_list(P) ++ "/{src,c_src}") ++ Acc - end, [], [$(call comma_list,$(patsubst %,'%',$(EDOC_SRC_DIRS)))]), - DefaultOpts = [{dir, "$(EDOC_OUTPUT)"}, {source_path, SrcPaths}, {subpackages, false}], - edoc:application($(1), ".", [$(2)] ++ DefaultOpts), - halt(0). -endef # Core targets. -ifneq ($(strip $(EDOC_SRC_DIRS)$(wildcard doc/overview.edoc)),) +ifneq ($(wildcard doc/overview.edoc),) docs:: edoc endif @@ -2417,91 +2294,30 @@ distclean:: distclean-edoc # Plugin-specific targets. edoc: distclean-edoc doc-deps - $(gen_verbose) $(call erlang,$(call edoc.erl,$(PROJECT),$(EDOC_OPTS))) + $(gen_verbose) $(ERL) -eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), halt().' distclean-edoc: - $(gen_verbose) rm -f $(EDOC_OUTPUT)/*.css $(EDOC_OUTPUT)/*.html $(EDOC_OUTPUT)/*.png $(EDOC_OUTPUT)/edoc-info + $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info -# Copyright (c) 2013-2016, Loïc Hoguin +# Copyright (c) 2014 Dave Cottlehuber # This file is part of erlang.mk and subject to the terms of the ISC License. -# Configuration. - -DTL_FULL_PATH ?= -DTL_PATH ?= templates/ -DTL_SUFFIX ?= _dtl -DTL_OPTS ?= - -# Verbosity. - -dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); -dtl_verbose = $(dtl_verbose_$(V)) - -# Core targets. - -DTL_PATH := $(abspath $(DTL_PATH)) -DTL_FILES := $(sort $(call core_find,$(DTL_PATH),*.dtl)) - -ifneq ($(DTL_FILES),) - -DTL_NAMES = $(addsuffix $(DTL_SUFFIX),$(DTL_FILES:$(DTL_PATH)/%.dtl=%)) -DTL_MODULES = $(if $(DTL_FULL_PATH),$(subst /,_,$(DTL_NAMES)),$(notdir $(DTL_NAMES))) -BEAM_FILES += $(addsuffix .beam,$(addprefix ebin/,$(DTL_MODULES))) - -ifneq ($(words $(DTL_FILES)),0) -# Rebuild templates when the Makefile changes. -$(ERLANG_MK_TMP)/last-makefile-change-erlydtl: $(MAKEFILE_LIST) - @mkdir -p $(ERLANG_MK_TMP) - @if test -f $@; then \ - touch $(DTL_FILES); \ - fi - @touch $@ - -ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change-erlydtl -endif - -define erlydtl_compile.erl - [begin - Module0 = case "$(strip $(DTL_FULL_PATH))" of - "" -> - filename:basename(F, ".dtl"); - _ -> - "$(DTL_PATH)/" ++ F2 = filename:rootname(F, ".dtl"), - re:replace(F2, "/", "_", [{return, list}, global]) - end, - Module = list_to_atom(string:to_lower(Module0) ++ "$(DTL_SUFFIX)"), - case erlydtl:compile(F, Module, [$(DTL_OPTS)] ++ [{out_dir, "ebin/"}, return_errors]) of - ok -> ok; - {ok, _} -> ok - end - end || F <- string:tokens("$(1)", " ")], - halt(). -endef - -ebin/$(PROJECT).app:: $(DTL_FILES) | ebin/ - $(if $(strip $?),\ - $(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$(call core_native_path,$?)),\ - -pa ebin/ $(DEPS_DIR)/erlydtl/ebin/)) - -endif - -# Copyright (c) 2016, Loïc Hoguin -# Copyright (c) 2014, Dave Cottlehuber -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: distclean-escript escript escript-zip +.PHONY: distclean-escript escript # Configuration. ESCRIPT_NAME ?= $(PROJECT) ESCRIPT_FILE ?= $(ESCRIPT_NAME) -ESCRIPT_SHEBANG ?= /usr/bin/env escript ESCRIPT_COMMENT ?= This is an -*- erlang -*- file -ESCRIPT_EMU_ARGS ?= -escript main $(ESCRIPT_NAME) -ESCRIPT_ZIP ?= 7z a -tzip -mx=9 -mtc=off $(if $(filter-out 0,$(V)),,> /dev/null) -ESCRIPT_ZIP_FILE ?= $(ERLANG_MK_TMP)/escript.zip +ESCRIPT_BEAMS ?= "ebin/*", "deps/*/ebin/*" +ESCRIPT_SYS_CONFIG ?= "rel/sys.config" +ESCRIPT_EMU_ARGS ?= -pa . \ + -sasl errlog_type error \ + -escript main $(ESCRIPT_NAME) +ESCRIPT_SHEBANG ?= /usr/bin/env escript +ESCRIPT_STATIC ?= "deps/*/priv/**", "priv/**" # Core targets. @@ -2514,28 +2330,44 @@ help:: # Plugin-specific targets. -escript-zip:: deps app - $(verbose) mkdir -p $(dir $(ESCRIPT_ZIP)) - $(verbose) rm -f $(ESCRIPT_ZIP_FILE) - $(gen_verbose) cd .. && $(ESCRIPT_ZIP) $(ESCRIPT_ZIP_FILE) $(PROJECT)/ebin/* -ifneq ($(DEPS),) - $(verbose) cd $(DEPS_DIR) && $(ESCRIPT_ZIP) $(ESCRIPT_ZIP_FILE) \ - `cat $(ERLANG_MK_TMP)/deps.log | sed 's/^$(subst /,\/,$(DEPS_DIR))\///' | sed 's/$$/\/ebin\/\*/'` -endif +# Based on https://github.com/synrc/mad/blob/master/src/mad_bundle.erl +# Copyright (c) 2013 Maxim Sokhatsky, Synrc Research Center +# Modified MIT License, https://github.com/synrc/mad/blob/master/LICENSE : +# Software may only be used for the great good and the true happiness of all +# sentient beings. -escript:: escript-zip - $(gen_verbose) printf "%s\n" \ - "#!$(ESCRIPT_SHEBANG)" \ - "%% $(ESCRIPT_COMMENT)" \ - "%%! $(ESCRIPT_EMU_ARGS)" > $(ESCRIPT_FILE) - $(verbose) cat $(ESCRIPT_ZIP_FILE) >> $(ESCRIPT_FILE) - $(verbose) chmod +x $(ESCRIPT_FILE) +define ESCRIPT_RAW +'Read = fun(F) -> {ok, B} = file:read_file(filename:absname(F)), B end,'\ +'Files = fun(L) -> A = lists:concat([filelib:wildcard(X)||X<- L ]),'\ +' [F || F <- A, not filelib:is_dir(F) ] end,'\ +'Squash = fun(L) -> [{filename:basename(F), Read(F) } || F <- L ] end,'\ +'Zip = fun(A, L) -> {ok,{_,Z}} = zip:create(A, L, [{compress,all},memory]), Z end,'\ +'Ez = fun(Escript) ->'\ +' Static = Files([$(ESCRIPT_STATIC)]),'\ +' Beams = Squash(Files([$(ESCRIPT_BEAMS), $(ESCRIPT_SYS_CONFIG)])),'\ +' Archive = Beams ++ [{ "static.gz", Zip("static.gz", Static)}],'\ +' escript:create(Escript, [ $(ESCRIPT_OPTIONS)'\ +' {archive, Archive, [memory]},'\ +' {shebang, "$(ESCRIPT_SHEBANG)"},'\ +' {comment, "$(ESCRIPT_COMMENT)"},'\ +' {emu_args, " $(ESCRIPT_EMU_ARGS)"}'\ +' ]),'\ +' file:change_mode(Escript, 8#755)'\ +'end,'\ +'Ez("$(ESCRIPT_FILE)"),'\ +'halt().' +endef + +ESCRIPT_COMMAND = $(subst ' ',,$(ESCRIPT_RAW)) + +escript:: distclean-escript deps app + $(gen_verbose) $(ERL) -eval $(ESCRIPT_COMMAND) distclean-escript: - $(gen_verbose) rm -f $(ESCRIPT_FILE) + $(gen_verbose) rm -f $(ESCRIPT_NAME) -# Copyright (c) 2015-2016, Loïc Hoguin # Copyright (c) 2014, Enrique Fernandez +# Copyright (c) 2015, Loïc Hoguin # This file is contributed to erlang.mk and subject to the terms of the ISC License. .PHONY: eunit apps-eunit @@ -2572,7 +2404,7 @@ define eunit.erl case "$(COVER)" of "" -> ok; _ -> - cover:export("$(COVER_DATA_DIR)/eunit.coverdata") + cover:export("eunit.coverdata") end, halt() endef @@ -2581,10 +2413,10 @@ EUNIT_ERL_OPTS += -pa $(TEST_DIR) $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(CURDIR ifdef t ifeq (,$(findstring :,$(t))) -eunit: test-build cover-data-dir +eunit: test-build $(gen_verbose) $(call erlang,$(call eunit.erl,['$(t)']),$(EUNIT_ERL_OPTS)) else -eunit: test-build cover-data-dir +eunit: test-build $(gen_verbose) $(call erlang,$(call eunit.erl,fun $(t)/0),$(EUNIT_ERL_OPTS)) endif else @@ -2594,90 +2426,28 @@ EUNIT_TEST_MODS = $(notdir $(basename $(call core_find,$(TEST_DIR)/,*.erl))) EUNIT_MODS = $(foreach mod,$(EUNIT_EBIN_MODS) $(filter-out \ $(patsubst %,%_tests,$(EUNIT_EBIN_MODS)),$(EUNIT_TEST_MODS)),'$(mod)') -eunit: test-build $(if $(IS_APP),,apps-eunit) cover-data-dir +eunit: test-build $(if $(IS_APP),,apps-eunit) $(gen_verbose) $(call erlang,$(call eunit.erl,[$(call comma_list,$(EUNIT_MODS))]),$(EUNIT_ERL_OPTS)) ifneq ($(ALL_APPS_DIRS),) apps-eunit: - $(verbose) eunit_retcode=0 ; for app in $(ALL_APPS_DIRS); do $(MAKE) -C $$app eunit IS_APP=1; \ - [ $$? -ne 0 ] && eunit_retcode=1 ; done ; \ - exit $$eunit_retcode + $(verbose) for app in $(ALL_APPS_DIRS); do $(MAKE) -C $$app eunit IS_APP=1; done endif endif -# Copyright (c) 2015-2017, Loïc Hoguin +# Copyright (c) 2013-2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. -ifeq ($(filter proper,$(DEPS) $(TEST_DEPS)),proper) -.PHONY: proper - -# Targets. - -tests:: proper - -define proper_check.erl - code:add_pathsa(["$(call core_native_path,$(CURDIR)/ebin)", "$(call core_native_path,$(DEPS_DIR)/*/ebin)"]), - Module = fun(M) -> - [true] =:= lists:usort([ - case atom_to_list(F) of - "prop_" ++ _ -> - io:format("Testing ~p:~p/0~n", [M, F]), - proper:quickcheck(M:F(), nocolors); - _ -> - true - end - || {F, 0} <- M:module_info(exports)]) - end, - try - case $(1) of - all -> [true] =:= lists:usort([Module(M) || M <- [$(call comma_list,$(3))]]); - module -> Module($(2)); - function -> proper:quickcheck($(2), nocolors) - end - of - true -> halt(0); - _ -> halt(1) - catch error:undef -> - io:format("Undefined property or module?~n~p~n", [erlang:get_stacktrace()]), - halt(0) - end. -endef - -ifdef t -ifeq (,$(findstring :,$(t))) -proper: test-build - $(verbose) $(call erlang,$(call proper_check.erl,module,$(t))) -else -proper: test-build - $(verbose) echo Testing $(t)/0 - $(verbose) $(call erlang,$(call proper_check.erl,function,$(t)())) -endif -else -proper: test-build - $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename $(wildcard ebin/*.beam)))))) - $(gen_verbose) $(call erlang,$(call proper_check.erl,all,undefined,$(MODULES))) -endif -endif - -# Copyright (c) 2013-2016, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: relx-rel relx-relup distclean-relx-rel run +.PHONY: relx-rel distclean-relx-rel distclean-relx run # Configuration. -RELX ?= $(ERLANG_MK_TMP)/relx +RELX ?= $(CURDIR)/relx RELX_CONFIG ?= $(CURDIR)/relx.config -RELX_URL ?= https://github.com/erlware/relx/releases/download/v3.23.0/relx +RELX_URL ?= https://github.com/erlware/relx/releases/download/v3.19.0/relx RELX_OPTS ?= RELX_OUTPUT_DIR ?= _rel -RELX_REL_EXT ?= -RELX_TAR ?= 1 - -ifdef SFX - RELX_TAR = 1 -endif ifeq ($(firstword $(RELX_OPTS)),-o) RELX_OUTPUT_DIR = $(word 2,$(RELX_OPTS)) @@ -2690,12 +2460,10 @@ endif ifeq ($(IS_DEP),) ifneq ($(wildcard $(RELX_CONFIG)),) rel:: relx-rel - -relup:: relx-relup endif endif -distclean:: distclean-relx-rel +distclean:: distclean-relx-rel distclean-relx # Plugin-specific targets. @@ -2704,43 +2472,31 @@ $(RELX): $(verbose) chmod +x $(RELX) relx-rel: $(RELX) rel-deps app - $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) release $(if $(filter 1,$(RELX_TAR)),tar) - -relx-relup: $(RELX) rel-deps app - $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) release relup $(if $(filter 1,$(RELX_TAR)),tar) + $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) distclean-relx-rel: $(gen_verbose) rm -rf $(RELX_OUTPUT_DIR) +distclean-relx: + $(gen_verbose) rm -rf $(RELX) + # Run target. ifeq ($(wildcard $(RELX_CONFIG)),) -run:: +run: else define get_relx_release.erl - {ok, Config} = file:consult("$(call core_native_path,$(RELX_CONFIG))"), - {release, {Name, Vsn0}, _} = lists:keyfind(release, 1, Config), - Vsn = case Vsn0 of - {cmd, Cmd} -> os:cmd(Cmd); - semver -> ""; - {semver, _} -> ""; - VsnStr -> Vsn0 - end, - io:format("~s ~s", [Name, Vsn]), + {ok, Config} = file:consult("$(RELX_CONFIG)"), + {release, {Name, _}, _} = lists:keyfind(release, 1, Config), + io:format("~s", [Name]), halt(0). endef -RELX_REL := $(shell $(call erlang,$(get_relx_release.erl))) -RELX_REL_NAME := $(word 1,$(RELX_REL)) -RELX_REL_VSN := $(word 2,$(RELX_REL)) +RELX_RELEASE = `$(call erlang,$(get_relx_release.erl))` -ifeq ($(PLATFORM),msys2) -RELX_REL_EXT := .cmd -endif - -run:: all - $(verbose) $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/bin/$(RELX_REL_NAME)$(RELX_REL_EXT) console +run: all + $(verbose) $(RELX_OUTPUT_DIR)/$(RELX_RELEASE)/bin/$(RELX_RELEASE) console help:: $(verbose) printf "%s\n" "" \ @@ -2749,8 +2505,8 @@ help:: endif -# Copyright (c) 2015-2016, Loïc Hoguin # Copyright (c) 2014, M Robert Martin +# Copyright (c) 2015, Loïc Hoguin # This file is contributed to erlang.mk and subject to the terms of the ISC License. .PHONY: shell @@ -2775,26 +2531,12 @@ help:: $(foreach dep,$(SHELL_DEPS),$(eval $(call dep_target,$(dep)))) build-shell-deps: $(ALL_SHELL_DEPS_DIRS) - $(verbose) set -e; for dep in $(ALL_SHELL_DEPS_DIRS) ; do $(MAKE) -C $$dep ; done + $(verbose) for dep in $(ALL_SHELL_DEPS_DIRS) ; do $(MAKE) -C $$dep ; done shell: build-shell-deps $(gen_verbose) $(SHELL_ERL) -pa $(SHELL_PATHS) $(SHELL_OPTS) -# Copyright (c) 2017, Jean-Sébastien Pédron -# This file is contributed to erlang.mk and subject to the terms of the ISC License. - -.PHONY: show-ERL_LIBS show-ERLC_OPTS show-TEST_ERLC_OPTS - -show-ERL_LIBS: - @echo $(ERL_LIBS) - -show-ERLC_OPTS: - @$(foreach opt,$(ERLC_OPTS) -pa ebin -I include,echo "$(opt)";) - -show-TEST_ERLC_OPTS: - @$(foreach opt,$(TEST_ERLC_OPTS) -pa ebin -I include,echo "$(opt)";) - -# Copyright (c) 2015-2016, Loïc Hoguin +# Copyright (c) 2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. ifeq ($(filter triq,$(DEPS) $(TEST_DEPS)),triq) @@ -2805,10 +2547,7 @@ ifeq ($(filter triq,$(DEPS) $(TEST_DEPS)),triq) tests:: triq define triq_check.erl - code:add_pathsa([ - "$(call core_native_path,$(CURDIR)/ebin)", - "$(call core_native_path,$(DEPS_DIR)/*/ebin)", - "$(call core_native_path,$(TEST_DIR))"]), + code:add_pathsa(["$(CURDIR)/ebin", "$(DEPS_DIR)/*/ebin"]), try case $(1) of all -> [true] =:= lists:usort([triq:check(M) || M <- [$(call comma_list,$(3))]]); @@ -2819,7 +2558,7 @@ define triq_check.erl true -> halt(0); _ -> halt(1) catch error:undef -> - io:format("Undefined property or module?~n~p~n", [erlang:get_stacktrace()]), + io:format("Undefined property or module~n"), halt(0) end. endef @@ -2835,13 +2574,11 @@ triq: test-build endif else triq: test-build - $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \ - $(wildcard ebin/*.beam) $(call core_find,$(TEST_DIR)/,*.beam)))))) + $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename $(wildcard ebin/*.beam)))))) $(gen_verbose) $(call erlang,$(call triq_check.erl,all,undefined,$(MODULES))) endif endif -# Copyright (c) 2016, Loïc Hoguin # Copyright (c) 2015, Erlang Solutions Ltd. # This file is part of erlang.mk and subject to the terms of the ISC License. @@ -2850,22 +2587,22 @@ endif # Configuration. ifeq ($(XREF_CONFIG),) - XREFR_ARGS := + XREF_ARGS := else - XREFR_ARGS := -c $(XREF_CONFIG) + XREF_ARGS := -c $(XREF_CONFIG) endif XREFR ?= $(CURDIR)/xrefr export XREFR -XREFR_URL ?= https://github.com/inaka/xref_runner/releases/download/1.1.0/xrefr +XREFR_URL ?= https://github.com/inaka/xref_runner/releases/download/0.2.2/xrefr # Core targets. help:: - $(verbose) printf '%s\n' '' \ - 'Xref targets:' \ - ' xref Run Xrefr using $$XREF_CONFIG as config file if defined' + $(verbose) printf "%s\n" "" \ + "Xref targets:" \ + " xref Run Xrefr using $XREF_CONFIG as config file if defined" distclean:: distclean-xref @@ -2881,29 +2618,29 @@ xref: deps app $(XREFR) distclean-xref: $(gen_verbose) rm -rf $(XREFR) -# Copyright (c) 2016, Loïc Hoguin -# Copyright (c) 2015, Viktor Söderqvist +# Copyright 2015, Viktor Söderqvist # This file is part of erlang.mk and subject to the terms of the ISC License. -COVER_REPORT_DIR ?= cover -COVER_DATA_DIR ?= $(CURDIR) +COVER_REPORT_DIR = cover # Hook in coverage to ct ifdef COVER ifdef CT_RUN -ifneq ($(wildcard $(TEST_DIR)),) +# All modules in 'ebin' +COVER_MODS = $(notdir $(basename $(call core_ls,ebin/*.beam))) + test-build:: $(TEST_DIR)/ct.cover.spec -$(TEST_DIR)/ct.cover.spec: cover-data-dir +$(TEST_DIR)/ct.cover.spec: + $(verbose) echo Cover mods: $(COVER_MODS) $(gen_verbose) printf "%s\n" \ - "{incl_app, '$(PROJECT)', details}." \ - '{export,"$(abspath $(COVER_DATA_DIR))/ct.coverdata"}.' > $@ + '{incl_mods,[$(subst $(space),$(comma),$(COVER_MODS))]}.' \ + '{export,"$(CURDIR)/ct.coverdata"}.' > $@ CT_RUN += -cover $(TEST_DIR)/ct.cover.spec endif endif -endif # Core targets @@ -2912,13 +2649,6 @@ ifneq ($(COVER_REPORT_DIR),) tests:: $(verbose) $(MAKE) --no-print-directory cover-report endif - -cover-data-dir: | $(COVER_DATA_DIR) - -$(COVER_DATA_DIR): - $(verbose) mkdir -p $(COVER_DATA_DIR) -else -cover-data-dir: endif clean:: coverdata-clean @@ -2932,7 +2662,7 @@ help:: "Cover targets:" \ " cover-report Generate a HTML coverage report from previously collected" \ " cover data." \ - " all.coverdata Merge all coverdata files into all.coverdata." \ + " all.coverdata Merge {eunit,ct}.coverdata into one coverdata file." \ "" \ "If COVER=1 is set, coverage data is generated by the targets eunit and ct. The" \ "target tests additionally generates a HTML coverage report from the combined" \ @@ -2941,20 +2671,17 @@ help:: # Plugin specific targets -COVERDATA = $(filter-out $(COVER_DATA_DIR)/all.coverdata,$(wildcard $(COVER_DATA_DIR)/*.coverdata)) +COVERDATA = $(filter-out all.coverdata,$(wildcard *.coverdata)) .PHONY: coverdata-clean coverdata-clean: - $(gen_verbose) rm -f $(COVER_DATA_DIR)/*.coverdata $(TEST_DIR)/ct.cover.spec + $(gen_verbose) rm -f *.coverdata ct.cover.spec # Merge all coverdata files into one. -define cover_export.erl - $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) - cover:export("$(COVER_DATA_DIR)/$@"), halt(0). -endef - -all.coverdata: $(COVERDATA) cover-data-dir - $(gen_verbose) $(call erlang,$(cover_export.erl)) +all.coverdata: $(COVERDATA) + $(gen_verbose) $(ERL) -eval ' \ + $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) \ + cover:export("$@"), halt(0).' # These are only defined if COVER_REPORT_DIR is non-empty. Set COVER_REPORT_DIR to # empty if you want the coverdata files but not the HTML report. @@ -2964,7 +2691,6 @@ ifneq ($(COVER_REPORT_DIR),) cover-report-clean: $(gen_verbose) rm -rf $(COVER_REPORT_DIR) - $(if $(shell ls -A $(COVER_DATA_DIR)/),,$(verbose) rmdir $(COVER_DATA_DIR)) ifeq ($(COVERDATA),) cover-report: @@ -2973,7 +2699,7 @@ else # Modules which include eunit.hrl always contain one line without coverage # because eunit defines test/0 which is never called. We compensate for this. EUNIT_HRL_MODS = $(subst $(space),$(comma),$(shell \ - grep -H -e '^\s*-include.*include/eunit\.hrl"' src/*.erl \ + grep -e '^\s*-include.*include/eunit\.hrl"' src/*.erl \ | sed "s/^src\/\(.*\)\.erl:.*/'\1'/" | uniq)) define cover_report.erl @@ -3008,171 +2734,8 @@ define cover_report.erl endef cover-report: - $(verbose) mkdir -p $(COVER_REPORT_DIR) + $(gen_verbose) mkdir -p $(COVER_REPORT_DIR) $(gen_verbose) $(call erlang,$(cover_report.erl)) endif endif # ifneq ($(COVER_REPORT_DIR),) - -# Copyright (c) 2016, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: sfx - -ifdef RELX_REL -ifdef SFX - -# Configuration. - -SFX_ARCHIVE ?= $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/$(RELX_REL_NAME)-$(RELX_REL_VSN).tar.gz -SFX_OUTPUT_FILE ?= $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME).run - -# Core targets. - -rel:: sfx - -# Plugin-specific targets. - -define sfx_stub -#!/bin/sh - -TMPDIR=`mktemp -d` -ARCHIVE=`awk '/^__ARCHIVE_BELOW__$$/ {print NR + 1; exit 0;}' $$0` -FILENAME=$$(basename $$0) -REL=$${FILENAME%.*} - -tail -n+$$ARCHIVE $$0 | tar -xzf - -C $$TMPDIR - -$$TMPDIR/bin/$$REL console -RET=$$? - -rm -rf $$TMPDIR - -exit $$RET - -__ARCHIVE_BELOW__ -endef - -sfx: - $(call render_template,sfx_stub,$(SFX_OUTPUT_FILE)) - $(gen_verbose) cat $(SFX_ARCHIVE) >> $(SFX_OUTPUT_FILE) - $(verbose) chmod +x $(SFX_OUTPUT_FILE) - -endif -endif - -# Copyright (c) 2013-2017, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -# External plugins. - -DEP_PLUGINS ?= - -$(foreach p,$(DEP_PLUGINS),\ - $(eval $(if $(findstring /,$p),\ - $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\ - $(call core_dep_plugin,$p/plugins.mk,$p)))) - -# Copyright (c) 2013-2015, Loïc Hoguin -# Copyright (c) 2015-2016, Jean-Sébastien Pédron -# This file is part of erlang.mk and subject to the terms of the ISC License. - -# Fetch dependencies recursively (without building them). - -.PHONY: fetch-deps fetch-doc-deps fetch-rel-deps fetch-test-deps \ - fetch-shell-deps - -.PHONY: $(ERLANG_MK_RECURSIVE_DEPS_LIST) \ - $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) \ - $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) \ - $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) \ - $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST) - -fetch-deps: $(ERLANG_MK_RECURSIVE_DEPS_LIST) -fetch-doc-deps: $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) -fetch-rel-deps: $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) -fetch-test-deps: $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) -fetch-shell-deps: $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST) - -ifneq ($(SKIP_DEPS),) -$(ERLANG_MK_RECURSIVE_DEPS_LIST) \ -$(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) \ -$(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) \ -$(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) \ -$(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST): - $(verbose) :> $@ -else -# By default, we fetch "normal" dependencies. They are also included no -# matter the type of requested dependencies. -# -# $(ALL_DEPS_DIRS) includes $(BUILD_DEPS). - -$(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_DEPS_DIRS) -$(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST): $(ALL_DEPS_DIRS) $(ALL_DOC_DEPS_DIRS) -$(ERLANG_MK_RECURSIVE_REL_DEPS_LIST): $(ALL_DEPS_DIRS) $(ALL_REL_DEPS_DIRS) -$(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST): $(ALL_DEPS_DIRS) $(ALL_TEST_DEPS_DIRS) -$(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST): $(ALL_DEPS_DIRS) $(ALL_SHELL_DEPS_DIRS) - -# Allow to use fetch-deps and $(DEP_TYPES) to fetch multiple types of -# dependencies with a single target. -ifneq ($(filter doc,$(DEP_TYPES)),) -$(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_DOC_DEPS_DIRS) -endif -ifneq ($(filter rel,$(DEP_TYPES)),) -$(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_REL_DEPS_DIRS) -endif -ifneq ($(filter test,$(DEP_TYPES)),) -$(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_TEST_DEPS_DIRS) -endif -ifneq ($(filter shell,$(DEP_TYPES)),) -$(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_SHELL_DEPS_DIRS) -endif - -ERLANG_MK_RECURSIVE_TMP_LIST := $(abspath $(ERLANG_MK_TMP)/recursive-tmp-deps.log) - -$(ERLANG_MK_RECURSIVE_DEPS_LIST) \ -$(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) \ -$(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) \ -$(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) \ -$(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST): -ifeq ($(IS_APP)$(IS_DEP),) - $(verbose) mkdir -p $(ERLANG_MK_TMP) - $(verbose) rm -f $(ERLANG_MK_RECURSIVE_TMP_LIST) -endif -ifndef IS_APP - $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \ - $(MAKE) -C $$dep $@ \ - IS_APP=1 \ - ERLANG_MK_RECURSIVE_TMP_LIST=$(ERLANG_MK_RECURSIVE_TMP_LIST); \ - done -endif - $(verbose) set -e; for dep in $^ ; do \ - if ! grep -qs ^$$dep$$ $(ERLANG_MK_RECURSIVE_TMP_LIST); then \ - echo $$dep >> $(ERLANG_MK_RECURSIVE_TMP_LIST); \ - if grep -qs -E "^[[:blank:]]*include[[:blank:]]+(erlang\.mk|.*/erlang\.mk|.*ERLANG_MK_FILENAME.*)$$" \ - $$dep/GNUmakefile $$dep/makefile $$dep/Makefile; then \ - $(MAKE) -C $$dep fetch-deps \ - IS_DEP=1 \ - ERLANG_MK_RECURSIVE_TMP_LIST=$(ERLANG_MK_RECURSIVE_TMP_LIST); \ - fi \ - fi \ - done -ifeq ($(IS_APP)$(IS_DEP),) - $(verbose) sort < $(ERLANG_MK_RECURSIVE_TMP_LIST) | uniq > $@ - $(verbose) rm $(ERLANG_MK_RECURSIVE_TMP_LIST) -endif -endif # ifneq ($(SKIP_DEPS),) - -# List dependencies recursively. - -.PHONY: list-deps list-doc-deps list-rel-deps list-test-deps \ - list-shell-deps - -list-deps: $(ERLANG_MK_RECURSIVE_DEPS_LIST) -list-doc-deps: $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) -list-rel-deps: $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) -list-test-deps: $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) -list-shell-deps: $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST) - -list-deps list-doc-deps list-rel-deps list-test-deps list-shell-deps: - $(verbose) cat $^ diff --git a/src/emqx_pubsub.erl b/src/emqx_pubsub.erl index 03647fa3c..b4ac146a8 100644 --- a/src/emqx_pubsub.erl +++ b/src/emqx_pubsub.erl @@ -73,12 +73,12 @@ route([], #mqtt_delivery{message = Msg}) -> dropped(Msg#mqtt_message.topic), ignore; %% Dispatch on the local node. -route([#mqtt_route{topic = To, node = Node}], +route([#route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) when Node =:= node() -> dispatch(To, Delivery#mqtt_delivery{flows = [{route, Node, To} | Flows]}); %% Forward to other nodes -route([#mqtt_route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) -> +route([#route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) -> forward(Node, To, Delivery#mqtt_delivery{flows = [{route, Node, To}|Flows]}); route(Routes, Delivery) -> diff --git a/src/emqx_router.erl b/src/emqx_router.erl index c07d9759d..060786590 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -196,7 +196,7 @@ add_local_route(Topic) -> del_local_route(Topic) -> gen_server:call(?ROUTER, {del_local_route, Topic}). --spec(match_local(binary()) -> [mqtt_route()]). +-spec(match_local(binary()) -> [route()]). match_local(Name) -> case ets:info(local_route, size) of 0 -> []; From f7f0f27e4d91b745dc29bc844819281d0da7b729 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 27 Feb 2018 23:45:55 +0800 Subject: [PATCH 022/520] Parse and serialize MQTT 5.0 protocol packets --- include/emqx.hrl | 14 +-- src/emqx_alarm.erl | 16 +-- src/emqx_cli.erl | 32 ++++++ src/emqx_ctl.erl | 39 +++---- src/emqx_metrics.erl | 176 ++++------------------------- src/emqx_mqtt5_props.erl | 113 +++++++++++++++++++ src/emqx_mqtt5_rscode.erl | 195 ++++++++++++++++++++++++++++++++ src/emqx_mqtt_app.erl | 29 +++++ src/emqx_mqtt_metrics.erl | 159 ++++++++++++++++++++++++++ src/emqx_mqueue.erl | 12 +- src/emqx_parser.erl | 230 ++++++++++++++++++++++++++++++-------- src/emqx_plugins.erl | 12 +- src/emqx_pool_sup.erl | 5 +- src/emqx_pooler.erl | 19 ++-- src/emqx_serializer.erl | 129 ++++++++++++++++++--- src/emqx_stats.erl | 7 +- src/emqx_sysmon.erl | 26 ++--- src/emqx_trace.erl | 11 +- 18 files changed, 921 insertions(+), 303 deletions(-) create mode 100644 src/emqx_cli.erl create mode 100644 src/emqx_mqtt5_props.erl create mode 100644 src/emqx_mqtt5_rscode.erl create mode 100644 src/emqx_mqtt_app.erl create mode 100644 src/emqx_mqtt_metrics.erl diff --git a/include/emqx.hrl b/include/emqx.hrl index 1d80f009e..ccfaad6d0 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -164,26 +164,26 @@ -type(route() :: #route{}). %%-------------------------------------------------------------------- -%% MQTT Alarm +%% Alarm %%-------------------------------------------------------------------- --record(mqtt_alarm, +-record(alarm, { id :: binary(), - severity :: warning | error | critical, + severity :: notice | warning | error | critical, title :: iolist() | binary(), summary :: iolist() | binary(), timestamp :: erlang:timestamp() }). --type(mqtt_alarm() :: #mqtt_alarm{}). +-type(alarm() :: #alarm{}). %%-------------------------------------------------------------------- -%% MQTT Plugin +%% Plugin %%-------------------------------------------------------------------- --record(mqtt_plugin, { name, version, descr, active = false }). +-record(plugin, { name, version, descr, active = false }). --type(mqtt_plugin() :: #mqtt_plugin{}). +-type(plugin() :: #plugin{}). %%-------------------------------------------------------------------- %% MQTT CLI Command. For example: 'broker metrics' diff --git a/src/emqx_alarm.erl b/src/emqx_alarm.erl index df2134b94..f82ed5f2d 100644 --- a/src/emqx_alarm.erl +++ b/src/emqx_alarm.erl @@ -56,15 +56,15 @@ alarm_fun(Bool) -> (clear, _AlarmId) when Bool =:= false -> alarm_fun(false) end. --spec(set_alarm(mqtt_alarm()) -> ok). -set_alarm(Alarm) when is_record(Alarm, mqtt_alarm) -> +-spec(set_alarm(alarm()) -> ok). +set_alarm(Alarm) when is_record(Alarm, alarm) -> gen_event:notify(?ALARM_MGR, {set_alarm, Alarm}). -spec(clear_alarm(any()) -> ok). clear_alarm(AlarmId) when is_binary(AlarmId) -> gen_event:notify(?ALARM_MGR, {clear_alarm, AlarmId}). --spec(get_alarms() -> list(mqtt_alarm())). +-spec(get_alarms() -> list(alarm())). get_alarms() -> gen_event:call(?ALARM_MGR, ?MODULE, get_alarms). @@ -83,10 +83,10 @@ delete_alarm_handler(Module) when is_atom(Module) -> init(_) -> {ok, []}. -handle_event({set_alarm, Alarm = #mqtt_alarm{id = AlarmId, - severity = Severity, - title = Title, - summary = Summary}}, Alarms)-> +handle_event({set_alarm, Alarm = #alarm{id = AlarmId, + severity = Severity, + title = Title, + summary = Summary}}, Alarms)-> TS = os:timestamp(), Json = mochijson2:encode([{id, AlarmId}, {severity, Severity}, @@ -94,7 +94,7 @@ handle_event({set_alarm, Alarm = #mqtt_alarm{id = AlarmId, {summary, iolist_to_binary(Summary)}, {ts, emqx_time:now_secs(TS)}]), emqx:publish(alarm_msg(alert, AlarmId, Json)), - {ok, [Alarm#mqtt_alarm{timestamp = TS} | Alarms]}; + {ok, [Alarm#alarm{timestamp = TS} | Alarms]}; handle_event({clear_alarm, AlarmId}, Alarms) -> Json = mochijson2:encode([{id, AlarmId}, {ts, emqx_time:now_secs()}]), diff --git a/src/emqx_cli.erl b/src/emqx_cli.erl new file mode 100644 index 000000000..97a5ca298 --- /dev/null +++ b/src/emqx_cli.erl @@ -0,0 +1,32 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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_cli). + +-export([print/1, print/2, usage/1]). + +print(Msg) -> + io:format(Msg). + +print(Format, Args) -> + io:format(Format, Args). + +usage(CmdList) -> + lists:foreach( + fun({Cmd, Descr}) -> + io:format("~-48s# ~s~n", [Cmd, Descr]) + end, CmdList). + diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index 5f07125b2..ee1616b52 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -18,14 +18,6 @@ -behaviour(gen_server). --author("Feng Lee "). - --include("emqx.hrl"). - --include("emqx_cli.hrl"). - --define(SERVER, ?MODULE). - %% API Function Exports -export([start_link/0, register_cmd/2, register_cmd/3, unregister_cmd/1, lookup/1, run/1]). @@ -36,7 +28,9 @@ -record(state, {seq = 0}). --define(CMD_TAB, mqttd_ctl_cmd). +-define(SERVER, ?MODULE). + +-define(TAB, ?MODULE). %%-------------------------------------------------------------------- %% API @@ -87,40 +81,40 @@ run([CmdS|Args]) -> %% @doc Lookup a command -spec(lookup(atom()) -> [{module(), atom()}]). lookup(Cmd) -> - case ets:match(?CMD_TAB, {{'_', Cmd}, '$1', '_'}) of + case ets:match(?TAB, {{'_', Cmd}, '$1', '_'}) of [El] -> El; [] -> [] end. %% @doc Usage usage() -> - ?PRINT("Usage: ~s~n", [?MODULE]), - [begin ?PRINT("~80..-s~n", [""]), Mod:Cmd(usage) end - || {_, {Mod, Cmd}, _} <- ets:tab2list(?CMD_TAB)]. + io:format("Usage: ~s~n", [?MODULE]), + [begin io:format("~80..-s~n", [""]), Mod:Cmd(usage) end + || {_, {Mod, Cmd}, _} <- ets:tab2list(?TAB)]. %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- init([]) -> - ets:new(?CMD_TAB, [ordered_set, named_table, protected]), + ets:new(?TAB, [ordered_set, named_table, protected]), {ok, #state{seq = 0}}. handle_call(_Request, _From, State) -> {reply, ok, State}. handle_cast({register_cmd, Cmd, MF, Opts}, State = #state{seq = Seq}) -> - case ets:match(?CMD_TAB, {{'$1', Cmd}, '_', '_'}) of + case ets:match(?TAB, {{'$1', Cmd}, '_', '_'}) of [] -> - ets:insert(?CMD_TAB, {{Seq, Cmd}, MF, Opts}); + ets:insert(?TAB, {{Seq, Cmd}, MF, Opts}); [[OriginSeq] | _] -> lager:warning("CLI: ~s is overidden by ~p", [Cmd, MF]), - ets:insert(?CMD_TAB, {{OriginSeq, Cmd}, MF, Opts}) + ets:insert(?TAB, {{OriginSeq, Cmd}, MF, Opts}) end, noreply(next_seq(State)); handle_cast({unregister_cmd, Cmd}, State) -> - ets:match_delete(?CMD_TAB, {{'_', Cmd}, '_', '_'}), + ets:match_delete(?TAB, {{'_', Cmd}, '_', '_'}), noreply(State); handle_cast(_Msg, State) -> @@ -136,7 +130,7 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- -%% Internal Function Definitions +%% Internal Function %%-------------------------------------------------------------------- noreply(State) -> @@ -159,9 +153,10 @@ register_cmd_test_() -> ok = emqx_ctl:terminate(shutdown, State) end, fun(State = #state{seq = Seq}) -> - emqx_ctl:handle_cast({register_cmd, test0, {?MODULE, test0}, []}, State), - [?_assertMatch([{{0,test0},{?MODULE, test0}, []}], ets:lookup(?CMD_TAB, {Seq,test0}))] + emqx_ctl:handle_cast({register_cmd, test0, {?MODULE, test0}, []}, State), + [?_assertMatch([{{0,test0},{?MODULE, test0}, []}], ets:lookup(?TAB, {Seq,test0}))] end }. -endif. + diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 7c3d8f763..cb477aaa9 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -18,19 +18,8 @@ -behaviour(gen_server). --author("Feng Lee "). - --include("emqx.hrl"). - --include("emqx_mqtt.hrl"). - --define(SERVER, ?MODULE). - %% API Function Exports --export([start_link/0]). - -%% Received/Sent Metrics --export([received/1, sent/1]). +-export([start_link/0, create/1]). -export([all/0, value/1, inc/1, inc/2, inc/3, dec/2, dec/3, set/2]). @@ -40,58 +29,9 @@ -record(state, {tick}). --define(METRIC_TAB, mqtt_metric). +-define(TAB, ?MODULE). -%% Bytes sent and received of Broker --define(SYSTOP_BYTES, [ - {counter, 'bytes/received'}, % Total bytes received - {counter, 'bytes/sent'} % Total bytes sent -]). - -%% Packets sent and received of Broker --define(SYSTOP_PACKETS, [ - {counter, 'packets/received'}, % All Packets received - {counter, 'packets/sent'}, % All Packets sent - {counter, 'packets/connect'}, % CONNECT Packets received - {counter, 'packets/connack'}, % CONNACK Packets sent - {counter, 'packets/publish/received'}, % PUBLISH packets received - {counter, 'packets/publish/sent'}, % PUBLISH packets sent - {counter, 'packets/puback/received'}, % PUBACK packets received - {counter, 'packets/puback/sent'}, % PUBACK packets sent - {counter, 'packets/puback/missed'}, % PUBACK packets missed - {counter, 'packets/pubrec/received'}, % PUBREC packets received - {counter, 'packets/pubrec/sent'}, % PUBREC packets sent - {counter, 'packets/pubrec/missed'}, % PUBREC packets missed - {counter, 'packets/pubrel/received'}, % PUBREL packets received - {counter, 'packets/pubrel/sent'}, % PUBREL packets sent - {counter, 'packets/pubrel/missed'}, % PUBREL packets missed - {counter, 'packets/pubcomp/received'}, % PUBCOMP packets received - {counter, 'packets/pubcomp/sent'}, % PUBCOMP packets sent - {counter, 'packets/pubcomp/missed'}, % PUBCOMP packets missed - {counter, 'packets/subscribe'}, % SUBSCRIBE Packets received - {counter, 'packets/suback'}, % SUBACK packets sent - {counter, 'packets/unsubscribe'}, % UNSUBSCRIBE Packets received - {counter, 'packets/unsuback'}, % UNSUBACK Packets sent - {counter, 'packets/pingreq'}, % PINGREQ packets received - {counter, 'packets/pingresp'}, % PINGRESP Packets sent - {counter, 'packets/disconnect'} % DISCONNECT Packets received -]). - -%% Messages sent and received of broker --define(SYSTOP_MESSAGES, [ - {counter, 'messages/received'}, % All Messages received - {counter, 'messages/sent'}, % All Messages sent - {counter, 'messages/qos0/received'}, % QoS0 Messages received - {counter, 'messages/qos0/sent'}, % QoS0 Messages sent - {counter, 'messages/qos1/received'}, % QoS1 Messages received - {counter, 'messages/qos1/sent'}, % QoS1 Messages sent - {counter, 'messages/qos2/received'}, % QoS2 Messages received - {counter, 'messages/qos2/sent'}, % QoS2 Messages sent - {counter, 'messages/qos2/dropped'}, % QoS2 Messages dropped - {gauge, 'messages/retained'}, % Messagea retained - {counter, 'messages/dropped'}, % Messages dropped - {counter, 'messages/forward'} % Messages forward -]). +-define(SERVER, ?MODULE). %%-------------------------------------------------------------------- %% API @@ -102,81 +42,13 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -%% @doc Count packets received. --spec(received(mqtt_packet()) -> ignore | non_neg_integer()). -received(Packet) -> - inc('packets/received'), - received1(Packet). -received1(?PUBLISH_PACKET(Qos, _PktId)) -> - inc('packets/publish/received'), - inc('messages/received'), - qos_received(Qos); -received1(?PACKET(Type)) -> - received2(Type). -received2(?CONNECT) -> - inc('packets/connect'); -received2(?PUBACK) -> - inc('packets/puback/received'); -received2(?PUBREC) -> - inc('packets/pubrec/received'); -received2(?PUBREL) -> - inc('packets/pubrel/received'); -received2(?PUBCOMP) -> - inc('packets/pubcomp/received'); -received2(?SUBSCRIBE) -> - inc('packets/subscribe'); -received2(?UNSUBSCRIBE) -> - inc('packets/unsubscribe'); -received2(?PINGREQ) -> - inc('packets/pingreq'); -received2(?DISCONNECT) -> - inc('packets/disconnect'); -received2(_) -> - ignore. -qos_received(?QOS_0) -> - inc('messages/qos0/received'); -qos_received(?QOS_1) -> - inc('messages/qos1/received'); -qos_received(?QOS_2) -> - inc('messages/qos2/received'). +create({gauge, Name}) -> + ets:insert(?TAB, {{Name, 0}, 0}); + +create({counter, Name}) -> + Schedulers = lists:seq(1, erlang:system_info(schedulers)), + ets:insert(?TAB, [{{Name, I}, 0} || I <- Schedulers]). -%% @doc Count packets received. Will not count $SYS PUBLISH. --spec(sent(mqtt_packet()) -> ignore | non_neg_integer()). -sent(?PUBLISH_PACKET(_Qos, <<"$SYS/", _/binary>>, _, _)) -> - ignore; -sent(Packet) -> - inc('packets/sent'), - sent1(Packet). -sent1(?PUBLISH_PACKET(Qos, _PktId)) -> - inc('packets/publish/sent'), - inc('messages/sent'), - qos_sent(Qos); -sent1(?PACKET(Type)) -> - sent2(Type). -sent2(?CONNACK) -> - inc('packets/connack'); -sent2(?PUBACK) -> - inc('packets/puback/sent'); -sent2(?PUBREC) -> - inc('packets/pubrec/sent'); -sent2(?PUBREL) -> - inc('packets/pubrel/sent'); -sent2(?PUBCOMP) -> - inc('packets/pubcomp/sent'); -sent2(?SUBACK) -> - inc('packets/suback'); -sent2(?UNSUBACK) -> - inc('packets/unsuback'); -sent2(?PINGRESP) -> - inc('packets/pingresp'); -sent2(_Type) -> - ignore. -qos_sent(?QOS_0) -> - inc('messages/qos0/sent'); -qos_sent(?QOS_1) -> - inc('messages/qos1/sent'); -qos_sent(?QOS_2) -> - inc('messages/qos2/sent'). %% @doc Get all metrics -spec(all() -> [{atom(), non_neg_integer()}]). @@ -188,12 +60,12 @@ all() -> {ok, Count} -> maps:put(Metric, Count+Val, Map); error -> maps:put(Metric, Val, Map) end - end, #{}, ?METRIC_TAB)). + end, #{}, ?TAB)). %% @doc Get metric value -spec(value(atom()) -> non_neg_integer()). value(Metric) -> - lists:sum(ets:select(?METRIC_TAB, [{{{Metric, '_'}, '$1'}, [], ['$1']}])). + lists:sum(ets:select(?TAB, [{{{Metric, '_'}, '$1'}, [], ['$1']}])). %% @doc Increase counter -spec(inc(atom()) -> non_neg_integer()). @@ -212,9 +84,9 @@ inc(Metric, Val) when is_atom(Metric) -> %% @doc Increase metric value -spec(inc(counter | gauge, atom(), pos_integer()) -> pos_integer()). inc(gauge, Metric, Val) -> - ets:update_counter(?METRIC_TAB, key(gauge, Metric), {2, Val}); + ets:update_counter(?TAB, key(gauge, Metric), {2, Val}); inc(counter, Metric, Val) -> - ets:update_counter(?METRIC_TAB, key(counter, Metric), {2, Val}). + ets:update_counter(?TAB, key(counter, Metric), {2, Val}). %% @doc Decrease metric value -spec(dec(gauge, atom()) -> integer()). @@ -224,13 +96,13 @@ dec(gauge, Metric) -> %% @doc Decrease metric value -spec(dec(gauge, atom(), pos_integer()) -> integer()). dec(gauge, Metric, Val) -> - ets:update_counter(?METRIC_TAB, key(gauge, Metric), {2, -Val}). + ets:update_counter(?TAB, key(gauge, Metric), {2, -Val}). %% @doc Set metric value set(Metric, Val) when is_atom(Metric) -> set(gauge, Metric, Val). set(gauge, Metric, Val) -> - ets:insert(?METRIC_TAB, {key(gauge, Metric), Val}). + ets:insert(?TAB, {key(gauge, Metric), Val}). %% @doc Metric Key key(gauge, Metric) -> @@ -239,16 +111,13 @@ key(counter, Metric) -> {Metric, erlang:system_info(scheduler_id)}. %%-------------------------------------------------------------------- -%% gen_server callbacks +%% gen_server Callbacks %%-------------------------------------------------------------------- init([]) -> emqx_time:seed(), - Metrics = ?SYSTOP_BYTES ++ ?SYSTOP_PACKETS ++ ?SYSTOP_MESSAGES, % Create metrics table - ets:new(?METRIC_TAB, [set, public, named_table, {write_concurrency, true}]), - % Init metrics - [create_metric(Metric) || Metric <- Metrics], + ets:new(?TAB, [set, public, named_table, {write_concurrency, true}]), % Tick to publish metrics {ok, #state{tick = emqx_broker:start_tick(tick)}, hibernate}. @@ -278,14 +147,7 @@ code_change(_OldVsn, State, _Extra) -> publish(Metric, Val) -> Msg = emqx_message:make(metrics, metric_topic(Metric), bin(Val)), - emqx:publish(emqx_message:set_flag(sys, Msg)). - -create_metric({gauge, Name}) -> - ets:insert(?METRIC_TAB, {{Name, 0}, 0}); - -create_metric({counter, Name}) -> - Schedulers = lists:seq(1, erlang:system_info(schedulers)), - ets:insert(?METRIC_TAB, [{{Name, I}, 0} || I <- Schedulers]). + emqx_broker:publish(emqx_message:set_flag(sys, Msg)). metric_topic(Metric) -> emqx_topic:systop(list_to_binary(lists:concat(['metrics/', Metric]))). diff --git a/src/emqx_mqtt5_props.erl b/src/emqx_mqtt5_props.erl new file mode 100644 index 000000000..70857d18c --- /dev/null +++ b/src/emqx_mqtt5_props.erl @@ -0,0 +1,113 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqx.io) +%% +%% 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_mqtt5_props). + +-author("Feng Lee "). + +-export([name/1, id/1]). + +%%-------------------------------------------------------------------- +%% Property id to name +%%-------------------------------------------------------------------- + +%% 01: Byte; PUBLISH, Will Properties +name(16#01) -> 'Payload-Format-Indicator'; +%% 02: Four Byte Integer; PUBLISH, Will Properties +name(16#02) -> 'Message-Expiry-Interval'; +%% 03: UTF-8 Encoded String; PUBLISH, Will Properties +name(16#03) -> 'Content-Type'; +%% 08: UTF-8 Encoded String; PUBLISH, Will Properties +name(16#08) -> 'Response-Topic'; +%% 09: Binary Data; PUBLISH, Will Properties +name(16#09) -> 'Correlation-Data'; +%% 11: Variable Byte Integer; PUBLISH, SUBSCRIBE +name(16#0B) -> 'Subscription-Identifier'; +%% 17: Four Byte Integer; CONNECT, CONNACK, DISCONNECT +name(16#11) -> 'Session-Expiry-Interval'; +%% 18: UTF-8 Encoded String; CONNACK +name(16#12) -> 'Assigned-Client-Identifier'; +%% 19: Two Byte Integer; CONNACK +name(16#13) -> 'Server-Keep-Alive'; +%% 21: UTF-8 Encoded String; CONNECT, CONNACK, AUTH +name(16#15) -> 'Authentication-Method'; +%% 22: Binary Data; CONNECT, CONNACK, AUTH +name(16#16) -> 'Authentication-Data'; +%% 23: Byte; CONNECT +name(16#17) -> 'Request-Problem-Information'; +%% 24: Four Byte Integer; Will Properties +name(16#18) -> 'Will-Delay-Interval'; +%% 25: Byte; CONNECT +name(16#19) -> 'Request-Response-Information'; +%% 26: UTF-8 Encoded String; CONNACK +name(16#1A) -> 'Response Information'; +%% 28: UTF-8 Encoded String; CONNACK, DISCONNECT +name(16#1C) -> 'Server-Reference'; +%% 31: UTF-8 Encoded String; CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBACK, UNSUBACK, DISCONNECT, AUTH +name(16#1F) -> 'Reason-String'; +%% 33: Two Byte Integer; CONNECT, CONNACK +name(16#21) -> 'Receive-Maximum'; +%% 34: Two Byte Integer; CONNECT, CONNACK +name(16#22) -> 'Topic-Alias-Maximum'; +%% 35: Two Byte Integer; PUBLISH +name(16#23) -> 'Topic Alias'; +%% 36: Byte; CONNACK +name(16#24) -> 'Maximum-QoS'; +%% 37: Byte; CONNACK +name(16#25) -> 'Retain-Available'; +%% 38: UTF-8 String Pair; ALL +name(16#26) -> 'User-Property'; +%% 39: Four Byte Integer; CONNECT, CONNACK +name(16#27) -> 'Maximum-Packet-Size'; +%% 40: Byte; CONNACK +name(16#28) -> 'Wildcard-Subscription-Available'; +%% 41: Byte; CONNACK +name(16#29) -> 'Subscription-Identifier-Available'; +%% 42: Byte; CONNACK +name(16#2A) -> 'Shared-Subscription-Available'. + +%%-------------------------------------------------------------------- +%% Property name to id +%%-------------------------------------------------------------------- + +id('Payload-Format-Indicator') -> 16#01; +id('Message-Expiry-Interval') -> 16#02; +id('Content-Type') -> 16#03; +id('Response-Topic') -> 16#08; +id('Correlation-Data') -> 16#09; +id('Subscription-Identifier') -> 16#0B; +id('Session-Expiry-Interval') -> 16#11; +id('Assigned-Client-Identifier') -> 16#12; +id('Server-Keep-Alive') -> 16#13; +id('Authentication-Method') -> 16#15; +id('Authentication Data') -> 16#16; +id('Request-Problem-Information') -> 16#17; +id('Will-Delay-Interval') -> 16#18; +id('Request-Response-Information') -> 16#19; +id('Response Information') -> 16#1A; +id('Server-Reference') -> 16#1C; +id('Reason-String') -> 16#1F; +id('Receive-Maximum') -> 16#21; +id('Topic-Alias-Maximum') -> 16#22; +id('Topic Alias') -> 16#23; +id('Maximum-QoS') -> 16#24; +id('Retain-Available') -> 16#25; +id('User-Property') -> 16#26; +id('Maximum-Packet-Size') -> 16#27; +id('Wildcard-Subscription-Available') -> 16#28; +id('Subscription-Identifier-Available') -> 16#29; +id('Shared-Subscription-Available') -> 16#2A. + diff --git a/src/emqx_mqtt5_rscode.erl b/src/emqx_mqtt5_rscode.erl new file mode 100644 index 000000000..10c137678 --- /dev/null +++ b/src/emqx_mqtt5_rscode.erl @@ -0,0 +1,195 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqx.io) +%% +%% 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_mqtt5_rscode). + +-author("Feng Lee "). + +-export([name/1, value/1]). + +%%-------------------------------------------------------------------- +%% Reason code to name +%%-------------------------------------------------------------------- + +0 +name(0x00 +Success +CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK, AUTH +0 +name(0x00 +Normal disconnection +DISCONNECT +0 +name(0x00 +Granted QoS 0 +SUBACK +1 +name(0x01 +Granted QoS 1 +SUBACK +2 +name(0x02 +Granted QoS 2 +SUBACK +4 +name(0x04 +Disconnect with Will Message +DISCONNECT +16 +name(0x10 +No matching subscribers +PUBACK, PUBREC +17 +name(0x11 +No subscription existed +UNSUBACK +24 +name(0x18 +Continue authentication +AUTH +25 +name(0x19 +Re-authenticate +AUTH +128 +name(0x80 +Unspecified error +CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT +129 +name(0x81 +Malformed Packet +CONNACK, DISCONNECT +130 +name(0x82 +Protocol Error +CONNACK, DISCONNECT +131 +name(0x83 +Implementation specific error +CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT +132 +name(0x84 +Unsupported Protocol Version +CONNACK +133 +name(0x85 +Client Identifier not valid +CONNACK +134 +name(0x86 +Bad User Name or Password +CONNACK +135 +name(0x87 +Not authorized +CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT +136 +name(0x88 +Server unavailable +CONNACK +137 +name(0x89 +Server busy +CONNACK, DISCONNECT +138 +name(0x8A +Banned +CONNACK +139 +name(0x8B +Server shutting down +DISCONNECT +140 +name(0x8C +Bad authentication method +CONNACK, DISCONNECT +141 +name(0x8D +Keep Alive timeout +DISCONNECT +142 +name(0x8E +Session taken over +DISCONNECT +143 +name(0x8F +Topic Filter invalid +SUBACK, UNSUBACK, DISCONNECT +144 +name(0x90 +Topic Name invalid +CONNACK, PUBACK, PUBREC, DISCONNECT +145 +name(0x91 +Packet Identifier in use +PUBACK, PUBREC, SUBACK, UNSUBACK +146 +name(0x92 +Packet Identifier not found +PUBREL, PUBCOMP +147 +name(0x93 +Receive Maximum exceeded +DISCONNECT +148 +name(0x94 +Topic Alias invalid +DISCONNECT +149 +name(0x95 +Packet too large +CONNACK, DISCONNECT +150 +name(0x96 +Message rate too high +DISCONNECT +151 +name(0x97 +Quota exceeded +CONNACK, PUBACK, PUBREC, SUBACK, DISCONNECT +%% 152 +name(0x98 +Administrative action +DISCONNECT +%% 153 +name(0x99 +Payload format invalid +CONNACK, PUBACK, PUBREC, DISCONNECT +%% 154 +name(0x9A +Retain not supported +CONNACK, DISCONNECT +%% 155 +name(0x9B +QoS not supported +CONNACK, DISCONNECT +%% 156 +name(0x9C +Use another server +CONNACK, DISCONNECT +%% 157: CONNACK, DISCONNECT +name(0x9D) -> 'Server-Moved'; +%% 158: SUBACK, DISCONNECT +name(0x9E) -> 'Shared-Subscriptions-Not-Supported'; +%% 159: CONNACK, DISCONNECT +name(0x9F) -> 'Connection-Rate-Exceeded'; +%% 160: DISCONNECT +name(0xA0) -> 'Maximum-Connect-Time'; +%% 161: SUBACK, DISCONNECT +name(0xA1) -> 'Subscription-Identifiers-Not-Supported'; +%% 162: SUBACK, DISCONNECT +name(0xA2) -> 'Wildcard-Subscriptions-Not-Supported'; + diff --git a/src/emqx_mqtt_app.erl b/src/emqx_mqtt_app.erl new file mode 100644 index 000000000..bde8f4633 --- /dev/null +++ b/src/emqx_mqtt_app.erl @@ -0,0 +1,29 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_app). + +-behaviour(application). + +-export([start/2, stop/1]). + +start(_Type, _Args) -> + emqx_mqtt_metrics:init(), + emqx_mqtt_sup:start_link(). + +stop(_State) -> + ok. + diff --git a/src/emqx_mqtt_metrics.erl b/src/emqx_mqtt_metrics.erl new file mode 100644 index 000000000..c9be6bdfc --- /dev/null +++ b/src/emqx_mqtt_metrics.erl @@ -0,0 +1,159 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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_metrics). + +-include("emqx_mqtt.hrl"). + +-import(emqx_metrics, [inc/1]). + +-export([init/0]). + +%% Received/Sent Metrics +-export([received/1, sent/1]). + +%% Bytes sent and received of Broker +-define(SYSTOP_BYTES, [ + {counter, 'bytes/received'}, % Total bytes received + {counter, 'bytes/sent'} % Total bytes sent +]). + +%% Packets sent and received of Broker +-define(SYSTOP_PACKETS, [ + {counter, 'packets/received'}, % All Packets received + {counter, 'packets/sent'}, % All Packets sent + {counter, 'packets/connect'}, % CONNECT Packets received + {counter, 'packets/connack'}, % CONNACK Packets sent + {counter, 'packets/publish/received'}, % PUBLISH packets received + {counter, 'packets/publish/sent'}, % PUBLISH packets sent + {counter, 'packets/puback/received'}, % PUBACK packets received + {counter, 'packets/puback/sent'}, % PUBACK packets sent + {counter, 'packets/puback/missed'}, % PUBACK packets missed + {counter, 'packets/pubrec/received'}, % PUBREC packets received + {counter, 'packets/pubrec/sent'}, % PUBREC packets sent + {counter, 'packets/pubrec/missed'}, % PUBREC packets missed + {counter, 'packets/pubrel/received'}, % PUBREL packets received + {counter, 'packets/pubrel/sent'}, % PUBREL packets sent + {counter, 'packets/pubrel/missed'}, % PUBREL packets missed + {counter, 'packets/pubcomp/received'}, % PUBCOMP packets received + {counter, 'packets/pubcomp/sent'}, % PUBCOMP packets sent + {counter, 'packets/pubcomp/missed'}, % PUBCOMP packets missed + {counter, 'packets/subscribe'}, % SUBSCRIBE Packets received + {counter, 'packets/suback'}, % SUBACK packets sent + {counter, 'packets/unsubscribe'}, % UNSUBSCRIBE Packets received + {counter, 'packets/unsuback'}, % UNSUBACK Packets sent + {counter, 'packets/pingreq'}, % PINGREQ packets received + {counter, 'packets/pingresp'}, % PINGRESP Packets sent + {counter, 'packets/disconnect'} % DISCONNECT Packets received +]). + +%% Messages sent and received of broker +-define(SYSTOP_MESSAGES, [ + {counter, 'messages/received'}, % All Messages received + {counter, 'messages/sent'}, % All Messages sent + {counter, 'messages/qos0/received'}, % QoS0 Messages received + {counter, 'messages/qos0/sent'}, % QoS0 Messages sent + {counter, 'messages/qos1/received'}, % QoS1 Messages received + {counter, 'messages/qos1/sent'}, % QoS1 Messages sent + {counter, 'messages/qos2/received'}, % QoS2 Messages received + {counter, 'messages/qos2/sent'}, % QoS2 Messages sent + {counter, 'messages/qos2/dropped'}, % QoS2 Messages dropped + {gauge, 'messages/retained'}, % Messagea retained + {counter, 'messages/dropped'}, % Messages dropped + {counter, 'messages/forward'} % Messages forward +]). + +% Init metrics +init() -> + lists:foreach(fun emqx_metrics:create/1, + ?SYSTOP_BYTES ++ ?SYSTOP_PACKETS ++ ?SYSTOP_MESSAGES). + +%% @doc Count packets received. +-spec(received(mqtt_packet()) -> ok). +received(Packet) -> + inc('packets/received'), + received1(Packet). +received1(?PUBLISH_PACKET(Qos, _PktId)) -> + inc('packets/publish/received'), + inc('messages/received'), + qos_received(Qos); +received1(?PACKET(Type)) -> + received2(Type). +received2(?CONNECT) -> + inc('packets/connect'); +received2(?PUBACK) -> + inc('packets/puback/received'); +received2(?PUBREC) -> + inc('packets/pubrec/received'); +received2(?PUBREL) -> + inc('packets/pubrel/received'); +received2(?PUBCOMP) -> + inc('packets/pubcomp/received'); +received2(?SUBSCRIBE) -> + inc('packets/subscribe'); +received2(?UNSUBSCRIBE) -> + inc('packets/unsubscribe'); +received2(?PINGREQ) -> + inc('packets/pingreq'); +received2(?DISCONNECT) -> + inc('packets/disconnect'); +received2(_) -> + ignore. +qos_received(?QOS_0) -> + inc('messages/qos0/received'); +qos_received(?QOS_1) -> + inc('messages/qos1/received'); +qos_received(?QOS_2) -> + inc('messages/qos2/received'). + +%% @doc Count packets received. Will not count $SYS PUBLISH. +-spec(sent(mqtt_packet()) -> ignore | non_neg_integer()). +sent(?PUBLISH_PACKET(_Qos, <<"$SYS/", _/binary>>, _, _)) -> + ignore; +sent(Packet) -> + inc('packets/sent'), + sent1(Packet). +sent1(?PUBLISH_PACKET(Qos, _PktId)) -> + inc('packets/publish/sent'), + inc('messages/sent'), + qos_sent(Qos); +sent1(?PACKET(Type)) -> + sent2(Type). +sent2(?CONNACK) -> + inc('packets/connack'); +sent2(?PUBACK) -> + inc('packets/puback/sent'); +sent2(?PUBREC) -> + inc('packets/pubrec/sent'); +sent2(?PUBREL) -> + inc('packets/pubrel/sent'); +sent2(?PUBCOMP) -> + inc('packets/pubcomp/sent'); +sent2(?SUBACK) -> + inc('packets/suback'); +sent2(?UNSUBACK) -> + inc('packets/unsuback'); +sent2(?PINGRESP) -> + inc('packets/pingresp'); +sent2(_Type) -> + ignore. +qos_sent(?QOS_0) -> + inc('messages/qos0/sent'); +qos_sent(?QOS_1) -> + inc('messages/qos1/sent'); +qos_sent(?QOS_2) -> + inc('messages/qos2/sent'). + diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index 5692577a2..190e525fc 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -43,8 +43,8 @@ -module(emqx_mqueue). --author("Feng Lee "). - +%% TODO: XYZ +%% -include("emqx.hrl"). -include("emqx_mqtt.hrl"). @@ -209,10 +209,10 @@ maybe_set_alarm(MQ = #mqueue{high_wm = undefined}) -> MQ; maybe_set_alarm(MQ = #mqueue{name = Name, len = Len, high_wm = HighWM, alarm_fun = AlarmFun}) when Len > HighWM -> - Alarm = #mqtt_alarm{id = iolist_to_binary(["queue_high_watermark.", Name]), - severity = warning, - title = io_lib:format("Queue ~s high-water mark", [Name]), - summary = io_lib:format("queue len ~p > high_watermark ~p", [Len, HighWM])}, + Alarm = #alarm{id = iolist_to_binary(["queue_high_watermark.", Name]), + severity = warning, + title = io_lib:format("Queue ~s high-water mark", [Name]), + summary = io_lib:format("queue len ~p > high_watermark ~p", [Len, HighWM])}, MQ#mqueue{alarm_fun = AlarmFun(alert, Alarm)}; maybe_set_alarm(MQ) -> MQ. diff --git a/src/emqx_parser.erl b/src/emqx_parser.erl index 7d8f0d314..c7539112e 100644 --- a/src/emqx_parser.erl +++ b/src/emqx_parser.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -14,7 +14,6 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc MQTT Packet Parser -module(emqx_parser). -author("Feng Lee "). @@ -74,12 +73,12 @@ parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value, Max true -> parse_frame(Rest, Header, FrameLen) end. -parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) -> +parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) -> case {Type, Bin} of {?CONNECT, <>} -> {ProtoName, Rest1} = parse_utf(FrameBin), %% Fix mosquitto bridge: 0x83, 0x84 - <> = Rest1, + <> = Rest1, <> = Rest2, - {ClientId, Rest4} = parse_utf(Rest3), - {WillTopic, Rest5} = parse_utf(Rest4, WillFlag), - {WillMsg, Rest6} = parse_msg(Rest5, WillFlag), - {UserName, Rest7} = parse_utf(Rest6, UsernameFlag), - {PasssWord, <<>>} = parse_utf(Rest7, PasswordFlag), + {Properties, Rest4} = parse_properties(ProtoVer, Rest3), + {ClientId, Rest5} = parse_utf(Rest4), + {WillProps, Rest6} = parse_will_props(Rest5, ProtoVer, WillFlag), + {WillTopic, Rest7} = parse_utf(Rest6, WillFlag), + {WillMsg, Rest8} = parse_msg(Rest7, WillFlag), + {UserName, Rest9} = parse_utf(Rest8, UsernameFlag), + {PasssWord, <<>>} = parse_utf(Rest9, PasswordFlag), case protocol_name_approved(ProtoVersion, ProtoName) of true -> wrap(Header, #mqtt_packet_connect{ - proto_ver = ProtoVersion, + proto_ver = ProtoVer, proto_name = ProtoName, will_retain = bool(WillRetain), will_qos = WillQos, @@ -106,11 +107,13 @@ parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) clean_sess = bool(CleanSess), keep_alive = KeepAlive, client_id = ClientId, + will_props = WillProps, will_topic = WillTopic, will_msg = WillMsg, username = UserName, password = PasssWord, - is_bridge = (BridgeTag =:= 8)}, Rest); + is_bridge = (BridgeTag =:= 8), + properties = Properties}, Rest); false -> {error, protocol_header_corrupt} end; @@ -120,55 +123,72 @@ parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) % return_code = ReturnCode }, Rest); {?PUBLISH, <>} -> {TopicName, Rest1} = parse_utf(FrameBin), - {PacketId, Payload} = case Qos of - 0 -> {undefined, Rest1}; - _ -> <> = Rest1, - {Id, R} - end, + {PacketId, Rest2} = case Qos of + 0 -> {undefined, Rest1}; + _ -> <> = Rest1, + {Id, R} + end, + {Properties, Payload} = parse_properties(ProtoVer, Rest), wrap(fixdup(Header), #mqtt_packet_publish{topic_name = TopicName, - packet_id = PacketId}, + packet_id = PacketId, + properties = Properties}, Payload, Rest); - {?PUBACK, <>} -> - <> = FrameBin, - wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest); - {?PUBREC, <>} -> - <> = FrameBin, - wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest); - {?PUBREL, <>} -> - %% 1 = Qos, - <> = FrameBin, - wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest); - {?PUBCOMP, <>} -> - <> = FrameBin, - wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest); + {PubAck, <>} + when PubAck == ?PUBACK; PubAck == ?PUBREC; PubAck == ?PUBREL; PubAck == ?PUBCOMP -> + <> = FrameBin, + case ProtoVer == ?MQTT_PROTO_V5 of + true -> + <> = Rest1, + {Properties, Rest3} = parse_properties(ProtoVer, Rest2), + wrap(Header, #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties}, Rest3); + false -> + wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest) + end; {?SUBSCRIBE, <>} -> %% 1 = Qos, <> = FrameBin, + {Properties, Rest2} = parse_properties(ProtoVer, Rest1), TopicTable = parse_topics(?SUBSCRIBE, Rest1, []), wrap(Header, #mqtt_packet_subscribe{packet_id = PacketId, + properties = Properties, topic_table = TopicTable}, Rest); %{?SUBACK, <>} -> % <> = FrameBin, - % wrap(Header, #mqtt_packet_suback{packet_id = PacketId, - % qos_table = parse_qos(Rest1, []) }, Rest); + % {Properties, Rest2/binary>> = parse_properties(ProtoVer, Rest1), + % wrap(Header, #mqtt_packet_suback{packet_id = PacketId, properties = Properties, + % reason_codes = parse_qos(Rest1, [])}, Rest); {?UNSUBSCRIBE, <>} -> %% 1 = Qos, <> = FrameBin, - Topics = parse_topics(?UNSUBSCRIBE, Rest1, []), - wrap(Header, #mqtt_packet_unsubscribe{packet_id = PacketId, - topics = Topics}, Rest); + {Properties, Rest2} = parse_properties(ProtoVer, Rest1), + Topics = parse_topics(?UNSUBSCRIBE, Rest2, []), + wrap(Header, #mqtt_packet_unsubscribe{packet_id = PacketId, + properties = Properties, + topics = Topics}, Rest); %{?UNSUBACK, <>} -> - % <> = FrameBin, - % wrap(Header, #mqtt_packet_unsuback { packet_id = PacketId }, Rest); + % <> = FrameBin, + % {Properties, Rest2} = parse_properties(ProtoVer, Rest1), + % wrap(Header, #mqtt_packet_unsuback { + % packet_id = PacketId, + % properties = Properties }, Rest); {?PINGREQ, Rest} -> Length = 0, wrap(Header, Rest); %{?PINGRESP, Rest} -> % Length = 0, % wrap(Header, Rest); - {?DISCONNECT, Rest} -> - Length = 0, - wrap(Header, Rest); + {?DISCONNECT, <>} -> + case ProtoVer == ?MQTT_PROTO_V5 of + true -> + <> = Rest, + {Properties, Rest2} = parse_properties(ProtoVer, Rest1), + wrap(Header, #mqtt_packet_disconnect{reason_code = Reason, + properties = Properties}, Rest2); + false -> + Lenght = 0, wrap(Header, Rest) + end; {_, TooShortBin} -> {more, fun(BinMore) -> parse_frame(<>, @@ -183,21 +203,134 @@ wrap(Header, Variable, Rest) -> wrap(Header, Rest) -> {ok, #mqtt_packet{header = Header}, Rest}. -%client function -%parse_qos(<<>>, Acc) -> -% lists:reverse(Acc); -%parse_qos(<>, Acc) -> -% parse_qos(Rest, [QoS | Acc]). +parse_will_props(Bin, ProtoVer = ?MQTT_PROTO_V5, 1) -> + parse_properties(ProtoVer, Bin); +parse_will_props(Bin, _ProtoVer, _WillFlag), + {#{}, Bin}. -parse_topics(_, <<>>, Topics) -> +parse_properties(?MQTT_PROTO_V5, Bin) -> + {Len, Rest} = parse_variable_byte_integer(Bin), + < + {#{}, Bin}. %% No properties. + +parse_property(<<>>, Props) -> + Props; +%% 01: 'Payload-Format-Indicator', Byte; +parse_property(<<16#01, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Payload-Format-Indicator' => Val}); +%% 02: 'Message-Expiry-Interval', Four Byte Integer; +parse_property(<<16#02, Val:32/big, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Message-Expiry-Interval' => Val}); +%% 03: 'Content-Type', UTF-8 Encoded String; +parse_property(<<16#03, Bin/binary>>, Props) -> + {Val, Rest} = parse_utf(Bin), + parse_property(Rest, Props#{'Content-Type' => Val}); +%% 08: 'Response-Topic', UTF-8 Encoded String; +parse_property(<<16#08, Bin/binary>>) -> + {Val, Rest} = parse_utf(Bin), + parse_property(Rest, Props#{'Response-Topic' => Val}); +%% 09: 'Correlation-Data', Binary Data; +parse_property(<<16#09, Len:16/big, Val:Len/binary, Bin/binary>>) -> + parse_property(Bin, Props#{'Correlation-Data' => Val}); +%% 11: 'Subscription-Identifier', Variable Byte Integer; +parse_property(<<16#0B, Bin/binary>>, Props) -> + {Val, Rest} = parse_variable_byte_integer(Bin), + parse_property(Rest, Props#{'Subscription-Identifier' => Val}); +%% 17: 'Session-Expiry-Interval', Four Byte Integer; +parse_property(<<16#11, Val:32/big, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Session-Expiry-Interval' => Val}); +%% 18: 'Assigned-Client-Identifier', UTF-8 Encoded String; +parse_property(<<16#12, Bin/binary>>) -> + {Val, Rest} = parse_utf(Bin), + parse_property(Rest, Props#{'Assigned-Client-Identifier' => Val}); +%% 19: 'Server-Keep-Alive', Two Byte Integer; +parse_property(<<16#13, Val:16, Bin/binary>>) -> + parse_property(Bin, Props#{'Server-Keep-Alive' => Val}); +%% 21: 'Authentication-Method', UTF-8 Encoded String; +parse_property(<<16#15, Bin/binary>>, Props) -> + {Val, Rest} = parse_utf(Bin), + parse_property(Rest, Props#{'Authentication-Method' => Val}) +%% 22: 'Authentication-Data', Binary Data; +parse_property(<<16#16, Len:16/big, Val:Len/binary, Bin/binary>>) -> + parse_property(Bin, Props#{'Authentication-Data' => Val}); +%% 23: 'Request-Problem-Information', Byte; +parse_property(<<16#17, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Request-Problem-Information' => Val}); +%% 24: 'Will-Delay-Interval', Four Byte Integer; +parse_property(<<16#18, Val:32, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Will-Delay-Interval' => Val}); +%% 25: 'Request-Response-Information', Byte; +parse_property(<<16#19, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Request-Response-Information' => Val}); +%% 26: 'Response Information', UTF-8 Encoded String; +parse_property(<<16#1A, Bin/binary>>, Props) -> + {Val, Rest} = parse_utf(Bin), + parse_property(Rest, Props#{'Response-Information' => Val}); +%% 28: 'Server-Reference', UTF-8 Encoded String; +parse_property(<<16#1C, Bin/binary>>, Props) -> + {Val, Rest} = parse_utf(Bin), + parse_property(Rest, Props#{'Server-Reference' => Val}); +%% 31: 'Reason-String', UTF-8 Encoded String; +parse_property(<<16#1F, Bin/binary, Props) -> + {Val, Rest} = parse_utf(Bin), + parse_property(Rest, Props#{'Reason-String' => Val}); +%% 33: 'Receive-Maximum', Two Byte Integer; +parse_property(<<16#21, Val:16/big, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Receive-Maximum' => Val}); +%% 34: 'Topic-Alias-Maximum', Two Byte Integer; +parse_property(<<16#22, Val:16/big, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Topic-Alias-Maximum' => Val}); +%% 35: 'Topic-Alias', Two Byte Integer; +parse_property(<<16#23, Val:16/big, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Topic-Alias' => Val}); +%% 36: 'Maximum-QoS', Byte; +parse_property(<<16#24, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Maximum-QoS' => Val}); +%% 37: 'Retain-Available', Byte; +parse_property(<<16#25, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Retain-Available' => Val}); +%% 38: 'User-Property', UTF-8 String Pair; +parse_property(<<16#26, Bin/binary>>, Props) -> + {Pair, Rest} = parse_utf_pair(Bin), + parse_property(Rest, case maps:find('User-Property', Props) of + {ok, UserProps} -> Props#{'User-Property' := [Pair | UserProps]}; + error -> Props#{'User-Property' := [Pair]} + end); +%% 39: 'Maximum-Packet-Size', Four Byte Integer; +parse_property(<<16#27, Val:32, Bin/binary>>, Props) -> + parse_property(Rest, Props#{'Maximum-Packet-Size' => Val}); +%% 40: 'Wildcard-Subscription-Available', Byte; +parse_property(<<16#28, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Wildcard-Subscription-Available' => Val}); +%% 41: 'Subscription-Identifier-Available', Byte; +parse_property(<<16#29, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Subscription-Identifier-Available' => Val}); +%% 42: 'Shared-Subscription-Available', Byte; +parse_property(<<16#2A, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Shared-Subscription-Available' => Val}). + +parse_variable_byte_integer(Bin) -> + parse_variable_byte_integer(Bin, 1, 0). +parse_variable_byte_integer(<<1:1, Len:7, Rest/binary>>, Multiplier, Value) -> + parse_variable_byte_integer(Rest, Multiplier * ?HIGHBIT, Value + Len * Multiplier); +parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) -> + {Value + Len * Multiplier, Rest}. + +parse_topics(_Packet, <<>>, Topics) -> lists:reverse(Topics); parse_topics(?SUBSCRIBE = Sub, Bin, Topics) -> - {Name, <<_:6, QoS:2, Rest/binary>>} = parse_utf(Bin), - parse_topics(Sub, Rest, [{Name, QoS}| Topics]); + {Name, <<<<_Reserved:2, RetainHandling:2, KeepRetain:1, NoLocal:1, QoS:2>>, Rest/binary>>} = parse_utf(Bin), + SubOpts = [{qos, Qos}, {retain_handling, RetainHandling}, {keep_retain, KeepRetain}, {no_local, NoLocal}], + parse_topics(Sub, Rest, [{Name, SubOpts}| Topics]); parse_topics(?UNSUBSCRIBE = Sub, Bin, Topics) -> {Name, <>} = parse_utf(Bin), parse_topics(Sub, Rest, [Name | Topics]). +parse_utf_pair(Bin) -> + [{Name, Value} || <> <= Bin]. + parse_utf(Bin, 0) -> {undefined, Bin}; parse_utf(Bin, _) -> @@ -229,3 +362,4 @@ fixdup(Header = #mqtt_packet_header{qos = ?QOS0, dup = true}) -> fixdup(Header = #mqtt_packet_header{qos = ?QOS2, dup = true}) -> Header#mqtt_packet_header{dup = false}; fixdup(Header) -> Header. + diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index a9b3c5c25..4eddf7331 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -157,16 +157,16 @@ stop_plugins(Names) -> [stop_app(App) || App <- Names]. %% @doc List all available plugins --spec(list() -> [mqtt_plugin()]). +-spec(list() -> [plugin()]). list() -> case emqx:env(plugins_etc_dir) of {ok, PluginsEtc} -> CfgFiles = filelib:wildcard("*.{conf,config}", PluginsEtc) ++ get_expand_plugin_config(), Plugins = [plugin(CfgFile) || CfgFile <- CfgFiles], StartedApps = names(started_app), - lists:map(fun(Plugin = #mqtt_plugin{name = Name}) -> + lists:map(fun(Plugin = #plugin{name = Name}) -> case lists:member(Name, StartedApps) of - true -> Plugin#mqtt_plugin{active = true}; + true -> Plugin#plugin{active = true}; false -> Plugin end end, Plugins); @@ -179,7 +179,7 @@ plugin(CfgFile) -> {ok, Attrs} = application:get_all_key(AppName), Ver = proplists:get_value(vsn, Attrs, "0"), Descr = proplists:get_value(description, Attrs, ""), - #mqtt_plugin{name = AppName, version = Ver, descr = Descr}. + #plugin{name = AppName, version = Ver, descr = Descr}. %% @doc Load a Plugin -spec(load(atom()) -> ok | {error, term()}). @@ -198,7 +198,7 @@ load(PluginName) when is_atom(PluginName) -> end end. -load_plugin(#mqtt_plugin{name = Name}, Persistent) -> +load_plugin(#plugin{name = Name}, Persistent) -> case load_app(Name) of ok -> start_app(Name, fun(App) -> plugin_loaded(App, Persistent) end); @@ -280,7 +280,7 @@ names(started_app) -> [Name || {Name, _Descr, _Ver} <- application:which_applications()]; names(Plugins) -> - [Name || #mqtt_plugin{name = Name} <- Plugins]. + [Name || #plugin{name = Name} <- Plugins]. plugin_loaded(_Name, false) -> ok; diff --git a/src/emqx_pool_sup.erl b/src/emqx_pool_sup.erl index 8cdc31910..45af0b81b 100644 --- a/src/emqx_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -14,11 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc Common Pool Supervisor -module(emqx_pool_sup). --author("Feng Lee "). - -behaviour(supervisor). %% API diff --git a/src/emqx_pooler.erl b/src/emqx_pooler.erl index 499c096b8..3cae2264d 100644 --- a/src/emqx_pooler.erl +++ b/src/emqx_pooler.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -20,8 +20,6 @@ -behaviour(gen_server). --include("emqx_internal.hrl"). - %% Start the pool supervisor -export([start_link/0]). @@ -44,7 +42,9 @@ start_link() -> -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> - gen_server:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id], []). + gen_server:start_link({local, name(Id)}, ?MODULE, [Pool, Id], []). + +name(Id) -> list_to_atom(lists:concat([?MODULE, "_", Id])). %% @doc Submit work to pooler submit(Fun) -> gen_server:call(worker(), {submit, Fun}, infinity). @@ -61,20 +61,17 @@ worker() -> %%-------------------------------------------------------------------- init([Pool, Id]) -> - ?GPROC_POOL(join, Pool, Id), + gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #state{pool = Pool, id = Id}}. handle_call({submit, Fun}, _From, State) -> - {reply, run(Fun), State}; + {reply, catch run(Fun), State}; handle_call(_Req, _From, State) -> {reply, ok, State}. handle_cast({async_submit, Fun}, State) -> - try run(Fun) - catch _:Error -> - lager:error("Pooler Error: ~p, ~p", [Error, erlang:get_stacktrace()]) - end, + try run(Fun) catch _:Error -> lager:error("Pooler Error: ~p, ~p", [Error, erlang:get_stacktrace()]) end, {noreply, State}; handle_cast(_Msg, State) -> @@ -84,7 +81,7 @@ handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> - ?GPROC_POOL(leave, Pool, Id), ok. + gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> {ok, State}. diff --git a/src/emqx_serializer.erl b/src/emqx_serializer.erl index 977b934ce..f99497d84 100644 --- a/src/emqx_serializer.erl +++ b/src/emqx_serializer.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -14,7 +14,6 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc MQTT Packet Serializer -module(emqx_serializer). -author("Feng Lee "). @@ -81,23 +80,28 @@ serialize_variable(?CONNECT, #mqtt_packet_connect{client_id = ClientId, {VariableBin, <>}; serialize_variable(?CONNACK, #mqtt_packet_connack{ack_flags = AckFlags, - return_code = ReturnCode}, undefined) -> - {<>, <<>>}; + reason_code = ReasonCode, + properties = Properties}, undefined) -> + PropsBin = serialize_properties(Properties), + {<>, <<>>}; serialize_variable(?SUBSCRIBE, #mqtt_packet_subscribe{packet_id = PacketId, topic_table = Topics }, undefined) -> {<>, serialize_topics(Topics)}; serialize_variable(?SUBACK, #mqtt_packet_suback{packet_id = PacketId, - qos_table = QosTable}, undefined) -> - {<>, << <> || Q <- QosTable >>}; + properties = Properties, + reason_codes = ReasonCodes}, undefined) -> + {<>, << <> || Code <- ReasonCodes >>}; serialize_variable(?UNSUBSCRIBE, #mqtt_packet_unsubscribe{packet_id = PacketId, topics = Topics }, undefined) -> {<>, serialize_topics(Topics)}; -serialize_variable(?UNSUBACK, #mqtt_packet_unsuback{packet_id = PacketId}, undefined) -> - {<>, <<>>}; +serialize_variable(?UNSUBACK, #mqtt_packet_unsuback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes}, undefined) -> + {<>, << <> || Code <- ReasonCodes >>}; serialize_variable(?PUBLISH, #mqtt_packet_publish{topic_name = TopicName, packet_id = PacketId }, PayloadBin) -> @@ -118,33 +122,132 @@ serialize_variable(?PINGREQ, undefined, undefined) -> serialize_variable(?PINGRESP, undefined, undefined) -> {<<>>, <<>>}; -serialize_variable(?DISCONNECT, undefined, undefined) -> - {<<>>, <<>>}. +serialize_variable(?DISCONNECT, #mqtt_packet_disconnect{reason_code = ReasonCode, + properties = Properties}, undefined) -> + {<>, <<>>}. + +serialize_variable(?AUTH, #mqtt_packet_auth{reason_code = ReasonCode, + properties = Properties}, undefined) -> + {<>, <<>>}. serialize_payload(undefined) -> undefined; serialize_payload(Bin) when is_binary(Bin) -> Bin. +serialize_properties(undefined) -> + <<>>; +serialize_properties(Props) -> + << serialize_property(Prop, Val) || {Prop, Val} <= (maps:to_list(Props)) >>. + +%% 01: Byte; +serialize_property('Payload-Format-Indicator', Val) -> + <<16#01, Val>>; +%% 02: Four Byte Integer; +serialize_property('Message-Expiry-Interval', Val) -> + <<16#02, Val:32/big>>; +%% 03: UTF-8 Encoded String; +serialize_property('Content-Type', Val) -> + <<16#03, (serialize_utf(Val))/binary>>; +%% 08: UTF-8 Encoded String; +serialize_property('Response-Topic', Val) -> + <<16#08, (serialize_utf(Val))/binary>>; +%% 09: Binary Data; +serialize_property('Correlation-Data', Val) -> + <<16#09, (iolist_size(Val)):16, Val/binary>>; +%% 11: Variable Byte Integer; +serialize_property('Subscription-Identifier', Val) -> + <<16#0B, (serialize_variable_byte_integer(Val))/binary>>; +%% 17: Four Byte Integer; +serialize_property('Session-Expiry-Interval', Val) -> + <<16#11, Val:32/big>>; +%% 18: UTF-8 Encoded String; +serialize_property('Assigned-Client-Identifier', Val) -> + <<16#12, (serialize_utf(Val))/binary>>; +%% 19: Two Byte Integer; +serialize_property('Server-Keep-Alive', Val) -> + <<16#13, Val:16/big>>; +%% 21: UTF-8 Encoded String; +serialize_property('Authentication-Method', Val) -> + <<16#15, (serialize_utf(Val))/binary>>; +%% 22: Binary Data; +serialize_property('Authentication-Data', Val) -> + <<16#16, (iolist_size(Val)):16, Val/binary>>; +%% 23: Byte; +serialize_property('Request-Problem-Information', Val) -> + <<16#17, Val>>; +%% 24: Four Byte Integer; +serialize_property('Will-Delay-Interval', Val) -> + <<16#18, Val:32/big>>; +%% 25: Byte; +serialize_property('Request-Response-Information', Val) -> + <<16#19, Val>>; +%% 26: UTF-8 Encoded String; +serialize_property('Response-Information', Val) -> + <<16#1A, (serialize_utf(Val))/binary>>; +%% 28: UTF-8 Encoded String; +serialize_property('Server-Reference', Val) -> + <<16#1C, (serialize_utf(Val))/binary>>; +%% 31: UTF-8 Encoded String; +serialize_property('Reason-String', Val) -> + <<16#1F, (serialize_utf(Val))/binary>>; +%% 33: Two Byte Integer; +serialize_property('Receive-Maximum', Val) -> + <<16#21, Val:16/big>>; +%% 34: Two Byte Integer; +serialize_property('Topic-Alias-Maximum', Val) -> + <<16#22, Val:16/big>>; +%% 35: Two Byte Integer; +serialize_property('Topic-Alias', Val) -> + <<16#23, Val:16/big>>; +%% 36: Byte; +serialize_property('Maximum-QoS', Val) -> + <<16#24, Val>>; +%% 37: Byte; +serialize_property('Retain-Available', Val) -> + <<16#25, Val>>; +%% 38: UTF-8 String Pair; +serialize_property('User-Property', Val) -> + <<16#26, (serialize_utf_pair(Val))/binary>>; +%% 39: Four Byte Integer; +serialize_property('Maximum-Packet-Size', Val) -> + <<16#27, Val:32/big>>; +%% 40: Byte; +serialize_property('Wildcard-Subscription-Available', Val) -> + <<16#28, Val>>; +%% 41: Byte; +serialize_property('Subscription-Identifier-Available', Val) -> + <<16#29, Val>>; +%% 42: Byte; +serialize_property('Shared-Subscription-Available', Val) -> + <<16#2A, Val>>. + serialize_topics([{_Topic, _Qos}|_] = Topics) -> << <<(serialize_utf(Topic))/binary, ?RESERVED:6, Qos:2>> || {Topic, Qos} <- Topics >>; serialize_topics([H|_] = Topics) when is_binary(H) -> << <<(serialize_utf(Topic))/binary>> || Topic <- Topics >>. +serialize_utf_pair({Name, Value}) -> + << <<(serialize_utf(S))/binary, (serialize_utf(S))/binary>> || S <- [Name, Value] >>. + serialize_utf(String) -> StringBin = unicode:characters_to_binary(String), Len = byte_size(StringBin), true = (Len =< 16#ffff), <>. -serialize_len(N) when N =< ?LOWBITS -> +serialize_len(I) -> + serialize_variable_byte_integer(I). %%TODO: refactor later. + +serialize_variable_byte_integer(N) when N =< ?LOWBITS -> <<0:1, N:7>>; -serialize_len(N) -> - <<1:1, (N rem ?HIGHBIT):7, (serialize_len(N div ?HIGHBIT))/binary>>. +serialize_variable_byte_integer(N) -> + <<1:1, (N rem ?HIGHBIT):7, (serialize_variable_byte_integer(N div ?HIGHBIT))/binary>>. opt(undefined) -> ?RESERVED; opt(false) -> 0; opt(true) -> 1; opt(X) when is_integer(X) -> X; opt(B) when is_binary(B) -> 1. + diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index fe89955ee..7146beb96 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -18,12 +18,13 @@ -behaviour(gen_server). --author("Feng Lee "). - -include("emqx.hrl"). -export([start_link/0, stop/0]). +%% Get all Stats +-export([all/0]). + %% Client and Session Stats -export([set_client_stats/2, get_client_stats/1, del_client_stats/1, set_session_stats/2, get_session_stats/1, del_session_stats/1]). @@ -115,6 +116,8 @@ get_session_stats(ClientId) -> del_session_stats(ClientId) -> ets:delete(?SESSION_STATS_TAB, ClientId). +all() -> ets:tab2list(?STATS_TAB). + %% @doc Generate stats fun -spec(statsfun(Stat :: atom()) -> fun()). statsfun(Stat) -> diff --git a/src/emqx_sysmon.erl b/src/emqx_sysmon.erl index c76f78dcd..7f4a2490d 100644 --- a/src/emqx_sysmon.erl +++ b/src/emqx_sysmon.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,12 +16,8 @@ -module(emqx_sysmon). --author("Feng Lee "). - -behavior(gen_server). --include("emqx_internal.hrl"). - -export([start_link/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -29,22 +25,21 @@ -record(state, {tickref, events = [], tracelog}). --define(LOG_FMT, [{formatter_config, [time, " ", message, "\n"]}]). +%%-define(LOG_FMT, [{formatter_config, [time, " ", message, "\n"]}]). -define(LOG(Msg, ProcInfo), - lager:warning([{sysmon, true}], "~s~n~p", [WarnMsg, ProcInfo])). + lager:warning([{sysmon, true}], "[SYSMON] ~s~n~p", [WarnMsg, ProcInfo])). -define(LOG(Msg, ProcInfo, PortInfo), - lager:warning([{sysmon, true}], "~s~n~p~n~p", [WarnMsg, ProcInfo, PortInfo])). + lager:warning([{sysmon, true}], "[SYSMON] ~s~n~p~n~p", [WarnMsg, ProcInfo, PortInfo])). %% @doc Start system monitor --spec(start_link(Opts :: list(tuple())) -> - {ok, pid()} | ignore | {error, term()}). +-spec(start_link(Opts :: list(tuple())) -> {ok, pid()} | ignore | {error, term()}). start_link(Opts) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [Opts], []). %%-------------------------------------------------------------------- -%% gen_server callbacks +%% gen_server Callbacks %%-------------------------------------------------------------------- init([Opts]) -> @@ -80,10 +75,12 @@ parse_opt([_Opt|Opts], Acc) -> parse_opt(Opts, Acc). handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). + lager:error("[SYSMON] Unexpected Call: ~p", [Req]), + {reply, ignore, State}. handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). + lager:error("[SYSMON] Unexpected Cast: ~p", [Msg]), + {noreply, State}. handle_info({monitor, Pid, long_gc, Info}, State) -> suppress({long_gc, Pid}, fun() -> @@ -131,7 +128,8 @@ handle_info(reset, State) -> {noreply, State#state{events = []}, hibernate}; handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). + lager:error("[SYSMON] Unexpected Info: ~p", [Info]), + {noreply, State}. terminate(_Reason, #state{tickref = TRef, tracelog = TraceLog}) -> timer:cancel(TRef), diff --git a/src/emqx_trace.erl b/src/emqx_trace.erl index 81da9989c..2cc4cd29e 100644 --- a/src/emqx_trace.erl +++ b/src/emqx_trace.erl @@ -20,8 +20,6 @@ -author("Feng Lee "). --include("emqx_internal.hrl"). - %% API Function Exports -export([start_link/0]). @@ -100,13 +98,16 @@ handle_call(all_traces, _From, State = #state{traces = Traces}) -> <- maps:to_list(Traces)], State}; handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). + lager:error("[TRACE] Unexpected Call: ~p", [Req]), + {reply, ignore, State}. handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). + lager:error("[TRACE] Unexpected Cast: ~p", [Msg]), + {noreply, State}. handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). + lager:error("[TRACE] Unexpected Info: ~p", [Info]), + {noreply, State}. terminate(_Reason, _State) -> ok. From d7eae533ac53815dd5c34ad109d2cb457f7923a1 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 28 Feb 2018 21:22:39 +0800 Subject: [PATCH 023/520] Merge the emqx-mqtt5 library --- include/emqx.hrl | 30 ++++- include/emqx_cli.hrl | 24 ---- include/emqx_internal.hrl | 7 - include/{emqx_common.hrl => emqx_misc.hrl} | 0 include/emqx_mqtt.hrl | 109 +++++++++++---- include/emqx_rest.hrl | 17 --- include/emqx_trie.hrl | 35 ----- src/emqx.erl | 2 +- src/emqx_access_control.erl | 2 +- src/emqx_bridge.erl | 5 +- src/emqx_cm.erl | 19 ++- src/emqx_cm_sup.erl | 12 +- src/{emqx_client.erl => emqx_conn.erl} | 124 +++++++++--------- src/emqx_misc.erl | 2 +- src/emqx_modules.erl | 2 +- src/emqx_pooler.erl | 2 - src/emqx_protocol.erl | 8 +- src/emqx_pubsub.erl | 9 +- src/emqx_server.erl | 7 +- src/emqx_session.erl | 14 +- src/emqx_session_sup.erl | 3 - src/emqx_sm.erl | 20 +-- src/emqx_sm_helper.erl | 12 +- src/emqx_sm_sup.erl | 7 +- src/emqx_trie.erl | 10 +- src/emqx_ws.erl | 2 +- src/{emqx_ws_client.erl => emqx_ws_conn.erl} | 10 +- ...ws_client_sup.erl => emqx_ws_conn_sup.erl} | 20 ++- test/emqx_trie_SUITE.erl | 6 +- 29 files changed, 233 insertions(+), 287 deletions(-) delete mode 100644 include/emqx_cli.hrl rename include/{emqx_common.hrl => emqx_misc.hrl} (100%) delete mode 100644 include/emqx_rest.hrl delete mode 100644 include/emqx_trie.hrl rename src/{emqx_client.erl => emqx_conn.erl} (71%) rename src/{emqx_ws_client.erl => emqx_ws_conn.erl} (98%) rename src/{emqx_ws_client_sup.erl => emqx_ws_conn_sup.erl} (71%) diff --git a/include/emqx.hrl b/include/emqx.hrl index ccfaad6d0..72fb71971 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -48,13 +48,6 @@ %% MQTT Topic %%-------------------------------------------------------------------- --record(mqtt_topic, - { topic :: binary(), - flags = [] :: [retained | static] - }). - --type(mqtt_topic() :: #mqtt_topic{}). - %%-------------------------------------------------------------------- %% MQTT Subscription %%-------------------------------------------------------------------- @@ -163,6 +156,29 @@ -type(route() :: #route{}). +%%-------------------------------------------------------------------- +%% Trie +%%-------------------------------------------------------------------- + +-type(trie_node_id() :: binary() | atom()). + +-record(trie_node, + { node_id :: trie_node_id(), + edge_count = 0 :: non_neg_integer(), + topic :: binary() | undefined, + flags :: list(atom()) + }). + +-record(trie_edge, + { node_id :: trie_node_id(), + word :: binary() | atom() + }). + +-record(trie, + { edge :: #trie_edge{}, + node_id :: trie_node_id() + }). + %%-------------------------------------------------------------------- %% Alarm %%-------------------------------------------------------------------- diff --git a/include/emqx_cli.hrl b/include/emqx_cli.hrl deleted file mode 100644 index b99038481..000000000 --- a/include/emqx_cli.hrl +++ /dev/null @@ -1,24 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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. -%%-------------------------------------------------------------------- - --define(PRINT_MSG(Msg), io:format(Msg)). - --define(PRINT(Format, Args), io:format(Format, Args)). - --define(PRINT_CMD(Cmd, Descr), io:format("~-48s# ~s~n", [Cmd, Descr])). - --define(USAGE(CmdList), [?PRINT_CMD(Cmd, Descr) || {Cmd, Descr} <- CmdList]). - diff --git a/include/emqx_internal.hrl b/include/emqx_internal.hrl index 44138cefd..2241c2fc8 100644 --- a/include/emqx_internal.hrl +++ b/include/emqx_internal.hrl @@ -26,13 +26,6 @@ -define(PROC_NAME(M, I), (list_to_atom(lists:concat([M, "_", I])))). --define(record_to_proplist(Def, Rec), - lists:zip(record_info(fields, Def), tl(tuple_to_list(Rec)))). - --define(record_to_proplist(Def, Rec, Fields), - [{K, V} || {K, V} <- ?record_to_proplist(Def, Rec), - lists:member(K, Fields)]). - -define(UNEXPECTED_REQ(Req, State), (begin lager:error("[~s] Unexpected Request: ~p", [?MODULE, Req]), diff --git a/include/emqx_common.hrl b/include/emqx_misc.hrl similarity index 100% rename from include/emqx_common.hrl rename to include/emqx_misc.hrl diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 8a0ad4478..a4946cc16 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -115,7 +115,7 @@ 'DISCONNECT', 'AUTH']). --type(mqtt_packet_type() :: ?RESERVED..?DISCONNECT). +-type(mqtt_packet_type() :: ?RESERVED..?AUTH). %%-------------------------------------------------------------------- %% MQTT Connect Return Codes @@ -158,10 +158,23 @@ %% MQTT Packets %%-------------------------------------------------------------------- +-type(mqtt_topic() :: binary()). + -type(mqtt_client_id() :: binary()). + -type(mqtt_username() :: binary() | undefined). + -type(mqtt_packet_id() :: 1..16#ffff | undefined). +-type(mqtt_reason_code() :: 1..16#ff | undefined). + +-type(mqtt_properties() :: undefined | map()). + +-type(mqtt_subopt() :: list({qos, mqtt_qos()} + | {retain_handling, boolean()} + | {keep_retain, boolean()} + | {no_local, boolean()})). + -record(mqtt_packet_connect, { client_id = <<>> :: mqtt_client_id(), proto_ver = ?MQTT_PROTO_V4 :: mqtt_vsn(), @@ -170,44 +183,68 @@ will_qos = ?QOS_1 :: mqtt_qos(), will_flag = false :: boolean(), clean_sess = false :: boolean(), + clean_start = true :: boolean(), keep_alive = 60 :: non_neg_integer(), + will_props = undefined :: undefined | map(), will_topic = undefined :: undefined | binary(), will_msg = undefined :: undefined | binary(), username = undefined :: undefined | binary(), password = undefined :: undefined | binary(), - is_bridge = false :: boolean() + is_bridge = false :: boolean(), + properties = undefined :: mqtt_properties() %% MQTT Version 5.0 }). -record(mqtt_packet_connack, { ack_flags = ?RESERVED :: 0 | 1, - return_code :: mqtt_connack() + reason_code :: mqtt_connack(), + properties :: map() }). -record(mqtt_packet_publish, { topic_name :: binary(), - packet_id :: mqtt_packet_id() + packet_id :: mqtt_packet_id(), + properties :: mqtt_properties() }). -record(mqtt_packet_puback, - { packet_id :: mqtt_packet_id() }). + { packet_id :: mqtt_packet_id(), + reason_code :: mqtt_reason_code(), + properties :: mqtt_properties() + }). -record(mqtt_packet_subscribe, - { packet_id :: mqtt_packet_id(), - topic_table :: list({binary(), mqtt_qos()}) + { packet_id :: mqtt_packet_id(), + properties :: mqtt_properties(), + topic_filters :: list({binary(), mqtt_subopt()}) }). -record(mqtt_packet_unsubscribe, - { packet_id :: mqtt_packet_id(), - topics :: list(binary()) + { packet_id :: mqtt_packet_id(), + properties :: mqtt_properties(), + topics :: list(binary()) }). -record(mqtt_packet_suback, - { packet_id :: mqtt_packet_id(), - qos_table :: list(mqtt_qos() | 128) + { packet_id :: mqtt_packet_id(), + properties :: mqtt_properties(), + reason_codes :: list(mqtt_reason_code()) }). -record(mqtt_packet_unsuback, - { packet_id :: mqtt_packet_id() }). + { packet_id :: mqtt_packet_id(), + properties :: mqtt_properties(), + reason_codes :: list(mqtt_reason_code()) + }). + +-record(mqtt_packet_disconnect, + { reason_code :: mqtt_reason_code(), + properties :: mqtt_properties() + }). + +-record(mqtt_packet_auth, + { reason_code :: mqtt_reason_code(), + properties :: mqtt_properties() + }). %%-------------------------------------------------------------------- %% MQTT Control Packet @@ -215,11 +252,18 @@ -record(mqtt_packet, { header :: #mqtt_packet_header{}, - variable :: #mqtt_packet_connect{} | #mqtt_packet_connack{} - | #mqtt_packet_publish{} | #mqtt_packet_puback{} - | #mqtt_packet_subscribe{} | #mqtt_packet_suback{} - | #mqtt_packet_unsubscribe{} | #mqtt_packet_unsuback{} - | mqtt_packet_id() | undefined, + variable :: #mqtt_packet_connect{} + | #mqtt_packet_connack{} + | #mqtt_packet_publish{} + | #mqtt_packet_puback{} + | #mqtt_packet_subscribe{} + | #mqtt_packet_suback{} + | #mqtt_packet_unsubscribe{} + | #mqtt_packet_unsuback{} + | #mqtt_packet_disconnect{} + | #mqtt_packet_auth{} + | mqtt_packet_id() + | undefined, payload :: binary() | undefined }). @@ -232,14 +276,20 @@ -define(CONNECT_PACKET(Var), #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, variable = Var}). --define(CONNACK_PACKET(ReturnCode), +-define(CONNACK_PACKET(ReasonCode), #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, - variable = #mqtt_packet_connack{return_code = ReturnCode}}). + variable = #mqtt_packet_connack{reason_code = ReturnCode}}). --define(CONNACK_PACKET(ReturnCode, SessPresent), +-define(CONNACK_PACKET(ReasonCode, SessPresent), #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, variable = #mqtt_packet_connack{ack_flags = SessPresent, - return_code = ReturnCode}}). + reason_code = ReturnCode}}). + +-define(CONNACK_PACKET(ReasonCode, SessPresent, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, + variable = #mqtt_packet_connack{ack_flags = SessPresent, + reason_code = ReasonCode, + properties = Properties}}). -define(PUBLISH_PACKET(Qos, PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, @@ -265,11 +315,18 @@ #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, qos = ?QOS_1}, variable = #mqtt_packet_subscribe{packet_id = PacketId, topic_table = TopicTable}}). --define(SUBACK_PACKET(PacketId, QosTable), + +-define(SUBACK_PACKET(PacketId, ReasonCodes), #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, - variable = #mqtt_packet_suback{packet_id = PacketId, - qos_table = QosTable}}). --define(UNSUBSCRIBE_PACKET(PacketId, Topics), + variable = #mqtt_packet_suback{packet_id = PacketId, + reason_codes = ReasonCodes}}). + +-define(SUBACK_PACKET(PacketId, Properties, ReasonCodes), + #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, + variable = #mqtt_packet_suback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes}}). +-define(UNSUBSCRIBE_PACKET(PacketId, Topics), #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, qos = ?QOS_1}, variable = #mqtt_packet_unsubscribe{packet_id = PacketId, topics = Topics}}). diff --git a/include/emqx_rest.hrl b/include/emqx_rest.hrl deleted file mode 100644 index c1c6b219d..000000000 --- a/include/emqx_rest.hrl +++ /dev/null @@ -1,17 +0,0 @@ - --define(SUCCESS, 0). %% Success --define(ERROR1, 101). %% badrpc --define(ERROR2, 102). %% Unknown error --define(ERROR3, 103). %% Username or password error --define(ERROR4, 104). %% Empty username or password --define(ERROR5, 105). %% User does not exist --define(ERROR6, 106). %% Admin can not be deleted --define(ERROR7, 107). %% Missing request parameter --define(ERROR8, 108). %% Request parameter type error --define(ERROR9, 109). %% Request parameter is not a json --define(ERROR10, 110). %% Plugin has been loaded --define(ERROR11, 111). %% Plugin has been loaded --define(ERROR12, 112). %% Client not online --define(ERROR13, 113). %% User already exist --define(ERROR14, 114). %% OldPassword error - diff --git a/include/emqx_trie.hrl b/include/emqx_trie.hrl deleted file mode 100644 index ce57b4438..000000000 --- a/include/emqx_trie.hrl +++ /dev/null @@ -1,35 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. -%% -%% 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. -%%-------------------------------------------------------------------- - --type(trie_node_id() :: binary() | atom()). - --record(trie_node, - { node_id :: trie_node_id(), - edge_count = 0 :: non_neg_integer(), - topic :: binary() | undefined, - flags :: list(atom()) - }). - --record(trie_edge, - { node_id :: trie_node_id(), - word :: binary() | atom() - }). - --record(trie, - { edge :: #trie_edge{}, - node_id :: trie_node_id() - }). - diff --git a/src/emqx.erl b/src/emqx.erl index 07e133cf7..6142cfa7d 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 2c6460acc..409b98065 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index fc6f33535..e9858793f 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -66,7 +66,7 @@ start_link(Pool, Id, Node, Topic, Options) -> %%-------------------------------------------------------------------- init([Pool, Id, Node, Topic, Options]) -> - ?GPROC_POOL(join, Pool, Id), + gproc_pool:connect_worker(Pool, {Pool, Id}), process_flag(trap_exit, true), case net_kernel:connect_node(Node) of true -> @@ -151,8 +151,7 @@ handle_info(Info, State) -> ?UNEXPECTED_INFO(Info, State). terminate(_Reason, #state{pool = Pool, id = Id}) -> - ?GPROC_POOL(leave, Pool, Id), - ok. + gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> {ok, State}. diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index f0b502170..4956b199d 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -14,18 +14,12 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc MQTT Client Manager - -module(emqx_cm). -behaviour(gen_server). --author("Feng Lee "). - -include("emqx.hrl"). --include("emqx_internal.hrl"). - %% API Exports -export([start_link/3]). @@ -78,7 +72,7 @@ pick(ClientId) -> gproc_pool:pick_worker(?POOL, ClientId). %%-------------------------------------------------------------------- init([Pool, Id, StatsFun]) -> - ?GPROC_POOL(join, Pool, Id), + gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #state{pool = Pool, id = Id, statsfun = StatsFun, monitors = dict:new()}}. handle_call({reg, Client = #mqtt_client{client_id = ClientId, @@ -92,7 +86,8 @@ handle_call({reg, Client = #mqtt_client{client_id = ClientId, end; handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). + lager:error("[MQTT-CM] Unexpected Call: ~p", [Req]), + {reply, ignore, State}. handle_cast({unreg, ClientId, Pid}, State) -> case lookup_proc(ClientId) of @@ -104,7 +99,8 @@ handle_cast({unreg, ClientId, Pid}, State) -> end; handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). + lager:error("[MQTT-CM] Unexpected Cast: ~p", [Msg]), + {noreply, State}. handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> case dict:find(MRef, State#state.monitors) of @@ -123,10 +119,11 @@ handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> end; handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). + lager:error("[CM] Unexpected Info: ~p", [Info]), + {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> - ?GPROC_POOL(leave, Pool, Id), ok. + gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> {ok, State}. diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index f28468d39..229e474bb 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -14,24 +14,16 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc Client Manager Supervisor. - -module(emqx_cm_sup). -behaviour(supervisor). --author("Feng Lee "). - --include("emqx.hrl"). - %% API -export([start_link/0]). %% Supervisor callbacks -export([init/1]). --define(CM, emqx_cm). - -define(TAB, mqtt_client). start_link() -> @@ -42,8 +34,8 @@ init([]) -> create_client_tab(), %% CM Pool Sup - MFA = {?CM, start_link, [emqx_stats:statsfun('clients/count', 'clients/max')]}, - PoolSup = emqx_pool_sup:spec([?CM, hash, erlang:system_info(schedulers), MFA]), + MFA = {emqx_cm, start_link, [emqx_stats:statsfun('clients/count', 'clients/max')]}, + PoolSup = emqx_pool_sup:spec([emqx_cm, hash, erlang:system_info(schedulers), MFA]), {ok, {{one_for_all, 10, 3600}, [PoolSup]}}. diff --git a/src/emqx_client.erl b/src/emqx_conn.erl similarity index 71% rename from src/emqx_client.erl rename to src/emqx_conn.erl index 0a3d7d8ce..a731bacc9 100644 --- a/src/emqx_client.erl +++ b/src/emqx_conn.erl @@ -14,9 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc MQTT/TCP Connection. - --module(emqx_client). +-module(emqx_conn). -behaviour(gen_server). @@ -24,7 +22,7 @@ -include("emqx_mqtt.hrl"). --include("emqx_internal.hrl"). +-include("emqx_misc.hrl"). -import(proplists, [get_value/2, get_value/3]). @@ -49,11 +47,10 @@ %% TODO: How to emit stats? -export([handle_pre_hibernate/1]). -%% Client State %% Unused fields: connname, peerhost, peerport --record(client_state, {connection, peername, conn_state, await_recv, - rate_limit, packet_size, parser, proto_state, - keepalive, enable_stats, idle_timeout, force_gc_count}). +-record(state, {connection, peername, conn_state, await_recv, + rate_limit, packet_size, parser, proto_state, + keepalive, enable_stats, idle_timeout, force_gc_count}). -define(INFO_KEYS, [peername, conn_state, await_recv]). @@ -61,7 +58,7 @@ -define(LOG(Level, Format, Args, State), lager:Level("Client(~s): " ++ Format, - [esockd_net:format(State#client_state.peername) | Args])). + [esockd_net:format(State#state.peername) | Args])). start_link(Conn, Env) -> {ok, proc_lib:spawn_link(?MODULE, init, [[Conn, Env]])}. @@ -117,17 +114,17 @@ do_init(Conn, Env, Peername) -> EnableStats = get_value(client_enable_stats, Env, false), IdleTimout = get_value(client_idle_timeout, Env, 30000), ForceGcCount = emqx_gc:conn_max_gc_count(), - State = run_socket(#client_state{connection = Conn, - peername = Peername, - await_recv = false, - conn_state = running, - rate_limit = RateLimit, - packet_size = PacketSize, - parser = Parser, - proto_state = ProtoState, - enable_stats = EnableStats, - idle_timeout = IdleTimout, - force_gc_count = ForceGcCount}), + State = run_socket(#state{connection = Conn, + peername = Peername, + await_recv = false, + conn_state = running, + rate_limit = RateLimit, + packet_size = PacketSize, + parser = Parser, + proto_state = ProtoState, + enable_stats = EnableStats, + idle_timeout = IdleTimout, + force_gc_count = ForceGcCount}), gen_server:enter_loop(?MODULE, [{hibernate_after, 10000}], State, self(), IdleTimout). @@ -135,7 +132,7 @@ send_fun(Conn, Peername) -> Self = self(), fun(Packet) -> Data = emqx_serializer:serialize(Packet), - ?LOG(debug, "SEND ~p", [Data], #client_state{peername = Peername}), + ?LOG(debug, "SEND ~p", [Data], #state{peername = Peername}), emqx_metrics:inc('bytes/sent', iolist_size(Data)), try Conn:async_send(Data) of ok -> ok; @@ -147,15 +144,15 @@ send_fun(Conn, Peername) -> end. handle_pre_hibernate(State) -> - {hibernate, emqx_gc:reset_conn_gc_count(#client_state.force_gc_count, emit_stats(State))}. + {hibernate, emqx_gc:reset_conn_gc_count(#state.force_gc_count, emit_stats(State))}. -handle_call(info, From, State = #client_state{proto_state = ProtoState}) -> +handle_call(info, From, State = #state{proto_state = ProtoState}) -> ProtoInfo = emqx_protocol:info(ProtoState), - ClientInfo = ?record_to_proplist(client_state, State, ?INFO_KEYS), + ClientInfo = ?record_to_proplist(state, State, ?INFO_KEYS), {reply, Stats, _, _} = handle_call(stats, From, State), reply(lists:append([ClientInfo, ProtoInfo, Stats]), State); -handle_call(stats, _From, State = #client_state{proto_state = ProtoState}) -> +handle_call(stats, _From, State = #state{proto_state = ProtoState}) -> reply(lists:append([emqx_misc:proc_stats(), emqx_protocol:stats(ProtoState), sock_stats(State)]), State); @@ -164,12 +161,12 @@ handle_call(kick, _From, State) -> {stop, {shutdown, kick}, ok, State}; handle_call({set_rate_limit, Rl}, _From, State) -> - reply(ok, State#client_state{rate_limit = Rl}); + reply(ok, State#state{rate_limit = Rl}); -handle_call(get_rate_limit, _From, State = #client_state{rate_limit = Rl}) -> +handle_call(get_rate_limit, _From, State = #state{rate_limit = Rl}) -> reply(Rl, State); -handle_call(session, _From, State = #client_state{proto_state = ProtoState}) -> +handle_call(session, _From, State = #state{proto_state = ProtoState}) -> reply(emqx_protocol:session(ProtoState), State); handle_call({clean_acl_cache, Topic}, _From, State) -> @@ -177,10 +174,12 @@ handle_call({clean_acl_cache, Topic}, _From, State) -> reply(ok, State); handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). + ?LOG(error, "Unexpected Call: ~p", [Req], State), + {reply, ignore, State}. handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). + ?LOG(error, "Unexpected Cast: ~p", [Msg], State), + {noreply, State}. handle_info({subscribe, TopicTable}, State) -> with_proto( @@ -204,7 +203,7 @@ handle_info({suback, PacketId, GrantedQos}, State) -> %% Fastlane handle_info({dispatch, _Topic, Message}, State) -> - handle_info({deliver, Message#mqtt_message{qos = ?QOS_0}}, State); + handle_info({deliver, Message#message{qos = ?QOS_0}}, State); handle_info({deliver, Message}, State) -> with_proto( @@ -233,13 +232,13 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> shutdown(conflict, State); handle_info(activate_sock, State) -> - {noreply, run_socket(State#client_state{conn_state = running})}; + {noreply, run_socket(State#state{conn_state = running})}; handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) -> Size = iolist_size(Data), ?LOG(debug, "RECV ~p", [Data], State), emqx_metrics:inc('bytes/received', Size), - received(Data, rate_limit(Size, State#client_state{await_recv = false})); + received(Data, rate_limit(Size, State#state{await_recv = false})); handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) -> shutdown(Reason, State); @@ -250,7 +249,7 @@ handle_info({inet_reply, _Sock, ok}, State) -> handle_info({inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); -handle_info({keepalive, start, Interval}, State = #client_state{connection = Conn}) -> +handle_info({keepalive, start, Interval}, State = #state{connection = Conn}) -> ?LOG(debug, "Keepalive at the interval of ~p", [Interval], State), StatFun = fun() -> case Conn:getstat([recv_oct]) of @@ -279,11 +278,12 @@ handle_info({keepalive, check}, State = #client_state{keepalive = KeepAlive}) -> end; handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). + ?LOG(error, "Unexpected Info: ~p", [Info], State), + {noreply, State}. -terminate(Reason, State = #client_state{connection = Conn, - keepalive = KeepAlive, - proto_state = ProtoState}) -> +terminate(Reason, State = #state{connection = Conn, + keepalive = KeepAlive, + proto_state = ProtoState}) -> ?LOG(debug, "Terminated for ~p", [Reason], State), Conn:fast_close(), @@ -308,26 +308,26 @@ code_change(_OldVsn, State, _Extra) -> received(<<>>, State) -> {noreply, gc(State)}; -received(Bytes, State = #client_state{parser = Parser, - packet_size = PacketSize, - proto_state = ProtoState, - idle_timeout = IdleTimeout}) -> +received(Bytes, State = #state{parser = Parser, + packet_size = PacketSize, + proto_state = ProtoState, + idle_timeout = IdleTimeout}) -> case catch emqx_parser:parse(Bytes, Parser) of {more, NewParser} -> - {noreply, run_socket(State#client_state{parser = NewParser}), IdleTimeout}; + {noreply, run_socket(State#state{parser = NewParser}), IdleTimeout}; {ok, Packet, Rest} -> emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> - received(Rest, State#client_state{parser = emqx_parser:initial_state(PacketSize), - proto_state = ProtoState1}); + received(Rest, State#state{parser = emqx_parser:initial_state(PacketSize), + proto_state = ProtoState1}); {error, Error} -> ?LOG(error, "Protocol error - ~p", [Error], State), shutdown(Error, State); {error, Error, ProtoState1} -> - shutdown(Error, State#client_state{proto_state = ProtoState1}); + shutdown(Error, State#state{proto_state = ProtoState1}); {stop, Reason, ProtoState1} -> - stop(Reason, State#client_state{proto_state = ProtoState1}) + stop(Reason, State#state{proto_state = ProtoState1}) end; {error, Error} -> ?LOG(error, "Framing error - ~p", [Error], State), @@ -338,34 +338,34 @@ received(Bytes, State = #client_state{parser = Parser, shutdown(parser_error, State) end. -rate_limit(_Size, State = #client_state{rate_limit = undefined}) -> +rate_limit(_Size, State = #state{rate_limit = undefined}) -> run_socket(State); -rate_limit(Size, State = #client_state{rate_limit = Rl}) -> +rate_limit(Size, State = #state{rate_limit = Rl}) -> case Rl:check(Size) of {0, Rl1} -> - run_socket(State#client_state{conn_state = running, rate_limit = Rl1}); + run_socket(State#state{conn_state = running, rate_limit = Rl1}); {Pause, Rl1} -> ?LOG(warning, "Rate limiter pause for ~p", [Pause], State), erlang:send_after(Pause, self(), activate_sock), - State#client_state{conn_state = blocked, rate_limit = Rl1} + State#state{conn_state = blocked, rate_limit = Rl1} end. -run_socket(State = #client_state{conn_state = blocked}) -> +run_socket(State = #state{conn_state = blocked}) -> State; -run_socket(State = #client_state{await_recv = true}) -> +run_socket(State = #state{await_recv = true}) -> State; -run_socket(State = #client_state{connection = Conn}) -> +run_socket(State = #state{connection = Conn}) -> Conn:async_recv(0, infinity), - State#client_state{await_recv = true}. + State#state{await_recv = true}. -with_proto(Fun, State = #client_state{proto_state = ProtoState}) -> +with_proto(Fun, State = #state{proto_state = ProtoState}) -> {ok, ProtoState1} = Fun(ProtoState), - {noreply, State#client_state{proto_state = ProtoState1}}. + {noreply, State#state{proto_state = ProtoState1}}. -emit_stats(State = #client_state{proto_state = ProtoState}) -> +emit_stats(State = #state{proto_state = ProtoState}) -> emit_stats(emqx_protocol:clientid(ProtoState), State). -emit_stats(_ClientId, State = #client_state{enable_stats = false}) -> +emit_stats(_ClientId, State = #state{enable_stats = false}) -> State; emit_stats(undefined, State) -> State; @@ -374,7 +374,7 @@ emit_stats(ClientId, State) -> emqx_stats:set_client_stats(ClientId, Stats), State. -sock_stats(#client_state{connection = Conn}) -> +sock_stats(#state{connection = Conn}) -> case Conn:getstat(?SOCK_STATS) of {ok, Ss} -> Ss; {error, _} -> [] end. reply(Reply, State) -> @@ -386,7 +386,7 @@ shutdown(Reason, State) -> stop(Reason, State) -> {stop, Reason, State}. -gc(State = #client_state{connection = Conn}) -> +gc(State = #state{connection = Conn}) -> Cb = fun() -> Conn:gc(), emit_stats(State) end, - emqx_gc:maybe_force_gc(#client_state.force_gc_count, State, Cb). + emqx_gc:maybe_force_gc(#state.force_gc_count, State, Cb). diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index 54a5bbd01..d10eb6415 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_modules.erl b/src/emqx_modules.erl index 3f51dbcdb..b3b8293f6 100644 --- a/src/emqx_modules.erl +++ b/src/emqx_modules.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_pooler.erl b/src/emqx_pooler.erl index 3cae2264d..71af6b279 100644 --- a/src/emqx_pooler.erl +++ b/src/emqx_pooler.erl @@ -16,8 +16,6 @@ -module(emqx_pooler). --author("Feng Lee "). - -behaviour(gen_server). %% Start the pool supervisor diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 7e5cd2402..c754774a3 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -22,8 +22,6 @@ -include("emqx_mqtt.hrl"). --include("emqx_internal.hrl"). - -import(proplists, [get_value/2, get_value/3]). %% API @@ -64,7 +62,7 @@ %% @doc Init protocol init(Peername, SendFun, Opts) -> - Backoff = get_value(keepalive_backoff, Opts, 1.25), + Backoff = get_value(keepalive_backoff, Opts, 0.75), EnableStats = get_value(client_enable_stats, Opts, false), MaxLen = get_value(max_clientid_len, Opts, ?MAX_CLIENTID_LEN), WsInitialHeaders = get_value(ws_initial_headers, Opts), @@ -569,10 +567,10 @@ sp(false) -> 0. %% The retained flag should be propagated for bridge. %%-------------------------------------------------------------------- -clean_retain(false, Msg = #mqtt_message{retain = true, headers = Headers}) -> +clean_retain(false, Msg = #message{retain = true, headers = Headers}) -> case lists:member(retained, Headers) of true -> Msg; - false -> Msg#mqtt_message{retain = false} + false -> Msg#message{retain = false} end; clean_retain(_IsBridge, Msg) -> Msg. diff --git a/src/emqx_pubsub.erl b/src/emqx_pubsub.erl index b4ac146a8..9554f30ad 100644 --- a/src/emqx_pubsub.erl +++ b/src/emqx_pubsub.erl @@ -20,8 +20,6 @@ -include("emqx.hrl"). --include("emqx_internal.hrl"). - -export([start_link/3]). %% PubSub API. @@ -46,8 +44,7 @@ -spec(start_link(atom(), pos_integer(), list()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id, Env) -> - gen_server:start_link({local, ?PROC_NAME(?MODULE, Id)}, - ?MODULE, [Pool, Id, Env], [{hibernate_after, 10000}]). + gen_server:start_link(?MODULE, [Pool, Id, Env], [{hibernate_after, 10000}]). %%-------------------------------------------------------------------- %% PubSub API @@ -164,7 +161,7 @@ pick(Topic) -> %%-------------------------------------------------------------------- init([Pool, Id, Env]) -> - ?GPROC_POOL(join, Pool, Id), + gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #state{pool = Pool, id = Id, env = Env}, hibernate}. handle_call({subscribe, Topic, Subscriber, Options}, _From, State) -> @@ -193,7 +190,7 @@ handle_info(Info, State) -> ?UNEXPECTED_INFO(Info, State). terminate(_Reason, #state{pool = Pool, id = Id}) -> - ?GPROC_POOL(leave, Pool, Id). + gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> {ok, State}. diff --git a/src/emqx_server.erl b/src/emqx_server.erl index 47aafad49..6aa9acce8 100644 --- a/src/emqx_server.erl +++ b/src/emqx_server.erl @@ -49,8 +49,7 @@ %% @doc Start the server -spec(start_link(atom(), pos_integer(), list()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id, Env) -> - gen_server:start_link({local, ?PROC_NAME(?MODULE, Id)}, - ?MODULE, [Pool, Id, Env], [{hibernate_after, 10000}]). + gen_server:start_link(?MODULE, [Pool, Id, Env], [{hibernate_after, 10000}]). %%-------------------------------------------------------------------- %% PubSub API @@ -186,7 +185,7 @@ dump() -> %%-------------------------------------------------------------------- init([Pool, Id, Env]) -> - ?GPROC_POOL(join, Pool, Id), + gproc_pool:connect_worker(Pool, {Pool, Id}), State = #state{pool = Pool, id = Id, env = Env, subids = #{}, submon = emqx_pmon:new()}, {ok, State, hibernate}. @@ -245,7 +244,7 @@ handle_info(Info, State) -> ?UNEXPECTED_INFO(Info, State). terminate(_Reason, #state{pool = Pool, id = Id}) -> - ?GPROC_POOL(leave, Pool, Id). + gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> {ok, State}. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 44cd44e00..4ce6f7cfd 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -53,8 +53,6 @@ -include("emqx_mqtt.hrl"). --include("emqx_internal.hrl"). - -import(emqx_misc, [start_timer/2]). -import(proplists, [get_value/2, get_value/3]). @@ -193,16 +191,16 @@ subscribe(Session, PacketId, TopicTable) -> %%TODO: the ack function??... gen_server:cast(Session, {subscribe, From, TopicTable, AckFun}). %% @doc Publish Message --spec(publish(pid(), mqtt_message()) -> ok | {error, term()}). -publish(_Session, Msg = #mqtt_message{qos = ?QOS_0}) -> +-spec(publish(pid(), message()) -> ok | {error, term()}). +publish(_Session, Msg = #message{qos = ?QOS_0}) -> %% Publish QoS0 Directly emqx_server:publish(Msg), ok; -publish(_Session, Msg = #mqtt_message{qos = ?QOS_1}) -> +publish(_Session, Msg = #message{qos = ?QOS_1}) -> %% Publish QoS1 message directly for client will PubAck automatically emqx_server:publish(Msg), ok; -publish(Session, Msg = #mqtt_message{qos = ?QOS_2}) -> +publish(Session, Msg = #message{qos = ?QOS_2}) -> %% Publish QoS2 to Session gen_server:call(Session, {publish, Msg}, ?TIMEOUT). @@ -517,7 +515,7 @@ handle_cast(Msg, State) -> ?UNEXPECTED_MSG(Msg, State). %% Ignore Messages delivered by self -handle_info({dispatch, _Topic, #mqtt_message{from = {ClientId, _}}}, +handle_info({dispatch, _Topic, #message{from = {ClientId, _}}}, State = #state{client_id = ClientId, ignore_loop_deliver = true}) -> {noreply, State}; @@ -637,7 +635,7 @@ expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) -> expire_awaiting_rel([], _Now, State) -> State#state{await_rel_timer = undefined}; -expire_awaiting_rel([{PacketId, Msg = #mqtt_message{timestamp = TS}} | Msgs], +expire_awaiting_rel([{PacketId, Msg = #message{timestamp = TS}} | Msgs], Now, State = #state{awaiting_rel = AwaitingRel, await_rel_timeout = Timeout}) -> case (timer:now_diff(Now, TS) div 1000) of diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl index 35a22779a..6453bcba9 100644 --- a/src/emqx_session_sup.erl +++ b/src/emqx_session_sup.erl @@ -14,11 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc Session Supervisor. -module(emqx_session_sup). --author("Feng Lee "). - -behavior(supervisor). -export([start_link/0, start_session/3]). diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index d75f68984..5440bbded 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -20,8 +20,6 @@ -include("emqx.hrl"). --include("emqx_internal.hrl"). - %% Mnesia Callbacks -export([mnesia/1]). @@ -73,7 +71,7 @@ mnesia(copy) -> %% @doc Start a session manager -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> - gen_server:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id], []). + gen_server:start_link(?MODULE, [Pool, Id], []). %% @doc Start a session -spec(start_session(boolean(), {binary(), binary() | undefined}) -> {ok, pid(), boolean()} | {error, term()}). @@ -129,7 +127,7 @@ local_sessions() -> %%-------------------------------------------------------------------- init([Pool, Id]) -> - ?GPROC_POOL(join, Pool, Id), + gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #state{pool = Pool, id = Id, monitors = dict:new()}}. %% Persistent Session @@ -163,10 +161,12 @@ handle_call({start_session, true, {ClientId, Username}, ClientPid}, _From, State end; handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). + lager:error("[MQTT-SM] Unexpected Request: ~p", [Req]), + {reply, ignore, State}. handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). + lager:error("[MQTT-SM] Unexpected Message: ~p", [Msg]), + {noreply, State}. handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> case dict:find(MRef, State#state.monitors) of @@ -186,10 +186,11 @@ handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> end; handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). + lager:error("[MQTT-SM] Unexpected Info: ~p", [Info]), + {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> - ?GPROC_POOL(leave, Pool, Id). + gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -202,7 +203,8 @@ code_change(_OldVsn, State, _Extra) -> create_session({CleanSess, {ClientId, Username}, ClientPid}, State) -> case create_session(CleanSess, {ClientId, Username}, ClientPid) of {ok, SessPid} -> - {reply, {ok, SessPid, false}, monitor_session(ClientId, SessPid, State)}; + {reply, {ok, SessPid, false}, + monitor_session(ClientId, SessPid, State)}; {error, Error} -> {reply, {error, Error}, State} end. diff --git a/src/emqx_sm_helper.erl b/src/emqx_sm_helper.erl index 905260c4f..9f8970c08 100644 --- a/src/emqx_sm_helper.erl +++ b/src/emqx_sm_helper.erl @@ -14,7 +14,6 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc Session Helper. -module(emqx_sm_helper). -author("Feng Lee "). @@ -23,8 +22,6 @@ -include("emqx.hrl"). --include("emqx_internal.hrl"). - -include_lib("stdlib/include/ms_transform.hrl"). %% API Function Exports @@ -49,10 +46,12 @@ init([StatsFun]) -> {ok, #state{stats_fun = StatsFun, ticker = TRef}}. handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). + lager:error("[SM-HELPER] Unexpected Call: ~p", [Req]), + {reply, ignore, State}. handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). + lager:error("[SM-HELPER] Unexpected Cast: ~p", [Msg]), + {noreply, State}. handle_info({membership, {mnesia, down, Node}}, State) -> Fun = fun() -> @@ -71,7 +70,8 @@ handle_info(tick, State) -> {noreply, setstats(State), hibernate}; handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). + lager:error("[SM-HELPER] Unexpected Info: ~p", [Info]), + {noreply, State}. terminate(_Reason, _State = #state{ticker = TRef}) -> timer:cancel(TRef), diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index 76734ed9c..1c9098715 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -14,15 +14,10 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc Session Manager Supervisor. - -module(emqx_sm_sup). -behaviour(supervisor). --author("Feng Lee "). - --include("emqx.hrl"). -define(SM, emqx_sm). @@ -39,7 +34,7 @@ start_link() -> init([]) -> %% Create session tables - ets:new(mqtt_local_session, [public, ordered_set, named_table, {write_concurrency, true}]), + _ = ets:new(mqtt_local_session, [public, ordered_set, named_table, {write_concurrency, true}]), %% Helper StatsFun = emqx_stats:statsfun('sessions/count', 'sessions/max'), diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index f99bbb725..ba2ead2d2 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -14,15 +14,9 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc MQTT Topic Trie: -%% [Trie](http://en.wikipedia.org/wiki/Trie) -%% @end - -module(emqx_trie). --author("Feng Lee "). - --include("emqx_trie.hrl"). +-include("emqx.hrl"). %% Mnesia Callbacks -export([mnesia/1]). diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl index ff9ea3a5b..3ff2551bb 100644 --- a/src/emqx_ws.erl +++ b/src/emqx_ws.erl @@ -52,7 +52,7 @@ handle_request('GET', "/mqtt", Req) -> Parser = emqx_parser:initial_state(PacketSize), %% Upgrade WebSocket. {ReentryWs, ReplyChannel} = mochiweb_websocket:upgrade_connection(Req, fun ?MODULE:ws_loop/3), - {ok, ClientPid} = emqx_ws_client_sup:start_client(self(), Req, ReplyChannel), + {ok, ClientPid} = emqx_ws_conn_sup:start_connection(self(), Req, ReplyChannel), ReentryWs(#wsocket_state{peername = Peername, parser = Parser, max_packet_size = PacketSize, diff --git a/src/emqx_ws_client.erl b/src/emqx_ws_conn.erl similarity index 98% rename from src/emqx_ws_client.erl rename to src/emqx_ws_conn.erl index b5f8d7b51..61e717a5c 100644 --- a/src/emqx_ws_client.erl +++ b/src/emqx_ws_conn.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -14,20 +14,14 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc MQTT WebSocket Connection. - --module(emqx_ws_client). +-module(emqx_ws_conn). -behaviour(gen_server). --author("Feng Lee "). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --include("emqx_internal.hrl"). - -import(proplists, [get_value/2, get_value/3]). %% API Exports diff --git a/src/emqx_ws_client_sup.erl b/src/emqx_ws_conn_sup.erl similarity index 71% rename from src/emqx_ws_client_sup.erl rename to src/emqx_ws_conn_sup.erl index d7806f3a6..f1a26a91b 100644 --- a/src/emqx_ws_client_sup.erl +++ b/src/emqx_ws_conn_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -14,24 +14,21 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_ws_client_sup). - --author("Feng Lee "). +-module(emqx_ws_conn_sup). -behavior(supervisor). --export([start_link/0, start_client/3]). +-export([start_link/0, start_connection/3]). -export([init/1]). -%% @doc Start websocket client supervisor -spec(start_link() -> {ok, pid()}). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). -%% @doc Start a WebSocket Connection. --spec(start_client(pid(), mochiweb_request:request(), fun()) -> {ok, pid()}). -start_client(WsPid, Req, ReplyChannel) -> +%% @doc Start a MQTT/WebSocket Connection. +-spec(start_connection(pid(), mochiweb_request:request(), fun()) -> {ok, pid()}). +start_connection(WsPid, Req, ReplyChannel) -> supervisor:start_child(?MODULE, [WsPid, Req, ReplyChannel]). %%-------------------------------------------------------------------- @@ -39,8 +36,9 @@ start_client(WsPid, Req, ReplyChannel) -> %%-------------------------------------------------------------------- init([]) -> + %%TODO: Cannot upgrade the environments, Use zone? Env = lists:append(emqx:env(client, []), emqx:env(protocol, [])), {ok, {{simple_one_for_one, 0, 1}, - [{ws_client, {emqx_ws_client, start_link, [Env]}, - temporary, 5000, worker, [emqx_ws_client]}]}}. + [{ws_conn, {emqx_ws_conn, start_link, [Env]}, + temporary, 5000, worker, [emqx_ws_conn]}]}}. diff --git a/test/emqx_trie_SUITE.erl b/test/emqx_trie_SUITE.erl index 69f251372..e0b0b569d 100644 --- a/test/emqx_trie_SUITE.erl +++ b/test/emqx_trie_SUITE.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,11 +16,9 @@ -module(emqx_trie_SUITE). --author("Feng Lee "). - -compile(export_all). --include("emqx_trie.hrl"). +-include("emqx.hrl"). -define(TRIE, emqx_trie). From aef5a206977ca0e843a6e8e59ad54c4b842ce32c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 1 Mar 2018 21:04:00 +0800 Subject: [PATCH 024/520] Add 'message' and 'delivery' record --- include/emqx.hrl | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/include/emqx.hrl b/include/emqx.hrl index 72fb71971..433e733ff 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -26,6 +26,58 @@ %%-define(ERTS_MINIMUM, "9.0"). +%%-------------------------------------------------------------------- +%% Message and Delivery +%%-------------------------------------------------------------------- + +-type(message_id() :: binary() | undefined). + +-type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | atom()). + +-type(message_from() :: #{node := atom(), + clientid := binary(), + protocol := protocol(), + connector => atom(), + peername => {inet:ip_address(), inet:port_number()}, + username => binary(), + atom() => term()}). + +-type(message_flags() :: #{dup => boolean(), %% Dup flag + qos => 0 | 1 | 2, %% QoS + sys => boolean(), %% $SYS flag + retain => boolean(), %% Retain flag + durable => boolean(), %% Durable flag + atom() => boolean()}). + +-type(message_headers() :: #{packet_id => pos_integer(), + priority => pos_integer(), + expiry => integer(), %% Time to live + atom() => term()}). + +%% See 'Application Message' in MQTT Version 5.0 +-record(message, + { id :: message_id(), %% Global unique id + from :: message_from(), %% Message from + sender :: pid(), %% The pid of the sender/publisher + flags :: message_flags(), %% Message flags + headers :: message_headers() %% Message headers + topic :: binary(), %% Message topic + properties :: map(), %% Message user properties + payload :: binary(), %% Message payload + timestamp :: erlang:timestamp() %% Timestamp + }). + +-type(message() :: #message{}). + +-record(delivery, + { %sender :: pid(), %% The pid of the sender/publisher + message :: message(), %% Message + flows :: list() + }). + +-type(delivery() :: #delivery{}). + + %%-------------------------------------------------------------------- %% Sys/Queue/Share Topics' Prefix %%-------------------------------------------------------------------- From 6a957e1b33bf4d303fd2083f30dd88d144d5e4f7 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 2 Mar 2018 20:18:27 +0800 Subject: [PATCH 025/520] Support MQTT Version 5.0 --- include/emqx.hrl | 104 +++------- include/emqx_internal.hrl | 54 ----- include/emqx_mqtt.hrl | 55 ++++- src/emqx.erl | 4 - src/emqx_access_control.erl | 2 - src/emqx_access_rule.erl | 4 +- src/emqx_acl_internal.erl | 8 +- src/emqx_acl_mod.erl | 4 +- src/emqx_alarm.erl | 28 ++- src/emqx_app.erl | 10 +- src/emqx_auth_mod.erl | 4 +- src/emqx_boot.erl | 2 +- src/emqx_bridge.erl | 13 +- src/emqx_bridge_sup.erl | 4 +- src/emqx_bridge_sup_sup.erl | 4 +- src/emqx_broker.erl | 4 +- src/emqx_cm.erl | 2 +- src/emqx_cm_sup.erl | 2 +- src/emqx_config.erl | 2 +- src/emqx_conn.erl | 10 +- src/emqx_gc.erl | 2 +- src/emqx_gen_mod.erl | 4 +- src/emqx_guid.erl | 2 +- src/emqx_hooks.erl | 4 +- src/emqx_inflight.erl | 6 +- src/emqx_json.erl | 36 ++++ src/emqx_lager_backend.erl | 4 +- src/emqx_message.erl | 14 +- src/emqx_mod_presence.erl | 2 +- src/emqx_mod_rewrite.erl | 4 +- src/emqx_mod_subscription.erl | 2 +- src/emqx_mod_sup.erl | 2 +- src/emqx_mqtt5_rscode.erl | 195 ------------------ src/emqx_mqtt_app.erl | 2 +- ...qx_mqtt5_props.erl => emqx_mqtt_props.erl} | 6 +- src/emqx_mqtt_rscode.erl | 115 +++++++++++ src/emqx_mqueue.erl | 2 +- src/emqx_net.erl | 2 +- src/emqx_packet.erl | 18 +- src/emqx_parser.erl | 117 ++++++----- src/emqx_plugins.erl | 4 +- src/emqx_protocol.erl | 14 +- src/emqx_pubsub.erl | 13 +- src/emqx_pubsub_sup.erl | 5 +- src/emqx_router.erl | 4 +- src/emqx_rpc.erl | 12 +- src/emqx_serializer.erl | 12 +- src/emqx_server.erl | 2 +- src/emqx_session.erl | 40 ++-- src/emqx_session_sup.erl | 2 +- src/emqx_sm.erl | 2 +- src/emqx_sm_helper.erl | 4 +- src/emqx_sm_sup.erl | 2 +- src/emqx_stats.erl | 2 +- src/emqx_sup.erl | 4 +- src/emqx_sysmon_sup.erl | 4 +- src/emqx_topic.erl | 2 +- src/emqx_trace.erl | 4 +- src/emqx_trace_sup.erl | 4 +- src/emqx_vm.erl | 2 +- src/emqx_ws.erl | 4 +- test/emqx_lib_SUITE.erl | 2 - test/emqx_mqueue_SUITE.erl | 4 +- test/emqx_topic_SUITE.erl | 4 +- 64 files changed, 424 insertions(+), 587 deletions(-) delete mode 100644 include/emqx_internal.hrl create mode 100644 src/emqx_json.erl delete mode 100644 src/emqx_mqtt5_rscode.erl rename src/{emqx_mqtt5_props.erl => emqx_mqtt_props.erl} (97%) create mode 100644 src/emqx_mqtt_rscode.erl diff --git a/include/emqx.hrl b/include/emqx.hrl index 433e733ff..386136a9c 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -26,6 +26,16 @@ %%-define(ERTS_MINIMUM, "9.0"). +%%-------------------------------------------------------------------- +%% Sys/Queue/Share Topics' Prefix +%%-------------------------------------------------------------------- + +-define(SYSTOP, <<"$SYS/">>). %% System Topic + +-define(QUEUE, <<"$queue/">>). %% Queue Topic + +-define(SHARE, <<"$share/">>). %% Shared Topic + %%-------------------------------------------------------------------- %% Message and Delivery %%-------------------------------------------------------------------- @@ -34,7 +44,8 @@ -type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | atom()). --type(message_from() :: #{node := atom(), +-type(message_from() :: #{zone := atom(), + node := atom(), clientid := binary(), protocol := protocol(), connector => atom(), @@ -60,7 +71,7 @@ from :: message_from(), %% Message from sender :: pid(), %% The pid of the sender/publisher flags :: message_flags(), %% Message flags - headers :: message_headers() %% Message headers + headers :: message_headers(), %% Message headers topic :: binary(), %% Message topic properties :: map(), %% Message user properties payload :: binary(), %% Message payload @@ -70,24 +81,12 @@ -type(message() :: #message{}). -record(delivery, - { %sender :: pid(), %% The pid of the sender/publisher - message :: message(), %% Message + { message :: message(), flows :: list() }). -type(delivery() :: #delivery{}). - -%%-------------------------------------------------------------------- -%% Sys/Queue/Share Topics' Prefix -%%-------------------------------------------------------------------- - --define(SYSTOP, <<"$SYS/">>). %% System Topic - --define(QUEUE, <<"$queue/">>). %% Queue Topic - --define(SHARE, <<"$share/">>). %% Shared Topic - %%-------------------------------------------------------------------- %% PubSub %%-------------------------------------------------------------------- @@ -97,20 +96,16 @@ -define(PS(PS), (PS =:= publish orelse PS =:= subscribe)). %%-------------------------------------------------------------------- -%% MQTT Topic +%% Subscription %%-------------------------------------------------------------------- -%%-------------------------------------------------------------------- -%% MQTT Subscription -%%-------------------------------------------------------------------- - --record(mqtt_subscription, - { subid :: binary() | atom(), - topic :: binary(), - qos :: 0 | 1 | 2 +-record(subscription, + { subid :: binary() | atom(), + topic :: binary(), + subopts :: list() }). --type(mqtt_subscription() :: #mqtt_subscription{}). +-type(subscription() :: #subscription{}). %%-------------------------------------------------------------------- %% MQTT Client @@ -149,57 +144,6 @@ -type(mqtt_session() :: #mqtt_session{}). -%%-------------------------------------------------------------------- -%% MQTT Message -%%-------------------------------------------------------------------- - --type(mqtt_msg_id() :: binary() | undefined). - --type(mqtt_pktid() :: 1..16#ffff | undefined). - --type(mqtt_msg_from() :: atom() | {binary(), undefined | binary()}). - --record(mqtt_message, - { %% Global unique message ID - id :: mqtt_msg_id(), - %% PacketId - pktid :: mqtt_pktid(), - %% ClientId and Username - from :: mqtt_msg_from(), - %% Topic that the message is published to - topic :: binary(), - %% Message QoS - qos = 0 :: 0 | 1 | 2, - %% Message Flags - flags = [] :: [retain | dup | sys], - %% Retain flag - retain = false :: boolean(), - %% Dup flag - dup = false :: boolean(), - %% $SYS flag - sys = false :: boolean(), - %% Headers - headers = [] :: list(), - %% Payload - payload :: binary(), - %% Timestamp - timestamp :: erlang:timestamp() - }). - --type(mqtt_message() :: #mqtt_message{}). - -%%-------------------------------------------------------------------- -%% MQTT Delivery -%%-------------------------------------------------------------------- - --record(mqtt_delivery, - { sender :: pid(), %% Pid of the sender/publisher - message :: mqtt_message(), %% Message - flows :: list() - }). - --type(mqtt_delivery() :: #mqtt_delivery{}). - %%-------------------------------------------------------------------- %% Route %%-------------------------------------------------------------------- @@ -254,10 +198,10 @@ -type(plugin() :: #plugin{}). %%-------------------------------------------------------------------- -%% MQTT CLI Command. For example: 'broker metrics' +%% Command %%-------------------------------------------------------------------- --record(mqtt_cli, { name, action, args = [], opts = [], usage, descr }). +-record(command, { name, action, args = [], opts = [], usage, descr }). --type(mqtt_cli() :: #mqtt_cli{}). +-type(command() :: #command{}). diff --git a/include/emqx_internal.hrl b/include/emqx_internal.hrl deleted file mode 100644 index 2241c2fc8..000000000 --- a/include/emqx_internal.hrl +++ /dev/null @@ -1,54 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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. -%%-------------------------------------------------------------------- - -%% Internal Header File - --define(GPROC_POOL(JoinOrLeave, Pool, Id), - (begin - case JoinOrLeave of - join -> gproc_pool:connect_worker(Pool, {Pool, Id}); - leave -> gproc_pool:disconnect_worker(Pool, {Pool, Id}) - end - end)). - --define(PROC_NAME(M, I), (list_to_atom(lists:concat([M, "_", I])))). - --define(UNEXPECTED_REQ(Req, State), - (begin - lager:error("[~s] Unexpected Request: ~p", [?MODULE, Req]), - {reply, {error, unexpected_request}, State} - end)). - --define(UNEXPECTED_MSG(Msg, State), - (begin - lager:error("[~s] Unexpected Message: ~p", [?MODULE, Msg]), - {noreply, State} - end)). - --define(UNEXPECTED_INFO(Info, State), - (begin - lager:error("[~s] Unexpected Info: ~p", [?MODULE, Info]), - {noreply, State} - end)). - --define(IF(Cond, TrueFun, FalseFun), - (case (Cond) of - true -> (TrueFun); - false-> (FalseFun) - end)). - --define(FULLSWEEP_OPTS, [{fullsweep_after, 10}]). - diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index a4946cc16..62ee0e7d3 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -311,10 +311,10 @@ #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, variable = #mqtt_packet_puback{packet_id = PacketId}}). --define(SUBSCRIBE_PACKET(PacketId, TopicTable), +-define(SUBSCRIBE_PACKET(PacketId, TopicFilters), #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, qos = ?QOS_1}, - variable = #mqtt_packet_subscribe{packet_id = PacketId, - topic_table = TopicTable}}). + variable = #mqtt_packet_subscribe{packet_id = PacketId, + topic_filters = TopicFilters}}). -define(SUBACK_PACKET(PacketId, ReasonCodes), #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, @@ -337,3 +337,52 @@ -define(PACKET(Type), #mqtt_packet{header = #mqtt_packet_header{type = Type}}). +%%-------------------------------------------------------------------- +%% MQTT Message +%%-------------------------------------------------------------------- + +-type(mqtt_msg_id() :: binary() | undefined). + +-type(mqtt_msg_from() :: atom() | {binary(), undefined | binary()}). + +-record(mqtt_message, + { %% Global unique message ID + id :: mqtt_msg_id(), + %% PacketId + packet_id :: mqtt_packet_id(), + %% ClientId and Username + from :: mqtt_msg_from(), + %% Topic that the message is published to + topic :: binary(), + %% Message QoS + qos = 0 :: mqtt_qos(), + %% Message Flags + flags = [] :: [retain | dup | sys], + %% Retain flag + retain = false :: boolean(), + %% Dup flag + dup = false :: boolean(), + %% $SYS flag + sys = false :: boolean(), + %% Headers + headers = [] :: list(), + %% Payload + payload :: binary(), + %% Timestamp + timestamp :: erlang:timestamp() + }). + +-type(mqtt_message() :: #mqtt_message{}). + +%%-------------------------------------------------------------------- +%% MQTT Delivery +%%-------------------------------------------------------------------- + +-record(mqtt_delivery, + { sender :: pid(), + message :: mqtt_message(), + flows :: list() + }). + +-type(mqtt_delivery() :: #mqtt_delivery{}). + diff --git a/src/emqx.erl b/src/emqx.erl index 6142cfa7d..73a750894 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -14,12 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc EMQ X Main Module. - -module(emqx). --author("Feng Lee "). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 409b98065..05d3cbacb 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -18,8 +18,6 @@ -behaviour(gen_server). --author("Feng Lee "). - -include("emqx.hrl"). %% API Function Exports diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 39cae031e..9c1d3f4d1 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_access_rule). --author("Feng Lee "). - -include("emqx.hrl"). -type(who() :: all | binary() | diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index 3353176c5..2cd1e5acb 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -18,12 +18,8 @@ -behaviour(emqx_acl_mod). --author("Feng Lee "). - -include("emqx.hrl"). --include("emqx_cli.hrl"). - -export([all_rules/0]). %% ACL callbacks @@ -116,7 +112,7 @@ reload_acl(#state{config = undefined}) -> reload_acl(State) -> case catch load_rules_from_file(State) of {'EXIT', Error} -> {error, Error}; - true -> ?PRINT("~s~n", ["reload acl_internal successfully"]), ok + true -> io:format("~s~n", ["reload acl_internal successfully"]), ok end. %% @doc ACL Module Description diff --git a/src/emqx_acl_mod.erl b/src/emqx_acl_mod.erl index fcdaf012d..9da3396b4 100644 --- a/src/emqx_acl_mod.erl +++ b/src/emqx_acl_mod.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_acl_mod). --author("Feng Lee "). - -include("emqx.hrl"). %%-------------------------------------------------------------------- diff --git a/src/emqx_alarm.erl b/src/emqx_alarm.erl index f82ed5f2d..1688e7bb2 100644 --- a/src/emqx_alarm.erl +++ b/src/emqx_alarm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_alarm). --author("Feng Lee "). - -behaviour(gen_event). -include("emqx.hrl"). @@ -88,17 +86,25 @@ handle_event({set_alarm, Alarm = #alarm{id = AlarmId, title = Title, summary = Summary}}, Alarms)-> TS = os:timestamp(), - Json = mochijson2:encode([{id, AlarmId}, - {severity, Severity}, - {title, iolist_to_binary(Title)}, - {summary, iolist_to_binary(Summary)}, - {ts, emqx_time:now_secs(TS)}]), - emqx:publish(alarm_msg(alert, AlarmId, Json)), + case catch emqx_json:encode([{id, AlarmId}, + {severity, Severity}, + {title, iolist_to_binary(Title)}, + {summary, iolist_to_binary(Summary)}, + {ts, emqx_time:now_secs(TS)}]) of + {'EXIT', Reason} -> + lager:error("Failed to encode set_alarm: ~p", [Reason]); + JSON -> + emqx_broker:publish(alarm_msg(alert, AlarmId, JSON)) + end, {ok, [Alarm#alarm{timestamp = TS} | Alarms]}; handle_event({clear_alarm, AlarmId}, Alarms) -> - Json = mochijson2:encode([{id, AlarmId}, {ts, emqx_time:now_secs()}]), - emqx:publish(alarm_msg(clear, AlarmId, Json)), + case catch emqx_json:encode([{id, AlarmId}, {ts, emqx_time:now_secs()}]) of + {'EXIT', Reason} -> + lager:error("Failed to encode clear_alarm: ~p", [Reason]); + JSON -> + emqx_broker:publish(alarm_msg(clear, AlarmId, JSON)) + end, {ok, lists:keydelete(AlarmId, 2, Alarms), hibernate}; handle_event(_, Alarms)-> diff --git a/src/emqx_app.erl b/src/emqx_app.erl index ab83e870b..48648d64e 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -18,10 +18,6 @@ -behaviour(application). --author("Feng Lee "). - --include("emqx_cli.hrl"). - -include("emqx_mqtt.hrl"). %% Application callbacks @@ -54,11 +50,11 @@ stop(_State) -> %%-------------------------------------------------------------------- print_banner() -> - ?PRINT("Starting ~s on node ~s~n", [?APP, node()]). + io:format("Starting ~s on node ~s~n", [?APP, node()]). print_vsn() -> {ok, Vsn} = application:get_key(vsn), - ?PRINT("~s ~s is running now!~n", [?APP, Vsn]). + io:format("~s ~s is running now!~n", [?APP, Vsn]). %%-------------------------------------------------------------------- %% Register default ACL File diff --git a/src/emqx_auth_mod.erl b/src/emqx_auth_mod.erl index b72494a72..658eea9de 100644 --- a/src/emqx_auth_mod.erl +++ b/src/emqx_auth_mod.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_auth_mod). --author("Feng Lee "). - -include("emqx.hrl"). -export([passwd_hash/2]). diff --git a/src/emqx_boot.erl b/src/emqx_boot.erl index 6a2d76b8c..8d103f3f0 100644 --- a/src/emqx_boot.erl +++ b/src/emqx_boot.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index e9858793f..2ef5a16a2 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -22,8 +22,6 @@ -include("emqx_mqtt.hrl"). --include("emqx_internal.hrl"). - %% API Function Exports -export([start_link/5]). @@ -104,10 +102,12 @@ qname(Node, Topic) -> iolist_to_binary(["Bridge:", Node, ":", Topic]). handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). + lager:error("[~s] Unexpected Call: ~p", [?MODULE, Req]), + {reply, ignore, State}. handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). + lager:error("[~s] Unexpected Cast: ~p", [?MODULE, Msg]), + {noreply, State}. handle_info({dispatch, _Topic, Msg}, State = #state{mqueue = MQ, status = down}) -> {noreply, State#state{mqueue = emqx_mqueue:in(Msg, MQ)}}; @@ -148,7 +148,8 @@ handle_info({'EXIT', _Pid, normal}, State) -> {noreply, State}; handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). + lager:error("[~s] Unexpected Info: ~p", [?MODULE, Info]), + {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> gproc_pool:disconnect_worker(Pool, {Pool, Id}). diff --git a/src/emqx_bridge_sup.erl b/src/emqx_bridge_sup.erl index 668023319..5e0deed34 100644 --- a/src/emqx_bridge_sup.erl +++ b/src/emqx_bridge_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_bridge_sup). --author("Feng Lee "). - -export([start_link/3]). %%-------------------------------------------------------------------- diff --git a/src/emqx_bridge_sup_sup.erl b/src/emqx_bridge_sup_sup.erl index 91deec7dd..762a5e8a4 100644 --- a/src/emqx_bridge_sup_sup.erl +++ b/src/emqx_bridge_sup_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -18,8 +18,6 @@ -behavior(supervisor). --author("Feng Lee "). - -export([start_link/0, bridges/0, start_bridge/2, start_bridge/3, stop_bridge/2]). -export([init/1]). diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 3bb9a1835..36b174404 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -18,8 +18,6 @@ -behaviour(gen_server). --author("Feng Lee "). - -include("emqx.hrl"). -include("emqx_internal.hrl"). diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 4956b199d..2c186571b 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index 229e474bb..bbaf0d855 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_config.erl b/src/emqx_config.erl index e7fca24a4..c9d71feef 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_conn.erl b/src/emqx_conn.erl index a731bacc9..bb80cb0fa 100644 --- a/src/emqx_conn.erl +++ b/src/emqx_conn.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -203,7 +203,7 @@ handle_info({suback, PacketId, GrantedQos}, State) -> %% Fastlane handle_info({dispatch, _Topic, Message}, State) -> - handle_info({deliver, Message#message{qos = ?QOS_0}}, State); + handle_info({deliver, Message#mqtt_message{qos = ?QOS_0}}, State); handle_info({deliver, Message}, State) -> with_proto( @@ -259,16 +259,16 @@ handle_info({keepalive, start, Interval}, State = #state{connection = Conn}) -> end, case emqx_keepalive:start(StatFun, Interval, {keepalive, check}) of {ok, KeepAlive} -> - {noreply, State#client_state{keepalive = KeepAlive}}; + {noreply, State#state{keepalive = KeepAlive}}; {error, Error} -> ?LOG(warning, "Keepalive error - ~p", [Error], State), shutdown(Error, State) end; -handle_info({keepalive, check}, State = #client_state{keepalive = KeepAlive}) -> +handle_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> case emqx_keepalive:check(KeepAlive) of {ok, KeepAlive1} -> - {noreply, State#client_state{keepalive = KeepAlive1}}; + {noreply, State#state{keepalive = KeepAlive1}}; {error, timeout} -> ?LOG(debug, "Keepalive timeout", [], State), shutdown(keepalive_timeout, State); diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 4bcfb4bc8..59d7ff8cc 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_gen_mod.erl b/src/emqx_gen_mod.erl index 18a8732a2..9b3e0ee3c 100644 --- a/src/emqx_gen_mod.erl +++ b/src/emqx_gen_mod.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_gen_mod). --author("Feng Lee "). - -ifdef(use_specs). -callback(load(Opts :: any()) -> ok | {error, term()}). diff --git a/src/emqx_guid.erl b/src/emqx_guid.erl index bedd0f0b7..e03baab4d 100644 --- a/src/emqx_guid.erl +++ b/src/emqx_guid.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index f8bef6ecf..530bf0ad3 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -18,8 +18,6 @@ -behaviour(gen_server). --author("Feng Lee "). - %% Start -export([start_link/0]). diff --git a/src/emqx_inflight.erl b/src/emqx_inflight.erl index 4e26366e9..b3ea24841 100644 --- a/src/emqx_inflight.erl +++ b/src/emqx_inflight.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -14,12 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc Inflight Window that wraps the gb_trees. - -module(emqx_inflight). --author("Feng Lee "). - -export([new/1, contain/2, lookup/2, insert/3, update/3, delete/2, values/1, to_list/1, size/1, max_size/1, is_full/1, is_empty/1, window/1]). diff --git a/src/emqx_json.erl b/src/emqx_json.erl new file mode 100644 index 000000000..6b204474f --- /dev/null +++ b/src/emqx_json.erl @@ -0,0 +1,36 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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_json). + +-export([encode/1, encode/2, decode/1, decode/2]). + +-spec(encode(jsx:json_term()) -> jsx:json_text()). +encode(Term) -> + jsx:encode(Term). + +-spec(encode(jsx:json_term(), jsx_to_json:config()) -> jsx:json_text()). +encode(Term, Opts) -> + jsx:encode(Term, Opts). + +-spec(decode(jsx:json_text()) -> jsx:json_term()). +decode(JSON) -> + jsx:decode(JSON). + +-spec(decode(jsx:json_text(), jsx_to_json:config()) -> jsx:json_term()). +decode(JSON, Opts) -> + jsx:decode(JSON, Opts). + diff --git a/src/emqx_lager_backend.erl b/src/emqx_lager_backend.erl index 60e7cd5bf..c675481c5 100644 --- a/src/emqx_lager_backend.erl +++ b/src/emqx_lager_backend.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_lager_backend). --author("Feng Lee "). - -behaviour(gen_event). -include_lib("lager/include/lager.hrl"). diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 912cf356f..2574418a9 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -14,12 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc MQTT Message Functions - -module(emqx_message). --author("Feng Lee "). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). @@ -57,7 +53,7 @@ from_packet(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, packet_id = PacketId}, payload = Payload}) -> #mqtt_message{id = msgid(), - pktid = PacketId, + packet_id = PacketId, qos = Qos, retain = Retain, dup = Dup, @@ -95,7 +91,7 @@ msgid() -> emqx_guid:gen(). %% @doc Message to Packet -spec(to_packet(mqtt_message()) -> mqtt_packet()). -to_packet(#mqtt_message{pktid = PkgId, +to_packet(#mqtt_message{packet_id = PkgId, qos = Qos, retain = Retain, dup = Dup, @@ -141,13 +137,13 @@ unset_flag(retain, Msg = #mqtt_message{retain = true}) -> unset_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg. %% @doc Format MQTT Message -format(#mqtt_message{id = MsgId, pktid = PktId, from = {ClientId, Username}, +format(#mqtt_message{id = MsgId, packet_id = PktId, from = {ClientId, Username}, qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s/~s, Topic=~s)", [i(Qos), i(Retain), i(Dup), MsgId, PktId, Username, ClientId, Topic]); %% TODO:... -format(#mqtt_message{id = MsgId, pktid = PktId, from = From, +format(#mqtt_message{id = MsgId, packet_id = PktId, from = From, qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s, Topic=~s)", [i(Qos), i(Retain), i(Dup), MsgId, PktId, From, Topic]). diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index 347213e31..a0d8228d5 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index ca8d02551..81480f212 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -18,6 +18,8 @@ -include_lib("emqx.hrl"). +-include_lib("emqx_mqtt.hrl"). + -export([load/1, unload/1]). -export([rewrite_subscribe/4, rewrite_unsubscribe/4, rewrite_publish/2]). diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index cae25842a..7230a6dd2 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_mod_sup.erl b/src/emqx_mod_sup.erl index 1f71dcc74..016708351 100644 --- a/src/emqx_mod_sup.erl +++ b/src/emqx_mod_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_mqtt5_rscode.erl b/src/emqx_mqtt5_rscode.erl deleted file mode 100644 index 10c137678..000000000 --- a/src/emqx_mqtt5_rscode.erl +++ /dev/null @@ -1,195 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqx.io) -%% -%% 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_mqtt5_rscode). - --author("Feng Lee "). - --export([name/1, value/1]). - -%%-------------------------------------------------------------------- -%% Reason code to name -%%-------------------------------------------------------------------- - -0 -name(0x00 -Success -CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK, AUTH -0 -name(0x00 -Normal disconnection -DISCONNECT -0 -name(0x00 -Granted QoS 0 -SUBACK -1 -name(0x01 -Granted QoS 1 -SUBACK -2 -name(0x02 -Granted QoS 2 -SUBACK -4 -name(0x04 -Disconnect with Will Message -DISCONNECT -16 -name(0x10 -No matching subscribers -PUBACK, PUBREC -17 -name(0x11 -No subscription existed -UNSUBACK -24 -name(0x18 -Continue authentication -AUTH -25 -name(0x19 -Re-authenticate -AUTH -128 -name(0x80 -Unspecified error -CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT -129 -name(0x81 -Malformed Packet -CONNACK, DISCONNECT -130 -name(0x82 -Protocol Error -CONNACK, DISCONNECT -131 -name(0x83 -Implementation specific error -CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT -132 -name(0x84 -Unsupported Protocol Version -CONNACK -133 -name(0x85 -Client Identifier not valid -CONNACK -134 -name(0x86 -Bad User Name or Password -CONNACK -135 -name(0x87 -Not authorized -CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT -136 -name(0x88 -Server unavailable -CONNACK -137 -name(0x89 -Server busy -CONNACK, DISCONNECT -138 -name(0x8A -Banned -CONNACK -139 -name(0x8B -Server shutting down -DISCONNECT -140 -name(0x8C -Bad authentication method -CONNACK, DISCONNECT -141 -name(0x8D -Keep Alive timeout -DISCONNECT -142 -name(0x8E -Session taken over -DISCONNECT -143 -name(0x8F -Topic Filter invalid -SUBACK, UNSUBACK, DISCONNECT -144 -name(0x90 -Topic Name invalid -CONNACK, PUBACK, PUBREC, DISCONNECT -145 -name(0x91 -Packet Identifier in use -PUBACK, PUBREC, SUBACK, UNSUBACK -146 -name(0x92 -Packet Identifier not found -PUBREL, PUBCOMP -147 -name(0x93 -Receive Maximum exceeded -DISCONNECT -148 -name(0x94 -Topic Alias invalid -DISCONNECT -149 -name(0x95 -Packet too large -CONNACK, DISCONNECT -150 -name(0x96 -Message rate too high -DISCONNECT -151 -name(0x97 -Quota exceeded -CONNACK, PUBACK, PUBREC, SUBACK, DISCONNECT -%% 152 -name(0x98 -Administrative action -DISCONNECT -%% 153 -name(0x99 -Payload format invalid -CONNACK, PUBACK, PUBREC, DISCONNECT -%% 154 -name(0x9A -Retain not supported -CONNACK, DISCONNECT -%% 155 -name(0x9B -QoS not supported -CONNACK, DISCONNECT -%% 156 -name(0x9C -Use another server -CONNACK, DISCONNECT -%% 157: CONNACK, DISCONNECT -name(0x9D) -> 'Server-Moved'; -%% 158: SUBACK, DISCONNECT -name(0x9E) -> 'Shared-Subscriptions-Not-Supported'; -%% 159: CONNACK, DISCONNECT -name(0x9F) -> 'Connection-Rate-Exceeded'; -%% 160: DISCONNECT -name(0xA0) -> 'Maximum-Connect-Time'; -%% 161: SUBACK, DISCONNECT -name(0xA1) -> 'Subscription-Identifiers-Not-Supported'; -%% 162: SUBACK, DISCONNECT -name(0xA2) -> 'Wildcard-Subscriptions-Not-Supported'; - diff --git a/src/emqx_mqtt_app.erl b/src/emqx_mqtt_app.erl index bde8f4633..e947855e1 100644 --- a/src/emqx_mqtt_app.erl +++ b/src/emqx_mqtt_app.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_mqtt5_props.erl b/src/emqx_mqtt_props.erl similarity index 97% rename from src/emqx_mqtt5_props.erl rename to src/emqx_mqtt_props.erl index 70857d18c..6001b5211 100644 --- a/src/emqx_mqtt5_props.erl +++ b/src/emqx_mqtt_props.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqx.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -14,9 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_mqtt5_props). - --author("Feng Lee "). +-module(emqx_mqtt_props). -export([name/1, id/1]). diff --git a/src/emqx_mqtt_rscode.erl b/src/emqx_mqtt_rscode.erl new file mode 100644 index 000000000..b5658c17f --- /dev/null +++ b/src/emqx_mqtt_rscode.erl @@ -0,0 +1,115 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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_rscode). + +-export([value/1]). + +%%-------------------------------------------------------------------- +%% Reason code to name +%%-------------------------------------------------------------------- + +%% 00: Success; CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK, AUTH +value('Success') -> 16#00; +%% 00: Normal disconnection; DISCONNECT +value('Normal-Disconnection') -> 16#00; +%% 00: Granted QoS 0; SUBACK +value('Granted-QoS0') -> 16#00; +%% 01: Granted QoS 1; SUBACK +value('Granted-QoS1') -> 16#01; +%% 02: Granted QoS 2; SUBACK +value('Granted-QoS2') -> 16#02; +%% 04: Disconnect with Will Message; DISCONNECT +value('Disconnect-With-Will-Message') -> 16#04; +%% 16: No matching subscribers; PUBACK, PUBREC +value('No-Matching-Subscribers') -> 16#10; +%% 17: No subscription existed; UNSUBACK +value('No-Subscription-Existed') -> 16#11; +%% 24: Continue authentication; AUTH +value('Continue-Authentication') -> 16#18; +%% 25: Re-Authenticate; AUTH +value('Re-Authenticate') -> 16#19; +%% 128: Unspecified error; CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT +value('Unspecified-Error') -> 16#80; +%% 129: Malformed Packet; CONNACK, DISCONNECT +value('Malformed-Packet') -> 16#81; +%% 130: Protocol Error; CONNACK, DISCONNECT +value('Protocol-Error') -> 16#82; +%% 131: Implementation specific error; CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT +value('Implementation-Specific-Error') -> 16#83; +%% 132: Unsupported Protocol Version; CONNACK +value('Unsupported-Protocol-Version') -> 16#84; +%% 133: Client Identifier not valid; CONNACK +value('Client-Identifier-not-Valid') -> 16#85; +%% 134: Bad User Name or Password; CONNACK +value('Bad-Username-or-Password') -> 16#86; +%% 135: Not authorized; CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT +value('Not-Authorized') -> 16#87; +%% 136: Server unavailable; CONNACK +value('Server-Unavailable') -> 16#88; +%% 137: Server busy; CONNACK, DISCONNECT +value('Server-Busy') -> 16#89; +%% 138: Banned; CONNACK +value('Banned') -> 16#8A; +%% 139: Server shutting down; DISCONNECT +value('Server-Shutting-Down') -> 16#8B; +%% 140: Bad authentication method; CONNACK, DISCONNECT +value('Bad-Authentication-Method') -> 16#8C; +%% 141: Keep Alive timeout; DISCONNECT +value('Keep-Alive-Timeout') -> 16#8D; +%% 142: Session taken over; DISCONNECT +value('Session-Taken-Over') -> 16#8E; +%% 143: Topic Filter invalid; SUBACK, UNSUBACK, DISCONNECT +value('Topic-Filter-Invalid') -> 16#8F; +%% 144: Topic Name invalid; CONNACK, PUBACK, PUBREC, DISCONNECT +value('Topic-Name-Invalid') -> 16#90; +%% 145: Packet Identifier in use; PUBACK, PUBREC, SUBACK, UNSUBACK +value('Packet-Identifier-Inuse') -> 16#91; +%% 146: Packet Identifier not found; PUBREL, PUBCOMP +value('Packet-Identifier-Not-Found') -> 16#92; +%% 147: Receive Maximum exceeded; DISCONNECT +value('Receive-Maximum-Exceeded') -> 16#93; +%% 148: Topic Alias invalid; DISCONNECT +value('Topic-Alias-Invalid') -> 16#94; +%% 149: Packet too large; CONNACK, DISCONNECT +value('Packet-Too-Large') -> 16#95; +%% 150: Message rate too high; DISCONNECT +value('Message-Rate-Too-High') -> 16#96; +%% 151: Quota exceeded; CONNACK, PUBACK, PUBREC, SUBACK, DISCONNECT +value('Quota-Exceeded') -> 16#97; +%% 152: Administrative action; DISCONNECT +value('Administrative-Action') -> 16#98; +%% 153: Payload format invalid; CONNACK, PUBACK, PUBREC, DISCONNECT +value('Payload-Format-Invalid') -> 16#99; +%% 154: Retain not supported; CONNACK, DISCONNECT +value('Retain-Not-Supported') -> 16#9A; +%% 155: QoS not supported; CONNACK, DISCONNECT +value('QoS-Not-Supported') -> 16#9B; +%% 156: Use another server; CONNACK, DISCONNECT +value('Use-Another-Server') -> 16#9C; +%% 157: Server moved; CONNACK, DISCONNECT +value('Server-Moved') -> 16#9D; +%% 158: Shared Subscriptions not supported; SUBACK, DISCONNECT +value('Shared-Subscriptions-Not-Supported') -> 16#9E; +%% 159: Connection rate exceeded; CONNACK, DISCONNECT +value('Connection-Rate-Exceeded') -> 16#9F; +%% 160: Maximum connect time; DISCONNECT +value('Maximum-Connect-Time') -> 16#A0; +%% 161: Subscription Identifiers not supported; SUBACK, DISCONNECT +value('Subscription-Identifiers-Not-Supported') -> 16#A1; +%% 162: Wildcard-Subscriptions-Not-Supported; SUBACK, DISCONNECT +value('Wildcard-Subscriptions-Not-Supported') -> 16#A2. + diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index 190e525fc..c139a6bdd 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_net.erl b/src/emqx_net.erl index 0e666dfb1..ee186ee5a 100644 --- a/src/emqx_net.erl +++ b/src/emqx_net.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index ad94f5d0c..39681f27f 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_packet). --author("Feng Lee "). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). @@ -92,8 +90,8 @@ format_variable(#mqtt_packet_connect{ io_lib:format(Format1, Args1); format_variable(#mqtt_packet_connack{ack_flags = AckFlags, - return_code = ReturnCode}) -> - io_lib:format("AckFlags=~p, RetainCode=~p", [AckFlags, ReturnCode]); + reason_code = ReasonCode}) -> + io_lib:format("AckFlags=~p, RetainCode=~p", [AckFlags, ReasonCode]); format_variable(#mqtt_packet_publish{topic_name = TopicName, packet_id = PacketId}) -> @@ -102,17 +100,17 @@ format_variable(#mqtt_packet_publish{topic_name = TopicName, format_variable(#mqtt_packet_puback{packet_id = PacketId}) -> io_lib:format("PacketId=~p", [PacketId]); -format_variable(#mqtt_packet_subscribe{packet_id = PacketId, - topic_table = TopicTable}) -> - io_lib:format("PacketId=~p, TopicTable=~p", [PacketId, TopicTable]); +format_variable(#mqtt_packet_subscribe{packet_id = PacketId, + topic_filters = TopicFilters}) -> + io_lib:format("PacketId=~p, TopicFilters=~p", [PacketId, TopicFilters]); format_variable(#mqtt_packet_unsubscribe{packet_id = PacketId, topics = Topics}) -> io_lib:format("PacketId=~p, Topics=~p", [PacketId, Topics]); format_variable(#mqtt_packet_suback{packet_id = PacketId, - qos_table = QosTable}) -> - io_lib:format("PacketId=~p, QosTable=~p", [PacketId, QosTable]); + reason_codes = ReasonCodes}) -> + io_lib:format("PacketId=~p, ReasonCodes=~p", [PacketId, ReasonCodes]); format_variable(#mqtt_packet_unsuback{packet_id = PacketId}) -> io_lib:format("PacketId=~p", [PacketId]); diff --git a/src/emqx_parser.erl b/src/emqx_parser.erl index c7539112e..a65b70894 100644 --- a/src/emqx_parser.erl +++ b/src/emqx_parser.erl @@ -16,8 +16,6 @@ -module(emqx_parser). --author("Feng Lee "). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). @@ -27,53 +25,55 @@ -type(max_packet_size() :: 1..?MAX_PACKET_SIZE). --spec(initial_state() -> {none, max_packet_size()}). +-type(state() :: #{maxlen := max_packet_size(), vsn := mqtt_vsn()}). + +-spec(initial_state() -> {none, state()}). initial_state() -> initial_state(?MAX_PACKET_SIZE). %% @doc Initialize a parser --spec(initial_state(max_packet_size()) -> {none, max_packet_size()}). +-spec(initial_state(max_packet_size()) -> {none, state()}). initial_state(MaxSize) -> - {none, MaxSize}. + {none, #{maxlen => MaxSize, vsn => ?MQTT_PROTO_V4}}. %% @doc Parse MQTT Packet --spec(parse(binary(), {none, pos_integer()} | fun()) +-spec(parse(binary(), {none, state()} | fun()) -> {ok, mqtt_packet()} | {error, term()} | {more, fun()}). -parse(<<>>, {none, MaxLen}) -> - {more, fun(Bin) -> parse(Bin, {none, MaxLen}) end}; -parse(<>, {none, Limit}) -> +parse(<<>>, {none, State}) -> + {more, fun(Bin) -> parse(Bin, {none, State}) end}; +parse(<>, {none, State}) -> parse_remaining_len(Rest, #mqtt_packet_header{type = Type, dup = bool(Dup), qos = fixqos(Type, QoS), - retain = bool(Retain)}, Limit); + retain = bool(Retain)}, State); parse(Bin, Cont) -> Cont(Bin). -parse_remaining_len(<<>>, Header, Limit) -> - {more, fun(Bin) -> parse_remaining_len(Bin, Header, Limit) end}; -parse_remaining_len(Rest, Header, Limit) -> - parse_remaining_len(Rest, Header, 1, 0, Limit). +parse_remaining_len(<<>>, Header, State) -> + {more, fun(Bin) -> parse_remaining_len(Bin, Header, State) end}; +parse_remaining_len(Rest, Header, State) -> + parse_remaining_len(Rest, Header, 1, 0, State). -parse_remaining_len(_Bin, _Header, _Multiplier, Length, MaxLen) +parse_remaining_len(_Bin, _Header, _Multiplier, Length, #{maxlen := MaxLen}) when Length > MaxLen -> {error, invalid_mqtt_frame_len}; -parse_remaining_len(<<>>, Header, Multiplier, Length, Limit) -> - {more, fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length, Limit) end}; -%% optimize: match PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK... -parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0, _Limit) -> - parse_frame(Rest, Header, 2); +parse_remaining_len(<<>>, Header, Multiplier, Length, State) -> + {more, fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length, State) end}; +%% Optimize: match PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK... +parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0, State) -> + parse_frame(Rest, Header, 2, State); %% optimize: match PINGREQ... -parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, _Limit) -> - parse_frame(Rest, Header, 0); -parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value, Limit) -> - parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier, Limit); -parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value, MaxLen) -> +parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, State) -> + parse_frame(Rest, Header, 0, State); +parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value, State) -> + parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier, State); +parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value, State = #{maxlen := MaxLen}) -> FrameLen = Value + Len * Multiplier, if FrameLen > MaxLen -> {error, invalid_mqtt_frame_len}; - true -> parse_frame(Rest, Header, FrameLen) + true -> parse_frame(Rest, Header, FrameLen, State) end. -parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) -> +parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length, State = #{vsn := Vsn}) -> case {Type, Bin} of {?CONNECT, <>} -> {ProtoName, Rest1} = parse_utf(FrameBin), @@ -95,7 +95,7 @@ parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) - {WillMsg, Rest8} = parse_msg(Rest7, WillFlag), {UserName, Rest9} = parse_utf(Rest8, UsernameFlag), {PasssWord, <<>>} = parse_utf(Rest9, PasswordFlag), - case protocol_name_approved(ProtoVersion, ProtoName) of + case protocol_name_approved(ProtoVer, ProtoName) of true -> wrap(Header, #mqtt_packet_connect{ @@ -128,7 +128,7 @@ parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) - _ -> <> = Rest1, {Id, R} end, - {Properties, Payload} = parse_properties(ProtoVer, Rest), + {Properties, Payload} = parse_properties(Vsn, Rest2), wrap(fixdup(Header), #mqtt_packet_publish{topic_name = TopicName, packet_id = PacketId, properties = Properties}, @@ -136,10 +136,10 @@ parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) - {PubAck, <>} when PubAck == ?PUBACK; PubAck == ?PUBREC; PubAck == ?PUBREL; PubAck == ?PUBCOMP -> <> = FrameBin, - case ProtoVer == ?MQTT_PROTO_V5 of + case Vsn == ?MQTT_PROTO_V5 of true -> <> = Rest1, - {Properties, Rest3} = parse_properties(ProtoVer, Rest2), + {Properties, Rest3} = parse_properties(Vsn, Rest2), wrap(Header, #mqtt_packet_puback{packet_id = PacketId, reason_code = ReasonCode, properties = Properties}, Rest3); @@ -149,11 +149,11 @@ parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) - {?SUBSCRIBE, <>} -> %% 1 = Qos, <> = FrameBin, - {Properties, Rest2} = parse_properties(ProtoVer, Rest1), - TopicTable = parse_topics(?SUBSCRIBE, Rest1, []), - wrap(Header, #mqtt_packet_subscribe{packet_id = PacketId, - properties = Properties, - topic_table = TopicTable}, Rest); + {Properties, Rest2} = parse_properties(Vsn, Rest1), + TopicFilters = parse_topics(?SUBSCRIBE, Rest2, []), + wrap(Header, #mqtt_packet_subscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}, Rest); %{?SUBACK, <>} -> % <> = FrameBin, % {Properties, Rest2/binary>> = parse_properties(ProtoVer, Rest1), @@ -162,7 +162,7 @@ parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) - {?UNSUBSCRIBE, <>} -> %% 1 = Qos, <> = FrameBin, - {Properties, Rest2} = parse_properties(ProtoVer, Rest1), + {Properties, Rest2} = parse_properties(Vsn, Rest1), Topics = parse_topics(?UNSUBSCRIBE, Rest2, []), wrap(Header, #mqtt_packet_unsubscribe{packet_id = PacketId, properties = Properties, @@ -180,20 +180,19 @@ parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) - % Length = 0, % wrap(Header, Rest); {?DISCONNECT, <>} -> - case ProtoVer == ?MQTT_PROTO_V5 of + if + Vsn == ?MQTT_PROTO_V5 -> + <> = FrameBin, + {Properties, Rest2} = parse_properties(Vsn, Rest1), + wrap(Header, #mqtt_packet_disconnect{reason_code = ReasonCode, + properties = Properties}, Rest2); true -> - <> = Rest, - {Properties, Rest2} = parse_properties(ProtoVer, Rest1), - wrap(Header, #mqtt_packet_disconnect{reason_code = Reason, - properties = Properties}, Rest2); - false -> - Lenght = 0, wrap(Header, Rest) + Length = 0, wrap(Header, Rest) end; {_, TooShortBin} -> {more, fun(BinMore) -> - parse_frame(<>, - Header, Length) - end} + parse_frame(<>, Header, Length, State) + end} end. wrap(Header, Variable, Payload, Rest) -> @@ -205,12 +204,12 @@ wrap(Header, Rest) -> parse_will_props(Bin, ProtoVer = ?MQTT_PROTO_V5, 1) -> parse_properties(ProtoVer, Bin); -parse_will_props(Bin, _ProtoVer, _WillFlag), +parse_will_props(Bin, _ProtoVer, _WillFlag) -> {#{}, Bin}. parse_properties(?MQTT_PROTO_V5, Bin) -> {Len, Rest} = parse_variable_byte_integer(Bin), - <> = Rest, {parse_property(PropsBin, #{}), Rest1}; parse_properties(_MQTT_PROTO_V3, Bin) -> {#{}, Bin}. %% No properties. @@ -228,11 +227,11 @@ parse_property(<<16#03, Bin/binary>>, Props) -> {Val, Rest} = parse_utf(Bin), parse_property(Rest, Props#{'Content-Type' => Val}); %% 08: 'Response-Topic', UTF-8 Encoded String; -parse_property(<<16#08, Bin/binary>>) -> +parse_property(<<16#08, Bin/binary>>, Props) -> {Val, Rest} = parse_utf(Bin), parse_property(Rest, Props#{'Response-Topic' => Val}); %% 09: 'Correlation-Data', Binary Data; -parse_property(<<16#09, Len:16/big, Val:Len/binary, Bin/binary>>) -> +parse_property(<<16#09, Len:16/big, Val:Len/binary, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Correlation-Data' => Val}); %% 11: 'Subscription-Identifier', Variable Byte Integer; parse_property(<<16#0B, Bin/binary>>, Props) -> @@ -242,18 +241,18 @@ parse_property(<<16#0B, Bin/binary>>, Props) -> parse_property(<<16#11, Val:32/big, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Session-Expiry-Interval' => Val}); %% 18: 'Assigned-Client-Identifier', UTF-8 Encoded String; -parse_property(<<16#12, Bin/binary>>) -> +parse_property(<<16#12, Bin/binary>>, Props) -> {Val, Rest} = parse_utf(Bin), parse_property(Rest, Props#{'Assigned-Client-Identifier' => Val}); %% 19: 'Server-Keep-Alive', Two Byte Integer; -parse_property(<<16#13, Val:16, Bin/binary>>) -> +parse_property(<<16#13, Val:16, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Server-Keep-Alive' => Val}); %% 21: 'Authentication-Method', UTF-8 Encoded String; parse_property(<<16#15, Bin/binary>>, Props) -> {Val, Rest} = parse_utf(Bin), - parse_property(Rest, Props#{'Authentication-Method' => Val}) + parse_property(Rest, Props#{'Authentication-Method' => Val}); %% 22: 'Authentication-Data', Binary Data; -parse_property(<<16#16, Len:16/big, Val:Len/binary, Bin/binary>>) -> +parse_property(<<16#16, Len:16/big, Val:Len/binary, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Authentication-Data' => Val}); %% 23: 'Request-Problem-Information', Byte; parse_property(<<16#17, Val, Bin/binary>>, Props) -> @@ -273,7 +272,7 @@ parse_property(<<16#1C, Bin/binary>>, Props) -> {Val, Rest} = parse_utf(Bin), parse_property(Rest, Props#{'Server-Reference' => Val}); %% 31: 'Reason-String', UTF-8 Encoded String; -parse_property(<<16#1F, Bin/binary, Props) -> +parse_property(<<16#1F, Bin/binary>>, Props) -> {Val, Rest} = parse_utf(Bin), parse_property(Rest, Props#{'Reason-String' => Val}); %% 33: 'Receive-Maximum', Two Byte Integer; @@ -300,7 +299,7 @@ parse_property(<<16#26, Bin/binary>>, Props) -> end); %% 39: 'Maximum-Packet-Size', Four Byte Integer; parse_property(<<16#27, Val:32, Bin/binary>>, Props) -> - parse_property(Rest, Props#{'Maximum-Packet-Size' => Val}); + parse_property(Bin, Props#{'Maximum-Packet-Size' => Val}); %% 40: 'Wildcard-Subscription-Available', Byte; parse_property(<<16#28, Val, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Wildcard-Subscription-Available' => Val}); @@ -321,8 +320,8 @@ parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) -> parse_topics(_Packet, <<>>, Topics) -> lists:reverse(Topics); parse_topics(?SUBSCRIBE = Sub, Bin, Topics) -> - {Name, <<<<_Reserved:2, RetainHandling:2, KeepRetain:1, NoLocal:1, QoS:2>>, Rest/binary>>} = parse_utf(Bin), - SubOpts = [{qos, Qos}, {retain_handling, RetainHandling}, {keep_retain, KeepRetain}, {no_local, NoLocal}], + {Name, <<_Reserved:2, RetainHandling:2, KeepRetain:1, NoLocal:1, QoS:2, Rest/binary>>} = parse_utf(Bin), + SubOpts = [{qos, QoS}, {retain_handling, RetainHandling}, {keep_retain, KeepRetain}, {no_local, NoLocal}], parse_topics(Sub, Rest, [{Name, SubOpts}| Topics]); parse_topics(?UNSUBSCRIBE = Sub, Bin, Topics) -> {Name, <>} = parse_utf(Bin), diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index 4eddf7331..0ec84245f 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_plugins). --author("Feng Lee "). - -include("emqx.hrl"). -export([init/0]). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index c754774a3..c2ab59be3 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,12 +16,12 @@ -module(emqx_protocol). --author("Feng Lee "). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). +-include("emqx_misc.hrl"). + -import(proplists, [get_value/2, get_value/3]). %% API @@ -241,8 +241,8 @@ process(?CONNECT_PACKET(Var), State0) -> end, %% Run hooks emqx_hooks:run('client.connected', [ReturnCode1], client(State3)), - %% Send connack - send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), State3), + %%TODO: Send Connack + %% send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), State3), %% stop if authentication failure stop_if_auth_failure(ReturnCode1, State3); @@ -567,10 +567,10 @@ sp(false) -> 0. %% The retained flag should be propagated for bridge. %%-------------------------------------------------------------------- -clean_retain(false, Msg = #message{retain = true, headers = Headers}) -> +clean_retain(false, Msg = #mqtt_message{retain = true, headers = Headers}) -> case lists:member(retained, Headers) of true -> Msg; - false -> Msg#message{retain = false} + false -> Msg#mqtt_message{retain = false} end; clean_retain(_IsBridge, Msg) -> Msg. diff --git a/src/emqx_pubsub.erl b/src/emqx_pubsub.erl index 9554f30ad..9099afc83 100644 --- a/src/emqx_pubsub.erl +++ b/src/emqx_pubsub.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -20,6 +20,8 @@ -include("emqx.hrl"). +-include("emqx_mqtt.hrl"). + -export([start_link/3]). %% PubSub API. @@ -173,7 +175,8 @@ handle_call({unsubscribe, Topic, Subscriber, Options}, _From, State) -> reply(ok, setstats(State)); handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). + lager:error("[~s] Unexpected Call: ~p", [?MODULE, Req]), + {reply, ignore, State}. handle_cast({subscribe, Topic, Subscriber, Options}, State) -> add_subscriber(Topic, Subscriber, Options), @@ -184,10 +187,12 @@ handle_cast({unsubscribe, Topic, Subscriber, Options}, State) -> noreply(setstats(State)); handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). + lager:error("[~s] Unexpected Cast: ~p", [?MODULE, Msg]), + {noreply, State}. handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). + lager:error("[~s] Unexpected Info: ~p", [?MODULE, Info]), + {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> gproc_pool:disconnect_worker(Pool, {Pool, Id}). diff --git a/src/emqx_pubsub_sup.erl b/src/emqx_pubsub_sup.erl index 6611e1bbd..a9978d011 100644 --- a/src/emqx_pubsub_sup.erl +++ b/src/emqx_pubsub_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -14,11 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc PubSub Supervisor. -module(emqx_pubsub_sup). --author("Feng Lee "). - -behaviour(supervisor). %% API diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 060786590..e46add4bf 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_router). --author("Feng Lee "). - -behaviour(gen_server). -include("emqx.hrl"). diff --git a/src/emqx_rpc.erl b/src/emqx_rpc.erl index a137b543c..f4ef07809 100644 --- a/src/emqx_rpc.erl +++ b/src/emqx_rpc.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -13,18 +13,12 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- -%% -%% @doc EMQ X Distributed RPC. -%% -%%-------------------------------------------------------------------- -module(emqx_rpc). --author("Feng Lee "). - -export([cast/4]). -%% @doc Wraps gen_rpc first. cast(Node, Mod, Fun, Args) -> - emqx_metrics:inc('messages/forward'), rpc:cast(Node, Mod, Fun, Args). + emqx_metrics:inc('messages/forward'), + rpc:cast(Node, Mod, Fun, Args). diff --git a/src/emqx_serializer.erl b/src/emqx_serializer.erl index f99497d84..63906e426 100644 --- a/src/emqx_serializer.erl +++ b/src/emqx_serializer.erl @@ -16,8 +16,6 @@ -module(emqx_serializer). --author("Feng Lee "). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). @@ -83,11 +81,11 @@ serialize_variable(?CONNACK, #mqtt_packet_connack{ack_flags = AckFlags, reason_code = ReasonCode, properties = Properties}, undefined) -> PropsBin = serialize_properties(Properties), - {<>, <<>>}; + {<>, <<>>}; serialize_variable(?SUBSCRIBE, #mqtt_packet_subscribe{packet_id = PacketId, - topic_table = Topics }, undefined) -> - {<>, serialize_topics(Topics)}; + topic_filters = TopicFilters}, undefined) -> + {<>, serialize_topics(TopicFilters)}; serialize_variable(?SUBACK, #mqtt_packet_suback{packet_id = PacketId, properties = Properties, @@ -124,7 +122,7 @@ serialize_variable(?PINGRESP, undefined, undefined) -> serialize_variable(?DISCONNECT, #mqtt_packet_disconnect{reason_code = ReasonCode, properties = Properties}, undefined) -> - {<>, <<>>}. + {<>, <<>>}; serialize_variable(?AUTH, #mqtt_packet_auth{reason_code = ReasonCode, properties = Properties}, undefined) -> @@ -138,7 +136,7 @@ serialize_payload(Bin) when is_binary(Bin) -> serialize_properties(undefined) -> <<>>; serialize_properties(Props) -> - << serialize_property(Prop, Val) || {Prop, Val} <= (maps:to_list(Props)) >>. + << <<(serialize_property(Prop, Val))/binary>> || {Prop, Val} <- maps:to_list(Props) >>. %% 01: Byte; serialize_property('Payload-Format-Indicator', Val) -> diff --git a/src/emqx_server.erl b/src/emqx_server.erl index 6aa9acce8..1f7ee65ba 100644 --- a/src/emqx_server.erl +++ b/src/emqx_server.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 4ce6f7cfd..4f89e73ae 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -47,12 +47,12 @@ -behaviour(gen_server). --author("Feng Lee "). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). +-include("emqx_misc.hrl"). + -import(emqx_misc, [start_timer/2]). -import(proplists, [get_value/2, get_value/3]). @@ -192,15 +192,15 @@ subscribe(Session, PacketId, TopicTable) -> %%TODO: the ack function??... %% @doc Publish Message -spec(publish(pid(), message()) -> ok | {error, term()}). -publish(_Session, Msg = #message{qos = ?QOS_0}) -> +publish(_Session, Msg = #mqtt_message{qos = ?QOS_0}) -> %% Publish QoS0 Directly emqx_server:publish(Msg), ok; -publish(_Session, Msg = #message{qos = ?QOS_1}) -> +publish(_Session, Msg = #mqtt_message{qos = ?QOS_1}) -> %% Publish QoS1 message directly for client will PubAck automatically emqx_server:publish(Msg), ok; -publish(Session, Msg = #message{qos = ?QOS_2}) -> +publish(Session, Msg = #mqtt_message{qos = ?QOS_2}) -> %% Publish QoS2 to Session gen_server:call(Session, {publish, Msg}, ?TIMEOUT). @@ -320,7 +320,7 @@ binding(ClientPid) -> handle_pre_hibernate(State) -> {hibernate, emqx_gc:reset_conn_gc_count(#state.force_gc_count, emit_stats(State))}. -handle_call({publish, Msg = #mqtt_message{qos = ?QOS_2, pktid = PacketId}}, _From, +handle_call({publish, Msg = #mqtt_message{qos = ?QOS_2, packet_id = PacketId}}, _From, State = #state{awaiting_rel = AwaitingRel, await_rel_timer = Timer, await_rel_timeout = Timeout}) -> @@ -347,7 +347,8 @@ handle_call(state, _From, State) -> reply(?record_to_proplist(state, State, ?STATE_KEYS), State); handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). + lager:error("[~s] Unexpected Call: ~p", [?MODULE, Req]), + {reply, ignore, State}. handle_cast({subscribe, From, TopicTable, AckFun}, State = #state{client_id = ClientId, @@ -512,10 +513,11 @@ handle_cast({destroy, ClientId}, shutdown(conflict, State); handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). + lager:error("[~s] Unexpected Cast: ~p", [?MODULE, Msg]), + {noreply, State}. %% Ignore Messages delivered by self -handle_info({dispatch, _Topic, #message{from = {ClientId, _}}}, +handle_info({dispatch, _Topic, #mqtt_message{from = {ClientId, _}}}, State = #state{client_id = ClientId, ignore_loop_deliver = true}) -> {noreply, State}; @@ -560,8 +562,9 @@ handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = ClientPid}) -> [ClientPid, Pid, Reason], State), {noreply, State, hibernate}; -handle_info(Info, Session) -> - ?UNEXPECTED_INFO(Info, Session). +handle_info(Info, State) -> + lager:error("[~s] Unexpected Info: ~p", [?MODULE, Info]), + {noreply, State}. terminate(Reason, #state{client_id = ClientId, username = Username}) -> %% Move to emqx_sm to avoid race condition @@ -608,7 +611,7 @@ retry_delivery(Force, [{Type, Msg, Ts} | Msgs], Now, if Force orelse (Diff >= Interval) -> case {Type, Msg} of - {publish, Msg = #mqtt_message{pktid = PacketId}} -> + {publish, Msg = #mqtt_message{packet_id = PacketId}} -> redeliver(Msg, State), Inflight1 = Inflight:update(PacketId, {publish, Msg, Now}), retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}); @@ -635,7 +638,7 @@ expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) -> expire_awaiting_rel([], _Now, State) -> State#state{await_rel_timer = undefined}; -expire_awaiting_rel([{PacketId, Msg = #message{timestamp = TS}} | Msgs], +expire_awaiting_rel([{PacketId, Msg = #mqtt_message{timestamp = TS}} | Msgs], Now, State = #state{awaiting_rel = AwaitingRel, await_rel_timeout = Timeout}) -> case (timer:now_diff(Now, TS) div 1000) of @@ -691,7 +694,7 @@ dispatch(Msg = #mqtt_message{qos = QoS}, true -> enqueue_msg(Msg, State); false -> - Msg1 = Msg#mqtt_message{pktid = MsgId}, + Msg1 = Msg#mqtt_message{packet_id = MsgId}, deliver(Msg1, State), await(Msg1, next_msg_id(State)) end. @@ -719,12 +722,15 @@ deliver(Msg, #state{client_pid = Pid, binding = remote}) -> %% Awaiting ACK for QoS1/QoS2 Messages %%-------------------------------------------------------------------- -await(Msg = #mqtt_message{pktid = PacketId}, +await(Msg = #mqtt_message{packet_id = PacketId}, State = #state{inflight = Inflight, retry_timer = RetryTimer, retry_interval = Interval}) -> %% Start retry timer if the Inflight is still empty - State1 = ?IF(RetryTimer == undefined, State#state{retry_timer = start_timer(Interval, retry_delivery)}, State), + State1 = case RetryTimer == undefined of + true -> State#state{retry_timer = start_timer(Interval, retry_delivery)}; + false -> State + end, State1#state{inflight = Inflight:insert(PacketId, {publish, Msg, os:timestamp()})}. acked(puback, PacketId, State = #state{client_id = ClientId, diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl index 6453bcba9..8a5fa8b2e 100644 --- a/src/emqx_session_sup.erl +++ b/src/emqx_session_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 5440bbded..30983d0b5 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_sm_helper.erl b/src/emqx_sm_helper.erl index 9f8970c08..6751fd54d 100644 --- a/src/emqx_sm_helper.erl +++ b/src/emqx_sm_helper.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_sm_helper). --author("Feng Lee "). - -behaviour(gen_server). -include("emqx.hrl"). diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index 1c9098715..feef64e2b 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index 7146beb96..1fbc7b245 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index cdcf03255..e115674f9 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -18,8 +18,6 @@ -behaviour(supervisor). --author("Feng Lee "). - -export([start_link/0, start_child/1, start_child/2, stop_child/1]). %% Supervisor callbacks diff --git a/src/emqx_sysmon_sup.erl b/src/emqx_sysmon_sup.erl index cfe6e7657..6bcbf76c8 100644 --- a/src/emqx_sysmon_sup.erl +++ b/src/emqx_sysmon_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_sysmon_sup). --author("Feng Lee "). - -behaviour(supervisor). %% API diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index 7f177e8d0..267786943 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_trace.erl b/src/emqx_trace.erl index 2cc4cd29e..99da331f7 100644 --- a/src/emqx_trace.erl +++ b/src/emqx_trace.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -18,8 +18,6 @@ -behaviour(gen_server). --author("Feng Lee "). - %% API Function Exports -export([start_link/0]). diff --git a/src/emqx_trace_sup.erl b/src/emqx_trace_sup.erl index 89557ba45..861b3ef79 100644 --- a/src/emqx_trace_sup.erl +++ b/src/emqx_trace_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_trace_sup). --author("Feng Lee "). - -behaviour(supervisor). %% API diff --git a/src/emqx_vm.erl b/src/emqx_vm.erl index a421ab54a..32959cc0c 100644 --- a/src/emqx_vm.erl +++ b/src/emqx_vm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl index 3ff2551bb..e854bd1cd 100644 --- a/src/emqx_ws.erl +++ b/src/emqx_ws.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_ws). --author("Feng Lee "). - -include("emqx_mqtt.hrl"). -import(proplists, [get_value/3]). diff --git a/test/emqx_lib_SUITE.erl b/test/emqx_lib_SUITE.erl index 398d6995f..0b24fb059 100644 --- a/test/emqx_lib_SUITE.erl +++ b/test/emqx_lib_SUITE.erl @@ -16,8 +16,6 @@ -module(emqx_lib_SUITE). --author("Feng Lee "). - -include_lib("eunit/include/eunit.hrl"). -compile(export_all). diff --git a/test/emqx_mqueue_SUITE.erl b/test/emqx_mqueue_SUITE.erl index a3b085354..3123ab94f 100644 --- a/test/emqx_mqueue_SUITE.erl +++ b/test/emqx_mqueue_SUITE.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_mqueue_SUITE). --author("Feng Lee "). - -compile(export_all). -include("emqx.hrl"). diff --git a/test/emqx_topic_SUITE.erl b/test/emqx_topic_SUITE.erl index fb812b520..a60921ada 100644 --- a/test/emqx_topic_SUITE.erl +++ b/test/emqx_topic_SUITE.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. @@ -16,8 +16,6 @@ -module(emqx_topic_SUITE). --author("Feng Lee "). - -include_lib("eunit/include/eunit.hrl"). %% CT From b144363f1e0759d8f9950e66ee316ed90357c5b7 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 3 Mar 2018 09:07:45 +0800 Subject: [PATCH 026/520] Depends on jsx 2.9.0 and gproc 0.7.0 --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7084cadaa..ac18f6fe4 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,8 @@ NO_AUTOPATCH = gen_rpc cuttlefish DEPS = goldrush gproc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx dep_goldrush = git https://github.com/basho/goldrush 0.1.9 -dep_gproc = git https://github.com/uwiger/gproc -dep_jsx = git https://github.com/talentdeficit/jsx +dep_gproc = git https://github.com/uwiger/gproc 0.7.0 +dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_getopt = git https://github.com/jcomellas/getopt v0.8.2 dep_lager = git https://github.com/basho/lager master dep_lager_syslog = git https://github.com/basho/lager_syslog From 22350f9117effb9bfbed64901293e62db6209edd Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 9 Mar 2018 13:26:46 +0800 Subject: [PATCH 027/520] Depends on canel-lock library --- Makefile | 4 +++- src/{emqx_conn.erl => emqx_connection.erl} | 0 src/{emqx_ws_conn.erl => emqx_ws_connection.erl} | 0 src/{emqx_ws_conn_sup.erl => emqx_ws_connection_sup.erl} | 0 4 files changed, 3 insertions(+), 1 deletion(-) rename src/{emqx_conn.erl => emqx_connection.erl} (100%) rename src/{emqx_ws_conn.erl => emqx_ws_connection.erl} (100%) rename src/{emqx_ws_conn_sup.erl => emqx_ws_connection_sup.erl} (100%) diff --git a/Makefile b/Makefile index ac18f6fe4..0718648f8 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PROJECT_VERSION = 3.0 NO_AUTOPATCH = gen_rpc cuttlefish -DEPS = goldrush gproc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx +DEPS = goldrush gproc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx canal_lock dep_goldrush = git https://github.com/basho/goldrush 0.1.9 dep_gproc = git https://github.com/uwiger/gproc 0.7.0 @@ -20,6 +20,8 @@ dep_mochiweb = git https://github.com/emqtt/mochiweb v4.2.2 dep_pbkdf2 = git https://github.com/emqtt/pbkdf2 2.0.1 dep_bcrypt = git https://github.com/smarkets/erlang-bcrypt master dep_clique = git https://github.com/emqtt/clique +dep_clique = git https://github.com/emqtt/clique +dep_canal_lock = git https://github.com/emqx/canal-lock ERLC_OPTS += +debug_info ERLC_OPTS += +'{parse_transform, lager_transform}' diff --git a/src/emqx_conn.erl b/src/emqx_connection.erl similarity index 100% rename from src/emqx_conn.erl rename to src/emqx_connection.erl diff --git a/src/emqx_ws_conn.erl b/src/emqx_ws_connection.erl similarity index 100% rename from src/emqx_ws_conn.erl rename to src/emqx_ws_connection.erl diff --git a/src/emqx_ws_conn_sup.erl b/src/emqx_ws_connection_sup.erl similarity index 100% rename from src/emqx_ws_conn_sup.erl rename to src/emqx_ws_connection_sup.erl From f218c6a35d0a593c8f211fd738d843f478e2b269 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 10 Mar 2018 13:28:14 +0800 Subject: [PATCH 028/520] Add a locker module --- src/emqx_locker.erl | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/emqx_locker.erl diff --git a/src/emqx_locker.erl b/src/emqx_locker.erl new file mode 100644 index 000000000..8d85b0c89 --- /dev/null +++ b/src/emqx_locker.erl @@ -0,0 +1,41 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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_locker). + +-export([start_link/0]). + +%% Lock/Unlock API based on canal-lock. +-export([lock/1, unlock/1]). + +%% @doc Starts the lock server +-spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +start_link() -> + canal_lock:start_link(?MODULE, 1). + +%% @doc Lock a Key +-spec(lock(binary()) -> boolean()). +lock(Key) -> + case canal_lock:acquire(?MODULE, Key, 1, 1) of + {acquired, 1} -> true; + full -> false + end. + +%% @doc Unlock a Key +-spec(unlock(binary()) -> ok). +unlock(Key) -> + canal_lock:release(?MODULE, Key, 1, 1). + From 282e341433bff1bcf47d1614b6a3d857b21ded3c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 21 Mar 2018 16:48:52 +0800 Subject: [PATCH 029/520] EMQ X R3.0 - Improve the design of pubsub and router --- include/emqx.hrl | 32 +- src/emqx.erl | 99 +++-- src/emqx_access_control.erl | 2 +- src/emqx_access_rule.erl | 25 +- src/emqx_acl_internal.erl | 2 +- src/emqx_acl_mod.erl | 2 +- src/emqx_alarm.erl | 2 +- src/emqx_app.erl | 2 +- src/emqx_auth_mod.erl | 2 +- src/emqx_base62.erl | 2 +- src/emqx_boot.erl | 2 +- src/emqx_bridge.erl | 5 +- src/emqx_bridge_sup.erl | 2 +- src/emqx_bridge_sup_sup.erl | 2 +- src/emqx_broker.erl | 409 ++++++++++++------ src/emqx_broker_helper.erl | 66 +++ ...mqx_pubsub_sup.erl => emqx_broker_sup.erl} | 88 ++-- src/emqx_cli.erl | 2 +- src/emqx_cm.erl | 2 +- src/emqx_cm_sup.erl | 2 +- src/emqx_config.erl | 2 +- src/emqx_connection.erl | 4 +- src/emqx_ctl.erl | 2 +- src/emqx_gc.erl | 2 +- src/emqx_gen_mod.erl | 2 +- src/emqx_guid.erl | 2 +- src/emqx_hooks.erl | 2 +- src/emqx_inflight.erl | 2 +- src/emqx_json.erl | 2 +- src/emqx_keepalive.erl | 2 +- src/emqx_lager_backend.erl | 2 +- src/emqx_locker.erl | 2 +- src/emqx_log.erl | 51 +++ src/emqx_message.erl | 2 +- src/emqx_metrics.erl | 2 +- src/emqx_misc.erl | 2 +- src/emqx_mod_presence.erl | 2 +- src/emqx_mod_rewrite.erl | 2 +- src/emqx_mod_subscription.erl | 2 +- src/emqx_mod_sup.erl | 2 +- src/emqx_modules.erl | 2 +- src/emqx_mqtt_app.erl | 2 +- src/emqx_mqtt_metrics.erl | 2 +- src/emqx_mqtt_props.erl | 2 +- src/emqx_mqtt_rscode.erl | 2 +- src/emqx_mqueue.erl | 2 +- src/emqx_net.erl | 2 +- src/emqx_packet.erl | 2 +- src/emqx_parser.erl | 2 +- src/emqx_plugins.erl | 2 +- src/emqx_pmon.erl | 2 +- src/emqx_pool_sup.erl | 2 +- src/emqx_pooler.erl | 2 +- src/emqx_protocol.erl | 4 +- src/emqx_pubsub.erl | 254 ----------- src/emqx_router.erl | 333 +++++++------- src/emqx_router_helper.erl | 158 +++++++ src/emqx_router_sup.erl | 21 +- src/emqx_rpc.erl | 2 +- src/emqx_server.erl | 326 -------------- src/emqx_session.erl | 10 +- src/emqx_session_sup.erl | 14 +- src/emqx_shared_pubsub.erl | 171 ++++++++ src/emqx_sm.erl | 53 ++- src/emqx_sm_helper.erl | 2 +- src/emqx_sm_locker.erl | 43 ++ src/emqx_sm_sup.erl | 2 +- src/emqx_stats.erl | 18 +- src/emqx_sup.erl | 10 +- src/emqx_sys.erl | 177 ++++++++ src/emqx_sysmon.erl | 2 +- src/emqx_sysmon_sup.erl | 2 +- src/emqx_time.erl | 2 +- src/emqx_topic.erl | 61 +-- src/emqx_trace.erl | 43 +- src/emqx_trace_sup.erl | 2 +- src/emqx_trie.erl | 32 +- src/emqx_vm.erl | 2 +- src/emqx_ws.erl | 2 +- src/emqx_ws_connection.erl | 4 +- src/emqx_ws_connection_sup.erl | 8 +- 81 files changed, 1457 insertions(+), 1168 deletions(-) create mode 100644 src/emqx_broker_helper.erl rename src/{emqx_pubsub_sup.erl => emqx_broker_sup.erl} (50%) create mode 100644 src/emqx_log.erl delete mode 100644 src/emqx_pubsub.erl create mode 100644 src/emqx_router_helper.erl delete mode 100644 src/emqx_server.erl create mode 100644 src/emqx_shared_pubsub.erl create mode 100644 src/emqx_sm_locker.erl create mode 100644 src/emqx_sys.erl diff --git a/include/emqx.hrl b/include/emqx.hrl index 386136a9c..c1b366b86 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -36,13 +36,41 @@ -define(SHARE, <<"$share/">>). %% Shared Topic +%%-------------------------------------------------------------------- +%% Client and Session +%%-------------------------------------------------------------------- + +-type(topic() :: binary()). + +-type(subscriber() :: pid() | binary() | {binary(), pid()}). + +-type(suboption() :: {qos, non_neg_integer()} | {share, {'$queue' | binary()}}). + +-type(client_id() :: binary() | atom()). + +-type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | atom()). + +-type(client() :: #{zone := atom(), + node := atom(), + clientid := client_id(), + protocol := protocol(), + connector => atom(), + peername => {inet:ip_address(), inet:port_number()}, + username => binary(), + atom() => term()}). + +-type(session() :: #{client_id := client_id(), + clean_start := boolean(), + expiry_interval := non_neg_integer()}). + + %%-------------------------------------------------------------------- %% Message and Delivery %%-------------------------------------------------------------------- -type(message_id() :: binary() | undefined). --type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | atom()). +%% -type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | atom()). -type(message_from() :: #{zone := atom(), node := atom(), @@ -148,7 +176,7 @@ %% Route %%-------------------------------------------------------------------- --record(route, { topic :: binary(), node :: node() }). +-record(route, { topic :: binary(), dest :: {binary(), node()} | node() }). -type(route() :: #route{}). diff --git a/src/emqx.erl b/src/emqx.erl index 73a750894..386fc222c 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -32,8 +32,11 @@ -export([subscribe/1, subscribe/2, subscribe/3, publish/1, unsubscribe/1, unsubscribe/2]). -%% PubSub Management API --export([setqos/3, topics/0, subscriptions/1, subscribers/1, subscribed/2]). +%% PubSub management API +-export([topics/0, subscriptions/1, subscribers/1, subscribed/2]). + +%% Get/Set suboptions +-export([getopts/2, setopts/3]). %% Hooks API -export([hook/4, hook/3, unhook/2, run_hooks/2, run_hooks/3]). @@ -46,21 +49,13 @@ -type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}). --type(subid() :: binary()). - --type(subscriber() :: pid() | subid() | {subid(), pid()}). - --type(suboption() :: local | {qos, non_neg_integer()} | {share, {'$queue' | binary()}}). - --export_type([subscriber/0, suboption/0]). - -define(APP, ?MODULE). %%-------------------------------------------------------------------- %% Bootstrap, environment, configuration, is_running... %%-------------------------------------------------------------------- -%% @doc Start emqx application. +%% @doc Start emqx application -spec(start() -> ok | {error, term()}). start() -> application:start(?APP). @@ -68,7 +63,7 @@ start() -> application:start(?APP). -spec(stop() -> ok | {error, term()}). stop() -> application:stop(?APP). -%% @doc Get Environment +%% @doc Get environment -spec(env(Key :: atom()) -> {ok, any()} | undefined). env(Key) -> application:get_env(?APP, Key). @@ -76,7 +71,7 @@ env(Key) -> application:get_env(?APP, Key). -spec(env(Key :: atom(), Default :: any()) -> undefined | any()). env(Key, Default) -> application:get_env(?APP, Key, Default). -%% @doc Is running? +%% @doc Is emqx running? -spec(is_running(node()) -> boolean()). is_running(Node) -> case rpc:call(Node, erlang, whereis, [?APP]) of @@ -110,12 +105,9 @@ start_listener({Proto, ListenOn, Opts}) when Proto == http; Proto == ws -> start_listener({Proto, ListenOn, Opts}) when Proto == https; Proto == wss -> {ok, _} = mochiweb:start_http('mqtt:wss', ListenOn, Opts, {emqx_ws, handle_request, []}). -% start_listener({Proto, ListenOn, Opts}) when Proto == api -> -% {ok, _} = mochiweb:start_http('mqtt:api', ListenOn, Opts, emqx_http:http_handler()). - start_listener(Proto, ListenOn, Opts) -> Env = lists:append(emqx:env(client, []), emqx:env(protocol, [])), - MFArgs = {emqx_client, start_link, [Env]}, + MFArgs = {emqx_connection, start_link, [Env]}, {ok, _} = esockd:open(Proto, ListenOn, merge_sockopts(Opts), MFArgs). listeners() -> @@ -169,58 +161,71 @@ merge_sockopts(Options) -> emqx_misc:merge_opts(Options, [{sockopts, SockOpts}]). %%-------------------------------------------------------------------- -%% PubSub APIs +%% PubSub API %%-------------------------------------------------------------------- -%% @doc Subscribe --spec(subscribe(iodata()) -> ok | {error, term()}). +-spec(subscribe(topic() | string()) -> ok | {error, term()}). subscribe(Topic) -> - emqx_server:subscribe(iolist_to_binary(Topic)). + emqx_broker:subscribe(iolist_to_binary(Topic)). --spec(subscribe(iodata(), subscriber()) -> ok | {error, term()}). +-spec(subscribe(topic() | iodata(), subscriber() | string()) -> ok | {error, term()}). subscribe(Topic, Subscriber) -> - emqx_server:subscribe(iolist_to_binary(Topic), Subscriber). + emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber)). --spec(subscribe(iodata(), subscriber(), [suboption()]) -> ok | {error, term()}). +-spec(subscribe(topic() | iodata(), subscriber() | string(), [suboption()]) -> ok | {error, term()}). subscribe(Topic, Subscriber, Options) -> - emqx_server:subscribe(iolist_to_binary(Topic), Subscriber, Options). + emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). -%% @doc Publish MQTT Message --spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore). +%% @doc Publish Message +-spec(publish(message()) -> {ok, delivery()} | ignore). publish(Msg) -> - emqx_server:publish(Msg). + emqx_broker:publish(Msg). -%% @doc Unsubscribe --spec(unsubscribe(iodata()) -> ok | {error, term()}). +-spec(unsubscribe(topic() | string()) -> ok | {error, term()}). unsubscribe(Topic) -> - emqx_server:unsubscribe(iolist_to_binary(Topic)). + emqx_broker:unsubscribe(iolist_to_binary(Topic)). --spec(unsubscribe(iodata(), subscriber()) -> ok | {error, term()}). +-spec(unsubscribe(topic() | string(), subscriber() | string()) -> ok | {error, term()}). unsubscribe(Topic, Subscriber) -> - emqx_server:unsubscribe(iolist_to_binary(Topic), Subscriber). + emqx_broker:unsubscribe(iolist_to_binary(Topic), list_to_subid(Subscriber)). %%-------------------------------------------------------------------- -%% PubSub Management API +%% PubSub management API %%-------------------------------------------------------------------- --spec(setqos(binary(), subscriber(), mqtt_qos()) -> ok). -setqos(Topic, Subscriber, Qos) -> - emqx_server:setqos(iolist_to_binary(Topic), Subscriber, Qos). +-spec(getopts(topic() | string(), subscriber()) -> [suboption()]). +getopts(Topic, Subscriber) -> + emqx_broker:getopts(iolist_to_binary(Topic), list_to_subid(Subscriber)). --spec(topics() -> [binary()]). +-spec(setopts(topic() | string(), subscriber(), [suboption()]) -> ok). +setopts(Topic, Subscriber, Options) when is_list(Options) -> + emqx_broker:setopts(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). + +-spec(topics() -> list(topic())). topics() -> emqx_router:topics(). --spec(subscribers(iodata()) -> list(subscriber())). +-spec(subscribers(topic() | string()) -> list(subscriber())). subscribers(Topic) -> - emqx_server:subscribers(iolist_to_binary(Topic)). + emqx_broker:subscribers(iolist_to_binary(Topic)). --spec(subscriptions(subscriber()) -> [{subscriber(), binary(), list(suboption())}]). +-spec(subscriptions(subscriber() | string()) -> [{topic(), list(suboption())}]). subscriptions(Subscriber) -> - emqx_server:subscriptions(Subscriber). + emqx_broker:subscriptions(Subscriber). --spec(subscribed(iodata(), subscriber()) -> boolean()). +-spec(subscribed(topic() | string(), subscriber()) -> boolean()). subscribed(Topic, Subscriber) -> - emqx_server:subscribed(iolist_to_binary(Topic), Subscriber). + emqx_broker:subscribed(iolist_to_binary(Topic), list_to_subid(Subscriber)). + +list_to_subid(SubId) when is_binary(SubId) -> + SubId; +list_to_subid(SubId) when is_list(SubId) -> + iolist_to_binary(SubId); +list_to_subid(SubPid) when is_pid(SubPid) -> + SubPid; +list_to_subid({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> + {SubId, SubPid}; +list_to_subid({SubId, SubPid}) when is_list(SubId), is_pid(SubPid) -> + {iolist_to_binary(SubId), SubPid}. %%-------------------------------------------------------------------- %% Hooks API @@ -257,7 +262,7 @@ shutdown() -> shutdown(normal). shutdown(Reason) -> - lager:error("EMQ shutdown for ~s", [Reason]), + emqx_log:error("EMQ shutdown for ~s", [Reason]), emqx_plugins:unload(), lists:foreach(fun application:stop/1, [emqx, ekka, mochiweb, esockd, gproc]). @@ -268,5 +273,5 @@ reboot() -> %% Debug %%-------------------------------------------------------------------- -dump() -> lists:append([emqx_server:dump(), emqx_router:dump()]). +dump() -> lists:append([emqx_broker:dump(), emqx_router:dump()]). diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 05d3cbacb..10aa68e91 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 9c1d3f4d1..1d54d0fef 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -25,8 +25,6 @@ -type(access() :: subscribe | publish | pubsub). --type(topic() :: binary()). - -type(rule() :: {allow, all} | {allow, who(), access(), list(topic())} | {deny, all} | @@ -42,7 +40,7 @@ compile({A, all}) when ?ALLOW_DENY(A) -> {A, all}; -compile({A, Who, Access, Topic}) when ?ALLOW_DENY(A) andalso is_binary(Topic) -> +compile({A, Who, Access, Topic}) when ?ALLOW_DENY(A), is_binary(Topic) -> {A, compile(who, Who), Access, [compile(topic, Topic)]}; compile({A, Who, Access, TopicFilters}) when ?ALLOW_DENY(A) -> @@ -70,7 +68,7 @@ compile(topic, {eq, Topic}) -> compile(topic, Topic) -> Words = emqx_topic:words(bin(Topic)), case 'pattern?'(Words) of - true -> {pattern, Words}; + true -> {pattern, Words}; false -> Words end. @@ -83,13 +81,14 @@ bin(L) when is_list(L) -> bin(B) when is_binary(B) -> B. -%% @doc Match Access Rule +%% @doc Match access rule -spec(match(mqtt_client(), topic(), rule()) -> {matched, allow} | {matched, deny} | nomatch). match(_Client, _Topic, {AllowDeny, all}) when (AllowDeny =:= allow) orelse (AllowDeny =:= deny) -> {matched, AllowDeny}; match(Client, Topic, {AllowDeny, Who, _PubSub, TopicFilters}) when (AllowDeny =:= allow) orelse (AllowDeny =:= deny) -> - case match_who(Client, Who) andalso match_topics(Client, Topic, TopicFilters) of + case match_who(Client, Who) + andalso match_topics(Client, Topic, TopicFilters) of true -> {matched, AllowDeny}; false -> nomatch end. @@ -123,15 +122,11 @@ match_topics(_Client, _Topic, []) -> false; match_topics(Client, Topic, [{pattern, PatternFilter}|Filters]) -> TopicFilter = feed_var(Client, PatternFilter), - case match_topic(emqx_topic:words(Topic), TopicFilter) of - true -> true; - false -> match_topics(Client, Topic, Filters) - end; + match_topic(emqx_topic:words(Topic), TopicFilter) + orelse match_topics(Client, Topic, Filters); match_topics(Client, Topic, [TopicFilter|Filters]) -> - case match_topic(emqx_topic:words(Topic), TopicFilter) of - true -> true; - false -> match_topics(Client, Topic, Filters) - end. + match_topic(emqx_topic:words(Topic), TopicFilter) + orelse match_topics(Client, Topic, Filters). match_topic(Topic, {eq, TopicFilter}) -> Topic =:= TopicFilter; diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index 2cd1e5acb..030af96c3 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_acl_mod.erl b/src/emqx_acl_mod.erl index 9da3396b4..40f72f898 100644 --- a/src/emqx_acl_mod.erl +++ b/src/emqx_acl_mod.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_alarm.erl b/src/emqx_alarm.erl index 1688e7bb2..83e957047 100644 --- a/src/emqx_alarm.erl +++ b/src/emqx_alarm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_app.erl b/src/emqx_app.erl index 48648d64e..00acf0d76 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_auth_mod.erl b/src/emqx_auth_mod.erl index 658eea9de..bc2057a06 100644 --- a/src/emqx_auth_mod.erl +++ b/src/emqx_auth_mod.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_base62.erl b/src/emqx_base62.erl index a3c00a979..a997912eb 100644 --- a/src/emqx_base62.erl +++ b/src/emqx_base62.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_boot.erl b/src/emqx_boot.erl index 8d103f3f0..26348b271 100644 --- a/src/emqx_boot.erl +++ b/src/emqx_boot.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index 2ef5a16a2..098b8a41a 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -70,7 +70,8 @@ init([Pool, Id, Node, Topic, Options]) -> true -> true = erlang:monitor_node(Node, true), Share = iolist_to_binary(["$bridge:", atom_to_list(Node), ":", Topic]), - emqx_server:subscribe(Topic, self(), [local, {share, Share}, {qos, ?QOS_0}]), + %% TODO:: local??? + emqx_broker:subscribe(Topic, self(), [local, {share, Share}, {qos, ?QOS_0}]), State = parse_opts(Options, #state{node = Node, subtopic = Topic}), MQueue = emqx_mqueue:new(qname(Node, Topic), [{max_len, State#state.max_queue_len}], diff --git a/src/emqx_bridge_sup.erl b/src/emqx_bridge_sup.erl index 5e0deed34..125c47124 100644 --- a/src/emqx_bridge_sup.erl +++ b/src/emqx_bridge_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_bridge_sup_sup.erl b/src/emqx_bridge_sup_sup.erl index 762a5e8a4..0f2d85212 100644 --- a/src/emqx_bridge_sup_sup.erl +++ b/src/emqx_bridge_sup_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 36b174404..067e2c6da 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -20,183 +20,328 @@ -include("emqx.hrl"). --include("emqx_internal.hrl"). +-export([start_link/2]). -%% API Function Exports --export([start_link/0]). +-export([subscribe/1, subscribe/2, subscribe/3, unsubscribe/1, unsubscribe/2]). -%% Event API --export([subscribe/1, notify/2]). +-export([publish/1, publish/2]). -%% Broker API --export([version/0, uptime/0, datetime/0, sysdescr/0, info/0]). +-export([subscriptions/1, subscribers/1, subscribed/2]). -%% Tick API --export([start_tick/1, stop_tick/1]). +-export([topics/0]). + +-export([getopts/2, setopts/3]). + +-export([dump/0]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {started_at, sys_interval, heartbeat, ticker, version, sysdescr}). +-record(state, {pool, id, subids :: map(), submon :: emqx_pmon:pmon()}). --define(APP, emqx). +-define(BROKER, ?MODULE). --define(SERVER, ?MODULE). - --define(BROKER_TAB, mqtt_broker). - -%% $SYS Topics of Broker --define(SYSTOP_BROKERS, [ - version, % Broker version - uptime, % Broker uptime - datetime, % Broker local datetime - sysdescr % Broker description -]). +-define(TIMEOUT, 120000). %%-------------------------------------------------------------------- -%% API +%% Start a broker %%-------------------------------------------------------------------- -%% @doc Start the broker --spec(start_link() -> {ok, pid()} | ignore | {error, term()}). -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). +-spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). +start_link(Pool, Id) -> + gen_server:start_link(?MODULE, [Pool, Id], [{hibernate_after, 1000}]). -%% @doc Subscribe broker event --spec(subscribe(EventType :: any()) -> ok). -subscribe(EventType) -> - gproc:reg({p, l, {broker, EventType}}). - -%% @doc Notify broker event --spec(notify(EventType :: any(), Event :: any()) -> ok). -notify(EventType, Event) -> - gproc:send({p, l, {broker, EventType}}, {notify, EventType, self(), Event}). +%%-------------------------------------------------------------------- +%% Sub/Unsub +%%-------------------------------------------------------------------- -%% @doc Get broker info --spec(info() -> list(tuple())). -info() -> - [{version, version()}, - {sysdescr, sysdescr()}, - {uptime, uptime()}, - {datetime, datetime()}]. +-spec(subscribe(topic()) -> ok | {error, term()}). +subscribe(Topic) when is_binary(Topic) -> + subscribe(Topic, self()). -%% @doc Get broker version --spec(version() -> string()). -version() -> - {ok, Version} = application:get_key(?APP, vsn), Version. +-spec(subscribe(topic(), subscriber()) -> ok | {error, term()}). +subscribe(Topic, Subscriber) when is_binary(Topic) -> + subscribe(Topic, Subscriber, []). -%% @doc Get broker description --spec(sysdescr() -> string()). -sysdescr() -> - {ok, Descr} = application:get_key(?APP, description), Descr. +-spec(subscribe(topic(), subscriber(), [suboption()]) -> ok | {error, term()}). +subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> + subscribe(Topic, Subscriber, Options, ?TIMEOUT). -%% @doc Get broker uptime --spec(uptime() -> string()). -uptime() -> gen_server:call(?SERVER, uptime). +-spec(subscribe(topic(), subscriber(), [suboption()], timeout()) + -> ok | {error, term()}). +subscribe(Topic, Subscriber, Options, Timeout) -> + {Topic1, Options1} = emqx_topic:parse(Topic, Options), + SubReq = {subscribe, Topic1, with_subpid(Subscriber), Options1}, + async_call(pick(Subscriber), SubReq, Timeout). -%% @doc Get broker datetime --spec(datetime() -> string()). -datetime() -> - {{Y, M, D}, {H, MM, S}} = calendar:local_time(), - lists:flatten( - io_lib:format( - "~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])). +-spec(unsubscribe(topic()) -> ok | {error, term()}). +unsubscribe(Topic) when is_binary(Topic) -> + unsubscribe(Topic, self()). -%% @doc Start a tick timer. -start_tick(Msg) -> - start_tick(emqx:env(broker_sys_interval, 60000), Msg). +-spec(unsubscribe(topic(), subscriber()) -> ok | {error, term()}). +unsubscribe(Topic, Subscriber) when is_binary(Topic) -> + unsubscribe(Topic, Subscriber, ?TIMEOUT). -start_tick(0, _Msg) -> - undefined; -start_tick(Interval, Msg) when Interval > 0 -> - {ok, TRef} = timer:send_interval(Interval, Msg), TRef. +-spec(unsubscribe(topic(), subscriber(), timeout()) + -> ok | {error, term()}). +unsubscribe(Topic, Subscriber, Timeout) -> + {Topic1, _} = emqx_topic:parse(Topic), + UnsubReq = {unsubscribe, Topic1, with_subpid(Subscriber)}, + async_call(pick(Subscriber), UnsubReq, Timeout). -%% @doc Stop tick timer -stop_tick(undefined) -> +%%-------------------------------------------------------------------- +%% Publish +%%-------------------------------------------------------------------- + +-spec(publish(message()) -> delivery() | stopped). +publish(Msg = #message{from = From}) -> + emqx_tracer:trace(publish, From, Msg), + case emqx_hooks:run('message.publish', [], Msg) of + {ok, Msg1 = #message{topic = Topic}} -> + publish(Topic, Msg1); + {stop, Msg1} -> + emqx_log:warning("Stop publishing: ~s", [emqx_message:format(Msg1)]), + stopped + end. + +publish(Topic, Msg) -> + route(emqx_router:match_routes(Topic), delivery(Msg)). + +route([], Delivery = #delivery{message = Msg}) -> + emqx_hooks:run('message.dropped', [undefined, Msg]), + dropped(Msg#message.topic), Delivery; + +route([{To, Node}], Delivery) when Node =:= node() -> + dispatch(To, Delivery); + +route([{To, Node}], Delivery = #delivery{flows = Flows}) when is_atom(Node) -> + forward(Node, To, Delivery#delivery{flows = [{route, Node, To}|Flows]}); + +route([{To, Group}], Delivery) when is_binary(Group) -> + emqx_shared_pubsub:dispatch(Group, To, Delivery); + +route(Routes, Delivery) -> + lists:foldl(fun(Route, Acc) -> route([Route], Acc) end, Delivery, Routes). + +%% @doc Forward message to another node. +forward(Node, To, Delivery) -> + case emqx_rpc:call(Node, ?BROKER, dispatch, [To, Delivery]) of + {badrpc, Reason} -> + emqx_log:error("[Broker] Failed to forward msg to ~s: ~s", [Node, Reason]), + Delivery; + Delivery1 -> Delivery1 + end. + +-spec(dispatch(topic(), delivery()) -> delivery()). +dispatch(Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> + case subscribers(Topic) of + [] -> + emqx_hooks:run('message.dropped', [undefined, Msg]), + dropped(Topic), Delivery; + [Sub] -> %% optimize? + dispatch(Sub, Topic, Msg), + Delivery#delivery{flows = [{dispatch, Topic, 1}|Flows]}; + Subscribers -> + Count = lists:foldl(fun(Sub, Acc) -> + dispatch(Sub, Topic, Msg), Acc + 1 + end, 0, Subscribers), + Delivery#delivery{flows = [{dispatch, Topic, Count}|Flows]} + end. + +dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> + SubPid ! {dispatch, Topic, Msg}; +dispatch({SubId, SubPid}, Topic, Msg) when is_binary(SubId), is_pid(SubPid) -> + SubPid ! {dispatch, Topic, Msg}; +dispatch(SubId, Topic, Msg) when is_binary(SubId) -> + emqx_sm:dispatch(SubId, Topic, Msg); +dispatch({share, _Group, _Sub}, _Topic, _Msg) -> + ignore. + +dropped(<<"$SYS/", _/binary>>) -> ok; -stop_tick(TRef) -> - timer:cancel(TRef). +dropped(_Topic) -> + emqx_metrics:inc('messages/dropped'). + +delivery(Msg) -> + #delivery{message = Msg, flows = []}. + +subscribers(Topic) -> + try ets:lookup_element(subscriber, Topic, 2) catch error:badarg -> [] end. + +subscriptions(Subscriber) -> + lists:map(fun({_, {share, _Group, Topic}}) -> + subscription(Topic, Subscriber); + ({_, Topic}) -> + subscription(Topic, Subscriber) + end, ets:lookup(subscription, Subscriber)). + +subscription(Topic, Subscriber) -> + {Topic, ets:lookup_element(suboption, {Topic, Subscriber}, 2)}. + +-spec(subscribed(topic(), subscriber()) -> boolean()). +subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> + ets:member(suboption, {Topic, SubPid}); +subscribed(Topic, SubId) when is_binary(Topic), is_binary(SubId) -> + length(ets:match_object(suboption, {{Topic, {SubId, '_'}}, '_'}, 1)) == 1; +subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), is_binary(SubId), is_pid(SubPid) -> + ets:member(suboption, {Topic, {SubId, SubPid}}). + +topics() -> emqx_router:topics(). + +getopts(Topic, Subscriber) when is_binary(Topic) -> + try ets:lookup_element(suboption, {Topic, Subscriber}, 2) catch error:badarg ->[] end. + +setopts(Topic, Subscriber, Opts) when is_binary(Topic), is_list(Opts) -> + gen_server:call(pick(Subscriber), {setopts, Topic, Subscriber, Opts}). + +with_subpid(SubPid) when is_pid(SubPid) -> + SubPid; +with_subpid(SubId) when is_binary(SubId) -> + {SubId, self()}; +with_subpid({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> + {SubId, SubPid}. + +async_call(Broker, Msg, Timeout) -> + From = {self(), Tag = make_ref()}, + ok = gen_server:cast(Broker, {From, Msg}), + receive + {Tag, Reply} -> Reply + after Timeout -> + {error, timeout} + end. + +pick(SubPid) when is_pid(SubPid) -> + gproc_pool:pick_worker(broker, SubPid); +pick(SubId) when is_binary(SubId) -> + gproc_pool:pick_worker(broker, SubId); +pick({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> + pick(SubId). + +dump() -> + [{Tab, ets:tab2list(Tab)} || Tab <- [subscription, subscriber, suboption]]. %%-------------------------------------------------------------------- -%% gen_server Callbacks +%% gen_server callbacks %%-------------------------------------------------------------------- -init([]) -> - emqx_time:seed(), - ets:new(?BROKER_TAB, [set, public, named_table]), - % Tick - {ok, #state{started_at = os:timestamp(), - heartbeat = start_tick(1000, heartbeat), - version = list_to_binary(version()), - sysdescr = list_to_binary(sysdescr()), - ticker = start_tick(tick)}, hibernate}. +init([Pool, Id]) -> + gproc_pool:connect_worker(Pool, {Pool, Id}), + {ok, #state{pool = Pool, id = Id, subids = #{}, submon = emqx_pmon:new()}}. -handle_call(uptime, _From, State) -> - {reply, uptime(State), State}; +handle_call({setopts, Topic, Subscriber, Opts}, _From, State) -> + case ets:lookup(suboption, {Topic, Subscriber}) of + [{_, OldOpts}] -> + Opts1 = lists:usort(lists:umerge(Opts, OldOpts)), + ets:insert(suboption, {{Topic, Subscriber}, Opts1}), + {reply, ok, State}; + [] -> + {reply, {error, not_found}, State} + end; -handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). +handle_call(Request, _From, State) -> + emqx_log:error("[Broker] Unexpected request: ~p", [Request]), + {reply, ignore, State}. + +handle_cast({From, {subscribe, Topic, Subscriber, Options}}, State) -> + case ets:lookup(suboption, {Topic, Subscriber}) of + [] -> + Group = proplists:get_value(share, Options), + true = do_subscribe(Group, Topic, Subscriber, Options), + emqx_shared_pubsub:subscribe(Group, Topic, subpid(Subscriber)), + emqx_router:add_route(From, Topic, dest(Options)), + {noreply, monitor_subscriber(Subscriber, State)}; + [_] -> + gen_server:reply(From, ok), + {noreply, State} + end; + +handle_cast({From, {unsubscribe, Topic, Subscriber}}, State) -> + case ets:lookup(suboption, {Topic, Subscriber}) of + [{_, Options}] -> + Group = proplists:get_value(share, Options), + true = do_unsubscribe(Group, Topic, Subscriber), + emqx_shared_pubsub:unsubscribe(Group, Topic, subpid(Subscriber)), + case ets:member(subscriber, Topic) of + false -> emqx_router:del_route(From, Topic, dest(Options)); + true -> gen_server:reply(From, ok) + end; + [] -> gen_server:reply(From, ok) + end, + {noreply, State}; handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). + emqx_log:error("[Broker] Unexpected msg: ~p", [Msg]), + {noreply, State}. -handle_info(heartbeat, State) -> - publish(uptime, list_to_binary(uptime(State))), - publish(datetime, list_to_binary(datetime())), - {noreply, State, hibernate}; - -handle_info(tick, State = #state{version = Version, sysdescr = Descr}) -> - retain(brokers), - retain(version, Version), - retain(sysdescr, Descr), - {noreply, State, hibernate}; +handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{subids = SubIds}) -> + Subscriber = case maps:find(SubPid, SubIds) of + {ok, SubId} -> {SubId, SubPid}; + error -> SubPid + end, + Topics = lists:map(fun({_, {share, _, Topic}}) -> + Topic; + ({_, Topic}) -> + Topic + end, ets:lookup(subscription, Subscriber)), + lists:foreach(fun(Topic) -> + case ets:lookup(suboption, {Topic, Subscriber}) of + [{_, Options}] -> + Group = proplists:get_value(share, Options), + true = do_unsubscribe(Group, Topic, Subscriber), + case ets:member(subscriber, Topic) of + false -> emqx_router:del_route(Topic, dest(Options)); + true -> ok + end; + [] -> ok + end + end, Topics), + {noreply, demonitor_subscriber(SubPid, State)}; handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). + emqx_log:error("[Broker] Unexpected info: ~p", [Info]), + {noreply, State}. -terminate(_Reason, #state{heartbeat = Hb, ticker = TRef}) -> - stop_tick(Hb), - stop_tick(TRef), - ok. +terminate(_Reason, #state{pool = Pool, id = Id}) -> + gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- -%% Internal functions +%% Internal Functions %%-------------------------------------------------------------------- -retain(brokers) -> - Payload = list_to_binary(string:join([atom_to_list(N) || - N <- ekka_mnesia:running_nodes()], ",")), - Msg = emqx_message:make(broker, <<"$SYS/brokers">>, Payload), - emqx:publish(emqx_message:set_flag(sys, emqx_message:set_flag(retain, Msg))). +do_subscribe(Group, Topic, Subscriber, Options) -> + ets:insert(subscription, {Subscriber, shared(Group, Topic)}), + ets:insert(subscriber, {Topic, shared(Group, Subscriber)}), + ets:insert(suboption, {{Topic, Subscriber}, Options}). -retain(Topic, Payload) when is_binary(Payload) -> - Msg = emqx_message:make(broker, emqx_topic:systop(Topic), Payload), - emqx:publish(emqx_message:set_flag(sys, emqx_message:set_flag(retain, Msg))). +do_unsubscribe(Group, Topic, Subscriber) -> + ets:delete_object(subscription, {Subscriber, shared(Group, Topic)}), + ets:delete_object(subscriber, {Topic, shared(Group, Subscriber)}), + ets:delete(suboption, {Topic, Subscriber}). -publish(Topic, Payload) when is_binary(Payload) -> - Msg = emqx_message:make(broker, emqx_topic:systop(Topic), Payload), - emqx:publish(emqx_message:set_flag(sys, Msg)). +monitor_subscriber(SubPid, State = #state{submon = SubMon}) when is_pid(SubPid) -> + State#state{submon = SubMon:monitor(SubPid)}; -uptime(#state{started_at = Ts}) -> - Secs = timer:now_diff(os:timestamp(), Ts) div 1000000, - lists:flatten(uptime(seconds, Secs)). +monitor_subscriber({SubId, SubPid}, State = #state{subids = SubIds, submon = SubMon}) -> + State#state{subids = maps:put(SubPid, SubId, SubIds), submon = SubMon:monitor(SubPid)}. -uptime(seconds, Secs) when Secs < 60 -> - [integer_to_list(Secs), " seconds"]; -uptime(seconds, Secs) -> - [uptime(minutes, Secs div 60), integer_to_list(Secs rem 60), " seconds"]; -uptime(minutes, M) when M < 60 -> - [integer_to_list(M), " minutes, "]; -uptime(minutes, M) -> - [uptime(hours, M div 60), integer_to_list(M rem 60), " minutes, "]; -uptime(hours, H) when H < 24 -> - [integer_to_list(H), " hours, "]; -uptime(hours, H) -> - [uptime(days, H div 24), integer_to_list(H rem 24), " hours, "]; -uptime(days, D) -> - [integer_to_list(D), " days,"]. +demonitor_subscriber(SubPid, State = #state{subids = SubIds, submon = SubMon}) -> + State#state{subids = maps:remove(SubPid, SubIds), submon = SubMon:demonitor(SubPid)}. + +dest(Options) -> + case proplists:get_value(share, Options) of + undefined -> node(); + Group -> {Group, node()} + end. + +subpid(SubPid) when is_pid(SubPid) -> + SubPid; +subpid({_SubId, SubPid}) when is_pid(SubPid) -> + SubPid. + +shared(undefined, Name) -> Name; +shared(Group, Name) -> {share, Group, Name}. diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl new file mode 100644 index 000000000..ec40e6cff --- /dev/null +++ b/src/emqx_broker_helper.erl @@ -0,0 +1,66 @@ +%%-------------------------------------------------------------------- +%% Copyright © 2013-2018 EMQ Inc. 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_broker_helper). + +-behaviour(gen_server). + +-export([start_link/1]). + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-define(SERVER, ?MODULE). + +-record(state, {stats_fun, stats_timer}). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +-spec(start_link(fun()) -> {ok, pid()} | ignore | {error, any()}). +start_link(StatsFun) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [StatsFun], []). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([StatsFun]) -> + {ok, TRef} = timer:send_interval(1000, stats), + {ok, #state{stats_fun = StatsFun, stats_timer = TRef}}. + +handle_call(Req, _From, State) -> + emqx_log:error("[BrokerHelper] Unexpected request: ~p", [Req]), + {reply, ignore, State}. + +handle_cast(Msg, State) -> + emqx_log:error("[BrokerHelper] Unexpected msg: ~p", [Msg]), + {noreply, State}. + +handle_info(stats, State = #state{stats_fun = StatsFun}) -> + StatsFun(), {noreply, State, hibernate}; + +handle_info(Info, State) -> + emqx_log:error("[BrokerHelper] Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #state{stats_timer = TRef}) -> + timer:cancel(TRef). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + diff --git a/src/emqx_pubsub_sup.erl b/src/emqx_broker_sup.erl similarity index 50% rename from src/emqx_pubsub_sup.erl rename to src/emqx_broker_sup.erl index a9978d011..96c004b49 100644 --- a/src/emqx_pubsub_sup.erl +++ b/src/emqx_broker_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -14,70 +14,80 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_pubsub_sup). +-module(emqx_broker_sup). -behaviour(supervisor). -%% API --export([start_link/0, pubsub_pool/0]). +-export([start_link/0]). -%% Supervisor callbacks -export([init/1]). -define(CONCURRENCY_OPTS, [{read_concurrency, true}, {write_concurrency, true}]). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). -pubsub_pool() -> - hd([Pid || {pubsub_pool, Pid, _, _} <- supervisor:which_children(?MODULE)]). - %%-------------------------------------------------------------------- -%% Supervisor Callbacks +%% Supervisor callbacks %%-------------------------------------------------------------------- init([]) -> - {ok, Env} = emqx:env(pubsub), - %% Create ETS Tables - [create_tab(Tab) || Tab <- [mqtt_subproperty, mqtt_subscriber, mqtt_subscription]], - {ok, { {one_for_all, 10, 3600}, [pool_sup(pubsub, Env), pool_sup(server, Env)]} }. + %% Create the pubsub tables + create_tabs(), + + %% Shared pubsub + Shared = {shared_pubsub, {emqx_shared_pubsub, start_link, []}, + permanent, 5000, worker, [emqx_shared_pubsub]}, + + %% Broker helper + Helper = {broker_helper, {emqx_broker_helper, start_link, [stats_fun()]}, + permanent, 5000, worker, [emqx_broker_helper]}, + + %% Broker pool + PoolArgs = [broker, hash, emqx_sys:schedulers() * 2, + {emqx_broker, start_link, []}], + + PoolSup = emqx_pool_sup:spec(broker_pool, PoolArgs), + + {ok, {{one_for_all, 0, 3600}, [Shared, Helper, PoolSup]}}. %%-------------------------------------------------------------------- -%% Pool +%% Create tables %%-------------------------------------------------------------------- -pool_size(Env) -> - Schedulers = erlang:system_info(schedulers), - proplists:get_value(pool_size, Env, Schedulers). +create_tabs() -> + lists:foreach(fun create_tab/1, [subscription, subscriber, suboption]). -pool_sup(Name, Env) -> - Pool = list_to_atom(atom_to_list(Name) ++ "_pool"), - Mod = list_to_atom("emqx_" ++ atom_to_list(Name)), - MFA = {Mod, start_link, [Env]}, - emqx_pool_sup:spec(Pool, [Name, hash, pool_size(Env), MFA]). +create_tab(suboption) -> + %% Suboption: {Topic, Sub} -> [{qos, 1}] + ensure_tab(suboption, [set | ?CONCURRENCY_OPTS]); -%%-------------------------------------------------------------------- -%% Create PubSub Tables -%%-------------------------------------------------------------------- - -create_tab(mqtt_subproperty) -> - %% Subproperty: {Topic, Sub} -> [{qos, 1}] - ensure_tab(mqtt_subproperty, [public, named_table, set | ?CONCURRENCY_OPTS]); - -create_tab(mqtt_subscriber) -> +create_tab(subscriber) -> %% Subscriber: Topic -> Sub1, Sub2, Sub3, ..., SubN %% duplicate_bag: o(1) insert - ensure_tab(mqtt_subscriber, [public, named_table, duplicate_bag | ?CONCURRENCY_OPTS]); + ensure_tab(subscriber, [duplicate_bag | ?CONCURRENCY_OPTS]); -create_tab(mqtt_subscription) -> +create_tab(subscription) -> %% Subscription: Sub -> Topic1, Topic2, Topic3, ..., TopicN %% bag: o(n) insert - ensure_tab(mqtt_subscription, [public, named_table, bag | ?CONCURRENCY_OPTS]). + ensure_tab(subscription, [bag | ?CONCURRENCY_OPTS]). ensure_tab(Tab, Opts) -> - case ets:info(Tab, name) of undefined -> ets:new(Tab, Opts); _ -> ok end. + case ets:info(Tab, name) of + undefined -> + ets:new(Tab, lists:usort([public, named_table | Opts])); + Tab -> Tab + end. + +%%-------------------------------------------------------------------- +%% Stats function +%%-------------------------------------------------------------------- + +stats_fun() -> + fun() -> + emqx_stats:setstat('subscribers/count', 'subscribers/max', + ets:info(subscriber, size)), + emqx_stats:setstat('subscriptions/count', 'subscriptions/max', + ets:info(subscription, size)) + end. diff --git a/src/emqx_cli.erl b/src/emqx_cli.erl index 97a5ca298..8ba645122 100644 --- a/src/emqx_cli.erl +++ b/src/emqx_cli.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 2c186571b..c929ecb1f 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index bbaf0d855..b73b2f5f5 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_config.erl b/src/emqx_config.erl index c9d71feef..15a3a014a 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index bb80cb0fa..2b2f36dee 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_conn). +-module(emqx_connection). -behaviour(gen_server). diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index ee1616b52..b39d4ea83 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 59d7ff8cc..2bc9f75c9 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_gen_mod.erl b/src/emqx_gen_mod.erl index 9b3e0ee3c..54996dead 100644 --- a/src/emqx_gen_mod.erl +++ b/src/emqx_gen_mod.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_guid.erl b/src/emqx_guid.erl index e03baab4d..543233887 100644 --- a/src/emqx_guid.erl +++ b/src/emqx_guid.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index 530bf0ad3..369df3f41 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_inflight.erl b/src/emqx_inflight.erl index b3ea24841..718e1dc79 100644 --- a/src/emqx_inflight.erl +++ b/src/emqx_inflight.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_json.erl b/src/emqx_json.erl index 6b204474f..b3f426113 100644 --- a/src/emqx_json.erl +++ b/src/emqx_json.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_keepalive.erl b/src/emqx_keepalive.erl index 7cfc4f48d..45158d709 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_lager_backend.erl b/src/emqx_lager_backend.erl index c675481c5..735b71467 100644 --- a/src/emqx_lager_backend.erl +++ b/src/emqx_lager_backend.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_locker.erl b/src/emqx_locker.erl index 8d85b0c89..196a3aa69 100644 --- a/src/emqx_locker.erl +++ b/src/emqx_locker.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_log.erl b/src/emqx_log.erl new file mode 100644 index 000000000..50f85c562 --- /dev/null +++ b/src/emqx_log.erl @@ -0,0 +1,51 @@ +%%-------------------------------------------------------------------- +%% Copyright © 2013-2018 EMQ Inc. 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_log). + +-compile({no_auto_import,[error/1]}). + +-export([debug/1, debug/2, + info/1, info/2, + warning/1, warning/2, + error/1, error/2, + critical/1, critical/2]). + +debug(Msg) -> + lager:debug(Msg). +debug(Format, Args) -> + lager:debug(Format, Args). + +info(Msg) -> + lager:info(Msg). +info(Format, Args) -> + lager:info(Format, Args). + +warning(Msg) -> + lager:warning(Msg). +warning(Format, Args) -> + lager:warning(Format, Args). + +error(Msg) -> + lager:error(Msg). +error(Format, Args) -> + lager:error(Format, Args). + +critical(Msg) -> + lager:critical(Msg). +critical(Format, Args) -> + lager:critical(Format, Args). + diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 2574418a9..acbee1ce7 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index cb477aaa9..f74537218 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index d10eb6415..bccdbbdf5 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index a0d8228d5..559556443 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index 81480f212..87e70021e 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index 7230a6dd2..b6e3726e5 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mod_sup.erl b/src/emqx_mod_sup.erl index 016708351..f3e24cf86 100644 --- a/src/emqx_mod_sup.erl +++ b/src/emqx_mod_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_modules.erl b/src/emqx_modules.erl index b3b8293f6..4974abf38 100644 --- a/src/emqx_modules.erl +++ b/src/emqx_modules.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mqtt_app.erl b/src/emqx_mqtt_app.erl index e947855e1..47e1eaba0 100644 --- a/src/emqx_mqtt_app.erl +++ b/src/emqx_mqtt_app.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mqtt_metrics.erl b/src/emqx_mqtt_metrics.erl index c9be6bdfc..05b9adcff 100644 --- a/src/emqx_mqtt_metrics.erl +++ b/src/emqx_mqtt_metrics.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mqtt_props.erl b/src/emqx_mqtt_props.erl index 6001b5211..d928b8ad1 100644 --- a/src/emqx_mqtt_props.erl +++ b/src/emqx_mqtt_props.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mqtt_rscode.erl b/src/emqx_mqtt_rscode.erl index b5658c17f..47450a5d3 100644 --- a/src/emqx_mqtt_rscode.erl +++ b/src/emqx_mqtt_rscode.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index c139a6bdd..eb334ed22 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_net.erl b/src/emqx_net.erl index ee186ee5a..45e05deda 100644 --- a/src/emqx_net.erl +++ b/src/emqx_net.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 39681f27f..3762aff7e 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_parser.erl b/src/emqx_parser.erl index a65b70894..f23b7e727 100644 --- a/src/emqx_parser.erl +++ b/src/emqx_parser.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index 0ec84245f..e9d980ca3 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index e65f34c6c..cd66414fc 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_pool_sup.erl b/src/emqx_pool_sup.erl index 45af0b81b..47bb4eaf0 100644 --- a/src/emqx_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_pooler.erl b/src/emqx_pooler.erl index 71af6b279..943c0642f 100644 --- a/src/emqx_pooler.erl +++ b/src/emqx_pooler.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index c2ab59be3..103fbf4ff 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -436,7 +436,7 @@ maybe_set_clientid(State) -> send_willmsg(_Client, undefined) -> ignore; send_willmsg(#mqtt_client{client_id = ClientId, username = Username}, WillMsg) -> - emqx_server:publish(WillMsg#mqtt_message{from = {ClientId, Username}}). + emqx_broker:publish(WillMsg#mqtt_message{from = {ClientId, Username}}). start_keepalive(0, _State) -> ignore; diff --git a/src/emqx_pubsub.erl b/src/emqx_pubsub.erl deleted file mode 100644 index 9099afc83..000000000 --- a/src/emqx_pubsub.erl +++ /dev/null @@ -1,254 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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_pubsub). - --behaviour(gen_server). - --include("emqx.hrl"). - --include("emqx_mqtt.hrl"). - --export([start_link/3]). - -%% PubSub API. --export([subscribe/3, async_subscribe/3, publish/2, unsubscribe/3, - async_unsubscribe/3, subscribers/1]). - --export([dispatch/2]). - -%% gen_server Callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {pool, id, env}). - --define(PUBSUB, ?MODULE). - --define(is_local(Options), lists:member(local, Options)). - -%%-------------------------------------------------------------------- -%% Start PubSub -%%-------------------------------------------------------------------- - --spec(start_link(atom(), pos_integer(), list()) -> {ok, pid()} | ignore | {error, term()}). -start_link(Pool, Id, Env) -> - gen_server:start_link(?MODULE, [Pool, Id, Env], [{hibernate_after, 10000}]). - -%%-------------------------------------------------------------------- -%% PubSub API -%%-------------------------------------------------------------------- - -%% @doc Subscribe to a Topic --spec(subscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> ok). -subscribe(Topic, Subscriber, Options) -> - call(pick(Topic), {subscribe, Topic, Subscriber, Options}). - --spec(async_subscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> ok). -async_subscribe(Topic, Subscriber, Options) -> - cast(pick(Topic), {subscribe, Topic, Subscriber, Options}). - -%% @doc Publish MQTT Message to a Topic --spec(publish(binary(), mqtt_message()) -> {ok, mqtt_delivery()} | ignore). -publish(Topic, Msg) -> - route(lists:append(emqx_router:match(Topic), - emqx_router:match_local(Topic)), delivery(Msg)). - -route([], #mqtt_delivery{message = Msg}) -> - emqx_hooks:run('message.dropped', [undefined, Msg]), - dropped(Msg#mqtt_message.topic), ignore; - -%% Dispatch on the local node. -route([#route{topic = To, node = Node}], - Delivery = #mqtt_delivery{flows = Flows}) when Node =:= node() -> - dispatch(To, Delivery#mqtt_delivery{flows = [{route, Node, To} | Flows]}); - -%% Forward to other nodes -route([#route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) -> - forward(Node, To, Delivery#mqtt_delivery{flows = [{route, Node, To}|Flows]}); - -route(Routes, Delivery) -> - {ok, lists:foldl(fun(Route, Acc) -> - {ok, Acc1} = route([Route], Acc), Acc1 - end, Delivery, Routes)}. - -delivery(Msg) -> #mqtt_delivery{sender = self(), message = Msg, flows = []}. - -%% @doc Forward message to another node... -forward(Node, To, Delivery) -> - emqx_rpc:cast(Node, ?PUBSUB, dispatch, [To, Delivery]), {ok, Delivery}. - -%% @doc Dispatch Message to Subscribers. --spec(dispatch(binary(), mqtt_delivery()) -> mqtt_delivery()). -dispatch(Topic, Delivery = #mqtt_delivery{message = Msg, flows = Flows}) -> - case subscribers(Topic) of - [] -> - emqx_hooks:run('message.dropped', [undefined, Msg]), - dropped(Topic), {ok, Delivery}; - [Sub] -> %% optimize? - dispatch(Sub, Topic, Msg), - {ok, Delivery#mqtt_delivery{flows = [{dispatch, Topic, 1}|Flows]}}; - Subscribers -> - Flows1 = [{dispatch, Topic, length(Subscribers)} | Flows], - lists:foreach(fun(Sub) -> dispatch(Sub, Topic, Msg) end, Subscribers), - {ok, Delivery#mqtt_delivery{flows = Flows1}} - end. - -%%TODO: Is SubPid aliving??? -dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> - SubPid ! {dispatch, Topic, Msg}; -dispatch({SubId, SubPid}, Topic, Msg) when is_binary(SubId), is_pid(SubPid) -> - SubPid ! {dispatch, Topic, Msg}; -dispatch({{share, _Share}, [Sub]}, Topic, Msg) -> - dispatch(Sub, Topic, Msg); -dispatch({{share, _Share}, []}, _Topic, _Msg) -> - ok; -dispatch({{share, _Share}, Subs}, Topic, Msg) -> %% round-robbin? - dispatch(lists:nth(rand:uniform(length(Subs)), Subs), Topic, Msg). - -subscribers(Topic) -> - group_by_share(try ets:lookup_element(mqtt_subscriber, Topic, 2) catch error:badarg -> [] end). - -group_by_share([]) -> []; - -group_by_share(Subscribers) -> - {Subs1, Shares1} = - lists:foldl(fun({share, Share, Sub}, {Subs, Shares}) -> - {Subs, dict:append({share, Share}, Sub, Shares)}; - (Sub, {Subs, Shares}) -> - {[Sub|Subs], Shares} - end, {[], dict:new()}, Subscribers), - lists:append(Subs1, dict:to_list(Shares1)). - -%% @private -%% @doc Ingore $SYS Messages. -dropped(<<"$SYS/", _/binary>>) -> - ok; -dropped(_Topic) -> - emqx_metrics:inc('messages/dropped'). - -%% @doc Unsubscribe --spec(unsubscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> ok). -unsubscribe(Topic, Subscriber, Options) -> - call(pick(Topic), {unsubscribe, Topic, Subscriber, Options}). - --spec(async_unsubscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> ok). -async_unsubscribe(Topic, Subscriber, Options) -> - cast(pick(Topic), {unsubscribe, Topic, Subscriber, Options}). - -call(PubSub, Req) when is_pid(PubSub) -> - gen_server:call(PubSub, Req, infinity). - -cast(PubSub, Msg) when is_pid(PubSub) -> - gen_server:cast(PubSub, Msg). - -pick(Topic) -> - gproc_pool:pick_worker(pubsub, Topic). - -%%-------------------------------------------------------------------- -%% gen_server Callbacks -%%-------------------------------------------------------------------- - -init([Pool, Id, Env]) -> - gproc_pool:connect_worker(Pool, {Pool, Id}), - {ok, #state{pool = Pool, id = Id, env = Env}, hibernate}. - -handle_call({subscribe, Topic, Subscriber, Options}, _From, State) -> - add_subscriber(Topic, Subscriber, Options), - reply(ok, setstats(State)); - -handle_call({unsubscribe, Topic, Subscriber, Options}, _From, State) -> - del_subscriber(Topic, Subscriber, Options), - reply(ok, setstats(State)); - -handle_call(Req, _From, State) -> - lager:error("[~s] Unexpected Call: ~p", [?MODULE, Req]), - {reply, ignore, State}. - -handle_cast({subscribe, Topic, Subscriber, Options}, State) -> - add_subscriber(Topic, Subscriber, Options), - noreply(setstats(State)); - -handle_cast({unsubscribe, Topic, Subscriber, Options}, State) -> - del_subscriber(Topic, Subscriber, Options), - noreply(setstats(State)); - -handle_cast(Msg, State) -> - lager:error("[~s] Unexpected Cast: ~p", [?MODULE, Msg]), - {noreply, State}. - -handle_info(Info, State) -> - lager:error("[~s] Unexpected Info: ~p", [?MODULE, Info]), - {noreply, State}. - -terminate(_Reason, #state{pool = Pool, id = Id}) -> - gproc_pool:disconnect_worker(Pool, {Pool, Id}). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internel Functions -%%-------------------------------------------------------------------- - -add_subscriber(Topic, Subscriber, Options) -> - Share = proplists:get_value(share, Options), - case ?is_local(Options) of - false -> add_global_subscriber(Share, Topic, Subscriber); - true -> add_local_subscriber(Share, Topic, Subscriber) - end. - -add_global_subscriber(Share, Topic, Subscriber) -> - case ets:member(mqtt_subscriber, Topic) and emqx_router:has_route(Topic) of - true -> ok; - false -> emqx_router:add_route(Topic) - end, - ets:insert(mqtt_subscriber, {Topic, shared(Share, Subscriber)}). - -add_local_subscriber(Share, Topic, Subscriber) -> - (not ets:member(mqtt_subscriber, {local, Topic})) andalso emqx_router:add_local_route(Topic), - ets:insert(mqtt_subscriber, {{local, Topic}, shared(Share, Subscriber)}). - -del_subscriber(Topic, Subscriber, Options) -> - Share = proplists:get_value(share, Options), - case ?is_local(Options) of - false -> del_global_subscriber(Share, Topic, Subscriber); - true -> del_local_subscriber(Share, Topic, Subscriber) - end. - -del_global_subscriber(Share, Topic, Subscriber) -> - ets:delete_object(mqtt_subscriber, {Topic, shared(Share, Subscriber)}), - (not ets:member(mqtt_subscriber, Topic)) andalso emqx_router:del_route(Topic). - -del_local_subscriber(Share, Topic, Subscriber) -> - ets:delete_object(mqtt_subscriber, {{local, Topic}, shared(Share, Subscriber)}), - (not ets:member(mqtt_subscriber, {local, Topic})) andalso emqx_router:del_local_route(Topic). - -shared(undefined, Subscriber) -> - Subscriber; -shared(Share, Subscriber) -> - {share, Share, Subscriber}. - -setstats(State) -> - emqx_stats:setstats('subscribers/count', 'subscribers/max', ets:info(mqtt_subscriber, size)), - State. - -reply(Reply, State) -> - {reply, Reply, State}. - -noreply(State) -> - {noreply, State}. - diff --git a/src/emqx_router.erl b/src/emqx_router.erl index e46add4bf..3903dc005 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -26,35 +26,27 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). --export([start_link/1]). - -%% For eunit tests --export([start/0, stop/0]). +%% Start +-export([start_link/2]). %% Topics --export([topics/0, local_topics/0]). +-export([topics/0]). -%% Route APIs --export([add_route/1, get_routes/1, del_route/1, has_route/1]). +%% Route Management APIs +-export([add_route/2, add_route/3, get_routes/1, del_route/2, del_route/3]). -%% Match and print --export([match/1, print/1]). +%% Match, print routes +-export([has_routes/1, match_routes/1, print_routes/1]). -%% Local Route API --export([get_local_routes/0, add_local_route/1, match_local/1, - del_local_route/1, clean_local_routes/0]). +-export([dump/0]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --export([dump/0]). +-record(state, {pool, id}). --record(state, {stats_fun, stats_timer}). - --define(ROUTER, ?MODULE). - --define(LOCK, {?ROUTER, clean_routes}). +-type(destination() :: node() | {binary(), node()}). %%-------------------------------------------------------------------- %% Mnesia Bootstrap @@ -68,14 +60,46 @@ mnesia(boot) -> {attributes, record_info(fields, route)}]); mnesia(copy) -> - ok = ekka_mnesia:copy_table(route, ram_copies). + ok = ekka_mnesia:copy_table(route). %%-------------------------------------------------------------------- -%% Start the Router +%% Start a router %%-------------------------------------------------------------------- -start_link(StatsFun) -> - gen_server:start_link({local, ?ROUTER}, ?MODULE, [StatsFun], []). +start_link(Pool, Id) -> + gen_server:start_link(?MODULE, [Pool, Id], [{hibernate_after, 1000}]). + +%%-------------------------------------------------------------------- +%% Add/Del Routes +%%-------------------------------------------------------------------- + +%% @doc Add a route +-spec(add_route(topic(), destination()) -> ok). +add_route(Topic, Dest) when is_binary(Topic) -> + cast(pick(Topic), {add_route, #route{topic = Topic, dest = Dest}}). + +-spec(add_route({pid(), reference()}, topic(), destination()) -> ok). +add_route(From, Topic, Dest) when is_binary(Topic) -> + cast(pick(Topic), {add_route, From, #route{topic = Topic, dest = Dest}}). + +%% @doc Get routes +-spec(get_routes(topic()) -> [route()]). +get_routes(Topic) -> + ets:lookup(route, Topic). + +%% @doc Delete a route +-spec(del_route(topic(), destination()) -> ok). +del_route(Topic, Dest) when is_binary(Topic) -> + cast(pick(Topic), {del_route, #route{topic = Topic, dest = Dest}}). + +-spec(del_route({pid(), reference()}, topic(), destination()) -> ok). +del_route(From, Topic, Dest) when is_binary(Topic) -> + cast(pick(Topic), {del_route, From, #route{topic = Topic, dest = Dest}}). + +%% @doc Has routes? +-spec(has_routes(topic()) -> boolean()). +has_routes(Topic) when is_binary(Topic) -> + ets:member(route, Topic). %%-------------------------------------------------------------------- %% Topics @@ -85,44 +109,116 @@ start_link(StatsFun) -> topics() -> mnesia:dirty_all_keys(route). --spec(local_topics() -> list(binary())). -local_topics() -> - ets:select(local_route, [{{'$1', '_'}, [], ['$1']}]). - %%-------------------------------------------------------------------- -%% Match API +%% Match Routes %%-------------------------------------------------------------------- -%% @doc Match Routes. --spec(match(Topic:: binary()) -> [route()]). -match(Topic) when is_binary(Topic) -> +%% @doc Match routes +-spec(match_routes(Topic:: topic()) -> [{topic(), binary() | node()}]). +match_routes(Topic) when is_binary(Topic) -> %% Optimize: ets??? Matched = mnesia:ets(fun emqx_trie:match/1, [Topic]), %% Optimize: route table will be replicated to all nodes. - lists:append([ets:lookup(route, To) || To <- [Topic | Matched]]). + aggre(lists:append([ets:lookup(route, To) || To <- [Topic | Matched]])). -%% @doc Print Routes. --spec(print(Topic :: binary()) -> [ok]). -print(Topic) -> - [io:format("~s -> ~s~n", [To, Node]) || - #route{topic = To, node = Node} <- match(Topic)]. +%% Aggregate routes +aggre([]) -> + []; +aggre([#route{topic = To, dest = Node}]) when is_atom(Node) -> + [{To, Node}]; +aggre([#route{topic = To, dest = {Group, _Node}}]) -> + [{To, Group}]; +aggre(Routes) -> + lists:foldl( + fun(#route{topic = To, dest = Node}, Acc) when is_atom(Node) -> + [{To, Node} | Acc]; + (#route{topic = To, dest = {Group, _}}, Acc) -> + lists:usort([{To, Group} | Acc]) + end, [], Routes). %%-------------------------------------------------------------------- -%% Route Management API +%% Print Routes %%-------------------------------------------------------------------- -%% @doc Add Route. --spec(add_route(binary() | route()) -> ok | {error, Reason :: term()}). -add_route(Topic) when is_binary(Topic) -> - add_route(#route{topic = Topic, node = node()}); -add_route(Route = #route{topic = Topic}) -> - case emqx_topic:wildcard(Topic) of - true -> case mnesia:is_transaction() of - true -> add_trie_route(Route); - false -> trans(fun add_trie_route/1, [Route]) - end; - false -> add_direct_route(Route) - end. +%% @doc Print routes to a topic +-spec(print_routes(topic()) -> ok). +print_routes(Topic) -> + lists:foreach(fun({To, Dest}) -> + io:format("~s -> ~s~n", [To, Dest]) + end, match_routes(Topic)). + +cast(Router, Msg) -> + gen_server:cast(Router, Msg). + +pick(Topic) -> + gproc_pool:pick_worker(router, Topic). + +%%FIXME: OOM? +dump() -> + [{route, [{To, Dest} || #route{topic = To, dest = Dest} <- ets:tab2list(route)]}]. + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([Pool, Id]) -> + gproc_pool:connect_worker(Pool, {Pool, Id}), + {ok, #state{pool = Pool, id = Id}}. + +handle_call(Req, _From, State) -> + emqx_log:error("[Router] Unexpected request: ~p", [Req]), + {reply, ignore, State}. + +handle_cast({add_route, From, Route}, State) -> + _ = handle_cast({add_route, Route}, State), + gen_server:reply(From, ok), + {noreply, State}; + +handle_cast({add_route, Route = #route{topic = Topic, dest = Dest}}, State) -> + case lists:member(Route, ets:lookup(route, Topic)) of + true -> ok; + false -> + ok = emqx_router_helper:monitor(Dest), + case emqx_topic:wildcard(Topic) of + true -> trans(fun add_trie_route/1, [Route]); + false -> add_direct_route(Route) + end + end, + {noreply, State}; + +handle_cast({del_route, From, Route}, State) -> + _ = handle_cast({del_route, Route}, State), + gen_server:reply(From, ok), + {noreply, State}; + +handle_cast({del_route, Route = #route{topic = Topic}}, State) -> + %% Confirm if there are still subscribers... + case ets:member(subscriber, Topic) of + true -> ok; + false -> + case emqx_topic:wildcard(Topic) of + true -> trans(fun del_trie_route/1, [Route]); + false -> del_direct_route(Route) + end + end, + {noreply, State}; + +handle_cast(Msg, State) -> + emqx_log:error("[Router] Unexpected msg: ~p", [Msg]), + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, #state{pool = Pool, id = Id}) -> + gproc_pool:disconnect_worker(Pool, {Pool, Id}). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal Functions +%%-------------------------------------------------------------------- add_direct_route(Route) -> mnesia:async_dirty(fun mnesia:write/1, [Route]). @@ -134,24 +230,6 @@ add_trie_route(Route = #route{topic = Topic}) -> end, mnesia:write(Route). -%% @doc Lookup Routes --spec(get_routes(binary()) -> [route()]). -get_routes(Topic) -> - ets:lookup(route, Topic). - -%% @doc Delete Route --spec(del_route(binary() | route()) -> ok | {error, Reason :: term()}). -del_route(Topic) when is_binary(Topic) -> - del_route(#route{topic = Topic, node = node()}); -del_route(Route = #route{topic = Topic}) -> - case emqx_topic:wildcard(Topic) of - true -> case mnesia:is_transaction() of - true -> del_trie_route(Route); - false -> trans(fun del_trie_route/1, [Route]) - end; - false -> del_direct_route(Route) - end. - del_direct_route(Route) -> mnesia:async_dirty(fun mnesia:delete_object/1, [Route]). @@ -165,126 +243,13 @@ del_trie_route(Route = #route{topic = Topic}) -> [] -> ok end. -%% @doc Has route? --spec(has_route(binary()) -> boolean()). -has_route(Topic) when is_binary(Topic) -> - ets:member(route, Topic). - %% @private -spec(trans(function(), list(any())) -> ok | {error, term()}). trans(Fun, Args) -> case mnesia:transaction(Fun, Args) of {atomic, _} -> ok; - {aborted, Error} -> {error, Error} + {aborted, Error} -> + emqx_log:error("[Router] Mnesia aborted: ~p", [Error]), + {error, Error} end. -%%-------------------------------------------------------------------- -%% Local Route API -%%-------------------------------------------------------------------- - --spec(get_local_routes() -> list({binary(), node()})). -get_local_routes() -> - ets:tab2list(local_route). - --spec(add_local_route(binary()) -> ok). -add_local_route(Topic) -> - gen_server:call(?ROUTER, {add_local_route, Topic}). - --spec(del_local_route(binary()) -> ok). -del_local_route(Topic) -> - gen_server:call(?ROUTER, {del_local_route, Topic}). - --spec(match_local(binary()) -> [route()]). -match_local(Name) -> - case ets:info(local_route, size) of - 0 -> []; - _ -> ets:foldl( - fun({Filter, Node}, Matched) -> - case emqx_topic:match(Name, Filter) of - true -> [#route{topic = {local, Filter}, node = Node} | Matched]; - false -> Matched - end - end, [], local_route) - end. - --spec(clean_local_routes() -> ok). -clean_local_routes() -> - gen_server:call(?ROUTER, clean_local_routes). - -dump() -> - [{route, ets:tab2list(route)}, {local_route, ets:tab2list(local_route)}]. - -%% For unit test. -start() -> - gen_server:start({local, ?ROUTER}, ?MODULE, [], []). - -stop() -> - gen_server:call(?ROUTER, stop). - -%%-------------------------------------------------------------------- -%% gen_server Callbacks -%%-------------------------------------------------------------------- - -init([StatsFun]) -> - ekka:monitor(membership), - ets:new(local_route, [set, named_table, protected]), - {ok, TRef} = timer:send_interval(timer:seconds(1), stats), - {ok, #state{stats_fun = StatsFun, stats_timer = TRef}}. - -handle_call({add_local_route, Topic}, _From, State) -> - %% why node()...? - ets:insert(local_route, {Topic, node()}), - {reply, ok, State}; - -handle_call({del_local_route, Topic}, _From, State) -> - ets:delete(local_route, Topic), - {reply, ok, State}; - -handle_call(clean_local_routes, _From, State) -> - ets:delete_all_objects(local_route), - {reply, ok, State}; - -handle_call(stop, _From, State) -> - {stop, normal, ok, State}; - -handle_call(_Req, _From, State) -> - {reply, ignore, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info({membership, {mnesia, down, Node}}, State) -> - global:trans({?LOCK, self()}, fun() -> clean_routes_(Node) end), - handle_info(stats, State); - -handle_info({membership, _Event}, State) -> - %% ignore - {noreply, State}; - -handle_info(stats, State = #state{stats_fun = StatsFun}) -> - StatsFun(mnesia:table_info(route, size)), - {noreply, State, hibernate}; - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, #state{stats_timer = TRef}) -> - timer:cancel(TRef), - ekka:unmonitor(membership). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internal Functions -%%-------------------------------------------------------------------- - -%% Clean routes on the down node. -clean_routes_(Node) -> - Pattern = #route{_ = '_', node = Node}, - Clean = fun() -> - [mnesia:delete_object(route, R, write) || - R <- mnesia:match_object(route, Pattern, write)] - end, - mnesia:transaction(Clean). - diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl new file mode 100644 index 000000000..97ba1811f --- /dev/null +++ b/src/emqx_router_helper.erl @@ -0,0 +1,158 @@ +%%-------------------------------------------------------------------- +%% Copyright © 2013-2018 EMQ Inc. 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_router_helper). + +-behaviour(gen_server). + +-include("emqx.hrl"). + +%% Mnesia Bootstrap +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +%% API +-export([start_link/1, monitor/1]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(routing_node, {name, ts}). + +-record(state, {nodes = [], stats_fun, stats_timer}). + +-compile({no_auto_import, [monitor/1]}). + +-define(SERVER, ?MODULE). + +-define(TABLE, routing_node). + +-define(LOCK, {?MODULE, clean_routes}). + +%%-------------------------------------------------------------------- +%% Mnesia bootstrap +%%-------------------------------------------------------------------- + +mnesia(boot) -> + ok = ekka_mnesia:create_table(?TABLE, [ + {type, set}, + {ram_copies, [node()]}, + {record_name, routing_node}, + {attributes, record_info(fields, routing_node)}]); + +mnesia(copy) -> + ok = ekka_mnesia:copy_table(?TABLE). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +%% @doc Starts the router helper +-spec(start_link(fun()) -> {ok, pid()} | ignore | {error, any()}). +start_link(StatsFun) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [StatsFun], []). + +%% @doc Monitor routing node +-spec(monitor(node()) -> ok). +monitor({_Group, Node}) -> + monitor(Node); +monitor(Node) when is_atom(Node) -> + case ekka:is_member(Node) orelse ets:member(?TABLE, Node) of + true -> ok; + false -> mnesia:dirty_write(#routing_node{name = Node, ts = os:timestamp()}) + end. + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([StatsFun]) -> + ekka:monitor(membership), + mnesia:subscribe({table, ?TABLE, simple}), + Nodes = lists:foldl( + fun(Node, Acc) -> + case ekka:is_member(Node) of + true -> Acc; + false -> _ = erlang:monitor_node(Node, true), + [Node | Acc] + end + end, [], mnesia:dirty_all_keys(?TABLE)), + {ok, TRef} = timer:send_interval(timer:seconds(1), stats), + {ok, #state{nodes = Nodes, stats_fun = StatsFun, stats_timer = TRef}}. + +handle_call(Req, _From, State) -> + emqx_log:error("[RouterHelper] Unexpected request: ~p", [Req]), + {reply, ignore, State}. + +handle_cast(Msg, State) -> + emqx_log:error("[RouterHelper] Unexpected msg: ~p", [Msg]), + {noreply, State}. + +handle_info({mnesia_table_event, {write, #routing_node{name = Node}, _}}, + State = #state{nodes = Nodes}) -> + emqx_log:info("[RouterHelper] New routing node: ~s", [Node]), + case ekka:is_member(Node) orelse lists:member(Node, Nodes) of + true -> {noreply, State}; + false -> _ = erlang:monitor_node(Node, true), + {noreply, State#state{nodes = [Node | Nodes]}} + end; + +handle_info({mnesia_table_event, _Event}, State) -> + {noreply, State}; + +handle_info({nodedown, Node}, State = #state{nodes = Nodes}) -> + global:trans({?LOCK, self()}, + fun() -> + mnesia:transaction(fun clean_routes/1, [Node]) + end), + mnesia:dirty_delete(routing_node, Node), + handle_info(stats, State#state{nodes = lists:delete(Node, Nodes)}); + +handle_info({membership, {mnesia, down, Node}}, State) -> + handle_info({nodedown, Node}, State); + +handle_info({membership, _Event}, State) -> + {noreply, State}; + +handle_info(stats, State = #state{stats_fun = StatsFun}) -> + ok = StatsFun(mnesia:table_info(route, size)), + {noreply, State, hibernate}; + +handle_info(Info, State) -> + emqx_log:error("[RouteHelper] Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #state{stats_timer = TRef}) -> + timer:cancel(TRef), + ekka:unmonitor(membership), + mnesia:unsubscribe({table, ?TABLE, simple}). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +clean_routes(Node) -> + Patterns = [#route{_ = '_', dest = Node}, + #route{_ = '_', dest = {'_', Node}}], + [mnesia:delete_object(R) || P <- Patterns, + R <- mnesia:match_object(P)]. + diff --git a/src/emqx_router_sup.erl b/src/emqx_router_sup.erl index ddcbfe097..bb3c59e3e 100644 --- a/src/emqx_router_sup.erl +++ b/src/emqx_router_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -27,12 +27,15 @@ start_link() -> init([]) -> StatsFun = emqx_stats:statsfun('routes/count', 'routes/max'), - SupFlags = #{strategy => one_for_all, intensity => 1, period => 5}, - Router = #{id => emqx_router, - start => {emqx_router, start_link, [StatsFun]}, - restart => permanent, - shutdown => 30000, - type => worker, - modules => [emqx_router]}, - {ok, {SupFlags, [Router]}}. + + %% Router helper + Helper = {router_helper, {emqx_router_helper, start_link, [StatsFun]}, + permanent, 5000, worker, [emqx_router_helper]}, + + %% Router pool + PoolSup = emqx_pool_sup:spec(router_pool, + [router, hash, emqx_sys:schedulers(), + {emqx_router, start_link, []}]), + + {ok, {{one_for_all, 0, 3600}, [Helper, PoolSup]}}. diff --git a/src/emqx_rpc.erl b/src/emqx_rpc.erl index f4ef07809..a95dd7323 100644 --- a/src/emqx_rpc.erl +++ b/src/emqx_rpc.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_server.erl b/src/emqx_server.erl deleted file mode 100644 index 1f7ee65ba..000000000 --- a/src/emqx_server.erl +++ /dev/null @@ -1,326 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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_server). - --behaviour(gen_server). - --include("emqx.hrl"). - --include("emqx_mqtt.hrl"). - --include("emqx_internal.hrl"). - --export([start_link/3]). - -%% PubSub API. --export([subscribe/1, subscribe/2, subscribe/3, publish/1, - unsubscribe/1, unsubscribe/2]). - -%% Async PubSub API. --export([async_subscribe/1, async_subscribe/2, async_subscribe/3, - async_unsubscribe/1, async_unsubscribe/2]). - -%% Management API. --export([setqos/3, subscriptions/1, subscribers/1, subscribed/2]). - -%% Debug API --export([dump/0]). - -%% gen_server Callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {pool, id, env, subids :: map(), submon :: emqx_pmon:pmon()}). - -%% @doc Start the server --spec(start_link(atom(), pos_integer(), list()) -> {ok, pid()} | ignore | {error, term()}). -start_link(Pool, Id, Env) -> - gen_server:start_link(?MODULE, [Pool, Id, Env], [{hibernate_after, 10000}]). - -%%-------------------------------------------------------------------- -%% PubSub API -%%-------------------------------------------------------------------- - -%% @doc Subscribe to a Topic. --spec(subscribe(binary()) -> ok | {error, term()}). -subscribe(Topic) when is_binary(Topic) -> - subscribe(Topic, self()). - --spec(subscribe(binary(), emqx:subscriber()) -> ok | {error, term()}). -subscribe(Topic, Subscriber) when is_binary(Topic) -> - subscribe(Topic, Subscriber, []). - --spec(subscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> - ok | {error, term()}). -subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> - call(pick(Subscriber), {subscribe, Topic, with_subpid(Subscriber), Options}). - -%% @doc Subscribe to a Topic asynchronously. --spec(async_subscribe(binary()) -> ok). -async_subscribe(Topic) when is_binary(Topic) -> - async_subscribe(Topic, self()). - --spec(async_subscribe(binary(), emqx:subscriber()) -> ok). -async_subscribe(Topic, Subscriber) when is_binary(Topic) -> - async_subscribe(Topic, Subscriber, []). - --spec(async_subscribe(binary(), emqx:subscriber(), [emqx:suboption()]) -> ok). -async_subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> - cast(pick(Subscriber), {subscribe, Topic, with_subpid(Subscriber), Options}). - -%% @doc Publish a message --spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore). -publish(Msg = #mqtt_message{from = From}) -> - trace(publish, From, Msg), - case emqx_hooks:run('message.publish', [], Msg) of - {ok, Msg1 = #mqtt_message{topic = Topic}} -> - emqx_pubsub:publish(Topic, Msg1); - {stop, Msg1} -> - lager:warning("Stop publishing: ~s", [emqx_message:format(Msg1)]), - ignore - end. - -%% @private -trace(publish, From, _Msg) when is_atom(From) -> - %% Dont' trace '$SYS' publish - ignore; -trace(publish, {ClientId, Username}, #mqtt_message{topic = Topic, payload = Payload}) -> - lager:info([{client, ClientId}, {topic, Topic}], - "~s/~s PUBLISH to ~s: ~p", [ClientId, Username, Topic, Payload]); -trace(publish, From, #mqtt_message{topic = Topic, payload = Payload}) when is_binary(From); is_list(From) -> - lager:info([{client, From}, {topic, Topic}], - "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). - -%% @doc Unsubscribe --spec(unsubscribe(binary()) -> ok | {error, term()}). -unsubscribe(Topic) when is_binary(Topic) -> - unsubscribe(Topic, self()). - -%% @doc Unsubscribe --spec(unsubscribe(binary(), emqx:subscriber()) -> ok | {error, term()}). -unsubscribe(Topic, Subscriber) when is_binary(Topic) -> - call(pick(Subscriber), {unsubscribe, Topic, with_subpid(Subscriber)}). - -%% @doc Async Unsubscribe --spec(async_unsubscribe(binary()) -> ok). -async_unsubscribe(Topic) when is_binary(Topic) -> - async_unsubscribe(Topic, self()). - --spec(async_unsubscribe(binary(), emqx:subscriber()) -> ok). -async_unsubscribe(Topic, Subscriber) when is_binary(Topic) -> - cast(pick(Subscriber), {unsubscribe, Topic, with_subpid(Subscriber)}). - --spec(setqos(binary(), emqx:subscriber(), mqtt_qos()) -> ok). -setqos(Topic, Subscriber, Qos) when is_binary(Topic) -> - call(pick(Subscriber), {setqos, Topic, with_subpid(Subscriber), Qos}). - -with_subpid(SubPid) when is_pid(SubPid) -> - SubPid; -with_subpid(SubId) when is_binary(SubId) -> - {SubId, self()}; -with_subpid({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> - {SubId, SubPid}. - --spec(subscriptions(emqx:subscriber()) -> [{emqx:subscriber(), binary(), list(emqx:suboption())}]). -subscriptions(SubPid) when is_pid(SubPid) -> - with_subproperty(ets:lookup(mqtt_subscription, SubPid)); - -subscriptions(SubId) when is_binary(SubId) -> - with_subproperty(ets:match_object(mqtt_subscription, {{SubId, '_'}, '_'})); - -subscriptions({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> - with_subproperty(ets:lookup(mqtt_subscription, {SubId, SubPid})). - -with_subproperty({Subscriber, {share, _Share, Topic}}) -> - with_subproperty({Subscriber, Topic}); -with_subproperty({Subscriber, Topic}) -> - {Subscriber, Topic, ets:lookup_element(mqtt_subproperty, {Topic, Subscriber}, 2)}; -with_subproperty(Subscriptions) when is_list(Subscriptions) -> - [with_subproperty(Subscription) || Subscription <- Subscriptions]. - --spec(subscribers(binary()) -> list(emqx:subscriber())). -subscribers(Topic) when is_binary(Topic) -> - emqx_pubsub:subscribers(Topic). - --spec(subscribed(binary(), emqx:subscriber()) -> boolean()). -subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> - ets:member(mqtt_subproperty, {Topic, SubPid}); -subscribed(Topic, SubId) when is_binary(Topic), is_binary(SubId) -> - length(ets:match_object(mqtt_subproperty, {{Topic, {SubId, '_'}}, '_'}, 1)) == 1; -subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), is_binary(SubId), is_pid(SubPid) -> - ets:member(mqtt_subproperty, {Topic, {SubId, SubPid}}). - -call(Server, Req) -> - gen_server:call(Server, Req, infinity). - -cast(Server, Msg) when is_pid(Server) -> - gen_server:cast(Server, Msg). - -pick(SubPid) when is_pid(SubPid) -> - gproc_pool:pick_worker(server, SubPid); -pick(SubId) when is_binary(SubId) -> - gproc_pool:pick_worker(server, SubId); -pick({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> - pick(SubId). - -dump() -> - [{Tab, ets:tab2list(Tab)} || Tab <- [mqtt_subproperty, mqtt_subscription, mqtt_subscriber]]. - -%%-------------------------------------------------------------------- -%% gen_server Callbacks -%%-------------------------------------------------------------------- - -init([Pool, Id, Env]) -> - gproc_pool:connect_worker(Pool, {Pool, Id}), - State = #state{pool = Pool, id = Id, env = Env, - subids = #{}, submon = emqx_pmon:new()}, - {ok, State, hibernate}. - -handle_call({subscribe, Topic, Subscriber, Options}, _From, State) -> - case do_subscribe(Topic, Subscriber, Options, State) of - {ok, NewState} -> reply(ok, setstats(NewState)); - {error, Error} -> reply({error, Error}, State) - end; - -handle_call({unsubscribe, Topic, Subscriber}, _From, State) -> - case do_unsubscribe(Topic, Subscriber, State) of - {ok, NewState} -> reply(ok, setstats(NewState)); - {error, Error} -> reply({error, Error}, State) - end; - -handle_call({setqos, Topic, Subscriber, Qos}, _From, State) -> - Key = {Topic, Subscriber}, - case ets:lookup(mqtt_subproperty, Key) of - [{_, Opts}] -> - Opts1 = lists:ukeymerge(1, [{qos, Qos}], Opts), - ets:insert(mqtt_subproperty, {Key, Opts1}), - reply(ok, State); - [] -> - reply({error, {subscription_not_found, Topic}}, State) - end; - -handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). - -handle_cast({subscribe, Topic, Subscriber, Options}, State) -> - case do_subscribe(Topic, Subscriber, Options, State) of - {ok, NewState} -> noreply(setstats(NewState)); - {error, _Error} -> noreply(State) - end; - -handle_cast({unsubscribe, Topic, Subscriber}, State) -> - case do_unsubscribe(Topic, Subscriber, State) of - {ok, NewState} -> noreply(setstats(NewState)); - {error, _Error} -> noreply(State) - end; - -handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). - -handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{subids = SubIds}) -> - case maps:find(DownPid, SubIds) of - {ok, SubId} -> - clean_subscriber({SubId, DownPid}); - error -> - clean_subscriber(DownPid) - end, - noreply(setstats(demonitor_subscriber(DownPid, State))); - -handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). - -terminate(_Reason, #state{pool = Pool, id = Id}) -> - gproc_pool:disconnect_worker(Pool, {Pool, Id}). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internal Functions -%%-------------------------------------------------------------------- - -do_subscribe(Topic, Subscriber, Options, State) -> - case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of - [] -> - emqx_pubsub:async_subscribe(Topic, Subscriber, Options), - Share = proplists:get_value(share, Options), - add_subscription(Share, Subscriber, Topic), - ets:insert(mqtt_subproperty, {{Topic, Subscriber}, Options}), - {ok, monitor_subscriber(Subscriber, State)}; - [_] -> - {error, {already_subscribed, Topic}} - end. - -add_subscription(undefined, Subscriber, Topic) -> - ets:insert(mqtt_subscription, {Subscriber, Topic}); -add_subscription(Share, Subscriber, Topic) -> - ets:insert(mqtt_subscription, {Subscriber, {share, Share, Topic}}). - -monitor_subscriber(SubPid, State = #state{submon = SubMon}) when is_pid(SubPid) -> - State#state{submon = SubMon:monitor(SubPid)}; -monitor_subscriber({SubId, SubPid}, State = #state{subids = SubIds, submon = SubMon}) -> - State#state{subids = maps:put(SubPid, SubId, SubIds), submon = SubMon:monitor(SubPid)}. - -do_unsubscribe(Topic, Subscriber, State) -> - case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of - [{_, Options}] -> - emqx_pubsub:async_unsubscribe(Topic, Subscriber, Options), - Share = proplists:get_value(share, Options), - del_subscription(Share, Subscriber, Topic), - ets:delete(mqtt_subproperty, {Topic, Subscriber}), - {ok, State}; - [] -> - {error, {subscription_not_found, Topic}} - end. - -del_subscription(undefined, Subscriber, Topic) -> - ets:delete_object(mqtt_subscription, {Subscriber, Topic}); -del_subscription(Share, Subscriber, Topic) -> - ets:delete_object(mqtt_subscription, {Subscriber, {share, Share, Topic}}). - -clean_subscriber(Subscriber) -> - lists:foreach(fun({_, {share, Share, Topic}}) -> - clean_subscriber(Share, Subscriber, Topic); - ({_, Topic}) -> - clean_subscriber(undefined, Subscriber, Topic) - end, ets:lookup(mqtt_subscription, Subscriber)), - ets:delete(mqtt_subscription, Subscriber). - -clean_subscriber(Share, Subscriber, Topic) -> - case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of - [] -> - %% TODO:....??? - Options = if Share == undefined -> []; true -> [{share, Share}] end, - emqx_pubsub:async_unsubscribe(Topic, Subscriber, Options); - [{_, Options}] -> - emqx_pubsub:async_unsubscribe(Topic, Subscriber, Options), - ets:delete(mqtt_subproperty, {Topic, Subscriber}) - end. - -demonitor_subscriber(SubPid, State = #state{subids = SubIds, submon = SubMon}) -> - State#state{subids = maps:remove(SubPid, SubIds), submon = SubMon:demonitor(SubPid)}. - -setstats(State) -> - emqx_stats:setstats('subscriptions/count', 'subscriptions/max', - ets:info(mqtt_subscription, size)), State. - -reply(Reply, State) -> - {reply, Reply, State}. - -noreply(State) -> - {noreply, State}. - diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 4f89e73ae..c8e069cd7 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -194,11 +194,11 @@ subscribe(Session, PacketId, TopicTable) -> %%TODO: the ack function??... -spec(publish(pid(), message()) -> ok | {error, term()}). publish(_Session, Msg = #mqtt_message{qos = ?QOS_0}) -> %% Publish QoS0 Directly - emqx_server:publish(Msg), ok; + emqx_broker:publish(Msg), ok; publish(_Session, Msg = #mqtt_message{qos = ?QOS_1}) -> %% Publish QoS1 message directly for client will PubAck automatically - emqx_server:publish(Msg), ok; + emqx_broker:publish(Msg), ok; publish(Session, Msg = #mqtt_message{qos = ?QOS_2}) -> %% Publish QoS2 to Session @@ -365,7 +365,7 @@ handle_cast({subscribe, From, TopicTable, AckFun}, ?LOG(warning, "Duplicated subscribe: ~s, qos = ~w", [Topic, NewQos], State), SubMap; {ok, OldQos} -> - emqx_server:setqos(Topic, ClientId, NewQos), + emqx_broker:setopts(Topic, ClientId, [{qos, NewQos}]), emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), ?LOG(warning, "Duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, NewQos], State), @@ -438,7 +438,7 @@ handle_cast({pubrel, PacketId}, State = #state{awaiting_rel = AwaitingRel}) -> {Msg, AwaitingRel1} -> %% Implement Qos2 by method A [MQTT 4.33] %% Dispatch to subscriber when received PUBREL - spawn(emqx_server, publish, [Msg]), %%:) + emqx_broker:publish(Msg), %% FIXME: gc(State#state{awaiting_rel = AwaitingRel1}); error -> ?LOG(warning, "Cannot find PUBREL: ~p", [PacketId], State), diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl index 8a5fa8b2e..c7e18e1cd 100644 --- a/src/emqx_session_sup.erl +++ b/src/emqx_session_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -18,7 +18,9 @@ -behavior(supervisor). --export([start_link/0, start_session/3]). +-include("emqx.hrl"). + +-export([start_link/0, start_session_process/1]). -export([init/1]). @@ -27,10 +29,10 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). -%% @doc Start a session --spec(start_session(boolean(), {binary(), binary() | undefined} , pid()) -> {ok, pid()}). -start_session(CleanSess, {ClientId, Username}, ClientPid) -> - supervisor:start_child(?MODULE, [CleanSess, {ClientId, Username}, ClientPid]). +%% @doc Start a session process +-spec(start_session_process(session()) -> {ok, pid()}). +start_session_process(Session) -> + supervisor:start_child(?MODULE, [Session]). %%-------------------------------------------------------------------- %% Supervisor callbacks diff --git a/src/emqx_shared_pubsub.erl b/src/emqx_shared_pubsub.erl new file mode 100644 index 000000000..bbc4d1930 --- /dev/null +++ b/src/emqx_shared_pubsub.erl @@ -0,0 +1,171 @@ +%%-------------------------------------------------------------------- +%% Copyright © 2013-2018 EMQ Inc. 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_shared_pubsub). + +-behaviour(gen_server). + +-include("emqx.hrl"). + +%% Mnesia bootstrap +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +%% API +-export([start_link/0]). + +-export([strategy/0]). + +-export([subscribe/3, unsubscribe/3]). + +-export([dispatch/3]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-define(SERVER, ?MODULE). + +-define(TABLE, shared_subscription). + +-record(state, {pmon}). + +-record(shared_subscription, {group, topic, subpid}). + +%%-------------------------------------------------------------------- +%% Mnesia bootstrap +%%-------------------------------------------------------------------- + +mnesia(boot) -> + ok = ekka_mnesia:create_table(?TABLE, [ + {type, bag}, + {ram_copies, [node()]}, + {record_name, shared_subscription}, + {attributes, record_info(fields, shared_subscription)}]); + +mnesia(copy) -> + ok = ekka_mnesia:copy_table(?TABLE). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +-spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +-spec(strategy() -> random | hash). +strategy() -> + application:get_env(emqx, load_balancing_strategy, random). + +subscribe(undefined, _Topic, _SubPid) -> + ok; +subscribe(Group, Topic, SubPid) when is_pid(SubPid) -> + mnesia:dirty_write(r(Group, Topic, SubPid)), + gen_server:cast(?SERVER, {monitor, SubPid}). + +unsubscribe(undefined, _Topic, _SubPid) -> + ok; +unsubscribe(Group, Topic, SubPid) when is_pid(SubPid) -> + mnesia:dirty_delete_object(r(Group, Topic, SubPid)). + +r(Group, Topic, SubPid) -> + #shared_subscription{group = Group, topic = Topic, subpid = SubPid}. + +dispatch(Group, Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> + case pick(subscribers(Group, Topic)) of + false -> Delivery; + SubPid -> SubPid ! {dispatch, Topic, Msg}, + Delivery#delivery{flows = [{dispatch, {Group, Topic}, 1} | Flows]} + end. + +pick([]) -> + false; +pick([SubPid]) -> + SubPid; +pick(SubPids) -> + X = abs(erlang:monotonic_time() + bxor erlang:unique_integer()), + lists:nth((X rem length(SubPids)) + 1, SubPids). + +subscribers(Group, Topic) -> + MP = {shared_subscription, Group, Topic, '$1'}, + ets:select(shared_subscription, [{MP, [], ['$1']}]). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([]) -> + {atomic, PMon} = mnesia:transaction(fun init_monitors/0), + mnesia:subscribe({table, ?TABLE, simple}), + {ok, #state{pmon = PMon}}. + +init_monitors() -> + mnesia:foldl( + fun(#shared_subscription{subpid = SubPid}, Mon) -> + Mon:monitor(SubPid) + end, emqx_pmon:new(), ?TABLE). + +handle_call(Req, _From, State) -> + emqx_log:error("[Shared] Unexpected request: ~p", [Req]), + {reply, ignore, State}. + +handle_cast({monitor, SubPid}, State= #state{pmon = PMon}) -> + {noreply, State#state{pmon = PMon:monitor(SubPid)}}; + +handle_cast(Msg, State) -> + emqx_log:error("[Shared] Unexpected msg: ~p", [Msg]), + {noreply, State}. + +handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = PMon}) -> + emqx_log:info("Shared subscription created: ~p", [NewRecord]), + #shared_subscription{subpid = SubPid} = NewRecord, + {noreply, State#state{pmon = PMon:monitor(SubPid)}}; + +handle_info({mnesia_table_event, {delete_object, OldRecord, _}}, State = #state{pmon = PMon}) -> + emqx_log:info("Shared subscription deleted: ~p", [OldRecord]), + #shared_subscription{subpid = SubPid} = OldRecord, + {noreply, State#state{pmon = PMon:demonitor(SubPid)}}; + +handle_info({mnesia_table_event, _Event}, State) -> + {noreply, State}; + +handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) -> + emqx_log:info("Shared subscription down: ~p", [SubPid]), + mnesia:transaction(fun clean_down/1, [SubPid]), + {noreply, State#state{pmon = PMon:erase(SubPid)}}; + +handle_info(Info, State) -> + emqx_log:error("[Shared] Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + mnesia:unsubscribe({table, ?TABLE, simple}). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +clean_down(SubPid) -> + MP = #shared_subscription{_ = '_', subpid = SubPid}, + lists:foreach(fun mnesia:delete_object/1, mnesia:match_object(MP)). + diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 30983d0b5..b90dd693b 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -29,7 +29,7 @@ %% API Function Exports -export([start_link/2]). --export([start_session/2, lookup_session/1, register_session/3, +-export([open_session/1, start_session/2, lookup_session/1, register_session/3, unregister_session/1, unregister_session/2]). -export([dispatch/3]). @@ -47,7 +47,7 @@ -define(TIMEOUT, 120000). -define(LOG(Level, Format, Args, Session), - lager:Level("SM(~s): " ++ Format, [Session#mqtt_session.client_id | Args])). + lager:Level("SM(~s): " ++ Format, [Session#session.client_id | Args])). %%-------------------------------------------------------------------- %% Mnesia callbacks @@ -55,7 +55,7 @@ mnesia(boot) -> %% Global Session Table - ok = ekka_mnesia:create_table(mqtt_session, [ + ok = ekka_mnesia:create_table(session, [ {type, set}, {ram_copies, [node()]}, {record_name, mqtt_session}, @@ -68,6 +68,49 @@ mnesia(copy) -> %% API %%-------------------------------------------------------------------- +%% Open a clean start session. +open_session(Session = #{client_id := ClientId, clean_start := true, expiry_interval := Interval}) -> + with_lock(ClientId, + fun() -> + {ResL, BadNodes} = emqx_rpc:multicall(ekka:nodelist(), ?MODULE, discard_session, [ClientId]), + io:format("ResL: ~p, BadNodes: ~p~n", [ResL, BadNodes]), + case Interval > 0 of + true -> + {ok, emqx_session_sup:start_session_process(Session)}; + false -> + {ok, emqx_session:init_state(Session)} + end + end). + +open_session(Session = #{client_id := ClientId, clean_start := false, expiry_interval := Interval}) -> + with_lock(ClientId, + fun() -> + {ResL, BadNodes} = emqx_rpc:multicall(ekka:nodelist(), ?MODULE, lookup_session, [ClientId]), + [SessionPid | _] = lists:flatten(ResL), + + + + end). + +lookup_session(ClientId) -> + ets:lookup(session, ClientId). + + +lookup_session(ClientId) -> + ets:lookup(session, ClientId). + +with_lock(undefined, Fun) -> + Fun(); + + +with_lock(ClientId, Fun) -> + case emqx_sm_locker:lock(ClientId) of + true -> Result = Fun(), + ok = emqx_sm_locker:unlock(ClientId), + Result; + false -> {error, client_id_unavailable} + end. + %% @doc Start a session manager -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> @@ -92,7 +135,7 @@ lookup_session(ClientId) -> register_session(ClientId, CleanSess, Properties) -> ets:insert(mqtt_local_session, {ClientId, self(), CleanSess, Properties}). -%% @doc Unregister a session. +%% @doc Unregister a Session. -spec(unregister_session(binary()) -> boolean()). unregister_session(ClientId) -> unregister_session(ClientId, self()). diff --git a/src/emqx_sm_helper.erl b/src/emqx_sm_helper.erl index 6751fd54d..068156b94 100644 --- a/src/emqx_sm_helper.erl +++ b/src/emqx_sm_helper.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_sm_locker.erl b/src/emqx_sm_locker.erl new file mode 100644 index 000000000..53af8a678 --- /dev/null +++ b/src/emqx_sm_locker.erl @@ -0,0 +1,43 @@ +%%-------------------------------------------------------------------- +%% Copyright © 2013-2018 EMQ Inc. 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_locker). + +-include("emqx.hrl"). + +-export([start_link/0]). + +%% Lock/Unlock API based on canal-lock. +-export([lock/1, unlock/1]). + +%% @doc Starts the lock server +-spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +start_link() -> + canal_lock:start_link(?MODULE, 1). + +%% @doc Lock a clientid +-spec(lock(client_id()) -> boolean()). +lock(ClientId) -> + case canal_lock:acquire(?MODULE, ClientId, 1, 1) of + {acquired, 1} -> true; + full -> false + end. + +%% @doc Unlock a clientid +-spec(unlock(client_id()) -> ok). +unlock(ClientId) -> + canal_lock:release(?MODULE, ClientId, 1, 1). + diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index feef64e2b..1b032a5ae 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index 1fbc7b245..219914997 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -30,7 +30,7 @@ set_session_stats/2, get_session_stats/1, del_session_stats/1]). %% Statistics API. --export([statsfun/1, statsfun/2, getstats/0, getstat/1, setstat/2, setstats/3]). +-export([statsfun/1, statsfun/2, getstats/0, getstat/1, setstat/2, setstat/3]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -124,10 +124,10 @@ statsfun(Stat) -> fun(Val) -> setstat(Stat, Val) end. -spec(statsfun(Stat :: atom(), MaxStat :: atom()) -> fun()). -statsfun(Stat, MaxStat) -> - fun(Val) -> setstats(Stat, MaxStat, Val) end. +statsfun(Stat, MaxStat) -> + fun(Val) -> setstat(Stat, MaxStat, Val) end. -%% @doc Get broker statistics +%% @doc Get all statistics -spec(getstats() -> [{atom(), non_neg_integer()}]). getstats() -> lists:sort(ets:tab2list(?STATS_TAB)). @@ -146,9 +146,9 @@ setstat(Stat, Val) -> ets:update_element(?STATS_TAB, Stat, {2, Val}). %% @doc Set stats with max --spec(setstats(Stat :: atom(), MaxStat :: atom(), Val :: pos_integer()) -> boolean()). -setstats(Stat, MaxStat, Val) -> - gen_server:cast(?MODULE, {setstats, Stat, MaxStat, Val}). +-spec(setstat(Stat :: atom(), MaxStat :: atom(), Val :: pos_integer()) -> boolean()). +setstat(Stat, MaxStat, Val) -> + gen_server:cast(?MODULE, {setstat, Stat, MaxStat, Val}). %%-------------------------------------------------------------------- %% gen_server callbacks @@ -172,7 +172,7 @@ handle_call(_Request, _From, State) -> {reply, error, State}. %% atomic -handle_cast({setstats, Stat, MaxStat, Val}, State) -> +handle_cast({setstat, Stat, MaxStat, Val}, State) -> MaxVal = ets:lookup_element(?STATS_TAB, MaxStat, 2), if Val > MaxVal -> diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index e115674f9..b1e229ed6 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -34,7 +34,6 @@ start_link() -> supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). - -spec(start_child(supervisor:child_spec()) -> startchild_ret()). start_child(ChildSpec) when is_tuple(ChildSpec) -> supervisor:start_child(?SUPERVISOR, ChildSpec). @@ -58,17 +57,16 @@ init([]) -> {ok, {{one_for_all, 0, 1}, [?CHILD(emqx_ctl, worker), ?CHILD(emqx_hooks, worker), - ?CHILD(emqx_router, worker), - ?CHILD(emqx_pubsub_sup, supervisor), ?CHILD(emqx_stats, worker), ?CHILD(emqx_metrics, worker), + ?CHILD(emqx_router_sup, supervisor), + ?CHILD(emqx_broker_sup, supervisor), ?CHILD(emqx_pooler, supervisor), ?CHILD(emqx_trace_sup, supervisor), ?CHILD(emqx_cm_sup, supervisor), ?CHILD(emqx_sm_sup, supervisor), ?CHILD(emqx_session_sup, supervisor), - ?CHILD(emqx_ws_client_sup, supervisor), - ?CHILD(emqx_broker, worker), + ?CHILD(emqx_ws_connection_sup, supervisor), ?CHILD(emqx_alarm, worker), ?CHILD(emqx_mod_sup, supervisor), ?CHILD(emqx_bridge_sup_sup, supervisor), diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl new file mode 100644 index 000000000..3c2ba1e3e --- /dev/null +++ b/src/emqx_sys.erl @@ -0,0 +1,177 @@ +%%-------------------------------------------------------------------- +%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_sys). + +-behaviour(gen_server). + +-include("emqx.hrl"). + +-export([start_link/0]). + +-export([schedulers/0]). + +-export([version/0, uptime/0, datetime/0, sysdescr/0, sys_interval/0]). + +-export([info/0]). + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {started_at, heartbeat, sys_ticker, version, sysdescr}). + +-define(APP, emqx). + +-define(SERVER, ?MODULE). + +%% $SYS Topics of Broker +-define(SYSTOP_BROKERS, [ + version, % Broker version + uptime, % Broker uptime + datetime, % Broker local datetime + sysdescr % Broker description +]). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +-spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +%% @doc Get schedulers +-spec(schedulers() -> pos_integer()). +schedulers() -> + erlang:system_info(schedulers). + +%% @doc Get sys version +-spec(version() -> string()). +version() -> + {ok, Version} = application:get_key(?APP, vsn), Version. + +%% @doc Get sys description +-spec(sysdescr() -> string()). +sysdescr() -> + {ok, Descr} = application:get_key(?APP, description), Descr. + +%% @doc Get sys uptime +-spec(uptime() -> string()). +uptime() -> gen_server:call(?SERVER, uptime). + +%% @doc Get sys datetime +-spec(datetime() -> string()). +datetime() -> + {{Y, M, D}, {H, MM, S}} = calendar:local_time(), + lists:flatten( + io_lib:format( + "~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])). + +sys_interval() -> + application:get_env(?APP, sys_interval, 60000). + +%% @doc Get sys info +-spec(info() -> list(tuple())). +info() -> + [{version, version()}, + {sysdescr, sysdescr()}, + {uptime, uptime()}, + {datetime, datetime()}]. + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([]) -> + Tick = fun(I, M) -> + {ok, TRef} = timer:send_interval(I, M), TRef + end, + {ok, #state{started_at = os:timestamp(), + heartbeat = Tick(1000, heartbeat), + sys_ticker = Tick(sys_interval(), tick), + version = iolist_to_binary(version()), + sysdescr = iolist_to_binary(sysdescr())}, hibernate}. + +handle_call(uptime, _From, State) -> + {reply, uptime(State), State}; + +handle_call(Req, _From, State) -> + emqx_log:error("[SYS] Unexpected request: ~p", [Req]), + {reply, ignore, State}. + +handle_cast(Msg, State) -> + emqx_log:error("[SYS] Unexpected msg: ~p", [Msg]), + {noreply, State}. + +handle_info(heartbeat, State) -> + publish(uptime, iolist_to_binary(uptime(State))), + publish(datetime, iolist_to_binary(datetime())), + {noreply, State, hibernate}; + +handle_info(tick, State = #state{version = Version, sysdescr = Descr}) -> + retain(brokers), + retain(version, Version), + retain(sysdescr, Descr), + {noreply, State, hibernate}; + +handle_info(Info, State) -> + emqx_log:error("[SYS] Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #state{heartbeat = Hb, sys_ticker = TRef}) -> + timer:cancel(Hb), + timer:cancel(TRef). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +retain(brokers) -> + Payload = list_to_binary(string:join([atom_to_list(N) || + N <- ekka_mnesia:running_nodes()], ",")), + Msg = emqx_message:make(broker, <<"$SYS/brokers">>, Payload), + emqx:publish(emqx_message:set_flag(sys, emqx_message:set_flag(retain, Msg))). + +retain(Topic, Payload) when is_binary(Payload) -> + Msg = emqx_message:make(broker, emqx_topic:systop(Topic), Payload), + emqx:publish(emqx_message:set_flag(sys, emqx_message:set_flag(retain, Msg))). + +publish(Topic, Payload) when is_binary(Payload) -> + Msg = emqx_message:make(broker, emqx_topic:systop(Topic), Payload), + emqx:publish(emqx_message:set_flag(sys, Msg)). + +uptime(#state{started_at = Ts}) -> + Secs = timer:now_diff(os:timestamp(), Ts) div 1000000, + lists:flatten(uptime(seconds, Secs)). + +uptime(seconds, Secs) when Secs < 60 -> + [integer_to_list(Secs), " seconds"]; +uptime(seconds, Secs) -> + [uptime(minutes, Secs div 60), integer_to_list(Secs rem 60), " seconds"]; +uptime(minutes, M) when M < 60 -> + [integer_to_list(M), " minutes, "]; +uptime(minutes, M) -> + [uptime(hours, M div 60), integer_to_list(M rem 60), " minutes, "]; +uptime(hours, H) when H < 24 -> + [integer_to_list(H), " hours, "]; +uptime(hours, H) -> + [uptime(days, H div 24), integer_to_list(H rem 24), " hours, "]; +uptime(days, D) -> + [integer_to_list(D), " days,"]. + diff --git a/src/emqx_sysmon.erl b/src/emqx_sysmon.erl index 7f4a2490d..7fc270a6a 100644 --- a/src/emqx_sysmon.erl +++ b/src/emqx_sysmon.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_sysmon_sup.erl b/src/emqx_sysmon_sup.erl index 6bcbf76c8..d4aa3aa98 100644 --- a/src/emqx_sysmon_sup.erl +++ b/src/emqx_sysmon_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_time.erl b/src/emqx_time.erl index d954c8204..4a92e12ae 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index 267786943..bc7c52f9e 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -16,6 +16,8 @@ -module(emqx_topic). +-include("emqx.hrl"). + -include("emqx_mqtt.hrl"). -import(lists, [reverse/1]). @@ -26,9 +28,7 @@ -export([parse/1, parse/2]). --type(topic() :: binary()). - --type(option() :: local | {qos, mqtt_qos()} | {share, '$queue' | binary()}). +-type(option() :: {qos, mqtt_qos()} | {share, '$queue' | binary()}). -type(word() :: '' | '+' | '#' | binary()). @@ -36,7 +36,7 @@ -type(triple() :: {root | binary(), word(), binary()}). --export_type([topic/0, option/0, word/0, triple/0]). +-export_type([option/0, word/0, triple/0]). -define(MAX_TOPIC_LEN, 4096). @@ -101,7 +101,7 @@ validate2([''|Words]) -> validate2(['+'|Words]) -> validate2(Words); validate2([W|Words]) -> - case validate3(W) of true -> validate2(Words); false -> false end. + validate3(W) andalso validate2(Words). validate3(<<>>) -> true; @@ -177,39 +177,24 @@ join(Words) -> parse(Topic) when is_binary(Topic) -> parse(Topic, []). -parse(<<"$local/", Topic1/binary>>, Options) -> - if_not_contain(local, Options, fun() -> - parse(Topic1, [local | Options]) - end); - -parse(<<"$fastlane/", Topic1/binary>>, Options) -> - if_not_contain(fastlane, Options, fun() -> - parse(Topic1, [fastlane | Options]) - end); - -parse(<<"$queue/", Topic1/binary>>, Options) -> - if_not_contain(share, Options,fun() -> - parse(Topic1, [{share, '$queue'} | Options]) - end); - -parse(<<"$share/", Topic1/binary>>, Options) -> - if_not_contain(share, Options, fun() -> - [Share, Topic2] = binary:split(Topic1, <<"/">>), - {Topic2, [{share, Share} | Options]} - end); - -parse(Topic, Options) -> - {Topic, Options}. - -if_not_contain(Key, Options, Fun) when Key == local; Key == fastlane -> - case lists:member(Key, Options) of - true -> error(invalid_topic); - false -> Fun() +parse(Topic = <<"$fastlane/", Topic1/binary>>, Options) -> + case lists:member(fastlane, Options) of + true -> error({invalid_topic, Topic}); + false -> parse(Topic1, [fastlane | Options]) end; -if_not_contain(share, Options, Fun) -> +parse(Topic = <<"$queue/", Topic1/binary>>, Options) -> case lists:keyfind(share, 1, Options) of - true -> error(invalid_topic); - false -> Fun() - end. + {share, _} -> error({invalid_topic, Topic}); + false -> parse(Topic1, [{share, '$queue'} | Options]) + end; + +parse(Topic = <<"$share/", Topic1/binary>>, Options) -> + case lists:keyfind(share, 1, Options) of + {share, _} -> error({invalid_topic, Topic}); + false -> [Group, Topic2] = binary:split(Topic1, <<"/">>), + {Topic2, [{share, Group} | Options]} + end; + +parse(Topic, Options) -> {Topic, Options}. diff --git a/src/emqx_trace.erl b/src/emqx_trace.erl index 99da331f7..fc68b3cb5 100644 --- a/src/emqx_trace.erl +++ b/src/emqx_trace.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -14,13 +14,16 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_trace). +-module(emqx_tracer). -behaviour(gen_server). -%% API Function Exports +-include("emqx.hrl"). + -export([start_link/0]). +-export([trace/3]). + -export([start_trace/2, stop_trace/1, all_traces/0]). %% gen_server Function Exports @@ -31,16 +34,36 @@ -type(trace_who() :: {client | topic, binary()}). --define(TRACE_OPTIONS, [{formatter_config, [time, " [",severity,"] ", message, "\n"]}]). +-define(OPTIONS, [{formatter_config, [time, " [",severity,"] ", message, "\n"]}]). %%-------------------------------------------------------------------- -%% API +%% Start the tracer %%-------------------------------------------------------------------- -spec(start_link() -> {ok, pid()}). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). +%%-------------------------------------------------------------------- +%% Trace +%%-------------------------------------------------------------------- + +trace(publish, From, _Msg) when is_atom(From) -> + %% Dont' trace '$SYS' publish + ignore; +trace(publish, {ClientId, Username}, #message{topic = Topic, payload = Payload}) -> + lager:info([{client, ClientId}, {topic, Topic}], + "~s/~s PUBLISH to ~s: ~p", [ClientId, Username, Topic, Payload]); +trace(publish, From, #message{topic = Topic, payload = Payload}) + when is_binary(From); is_list(From) -> + lager:info([{client, From}, {topic, Topic}], + "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). + + +%%-------------------------------------------------------------------- +%% Start/Stop Trace +%%-------------------------------------------------------------------- + %% @doc Start to trace client or topic. -spec(start_trace(trace_who(), string()) -> ok | {error, term()}). start_trace({client, ClientId}, LogFile) -> @@ -67,10 +90,10 @@ all_traces() -> gen_server:call(?MODULE, all_traces). %%-------------------------------------------------------------------- init([]) -> - {ok, #state{level = debug, traces = #{}}}. + {ok, #state{level = emqx:env(trace_level, debug), traces = #{}}}. handle_call({start_trace, Who, LogFile}, _From, State = #state{level = Level, traces = Traces}) -> - case lager:trace_file(LogFile, [Who], Level, ?TRACE_OPTIONS) of + case lager:trace_file(LogFile, [Who], Level, ?OPTIONS) of {ok, exists} -> {reply, {error, existed}, State}; {ok, Trace} -> @@ -96,15 +119,15 @@ handle_call(all_traces, _From, State = #state{traces = Traces}) -> <- maps:to_list(Traces)], State}; handle_call(Req, _From, State) -> - lager:error("[TRACE] Unexpected Call: ~p", [Req]), + emqx_log:error("[TRACE] Unexpected Call: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - lager:error("[TRACE] Unexpected Cast: ~p", [Msg]), + emqx_log:error("[TRACE] Unexpected Cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> - lager:error("[TRACE] Unexpected Info: ~p", [Info]), + emqx_log:error("[TRACE] Unexpected Info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> diff --git a/src/emqx_trace_sup.erl b/src/emqx_trace_sup.erl index 861b3ef79..e5f7bc2f2 100644 --- a/src/emqx_trace_sup.erl +++ b/src/emqx_trace_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index ba2ead2d2..9f13ba9d2 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -18,7 +18,7 @@ -include("emqx.hrl"). -%% Mnesia Callbacks +%% Mnesia bootstrap -export([mnesia/1]). -boot_mnesia({mnesia, [boot]}). @@ -28,10 +28,10 @@ -export([insert/1, match/1, lookup/1, delete/1]). %%-------------------------------------------------------------------- -%% Mnesia Callbacks +%% Mnesia Bootstrap %%-------------------------------------------------------------------- -%% @doc Create or Replicate trie tables. +%% @doc Create or replicate trie tables. -spec(mnesia(boot | copy) -> ok). mnesia(boot) -> %% Trie Table @@ -55,8 +55,8 @@ mnesia(copy) -> %% Trie API %%-------------------------------------------------------------------- -%% @doc Insert topic to trie --spec(insert(Topic :: binary()) -> ok). +%% @doc Insert a topic into the trie +-spec(insert(Topic :: topic()) -> ok). insert(Topic) when is_binary(Topic) -> case mnesia:read(trie_node, Topic) of [#trie_node{topic = Topic}] -> @@ -64,25 +64,25 @@ insert(Topic) when is_binary(Topic) -> [TrieNode = #trie_node{topic = undefined}] -> write_trie_node(TrieNode#trie_node{topic = Topic}); [] -> - % Add trie path + %% Add trie path lists:foreach(fun add_path/1, emqx_topic:triples(Topic)), - % Add last node + %% Add last node write_trie_node(#trie_node{node_id = Topic, topic = Topic}) end. -%% @doc Find trie nodes that match topic --spec(match(Topic :: binary()) -> list(MatchedTopic :: binary())). +%% @doc Find trie nodes that match the topic +-spec(match(Topic :: topic()) -> list(MatchedTopic :: topic())). match(Topic) when is_binary(Topic) -> TrieNodes = match_node(root, emqx_topic:words(Topic)), [Name || #trie_node{topic = Name} <- TrieNodes, Name =/= undefined]. -%% @doc Lookup a Trie Node +%% @doc Lookup a trie node -spec(lookup(NodeId :: binary()) -> [#trie_node{}]). lookup(NodeId) -> mnesia:read(trie_node, NodeId). -%% @doc Delete topic from trie --spec(delete(Topic :: binary()) -> ok). +%% @doc Delete a topic from the trie +-spec(delete(Topic :: topic()) -> ok). delete(Topic) when is_binary(Topic) -> case mnesia:read(trie_node, Topic) of [#trie_node{edge_count = 0}] -> @@ -95,11 +95,11 @@ delete(Topic) when is_binary(Topic) -> end. %%-------------------------------------------------------------------- -%% Internal Functions +%% Internal functions %%-------------------------------------------------------------------- %% @private -%% @doc Add path to trie tree. +%% @doc Add a path to the trie. add_path({Node, Word, Child}) -> Edge = #trie_edge{node_id = Node, word = Word}, case mnesia:read(trie_node, Node) of @@ -146,7 +146,7 @@ match_node(NodeId, [W|Words], ResAcc) -> end. %% @private -%% @doc Delete paths from trie tree. +%% @doc Delete paths from the trie. delete_path([]) -> ok; delete_path([{NodeId, Word, _} | RestPath]) -> diff --git a/src/emqx_vm.erl b/src/emqx_vm.erl index 32959cc0c..0bf7a6c30 100644 --- a/src/emqx_vm.erl +++ b/src/emqx_vm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl index e854bd1cd..21b4ffcec 100644 --- a/src/emqx_ws.erl +++ b/src/emqx_ws.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 61e717a5c..bee19841a 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_ws_conn). +-module(emqx_ws_connection). -behaviour(gen_server). diff --git a/src/emqx_ws_connection_sup.erl b/src/emqx_ws_connection_sup.erl index f1a26a91b..330272102 100644 --- a/src/emqx_ws_connection_sup.erl +++ b/src/emqx_ws_connection_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright © 2013-2018 EMQ Inc. 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. @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_ws_conn_sup). +-module(emqx_ws_connection_sup). -behavior(supervisor). @@ -39,6 +39,6 @@ init([]) -> %%TODO: Cannot upgrade the environments, Use zone? Env = lists:append(emqx:env(client, []), emqx:env(protocol, [])), {ok, {{simple_one_for_one, 0, 1}, - [{ws_conn, {emqx_ws_conn, start_link, [Env]}, - temporary, 5000, worker, [emqx_ws_conn]}]}}. + [{ws_connection, {emqx_ws_connection, start_link, [Env]}, + temporary, 5000, worker, [emqx_ws_connection]}]}}. From 56b88dd7f7a3a93a4045bfad226d8fa8e6e310d4 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 23 Mar 2018 16:39:23 +0800 Subject: [PATCH 030/520] Improve the design of session management --- include/emqx.hrl | 1 + src/emqx.erl | 2 +- src/emqx_protocol.erl | 7 +- src/emqx_session.erl | 51 ++- src/emqx_session_sup.erl | 8 +- src/emqx_sm.erl | 358 +++++------------- src/emqx_sm_helper.erl | 87 ----- src/emqx_sm_locker.erl | 16 +- src/emqx_sm_sup.erl | 29 +- src/emqx_sup.erl | 1 + src/emqx_tables.erl | 27 ++ src/{emqx_trace.erl => emqx_tracer.erl} | 0 ...emqx_trace_sup.erl => emqx_tracer_sup.erl} | 10 +- 13 files changed, 178 insertions(+), 419 deletions(-) delete mode 100644 src/emqx_sm_helper.erl create mode 100644 src/emqx_tables.erl rename src/{emqx_trace.erl => emqx_tracer.erl} (100%) rename src/{emqx_trace_sup.erl => emqx_tracer_sup.erl} (81%) diff --git a/include/emqx.hrl b/include/emqx.hrl index c1b366b86..5c67236d0 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -63,6 +63,7 @@ clean_start := boolean(), expiry_interval := non_neg_integer()}). +-record(session, {client_id, sess_pid}). %%-------------------------------------------------------------------- %% Message and Delivery diff --git a/src/emqx.erl b/src/emqx.erl index 386fc222c..967478a8c 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (C) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 103fbf4ff..7f58dee51 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -219,8 +219,11 @@ process(?CONNECT_PACKET(Var), State0) -> State2 = maybe_set_clientid(State1), %% Start session - case emqx_sm:start_session(CleanSess, {clientid(State2), Username}) of - {ok, Session, SP} -> + case emqx_sm:open_session(#{clean_start => CleanSess, + client_id => clientid(State2), + username => Username}) of + {ok, Session} -> %% TODO:... + SP = true, %% TODO:... %% Register the client emqx_cm:reg(client(State2)), %% Start keepalive diff --git a/src/emqx_session.erl b/src/emqx_session.erl index c8e069cd7..611e967be 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -14,35 +14,6 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% -%% @doc MQTT Session -%% -%% A stateful interaction between a Client and a Server. Some Sessions -%% last only as long as the Network Connection, others can span multiple -%% consecutive Network Connections between a Client and a Server. -%% -%% The Session state in the Server consists of: -%% -%% The existence of a Session, even if the rest of the Session state is empty. -%% -%% The Client’s subscriptions. -%% -%% QoS 1 and QoS 2 messages which have been sent to the Client, but have not -%% been completely acknowledged. -%% -%% QoS 1 and QoS 2 messages pending transmission to the Client. -%% -%% QoS 2 messages which have been received from the Client, but have not -%% been completely acknowledged. -%% -%% Optionally, QoS 0 messages pending transmission to the Client. -%% -%% If the session is currently disconnected, the time at which the Session state -%% will be deleted. -%% -%% @end -%% - -module(emqx_session). -behaviour(gen_server). @@ -76,6 +47,28 @@ -define(MQueue, emqx_mqueue). +%% A stateful interaction between a Client and a Server. Some Sessions +%% last only as long as the Network Connection, others can span multiple +%% consecutive Network Connections between a Client and a Server. +%% +%% The Session state in the Server consists of: +%% +%% The existence of a Session, even if the rest of the Session state is empty. +%% +%% The Client’s subscriptions. +%% +%% QoS 1 and QoS 2 messages which have been sent to the Client, but have not +%% been completely acknowledged. +%% +%% QoS 1 and QoS 2 messages pending transmission to the Client. +%% +%% QoS 2 messages which have been received from the Client, but have not +%% been completely acknowledged. +%% +%% Optionally, QoS 0 messages pending transmission to the Client. +%% +%% If the session is currently disconnected, the time at which the Session state +%% will be deleted. -record(state, { %% Clean Session Flag diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl index c7e18e1cd..3e61f09d1 100644 --- a/src/emqx_session_sup.erl +++ b/src/emqx_session_sup.erl @@ -20,18 +20,16 @@ -include("emqx.hrl"). --export([start_link/0, start_session_process/1]). +-export([start_link/0, start_session/1]). -export([init/1]). -%% @doc Start session supervisor -spec(start_link() -> {ok, pid()}). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). -%% @doc Start a session process --spec(start_session_process(session()) -> {ok, pid()}). -start_session_process(Session) -> +-spec(start_session(session()) -> {ok, pid()}). +start_session(Session) -> supervisor:start_child(?MODULE, [Session]). %%-------------------------------------------------------------------- diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index b90dd693b..a3f9e88cb 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -20,220 +20,152 @@ -include("emqx.hrl"). -%% Mnesia Callbacks --export([mnesia/1]). +-export([start_link/1]). --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). +-export([open_session/1, lookup_session/1, close_session/1]). +-export([resume_session/1, discard_session/1]). +-export([register_session/1, unregister_session/2]). -%% API Function Exports --export([start_link/2]). - --export([open_session/1, start_session/2, lookup_session/1, register_session/3, - unregister_session/1, unregister_session/2]). +%% lock_session/1, create_session/1, unlock_session/1, -export([dispatch/3]). --export([local_sessions/0]). - -%% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {pool, id, monitors}). +-record(state, {stats_fun, stats_timer, monitors = #{}}). --define(POOL, ?MODULE). +-spec(start_link(StatsFun :: fun()) -> {ok, pid()} | ignore | {error, term()}). +start_link(StatsFun) -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [StatsFun], []). --define(TIMEOUT, 120000). - --define(LOG(Level, Format, Args, Session), - lager:Level("SM(~s): " ++ Format, [Session#session.client_id | Args])). - -%%-------------------------------------------------------------------- -%% Mnesia callbacks -%%-------------------------------------------------------------------- - -mnesia(boot) -> - %% Global Session Table - ok = ekka_mnesia:create_table(session, [ - {type, set}, - {ram_copies, [node()]}, - {record_name, mqtt_session}, - {attributes, record_info(fields, mqtt_session)}]); - -mnesia(copy) -> - ok = ekka_mnesia:copy_table(mqtt_session). - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -%% Open a clean start session. -open_session(Session = #{client_id := ClientId, clean_start := true, expiry_interval := Interval}) -> +open_session(Session = #{client_id := ClientId, clean_start := true}) -> with_lock(ClientId, fun() -> - {ResL, BadNodes} = emqx_rpc:multicall(ekka:nodelist(), ?MODULE, discard_session, [ClientId]), - io:format("ResL: ~p, BadNodes: ~p~n", [ResL, BadNodes]), - case Interval > 0 of - true -> - {ok, emqx_session_sup:start_session_process(Session)}; - false -> - {ok, emqx_session:init_state(Session)} + case rpc:multicall(ekka:nodelist(), ?MODULE, discard_session, [ClientId]) of + {_Res, []} -> ok; + {_Res, BadNodes} -> emqx_log:error("[SM] Bad nodes found when lock a session: ~p", [BadNodes]) + end, + {ok, emqx_session_sup:start_session(Session)} + end); + +open_session(Session = #{client_id := ClientId, clean_start := false}) -> + with_lock(ClientId, + fun() -> + {ResL, _BadNodes} = emqx_rpc:multicall(ekka:nodelist(), ?MODULE, lookup_session, [ClientId]), + case lists:flatten([Pid || Pid <- ResL, Pid =/= undefined]) of + [] -> + {ok, emqx_session_sup:start_session(Session)}; + [SessPid|_] -> + case resume_session(SessPid) of + ok -> {ok, SessPid}; + {error, Reason} -> + emqx_log:error("[SM] Failed to resume session: ~p, ~p", [Session, Reason]), + {ok, emqx_session_sup:start_session(Session)} + end end end). -open_session(Session = #{client_id := ClientId, clean_start := false, expiry_interval := Interval}) -> - with_lock(ClientId, - fun() -> - {ResL, BadNodes} = emqx_rpc:multicall(ekka:nodelist(), ?MODULE, lookup_session, [ClientId]), - [SessionPid | _] = lists:flatten(ResL), +resume_session(SessPid) when node(SessPid) == node() -> + case is_process_alive(SessPid) of + true -> + emqx_session:resume(SessPid, self()); + false -> + emqx_log:error("Cannot resume ~p which seems already dead!", [SessPid]), + {error, session_died} + end; + +resume_session(SessPid) -> + case rpc:call(node(SessPid), emqx_session, resume, [SessPid]) of + ok -> {ok, SessPid}; + {badrpc, Reason} -> + {error, Reason}; + {error, Reason} -> + {error, Reason} + end. - - - end). +discard_session(ClientId) -> + case lookup_session(ClientId) of + undefined -> ok; + Pid -> emqx_session:discard(Pid) + end. lookup_session(ClientId) -> - ets:lookup(session, ClientId). - - -lookup_session(ClientId) -> - ets:lookup(session, ClientId). - -with_lock(undefined, Fun) -> - Fun(); + try ets:lookup_element(session, ClientId, 2) catch error:badarg -> undefined end. +close_session(SessPid) -> + emqx_session:close(SessPid). with_lock(ClientId, Fun) -> case emqx_sm_locker:lock(ClientId) of - true -> Result = Fun(), - ok = emqx_sm_locker:unlock(ClientId), - Result; - false -> {error, client_id_unavailable} + true -> Result = Fun(), + emqx_sm_locker:unlock(ClientId), + Result; + false -> {error, client_id_unavailable}; + {error, Reason} -> {error, Reason} end. -%% @doc Start a session manager --spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). -start_link(Pool, Id) -> - gen_server:start_link(?MODULE, [Pool, Id], []). - -%% @doc Start a session --spec(start_session(boolean(), {binary(), binary() | undefined}) -> {ok, pid(), boolean()} | {error, term()}). -start_session(CleanSess, {ClientId, Username}) -> - SM = gproc_pool:pick_worker(?POOL, ClientId), - call(SM, {start_session, CleanSess, {ClientId, Username}, self()}). - -%% @doc Lookup a Session --spec(lookup_session(binary()) -> mqtt_session() | undefined). -lookup_session(ClientId) -> - case mnesia:dirty_read(mqtt_session, ClientId) of - [Session] -> Session; - [] -> undefined - end. - -%% @doc Register a session with info. --spec(register_session(binary(), boolean(), [tuple()]) -> true). -register_session(ClientId, CleanSess, Properties) -> - ets:insert(mqtt_local_session, {ClientId, self(), CleanSess, Properties}). - -%% @doc Unregister a Session. --spec(unregister_session(binary()) -> boolean()). -unregister_session(ClientId) -> - unregister_session(ClientId, self()). +-spec(register_session(client_id()) -> true). +register_session(ClientId) -> + ets:insert(session, {ClientId, self()}). unregister_session(ClientId, Pid) -> - case ets:lookup(mqtt_local_session, ClientId) of - [LocalSess = {_, Pid, _, _}] -> - emqx_stats:del_session_stats(ClientId), - ets:delete_object(mqtt_local_session, LocalSess); + case ets:lookup(session, ClientId) of + [{_, Pid}] -> + ets:delete_object(session, {ClientId, Pid}); _ -> false end. dispatch(ClientId, Topic, Msg) -> - try ets:lookup_element(mqtt_local_session, ClientId, 2) of - Pid -> Pid ! {dispatch, Topic, Msg} - catch - error:badarg -> - emqx_hooks:run('message.dropped', [ClientId, Msg]), - ok %%TODO: How?? + case lookup_session(ClientId) of + Pid when is_pid(Pid) -> + Pid ! {dispatch, Topic, Msg}; + undefined -> + emqx_hooks:run('message.dropped', [ClientId, Msg]) end. -call(SM, Req) -> - gen_server:call(SM, Req, ?TIMEOUT). %%infinity). - -%% @doc for debug. -local_sessions() -> - ets:tab2list(mqtt_local_session). - %%-------------------------------------------------------------------- -%% gen_server Callbacks +%% gen_server callbacks %%-------------------------------------------------------------------- -init([Pool, Id]) -> - gproc_pool:connect_worker(Pool, {Pool, Id}), - {ok, #state{pool = Pool, id = Id, monitors = dict:new()}}. - -%% Persistent Session -handle_call({start_session, false, {ClientId, Username}, ClientPid}, _From, State) -> - case lookup_session(ClientId) of - undefined -> - %% Create session locally - create_session({false, {ClientId, Username}, ClientPid}, State); - Session -> - case resume_session(Session, ClientPid) of - {ok, SessPid} -> - {reply, {ok, SessPid, true}, State}; - {error, Erorr} -> - {reply, {error, Erorr}, State} - end - end; - -%% Transient Session -handle_call({start_session, true, {ClientId, Username}, ClientPid}, _From, State) -> - Client = {true, {ClientId, Username}, ClientPid}, - case lookup_session(ClientId) of - undefined -> - create_session(Client, State); - Session -> - case destroy_session(Session) of - ok -> - create_session(Client, State); - {error, Error} -> - {reply, {error, Error}, State} - end - end; +init([StatsFun]) -> + {ok, TRef} = timer:send_interval(timer:seconds(1), stats), + {ok, #state{stats_fun = StatsFun, stats_timer = TRef}}. handle_call(Req, _From, State) -> - lager:error("[MQTT-SM] Unexpected Request: ~p", [Req]), + emqx_log:error("[SM] Unexpected request: ~p", [Req]), {reply, ignore, State}. +handle_cast({monitor_session, SessionPid, ClientId}, + State = #state{monitors = Monitors}) -> + MRef = erlang:monitor(process, SessionPid), + {noreply, State#state{monitors = maps:put(MRef, ClientId, Monitors)}}; + handle_cast(Msg, State) -> - lager:error("[MQTT-SM] Unexpected Message: ~p", [Msg]), + emqx_log:error("[SM] Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> - case dict:find(MRef, State#state.monitors) of - {ok, ClientId} -> - case mnesia:dirty_read({mqtt_session, ClientId}) of - [] -> - ok; - [Sess = #mqtt_session{sess_pid = DownPid}] -> - mnesia:dirty_delete_object(Sess); - [_Sess] -> - ok - end, - {noreply, erase_monitor(MRef, State), hibernate}; +handle_info(stats, State) -> + {noreply, setstats(State), hibernate}; + +handle_info({'DOWN', MRef, process, DownPid, _Reason}, + State = #state{monitors = Monitors}) -> + case maps:find(MRef, Monitors) of + {ok, {ClientId, Pid}} -> + ets:delete_object(session, {ClientId, Pid}), + {noreply, setstats(State#state{monitors = maps:remove(MRef, Monitors)})}; error -> - lager:error("MRef of session ~p not found", [DownPid]), + emqx_log:error("session ~p not found", [DownPid]), {noreply, State} end; handle_info(Info, State) -> - lager:error("[MQTT-SM] Unexpected Info: ~p", [Info]), + emqx_log:error("[SM] Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{pool = Pool, id = Id}) -> - gproc_pool:disconnect_worker(Pool, {Pool, Id}). +terminate(_Reason, _State = #state{stats_timer = TRef}) -> + timer:cancel(TRef). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -242,100 +174,6 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -%% Create Session Locally -create_session({CleanSess, {ClientId, Username}, ClientPid}, State) -> - case create_session(CleanSess, {ClientId, Username}, ClientPid) of - {ok, SessPid} -> - {reply, {ok, SessPid, false}, - monitor_session(ClientId, SessPid, State)}; - {error, Error} -> - {reply, {error, Error}, State} - end. - -create_session(CleanSess, {ClientId, Username}, ClientPid) -> - case emqx_session_sup:start_session(CleanSess, {ClientId, Username}, ClientPid) of - {ok, SessPid} -> - Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid, clean_sess = CleanSess}, - case insert_session(Session) of - {aborted, {conflict, ConflictPid}} -> - %% Conflict with othe node? - lager:error("SM(~s): Conflict with ~p", [ClientId, ConflictPid]), - {error, mnesia_conflict}; - {atomic, ok} -> - {ok, SessPid} - end; - {error, Error} -> - {error, Error} - end. - -insert_session(Session = #mqtt_session{client_id = ClientId}) -> - mnesia:transaction( - fun() -> - case mnesia:wread({mqtt_session, ClientId}) of - [] -> - mnesia:write(mqtt_session, Session, write); - [#mqtt_session{sess_pid = SessPid}] -> - mnesia:abort({conflict, SessPid}) - end - end). - -%% Local node -resume_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid}, ClientPid) - when node(SessPid) =:= node() -> - - case is_process_alive(SessPid) of - true -> - emqx_session:resume(SessPid, ClientId, ClientPid), - {ok, SessPid}; - false -> - ?LOG(error, "Cannot resume ~p which seems already dead!", [SessPid], Session), - {error, session_died} - end; - -%% Remote node -resume_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid}, ClientPid) -> - Node = node(SessPid), - case rpc:call(Node, emqx_session, resume, [SessPid, ClientId, ClientPid]) of - ok -> - {ok, SessPid}; - {badrpc, nodedown} -> - ?LOG(error, "Session died for node '~s' down", [Node], Session), - remove_session(Session), - {error, session_nodedown}; - {badrpc, Reason} -> - ?LOG(error, "Failed to resume from node ~s for ~p", [Node, Reason], Session), - {error, Reason} - end. - -%% Local node -destroy_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid}) - when node(SessPid) =:= node() -> - emqx_session:destroy(SessPid, ClientId), - remove_session(Session); - -%% Remote node -destroy_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid}) -> - Node = node(SessPid), - case rpc:call(Node, emqx_session, destroy, [SessPid, ClientId]) of - ok -> - remove_session(Session); - {badrpc, nodedown} -> - ?LOG(error, "Node '~s' down", [Node], Session), - remove_session(Session); - {badrpc, Reason} -> - ?LOG(error, "Failed to destory ~p on remote node ~p for ~s", - [SessPid, Node, Reason], Session), - {error, Reason} - end. - -remove_session(Session) -> - mnesia:dirty_delete_object(Session). - -monitor_session(ClientId, SessPid, State = #state{monitors = Monitors}) -> - MRef = erlang:monitor(process, SessPid), - State#state{monitors = dict:store(MRef, ClientId, Monitors)}. - -erase_monitor(MRef, State = #state{monitors = Monitors}) -> - erlang:demonitor(MRef, [flush]), - State#state{monitors = dict:erase(MRef, Monitors)}. +setstats(State = #state{stats_fun = StatsFun}) -> + StatsFun(ets:info(session, size)), State. diff --git a/src/emqx_sm_helper.erl b/src/emqx_sm_helper.erl deleted file mode 100644 index 068156b94..000000000 --- a/src/emqx_sm_helper.erl +++ /dev/null @@ -1,87 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. 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_helper). - --behaviour(gen_server). - --include("emqx.hrl"). - --include_lib("stdlib/include/ms_transform.hrl"). - -%% API Function Exports --export([start_link/1]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {stats_fun, ticker}). - --define(LOCK, {?MODULE, clean_sessions}). - -%% @doc Start a session helper --spec(start_link(fun()) -> {ok, pid()} | ignore | {error, term()}). -start_link(StatsFun) -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [StatsFun], []). - -init([StatsFun]) -> - ekka:monitor(membership), - {ok, TRef} = timer:send_interval(timer:seconds(1), tick), - {ok, #state{stats_fun = StatsFun, ticker = TRef}}. - -handle_call(Req, _From, State) -> - lager:error("[SM-HELPER] Unexpected Call: ~p", [Req]), - {reply, ignore, State}. - -handle_cast(Msg, State) -> - lager:error("[SM-HELPER] Unexpected Cast: ~p", [Msg]), - {noreply, State}. - -handle_info({membership, {mnesia, down, Node}}, State) -> - Fun = fun() -> - ClientIds = - mnesia:select(mqtt_session, [{#mqtt_session{client_id = '$1', sess_pid = '$2', _ = '_'}, - [{'==', {node, '$2'}, Node}], ['$1']}]), - lists:foreach(fun(ClientId) -> mnesia:delete({mqtt_session, ClientId}) end, ClientIds) - end, - global:trans({?LOCK, self()}, fun() -> mnesia:async_dirty(Fun) end), - {noreply, State, hibernate}; - -handle_info({membership, _Event}, State) -> - {noreply, State}; - -handle_info(tick, State) -> - {noreply, setstats(State), hibernate}; - -handle_info(Info, State) -> - lager:error("[SM-HELPER] Unexpected Info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, _State = #state{ticker = TRef}) -> - timer:cancel(TRef), - ekka:unmonitor(membership). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -setstats(State = #state{stats_fun = StatsFun}) -> - StatsFun(ets:info(mqtt_local_session, size)), State. - diff --git a/src/emqx_sm_locker.erl b/src/emqx_sm_locker.erl index 53af8a678..fc61e6b06 100644 --- a/src/emqx_sm_locker.erl +++ b/src/emqx_sm_locker.erl @@ -18,26 +18,16 @@ -include("emqx.hrl"). --export([start_link/0]). - %% Lock/Unlock API based on canal-lock. -export([lock/1, unlock/1]). -%% @doc Starts the lock server --spec(start_link() -> {ok, pid()} | ignore | {error, any()}). -start_link() -> - canal_lock:start_link(?MODULE, 1). - %% @doc Lock a clientid --spec(lock(client_id()) -> boolean()). +-spec(lock(client_id()) -> boolean() | {error, term()}). lock(ClientId) -> - case canal_lock:acquire(?MODULE, ClientId, 1, 1) of - {acquired, 1} -> true; - full -> false - end. + emqx_rpc:call(ekka:leader(), emqx_sm_locker, lock, [ClientId]). %% @doc Unlock a clientid -spec(unlock(client_id()) -> ok). unlock(ClientId) -> - canal_lock:release(?MODULE, ClientId, 1, 1). + emqx_rpc:call(ekka:leader(), emqx_locker, unlock, [ClientId]). diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index 1b032a5ae..52138f62a 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -18,31 +18,28 @@ -behaviour(supervisor). - --define(SM, emqx_sm). - --define(HELPER, emqx_sm_helper). - -%% API -export([start_link/0]). -%% Supervisor callbacks -export([init/1]). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - %% Create session tables - _ = ets:new(mqtt_local_session, [public, ordered_set, named_table, {write_concurrency, true}]), + %% Create tables + create_tabs(), - %% Helper StatsFun = emqx_stats:statsfun('sessions/count', 'sessions/max'), - Helper = {?HELPER, {?HELPER, start_link, [StatsFun]}, - permanent, 5000, worker, [?HELPER]}, - %% SM Pool Sup - MFA = {?SM, start_link, []}, - PoolSup = emqx_pool_sup:spec([?SM, hash, erlang:system_info(schedulers), MFA]), - {ok, {{one_for_all, 10, 3600}, [Helper, PoolSup]}}. + SM = {emqx_sm, {emqx_sm, start_link, [StatsFun]}, + permanent, 5000, worker, [emqx_sm]}, + + {ok, {{one_for_all, 0, 3600}, [SM]}}. + +create_tabs() -> + lists:foreach(fun create_tab/1, [session, session_stats, session_attrs]). + +create_tab(Tab) -> + emqx_tables:create(Tab, [public, ordered_set, named_table, + {write_concurrency, true}]). diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index b1e229ed6..ff632d4e7 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -57,6 +57,7 @@ init([]) -> {ok, {{one_for_all, 0, 1}, [?CHILD(emqx_ctl, worker), ?CHILD(emqx_hooks, worker), + ?CHILD(emqx_locker, worker), ?CHILD(emqx_stats, worker), ?CHILD(emqx_metrics, worker), ?CHILD(emqx_router_sup, supervisor), diff --git a/src/emqx_tables.erl b/src/emqx_tables.erl new file mode 100644 index 000000000..d971b99cf --- /dev/null +++ b/src/emqx_tables.erl @@ -0,0 +1,27 @@ +%%-------------------------------------------------------------------- +%% Copyright © 2013-2018 EMQ Inc. 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_tables). + +-export([create/2]). + +create(Tab, Opts) -> + case ets:info(Tab, name) of + undefined -> + ets:new(Tab, lists:usort([named_table|Opts])); + Tab -> Tab + end. + diff --git a/src/emqx_trace.erl b/src/emqx_tracer.erl similarity index 100% rename from src/emqx_trace.erl rename to src/emqx_tracer.erl diff --git a/src/emqx_trace_sup.erl b/src/emqx_tracer_sup.erl similarity index 81% rename from src/emqx_trace_sup.erl rename to src/emqx_tracer_sup.erl index e5f7bc2f2..35c0b1d5b 100644 --- a/src/emqx_trace_sup.erl +++ b/src/emqx_tracer_sup.erl @@ -14,21 +14,19 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_trace_sup). +-module(emqx_tracer_sup). -behaviour(supervisor). -%% API -export([start_link/0]). -%% Supervisor callbacks -export([init/1]). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - Trace = {trace, {emqx_trace, start_link, []}, - permanent, 5000, worker, [emqx_trace]}, - {ok, {{one_for_one, 10, 3600}, [Trace]}}. + Tracer = {tracer, {emqx_tracer, start_link, []}, + permanent, 5000, worker, [emqx_tracer]}, + {ok, {{one_for_one, 10, 3600}, [Tracer]}}. From f007f69abe1e76c3f8966c7e02051969f3bf96ac Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 23 Mar 2018 16:45:50 +0800 Subject: [PATCH 031/520] Update emqx.app.src --- src/emqx.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx.app.src b/src/emqx.app.src index 4a649331f..8798b6eab 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -3,7 +3,7 @@ {vsn,"3.0"}, {modules,[]}, {registered,[emqx_sup]}, - {applications,[kernel,stdlib,gproc,gen_rpc,lager,esockd,mochiweb,lager_syslog,pbkdf2,bcrypt,clique,jsx]}, + {applications,[kernel,stdlib,gproc,lager,esockd,mochiweb,lager_syslog,pbkdf2,bcrypt,clique,jsx]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, From 9976327c8d0c40597e68b7031bf9f6ea9eb7ed42 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 23 Mar 2018 18:13:09 +0800 Subject: [PATCH 032/520] Add emqx_mqtt module --- include/emqx.hrl | 9 ++- src/emqx.erl | 88 ---------------------------- src/emqx_app.erl | 8 ++- src/emqx_bridge.erl | 8 +-- src/emqx_connection.erl | 4 +- src/emqx_message.erl | 112 ++++++++++++++++++------------------ src/emqx_metrics.erl | 12 ++-- src/emqx_mod_rewrite.erl | 4 +- src/emqx_mqtt.erl | 115 +++++++++++++++++++++++++++++++++++++ src/emqx_mqtt_app.erl | 29 ---------- src/emqx_mqueue.erl | 8 +-- src/emqx_protocol.erl | 22 +++---- src/emqx_session.erl | 42 +++++++------- src/emqx_stats.erl | 5 +- src/emqx_sup.erl | 5 +- src/emqx_ws_connection.erl | 4 +- 16 files changed, 244 insertions(+), 231 deletions(-) create mode 100644 src/emqx_mqtt.erl delete mode 100644 src/emqx_mqtt_app.erl diff --git a/include/emqx.hrl b/include/emqx.hrl index 5c67236d0..d584f2cd3 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -76,7 +76,6 @@ -type(message_from() :: #{zone := atom(), node := atom(), clientid := binary(), - protocol := protocol(), connector => atom(), peername => {inet:ip_address(), inet:port_number()}, username => binary(), @@ -99,8 +98,14 @@ { id :: message_id(), %% Global unique id from :: message_from(), %% Message from sender :: pid(), %% The pid of the sender/publisher - flags :: message_flags(), %% Message flags + packet_id, + dup :: boolean(), %% Dup flag + qos :: 0 | 1 | 2, %% QoS + sys :: boolean(), %% $SYS flag + retain :: boolean(), %% Retain flag + flags = [], %% :: message_flags(), %% Message flags headers :: message_headers(), %% Message headers + protocol :: protocol(), topic :: binary(), %% Message topic properties :: map(), %% Message user properties payload :: binary(), %% Message payload diff --git a/src/emqx.erl b/src/emqx.erl index 967478a8c..13fe0db90 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -18,16 +18,9 @@ -include("emqx.hrl"). --include("emqx_mqtt.hrl"). - %% Start/Stop Application -export([start/0, env/1, env/2, is_running/1, stop/0]). -%% Start/Stop Listeners --export([start_listeners/0, start_listener/1, listeners/0, - stop_listeners/0, stop_listener/1, - restart_listeners/0, restart_listener/1]). - %% PubSub API -export([subscribe/1, subscribe/2, subscribe/3, publish/1, unsubscribe/1, unsubscribe/2]). @@ -47,8 +40,6 @@ %% Shutdown and reboot -export([shutdown/0, shutdown/1, reboot/0]). --type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}). - -define(APP, ?MODULE). %%-------------------------------------------------------------------- @@ -80,85 +71,6 @@ is_running(Node) -> Pid when is_pid(Pid) -> true end. -%%-------------------------------------------------------------------- -%% Start/Stop Listeners -%%-------------------------------------------------------------------- - -%% @doc Start Listeners. --spec(start_listeners() -> ok). -start_listeners() -> lists:foreach(fun start_listener/1, env(listeners, [])). - -%% Start mqtt listener --spec(start_listener(listener()) -> {ok, pid()} | {error, any()}). -start_listener({tcp, ListenOn, Opts}) -> - start_listener('mqtt:tcp', ListenOn, Opts); - -%% Start mqtt(SSL) listener -start_listener({ssl, ListenOn, Opts}) -> - start_listener('mqtt:ssl', ListenOn, Opts); - -%% Start http listener -start_listener({Proto, ListenOn, Opts}) when Proto == http; Proto == ws -> - {ok, _} = mochiweb:start_http('mqtt:ws', ListenOn, Opts, {emqx_ws, handle_request, []}); - -%% Start https listener -start_listener({Proto, ListenOn, Opts}) when Proto == https; Proto == wss -> - {ok, _} = mochiweb:start_http('mqtt:wss', ListenOn, Opts, {emqx_ws, handle_request, []}). - -start_listener(Proto, ListenOn, Opts) -> - Env = lists:append(emqx:env(client, []), emqx:env(protocol, [])), - MFArgs = {emqx_connection, start_link, [Env]}, - {ok, _} = esockd:open(Proto, ListenOn, merge_sockopts(Opts), MFArgs). - -listeners() -> - [Listener || Listener = {{Proto, _}, _Pid} <- esockd:listeners(), is_mqtt(Proto)]. - -is_mqtt('mqtt:tcp') -> true; -is_mqtt('mqtt:ssl') -> true; -is_mqtt('mqtt:ws') -> true; -is_mqtt('mqtt:wss') -> true; -is_mqtt(_Proto) -> false. - -%% @doc Stop Listeners --spec(stop_listeners() -> ok). -stop_listeners() -> lists:foreach(fun stop_listener/1, env(listeners, [])). - --spec(stop_listener(listener()) -> ok | {error, any()}). -stop_listener({tcp, ListenOn, _Opts}) -> - esockd:close('mqtt:tcp', ListenOn); -stop_listener({ssl, ListenOn, _Opts}) -> - esockd:close('mqtt:ssl', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> - mochiweb:stop_http('mqtt:ws', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> - mochiweb:stop_http('mqtt:wss', ListenOn); -% stop_listener({Proto, ListenOn, _Opts}) when Proto == api -> -% mochiweb:stop_http('mqtt:api', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) -> - esockd:close(Proto, ListenOn). - -%% @doc Restart Listeners --spec(restart_listeners() -> ok). -restart_listeners() -> lists:foreach(fun restart_listener/1, env(listeners, [])). - --spec(restart_listener(listener()) -> any()). -restart_listener({tcp, ListenOn, _Opts}) -> - esockd:reopen('mqtt:tcp', ListenOn); -restart_listener({ssl, ListenOn, _Opts}) -> - esockd:reopen('mqtt:ssl', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> - mochiweb:restart_http('mqtt:ws', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> - mochiweb:restart_http('mqtt:wss', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == api -> - mochiweb:restart_http('mqtt:api', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) -> - esockd:reopen(Proto, ListenOn). - -merge_sockopts(Options) -> - SockOpts = emqx_misc:merge_opts( - ?MQTT_SOCKOPTS, proplists:get_value(sockopts, Options, [])), - emqx_misc:merge_opts(Options, [{sockopts, SockOpts}]). %%-------------------------------------------------------------------- %% PubSub API diff --git a/src/emqx_app.erl b/src/emqx_app.erl index 00acf0d76..50c9810c6 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -26,13 +26,15 @@ -define(APP, emqx). %%-------------------------------------------------------------------- -%% Application Callbacks +%% Application callbacks %%-------------------------------------------------------------------- start(_Type, _Args) -> print_banner(), ekka:start(), {ok, Sup} = emqx_sup:start_link(), + %%TODO: fixme later + emqx_mqtt_metrics:init(), ok = register_acl_mod(), emqx_modules:load(), start_autocluster(), @@ -43,7 +45,7 @@ start(_Type, _Args) -> -spec(stop(State :: term()) -> term()). stop(_State) -> emqx_modules:unload(), - catch emqx:stop_listeners(). + catch emqx_mqtt:shutdown(). %%-------------------------------------------------------------------- %% Print Banner @@ -78,5 +80,5 @@ start_autocluster() -> after_autocluster() -> emqx_plugins:init(), emqx_plugins:load(), - emqx:start_listeners(). + emqx_mqtt:bootstrap(). diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index 098b8a41a..5c20538f3 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -167,11 +167,11 @@ dequeue(State = #state{mqueue = MQ}) -> {empty, MQ1} -> State#state{mqueue = MQ1}; {{value, Msg}, MQ1} -> - handle_info({dispatch, Msg#mqtt_message.topic, Msg}, State), + handle_info({dispatch, Msg#message.topic, Msg}, State), dequeue(State#state{mqueue = MQ1}) end. -transform(Msg = #mqtt_message{topic = Topic}, #state{topic_prefix = Prefix, - topic_suffix = Suffix}) -> - Msg#mqtt_message{topic = <>}. +transform(Msg = #message{topic = Topic}, #state{topic_prefix = Prefix, + topic_suffix = Suffix}) -> + Msg#message{topic = <>}. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 2b2f36dee..1555f29cb 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -203,7 +203,7 @@ handle_info({suback, PacketId, GrantedQos}, State) -> %% Fastlane handle_info({dispatch, _Topic, Message}, State) -> - handle_info({deliver, Message#mqtt_message{qos = ?QOS_0}}, State); + handle_info({deliver, Message#message{qos = ?QOS_0}}, State); handle_info({deliver, Message}, State) -> with_proto( @@ -316,7 +316,7 @@ received(Bytes, State = #state{parser = Parser, {more, NewParser} -> {noreply, run_socket(State#state{parser = NewParser}), IdleTimeout}; {ok, Packet, Rest} -> - emqx_metrics:received(Packet), + emqx_mqtt_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> received(Rest, State#state{parser = emqx_parser:initial_state(PacketSize), diff --git a/src/emqx_message.erl b/src/emqx_message.erl index acbee1ce7..5ef4f8f8f 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -30,21 +30,21 @@ -type(msg_from() :: atom() | {binary(), undefined | binary()}). %% @doc Make a message --spec(make(msg_from(), binary(), binary()) -> mqtt_message()). +-spec(make(msg_from(), binary(), binary()) -> message()). make(From, Topic, Payload) -> make(From, ?QOS_0, Topic, Payload). --spec(make(msg_from(), mqtt_qos(), binary(), binary()) -> mqtt_message()). +-spec(make(msg_from(), mqtt_qos(), binary(), binary()) -> message()). make(From, Qos, Topic, Payload) -> - #mqtt_message{id = msgid(), - from = From, - qos = ?QOS_I(Qos), - topic = Topic, - payload = Payload, - timestamp = os:timestamp()}. + #message{id = msgid(), + from = From, + qos = ?QOS_I(Qos), + topic = Topic, + payload = Payload, + timestamp = os:timestamp()}. %% @doc Message from Packet --spec(from_packet(mqtt_packet()) -> mqtt_message()). +-spec(from_packet(mqtt_packet()) -> message()). from_packet(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, retain = Retain, qos = Qos, @@ -52,14 +52,14 @@ from_packet(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, variable = #mqtt_packet_publish{topic_name = Topic, packet_id = PacketId}, payload = Payload}) -> - #mqtt_message{id = msgid(), - packet_id = PacketId, - qos = Qos, - retain = Retain, - dup = Dup, - topic = Topic, - payload = Payload, - timestamp = os:timestamp()}; + #message{id = msgid(), + packet_id = PacketId, + qos = Qos, + retain = Retain, + dup = Dup, + topic = Topic, + payload = Payload, + timestamp = os:timestamp()}; from_packet(#mqtt_packet_connect{will_flag = false}) -> undefined; @@ -70,33 +70,33 @@ from_packet(#mqtt_packet_connect{client_id = ClientId, will_qos = Qos, will_topic = Topic, will_msg = Msg}) -> - #mqtt_message{id = msgid(), - topic = Topic, - from = {ClientId, Username}, - retain = Retain, - qos = Qos, - dup = false, - payload = Msg, - timestamp = os:timestamp()}. + #message{id = msgid(), + topic = Topic, + from = {ClientId, Username}, + retain = Retain, + qos = Qos, + dup = false, + payload = Msg, + timestamp = os:timestamp()}. from_packet(ClientId, Packet) -> Msg = from_packet(Packet), - Msg#mqtt_message{from = ClientId}. + Msg#message{from = ClientId}. from_packet(Username, ClientId, Packet) -> Msg = from_packet(Packet), - Msg#mqtt_message{from = {ClientId, Username}}. + Msg#message{from = {ClientId, Username}}. msgid() -> emqx_guid:gen(). %% @doc Message to Packet --spec(to_packet(mqtt_message()) -> mqtt_packet()). -to_packet(#mqtt_message{packet_id = PkgId, - qos = Qos, - retain = Retain, - dup = Dup, - topic = Topic, - payload = Payload}) -> +-spec(to_packet(message()) -> mqtt_packet()). +to_packet(#message{packet_id = PkgId, + qos = Qos, + retain = Retain, + dup = Dup, + topic = Topic, + payload = Payload}) -> #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = Qos, @@ -111,40 +111,42 @@ to_packet(#mqtt_message{packet_id = PkgId, payload = Payload}. %% @doc set dup, retain flag --spec(set_flag(mqtt_message()) -> mqtt_message()). +-spec(set_flag(message()) -> message()). set_flag(Msg) -> - Msg#mqtt_message{dup = true, retain = true}. + Msg#message{dup = true, retain = true}. --spec(set_flag(atom(), mqtt_message()) -> mqtt_message()). -set_flag(dup, Msg = #mqtt_message{dup = false}) -> - Msg#mqtt_message{dup = true}; -set_flag(sys, Msg = #mqtt_message{sys = false}) -> - Msg#mqtt_message{sys = true}; -set_flag(retain, Msg = #mqtt_message{retain = false}) -> - Msg#mqtt_message{retain = true}; -set_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg. +-spec(set_flag(atom(), message()) -> message()). +set_flag(dup, Msg = #message{dup = false}) -> + Msg#message{dup = true}; +set_flag(sys, Msg = #message{sys = false}) -> + Msg#message{sys = true}; +set_flag(retain, Msg = #message{retain = false}) -> + Msg#message{retain = true}; +set_flag(Flag, Msg) when Flag =:= dup; + Flag =:= retain; + Flag =:= sys -> Msg. %% @doc Unset dup, retain flag --spec(unset_flag(mqtt_message()) -> mqtt_message()). +-spec(unset_flag(message()) -> message()). unset_flag(Msg) -> - Msg#mqtt_message{dup = false, retain = false}. + Msg#message{dup = false, retain = false}. --spec(unset_flag(dup | retain | atom(), mqtt_message()) -> mqtt_message()). -unset_flag(dup, Msg = #mqtt_message{dup = true}) -> - Msg#mqtt_message{dup = false}; -unset_flag(retain, Msg = #mqtt_message{retain = true}) -> - Msg#mqtt_message{retain = false}; +-spec(unset_flag(dup | retain | atom(), message()) -> message()). +unset_flag(dup, Msg = #message{dup = true}) -> + Msg#message{dup = false}; +unset_flag(retain, Msg = #message{retain = true}) -> + Msg#message{retain = false}; unset_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg. %% @doc Format MQTT Message -format(#mqtt_message{id = MsgId, packet_id = PktId, from = {ClientId, Username}, - qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> +format(#message{id = MsgId, packet_id = PktId, from = {ClientId, Username}, + qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s/~s, Topic=~s)", [i(Qos), i(Retain), i(Dup), MsgId, PktId, Username, ClientId, Topic]); %% TODO:... -format(#mqtt_message{id = MsgId, packet_id = PktId, from = From, - qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> +format(#message{id = MsgId, packet_id = PktId, from = From, + qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s, Topic=~s)", [i(Qos), i(Retain), i(Dup), MsgId, PktId, From, Topic]). diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index f74537218..cf41669ed 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -29,7 +29,7 @@ -record(state, {tick}). --define(TAB, ?MODULE). +-define(TAB, metrics). -define(SERVER, ?MODULE). @@ -117,9 +117,10 @@ key(counter, Metric) -> init([]) -> emqx_time:seed(), % Create metrics table - ets:new(?TAB, [set, public, named_table, {write_concurrency, true}]), + _ = ets:new(?TAB, [set, public, named_table, {write_concurrency, true}]), % Tick to publish metrics - {ok, #state{tick = emqx_broker:start_tick(tick)}, hibernate}. + {ok, TRef} = timer:send_after(emqx_sys:sys_interval(), tick), + {ok, #state{tick = TRef}, hibernate}. handle_call(_Req, _From, State) -> {reply, error, State}. @@ -136,7 +137,8 @@ handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, #state{tick = TRef}) -> - emqx_broker:stop_tick(TRef). + %%TODO: + timer:cancel(TRef). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -145,6 +147,8 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- +%% TODO: the depencies are not right + publish(Metric, Val) -> Msg = emqx_message:make(metrics, metric_topic(Metric), bin(Val)), emqx_broker:publish(emqx_message:set_flag(sys, Msg)). diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index 87e70021e..9a344aed5 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -42,7 +42,7 @@ rewrite_unsubscribe(_ClientId, _Username, TopicTable, Rules) -> lager:info("Rewrite unsubscribe: ~p", [TopicTable]), {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. -rewrite_publish(Message=#mqtt_message{topic = Topic}, Rules) -> +rewrite_publish(Message = #message{topic = Topic}, Rules) -> %%TODO: this will not work if the client is always online. RewriteTopic = case get({rewrite, Topic}) of @@ -52,7 +52,7 @@ rewrite_publish(Message=#mqtt_message{topic = Topic}, Rules) -> DestTopic -> DestTopic end, - {ok, Message#mqtt_message{topic = RewriteTopic}}. + {ok, Message#message{topic = RewriteTopic}}. unload(_) -> emqx:unhook('client.subscribe', fun ?MODULE:rewrite_subscribe/4), diff --git a/src/emqx_mqtt.erl b/src/emqx_mqtt.erl new file mode 100644 index 000000000..8bb1a53bd --- /dev/null +++ b/src/emqx_mqtt.erl @@ -0,0 +1,115 @@ +%%-------------------------------------------------------------------- +%% Copyright © 2013-2018 EMQ Inc. 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). + +-include("emqx_mqtt.hrl"). + +-export([bootstrap/0, shutdown/0]). + +-export([start_listeners/0, start_listener/1]). +-export([stop_listeners/0, stop_listener/1]). +-export([restart_listeners/0, restart_listener/1]). +-export([listeners/0]). + +-type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}). + +bootstrap() -> + start_listeners(). + +shutdown() -> + stop_listeners(). + +%%-------------------------------------------------------------------- +%% Start/Stop Listeners +%%-------------------------------------------------------------------- + +%% @doc Start Listeners. +-spec(start_listeners() -> ok). +start_listeners() -> + lists:foreach(fun start_listener/1, emqx:env(listeners, [])). + +%% Start mqtt listener +-spec(start_listener(listener()) -> {ok, pid()} | {error, any()}). +start_listener({tcp, ListenOn, Opts}) -> + start_listener('mqtt:tcp', ListenOn, Opts); + +%% Start mqtt(SSL) listener +start_listener({ssl, ListenOn, Opts}) -> + start_listener('mqtt:ssl', ListenOn, Opts); + +%% Start http listener +start_listener({Proto, ListenOn, Opts}) when Proto == http; Proto == ws -> + {ok, _} = mochiweb:start_http('mqtt:ws', ListenOn, Opts, {emqx_ws, handle_request, []}); + +%% Start https listener +start_listener({Proto, ListenOn, Opts}) when Proto == https; Proto == wss -> + {ok, _} = mochiweb:start_http('mqtt:wss', ListenOn, Opts, {emqx_ws, handle_request, []}). + +start_listener(Proto, ListenOn, Opts) -> + Env = lists:append(emqx:env(client, []), emqx:env(protocol, [])), + MFArgs = {emqx_connection, start_link, [Env]}, + {ok, _} = esockd:open(Proto, ListenOn, merge_sockopts(Opts), MFArgs). + +listeners() -> + [Listener || Listener = {{Proto, _}, _Pid} <- esockd:listeners(), is_mqtt(Proto)]. + +is_mqtt('mqtt:tcp') -> true; +is_mqtt('mqtt:ssl') -> true; +is_mqtt('mqtt:ws') -> true; +is_mqtt('mqtt:wss') -> true; +is_mqtt(_Proto) -> false. + +%% @doc Stop Listeners +-spec(stop_listeners() -> ok). +stop_listeners() -> lists:foreach(fun stop_listener/1, emqx:env(listeners, [])). + +-spec(stop_listener(listener()) -> ok | {error, any()}). +stop_listener({tcp, ListenOn, _Opts}) -> + esockd:close('mqtt:tcp', ListenOn); +stop_listener({ssl, ListenOn, _Opts}) -> + esockd:close('mqtt:ssl', ListenOn); +stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> + mochiweb:stop_http('mqtt:ws', ListenOn); +stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> + mochiweb:stop_http('mqtt:wss', ListenOn); +% stop_listener({Proto, ListenOn, _Opts}) when Proto == api -> +% mochiweb:stop_http('mqtt:api', ListenOn); +stop_listener({Proto, ListenOn, _Opts}) -> + esockd:close(Proto, ListenOn). + +%% @doc Restart Listeners +-spec(restart_listeners() -> ok). +restart_listeners() -> lists:foreach(fun restart_listener/1, emqx:env(listeners, [])). + +-spec(restart_listener(listener()) -> any()). +restart_listener({tcp, ListenOn, _Opts}) -> + esockd:reopen('mqtt:tcp', ListenOn); +restart_listener({ssl, ListenOn, _Opts}) -> + esockd:reopen('mqtt:ssl', ListenOn); +restart_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> + mochiweb:restart_http('mqtt:ws', ListenOn); +restart_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> + mochiweb:restart_http('mqtt:wss', ListenOn); +restart_listener({Proto, ListenOn, _Opts}) when Proto == api -> + mochiweb:restart_http('mqtt:api', ListenOn); +restart_listener({Proto, ListenOn, _Opts}) -> + esockd:reopen(Proto, ListenOn). + +merge_sockopts(Options) -> + SockOpts = emqx_misc:merge_opts( + ?MQTT_SOCKOPTS, proplists:get_value(sockopts, Options, [])), + emqx_misc:merge_opts(Options, [{sockopts, SockOpts}]). diff --git a/src/emqx_mqtt_app.erl b/src/emqx_mqtt_app.erl deleted file mode 100644 index 47e1eaba0..000000000 --- a/src/emqx_mqtt_app.erl +++ /dev/null @@ -1,29 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. 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_app). - --behaviour(application). - --export([start/2, stop/1]). - -start(_Type, _Args) -> - emqx_mqtt_metrics:init(), - emqx_mqtt_sup:start_link(). - -stop(_State) -> - ok. - diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index eb334ed22..acdb5ee8c 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -154,8 +154,8 @@ stats(#mqueue{type = Type, q = Q, max_len = MaxLen, len = Len, dropped = Dropped end} | [{max_len, MaxLen}, {dropped, Dropped}]]. %% @doc Enqueue a message. --spec(in(mqtt_message(), mqueue()) -> mqueue()). -in(#mqtt_message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> +-spec(in(message(), mqueue()) -> mqueue()). +in(#message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> MQ; in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}; @@ -166,7 +166,7 @@ in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = MaxLen, dropped in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len}) -> maybe_set_alarm(MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}); -in(Msg = #mqtt_message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, +in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, priorities = Priorities, max_len = 0}) -> case lists:keysearch(Topic, 1, Priorities) of @@ -176,7 +176,7 @@ in(Msg = #mqtt_message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, {Pri, MQ1} = insert_p(Topic, 0, MQ), MQ1#mqueue{q = ?PQUEUE:in(Msg, Pri, Q)} end; -in(Msg = #mqtt_message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, +in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, priorities = Priorities, max_len = MaxLen}) -> case lists:keysearch(Topic, 1, Priorities) of diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 7f58dee51..b61040443 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -120,7 +120,7 @@ client(#proto_state{client_id = ClientId, connected_at = Time}) -> WillTopic = if WillMsg =:= undefined -> undefined; - true -> WillMsg#mqtt_message.topic + true -> WillMsg#message.topic end, #mqtt_client{client_id = ClientId, client_pid = ClientPid, @@ -352,18 +352,18 @@ with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), ?LOG(error, "PUBLISH ~p error: ~p", [PacketId, Error], State) end. --spec(send(mqtt_message() | mqtt_packet(), proto_state()) -> {ok, proto_state()}). +-spec(send(message() | mqtt_packet(), proto_state()) -> {ok, proto_state()}). send(Msg, State = #proto_state{client_id = ClientId, username = Username, mountpoint = MountPoint, is_bridge = IsBridge}) - when is_record(Msg, mqtt_message) -> + when is_record(Msg, message) -> emqx_hooks:run('message.delivered', [ClientId, Username], Msg), send(emqx_message:to_packet(unmount(MountPoint, clean_retain(IsBridge, Msg))), State); send(Packet = ?PACKET(Type), State = #proto_state{sendfun = SendFun, stats_data = Stats}) -> trace(send, Packet, State), - emqx_metrics:sent(Packet), + emqx_mqtt_metrics:sent(Packet), SendFun(Packet), {ok, State#proto_state{stats_data = inc_stats(send, Type, Stats)}}. @@ -439,7 +439,7 @@ maybe_set_clientid(State) -> send_willmsg(_Client, undefined) -> ignore; send_willmsg(#mqtt_client{client_id = ClientId, username = Username}, WillMsg) -> - emqx_broker:publish(WillMsg#mqtt_message{from = {ClientId, Username}}). + emqx_broker:publish(WillMsg#message{from = {ClientId, Username}}). start_keepalive(0, _State) -> ignore; @@ -570,10 +570,10 @@ sp(false) -> 0. %% The retained flag should be propagated for bridge. %%-------------------------------------------------------------------- -clean_retain(false, Msg = #mqtt_message{retain = true, headers = Headers}) -> +clean_retain(false, Msg = #message{retain = true, headers = Headers}) -> case lists:member(retained, Headers) of true -> Msg; - false -> Msg#mqtt_message{retain = false} + false -> Msg#message{retain = false} end; clean_retain(_IsBridge, Msg) -> Msg. @@ -596,16 +596,16 @@ feed_var({<<"%u">>, Username}, MountPoint) -> mount(undefined, Any) -> Any; -mount(MountPoint, Msg = #mqtt_message{topic = Topic}) -> - Msg#mqtt_message{topic = <>}; +mount(MountPoint, Msg = #message{topic = Topic}) -> + Msg#message{topic = <>}; mount(MountPoint, TopicTable) when is_list(TopicTable) -> [{<>, Opts} || {Topic, Opts} <- TopicTable]. unmount(undefined, Any) -> Any; -unmount(MountPoint, Msg = #mqtt_message{topic = Topic}) -> +unmount(MountPoint, Msg = #message{topic = Topic}) -> case catch split_binary(Topic, byte_size(MountPoint)) of - {MountPoint, Topic0} -> Msg#mqtt_message{topic = Topic0}; + {MountPoint, Topic0} -> Msg#message{topic = Topic0}; _ -> Msg end. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 611e967be..1c18907bc 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -185,15 +185,15 @@ subscribe(Session, PacketId, TopicTable) -> %%TODO: the ack function??... %% @doc Publish Message -spec(publish(pid(), message()) -> ok | {error, term()}). -publish(_Session, Msg = #mqtt_message{qos = ?QOS_0}) -> +publish(_Session, Msg = #message{qos = ?QOS_0}) -> %% Publish QoS0 Directly emqx_broker:publish(Msg), ok; -publish(_Session, Msg = #mqtt_message{qos = ?QOS_1}) -> +publish(_Session, Msg = #message{qos = ?QOS_1}) -> %% Publish QoS1 message directly for client will PubAck automatically emqx_broker:publish(Msg), ok; -publish(Session, Msg = #mqtt_message{qos = ?QOS_2}) -> +publish(Session, Msg = #message{qos = ?QOS_2}) -> %% Publish QoS2 to Session gen_server:call(Session, {publish, Msg}, ?TIMEOUT). @@ -313,7 +313,7 @@ binding(ClientPid) -> handle_pre_hibernate(State) -> {hibernate, emqx_gc:reset_conn_gc_count(#state.force_gc_count, emit_stats(State))}. -handle_call({publish, Msg = #mqtt_message{qos = ?QOS_2, packet_id = PacketId}}, _From, +handle_call({publish, Msg = #message{qos = ?QOS_2, packet_id = PacketId}}, _From, State = #state{awaiting_rel = AwaitingRel, await_rel_timer = Timer, await_rel_timeout = Timeout}) -> @@ -510,12 +510,12 @@ handle_cast(Msg, State) -> {noreply, State}. %% Ignore Messages delivered by self -handle_info({dispatch, _Topic, #mqtt_message{from = {ClientId, _}}}, +handle_info({dispatch, _Topic, #message{from = {ClientId, _}}}, State = #state{client_id = ClientId, ignore_loop_deliver = true}) -> {noreply, State}; %% Dispatch Message -handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, mqtt_message) -> +handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, message) -> {noreply, gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))}; %% Do nothing if the client has been disconnected. @@ -604,7 +604,7 @@ retry_delivery(Force, [{Type, Msg, Ts} | Msgs], Now, if Force orelse (Diff >= Interval) -> case {Type, Msg} of - {publish, Msg = #mqtt_message{packet_id = PacketId}} -> + {publish, Msg = #message{packet_id = PacketId}} -> redeliver(Msg, State), Inflight1 = Inflight:update(PacketId, {publish, Msg, Now}), retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}); @@ -631,7 +631,7 @@ expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) -> expire_awaiting_rel([], _Now, State) -> State#state{await_rel_timer = undefined}; -expire_awaiting_rel([{PacketId, Msg = #mqtt_message{timestamp = TS}} | Msgs], +expire_awaiting_rel([{PacketId, Msg = #message{timestamp = TS}} | Msgs], Now, State = #state{awaiting_rel = AwaitingRel, await_rel_timeout = Timeout}) -> case (timer:now_diff(Now, TS) div 1000) of @@ -651,8 +651,8 @@ sortfun(inflight) -> fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end; sortfun(awaiting_rel) -> - fun({_, #mqtt_message{timestamp = Ts1}}, - {_, #mqtt_message{timestamp = Ts2}}) -> + fun({_, #message{timestamp = Ts1}}, + {_, #message{timestamp = Ts2}}) -> Ts1 < Ts2 end. @@ -677,17 +677,17 @@ dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) -> end; %% Deliver qos0 message directly to client -dispatch(Msg = #mqtt_message{qos = ?QOS0}, State) -> +dispatch(Msg = #message{qos = ?QOS0}, State) -> deliver(Msg, State), State; -dispatch(Msg = #mqtt_message{qos = QoS}, +dispatch(Msg = #message{qos = QoS}, State = #state{next_msg_id = MsgId, inflight = Inflight}) when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 -> case Inflight:is_full() of true -> enqueue_msg(Msg, State); false -> - Msg1 = Msg#mqtt_message{packet_id = MsgId}, + Msg1 = Msg#message{packet_id = MsgId}, deliver(Msg1, State), await(Msg1, next_msg_id(State)) end. @@ -700,8 +700,8 @@ enqueue_msg(Msg, State = #state{mqueue = Q}) -> %% Deliver %%-------------------------------------------------------------------- -redeliver(Msg = #mqtt_message{qos = QoS}, State) -> - deliver(Msg#mqtt_message{dup = if QoS =:= ?QOS2 -> false; true -> true end}, State); +redeliver(Msg = #message{qos = QoS}, State) -> + deliver(Msg#message{dup = if QoS =:= ?QOS2 -> false; true -> true end}, State); redeliver({pubrel, PacketId}, #state{client_pid = Pid}) -> Pid ! {redeliver, {?PUBREL, PacketId}}. @@ -715,7 +715,7 @@ deliver(Msg, #state{client_pid = Pid, binding = remote}) -> %% Awaiting ACK for QoS1/QoS2 Messages %%-------------------------------------------------------------------- -await(Msg = #mqtt_message{packet_id = PacketId}, +await(Msg = #message{packet_id = PacketId}, State = #state{inflight = Inflight, retry_timer = RetryTimer, retry_interval = Interval}) -> @@ -780,13 +780,13 @@ dequeue2(State = #state{mqueue = Q}) -> %% Tune QoS %%-------------------------------------------------------------------- -tune_qos(Topic, Msg = #mqtt_message{qos = PubQoS}, +tune_qos(Topic, Msg = #message{qos = PubQoS}, #state{subscriptions = SubMap, upgrade_qos = UpgradeQoS}) -> case maps:find(Topic, SubMap) of {ok, SubQoS} when UpgradeQoS andalso (SubQoS > PubQoS) -> - Msg#mqtt_message{qos = SubQoS}; + Msg#message{qos = SubQoS}; {ok, SubQoS} when (not UpgradeQoS) andalso (SubQoS < PubQoS) -> - Msg#mqtt_message{qos = SubQoS}; + Msg#message{qos = SubQoS}; {ok, _} -> Msg; error -> @@ -797,8 +797,8 @@ tune_qos(Topic, Msg = #mqtt_message{qos = PubQoS}, %% Reset Dup %%-------------------------------------------------------------------- -reset_dup(Msg = #mqtt_message{dup = true}) -> - Msg#mqtt_message{dup = false}; +reset_dup(Msg = #message{dup = true}) -> + Msg#message{dup = false}; reset_dup(Msg) -> Msg. %%-------------------------------------------------------------------- diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index 219914997..d93c1b969 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -163,7 +163,8 @@ init([]) -> Topics = ?SYSTOP_CLIENTS ++ ?SYSTOP_SESSIONS ++ ?SYSTOP_PUBSUB ++ ?SYSTOP_RETAINED, ets:insert(?STATS_TAB, [{Topic, 0} || Topic <- Topics]), % Tick to publish stats - {ok, #state{tick = emqx_broker:start_tick(tick)}, hibernate}. + {ok, TRef} = timer:send_after(emqx_sys:sys_interval(), tick), + {ok, #state{tick = TRef}, hibernate}. handle_call(stop, _From, State) -> {stop, normal, ok, State}; @@ -194,7 +195,7 @@ handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, #state{tick = TRef}) -> - emqx_broker:stop_tick(TRef). + timer:cancel(TRef). code_change(_OldVsn, State, _Extra) -> {ok, State}. diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index ff632d4e7..d06a8df79 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -54,16 +54,17 @@ stop_child(ChildId) -> %%-------------------------------------------------------------------- init([]) -> - {ok, {{one_for_all, 0, 1}, + {ok, {{one_for_all, 10, 3600}, [?CHILD(emqx_ctl, worker), ?CHILD(emqx_hooks, worker), ?CHILD(emqx_locker, worker), ?CHILD(emqx_stats, worker), ?CHILD(emqx_metrics, worker), + ?CHILD(emqx_sys, worker), ?CHILD(emqx_router_sup, supervisor), ?CHILD(emqx_broker_sup, supervisor), ?CHILD(emqx_pooler, supervisor), - ?CHILD(emqx_trace_sup, supervisor), + ?CHILD(emqx_tracer_sup, supervisor), ?CHILD(emqx_cm_sup, supervisor), ?CHILD(emqx_sm_sup, supervisor), ?CHILD(emqx_session_sup, supervisor), diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index bee19841a..fc632cb6a 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -140,7 +140,7 @@ handle_call(Req, _From, State) -> reply({error, unexpected_request}, State). handle_cast({received, Packet}, State = #wsclient_state{proto_state = ProtoState}) -> - emqx_metrics:received(Packet), + emqx_mqtt_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> {noreply, gc(State#wsclient_state{proto_state = ProtoState1}), hibernate}; @@ -178,7 +178,7 @@ handle_info({suback, PacketId, GrantedQos}, State) -> %% Fastlane handle_info({dispatch, _Topic, Message}, State) -> - handle_info({deliver, Message#mqtt_message{qos = ?QOS_0}}, State); + handle_info({deliver, Message#message{qos = ?QOS_0}}, State); handle_info({deliver, Message}, State) -> with_proto( From c79fa29587f539b8c3db2619b8a4e338c4aaef9f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 30 Mar 2018 17:17:18 +0800 Subject: [PATCH 033/520] Fix the schema of lager console backend --- priv/emqx.schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index 8c0a35397..8aeeb0ebf 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -434,7 +434,7 @@ end}. ConsoleLogLevel = cuttlefish:conf_get("log.console.level", Conf), ConsoleLogFile = cuttlefish:conf_get("log.console.file", Conf), - ConsoleHandler = {lager_console_backend, [{level, ConsoleLogLevel}]}, + ConsoleHandler = {lager_console_backend, [ConsoleLogLevel]}, ConsoleFileHandler = {lager_file_backend, [{file, ConsoleLogFile}, {level, ConsoleLogLevel}, {size, cuttlefish:conf_get("log.console.size", Conf)}, From 2eed46310c58bf80697b308255e72f972eb942d5 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 30 Mar 2018 17:18:08 +0800 Subject: [PATCH 034/520] Misc fix for the MQTT Version 5.0 --- include/emqx.hrl | 196 ++++++++++++++++------------------ include/emqx_mqtt.hrl | 26 ++++- src/emqx_access_control.erl | 29 ++--- src/emqx_access_rule.erl | 18 ++-- src/emqx_acl_internal.erl | 2 +- src/emqx_acl_mod.erl | 5 +- src/emqx_auth_mod.erl | 2 +- src/emqx_cm.erl | 116 +++++++++----------- src/emqx_cm_sup.erl | 27 ++--- src/emqx_connection.erl | 4 +- src/emqx_message.erl | 167 ++++++++--------------------- src/emqx_mod_presence.erl | 49 +++++---- src/emqx_mod_subscription.erl | 8 +- src/emqx_packet.erl | 46 +++++++- src/emqx_protocol.erl | 61 +++++------ src/emqx_rpc.erl | 7 +- src/emqx_serializer.erl | 1 + src/emqx_session.erl | 82 +++++++------- src/emqx_sm.erl | 15 ++- src/emqx_sm_locker.erl | 4 +- src/emqx_sm_sup.erl | 11 +- src/emqx_tracer.erl | 4 +- 22 files changed, 429 insertions(+), 451 deletions(-) diff --git a/include/emqx.hrl b/include/emqx.hrl index d584f2cd3..ddd91dbca 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -18,97 +18,118 @@ %% Banner %%-------------------------------------------------------------------- --define(COPYRIGHT, "Copyright (c) 2013-2018 EMQ Enterprise, Inc."). +-define(COPYRIGHT, "Copyright (c) 2013-2018 EMQ Inc."). -define(LICENSE_MESSAGE, "Licensed under the Apache License, Version 2.0"). -define(PROTOCOL_VERSION, "MQTT/5.0"). -%%-define(ERTS_MINIMUM, "9.0"). +-define(ERTS_MINIMUM_REQUIRED, "9.2"). %%-------------------------------------------------------------------- -%% Sys/Queue/Share Topics' Prefix +%% Topics' prefix: $SYS | $queue | $share %%-------------------------------------------------------------------- --define(SYSTOP, <<"$SYS/">>). %% System Topic +%% System Topic +-define(SYSTOP, <<"$SYS/">>). --define(QUEUE, <<"$queue/">>). %% Queue Topic +%% Queue Topic +-define(QUEUE, <<"$queue/">>). --define(SHARE, <<"$share/">>). %% Shared Topic +%% Shared Topic +-define(SHARE, <<"$share/">>). %%-------------------------------------------------------------------- -%% Client and Session +%% Topic, subscription and subscriber %%-------------------------------------------------------------------- +-type(qos() :: integer()). + -type(topic() :: binary()). --type(subscriber() :: pid() | binary() | {binary(), pid()}). +-type(suboption() :: {qos, qos()} + | {share, '$queue'} + | {share, binary()} + | {atom(), term()}). --type(suboption() :: {qos, non_neg_integer()} | {share, {'$queue' | binary()}}). +-record(subscription, + { subid :: binary() | atom(), + topic :: topic(), + subopts :: list(suboption()) + }). + +-type(subscription() :: #subscription{}). + +-type(subscriber() :: binary() | pid() | {binary(), pid()}). + +%%-------------------------------------------------------------------- +%% Client and session +%%-------------------------------------------------------------------- + +-type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). + +-type(peername() :: {inet:ip_address(), inet:port_number()}). -type(client_id() :: binary() | atom()). --type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | atom()). +-type(username() :: binary() | atom()). --type(client() :: #{zone := atom(), - node := atom(), - clientid := client_id(), - protocol := protocol(), - connector => atom(), - peername => {inet:ip_address(), inet:port_number()}, - username => binary(), - atom() => term()}). +-type(mountpoint() :: binary()). --type(session() :: #{client_id := client_id(), - clean_start := boolean(), - expiry_interval := non_neg_integer()}). +-type(connector() :: atom()). --record(session, {client_id, sess_pid}). +-type(zone() :: atom()). + +-record(client, + { id :: client_id(), + pid :: pid(), + zone :: zone(), + node :: node(), + username :: username(), + peername :: peername(), + protocol :: protocol(), + connector :: connector(), + mountpoint :: mountpoint(), + attributes :: #{atom() => term()} + }). + +-type(client() :: #client{}). + +-record(session, + { client_id :: client_id(), + pid :: pid() + }). + +-type(session() :: #session{}). %%-------------------------------------------------------------------- -%% Message and Delivery +%% Message and delivery %%-------------------------------------------------------------------- -type(message_id() :: binary() | undefined). -%% -type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | atom()). +-type(message_flag() :: sys | dup | retain | atom()). --type(message_from() :: #{zone := atom(), - node := atom(), - clientid := binary(), - connector => atom(), - peername => {inet:ip_address(), inet:port_number()}, - username => binary(), - atom() => term()}). - --type(message_flags() :: #{dup => boolean(), %% Dup flag - qos => 0 | 1 | 2, %% QoS - sys => boolean(), %% $SYS flag - retain => boolean(), %% Retain flag - durable => boolean(), %% Durable flag - atom() => boolean()}). +-type(message_flags() :: #{message_flag() => boolean()}). -type(message_headers() :: #{packet_id => pos_integer(), - priority => pos_integer(), - expiry => integer(), %% Time to live + priority => non_neg_integer(), + ttl => pos_integer(), atom() => term()}). +-type(payload() :: binary()). + %% See 'Application Message' in MQTT Version 5.0 -record(message, { id :: message_id(), %% Global unique id - from :: message_from(), %% Message from + qos :: qos(), %% Message QoS + from :: atom() | client(), %% Message from sender :: pid(), %% The pid of the sender/publisher - packet_id, - dup :: boolean(), %% Dup flag - qos :: 0 | 1 | 2, %% QoS - sys :: boolean(), %% $SYS flag - retain :: boolean(), %% Retain flag - flags = [], %% :: message_flags(), %% Message flags + flags :: message_flags(), %% Message flags headers :: message_headers(), %% Message headers - protocol :: protocol(), topic :: binary(), %% Message topic properties :: map(), %% Message user properties - payload :: binary(), %% Message payload + payload :: payload(), %% Message payload timestamp :: erlang:timestamp() %% Timestamp }). @@ -127,62 +148,16 @@ -type(pubsub() :: publish | subscribe). --define(PS(PS), (PS =:= publish orelse PS =:= subscribe)). - -%%-------------------------------------------------------------------- -%% Subscription -%%-------------------------------------------------------------------- - --record(subscription, - { subid :: binary() | atom(), - topic :: binary(), - subopts :: list() - }). - --type(subscription() :: #subscription{}). - -%%-------------------------------------------------------------------- -%% MQTT Client -%%-------------------------------------------------------------------- - --type(ws_header_key() :: atom() | binary() | string()). --type(ws_header_val() :: atom() | binary() | string() | integer()). - --record(mqtt_client, - { client_id :: binary() | undefined, - client_pid :: pid(), - username :: binary() | undefined, - peername :: {inet:ip_address(), inet:port_number()}, - clean_sess :: boolean(), - proto_ver :: 3 | 4, - keepalive = 0, - will_topic :: undefined | binary(), - ws_initial_headers :: list({ws_header_key(), ws_header_val()}), - mountpoint :: undefined | binary(), - connected_at :: erlang:timestamp(), - %%TODO: Headers - headers = [] :: list() - }). - --type(mqtt_client() :: #mqtt_client{}). - -%%-------------------------------------------------------------------- -%% MQTT Session -%%-------------------------------------------------------------------- - --record(mqtt_session, - { client_id :: binary(), - sess_pid :: pid(), - clean_sess :: boolean() - }). - --type(mqtt_session() :: #mqtt_session{}). +-define(PS(I), (I =:= publish orelse I =:= subscribe)). %%-------------------------------------------------------------------- %% Route %%-------------------------------------------------------------------- --record(route, { topic :: binary(), dest :: {binary(), node()} | node() }). +-record(route, + { topic :: topic(), + dest :: {binary(), node()} | node() + }). -type(route() :: #route{}). @@ -227,7 +202,15 @@ %% Plugin %%-------------------------------------------------------------------- --record(plugin, { name, version, descr, active = false }). +-record(plugin, + { name :: atom(), + version :: string(), + dir :: string(), + descr :: string(), + vendor :: string(), + active :: boolean(), + info :: map() + }). -type(plugin() :: #plugin{}). @@ -235,7 +218,14 @@ %% Command %%-------------------------------------------------------------------- --record(command, { name, action, args = [], opts = [], usage, descr }). +-record(command, + { name, + action, + args = [], + opts = [], + usage, + descr + }). -type(command() :: #command{}). diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 62ee0e7d3..65f6a1274 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -77,6 +77,26 @@ -define(MAX_CLIENTID_LEN, 1024). +%%-------------------------------------------------------------------- +%% MQTT Client +%%-------------------------------------------------------------------- + +-record(mqtt_client, + { client_id :: binary() | undefined, + client_pid :: pid(), + username :: binary() | undefined, + peername :: {inet:ip_address(), inet:port_number()}, + clean_sess :: boolean(), + proto_ver :: 3 | 4, + keepalive = 0, + will_topic :: undefined | binary(), + mountpoint :: undefined | binary(), + connected_at :: erlang:timestamp(), + attributes :: map() + }). + +-type(mqtt_client() :: #mqtt_client{}). + %%-------------------------------------------------------------------- %% MQTT Control Packet Types %%-------------------------------------------------------------------- @@ -278,12 +298,12 @@ -define(CONNACK_PACKET(ReasonCode), #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, - variable = #mqtt_packet_connack{reason_code = ReturnCode}}). + variable = #mqtt_packet_connack{reason_code = ReasonCode}}). -define(CONNACK_PACKET(ReasonCode, SessPresent), #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, - variable = #mqtt_packet_connack{ack_flags = SessPresent, - reason_code = ReturnCode}}). + variable = #mqtt_packet_connack{ack_flags = SessPresent, + reason_code = ReasonCode}}). -define(CONNACK_PACKET(ReasonCode, SessPresent, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 10aa68e91..4d573e283 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -30,7 +30,7 @@ -define(SERVER, ?MODULE). --define(ACCESS_CONTROL_TAB, mqtt_access_control). +-define(TAB, access_control). -type(password() :: undefined | binary()). @@ -45,9 +45,10 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -%% @doc Authenticate MQTT Client. --spec(auth(Client :: mqtt_client(), Password :: password()) -> ok | {ok, boolean()} | {error, term()}). -auth(Client, Password) when is_record(Client, mqtt_client) -> +%% @doc Authenticate Client. +-spec(auth(Client :: client(), Password :: password()) + -> ok | {ok, boolean()} | {error, term()}). +auth(Client, Password) when is_record(Client, client) -> auth(Client, Password, lookup_mods(auth)). auth(_Client, _Password, []) -> case emqx:env(allow_anonymous, false) of @@ -65,7 +66,7 @@ auth(Client, Password, [{Mod, State, _Seq} | Mods]) -> %% @doc Check ACL -spec(check_acl(Client, PubSub, Topic) -> allow | deny when - Client :: mqtt_client(), + Client :: client(), PubSub :: pubsub(), Topic :: binary()). check_acl(Client, PubSub, Topic) when ?PS(PubSub) -> @@ -102,7 +103,7 @@ unregister_mod(Type, Mod) when Type =:= auth; Type =:= acl -> %% @doc Lookup authentication or ACL modules. -spec(lookup_mods(auth | acl) -> list()). lookup_mods(Type) -> - case ets:lookup(?ACCESS_CONTROL_TAB, tab_key(Type)) of + case ets:lookup(?TAB, tab_key(Type)) of [] -> []; [{_, Mods}] -> Mods end. @@ -111,14 +112,15 @@ tab_key(auth) -> auth_modules; tab_key(acl) -> acl_modules. %% @doc Stop access control server. -stop() -> gen_server:call(?MODULE, stop). +stop() -> + gen_server:call(?MODULE, stop). %%-------------------------------------------------------------------- %% gen_server Callbacks %%-------------------------------------------------------------------- init([]) -> - ets:new(?ACCESS_CONTROL_TAB, [set, named_table, protected, {read_concurrency, true}]), + _ = ets:new(?TAB, [set, named_table, protected, {read_concurrency, true}]), {ok, #state{}}. handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) -> @@ -130,7 +132,7 @@ handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) -> NewMods = lists:sort(fun({_, _, Seq1}, {_, _, Seq2}) -> Seq1 >= Seq2 end, [{Mod, ModState, Seq} | Mods]), - ets:insert(?ACCESS_CONTROL_TAB, {tab_key(Type), NewMods}), + ets:insert(?TAB, {tab_key(Type), NewMods}), ok; {error, Error} -> {error, Error}; @@ -145,7 +147,7 @@ handle_call({unregister_mod, Type, Mod}, _From, State) -> false -> {reply, {error, not_found}, State}; _ -> - ets:insert(?ACCESS_CONTROL_TAB, {tab_key(Type), lists:keydelete(Mod, 1, Mods)}), + _ = ets:insert(?TAB, {tab_key(Type), lists:keydelete(Mod, 1, Mods)}), {reply, ok, State} end; @@ -172,7 +174,8 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -if_existed(false, Fun) -> Fun(); - -if_existed(_Mod, _Fun) -> {error, already_existed}. +if_existed(false, Fun) -> + Fun(); +if_existed(_Mod, _Fun) -> + {error, already_existed}. diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 1d54d0fef..63bf3b9ad 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -82,7 +82,7 @@ bin(B) when is_binary(B) -> B. %% @doc Match access rule --spec(match(mqtt_client(), topic(), rule()) -> {matched, allow} | {matched, deny} | nomatch). +-spec(match(client(), topic(), rule()) -> {matched, allow} | {matched, deny} | nomatch). match(_Client, _Topic, {AllowDeny, all}) when (AllowDeny =:= allow) orelse (AllowDeny =:= deny) -> {matched, AllowDeny}; match(Client, Topic, {AllowDeny, Who, _PubSub, TopicFilters}) @@ -99,13 +99,13 @@ match_who(_Client, {user, all}) -> true; match_who(_Client, {client, all}) -> true; -match_who(#mqtt_client{client_id = ClientId}, {client, ClientId}) -> +match_who(#client{id = ClientId}, {client, ClientId}) -> true; -match_who(#mqtt_client{username = Username}, {user, Username}) -> +match_who(#client{username = Username}, {user, Username}) -> true; -match_who(#mqtt_client{peername = undefined}, {ipaddr, _Tup}) -> +match_who(#client{peername = undefined}, {ipaddr, _Tup}) -> false; -match_who(#mqtt_client{peername = {IP, _}}, {ipaddr, CIDR}) -> +match_who(#client{peername = {IP, _}}, {ipaddr, CIDR}) -> esockd_cidr:match(IP, CIDR); match_who(Client, {'and', Conds}) when is_list(Conds) -> lists:foldl(fun(Who, Allow) -> @@ -137,13 +137,13 @@ feed_var(Client, Pattern) -> feed_var(Client, Pattern, []). feed_var(_Client, [], Acc) -> lists:reverse(Acc); -feed_var(Client = #mqtt_client{client_id = undefined}, [<<"%c">>|Words], Acc) -> +feed_var(Client = #client{id = undefined}, [<<"%c">>|Words], Acc) -> feed_var(Client, Words, [<<"%c">>|Acc]); -feed_var(Client = #mqtt_client{client_id = ClientId}, [<<"%c">>|Words], Acc) -> +feed_var(Client = #client{id = ClientId}, [<<"%c">>|Words], Acc) -> feed_var(Client, Words, [ClientId |Acc]); -feed_var(Client = #mqtt_client{username = undefined}, [<<"%u">>|Words], Acc) -> +feed_var(Client = #client{username = undefined}, [<<"%u">>|Words], Acc) -> feed_var(Client, Words, [<<"%u">>|Acc]); -feed_var(Client = #mqtt_client{username = Username}, [<<"%u">>|Words], Acc) -> +feed_var(Client = #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_internal.erl b/src/emqx_acl_internal.erl index 030af96c3..4694f43c3 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -77,7 +77,7 @@ filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) -> %% @doc Check ACL -spec(check_acl({Client, PubSub, Topic}, State) -> allow | deny | ignore when - Client :: mqtt_client(), + Client :: client(), PubSub :: pubsub(), Topic :: binary(), State :: #state{}). diff --git a/src/emqx_acl_mod.erl b/src/emqx_acl_mod.erl index 40f72f898..e01c0ed95 100644 --- a/src/emqx_acl_mod.erl +++ b/src/emqx_acl_mod.erl @@ -26,9 +26,10 @@ -callback(init(AclOpts :: list()) -> {ok, State :: any()}). --callback(check_acl({Client :: mqtt_client(), +-callback(check_acl({Client :: client(), PubSub :: pubsub(), - Topic :: binary()}, State :: any()) -> allow | deny | ignore). + Topic :: topic()}, State :: any()) + -> allow | deny | ignore). -callback(reload_acl(State :: any()) -> ok | {error, term()}). diff --git a/src/emqx_auth_mod.erl b/src/emqx_auth_mod.erl index bc2057a06..2cf222d65 100644 --- a/src/emqx_auth_mod.erl +++ b/src/emqx_auth_mod.erl @@ -30,7 +30,7 @@ -callback(init(AuthOpts :: list()) -> {ok, State :: any()}). --callback(check(Client :: mqtt_client(), +-callback(check(Client :: client(), Password :: binary(), State :: any()) -> ok | {ok, boolean()} | ignore | {error, string()}). diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index c929ecb1f..4d08a499d 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -20,110 +20,93 @@ -include("emqx.hrl"). -%% API Exports --export([start_link/3]). +-export([start_link/1]). --export([lookup/1, lookup_proc/1, reg/1, unreg/1]). +-export([lookup/1, reg/1, unreg/1]). -%% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {pool, id, statsfun, monitors}). +-record(state, {stats_fun, stats_timer, monitors}). --define(POOL, ?MODULE). +-define(SERVER, ?MODULE). %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- -%% @doc Start Client Manager --spec(start_link(atom(), pos_integer(), fun()) -> {ok, pid()} | ignore | {error, term()}). -start_link(Pool, Id, StatsFun) -> - gen_server:start_link(?MODULE, [Pool, Id, StatsFun], []). +%% @doc Start the client manager +-spec(start_link(fun()) -> {ok, pid()} | ignore | {error, term()}). +start_link(StatsFun) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [StatsFun], []). -%% @doc Lookup Client by ClientId --spec(lookup(binary()) -> mqtt_client() | undefined). +%% @doc Lookup ClientPid by ClientId +-spec(lookup(client_id()) -> pid() | undefined). lookup(ClientId) when is_binary(ClientId) -> - case ets:lookup(mqtt_client, ClientId) of [Client] -> Client; [] -> undefined end. - -%% @doc Lookup client pid by clientId --spec(lookup_proc(binary()) -> pid() | undefined). -lookup_proc(ClientId) when is_binary(ClientId) -> - try ets:lookup_element(mqtt_client, ClientId, #mqtt_client.client_pid) + try ets:lookup_element(client, ClientId, 2) catch error:badarg -> undefined end. -%% @doc Register ClientId with Pid. --spec(reg(mqtt_client()) -> ok). -reg(Client = #mqtt_client{client_id = ClientId}) -> - gen_server:call(pick(ClientId), {reg, Client}, 120000). +%% @doc Register a clientId +-spec(reg(client_id()) -> ok). +reg(ClientId) -> + gen_server:cast(?SERVER, {reg, ClientId, self()}). %% @doc Unregister clientId with pid. --spec(unreg(binary()) -> ok). -unreg(ClientId) when is_binary(ClientId) -> - gen_server:cast(pick(ClientId), {unreg, ClientId, self()}). - -pick(ClientId) -> gproc_pool:pick_worker(?POOL, ClientId). +-spec(unreg(client_id()) -> ok). +unreg(ClientId) -> + gen_server:cast(?SERVER, {unreg, ClientId, self()}). %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- -init([Pool, Id, StatsFun]) -> - gproc_pool:connect_worker(Pool, {Pool, Id}), - {ok, #state{pool = Pool, id = Id, statsfun = StatsFun, monitors = dict:new()}}. - -handle_call({reg, Client = #mqtt_client{client_id = ClientId, - client_pid = Pid}}, _From, State) -> - case lookup_proc(ClientId) of - Pid -> - {reply, ok, State}; - _ -> - ets:insert(mqtt_client, Client), - {reply, ok, setstats(monitor_client(ClientId, Pid, State))} - end; +init([StatsFun]) -> + {ok, Ref} = timer:send_interval(timer:seconds(1), stats), + {ok, #state{stats_fun = StatsFun, stats_timer = Ref, monitors = dict:new()}}. handle_call(Req, _From, State) -> - lager:error("[MQTT-CM] Unexpected Call: ~p", [Req]), + emqx_log:error("[CM] Unexpected request: ~p", [Req]), {reply, ignore, State}. +handle_cast({reg, ClientId, Pid}, State) -> + _ = ets:insert(client, {ClientId, Pid}), + {noreply, monitor_client(ClientId, Pid, State)}; + handle_cast({unreg, ClientId, Pid}, State) -> - case lookup_proc(ClientId) of - Pid -> - ets:delete(mqtt_client, ClientId), - {noreply, setstats(State)}; - _ -> - {noreply, State} - end; + case lookup(ClientId) of + Pid -> remove_client({ClientId, Pid}); + _ -> ok + end, + {noreply, State}; handle_cast(Msg, State) -> - lager:error("[MQTT-CM] Unexpected Cast: ~p", [Msg]), + emqx_log:error("[CM] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> case dict:find(MRef, State#state.monitors) of - {ok, {ClientId, DownPid}} -> - case lookup_proc(ClientId) of - DownPid -> - emqx_stats:del_client_stats(ClientId), - ets:delete(mqtt_client, ClientId); - _ -> - ignore + {ok, ClientId} -> + case lookup(ClientId) of + DownPid -> remove_client({ClientId, DownPid}); + _ -> ok end, - {noreply, setstats(erase_monitor(MRef, State))}; + {noreply, erase_monitor(MRef, State)}; error -> - lager:error("MRef of client ~p not found", [DownPid]), + emqx_log:error("[CM] down client ~p not found", [DownPid]), {noreply, State} end; +handle_info(stats, State) -> + {noreply, setstats(State), hibernate}; + handle_info(Info, State) -> lager:error("[CM] Unexpected Info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{pool = Pool, id = Id}) -> - gproc_pool:disconnect_worker(Pool, {Pool, Id}). +terminate(_Reason, _State = #state{stats_timer = TRef}) -> + timer:cancel(TRef). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -132,14 +115,19 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- +remove_client(Client) -> + ets:delete_object(client, Client), + ets:delete(client_stats, Client), + ets:delete(client_attrs, Client). + monitor_client(ClientId, Pid, State = #state{monitors = Monitors}) -> MRef = erlang:monitor(process, Pid), - State#state{monitors = dict:store(MRef, {ClientId, Pid}, Monitors)}. + State#state{monitors = dict:store(MRef, ClientId, Monitors)}. erase_monitor(MRef, State = #state{monitors = Monitors}) -> - erlang:demonitor(MRef, [flush]), + erlang:demonitor(MRef), State#state{monitors = dict:erase(MRef, Monitors)}. -setstats(State = #state{statsfun = StatsFun}) -> - StatsFun(ets:info(mqtt_client, size)), State. +setstats(State = #state{stats_fun = StatsFun}) -> + StatsFun(ets:info(client, size)), State. diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index b73b2f5f5..04446c725 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -18,33 +18,24 @@ -behaviour(supervisor). -%% API -export([start_link/0]). -%% Supervisor callbacks -export([init/1]). --define(TAB, mqtt_client). - start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - %% Create client table - create_client_tab(), + %% Create table + lists:foreach(fun create_tab/1, [client, client_stats, client_attrs]), - %% CM Pool Sup - MFA = {emqx_cm, start_link, [emqx_stats:statsfun('clients/count', 'clients/max')]}, - PoolSup = emqx_pool_sup:spec([emqx_cm, hash, erlang:system_info(schedulers), MFA]), + StatsFun = emqx_stats:statsfun('clients/count', 'clients/max'), + + CM = {emqx_cm, {emqx_cm, start_link, [StatsFun]}, + permanent, 5000, worker, [emqx_cm]}, - {ok, {{one_for_all, 10, 3600}, [PoolSup]}}. + {ok, {{one_for_all, 10, 3600}, [CM]}}. -create_client_tab() -> - case ets:info(?TAB, name) of - undefined -> - ets:new(?TAB, [ordered_set, named_table, public, - {keypos, 2}, {write_concurrency, true}]); - _ -> - ok - end. +create_tab(Tab) -> + emqx_tables:create(Tab, [public, ordered_set, named_table, {write_concurrency, true}]). diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 1555f29cb..e09ad83b2 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -202,8 +202,8 @@ handle_info({suback, PacketId, GrantedQos}, State) -> end, State); %% Fastlane -handle_info({dispatch, _Topic, Message}, State) -> - handle_info({deliver, Message#message{qos = ?QOS_0}}, State); +handle_info({dispatch, _Topic, Msg}, State) -> + handle_info({deliver, emqx_message:set_flag(qos, ?QOS_0, Msg)}, State); handle_info({deliver, Message}, State) -> with_proto( diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 5ef4f8f8f..50198f3e1 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -18,139 +18,62 @@ -include("emqx.hrl"). --include("emqx_mqtt.hrl"). +-export([make/3]). --export([make/3, make/4, from_packet/1, from_packet/2, from_packet/3, - to_packet/1]). +-export([get_flag/2, get_flag/3, set_flag/2, unset_flag/2]). --export([set_flag/1, set_flag/2, unset_flag/1, unset_flag/2]). +-export([get_header/2, get_header/3, set_header/3]). --export([format/1]). +-export([get_user_property/2, get_user_property/3, set_user_property/3]). --type(msg_from() :: atom() | {binary(), undefined | binary()}). - -%% @doc Make a message --spec(make(msg_from(), binary(), binary()) -> message()). -make(From, Topic, Payload) -> - make(From, ?QOS_0, Topic, Payload). - --spec(make(msg_from(), mqtt_qos(), binary(), binary()) -> message()). -make(From, Qos, Topic, Payload) -> - #message{id = msgid(), - from = From, - qos = ?QOS_I(Qos), - topic = Topic, - payload = Payload, - timestamp = os:timestamp()}. - -%% @doc Message from Packet --spec(from_packet(mqtt_packet()) -> message()). -from_packet(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - retain = Retain, - qos = Qos, - dup = Dup}, - variable = #mqtt_packet_publish{topic_name = Topic, - packet_id = PacketId}, - payload = Payload}) -> - #message{id = msgid(), - packet_id = PacketId, - qos = Qos, - retain = Retain, - dup = Dup, - topic = Topic, - payload = Payload, - timestamp = os:timestamp()}; - -from_packet(#mqtt_packet_connect{will_flag = false}) -> - undefined; - -from_packet(#mqtt_packet_connect{client_id = ClientId, - username = Username, - will_retain = Retain, - will_qos = Qos, - will_topic = Topic, - will_msg = Msg}) -> - #message{id = msgid(), - topic = Topic, - from = {ClientId, Username}, - retain = Retain, - qos = Qos, - dup = false, - payload = Msg, - timestamp = os:timestamp()}. - -from_packet(ClientId, Packet) -> - Msg = from_packet(Packet), - Msg#message{from = ClientId}. - -from_packet(Username, ClientId, Packet) -> - Msg = from_packet(Packet), - Msg#message{from = {ClientId, Username}}. +%% Create a default message +-spec(make(atom() | client(), topic(), payload()) -> message()). +make(From, Topic, Payload) when is_atom(From); is_record(From, client) -> + #message{id = msgid(), + qos = 0, + from = From, + sender = self(), + flags = #{}, + headers = #{}, + topic = Topic, + properties = #{}, + payload = Payload, + timestamp = os:timestamp()}. msgid() -> emqx_guid:gen(). -%% @doc Message to Packet --spec(to_packet(message()) -> mqtt_packet()). -to_packet(#message{packet_id = PkgId, - qos = Qos, - retain = Retain, - dup = Dup, - topic = Topic, - payload = Payload}) -> +%% @doc Get flag +get_flag(Flag, Msg) -> + get_flag(Flag, Msg, false). +get_flag(Flag, #message{flags = Flags}, Default) -> + maps:get(Flag, Flags, Default). - #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - qos = Qos, - retain = Retain, - dup = Dup}, - variable = #mqtt_packet_publish{topic_name = Topic, - packet_id = if - Qos =:= ?QOS_0 -> undefined; - true -> PkgId - end - }, - payload = Payload}. +%% @doc Set flag +-spec(set_flag(message_flag(), message()) -> message()). +set_flag(Flag, Msg = #message{flags = Flags}) -> + Msg#message{flags = maps:put(Flag, true, Flags)}. -%% @doc set dup, retain flag --spec(set_flag(message()) -> message()). -set_flag(Msg) -> - Msg#message{dup = true, retain = true}. +%% @doc Unset flag +-spec(unset_flag(message_flag(), message()) -> message()). +unset_flag(Flag, Msg = #message{flags = Flags}) -> + Msg#message{flags = maps:remove(Flag, Flags)}. --spec(set_flag(atom(), message()) -> message()). -set_flag(dup, Msg = #message{dup = false}) -> - Msg#message{dup = true}; -set_flag(sys, Msg = #message{sys = false}) -> - Msg#message{sys = true}; -set_flag(retain, Msg = #message{retain = false}) -> - Msg#message{retain = true}; -set_flag(Flag, Msg) when Flag =:= dup; - Flag =:= retain; - Flag =:= sys -> Msg. +%% @doc Get header +get_header(Hdr, Msg) -> + get_header(Hdr, Msg, undefined). +get_header(Hdr, #message{headers = Headers}, Default) -> + maps:get(Hdr, Headers, Default). -%% @doc Unset dup, retain flag --spec(unset_flag(message()) -> message()). -unset_flag(Msg) -> - Msg#message{dup = false, retain = false}. +%% @doc Set header +set_header(Hdr, Val, Msg = #message{headers = Headers}) -> + Msg#message{headers = maps:put(Hdr, Val, Headers)}. --spec(unset_flag(dup | retain | atom(), message()) -> message()). -unset_flag(dup, Msg = #message{dup = true}) -> - Msg#message{dup = false}; -unset_flag(retain, Msg = #message{retain = true}) -> - Msg#message{retain = false}; -unset_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg. +%% @doc Get user property +get_user_property(Key, Msg) -> + get_user_property(Key, Msg, undefined). +get_user_property(Key, #message{properties = Props}, Default) -> + maps:get(Key, Props, Default). -%% @doc Format MQTT Message -format(#message{id = MsgId, packet_id = PktId, from = {ClientId, Username}, - qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> - io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s/~s, Topic=~s)", - [i(Qos), i(Retain), i(Dup), MsgId, PktId, Username, ClientId, Topic]); - -%% TODO:... -format(#message{id = MsgId, packet_id = PktId, from = From, - qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> - io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s, Topic=~s)", - [i(Qos), i(Retain), i(Dup), MsgId, PktId, From, Topic]). - -i(true) -> 1; -i(false) -> 0; -i(I) when is_integer(I) -> I. +set_user_property(Key, Val, Msg = #message{properties = Props}) -> + Msg#message{properties = maps:put(Key, Val, Props)}. diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index 559556443..ab9c553b1 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -28,44 +28,55 @@ load(Env) -> emqx:hook('client.connected', fun ?MODULE:on_client_connected/3, [Env]), emqx:hook('client.disconnected', fun ?MODULE:on_client_disconnected/3, [Env]). -on_client_connected(ConnAck, Client = #mqtt_client{client_id = ClientId, - username = Username, - peername = {IpAddr, _}, - clean_sess = CleanSess, - proto_ver = ProtoVer}, Env) -> - Payload = mochijson2:encode([{clientid, ClientId}, +on_client_connected(ConnAck, Client = #client{id = ClientId, + username = Username, + peername = {IpAddr, _} + %%clean_sess = CleanSess, + %%proto_ver = ProtoVer + }, Env) -> + case catch emqx_json:encode([{clientid, ClientId}, {username, Username}, {ipaddress, iolist_to_binary(emqx_net:ntoa(IpAddr))}, - {clean_sess, CleanSess}, - {protocol, ProtoVer}, + %%{clean_sess, CleanSess}, %%TODO:: fixme later + %%{protocol, ProtoVer}, {connack, ConnAck}, - {ts, emqx_time:now_secs()}]), - Msg = message(qos(Env), topic(connected, ClientId), Payload), - emqx:publish(emqx_message:set_flag(sys, Msg)), + {ts, emqx_time:now_secs()}]) of + Payload when is_binary(Payload) -> + Msg = message(qos(Env), topic(connected, ClientId), Payload), + emqx:publish(emqx_message:set_flag(sys, Msg)); + {'EXIT', Reason} -> + emqx_log:error("[Presence Module] json error: ~p", [Reason]) + end, {ok, Client}. -on_client_disconnected(Reason, #mqtt_client{client_id = ClientId, - username = Username}, Env) -> - Payload = mochijson2:encode([{clientid, ClientId}, +on_client_disconnected(Reason, #client{id = ClientId, + username = Username}, Env) -> + case catch emqx_json:encode([{clientid, ClientId}, {username, Username}, {reason, reason(Reason)}, - {ts, emqx_time:now_secs()}]), - Msg = message(qos(Env), topic(disconnected, ClientId), Payload), - emqx:publish(emqx_message:set_flag(sys, Msg)), ok. + {ts, emqx_time:now_secs()}]) of + Payload when is_binary(Payload) -> + Msg = message(qos(Env), topic(disconnected, ClientId), Payload), + emqx:publish(emqx_message:set_flag(sys, Msg)); + {'EXIT', Reason} -> + emqx_log:error("[Presence Module] json error: ~p", [Reason]) + end, ok. unload(_Env) -> emqx:unhook('client.connected', fun ?MODULE:on_client_connected/3), emqx:unhook('client.disconnected', fun ?MODULE:on_client_disconnected/3). message(Qos, Topic, Payload) -> - emqx_message:make(presence, Qos, Topic, iolist_to_binary(Payload)). + Msg = emqx_message:make(presence, Topic, iolist_to_binary(Payload)), + emqx_message:set_header(qos, Qos, Msg). topic(connected, ClientId) -> emqx_topic:systop(list_to_binary(["clients/", ClientId, "/connected"])); topic(disconnected, ClientId) -> emqx_topic:systop(list_to_binary(["clients/", ClientId, "/disconnected"])). -qos(Env) -> proplists:get_value(qos, Env, 0). +qos(Env) -> + proplists:get_value(qos, Env, 0). reason(Reason) when is_atom(Reason) -> Reason; reason({Error, _}) when is_atom(Error) -> Error; diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index b6e3726e5..d37a1bec4 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -33,9 +33,9 @@ load(Topics) -> emqx:hook('client.connected', fun ?MODULE:on_client_connected/3, [Topics]). -on_client_connected(?CONNACK_ACCEPT, Client = #mqtt_client{client_id = ClientId, - client_pid = ClientPid, - username = Username}, Topics) -> +on_client_connected(?CONNACK_ACCEPT, Client = #client{id = ClientId, + pid = ClientPid, + username = Username}, Topics) -> Replace = fun(Topic) -> rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) end, TopicTable = [{Replace(Topic), Qos} || {Topic, Qos} <- Topics], @@ -49,7 +49,7 @@ unload(_) -> emqx:unhook('client.connected', fun ?MODULE:on_client_connected/3). %%-------------------------------------------------------------------- -%% Internal Functions +%% Internal functions %%-------------------------------------------------------------------- rep(<<"%c">>, ClientId, Topic) -> diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 3762aff7e..acdd52aab 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -20,11 +20,12 @@ -include("emqx_mqtt.hrl"). -%% API -export([protocol_name/1, type_name/1, connack_name/1]). -export([format/1]). +-export([to_message/1, from_message/1]). + %% @doc Protocol name of version -spec(protocol_name(mqtt_vsn()) -> binary()). protocol_name(?MQTT_PROTO_V3) -> <<"MQIsdp">>; @@ -45,6 +46,49 @@ connack_name(?CONNACK_SERVER) -> 'CONNACK_SERVER'; connack_name(?CONNACK_CREDENTIALS) -> 'CONNACK_CREDENTIALS'; connack_name(?CONNACK_AUTH) -> 'CONNACK_AUTH'. +%% @doc From Message to Packet +-spec(from_message(message()) -> mqtt_packet()). +from_message(Msg = #message{qos = Qos, + topic = Topic, + payload = Payload}) -> + Dup = emqx_message:get_flag(dup, Msg, false), + Retain = emqx_message:get_flag(retain, Msg, false), + PacketId = emqx_message:get_header(packet_id, Msg), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = Qos, + retain = Retain, + dup = Dup}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId}, + payload = Payload}. + +%% @doc Message from Packet +-spec(to_message(mqtt_packet()) -> message()). +to_message(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + retain = Retain, + qos = Qos, + dup = Dup}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId, + properties = Props}, + payload = Payload}) -> + Msg = emqx_message:make(undefined, Topic, Payload), + Msg#message{qos = Qos, + flags = #{dup => Dup, retain => Retain}, + headers = #{packet_id => PacketId}, + properties = Props}; +to_message(#mqtt_packet_connect{will_flag = false}) -> + undefined; +to_message(#mqtt_packet_connect{will_retain = Retain, + will_qos = Qos, + will_topic = Topic, + will_props = Props, + will_msg = Payload}) -> + Msg = emqx_message:make(undefined, Topic, Payload), + Msg#message{flags = #{retain => Retain}, + headers = #{qos => Qos}, + properties = Props}. + %% @doc Format packet -spec(format(mqtt_packet()) -> iolist()). format(#mqtt_packet{header = Header, variable = Variable, payload = Payload}) -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index b61040443..1623432a0 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -122,17 +122,11 @@ client(#proto_state{client_id = ClientId, WillMsg =:= undefined -> undefined; true -> WillMsg#message.topic end, - #mqtt_client{client_id = ClientId, - client_pid = ClientPid, - username = Username, - peername = Peername, - clean_sess = CleanSess, - proto_ver = ProtoVer, - keepalive = Keepalive, - will_topic = WillTopic, - ws_initial_headers = WsInitialHeaders, - mountpoint = MountPoint, - connected_at = Time}. + #client{id = ClientId, + pid = ClientPid, + username = Username, + peername = Peername, + mountpoint = MountPoint}. session(#proto_state{session = Session}) -> Session. @@ -220,12 +214,14 @@ process(?CONNECT_PACKET(Var), State0) -> %% Start session case emqx_sm:open_session(#{clean_start => CleanSess, - client_id => clientid(State2), - username => Username}) of + client_id => clientid(State2), + username => Username, + client_pid => self()}) of {ok, Session} -> %% TODO:... SP = true, %% TODO:... - %% Register the client - emqx_cm:reg(client(State2)), + %% TODO: Register the client + emqx_cm:reg(clientid(State2)), + %%emqx_cm:reg(client(State2)), %% Start keepalive start_keepalive(KeepAlive, State2), %% Emit Stats @@ -245,7 +241,7 @@ process(?CONNECT_PACKET(Var), State0) -> %% Run hooks emqx_hooks:run('client.connected', [ReturnCode1], client(State3)), %%TODO: Send Connack - %% send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), State3), + send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), State3), %% stop if authentication failure stop_if_auth_failure(ReturnCode1, State3); @@ -330,8 +326,9 @@ publish(Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), username = Username, mountpoint = MountPoint, session = Session}) -> - Msg = emqx_message:from_packet(Username, ClientId, Packet), - emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg)); + Msg = emqx_packet:to_message(Packet), + Msg1 = Msg#message{from = #client{id = ClientId, username = Username}}, + emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)); publish(Packet = ?PUBLISH_PACKET(?QOS_1, _PacketId), State) -> with_puback(?PUBACK, Packet, State); @@ -344,8 +341,10 @@ with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), username = Username, mountpoint = MountPoint, session = Session}) -> - Msg = emqx_message:from_packet(Username, ClientId, Packet), - case emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg)) of + %% TODO: ... + Msg = emqx_packet:to_message(Packet), + Msg1 = Msg#message{from = #client{id = ClientId, username = Username}}, + case emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)) of ok -> send(?PUBACK_PACKET(Type, PacketId), State); {error, Error} -> @@ -359,7 +358,7 @@ send(Msg, State = #proto_state{client_id = ClientId, is_bridge = IsBridge}) when is_record(Msg, message) -> emqx_hooks:run('message.delivered', [ClientId, Username], Msg), - send(emqx_message:to_packet(unmount(MountPoint, clean_retain(IsBridge, Msg))), State); + send(emqx_packet:from_message(unmount(MountPoint, clean_retain(IsBridge, Msg))), State); send(Packet = ?PACKET(Type), State = #proto_state{sendfun = SendFun, stats_data = Stats}) -> trace(send, Packet, State), @@ -421,7 +420,7 @@ shutdown(Error, State = #proto_state{will_msg = WillMsg}) -> ok. willmsg(Packet, State = #proto_state{mountpoint = MountPoint}) when is_record(Packet, mqtt_packet_connect) -> - case emqx_message:from_packet(Packet) of + case emqx_packet:to_message(Packet) of undefined -> undefined; Msg -> mount(replvar(MountPoint, State), Msg) end. @@ -438,8 +437,8 @@ maybe_set_clientid(State) -> send_willmsg(_Client, undefined) -> ignore; -send_willmsg(#mqtt_client{client_id = ClientId, username = Username}, WillMsg) -> - emqx_broker:publish(WillMsg#message{from = {ClientId, Username}}). +send_willmsg(Client, WillMsg) -> + emqx_broker:publish(WillMsg#message{from = Client}). start_keepalive(0, _State) -> ignore; @@ -507,12 +506,13 @@ validate_packet(_Packet) -> validate_topics(_Type, []) -> {error, empty_topics}; -validate_topics(Type, TopicTable = [{_Topic, _Qos}|_]) +validate_topics(Type, TopicTable = [{_Topic, _SubOpts}|_]) when Type =:= name orelse Type =:= filter -> Valid = fun(Topic, Qos) -> emqx_topic:validate({Type, Topic}) and validate_qos(Qos) end, - case [Topic || {Topic, Qos} <- TopicTable, not Valid(Topic, Qos)] of + case [Topic || {Topic, SubOpts} <- TopicTable, + not Valid(Topic, proplists:get_value(qos, SubOpts))] of [] -> ok; _ -> {error, badtopic} end; @@ -531,9 +531,10 @@ validate_qos(_) -> false. parse_topic_table(TopicTable) -> - lists:map(fun({Topic0, Qos}) -> + lists:map(fun({Topic0, SubOpts}) -> {Topic, Opts} = emqx_topic:parse(Topic0), - {Topic, [{qos, Qos}|Opts]} + %%TODO: + {Topic, lists:usort(lists:umerge(Opts, SubOpts))} end, TopicTable). parse_topics(Topics) -> @@ -570,10 +571,10 @@ sp(false) -> 0. %% The retained flag should be propagated for bridge. %%-------------------------------------------------------------------- -clean_retain(false, Msg = #message{retain = true, headers = Headers}) -> +clean_retain(false, Msg = #message{flags = #{retain := true}, headers = Headers}) -> case lists:member(retained, Headers) of true -> Msg; - false -> Msg#message{retain = false} + false -> emqx_message:set_flag(retain, false, Msg) end; clean_retain(_IsBridge, Msg) -> Msg. diff --git a/src/emqx_rpc.erl b/src/emqx_rpc.erl index a95dd7323..f76714f76 100644 --- a/src/emqx_rpc.erl +++ b/src/emqx_rpc.erl @@ -16,9 +16,12 @@ -module(emqx_rpc). --export([cast/4]). +-export([call/4, cast/4]). + +call(Node, Mod, Fun, Args) -> + rpc:call(Node, Mod, Fun, Args). cast(Node, Mod, Fun, Args) -> + %%TODO: not right emqx_metrics:inc('messages/forward'), rpc:cast(Node, Mod, Fun, Args). - diff --git a/src/emqx_serializer.erl b/src/emqx_serializer.erl index 63906e426..eb1cec4db 100644 --- a/src/emqx_serializer.erl +++ b/src/emqx_serializer.erl @@ -90,6 +90,7 @@ serialize_variable(?SUBSCRIBE, #mqtt_packet_subscribe{packet_id = PacketId, serialize_variable(?SUBACK, #mqtt_packet_suback{packet_id = PacketId, properties = Properties, reason_codes = ReasonCodes}, undefined) -> + io:format("SubAck ReasonCodes: ~p~n", [ReasonCodes]), {<>, << <> || Code <- ReasonCodes >>}; serialize_variable(?UNSUBSCRIBE, #mqtt_packet_unsubscribe{packet_id = PacketId, diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 1c18907bc..b9c1c87f0 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -29,7 +29,7 @@ -import(proplists, [get_value/2, get_value/3]). %% Session API --export([start_link/3, resume/3, destroy/2]). +-export([start_link/1, resume/3, discard/2]). %% Management and Monitor API -export([state/1, info/1, stats/1]). @@ -42,9 +42,6 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% TODO: gen_server Message Priorities --export([handle_pre_hibernate/1]). - -define(MQueue, emqx_mqueue). %% A stateful interaction between a Client and a Server. Some Sessions @@ -71,8 +68,8 @@ %% will be deleted. -record(state, { - %% Clean Session Flag - clean_sess = false :: boolean(), + %% Clean Start Flag + clean_start = false :: boolean(), %% Client Binding: local | remote binding = local :: local | remote, @@ -150,9 +147,9 @@ -define(TIMEOUT, 60000). --define(INFO_KEYS, [clean_sess, client_id, username, client_pid, binding, created_at]). +-define(INFO_KEYS, [clean_start, client_id, username, client_pid, binding, created_at]). --define(STATE_KEYS, [clean_sess, client_id, username, binding, client_pid, old_client_pid, +-define(STATE_KEYS, [clean_start, client_id, username, binding, client_pid, old_client_pid, next_msg_id, max_subscriptions, subscriptions, upgrade_qos, inflight, max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, await_rel_timeout, expiry_interval, enable_stats, force_gc_count, @@ -163,10 +160,9 @@ "Session(~s): " ++ Format, [State#state.client_id | Args])). %% @doc Start a Session --spec(start_link(boolean(), {mqtt_client_id(), mqtt_username()}, pid()) -> {ok, pid()} | {error, term()}). -start_link(CleanSess, {ClientId, Username}, ClientPid) -> - gen_server:start_link(?MODULE, [CleanSess, {ClientId, Username}, ClientPid], - [{hibernate_after, 10000}]). +-spec(start_link(map()) -> {ok, pid()} | {error, term()}). +start_link(ClientAttrs) -> + gen_server:start_link(?MODULE, ClientAttrs, [{hibernate_after, 10000}]). %%-------------------------------------------------------------------- %% PubSub API @@ -215,12 +211,12 @@ pubcomp(Session, PacketId) -> gen_server:cast(Session, {pubcomp, PacketId}). %% @doc Unsubscribe the topics --spec(unsubscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok). +-spec(unsubscribe(pid(), [{binary(), [suboption()]}]) -> ok). unsubscribe(Session, TopicTable) -> gen_server:cast(Session, {unsubscribe, self(), TopicTable}). %% @doc Resume the session --spec(resume(pid(), mqtt_client_id(), pid()) -> ok). +-spec(resume(pid(), client_id(), pid()) -> ok). resume(Session, ClientId, ClientPid) -> gen_server:cast(Session, {resume, ClientId, ClientPid}). @@ -260,16 +256,19 @@ stats(#state{max_subscriptions = MaxSubscriptions, {deliver_msg, get(deliver_msg)}, {enqueue_msg, get(enqueue_msg)}]). -%% @doc Destroy the session --spec(destroy(pid(), mqtt_client_id()) -> ok). -destroy(Session, ClientId) -> - gen_server:cast(Session, {destroy, ClientId}). +%% @doc Discard the session +-spec(discard(pid(), client_id()) -> ok). +discard(Session, ClientId) -> + gen_server:cast(Session, {discard, ClientId}). %%-------------------------------------------------------------------- %% gen_server Callbacks %%-------------------------------------------------------------------- -init([CleanSess, {ClientId, Username}, ClientPid]) -> +init(#{clean_start := CleanStart, + client_id := ClientId, + username := Username, + client_pid := ClientPid}) -> process_flag(trap_exit, true), true = link(ClientPid), init_stats([deliver_msg, enqueue_msg]), @@ -280,7 +279,7 @@ init([CleanSess, {ClientId, Username}, ClientPid]) -> ForceGcCount = emqx_gc:conn_max_gc_count(), IgnoreLoopDeliver = get_value(ignore_loop_deliver, Env, false), MQueue = ?MQueue:new(ClientId, QEnv, emqx_alarm:alarm_fun()), - State = #state{clean_sess = CleanSess, + State = #state{clean_start = CleanStart, binding = binding(ClientPid), client_id = ClientId, client_pid = ClientPid, @@ -300,8 +299,9 @@ init([CleanSess, {ClientId, Username}, ClientPid]) -> force_gc_count = ForceGcCount, ignore_loop_deliver = IgnoreLoopDeliver, created_at = os:timestamp()}, - emqx_sm:register_session(ClientId, CleanSess, info(State)), + %%emqx_sm:register_session(ClientId, info(State)), emqx_hooks:run('session.created', [ClientId, Username]), + io:format("Session started: ~p~n", [self()]), {ok, emit_stats(State), hibernate}. init_stats(Keys) -> @@ -313,7 +313,7 @@ binding(ClientPid) -> handle_pre_hibernate(State) -> {hibernate, emqx_gc:reset_conn_gc_count(#state.force_gc_count, emit_stats(State))}. -handle_call({publish, Msg = #message{qos = ?QOS_2, packet_id = PacketId}}, _From, +handle_call({publish, Msg = #message{qos = ?QOS_2, headers = #{packet_id := PacketId}}}, _From, State = #state{awaiting_rel = AwaitingRel, await_rel_timer = Timer, await_rel_timeout = Timeout}) -> @@ -350,6 +350,7 @@ handle_cast({subscribe, From, TopicTable, AckFun}, ?LOG(info, "Subscribe ~p", [TopicTable], State), {GrantedQos, Subscriptions1} = lists:foldl(fun({Topic, Opts}, {QosAcc, SubMap}) -> + io:format("SubOpts: ~p~n", [Opts]), Fastlane = lists:member(fastlane, Opts), NewQos = if Fastlane == true -> ?QOS_0; true -> get_value(qos, Opts) end, SubMap1 = @@ -373,6 +374,7 @@ handle_cast({subscribe, From, TopicTable, AckFun}, end, {[NewQos|QosAcc], SubMap1} end, {[], Subscriptions}, TopicTable), + io:format("GrantedQos: ~p~n", [GrantedQos]), AckFun(lists:reverse(GrantedQos)), {noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate}; @@ -456,7 +458,7 @@ handle_cast({pubcomp, PacketId}, State = #state{inflight = Inflight}) -> handle_cast({resume, ClientId, ClientPid}, State = #state{client_id = ClientId, client_pid = OldClientPid, - clean_sess = CleanSess, + clean_start = CleanStart, retry_timer = RetryTimer, await_rel_timer = AwaitTimer, expiry_timer = ExpireTimer}) -> @@ -477,7 +479,7 @@ handle_cast({resume, ClientId, ClientPid}, State1 = State#state{client_pid = ClientPid, binding = binding(ClientPid), old_client_pid = OldClientPid, - clean_sess = false, + clean_start = false, retry_timer = undefined, awaiting_rel = #{}, await_rel_timer = undefined, @@ -485,22 +487,23 @@ handle_cast({resume, ClientId, ClientPid}, %% Clean Session: true -> false? if - CleanSess =:= true -> - ?LOG(error, "CleanSess changed to false.", [], State1), - emqx_sm:register_session(ClientId, false, info(State1)); - CleanSess =:= false -> + CleanStart =:= true -> + ?LOG(error, "CleanSess changed to false.", [], State1); + %%TODO:: + %%emqx_sm:register_session(ClientId, info(State1)); + CleanStart =:= false -> ok end, %% Replay delivery and Dequeue pending messages {noreply, emit_stats(dequeue(retry_delivery(true, State1)))}; -handle_cast({destroy, ClientId}, +handle_cast({discard, ClientId}, State = #state{client_id = ClientId, client_pid = undefined}) -> ?LOG(warning, "Destroyed", [], State), - shutdown(destroy, State); + shutdown(discard, State); -handle_cast({destroy, ClientId}, +handle_cast({discard, ClientId}, State = #state{client_id = ClientId, client_pid = OldClientPid}) -> ?LOG(warning, "kickout ~p", [OldClientPid], State), shutdown(conflict, State); @@ -533,11 +536,11 @@ handle_info({timeout, _Timer, expired}, State) -> shutdown(expired, State); handle_info({'EXIT', ClientPid, _Reason}, - State = #state{clean_sess = true, client_pid = ClientPid}) -> + State = #state{clean_start= true, client_pid = ClientPid}) -> {stop, normal, State}; handle_info({'EXIT', ClientPid, Reason}, - State = #state{clean_sess = false, + State = #state{clean_start = false, client_pid = ClientPid, expiry_interval = Interval}) -> ?LOG(info, "Client ~p EXIT for ~p", [ClientPid, Reason], State), @@ -604,7 +607,7 @@ retry_delivery(Force, [{Type, Msg, Ts} | Msgs], Now, if Force orelse (Diff >= Interval) -> case {Type, Msg} of - {publish, Msg = #message{packet_id = PacketId}} -> + {publish, Msg = #message{headers = #{packet_id := PacketId}}} -> redeliver(Msg, State), Inflight1 = Inflight:update(PacketId, {publish, Msg, Now}), retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}); @@ -687,7 +690,7 @@ dispatch(Msg = #message{qos = QoS}, true -> enqueue_msg(Msg, State); false -> - Msg1 = Msg#message{packet_id = MsgId}, + Msg1 = emqx_message:set_header(packet_id, MsgId, Msg), deliver(Msg1, State), await(Msg1, next_msg_id(State)) end. @@ -701,7 +704,7 @@ enqueue_msg(Msg, State = #state{mqueue = Q}) -> %%-------------------------------------------------------------------- redeliver(Msg = #message{qos = QoS}, State) -> - deliver(Msg#message{dup = if QoS =:= ?QOS2 -> false; true -> true end}, State); + deliver(if QoS =:= ?QOS2 -> Msg; true -> emqx_message:set_flag(dup, Msg) end, State); redeliver({pubrel, PacketId}, #state{client_pid = Pid}) -> Pid ! {redeliver, {?PUBREL, PacketId}}. @@ -715,7 +718,7 @@ deliver(Msg, #state{client_pid = Pid, binding = remote}) -> %% Awaiting ACK for QoS1/QoS2 Messages %%-------------------------------------------------------------------- -await(Msg = #message{packet_id = PacketId}, +await(Msg = #message{headers = #{packet_id := PacketId}}, State = #state{inflight = Inflight, retry_timer = RetryTimer, retry_interval = Interval}) -> @@ -797,9 +800,8 @@ tune_qos(Topic, Msg = #message{qos = PubQoS}, %% Reset Dup %%-------------------------------------------------------------------- -reset_dup(Msg = #message{dup = true}) -> - Msg#message{dup = false}; -reset_dup(Msg) -> Msg. +reset_dup(Msg) -> + emqx_message:unset_flag(dup, Msg). %%-------------------------------------------------------------------- %% Next Msg Id diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index a3f9e88cb..d0e974456 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -24,7 +24,7 @@ -export([open_session/1, lookup_session/1, close_session/1]). -export([resume_session/1, discard_session/1]). --export([register_session/1, unregister_session/2]). +-export([register_session/1, unregister_session/1, unregister_session/2]). %% lock_session/1, create_session/1, unlock_session/1, @@ -42,17 +42,19 @@ start_link(StatsFun) -> open_session(Session = #{client_id := ClientId, clean_start := true}) -> with_lock(ClientId, fun() -> - case rpc:multicall(ekka:nodelist(), ?MODULE, discard_session, [ClientId]) of + io:format("Nodelist: ~p~n", [ekka_membership:nodelist()]), + case rpc:multicall(ekka_membership:nodelist(), ?MODULE, discard_session, [ClientId]) of {_Res, []} -> ok; {_Res, BadNodes} -> emqx_log:error("[SM] Bad nodes found when lock a session: ~p", [BadNodes]) end, - {ok, emqx_session_sup:start_session(Session)} + io:format("Begin to start session: ~p~n", [Session]), + emqx_session_sup:start_session(Session) end); open_session(Session = #{client_id := ClientId, clean_start := false}) -> with_lock(ClientId, fun() -> - {ResL, _BadNodes} = emqx_rpc:multicall(ekka:nodelist(), ?MODULE, lookup_session, [ClientId]), + {ResL, _BadNodes} = rpc:multicall(ekka_membership:nodelist(), ?MODULE, lookup_session, [ClientId]), case lists:flatten([Pid || Pid <- ResL, Pid =/= undefined]) of [] -> {ok, emqx_session_sup:start_session(Session)}; @@ -61,7 +63,7 @@ open_session(Session = #{client_id := ClientId, clean_start := false}) -> ok -> {ok, SessPid}; {error, Reason} -> emqx_log:error("[SM] Failed to resume session: ~p, ~p", [Session, Reason]), - {ok, emqx_session_sup:start_session(Session)} + emqx_session_sup:start_session(Session) end end end). @@ -109,6 +111,9 @@ with_lock(ClientId, Fun) -> register_session(ClientId) -> ets:insert(session, {ClientId, self()}). +unregister_session(ClientId) -> + unregister_session(ClientId, self()). + unregister_session(ClientId, Pid) -> case ets:lookup(session, ClientId) of [{_, Pid}] -> diff --git a/src/emqx_sm_locker.erl b/src/emqx_sm_locker.erl index fc61e6b06..150481979 100644 --- a/src/emqx_sm_locker.erl +++ b/src/emqx_sm_locker.erl @@ -24,10 +24,10 @@ %% @doc Lock a clientid -spec(lock(client_id()) -> boolean() | {error, term()}). lock(ClientId) -> - emqx_rpc:call(ekka:leader(), emqx_sm_locker, lock, [ClientId]). + rpc:call(ekka_membership:leader(), emqx_locker, lock, [ClientId]). %% @doc Unlock a clientid -spec(unlock(client_id()) -> ok). unlock(ClientId) -> - emqx_rpc:call(ekka:leader(), emqx_locker, unlock, [ClientId]). + rpc:call(ekka_membership:leader(), emqx_locker, unlock, [ClientId]). diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index 52138f62a..51a495f58 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -26,20 +26,15 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - %% Create tables - create_tabs(), + lists:foreach(fun create_tab/1, [session, session_stats, session_attrs]), StatsFun = emqx_stats:statsfun('sessions/count', 'sessions/max'), SM = {emqx_sm, {emqx_sm, start_link, [StatsFun]}, permanent, 5000, worker, [emqx_sm]}, - {ok, {{one_for_all, 0, 3600}, [SM]}}. - -create_tabs() -> - lists:foreach(fun create_tab/1, [session, session_stats, session_attrs]). + {ok, {{one_for_all, 10, 3600}, [SM]}}. create_tab(Tab) -> - emqx_tables:create(Tab, [public, ordered_set, named_table, - {write_concurrency, true}]). + emqx_tables:create(Tab, [public, ordered_set, named_table, {write_concurrency, true}]). diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index fc68b3cb5..fa4c25892 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -51,7 +51,8 @@ start_link() -> trace(publish, From, _Msg) when is_atom(From) -> %% Dont' trace '$SYS' publish ignore; -trace(publish, {ClientId, Username}, #message{topic = Topic, payload = Payload}) -> +trace(publish, #client{id = ClientId, username = Username}, + #message{topic = Topic, payload = Payload}) -> lager:info([{client, ClientId}, {topic, Topic}], "~s/~s PUBLISH to ~s: ~p", [ClientId, Username, Topic, Payload]); trace(publish, From, #message{topic = Topic, payload = Payload}) @@ -59,7 +60,6 @@ trace(publish, From, #message{topic = Topic, payload = Payload}) lager:info([{client, From}, {topic, Topic}], "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). - %%-------------------------------------------------------------------- %% Start/Stop Trace %%-------------------------------------------------------------------- From 64594810245f7a801ed8729de5f6d83bb1df850b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 3 Apr 2018 20:22:01 +0800 Subject: [PATCH 035/520] Add multicall/4 function --- src/emqx_rpc.erl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/emqx_rpc.erl b/src/emqx_rpc.erl index f76714f76..7c6b13846 100644 --- a/src/emqx_rpc.erl +++ b/src/emqx_rpc.erl @@ -18,10 +18,14 @@ -export([call/4, cast/4]). +-export([multicall/4]). + call(Node, Mod, Fun, Args) -> rpc:call(Node, Mod, Fun, Args). +multicall(Nodes, Mod, Fun, Args) -> + rpc:multicall(Nodes, Mod, Fun, Args). + cast(Node, Mod, Fun, Args) -> - %%TODO: not right - emqx_metrics:inc('messages/forward'), rpc:cast(Node, Mod, Fun, Args). + From 39548cc3998e420b337e61ebcf30a1ae755a0ce4 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 4 Apr 2018 15:28:01 +0800 Subject: [PATCH 036/520] Improve the session management --- include/emqx.hrl | 4 +- src/emqx_access_rule.erl | 6 +- src/emqx_banned.erl | 72 ++++++++++++++ src/emqx_flapping.erl | 74 ++++++++++++++ src/emqx_mod_presence.erl | 8 +- src/emqx_mod_subscription.erl | 6 +- src/emqx_protocol.erl | 8 +- src/emqx_router.erl | 3 +- src/emqx_session.erl | 2 +- src/emqx_sm.erl | 182 ++++++++++++++++++---------------- src/emqx_sm_locker.erl | 44 ++++++-- src/emqx_tracer.erl | 2 +- 12 files changed, 295 insertions(+), 116 deletions(-) create mode 100644 src/emqx_banned.erl create mode 100644 src/emqx_flapping.erl diff --git a/include/emqx.hrl b/include/emqx.hrl index ddd91dbca..0639ae19d 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -81,8 +81,8 @@ -type(zone() :: atom()). -record(client, - { id :: client_id(), - pid :: pid(), + { client_id :: client_id(), + client_pid :: pid(), zone :: zone(), node :: node(), username :: username(), diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 63bf3b9ad..a96094065 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -99,7 +99,7 @@ match_who(_Client, {user, all}) -> true; match_who(_Client, {client, all}) -> true; -match_who(#client{id = ClientId}, {client, ClientId}) -> +match_who(#client{client_id = ClientId}, {client, ClientId}) -> true; match_who(#client{username = Username}, {user, Username}) -> true; @@ -137,9 +137,9 @@ feed_var(Client, Pattern) -> feed_var(Client, Pattern, []). feed_var(_Client, [], Acc) -> lists:reverse(Acc); -feed_var(Client = #client{id = undefined}, [<<"%c">>|Words], Acc) -> +feed_var(Client = #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 = #client{client_id = ClientId}, [<<"%c">>|Words], Acc) -> feed_var(Client, Words, [ClientId |Acc]); feed_var(Client = #client{username = undefined}, [<<"%u">>|Words], Acc) -> feed_var(Client, Words, [<<"%u">>|Acc]); diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl new file mode 100644 index 000000000..66cdf87b5 --- /dev/null +++ b/src/emqx_banned.erl @@ -0,0 +1,72 @@ +%%-------------------------------------------------------------------- +%% Copyright © 2013-2018 EMQ Inc. 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. +%%-------------------------------------------------------------------- + +%% Banned an IP Address, ClientId? +-module(emqx_banned). + +-behaviour(gen_server). + +%% API +-export([start_link/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-define(SERVER, ?MODULE). + +-record(state, {}). + +%%%=================================================================== +%%% API +%%%=================================================================== + +%% @doc Starts the server +-spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +init([]) -> + {ok, #state{}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + + + + + diff --git a/src/emqx_flapping.erl b/src/emqx_flapping.erl new file mode 100644 index 000000000..a0c1c3d45 --- /dev/null +++ b/src/emqx_flapping.erl @@ -0,0 +1,74 @@ +%%-------------------------------------------------------------------- +%% Copyright (C) 2013-2018 EMQ Inc. 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. +%%-------------------------------------------------------------------- + +%% 1. Flapping Detection +%% 2. Conflict Detection? +-module(emqx_flapping). + +%% Use ets:update_counter??? + +-behaviour(gen_server). + +-export([start_link/0]). + +-export([is_banned/1, banned/1]). + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-define(SERVER, ?MODULE). + +-record(state, {}). + +-spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +is_banned(ClientId) -> + ets:member(banned, ClientId). + +banned(ClientId) -> + ets:insert(banned, {ClientId, os:timestamp()}). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([]) -> + _ = ets:new(banned, [public, ordered_set, named_table]), + {ok, #state{}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + + diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index ab9c553b1..91cf654fc 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -28,9 +28,9 @@ load(Env) -> emqx:hook('client.connected', fun ?MODULE:on_client_connected/3, [Env]), emqx:hook('client.disconnected', fun ?MODULE:on_client_disconnected/3, [Env]). -on_client_connected(ConnAck, Client = #client{id = ClientId, - username = Username, - peername = {IpAddr, _} +on_client_connected(ConnAck, Client = #client{client_id = ClientId, + username = Username, + peername = {IpAddr, _} %%clean_sess = CleanSess, %%proto_ver = ProtoVer }, Env) -> @@ -49,7 +49,7 @@ on_client_connected(ConnAck, Client = #client{id = ClientId, end, {ok, Client}. -on_client_disconnected(Reason, #client{id = ClientId, +on_client_disconnected(Reason, #client{client_id = ClientId, username = Username}, Env) -> case catch emqx_json:encode([{clientid, ClientId}, {username, Username}, diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index d37a1bec4..83badcef6 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -33,9 +33,9 @@ load(Topics) -> emqx:hook('client.connected', fun ?MODULE:on_client_connected/3, [Topics]). -on_client_connected(?CONNACK_ACCEPT, Client = #client{id = ClientId, - pid = ClientPid, - username = Username}, Topics) -> +on_client_connected(?CONNACK_ACCEPT, Client = #client{client_id = ClientId, + client_pid = ClientPid, + username = Username}, Topics) -> Replace = fun(Topic) -> rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) end, TopicTable = [{Replace(Topic), Qos} || {Topic, Qos} <- Topics], diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 1623432a0..2976c79a5 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -122,8 +122,8 @@ client(#proto_state{client_id = ClientId, WillMsg =:= undefined -> undefined; true -> WillMsg#message.topic end, - #client{id = ClientId, - pid = ClientPid, + #client{client_id = ClientId, + client_pid = ClientPid, username = Username, peername = Peername, mountpoint = MountPoint}. @@ -327,7 +327,7 @@ publish(Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), mountpoint = MountPoint, session = Session}) -> Msg = emqx_packet:to_message(Packet), - Msg1 = Msg#message{from = #client{id = ClientId, username = Username}}, + Msg1 = Msg#message{from = #client{client_id = ClientId, username = Username}}, emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)); publish(Packet = ?PUBLISH_PACKET(?QOS_1, _PacketId), State) -> @@ -343,7 +343,7 @@ with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), session = Session}) -> %% TODO: ... Msg = emqx_packet:to_message(Packet), - Msg1 = Msg#message{from = #client{id = ClientId, username = Username}}, + Msg1 = Msg#message{from = #client{client_id = ClientId, username = Username}}, case emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)) of ok -> send(?PUBACK_PACKET(Type, PacketId), State); diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 3903dc005..e4741c8c4 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -153,9 +153,8 @@ cast(Router, Msg) -> pick(Topic) -> gproc_pool:pick_worker(router, Topic). -%%FIXME: OOM? dump() -> - [{route, [{To, Dest} || #route{topic = To, dest = Dest} <- ets:tab2list(route)]}]. + ets:tab2list(route). %%-------------------------------------------------------------------- %% gen_server callbacks diff --git a/src/emqx_session.erl b/src/emqx_session.erl index b9c1c87f0..d1b691d32 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -299,7 +299,7 @@ init(#{clean_start := CleanStart, force_gc_count = ForceGcCount, ignore_loop_deliver = IgnoreLoopDeliver, created_at = os:timestamp()}, - %%emqx_sm:register_session(ClientId, info(State)), + emqx_sm:register_session(ClientId, self()), emqx_hooks:run('session.created', [ClientId, Username]), io:format("Session started: ~p~n", [self()]), {ok, emit_stats(State), hibernate}. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index d0e974456..f4ea1468c 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -24,128 +24,134 @@ -export([open_session/1, lookup_session/1, close_session/1]). -export([resume_session/1, discard_session/1]). --export([register_session/1, unregister_session/1, unregister_session/2]). +-export([register_session/1, register_session/2]). +-export([unregister_session/1, unregister_session/2]). -%% lock_session/1, create_session/1, unlock_session/1, - --export([dispatch/3]). +%% Internal functions for rpc +-export([lookup/1, dispatch/3]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {stats_fun, stats_timer, monitors = #{}}). +-record(state, {stats, pids = #{}}). --spec(start_link(StatsFun :: fun()) -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link(fun()) -> {ok, pid()} | ignore | {error, term()}). start_link(StatsFun) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [StatsFun], []). -open_session(Session = #{client_id := ClientId, clean_start := true}) -> - with_lock(ClientId, - fun() -> - io:format("Nodelist: ~p~n", [ekka_membership:nodelist()]), - case rpc:multicall(ekka_membership:nodelist(), ?MODULE, discard_session, [ClientId]) of - {_Res, []} -> ok; - {_Res, BadNodes} -> emqx_log:error("[SM] Bad nodes found when lock a session: ~p", [BadNodes]) +open_session(Attrs = #{clean_start := true, + client_id := ClientId, client_pid := ClientPid}) -> + CleanStart = fun(_) -> + discard_session(ClientId, ClientPid), + emqx_session_sup:start_session(Attrs) + end, + emqx_sm_locker:trans(ClientId, CleanStart); + +open_session(Attrs = #{clean_start := false, + client_id := ClientId, client_pid := ClientPid}) -> + ResumeStart = fun(_) -> + case resume_session(ClientId, ClientPid) of + {ok, SessionPid} -> + {ok, SessionPid}; + {error, not_found} -> + emqx_session_sup:start_session(Attrs); + {error, Reason} -> + {error, Reason} + end end, - io:format("Begin to start session: ~p~n", [Session]), - emqx_session_sup:start_session(Session) - end); - -open_session(Session = #{client_id := ClientId, clean_start := false}) -> - with_lock(ClientId, - fun() -> - {ResL, _BadNodes} = rpc:multicall(ekka_membership:nodelist(), ?MODULE, lookup_session, [ClientId]), - case lists:flatten([Pid || Pid <- ResL, Pid =/= undefined]) of - [] -> - {ok, emqx_session_sup:start_session(Session)}; - [SessPid|_] -> - case resume_session(SessPid) of - ok -> {ok, SessPid}; - {error, Reason} -> - emqx_log:error("[SM] Failed to resume session: ~p, ~p", [Session, Reason]), - emqx_session_sup:start_session(Session) - end - end - end). - -resume_session(SessPid) when node(SessPid) == node() -> - case is_process_alive(SessPid) of - true -> - emqx_session:resume(SessPid, self()); - false -> - emqx_log:error("Cannot resume ~p which seems already dead!", [SessPid]), - {error, session_died} - end; - -resume_session(SessPid) -> - case rpc:call(node(SessPid), emqx_session, resume, [SessPid]) of - ok -> {ok, SessPid}; - {badrpc, Reason} -> - {error, Reason}; - {error, Reason} -> - {error, Reason} - end. + emqx_sm_locker:trans(ClientId, ResumeStart). discard_session(ClientId) -> + discard_session(ClientId, self()). + +discard_session(ClientId, ClientPid) -> + lists:foreach(fun({_, SessionPid}) -> + catch emqx_session:discard(SessionPid, ClientPid) + end, lookup_session(ClientId)). + +resume_session(ClientId) -> + resume_session(ClientId, self()). + +resume_session(ClientId, ClientPid) -> case lookup_session(ClientId) of - undefined -> ok; - Pid -> emqx_session:discard(Pid) + [] -> {error, not_found}; + [{_, SessionPid}] -> + ok = emqx_session:resume(SessionPid, ClientPid), + {ok, SessionPid}; + [{_, SessionPid}|_More] = Sessions -> + emqx_log:error("[SM] More than one session found: ~p", [Sessions]), + ok = emqx_session:resume(SessionPid, ClientPid), + {ok, SessionPid} end. lookup_session(ClientId) -> - try ets:lookup_element(session, ClientId, 2) catch error:badarg -> undefined end. + {ResL, _} = multicall(?MODULE, lookup, [ClientId]), + lists:append(ResL). -close_session(SessPid) -> - emqx_session:close(SessPid). +close_session(ClientId) -> + lists:foreach(fun(#session{pid = SessionPid}) -> + emqx_session:close(SessionPid) + end, lookup_session(ClientId)). -with_lock(ClientId, Fun) -> - case emqx_sm_locker:lock(ClientId) of - true -> Result = Fun(), - emqx_sm_locker:unlock(ClientId), - Result; - false -> {error, client_id_unavailable}; - {error, Reason} -> {error, Reason} - end. - --spec(register_session(client_id()) -> true). register_session(ClientId) -> - ets:insert(session, {ClientId, self()}). + register_session(ClientId, self()). + +register_session(ClientId, SessionPid) -> + ets:insert(session, {ClientId, SessionPid}). unregister_session(ClientId) -> unregister_session(ClientId, self()). -unregister_session(ClientId, Pid) -> +unregister_session(ClientId, SessionPid) -> case ets:lookup(session, ClientId) of - [{_, Pid}] -> - ets:delete_object(session, {ClientId, Pid}); + [Session = {ClientId, SessionPid}] -> + ets:delete(session_attrs, Session), + ets:delete(session_stats, Session), + ets:delete_object(session, Session); _ -> false end. dispatch(ClientId, Topic, Msg) -> - case lookup_session(ClientId) of - Pid when is_pid(Pid) -> + case lookup(ClientId) of + [{_, Pid}] -> Pid ! {dispatch, Topic, Msg}; - undefined -> + [] -> emqx_hooks:run('message.dropped', [ClientId, Msg]) end. +lookup(ClientId) -> + ets:lookup(session, ClientId). + +multicall(Mod, Fun, Args) -> + multicall(ekka:nodelist(up), Mod, Fun, Args). + +multicall([Node], Mod, Fun, Args) when Node == node() -> + Res = erlang:apply(Mod, Fun, Args), [Res]; + +multicall(Nodes, Mod, Fun, Args) -> + {ResL, _} = emqx_rpc:multicall(Nodes, Mod, Fun, Args), + ResL. + %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- init([StatsFun]) -> + {ok, sched_stats(StatsFun, #state{pids = #{}})}. + +sched_stats(Fun, State) -> {ok, TRef} = timer:send_interval(timer:seconds(1), stats), - {ok, #state{stats_fun = StatsFun, stats_timer = TRef}}. + State#state{stats = #{func => Fun, timer => TRef}}. handle_call(Req, _From, State) -> emqx_log:error("[SM] Unexpected request: ~p", [Req]), {reply, ignore, State}. -handle_cast({monitor_session, SessionPid, ClientId}, - State = #state{monitors = Monitors}) -> - MRef = erlang:monitor(process, SessionPid), - {noreply, State#state{monitors = maps:put(MRef, ClientId, Monitors)}}; +handle_cast({registered, ClientId, SessionPid}, + State = #state{pids = Pids}) -> + _ = erlang:monitor(process, SessionPid), + {noreply, State#state{pids = maps:put(SessionPid, ClientId, Pids)}}; handle_cast(Msg, State) -> emqx_log:error("[SM] Unexpected msg: ~p", [Msg]), @@ -154,14 +160,14 @@ handle_cast(Msg, State) -> handle_info(stats, State) -> {noreply, setstats(State), hibernate}; -handle_info({'DOWN', MRef, process, DownPid, _Reason}, - State = #state{monitors = Monitors}) -> - case maps:find(MRef, Monitors) of - {ok, {ClientId, Pid}} -> - ets:delete_object(session, {ClientId, Pid}), - {noreply, setstats(State#state{monitors = maps:remove(MRef, Monitors)})}; +handle_info({'DOWN', _MRef, process, DownPid, _Reason}, + State = #state{pids = Pids}) -> + case maps:find(DownPid, Pids) of + {ok, ClientId} -> + unregister_session(ClientId, DownPid), + {noreply, State#state{pids = maps:remove(DownPid, Pids)}}; error -> - emqx_log:error("session ~p not found", [DownPid]), + emqx_log:error("[SM] Session ~p not found", [DownPid]), {noreply, State} end; @@ -169,7 +175,7 @@ handle_info(Info, State) -> emqx_log:error("[SM] Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, _State = #state{stats_timer = TRef}) -> +terminate(_Reason, _State = #state{stats = #{timer := TRef}}) -> timer:cancel(TRef). code_change(_OldVsn, State, _Extra) -> @@ -179,6 +185,6 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -setstats(State = #state{stats_fun = StatsFun}) -> - StatsFun(ets:info(session, size)), State. +setstats(State = #state{stats = #{func := Fun}}) -> + Fun(ets:info(session, size)), State. diff --git a/src/emqx_sm_locker.erl b/src/emqx_sm_locker.erl index 150481979..116b75bd7 100644 --- a/src/emqx_sm_locker.erl +++ b/src/emqx_sm_locker.erl @@ -18,16 +18,44 @@ -include("emqx.hrl"). -%% Lock/Unlock API based on canal-lock. --export([lock/1, unlock/1]). +-export([start_link/0]). -%% @doc Lock a clientid --spec(lock(client_id()) -> boolean() | {error, term()}). +-export([trans/2, trans/3]). + +-export([lock/1, lock/2, unlock/1]). + +-spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +start_link() -> + ekka_locker:start_link(?MODULE). + +-spec(trans(client_id(), fun(([node()]) -> any())) -> any()). +trans(ClientId, Fun) -> + trans(ClientId, Fun, undefined). + +-spec(trans(client_id(), fun(([node()]) -> any()), + ekka_locker:piggyback()) -> any()). +trans(ClientId, Fun, Piggyback) -> + case lock(ClientId, Piggyback) of + {true, Nodes} -> + try Fun(Nodes) after unlock(ClientId) end; + {false, _Nodes} -> + {error, client_id_unavailable} + end. + +-spec(lock(client_id()) -> ekka_locker:lock_result()). lock(ClientId) -> - rpc:call(ekka_membership:leader(), emqx_locker, lock, [ClientId]). + ekka_locker:aquire(?MODULE, ClientId, strategy()). -%% @doc Unlock a clientid --spec(unlock(client_id()) -> ok). +-spec(lock(client_id(), ekka_locker:piggyback()) + -> ekka_locker:lock_result()). +lock(ClientId, Piggyback) -> + ekka_locker:aquire(?MODULE, ClientId, strategy(), Piggyback). + +-spec(unlock(client_id()) -> {boolean(), [node()]}). unlock(ClientId) -> - rpc:call(ekka_membership:leader(), emqx_locker, unlock, [ClientId]). + ekka_locker:release(?MODULE, ClientId, strategy()). + +-spec(strategy() -> local | one | quorum | all). +strategy() -> + application:get_env(emqx, session_locking_strategy, quorum). diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index fa4c25892..74284c35a 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -51,7 +51,7 @@ start_link() -> trace(publish, From, _Msg) when is_atom(From) -> %% Dont' trace '$SYS' publish ignore; -trace(publish, #client{id = ClientId, username = Username}, +trace(publish, #client{client_id = ClientId, username = Username}, #message{topic = Topic, payload = Payload}) -> lager:info([{client, ClientId}, {topic, Topic}], "~s/~s PUBLISH to ~s: ~p", [ClientId, Username, Topic, Payload]); From bfb23ff0b26192ff3fcffdca3637aea44b9db23c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 8 Apr 2018 15:16:05 +0800 Subject: [PATCH 037/520] Improve the design of MQTT session management --- include/emqx.hrl | 4 +- src/emqx_acl_mod.erl | 2 +- src/emqx_alarm.erl | 2 +- src/emqx_auth_mod.erl | 2 +- src/emqx_banned.erl | 2 +- src/emqx_base62.erl | 2 +- src/emqx_boot.erl | 2 +- src/emqx_bridge.erl | 2 +- src/emqx_broker.erl | 6 +- src/emqx_broker_sup.erl | 6 +- src/emqx_config.erl | 5 + src/emqx_locker.erl | 41 ---- src/emqx_pmon.erl | 26 ++- src/emqx_router_helper.erl | 2 +- src/emqx_router_sup.erl | 2 +- src/emqx_serializer.erl | 2 +- src/emqx_session.erl | 198 +++++++++--------- ..._shared_pubsub.erl => emqx_shared_sub.erl} | 38 ++-- src/emqx_sm.erl | 190 +++++++++-------- src/emqx_sm_registry.erl | 109 ++++++++++ src/emqx_sm_stats.erl | 72 +++++++ src/emqx_sm_sup.erl | 19 +- src/emqx_stats.erl | 19 +- src/emqx_sup.erl | 1 - 24 files changed, 446 insertions(+), 308 deletions(-) delete mode 100644 src/emqx_locker.erl rename src/{emqx_shared_pubsub.erl => emqx_shared_sub.erl} (88%) create mode 100644 src/emqx_sm_registry.erl create mode 100644 src/emqx_sm_stats.erl diff --git a/include/emqx.hrl b/include/emqx.hrl index 0639ae19d..a0b96c1c7 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -96,8 +96,8 @@ -type(client() :: #client{}). -record(session, - { client_id :: client_id(), - pid :: pid() + { sid :: client_id(), + pid :: pid() }). -type(session() :: #session{}). diff --git a/src/emqx_acl_mod.erl b/src/emqx_acl_mod.erl index e01c0ed95..7c0866e61 100644 --- a/src/emqx_acl_mod.erl +++ b/src/emqx_acl_mod.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_alarm.erl b/src/emqx_alarm.erl index 83e957047..0f639af3b 100644 --- a/src/emqx_alarm.erl +++ b/src/emqx_alarm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_auth_mod.erl b/src/emqx_auth_mod.erl index 2cf222d65..1a2fd72c9 100644 --- a/src/emqx_auth_mod.erl +++ b/src/emqx_auth_mod.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index 66cdf87b5..85cfd8b6c 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_base62.erl b/src/emqx_base62.erl index a997912eb..3d0d01969 100644 --- a/src/emqx_base62.erl +++ b/src/emqx_base62.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_boot.erl b/src/emqx_boot.erl index 26348b271..dadc4c4cd 100644 --- a/src/emqx_boot.erl +++ b/src/emqx_boot.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index 5c20538f3..bdd077106 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 067e2c6da..73b557eac 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -119,7 +119,7 @@ route([{To, Node}], Delivery = #delivery{flows = Flows}) when is_atom(Node) -> forward(Node, To, Delivery#delivery{flows = [{route, Node, To}|Flows]}); route([{To, Group}], Delivery) when is_binary(Group) -> - emqx_shared_pubsub:dispatch(Group, To, Delivery); + emqx_shared_sub:dispatch(Group, To, Delivery); route(Routes, Delivery) -> lists:foldl(fun(Route, Acc) -> route([Route], Acc) end, Delivery, Routes). @@ -248,7 +248,7 @@ handle_cast({From, {subscribe, Topic, Subscriber, Options}}, State) -> [] -> Group = proplists:get_value(share, Options), true = do_subscribe(Group, Topic, Subscriber, Options), - emqx_shared_pubsub:subscribe(Group, Topic, subpid(Subscriber)), + emqx_shared_sub:subscribe(Group, Topic, subpid(Subscriber)), emqx_router:add_route(From, Topic, dest(Options)), {noreply, monitor_subscriber(Subscriber, State)}; [_] -> @@ -261,7 +261,7 @@ handle_cast({From, {unsubscribe, Topic, Subscriber}}, State) -> [{_, Options}] -> Group = proplists:get_value(share, Options), true = do_unsubscribe(Group, Topic, Subscriber), - emqx_shared_pubsub:unsubscribe(Group, Topic, subpid(Subscriber)), + emqx_shared_sub:unsubscribe(Group, Topic, subpid(Subscriber)), case ets:member(subscriber, Topic) of false -> emqx_router:del_route(From, Topic, dest(Options)); true -> gen_server:reply(From, ok) diff --git a/src/emqx_broker_sup.erl b/src/emqx_broker_sup.erl index 96c004b49..d962526eb 100644 --- a/src/emqx_broker_sup.erl +++ b/src/emqx_broker_sup.erl @@ -35,9 +35,9 @@ init([]) -> %% Create the pubsub tables create_tabs(), - %% Shared pubsub - Shared = {shared_pubsub, {emqx_shared_pubsub, start_link, []}, - permanent, 5000, worker, [emqx_shared_pubsub]}, + %% Shared subscription + Shared = {shared_sub, {emqx_shared_sub, start_link, []}, + permanent, 5000, worker, [emqx_shared_sub]}, %% Broker helper Helper = {broker_helper, {emqx_broker_helper, start_link, [stats_fun()]}, diff --git a/src/emqx_config.erl b/src/emqx_config.erl index 15a3a014a..954539e16 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -29,6 +29,11 @@ -type(env() :: {atom(), term()}). +-define(APP, emqx). + +get_env(Key) -> + application:get_env(?APP, Key). + %% @doc Read the configuration of an application. -spec(read(atom()) -> {ok, list(env())} | {error, term()}). read(App) -> diff --git a/src/emqx_locker.erl b/src/emqx_locker.erl deleted file mode 100644 index 196a3aa69..000000000 --- a/src/emqx_locker.erl +++ /dev/null @@ -1,41 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. 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_locker). - --export([start_link/0]). - -%% Lock/Unlock API based on canal-lock. --export([lock/1, unlock/1]). - -%% @doc Starts the lock server --spec(start_link() -> {ok, pid()} | ignore | {error, any()}). -start_link() -> - canal_lock:start_link(?MODULE, 1). - -%% @doc Lock a Key --spec(lock(binary()) -> boolean()). -lock(Key) -> - case canal_lock:acquire(?MODULE, Key, 1, 1) of - {acquired, 1} -> true; - full -> false - end. - -%% @doc Unlock a Key --spec(unlock(binary()) -> ok). -unlock(Key) -> - canal_lock:release(?MODULE, Key, 1, 1). - diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index cd66414fc..774b3d8ec 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -16,7 +16,9 @@ -module(emqx_pmon). --export([new/0, monitor/2, demonitor/2, erase/2]). +-export([new/0, monitor/2, monitor/3, demonitor/2, find/2, erase/2]). + +-compile({no_auto_import,[monitor/3]}). -type(pmon() :: {?MODULE, map()}). @@ -26,25 +28,35 @@ new() -> {?MODULE, [maps:new()]}. -spec(monitor(pid(), pmon()) -> pmon()). -monitor(Pid, PM = {?MODULE, [M]}) -> +monitor(Pid, PM) -> + monitor(Pid, undefined, PM). + +monitor(Pid, Val, PM = {?MODULE, [M]}) -> case maps:is_key(Pid, M) of - true -> - PM; + true -> PM; false -> Ref = erlang:monitor(process, Pid), - {?MODULE, [maps:put(Pid, Ref, M)]} + {?MODULE, [maps:put(Pid, {Ref, Val}, M)]} end. -spec(demonitor(pid(), pmon()) -> pmon()). demonitor(Pid, PM = {?MODULE, [M]}) -> case maps:find(Pid, M) of - {ok, Ref} -> + {ok, {Ref, _Val}} -> erlang:demonitor(Ref, [flush]), {?MODULE, [maps:remove(Pid, M)]}; error -> PM end. +-spec(find(pid(), pmon()) -> undefined | term()). +find(Pid, {?MODULE, [M]}) -> + case maps:find(Pid, M) of + {ok, {_Ref, Val}} -> + Val; + error -> undefined + end. + -spec(erase(pid(), pmon()) -> pmon()). erase(Pid, {?MODULE, [M]}) -> {?MODULE, [maps:remove(Pid, M)]}. diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index 97ba1811f..f8c41efc0 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_router_sup.erl b/src/emqx_router_sup.erl index bb3c59e3e..2fa5c21e1 100644 --- a/src/emqx_router_sup.erl +++ b/src/emqx_router_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_serializer.erl b/src/emqx_serializer.erl index eb1cec4db..82c888665 100644 --- a/src/emqx_serializer.erl +++ b/src/emqx_serializer.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index d1b691d32..19147c242 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -67,82 +67,81 @@ %% If the session is currently disconnected, the time at which the Session state %% will be deleted. -record(state, - { - %% Clean Start Flag - clean_start = false :: boolean(), + { %% Clean Start Flag + clean_start = false :: boolean(), - %% Client Binding: local | remote - binding = local :: local | remote, + %% Client Binding: local | remote + binding = local :: local | remote, - %% ClientId: Identifier of Session - client_id :: binary(), + %% ClientId: Identifier of Session + client_id :: binary(), - %% Username - username :: binary() | undefined, + %% Username + username :: binary() | undefined, - %% Client Pid binding with session - client_pid :: pid(), + %% Client Pid binding with session + client_pid :: pid(), - %% Old Client Pid that has been kickout - old_client_pid :: pid(), + %% Old Client Pid that has been kickout + old_client_pid :: pid(), - %% Next message id of the session - next_msg_id = 1 :: mqtt_packet_id(), + %% Next message id of the session + next_msg_id = 1 :: mqtt_packet_id(), - max_subscriptions :: non_neg_integer(), + max_subscriptions :: non_neg_integer(), - %% Client’s subscriptions. - subscriptions :: map(), + %% Client’s subscriptions. + subscriptions :: map(), - %% Upgrade Qos? - upgrade_qos = false :: boolean(), + %% Upgrade Qos? + upgrade_qos = false :: boolean(), - %% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked. - inflight :: emqx_inflight:inflight(), + %% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked. + inflight :: emqx_inflight:inflight(), - %% Max Inflight Size - max_inflight = 32 :: non_neg_integer(), + %% Max Inflight Size + max_inflight = 32 :: non_neg_integer(), - %% Retry interval for redelivering QoS1/2 messages - retry_interval = 20000 :: timeout(), + %% Retry interval for redelivering QoS1/2 messages + retry_interval = 20000 :: timeout(), - %% Retry Timer - retry_timer :: reference() | undefined, + %% Retry Timer + retry_timer :: reference() | undefined, - %% 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 :: ?MQueue:mqueue(), + %% 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 :: ?MQueue:mqueue(), - %% Client -> Broker: Inflight QoS2 messages received from client and waiting for pubrel. - awaiting_rel :: map(), + %% Client -> Broker: Inflight QoS2 messages received from client and waiting for pubrel. + awaiting_rel :: map(), - %% Max Packets that Awaiting PUBREL - max_awaiting_rel = 100 :: non_neg_integer(), + %% Max Packets that Awaiting PUBREL + max_awaiting_rel = 100 :: non_neg_integer(), - %% Awaiting PUBREL timeout - await_rel_timeout = 20000 :: timeout(), + %% Awaiting PUBREL timeout + await_rel_timeout = 20000 :: timeout(), - %% Awaiting PUBREL timer - await_rel_timer :: reference() | undefined, + %% Awaiting PUBREL timer + await_rel_timer :: reference() | undefined, - %% Session Expiry Interval - expiry_interval = 7200000 :: timeout(), + %% Session Expiry Interval + expiry_interval = 7200000 :: timeout(), - %% Expired Timer - expiry_timer :: reference() | undefined, + %% Expired Timer + expiry_timer :: reference() | undefined, - %% Enable Stats - enable_stats :: boolean(), + %% Enable Stats + enable_stats :: boolean(), - %% Force GC Count - force_gc_count :: undefined | integer(), + %% Force GC reductions + reductions = 0 :: non_neg_integer(), - %% Ignore loop deliver? - ignore_loop_deliver = false :: boolean(), + %% Ignore loop deliver? + ignore_loop_deliver = false :: boolean(), - created_at :: erlang:timestamp() + created_at :: erlang:timestamp() }). -define(TIMEOUT, 60000). @@ -161,8 +160,8 @@ %% @doc Start a Session -spec(start_link(map()) -> {ok, pid()} | {error, term()}). -start_link(ClientAttrs) -> - gen_server:start_link(?MODULE, ClientAttrs, [{hibernate_after, 10000}]). +start_link(Attrs) -> + gen_server:start_link(?MODULE, Attrs, [{hibernate_after, 10000}]). %%-------------------------------------------------------------------- %% PubSub API @@ -170,71 +169,71 @@ start_link(ClientAttrs) -> %% @doc Subscribe topics -spec(subscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok). -subscribe(Session, TopicTable) -> %%TODO: the ack function??... - gen_server:cast(Session, {subscribe, self(), TopicTable, fun(_) -> ok end}). +subscribe(SessionPid, TopicTable) -> %%TODO: the ack function??... + gen_server:cast(SessionPid, {subscribe, self(), TopicTable, fun(_) -> ok end}). -spec(subscribe(pid(), mqtt_packet_id(), [{binary(), [emqx_topic:option()]}]) -> ok). -subscribe(Session, PacketId, TopicTable) -> %%TODO: the ack function??... +subscribe(SessionPid, PacketId, TopicTable) -> %%TODO: the ack function??... From = self(), AckFun = fun(GrantedQos) -> From ! {suback, PacketId, GrantedQos} end, - gen_server:cast(Session, {subscribe, From, TopicTable, AckFun}). + gen_server:cast(SessionPid, {subscribe, From, TopicTable, AckFun}). %% @doc Publish Message -spec(publish(pid(), message()) -> ok | {error, term()}). -publish(_Session, Msg = #message{qos = ?QOS_0}) -> +publish(_SessionPid, Msg = #message{qos = ?QOS_0}) -> %% Publish QoS0 Directly emqx_broker:publish(Msg), ok; -publish(_Session, Msg = #message{qos = ?QOS_1}) -> +publish(_SessionPid, Msg = #message{qos = ?QOS_1}) -> %% Publish QoS1 message directly for client will PubAck automatically emqx_broker:publish(Msg), ok; -publish(Session, Msg = #message{qos = ?QOS_2}) -> +publish(SessionPid, Msg = #message{qos = ?QOS_2}) -> %% Publish QoS2 to Session - gen_server:call(Session, {publish, Msg}, ?TIMEOUT). + gen_server:call(SessionPid, {publish, Msg}, ?TIMEOUT). %% @doc PubAck Message -spec(puback(pid(), mqtt_packet_id()) -> ok). -puback(Session, PacketId) -> - gen_server:cast(Session, {puback, PacketId}). +puback(SessionPid, PacketId) -> + gen_server:cast(SessionPid, {puback, PacketId}). -spec(pubrec(pid(), mqtt_packet_id()) -> ok). -pubrec(Session, PacketId) -> - gen_server:cast(Session, {pubrec, PacketId}). +pubrec(SessionPid, PacketId) -> + gen_server:cast(SessionPid, {pubrec, PacketId}). -spec(pubrel(pid(), mqtt_packet_id()) -> ok). -pubrel(Session, PacketId) -> - gen_server:cast(Session, {pubrel, PacketId}). +pubrel(SessionPid, PacketId) -> + gen_server:cast(SessionPid, {pubrel, PacketId}). -spec(pubcomp(pid(), mqtt_packet_id()) -> ok). -pubcomp(Session, PacketId) -> - gen_server:cast(Session, {pubcomp, PacketId}). +pubcomp(SessionPid, PacketId) -> + gen_server:cast(SessionPid, {pubcomp, PacketId}). %% @doc Unsubscribe the topics -spec(unsubscribe(pid(), [{binary(), [suboption()]}]) -> ok). -unsubscribe(Session, TopicTable) -> - gen_server:cast(Session, {unsubscribe, self(), TopicTable}). +unsubscribe(SessionPid, TopicTable) -> + gen_server:cast(SessionPid, {unsubscribe, self(), TopicTable}). %% @doc Resume the session -spec(resume(pid(), client_id(), pid()) -> ok). -resume(Session, ClientId, ClientPid) -> - gen_server:cast(Session, {resume, ClientId, ClientPid}). +resume(SessionPid, ClientId, ClientPid) -> + gen_server:cast(SessionPid, {resume, ClientId, ClientPid}). %% @doc Get session state -state(Session) when is_pid(Session) -> - gen_server:call(Session, state). +state(SessionPid) when is_pid(SessionPid) -> + gen_server:call(SessionPid, state). %% @doc Get session info -spec(info(pid() | #state{}) -> list(tuple())). -info(Session) when is_pid(Session) -> - gen_server:call(Session, info); +info(SessionPid) when is_pid(SessionPid) -> + gen_server:call(SessionPid, info); info(State) when is_record(State, state) -> ?record_to_proplist(state, State, ?INFO_KEYS). -spec(stats(pid() | #state{}) -> list({atom(), non_neg_integer()})). -stats(Session) when is_pid(Session) -> - gen_server:call(Session, stats); +stats(SessionPid) when is_pid(SessionPid) -> + gen_server:call(SessionPid, stats); stats(#state{max_subscriptions = MaxSubscriptions, subscriptions = Subscriptions, @@ -258,8 +257,8 @@ stats(#state{max_subscriptions = MaxSubscriptions, %% @doc Discard the session -spec(discard(pid(), client_id()) -> ok). -discard(Session, ClientId) -> - gen_server:cast(Session, {discard, ClientId}). +discard(SessionPid, ClientId) -> + gen_server:call(SessionPid, {discard, ClientId}). %%-------------------------------------------------------------------- %% gen_server Callbacks @@ -276,7 +275,6 @@ init(#{clean_start := CleanStart, {ok, QEnv} = emqx:env(mqueue), MaxInflight = get_value(max_inflight, Env, 0), EnableStats = get_value(enable_stats, Env, false), - ForceGcCount = emqx_gc:conn_max_gc_count(), IgnoreLoopDeliver = get_value(ignore_loop_deliver, Env, false), MQueue = ?MQueue:new(ClientId, QEnv, emqx_alarm:alarm_fun()), State = #state{clean_start = CleanStart, @@ -296,10 +294,9 @@ init(#{clean_start := CleanStart, max_awaiting_rel = get_value(max_awaiting_rel, Env), expiry_interval = get_value(expiry_interval, Env), enable_stats = EnableStats, - force_gc_count = ForceGcCount, ignore_loop_deliver = IgnoreLoopDeliver, created_at = os:timestamp()}, - emqx_sm:register_session(ClientId, self()), + emqx_sm:register_session(#session{sid = ClientId, pid = self()}, info(State)), emqx_hooks:run('session.created', [ClientId, Username]), io:format("Session started: ~p~n", [self()]), {ok, emit_stats(State), hibernate}. @@ -310,8 +307,13 @@ init_stats(Keys) -> binding(ClientPid) -> case node(ClientPid) =:= node() of true -> local; false -> remote end. -handle_pre_hibernate(State) -> - {hibernate, emqx_gc:reset_conn_gc_count(#state.force_gc_count, emit_stats(State))}. +handle_call({discard, ClientPid}, _From, State = #state{client_pid = undefined}) -> + ?LOG(warning, "Discarded by ~p", [ClientPid], State), + {stop, {shutdown, discard}, ok, State}; + +handle_call({discard, ClientPid}, _From, State = #state{client_pid = OldClientPid}) -> + ?LOG(warning, " ~p kickout ~p", [ClientPid, OldClientPid], State), + {stop, {shutdown, conflict}, ok, State}; handle_call({publish, Msg = #message{qos = ?QOS_2, headers = #{packet_id := PacketId}}}, _From, State = #state{awaiting_rel = AwaitingRel, @@ -498,16 +500,6 @@ handle_cast({resume, ClientId, ClientPid}, %% Replay delivery and Dequeue pending messages {noreply, emit_stats(dequeue(retry_delivery(true, State1)))}; -handle_cast({discard, ClientId}, - State = #state{client_id = ClientId, client_pid = undefined}) -> - ?LOG(warning, "Destroyed", [], State), - shutdown(discard, State); - -handle_cast({discard, ClientId}, - State = #state{client_id = ClientId, client_pid = OldClientPid}) -> - ?LOG(warning, "kickout ~p", [OldClientPid], State), - shutdown(conflict, State); - handle_cast(Msg, State) -> lager:error("[~s] Unexpected Cast: ~p", [?MODULE, Msg]), {noreply, State}. @@ -563,10 +555,9 @@ handle_info(Info, State) -> {noreply, State}. terminate(Reason, #state{client_id = ClientId, username = Username}) -> - %% Move to emqx_sm to avoid race condition - %% emqx_stats:del_session_stats(ClientId), + emqx_hooks:run('session.terminated', [ClientId, Username, Reason]), - emqx_sm:unregister_session(ClientId). + emqx_sm:unregister_session(#session{sid = ClientId, pid = self()}). code_change(_OldVsn, Session, _Extra) -> {ok, Session}. @@ -574,6 +565,7 @@ code_change(_OldVsn, Session, _Extra) -> %%-------------------------------------------------------------------- %% Kickout old client %%-------------------------------------------------------------------- + kick(_ClientId, undefined, _Pid) -> ignore; kick(_ClientId, Pid, Pid) -> @@ -820,7 +812,8 @@ next_msg_id(State = #state{next_msg_id = Id}) -> emit_stats(State = #state{enable_stats = false}) -> State; emit_stats(State = #state{client_id = ClientId}) -> - emqx_stats:set_session_stats(ClientId, stats(State)), + Session = #session{sid = ClientId, pid = self()}, + emqx_sm_stats:set_session_stats(Session, stats(State)), State. inc_stats(Key) -> put(Key, get(Key) + 1). @@ -836,5 +829,6 @@ shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. gc(State) -> - emqx_gc:maybe_force_gc(#state.force_gc_count, State). + State. + %%emqx_gc:maybe_force_gc(#state.force_gc_count, State). diff --git a/src/emqx_shared_pubsub.erl b/src/emqx_shared_sub.erl similarity index 88% rename from src/emqx_shared_pubsub.erl rename to src/emqx_shared_sub.erl index bbc4d1930..6d5efbd51 100644 --- a/src/emqx_shared_pubsub.erl +++ b/src/emqx_shared_sub.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -14,18 +14,12 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_shared_pubsub). +-module(emqx_shared_sub). -behaviour(gen_server). -include("emqx.hrl"). -%% Mnesia bootstrap --export([mnesia/1]). - --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). - %% API -export([start_link/0]). @@ -41,32 +35,24 @@ -define(SERVER, ?MODULE). --define(TABLE, shared_subscription). +-define(TAB, shared_subscription). -record(state, {pmon}). -record(shared_subscription, {group, topic, subpid}). -%%-------------------------------------------------------------------- -%% Mnesia bootstrap -%%-------------------------------------------------------------------- - -mnesia(boot) -> - ok = ekka_mnesia:create_table(?TABLE, [ - {type, bag}, - {ram_copies, [node()]}, - {record_name, shared_subscription}, - {attributes, record_info(fields, shared_subscription)}]); - -mnesia(copy) -> - ok = ekka_mnesia:copy_table(?TABLE). - %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- -spec(start_link() -> {ok, pid()} | ignore | {error, any()}). start_link() -> + ok = ekka_mnesia:create_table(?TAB, [ + {type, bag}, + {ram_copies, [node()]}, + {record_name, shared_subscription}, + {attributes, record_info(fields, shared_subscription)}]), + ok = ekka_mnesia:copy_table(?TAB), gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -spec(strategy() -> random | hash). @@ -113,14 +99,14 @@ subscribers(Group, Topic) -> init([]) -> {atomic, PMon} = mnesia:transaction(fun init_monitors/0), - mnesia:subscribe({table, ?TABLE, simple}), + mnesia:subscribe({table, ?TAB, simple}), {ok, #state{pmon = PMon}}. init_monitors() -> mnesia:foldl( fun(#shared_subscription{subpid = SubPid}, Mon) -> Mon:monitor(SubPid) - end, emqx_pmon:new(), ?TABLE). + end, emqx_pmon:new(), ?TAB). handle_call(Req, _From, State) -> emqx_log:error("[Shared] Unexpected request: ~p", [Req]), @@ -156,7 +142,7 @@ handle_info(Info, State) -> {noreply, State}. terminate(_Reason, _State) -> - mnesia:unsubscribe({table, ?TABLE, simple}). + mnesia:unsubscribe({table, ?TAB, simple}). code_change(_OldVsn, State, _Extra) -> {ok, State}. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index f4ea1468c..56587501e 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -20,29 +20,34 @@ -include("emqx.hrl"). --export([start_link/1]). +-export([start_link/0]). -export([open_session/1, lookup_session/1, close_session/1]). --export([resume_session/1, discard_session/1]). --export([register_session/1, register_session/2]). --export([unregister_session/1, unregister_session/2]). +-export([resume_session/1, resume_session/2, discard_session/1, discard_session/2]). +-export([register_session/2, unregister_session/1]). %% Internal functions for rpc --export([lookup/1, dispatch/3]). +-export([dispatch/3]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {stats, pids = #{}}). +-record(state, {pmon}). --spec(start_link(fun()) -> {ok, pid()} | ignore | {error, term()}). -start_link(StatsFun) -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [StatsFun], []). +-define(SM, ?MODULE). + +-spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +%%-------------------------------------------------------------------- +%% Open Session +%%-------------------------------------------------------------------- open_session(Attrs = #{clean_start := true, client_id := ClientId, client_pid := ClientPid}) -> CleanStart = fun(_) -> - discard_session(ClientId, ClientPid), + ok = discard_session(ClientId, ClientPid), emqx_session_sup:start_session(Attrs) end, emqx_sm_locker:trans(ClientId, CleanStart); @@ -61,13 +66,26 @@ open_session(Attrs = #{clean_start := false, end, emqx_sm_locker:trans(ClientId, ResumeStart). -discard_session(ClientId) -> +%%-------------------------------------------------------------------- +%% Discard Session +%%-------------------------------------------------------------------- + +discard_session(ClientId) when is_binary(ClientId) -> discard_session(ClientId, self()). -discard_session(ClientId, ClientPid) -> - lists:foreach(fun({_, SessionPid}) -> - catch emqx_session:discard(SessionPid, ClientPid) - end, lookup_session(ClientId)). +discard_session(ClientId, ClientPid) when is_binary(ClientId) -> + lists:foreach( + fun(#session{pid = SessionPid}) -> + case catch emqx_session:discard(SessionPid, ClientPid) of + {'EXIT', Error} -> + emqx_log:error("[SM] Failed to discard ~p: ~p", [SessionPid, Error]); + ok -> ok + end + end, lookup_session(ClientId)). + +%%-------------------------------------------------------------------- +%% Resume Session +%%-------------------------------------------------------------------- resume_session(ClientId) -> resume_session(ClientId, self()). @@ -75,99 +93,106 @@ resume_session(ClientId) -> resume_session(ClientId, ClientPid) -> case lookup_session(ClientId) of [] -> {error, not_found}; - [{_, SessionPid}] -> + [#session{pid = SessionPid}] -> ok = emqx_session:resume(SessionPid, ClientPid), {ok, SessionPid}; - [{_, SessionPid}|_More] = Sessions -> + Sessions -> + [#session{pid = SessionPid}|StaleSessions] = lists:reverse(Sessions), emqx_log:error("[SM] More than one session found: ~p", [Sessions]), + lists:foreach(fun(#session{pid = Pid}) -> + catch emqx_session:discard(Pid, ClientPid) + end, StaleSessions), ok = emqx_session:resume(SessionPid, ClientPid), {ok, SessionPid} end. +%%-------------------------------------------------------------------- +%% Close a session +%%-------------------------------------------------------------------- + +close_session(#session{pid = SessionPid}) -> + emqx_session:close(SessionPid). + +%%-------------------------------------------------------------------- +%% Create/Delete a session +%%-------------------------------------------------------------------- + +register_session(Session, Attrs) when is_record(Session, session) -> + ets:insert(session, Session), + ets:insert(session_attrs, {Session, Attrs}), + emqx_sm_registry:register_session(Session), + gen_server:cast(?MODULE, {registered, Session}). + +unregister_session(Session) when is_record(Session, session) -> + emqx_sm_registry:unregister_session(Session), + emqx_sm_stats:del_session_stats(Session), + ets:delete(session_attrs, Session), + ets:delete_object(session, Session), + gen_server:cast(?MODULE, {unregistered, Session}). + +%%-------------------------------------------------------------------- +%% Lookup a session from registry +%%-------------------------------------------------------------------- + lookup_session(ClientId) -> - {ResL, _} = multicall(?MODULE, lookup, [ClientId]), - lists:append(ResL). + emqx_sm_registry:lookup_session(ClientId). -close_session(ClientId) -> - lists:foreach(fun(#session{pid = SessionPid}) -> - emqx_session:close(SessionPid) - end, lookup_session(ClientId)). - -register_session(ClientId) -> - register_session(ClientId, self()). - -register_session(ClientId, SessionPid) -> - ets:insert(session, {ClientId, SessionPid}). - -unregister_session(ClientId) -> - unregister_session(ClientId, self()). - -unregister_session(ClientId, SessionPid) -> - case ets:lookup(session, ClientId) of - [Session = {ClientId, SessionPid}] -> - ets:delete(session_attrs, Session), - ets:delete(session_stats, Session), - ets:delete_object(session, Session); - _ -> - false - end. +%%-------------------------------------------------------------------- +%% Dispatch by client Id +%%-------------------------------------------------------------------- dispatch(ClientId, Topic, Msg) -> - case lookup(ClientId) of - [{_, Pid}] -> + case lookup_session_pid(ClientId) of + Pid when is_pid(Pid) -> Pid ! {dispatch, Topic, Msg}; - [] -> + undefined -> emqx_hooks:run('message.dropped', [ClientId, Msg]) end. -lookup(ClientId) -> - ets:lookup(session, ClientId). - -multicall(Mod, Fun, Args) -> - multicall(ekka:nodelist(up), Mod, Fun, Args). - -multicall([Node], Mod, Fun, Args) when Node == node() -> - Res = erlang:apply(Mod, Fun, Args), [Res]; - -multicall(Nodes, Mod, Fun, Args) -> - {ResL, _} = emqx_rpc:multicall(Nodes, Mod, Fun, Args), - ResL. +lookup_session_pid(ClientId) -> + try ets:lookup_element(session, ClientId, #session.pid) + catch error:badarg -> + undefined + end. %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- -init([StatsFun]) -> - {ok, sched_stats(StatsFun, #state{pids = #{}})}. - -sched_stats(Fun, State) -> - {ok, TRef} = timer:send_interval(timer:seconds(1), stats), - State#state{stats = #{func => Fun, timer => TRef}}. +init([]) -> + _ = emqx_tables:create(session, [public, set, {keypos, 2}, + {read_concurrency, true}, + {write_concurrency, true}]), + _ = emqx_tables:create(session_attrs, [public, set, + {write_concurrency, true}]), + {ok, #state{pmon = emqx_pmon:new()}}. handle_call(Req, _From, State) -> emqx_log:error("[SM] Unexpected request: ~p", [Req]), {reply, ignore, State}. -handle_cast({registered, ClientId, SessionPid}, - State = #state{pids = Pids}) -> - _ = erlang:monitor(process, SessionPid), - {noreply, State#state{pids = maps:put(SessionPid, ClientId, Pids)}}; +handle_cast({registered, #session{sid = ClientId, pid = SessionPid}}, + State = #state{pmon = PMon}) -> + {noreply, State#state{pmon = PMon:monitor(SessionPid, ClientId)}}; + +handle_cast({unregistered, #session{sid = _ClientId, pid = SessionPid}}, + State = #state{pmon = PMon}) -> + {noreply, State#state{pmon = PMon:erase(SessionPid)}}; handle_cast(Msg, State) -> emqx_log:error("[SM] Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info(stats, State) -> - {noreply, setstats(State), hibernate}; - handle_info({'DOWN', _MRef, process, DownPid, _Reason}, - State = #state{pids = Pids}) -> - case maps:find(DownPid, Pids) of + State = #state{pmon = PMon}) -> + case PMon:find(DownPid) of {ok, ClientId} -> - unregister_session(ClientId, DownPid), - {noreply, State#state{pids = maps:remove(DownPid, Pids)}}; - error -> - emqx_log:error("[SM] Session ~p not found", [DownPid]), + case ets:lookup(session, ClientId) of + [] -> ok; + _ -> unregister_session(#session{sid = ClientId, pid = DownPid}) + end, + {noreply, State}; + undefined -> {noreply, State} end; @@ -175,16 +200,9 @@ handle_info(Info, State) -> emqx_log:error("[SM] Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, _State = #state{stats = #{timer := TRef}}) -> - timer:cancel(TRef). +terminate(_Reason, _State) -> + ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -setstats(State = #state{stats = #{func := Fun}}) -> - Fun(ets:info(session, size)), State. - diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl new file mode 100644 index 000000000..e718ee3e7 --- /dev/null +++ b/src/emqx_sm_registry.erl @@ -0,0 +1,109 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Inc. 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_registry). + +-behaviour(gen_server). + +-include("emqx.hrl"). + +%% API +-export([start_link/0]). + +-export([register_session/1, lookup_session/1, unregister_session/1]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-define(SERVER, ?MODULE). + +-define(TAB, session_registry). + +-define(LOCK, {?MODULE, cleanup_sessions}). + +-record(state, {}). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +start_link() -> + ok = ekka_mnesia:create_table(?TAB, [ + {type, bag}, + {ram_copies, [node()]}, + {record_name, session}, + {attributes, record_info(fields, session)}]), + ok = ekka_mnesia:copy_table(?TAB), + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +-spec(lookup_session(client_id()) -> list(session())). +lookup_session(ClientId) -> + mnesia:dirty_read(?TAB, ClientId). + +-spec(register_session(session()) -> ok). +register_session(Session) when is_record(Session, session) -> + mnesia:dirty_write(?TAB, Session). + +-spec(unregister_session(session()) -> ok). +unregister_session(Session) when is_record(Session, session) -> + mnesia:dirty_delete_object(?TAB, Session). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +init([]) -> + ekka:monitor(membership), + {ok, #state{}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({membership, {mnesia, down, Node}}, State) -> + global:trans({?LOCK, self()}, + fun() -> + mnesia:transaction(fun cleanup_sessions/1, [Node]) + end), + {noreply, State}; + +handle_info({membership, _Event}, State) -> + {noreply, State}; + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +cleanup_sessions(Node) -> + Pat = [{#session{pid = '$1', _ = '_'}, + [{'==', {node, '$1'}, Node}], ['$_']}], + lists:foreach(fun(Session) -> + mnesia:delete_object(?TAB, Session) + end, mnesia:select(?TAB, Pat)). + diff --git a/src/emqx_sm_stats.erl b/src/emqx_sm_stats.erl new file mode 100644 index 000000000..4d7766f45 --- /dev/null +++ b/src/emqx_sm_stats.erl @@ -0,0 +1,72 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Inc. 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_stats). + +-behaviour(gen_statem). + +-include("emqx.hrl"). + +%% API +-export([start_link/0]). + +-export([set_session_stats/2, get_session_stats/1, del_session_stats/1]). + +%% gen_statem callbacks +-export([init/1, callback_mode/0, handle_event/4, terminate/3, code_change/4]). + +-define(TAB, session_stats). + +-record(state, {statsfun}). + +start_link() -> + gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []). + +-spec(set_session_stats(session(), emqx_stats:stats()) -> true). +set_session_stats(Session, Stats) when is_record(Session, session) -> + ets:insert(?TAB, {Session, [{'$ts', emqx_time:now_secs()}|Stats]}). + +-spec(get_session_stats(session()) -> emqx_stats:stats()). +get_session_stats(Session) -> + case ets:lookup(?TAB, Session) of + [{_, Stats}] -> Stats; + [] -> [] + end. + +-spec(del_session_stats(session()) -> true). +del_session_stats(Session) -> + ets:delete(?TAB, Session). + +init([]) -> + _ = emqx_tables:create(?TAB, [public, {write_concurrency, true}]), + StatsFun = emqx_stats:statsfun('sessions/count', 'sessions/max'), + {ok, idle, #state{statsfun = StatsFun}, timer:seconds(1)}. + +callback_mode() -> handle_event_function. + +handle_event(timeout, _Timeout, idle, State = #state{statsfun = StatsFun}) -> + case ets:info(session, size) of + undefined -> ok; + Size -> StatsFun(Size) + end, + {next_state, idle, State, timer:seconds(1)}. + +terminate(_Reason, _StateName, _State) -> + ok. + +code_change(_OldVsn, StateName, State, _Extra) -> + {ok, StateName, State}. + diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index 51a495f58..f65dead14 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -26,15 +26,12 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - lists:foreach(fun create_tab/1, [session, session_stats, session_attrs]), + Childs = [child(M) || M <- [emqx_sm_locker, + emqx_sm_registry, + emqx_sm_stats, + emqx_sm]], + {ok, {{one_for_all, 10, 3600}, Childs}}. - StatsFun = emqx_stats:statsfun('sessions/count', 'sessions/max'), - - SM = {emqx_sm, {emqx_sm, start_link, [StatsFun]}, - permanent, 5000, worker, [emqx_sm]}, - - {ok, {{one_for_all, 10, 3600}, [SM]}}. - -create_tab(Tab) -> - emqx_tables:create(Tab, [public, ordered_set, named_table, {write_concurrency, true}]). +child(M) -> + {M, {M, start_link, []}, permanent, 5000, worker, [M]}. diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index d93c1b969..83048a001 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -26,8 +26,7 @@ -export([all/0]). %% Client and Session Stats --export([set_client_stats/2, get_client_stats/1, del_client_stats/1, - set_session_stats/2, get_session_stats/1, del_session_stats/1]). +-export([set_client_stats/2, get_client_stats/1, del_client_stats/1]). %% Statistics API. -export([statsfun/1, statsfun/2, getstats/0, getstat/1, setstat/2, setstat/3]). @@ -40,6 +39,8 @@ -type(stats() :: list({atom(), non_neg_integer()})). +-export_type([stats/0]). + -define(STATS_TAB, mqtt_stats). -define(CLIENT_STATS_TAB, mqtt_client_stats). -define(SESSION_STATS_TAB, mqtt_session_stats). @@ -101,20 +102,6 @@ get_client_stats(ClientId) -> del_client_stats(ClientId) -> ets:delete(?CLIENT_STATS_TAB, ClientId). --spec(set_session_stats(binary(), stats()) -> true). -set_session_stats(ClientId, Stats) -> - ets:insert(?SESSION_STATS_TAB, {ClientId, [{'$ts', emqx_time:now_secs()}|Stats]}). - --spec(get_session_stats(binary()) -> stats()). -get_session_stats(ClientId) -> - case ets:lookup(?SESSION_STATS_TAB, ClientId) of - [{_, Stats}] -> Stats; - [] -> [] - end. - --spec(del_session_stats(binary()) -> true). -del_session_stats(ClientId) -> - ets:delete(?SESSION_STATS_TAB, ClientId). all() -> ets:tab2list(?STATS_TAB). diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index d06a8df79..ec05316c7 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -57,7 +57,6 @@ init([]) -> {ok, {{one_for_all, 10, 3600}, [?CHILD(emqx_ctl, worker), ?CHILD(emqx_hooks, worker), - ?CHILD(emqx_locker, worker), ?CHILD(emqx_stats, worker), ?CHILD(emqx_metrics, worker), ?CHILD(emqx_sys, worker), From d756ce93c6c76f20f06cabbf23a23a5c9134360f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 8 Apr 2018 15:16:36 +0800 Subject: [PATCH 038/520] Remove CONTRIBUTORS.md --- CONTRIBUTORS.md | 26 --- LICENSE-MPL-RabbitMQ | 455 ------------------------------------------- 2 files changed, 481 deletions(-) delete mode 100644 CONTRIBUTORS.md delete mode 100644 LICENSE-MPL-RabbitMQ diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md deleted file mode 100644 index 0e2b17bb8..000000000 --- a/CONTRIBUTORS.md +++ /dev/null @@ -1,26 +0,0 @@ - -* [@callbay](https://github.com/callbay) - -* [@lsxredrain](https://github.com/lsxredrain) - -* [@hejin1026](https://github.com/hejin1026) - -* [@desoulter](https://github.com/desoulter) - -* [@turtleDeng](https://github.com/turtleDeng) - -* [@Hades32](https://github.com/Hades32) - -* [@huangdan](https://github.com/huangdan) - -* [@phanimahesh](https://github.com/phanimahesh) - -* [@dvliman](https://github.com/dvliman) - -* [@vowstar](https://github.com/vowstar) - -* [@TheWaWaR](https://github.com/TheWaWaR) - -* [@hejin1026](https://github.com/hejin1026) - -* [@farhadi](https://github.com/farhadi) diff --git a/LICENSE-MPL-RabbitMQ b/LICENSE-MPL-RabbitMQ deleted file mode 100644 index f1ba9a5ca..000000000 --- a/LICENSE-MPL-RabbitMQ +++ /dev/null @@ -1,455 +0,0 @@ - MOZILLA PUBLIC LICENSE - Version 1.1 - - --------------- - -1. Definitions. - - 1.0.1. "Commercial Use" means distribution or otherwise making the - Covered Code available to a third party. - - 1.1. "Contributor" means each entity that creates or contributes to - the creation of Modifications. - - 1.2. "Contributor Version" means the combination of the Original - Code, prior Modifications used by a Contributor, and the Modifications - made by that particular Contributor. - - 1.3. "Covered Code" means the Original Code or Modifications or the - combination of the Original Code and Modifications, in each case - including portions thereof. - - 1.4. "Electronic Distribution Mechanism" means a mechanism generally - accepted in the software development community for the electronic - transfer of data. - - 1.5. "Executable" means Covered Code in any form other than Source - Code. - - 1.6. "Initial Developer" means the individual or entity identified - as the Initial Developer in the Source Code notice required by Exhibit - A. - - 1.7. "Larger Work" means a work which combines Covered Code or - portions thereof with code not governed by the terms of this License. - - 1.8. "License" means this document. - - 1.8.1. "Licensable" means having the right to grant, to the maximum - extent possible, whether at the time of the initial grant or - subsequently acquired, any and all of the rights conveyed herein. - - 1.9. "Modifications" means any addition to or deletion from the - substance or structure of either the Original Code or any previous - Modifications. When Covered Code is released as a series of files, a - Modification is: - A. Any addition to or deletion from the contents of a file - containing Original Code or previous Modifications. - - B. Any new file that contains any part of the Original Code or - previous Modifications. - - 1.10. "Original Code" means Source Code of computer software code - which is described in the Source Code notice required by Exhibit A as - Original Code, and which, at the time of its release under this - License is not already Covered Code governed by this License. - - 1.10.1. "Patent Claims" means any patent claim(s), now owned or - hereafter acquired, including without limitation, method, process, - and apparatus claims, in any patent Licensable by grantor. - - 1.11. "Source Code" means the preferred form of the Covered Code for - making modifications to it, including all modules it contains, plus - any associated interface definition files, scripts used to control - compilation and installation of an Executable, or source code - differential comparisons against either the Original Code or another - well known, available Covered Code of the Contributor's choice. The - Source Code can be in a compressed or archival form, provided the - appropriate decompression or de-archiving software is widely available - for no charge. - - 1.12. "You" (or "Your") means an individual or a legal entity - exercising rights under, and complying with all of the terms of, this - License or a future version of this License issued under Section 6.1. - For legal entities, "You" includes any entity which controls, is - controlled by, or is under common control with You. For purposes of - this definition, "control" means (a) the power, direct or indirect, - to cause the direction or management of such entity, whether by - contract or otherwise, or (b) ownership of more than fifty percent - (50%) of the outstanding shares or beneficial ownership of such - entity. - -2. Source Code License. - - 2.1. The Initial Developer Grant. - The Initial Developer hereby grants You a world-wide, royalty-free, - non-exclusive license, subject to third party intellectual property - claims: - (a) under intellectual property rights (other than patent or - trademark) Licensable by Initial Developer to use, reproduce, - modify, display, perform, sublicense and distribute the Original - Code (or portions thereof) with or without Modifications, and/or - as part of a Larger Work; and - - (b) under Patents Claims infringed by the making, using or - selling of Original Code, to make, have made, use, practice, - sell, and offer for sale, and/or otherwise dispose of the - Original Code (or portions thereof). - - (c) the licenses granted in this Section 2.1(a) and (b) are - effective on the date Initial Developer first distributes - Original Code under the terms of this License. - - (d) Notwithstanding Section 2.1(b) above, no patent license is - granted: 1) for code that You delete from the Original Code; 2) - separate from the Original Code; or 3) for infringements caused - by: i) the modification of the Original Code or ii) the - combination of the Original Code with other software or devices. - - 2.2. Contributor Grant. - Subject to third party intellectual property claims, each Contributor - hereby grants You a world-wide, royalty-free, non-exclusive license - - (a) under intellectual property rights (other than patent or - trademark) Licensable by Contributor, to use, reproduce, modify, - display, perform, sublicense and distribute the Modifications - created by such Contributor (or portions thereof) either on an - unmodified basis, with other Modifications, as Covered Code - and/or as part of a Larger Work; and - - (b) under Patent Claims infringed by the making, using, or - selling of Modifications made by that Contributor either alone - and/or in combination with its Contributor Version (or portions - of such combination), to make, use, sell, offer for sale, have - made, and/or otherwise dispose of: 1) Modifications made by that - Contributor (or portions thereof); and 2) the combination of - Modifications made by that Contributor with its Contributor - Version (or portions of such combination). - - (c) the licenses granted in Sections 2.2(a) and 2.2(b) are - effective on the date Contributor first makes Commercial Use of - the Covered Code. - - (d) Notwithstanding Section 2.2(b) above, no patent license is - granted: 1) for any code that Contributor has deleted from the - Contributor Version; 2) separate from the Contributor Version; - 3) for infringements caused by: i) third party modifications of - Contributor Version or ii) the combination of Modifications made - by that Contributor with other software (except as part of the - Contributor Version) or other devices; or 4) under Patent Claims - infringed by Covered Code in the absence of Modifications made by - that Contributor. - -3. Distribution Obligations. - - 3.1. Application of License. - The Modifications which You create or to which You contribute are - governed by the terms of this License, including without limitation - Section 2.2. The Source Code version of Covered Code may be - distributed only under the terms of this License or a future version - of this License released under Section 6.1, and You must include a - copy of this License with every copy of the Source Code You - distribute. You may not offer or impose any terms on any Source Code - version that alters or restricts the applicable version of this - License or the recipients' rights hereunder. However, You may include - an additional document offering the additional rights described in - Section 3.5. - - 3.2. Availability of Source Code. - Any Modification which You create or to which You contribute must be - made available in Source Code form under the terms of this License - either on the same media as an Executable version or via an accepted - Electronic Distribution Mechanism to anyone to whom you made an - Executable version available; and if made available via Electronic - Distribution Mechanism, must remain available for at least twelve (12) - months after the date it initially became available, or at least six - (6) months after a subsequent version of that particular Modification - has been made available to such recipients. You are responsible for - ensuring that the Source Code version remains available even if the - Electronic Distribution Mechanism is maintained by a third party. - - 3.3. Description of Modifications. - You must cause all Covered Code to which You contribute to contain a - file documenting the changes You made to create that Covered Code and - the date of any change. You must include a prominent statement that - the Modification is derived, directly or indirectly, from Original - Code provided by the Initial Developer and including the name of the - Initial Developer in (a) the Source Code, and (b) in any notice in an - Executable version or related documentation in which You describe the - origin or ownership of the Covered Code. - - 3.4. Intellectual Property Matters - (a) Third Party Claims. - If Contributor has knowledge that a license under a third party's - intellectual property rights is required to exercise the rights - granted by such Contributor under Sections 2.1 or 2.2, - Contributor must include a text file with the Source Code - distribution titled "LEGAL" which describes the claim and the - party making the claim in sufficient detail that a recipient will - know whom to contact. If Contributor obtains such knowledge after - the Modification is made available as described in Section 3.2, - Contributor shall promptly modify the LEGAL file in all copies - Contributor makes available thereafter and shall take other steps - (such as notifying appropriate mailing lists or newsgroups) - reasonably calculated to inform those who received the Covered - Code that new knowledge has been obtained. - - (b) Contributor APIs. - If Contributor's Modifications include an application programming - interface and Contributor has knowledge of patent licenses which - are reasonably necessary to implement that API, Contributor must - also include this information in the LEGAL file. - - (c) Representations. - Contributor represents that, except as disclosed pursuant to - Section 3.4(a) above, Contributor believes that Contributor's - Modifications are Contributor's original creation(s) and/or - Contributor has sufficient rights to grant the rights conveyed by - this License. - - 3.5. Required Notices. - You must duplicate the notice in Exhibit A in each file of the Source - Code. If it is not possible to put such notice in a particular Source - Code file due to its structure, then You must include such notice in a - location (such as a relevant directory) where a user would be likely - to look for such a notice. If You created one or more Modification(s) - You may add your name as a Contributor to the notice described in - Exhibit A. You must also duplicate this License in any documentation - for the Source Code where You describe recipients' rights or ownership - rights relating to Covered Code. You may choose to offer, and to - charge a fee for, warranty, support, indemnity or liability - obligations to one or more recipients of Covered Code. However, You - may do so only on Your own behalf, and not on behalf of the Initial - Developer or any Contributor. You must make it absolutely clear than - any such warranty, support, indemnity or liability obligation is - offered by You alone, and You hereby agree to indemnify the Initial - Developer and every Contributor for any liability incurred by the - Initial Developer or such Contributor as a result of warranty, - support, indemnity or liability terms You offer. - - 3.6. Distribution of Executable Versions. - You may distribute Covered Code in Executable form only if the - requirements of Section 3.1-3.5 have been met for that Covered Code, - and if You include a notice stating that the Source Code version of - the Covered Code is available under the terms of this License, - including a description of how and where You have fulfilled the - obligations of Section 3.2. The notice must be conspicuously included - in any notice in an Executable version, related documentation or - collateral in which You describe recipients' rights relating to the - Covered Code. You may distribute the Executable version of Covered - Code or ownership rights under a license of Your choice, which may - contain terms different from this License, provided that You are in - compliance with the terms of this License and that the license for the - Executable version does not attempt to limit or alter the recipient's - rights in the Source Code version from the rights set forth in this - License. If You distribute the Executable version under a different - license You must make it absolutely clear that any terms which differ - from this License are offered by You alone, not by the Initial - Developer or any Contributor. You hereby agree to indemnify the - Initial Developer and every Contributor for any liability incurred by - the Initial Developer or such Contributor as a result of any such - terms You offer. - - 3.7. Larger Works. - You may create a Larger Work by combining Covered Code with other code - not governed by the terms of this License and distribute the Larger - Work as a single product. In such a case, You must make sure the - requirements of this License are fulfilled for the Covered Code. - -4. Inability to Comply Due to Statute or Regulation. - - If it is impossible for You to comply with any of the terms of this - License with respect to some or all of the Covered Code due to - statute, judicial order, or regulation then You must: (a) comply with - the terms of this License to the maximum extent possible; and (b) - describe the limitations and the code they affect. Such description - must be included in the LEGAL file described in Section 3.4 and must - be included with all distributions of the Source Code. Except to the - extent prohibited by statute or regulation, such description must be - sufficiently detailed for a recipient of ordinary skill to be able to - understand it. - -5. Application of this License. - - This License applies to code to which the Initial Developer has - attached the notice in Exhibit A and to related Covered Code. - -6. Versions of the License. - - 6.1. New Versions. - Netscape Communications Corporation ("Netscape") may publish revised - and/or new versions of the License from time to time. Each version - will be given a distinguishing version number. - - 6.2. Effect of New Versions. - Once Covered Code has been published under a particular version of the - License, You may always continue to use it under the terms of that - version. You may also choose to use such Covered Code under the terms - of any subsequent version of the License published by Netscape. No one - other than Netscape has the right to modify the terms applicable to - Covered Code created under this License. - - 6.3. Derivative Works. - If You create or use a modified version of this License (which you may - only do in order to apply it to code which is not already Covered Code - governed by this License), You must (a) rename Your license so that - the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", - "MPL", "NPL" or any confusingly similar phrase do not appear in your - license (except to note that your license differs from this License) - and (b) otherwise make it clear that Your version of the license - contains terms which differ from the Mozilla Public License and - Netscape Public License. (Filling in the name of the Initial - Developer, Original Code or Contributor in the notice described in - Exhibit A shall not of themselves be deemed to be modifications of - this License.) - -7. DISCLAIMER OF WARRANTY. - - COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, - WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, - WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF - DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. - THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE - IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, - YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE - COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER - OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF - ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. - -8. TERMINATION. - - 8.1. This License and the rights granted hereunder will terminate - automatically if You fail to comply with terms herein and fail to cure - such breach within 30 days of becoming aware of the breach. All - sublicenses to the Covered Code which are properly granted shall - survive any termination of this License. Provisions which, by their - nature, must remain in effect beyond the termination of this License - shall survive. - - 8.2. If You initiate litigation by asserting a patent infringement - claim (excluding declatory judgment actions) against Initial Developer - or a Contributor (the Initial Developer or Contributor against whom - You file such action is referred to as "Participant") alleging that: - - (a) such Participant's Contributor Version directly or indirectly - infringes any patent, then any and all rights granted by such - Participant to You under Sections 2.1 and/or 2.2 of this License - shall, upon 60 days notice from Participant terminate prospectively, - unless if within 60 days after receipt of notice You either: (i) - agree in writing to pay Participant a mutually agreeable reasonable - royalty for Your past and future use of Modifications made by such - Participant, or (ii) withdraw Your litigation claim with respect to - the Contributor Version against such Participant. If within 60 days - of notice, a reasonable royalty and payment arrangement are not - mutually agreed upon in writing by the parties or the litigation claim - is not withdrawn, the rights granted by Participant to You under - Sections 2.1 and/or 2.2 automatically terminate at the expiration of - the 60 day notice period specified above. - - (b) any software, hardware, or device, other than such Participant's - Contributor Version, directly or indirectly infringes any patent, then - any rights granted to You by such Participant under Sections 2.1(b) - and 2.2(b) are revoked effective as of the date You first made, used, - sold, distributed, or had made, Modifications made by that - Participant. - - 8.3. If You assert a patent infringement claim against Participant - alleging that such Participant's Contributor Version directly or - indirectly infringes any patent where such claim is resolved (such as - by license or settlement) prior to the initiation of patent - infringement litigation, then the reasonable value of the licenses - granted by such Participant under Sections 2.1 or 2.2 shall be taken - into account in determining the amount or value of any payment or - license. - - 8.4. In the event of termination under Sections 8.1 or 8.2 above, - all end user license agreements (excluding distributors and resellers) - which have been validly granted by You or any distributor hereunder - prior to termination shall survive termination. - -9. LIMITATION OF LIABILITY. - - UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT - (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL - DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, - OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR - ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY - CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, - WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER - COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN - INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF - LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY - RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW - PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE - EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO - THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. - -10. U.S. GOVERNMENT END USERS. - - The Covered Code is a "commercial item," as that term is defined in - 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer - software" and "commercial computer software documentation," as such - terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 - C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), - all U.S. Government End Users acquire Covered Code with only those - rights set forth herein. - -11. MISCELLANEOUS. - - This License represents the complete agreement concerning subject - matter hereof. If any provision of this License is held to be - unenforceable, such provision shall be reformed only to the extent - necessary to make it enforceable. This License shall be governed by - California law provisions (except to the extent applicable law, if - any, provides otherwise), excluding its conflict-of-law provisions. - With respect to disputes in which at least one party is a citizen of, - or an entity chartered or registered to do business in the United - States of America, any litigation relating to this License shall be - subject to the jurisdiction of the Federal Courts of the Northern - District of California, with venue lying in Santa Clara County, - California, with the losing party responsible for costs, including - without limitation, court costs and reasonable attorneys' fees and - expenses. The application of the United Nations Convention on - Contracts for the International Sale of Goods is expressly excluded. - Any law or regulation which provides that the language of a contract - shall be construed against the drafter shall not apply to this - License. - -12. RESPONSIBILITY FOR CLAIMS. - - As between Initial Developer and the Contributors, each party is - responsible for claims and damages arising, directly or indirectly, - out of its utilization of rights under this License and You agree to - work with Initial Developer and Contributors to distribute such - responsibility on an equitable basis. Nothing herein is intended or - shall be deemed to constitute any admission of liability. - -13. MULTIPLE-LICENSED CODE. - - Initial Developer may designate portions of the Covered Code as - "Multiple-Licensed". "Multiple-Licensed" means that the Initial - Developer permits you to utilize portions of the Covered Code under - Your choice of the NPL or the alternative licenses, if any, specified - by the Initial Developer in the file described in Exhibit A. - -EXHIBIT A -Mozilla Public License. - - ``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. - - The Initial Developer of the Original Code is Pivotal Software, Inc. - Copyright (c) 2007-2016 Pivotal Software, Inc. All rights reserved.'' - - [NOTE: The text of this Exhibit A may differ slightly from the text of - the notices in the Source Code files of the Original Code. You should - use the text of this Exhibit A rather than the text found in the - Original Code Source Code for Your Modifications.] From 5f32f3c560129623e97ed6be8fe05114129b4a12 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 8 Apr 2018 16:37:30 +0800 Subject: [PATCH 039/520] Pass the paho interoperability tests --- src/emqx_protocol.erl | 2 +- src/emqx_session.erl | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 2976c79a5..90560e784 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -572,7 +572,7 @@ sp(false) -> 0. %%-------------------------------------------------------------------- clean_retain(false, Msg = #message{flags = #{retain := true}, headers = Headers}) -> - case lists:member(retained, Headers) of + case maps:get(retained, Headers, false) of true -> Msg; false -> emqx_message:set_flag(retain, false, Msg) end; diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 19147c242..f77d9da24 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -29,7 +29,7 @@ -import(proplists, [get_value/2, get_value/3]). %% Session API --export([start_link/1, resume/3, discard/2]). +-export([start_link/1, resume/2, discard/2]). %% Management and Monitor API -export([state/1, info/1, stats/1]). @@ -215,9 +215,9 @@ unsubscribe(SessionPid, TopicTable) -> gen_server:cast(SessionPid, {unsubscribe, self(), TopicTable}). %% @doc Resume the session --spec(resume(pid(), client_id(), pid()) -> ok). -resume(SessionPid, ClientId, ClientPid) -> - gen_server:cast(SessionPid, {resume, ClientId, ClientPid}). +-spec(resume(pid(), pid()) -> ok). +resume(SessionPid, ClientPid) -> + gen_server:cast(SessionPid, {resume, ClientPid}). %% @doc Get session state state(SessionPid) when is_pid(SessionPid) -> @@ -457,7 +457,7 @@ handle_cast({pubcomp, PacketId}, State = #state{inflight = Inflight}) -> end, hibernate}; %% RESUME: -handle_cast({resume, ClientId, ClientPid}, +handle_cast({resume, ClientPid}, State = #state{client_id = ClientId, client_pid = OldClientPid, clean_start = CleanStart, From 39ff658e587f2b9399c97271d326a8b88c2c880e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 8 Apr 2018 17:27:46 +0800 Subject: [PATCH 040/520] MQTT Specification Version 5.0 --- docs/mqtt-v5.0.pdf | Bin 0 -> 1880627 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/mqtt-v5.0.pdf diff --git a/docs/mqtt-v5.0.pdf b/docs/mqtt-v5.0.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cbcc0d029ec9270858555a05672ddf1b8aac0130 GIT binary patch literal 1880627 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

tM#Au8I*~?lA@q*{f6IFw*Pw0_1D!&S&st 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
`!ORDj8x?R zN~O-wC>E?OaN{a)R4wC>)9RF+eI1#U?T#hdTBi<~Bgp4Yk7k%wsO!O>q0KjOnu8{t z=V^vBMB_S>)HWm4nd}^8pXVIoOj8P+8Q_r|ON^_|aaz4|qFRHyIm%UMo^z@;8u)a^ zXEJ_+KF3*%S-C(5oh3@0bFp)|nxsr|u2S}^ql_3e$64O*x^umjsfL`D&dsz6 zIBT3+o!iu@&Rx#E&PH{j^N2Rfxl74%wmDCt3NfyorrPT~=e+2=?7Zd@)Ebw~sBp!p zC7RErP~~?S=-cS>=~ICxINMx<^$gc=rNuSc*{F?mjR1H+hrhLVqVn$t{eI*_r;+8# zM9=Zs7JC)EEx9JS?(f2(|5)I4uE~x?`eto|E8l*`HO+b1Rp_G3uW%Ki#$KX2=9=$X z1iRLAO>?=H=#?}U)lb(7eU~#2nUm{UjkPI7Pj;m4Q0wcE81UK;AEbu3b{T?dsaSF`IlR>K%@=D@Bt*J-`Pb)G4g90$R9#dX~+xnn4k z-SKYC?Q#d)iBzfFL)|0XDat%|sO&QeC z4*U!%1}dStp7eBIaDVE)3VUa|J3Us9?CI;#JswZUljKSEj8Z=JjPay-GCbov6FoUf zsVC1f)id2Q(=*3Y>?!dq_AK|TayEL()f~@yPo-zGr^X)eY*n&RNvdeQHc~y?JiCCW z(R}ypMeMRXjh-W(HYMA0(wIQ`<2h%q@LV(|P<%b-^vj;ho@?rQuVAFAm0p`S&Z~G0 zPhYRk66KPf(Gz!M zddJ(Vv5uUldPTT*65&{<+O=k5o@O8 zt63T0i1Tj2+B-st>31EYj8_8QT5r8J*82|44n=~tm>ud=(09|!CGSOM#}$mY&wEg5 zKwa9PZE@!sF|K^g=WX^L_nuZ~dXFnH*g@rZ&qLZJeK}UNU3#Mu?>VOyBX5!O-YeW< zTz$Du^2KPGXydU98taR9=c2{*^#y#1m}|90s&A-o1htrbNxl?yrZ3f*Z|Zoahpr6y4+Xot263o zH784r7^klJ^c-J!l|?V#D@+v7td-4*J3-yvU%tI*f(JEO&+ZPCy9Mqpl#@?G#A zr~M)IwY!|U?^EAZZM5F#296p#)2Y*3(^paJ>vSJ7V*FO$P;T4ZF04%{6j|t*ZPfXO z>N$Sd+?8oQe_yBWtp`Q-d;B3p;hfs$Pl8r1UklNlB*do~Xn-*ZXUfHU6!rGx7dyv;+6=^6$l}CmAWA zl+%9R-^ln8#@n<@gwu-YKdGGtE)yL)rS-tARN4IJoQu_o{z}3faVGA_q%(;BBJK9| zsj%ZR_DM7SR@Z!twcn9I`p_KlUqc;R6A%J6qU)Et=z%z;C{W~Gi#2*bt*2NYo1MjhC72HrT|UD7 z*9dnWA>6y0aKG%?8(4u^JXURVPqC-pz@2m8!$pDBl*z_^=)5+t!B-L3$aoy%TO5Uf zS|!V!qK*mFL+@ID8_rf^0`CNN2WkWR0tel$Kz#&nCLHo>1IL;Fw4*j~UO5!Fq;C#f zQC0@d2d*nKf>JQ12Odv&P$S%zixsmr=yI>o5`0;N!{=*g2XzAv255hRo$6^l$K8T; zt=N?rOw@CNL!HTV5_IMTM`)vs7Uk1mihFi&L@?F4S*dm`a;F5-J#F?n!X2jx2c7o` z!E|j4BA!lro?yDW1*=#cBKcd2eX0aYNs(Dj|8U#3xYF(vxD=r(ZL15 zQf*6cX>cWW7Y)HR>hj>aV1-&kZ4j%88+k~z2-z}9ZPXW2t=5cSwLfG`h_rRVhTx9i zp5XrApt$B-WKghJY+$l2CB)tcwo zo@Z6_l*;GRP*N!Q=2NP94h@Y8jR~cNGD71*6GJ(nywKFp^w7-EoKSJ7B(&IUO*x_E zp;e*s(E3niXfur)s^M|x(72(kk#jVin&;4|Ikb&537MMAxuHpDuW5x@8|jQ3YNXRK zTDFVM?2ybSYpO`xcOp(+SUW;TXzd8K5hZkzW?yJ;=$twxbTLwCN3oulL)ZEXzH0Sa zf14n79*yc0#4k45;(&i_i$(hpwO@=6FBZh`Th=OW-xowdu=#-Rx2|H!+uY7#N)@*= zxLty_ncKH4$GF`?bZP~`(m`$bC2r~0`F$dz)HD7T)6I4|x1+h;&h4MMUC3=AxAcw% zq4Pt=mqN}b)Hi%3e28V5+HxD^VaiU{XB*@H3+*oJYiQS6PBQ0Cma`JLP253r>vV3b z`EE7$`X#sY?u5wun8Ix=C!gE>-0l)YsnVtiqU9X5(mJwHdVz53IHtV7l#5I`$(%Y< zmJlALG0zgF{Em65nbJTM>wM-9aC}UcE5f zyLl$gS$Bf*BIZeCyp4H^89%|cj3PNv*LcKfJbDb{$Eg)NskO#(o5CaBOVY$y%;RRB zt&IPI@$;;$%A@Rv`Y+OlDLa_wBU2`i@_Ux{0qby%>Fq3eA9H@fcg zMWzg79U7Q2hO;z*@!=f(}ZENu*1SIBXkZDum* zBdsEfU}F)D7_qvDZ9yCT@8-;6k4+>A(GK=iof$PVqd3+V*#nQUXC(HLi}3*A@F%s6 zW}Xu+u3*X%;t_{(yh@pFMDTRZy)=p@b0!j}a4j-BO#g!(HPxhZG*_^VJlBLabB?ek z`+1bh?3rJ%-(KLcE}NcVnZr!0*<8TTQz-UQ4)dTGYpAH*<6j znDdnGS|yH7bo3P9zh{{_9N)d{frq)T$E2J3nDve6cQNHUYdDDYe}%0c#8&^#j3D8{ z8TRl_wyTEY)yaMTCvz5>b@+>o;0c?R(43V-L5`6=Zl9!6>=@eDL861NXuZsWI!S8F9|Nq0HHveK>?XEVZzlAzVvcq z$)DVwX74>@E9Tlbkn)Y^oosrHc*M`l)r$Gkne#8i2@g}>1-3_ld)a$u=q|iRcfITf zFZB{mP;1dy!Vty-;a9k34&>PVm^|4zlk$r1o@4qt#y_&IxO`e-;BegwD^N4-;J-!=ldl}x_XVy=anPjes%-_hI zNv3Z(bB?jJh1{+*cOaY(ce5O>6;UPT=uBV4(fKzXYY}_wD9_WO=H7rgud*~3<1t)? zGP&1iru61mnDr z6~~rElo;N1*d`GrMq-Q7I0K(%d=~rgG1lbw=B#1)9LHYCJTG{+X?vA%UYBhH**{~= z*~I?5!0{5UpNOIe#W_rnQ)BTx8uOio&{8u_oy}N)=bLYQFoZ zIZBjSU8#mlWo>hbU;a63_yT)?ceb_=+dj+OeX&-YW1^MiJR7N#wAy}V)+^S;7G-8^ z8dq!@&j@n%J8NZXQuI669Wh<_jyZ3McwLHog)@n_}8|1h2 zs1mA9%sIrYW9H78=g6;E=T|s4xawLzCkfGo5qbjG7OqY%90!T z?p<8BL*}~7lquYP!2IbcA9C#~WbZxB@*n3Y^E|Tk;^^}_ zZkxpT1017V9ygaM23x(GDA;F`$6jI^TX?LO%zR)xk9oG66__*RudG``^g6WvZtg`{ z=i%Izaj$mnb-}FpyiymK=S1%N5X=8DSs{*Q${e=6#q=0w(I3rhr@K+>%~PJ~CAOl@ z%v&C-liLcW#4!FmYqFb1pA|VZ7n2-bd0Tmn{)q9<7=MS?)93iC_Xdxf$N68x`fx?E zEM|YQhb@cQ+oo?H=er?3O;qwOWgX>V)Q@==trL#568m23o~U}>C*+b$=A6TK&vV~6 zZYSDO(Cb(fRU4i)7Ct{p(NXlqzdly7j?Z-Bqfv3V8*9bxRQy%sG6kcnvn$w8As1;7|`C=O5A2Pj@ z+g5H5n5XQ{!Dt26TwqEv%gkk&$=r7vSC2RW^Ys6)_dVcM9o4=wv;Urx^DmUrG}0I; zMw%NDQ;I1fMda}iDW;J|ibyG9%B9FdM5J6qMC3&-O%YQ{Q!d7c7->Wv54jY9rbsD8 zq!=lWhlogdy@-`k9*^5rOx{|*+52RlLjnmT{OQ+ke{0P;Yi8E0Su?X|_TFpeh+@H~ z_L}SoW<6^0d6wC#KozO}>K>dqHcZ{C?oXa6_Mn=ezN99puc)u%+^|R0Y_-tXWI8zG zdl8uxL^l4#V2l_q%ES~gov2x2u2>)zi6vs0SRraCtQBj;2C-Sxi=ASRXb=a5 zY`V#g5iVjZgo(FJ=0^XCSwA&;tJLT98XUNU`NsP|*Eg?;^t#x{Z(h}^A;1_13hP$= z{y^>1?*>3Wuh%lbi}b1rat7+v8G2|$bRk?Ns`+d}^AAFaZYkbx{7)x+#We8^(O*0& zzAXmB`|UGg9bNZ|XXzR#UZLx=;v`+grbAY|&wRlAf+#V+Xns-LZ$4x`BudRMnG?n5 z%`&r0JYY^Sr-(7;G;^Bxf;rusF2Tfh?3oa-bX{hs$CLOXXNOfv!n%D&ZM&wwxyy(zRGFmCNNy zxr%aHY!tvRYHa3D%DMOm-AE) z)l2nOeW}k4QbXzLrAAPS5;dCo!L8~Px3RSePi;>S-T}PAT+VYG_;&*To!@+#jZDi) zjE#}tKMhWS^(z{W|Bq=)0t<5)X309tM9Z*3s6#0%=SN5r2EK{0GaS-h12zD5n} zX3)qoUg4AZX;rhES8L|&+*deO?BX*;jed+}9!~H7K5~7^c!p<8(6c#~dfRRMCd8pAZbe8`zV_K{C68fxXB~C~_!PT*TZSAmLa36Ljx&Iupi2s4B z><9X!XoA1zgYfr!$T(-bEi2*axg4IJHSqLofT!o5;pzFR{Yz)K8VvuWE+R+@>s9O+P%bN~V*JysU58EZ$FFL@lyRR2? z;dr)>XV+w05C%tfrvT^kf$QXfuN&;g^=2CkqcVNlcx4CMkYyz6Sn0)ic7#md9)6qq z`MAuH(NW1Z_GNQ8M(Y52e6$SVl<0Kd?#a+A8?vZ9lcBF8+l%z#Iz(qh=U$NCt6MT$ z5M2~q5?vNu5v>X8nNpW@U9(c{khI69fNoSD-+s`C>*j@MkHGI_pY?6&*$+Iw4Qiun zgJmSc>Q?IS_t)yw`X_bb{=<3wHfOyyc)aw`PqR5%AKe+<6K#kdj2?*|Z`Cd>glOM3 z#9u01yUIzo^QP4Hx>yJ~{_m4sy;}=CyQJGXdRp7*Qla1ONjnF_Ks|zeKcx>a?_BhJ zOvPNL{V;h?_Qq!{7VDZ!ldZ>PEH{?t->+i@vBFqUY@lzmq+awpHY7GYRvar$_8(3^ zHZ~zPiEK_~C&#&$CpI-U1NU2&TOL~(Tjh^|*qYe7 z*v8nF*!I{iz~0#Y*rC`_@Lr3(&cBiFOtcT~m+5UxHjKSRveNnf_~*JcHkaYY$IW;+ zo``o#w&kVz-e7&7PloAzD*b+yc3<*s6z?AI5$_f6?cYP z0WHl>Y5e%0_|W)>cu9P8d|Z5Dd~$qRd}eT-iqDD9kC(?Q;+65LlsVL!XVW2G9bb(( z*9TutT^#dQd{cZ|e1~o;)V~#o?~d! zaPCQDC2|n$Mr=W2@(SmgOP7oVr0->iBX9$iSdcD#FWJJ#H_^J z#Dc`4#FE6a#EL{sqBgNMu_3WJQJ>hE*pp~TzZbSN_a*0);C;B2Fui|WAkUBId2Vo^ zzqN4RPnnC-=A`8Mz#k|698}rdmynKRBo2D>vuF+zM|6App%0g;XRG(xU|$Y~-ndE5 zcj>>gQuX%h6AY8{e%d`LC2URKoK$;WP@XqeUNEnwhrL}QM}UQk}<*vttnt-F1>nUgZ7ri{U~`;&hk%AAoo+g}gy*hd`Q z4>RY*57SfEx2)$0&#~y~qv;Dx$B2H2pC;u`?YNMQbxjo8G=jZgtWvyzd#3STqF)64 zD%0kxpuftrjW6Lwccv9+3EC7$BS3!*^xa6m2I=o-+IbB0PNpT~3&>YUBay}`b(eGL zjP1J_yO3-`vU3|GROr$<)_fJ|b?NhT>C91sATyUa#+{&_LN0uNmXlG|5#)LUX`q{g zZsuRX|2opV1p2tvA6m_Y4)-AC0mMFnnmz!20djQ#|9jxiMXm>+=RxpaN1D4pS0T+% zm-r=z}qLF^3Z_BM1o4;?~Cxd8mr;J*b4FMvM*^cm2Pq7=MoHupeU8DhI3HUqI+ z!9Rj>Zw4KPXE3LaUi*H5aXH>Q=_PWaBpEtw7!vp)F_wv@6hFz+tqw zi&~qy)^DQpS)lQqT=j-DSX({^ZO4HBbI>)Qe}^;+kY+pRO`s1$&fkz`CTQIvA6lw||6C(lor zFBgN4Z=`Aw^nniEo7mBU|Lsg#j|(L-QrQ3JS+zBE@CkR?@^bFVFWA1DuOj)o!BV0i0xvR*h_dn(TBuQ@tSyD zoDpw{Mrq2h*eesVo9vFO2SG2{n{Z#c`pZFbs2oARO5|ucPEM4QGddM-F*mTM z15!r7{|^2!^=qI{{xeGawSLpt{BF`%z_IFK^G>b{bL36dgMxkZviTb_B-{;L20TpD$Xmm-xtAq- ztXxb^uT!+KEb=ZRh-UlicKUp^`d#a-0)9}`8?u0yF!hl>&Thw1x7iMU^siqBK; znSp;toQL=3znA#>K&g5Vl%Q`-6qCg?N;6K(5%Wd4s1TKS`@dS$iS=TW*d}%m-Yxct z1LCkaM)&~X3c5~En;ql1S*>i^i!W1e^1D{%7`aT#+YtL>j#Z;UzXEza@FJw2;_*5C zS#bIQ4*~AN-?pe+fmr@uwFacmgM>WfItly@q-li5>^q?Kf7JdOSX+~8z}9@4#!~K$ z{w^2Z(*2EF?f&*4d#F9aF0n`3-NWuLX*afD+NXT1}#k2zhOEc>jJT zl~hj#lJ3lP7TD9M&Q+u<1DD27;w+-HBsWava^IFPS)Tob5@Y; z58E@H8mHD-OP1U~;bx~Eq z{!psTkre$?fZ|x^xGsZ2Y9AoXF z?gVEUThg6GaZ{b1R3f#ZJHwsr&T|)1j}Y!+cd5JFUFoiJ*SPE4jqWOYy}QNT?zbeD z;O?T58D=00QF3Yk1QDGa+~LkSAIk!HK?L80z;b*P6sHPnlwdsnD;sBfr$ zXi#V<@kfvaM}hxUaIgbs&}*%L!2>~Wz})Os#@ z)*yRZ=xpenun61XNVp5vCUn-`5zY$dgnLq$AMWGZ%b6AK7akBE?AYO9bO#y@yX()`sWOeeP5eFt(lA@B-3d zQE2i7J-EUV0JKkd2}5{Ucm=YN98lD*ZnShlIne$bg^m8m_@uf)Q9O_8$K64A3m3%GTe-qGr&Cq z`&Cnan&!-M&xBUf-Fb&SI-{%Gm2^Fz@5MAac4uT~^w2Kh|KH=qp zeW2l5y^DzD%Toec#iqdP4CJQY!89s?apz~4fVT3ff$Tb8Os0Dw5Zac=I z)r{Rb#t!)Q5=fiNmLWN_+2<6cA7!8s9JCz(3+zJabDC^;UD<3gl)DA^ zZs1BUFKf~Vn)KKGu#wl^bHLvTJP&rRMy?^K^+c2&MvEMTWl9)ZUA4}-bl4EJAe&D^ zE3lO&p_J2*S*-oez$x}@IFZ-IkdIwla=W%9G~A-c1+?0N^ao(8QMymU4%1Pu8syr; zbuo*eVV&0^_2Ab-&VKZ({oL-*WRLdd1OF)6u}dSrcN&Slyr+u+IR{TM#=RI&23%3C?1~=7Ez1y3!lJ zT8A1)n+DBwOL59cjLqKA?W?G1E_9m(e43tq$THA>FIsl0zR$?Rc=qS^)lVuI_3AC? zITiAcqs37RF~)0Ow(~~lP@qR5umFyTZuInUl%9TPi*^&8?azKR#Ojim)DX`LEVDTTHbWvy6*PM+!v_3uUC~9zbQ@MbVx71 zDgYG$@O9hfD#6*!1vp*Dp1GykyJbSVO=1-A9CD*fNg_uz78B7_QGQU9V*hw zuJGE^4}-c_d*O)|@>fPu>v)RbEWtZzdIsx~u4}p-(!=yJtG&9l7W#eGuY+&DZOt;G z;v!|WRR44x(!))GcJRyhdFi2-CTbHz2)YntUBoWeOPFrw*4pb5VY1$r2-9us+v!qa z%J@nT>sru1eJpTUIRrgDx@RC=pf5-B18Mqb`O$unJ02^J76@?Z(UGLN@H|3=w- zPu$l+8~Sxf=lkVuOD!YW<~0Phf%>+kuchB_FLu9b>ApnyqH75@1p3fsua89nI=6No z^6T2t{FKIz))VX`*hA1ja4?cGPeqRq949!LCTm;E5Ir5ybFL4*oO51yK2jc29)Cyc z5OX!KzOfiVSAuMU+(^n?8p|Unh@{-7V})K=6sU7LkgZ|^b$wAsg4hr*-|$p>#fk|^ z3C0pkAecljm0$+JY=U_N3keptp??JDl#AV$+uGm!&>vf^%{{G!!FywHtfz;5o3|FO zZv`$@{-y4{E!i)9{IoRxUn&f)4_XS_(>_7Dt<0G%mfuoX7brhycMoDq36>MA)MFvG z%7Zlo>mto-RX?vUm;3R?Nb~ruj-;-Q{juk-i@p4@Ed<*Mb_MjP(_@auUQk|aZ=~gQ zwVsgB;Lm5M$#`(64}h1RUfw{|e4akU1Be1E3!O zzYFj|;Jd&diZs)K{{lI0qr?#C(@66I@EPR#JW}ofryDq1!MPdfzs)!d%|ob#t83RE zu@#UE?CNsg1Rjf6XlsoDt^sEO@Ft}B8!+@Y9|l&yZvf8+{u#>31^zfVC`Ek^_^ZJ8 z0q;UAv^C3sbATTL&O(~eDE$fO{0Q(sVARVRhuC$XpFxSx$LawI&ms00=pyiK$kerS zf$xX3OyCGo{sg%)K+k|oXkwvuhNinh4kXZXJwM&=mz(KnK5^k`;?^vyT$omcRUvM+ z)>-RCH|v+yzl&R~XRT*NcWbNloVX3=i09%Q@ejKXx(|tux@B&e=o>OarnoEgEs_~e!jOumV>Cg3IFO)|3(g>X8BLtr^Li8KJ>`PE+Zv{rIeE@hm@G8d6 zcY*r@yHv_%?4B2P576&`{tECMjKR@_SIOZtJ|Z~3Us#K*$7rPT`TZ8o?+@er{-{-F z{X%3}o2*SVGB#UVXk^sWT#@TeaK9{igjC27d7+0x4~t$MDvPA>HwG!v!1f^Gg)kM<@;B}(;9JSt}`D8Wr)UZ7M4B-3l2K8^E`82g1z>vu%{KseTE?xZN zE&po;(Orb1`;#Tcik@QFdvFy&2}>357Zp{g3W@Hk9uT%FSLLE0yghtce3)7$K`k@* znrnK!fD)$sC;4IO%5Td56o!0T#YH&$qww?M145auu&6^{YaQQ%_uxIacA&()LVqco zASQ`yd>#CNs1v^sw_}$55Y4~)X`OMDu8)ahbQOu?blolfgRcJKFLZre{FSZ&;v8L{ z5O0aM#X#|Qx(11V(Dg~Fq!NSK%T|0!+O(P&B3&60|3zlV4DnxOR7S-x8JBVKX_+ZA z#c+9(yh(gU-YjnxBjhde7ICk>Xt`W27Z1=%yGo3aE9hzB3v#7gDaO)z zyIOov*3fgsI6gl?JV@*B)ndG?rKgOCx>p&m0rwI=N1KnOEhaOm3hj zkcZ_)xlv4#&&p@TKg(aqUx~?btK2FckN$wT5% z`A7LjF&0;i6cS(n@Cn;Ynhu zzb=vnucjKmgEdtX`xP{arc&aEnWj0ISJW?Iy|k6n$fuC6e0JhdrX8wiBLjBDb7*VYAeNUSGz@y`mOp8@gd_m<2f;y>T{3y z6xHd77?QCl<1sNp@Kf3rdT?t7{w>ph`9=VkS4=C43RS79sI=9pjx4rGZBskcZnaMx zP>0nqbwZt@@T_{r5Qc3;j4no&kz@2U@{K-5KVyI&CgTSi!;F!}C}WH<-Y7Gs7}Jee z#$028FUMG9EHRcDD~uYW)>vz7Fg6?Y#!h38(O?`jju^*{lg4S|oN=B*lV)wsZ^q27 zX11AY=9vX%p;=@OG>4eO&0@1utv1J+6U<4*0CTE2!<=N!Hs_fO&Bf+YbGf%Y&Sw=4S6V=dRu+1{?;IKf;H3{;kT1jVvV-O zSrdJ}HQAbG&9vrN^R05L!m4c5E}mY=QFapeVPFTNW`_2?0sb^Ne*uOMo9qGlhrstR zZmMhcXOsUI_#=od2fqtAkAnl>IRRfg@j>K$0XPR~Lg2%PO~9+F=?B^;2eID;MlP`w z^i81Qp(HG%&jddP&c6cx6Zjjo7ZWfz0`+a$-|WF9e+>L7#I8duBscyWp3(lu_`J3Q zyz<2ZNb@!@dx$KLKm6!hb>D{|@>$z(v6Kg8v1S{xnMeDI{pm ztN#NGYc!pK{_r=GkSu==&OTt+N`4PCJO-uKA0COGhuibO4}lNgMF~$Kn(vr{SOM=N z?O`ae+d$lc(x)M%_FMY{IPk%1d=v5w#LffGMViOJkAed~LkVv^@g(^0mXxmme;s^y zE*Yr1(GzLLBler1(TgPdk1PZSd6o7ytbl~SBky_O`w;s}#KKe2^NoB2I0Jn4420g8 z*?M7&(DNSMwFC17W{NP+DC#Nual6uf%C5G5WY^d~wpZCd4MjpRPexbD*_|$y!tV$a zN%?*o^Yv&UAN%}WN$#Z=&F#y2VSAFM*mkUTr;}fn#gUq|&46LLKSx9t|97wVCatOt;4xEqBeZU~kNV<{$CehH-VE#rbr$V7wBpdP+S- z0{9J}G`1RBg`(ME4oyan+jYXEHjjuNp%}IN?HLsrkJIWrD6<=d+;@vRRB9Hjz;bYv zbg0~(Bq1N`K_U7O&3&HkDj2C5ty(PCOCOA9{A$1k#bkrhlysM`Yf$#ZHE8HW4H_=! zQ^E3o>&50#SRlre1{bsGm9`tN$a~W@A*o5{l#@^EgFd(>itJ7;NspW2PU$mx^pp^c z?lkE+*d5vN(zB@ra_JgHFs2jb=i-dYj+ftE*fgv1RZ1|n6H4fep3~5&J*S~ldrm_q z_ndV39o=&pI=$yKv@ri)spQ|bIRAH))b#nkqvWy$#&~!3k0xb?)sK>^*V}0IdO+l< zgX);*qmI)`_T$DDV~e;4>)AnA&wdi?*}+)P4ksybW2^mrd#U{c`w4rQU1hJZe`x>2 zuC;$=*FlG%rIvV>`sfwZ=xS-~x584d6>bEbc|B4{E0S)W#ur=TwN<8GeN0NOK3bD{ zC8M-$n)vIJ$;tJ}wbP|5&Im{yOM0xeZ$y-Kz=&vHxqiPM-2wf&{Wb1Ib?$M2)%-cW zeRUX@oJ+5?4(%=9pAE++XTz(l!KG~3aE)!*_VPQ+mTlL7ZMh^li{A+MdEI79lPiQ9 zL1(|6E3t;WQMBgSZ&Kjd`&!R_lLJrZ*G`uLTCEq-HHDzedy9JQwBhkv+9Bija%R%e z9Wn~rSF%5r#&pP7YJb_|z30I6XNfDhU;m`d8t7-m33yf zO|Z{AU>-J)nJ3Is1ZT~6EMeJJ#OgwjWgfP2te#fB)rX*;HNYBd4Kw#yBPrJjYm_y{ z8tOJy zSz(W*bj1V{>`C@idxm+!o=q^%UT80-_FZnCvX@h`*Ao70`32kANe zqQBDZ68Pox^6ldEcKW9NvJ0I4&LD!J_CRL@>*AC+qn&ZAi!+fmb0*tooN3NX#La0r zu48(LGv6t9Dx7kAp;PHpIn~Z;r;cE~v&q?(+9y&|wxds^^n#1F`^8$iL)zBa;p}$y zIR~7>&M|ASbHX|0oF#b26?U#`yAkWW8*#h1S=M+r$L(o#aeF$GsmD&S_PF_OAM1$Q z4>d~Z1?~VhpHk+Nt~;DCX}8xMY#nrmSsSQTj#(q!k)%Pfw!Aya9YZi4V{@6+;Fb|g z@oYBIoo*emXQKo^cs=;=#A|8-|2&s&z?Vj!PbHvd6Ixbo5TAv zX7sssCdRS}FG20)<(+Ic4;ZKX85l<<{L0Knf#Kh1pau0=eEKvk^Y_q4pUa0kgUW{H zsEgA5S)b`Qm-%Kk(!*;GevbyNwYuVo675M)m|Q`&&%?@~_1$Xw8ENQ)nN#nx#_YHa6Td!vlXrkCecS7| zK7S+Sc~vF9lJ>pCSJErb$=2t34fgJ9ceQ&YN_!>AVSA6o)Agc&qzux4tgfD>XWb}l zAA{}VuziAT|8tQE+xLO(KMGw7GrpJcJ#i=6q3>ntW71tAON_%m5G=kfG-Ce-wsiS* zp^a{@Zr5FVWnD+@m5ufqmefYytuNo+6EHrOba3siL@RCei5MkIuOq$u`TEN4O8z|5 zaneqEEvfee4EN;qyUewq-v<0o7C3b`*#`YOunjI(FAM(@o=sO7@hYz)&1~#I?T%}b z@XqbNPIOb~9eh`n-pcS}mnH9Nd_y*<*X3a4z`IgLrD)5$PL`{uuS2aBY0c}pdZKc~ zyfhnk_LIleX_uU29p$^Sh9ZHyTQjAuyxZ>G zTw6;N(3L~FjTgKxn%|66yo>A0_a$DrP9Unc=MVU?i@pPD@A|ea_sM+GM+~I2Q%HJ| z_nxeEEwueROB?Rte2Uw0qMQ8^Thr$9JwZCdj(w84s~bvl6P}j^g3n#7BKf}m<&B`b z5zyQS_>SHPx|=li`_j1QlXq6o@4ob0oO8oyZ{Q>$K5Mat=>AlTi(CFm?%bW%0{sKE zP&Z^9EHQwt-qd?$Qu*e{Jrms6YxAvW@L)+}YUr_ja zX$9Vej1uKn{BEGLZI+wVSls~n`fp$KS0yD%)1KC^KaGumz6SiUuXl}2?4-!?zK|K% zQL*?s*ZKMxcWu32UN7UWZTenMmLu~AM^)2r{F z(RMw9UKYHjzS&&>lMnzvowqYxzCDty=R=x&q9kyxxd;=XNhoJ-ORn zgR{Ha{jU|gYx6RoSPqjTFfR=Pq6VxO%mC7Z(m`>4)b@KJ`b!kBl)Q9CFAElY0W<#gxa;KW77OKT+DWzX7 z Gam0F|Lsf}t2wZty9H$@kcCA>Yy-fm;PlNftdmEJw9_q-kj2RnoHPDi~1SnvAv z_WNQ#vBdu#=pDTZJ9@qSyV$jCV9%nrD;K*+75200y|zu*Pb>>Wa(AQpG-7i>W3R2k zo=%0GqsD)N#vWPy4+h>2QS5+Jde1TTkLsPTdJm=EearuhBBYrBy%Mo{pDT7kduQ>2fTLhHT4_x$qjXl2RJkZ$FsrM)w_#X-N55(&Io!F6S=)Kq2 zmw}y~{z)VLF2Ewud}h8?H(w$$=J3+R-*SGh0b2r3muzau}H6e-;l%hf0jTBB8> z;&D9X9A(j~l3VAE@g;eQ;`a=@Gf-Ts@5}q);O3~xdSb=Ub07WKAWG#C)IQFj+GGmL zW;t}^(s>SBM0_?|O8;MpV-0aVF@K0fv@TAJ#LtN>aKnb2fQ>4~3I`_;W5p<0Cs1aK z5gRA4Q^yIE`eImy6ZmTE1Y)%~7iCa?ADP5TzK^^kTrZz(zYC5PgcHo6?>gHK7s|%` zl)FA+djBZF(l9gNth;EO@qbG9o@YGxX}AjE9kTRMvQL&;v$ik&r!}1qBz_2v;pkB! z)rz-8(#50#zJou3l^BXwMN&%V+u{@;a%?f|=ZRfWso$QlsBhY^HUS+?5jQWLBR&M# zl=h5tDpm&`py_=miM@Q~srj4N6 z33n;iDZPQ%Rl6d60yR&!3(L8Hd(Q8=%BFr%7G1SMM;P~LBUNj0l5tNlGTNO@M-Hy5 z3=7qlEK@pJrGa#&EYn#0((&tynQmCC!_>%mK&*+v5=?9=M7#9l8sKsu_7FRP_GB?u znknuVRkP^2rhb%tONpvqI>Y)XzninN``T|j@J&1=5ve2ay>59vN6pdhit$$c?PvKt zDG70Aw|4~rE78OKgQB71AsiKakPuNH_6)i+7(N=U2okcg% zOY||Q`TkYST(J>Rs*ZC)r@|Ptu(8()z0c@brIFA?pC_h*BWRIiAM8WjrzGja}Dl2 z70<4zHQX9yjkP9NS=J1#qh?N{qt-53uV{_Vs*8>{69)Kc?wW1QxAIK*msl&T)z&&| zleNv-W$m$wtV7lj>zIx`tJt<}sTn)oPPCKl8g^~F-f7YfPb|r6x^FJ9Hz3#R9>LC- z9rpAdbm(7m?CHm^RA~Cq=-YDicFm4+dJQVH%Glq4*cp#wXFt8t64!m_Lg1r74jwCb zF+5T4d1&@;4B#5Q)|B4K5PLA1KKQe+oZzGPGt?_K@mt~BUvT9XpkCDpVbi^J0gbp` zgX(U$@4#KqSFbcC5C(?OM(P~}tOhZB~pkTEo%`m8#lQ ztg+g?U+-u`&Lw(nS1c#apm$Zt;G%&^nDaguf8MAGTs+coG0LsRX3&_R^VfA#Dn;E4 z9s{5FE$PL%XVYWqF3=1&C=cCWEjxV4#nx~02G20MQ){5Q^KS4$7Vlxb>u`YwR+qkl zI(m-S?|-ttMPDWowa9kb0j``4&~!j}P;e=ZL%~)|>kfy1eg^NcZpmLDZ`QQMR`+(I z7bm#EZn;xmV%fIo9ep9d4`^}P?1DY_`^|@c9&W!7 z%7c6kv&9d0-(c?t;U>_sA%C8j<*!HkV$}AFP3b0a=%cle_BtgDrghH7~-NYbpMnm^eoei?P9gsLGVET*4`gX zQah;Wc%_tHjD}J{p;7}{u^y{2tyz=C$V2o;vJ2~aO_`Qv8=Vlu0 z!Af&@<2~f9zM&V#4g9^0v|gxqGyYdU@mm`tOTWYATB0^PVQH2&B0jv4$3RD4@sZ~v zin9s(Q}552CXRU{W^DpNZ$ETH(oXoPpTh7v<*L2z+K>x2j`a*2p=pnlj^`4W4(yEz zXMSgROVy}XZ3GnD3$<#jwL{sirM!ZNWk-d)#l9k}evWPWgs_~1IjH&$@%7+{v&8jb zpfK}_;&jMDks&;LHXbldi&&Bwq5ySb**~jf?+4dE4Drt49)+ilJvQkh!-rci1Xfu> z_Lb_!9p3W^b6@nZ^$X>zydm9#1<8@_fB?_OZ7Rig_$}8=LPZ3sGi1Tgr!41lT?j$jQ+34 z`Uj;yN7=usWz>?x4^7sa;e%5JEOr^$?lKcMc*o~u0zgA2N31Z#s2}>IGx* z_Wmf$Jjjz)S<@1*&d`AM#7_-5ABnZoW`a>j@lHBUT17D{D=Z>c!9{dR#SyFa1t09l zkTpE%zFah&Ce-~rOfK`i_xR-;Ym=7!1fK%K;w3ead8}DHQEp}Yy`@-qMQUPqn!eo8 zJK47GNa1(^NmY|~Ux#PXV?r8TW~HQXe# zfcf&)h_6_sMS3|&Su+9OzUapq69vO>?K4Rm*UsxH$h*Q1_WTjCduZ;MIlX4rk6Vr* z&FezVTy5e5?dx_T%?guG^DGFrt3sX6p_3l30}|Xv^@UQ!AMXDmWR_e)<2 z9PCK7!WFAl@#M~$bpC|Byw_jq@I8kj0bLjAQ`dHm-h9-jcV3B}`K+?v)bHQuIwuMs zp6h#49R_8y``Z_YD~1`2!s)_Q%ogf3PoZ7W+C$a`&p+#_gugTbCpaTU%TK?%vL#(F zE7zYWwc3Evf7B>7)bXhbcLL8Q-@uwig1fW;DUalgW^?My8i6N^x2IyREbU2aF-x8{Ic7$P`)D14aA>4G7&*9YrOA$>5w zY{SF_;eoj4ozE4lFM@id{G#Ae#d`st2rmFO6|Qoj8(0+ z4r~!;5wUc=vD6L%e?L1^YEM7~uk^*4qUcWLXGL`}Yxj?-`DV{ zk=G1vRa38xu6sE&^S3+RJ8oEVdfm zlvbZ?o!si$2iNv7+XNs_1%-&&CINAS;M#m^1woDp?LkLwUhUb5Oi|JzQtGLhi3Dir%^#KeDPgWnP5y$(1S{dz+nl z%*E{4kCR<`jd-YKt|-Hhr(Xz4o|LaeXWHz_lErQxSCp>Xh&Gu~%|{nC_$pr@l=x0@ zCJ)q?jgJ^fm0>i+*yOR#5@P1FPcX544I8?)yeo=bCmeR$YKlbxwA~^zPk0MpPvp-pSa|;Gp95Bd1($`uWSY=rgnAXggZesZrrn zbqK5veB#hKlja)Bm_&iivLKnv>8h3Cqi%tyl+q8)bmrNsmgIXE!1T{vRhMW`^xQ9% zGG2E-ugquV`x3?1^|<9wi`j(pp0?U-(4oDb0KFE+{1q75HFD}tOQR8$3>i)S)M^tg zgTkpxl0;#`WmFZfxB{K~MaQ{nIct8tJ>Je)j|ugxWfxi(!mNw3qG?<8vg-?@2_iyt zHWu|7E_EXQCIjRvw^LtdnT|aTLN>7NO3vf<*+xsIEd*CFmKC5qvpNRVghoGVmRr{S zBE~qg8IF`IzdgUP8kbd;ck;IG!MrJlXZJ0k35Qo_hi;|8B+~Nf{H9T_ZlwZ%FExg1 z^>X&RlAe-Tt&8>~OKazZD%mTr1G+}IbDFXP>Xv!sdVF^1bnN-EceWjWY$(qztq)7l zrW^I5xA}6dBaUcWU^1)Xfx9c@V%G~0`-hsbI9U+C3939YJ)h9@dvQ2>e$XMQXT*Ab zH#VtQ7?UBm6sC?SRhv!8q!tqZ1d2` z)-rXb8rM~#^Nt|$Rl~E!J9Uy1uai<0>vq@7EOWJHrLv~!Nn6X3C7aVbSF`r^wH5w; zx`>L`yA5LavkPM6#ZZ#S(+kOSs)Y@b`xWsI;X8~+&l_0%@EZ&odF-?XiO#Fs@arAZ zu+Xz!9Ly=v2i$LFF67REn@3PB$A8rhOBi#RiUgSxp?)|oQ_L)IJmSGPwlLLaO}XC`g5E>NtdB3I3&U|~+I2K^)3qF4>C zSdCSlTKUuocY&Hrff{{*noYsN?PNvF)CqK!r>suFMzmPBr&xD}@-f$KbTS7XJs$R% zEz>d^tLK{KjivET+t}gU;-L%AjiupDTi@Z_?4e7^R7Kj<3B%#s>Y+>XR7J?t31NX+ z`qYW;;oS70OU_h9($tA>ftppp0>orR^wi0>0yV8fPh_0eWt>++vEwEV&9~p?OTFfg zQUz-@DC;p|?c!0cSy8Ut%*!t9%PvfwuY-qkbBDKD1!^1x3k8!E?Nb$@QxzpsCrkxu zA4w#4CTZ6WZ+QyT>TL)d(zUM7}oQ6Vkcs>N-6cZ}+ zp_{6+)5ks@YW0G?^!LM^jybE0@OxkyA0^~T9;KO1(fxy1K}Id*xu^cdn=uVh-~yC|-A zR~+NL#Tk;NcD)?Sv?<>6yJ9vu{@|5)T)N4!!M&%H7Jdi_KmL6pR@!B&|IwjZJaS?z zqaih)yfDgr1+AgBPJIH*F!u3iuqL*PN8GEbkMEFSPi`LdGLiDg>CpR?*!{CRL1s7W z$~KLXXPD=sxDVly=C3&9Vb!Z|ORD_jE5mnJSXTy*VW;FgBgKx5nG6>RStcxxJY0hO zjO`;EBMes@_3fu5L@7sCu~!ycB5#uXWI2gsyAqFjj#2ouL!+>c<(ZT)BYLB@R|-o? z{FGk6_4?SCPH&;RB)JjaE9Cla@9#(nL3{FC()_fcqXMG}R}^U^ddWMxB3zQDK%6VK zGzzC&KRSeGV3l$o15lEN6P{~A-)BPvy!gCnoziF|CVk>_&dqB1#I2nlcIpK+u#P;%ChYu~~CL<||AYxpkH4P&Ug7vZ;4suRhuHN?)xl#bH2fDtb)bB~WJTKc+crj;D{tn?*HOHi{Or*1?en~ocD7o&; znsH?UNnXe0qnNi}J<*bbDoEBbl80B&MHPy0Xvf2(ydBkpExSLl3`r{QO^l{S(nW*d zjWh?Ox2N>z{DzwY)H`r`!1E^5>)F_~yKH{sdC_?z)EifmpD@DE8(WZnHzLrJ^NEw0 zJsB>^9zJ-1f1_Z`eH-G}HRniFD-h>IcI{;Da+LZZ)s5AvaqGFbM~7pS8`vo)K>5z# zCn7)}F6j|SV7k%J{oH-SGvfCg`;fJJS?V=xr}1THsP#hOxte^q!q}zzhhJ6BvEB~H zAFJovm#4R}Hy2--?qt72y`k>)?%tY{jWCD6wx7Bbt|p8;riK746F;u^fSf|Y`7xoh zYAUxD`pK1T?RB`j5O+s5z8yMwB3y(j@yjKvDfyO_SZXK;dj;Uc)!SZ|BZ3G z?Tww1$NR_o=c-qBU#;#Czl@E=?zQfP?moMt<}0ErqC3AW0SSZFc%5(Dvje7A$P7#Uw_ha>6}q7pCb1Ak4` z8&*cEzDNI>V&*q2@Y&MpxAL zQoBz4$yRdGTEFLpq8-Iw>3;hJ#>C=)-*Y9G07jIG7&svYhGOTEvGoad`H=^nN{cgL)n29}MY|*-MFlO2nqN*37 zj^gz&>6q7D+R?}Ns+2oL=;?{qy=C?`W#Elyqg3o%&FGuwShzLY9a&f26Z3sz2Sr!W z`{74!UDYf4R!zMvAvMgcRES(3%V4|Z#NWPIR){f&UWxX0-^;w_-seu29}iqqk~NwY zqt{}MR!@2DrEq-gytimw_DGcz$Ci`lYUk555%W(dFsiH7$)uwr?`UT1UvJoW$hKzm zV%&^qlIFs!pD3_?4=;a`q_n2c>(DtJw_5fKSeu?3=U=UalXqQCG2wKvkfM5TGa?A8 zM=}e)r3p{6q!#U*x+qmWpO~?toE@_K;{tkkqE}Ee6|Kf{$`?ut#vp@EY)77RGSTT8 ze{&|-=T>K7XTujSYUGhIe@<-23Tf8z7_^k;l$w8*`7BUJcSUYcOZp;b=E<1OuouVD zAm^d^3LkdswBgd$NZ%^8^h0BiIoU-=E%|Hi_Xlwk z8>c;3jQ2Baj^~~uZ|5z&M29-M@+|rKb7v}UMLA#&)`=HegPf|zWA#G&gb*R!$!tr@ zWKyI3NCl!Q=GKYmmXd)ISG641-hL!}k+26J4(-ZUtx{{l4JqbK_UX2T_lQO16MLPJ zI?2}!v_)}-Qd6MFQ;VhXc=5B%@y_@R?dBGu2I{g+^7&feoLj$_*DRL&y6z@_h*_uj ztq0m6;asSyn?&r|4l4oz_)`wzw(%$GDn<}Ksz7;|KvDgyz>Sm9+q zy8;y-A$}uM`-HH}QsjJ18rq_@xYF>E`o`Hyk+T-g@F?x$^TF$UOO^aeu?n2!P}^{i zn>epHNB{km;qgM&r36LRsRT_{O^>&Nk?FIZQv+H@5un;F`;>%o_C zg^js(u5-L#yA~(dgSP9>2d&qz2NiP^t5CKK1`K1?G#X(uo#&RSa0*V zgE4ZS;isDB7n##?4}#o*xyo52j{QP)B9-V?1C({b=wYOnS)4Qx^a~LUs8z?RG8Nf# z!>50k;1$D945kcHGVR%G!?vc)jVsFp zXVtEZ?KOPEgr>3eET__5KeCJ%8&ei(&zkNiy+U>Lv6eQL#LoH=y0tr#=3yx*GA*JoKkKwrc_3^bVFp zEK)5AguXB|sm%{ko9u7P2nCHEX?&w}?fgf%`=YFM87r2u!s%llFCvOa8h{T7F;3M^dIx-+x*2qhQ;wlE22S+bV#XITJI#k~d`Hj9Vu(WVVhACA5x{ntcUQjKi6+ z0aeB|%^-lc}M;$sd^)nM4wV4l!H<|>m2K{!CvTlVY~6_G-6eF0jpsckME-0FrQU-mAB_?h@E}ME3>H)* z4l%wJwDk9M30MyB4ERBg2Z;-l9&e3nj$$raZ;~sM%LjA~hzVdI=YeU1@{B*lIV3m~ zuea0J*Z&Oc4KN6ZCl`klgJG74L6rV%91lW}h6j54NMWK zPgh%$qR{B8IGZEWSg_h;qg)!cI&LCfTD3ZAB3W9tI&dLh>ODDiAzPX|Il84xJHNe! zPB^_ly!#Q4e;oZsED?V%`j#>d|G>m^Mnd+oz7s#8q4QCf5E!yUK3J?jqSWmwR;b>BhT-?S8xPx}XLUbr1zb89PiOeK&>Tqq&78r-iA2sz{VZ zn*TgcsllSovFLC7s$W2y^sb+%fBjddS?dxv;#X@^EBsfdRBQED=LO(fRGVuUx19a8 z0WMrVPC*GF4V{|O+kvoe0@&$^f?z=cxdV23qhMITK#%~m9%)Dx6p#oYu}2!51r|iB zuM2h!mnscN(`W9LhGxM583Q!*>wAm?JTO2N`npgo7$9YUQm=6k5IPajlrl9<-!$-J zGYMv@hQ2P$HFxSlF9@0i4`dB+>H&eVz(^yd;sdz!*FRlDr*iawpjfa#+JNgG5CjV{ z$Qy9o3xa9*1Of-Z_v{7&p%UTuWdL@89%vvT0DLbUekZ%u}=Vq?4^Tlzy`JJuR~okq*CZ7`)h#j^8qdbJdi<(0I6O&s0IWO zGhnob4yFMg#O=}fSE$2BKLTI+J^vC^j1PQ;DW^U`N6MIQrYz9{WT!= zjQ~VJkJunhfK_iL)HQ6XT#q-nDM6}>{(QhAI;cdSAF6>6X~sHE;=bA0D9R{|F9Z0JQgAMC!!hzKEyTFw` zN<&G5rPlO%Bko%Pyn0T-8el-W0I%Lts0L&ZKj69N6sCa)M-*t>`x$n|OeQ!0dh zL=SqvSMY1ZRJ$H(gfjlf2F<(&OzhwMycP7jD*9o|;SrBtqJDqzFYvXlcr+zMnSP)g46-#Y>(z_V8Z;GG9PD8TrBEAK|h$V^jVhD zly}}*V3To`hSq$xi*c5Q+I-fnq37N?FbawmnwFX9m7DE*!PD~b3c;;Pgr3K> ze(7VnWpQKrZEn}1+Qv%Ra9pm1KQ}TU)2n5}p)IVzA?8Mq>;S!X_M=2)9y1`S7R9(Ipe+r+)q_N3nMSM;JMwz{WqhvtI~9`OhQ80n zX_R6#=*O1i^Mi!a7-yckV-n3f%MvyK|2OZD;@CV9}Kf%G{MZM5*W zDwxNmbhA<#rIGdW@5a4@a-w62AthSpR_;WZzOZH}ij&E|ne>V+P9-pA%&KM6 zXG>e=0Zau;^{i8WJnK)MFx;RO+ejNPtr>j-j z_o3Fsk07}{Fyx@<)`@exCGTfVv>4Le1CjI@JrQ%t9^I&Ad6NT-gn=g9?&3T z325qbs(*aTL#g(LXs{VjhXRG_7LecR3$h1wY$pQpB|#rLy|)$x|uWzFVPy4?_J zjCb!3ok?Tc1pkV9=V&##Y52mJWy?jxeZOLzhBXuIwy#W;R8`V3R_X2M<#hR%n**qR z?@P?4O|{6_ek98kOFnUIzV@V`?1O3f5bK7ihL3Yj^iV^yFYNu@gYV#{h39yy@eE6M z<_ya_>XegiTJb@L6gyvYij${px5y2M){O6(0fo;ksUf@lqjwr<^xoI5-*d9!Dk-@W z0JG^zF19HeWM+JMuH$p7oA<&cTzWq+@1>B#)%d>j#Z>!> z1pgzCK6+_lv)BF8gx4{ji7`g^bcJxKT@Tvk4r$Aj<+Rfd->FWl-RO-MKqWLKYP~cb zJN%I&E!8OzXn0Unaj)xs0i3lA#85}8v|IntWR{h`ZjQuf)h;$x{H{BO0V4};Nr}ga z#tHwxZO{G(F{Br+hf|1cA7e;U9=?M9MDVQ2_MyC7VBIsOe`VIG$syPs$Wyt#1g@td zk;fe+Pasx`M5Y{yQr`D@&LH?0AmoDmw>Z?yGKU#HhjCl#@q5>gnggu|q^^t$p3%3V zl9hv2@G0Q+M%Mz1@i%T3=-gz$q@e4<5j#;OIIztju;g!xa zU(dG&zUA>+s#iLSQgR9we+gkGBu+w3Fis|#COt4ya4Luf(pEufDAGMBMP1^5z6{{& zBYkNV1VMdlhlFAHv=6=k$pF~}1%l{8)Q0qiX~BMAu!sG_x2d?vyy>|4+=J1B>|YT; z1h()=0J<8=7hD_W1-*r^1*HYH1*Zk-0S7?Tg3*H5LeK*7z-3SM2Y!=bGo@#(r^X+u zrzXJ5-z(tR|2ZI}2cm~`lLM~mBw%vtNj{H-wY-D0N~f?XveA`FEXn8}MEtz1s`6tt zxJX3%jQ>mki+@d4D59XOy%XtOkEpDDJeu|{Bon~oq-@Zzb1zy6vV)+~SGlCC+E0B8s{H!;C%F-!%TXN@v%J+QZv)YmfW8S>HQ#`o# z`d-pZ~YYQ%s$RKl+7xq~Kwi74tON0=0#B>pv3K&XJM81C*F?jvQ zx>}1sbd)cUw3uCZ>hZTO8*BHmuXn&i$8k_r8jsWXh1)nNv42P{LT+V(2)PT@eqGVq zle01fJtvQB{eEKPrcSn{3b*?nBEx`fZGXk9G)F=QyHQ#P6c^Y;(Od^vSqZ;szM8v|#6!S;L9zLSqO0&Z z^qT>|bo%{|^D*qMPVPyUletq*)~B^Q^E~6tmO%DU*k+M9mOeTrYEykI$z0>&U#GmT3 zNf_Az;R}q=^Lp1#V|Nt93gndiqU`CD4PAz(27{x9@(}fC?aM7~5r!l^d5e#q0%I+7 z#5E0$4=>~Tj{apG)x*1q?`5ryv+EU`fS(|$u0A3E+}Gl)lbXOHtJ9JCCxE!cMO?eP zgA4)FZM6?4)33B=9Op;Ew@ndh2*_EbzA$;}LWz~q75#*60vn$n3KU1)JJfD}5clGh z#(uTY{``I~C5FgwpIG9X@nGGfKuOx@IT~R3I6L$EyF1)E|MYjw%&vXK=Sd(Tfl@?# z!|!Q@3fl`phSnL!E}}(&TNjIXNh*wZYbSyUEQz6qxIbrQY(D7ePNvCfmd3xc4X~cj z5B~gYj50k=B?>~)FQ3~C_iyu9KVWA(c5lO+QYh37Y}3SRAW|7 zGb$S=W;*HEqn(4Q>8h?N_kD0WbwVw&L_sQbw!+50DS#KD#^59~{=~sqv|3oBRtJza z>_(z$H2E1(g&@64RjP~Be6iF>V*3perU$c^l=}X%thHL;Hev8!C+CE`VrBOP`xCq( zlx{GJ{TBfBEbVOgoa@leYm$j^ro*~K&c#Wv^WAm)<^2`0g~^M9FGIli!>6<}6Y{F< zebPYHaL9^Jp~GLl4Uv--_zR)@=m8UaZ%61cynkK)CNDo(!`-v}CN=8rpT??d07)CJ@@Bsf{G`I42pHyM)3F%H?Bhy!JTaN3hlAU7n72!6jt9@MU z^2cx3KWBeX5y{-w&vqP>(1%LBnY*tDyB)0Wfs1VLYmEThDs`i$emf<@K={CCHnQv4 zr6e4Lvwfdmi{-`h7E_1jS=%1@$AaU3zN}Te zz%a;vH*cUTfaGRsGY(Iv4xc;KJpGfBeY*@cHRxk93V%l0iI2z@DpO=fscj0MvSII~ z$f$@G^ix58K3rFKCAAwz%v^GL@m_FT?po*31>OL{wVS7AL+g(6p}O0TNPkw?X2|_EkzxhmQxPNEx+)GD$S3Zni2TP;D8ANYzl5 z96fI^6mxfqd4*s=dI8L*t)YEiUt$S=tAnlMt<$ifD+}oDwO4yWcW~IEPf&tLaiV#9 zCSVj_iY7~C6flXy-wD0nFecH_Jr<;+4okiVdyu%vvCDO~q}WiJd){tght_@RJ9(@3 z?W=J2w_5-oFp6|m4Qk}mEKAZ5{}gZbr6whBEYblTf#?uLgVuqs1KBu4cCV_-WhdbY zmD?@-c`oxZBwzKLwA=4CSw@u{9~HeO%=`zXvU{#(D#L=d)Z@i2944K@!z6VyDW_$N zmRYljUE`kn*>uFXW?w_>&cY!u`N&?>{>j)7V7;oNGwpDPZE zD;}>%nlYc`H**IDsp@~Vd$Vo?6oY5RGy4bZx`X5op#u^8gY0(6^E& zi8%bNc-x(*1lnp5!ie8==Ctlh^w*QYa$zp*#MQ|~+UVX2vu8||+?odj=)z|~roD}RgUpme(P$|qmtU0)-p5MlnTK+Gr4U03isVd|3%S=ZN zH4ai2RipO)X9rY0hH;D(GURRNW5vD8EsE;N)fCIqx$2C^Mr_^p?QMz<@1V<0qd;lW zA&a}TAtIj2im_>BH8q>vd5zpAly94{!)8$0dX`K}POkg#f9@?Nq{cbA<}TuK@x{ck zBP0&tx$u2WnzXsIvlgmO7x2GEP;J~kar#Ly)TzvFdq$U7YEL-~S#^nmis(~rRa0NO zvcGvP_+|i3qOeXKZN_)_2X?`glB(+^EmQY%%4*JCyD+W4v6qIkb|$FmplU0qrJ@W! zvfiRQ<@zH$p%&TY*>u=TZ8}809<39^CN0TekZrpc1R9)E(?$1&chH(5H?gyDL z@F|{tXm?HHJaDhiz2z~mDnT#}88KI0D}Wv@L{YE+jDppYwpP?GQqXwzU0na!%jF?Q z`CaPmkcX|5!h`g}LVQ@?VFiAE7ED@~+biX9-6b*^Zm|5Zx|I+`dS%TYGlygt!Pt3y zMh3}!V$c^+Gl9_cZeO%)sevZ=KCm&kknI{4k$Pbb$$dRg#;)h(4^z=}FyBQmtgW*}PwHSVHUgcUw!#)9-31FxhO4yRRG@K+fD4y}{rm7-1l zGw<$u9zPL(7SvxD;c$thQ)h)^aj0$4+J6YV6{olwl}EYlxh`cU5}LPRvLJlSaKW4u zFBu@Qi*+F#9uJxlASTJ2#ka9Pb0 z_;})U3&Ef)1RWBQ@ZBZ@C_Yiv^Bf8uF!DT%e||55=M`)pn+JTlO7yZz`2EJYh=B~! ze7+s*eJDt6wY`vnCai9ja%62t3cZG|G%(FRp>M?@D#5eA% z>@*#=5H5^tMlE2}%M#Ffw&G zkEGVJzYHrSkIlYEs~Ta>y@<)R715hWl|NF9+;K2&Sex@c{%qj5IH}rA1(RZ%sc7pM z{oRQ8IP&!QU~jY1kcX4N+WlvZ;e5HTciLFfSvW=fP%!ptl3g#*{RY}A1lsAl$?f?( z{!+(+`KsL_A=(<91$09JuR%_CbVnhvyf<}%%8OVMO9Hy5PAeDXue_44_;9PA_ZXU2 zW>$^*-+I;L?&=1E%{Znj^WIGD@V4VeK#?AU4Mt%YXwx$1Pecdg&E6k zTthtV2FNh@zk{ISOWsi`MIm}HTO!`GsLx>Nb9=OfP!NY{ZA?(TI7HC}QQuvbNq@g3 ze$giv@(&39^rSs7!W|P2@8Pm|&m2D`aXx>1EBpeb=|$eLkDQ@vORTmlg1ySUBsR)Y zygAp)D&3Ip&XLj4rgX&g6RPNzIwoe*@mOS8f#jmxJLvlQ{0;)GHC5BH>{ofoUP2Oy zvWgtDbz7`s!dN?@UpPCKK|)$ZF6&vBsscR9d87J~PEG1AT#x z#=N)R@VuorgZoA6bH(w%nDIv_8B0}bW(lj`I`Lm4_{>%wW_kJ?<%}Zaabg` z-+_u`&;vwbN$ns5!3XM@Ry?ivhx?p0TB=`S!`v z0iB%&Y-LH>fhK#^kue?FiH6V~NF^n~XM~qT$}Dy@&8k>|H8-J)BfOa2TXsYG#dym1 zD9lUFP-4#5t$msc9cGhPo0*CzeI2>q(8T-R09i-#?ApUltC^D`k+yf8d+pluWD2m6 zV$gybqTBuXHs9t_>MR$^=0U@gMR`R*PSD#XAj2Y)EnAcZv&=vOOOub>-Z=u(sN|G) zroa*zI!PKy;od?QW>Y_Ho}{hLUQ`<#?rTWlSdC{!IrS({f~OPA_CZdQj8nDp20D(1Cv8>0|Ow1GKrVJj0sR`y~mfG z6>?=ZG9x5a`Xq*c+^Xv0MxKN*fP)2otD_yu!i zYzMfBi)Q(MZ9Vzqcde=VNv>5*=d1Y|j2ZQ}iVAj-&miGt;rpsC%sQsBvswFJ6E7_D zS2gVW%;|ONmMRbrIGL&3|3sK~f+_kcGuG~WriO@wqHTo4C^INd(cwc3K9khq^`q|6 zkPFA=JmI;Ghq+T;Nay!STmTDWQs~82WzhZJV!CiiUzEtoS@2q?SIbyDIntaHMq}BuOHLo6en8#{ zWn{G~S>rWduY(rD&8&#jmLeQQsQhSwr(QM(m5;xZw_m=pT@hdTUZo4_Ca=WA+|m{h zcztQ6V!fD&>?9l-p)>7brz7wu&whpgjs_J!%~6?9>j&E_thAU6Ui1~4z>+uT(!!Nt6dQ7azwc3 z7``?bAf1`|`Nlz3;cTLTncX717LyuoJ!BtQW4^Fj;oZCJAYaF7>PY*a4F)h1lKS?8l(Za-bQ=x<4vKW<-DdmAMfPE*wh&OC18Bkg7%-{o>+BBl>m-QGMW`ir9B7H`f%67SAxN3b zuly7#6#khhY^vC3OAw2UV812sT;wSR)nMjmRj`xn0Z1T3jT-*tNonF2@xycLM@7Ij zSZ@IbmcY*;li4fH=q~e7<(xL)1$SC3mo&d9A-Lwxj7uV?Ztn2!8*khNmUqU<6YFV~ zCMztZ&QI^Qprb({WkE*PxSAKMtdtd>eMyu{?P9EFQZhFBT(DTKACapxlPp%EqLJXg z&vH>Y;Pu}t^D7awvwNQO+H{pM%Ih79IJDS$ioo294^J^Q96=4nO=DKz8`uW8rS%&* zD&kIKH;mSSNiKL}JCg<3dX3Ofj4rMjkEz|>vWDv*$e(C#P}!DO zPhA1?>IwD*<-&rbS3<>@)B&X@OC3?WIkRe9?0&J#Q{u;6Qr=6w(D7Z{SHVdaAqG-U3&UG8!Q`UDL_OV$npX7av_= zaU#H?a54NlskvBFDMlbd-}^{S7)!vS^=?OvMe)p8lLkBFhN1HOmc_}I;8NB^;JnW5 zcJfY&7k=1~OgWIUClIYe6?RGG)Khaw4y?^6`2nS=9#U}D!IW?^CZ zo8!_VW@BUjKWTqC`LJbY=lDnCU}h%%tNnv#Vj||?;QR;w&rw+a4f&UE&X3Xlf&M`J zh5j4)@0S1c^5LJI^FPc60QmP=efazr&CL9P=YMznTNFQZ|6%n1mHy9O{tqJg z;rlPt|BER8w=@0s`Tfr^{p%|J`*?n6{<_ltHJ1M=e9Rvf|91Paf4Kas{dN4u{%`qz z?Ei`QkCMOX|4s8R-QQ#UW%Zw>|K$EPi+}n3&$xeC{_Wv^vi#e_f9?Oa`@hTotK@&; z|1si!xgS~n4k#}#qnL%Yvxy_4n6-hkiHM1jov{g{w27^mvpF#vCo320zcEb2Ow7!z z09Jl}M7X~{Alx%gy*&hHRqpS(?s*`{hG|H@4@Z$jf>*cpQupoufQO?gNT$nAeG1Mq zP5+)uZ{DhGc~p=*r&Pw3OHoQuY*W0nef2ZhG(9QJ=GRwmzZ@?!Xq1z(dmlf)m-YKI zFWwxlZny4(Y_IcHJwZcKI56zt>}XarC+xJx{R>FkvM-(kC14cvr3N z%Dg2L*Z~!hCcQ67x2Gw5q%K?XIimg7Ad#}Wyd{Em)=0+o=&%iu3J+R>ndzh#f&zQ`SKGcnZ%)xReTEFP^TG=j} zG3%UiQ&PLZh-=UREk8s!1+;!FG2M^Rmail=%&paH{b;qd(;WUgy!MYQ3{69oFOTA7C5K~T^ zcw;G*UM}*O;gm1H4a9N7qiQB=Xbd=qrGOHYJ3C^aR5Ipq(z!MO$!Pxfm2AR?$F+9?K zP1!~jbIIhH4ATmqlW58sQHOixFNhz33(#1(-e*e~!|QexL^`;MnDAz%&6< z3T^}(1nn1O<0~<7rY0Y47`zR&CKpVOTSj$!Nw_Ztl7_yN;pqGz-#J8IhWm|Aejwz@ zK%@Gz${c28vLqS}$^q(Plbj#nC|V?EE)Im!Mr`d#zZNo2BSuD?; z-;@*g$}_&t0;5yP>81fcszm@c`@|sp2H*G=n;rwjF(~Xa3dz(!cIW}ATC%InPl2%a zOEOAN(%erR-Tt>RgAB8P1oZOXbWr=vF%W=rN_7l!qfS>+xj*FDB8-9@g;Bf-*AhUA zvesK(oHm}cC&6(Zyg#F*b%^Orc=cDq4Ip~CSZSTBOdUokWbtnsd+CHquQ0+p^`(&Q zip5^okCTjeYq3)tqFj1#Sd@oDvh49X-GPsVhXM*-uK=GKS4$L6-h9_Gi0L@58-pk# z-WyY*LHRI3(%~PgLNq<)^xIji5s0%X;?tzdG~l^kzy1`4;DRvPh;@kljcYoI5F=`h zTEV!TTsyE)LS(FjBEmI#_fT`3avmw}}vc?RRZ9rGMaeaQmKMC;Gc z0%}Wlqt-*HE$zw6un60ztNsmEY!+%vM$fCT!#h*4$i0q26(>Ne9}qTe0X@$LpxbM? zIc=czd^-=o>K1b;)@mEoz-lbS{=0#OBK|%7MxW4=^croV1GFBiyb!s&>1P_lx1;={ z`UU+0${m2bB)_Z3)AB@4Y|!pP)uwE*zCE(l1 z@CNXHKF%NVcv!X{c|PZG4>qwk$IuG?jy@Ly`91!D{Wyk$u(DV8-+rM978*avPw_^c z%EOVih6^|kx)-I2_>z|@8HjPnXDK8v_m+HoK=OGWmhxL#3H}zP)?zP!FcN8}UFj?N zZa#vMKg;`3J_73yS08%f{|yhuzJ`5Rjw%GZ+=t$VgjPcHYj_v`qQ)}c@tg2J zJahG=x#xOaxR!@WRx*(6<&sT(*!XGcmXAB+R#@K#p+~mR-lHnqhW#${O*Bn~m0YRP z>!8*0{RibMk(`g3%kwl-y-&m*T|iIJ8dwsrXo=N^UQj*_f;^5xH~gRh8z70B;2#*V z4*Nkzuo`zGSN5`o*6>$+i7#P)Pv#f-NB)eXMIof-#J)+PXg-CM&-feOgBIU_6xO1z zWAGc^p@V!2Psa!k(k_hA23>vt8W=?1K-+hL-k`@}Kknt5K(Byy@#B2*sqG zIYQ|JaX(Nl{Xie_FIca4kR?X$W7_Z`e{5IFX4!#D>t>!rFj4-~L)3v$urmDY= zU*@NIFK-9MDT-C3*V8^opyB&fN=0dK{rUM*Wcn0UzY!s@G&Nkx2Y5PYGj�Tx`Lyb2zv!;)7j>1jCW*U)y4oomQw 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

%%=_v@VJ=>zdHo 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

E^jA2IIsFJHKx17 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

<8?>uYKA69s37bjr~J= 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

0C-;o(XGXMMP8n-+)%1SCIHh4; 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
mj9)dW+Mtne2x9@99Aq!v|L(@w0A7nQx! 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
(#($I7G2YOhd$Uh0 zAE<9iDfq@dgkj0~Dul&j6j0Gi$i}maOHhQQiDRIIQcQ`< zIg2YH_oh6=ri7$wg(NOY%MU)0*8urduZ$j5R4*ov44Ju1^ET<ln!M;@g5RJkTZh)Be(biRGRz{H5Em$U|mnQKWmYs6&8Mu3bVJb2@?Z9EW;L%;dGT zs7{uYNz{-6u8Kg@RfQ~}byY)dc}`Ls|UI)zZ1#q`Vv@^32n-k71G|81K?7mpiSqcKBFl?BD_o57~a zSi~-wp{C01#@_z#(l~7yjo3Le6m7Zr*h@3$C28*1F*BrpTyeMAN$5*EN8+wt0`Wp? zm48@oFRwW0?VKhazU?D1RImFuO7gi7BI<#s*vLT#$M5OVicfMp6h(vkBEghrJbaiK zQG-jL5u~<>_(<8K27_Z9#92HMnz2-(26sdfDFM0MqZA*-!r*0$v-roL0!l{?34V&E zwvnN~Oml5zgFa_nt!aW((eHktdk{HB55|99r99)55T=+IbUJ1^3upsIReKG6INK$C z3~9S0AwM4UI{U4jYa94mKKBj>9agKtffmTHl! z-JWdCotC+q!)MKsw&E4Jo9ofeV3)AjqO@1t-lY)~g(T-+a6O$?q z)b2+cftW*8u{-NBb*dV}qx17~(r860^ zINS_5=al9b)WV*<6Re)ls1xfwY5rkch37nywC$NGGf1H@r0C@zo1Kp&kgFA8dRzQP z(nS&J3PMa|=?Wtp8Hx-36Hx!pfUPBlB`PdY1bR=9-{*3jje|$UiaL^^wV6L7T;Y#cUS;3fYwYiP)kqEuk0Ra)+Q;h-py} z4u7cu+rA+#U8-wjoZZ-c1?`{lgek<>e zy#A8jtxWto*rI2_5!k)&jk@os#yl)IW)#4lu35BRN4w$?=ozndI&3?pZS7*Il{<7C z95K;cRCkTkzb-Q8SyXTx$&UJA;kKyn8vO%}UgywX1QW2~-PYCq<-;q~(m8Av;qmG= zsMS2=^6FWub$fa^6uR#cu2nj88k{%zgSz{m;p-m4-4FTJtP7$jjm(^0022N|j?+;= zq>16W(?h`gGvsu-Du^~Qa(?>I)-~z+1aA<7yBX|({+Wt(IQ(=IkOB?K-R65(cSa?j z5`G)D-$vIPqipWC-&7V%`_LS9(Ckmx9Gc$TXAjo+xW(p67=1bLNxVsL~$1bUhk;425kuT zkgDPI(_QkP;Y}ONV)MB6J5_zU=iF_~_CKq7R=&=%u8|k`o<;$0|3mxH)|>Q`NZT$|gc8|fweP7Sfy$d=? zNwI6YZVXqqNJrz{9S5Pd41S$QZ}>h-lbl3v=HKE8Cq-{fO9dCZ~>2x>6MmR@#}lxro65uKiNIak9sLA}at8!0&|XL}?f4 zQT67pj7t@Z?O)Els@)GaIvr6}KM8@+p`rG>x;Gjfk@i0YZWMvN$?ksyT7x3lq*woo zy|)aDtLYMU6A~aeA-L1H)4016Ah^4`yEN_+oZu4N9U2R+!Ciy9lLmsFex7&co%hW3 zednB+AK#C2P5-LiU3*p4+I8(-tE$$$n14wj1n!Vr2#}m85gwDkjX|V7c|n4s{%wzCB9EZ z(0pWU6s5lHC5`E&81DsC_EH&(SJonhJ0hkbtQN>mhS5uN# z#o-W_SCX59M}H*HOWT_r&q;UHBsV zUKAPk_rh01^$S`-=4;_0lXpZ;LE0hXpNOu4+{24khQF7)a8F<9!Xm_hbFcEqH?Qb@ z10R6OF&H3(cxlA%A4dXjd+B`xV|tlkj2m&u2cw*fJJLz1qGSLx0Rb=w3=w%j2uvYg zjqB3^#Zgwq;B+$cC@tgW^iS7I{cFOgK<2M1<7m4WNTN)cnz+u({RLmgsj#CdjmtEt zQcLb_jV1CzG-=ggWuxYy2^r}ZB7YkPFVS#D{xptWqHB&kGY(&(b&p&(j$fjOL_Ti^ zaMPeh_H9RS)5S&ZY=>~uDn!n1Uj!&+eYAj{7tq)JkUlUFAfbzq_DA02Ig{7RGmqvAW%tIzy6%rFX9tiLd7V4TECA{Mt}J?S?h z@3w>QX#^uDx1;arx?o2mFSf()Y5gNNp?n?s*^yh;;BgwZk#W}Oar{!~rsgSY_&BW# zY&l%{*cxm6I6*1x4XpLpQ+)sf4HE3}aBrY@eFOtt^vHI72m`Gg?D5!4eT*tB{^*CY z)C6VnLQ~hgx;feGIk{kV*(7#3xxBJQxD1)hn1s3+CZZWyJywN5IgjD&B!s}``bq}s zu`$H@f=UYdF}nJ4C{1MyKM+!1T$uux8Cj{1tE4{}`BNVV#g&W<)<@dnAJS!pCi5=y zOO!z&KV!lqP%ALi4(8cRXIY^+jmT6AiVjZIGu2p@cTW>ARfH0RQ~gX0mK7ms?{5jq)qcM?m^Uh=srj2EHXGDUG zOpz&7PE;~Smo5$Vt1j^mXNlpFB>FwNHtlKzD=+6NUFyY>bT25YIGxH4==SX>Dub(K zynr=&tdg#wc|1qxsRQ0@qnx#zwamHfy8OCKHk?sl?6vGj?F)aWc{z5udUr2M=* zr(D0BtXw=rD&_lixi4K_FI_&akDTflro}~n?o19e62I!W^H@cEp|(w98_jK!NQtRH zUHlA`bQdO0AwePa9xx~#qxN19Q-oYRBqkj{^&VTC;q&}G=XWRV@)!(uE3%YE1VhqrHpr?Wc8BAtfOdA&u@Y-KyQn-AdhR-74K0pf4b0 zkP_&*@0A{1i%uDBX>M!ZTnt{_~Tnl6eg8av9Ao7`LBw&T!jI6J=9TV|lHY<&1U{s)S&S%4uKy3gSxniuH=*iunrX z3Sv=L#3Ezpz)<-!OBfC zTf$2INJQ6zWJ^~$r2v~9J@tpCU@vd>N@euJ538F&mq$+woLWt<`te=ed zo7o##8-a~nuF91|`eA}$T~YVk!z@cI{Vd~OZ3Ey;4a3U9tiq1M@WQ;JVA|Gdw?TAY zCy^&nm@$#Av$vwRDz@6UN`NK6?z%=Lvlv$ESntAVOIGa|PLr5vBUf5ZQzR1`6KWH# zfpEg24+%T5Q_?Xiu??{}A0nfw)<_IW>`Kv{uZjE$9zjZ03VG|MQ*B*#YIRNq?P zPTxe|V#9F5a>IDTe8XtNYQtp1X2U_S@;)W)=i)fp_~iKHPiD-4YnP?E&n7Y!BgP}< zBUZHK5zs_>bkQrub()NIWneQcISIMFS|_bBuH@8_GRU4NG`c2H_4PVnuXZnHFKaJxuiV&ZqiL;yzm~tHqp_pO_lxd0Nvgqu z>q5)|!$Rf4=>p*be^ipzxOOkT)x0(e6ar_;L2q45e`=oEqF$@ss9vvLr{1(syMRoP zfl?V;`Lpt86(f|oU{`5Zbyh`GnO(VFDNy;0rN%bGJHacHz)_-eppB!GrkSRtp{=3A zrP)F=u`e{b&v7Du^67;BMCXL>#OK7~MB@Za2Tq4d$6RMY)rBpMF`h9O;DFtX^}y`M zrhdhx$JxTzqrRX%p?)PqI5F64ZWP_nRF(ZV>>;VDp(?tnqAIiM@wJwxGiriI`-*3X zUc+fh(spjpwwH5&bDndSbCHv|(w#|pZNe;St!<&Xrx`eZv1Yew!-vkvAHS@yq_8}( zG_gFf%Cy9^!n9f)B0s6q%-8%Fb^0(4S%dUKrXd`aKIU;F7)j(*7N4urpMbEv6Be#9 zf`f#gNOt0A2~?Phoj<3fmjmZj)+ghlo|dKRgro-F8%qwwMB?GF3Bnc0=Fy@a_M|re z4~(m!Ur3Avhn`d!ri|+bOyubBOUa;7V@%DWwgXGDbtzbQjo!AMOH_3~dwDGsal4?gpoUOKf#GFY?HpB#+z>za^w+YsJTK+>G^{zMR#Z zA?O>6e;(B>^la)Hf(VI%$U;UTbWs59p^f2d~59mQIk*&IFjMGf5dTdTKv4out>Owv8cT0JfNtzqfNl6 z>e1>BhJ;hjCq)%`&%22|bgGEYA4HY@S@=Eun~$N~JZ=mlje{u-P_Z9RkEy|3EvZqP2FYh+j$=rHM2r>+6Y0CtCG12ya!(GY*P>Pc@hfI`@26C+z(vIZKn;@ zL>YYQ=JeNFvR|*^-?zZ*{w@F1eqVDXw1&5?wYI#*v);D$(3@(5(6T~w*4##P&OP8( zbyi)nR2{qibB%G`Z!LH&GV0>kiMlNbYbyne)2h+7*t!Vj0&_j1o&1h`41G*`%zO-g z)DZm>@)=?I(>2|-O;3n2Lb>Yee4?RzgiM6IBR#5ZQOGEQ8iqCHoQ!4S6o0;iXhldf z!WjlHg)4Ge@Ot{D0>n`EFY zEidb}y!&Dc`?0;9PjOQH^XYl@%>-}zBY!7+7uQg5j^fkZlYe`SUylN?;U;?-CGwq z>B;HllE>2Ba{Q9w^5in}QZA3tUV$x<0x~sfdc7UD9nTpzLJVW(;8FiW!ffbl%53ax zaNpJ?w{P56^6dy7*0`WTxP6Af9v-T=Z%G43BTmj8ge27cBU`)9*W7+Q&phwA#ke1` z^0NRFjuRgyG$+I-R43RcL`i3>;W*E5I=UBpGY<2twdwI#^>$*GN`73AD~+3PjkF)BQ0*KgBn zGiU?ryC$yB%AB$Y%iRW$993gcuST_OqhJoU_=Wdswh#97*XOLksUcy`)%Za$nfW7g zJaZzmCvzZkC=-oh9jimFW4>d(!=)p-gP^0mBfJBxLnyGCAc6QBakl;^-1fR7r=!?_ z##Kzsc7&r!R|Q@ny+0{!ncozE=hb}cNyPWE8+sXC{C<>|9v&rW-!IAxFZB&EH#~&5 zss`ithORQNb*^-7RxektKix+@oj(!BjL^TQs|l;w>+=nA*v*9Qv8G&k>G?9Vc)L7! zJqbR&JiUDid=h!;G0^HXxlg*EePFvEqbuz^30!S&(>)jXG2A(HKlD%%xakuM%6yG@ z4SEfGRX~R$+#C(c2wDl!6!}E@hYFGRL*o09Ak4RNU+L~YwsyftLe$=^;$V})s0O^< zS)>@o(0q?+@(jWBgcTGK5&`wR=@HI*!A1;22t$(mAc-)DIEXNaM1@d{VE(}z5&Hu+ z43cM?VUb~{VUl5kVToayVa=~}C?BQqSIn=hUqQc$e`)kM1nK;M_owiry$8gYhWyeN z{t$=7iPMY$jbHspg`g6W9g-co9x4#RZwMD1%9<}*YQAeIF8zTm1TJ4DU8@Hj6~QH> zGsG|CIpkdcrS9Je0frEVfW?=ew-~kvw{*Aow_r$-YTh(oD)zed{_bV$t?D)2;@q;| z65Jx&Qre>3!rSuNlGsu+MBT*Ncz1s3vW2uozeTVm^$Tq%;yCI!(pZ8~e0`MBSl1LA zR)S0^&N9L>#4_0uP#bv=iTyU$6N?T{8Gsv^RGsCmlGw$Bv%KOUvO5F+gHSR7|XQ*W0U;t+zZD4WWF2L)l z_Ok$XccQo~olLoOxs0{6_1o9hs_)q$lrkybWxS+GniMwd91l8j*z z>4L(<3iE(dnwEs@#2Iho--B;3E%DijGGLPr+NSs|1smUJe_#y47L&^>nqsx2Y{XxA zV;ANw(w=WJC2@%1+7|TkgzSyt7tl2fFjK16uJ^8otJl@7y64n#f;p}hUE%!O|{fRxzJ{2IB2ALxt(On;3!(E46b97u4y3qnXv&8@OJN zVfWMaSMpbUdE+@-v+J-&vJ0Xsr!mHK!}^UWM1xO3kXK|{WLhcVyR@3CQZ3dI zVwY$hI$eK^jGbyarny9MkUBS}La5x9(migzcZBhG;{xL_<7Q*xujMUt?lkUn5E=*_ zgjRvpo-T)OgXZPSQ`vpxy&PUyWkF?mWm#oWWz~}i-eZb+cKK%&cV%~Vca5k~H5<)H zIv`~}JB@sd!Dpd}Q{%K@t8z@^E zq>4B=D5(}Q`kl3cCPf}g^|$hGwcjegHBi)1RAN=-)#a6bDlvXxtj;s9QL|IGQ#)f? zY*2&|c+5JkhrT1qk28+rjuVc*9j6=%Ts8{5RpOH+s;Wj-J+p@L4Kw~C93Jx?gO9zB z{~W^|lgy#a*Bx`akwX*evAMapA-T!9RXDrxiDZ9QP%kM}nYTKw7XEdfZ~iWnPw%G& z(wu0MG^4uQoHEDv)QWlQSnZ01G9$?Ys(?v5Ws7n~Ygs$B)j8BAl{00ZIek)h_<1tP zGbq<`YN@xd|Iv0?^|d}DDrlsg$%9Qil8 zCX`BPRwU`W%))m|&5U#WX324yO}JPsF)Lks5^}%0`P*6KDdg#08$@<7jy`sY3}8lG z25m-JhE)b$25d%XhD63d23AH>24jX@hFFG446MI`iWjH?LhTXEE_ZJBK4wqo7fcw#-gGo zcRR%@mA}r!ctO`)$bO(P&!8^GnIc~y7|?FB1&|9qwkO_OpH05b^xygJMdntMMj0&V z&+1#~Ug_-B-#1*WY%75+2XSf&D{7+6lCLJj(3%lq6Y z9EaH4u!)Kqj!u7&+^M1uySR4J)9k{6^T7#VB`^Yb1WX$vC8)!t!G%`3E7$Eb!gbPo z1O-+47bTIr3{nfBg z3#0~;2kC*tUZY>AcCv+FwjZPh`D>6h0xSA}KLp}K33_6;uzv_C25C8_Ilt3GurVa6 z3|GPpc9ImE451}0Xg5MDEC~N)+lrF~e;4@J`~2(WQrHt+_YGQT^skSXmWD->0Tw+- zTLYKMo;)2e`mnF>YXU)Gi#-HeKQGl=aW~*D0z7&tFa7V|_@WEKkOV9Kpx#muR_lIm z5LVO6wPmoi`Aa9J_664;Mkv(a2lLkACEvZk6T%D3YiJ1BmNW%Ms5p7b*HLM#?{LbW zmQI|c*}n74z*t2#N@9FP{eUF%4&sd#66_e*#Ux289-sFi4*f%9Iyzfoq$wG8h@@=< zkrSyv;L!P7fX|x|N$0Qn!*5K{S#d|C5Wk9=aoq09AGQXNd^rTGv%v)w;Y*F`uLT6SZVjYGX_t1mJ}12MuN?-+b*2*C+_o> zfSs2QUNQU^>QN==w=^NqL?jU_JkcdM;cR_c4%hc+j-YxMTK9dn4 zQIbN%sggssTi8DsegnhTBvbK+(5xD_lg>t6yz1{NBIkRpP2B3dHT@V+ZK62(GEA;C zkt!({P;KHVjfv_g81UqJG&Si`!`yoP`@x2Kb@sRfJH|&p@^FldKL?4VXE`0tr(@5W zu$WA*0;%_hV1DFJ_IR6kX&O`talRo4K^eTRgQ-L(fD``%T=KMq;l#=elOHlSaj9)v zLbSz#d}@Z%iLM&rG$g2#Uoy8vQjf9gYc}Dl&0f;CC2>dR3n3baGa9N>S;9MIa7X2f z*Bh)hTC5{jVm4D3a3^RFUhg|MVZM^)M~ol*iv*4jC4pn>!`DPF4iWr9xzgd&bt6W*#Bm1HXqUFXj|G?RfzEr!{_7&)Q zyBT-U@3BL9PwdOwg|-<_@&j!<{Ic)<;}eZPG8DtstGHuv<#O-!1mlm;9b?c}v%__1 zaJ6}cFR?UkJh$#z6%>XpBxx{eSOHz*(hM&N!% zCcBP=F~(IAk0F)BD2$ly$2DePd7{QpiL~l>HHHHwrAv%c?W4Oydi9GM1M4tLU+5!p zBj$x626DEfoEVuAjQ#2b=}@pd{NO87LCBIAA!*!4>PVG-ylsA`z*cEpKs7p7#Ad{5 zKhZXmQ@p1%KOhHPG4h}vX`AvY3i|NLHZU#%USA{cKZhkiGkt1(s}xnItVoz2;Wb6p zN~@Gwpe6{Q6J}VBu>H(QmYKTvRy)aV*xeLnnOGHu_aE6n>!F!DP}tvr1I*u~?IwvT zi$}>I&{C2?P0<8vWie|RqB3eD3^FAQa;HR6nVBESunZIuE7I`G(3GW2Q@EE2{;bkr z!?s@t4kh*z{6_puLC$Y@A_zaDlHH6LnSSKSqD7B+ixPANeG9Sjgl4^!ecvzwyQ}Ps z*OsL#QVjqB7QYH5*^IdDdON>sOIQ=Pr)v2+u*>NT-(BblO8q%4!ko8V;4gM!w(|+gb5Wo3q%2!*Ip)#47V6p^)U1 z{8+N-sp+q_hxWg4Tj6p<48P*;!d1QHB!!z1#;b&@>9P2L?~LLV_BITrkEQQJpJktB zpLicdA7-CwA7LMVAD~aUkF~G-dazd#njzV|PrZ+`kEu_;?`@w*pHUyMcgPpi+X zPomGPZyv;f+vTfbRQjwIp3k9TZ0Ycsp$6y}dnDPVWQs{ZDOslUZWOupBd}l)x$q%V zGC01B{gw;WSQaI8&?s+#CF+RdGm8Sd0;dAoMClhhO*k-$5S|9XA})xtnys44p4A?R z9URq1yd${tws_BR&2!Kb^_I{c7sT;m-eKWu>HD-^FGi`Nrs4+ORzRlV?++Xlqn?c~ zW-7KXKBvl;u}%bb!jGDAOjBi!*gI$FQgM|E-OImKnZjf>%XI$SR;*vVQOvJGq9UXM zntd~CIE${*RrZ5@j(uF!6yF}-3f~Oh4&MgAMSg#CPj}Cd{p|DkQCML6 z>yfmV$Tp0n^pkQvz-jaL7l=Vd95kO8@oX?hv1C5s5rq-5Y{(f@9y0Lxn^nV4TQ0uw z>I(?vpHv*G>z}auy%(xf#Tg~vEHW&Uze`o*Ge$xKzKHEOhKK=elp41h+%6Csge@lN;e_MYTFeZt} zw{69#;tylU@P1L{pYs7!4ff{midCiwlj*e0nn!Hmt+8ajNwLM75l`|bcwr>dPnhSt z`yJ);4UH`~Fla!h*yv*_R)U&9g{1mfC9f8v$TWbgLQH+J(q3!*TYV_6TvAGc8H^p0 zPAc0N5+aURgQRKfIDQgGm*}3cy4Qg*ER9}>7JwmzQH^1qfrFtgm0M0lPEIbPsG+Dm z)MQKB6FUL60hfWPICy$B(E`tywlStIrYWZ3bK~dc&rRdC<8@G4b!}z+lj*YYvN>%X zVRKe(mXS7|27?CUV!Hky`Pip1mofS=f-!iytf)Ld*{CYATD-OJv+#g&Z0Vrtr*fV{ z%f=#%Qiv+zKmo5(cA34ZM>+gJv8O8UK*{^$EM+BYS-B(HIss+R}_gsVelPYI(zTp>)L%rN^@1ym6;ED;HfTnt8E|V233u0>{~h_na?GKV_Ze3WolOQno9KMlu*jph;_5FdPPd}N~QZ>N8I2lMQnyM?0(km zYu{A*RlchVu6$U?Ul2WEIdMGsa3Xy|b7Fa-dBS}neq#FNI^90oEZZ*I1d5Wj&@+nOJmVVgsn2bw9Hi}LuCZC7np9r$e9FXcfL9zg{;Bf}=XCexe z%$5!5ggisuK?)%^dHUDU&&AJ+*I^^{j|7iWj}}v`WoMR$5pMlF8N3-h+eD0nP=GCw z9nl%#8Bt_hTpR`bSiQyyJ1>*O17u zEth*D5vl6Jmcs7BDd0G;7dQ$W1TF*TfvdnF;39CcuD@2wKhg(jRZoJfk0n$Ut! zgHVI0o5!Aqm?wv&GkH>=&s!1_bBw2#pP@|Ps#4B(!n>1oHK9JiIq}}2JZUGHcQ3E7 zz{xthHOVnCuf*6%Ij?NnB`k|)Lid34K;c0DU>eaag?hqb0%sy^qGcj}La3{(GMPi& zSj4PI&C7yvWWc#7a7C7DO6Oy12$NFicGL^sPb{R;>RVSHJ*F@ESKy~=(pdvM)%;FY zq&bcKHf%7ILf_5_hUQghYi@UMLv4*~jRPyRDx@rGR=3%=+Jnzo)lv_tajNgCg>*sS zzO}7$X%8$90grbcpFCVV=sk2ie2NzU&T?zYT&I$2>6xwsYmzM^cJ3D7hBb9A=$^)z zeRzAx>Jg;1(>{0g0u|h_-gfTmk?)i4^W7)eXWZkMiGICe&1Q{YJzRKiFXXY~G3b$1 z#iwnrYsY`rf7}1qf8YPeA9j?PBpo8B^-z=Rib)nK(u2Y}&Jt zI9uwXhyVM~FJKVs=c!NErasOBodSORej?=Nq$<$N4tQI5BaOy`K2$;akV7j&IO&mlK#)e>@-A)G|J9OmEcX$+Io<4e-tLt+)SZ?`< zIbGjhKVRQkKVrN(|4n>Ld`%3=xy-rEdApYp4c#-Lhl18mHVlM&_M7emXqH@m|Gsqc zrX)J5vI&^3~3rD7hmO91nkF^`$Z>W*)P+QWC6)hb;TLPjy~B zJav6#UQIhW**nq62eBp7{u;kyykdMT4{lg*L27h_JfPmAKEwr4O~`DPA?jsCca1?$ zRM>Q#?1g6Obn$hyclmTZcENWYbS-wZbSb_#PK`%G3Fy>+lAZ;gjJwDGVy0*dZBswt z`B!Xi-8}@q5Q3_^(}X5B7dIz`mW29qFPLxR?-U`5SBk$UuO}gscgzsxtC|~!H_$EZ z7vopdS9?$-C>~Vv`t`N+we_{}HRiS8HSsm-wfr^X75u94%J@q3dhzo3(h^uJY)Z=e zg>s5|N{EG<1;9eb!o$MBLe9cf%Tk+Oc4LcZi*$-`ij;+%g_?zsg_MP+i@JumhOmaj zkIawSf%=4nj1&e>9;6nG5*!=+Gq@_~ESM-rrf1hsmW6SNau`m4gqG$HjqofyDnNum z`aN4v*mYMt{UL&PaK52^tp*G867>@Ek`OlwH%&ST10t%$sL-b%mEfD8n_#&h`XKt? zilB;M8&Nf}CQ-Mp^0!ThO{h&Ml&tc3O!@Zt=VS^I1hQz6qT637C{RSmw4^7JgV@RJ zq)7mxgV*NhMAsqCV$Y)Q#DYa(^F@n^*%Oc8W07Lfpm21Qzp>~apkQ>ApGZ(NIx-_d z6>=5gGp06yHpvS53hoN#3O*gG9g-c|*@rXaGsH90ln_V=Bn%P?2}cb<4MPn@4UY?* z2xAIFoksZr0IO{Pyz#uTy!AH75ywy`$X=0DEpftZFInr&$Y)zHi%GT zmw_X7UQ@r2d*5w5Gra|PVZ1Tq8}Wd9BQ;)bG6yo_i6nu(6+ul7=HLFXyx?@l(p51gHVLyfJ2Fs#Rh|m!+3M)gQN% zkPK7f=IeTssiHGbtS|=OD%z!UFiP0Bysd{Kl0F41o3CA(^GTln@s*hGWAg=sK-xc{L-mk1;ReWWxksoDI?N~1Wj80 z&b0yiLOdzbu3`0Fa;_o-+FPQVhx!7A?X4};S2pE5rYv{VoCCj9Cl=ye!6Zl*h*56G zT_LzWp6l&&zYtWKqu97#jQ#;~#KIDXg>h(QkGX>}`wII2 zH$$oZ=j7zqH+>V-L*0(3hv~^#o2Gt6C}R~4v1Ky4V%4``mqwNCFrPFp^&*Y$|sE_cWdH=d+g)I;kBo~1npV!Q1Sp@eU<1%lLCCc{ruL!v57%@it3 zIi)0eOJg}R|*4)+qoD|oUbO!We&L9a)~}_@#b~oVI&ki zN*FbHKBFm?^U)Mfw5C!s*;X$0+zb1*c;|Y_!FB5E^RzFY#T-y0Uc_V-fUEO!9CWW` zM(5qC1%)EI3DUS*x5|86Weha-vUc56K1p`^fvxv=Zbqz!j#Y_3);lX9R`7qCnX_DX zYR&{g>&-qLuQBEej*Fbj3uY$0Y~ASEZCRh59^ht}L(e?dA_4@E0OY;ofMeC812)*o zq^4CgU+ndA6280FArW0(^Vu1&pFl)=GTJum>#0E#=#0e0bpCaW+@t~g+9`~0Jqg+C zhbO%1bUwXOweMi#;!2+-r=(ZdFO$+L}*2vp$9VgyZod zK1U6G<9gLe7hRvP($%{uBIxq6R^YjGE1ruX{yTw(rT98weUr#6Y$3IyM`Na{X7}Tc zQOSy8`R-T7$hkNu$o9q9ZhtwsweORGea<4tI9dhm6h zf(T_d2ZQ`PGT}u}XWETw=+;RD8f1=7hF5S@U9_n;EZ01JdrsUY(<(0J3|?06<-)Tc zX`IaAl9-0aoxPvnj)`$quB`XH`#X}=3h0b5Y0sU<+;%Z zz0TN2)8Q@23pCf!Nudnx-?P8QiTkMMy8Qudixmesn{D)r?V(#0jTJHUGdjf+HLJf& zCVI_b3k!Mq6D3Q1cQQ{BOXxm5s?v?`DG+Xcl!Z;3*?uxse=*1IR!*7W^LBOJo7vLh zHPoyGZ8Y>oYo}!=&$wXZxda*HCfC!_^0cYi*AS}D&m_$>>K{p<4`QS}n>gdbnTl3vek8;(=#isV&M)SE!+k(IsW`kZy{s;Z{qMFN{` zNEDq5GpTWPiyO_=C=QiZo;wpceUK(^UVZHwSMuNIi|_snG|U)h7gC=|XFeD^1KUN4 z+@HNZru3#ZrmJe!tn}dB-S_t#a-uC%$2!$?3<e!N*VJ5@*ZtJk>-)L$B8~4W5*F~zFmY?1N~C=0 zSt~K~yBDDZz5>)=<@dlaFLT(H`yd0{x>ug=Q&e>3!;}+b({|@aRs;F6G6r>x{c;79 zqGC5$vyB&$)9F{&gzA{3Ca!zF$CP5H-lro!R;_sKD#BH&lfJaF@&TRhaXrjQf;YgT{%e3Km!MIoCCIe8u~n{Y*MmNQ|QM3V&ui`8!y z-mVN~b>D2;HN3m=YgQZM=s$sLJOLzh9V(hWLk!H4RZO_GQ;EqX-rP4rer8h!x zpaT}%5xXI^UBm$omN=))MZh@eE}=ynLY*q>Y`)2p>44;(Zdha8d7_Dq1j-(b-0m=X zoziTo^O=XM!|$b6NXz4*?`H0#N7v1Ruh7Na&9irRN!a(a1~H5=O6n{P2#p>ro0%h( z@X}&7Ig6s~D$gFJ-wXRwn79Gbbip4_XC(hP$Ca%aL;)aRAs=S;+d!upoFm^JLHW8R z^>En*c)#t|1U;kTd)oFfr$Na9qim$q258!smdB;3{=`s7kAFq(bzvU1x`5G;<9PU| z^^s>jd7(paBj$I^;AGL}yS}flJ1{SlELb@xlZJD{(xeM2-c%n`8+1!bhJ8SVsVpinZntDZgeF*-XkhmTFp(Uhvi&L2 zIO$K1G^Azv46yyc<*$J;!q6n_?rC`_z9M)1XR#2;%J1%jWU5|bKgRV}Qb9@+2jWgH zl7e1&X@aLGb%Ky9Q@&;xa-8?)sJI8oP=FGB?Hf_TfZDtprsbVlP3a$9KoMDlR12a@ z>16b*!yqge*d@3$^p&5?RPRUI>ZUF(g9(;{vqZBd0>4L;O3$>X{dJhlRD2&$~rX==RzFkYaFi z1G;A=wThGAHmb3dcTPx;d0XBv7#mWJIfm>^h$1LGQgdupn+nas5Ck!j6mQtGkQ3b2 zuE>=i+4z3Z=-TQq5sM*)1LaeNs(V$=Y&bOp3s*^A(=g{?nr&1sn`SyetRv^>QMKR= ziW(L6EW%ytL3Bi-Nq4_(GP3BZ(X8(hztYVtd8P)SbMd;A=iel}WnK8pdRBC<`di|p ze|?nT*~lO4lt3S5!F?8z+smLK9ssO(jQfmgUe8Y$MpMw+EP5e!F86?SwJD4&6!mEL zKnoL`{03QbF;kf!p+MS*vStj!O%GoY-^GL2lgCT0DOSUUo(A%Q)a$q#CU5xzb_pq^ z+&o}JDhS3L;kwEO7uObL(jmHU(e{;s8g<~66qJJ&i zHDXTP-Fr1MUuC7)_xG&F^UU`4ApG9qE~-T_Ko6k=iQ+13u0k=HkY2 z{{7F8T*yj%B5~j!F$#j4q9RV2Spk&?IBZ4tcV0O1nbjXu+ga1jvFKFbIQcL!GO=cd zDMPm~Y(hTGRbaL!2IU50jCJL+eW_A!pqyvZ{D||;%ydjU=J%zHM8jUd5w$aI&QEQD z*(ISl_)e)$7&QSnV(}|X)95cjhCy~PS#%s|E&ZSBm+XRHk=d7S zRp6bDxzYQ#*xi2ElC9!H=sgT!I&_u!U>>P@f8@tvDKFqsw7!H}r@2J2?TU{^upH2xJ$-=B%a zc3BR3$mE^8aG`IoZ)YLcK@J>n=r|wYF{xG4G2AY;Txh*}NHen$u!PIu-dbA-8-=N6 zxy`K96(^o`8ij7;W$wAv1Gm+`cze7`@9I00G9tx{F2^Kxds_e0pnZVeph_?Ejml`S z(9q=EecF}#s`;Chd|H3CY%1@Ju%mL+gp4A+Y(43qjHy|1I1~SrkrqiVHNhgZiwuav2ghg2Y~~1Z!L)Df z46-!-LBpQ({`3&c2;+73$PB0plJ@`8`2#0PPA9I(%Il$@3$7w4l4QN>EdgrS9sxB0 z43#iDI^zA?tpIl6_kjG9H>8N9Zv(&DA?QiK)J>((n@YU{PBHk15@r;R-)qqUaK$KSUDf;~Wm<*B)lfNw5S?lF77jFWyn{h!VUn1lLTu+!~64i>(0l0YIqR=k84Vy9;|K}WGsrtxt9r!W}H6tQbaHN2|pjHq}` zNK5g^d|L^FqRv6>xSNlM-m|^Be-FC*k19by8kn8>yZFb0@7VAV6~3VsMBJ}nXK z`qb}xI?hk>f)Xgw_2eJ+Zfs}){tM4{q@WCZZoBwN-)}7z_fw`n>0{wE>I5>V)QAl( zsb{O--*Vg}G%P2wT80xeKCH?`*396pUv}jC`);-cE+$cmGSw}av6>^z3SIKZvf3-Y zIe#Q3t?Q$)ua@RXVgeO6(=9yXI5g&~4_pZfbwmp;4chHp@d-wyoEz|v36O!a$mW(? zN2hZpmQDPD0NX2e2p`OZl4(Gaq>HmIrVOhGkoz{=kn-C8mC%`3E$d<3=$x;x@u8zN zJ@lyEGxgqR%z?z%L@cVf#bs>lf_}|Em}ArPfu&*Se5l_gIZA@yDLrCQl`l*rgMgds zSYVnQ<=`Z{2iNtgWD|{TW@Eq*Kk%g?6lp&WJ2Gj6NpdLDjXdKfawi2JD`$O;I#B4X z@a8W+6ht$7Qx{ifGb6jdiw?%tD2PB-Zcb7b(!b06{LHFej%LjAX0Ar2My^K8DsINE ze_ha&|IGZ^%-+J)l9Ua|!yzb$g81)p_=P+jjZAFJTuF`1EUfGW02l4u08%Sc0e}XF zJd3=en3<)Ow70XFinoF)H0g~Ak10S%kl&Nn)6UTj+6<|uovpnKucrXO$kf5uj2Bw| z>oPNd^zSOJHUa=PCLkjh3lJcP$nR`w&Z{gg`ENHtzX<>=U0oe{nVCI2JeWM#m>irf zn1MVzJj^Vt%&e@8&>D;`UiPj=o{aV`FDNc`}Yt{O_kg00nGqvEwSvbPWbcru!rnH#y;x&j2{l~h$p)y2jsKq)%*2gc%><#(n46IW$jHK~3S{SH z2l4`;We#2zs4D+c&A;!IcQCaw_xk^G=fBka?@k(M2ju1dpEY4;_wUM(m**9CFmd~9 zI?0F&y17}I^0IS?i%E(A#Ta=+xg;6cC0Rrmxy3m}8O7Oo*tkWx*;#=S&}aB}EB|`$ zUjv0old*SkHL^D``|n8mmwW#ggD3qz@Im}P&<4~#_&@OZpGN*mO8y^p{g1l-OB(nu zRsNsZ^*`$RFKOVvRQdl$v+Ey!`%p6-pW*r!e@5KO z#nINt3u@n}{$(7Kx;nd={pG$uh5Wnw!~HMS|8{>gmCekVRjjN@SE#&aMKy z{VgN1$!)p`)`OE;XsXE*si#)}H{#8HFU}Xi7mXi% z40ZIDUec%V&pb7K-}6n1;+Ugrs`j=K1Q-;J0(@gG0B<)R&@_WIRp zf8K6`3wOI<#!iPH-hRKm*1l%coMyYQ@G`(M{@`+tsI^4_SO3J3K&_rzuEeg4CyXK(B>sPo&Sw%cjw zXFr`Wpx4mdc3HLR>Mgq5v)4A+Uk^Fy>X(`qoq29U*I#e^$bo16d%az=Ck$Qd@g6(2 zd*HhKh)+5X8L`>@j}CZXuk}CKWyXasb-d!1^?SVayECWU*5mAp-<+`IAJ<(urOSYe ze%JlkCr3Zpeg3(xu2sJG$}Vd!SbF8)MK{0sO7E4Y4Y~H1NBjJ3?)Lk1`r!WK&wTM8 zmpyvLiU01j@kc)!z2vqn7e4vKF`r)8Wy}A0^6ee(o8N6jpFiFb8$Ex-%68=^e)IW{ z7Y^+5_VBB=-~Zm<{Pw>$ZN7HzeL9S|sq5U^{xSbYA3pTvea|i2;rgZXAD%VtwVgLV zaDKki{;PY9+-h(B|J(g<`*fq%FYdC*yNe&a_M{^>>wn{hyDooh)@OV7J8J&Wm*<`M z@1NcK){mDie|diUeZQEwV!O`U{_CkrAN$w*YrZ)DtbZT=$*jyiH~;Fgod!L5$)FwE zEnnD!|2Otrd->MwSAKZX#s7M#!<>0b&z~22r0I@Pjc;^6prQMmUFScu&PGpdcSgs9 zcONk4s>$aKSajV1gS$S_eDLjEr`+E4!_4O&9Q@EaPu=^BXM z{dDw|o&GXt!+r1VS=@fJACKQ=`uLlU?{e*uU7tLAzm<3QxVQVknID~aWB>dA)%Bg_ z`(CyEts}NN=z^|eZtL;D-yfLYY2RBqrGI?zCAZ%`aM8knn>{oC4~Oje>zm%(>G^Xu zA9q)F#+U)!uK(fG9%sL|u*b)@zIEh|AB_9_fpM=se&L)szZ`zU>g1ODe!6hvbAx}j z^^1SKtM`s@>29ybk{ z@x?x)zr5_4N1ys)-A>DQ*z*T>^f>gWtJ>Xh?eytGd$;@Im9;PK@~f*KKl0pFl?xu* z|9|#8X6H%2c=*kZul;n%;qUzY{S`NFIQ*?IhHQV!6<4pG`ReB7TW_qp-4Yv!0?4TDZR zFSmQC*Zh-rJ0h{^lgs{n=^5>3|7`x%GZ)Mr^!5*z{Na%4J4|mEfA{g}lm4*nv;E(m z(Y#yy)N}s4tlgwV+pgYXgD*P{?ceeEzNdHHa9F$FKXk^=dmOa!x|06O!?{U zy6?FEDX(^ax&4wWnjT0Tdfs~g9)b>UC=BoIRF&e$tG~+m$XJJ8)U@>eKF?e9+)SGwVLF;qQ*mme(qsH1@5I zE64n5)mHD18FqcU1wA_Mzxk+<_g%i_oYlkQ%X_{1_AWCs7wj^1`J(H_benVji_@PQ zf9sIT295lD^2mJ`ys>)A$y*Oz*6s6)e)5ZzA3SlxN4s`@V#M;fcZ}IP{`stq9TvYa zdY4CsJoJa7@4DopwXeKxqrLZ>5KkWa^zoGwyUjUi*^vJE)S&jO_ujhK-1O|Xj_CHl zAN$2m>s`6q7ybO)!yY(gZjbh#uUg(=mc6k5t5#@e;ddTviq<8mW6`Tdl=<42y8>G1KWlb)#@y4jM)9-n_n{@f2|-MIUdN}qjtjZAG)IOd$e zJ=Qz#jY8$YQtyoxo!L-&?&3w~9J>9C>5KQ@{ON0c{_K(oO;g|O&i~)bFFWtESFhV` zlj7M;x9v7?;en^T@N9AQl|TPS`TX>!AB_2Q;mfNJp11muW7Zx1^C`QePUv#sx}VN^ z(XxjC$9PHqUWy}@SCSU9DD54W6z!5=iHU2 zEM54|%Zn>cp8QPzh0ULCdU?mcT;K4-rMHav@|ks4a3jYJE7o3r#lsb@!kF9fcbBX@ zCh^3G%WnMD`UfBJ*Bz&ied4GyMjW{AFMoM?hu@vQV`hsByL^y1V#SapJ;&d1O1JzE zo?brXZ_Bqmc=bttp8mwmqmNv>vP18kZ(4BX)9n`BxcRl$$NIc|saPf187PlO@ z!MV#e+I-;|8}_|;ao6d~x;${;o_(Hwcj3jm{c_st6E7b#f5M$x&g%Te=1(8J`;Xsx z;f$Aex}d|&7bV`EeD1%NKm7Y0XWahiNjtvtO8m~k{#W$f{pJmpJlJFIuTEb1#@LzH zPv7?+Cp|u(|FAvJKWt#{FUR#<`ild5@7ZhY)b6M5_V9iyZcH^coOoL5e`c=O=hx@% z&~cNslaKGycm4%i4d3Lz(SN$AQdru4_WS#9cj~&&-|)i2{RYH$E1W&;gje=kwaqL0 zZNJquf1Q%OVbUdw#xzVA_uhZqv*@zXiw2LHc>3Ny&z-Zwi(Nn7H2wCXd*+QBx9cwJ z9iBUR!+plh+~D1xoYA!Ygtx9=`s^nw+s%F{d(gJiHn_RCeXsjD40_vDy}IoY&n|jr zO5-o`hb-!I(wwb!>Ga!!e$s3F&%SS(WdBNW*r;NF0@lDOM&+a$-{RMyQ`1=LjPW@{OYS~|LJE3?DWoxzYjj@cLP40GobMD;iKOe z`KQDBfAsKzZEu_OQ1-cY3m*QoTfZTDFaF~vpIo--^bI>dS57|jmtEicxZk1+N1wF& zOLHHc`}y#v?rVH(?+N?d@xurInY;7{pZsFZeV_g5_Bq=;ecl%rJvDuc7uLOI%M(xA z`M7O9KD}_zC&SKN`>B_6@4Nt%Pi}kx7cJ#J~K05l|GhS#s@Y465zoq@*o!8p# zo!8b~|CQseo%8XmbB;b~uXpb#-oASF4NE`UdD|1d*lNdBXU>}b*vdN|xbUYx+2E=P z=PzAn>o0yZkjyHGRVbP4gFZk@msS9tI zFk<+6D<>TB@ar!Q*m~`kHrh1bcgRogI{ky~9vS-hU5^Y~|IX73&mTYcl;?`K9o2D< zm(TBf;lpQadcd;-9$j?j5g*<^e#c%5PH0^IPPZTS8NB7gv-))ycJ>xeKJ)8S|1xdQ z_2%s};*?iUU9mVetNHq)PVIGUyPG%JXwdHl9RKK$E$;f@gu9-ZcIwuzmrniJVM{vq ztIS>dfG_%ucyry6Z}wjF*`d2NciQ@&_l;Wo{K@l6mvwt<;Ggg7-!=2zsST5ld->>B zjvM#VT7!E`ylvJ+#s1?q=zPdI2X*Ln*uqWUT5qr4&%Eifj|SXu%h?BI2X64r(<9cK zf6&&`yDWS0(0&U$4!Gi=+yUG4|I7R9Z@%L6i?`q5;^W8cI(w&K`@R0=`yZarap+o? z>^*t&qkpvC{-gR%m~i6l%g$JD#$`+xpgH3tOK1<#8{J z+2r%#qrbd*-k7y!e{sNG>s@%zF%6?GTDo}f(m#H&*WTA%)wJ!hwMNgrY~mRwKUulv zk59gM?T$@{_HX~(33snp-SwCI&EI*WjW+r9s97gpa{bcEusxfujo*9ss^MMUzUArt zM*Q-oT{qmf-;=YizWl?xr@s4C*mDulRkN+uzTO@-@mNg z-04rxxMI#0_r3M&#-;yt!ns{8UD$c?k`6;RS^M6rV<+FAzhbXpkB*$aV)CksI*sl8 zLGiHZxBqEc{|U$Tx?}U{L)ZUc>lw!;mbYshf9&;_wCgq?etP>Y=HL=fntt}2r>38M zVEfga0XaW*#PpG$v_D|+T9X#ETi!4J(WbpVUuVQ=$Mza`)|lU}b=Ai0|Ma6JZ@xEu zo%T;{Idh}w?`-n+=IjRej<(e@cAKN(j*wL^5_>Lz!9X0Q&&C31T zcbM~oW53*Ki`NfZa{p7)d;jA6E4Q0`+wv`Uop9N_7cN?VzpJ;Hc5IjZcI$fU#|Pi~ z@mq_ZnsvgWN2VS3v&@C}9+N-d&+}f`dBlll&+op=L$`c!)rr%#eRH413%hmiFm>AE zhlcc8Hf!!{YtPzi@gbLAwAMYT-?YE?>S=}E8(!Pzl9T^dUh$^`pX~a{ePgn}JF>_4 z+h2O{_2j!p_P^!df9qNKtmk8ck30OZv#z-Bsok3=Zo1nCFD#guSm%hDixxNUKEK=g z?Qi?r9!K7D#6x%g?dMN+o}2&E*1Mm6?93b1sSLbf?8XBpJ-zP0_y7IIr6UV>Y(4tG zT?XBL|M>3DH+9(c_QCINGj#6Ui>7y(_u%X<@7#aV(1jb820ixNvwa`C{eyosT|KMg z#(j3(d57|@M}N_wd-(@TFF0eykJ`=Hq5RlUyd^L~2?(CovPU>e!SOCp|%`~8BIjQuua=#hx$(zf4x-p5rU5jQ-UNF=i6+e9kv z^H3s@PWv=ZBr-VzAI;gaTq}L#Q>I=ro;Cd@lXh%~k%0x4^yz`vTKE{6`IhrVeI8FR zC2HuANu~U6lktM#p=2Un@cAW~NE!Sk6PBNoiA>z5TQZT&_;^Vsat7vPf-@OCeoj)+ zr*ATuGW19$Gf5u@$z;~>P!gQ_cuuC`hDKo2@Q_Y%_4z)TvOJVbqp#BOoRO_G@cFn;x59tvL^kcqWjc{JGLzPN9B*u_A4vFPODEwXQ!kbF zc_^LCru^|j%DjOkWo0X!LX!MG;47b&=~N+MU;#jbmvqYTS~?9A`F$`eVCa#~7R=bv zZ0Pg9#atR(WvFQCWeNtqOu_JdIvY3oDxFOjUQ1__#+Ic)y}?Ts2f>FWn>RL|;aXnH z#WMy6u%sDpt`(f-GDf$f?F)vMIh+O`K8kD^3eeQc#BEvD$aEU>VaAcq8(o|(B#aG6 zqpr+&3z>|8uaGr1FO3@a`6?5S8{aX5AK~+TCZ0BWEfdFG^>C13f2-*u5jTEGCXq5S zn88N*c*!KPh9_Zh!;_gr-ok?2GVNiI49pk?TQ6<=#7vT-iT&{=*~#mdq1;U$sZ_%B z0qgp*kN$$k7JtLnO4Nb$vKc&Wzg{L~{Htun`d4^*Mz-*5jULZtQx;cQtFO>5#%`h7 zjU4f5+R!bRF>zitm&+Oaz1L4a~&JzMSWAPi+|vzK<8sZ19{(7{3^=&d8wDz0bRN&$eDR zZgMxUpY4ORtAPcBWMIy4{$S>}_@e}j8yz$raFr}d{1o8DQp@Fd}`Fz2|6@@g; zw}!x zhQPHjyo*O;c$W-{pT8%7jr%z_rI3BPQAQM2KMu<9j7?e2_lKxw#*2bY`udczl&?=I zYo%XIvVnypsD~@E&m?nw_>v@BOj+L7iF^z4{d%c{pLZmIN5pFCW&QYwdPt#PFHK-; z$}+wzkd`MZG4%?*E=fQli@%J?8Bx#VHz+IkvIRnuemRH}cDLqv3JZ zHv2k}Z<(m`;p1j#PZtB8TNqs%Gf|eZI89}J8$~@lEYpXv3j}#_Ki^K7nJ=Tv#O#!j zaQFL2o7jqa1wRhJBXm1v>&KP>7{5L2r7gqK`1P{JXJw|%NelnRPb_dN4Ycl|n|xGRtmxjVVZ97MNo;IwD2#!N5Xr z@9Pqg*We15)$~!o(D?j^tQtN=R!twss*gV`nbCD*>WwX9<&42WnrSd!W?(C0Yne+m zctLX--&W$n??^9;|Q)Z)#>}Sy9zFe}J#^97W zQe(UE?u~xQupG(fKh2x^^vE!W=JP#MK*lcM`566-B{TjrVWXd8#62}RBg)KL7s||> zl3ZI0GmBk(SwK1rEm4!Ld~4%3TxFvtC4vSoX^eoU512_cv2!ME=4+W`!sYe9RYg;l zHS$a5&|im=$S_H2aKMzNX%Dy9^owie$JlU?KW9T9I7B{t#Nxh=&0zKoEHr29F>&Cx z$65%3=T>w()~JX|uzkW8TBjRQ+&bTM<*#$MuW zm_7)%Y(3b;#~)NPa>?|nvE8tN;b$_XMxUaIjlCpQYIG;2!st%2Du(Z~0V9xY_t6%)^@8 zAtABJ9kR^GUsHtlO!CWvIS0S^vPF1g;tw(f2G2|-7@QJ{8~MOpGd>Mon34S)Cd#)d zIYhz7GZ|GAGZF5aJQpg$pZ{Rh4N-)T0~~GBZ_3P@@+}EF^KHS{OU>*0IwD8<-sdYq zPQ&B4vBn?5%`rNb%$LcBlUyg8os)a5HI8VXs*}ijXc4Nk9(zV zd|Ak|zRA=hp7v?U)Th6Og_VA+knr)x+@iq&(uC zh#3cdlNm?etY={LGg%LR9C@?$4GX~JgjWww#rTO#85)^kuGhqk1=!DruYf-Fd9u)o zK4nhW#H(m=GasN-n(2cDls>Kq)r>!+<*eSfB#ew7OPR@cQfBgU2P^l@dJxKt3{qzH7Es3KD!&hwDEW9H5b%8lydlzQ z{ow{T zHJN%jUq`T|#@KVpSU}``OP<>Iuc&8i9%V!&=3BFGjCy8$4rMs*{X}?FWk#PeRb=KwG<{_F3NOdE6`Bn-a>OblKgOY+S^t+z+jyTc6Yo=& z_t(u)X7V|jqVi)6rlT@`4j|b|oJ<+na1W<|=i61LkJxeL)yqL5Usmz|{5ck_p!e5V z;_$QKz;BPdfZrY~f7+Gc`v*jM zX8#st#zs+Q=3JTCVJLpTtYz|HAtE;8CC}PQJ>zpR`)hboGr|T}%uf4$Haoh^niI;5 zy`;?K4w=R=`>`pre86;%KOaT#X4VB!X6DeqJc91wkI2-Yn}FnT(?^zpm~S(9g?@Xa zT0GfLWwCjloKpsX{yR^!`h2cRE2Q)bpUP=HmrB3Zug zO_I>UjDq&{TAH+luX89vp?WYgFYenY+$&b`dG*+a>*si=N1$x#nS47DA_0 zg~Tv|?te?V$;TB|-=70dW8#gx;mP|sBkEbbmSdf-k3US5zt)f0TC)yCGq^?;awMjF z`5=Sm=jPI|pYKmnX4Y=8)WF11c(c}8%E%@0 zjK2?(2*R8xL77<#Mi~of>*1q}M3*VUQ8Z-$?U%6-z>{CN4`X3@f?)dd!n} z{7gNw?uIfpy87R;?a3bpv&p_+LKtAyb}KY6IACS6PY+orKetYqnV(@91(RMLd?X6} z_>y`?kGmxpJ`On7#n)jOG>)&EC}Wqg->;0aUq-BJ>WL}6dc>iA4mqQ|kEw@__1hy) z1B8JO|<fwcb!Uk8$ z=lDE?TkPvFn2IIPetYD2d>w&C^y3P`X|o5FGP71vJJ)@`B!dL{z8+;JcFt(d#;3WC z-|+M^^^DKTq$!f_jg3=DeA`XEypbd3XZ)CnsGEr2Z;w>24-0c5ejm(k8DEMrGiO7Y zIe!BeiCLTaex-?h`%78Y&%IK{;vWB7M8TGsc{-NUpeVg>S#0IcrLs&W>(8NS>5OmV zDI;q4+QTIK^P$>N=*O3onf(xyk<9hmOBvsmY$uAy)FYVm%Q(=;zz4&4vOuJB3^|)uTp2-Pn zS(R^BDKohi%FOzE76zDkQ_9SGGZZYz2M-qf13zEP@f%nHzaA6Wp6|mFFS9Oyy~sFbCKpaw&WsJW#Gg0Nifsc+8uWR-ik5EqIWo%3-Y?3` z{1Ihl%>-rU{8$KNVoA!(*>?=Wtm&o9#L1+1%v=$oVCGyU9=_j6nbB*MnSHKoAhPYT zLfy2dGr&9@tBjVfvpI~+AC`unMIzq$qPx;oIVL%yyqu(A=^gizbw2@T+^w*H$ zDVse+l(9p_^kL@Ts0Rj3Jrh?@U~~#O;wUEWNa*-|$hoL5V`S!B63Wb6f-vCoJ!J-0tOlXf?}O!z9?hx8k{Z7r0_@R^ zX_jJnc2Te8Ot0@duH!7^-L~D^o@$&KsLsL}kj%+6um95sP1soSTn(cBT6I0Jq7UyG5Dt$0;-ZIAs_H z)2~@~Lp>AkQ)bRmmb>QrYl_Ou*vJF>J~c*_M2H6qVTzGePE$3q%1pVRdqBOIJ-m3z zW?e252`p{#`ps$OpM}Yo3H;k$jtaJIx@-2Cju`nVZDxJ13t-{`aURQKE6Kw zU=btD`DH8y^K=t)IEo_b#-TD$tzNlL{7gRS1QAksP986 z!qcbJV|vgV2jb?RsjUzF_4g`cWmK7CkI!kU`%mexkF{Et@`cJzf*_kVwHHO4*#bhY#cJNJT(>q)~L&2hfO|| zXNN!^d{S(z?hC<}ZHjTtEI94ZsHJZ+1+d*ryzBO!aKmxc5Tea63*rZ^C zoFW6)M-@2b?p;X@a9Z0`4;(A_!lDRsitk*VR)JHr@oj-~ppt|{d4^*Y$T~UO`D(qD}Mxc_3 zuZPri@hvIt=B!+Y5u|R%wg;)pfOwE<0<0ENT!65k3TdsZaGnaUza?~Thq?!g^R_%# zl>DrP#jTEZ1yxvD#7&uNy{m)H?Md>$a~MIq`j#hgW3`KTT$@A7CHIV{;GTPrpV(ZU zs2UMl^&Y0D2&k%h>A90y>lN#ntRyRBw=>xi{swDrSkuJmvIH_L%3v)7CXU}MgUROf zMSpJ2MqQ}MNGlAR(lpuW!a-xl9juqVh78(am*S*@VvUtjrr1Qti#Hg67$9_GDc+Sk0O-%j1*(PX_E&Gb8UYy%r41uE96qr) zZqfu&Uu}FdTPsQY1JGSqijz!wa`?!{+ z1`Nx^BC!}SEFTMn!uz6yWg3U{?GFitm#0nUpMi&B>w8B(`b0kl)}byK!+%6X*0k0j zGT^9s@pp?4YQ*0qvg^d3JZy*=iND)$>E3^D;_vnhx+d2k{zWF7n$oG}60t|Txj9|R zmr8^%r4s9p6NzH6Ui{rPXjKMkb=*oU)Q(#dKK0|)R=l7@~vdA=lA=!kZT;s4J?va-H%5iIPXzjSQz|cKc z$E~zTP$GJ3Lz;W~9>@I+=v{1P3ZE5j>0&vTsWfT95VKzu<|&$sP~9_gK}1khXn~p2 zl#qkG*34C1!xl656Zia%D&!iVo%_ukO0^vO)u2nqJkKexyWzmAoqdhx6qx4-4I{tGa|%puRIT1~ z3OTpl)pBPl)FLI{3Q{x>4MPL5NO1<%AfQfgVq zLrQ2H>bcn2(f}C8DJ<@^tir)`Yr;xoxDbRIFB|*bz(l!2C3(;WmSwx&9L!hsxa-g& z#rLhzA`$+2v^ZTDRrAmi8p(RJIGtI87F4)J%Nm0ZcbscD9?lbNApakrBA;$x^2q%r z7;Yfga=%$b*1+6^`(29`XVSkdTAbyN#{1RWq8Z~V_B^x%mcN@-C@@O?6t8~ zHWpeo#!-m5(;DnRoE3@M{YC?EM&`Q@#HqaSb4AOoiSe2>zN1b}3*TNnx&%T^3*SMP zXdPQ9G)^6T2f1^;iJ%YzG{%f@x=324m@ScpE2qm$66t^$Ci5w0E{OD8B1^oHEwXTv_A*74 zGwD=d-5{2R`f0uAnR#iVPJPd876ul zE%7h^@TbTYQCbHYZE4{sEs})LUHviX%l;ous$yjdOIT7_l6^`ZvUsYA^S>&rJtq## zHgUOQneUisWc2En1#8)+G>HLj6wIZ|OgOS6opMdCvpAT8lz~CLGQ*f-e42n|LKCd8 z&YxAI%!ewGX z)-|%liPfu`JA{5DU0Er>LBJVs1b;YsrLxVm#N<|LJr$@nyDI zq+|YGeAz@BY^#Llv_t^B!4goMb~y6J;mqMP!Wtz ztPnpzJHSdiu$^F5eOiLQkt@MZ{iNt8_@JloR0;9Tv&QH65I)zqg7E++auSTo(ILmU ziadjV@kj8UH*Or&{y9z}fKzb$^q~Df-Z?*5c;UUH1AL{k!|@aapTl=i_|kJA=g`DG zgTH`J*v%Qj$9H^91`T}xKRUTC27Cm(jvjOp$ZZymVdaeDm%P&#P2f}gsGQ6U|B4<( z@ilUf;J^yvR~-p1x&nu$%e4z6&?g~(h|ban6~T#~fPaJs$vJ1*q!|_8Q$zZRa@v9K zk>nC(&ESFdB?mksZw@}qI+V$22IGV--&X#dW)LfDT3H=nuzg3k&)Oy~F}Xrz@M273mrOMl-OAkn{zP;1zJ$#AHPo2%D+XtR=OKu>$T( z;223`Ba-w(k+n^#msT6p6|Ix$W@HmtcmHausM?l&L4KL>Nu?{&zt|qI2F~DjR_9XB z$samghq{AFjfxhb6Y`N{R>zG)i!kHaB5N|RW>C07U+@j|7M+nx@evX#TfoGN8;g@0 zYz}KW-POvwPS@#=zPn`ej=n`jMq44jav|F!BL? z`9Cqeu;j+WQr$wf87V0NpC}4vKpXas2z6Rx%XMx#6U@;C34mry)-q%cFDwYYe+!A$A;b$=&;90l* z)N@_4s}H>N+5~q%Z*&T)@Pl&X5T1co(S5iyAQZ|KIU%Z|-X)mQ|KKaYBe}6aF2N`G z7B2{Hr#~ZoE4eO$KWruVl5~{dI`_TwREWP&UpakIM#4A)guXctni1CUk@!J+N5?GC zieLd(z@_s+@HW7-ycO{aeggc0eGK%5_yhdGC&)Exh~Qr|74}!}(cj`T`jftj+7YEh zF|}8eJ`L(g?seq~?V}su{XmAHF$YewTLnDG?#NHUUvhjxtF9i}mw$l^-RAr^WXjd2 zeW&-Z6ONB1Z%Da380fC^eWY(fd?9^0ZwcuP`x9J|{c-d_52uB1R!-}CYT}o{c`yU| zEQMErzC@1L%T_HHKXb?#<#43@8ZaE-Rr(h_(tEn50*mj4 zN?^M&ysvS}KcLe1NcQRC-?LR{(0h{kfPRgI=@%PAIrKxAHf&s zGjsvH1oe=?iX;q3LU@3KQ*9ibWk2Kz3Le^b`W`-(y^_ACW>I{k_K}f*kDVT){fh7m zXGr%^A2@kt90lM>IPX#8MYjj^1!mz%@H_g`CGtVL;scbm_ze8Y7QpWskK-2|Y)rqr zmF8z0$tIooMLXgK=@CSLU$RFv_E+-Z_#me?Whc>oj64o?vkB)cF&UpW@^lofzO9e8DlooBUOJkiUg6 zGH%)RP=7oBOQV**@8T2s&>}<)UHMx<|K2k_Ke z^iQqa@E&(n^8_auPNjUzM{l%w+@1?iHcAcEkKF`hIgMY!C^kWXPs5Eq@M-WafLd`Ra-}np^u7Lo$B;gXxg1?si774?uZg?xB7l&Z@@%l+^d?K$ zET2lhWoKmf86`3V&*RJLP4GuRi+O-jekDT#hx-0&|1cI{bM-=4^_z(aQA69O!LNhq zYg_vIzP@V>>_2`75-2GS;b62?h_EZ@Gs%t9DXtuhMemhAfZp&ku~+mJJRiL$gW&YJ z+tC%32HtG&1Zs)Ukrt#Z!UeJ&V2xJzUcO!Z`x;NzweNo)c)J0te2=n0e)uhd{%scipdv6=zpp|V*fTVvJJ@LDfy&SOyz2e16zW^2*0R05b!4;L2Z_&SsgQQU-`IE%TP7;YA z+0GB)S$ZD-hfPjWiyA^cI}a&sV|6t_QpeS692*!L)uW!c-LSRq$b!BTa#> zRV>IK`4;%!$_FS@hrJCY;h8TnDfC4VAGe2-0(UxH*SX9u3}HSrf+oFVxq z+Y{MiD5*S=wxP-&#vhd*RP}eHAjykjhu~AE^MbtaSC01oR=8?$XMV3)f0cwU*%{@j zg189niv1N{u#QNA)1UlHdAUVT$}h(+LQmt7Dqh6@aefm2MtuIA^cMac)DP&YyuaWI z@^AV<-VD8g-BUbFIk`#tb9qGIi}HFwJT40$xcRrrPv1!|8=SVzA@Ev+ulg_Wj>%Ct ze^9|4JBN7>)XFlsnxt1kOYCw~-2As zNmKh|wD{ZQAf3maa_~Ewj6JPpp5%WJZ@Y{oZRo3{%RI8`j%VRl8WQXY|IVKz56&~W zAv_)R=jMdiWlSq@isB2kM^;&$5Wm1Je-~ZGvrMg&D^>o9BOLf%@ks)UtjI+*-7FRw zic${F09ZE?W|4pt-w$q8`@|^9ndzDTSb41a$tHd)22;jQ8cyF*k>@1+;nnFqF$cZ^ z@IiUnLR5JMY(}c_XN4(XRz#xlkX++gIc;n&zja_C84X&}J<6Ca@f$dzKGLOGF~xEk zyJmibDIn!*;6g(<%f+*7D_KH4<9Cs%)A!^;63oAl(i26(0aS@SstZn*obxL}Y@V6 zK2ZhF?0*!e@fE%@&&pYmndf_Q3gSG*3rmA9vW2v#ktt)_q|6l0%n`t8(h|(5@yz@k z&m?r=M8Q*0dw7|e+l3RzT+<)5Gvo`AJQfB44?@E;Oa!Nr_t5+lnD^o*C?Hl;4|uDz<2Vmoy6lXw0X)i}z}aM4;cWUtE`g66FJMcL z!diS^7QVr}vNw`Is4H;Luds?ZU2c>@CZ46=D$<F}jAb)z+d_CR@dWXy^ z=!MGCBho{RpFAPa0>?Sg1G*oy>U-q`h&{m{SybAmZ^o>dEeUfII$N=+=Bbz|VZ2Bz z+#nr^ECQ}(hB&m8GQ_2Ls`AWThf^k^4ySIm(5oe#c?iD179oiffpw<@i#mI zv`P3^&%y(ogEvitC1pl^Wn$!AV*`Le8iW3*sZ|kfodQ}S4>vYD$p<~qiY&d}LSxww zU~!6;O;<=7Ldko{15nbwEFIW@Kag?80bRgLUeEYiJnMb3Nt?D9X^{PsRRotnEZv}a z2&fHiWuufm2XJW$WuU=L#lczub{HEUR(_1uJb(@UV+NgPyiI9m+9#vR_hnioN&_F# z4&rjn-|JGICaGvfBp$>{DPq?=6i=F)mZp&eD63B9k3S{l+@YG})cLw#k2M8gq{w*D zJkU|X#P>)kdI#MCq{1_D83%fD0m3-XnoZS;4tXcoNcD%dSN&#MP(}yM1P+2)Xe!<# ziRfi`Y%FNXa)-~hYe{oPI4-6jb^{lS?EHVr%$AXCpd)n z;*l~))?2N`5Kn-4>cflRRTwVw4mwHmB9F?4%OcYj&zeO+SHn=~59(t*(4_D!q=yXI zGUf0R@hpQtlY%qk3jGE`iEqIoa>ly~MuZCRm^>7aN#uYgWmK$C0VnMAM}I03($$qn z4RV2xB1-~aK#~djA%hS}*#Kp>o zDh9`!LA7yb2JOjDg2I~JS8bkUyA;`@6Bs`;PvEtPq-kyzW(BKS?IddvXnMg@q8GN_ z&;n?nf8{NiMGzGkF*ARBhm6V?p)y59d{GG~R6y@)WN?i%0$8Osnh~h52UK9)m+Te2 zQW-30rIuz1obKXLE2u6&-b>7}e1;B*JgS<^i5z^Tv|7DsV%FDo^X2;M6S*3GX(^;mM*eQc1PpegemNVTG3 z`b3(Uu>wCxFOAYC8d0lZ&``h*=#Je52|UENTK7XG_zpiTnBS&IG9^D3NPvskI>slh zD87R`u{F|he9XTf7n#-kCisGm&Kr^LpcPp-EE#o_H?KhF5}08mloa_~)`Dk7r#L~{ z8;bA@F)~H;zt-U(v-Cn8XQ}WudDiTp;uG<;lnlSY1O;e5;G-q-$mk!fYXg2XqUOn@ z3&=U4ZRDFGV@@a2o`e9Nb`LK5qV5t6U@qI72lED1Bd7)Mvm5G zen9k*jc07|6S9N05{Dy;U=Th)qd43EEBCjs2^q^LBZx{xS3-U6lLbx$3=JsD*~C~_ zu?4nh6Fw)8EzXg3MK+m_Wxh-=q?5@+Kru8PPz#CB6%2}}hz8V(d^Y4t>jxMhaY{vA z9a@6#=t;limEyZ-#G*W7Lnp&K;z@zWcTSgSba-=ohb+>bM#&FpRa&e#ykWOAVp;-^ zm{DmnEV<)6bSeyqp65NY3s^BwN>5t7AP-OT>R1`^0HA0_UEVC#4ecO0pv8f=EGq*w znbmY|FCKEX0_Q09fJdM?-Vact^-$etF|;T>fJN{s?$Oui73Kzj5xN1PGG*`{oJhl9 z4~jTzPC4zM>p=nU(H!6!Y{)}x2D_|k1bSI7v@Z~86)7;lWq6S25o{IEz_V4tPA(3xORS``Eadu4t#Kdw6)?RID``H1fCzO1i`5oMVQoDWPDgohSC8NTD6Ez zj!fe7z!&&9e1pVecY_!ip8_gEH*pY1Z327nA)}HsLLYRJpcea9#G%kk&uCGev8J>t zj|T|?TIC7ATTY%GIzTXhIn4mO^@y~MgnL5yk44?J%XdqIf6$KF8!;0$A!QH z=LG9NWg-Lzyo)As^EkkZ_azSpzN@Zd(la!7Yu{WP#@zNjUUa3J&wi?r=U$_Pk~T+R^t!XJ;~}Q z>x!1B)<X?yZ>)#}O!F}EVYE(7d3z#s(Fv)89ncZVXki0snXHv-j~t=!29twZQ`NEw4cHyuuqA0~`VglBp+tDWR2$-7eAt4yg|ZAIKoz5uc*2dBj~Jhh-|#kn%O{ra$oFuLo|zXx&XmOkIP|4*v+UNutJlcoe@jbaf$Yv;m{giVRq@?jZa)1Ww+&zDbKD9FWGH6;29Jvf|mShyz+4H0l^oZ z74Mb1Q})}Qg?QqEDIVcj84q#@ zaHv+#%0DKXjEBm5#>z9}$3NA3aVn$IdvUtlN1ho4&GU@V@GN{ee*+xSpX@i{fTsgD zx_KG;LzPGws6S~5p0SLI>3LS>RQiec;vDpZ%NEnO{)nR$hbU^$YF=1`?&Pn?JB&t# zAHlQMFv;&x{#7|%@dJLXp6N{^mp=pjd4?k#{lmFL2FSB(Uvr0YJdiUy2Qb4hyZDe(!fy=xSFMl4o?r{{Wt=WoJl5jZ z*#4AFK$FV9!QXKH5H6JS(xc#P%4w~5_<01TG=j!7Ftd0CZ*(@}= z^B=c|HnKm;kC4AoR@dz-C1zGE z$2vdxJBs+VR)APUF{ffu{pg-($_B}A(tNY=_mJ!GJMtd#%fyFT!>XK0Eng~IL#_lD zUMm=2T&-7exf3$K^3U)U-FxM*Ndah{O#IunFOz%`-m5ZfS^?vJldx#pmnkU@b0@eT z;V<}(89f)VZA6)bV|UUQeQA^fbV&af1okKQX+Dm_eYrslo!d*L6KzxDT3D&JRq zkG|F{0?|rT>hhs2@3jKB{=IU#E$@N9ZC|GHfg$}g*Bs(kT14}3T5a`J@1dLWl#1u7 zSVc1_UL?0kWE$iJ+V*9(VCVnTFEX-DPNd!H|FrGPti^|R2Zj4=S)xEDBFJyS(>gD$ zZC_>$uJEskLluz955Wd#MS=VYt^cJ=ehG6A#PKelr1@t#)ml@~wlA{=k4_$yZ&Th> z^C12^=E6|1ZTm7cH};?ZAt$N40mg~>#M(c2$8GyE+xBHLe@NDvS!L%_l1t}V^V7~V zVznL?)XhgL2Gbf*X*jJPr=rV!YxiC-7t0K&=0qjm)W*2Q#*ay_GPy1%Vds>bw%v9Xxo>mD3qwPZC@td68XlqeVJ|hGTZiLw(ZMg z-$C2HOs(e)_HnlD%WT`1>2@c!?aOT2mszvl)$OBg+n1?%nzntJ%+Iv#%VegwZC~d9 z6ZUNv5 z|8#h)Ck&@e{5fq$?LACePQz=P8+#4WQ+RD|WAE`3o68eb5?<8XIJ8`vtmoh+@(zuC z#*dp^9yfVXtf$)Q+bh=79ZjNxDRc~mw&k)7l4Gwp^p0alIP8Tr$Q*}~w|{h(;-rIOje?f_#yWE)Riuf0swr8@=wPGfWQwzb zV)e+TpyhhdZ!mW4TGi#j!Ps>{w4PeV9^K#?jy)IM-TuEa_DnI+%rfhuuKY>llko&6 z&7=$Y1RDuTNls(($6k9i>R0d7brcTS!$V%nj^UQ8UR(QBd;P0phk#lClWoS?+Wo_8 z06x0In{CD(@X2RvR|H~MN7F-C0;IR%FyGu z+*eCmIPO9uFrfey!u{-ar{mv%-nn?WnCFa44pzjV zad26wIag}Nrj+9OVpFo{(>qd(UnSq^cs;$-ai8AlcrCrt@$a49X)Z$#tk)X&yqJ$y zQk4=Xo+X;uc%3M*E2Sx!DPxl3&E;gyAGb!aMtUbadUKt6y$NMqTU3Ix!K%0L>3frJ zTZR}LL`dvt0~X`l zoUkMpV3m@f^H?Guy~;=Es45MMGbM;e9&S#>MR{_ zM2v$8!dLEMd-dv0QgJ_`SK)~(G0sy8D?3+2b(~puKcZI=ev=V?>zoMaX4pwJo*t5h@I0> zNl4(x2E-!G%#W~6q_bk_P>=8u31euQ0ctX9Y| z>4?!dSud=ViaLwhEz=4mrdV5YZrLA)jdA^h^F1la^}WluP%Um7LY`WW3P8dL36@CuYKT zIPhv`Ul$KJw>v_^$d*9Dmx)Ny%NO1!EZIR-H{p*q9R6$hDLrTE2K2qvA1<|!g z$rRRzlon1Yq*^#4S7=~AkV9lGQj!Ib)e5ILjrFaOBB6^$`_-J{>}OQXLrNg^^+*XE z=l~dr59ZP$g(FwLno|OG8?;a(KFMUdb@55%TN9taO%GaW>8!P1HS865^U-L(5-EWl zh^l!=3G}azlzL9VzFVXulG!yOrG-;!kU}o1fwL41r?7puffJ})dENNFgH8!t{b;mn zK*}0)in=8)Rp&njCNrRrI0XY?k&;=HP6=#n&_WGTIGd?~9GyW53HydjSYC@1XQ+c# zTH}wFvbv9?Cchpj&bN=Md7KhhJs&9%r${rmWMmur=VR3bOaELfw95X+5tHcSZ<47E zoc##Cfrl0kEGJ0b>OV)Y#;_<&QK$l=qC7?62*M%=$jO&=AL0iyNm5_LhoQmm5Ppao zm~gJ3QkOxGq?;1elv7y5ht;f7P=rlMR1-3;h#VuXejN}=;%WU?GoU2?oBpdU!huUx zgTbIJh?lGeXJHW^Rs*D<2;Y?4a9vy2&}3vsg8^~fO-%pQ_J|wCF3|cSYK2Z&hzWK@ z;q(VyS+$4{!+@(=RG=vE_@W}hEsn^oYo1Ivr@hw~@nN?9Ly}^g7++Tl8gd0heG#D6 z(=nOnL;iDP^h7-}Hlt$SVL~8qlOryz6^a%?s1=ac{!Vx)z8PtC@ zA%_@i5NLmwyucSF&)N_ zA>G+=7t?>WAHo^zK&~r-GdPf-t_YfQmUDd(ZAEnVu&Ro}(RZV$laBE7@8o2&kh28< zTKM)JX%K3)!0_$eHx8evgW)^)Ixd^g0>gK33|tzbrG@WcMO|2_7F;k8&1QwU^&sud z?#z%cle(SZKgP-yX(H)t0H>c1d&GoH^ znDQ_liG}kYnBZiT(V4t;DV3`5x8zTviBzve(I;RqqQd}(G#cd~_!UO>{0h*Zw_*B# zs~Q3t)Mk)^Bt_69u_FI(HPIC4x`@QVEX8+AG;v)hXVEpBAXuey(f#b~f!cg!+}V^iDB`}P_# zXop_AwVcMhN07(u$tg9QozH{_nOL2E&j||rPy4o6y`vMdv`MaY7brWfH!z3q=b4G^Fzvd1iTIMWl?#|L~K4#9@`J3l&oyg4%A3C_3 z(^|Qo#qAVlH!LTU>)3hDqtedwpnc9u)oIzBFHReRkrn?slL;twnz_y{(VbnoX_tFW zIFWEm~x&whBaw0cpgomf^>i!kZs3(2sPK{;9I;&qe1(sXRf>UI5 zhZ5&5=@t&%Lg7xY&}l)OViBD78=P)NzDFZ-rwDUj0?_O9VBIOe4P-#2+aEZMA-ugK zIQ_Nlv|z~02^G4{fZJQY*=fC;`N(NYA>W3lA8M5^c-HM*nk_^I+-(qWOu#?x^eZ?I zn&@sk=-}Os!dde=wMC~qg{ReVQgZ-TaQddJ5B)jSMW;P;CLQvj`##|V+SNTt;%%KP zAKg|FoCc%QgPnZA15R#rx}CdKL}&5yeQ*kKAb0MxY4IDoyrTYt(~re(U`nS(bIXNp zD{;46z>n}H`Z^e=liTn#X%@vedDkhBI*kTNL3p?oq}9`p1AGR0#eL^)1(QDbTK;#Z zj0ESCNp5s5Cw+$a1T#978HpDjC1;${$2nW<*wMND*lXI4bK-uUH=qtV)lC(WNu4wi z+*(n0dWq9Mx`%=L8fb$yb$*33p#G&Kxc?zI|B1i3nLy>-Y)OCUF~-ELn$V0Gj8iTV zNHi1>b6$l`a8xZ$_2DFEos_8)-*g9pPQBE7)f4@>eIn3oL;~FXD()Pq&hA>xAxzke+k*CDIDtGahbx@$5;3(@3Ep z*oE3U5yb7%3h(s*R^6kaI~Ksdvn?uNsQj%vK6E!I9;mBVk!0yUEoi3Gym^L8Jbg+l zI)|NgS;8mZnm?{QFRctrU=6g0i?}(2`qImw9bQ38^9~%tdpZ$Ow@7e1P$*xbK7$JQ zS#ky5ScD>)10S>s=o-=myNkBrEwam`57Y?o4gKgxb~n16NqUQWPbd)$bn^>iui+Qn zMC9}nU65=D0s5nx$inyFR@Pj2)*k=>$Lzp& zG*AgrsMG3o<_r1;4&h(jB?`~FQ_aCRqjR?_1sDfoXf{zxCmjMmcbvcl+y+Q3956*hP&Vxn;_RcZj$i|BV(*U?g&jSy$3D|-2FCK3T`W-J{|Bs>EArKKzeoQ2ltwB zW-cayn;=!fk-ECgSJ*`NB6YeKS>BoMpcC#mVG|$7-4P~U1A3i2NC{ZbO_=C8-Rc4k zz^cQZ&J=?dK(4z4Ik%o(xH}hpB`v9YL>LwVkI%(GxrvXjv^Yl6z~MVs6mELsJ~Z8& z#?nUht{c$Y>14t*S`XdA4UgO#qg#@(UHD8oK~>U;9P9cwI7T)}a?4GojEp;-kOJ0SrJCzQu=n<0=*?4Ot+_SL!9Iqo<~#BJ~o;gRfJXU zU32~EMj39uorQ3~>Gq$OqhQMQck7AW!G&(q(Ov4=0jsa&wF*}8A9jFJ zZf=7{g1~7hAe5$q*PUMg7DY?+jc#G)zIUvQZqr3+qvNp>u!U?gcVf!t*1eC&j?M?v zU5dK76rRxCi;M?J7w?Ac;jKswaHAhLBD&hTm5;j}fJe8qX@xa03up66Ds=O+1}lO?-GaVlFfp z9t9PVdAL_677H5PCIxJ8Ci(#j3sW(6WE?E%-cM?XdyybQ&1rJ^51iHo zZ|e*`@vb}+6b>?u24L_|1lf~C6Q|&v!!yuCUbQ3yf0> zt}clZnYbM#kYDad1wVm}M_Y3PmT&_Mw4s~Lbodx$x(@+AgfS{om2QO=aFleRVoUrX zT9p+c?8R%+onFYAI9Q=0ovI-nxy$&}65OnNKV^lHVqh(!Gid@TbT>%jrYKegE3zmw zDh>k$;FC3%TJbH;!5-*7FWoi9jaRxg6b^C`C~^wc&`^qH%DOm{wsh+xH(- z85+zwjrls@oPykGs19`>zBil{ivT3?a3n@kK z;!n~be4v});b?S`Y^D59d?b34oS~0^o9PAQg&Qf+@yasjesA#-c2zfQNq^ECltVss z!#MCt&kHGxE4X0`ZvZ`^`(n`q@@1iB5cBI4d+s2WumGv`4Y$Q zg>EM1wmdX~%T~}fa>MBH6_H+PcKHZs8@K{|!6)KzAO|YOA_)RJvL{$BXd|_Ug=7G_ z>00;mDMH2)!!y`UFrk>n!)=Bk26T%(-iF|V zC$NI|pjn&;K z#|ULn%Jd>l$6YO;8qX9jM)%$+9&gfZwDN;!m3ACIQjb23SaOy z@`$DYVt%7h`7O0Wq^bA>U5u?qCgp?5Y7uuK9HL!% zvQKcSVhg||g3%4=x(i(2;;+ef(KElL70Ag#P1#o>IN%ZM>ThkG0KM=cIA?5lqw<1X zBtUi)X_5WK&chdw1gol>b9Hl-ZkI>8pn%K#(3(8hxHuUq;set>H^S?0SqrC?z%65N zHcVa-6oGU250!wPfNL1J>@;%ZA}Dfd0#y1LNC6kjGYE(Ue}Y+^e8YGdyCPTJO^xzZ zeht2X|M?QVjE@CfP_yo4HE>0wiM7M;!4AnfDK6Dbr|u>h)s@2zM9O>%CdG2#O;S#M zq(aYVVLVB22de|4?5}JZwu6!JTaqf?0UsJ8K*MFw9;>QYMfo22nE3G6E9%L%ASJrn z5$p{$_baj7PV*k<*0o=n8C11%C?OMgnM! ze&n6$He%g1OdHBdN!J4_fH;_F8jMNN3y)YMFpRxt^fIk*o%lpwpdg!oR&5i(U^UZN zT-`p-;AHE80c6PvLP6Fp0fFX3@NJ+x-aWcNGAnVEf>3tKjX_Zsawg3s*FDbY7!vpf z3*eZz!r4n{L{S_&%&57y772qxu)*RGyiLh0@aS$pc_{EHR3IM#nZc&GRUVu3deCHe zYCsM))AHHCt8fWM=vBUg&OcGCt=qSSQvUOPr64L4<5?nzW}o|TQkp2n)w>f58VqTXc6#SZ_rnIMbLtL^dOXkH{^Yw z8DLe}G5&*(@If@EK)Z|ynnM~HB;EHR4wa8dR-RgtQoJU(1}V}YktZY#{f`jhb0{yV z8z{&(@fF{|bofHjf{aTuv5Mx=f{dGdCp}6g@m-|Bcmh6j47y3Y4?UqV_=hT3Am!m8 zBmX03fc9Z*^r@L5-DTViOKCPqTAr{Ax?rstyu3rK2vk99NgjD88WM-WiOe+fAA^Dy zsSL6ePm+5SKjO=fXN2eVlYgOaiE4P6Xfte;FeTZbL^z@ezLyt+A1BSPh!La0@;M%L ze>k3>JWNJ~7erlT$oUTN2w%%5FF5+ZS-`F=7Y-r`&FENh^f ztCe+v7IY^3;Vs}LD5oV^<@OhVAydK|bRvDpqT!7|QgIYojJ@wH88<6uetLC;k;2Xes~%GXTJEA}1)Q z{2$sgMcl}k0Y$ZtUZGU6r09=tAuj@06nSHrc#HQB8Z|pWJMvzg=gardS0k3zMOTAo zC@G5%6DfL7o*8dgR7cA2K=2~bT+a7kl(?2;_)xmwfX2Wtb^>_O#_%LiIN6Y20DRD2 zT1-63aPjnIcO2IdsZ&w;SY?=qkEEyJO=T010{X+VgH3{b1o5PvapfVZ{ z9ukI?cV*np6DGF`l#1w~6XkFZ!U#=39CH9XNmGJG7*ZCFoGQ2wF_ae(uaXURkmEZE zX8462dRTfeMNJon!zJ=(;Z2m2vI^Ke=M!^hI^&bZkp;qrf)VrpARrLn899e~Dns`8 z1^>zCaavfJHO331_;TPz_h2B6@H;#Kk0`r>F3L7LFI{&KYyE^W50V-A*&J{{yVyy% z0?h)Y&|>0xXdwF!Zuq5R4~TM|?IEs1X%TDUmy%_Z76gCn^x?On_`p*Ii$EyO!>0s` z;!D|K`~hWH=v^Lpa63ENgi;uU-qAa1#c`9kR?-GPNM>dC71=@q#VqnL@i&1y$b@5i zc%Ohwk`mc)`2={aj8_qtvRmj>y#uN8E5R9K1AjbGQAm>qg>Hh&6{m`yp#xeKjL64T zJcJ#Wc%hnr0ys584VER3Pz_G!4LFe^!-tSHQamfzfDwPfd!4theSr6L%$*hTq4co6J4 zjt1Sp1)acxRv#f9$g1+ivhIq6i9jX8aG_#KWssFO!bj%|kjb~wA+oCSh@~mP6wN9p zj#n+z%f)2GbStkMPC)NS78#9X9_2(`w7l$zBm#N?j63{KP)mXvWO zG7y*IJCGgV3p7oDU5#CILQv#YposK@Nm87}fEgKn8+gRq2P1eiU=H5n3BHj!MUyH& zCQ2fAU=EEUiRP`=$YAa1Q5KO0%D}hA4zE`z*VvokC3LvP;`p2SQ}`2477F19OV>ya zp}lM^qtTe)Yvt|a+iHf2@4*C_AfTm;aS}Pgnc@lYBr|5jiY~I_U-7#0JQ;=jFtj|B zCC)p!_BzU4EmpqVhq>HSj$wg-_!0 zj%YNsMxI&{$d~XEkYU}??ed*zR7l6V#7cP%}pCRuGTCe~i|_iMNWz zLIKXLtJOdVE09nV zYP%U3C>GF<5#j-9wHmV$w8E@Em?7^(yYQH3fIdKw;4}PPu&1#TdBCO06G@*s4G9+T zTM>D|tT;epKs!V&L{BB~sku&soPXtWfGdYH>2clzk31$M zOnMkeRpcQ{rS&6d5EvOD7Yy(#`7HPgY@j`W8A{+WV^d{S;k;l>XgOuw@xwKG@E~sH zZ)JF;^?0vXUY<4M0WRP~f)w*hze{+v?gTpsjWoY3e_WXjtvJ!Ft>z$+BHl=I!B>1i z|BP3@1oWX)>!?N6UVeet6RuHQt2jzFQYez&0uB{r(65_Ep%z$^-KKBs zo}3%nmj5MB1}@-VIDlT9H^X?;HzT1OPUAg<14AxaMZYi#=u33uEV5(`4Fjf7M&QTc zE8q*_GtCZ@BZ4wyWZvDJJ18wm>Ju#dO~3*k`W%6LDx z#_KK)z_s*Y)Db(18r7Py8_2a39yB5Phpr4=z$s^TG9=C`jxRBx zMOL%~85XcLOyDkRK*w@B0RU^;5zWJDFDDCwH!%PJDF~k!%4hdm3gcqzj92M~QMENz z){-Lz_2GklcZPVtQp7SUvxK4kk*rQmgGgA;b~&6Spu=H~;n0ra2xG>bWmH9TADmR` zV9Yz&d@xPHWKrsJlPa7noFxZ@qF50p?o8nHWbcAZFrjLX@lQjWh9yErqI)=!JU=o0Y!yRM# zsAM3qNXMAnUN~73NajqYk(D(eW!77q%r#DOl7I>5WZ{cKtxP1D^#DqI$7I5Of5Eu$ z@#K;SY;;@>-fnCW9E=sWTL!^CR&K1CL*iRaJF26QgTfF4-r`=_|<|!KKiP$ zPlZFwWii#9e>x&d6Y?Xn{D>^=up8}C4k0u%?a|8bf(e0^!$<60PQuHFiK2qlfTf0V zg(1N;+8q01=6l&o_{=QL6>R~{4eePP5cZGk58Y3HXrAcPvj0)k@NS?}pbF{i3YsaF zUm5|gomjm`x;D}pO_rTnX@Y1pXfepU!blLEKbEHG);z88vTLt?7^_Yo@lI)=+|O@m z!pux3H#k3H^fvlsoakRz8oW#11kDc}5HFTATQpkvGtpsb(>(l$EVKUD60#_?>nb?o zV@1saR&ID&cxAy;>uuH*TMUcPSTc05%L5ZdsIAEG(ZNktP0spAViW zo7M0yJR0&E`4L$)5XG!4=L@cleZY*!ho{|U6wyb1h%67J|VVtmb?KR0CryoBe zOMY&hSMVdU#Mfchc>Q#SgRK+r26X{iluuBnAiy2@lca6nuk<6b*nI#}enb{tCHbrI zz-i~pkI0hq&FtDjNs?od{eDE2PK>i>jh!J^r}nt~h%9+U{fI0)`&$bkfW-F`%t&T{J1xF3<_M`ZaCS!{T4w%Cuz z!sEcsj5*K4zM4(~`w>}wL>4@Tx-qJ2A#-}n((engfZk>y8Z`4L%uM3x_s^<$IhL_RrLeXc!q_5EP^uHKrj`7bxpFCe@JXo=MkMXK1xT zpq$##RJ?VHT=lH28>^@IW9|uYQBhGlK^20W+EIft^|j;b(wTZS+9jkk{rZNwOloZD zgv|~Zvel6GLxLq0I|n-SJz}e&3kJWuL+=Co4mhy=euuqyc>Cu!e}1G%0 zg>;HKISXlrM3vTdE|^J$%d4}&sxXymBal_`Lgr)r ztdd@oxaGg1c&frczI@fOToFl!KEc-lGRxzN!E=Y`6*6X*3wK3&8uYhV%l{HGW@yIh zrm;Y=d|TlZ4X)V_nUFUx6b$A$v&cgsSN}8*g~m&1nsSWJ^9Bl5sWmqg9K!nF0_RMP z_FoKzJTBPG-4wp8puVs9(9QYKrx-!ZvO81jGnV46ggP}QZ_L6wH*OlPkt;r1$TboL zF^ZdxYZz3pVE&81XYj>=iVZ%SUlh}7qA*r^6M?TW>6s`X>rDv0!bop>c9hxt5iP(V zB{vKmRgE4&VbRjE-kDJ&MmAWV`_!OPoLDQ3BG6DbP6|c0+A+0tgHJ@dful*M`0wDu zQuU+N;M5pde^sjpdTQzZht*t02+2YBP z%^KAf45nJ>vN6tU@YcbMJkLowYC)doBpYQWrt-A&{Olo{sc-F;BQ&? z%*mtDnO=3NiFDWquK#L?-`wog`SYtU{O84g*|1^5aZfdCG5Jr;PCs+MCw6cD-TbY( zE#0l{sTb~g%(Qi(%CE28es1uo=;dp+*fw>;>0Oo`FlNR3pT^Jl$LTMCuamtDJI{MKh&_u_~y&+jqg&OMtf>2zI}X@f3b-C<_%smVLe z`qP5JKWtIm`9J4fu;p3PzB=^mDXG2A|LUx*Bf%kOeSOpH!ChXhJ3IKNr&?dWVCA|l z@6T@kK;qk-F5i5s?rpY-1kZoxtgULh?vg$Jt|8l^)+P4pwb-^(X}Xwgo4|T6%8)R_$jD-p zDKsG*iuF0T; zEKJsCSIw*6tn!j&ecLXX*{US@e&y*G>yKu`95_M!xx<1zkk~^ zGPkyyaQEZ$o6b%>TGMNBtEn3X-ny{Gf(a#oP-Uk}%0Bo{mqVJAta^TG?=MOMUv_=; z<7u6@t`1-P=aGF=XI%c$RtsBZ$Dg_A-0RLCJh;_fyKOaY&}Nml)Ia*~ZA*rn|H$I? zk8eA&*KONAFz+vAf9_Uu&Zkf9T(|!6`CazBY3kH%j(V;1fg|4B;pFgX$9Ig6eRZE} zZwNjARQ;4TGd>7h(f`VqZoKP}^`{?O_rQOG$6Z)eI;&wuv_tbb*LQ#Cfx0$Z-QMkq zzoYu9C;wTG^$o)WzFjHV@hKdL{gb>P3BwC-eK2Ed#wNW(#4;B^-7Z|9TM9=KcnoSGY&W>diUVg|LVK? zy#9-qwr+M}&oOhFm3}bw=G~vU;Mf;hv>R~l-A&d{>2ScM=TBMv?E{r9doS4Kpg|Km z>=gOG`fYFR*X{gd#U3wq=(RO|Syy#oWj-t*8SJ3hBf;M1MI>C|q?g_8#^9cE?iXwkW3-ML@Q7(4Bp|8CZ4 z>8_L3JUe+tZHxBbhmKso;^JrP)*t-YkW=>W``Yc_9DdIadrZ0Tk_%s$xl8N8V}jRQ zwCs_8_k3`d#Pzpb@#XHHANqQmCa2Ea<@4IZ#xy;-<>+Szq+8s76-UW z+kDESS3WxE@N{asSQ=#j2Id%IZ=9WBC`S+>)zBq5?vhLl^Jg(UhU*Ec- zNw?!m?|$#%-KQL|y2)vQmS>+ea>pI6{CoeujM#hW-v$M@d~wTuT|(`fj(&1d<#uz< z+~fEi7VohAlzm<<9dOnc?}Z=PqUOwRPn@;(jG;I0_4t-&ZFOelj$8brd&5p`p5Fcb z9=q=}{^>2Z?%Dc^VQqKq(<8iNsCBR9ZTej?=+@ou=sBm&xCOHogg%Vc?euZ?Rjn_( z;@A%lT(j#xD-XK!f>t*-U(x;5J-+PK=H&?uTU)ZvE@6^{bbBy!X6)r#{!=fp$x_JLHt-KJ34ybKR~JBkN1nH+yvI zcUN`pGNZC#zd6ln`$o^{_uOBb4UP@^>hJ-B=C(ihs>}L5-?wMqOZ)EKcfY<>x3#{l z?``w??t9hTtEL~Y-JsWIE*~=Rgy_`+Z|rx>VQ=>tJLA~fzN*}E)F;t?*T)Z>eodd@ z6?ffI{_?_C_I&xB{SG+bhW4klA9#54{mPEKchDg-Pwl$?{x$o~-Qw+Y_w0SsfNK{G zeP#TjfvrI*WdNcT?>aCIOLuo2j4yEh}i=#xidSX`OIgI7(VmJznNv!f49oBik6r4OC((50itJs7;E=|xXHpS>r$N7dU^lV6X${M8%H zN6vb+=H*rY{^91xfM*Bxx_R&|^<6%?ec^~>=G5F*bIq!w-`?l#^Cmqq{gQW@%}Y)i zzINfqUtY3k_m9dijo(|nbjC^R)*e22x9^92Kk=JRYiF(4p=HZfXSeFTZLHO_(27X) zuHWpMZFOYJ>A_FSt6HsZdBMf|Up%1Iyz0^Sf4^kOr~7|()XDSjf8wOS+`n|fV<(;W z`OO#H*5}MVS3I}b)bnnS4j#X#VNvO#*rL-SZ60j@;QH*B*>x}8v3%9?iZ8ZXzH<3q z9XcF%Wru5qM~`~^zE|)6a9H!BUmt$ReO1HOAN|stJ7>>L9X99WhZjEZ&X~zBZ87_# z`*(Wt)y4leyT{mL#`c(V!`M#GPJQN-i(6baYmd|4y0qq_+G}fv)~=h-WOT)cXWz2* zVLKl?FnYsZ!Y8eK`l}1x{q&emA6z;9&3DJYeBxH)+t-aeX~KU;ez2ha?voy0_VkIR zCmsFatDmlYX36VEz4!P(*I&BLqIS<`9_@F-3qyy-hu$&ztkJV>xNi8JZ|(E&TdS^L z69)uMCnJvZC)qRvMje8|MPla^21bIo^ey|D0r7e>!|RGWPwK zzIa4`qLM_yywfw?~nN8*>!LIXWi0w-#m2D={IfoZ^^eMqn>=_ zj2)&-pR!GpflX6gXLN1Xb&tQd`uo;@f9>y=RrIe2SA5d#n{LNce00wA(l+PxIQyRM z9(yVD-s;4-X+NC!@am?^MzmS6(@8tMw$s4YV_V;L#es*995Uh6xihz%`qI7^?HliL z*2n>C=dL&}bWP~N^l3L#O#gG_w(Ro{4m)r0`ukU&^x`8&opj0v?>xKZ)3Yv}f79_- z2L_JpH{`Hs(}!LA_1^onzoYXVr+o7C9oyV~?10z%HE$RHTjiiX4?g*}ZToLCea>k| z|EuBJkN>%-_NDu#9UlDP{r>Z=TN1hKUxRM^=Z)hMS6_AgRdeHCZWF(L_zQdV{@~WJ zegFGW(>1Zx$A13(*Smao?39OZ{=e0o-|jT8^Y=Z@9skVSi;tZ#>%KvY_bqL9#6|B^ zpPQ+9>ATB!xp8)<(d|3!_idL;2Msu3Vd(yI6a7w&y%Bsk@bKWO1NOV7;a^`(n|IpD z^P?4W`<=TWyWsjSqRaOF_QIuof0+8+Nly$s?XVx7OO9T(p4d6NxI-)hl%n?=p%92h?4p)rU5>;0GidCD~}UsHYH z`gseUnECjMn{MmVZ}>I4b{PKnrymcTap-F;?rPTWqS~@`M?W#H=@S(X-IBg-(esxa zF#qb+{W@O#=Cwy&bJ~6%Tz$+pOO}0f=S}AwdE2Z{hOL}C>4jEnT77=~@X?dsShe5K zva$}R&+T}^G3lm{-E!;uH+}iw>&M;QaPQK&Q|5GiV#Z_pKXB*7zb<*Ee7E^Swz;eJ z_03=1eetdHNB!sa9d1~E!((&49y@CB3HQxz`0qz!Zy7s&&UrPpch8=A&(f~%TrvNH zB@3Uvapn9+HVk^WU+vJ3@16D7KOcK+-P5a*!#_H5=)*%#|Ll`V4;_2#yN}g=^TgG-_wt7^HH~jH;r0Xd+5Z!e_Qe29{;-N*^ZAM zcf#CHCq4T4g)dzF*ZGfqciKZw&Hn4>cfR%7^3i*~ar8m^j+?$>&B}u&AH3q5^}9{m z?b_M_Pb`^uz_QnGd}~qd_;;7RJMpCWx2tYAW&OG>J}Wz|%cRORl`B*KY4d%r*{2WN zaMI0h&Hnc54{m;T==7%#essfC>(;(@(*uXjU)$l7XC@rEe(8#dZ*TK#i+kqnJ@1WU z2b{9xi@s}4xNp|iWna#Z&6_i6@`tta=g(R@aOGzc+f0tEo%899s~&rMLt?|C*p89a zD<&`e_U}8i+iTpRtJZ(`!yVt(|NEnl&Z>W+@`)jnXaDf#ngic|>#P~mX6(K7(C3es z)-*Vw$D&1>O?Yg#lAhs`7e}xDE;#nZ4!O* z+{C$CPH*yF;O^%4g^zD{>3g4_Rx+-x)jgFJ&5oKo^{-8KoZD>A!w%VgyLpk4GydJ= z$)O*={PKItXUv$@Y1vU5I+pw`yTgW^|NQ3Dhpve2d*<9>O)t9Ot7q5j^}zo1do^xM z7rIv*`*jmK@u=EiIhq}h6REK6hF|Uf_ytZp%Z7KH0QP)+L(MnTd_&DQ)OH(@Ok(hVDb z6ZVg_32Px#V=HQ%+bCX9e_Q>Je-eW{GO!32-TBTG--xua{o}g+ab5qou76zDKd$Q^*Y%I<`p0$s{CH{5^LJd5PzzoHHC)lRk82vs?;^9hEcUO1FKHRq#FXA%PD(L zWnZhpFO_GhImy3r>caAHl8<3NsTy*U%Dx%uVWm_N%T%DvP;polh^cUE+EuxjipIIh zz*MqTEd)~`I9X2qbL#O0sZB`b;CPa1hLQ5HUBNg+@d#zJic>!`8?7=GlDUbxY)Lbo z@_3TEhAC5-RZFU#RgaZgh$^kgk17|FYo2Ous<=!gWh&)nOg~g{jrkR={fgFpMeFj2 zY7khJzD?zERgk8hwK0#G{ zGp2Gkd<@Q2t;*ifFfdK27BU&W9I2-lxp;>8~m8{Q$=sTqIJ-$ zmsL%fqoU*epA7k8rJ09W6k`E)-g^{m0Q>ulVlD9pKv^mHsDvZhN7XpXfy)$ zM51Z%Cjs?}##z>h588 z#nB@$cl?Uh2{a&NJlr}$^=e>)&gECMu9gl2-@sbNh7ZR5iq?|Tensmrri7|g@+Xdm zBrboCd^z}Gz!XZ6p{oQVA$&tdxrrsXP5zh=_D4dsKBLr!r$)ToFIg06>U4lxC`d6? zNKQ(>=5rQ3sG6%(Bpyp+_au;L;A55;FWMKM%R`7#Tw3Z9 zJ&v-avI)XfeD6R2s|{ONcZJZ~!qSAXw^W%;^M`p(AEB>b*Y#Xn@a=333uNoI(}hbE-Yp=g!SBdcQk#fsA!&=yUHv$7KD zm3|C#%YlXQxkbaVu(4>=KdHH)bXJ01m=pfFEP&z}`i{p#yh-n3W-M3&Ei_14-lPuH zvoeG;1FE#d<3WDM&|BrBV`j9+=o#x*R&1Q#dWLzj9?S~84*OFp zAnh2>6R=0W!6PTiO9NT(1EY13Y7h2^=j7pS@n9txMpSh2o{EF6o zMQgvJwO`TNuV@V~jlel^cCg!n&X_@ZQ{!8^IDSQI$uZ~DwB+hoH5?%UT|)U#U=`m4E&U&YEQh|?b3iZ0qQLse$Vei_Jcx!Z z%BzO<*mn~pL(@U*Bz=oPfcDR+1!J*P<0CA`E11Ht38HY1f<{XoIgI0kCJZgN{Fiez24H7VRI) zPrC#36T@;qcaP9sj;zp(<;nCbTFcUccVZRAk)znztWXB~1g?&x!19wEVKq~Ddf8Xt zPGg7bBm`?-9Gl<;wD_Z*%t^JDDeno4)qP1Vq+OKHsSG4vk zTKg5P{fgFpMQiPm=`^Cwr{jOaWWm5(Ow~*!I0VA zv-Ul)`?%KmGq%j=FRfih9SYG-uCDP&hA?aBPiJT3)x}YOch{UC4YMQ4IbZA={0tF{ zH%y%r8-<*4#VS+;0H;m(A4`*K?Ok#lP8*5r-mwR3-l(0y{11Du1y@eHcKMvoKlZoL ziq?$Gn-T1{tryMo>}ZLCf}vQ)R%20l$KSeMH1I8$-A_l~gzSQZx#%0KO+@j6Nzjp32NcH0GZR7PzsBk!;r2Z8v|uK?4|JnyB5ryAeUUR z%kr=jW0b4RSRS^6jEZWeVZ|3fQ*k_GYzzhl1&RyomWP~>r~j$IZh6QlbTB;7SoBW5 ztuT(q%*BGjFR2@6P1pgARW~l&XpoYbvU8)Ih~hV<7s{*}s1}yxh9l%by~!%X;uk7d z;oqaUMA&8nx#ALJNVO*ayA+o&ptjIuBjrpjz&i_GKr{|(C?Fb}Hxv_%nW6QH!g8i~ zwar9}l4zRAJMVJ9@25rmgQZ0c=lR<&<1r3HQ^`q-$3zXrMzyer-y>Hl<&i z(yvYF*QWGqQ~I?j{o0g%ZA!m3rC*!UuTA+!sZGfVzTeq>5Xoh$IV@E_8jn=OcG{Hv zF75;K`o?T~%Y##7+nbTOjJmLI+yB9A`$(a~*v~WT=b82M%=&p|{XDaNo>@Q7teTm)yim5=m`}I}ccG$zb6moVJ+f*`!A6em0cAXP|$A41ah>l2j3%wup|Bxb7o7 z#nTp5#$4BZ9Q^XMnZq)!`v}zW+(+P-r;W_Io;FfIxY|O&P|`i$P>@q-p8Gf*=V>D{ z$K4hRCEa5Qg-9&oxi8{@b0`#HX^Y_!3UO}E+vb5mNV%6h_Yp(qZS$;UNSQ7?_i>ia z)0QYiYX-`}zfdCK#sh)C(-sbT#}W>fyVpDv4uNGZ7=%f?;b{v;JnI$?C%yfVNW}9j z!GWH(M%FxRPHz?AUx>&sZ+|flZH0(3@{W(Qk)HP^RD#&`4v5;*Msh!QTO=54q%G!o zFQI0hcR;G1c_>8C^A3(~dfGTt=WT0beC1&eoQc=-;AAA$$UMlj;&}&gbFQ{L3rTCd{UtoO5mjOV_dD==d&Wm7 zr{^66Lwd#sTXV0uLLc3@LDGYWXMH)C?0E-qrJnVTB^r4LLexDE0!uw(AtbhuHV>a9 zDUyyw6|WHqhdg5;h}5&bafsUUEK$>*wy1};$h+X-Lj>@8*Oyb$p81lZ%llq{<#{gy z^tL6vbMx{#!i^gli&v&Z6G4v*rOhL^q6t#%y2pYd<{1m(%>%zgIN`yo1jo-k{Y49v zG}e^xUE-v@?=`2#i`SfJUl06J@lk zm28CPBuNMDf=fAoaKn$bgd2Y4K@yO7o(+3&Tp0@7Fs03-bCKaB;a*FkxY6p1=}nOZ z-u{T@_S{DTM{ir)4SVV~c;p*x9=(LZ4IbE2g~0Q@*?5$@9F84J+! z(3ZrO_gSxPkO-o*dfwrs^91?$-Eg7Jqu(PMJu)-_9eLgnO?vc*1gf4J&Y*$E?xxLS znTI3X(Jztr;QbMwPAI;|tFp?v%*g>0G|76GmJJ(}$qsH98^EhUqL{E93Rd5cs%tPl$q?!m zNsUU|wd*mkXDM#QV8A&8V};{YrbcAcZK^I)(-43J&9Dn11+hBqMpI>p8dYRRvRe+z zRF6tk)J{;FGvi!fo-wdZZ@ubj$DOEqSTWNuBvn-(C>zXs2dQpThUVfJHqpi}9jGw^ zP4~_~jag$_@&hG~!yZI_pz{6{4wUpzcEe8&)PjX|1PQzndVJ zq-DM(Baii|Fow>wIjfY5e3Yqa202Itu5i&gC^sa~SxlC!K=DI>XwQJ~qGGUaahB5) z=GeV!pmR7&qAWx=p|~c+7S<=JZNnrQBJB}LiefQ72g4|}>b7g8I<&^kRoBY?2X*b2 zt6C=6X9hPQH8v9{>t2`2Htyl$OOEsxGe%g;FMc zm@Br@%-sTVKJ2iuPlaGK0J2vmQQlD;9I_0A6TyzLa8jyuAe2ZJv`2I8McoL9&T0(J zhe8%3tOXk9HpOO&#Rw$D(P#!x&TT(g=S-~CfYq;aMm#pUu~j|D)NdkQTe+q&8i?3N zYeowt_cz(Z&@yq%u+gC;bmUGZCLDh|S;vUKB}11Y)5-X#+?7>W4X6&}4O5*Vo3sdPqHIbADnmlL9LS}t$>c*wXftKbC%AIZcAq^LGc*smg z_Aa8#sqV(;$iGd_urwpZ;l=&Ril>a?%*3ioWuCYDVYIlM$))DkIrHnBDR;eJ=ZuY2 z@;o$_XF<6cln_?g7?cFhRC?53lRsTq>#aNu{5GB%zs{LzxMA!VzLh0IM-`4A{JCta zzy`?(NQF`HIr3#HmnXRs$$zLy2;>CiCYT&~J*i=-Oq+(!sTM+l}xb87z^GgBQ<&ZIm#qE6@-PcS7@NFYOf3Fww1513Enfc5K~8I8=;I8&`Nzs?!LAdT=f zl@-AqOiA=@vd|(uz%6BrN7p6CykF-G%c2V2$-Fxzs{L}0zjKh-zUgxJUecG&k==g2p* z$jgS7JwSu9aFgRbfhJ2{dewiR76jSfaDypBw)9)75|gQ08Y)|WY)Mja%4ncu;U-7D z5s7HcXvNaHRQU_(2+QL$>V~Sg^8_bS$Tm(kNV1{ht)OpecQJZ&H!_VYZ#M^gkha82 za&UtRQ_Aa&9|B#DEa@cV=2@czMtOb&i>Xrg7}g;Zurs$XCWE89Q(iV?so z@NbycBPWC^HR0M!VXiz+T~fRs&5RnSYohv1*4rUN4pte638lDwZmT2<)} zuPohjh*iJgn~Vp3uDTE*Ud@cj-brdjRmkIi*eJZ64J~F)wodpYwepxfK2r@;{8`=y ze&bc3QiwRIU+2uPa|ZQdiy?vKtVi-AHNh3(t7^$0?>YV?(F)dyrl`2K?lj?9n!$@E%NkWcd5F8EU4N>l)!U=Sy z3XKp6Iss@3Q7{tdikdPYt?GiR@?!`M1?@vMa=10YlT@;S3V9M5gJv0z_9>*HP^ANo zpxP&TUQ!>82TmZZ2Tq2+&98HY%7$*nJ`0>R1KX*>XD)ssp5)g#^Xr^R%&F>77Ht{y zK%<7zk!{qmi_80k`wRL?puO`&ER3DNEf^QvMU}WLSJJh$rvQCR7l-y>Eb^5>#~ip; zWgaA;>^4y;)nV~Ps#1{N7Uasf*WoSfB|_Kaq~?1n6EPI|<;Y|$4gxch=RAdArZOzt zCQVOln$r`yi#Qlpj1L|IZ!)!tsN$BoELG zSqs0;nP2Bj73-w6B8~kzXMUYCR6aC1zs?!zw&@ z&ip!Oew{PF&Y555%&&9S_&HqmcnBGB1wJSo!U=rXGTCk*a5A1svGq3pgo3mKj*SfRm!m)#Sn)Cur@L)FCsVw#G$}VL_13FAM%L z%i)ZDDtRT)3m1b(H^^iS7>lH=C(G)Mw-9-u1&E%bT`Nh#LyLQ9)tFm^_HN6(Mnz~`&tqWT$ zm{1Z3Rd%|h?1TSwIiyL+s^_Qn{-PxCW!FbPp4NG5KSR5pq215W?q_KCGqn2|+Wide zeuj2GL%W}$-Otee$IH-;1uS#MWw>lo_kj|387n;JK5){x{4VYT^LpD7?9;7VMXEki zP0AZiAvR*$Yh-TN_{GuwXturMY*h*}zc`w2di$ogZ+iQtw{Lp;rnhf;`=+;Vdi$og zZ+idnOmCBjI9Jh(!Wd-i%%@e(S2W`rf@}-EQF2|Yq#0k8C)uHN@}U#?o(zPOrhb?z zZNv*Yq|SMlR@rKAbjp9gGBlcJAEaYIEnfPY@gIu9ra@MbBLCCB=uk zPC|k&d;&eV&~3w0UZVsLfpihc@C0J?|ikpQnv#0`4{v)i=`St`(7p zB&g)&d6sZRPaCm~p0=2~rVjnZ+%-PP=0KVS*Rw#tT|D9qL0NM3JT zB>#W%dRy7LdHGN0^(GNwFl6WT*7keefAcdFdy{R*_D?zavQ0TWlGpi(&w6BblwnT6d6W9hIsz+hab}AKlO(tM@Q}{x?a~YVQZcFAH6O+aomlfS>p-vz z{TlcSKdkTh#>^x4e)Jn_oEo=#Jv1-iggQb3J4VR&W*xXD5fTb9iCwJmkdVrxWOnGB zHL+7P+v6r#ls!J4diwQ`QSOa?LmxCn<3peq!T?)>-@@t8v07z#qXubWXCJD6`^u?obM7 z+UW&*Hve&(h31r7cv1vPLH|K&^I zZN@A7Au*Nk%fe?c)=EhYM`pNiN%%!INkW(ukmd@g@lSK+4$=XEevJh}@H|{i-@wuN zu~L&m`sOn&FwZKu5Z7>X^Ot*+YD_5tp?2Y^QfBjxYR0RSUDfdAFpNeRFRn>aF2b{u zO3-b-Pw}p_Sw1)#$qcHvkNeGY^h3%L>g0+xc~iA;i**;0NWMV7Cf8sVB9@^5)L1h+ z#kWm=N+QPFfTWVDRg)xzbRw)Xa>C3b0d#^UMYz(F#o(q@!W-6G>#P)w!e=E%A{ngY z5o0oR!&ps;1&xPMf`W`2e9=0wGRP*zohC_|=#L_OMRE_h$Q&rKi`A#3Z$O+ig*HfZ zLfRkUt?+^L4nb!=WP3<5{vhKZRh;O6?*N6B6a=ZEgilKRXx0S^VEwS;NOi`@ls81h z{!HRUpi43qI0M%pJSzu|DAkJisN^jv`G9v)bk7Xnx57_;#043Jfv5OCS!RHoQnKl{ z1!oe1y0A`lgGf08=MY|N&7d(>AZyyeOmGGu@SM`NC6I7h4<)t|HE2oU|G>+FBdU-^ zm%tNv3l}tQ!#~Am#cxH|q@_tv%p7!(A zyn`gD%!2emi~~K&%v1USFSYo%@WrC-mHiC;Ay`ZxR@JeIry>*1ihPx=P^BX=!4 zRzf)R6;pDB@k>7Fe#VJ>68sqt$q3ONwXX1X^B28BDrrc6Fl)g}e3?0d z@4`0=uXq9PrPZX6jFM`Oq;i6T^c(RJNWhXm+?SKT%*#scuLPB>3w#Fxbn!)DN{;Wr z2S^wNv*5jwr%QH%Q-t7@4=^y3N0h{w1C&eP8gEoJAT*FW{X(01Z|Nt}HlM?=|>k_>S;f z>HOf^bRvEuIiPg9IsQ{fH+&Dh#Ymwies7PPVr#UMwy#K^@aTK_=S)&xCA1{%mcdWx zqW~|J5(T;j<=}uu4nu!T21+&jOvyFP_a@OVHWFhN{vy8_tMDFPZPs6A9G|Ji%=Ao%yL|H09vA(I-iq#+CIAcx0~+Z?V=Tsbpu8 zYfu;1g%%+Rik`!7BsgzVuhr72^jUI7a-Ug3L*P8RtiG{+G-x3YBavRo)PSKgs@z@9 z-GzUd3u7}a_E`0qG>lpe^m{?ijOj;vDeUpL|BU=sIZD|X21a@Vv(1eNUIRZ9-G~QS z?Lf@=Uj74Rc|+dkp9kli@9`iQey+Tex&AF}*6s^Zls;v&61#tB+y2B@q&J6L-{(eR zzE?{B;^!@G*Nj2W!)(xy_-l`n ze#H~vw361-d7ggZ`|ulxL4FnUV`#(v3~Ct-O#IA#9vU3$L%0^7NB^Q#@0PuqmRXSimleWnGfka(N)j};D?4k{&;V{ z)3fQ^k0vFvaPc&)vn(k*Pi5B!?7cSex^iw^9pCzxsMAjx;e zE5BUY=mku|l$4TvE_{ZMq0cyUpYN;i)28aT@EyPTXYt2k{$2<%=IziC^bGyT;*vgL z?0b1w!64Ze@K+?R^js;`_!FUHygf!wLLRe{@)N04q4Z&_S?DXTCy7Dg7nL;$M-E(xG<^EZc2 zzltyaD17ty0@X%cpsODPm6^FDf2BS}^hzdQ!E%37fOX(EwWAy~4$i7g2c$K9I@d?=So7Bc! zQ`m%*$7-$8ZxRx7ZRQTHa;-bfE*2b$`KrtU>#UimvjB;CT6z+gp9RvAuT`?-jZKfI zkNznEA;_ygJQNg1(D(rZ{TUid$ED3;`Pn+Fp(mx6=UU^h7XDy+nU&#vDrO^JAhs%e zP<9zBAwj`P$peb_P%EW*nf1={nKar!grZ-Jz<5K=%0=;Th@yb3)`l6<6FRam0)%6T zjGQ%MPmO!{9s=c>q||s$7)iO{ubn!sp;_6U+)p=LXZd5{G;d*Lm4b?bIegF21+LK} zuzl5^IDvRK(3gF{=ot?jRlXU<3P+TdBa0YJ;C?(2T?EPH3jOyJaGv;UtF_)$2IOPFj3>7N;lW?K&!k6n5p-1 zHzk?yBA8MtV5Z>9?f}>57s6DY$FI%kFd}0NJ zih2XuD*YJD1D3`+BWN%lc~HP?ygy(z?~^ABuM0EMYHLRYxq)U1<^y?oZ$yEtu7(4D z1yzLUQln%taV`Evp&0JhdLY60f^tywXU3yntRwgaC8R|Ozy^Oc-zT9Dz5tJShLZ&p zC;@@tY)}TBu>ztD>2R{cz+E#}?Y_xk7SF>gMqm0Y3NZgeI`AFrR)=y_G)4Rfo+#L} z&Ttdvi#-QjKuPp2{(&@*Eyn248I27lpA)`wu7MK$TRH&{l$?fAL|5fNo`2xEd~O-j zBP~D;?SyrVv9^p5E+XF-U|}5E4d-+N^rveeA#DY|14IBXm?u6WKE(X-6Jq^Sosu`e z`vI$-mrnrO6ZqjprGMVdoVAxDV#X^A|6v!K5m45W`9iT^gLovg$Z)kYWQu=@a&-+q z2CrFJeT%jPt!f7p7=qu%n@<12T&M=TmgfU4PGI6UIKpTt79y13lXiKtXwz6&VrJYg zPmHu{WB_9j$KZY3sZ$ZAEE!yaGGpMTC?D{!R(RRLlfxs!c7dZq9$@DO(pmLszmCy*A4 zW=6t6l}f^5=X(mTi_&Z>z{jjjvyI8y*bDDcMp6itWj@fNdJ zBiWasOcNs&oT1AgqgR=#C+%xm~bazfPdm-l2+g~Px3xSs)B~PX6k$| zhc_~r`?d4K*+oW}mb?XXIH#i3gVV9c%oy0Wk=6mm7|w^j1nsg$)vOk{1Wbf`XgJb0 zSR2u{_OS5%z-#2=6erBc=gj+4tSV~;3b2RG(rBLeRKObTe!;Wgg>=t5p%$*d5WW|V zWB{_h)maST1d!)`a1po)!Wq6}oy2*eM`4g8GJSEaT@-jV2nGM(exwJS6uecQeZJ4o z4lZHOG6^^ALqUNmKMYu;YgB*va#^g^C}h065=40MEl^h*`*if7LFpt zG(&t9M#?Z2<_@Q(AD}^ha1UV&y`cRtOYvc3BtPgMj9s2M_CR^R*}G+G!eaSDWrL&5 zK)#ePqCe?LtS}s#GGaWf6%ieXYVm!++0htTL!<+60$G77SZ$<*#pyL33URAU5j_AF z-Y>r;C!(~9%$S`&o>P$=fPki?RYVsRfwBtlUCj)v5k~;3+zn@BRKS88IQJ!a#ap=< zD4+vrR$Pn2Vnd4;F&EJeqo#N5Rf_v@8Vo=oTX{;h8M+BiXQci?#Xt+KOf)VZkoMEH zZ^)W(r(~=4tm&TdfT^r6C=TP~{1$gY5qvKz7%fj$J(65kRq6*>BppU{%r&yX;1t$?dXdfsUFrM)6U0u*N~?oQ z@EmW_CuyZB7Q_s-^2|-817+4q5(6lHiI-`1XmdOVEz+N6$p>*&dZa8Va6xWq#`FXn z5u@T}NOFUD@Kg{IKF{|mKnOl@t+PCmT8stAA!URE422QHM&ZH8D$ang4sdbCn~{`Z zHQCiPYA+gcEDPqqrJ!nYJ+vQ2tulnN<-%}L-T)MVtFT8;!&lfFV2rFABb28M+!IG; zN{pKomo%rHe&F?hfbZcPz#3>sL#4PaW7V#`q!-+m5$O~uV*tyvgR4DLc5$Fp=|z$= zlH>T8VRfQ#aYM!}E(Wl{U{+tN#`nxoG%q_wJMt>_gPsEh>tq+j1%aAQq{^0KUz%A3 z(E{MjT*E)X84(GZ1J6^e4eY_f2K%54-U>u`3$65u#sybHk7dC`Vb!#9tsaqSveTuz zLTNm$a|l>~nw2;v-$;`%7Lc$5J&Q9#;?&?Zk_!z0o&}GQUaa?t8zCoI0eLGBj9@Km z&U2zv&h{`fNq;m9aTs_Bsyv{h+Fbs^R* zYnu}ltc$E!ENbWj+m}8hQ_vixd2)?BQJHzxU0xEb3t2tD7!C)%2pgCH!x7`w-k9tL zqzAB4Azu&#_+rxuXTdn_RUwOjlsJc>QFAf_z6`cx`5wv2n56YEPjmt(kGad1K=VP% z0uFFwSy9Loa7H{1NWs%|7646|7RhdGcL58S@iwdu@mWrn0be)~@~_kmbCa zNS14C0&q|opYRJzvI#Itmo22U9>m11Cc3gA{)67s@J&dB>D51IxA-3lPmr07R> zgt!(KrnDSZN*0dphD8Hm7#-|FwuG_T!BF4{jhQngq7v|hQln^j^45qH)u+z5LVxfI zt*1OnIunNmDlBJJnI#a_+@T?3UCG7;gkYIgo$-MM;?}Z{zz|v}n1<%ZDM4T=L=hx) z78xB@<&PPG%95dzLzCz{-~~Dk&p`3WUCTyBr(hLXH(?M!tpa-BA+r)SvOe%6jaulN z6^5E~hTsC$4#ioSwF;bNbW()T%1;2?IE^SO=LrKj?Estei1ZDG0SWD*NCz%BZ^gB| zg2))H0&^7JN#BsxDoLi5lZA(^qLpKXSrhr{taEje>^i09yj9xi`6IufDG`|VVWh+8oSOXhSm;_O zs19^szVc`x1L+yBmFW*3q2OfvDdIQmcrp=<1C-)Q6A%VSk77N59A^u`Su99NUCSnv z&ZYHWou%oqAH>^DzkCLcjL+(qvg7u;=-jh>7S4H1kZ7Pf^2PY%4Od3P$gLlJF%o#L;0%tl zSQ7L+k6zC6VxGcFgO9vV{syx@X0%B&KH)Xv6aG7R2-6cM6uvV)7Fy$hTbgrwqIR1u zc4h~}qH{BLwr6;zg%p?d(YC{h$JEs);jgB1r5>_rXymityK%bkWd!0QEmo(FK+ zVUo`Z?S*UYD+%}HlT**y-2jWY3--wvBI4qVTy`S;N#^N3-Y2dlt3%IW6(E6hzs88P zG}pRc`vlT-xgXp!=i0fJHy(e0uHj(d2Tw^OMyKO5xXo|r#L^vk9?a1-`zFwtytoVw zektE9(Hdy=nz{6EacLwlnju=1tUUNSJ~gy5zDF86`!HxY^u|+&UEXizC#-h7+tSD6 zXP1W;?=~;s8W78Z7X50z(s0lLfft$;-^+I=@3+}e<9ReEuC-FEF0IlxSwo&jN8wtW z8mo%_MFo;miU<%qK^=&ZfpIO52R;NaRHtX9ALETiL*;ws$~E&xKh^ibRA!~`h3Qfs zxn>r0&oxNJwcus+4PeOoB)=gCG##+f?8}G~K+3{jo)?$k8p(*<66VNrD*nXx!W{U7 z@fP!L{RpFFhsbKsXu>KRGC{6Q|Jpm0;sKqZIRF`^DNRb6zBE?(-BEvdz6wDCqzvR_;ekzDV-fPb_V5(t zz<-Qi>KxbRdDb6qjA&D~qWti>){a*OSipbiTdYC56!O{08-%2%KfG5w=lTH~M0>6u zIJYqPr+?@lZMB+`9c6aFB8kII)NQI^RBcW7QEB~2)lDP}R6+QS#Ed!-w8I0SeMf?X z_!kI3w}C(;8R{5}TMej59nE)qSs0>e;%S{IR$?pNF|;PK){6U#1r>)$AZppLqmI@q zh}fj?2W`bY7DTZI6CFb)SX9BCp^jm5K_vKi<92(|z!Zx6V*_(STZa=* zYDdWC2ZogO4(!*2uqJ++5Z1)c3t?4KgWRP35LQ2g6?MRREq4hbOv*mO5P^<+0a1r)D;G7Yy;1!!m>pFbxFU3U95aj` zzLD!)YLXwq>W8pOwMQ-07ALr&jeS3a6-8BMAF3@%Dyp9!!isBJ<`!zTAHpiv7v>qR zO+SPcu>)RcyFo4$T)%nwBUcixJ~_Zm2rjM`nY|o|#HA<`(GOwOu@D`>U<(sgBb(c# zP=L;KbX6`ncK0n;B2Fr~Hf4^zK#c3q1AG1;Z{P0*I^IHKu3Fwn+Yem z^fR^#%=dEl<1NvaB7fP?(YA?>5&9vlIxNJG!FNA|)em9C>1zGJ`|M9plpn&%_JN$e zW-Gvf3tRifUe;Eg*`8+028)jEDBSDF1wVwW8qh)r!}e4S4hwEF&y2 zKZKRDAxK3ap<{8_`5X(x4^x z`XQ`-2&*5$>W8oraDdCf4`KB~Sp5)IKZMl}Vf8~;{Sa0^gw+pW^+Qt;A zu(BiPhp++zKZI3*o_+|c&WBRugY*N0taAER=W-Py>xZyzG=$YIuUcGM)D%s1B0YPC z0t(ZlmTOthNFd}?x9u4XsOYda{?!y$bsp|DI3BQKR7=Zx4Ne3ae?qlat%%k#qHPIh z9g>;Qz@I*2QzJ4J&WCQ!hd#yOttJfDd7)KO)dcJ^;30v|#8N8{LR=BOre3W|O`F+t ztyB!JGq;M`y6Q}wTAT{GcD382GyYsv-OlQ(d)Gi`lifwRJ(NjAk~>h`S5WXwG6NaNnA`jLS$4klBwn;fcaq(_p8Y%)k3bR<$u zzD5d(g~|iPE0rva$^98~x8YxA&E2Vu>&%@1U>i2p++#tfD)R3#ck)+-Qo#h&O7Ce30WTj(+*sY*d|P1@v_S_8Svh$A zt1IXx3*(A^WbjTfIdd!;ytP!;IPwE+>{G$ut@Zq$2Om@A_*_tQG1gh-pPj&Tr(|?2 z=pbGm)pE!;nCNrWi8n;~+aOw3b?M-4j^gYp)+UPEqBioLg{zZQ;h>5`CWDbohMW<> zcy(2{I!hQrvKZs6GTb&Qm01O*Rpr?lc|n0`GPc@{!v!9`U06E#r9SYC4UQ-y?Z`J#@QNYC3H(GMqD*5e<`hO`oZLO;H!m>652~peH3GmO= zoen4RNLVv&%$OoxZQ+5{1KBn@irFbS0(m$(!ZEGS;b_NX(*9h|J$8E$9y??)JWuwG zeky#(;Ftr4jgp*&H_mMo;Opl%j0>L z^hB;U(zbixM^s?BdmiAVl9Cj?gEr5Y$;T6RkB{IhVRZ4ciJ+LNyDj2|3!FrHpzA&% zo4YOSffZ@`!pJArvtcD#akUX}=4msM*mYlAij%u7>KzMj_S{GCprMztE|hM9Drw(Cs~WqvSEWV*})BC84HNVDq7l9eM73Q!D3h>S_gAW+qLU4uxIK2 E17OX^xBvhE literal 0 HcmV?d00001 From 71acf91ace45965fef1c2b99409cdedf0de13af0 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 9 Apr 2018 12:34:53 +0800 Subject: [PATCH 041/520] Replace lager with emqx_log --- src/emqx.erl | 15 ++----- src/emqx_access_control.erl | 32 ++++++++------- src/emqx_access_rule.erl | 2 +- src/emqx_acl_internal.erl | 15 ++++--- src/emqx_acl_mod.erl | 2 +- src/emqx_alarm.erl | 6 +-- src/emqx_app.erl | 4 +- src/emqx_auth_mod.erl | 8 ++-- src/emqx_bridge.erl | 14 +++---- src/emqx_bridge_sup.erl | 8 ++-- src/emqx_bridge_sup_sup.erl | 4 +- src/emqx_broker.erl | 2 +- src/emqx_broker_helper.erl | 4 +- src/emqx_broker_sup.erl | 7 +--- src/emqx_cli.erl | 2 +- src/emqx_cm.erl | 14 ++++--- src/emqx_cm_stats.erl | 72 ++++++++++++++++++++++++++++++++++ src/emqx_cm_sup.erl | 17 +++----- src/emqx_config.erl | 10 ++++- src/emqx_connection.erl | 6 +-- src/emqx_ctl.erl | 4 +- src/emqx_flapping.erl | 2 +- src/emqx_gc.erl | 2 +- src/emqx_gen_mod.erl | 2 +- src/emqx_guid.erl | 2 +- src/emqx_hooks.erl | 2 +- src/emqx_inflight.erl | 2 +- src/emqx_json.erl | 2 +- src/emqx_keepalive.erl | 2 +- src/emqx_lager_backend.erl | 2 +- src/emqx_log.erl | 2 +- src/emqx_message.erl | 2 +- src/emqx_metrics.erl | 2 +- src/emqx_misc.erl | 2 +- src/emqx_mod_presence.erl | 2 +- src/emqx_mod_rewrite.erl | 6 +-- src/emqx_mod_subscription.erl | 2 +- src/emqx_mod_sup.erl | 2 +- src/emqx_modules.erl | 6 +-- src/emqx_mqtt.erl | 14 ++++--- src/emqx_mqtt_metrics.erl | 2 +- src/emqx_mqtt_props.erl | 2 +- src/emqx_mqtt_rscode.erl | 2 +- src/emqx_mqueue.erl | 2 +- src/emqx_net.erl | 2 +- src/emqx_packet.erl | 2 +- src/emqx_parser.erl | 2 +- src/emqx_plugins.erl | 53 +++++++++++++------------ src/emqx_pmon.erl | 2 +- src/emqx_pool_sup.erl | 2 +- src/emqx_pooler.erl | 7 +++- src/emqx_protocol.erl | 8 ++-- src/emqx_router.erl | 2 +- src/emqx_router_helper.erl | 2 +- src/emqx_router_sup.erl | 2 +- src/emqx_rpc.erl | 2 +- src/emqx_serializer.erl | 2 +- src/emqx_session.erl | 16 ++++---- src/emqx_session_sup.erl | 2 +- src/emqx_shared_sub.erl | 2 +- src/emqx_sm.erl | 2 +- src/emqx_sm_locker.erl | 2 +- src/emqx_sm_registry.erl | 2 +- src/emqx_sm_stats.erl | 2 +- src/emqx_sm_sup.erl | 2 +- src/emqx_stats.erl | 31 ++------------- src/emqx_sup.erl | 2 +- src/emqx_sys.erl | 2 +- src/emqx_sysmon.erl | 10 ++--- src/emqx_sysmon_sup.erl | 4 +- src/emqx_tables.erl | 2 +- src/emqx_time.erl | 2 +- src/emqx_topic.erl | 2 +- src/emqx_tracer.erl | 15 +++---- src/emqx_tracer_sup.erl | 2 +- src/emqx_trie.erl | 2 +- src/emqx_vm.erl | 2 +- src/emqx_ws.erl | 22 +++++------ src/emqx_ws_connection.erl | 6 +-- src/emqx_ws_connection_sup.erl | 4 +- 80 files changed, 297 insertions(+), 243 deletions(-) create mode 100644 src/emqx_cm_stats.erl diff --git a/src/emqx.erl b/src/emqx.erl index 13fe0db90..24668029a 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (C) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -19,7 +19,7 @@ -include("emqx.hrl"). %% Start/Stop Application --export([start/0, env/1, env/2, is_running/1, stop/0]). +-export([start/0, is_running/1, stop/0]). %% PubSub API -export([subscribe/1, subscribe/2, subscribe/3, publish/1, @@ -43,7 +43,7 @@ -define(APP, ?MODULE). %%-------------------------------------------------------------------- -%% Bootstrap, environment, configuration, is_running... +%% Bootstrap, is_running... %%-------------------------------------------------------------------- %% @doc Start emqx application @@ -54,14 +54,6 @@ start() -> application:start(?APP). -spec(stop() -> ok | {error, term()}). stop() -> application:stop(?APP). -%% @doc Get environment --spec(env(Key :: atom()) -> {ok, any()} | undefined). -env(Key) -> application:get_env(?APP, Key). - -%% @doc Get environment with default --spec(env(Key :: atom(), Default :: any()) -> undefined | any()). -env(Key, Default) -> application:get_env(?APP, Key, Default). - %% @doc Is emqx running? -spec(is_running(node()) -> boolean()). is_running(Node) -> @@ -71,7 +63,6 @@ is_running(Node) -> Pid when is_pid(Pid) -> true end. - %%-------------------------------------------------------------------- %% PubSub API %%-------------------------------------------------------------------- diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 4d573e283..7f665211e 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -51,7 +51,7 @@ start_link() -> auth(Client, Password) when is_record(Client, client) -> auth(Client, Password, lookup_mods(auth)). auth(_Client, _Password, []) -> - case emqx:env(allow_anonymous, false) of + case emqx_conf:get_env(allow_anonymous, false) of true -> ok; false -> {error, "No auth module to check!"} end; @@ -68,12 +68,12 @@ auth(Client, Password, [{Mod, State, _Seq} | Mods]) -> -spec(check_acl(Client, PubSub, Topic) -> allow | deny when Client :: client(), PubSub :: pubsub(), - Topic :: binary()). + Topic :: topic()). check_acl(Client, PubSub, Topic) when ?PS(PubSub) -> check_acl(Client, PubSub, Topic, lookup_mods(acl)). check_acl(_Client, _PubSub, _Topic, []) -> - emqx:env(acl_nomatch, allow); + emqx_conf:get_env(acl_nomatch, allow); check_acl(Client, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) -> case Mod:check_acl({Client, PubSub, Topic}, State) of allow -> allow; @@ -88,15 +88,17 @@ reload_acl() -> %% @doc Register Authentication or ACL module. -spec(register_mod(auth | acl, atom(), list()) -> ok | {error, term()}). -register_mod(Type, Mod, Opts) when Type =:= auth; Type =:= acl-> +register_mod(Type, Mod, Opts) when Type =:= auth; Type =:= acl -> register_mod(Type, Mod, Opts, 0). --spec(register_mod(auth | acl, atom(), list(), non_neg_integer()) -> ok | {error, term()}). +-spec(register_mod(auth | acl, atom(), list(), non_neg_integer()) + -> ok | {error, term()}). register_mod(Type, Mod, Opts, Seq) when Type =:= auth; Type =:= acl-> gen_server:call(?SERVER, {register_mod, Type, Mod, Opts, Seq}). %% @doc Unregister authentication or ACL module --spec(unregister_mod(Type :: auth | acl, Mod :: atom()) -> ok | {error, not_found | term()}). +-spec(unregister_mod(Type :: auth | acl, Mod :: atom()) + -> ok | {error, not_found | term()}). unregister_mod(Type, Mod) when Type =:= auth; Type =:= acl -> gen_server:call(?SERVER, {unregister_mod, Type, Mod}). @@ -104,7 +106,7 @@ unregister_mod(Type, Mod) when Type =:= auth; Type =:= acl -> -spec(lookup_mods(auth | acl) -> list()). lookup_mods(Type) -> case ets:lookup(?TAB, tab_key(Type)) of - [] -> []; + [] -> []; [{_, Mods}] -> Mods end. @@ -116,11 +118,11 @@ stop() -> gen_server:call(?MODULE, stop). %%-------------------------------------------------------------------- -%% gen_server Callbacks +%% gen_server callbacks %%-------------------------------------------------------------------- init([]) -> - _ = ets:new(?TAB, [set, named_table, protected, {read_concurrency, true}]), + _ = emqx_tables:create(?TAB, [set, protected, {read_concurrency, true}]), {ok, #state{}}. handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) -> @@ -155,13 +157,15 @@ handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(Req, _From, State) -> - lager:error("Bad Request: ~p", [Req]), - {reply, {error, badreq}, State}. + emqx_log:error("[AccessControl] Unexpected request: ~p", [Req]), + {reply, ignore, State}. -handle_cast(_Msg, State) -> +handle_cast(Msg, State) -> + emqx_log:error("[AccessControl] Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info(_Info, State) -> +handle_info(Info, State) -> + emqx_log:error("[AccessControl] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index a96094065..7eb4f2dc8 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index 4694f43c3..b7275af22 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -48,19 +48,18 @@ all_rules() -> %% @doc Init internal ACL -spec(init([File :: string()]) -> {ok, State :: any()}). init([File]) -> - ets:new(?ACL_RULE_TAB, [set, public, named_table, {read_concurrency, true}]), - State = #state{config = File}, - true = load_rules_from_file(State), - {ok, State}. + _ = emqx_tables:create(?ACL_RULE_TAB, [set, public, {read_concurrency, true}]), + {ok, load_rules_from_file(#state{config = File})}. -load_rules_from_file(#state{config = AclFile}) -> +load_rules_from_file(State = #state{config = AclFile}) -> {ok, Terms} = file:consult(AclFile), Rules = [emqx_access_rule:compile(Term) || Term <- Terms], lists:foreach(fun(PubSub) -> ets:insert(?ACL_RULE_TAB, {PubSub, lists:filter(fun(Rule) -> filter(PubSub, Rule) end, Rules)}) end, [publish, subscribe]), - ets:insert(?ACL_RULE_TAB, {all_rules, Terms}). + ets:insert(?ACL_RULE_TAB, {all_rules, Terms}), + State. filter(_PubSub, {allow, all}) -> true; @@ -79,7 +78,7 @@ filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) -> -spec(check_acl({Client, PubSub, Topic}, State) -> allow | deny | ignore when Client :: client(), PubSub :: pubsub(), - Topic :: binary(), + Topic :: topic(), State :: #state{}). check_acl(_Who, #state{config = undefined}) -> allow; diff --git a/src/emqx_acl_mod.erl b/src/emqx_acl_mod.erl index 7c0866e61..217b2ecd6 100644 --- a/src/emqx_acl_mod.erl +++ b/src/emqx_acl_mod.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_alarm.erl b/src/emqx_alarm.erl index 0f639af3b..73e2d2ca2 100644 --- a/src/emqx_alarm.erl +++ b/src/emqx_alarm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -92,7 +92,7 @@ handle_event({set_alarm, Alarm = #alarm{id = AlarmId, {summary, iolist_to_binary(Summary)}, {ts, emqx_time:now_secs(TS)}]) of {'EXIT', Reason} -> - lager:error("Failed to encode set_alarm: ~p", [Reason]); + emqx_log:error("[Alarm] Failed to encode set_alarm: ~p", [Reason]); JSON -> emqx_broker:publish(alarm_msg(alert, AlarmId, JSON)) end, @@ -101,7 +101,7 @@ handle_event({set_alarm, Alarm = #alarm{id = AlarmId, handle_event({clear_alarm, AlarmId}, Alarms) -> case catch emqx_json:encode([{id, AlarmId}, {ts, emqx_time:now_secs()}]) of {'EXIT', Reason} -> - lager:error("Failed to encode clear_alarm: ~p", [Reason]); + emqx_log:error("[Alarm] Failed to encode clear_alarm: ~p", [Reason]); JSON -> emqx_broker:publish(alarm_msg(clear, AlarmId, JSON)) end, diff --git a/src/emqx_app.erl b/src/emqx_app.erl index 50c9810c6..fc1774a7b 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -63,7 +63,7 @@ print_vsn() -> %%-------------------------------------------------------------------- register_acl_mod() -> - case emqx:env(acl_file) of + case emqx_conf:get_env(acl_file) of {ok, File} -> emqx_access_control:register_mod(acl, emqx_acl_internal, [File]); undefined -> ok end. diff --git a/src/emqx_auth_mod.erl b/src/emqx_auth_mod.erl index 1a2fd72c9..123527177 100644 --- a/src/emqx_auth_mod.erl +++ b/src/emqx_auth_mod.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -61,12 +61,14 @@ passwd_hash(sha256, Password) -> passwd_hash(pbkdf2, {Salt, Password, Macfun, Iterations, Dklen}) -> case pbkdf2:pbkdf2(Macfun, Password, Salt, Iterations, Dklen) of {ok, Hexstring} -> pbkdf2:to_hex(Hexstring); - {error, Error} -> lager:error("PasswdHash with pbkdf2 error:~p", [Error]), <<>> + {error, Error} -> + emqx_log:error("[AuthMod] PasswdHash with pbkdf2 error:~p", [Error]), <<>> end; passwd_hash(bcrypt, {Salt, Password}) -> case bcrypt:hashpw(Password, Salt) of {ok, HashPassword} -> list_to_binary(HashPassword); - {error, Error}-> lager:error("PasswdHash with bcrypt error:~p", [Error]), <<>> + {error, Error}-> + emqx_log:error("[AuthMod] PasswdHash with bcrypt error:~p", [Error]), <<>> end. hexstring(<>) -> diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index bdd077106..1e998f6cb 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -64,8 +64,8 @@ start_link(Pool, Id, Node, Topic, Options) -> %%-------------------------------------------------------------------- init([Pool, Id, Node, Topic, Options]) -> - gproc_pool:connect_worker(Pool, {Pool, Id}), process_flag(trap_exit, true), + gproc_pool:connect_worker(Pool, {Pool, Id}), case net_kernel:connect_node(Node) of true -> true = erlang:monitor_node(Node, true), @@ -103,11 +103,11 @@ qname(Node, Topic) -> iolist_to_binary(["Bridge:", Node, ":", Topic]). handle_call(Req, _From, State) -> - lager:error("[~s] Unexpected Call: ~p", [?MODULE, Req]), + emqx_log:error("[Bridge] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - lager:error("[~s] Unexpected Cast: ~p", [?MODULE, Msg]), + emqx_log:error("[Bridge] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info({dispatch, _Topic, Msg}, State = #state{mqueue = MQ, status = down}) -> @@ -118,7 +118,7 @@ handle_info({dispatch, _Topic, Msg}, State = #state{node = Node, status = up}) - {noreply, State, hibernate}; handle_info({nodedown, Node}, State = #state{node = Node, ping_down_interval = Interval}) -> - lager:warning("Bridge Node Down: ~p", [Node]), + emqx_log:warning("[Bridge] Node Down: ~s", [Node]), erlang:send_after(Interval, self(), ping_down_node), {noreply, State#state{status = down}, hibernate}; @@ -126,7 +126,7 @@ handle_info({nodeup, Node}, State = #state{node = Node}) -> %% TODO: Really fast?? case emqx:is_running(Node) of true -> - lager:warning("Bridge Node Up: ~p", [Node]), + emqx_log:warning("[Bridge] Node up: ~s", [Node]), {noreply, dequeue(State#state{status = up})}; false -> self() ! {nodedown, Node}, @@ -149,7 +149,7 @@ handle_info({'EXIT', _Pid, normal}, State) -> {noreply, State}; handle_info(Info, State) -> - lager:error("[~s] Unexpected Info: ~p", [?MODULE, Info]), + emqx_log:error("[Bridge] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> diff --git a/src/emqx_bridge_sup.erl b/src/emqx_bridge_sup.erl index 125c47124..4f389c0dc 100644 --- a/src/emqx_bridge_sup.erl +++ b/src/emqx_bridge_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -18,12 +18,10 @@ -export([start_link/3]). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- %% @doc Start bridge pool supervisor --spec(start_link(atom(), binary(), [emqx_bridge:option()]) -> {ok, pid()} | {error, term()}). +-spec(start_link(atom(), binary(), [emqx_bridge:option()]) + -> {ok, pid()} | {error, term()}). start_link(Node, Topic, Options) -> MFA = {emqx_bridge, start_link, [Node, Topic, Options]}, emqx_pool_sup:start_link({bridge, Node, Topic}, random, MFA). diff --git a/src/emqx_bridge_sup_sup.erl b/src/emqx_bridge_sup_sup.erl index 0f2d85212..88e75792e 100644 --- a/src/emqx_bridge_sup_sup.erl +++ b/src/emqx_bridge_sup_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -46,7 +46,7 @@ start_bridge(Node, Topic) when is_atom(Node) andalso is_binary(Topic) -> start_bridge(Node, _Topic, _Options) when Node =:= node() -> {error, bridge_to_self}; start_bridge(Node, Topic, Options) when is_atom(Node) andalso is_binary(Topic) -> - {ok, BridgeEnv} = emqx:env(bridge), + {ok, BridgeEnv} = emqx_conf:get_env(bridge), Options1 = emqx_misc:merge_opts(BridgeEnv, Options), supervisor:start_child(?MODULE, bridge_spec(Node, Topic, Options1)). diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 73b557eac..7cd80a33e 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index ec40e6cff..fc9a84be9 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -40,7 +40,7 @@ start_link(StatsFun) -> %%-------------------------------------------------------------------- init([StatsFun]) -> - {ok, TRef} = timer:send_interval(1000, stats), + {ok, TRef} = timer:send_interval(timer:seconds(1), stats), {ok, #state{stats_fun = StatsFun, stats_timer = TRef}}. handle_call(Req, _From, State) -> diff --git a/src/emqx_broker_sup.erl b/src/emqx_broker_sup.erl index d962526eb..b7e2b1c5b 100644 --- a/src/emqx_broker_sup.erl +++ b/src/emqx_broker_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -33,7 +33,7 @@ start_link() -> init([]) -> %% Create the pubsub tables - create_tabs(), + lists:foreach(fun create_tab/1, [subscription, subscriber, suboption]), %% Shared subscription Shared = {shared_sub, {emqx_shared_sub, start_link, []}, @@ -55,9 +55,6 @@ init([]) -> %% Create tables %%-------------------------------------------------------------------- -create_tabs() -> - lists:foreach(fun create_tab/1, [subscription, subscriber, suboption]). - create_tab(suboption) -> %% Suboption: {Topic, Sub} -> [{qos, 1}] ensure_tab(suboption, [set | ?CONCURRENCY_OPTS]); diff --git a/src/emqx_cli.erl b/src/emqx_cli.erl index 8ba645122..3a98f95b4 100644 --- a/src/emqx_cli.erl +++ b/src/emqx_cli.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 4d08a499d..568cbd951 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -62,9 +62,13 @@ unreg(ClientId) -> %% gen_server callbacks %%-------------------------------------------------------------------- -init([StatsFun]) -> - {ok, Ref} = timer:send_interval(timer:seconds(1), stats), - {ok, #state{stats_fun = StatsFun, stats_timer = Ref, monitors = dict:new()}}. +init([]) -> + _ = emqx_tables:create(client, [public, set, {keypos, 2}, + {read_concurrency, true}, + {write_concurrency, true}]), + _ = emqx_tables:create(client_attrs, [public, set, + {write_concurrency, true}]), + {ok, #state{monitors = dict:new()}}. handle_call(Req, _From, State) -> emqx_log:error("[CM] Unexpected request: ~p", [Req]), @@ -102,7 +106,7 @@ handle_info(stats, State) -> {noreply, setstats(State), hibernate}; handle_info(Info, State) -> - lager:error("[CM] Unexpected Info: ~p", [Info]), + emqx_log:error("[CM] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State = #state{stats_timer = TRef}) -> diff --git a/src/emqx_cm_stats.erl b/src/emqx_cm_stats.erl new file mode 100644 index 000000000..475087c37 --- /dev/null +++ b/src/emqx_cm_stats.erl @@ -0,0 +1,72 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Inc. 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_stats). + +-behaviour(gen_statem). + +-include("emqx.hrl"). + +%% API +-export([start_link/0]). + +-export([set_client_stats/2, get_client_stats/1, del_client_stats/1]). + +%% gen_statem callbacks +-export([init/1, callback_mode/0, handle_event/4, terminate/3, code_change/4]). + +-define(TAB, client_stats). + +-record(state, {statsfun}). + +start_link() -> + gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []). + +-spec(set_client_stats(client_id(), emqx_stats:stats()) -> true). +set_client_stats(ClientId, Stats) -> + ets:insert(?TAB, {ClientId, [{'$ts', emqx_time:now_secs()}|Stats]}). + +-spec(get_client_stats(client_id()) -> emqx_stats:stats()). +get_client_stats(ClientId) -> + case ets:lookup(?TAB, ClientId) of + [{_, Stats}] -> Stats; + [] -> [] + end. + +-spec(del_client_stats(client_id()) -> true). +del_client_stats(ClientId) -> + ets:delete(?TAB, ClientId). + +init([]) -> + _ = emqx_tables:create(?TAB, [public, {write_concurrency, true}]), + StatsFun = emqx_stats:statsfun('clients/count', 'clients/max'), + {ok, idle, #state{statsfun = StatsFun}, timer:seconds(1)}. + +callback_mode() -> handle_event_function. + +handle_event(timeout, _Timeout, idle, State = #state{statsfun = StatsFun}) -> + case ets:info(client, size) of + undefined -> ok; + Size -> StatsFun(Size) + end, + {next_state, idle, State, timer:seconds(1)}. + +terminate(_Reason, _StateName, _State) -> + ok. + +code_change(_OldVsn, StateName, State, _Extra) -> + {ok, StateName, State}. + diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index 04446c725..89618e1c9 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -26,16 +26,9 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - %% Create table - lists:foreach(fun create_tab/1, [client, client_stats, client_attrs]), - - StatsFun = emqx_stats:statsfun('clients/count', 'clients/max'), - - CM = {emqx_cm, {emqx_cm, start_link, [StatsFun]}, + Stats = {emqx_cm_stats, {emqx_cm_stats, start_link, []}, + permanent, 5000, worker, [emqx_cm_stats]}, + CM = {emqx_cm, {emqx_cm, start_link, []}, permanent, 5000, worker, [emqx_cm]}, - - {ok, {{one_for_all, 10, 3600}, [CM]}}. - -create_tab(Tab) -> - emqx_tables:create(Tab, [public, ordered_set, named_table, {write_concurrency, true}]). + {ok, {{one_for_all, 10, 3600}, [Stats, CM]}}. diff --git a/src/emqx_config.erl b/src/emqx_config.erl index 954539e16..a1ea69394 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -25,12 +25,20 @@ -module(emqx_config). +-export([get_env/1, get_env/2]). + -export([read/1, write/2, dump/2, reload/1, get/2, get/3, set/3]). -type(env() :: {atom(), term()}). -define(APP, emqx). +-spec(get_env(Key :: atom(), Default :: any()) -> undefined | any()). +get_env(Key, Default) -> + application:get_env(?APP, Key, Default). + +%% @doc Get environment +-spec(get_env(Key :: atom()) -> {ok, any()} | undefined). get_env(Key) -> application:get_env(?APP, Key). diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index e09ad83b2..4b693a3a4 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -57,8 +57,8 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). -define(LOG(Level, Format, Args, State), - lager:Level("Client(~s): " ++ Format, - [esockd_net:format(State#state.peername) | Args])). + emqx_log:Level("Client(~s): " ++ Format, + [esockd_net:format(State#state.peername) | Args])). start_link(Conn, Env) -> {ok, proc_lib:spawn_link(?MODULE, init, [[Conn, Env]])}. diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index b39d4ea83..15f86b785 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -108,7 +108,7 @@ handle_cast({register_cmd, Cmd, MF, Opts}, State = #state{seq = Seq}) -> [] -> ets:insert(?TAB, {{Seq, Cmd}, MF, Opts}); [[OriginSeq] | _] -> - lager:warning("CLI: ~s is overidden by ~p", [Cmd, MF]), + emqx_log:warning("[CLI] ~s is overidden by ~p", [Cmd, MF]), ets:insert(?TAB, {{OriginSeq, Cmd}, MF, Opts}) end, noreply(next_seq(State)); diff --git a/src/emqx_flapping.erl b/src/emqx_flapping.erl index a0c1c3d45..bf19c50ee 100644 --- a/src/emqx_flapping.erl +++ b/src/emqx_flapping.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (C) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 2bc9f75c9..e0fbed270 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_gen_mod.erl b/src/emqx_gen_mod.erl index 54996dead..184b77fb8 100644 --- a/src/emqx_gen_mod.erl +++ b/src/emqx_gen_mod.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_guid.erl b/src/emqx_guid.erl index 543233887..02941baa3 100644 --- a/src/emqx_guid.erl +++ b/src/emqx_guid.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index 369df3f41..3f926a0bb 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_inflight.erl b/src/emqx_inflight.erl index 718e1dc79..3bdc8b02a 100644 --- a/src/emqx_inflight.erl +++ b/src/emqx_inflight.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_json.erl b/src/emqx_json.erl index b3f426113..907b6e76d 100644 --- a/src/emqx_json.erl +++ b/src/emqx_json.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_keepalive.erl b/src/emqx_keepalive.erl index 45158d709..b1d8d87b0 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_lager_backend.erl b/src/emqx_lager_backend.erl index 735b71467..a307d5d8b 100644 --- a/src/emqx_lager_backend.erl +++ b/src/emqx_lager_backend.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_log.erl b/src/emqx_log.erl index 50f85c562..16b70b676 100644 --- a/src/emqx_log.erl +++ b/src/emqx_log.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 50198f3e1..3c732f676 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index cf41669ed..ba5ddf3c0 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index bccdbbdf5..0d3eecc5f 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index 91cf654fc..17ece8211 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index 9a344aed5..b9c664afd 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -35,11 +35,11 @@ load(Rules0) -> emqx:hook('message.publish', fun ?MODULE:rewrite_publish/2, [Rules]). rewrite_subscribe(_ClientId, _Username, TopicTable, Rules) -> - lager:info("Rewrite subscribe: ~p", [TopicTable]), + emqx_log:info("Rewrite subscribe: ~p", [TopicTable]), {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. rewrite_unsubscribe(_ClientId, _Username, TopicTable, Rules) -> - lager:info("Rewrite unsubscribe: ~p", [TopicTable]), + emqx_log:info("Rewrite unsubscribe: ~p", [TopicTable]), {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. rewrite_publish(Message = #message{topic = Topic}, Rules) -> diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index 83badcef6..ed3331d51 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mod_sup.erl b/src/emqx_mod_sup.erl index f3e24cf86..d45a33810 100644 --- a/src/emqx_mod_sup.erl +++ b/src/emqx_mod_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_modules.erl b/src/emqx_modules.erl index 4974abf38..a7407abcc 100644 --- a/src/emqx_modules.erl +++ b/src/emqx_modules.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -23,11 +23,11 @@ load() -> fun({Mod, Env}) -> ok = Mod:load(Env), io:format("Load ~s module successfully.~n", [Mod]) - end, emqx:env(modules, [])). + end, emqx_conf:get_env(modules, [])). unload() -> lists:foreach( fun({Mod, Env}) -> Mod:unload(Env) end, - emqx:env(modules, [])). + emqx_conf:get_env(modules, [])). diff --git a/src/emqx_mqtt.erl b/src/emqx_mqtt.erl index 8bb1a53bd..664fc88a0 100644 --- a/src/emqx_mqtt.erl +++ b/src/emqx_mqtt.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -40,7 +40,7 @@ shutdown() -> %% @doc Start Listeners. -spec(start_listeners() -> ok). start_listeners() -> - lists:foreach(fun start_listener/1, emqx:env(listeners, [])). + lists:foreach(fun start_listener/1, emqx_conf:get_env(listeners, [])). %% Start mqtt listener -spec(start_listener(listener()) -> {ok, pid()} | {error, any()}). @@ -60,7 +60,7 @@ start_listener({Proto, ListenOn, Opts}) when Proto == https; Proto == wss -> {ok, _} = mochiweb:start_http('mqtt:wss', ListenOn, Opts, {emqx_ws, handle_request, []}). start_listener(Proto, ListenOn, Opts) -> - Env = lists:append(emqx:env(client, []), emqx:env(protocol, [])), + Env = lists:append(emqx_conf:get_env(client, []), emqx_conf:get_env(protocol, [])), MFArgs = {emqx_connection, start_link, [Env]}, {ok, _} = esockd:open(Proto, ListenOn, merge_sockopts(Opts), MFArgs). @@ -75,7 +75,8 @@ is_mqtt(_Proto) -> false. %% @doc Stop Listeners -spec(stop_listeners() -> ok). -stop_listeners() -> lists:foreach(fun stop_listener/1, emqx:env(listeners, [])). +stop_listeners() -> + lists:foreach(fun stop_listener/1, emqx_conf:get_env(listeners, [])). -spec(stop_listener(listener()) -> ok | {error, any()}). stop_listener({tcp, ListenOn, _Opts}) -> @@ -93,7 +94,9 @@ stop_listener({Proto, ListenOn, _Opts}) -> %% @doc Restart Listeners -spec(restart_listeners() -> ok). -restart_listeners() -> lists:foreach(fun restart_listener/1, emqx:env(listeners, [])). +restart_listeners() -> + lists:foreach(fun restart_listener/1, + emqx_conf:get_env(listeners, [])). -spec(restart_listener(listener()) -> any()). restart_listener({tcp, ListenOn, _Opts}) -> @@ -113,3 +116,4 @@ merge_sockopts(Options) -> SockOpts = emqx_misc:merge_opts( ?MQTT_SOCKOPTS, proplists:get_value(sockopts, Options, [])), emqx_misc:merge_opts(Options, [{sockopts, SockOpts}]). + diff --git a/src/emqx_mqtt_metrics.erl b/src/emqx_mqtt_metrics.erl index 05b9adcff..175990f0b 100644 --- a/src/emqx_mqtt_metrics.erl +++ b/src/emqx_mqtt_metrics.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mqtt_props.erl b/src/emqx_mqtt_props.erl index d928b8ad1..7e4633657 100644 --- a/src/emqx_mqtt_props.erl +++ b/src/emqx_mqtt_props.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mqtt_rscode.erl b/src/emqx_mqtt_rscode.erl index 47450a5d3..5bc7c0210 100644 --- a/src/emqx_mqtt_rscode.erl +++ b/src/emqx_mqtt_rscode.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index acdb5ee8c..4f46526ed 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_net.erl b/src/emqx_net.erl index 45e05deda..ea5bb2462 100644 --- a/src/emqx_net.erl +++ b/src/emqx_net.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index acdd52aab..f78c01362 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_parser.erl b/src/emqx_parser.erl index f23b7e727..f72e8ea4e 100644 --- a/src/emqx_parser.erl +++ b/src/emqx_parser.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index e9d980ca3..9e5f0dff3 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -31,7 +31,7 @@ %% @doc Init plugins' config -spec(init() -> ok). init() -> - case emqx:env(plugins_etc_dir) of + case emqx_conf:get_env(plugins_etc_dir) of {ok, PluginsEtc} -> CfgFiles = [filename:join(PluginsEtc, File) || File <- filelib:wildcard("*.config", PluginsEtc)], @@ -50,7 +50,7 @@ init_config(CfgFile) -> -spec(load() -> list() | {error, term()}). load() -> load_expand_plugins(), - case emqx:env(plugins_loaded_file) of + case emqx_conf:get_env(plugins_loaded_file) of {ok, File} -> ensure_file(File), with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end); @@ -60,7 +60,7 @@ load() -> end. load_expand_plugins() -> - case emqx:env(expand_plugins_dir) of + case emqx_conf:get_env(expand_plugins_dir) of {ok, Dir} -> PluginsDir = filelib:wildcard("*", Dir), lists:foreach(fun(PluginDir) -> @@ -83,7 +83,8 @@ load_expand_plugin(PluginDir) -> end, Modules), case filelib:wildcard(Ebin ++ "/*.app") of [App|_] -> application:load(list_to_atom(filename:basename(App, ".app"))); - _ -> lager:error("load application fail"), {error, load_app_fail} + _ -> emqx_log:error("App file cannot be found."), + {error, load_app_fail} end. init_expand_plugin_config(PluginDir) -> @@ -100,7 +101,7 @@ init_expand_plugin_config(PluginDir) -> end, AppsEnv). get_expand_plugin_config() -> - case emqx:env(expand_plugins_dir) of + case emqx_conf:get_env(expand_plugins_dir) of {ok, Dir} -> PluginsDir = filelib:wildcard("*", Dir), lists:foldl(fun(PluginDir, Acc) -> @@ -127,7 +128,7 @@ with_loaded_file(File, SuccFun) -> {ok, Names} -> SuccFun(Names); {error, Error} -> - lager:error("Failed to read: ~p, error: ~p", [File, Error]), + emqx_log:error("[Plugins] Failed to read: ~p, error: ~p", [File, Error]), {error, Error} end. @@ -135,7 +136,7 @@ load_plugins(Names, Persistent) -> Plugins = list(), NotFound = Names -- names(Plugins), case NotFound of [] -> ok; - NotFound -> lager:error("Cannot find plugins: ~p", [NotFound]) + NotFound -> emqx_log:error("[Plugins] Cannot find plugins: ~p", [NotFound]) end, NeedToLoad = Names -- NotFound -- names(started_app), [load_plugin(find_plugin(Name, Plugins), Persistent) || Name <- NeedToLoad]. @@ -143,7 +144,7 @@ load_plugins(Names, Persistent) -> %% @doc Unload all plugins before broker stopped. -spec(unload() -> list() | {error, term()}). unload() -> - case emqx:env(plugins_loaded_file) of + case emqx_conf:get_env(plugins_loaded_file) of {ok, File} -> with_loaded_file(File, fun stop_plugins/1); undefined -> @@ -157,7 +158,7 @@ stop_plugins(Names) -> %% @doc List all available plugins -spec(list() -> [plugin()]). list() -> - case emqx:env(plugins_etc_dir) of + case emqx_conf:get_env(plugins_etc_dir) of {ok, PluginsEtc} -> CfgFiles = filelib:wildcard("*.{conf,config}", PluginsEtc) ++ get_expand_plugin_config(), Plugins = [plugin(CfgFile) || CfgFile <- CfgFiles], @@ -184,12 +185,12 @@ plugin(CfgFile) -> load(PluginName) when is_atom(PluginName) -> case lists:member(PluginName, names(started_app)) of true -> - lager:error("Plugin ~p is already started", [PluginName]), + emqx_log:error("[Plugins] Plugin ~s is already started", [PluginName]), {error, already_started}; false -> case find_plugin(PluginName) of false -> - lager:error("Plugin ~s not found", [PluginName]), + emqx_log:error("[Plugins] Plugin ~s not found", [PluginName]), {error, not_found}; Plugin -> load_plugin(Plugin, true) @@ -217,12 +218,12 @@ load_app(App) -> start_app(App, SuccFun) -> case application:ensure_all_started(App) of {ok, Started} -> - lager:info("Started Apps: ~p", [Started]), - lager:info("Load plugin ~p successfully", [App]), + emqx_log:info("Started Apps: ~p", [Started]), + emqx_log:info("Load plugin ~s successfully", [App]), SuccFun(App), {ok, Started}; {error, {ErrApp, Reason}} -> - lager:error("load plugin ~p error, cannot start app ~s for ~p", [App, ErrApp, Reason]), + emqx_log:error("Load plugin ~s error, cannot start app ~s for ~p", [App, ErrApp, Reason]), {error, {ErrApp, Reason}} end. @@ -239,10 +240,10 @@ unload(PluginName) when is_atom(PluginName) -> {true, true} -> unload_plugin(PluginName, true); {false, _} -> - lager:error("Plugin ~p is not started", [PluginName]), + emqx_log:error("Plugin ~s is not started", [PluginName]), {error, not_started}; {true, false} -> - lager:error("~s is not a plugin, cannot unload it", [PluginName]), + emqx_log:error("~s is not a plugin, cannot unload it", [PluginName]), {error, not_found} end. @@ -257,11 +258,11 @@ unload_plugin(App, Persistent) -> stop_app(App) -> case application:stop(App) of ok -> - lager:info("Stop plugin ~p successfully~n", [App]), ok; + emqx_log:info("Stop plugin ~s successfully", [App]), ok; {error, {not_started, App}} -> - lager:error("Plugin ~p is not started~n", [App]), ok; + emqx_log:error("Plugin ~s is not started", [App]), ok; {error, Reason} -> - lager:error("Stop plugin ~p error: ~p", [App]), {error, Reason} + emqx_log:error("Stop plugin ~s error: ~p", [App]), {error, Reason} end. %%-------------------------------------------------------------------- @@ -293,7 +294,7 @@ plugin_loaded(Name, true) -> ignore end; {error, Error} -> - lager:error("Cannot read loaded plugins: ~p", [Error]) + emqx_log:error("Cannot read loaded plugins: ~p", [Error]) end. plugin_unloaded(_Name, false) -> @@ -305,14 +306,14 @@ plugin_unloaded(Name, true) -> true -> write_loaded(lists:delete(Name, Names)); false -> - lager:error("Cannot find ~s in loaded_file", [Name]) + emqx_log:error("Cannot find ~s in loaded_file", [Name]) end; {error, Error} -> - lager:error("Cannot read loaded_plugins: ~p", [Error]) + emqx_log:error("Cannot read loaded_plugins: ~p", [Error]) end. read_loaded() -> - case emqx:env(plugins_loaded_file) of + case emqx_conf:get_env(plugins_loaded_file) of {ok, File} -> read_loaded(File); undefined -> {error, not_found} end. @@ -320,14 +321,14 @@ read_loaded() -> read_loaded(File) -> file:consult(File). write_loaded(AppNames) -> - {ok, File} = emqx:env(plugins_loaded_file), + {ok, File} = emqx_conf:get_env(plugins_loaded_file), case file:open(File, [binary, write]) of {ok, Fd} -> lists:foreach(fun(Name) -> file:write(Fd, iolist_to_binary(io_lib:format("~s.~n", [Name]))) end, AppNames); {error, Error} -> - lager:error("Open File ~p Error: ~p", [File, Error]), + emqx_log:error("Open File ~p Error: ~p", [File, Error]), {error, Error} end. diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index 774b3d8ec..7e76e0a18 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_pool_sup.erl b/src/emqx_pool_sup.erl index 47bb4eaf0..789e00a75 100644 --- a/src/emqx_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_pooler.erl b/src/emqx_pooler.erl index 943c0642f..9bce4e2b5 100644 --- a/src/emqx_pooler.erl +++ b/src/emqx_pooler.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -69,7 +69,10 @@ handle_call(_Req, _From, State) -> {reply, ok, State}. handle_cast({async_submit, Fun}, State) -> - try run(Fun) catch _:Error -> lager:error("Pooler Error: ~p, ~p", [Error, erlang:get_stacktrace()]) end, + try run(Fun) + catch _:Error -> + emqx_log:error("Pooler Error: ~p, ~p", [Error, erlang:get_stacktrace()]) + end, {noreply, State}; handle_cast(_Msg, State) -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 90560e784..0b0abb903 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -57,8 +57,8 @@ -define(STATS_KEYS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(LOG(Level, Format, Args, State), - lager:Level([{client, State#proto_state.client_id}], "Client(~s@~s): " ++ Format, - [State#proto_state.client_id, esockd_net:format(State#proto_state.peername) | Args])). + emqx_log:Level([{client, State#proto_state.client_id}], "Client(~s@~s): " ++ Format, + [State#proto_state.client_id, esockd_net:format(State#proto_state.peername) | Args])). %% @doc Init protocol init(Peername, SendFun, Opts) -> @@ -549,7 +549,7 @@ authenticate(Client, Password) -> %% PUBLISH ACL is cached in process dictionary. check_acl(publish, Topic, Client) -> - IfCache = emqx:env(cache_acl, true), + IfCache = emqx_conf:get_env(cache_acl, true), case {IfCache, get({acl, publish, Topic})} of {true, undefined} -> AllowDeny = emqx_access_control:check_acl(Client, publish, Topic), diff --git a/src/emqx_router.erl b/src/emqx_router.erl index e4741c8c4..58aba1e68 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index f8c41efc0..071bc7481 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_router_sup.erl b/src/emqx_router_sup.erl index 2fa5c21e1..98d4eb68d 100644 --- a/src/emqx_router_sup.erl +++ b/src/emqx_router_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_rpc.erl b/src/emqx_rpc.erl index 7c6b13846..b133b2292 100644 --- a/src/emqx_rpc.erl +++ b/src/emqx_rpc.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_serializer.erl b/src/emqx_serializer.erl index 82c888665..0450a462f 100644 --- a/src/emqx_serializer.erl +++ b/src/emqx_serializer.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index f77d9da24..208af034b 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -155,8 +155,8 @@ created_at]). -define(LOG(Level, Format, Args, State), - lager:Level([{client, State#state.client_id}], - "Session(~s): " ++ Format, [State#state.client_id | Args])). + emqx_log:Level([{client, State#state.client_id}], + "Session(~s): " ++ Format, [State#state.client_id | Args])). %% @doc Start a Session -spec(start_link(map()) -> {ok, pid()} | {error, term()}). @@ -271,8 +271,8 @@ init(#{clean_start := CleanStart, process_flag(trap_exit, true), true = link(ClientPid), init_stats([deliver_msg, enqueue_msg]), - {ok, Env} = emqx:env(session), - {ok, QEnv} = emqx:env(mqueue), + {ok, Env} = emqx_conf:get_env(session), + {ok, QEnv} = emqx_conf:get_env(mqueue), MaxInflight = get_value(max_inflight, Env, 0), EnableStats = get_value(enable_stats, Env, false), IgnoreLoopDeliver = get_value(ignore_loop_deliver, Env, false), @@ -342,7 +342,7 @@ handle_call(state, _From, State) -> reply(?record_to_proplist(state, State, ?STATE_KEYS), State); handle_call(Req, _From, State) -> - lager:error("[~s] Unexpected Call: ~p", [?MODULE, Req]), + emqx_log:error("[Session] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast({subscribe, From, TopicTable, AckFun}, @@ -501,7 +501,7 @@ handle_cast({resume, ClientPid}, {noreply, emit_stats(dequeue(retry_delivery(true, State1)))}; handle_cast(Msg, State) -> - lager:error("[~s] Unexpected Cast: ~p", [?MODULE, Msg]), + emqx_log:error("[Session] Unexpected msg: ~p", [Msg]), {noreply, State}. %% Ignore Messages delivered by self @@ -551,7 +551,7 @@ handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = ClientPid}) -> {noreply, State, hibernate}; handle_info(Info, State) -> - lager:error("[~s] Unexpected Info: ~p", [?MODULE, Info]), + emqx_log:error("[Session] Unexpected info: ~p", [Info]), {noreply, State}. terminate(Reason, #state{client_id = ClientId, username = Username}) -> diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl index 3e61f09d1..6a19e29ef 100644 --- a/src/emqx_session_sup.erl +++ b/src/emqx_session_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 6d5efbd51..6b31cc864 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 56587501e..85c4c4423 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_sm_locker.erl b/src/emqx_sm_locker.erl index 116b75bd7..07770061b 100644 --- a/src/emqx_sm_locker.erl +++ b/src/emqx_sm_locker.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index e718ee3e7..ad3597bcc 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_sm_stats.erl b/src/emqx_sm_stats.erl index 4d7766f45..dab8d4a5d 100644 --- a/src/emqx_sm_stats.erl +++ b/src/emqx_sm_stats.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index f65dead14..9f2b44fa3 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index 83048a001..4aad2fb3d 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -25,9 +25,6 @@ %% Get all Stats -export([all/0]). -%% Client and Session Stats --export([set_client_stats/2, get_client_stats/1, del_client_stats/1]). - %% Statistics API. -export([statsfun/1, statsfun/2, getstats/0, getstat/1, setstat/2, setstat/3]). @@ -41,9 +38,7 @@ -export_type([stats/0]). --define(STATS_TAB, mqtt_stats). --define(CLIENT_STATS_TAB, mqtt_client_stats). --define(SESSION_STATS_TAB, mqtt_session_stats). +-define(STATS_TAB, stats). %% $SYS Topics for Clients -define(SYSTOP_CLIENTS, [ @@ -87,22 +82,6 @@ start_link() -> stop() -> gen_server:call(?MODULE, stop). --spec(set_client_stats(binary(), stats()) -> true). -set_client_stats(ClientId, Stats) -> - ets:insert(?CLIENT_STATS_TAB, {ClientId, [{'$ts', emqx_time:now_secs()}|Stats]}). - --spec(get_client_stats(binary()) -> stats()). -get_client_stats(ClientId) -> - case ets:lookup(?CLIENT_STATS_TAB, ClientId) of - [{_, Stats}] -> Stats; - [] -> [] - end. - --spec(del_client_stats(binary()) -> true). -del_client_stats(ClientId) -> - ets:delete(?CLIENT_STATS_TAB, ClientId). - - all() -> ets:tab2list(?STATS_TAB). %% @doc Generate stats fun @@ -143,10 +122,8 @@ setstat(Stat, MaxStat, Val) -> init([]) -> emqx_time:seed(), - lists:foreach( - fun(Tab) -> - Tab = ets:new(Tab, [set, public, named_table, {write_concurrency, true}]) - end, [?STATS_TAB, ?CLIENT_STATS_TAB, ?SESSION_STATS_TAB]), + _ = emqx_tables:create(?STATS_TAB, [set, public, named_table, + {write_concurrency, true}]), Topics = ?SYSTOP_CLIENTS ++ ?SYSTOP_SESSIONS ++ ?SYSTOP_PUBSUB ++ ?SYSTOP_RETAINED, ets:insert(?STATS_TAB, [{Topic, 0} || Topic <- Topics]), % Tick to publish stats diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index ec05316c7..ce2c438f0 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl index 3c2ba1e3e..b18cc195b 100644 --- a/src/emqx_sys.erl +++ b/src/emqx_sys.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_sysmon.erl b/src/emqx_sysmon.erl index 7fc270a6a..d0c3d9001 100644 --- a/src/emqx_sysmon.erl +++ b/src/emqx_sysmon.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -28,10 +28,10 @@ %%-define(LOG_FMT, [{formatter_config, [time, " ", message, "\n"]}]). -define(LOG(Msg, ProcInfo), - lager:warning([{sysmon, true}], "[SYSMON] ~s~n~p", [WarnMsg, ProcInfo])). + emqx_log:warning([{sysmon, true}], "[SYSMON] ~s~n~p", [WarnMsg, ProcInfo])). -define(LOG(Msg, ProcInfo, PortInfo), - lager:warning([{sysmon, true}], "[SYSMON] ~s~n~p~n~p", [WarnMsg, ProcInfo, PortInfo])). + emqx_log:warning([{sysmon, true}], "[SYSMON] ~s~n~p~n~p", [WarnMsg, ProcInfo, PortInfo])). %% @doc Start system monitor -spec(start_link(Opts :: list(tuple())) -> {ok, pid()} | ignore | {error, term()}). @@ -75,11 +75,11 @@ parse_opt([_Opt|Opts], Acc) -> parse_opt(Opts, Acc). handle_call(Req, _From, State) -> - lager:error("[SYSMON] Unexpected Call: ~p", [Req]), + emqx_log:error("[SYSMON] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - lager:error("[SYSMON] Unexpected Cast: ~p", [Msg]), + emqx_log:error("[SYSMON] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info({monitor, Pid, long_gc, Info}, State) -> diff --git a/src/emqx_sysmon_sup.erl b/src/emqx_sysmon_sup.erl index d4aa3aa98..bdaf1c48d 100644 --- a/src/emqx_sysmon_sup.erl +++ b/src/emqx_sysmon_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -28,7 +28,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - {ok, Env} = emqx:env(sysmon), + {ok, Env} = emqx_conf:get_env(sysmon), Sysmon = {sysmon, {emqx_sysmon, start_link, [Env]}, permanent, 5000, worker, [emqx_sysmon]}, {ok, {{one_for_one, 10, 100}, [Sysmon]}}. diff --git a/src/emqx_tables.erl b/src/emqx_tables.erl index d971b99cf..78993104c 100644 --- a/src/emqx_tables.erl +++ b/src/emqx_tables.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_time.erl b/src/emqx_time.erl index 4a92e12ae..cfcfadf63 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index bc7c52f9e..db7332be7 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index 74284c35a..510bd0439 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -53,12 +53,12 @@ trace(publish, From, _Msg) when is_atom(From) -> ignore; trace(publish, #client{client_id = ClientId, username = Username}, #message{topic = Topic, payload = Payload}) -> - lager:info([{client, ClientId}, {topic, Topic}], - "~s/~s PUBLISH to ~s: ~p", [ClientId, Username, Topic, Payload]); + emqx_log:info([{client, ClientId}, {topic, Topic}], + "~s/~s PUBLISH to ~s: ~p", [ClientId, Username, Topic, Payload]); trace(publish, From, #message{topic = Topic, payload = Payload}) when is_binary(From); is_list(From) -> - lager:info([{client, From}, {topic, Topic}], - "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). + emqx_log:info([{client, From}, {topic, Topic}], + "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). %%-------------------------------------------------------------------- %% Start/Stop Trace @@ -83,14 +83,15 @@ stop_trace({topic, Topic}) -> %% @doc Lookup all traces. -spec(all_traces() -> [{Who :: trace_who(), LogFile :: string()}]). -all_traces() -> gen_server:call(?MODULE, all_traces). +all_traces() -> + gen_server:call(?MODULE, all_traces). %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- init([]) -> - {ok, #state{level = emqx:env(trace_level, debug), traces = #{}}}. + {ok, #state{level = emqx_conf:get_env(trace_level, debug), traces = #{}}}. handle_call({start_trace, Who, LogFile}, _From, State = #state{level = Level, traces = Traces}) -> case lager:trace_file(LogFile, [Who], Level, ?OPTIONS) of diff --git a/src/emqx_tracer_sup.erl b/src/emqx_tracer_sup.erl index 35c0b1d5b..bc5faa82c 100644 --- a/src/emqx_tracer_sup.erl +++ b/src/emqx_tracer_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index 9f13ba9d2..fc818a81b 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_vm.erl b/src/emqx_vm.erl index 0bf7a6c30..c367ddbc4 100644 --- a/src/emqx_vm.erl +++ b/src/emqx_vm.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl index 21b4ffcec..0fbea0639 100644 --- a/src/emqx_ws.erl +++ b/src/emqx_ws.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -26,8 +26,8 @@ -record(wsocket_state, {peername, client_pid, max_packet_size, parser}). -define(WSLOG(Level, Format, Args, State), - lager:Level("WsClient(~s): " ++ Format, - [esockd_net:format(State#wsocket_state.peername) | Args])). + emqx_log:Level("WsClient(~s): " ++ Format, + [esockd_net:format(State#wsocket_state.peername) | Args])). handle_request(Req) -> @@ -38,14 +38,14 @@ handle_request(Req) -> %%-------------------------------------------------------------------- handle_request('GET', "/mqtt", Req) -> - lager:debug("WebSocket Connection from: ~s", [Req:get(peer)]), + emqx_log:debug("WebSocket Connection from: ~s", [Req:get(peer)]), Upgrade = Req:get_header_value("Upgrade"), Proto = check_protocol_header(Req), case {is_websocket(Upgrade), Proto} of {true, "mqtt" ++ _Vsn} -> case Req:get(peername) of {ok, Peername} -> - {ok, ProtoEnv} = emqx:env(protocol), + {ok, ProtoEnv} = emqx_conf:get_env(protocol), PacketSize = get_value(max_packet_size, ProtoEnv, ?MAX_PACKET_SIZE), Parser = emqx_parser:initial_state(PacketSize), %% Upgrade WebSocket. @@ -56,27 +56,27 @@ handle_request('GET', "/mqtt", Req) -> max_packet_size = PacketSize, client_pid = ClientPid}); {error, Reason} -> - lager:error("Get peername with error ~s", [Reason]), + emqx_log:error("Get peername with error ~s", [Reason]), Req:respond({400, [], <<"Bad Request">>}) end; {false, _} -> - lager:error("Not WebSocket: Upgrade = ~s", [Upgrade]), + emqx_log:error("Not WebSocket: Upgrade = ~s", [Upgrade]), Req:respond({400, [], <<"Bad Request">>}); {_, Proto} -> - lager:error("WebSocket with error Protocol: ~s", [Proto]), + emqx_log:error("WebSocket with error Protocol: ~s", [Proto]), Req:respond({400, [], <<"Bad WebSocket Protocol">>}) end; handle_request(Method, Path, Req) -> - lager:error("Unexpected WS Request: ~s ~s", [Method, Path]), + emqx_log:error("Unexpected WS Request: ~s ~s", [Method, Path]), Req:not_found(). is_websocket(Upgrade) -> - (not emqx:env(websocket_check_upgrade_header, true)) orelse + (not emqx_conf:get_env(websocket_check_upgrade_header, true)) orelse (Upgrade =/= undefined andalso string:to_lower(Upgrade) =:= "websocket"). check_protocol_header(Req) -> - case emqx:env(websocket_protocol_header, false) of + case emqx_conf:get_env(websocket_protocol_header, false) of true -> get_protocol_header(Req); false -> "mqtt-v3.1.1" end. diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index fc632cb6a..643f8d825 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -50,8 +50,8 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). -define(WSLOG(Level, Format, Args, State), - lager:Level("WsClient(~s): " ++ Format, - [esockd_net:format(State#wsclient_state.peername) | Args])). + emqx_log:Level("WsClient(~s): " ++ Format, + [esockd_net:format(State#wsclient_state.peername) | Args])). %% @doc Start WebSocket Client. start_link(Env, WsPid, Req, ReplyChannel) -> diff --git a/src/emqx_ws_connection_sup.erl b/src/emqx_ws_connection_sup.erl index 330272102..5073c8002 100644 --- a/src/emqx_ws_connection_sup.erl +++ b/src/emqx_ws_connection_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright © 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2013-2018 EMQ Inc. 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. @@ -37,7 +37,7 @@ start_connection(WsPid, Req, ReplyChannel) -> init([]) -> %%TODO: Cannot upgrade the environments, Use zone? - Env = lists:append(emqx:env(client, []), emqx:env(protocol, [])), + Env = lists:append(emqx_conf:get_env(client, []), emqx_conf:get_env(protocol, [])), {ok, {{simple_one_for_one, 0, 1}, [{ws_connection, {emqx_ws_connection, start_link, [Env]}, temporary, 5000, worker, [emqx_ws_connection]}]}}. From c194e82807809aef3fa99593c8a6c21f897e249d Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 9 Apr 2018 14:31:34 +0800 Subject: [PATCH 042/520] Export dispatch/2, dispatch/3 for RPC --- src/emqx_broker.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 7cd80a33e..cb422b9e4 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -26,6 +26,8 @@ -export([publish/1, publish/2]). +-export([dispatch/2, dispatch/3]). + -export([subscriptions/1, subscribers/1, subscribed/2]). -export([topics/0]). @@ -128,7 +130,7 @@ route(Routes, Delivery) -> forward(Node, To, Delivery) -> case emqx_rpc:call(Node, ?BROKER, dispatch, [To, Delivery]) of {badrpc, Reason} -> - emqx_log:error("[Broker] Failed to forward msg to ~s: ~s", [Node, Reason]), + emqx_log:error("[Broker] Failed to forward msg to ~s: ~p", [Node, Reason]), Delivery; Delivery1 -> Delivery1 end. From a902f508b5858e3018ea92795917367cc15749c2 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 9 Apr 2018 14:32:49 +0800 Subject: [PATCH 043/520] Use emqx_config:get_env/1 to read env --- src/emqx_access_control.erl | 4 ++-- src/emqx_app.erl | 2 +- src/emqx_bridge_sup_sup.erl | 2 +- src/emqx_cm.erl | 14 ++++---------- src/emqx_gc.erl | 2 +- src/emqx_log.erl | 20 +++++++++++++++----- src/emqx_modules.erl | 4 ++-- src/emqx_mqtt.erl | 9 +++++---- src/emqx_plugins.erl | 16 ++++++++-------- src/emqx_protocol.erl | 2 +- src/emqx_session.erl | 4 ++-- src/emqx_sysmon_sup.erl | 2 +- src/emqx_tracer.erl | 2 +- src/emqx_ws.erl | 6 +++--- src/emqx_ws_connection_sup.erl | 2 +- 15 files changed, 48 insertions(+), 43 deletions(-) diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 7f665211e..988a625dc 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -51,7 +51,7 @@ start_link() -> auth(Client, Password) when is_record(Client, client) -> auth(Client, Password, lookup_mods(auth)). auth(_Client, _Password, []) -> - case emqx_conf:get_env(allow_anonymous, false) of + case emqx_config:get_env(allow_anonymous, false) of true -> ok; false -> {error, "No auth module to check!"} end; @@ -73,7 +73,7 @@ check_acl(Client, PubSub, Topic) when ?PS(PubSub) -> check_acl(Client, PubSub, Topic, lookup_mods(acl)). check_acl(_Client, _PubSub, _Topic, []) -> - emqx_conf:get_env(acl_nomatch, allow); + emqx_config:get_env(acl_nomatch, allow); check_acl(Client, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) -> case Mod:check_acl({Client, PubSub, Topic}, State) of allow -> allow; diff --git a/src/emqx_app.erl b/src/emqx_app.erl index fc1774a7b..a4c0493d8 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -63,7 +63,7 @@ print_vsn() -> %%-------------------------------------------------------------------- register_acl_mod() -> - case emqx_conf:get_env(acl_file) of + case emqx_config:get_env(acl_file) of {ok, File} -> emqx_access_control:register_mod(acl, emqx_acl_internal, [File]); undefined -> ok end. diff --git a/src/emqx_bridge_sup_sup.erl b/src/emqx_bridge_sup_sup.erl index 88e75792e..7c26cd047 100644 --- a/src/emqx_bridge_sup_sup.erl +++ b/src/emqx_bridge_sup_sup.erl @@ -46,7 +46,7 @@ start_bridge(Node, Topic) when is_atom(Node) andalso is_binary(Topic) -> start_bridge(Node, _Topic, _Options) when Node =:= node() -> {error, bridge_to_self}; start_bridge(Node, Topic, Options) when is_atom(Node) andalso is_binary(Topic) -> - {ok, BridgeEnv} = emqx_conf:get_env(bridge), + {ok, BridgeEnv} = emqx_config:get_env(bridge), Options1 = emqx_misc:merge_opts(BridgeEnv, Options), supervisor:start_child(?MODULE, bridge_spec(Node, Topic, Options1)). diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 568cbd951..5a2ff93b4 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -20,7 +20,7 @@ -include("emqx.hrl"). --export([start_link/1]). +-export([start_link/0]). -export([lookup/1, reg/1, unreg/1]). @@ -36,9 +36,9 @@ %%-------------------------------------------------------------------- %% @doc Start the client manager --spec(start_link(fun()) -> {ok, pid()} | ignore | {error, term()}). -start_link(StatsFun) -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [StatsFun], []). +-spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %% @doc Lookup ClientPid by ClientId -spec(lookup(client_id()) -> pid() | undefined). @@ -102,9 +102,6 @@ handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> {noreply, State} end; -handle_info(stats, State) -> - {noreply, setstats(State), hibernate}; - handle_info(Info, State) -> emqx_log:error("[CM] Unexpected info: ~p", [Info]), {noreply, State}. @@ -132,6 +129,3 @@ erase_monitor(MRef, State = #state{monitors = Monitors}) -> erlang:demonitor(MRef), State#state{monitors = dict:erase(MRef, Monitors)}. -setstats(State = #state{stats_fun = StatsFun}) -> - StatsFun(ets:info(client, size)), State. - diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index e0fbed270..4901cce5e 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -23,7 +23,7 @@ -spec(conn_max_gc_count() -> integer()). conn_max_gc_count() -> - case emqx:env(conn_force_gc_count) of + case emqx_config:get_env(conn_force_gc_count) of {ok, I} when I > 0 -> I + rand:uniform(I); {ok, I} when I =< 0 -> undefined; undefined -> undefined diff --git a/src/emqx_log.erl b/src/emqx_log.erl index 16b70b676..7f85b3309 100644 --- a/src/emqx_log.erl +++ b/src/emqx_log.erl @@ -18,34 +18,44 @@ -compile({no_auto_import,[error/1]}). --export([debug/1, debug/2, - info/1, info/2, - warning/1, warning/2, - error/1, error/2, - critical/1, critical/2]). +-export([debug/1, debug/2, debug/3, + info/1, info/2, info/3, + warning/1, warning/2, warning/3, + error/1, error/2, error/3, + critical/1, critical/2, critical/3]). debug(Msg) -> lager:debug(Msg). debug(Format, Args) -> lager:debug(Format, Args). +debug(Metadata, Format, Args) when is_list(Metadata) -> + lager:debug(Metadata, Format, Args). info(Msg) -> lager:info(Msg). info(Format, Args) -> lager:info(Format, Args). +info(Metadata, Format, Args) when is_list(Metadata) -> + lager:info(Metadata, Format, Args). warning(Msg) -> lager:warning(Msg). warning(Format, Args) -> lager:warning(Format, Args). +warning(Metadata, Format, Args) when is_list(Metadata) -> + lager:warning(Metadata, Format, Args). error(Msg) -> lager:error(Msg). error(Format, Args) -> lager:error(Format, Args). +error(Metadata, Format, Args) when is_list(Metadata) -> + lager:error(Metadata, Format, Args). critical(Msg) -> lager:critical(Msg). critical(Format, Args) -> lager:critical(Format, Args). +critical(Metadata, Format, Args) when is_list(Metadata) -> + lager:critical(Metadata, Format, Args). diff --git a/src/emqx_modules.erl b/src/emqx_modules.erl index a7407abcc..31dbd3c4a 100644 --- a/src/emqx_modules.erl +++ b/src/emqx_modules.erl @@ -23,11 +23,11 @@ load() -> fun({Mod, Env}) -> ok = Mod:load(Env), io:format("Load ~s module successfully.~n", [Mod]) - end, emqx_conf:get_env(modules, [])). + end, emqx_config:get_env(modules, [])). unload() -> lists:foreach( fun({Mod, Env}) -> Mod:unload(Env) end, - emqx_conf:get_env(modules, [])). + emqx_config:get_env(modules, [])). diff --git a/src/emqx_mqtt.erl b/src/emqx_mqtt.erl index 664fc88a0..d7a7281cf 100644 --- a/src/emqx_mqtt.erl +++ b/src/emqx_mqtt.erl @@ -40,7 +40,7 @@ shutdown() -> %% @doc Start Listeners. -spec(start_listeners() -> ok). start_listeners() -> - lists:foreach(fun start_listener/1, emqx_conf:get_env(listeners, [])). + lists:foreach(fun start_listener/1, emqx_config:get_env(listeners, [])). %% Start mqtt listener -spec(start_listener(listener()) -> {ok, pid()} | {error, any()}). @@ -60,7 +60,8 @@ start_listener({Proto, ListenOn, Opts}) when Proto == https; Proto == wss -> {ok, _} = mochiweb:start_http('mqtt:wss', ListenOn, Opts, {emqx_ws, handle_request, []}). start_listener(Proto, ListenOn, Opts) -> - Env = lists:append(emqx_conf:get_env(client, []), emqx_conf:get_env(protocol, [])), + Env = lists:append(emqx_config:get_env(client, []), + emqx_config:get_env(protocol, [])), MFArgs = {emqx_connection, start_link, [Env]}, {ok, _} = esockd:open(Proto, ListenOn, merge_sockopts(Opts), MFArgs). @@ -76,7 +77,7 @@ is_mqtt(_Proto) -> false. %% @doc Stop Listeners -spec(stop_listeners() -> ok). stop_listeners() -> - lists:foreach(fun stop_listener/1, emqx_conf:get_env(listeners, [])). + lists:foreach(fun stop_listener/1, emqx_config:get_env(listeners, [])). -spec(stop_listener(listener()) -> ok | {error, any()}). stop_listener({tcp, ListenOn, _Opts}) -> @@ -96,7 +97,7 @@ stop_listener({Proto, ListenOn, _Opts}) -> -spec(restart_listeners() -> ok). restart_listeners() -> lists:foreach(fun restart_listener/1, - emqx_conf:get_env(listeners, [])). + emqx_config:get_env(listeners, [])). -spec(restart_listener(listener()) -> any()). restart_listener({tcp, ListenOn, _Opts}) -> diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index 9e5f0dff3..3d40d1849 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -31,7 +31,7 @@ %% @doc Init plugins' config -spec(init() -> ok). init() -> - case emqx_conf:get_env(plugins_etc_dir) of + case emqx_config:get_env(plugins_etc_dir) of {ok, PluginsEtc} -> CfgFiles = [filename:join(PluginsEtc, File) || File <- filelib:wildcard("*.config", PluginsEtc)], @@ -50,7 +50,7 @@ init_config(CfgFile) -> -spec(load() -> list() | {error, term()}). load() -> load_expand_plugins(), - case emqx_conf:get_env(plugins_loaded_file) of + case emqx_config:get_env(plugins_loaded_file) of {ok, File} -> ensure_file(File), with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end); @@ -60,7 +60,7 @@ load() -> end. load_expand_plugins() -> - case emqx_conf:get_env(expand_plugins_dir) of + case emqx_config:get_env(expand_plugins_dir) of {ok, Dir} -> PluginsDir = filelib:wildcard("*", Dir), lists:foreach(fun(PluginDir) -> @@ -101,7 +101,7 @@ init_expand_plugin_config(PluginDir) -> end, AppsEnv). get_expand_plugin_config() -> - case emqx_conf:get_env(expand_plugins_dir) of + case emqx_config:get_env(expand_plugins_dir) of {ok, Dir} -> PluginsDir = filelib:wildcard("*", Dir), lists:foldl(fun(PluginDir, Acc) -> @@ -144,7 +144,7 @@ load_plugins(Names, Persistent) -> %% @doc Unload all plugins before broker stopped. -spec(unload() -> list() | {error, term()}). unload() -> - case emqx_conf:get_env(plugins_loaded_file) of + case emqx_config:get_env(plugins_loaded_file) of {ok, File} -> with_loaded_file(File, fun stop_plugins/1); undefined -> @@ -158,7 +158,7 @@ stop_plugins(Names) -> %% @doc List all available plugins -spec(list() -> [plugin()]). list() -> - case emqx_conf:get_env(plugins_etc_dir) of + case emqx_config:get_env(plugins_etc_dir) of {ok, PluginsEtc} -> CfgFiles = filelib:wildcard("*.{conf,config}", PluginsEtc) ++ get_expand_plugin_config(), Plugins = [plugin(CfgFile) || CfgFile <- CfgFiles], @@ -313,7 +313,7 @@ plugin_unloaded(Name, true) -> end. read_loaded() -> - case emqx_conf:get_env(plugins_loaded_file) of + case emqx_config:get_env(plugins_loaded_file) of {ok, File} -> read_loaded(File); undefined -> {error, not_found} end. @@ -321,7 +321,7 @@ read_loaded() -> read_loaded(File) -> file:consult(File). write_loaded(AppNames) -> - {ok, File} = emqx_conf:get_env(plugins_loaded_file), + {ok, File} = emqx_config:get_env(plugins_loaded_file), case file:open(File, [binary, write]) of {ok, Fd} -> lists:foreach(fun(Name) -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 0b0abb903..a86c19459 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -549,7 +549,7 @@ authenticate(Client, Password) -> %% PUBLISH ACL is cached in process dictionary. check_acl(publish, Topic, Client) -> - IfCache = emqx_conf:get_env(cache_acl, true), + IfCache = emqx_config:get_env(cache_acl, true), case {IfCache, get({acl, publish, Topic})} of {true, undefined} -> AllowDeny = emqx_access_control:check_acl(Client, publish, Topic), diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 208af034b..f6f88de40 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -271,8 +271,8 @@ init(#{clean_start := CleanStart, process_flag(trap_exit, true), true = link(ClientPid), init_stats([deliver_msg, enqueue_msg]), - {ok, Env} = emqx_conf:get_env(session), - {ok, QEnv} = emqx_conf:get_env(mqueue), + {ok, Env} = emqx_config:get_env(session), + {ok, QEnv} = emqx_config:get_env(mqueue), MaxInflight = get_value(max_inflight, Env, 0), EnableStats = get_value(enable_stats, Env, false), IgnoreLoopDeliver = get_value(ignore_loop_deliver, Env, false), diff --git a/src/emqx_sysmon_sup.erl b/src/emqx_sysmon_sup.erl index bdaf1c48d..0c4566bc3 100644 --- a/src/emqx_sysmon_sup.erl +++ b/src/emqx_sysmon_sup.erl @@ -28,7 +28,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - {ok, Env} = emqx_conf:get_env(sysmon), + {ok, Env} = emqx_config:get_env(sysmon), Sysmon = {sysmon, {emqx_sysmon, start_link, [Env]}, permanent, 5000, worker, [emqx_sysmon]}, {ok, {{one_for_one, 10, 100}, [Sysmon]}}. diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index 510bd0439..1d0211e78 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -91,7 +91,7 @@ all_traces() -> %%-------------------------------------------------------------------- init([]) -> - {ok, #state{level = emqx_conf:get_env(trace_level, debug), traces = #{}}}. + {ok, #state{level = emqx_config:get_env(trace_level, debug), traces = #{}}}. handle_call({start_trace, Who, LogFile}, _From, State = #state{level = Level, traces = Traces}) -> case lager:trace_file(LogFile, [Who], Level, ?OPTIONS) of diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl index 0fbea0639..b2bdc2e54 100644 --- a/src/emqx_ws.erl +++ b/src/emqx_ws.erl @@ -45,7 +45,7 @@ handle_request('GET', "/mqtt", Req) -> {true, "mqtt" ++ _Vsn} -> case Req:get(peername) of {ok, Peername} -> - {ok, ProtoEnv} = emqx_conf:get_env(protocol), + {ok, ProtoEnv} = emqx_config:get_env(protocol), PacketSize = get_value(max_packet_size, ProtoEnv, ?MAX_PACKET_SIZE), Parser = emqx_parser:initial_state(PacketSize), %% Upgrade WebSocket. @@ -72,11 +72,11 @@ handle_request(Method, Path, Req) -> Req:not_found(). is_websocket(Upgrade) -> - (not emqx_conf:get_env(websocket_check_upgrade_header, true)) orelse + (not emqx_config:get_env(websocket_check_upgrade_header, true)) orelse (Upgrade =/= undefined andalso string:to_lower(Upgrade) =:= "websocket"). check_protocol_header(Req) -> - case emqx_conf:get_env(websocket_protocol_header, false) of + case emqx_config:get_env(websocket_protocol_header, false) of true -> get_protocol_header(Req); false -> "mqtt-v3.1.1" end. diff --git a/src/emqx_ws_connection_sup.erl b/src/emqx_ws_connection_sup.erl index 5073c8002..b58e7c956 100644 --- a/src/emqx_ws_connection_sup.erl +++ b/src/emqx_ws_connection_sup.erl @@ -37,7 +37,7 @@ start_connection(WsPid, Req, ReplyChannel) -> init([]) -> %%TODO: Cannot upgrade the environments, Use zone? - Env = lists:append(emqx_conf:get_env(client, []), emqx_conf:get_env(protocol, [])), + Env = lists:append(emqx_config:get_env(client, []), emqx_config:get_env(protocol, [])), {ok, {{simple_one_for_one, 0, 1}, [{ws_connection, {emqx_ws_connection, start_link, [Env]}, temporary, 5000, worker, [emqx_ws_connection]}]}}. From bbb66ff92edab89f79b10ce4fc95ecb5e586a7d5 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 10 Apr 2018 18:17:07 +0800 Subject: [PATCH 044/520] Refactor the broker, router modules --- include/emqx.hrl | 7 +-- src/emqx_broker.erl | 122 +++++++++++++++++++++---------------- src/emqx_broker_sup.erl | 20 +++--- src/emqx_router.erl | 82 +++++++++++-------------- src/emqx_router_helper.erl | 29 ++++----- src/emqx_shared_sub.erl | 29 ++++++--- src/emqx_trie.erl | 44 ++++++------- test/emqx_trie_SUITE.erl | 2 +- 8 files changed, 174 insertions(+), 161 deletions(-) diff --git a/include/emqx.hrl b/include/emqx.hrl index a0b96c1c7..cb06118da 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -154,10 +154,7 @@ %% Route %%-------------------------------------------------------------------- --record(route, - { topic :: topic(), - dest :: {binary(), node()} | node() - }). +-record(route, { topic :: topic(), dest }). -type(route() :: #route{}). @@ -170,7 +167,7 @@ -record(trie_node, { node_id :: trie_node_id(), edge_count = 0 :: non_neg_integer(), - topic :: binary() | undefined, + topic :: topic() | undefined, flags :: list(atom()) }). diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index cb422b9e4..79f7911f7 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -32,30 +32,33 @@ -export([topics/0]). --export([getopts/2, setopts/3]). - --export([dump/0]). +-export([get_subopts/2, set_subopts/3]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {pool, id, subids :: map(), submon :: emqx_pmon:pmon()}). +-record(state, {pool, id, submon}). -define(BROKER, ?MODULE). -define(TIMEOUT, 120000). +%% ETS tables +-define(SUBOPTION, emqx_suboption). +-define(SUBSCRIBER, emqx_subscriber). +-define(SUBSCRIPTION, emqx_subscription). + %%-------------------------------------------------------------------- %% Start a broker %%-------------------------------------------------------------------- -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> - gen_server:start_link(?MODULE, [Pool, Id], [{hibernate_after, 1000}]). + gen_server:start_link(?MODULE, [Pool, Id], [{hibernate_after, 2000}]). %%-------------------------------------------------------------------- -%% Sub/Unsub +%% Subscriber/Unsubscribe %%-------------------------------------------------------------------- -spec(subscribe(topic()) -> ok | {error, term()}). @@ -108,7 +111,7 @@ publish(Msg = #message{from = From}) -> end. publish(Topic, Msg) -> - route(emqx_router:match_routes(Topic), delivery(Msg)). + route(aggre(emqx_router:match_routes(Topic)), delivery(Msg)). route([], Delivery = #delivery{message = Msg}) -> emqx_hooks:run('message.dropped', [undefined, Msg]), @@ -120,14 +123,29 @@ route([{To, Node}], Delivery) when Node =:= node() -> route([{To, Node}], Delivery = #delivery{flows = Flows}) when is_atom(Node) -> forward(Node, To, Delivery#delivery{flows = [{route, Node, To}|Flows]}); -route([{To, Group}], Delivery) when is_binary(Group) -> - emqx_shared_sub:dispatch(Group, To, Delivery); +route([{To, Shared}], Delivery) when is_tuple(Shared); is_binary(Shared) -> + emqx_shared_sub:dispatch(Shared, To, Delivery); route(Routes, Delivery) -> lists:foldl(fun(Route, Acc) -> route([Route], Acc) end, Delivery, Routes). +aggre([]) -> + []; +aggre([{To, Dest}]) -> + [{To, Dest}]; +aggre(Routes) -> + lists:foldl( + fun({To, Node}, Acc) when is_atom(Node) -> + [{To, Node} | Acc]; + ({To, {Group, _Node}}, Acc) -> + lists:usort([{To, Group} | Acc]); + ({To, {Cluster, Group, _Node}}, Acc) -> + lists:usort([{To, {Cluster, Group}} | Acc]) + end, [], Routes). + %% @doc Forward message to another node. forward(Node, To, Delivery) -> + %% rpc:call to ensure the delivery, but the latency:( case emqx_rpc:call(Node, ?BROKER, dispatch, [To, Delivery]) of {badrpc, Reason} -> emqx_log:error("[Broker] Failed to forward msg to ~s: ~p", [Node, Reason]), @@ -169,33 +187,37 @@ delivery(Msg) -> #delivery{message = Msg, flows = []}. subscribers(Topic) -> - try ets:lookup_element(subscriber, Topic, 2) catch error:badarg -> [] end. + try ets:lookup_element(?SUBSCRIBER, Topic, 2) catch error:badarg -> [] end. subscriptions(Subscriber) -> lists:map(fun({_, {share, _Group, Topic}}) -> subscription(Topic, Subscriber); ({_, Topic}) -> subscription(Topic, Subscriber) - end, ets:lookup(subscription, Subscriber)). + end, ets:lookup(?SUBSCRIPTION, Subscriber)). subscription(Topic, Subscriber) -> - {Topic, ets:lookup_element(suboption, {Topic, Subscriber}, 2)}. + {Topic, ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2)}. -spec(subscribed(topic(), subscriber()) -> boolean()). subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> - ets:member(suboption, {Topic, SubPid}); + ets:member(?SUBOPTION, {Topic, SubPid}); subscribed(Topic, SubId) when is_binary(Topic), is_binary(SubId) -> - length(ets:match_object(suboption, {{Topic, {SubId, '_'}}, '_'}, 1)) == 1; -subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), is_binary(SubId), is_pid(SubPid) -> - ets:member(suboption, {Topic, {SubId, SubPid}}). + length(ets:match_object(?SUBOPTION, {{Topic, {SubId, '_'}}, '_'}, 1)) == 1; +subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), + is_binary(SubId), + is_pid(SubPid) -> + ets:member(?SUBOPTION, {Topic, {SubId, SubPid}}). topics() -> emqx_router:topics(). -getopts(Topic, Subscriber) when is_binary(Topic) -> - try ets:lookup_element(suboption, {Topic, Subscriber}, 2) catch error:badarg ->[] end. +get_subopts(Topic, Subscriber) when is_binary(Topic) -> + try ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2) + catch error:badarg -> [] + end. -setopts(Topic, Subscriber, Opts) when is_binary(Topic), is_list(Opts) -> - gen_server:call(pick(Subscriber), {setopts, Topic, Subscriber, Opts}). +set_subopts(Topic, Subscriber, Opts) when is_binary(Topic), is_list(Opts) -> + gen_server:call(pick(Subscriber), {set_subopts, Topic, Subscriber, Opts}). with_subpid(SubPid) when is_pid(SubPid) -> SubPid; @@ -220,22 +242,19 @@ pick(SubId) when is_binary(SubId) -> pick({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> pick(SubId). -dump() -> - [{Tab, ets:tab2list(Tab)} || Tab <- [subscription, subscriber, suboption]]. - %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- init([Pool, Id]) -> gproc_pool:connect_worker(Pool, {Pool, Id}), - {ok, #state{pool = Pool, id = Id, subids = #{}, submon = emqx_pmon:new()}}. + {ok, #state{pool = Pool, id = Id, submon = emqx_pmon:new()}}. -handle_call({setopts, Topic, Subscriber, Opts}, _From, State) -> - case ets:lookup(suboption, {Topic, Subscriber}) of +handle_call({set_subopts, Topic, Subscriber, Opts}, _From, State) -> + case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of [{_, OldOpts}] -> Opts1 = lists:usort(lists:umerge(Opts, OldOpts)), - ets:insert(suboption, {{Topic, Subscriber}, Opts1}), + ets:insert(?SUBOPTION, {{Topic, Subscriber}, Opts1}), {reply, ok, State}; [] -> {reply, {error, not_found}, State} @@ -246,12 +265,12 @@ handle_call(Request, _From, State) -> {reply, ignore, State}. handle_cast({From, {subscribe, Topic, Subscriber, Options}}, State) -> - case ets:lookup(suboption, {Topic, Subscriber}) of + case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of [] -> Group = proplists:get_value(share, Options), true = do_subscribe(Group, Topic, Subscriber, Options), emqx_shared_sub:subscribe(Group, Topic, subpid(Subscriber)), - emqx_router:add_route(From, Topic, dest(Options)), + emqx_router:add_route(From, Topic, destination(Options)), {noreply, monitor_subscriber(Subscriber, State)}; [_] -> gen_server:reply(From, ok), @@ -259,13 +278,13 @@ handle_cast({From, {subscribe, Topic, Subscriber, Options}}, State) -> end; handle_cast({From, {unsubscribe, Topic, Subscriber}}, State) -> - case ets:lookup(suboption, {Topic, Subscriber}) of + case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of [{_, Options}] -> Group = proplists:get_value(share, Options), true = do_unsubscribe(Group, Topic, Subscriber), emqx_shared_sub:unsubscribe(Group, Topic, subpid(Subscriber)), - case ets:member(subscriber, Topic) of - false -> emqx_router:del_route(From, Topic, dest(Options)); + case ets:member(?SUBSCRIBER, Topic) of + false -> emqx_router:del_route(From, Topic, destination(Options)); true -> gen_server:reply(From, ok) end; [] -> gen_server:reply(From, ok) @@ -276,23 +295,24 @@ handle_cast(Msg, State) -> emqx_log:error("[Broker] Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{subids = SubIds}) -> - Subscriber = case maps:find(SubPid, SubIds) of - {ok, SubId} -> {SubId, SubPid}; - error -> SubPid +handle_info({'DOWN', _MRef, process, SubPid, _Reason}, + State = #state{submon = SubMon}) -> + Subscriber = case SubMon:find(SubPid) of + undefined -> SubPid; + SubId -> {SubId, SubPid} end, Topics = lists:map(fun({_, {share, _, Topic}}) -> Topic; ({_, Topic}) -> Topic - end, ets:lookup(subscription, Subscriber)), + end, ets:lookup(?SUBSCRIPTION, Subscriber)), lists:foreach(fun(Topic) -> - case ets:lookup(suboption, {Topic, Subscriber}) of + case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of [{_, Options}] -> Group = proplists:get_value(share, Options), true = do_unsubscribe(Group, Topic, Subscriber), - case ets:member(subscriber, Topic) of - false -> emqx_router:del_route(Topic, dest(Options)); + case ets:member(?SUBSCRIBER, Topic) of + false -> emqx_router:del_route(Topic, destination(Options)); true -> ok end; [] -> ok @@ -315,25 +335,25 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- do_subscribe(Group, Topic, Subscriber, Options) -> - ets:insert(subscription, {Subscriber, shared(Group, Topic)}), - ets:insert(subscriber, {Topic, shared(Group, Subscriber)}), - ets:insert(suboption, {{Topic, Subscriber}, Options}). + ets:insert(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), + ets:insert(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}), + ets:insert(?SUBOPTION, {{Topic, Subscriber}, Options}). do_unsubscribe(Group, Topic, Subscriber) -> - ets:delete_object(subscription, {Subscriber, shared(Group, Topic)}), - ets:delete_object(subscriber, {Topic, shared(Group, Subscriber)}), - ets:delete(suboption, {Topic, Subscriber}). + ets:delete_object(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), + ets:delete_object(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}), + ets:delete(?SUBOPTION, {Topic, Subscriber}). monitor_subscriber(SubPid, State = #state{submon = SubMon}) when is_pid(SubPid) -> State#state{submon = SubMon:monitor(SubPid)}; -monitor_subscriber({SubId, SubPid}, State = #state{subids = SubIds, submon = SubMon}) -> - State#state{subids = maps:put(SubPid, SubId, SubIds), submon = SubMon:monitor(SubPid)}. +monitor_subscriber({SubId, SubPid}, State = #state{submon = SubMon}) -> + State#state{submon = SubMon:monitor(SubPid, SubId)}. -demonitor_subscriber(SubPid, State = #state{subids = SubIds, submon = SubMon}) -> - State#state{subids = maps:remove(SubPid, SubIds), submon = SubMon:demonitor(SubPid)}. +demonitor_subscriber(SubPid, State = #state{submon = SubMon}) -> + State#state{submon = SubMon:demonitor(SubPid)}. -dest(Options) -> +destination(Options) -> case proplists:get_value(share, Options) of undefined -> node(); Group -> {Group, node()} diff --git a/src/emqx_broker_sup.erl b/src/emqx_broker_sup.erl index b7e2b1c5b..84bde9c6d 100644 --- a/src/emqx_broker_sup.erl +++ b/src/emqx_broker_sup.erl @@ -33,7 +33,8 @@ start_link() -> init([]) -> %% Create the pubsub tables - lists:foreach(fun create_tab/1, [subscription, subscriber, suboption]), + lists:foreach(fun create_tab/1, + [subscription, subscriber, suboption]), %% Shared subscription Shared = {shared_sub, {emqx_shared_sub, start_link, []}, @@ -57,24 +58,17 @@ init([]) -> create_tab(suboption) -> %% Suboption: {Topic, Sub} -> [{qos, 1}] - ensure_tab(suboption, [set | ?CONCURRENCY_OPTS]); + emqx_tables:create(emqx_suboption, [public, set | ?CONCURRENCY_OPTS]); create_tab(subscriber) -> %% Subscriber: Topic -> Sub1, Sub2, Sub3, ..., SubN %% duplicate_bag: o(1) insert - ensure_tab(subscriber, [duplicate_bag | ?CONCURRENCY_OPTS]); + emqx_tables:create(emqx_subscriber, [public, duplicate_bag | ?CONCURRENCY_OPTS]); create_tab(subscription) -> %% Subscription: Sub -> Topic1, Topic2, Topic3, ..., TopicN %% bag: o(n) insert - ensure_tab(subscription, [bag | ?CONCURRENCY_OPTS]). - -ensure_tab(Tab, Opts) -> - case ets:info(Tab, name) of - undefined -> - ets:new(Tab, lists:usort([public, named_table | Opts])); - Tab -> Tab - end. + emqx_tables:create(emqx_subscription, [public, bag | ?CONCURRENCY_OPTS]). %%-------------------------------------------------------------------- %% Stats function @@ -83,8 +77,8 @@ ensure_tab(Tab, Opts) -> stats_fun() -> fun() -> emqx_stats:setstat('subscribers/count', 'subscribers/max', - ets:info(subscriber, size)), + ets:info(emqx_subscriber, size)), emqx_stats:setstat('subscriptions/count', 'subscriptions/max', - ets:info(subscription, size)) + ets:info(emqx_subscription, size)) end. diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 58aba1e68..7d16d12d8 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -20,6 +20,8 @@ -include("emqx.hrl"). +-include_lib("ekka/include/ekka.hrl"). + %% Mnesia Bootstrap -export([mnesia/1]). @@ -32,42 +34,44 @@ %% Topics -export([topics/0]). -%% Route Management APIs +%% Route management APIs -export([add_route/2, add_route/3, get_routes/1, del_route/2, del_route/3]). -%% Match, print routes -export([has_routes/1, match_routes/1, print_routes/1]). --export([dump/0]). - %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-type(group() :: binary()). + +-type(destination() :: node() | {group(), node()} + | {cluster(), group(), node()}). + -record(state, {pool, id}). --type(destination() :: node() | {binary(), node()}). +-define(ROUTE, emqx_route). %%-------------------------------------------------------------------- %% Mnesia Bootstrap %%-------------------------------------------------------------------- mnesia(boot) -> - ok = ekka_mnesia:create_table(route, [ + ok = ekka_mnesia:create_table(?ROUTE, [ {type, bag}, {ram_copies, [node()]}, {record_name, route}, {attributes, record_info(fields, route)}]); mnesia(copy) -> - ok = ekka_mnesia:copy_table(route). + ok = ekka_mnesia:copy_table(?ROUTE). %%-------------------------------------------------------------------- %% Start a router %%-------------------------------------------------------------------- start_link(Pool, Id) -> - gen_server:start_link(?MODULE, [Pool, Id], [{hibernate_after, 1000}]). + gen_server:start_link(?MODULE, [Pool, Id], [{hibernate_after, 10000}]). %%-------------------------------------------------------------------- %% Add/Del Routes @@ -85,7 +89,7 @@ add_route(From, Topic, Dest) when is_binary(Topic) -> %% @doc Get routes -spec(get_routes(topic()) -> [route()]). get_routes(Topic) -> - ets:lookup(route, Topic). + ets:lookup(?ROUTE, Topic). %% @doc Delete a route -spec(del_route(topic(), destination()) -> ok). @@ -99,45 +103,29 @@ del_route(From, Topic, Dest) when is_binary(Topic) -> %% @doc Has routes? -spec(has_routes(topic()) -> boolean()). has_routes(Topic) when is_binary(Topic) -> - ets:member(route, Topic). + ets:member(?ROUTE, Topic). %%-------------------------------------------------------------------- %% Topics %%-------------------------------------------------------------------- -spec(topics() -> list(binary())). -topics() -> - mnesia:dirty_all_keys(route). +topics() -> mnesia:dirty_all_keys(?ROUTE). %%-------------------------------------------------------------------- -%% Match Routes +%% Match routes %%-------------------------------------------------------------------- %% @doc Match routes +%% Optimize: routing table will be replicated to all router nodes. -spec(match_routes(Topic:: topic()) -> [{topic(), binary() | node()}]). match_routes(Topic) when is_binary(Topic) -> - %% Optimize: ets??? Matched = mnesia:ets(fun emqx_trie:match/1, [Topic]), - %% Optimize: route table will be replicated to all nodes. - aggre(lists:append([ets:lookup(route, To) || To <- [Topic | Matched]])). - -%% Aggregate routes -aggre([]) -> - []; -aggre([#route{topic = To, dest = Node}]) when is_atom(Node) -> - [{To, Node}]; -aggre([#route{topic = To, dest = {Group, _Node}}]) -> - [{To, Group}]; -aggre(Routes) -> - lists:foldl( - fun(#route{topic = To, dest = Node}, Acc) when is_atom(Node) -> - [{To, Node} | Acc]; - (#route{topic = To, dest = {Group, _}}, Acc) -> - lists:usort([{To, Group} | Acc]) - end, [], Routes). + Routes = [ets:lookup(?ROUTE, To) || To <- [Topic | Matched]], + [{To, Dest} || #route{topic = To, dest = Dest} <- lists:append(Routes)]. %%-------------------------------------------------------------------- -%% Print Routes +%% Print routes %%-------------------------------------------------------------------- %% @doc Print routes to a topic @@ -147,15 +135,16 @@ print_routes(Topic) -> io:format("~s -> ~s~n", [To, Dest]) end, match_routes(Topic)). +%%-------------------------------------------------------------------- +%% Utility functions +%%-------------------------------------------------------------------- + cast(Router, Msg) -> gen_server:cast(Router, Msg). pick(Topic) -> gproc_pool:pick_worker(router, Topic). -dump() -> - ets:tab2list(route). - %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- @@ -174,7 +163,7 @@ handle_cast({add_route, From, Route}, State) -> {noreply, State}; handle_cast({add_route, Route = #route{topic = Topic, dest = Dest}}, State) -> - case lists:member(Route, ets:lookup(route, Topic)) of + case lists:member(Route, ets:lookup(?ROUTE, Topic)) of true -> ok; false -> ok = emqx_router_helper:monitor(Dest), @@ -192,7 +181,7 @@ handle_cast({del_route, From, Route}, State) -> handle_cast({del_route, Route = #route{topic = Topic}}, State) -> %% Confirm if there are still subscribers... - case ets:member(subscriber, Topic) of + case ets:member(emqx_subscriber, Topic) of true -> ok; false -> case emqx_topic:wildcard(Topic) of @@ -206,7 +195,8 @@ handle_cast(Msg, State) -> emqx_log:error("[Router] Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info(_Info, State) -> +handle_info(Info, State) -> + emqx_log:error("[Router] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> @@ -216,29 +206,29 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- -%% Internal Functions +%% Internal functions %%-------------------------------------------------------------------- add_direct_route(Route) -> - mnesia:async_dirty(fun mnesia:write/1, [Route]). + mnesia:async_dirty(fun mnesia:write/3, [?ROUTE, Route, sticky_write]). add_trie_route(Route = #route{topic = Topic}) -> - case mnesia:wread({route, Topic}) of + case mnesia:wread({?ROUTE, Topic}) of [] -> emqx_trie:insert(Topic); _ -> ok end, - mnesia:write(Route). + mnesia:write(?ROUTE, Route, sticky_write). del_direct_route(Route) -> - mnesia:async_dirty(fun mnesia:delete_object/1, [Route]). + mnesia:async_dirty(fun mnesia:delete_object/3, [?ROUTE, Route, sticky_write]). del_trie_route(Route = #route{topic = Topic}) -> - case mnesia:wread({route, Topic}) of + case mnesia:wread({?ROUTE, Topic}) of [Route] -> %% Remove route and trie - mnesia:delete_object(Route), + mnesia:delete_object(?ROUTE, Route, sticky_write), emqx_trie:delete(Topic); [_|_] -> %% Remove route only - mnesia:delete_object(Route); + mnesia:delete_object(?ROUTE, Route, sticky_write); [] -> ok end. diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index 071bc7481..5a4de5f69 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -41,7 +41,7 @@ -define(SERVER, ?MODULE). --define(TABLE, routing_node). +-define(TAB, emqx_routing_node). -define(LOCK, {?MODULE, clean_routes}). @@ -50,14 +50,14 @@ %%-------------------------------------------------------------------- mnesia(boot) -> - ok = ekka_mnesia:create_table(?TABLE, [ + ok = ekka_mnesia:create_table(?TAB, [ {type, set}, {ram_copies, [node()]}, {record_name, routing_node}, {attributes, record_info(fields, routing_node)}]); mnesia(copy) -> - ok = ekka_mnesia:copy_table(?TABLE). + ok = ekka_mnesia:copy_table(?TAB). %%-------------------------------------------------------------------- %% API @@ -73,9 +73,10 @@ start_link(StatsFun) -> monitor({_Group, Node}) -> monitor(Node); monitor(Node) when is_atom(Node) -> - case ekka:is_member(Node) orelse ets:member(?TABLE, Node) of + case ekka:is_member(Node) orelse ets:member(?TAB, Node) of true -> ok; - false -> mnesia:dirty_write(#routing_node{name = Node, ts = os:timestamp()}) + false -> + mnesia:dirty_write(?TAB, #routing_node{name = Node, ts = os:timestamp()}) end. %%-------------------------------------------------------------------- @@ -84,7 +85,7 @@ monitor(Node) when is_atom(Node) -> init([StatsFun]) -> ekka:monitor(membership), - mnesia:subscribe({table, ?TABLE, simple}), + mnesia:subscribe({table, ?TAB, simple}), Nodes = lists:foldl( fun(Node, Acc) -> case ekka:is_member(Node) of @@ -92,7 +93,7 @@ init([StatsFun]) -> false -> _ = erlang:monitor_node(Node, true), [Node | Acc] end - end, [], mnesia:dirty_all_keys(?TABLE)), + end, [], mnesia:dirty_all_keys(?TAB)), {ok, TRef} = timer:send_interval(timer:seconds(1), stats), {ok, #state{nodes = Nodes, stats_fun = StatsFun, stats_timer = TRef}}. @@ -119,9 +120,9 @@ handle_info({mnesia_table_event, _Event}, State) -> handle_info({nodedown, Node}, State = #state{nodes = Nodes}) -> global:trans({?LOCK, self()}, fun() -> - mnesia:transaction(fun clean_routes/1, [Node]) + mnesia:transaction(fun cleanup_routes/1, [Node]) end), - mnesia:dirty_delete(routing_node, Node), + mnesia:dirty_delete(?TAB, Node), handle_info(stats, State#state{nodes = lists:delete(Node, Nodes)}); handle_info({membership, {mnesia, down, Node}}, State) -> @@ -131,7 +132,7 @@ handle_info({membership, _Event}, State) -> {noreply, State}; handle_info(stats, State = #state{stats_fun = StatsFun}) -> - ok = StatsFun(mnesia:table_info(route, size)), + ok = StatsFun(mnesia:table_info(emqx_route, size)), {noreply, State, hibernate}; handle_info(Info, State) -> @@ -141,7 +142,7 @@ handle_info(Info, State) -> terminate(_Reason, #state{stats_timer = TRef}) -> timer:cancel(TRef), ekka:unmonitor(membership), - mnesia:unsubscribe({table, ?TABLE, simple}). + mnesia:unsubscribe({table, ?TAB, simple}). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -150,9 +151,9 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -clean_routes(Node) -> +cleanup_routes(Node) -> Patterns = [#route{_ = '_', dest = Node}, #route{_ = '_', dest = {'_', Node}}], - [mnesia:delete_object(R) || P <- Patterns, - R <- mnesia:match_object(P)]. + [mnesia:delete_object(?TAB, R, write) + || Pat <- Patterns, R <- mnesia:match_object(?TAB, Pat, write)]. diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 6b31cc864..a5a3a5d9f 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -35,7 +35,7 @@ -define(SERVER, ?MODULE). --define(TAB, shared_subscription). +-define(TAB, emqx_shared_subscription). -record(state, {pmon}). @@ -57,22 +57,30 @@ start_link() -> -spec(strategy() -> random | hash). strategy() -> - application:get_env(emqx, load_balancing_strategy, random). + emqx_config:get_env(shared_subscription_strategy, random). subscribe(undefined, _Topic, _SubPid) -> ok; subscribe(Group, Topic, SubPid) when is_pid(SubPid) -> - mnesia:dirty_write(r(Group, Topic, SubPid)), + mnesia:dirty_write(?TAB, r(Group, Topic, SubPid)), gen_server:cast(?SERVER, {monitor, SubPid}). unsubscribe(undefined, _Topic, _SubPid) -> ok; unsubscribe(Group, Topic, SubPid) when is_pid(SubPid) -> - mnesia:dirty_delete_object(r(Group, Topic, SubPid)). + mnesia:dirty_delete_object(?TAB, r(Group, Topic, SubPid)). r(Group, Topic, SubPid) -> #shared_subscription{group = Group, topic = Topic, subpid = SubPid}. +dispatch({Cluster, Group}, Topic, Delivery) -> + case ekka:cluster_name() of + Cluster -> + dispatch(Group, Topic, Delivery); + _ -> Delivery + end; + +%% TODO: ensure the delivery... dispatch(Group, Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> case pick(subscribers(Group, Topic)) of false -> Delivery; @@ -90,8 +98,7 @@ pick(SubPids) -> lists:nth((X rem length(SubPids)) + 1, SubPids). subscribers(Group, Topic) -> - MP = {shared_subscription, Group, Topic, '$1'}, - ets:select(shared_subscription, [{MP, [], ['$1']}]). + ets:select(?TAB, [{{shared_subscription, Group, Topic, '$1'}, [], ['$1']}]). %%-------------------------------------------------------------------- %% gen_server callbacks @@ -134,7 +141,7 @@ handle_info({mnesia_table_event, _Event}, State) -> handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) -> emqx_log:info("Shared subscription down: ~p", [SubPid]), - mnesia:transaction(fun clean_down/1, [SubPid]), + mnesia:async_dirty(fun cleanup_down/1, [SubPid]), {noreply, State#state{pmon = PMon:erase(SubPid)}}; handle_info(Info, State) -> @@ -151,7 +158,9 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -clean_down(SubPid) -> - MP = #shared_subscription{_ = '_', subpid = SubPid}, - lists:foreach(fun mnesia:delete_object/1, mnesia:match_object(MP)). +cleanup_down(SubPid) -> + Pat = #shared_subscription{_ = '_', subpid = SubPid}, + lists:foreach(fun(Record) -> + mnesia:delete_object(?TAB, Record) + end, mnesia:match_object(Pat)). diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index fc818a81b..a1b67561d 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -27,6 +27,10 @@ %% Trie API -export([insert/1, match/1, lookup/1, delete/1]). +%% Tables +-define(TRIE, emqx_trie). +-define(TRIE_NODE, emqx_trie_node). + %%-------------------------------------------------------------------- %% Mnesia Bootstrap %%-------------------------------------------------------------------- @@ -35,21 +39,21 @@ -spec(mnesia(boot | copy) -> ok). mnesia(boot) -> %% Trie Table - ok = ekka_mnesia:create_table(trie, [ + ok = ekka_mnesia:create_table(?TRIE, [ {ram_copies, [node()]}, {record_name, trie}, {attributes, record_info(fields, trie)}]), %% Trie Node Table - ok = ekka_mnesia:create_table(trie_node, [ + ok = ekka_mnesia:create_table(?TRIE_NODE, [ {ram_copies, [node()]}, {record_name, trie_node}, {attributes, record_info(fields, trie_node)}]); mnesia(copy) -> %% Copy Trie Table - ok = ekka_mnesia:copy_table(trie), + ok = ekka_mnesia:copy_table(?TRIE), %% Copy Trie Node Table - ok = ekka_mnesia:copy_table(trie_node). + ok = ekka_mnesia:copy_table(?TRIE_NODE). %%-------------------------------------------------------------------- %% Trie API @@ -58,7 +62,7 @@ mnesia(copy) -> %% @doc Insert a topic into the trie -spec(insert(Topic :: topic()) -> ok). insert(Topic) when is_binary(Topic) -> - case mnesia:read(trie_node, Topic) of + case mnesia:read(?TRIE_NODE, Topic) of [#trie_node{topic = Topic}] -> ok; [TrieNode = #trie_node{topic = undefined}] -> @@ -79,14 +83,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, NodeId). %% @doc Delete a topic from the trie -spec(delete(Topic :: topic()) -> ok). delete(Topic) when is_binary(Topic) -> - case mnesia:read(trie_node, Topic) of + case mnesia:read(?TRIE_NODE, Topic) of [#trie_node{edge_count = 0}] -> - mnesia:delete({trie_node, Topic}), + mnesia:delete({?TRIE_NODE, Topic}), delete_path(lists:reverse(emqx_topic:triples(Topic))); [TrieNode] -> write_trie_node(TrieNode#trie_node{topic = undefined}); @@ -102,9 +106,9 @@ delete(Topic) when is_binary(Topic) -> %% @doc Add a path to the trie. add_path({Node, Word, Child}) -> Edge = #trie_edge{node_id = Node, word = Word}, - case mnesia:read(trie_node, Node) of + case mnesia:read(?TRIE_NODE, Node) of [TrieNode = #trie_node{edge_count = Count}] -> - case mnesia:wread({trie, Edge}) of + case mnesia:wread({?TRIE, Edge}) of [] -> write_trie_node(TrieNode#trie_node{edge_count = Count+1}), write_trie(#trie{edge = Edge, node_id = Child}); @@ -125,11 +129,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, 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, #trie_edge{node_id = NodeId, word = WArg}) of [#trie{node_id = ChildId}] -> match_node(ChildId, Words, Acc); [] -> Acc end @@ -138,9 +142,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, #trie_edge{node_id = NodeId, word = '#'}) of [#trie{node_id = ChildId}] -> - mnesia:read(trie_node, ChildId) ++ ResAcc; + mnesia:read(?TRIE_NODE, ChildId) ++ ResAcc; [] -> ResAcc end. @@ -150,10 +154,10 @@ match_node(NodeId, [W|Words], ResAcc) -> delete_path([]) -> ok; delete_path([{NodeId, Word, _} | RestPath]) -> - mnesia:delete({trie, #trie_edge{node_id = NodeId, word = Word}}), - case mnesia:read(trie_node, NodeId) of + mnesia:delete({?TRIE, #trie_edge{node_id = NodeId, word = Word}}), + case mnesia:read(?TRIE_NODE, NodeId) of [#trie_node{edge_count = 1, topic = undefined}] -> - mnesia:delete({trie_node, NodeId}), + mnesia:delete({?TRIE_NODE, NodeId}), delete_path(RestPath); [TrieNode = #trie_node{edge_count = 1, topic = _}] -> write_trie_node(TrieNode#trie_node{edge_count = 0}); @@ -163,11 +167,9 @@ delete_path([{NodeId, Word, _} | RestPath]) -> mnesia:abort({node_not_found, NodeId}) end. -%% @private write_trie(Trie) -> - mnesia:write(trie, Trie, write). + mnesia:write(?TRIE, Trie, write). -%% @private write_trie_node(TrieNode) -> - mnesia:write(trie_node, TrieNode, write). + mnesia:write(?TRIE_NODE, TrieNode, write). diff --git a/test/emqx_trie_SUITE.erl b/test/emqx_trie_SUITE.erl index e0b0b569d..87eb52fea 100644 --- a/test/emqx_trie_SUITE.erl +++ b/test/emqx_trie_SUITE.erl @@ -131,5 +131,5 @@ t_delete3(_) -> end). clear_tables() -> - lists:foreach(fun mnesia:clear_table/1, [trie, trie_node]). + lists:foreach(fun mnesia:clear_table/1, [emqx_trie, emqx_trie_node]). From 2a4ffc6645d21c12d6e0df9ac6bf1f372d368984 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 18 Apr 2018 16:34:23 +0800 Subject: [PATCH 045/520] Add more service modules for MQTT Version 5.0 --- include/emqx.hrl | 35 +-- src/emqx.erl | 57 ++--- src/emqx_access_control.erl | 41 ++-- src/emqx_acl_internal.erl | 34 +-- src/emqx_alarm.erl | 4 +- src/emqx_app.erl | 1 - src/emqx_auth_mod.erl | 4 +- src/emqx_banned.erl | 138 +++++++++--- src/emqx_bridge.erl | 10 +- src/emqx_bridge_sup_sup.erl | 56 ++--- src/emqx_broker.erl | 79 ++++--- src/emqx_broker_helper.erl | 85 +++++--- src/emqx_broker_sup.erl | 69 +++--- src/emqx_cli.erl | 30 +-- src/emqx_client.erl | 70 ++++++ src/emqx_cm.erl | 205 ++++++++++------- src/emqx_cm_stats.erl | 72 ------ src/emqx_cm_sup.erl | 34 ++- src/emqx_connection.erl | 8 +- src/emqx_ctl.erl | 112 +++++----- src/emqx_flow_control.erl | 70 ++++++ src/emqx_hooks.erl | 46 ++-- src/emqx_inflight.erl | 30 +-- src/emqx_json.erl | 71 ++++-- src/emqx_keepalive.erl | 49 +++-- src/emqx_kernel_sup.erl | 43 ++++ src/{emqx_log.erl => emqx_logger.erl} | 32 +-- src/emqx_message.erl | 2 +- src/emqx_metrics.erl | 254 ++++++++++++++++------ src/emqx_misc.erl | 37 ++-- src/emqx_mod_presence.erl | 70 +++--- src/emqx_mod_rewrite.erl | 4 +- src/emqx_mod_subscription.erl | 30 +-- src/emqx_modules.erl | 32 +-- src/emqx_mqtt_metrics.erl | 159 -------------- src/emqx_parser.erl | 30 +-- src/emqx_plugins.erl | 64 +++--- src/emqx_pmon.erl | 37 ++-- src/emqx_pool.erl | 106 +++++++++ src/emqx_pool_sup.erl | 35 ++- src/emqx_pooler.erl | 98 --------- src/emqx_protocol.erl | 27 ++- src/emqx_rate_limiter.erl | 67 ++++++ src/emqx_router.erl | 105 ++++----- src/emqx_router_helper.erl | 109 +++++----- src/emqx_router_sup.erl | 42 ++-- src/emqx_rpc.erl | 38 ++-- src/emqx_session.erl | 17 +- src/emqx_session_sup.erl | 37 ++-- src/emqx_shared_sub.erl | 97 +++++---- src/emqx_sm.erl | 239 ++++++++++++-------- src/emqx_sm_locker.erl | 35 +-- src/emqx_sm_registry.erl | 108 +++++---- src/emqx_sm_stats.erl | 72 ------ src/emqx_sm_sup.erl | 49 +++-- src/emqx_stats.erl | 237 ++++++++++++-------- src/emqx_sup.erl | 107 +++++---- src/emqx_sys.erl | 161 ++++++++------ src/{emqx_sysmon.erl => emqx_sys_mon.erl} | 105 +++++---- src/emqx_sys_sup.erl | 36 +++ src/emqx_sysmon_sup.erl | 35 --- src/emqx_tables.erl | 37 ++-- src/emqx_time.erl | 30 +-- src/emqx_topic.erl | 37 ++-- src/emqx_tracer.erl | 96 ++++---- src/emqx_tracer_sup.erl | 32 --- src/emqx_trie.erl | 48 ++-- src/emqx_ws.erl | 14 +- src/emqx_ws_connection.erl | 8 +- 69 files changed, 2428 insertions(+), 2040 deletions(-) create mode 100644 src/emqx_client.erl delete mode 100644 src/emqx_cm_stats.erl create mode 100644 src/emqx_flow_control.erl create mode 100644 src/emqx_kernel_sup.erl rename src/{emqx_log.erl => emqx_logger.erl} (60%) delete mode 100644 src/emqx_mqtt_metrics.erl create mode 100644 src/emqx_pool.erl delete mode 100644 src/emqx_pooler.erl create mode 100644 src/emqx_rate_limiter.erl delete mode 100644 src/emqx_sm_stats.erl rename src/{emqx_sysmon.erl => emqx_sys_mon.erl} (61%) create mode 100644 src/emqx_sys_sup.erl delete mode 100644 src/emqx_sysmon_sup.erl delete mode 100644 src/emqx_tracer_sup.erl diff --git a/include/emqx.hrl b/include/emqx.hrl index cb06118da..9ee90b186 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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. +%%%=================================================================== %%-------------------------------------------------------------------- %% Banner @@ -154,7 +154,10 @@ %% Route %%-------------------------------------------------------------------- --record(route, { topic :: topic(), dest }). +-record(route, + { topic :: topic(), + dest :: node() | {binary(), node()} + }). -type(route() :: #route{}). diff --git a/src/emqx.erl b/src/emqx.erl index 24668029a..7d6e89427 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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). @@ -29,7 +29,7 @@ -export([topics/0, subscriptions/1, subscribers/1, subscribed/2]). %% Get/Set suboptions --export([getopts/2, setopts/3]). +-export([get_subopts/2, set_subopts/3]). %% Hooks API -export([hook/4, hook/3, unhook/2, run_hooks/2, run_hooks/3]). @@ -47,12 +47,17 @@ %%-------------------------------------------------------------------- %% @doc Start emqx application --spec(start() -> ok | {error, term()}). -start() -> application:start(?APP). +-spec(start() -> {ok, list(atom())} | {error, term()}). +start() -> + %% Check OS + %% Check VM + %% Check Mnesia + application:ensure_all_started(?APP). %% @doc Stop emqx application. -spec(stop() -> ok | {error, term()}). -stop() -> application:stop(?APP). +stop() -> + application:stop(?APP). %% @doc Is emqx running? -spec(is_running(node()) -> boolean()). @@ -96,13 +101,13 @@ unsubscribe(Topic, Subscriber) -> %% PubSub management API %%-------------------------------------------------------------------- --spec(getopts(topic() | string(), subscriber()) -> [suboption()]). -getopts(Topic, Subscriber) -> - emqx_broker:getopts(iolist_to_binary(Topic), list_to_subid(Subscriber)). +-spec(get_subopts(topic() | string(), subscriber()) -> [suboption()]). +get_subopts(Topic, Subscriber) -> + emqx_broker:get_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber)). --spec(setopts(topic() | string(), subscriber(), [suboption()]) -> ok). -setopts(Topic, Subscriber, Options) when is_list(Options) -> - emqx_broker:setopts(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). +-spec(set_subopts(topic() | string(), subscriber(), [suboption()]) -> ok). +set_subopts(Topic, Subscriber, Options) when is_list(Options) -> + emqx_broker:set_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). -spec(topics() -> list(topic())). topics() -> emqx_router:topics(). @@ -165,7 +170,7 @@ shutdown() -> shutdown(normal). shutdown(Reason) -> - emqx_log:error("EMQ shutdown for ~s", [Reason]), + emqx_logger:error("EMQ shutdown for ~s", [Reason]), emqx_plugins:unload(), lists:foreach(fun application:stop/1, [emqx, ekka, mochiweb, esockd, gproc]). diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 988a625dc..85713b7cc 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_access_control). @@ -28,10 +28,9 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-define(TAB, ?MODULE). -define(SERVER, ?MODULE). --define(TAB, access_control). - -type(password() :: undefined | binary()). -record(state, {}). @@ -122,7 +121,7 @@ stop() -> %%-------------------------------------------------------------------- init([]) -> - _ = emqx_tables:create(?TAB, [set, protected, {read_concurrency, true}]), + _ = emqx_tables:new(?TAB, [set, protected, {read_concurrency, true}]), {ok, #state{}}. handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) -> @@ -157,15 +156,15 @@ handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(Req, _From, State) -> - emqx_log:error("[AccessControl] Unexpected request: ~p", [Req]), + emqx_logger:error("[AccessControl] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - emqx_log:error("[AccessControl] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[AccessControl] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> - emqx_log:error("[AccessControl] Unexpected info: ~p", [Info]), + emqx_logger:error("[AccessControl] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index b7275af22..3f2824fbc 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_acl_internal). @@ -25,7 +25,7 @@ %% ACL callbacks -export([init/1, check_acl/2, reload_acl/1, description/0]). --define(ACL_RULE_TAB, mqtt_acl_rule). +-define(ACL_RULE_TAB, emqx_acl_rule). -record(state, {config}). @@ -48,7 +48,7 @@ all_rules() -> %% @doc Init internal ACL -spec(init([File :: string()]) -> {ok, State :: any()}). init([File]) -> - _ = emqx_tables:create(?ACL_RULE_TAB, [set, public, {read_concurrency, true}]), + _ = emqx_tables:new(?ACL_RULE_TAB, [set, public, {read_concurrency, true}]), {ok, load_rules_from_file(#state{config = File})}. load_rules_from_file(State = #state{config = AclFile}) -> diff --git a/src/emqx_alarm.erl b/src/emqx_alarm.erl index 73e2d2ca2..2b5990db4 100644 --- a/src/emqx_alarm.erl +++ b/src/emqx_alarm.erl @@ -92,7 +92,7 @@ handle_event({set_alarm, Alarm = #alarm{id = AlarmId, {summary, iolist_to_binary(Summary)}, {ts, emqx_time:now_secs(TS)}]) of {'EXIT', Reason} -> - emqx_log:error("[Alarm] Failed to encode set_alarm: ~p", [Reason]); + emqx_logger:error("[Alarm] Failed to encode set_alarm: ~p", [Reason]); JSON -> emqx_broker:publish(alarm_msg(alert, AlarmId, JSON)) end, @@ -101,7 +101,7 @@ handle_event({set_alarm, Alarm = #alarm{id = AlarmId, handle_event({clear_alarm, AlarmId}, Alarms) -> case catch emqx_json:encode([{id, AlarmId}, {ts, emqx_time:now_secs()}]) of {'EXIT', Reason} -> - emqx_log:error("[Alarm] Failed to encode clear_alarm: ~p", [Reason]); + emqx_logger:error("[Alarm] Failed to encode clear_alarm: ~p", [Reason]); JSON -> emqx_broker:publish(alarm_msg(clear, AlarmId, JSON)) end, diff --git a/src/emqx_app.erl b/src/emqx_app.erl index a4c0493d8..f7a51fc7f 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -34,7 +34,6 @@ start(_Type, _Args) -> ekka:start(), {ok, Sup} = emqx_sup:start_link(), %%TODO: fixme later - emqx_mqtt_metrics:init(), ok = register_acl_mod(), emqx_modules:load(), start_autocluster(), diff --git a/src/emqx_auth_mod.erl b/src/emqx_auth_mod.erl index 123527177..620584488 100644 --- a/src/emqx_auth_mod.erl +++ b/src/emqx_auth_mod.erl @@ -62,13 +62,13 @@ passwd_hash(pbkdf2, {Salt, Password, Macfun, Iterations, Dklen}) -> case pbkdf2:pbkdf2(Macfun, Password, Salt, Iterations, Dklen) of {ok, Hexstring} -> pbkdf2:to_hex(Hexstring); {error, Error} -> - emqx_log:error("[AuthMod] PasswdHash with pbkdf2 error:~p", [Error]), <<>> + emqx_logger:error("[AuthMod] PasswdHash with pbkdf2 error:~p", [Error]), <<>> end; passwd_hash(bcrypt, {Salt, Password}) -> case bcrypt:hashpw(Password, Salt) of {ok, HashPassword} -> list_to_binary(HashPassword); {error, Error}-> - emqx_log:error("[AuthMod] PasswdHash with bcrypt error:~p", [Error]), <<>> + emqx_logger:error("[AuthMod] PasswdHash with bcrypt error:~p", [Error]), <<>> end. hexstring(<>) -> diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index 85cfd8b6c..ff5250f27 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -1,72 +1,138 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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. +%%%=================================================================== -%% Banned an IP Address, ClientId? -module(emqx_banned). -behaviour(gen_server). +-include("emqx.hrl"). + +%% Mnesia bootstrap +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + %% API -export([start_link/0]). +-export([check/1]). +-export([add/1, del/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-define(TAB, ?MODULE). -define(SERVER, ?MODULE). --record(state, {}). +-type(key() :: {client_id, client_id()} | + {ipaddr, inet:ip_address()} | + {username, username()}). -%%%=================================================================== -%%% API -%%%=================================================================== +-record(state, {expiry_timer}). -%% @doc Starts the server +-record(banned, {key :: key(), reason, by, desc, until}). + +%%-------------------------------------------------------------------- +%% Mnesia bootstrap +%%-------------------------------------------------------------------- + +mnesia(boot) -> + ok = ekka_mnesia:create_table(?TAB, [ + {type, ordered_set}, + {disc_copies, [node()]}, + {record_name, banned}, + {attributes, record_info(fields, banned)}]); + +mnesia(copy) -> + ok = ekka_mnesia:copy_table(?TAB). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +%% @doc Start the banned server -spec(start_link() -> {ok, pid()} | ignore | {error, any()}). start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -%%%=================================================================== -%%% gen_server callbacks -%%%=================================================================== +-spec(check(client()) -> boolean()). +check(#client{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}). + +add(Record) when is_record(Record, banned) -> + mnesia:dirty_write(?TAB, Record). + +del(Key) -> + mnesia:dirty_delete(?TAB, Key). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- init([]) -> - {ok, #state{}}. + emqx_timer:seed(), + {ok, ensure_expiry_timer(#state{})}. -handle_call(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. +handle_call(Req, _From, State) -> + emqx_logger:error("[BANNED] Unexpected request: ~p", [Req]), + {reply, ignore, State}. -handle_cast(_Msg, State) -> +handle_cast(Msg, State) -> + emqx_logger:error("[BANNED] Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info(_Info, State) -> +handle_info({timeout, Ref, expire}, State = #state{expiry_timer = Ref}) -> + mnesia:async_dirty(fun expire_banned_items/1, [erlang:timestamp()]), + {noreply, ensure_expiry_timer(State), hibernate}; + +handle_info(Info, State) -> + emqx_logger:error("[BANNED] Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, _State) -> - ok. +terminate(_Reason, #state{expiry_timer = Timer}) -> + emqx_misc:cancel_timer(Timer). code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%%=================================================================== -%%% Internal functions -%%%=================================================================== - +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- +ensure_expiry_timer(State) -> + Interval = emqx_config:get_env(banned_expiry_interval, timer:minutes(5)), + State#state{expiry_timer = emqx_misc:start_timer( + Interval + rand:uniform(Interval), expire)}. +expire_banned_items(Now) -> + expire_banned_item(mnesia:first(?TAB), Now). +expire_banned_item('$end_of_table', _Now) -> + ok; +expire_banned_item(Key, Now) -> + case mnesia:read(?TAB, Key) of + [#banned{until = undefined}] -> ok; + [B = #banned{until = Until}] when Until < Now -> + mnesia:delete_object(?TAB, B, sticky_write); + [] -> ok + end, + expire_banned_item(mnesia:next(Key), Now). diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index 1e998f6cb..f45e4045c 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -103,11 +103,11 @@ qname(Node, Topic) -> iolist_to_binary(["Bridge:", Node, ":", Topic]). handle_call(Req, _From, State) -> - emqx_log:error("[Bridge] Unexpected request: ~p", [Req]), + emqx_logger:error("[Bridge] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - emqx_log:error("[Bridge] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Bridge] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info({dispatch, _Topic, Msg}, State = #state{mqueue = MQ, status = down}) -> @@ -118,7 +118,7 @@ handle_info({dispatch, _Topic, Msg}, State = #state{node = Node, status = up}) - {noreply, State, hibernate}; handle_info({nodedown, Node}, State = #state{node = Node, ping_down_interval = Interval}) -> - emqx_log:warning("[Bridge] Node Down: ~s", [Node]), + emqx_logger:warning("[Bridge] Node Down: ~s", [Node]), erlang:send_after(Interval, self(), ping_down_node), {noreply, State#state{status = down}, hibernate}; @@ -126,7 +126,7 @@ handle_info({nodeup, Node}, State = #state{node = Node}) -> %% TODO: Really fast?? case emqx:is_running(Node) of true -> - emqx_log:warning("[Bridge] Node up: ~s", [Node]), + emqx_logger:warning("[Bridge] Node up: ~s", [Node]), {noreply, dequeue(State#state{status = up})}; false -> self() ! {nodedown, Node}, @@ -149,7 +149,7 @@ handle_info({'EXIT', _Pid, normal}, State) -> {noreply, State}; handle_info(Info, State) -> - emqx_log:error("[Bridge] Unexpected info: ~p", [Info]), + emqx_logger:error("[Bridge] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> diff --git a/src/emqx_bridge_sup_sup.erl b/src/emqx_bridge_sup_sup.erl index 7c26cd047..14fecaecc 100644 --- a/src/emqx_bridge_sup_sup.erl +++ b/src/emqx_bridge_sup_sup.erl @@ -1,24 +1,27 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_bridge_sup_sup). -behavior(supervisor). --export([start_link/0, bridges/0, start_bridge/2, start_bridge/3, stop_bridge/2]). +-include("emqx.hrl"). + +-export([start_link/0, bridges/0]). +-export([start_bridge/2, start_bridge/3, stop_bridge/2]). -export([init/1]). @@ -32,27 +35,28 @@ start_link() -> %%-------------------------------------------------------------------- %% @doc List all bridges --spec(bridges() -> [{node(), binary(), pid()}]). +-spec(bridges() -> [{node(), topic(), pid()}]). bridges() -> [{Node, Topic, Pid} || {?CHILD_ID(Node, Topic), Pid, supervisor, _} - <- supervisor:which_children(?MODULE)]. + <- supervisor:which_children(?MODULE)]. %% @doc Start a bridge --spec(start_bridge(atom(), binary()) -> {ok, pid()} | {error, term()}). -start_bridge(Node, Topic) when is_atom(Node) andalso is_binary(Topic) -> +-spec(start_bridge(node(), topic()) -> {ok, pid()} | {error, term()}). +start_bridge(Node, Topic) when is_atom(Node), is_binary(Topic) -> start_bridge(Node, Topic, []). --spec(start_bridge(atom(), binary(), [emqx_bridge:option()]) -> {ok, pid()} | {error, term()}). +-spec(start_bridge(node(), topic(), [emqx_bridge:option()]) + -> {ok, pid()} | {error, term()}). start_bridge(Node, _Topic, _Options) when Node =:= node() -> {error, bridge_to_self}; -start_bridge(Node, Topic, Options) when is_atom(Node) andalso is_binary(Topic) -> +start_bridge(Node, Topic, Options) when is_atom(Node), is_binary(Topic) -> {ok, BridgeEnv} = emqx_config:get_env(bridge), Options1 = emqx_misc:merge_opts(BridgeEnv, Options), supervisor:start_child(?MODULE, bridge_spec(Node, Topic, Options1)). %% @doc Stop a bridge --spec(stop_bridge(atom(), binary()) -> {ok, pid()} | ok). -stop_bridge(Node, Topic) when is_atom(Node) andalso is_binary(Topic) -> +-spec(stop_bridge(node(), topic()) -> ok | {error, term()}). +stop_bridge(Node, Topic) when is_atom(Node), is_binary(Topic) -> ChildId = ?CHILD_ID(Node, Topic), case supervisor:terminate_child(?MODULE, ChildId) of ok -> supervisor:delete_child(?MODULE, ChildId); @@ -68,6 +72,6 @@ init([]) -> bridge_spec(Node, Topic, Options) -> {?CHILD_ID(Node, Topic), - {emqx_bridge_sup, start_link, [Node, Topic, Options]}, - permanent, infinity, supervisor, [emqx_bridge_sup]}. + {emqx_bridge_sup, start_link, [Node, Topic, Options]}, + permanent, infinity, supervisor, [emqx_bridge_sup]}. diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 79f7911f7..e40f181cb 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_broker). @@ -45,17 +45,19 @@ -define(TIMEOUT, 120000). %% ETS tables --define(SUBOPTION, emqx_suboption). --define(SUBSCRIBER, emqx_subscriber). +-define(SUBOPTION, emqx_suboption). +-define(SUBSCRIBER, emqx_subscriber). -define(SUBSCRIPTION, emqx_subscription). %%-------------------------------------------------------------------- %% Start a broker %%-------------------------------------------------------------------- --spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link(atom(), pos_integer()) + -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> - gen_server:start_link(?MODULE, [Pool, Id], [{hibernate_after, 2000}]). + gen_server:start_link(emqx_misc:proc_name(?MODULE, Id), + ?MODULE, [Pool, Id], [{hibernate_after, 2000}]). %%-------------------------------------------------------------------- %% Subscriber/Unsubscribe @@ -101,15 +103,32 @@ unsubscribe(Topic, Subscriber, Timeout) -> -spec(publish(message()) -> delivery() | stopped). publish(Msg = #message{from = From}) -> - emqx_tracer:trace(publish, From, Msg), + %% Hook to trace? + trace(public, From, Msg), case emqx_hooks:run('message.publish', [], Msg) of {ok, Msg1 = #message{topic = Topic}} -> publish(Topic, Msg1); {stop, Msg1} -> - emqx_log:warning("Stop publishing: ~s", [emqx_message:format(Msg1)]), + emqx_logger:warning("Stop publishing: ~s", [emqx_message:format(Msg1)]), stopped end. +%%-------------------------------------------------------------------- +%% Trace +%%-------------------------------------------------------------------- + +trace(publish, From, _Msg) when is_atom(From) -> + %% Dont' trace '$SYS' publish + ignore; +trace(public, #client{client_id = ClientId, username = Username}, + #message{topic = Topic, payload = Payload}) -> + emqx_logger:info([{client, ClientId}, {topic, Topic}], + "~s/~s PUBLISH to ~s: ~p", [ClientId, Username, Topic, Payload]); +trace(public, From, #message{topic = Topic, payload = Payload}) + when is_binary(From); is_list(From) -> + emqx_logger:info([{client, From}, {topic, Topic}], + "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). + publish(Topic, Msg) -> route(aggre(emqx_router:match_routes(Topic)), delivery(Msg)). @@ -131,16 +150,14 @@ route(Routes, Delivery) -> aggre([]) -> []; -aggre([{To, Dest}]) -> +aggre([#route{topic = To, dest = Dest}]) -> [{To, Dest}]; aggre(Routes) -> lists:foldl( - fun({To, Node}, Acc) when is_atom(Node) -> + fun(#route{topic = To, dest = Node}, Acc) when is_atom(Node) -> [{To, Node} | Acc]; - ({To, {Group, _Node}}, Acc) -> - lists:usort([{To, Group} | Acc]); - ({To, {Cluster, Group, _Node}}, Acc) -> - lists:usort([{To, {Cluster, Group}} | Acc]) + (#route{topic = To, dest = {Group, _Node}}, Acc) -> + lists:usort([{To, Group} | Acc]) end, [], Routes). %% @doc Forward message to another node. @@ -148,7 +165,7 @@ forward(Node, To, Delivery) -> %% rpc:call to ensure the delivery, but the latency:( case emqx_rpc:call(Node, ?BROKER, dispatch, [To, Delivery]) of {badrpc, Reason} -> - emqx_log:error("[Broker] Failed to forward msg to ~s: ~p", [Node, Reason]), + emqx_logger:error("[Broker] Failed to forward msg to ~s: ~p", [Node, Reason]), Delivery; Delivery1 -> Delivery1 end. @@ -261,7 +278,7 @@ handle_call({set_subopts, Topic, Subscriber, Opts}, _From, State) -> end; handle_call(Request, _From, State) -> - emqx_log:error("[Broker] Unexpected request: ~p", [Request]), + emqx_logger:error("[Broker] Unexpected request: ~p", [Request]), {reply, ignore, State}. handle_cast({From, {subscribe, Topic, Subscriber, Options}}, State) -> @@ -292,7 +309,7 @@ handle_cast({From, {unsubscribe, Topic, Subscriber}}, State) -> {noreply, State}; handle_cast(Msg, State) -> - emqx_log:error("[Broker] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Broker] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info({'DOWN', _MRef, process, SubPid, _Reason}, @@ -321,7 +338,7 @@ handle_info({'DOWN', _MRef, process, SubPid, _Reason}, {noreply, demonitor_subscriber(SubPid, State)}; handle_info(Info, State) -> - emqx_log:error("[Broker] Unexpected info: ~p", [Info]), + emqx_logger:error("[Broker] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index fc9a84be9..f461c44c7 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -1,66 +1,79 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_broker_helper). -behaviour(gen_server). --export([start_link/1]). +-export([start_link/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --define(SERVER, ?MODULE). +-define(HELPER, ?MODULE). --record(state, {stats_fun, stats_timer}). +-record(state, {}). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - --spec(start_link(fun()) -> {ok, pid()} | ignore | {error, any()}). -start_link(StatsFun) -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [StatsFun], []). +-spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +start_link() -> + gen_server:start_link({local, ?HELPER}, ?MODULE, [], []). %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- -init([StatsFun]) -> - {ok, TRef} = timer:send_interval(timer:seconds(1), stats), - {ok, #state{stats_fun = StatsFun, stats_timer = TRef}}. +init([]) -> + emqx_stats:update_interval(broker_stats, stats_fun()), + {ok, #state{}, hibernate}. handle_call(Req, _From, State) -> - emqx_log:error("[BrokerHelper] Unexpected request: ~p", [Req]), + emqx_logger:error("[BrokerHelper] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - emqx_log:error("[BrokerHelper] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[BrokerHelper] Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info(stats, State = #state{stats_fun = StatsFun}) -> - StatsFun(), {noreply, State, hibernate}; - handle_info(Info, State) -> - emqx_log:error("[BrokerHelper] Unexpected info: ~p", [Info]), + emqx_logger:error("[BrokerHelper] Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{stats_timer = TRef}) -> - timer:cancel(TRef). +terminate(_Reason, #state{}) -> + emqx_stats:cancel_update(broker_stats). code_change(_OldVsn, State, _Extra) -> {ok, State}. +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +stats_fun() -> + fun() -> + safe_update_stats(emqx_subscriber, + 'subscribers/count', 'subscribers/max'), + safe_update_stats(emqx_subscription, + 'subscriptions/count', 'subscriptions/max'), + safe_update_stats(emqx_suboptions, + 'suboptions/count', 'suboptions/max') + end. + +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_broker_sup.erl b/src/emqx_broker_sup.erl index 84bde9c6d..c16b5c63d 100644 --- a/src/emqx_broker_sup.erl +++ b/src/emqx_broker_sup.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_broker_sup). @@ -22,7 +22,11 @@ -export([init/1]). --define(CONCURRENCY_OPTS, [{read_concurrency, true}, {write_concurrency, true}]). +-import(lists, [foreach/2]). + +-define(TAB_OPTS, [public, + {read_concurrency, true}, + {write_concurrency, true}]). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). @@ -33,24 +37,23 @@ start_link() -> init([]) -> %% Create the pubsub tables - lists:foreach(fun create_tab/1, - [subscription, subscriber, suboption]), + foreach(fun create_tab/1, [subscription, subscriber, suboption]), %% Shared subscription - Shared = {shared_sub, {emqx_shared_sub, start_link, []}, - permanent, 5000, worker, [emqx_shared_sub]}, + SharedSub = {shared_sub, {emqx_shared_sub, start_link, []}, + permanent, 5000, worker, [emqx_shared_sub]}, %% Broker helper - Helper = {broker_helper, {emqx_broker_helper, start_link, [stats_fun()]}, + Helper = {broker_helper, {emqx_broker_helper, start_link, []}, permanent, 5000, worker, [emqx_broker_helper]}, %% Broker pool - PoolArgs = [broker, hash, emqx_sys:schedulers() * 2, + PoolArgs = [broker, hash, emqx_vm:schedulers() * 2, {emqx_broker, start_link, []}], - PoolSup = emqx_pool_sup:spec(broker_pool, PoolArgs), + PoolSup = emqx_pool_sup:spec(eqmx_broker_pool, PoolArgs), - {ok, {{one_for_all, 0, 3600}, [Shared, Helper, PoolSup]}}. + {ok, {{one_for_all, 0, 1}, [SharedSub, Helper, PoolSup]}}. %%-------------------------------------------------------------------- %% Create tables @@ -58,27 +61,15 @@ init([]) -> create_tab(suboption) -> %% Suboption: {Topic, Sub} -> [{qos, 1}] - emqx_tables:create(emqx_suboption, [public, set | ?CONCURRENCY_OPTS]); + emqx_tables:new(emqx_suboption, [set | ?TAB_OPTS]); create_tab(subscriber) -> %% Subscriber: Topic -> Sub1, Sub2, Sub3, ..., SubN %% duplicate_bag: o(1) insert - emqx_tables:create(emqx_subscriber, [public, duplicate_bag | ?CONCURRENCY_OPTS]); + emqx_tables:new(emqx_subscriber, [duplicate_bag | ?TAB_OPTS]); create_tab(subscription) -> %% Subscription: Sub -> Topic1, Topic2, Topic3, ..., TopicN %% bag: o(n) insert - emqx_tables:create(emqx_subscription, [public, bag | ?CONCURRENCY_OPTS]). - -%%-------------------------------------------------------------------- -%% Stats function -%%-------------------------------------------------------------------- - -stats_fun() -> - fun() -> - emqx_stats:setstat('subscribers/count', 'subscribers/max', - ets:info(emqx_subscriber, size)), - emqx_stats:setstat('subscriptions/count', 'subscriptions/max', - ets:info(emqx_subscription, size)) - end. + emqx_tables:new(emqx_subscription, [bag | ?TAB_OPTS]). diff --git a/src/emqx_cli.erl b/src/emqx_cli.erl index 3a98f95b4..ffbacb35e 100644 --- a/src/emqx_cli.erl +++ b/src/emqx_cli.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_cli). diff --git a/src/emqx_client.erl b/src/emqx_client.erl new file mode 100644 index 000000000..06399b1b5 --- /dev/null +++ b/src/emqx_client.erl @@ -0,0 +1,70 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%%=================================================================== + +-module(emqx_client). + +-behaviour(gen_server). + +%% API +-export([start_link/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-define(SERVER, ?MODULE). + +-record(state, {}). + +%%%=================================================================== +%%% API +%%%=================================================================== + +%% @doc Starts the server +-spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +init([]) -> + {ok, #state{}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + + + + diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 5a2ff93b4..a7e7f6356 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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). @@ -22,92 +22,145 @@ -export([start_link/0]). --export([lookup/1, reg/1, unreg/1]). +-export([lookup_client/1, register_client/1, register_client/2, + unregister_client/1]). + +-export([get_client_attrs/1, lookup_client_pid/1]). + +-export([get_client_stats/1, set_client_stats/2]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {stats_fun, stats_timer, monitors}). +-record(state, {client_pmon}). --define(SERVER, ?MODULE). +-define(CM, ?MODULE). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- +%% ETS Tables. +-define(CLIENT, emqx_client). +-define(CLIENT_ATTRS, emqx_client_attrs). +-define(CLIENT_STATS, emqx_client_stats). -%% @doc Start the client manager +%% @doc Start the client manager. -spec(start_link() -> {ok, pid()} | ignore | {error, term()}). start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + gen_server:start_link({local, ?CM}, ?MODULE, [], []). -%% @doc Lookup ClientPid by ClientId --spec(lookup(client_id()) -> pid() | undefined). -lookup(ClientId) when is_binary(ClientId) -> - try ets:lookup_element(client, ClientId, 2) +%% @doc Lookup a client. +-spec(lookup_client(client_id()) -> list({client_id(), pid()})). +lookup_client(ClientId) when is_binary(ClientId) -> + ets:lookup(?CLIENT, ClientId). + +%% @doc Register a client. +-spec(register_client(client_id() | {client_id(), pid()}) -> ok). +register_client(ClientId) when is_binary(ClientId) -> + register_client({ClientId, self()}); + +register_client({ClientId, ClientPid}) when is_binary(ClientId), + is_pid(ClientPid) -> + register_client({ClientId, ClientPid}, []). + +-spec(register_client({client_id(), pid()}, list()) -> ok). +register_client({ClientId, ClientPid}, Attrs) when is_binary(ClientId), + is_pid(ClientPid) -> + ets:insert(?CLIENT, {ClientId, ClientPid}), + ets:insert(?CLIENT_ATTRS, {{ClientId, ClientPid}, Attrs}), + notify({registered, ClientId, ClientPid}). + +%% @doc Get client attrs +-spec(get_client_attrs({client_id(), pid()}) -> list()). +get_client_attrs({ClientId, ClientPid}) when is_binary(ClientId), + is_pid(ClientPid) -> + try ets:lookup_element(?CLIENT_ATTRS, {ClientId, ClientPid}, 2) catch - error:badarg -> undefined + error:badarg -> [] end. -%% @doc Register a clientId --spec(reg(client_id()) -> ok). -reg(ClientId) -> - gen_server:cast(?SERVER, {reg, ClientId, self()}). +%% @doc Unregister a client. +-spec(unregister_client(client_id() | {client_id(), pid()}) -> ok). +unregister_client(ClientId) when is_binary(ClientId) -> + unregister_client({ClientId, self()}); -%% @doc Unregister clientId with pid. --spec(unreg(client_id()) -> ok). -unreg(ClientId) -> - gen_server:cast(?SERVER, {unreg, ClientId, self()}). +unregister_client({ClientId, ClientPid}) when is_binary(ClientId), + is_pid(ClientPid) -> + ets:delete(?CLIENT_STATS, {ClientId, ClientPid}), + ets:delete(?CLIENT_ATTRS, {ClientId, ClientPid}), + ets:delete_object(?CLIENT, {ClientId, ClientPid}), + notify({unregistered, ClientId, ClientPid}). + +%% @doc Lookup client pid +-spec(lookup_client_pid(client_id()) -> pid() | undefined). +lookup_client_pid(ClientId) when is_binary(ClientId) -> + case lookup_client_pid(ClientId) of + [] -> undefined; + [{_, Pid}] -> Pid + end. + +%% @doc Get client stats +-spec(get_client_stats({client_id(), pid()}) -> list(emqx_stats:stats())). +get_client_stats({ClientId, ClientPid}) when is_binary(ClientId), + is_pid(ClientPid) -> + try ets:lookup_element(?CLIENT_STATS, {ClientId, ClientPid}, 2) + catch + error:badarg -> [] + end. + +%% @doc Set client stats. +-spec(set_client_stats(client_id(), list(emqx_stats:stats())) -> boolean()). +set_client_stats(ClientId, Stats) when is_binary(ClientId) -> + set_client_stats({ClientId, self()}, Stats); + +set_client_stats({ClientId, ClientPid}, Stats) when is_binary(ClientId), + is_pid(ClientPid) -> + ets:insert(?CLIENT_STATS, {{ClientId, ClientPid}, Stats}). + +notify(Msg) -> + gen_server:cast(?CM, {notify, Msg}). %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- init([]) -> - _ = emqx_tables:create(client, [public, set, {keypos, 2}, - {read_concurrency, true}, - {write_concurrency, true}]), - _ = emqx_tables:create(client_attrs, [public, set, - {write_concurrency, true}]), - {ok, #state{monitors = dict:new()}}. + TabOpts = [public, set, {write_concurrency, true}], + _ = emqx_tables:new(?CLIENT, [{read_concurrency, true} | TabOpts]), + _ = emqx_tables:new(?CLIENT_ATTRS, TabOpts), + _ = emqx_tables:new(?CLIENT_STATS, TabOpts), + ok = emqx_stats:update_interval(cm_stats, fun update_client_stats/0), + {ok, #state{client_pmon = emqx_pmon:new()}}. handle_call(Req, _From, State) -> - emqx_log:error("[CM] Unexpected request: ~p", [Req]), + emqx_logger:error("[CM] Unexpected request: ~p", [Req]), {reply, ignore, State}. -handle_cast({reg, ClientId, Pid}, State) -> - _ = ets:insert(client, {ClientId, Pid}), - {noreply, monitor_client(ClientId, Pid, State)}; +handle_cast({notify, {registered, ClientId, Pid}}, + State = #state{client_pmon = PMon}) -> + {noreply, State#state{client_pmon = PMon:monitor(Pid, ClientId)}}; -handle_cast({unreg, ClientId, Pid}, State) -> - case lookup(ClientId) of - Pid -> remove_client({ClientId, Pid}); - _ -> ok - end, - {noreply, State}; +handle_cast({notify, {unregistered, _ClientId, Pid}}, + State = #state{client_pmon = PMon}) -> + {noreply, State#state{client_pmon = PMon:demonitor(Pid)}}; handle_cast(Msg, State) -> - emqx_log:error("[CM] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[CM] Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> - case dict:find(MRef, State#state.monitors) of - {ok, ClientId} -> - case lookup(ClientId) of - DownPid -> remove_client({ClientId, DownPid}); - _ -> ok - end, - {noreply, erase_monitor(MRef, State)}; - error -> - emqx_log:error("[CM] down client ~p not found", [DownPid]), - {noreply, State} +handle_info({'DOWN', _MRef, process, DownPid, _Reason}, + State = #state{client_pmon = PMon}) -> + case PMon:find(DownPid) of + undefined -> + {noreply, State}; + ClientId -> + unregister_client({ClientId, DownPid}), + {noreply, State#state{client_pmon = PMon:erase(DownPid)}} end; handle_info(Info, State) -> - emqx_log:error("[CM] Unexpected info: ~p", [Info]), + emqx_logger:error("[CM] Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, _State = #state{stats_timer = TRef}) -> - timer:cancel(TRef). +terminate(_Reason, _State = #state{}) -> + emqx_stats:cancel_update(cm_stats). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -116,16 +169,10 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -remove_client(Client) -> - ets:delete_object(client, Client), - ets:delete(client_stats, Client), - ets:delete(client_attrs, Client). - -monitor_client(ClientId, Pid, State = #state{monitors = Monitors}) -> - MRef = erlang:monitor(process, Pid), - State#state{monitors = dict:store(MRef, ClientId, Monitors)}. - -erase_monitor(MRef, State = #state{monitors = Monitors}) -> - erlang:demonitor(MRef), - State#state{monitors = dict:erase(MRef, Monitors)}. +update_client_stats() -> + case ets:info(?CLIENT, size) of + undefined -> ok; + Size -> + emqx_stats:setstat('clients/count', 'clients/max', Size) + end. diff --git a/src/emqx_cm_stats.erl b/src/emqx_cm_stats.erl deleted file mode 100644 index 475087c37..000000000 --- a/src/emqx_cm_stats.erl +++ /dev/null @@ -1,72 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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_stats). - --behaviour(gen_statem). - --include("emqx.hrl"). - -%% API --export([start_link/0]). - --export([set_client_stats/2, get_client_stats/1, del_client_stats/1]). - -%% gen_statem callbacks --export([init/1, callback_mode/0, handle_event/4, terminate/3, code_change/4]). - --define(TAB, client_stats). - --record(state, {statsfun}). - -start_link() -> - gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []). - --spec(set_client_stats(client_id(), emqx_stats:stats()) -> true). -set_client_stats(ClientId, Stats) -> - ets:insert(?TAB, {ClientId, [{'$ts', emqx_time:now_secs()}|Stats]}). - --spec(get_client_stats(client_id()) -> emqx_stats:stats()). -get_client_stats(ClientId) -> - case ets:lookup(?TAB, ClientId) of - [{_, Stats}] -> Stats; - [] -> [] - end. - --spec(del_client_stats(client_id()) -> true). -del_client_stats(ClientId) -> - ets:delete(?TAB, ClientId). - -init([]) -> - _ = emqx_tables:create(?TAB, [public, {write_concurrency, true}]), - StatsFun = emqx_stats:statsfun('clients/count', 'clients/max'), - {ok, idle, #state{statsfun = StatsFun}, timer:seconds(1)}. - -callback_mode() -> handle_event_function. - -handle_event(timeout, _Timeout, idle, State = #state{statsfun = StatsFun}) -> - case ets:info(client, size) of - undefined -> ok; - Size -> StatsFun(Size) - end, - {next_state, idle, State, timer:seconds(1)}. - -terminate(_Reason, _StateName, _State) -> - ok. - -code_change(_OldVsn, StateName, State, _Extra) -> - {ok, StateName, State}. - diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index 89618e1c9..9b98e0c97 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_sup). @@ -26,9 +26,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - Stats = {emqx_cm_stats, {emqx_cm_stats, start_link, []}, - permanent, 5000, worker, [emqx_cm_stats]}, CM = {emqx_cm, {emqx_cm, start_link, []}, permanent, 5000, worker, [emqx_cm]}, - {ok, {{one_for_all, 10, 3600}, [Stats, CM]}}. + {ok, {{one_for_all, 10, 3600}, [CM]}}. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 4b693a3a4..c034e4701 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -57,8 +57,8 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). -define(LOG(Level, Format, Args, State), - emqx_log:Level("Client(~s): " ++ Format, - [esockd_net:format(State#state.peername) | Args])). + emqx_logger:Level("Client(~s): " ++ Format, + [esockd_net:format(State#state.peername) | Args])). start_link(Conn, Env) -> {ok, proc_lib:spawn_link(?MODULE, init, [[Conn, Env]])}. @@ -316,7 +316,7 @@ received(Bytes, State = #state{parser = Parser, {more, NewParser} -> {noreply, run_socket(State#state{parser = NewParser}), IdleTimeout}; {ok, Packet, Rest} -> - emqx_mqtt_metrics:received(Packet), + emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> received(Rest, State#state{parser = emqx_parser:initial_state(PacketSize), @@ -371,7 +371,7 @@ emit_stats(undefined, State) -> State; emit_stats(ClientId, State) -> {reply, Stats, _, _} = handle_call(stats, undefined, State), - emqx_stats:set_client_stats(ClientId, Stats), + emqx_cm:set_client_stats(ClientId, Stats), State. sock_stats(#state{connection = Conn}) -> diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index 15f86b785..805982e0e 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -1,26 +1,26 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%%=================================================================== -module(emqx_ctl). -behaviour(gen_server). -%% API Function Exports --export([start_link/0, register_cmd/2, register_cmd/3, unregister_cmd/1, - lookup/1, run/1]). +-export([start_link/0]). +-export([register_command/2, register_command/3, unregister_command/1]). +-export([run_command/2, lookup_command/1]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -28,9 +28,10 @@ -record(state, {seq = 0}). --define(SERVER, ?MODULE). +-type(cmd() :: atom()). --define(TAB, ?MODULE). +-define(SERVER, ?MODULE). +-define(TAB, emqx_command). %%-------------------------------------------------------------------- %% API @@ -40,47 +41,44 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %% @doc Register a command --spec(register_cmd(atom(), {module(), atom()}) -> ok). -register_cmd(Cmd, MF) -> - register_cmd(Cmd, MF, []). +-spec(register_command(cmd(), {module(), atom()}) -> ok). +register_command(Cmd, MF) when is_atom(Cmd) -> + register_command(Cmd, MF, []). -%% @doc Register a command with opts --spec(register_cmd(atom(), {module(), atom()}, list()) -> ok). -register_cmd(Cmd, MF, Opts) -> - cast({register_cmd, Cmd, MF, Opts}). +%% @doc Register a command with options +-spec(register_command(cmd(), {module(), atom()}, list()) -> ok). +register_command(Cmd, MF, Opts) when is_atom(Cmd) -> + cast({register_command, Cmd, MF, Opts}). %% @doc Unregister a command --spec(unregister_cmd(atom()) -> ok). -unregister_cmd(Cmd) -> - cast({unregister_cmd, Cmd}). +-spec(unregister_command(cmd()) -> ok). +unregister_command(Cmd) when is_atom(Cmd) -> + cast({unregister_command, Cmd}). cast(Msg) -> gen_server:cast(?SERVER, Msg). %% @doc Run a command --spec(run([string()]) -> any()). -run([]) -> usage(), ok; - -run(["help"]) -> usage(), ok; - -run([CmdS|Args]) -> - case lookup(list_to_atom(CmdS)) of +-spec(run_command(cmd(), [string()]) -> ok | {error, term()}). +run_command(help, []) -> + usage(); +run_command(Cmd, Args) when is_atom(Cmd) -> + case lookup_command(Cmd) of [{Mod, Fun}] -> try Mod:Fun(Args) of - _ -> ok + _ -> ok catch _:Reason -> - io:format("Reason:~p, get_stacktrace:~p~n", - [Reason, erlang:get_stacktrace()]), + emqx_logger:error("[CTL] Cmd error:~p, stacktrace:~p", + [Reason, erlang:get_stacktrace()]), {error, Reason} end; [] -> - usage(), - {error, cmd_not_found} + usage(), {error, cmd_not_found} end. %% @doc Lookup a command --spec(lookup(atom()) -> [{module(), atom()}]). -lookup(Cmd) -> +-spec(lookup_command(cmd()) -> [{module(), atom()}]). +lookup_command(Cmd) when is_atom(Cmd) -> case ets:match(?TAB, {{'_', Cmd}, '$1', '_'}) of [El] -> El; [] -> [] @@ -97,30 +95,32 @@ usage() -> %%-------------------------------------------------------------------- init([]) -> - ets:new(?TAB, [ordered_set, named_table, protected]), + _ = emqx_tables:new(?TAB, [ordered_set, protected]), {ok, #state{seq = 0}}. -handle_call(_Request, _From, State) -> - {reply, ok, State}. +handle_call(Req, _From, State) -> + emqx_logger:error("Unexpected request: ~p", [Req]), + {reply, ignore, State}. -handle_cast({register_cmd, Cmd, MF, Opts}, State = #state{seq = Seq}) -> +handle_cast({register_command, Cmd, MF, Opts}, State = #state{seq = Seq}) -> case ets:match(?TAB, {{'$1', Cmd}, '_', '_'}) of - [] -> - ets:insert(?TAB, {{Seq, Cmd}, MF, Opts}); + [] -> ets:insert(?TAB, {{Seq, Cmd}, MF, Opts}); [[OriginSeq] | _] -> - emqx_log:warning("[CLI] ~s is overidden by ~p", [Cmd, MF]), + emqx_logger:warning("[CTL] cmd ~s is overidden by ~p", [Cmd, MF]), ets:insert(?TAB, {{OriginSeq, Cmd}, MF, Opts}) end, noreply(next_seq(State)); -handle_cast({unregister_cmd, Cmd}, State) -> +handle_cast({unregister_command, Cmd}, State) -> ets:match_delete(?TAB, {{'_', Cmd}, '_', '_'}), noreply(State); -handle_cast(_Msg, State) -> +handle_cast(Msg, State) -> + emqx_logger:error("Unexpected msg: ~p", [Msg]), noreply(State). -handle_info(_Info, State) -> +handle_info(Info, State) -> + emqx_logger:error("Unexpected info: ~p", [Info]), noreply(State). terminate(_Reason, _State) -> @@ -143,7 +143,7 @@ next_seq(State = #state{seq = Seq}) -> -include_lib("eunit/include/eunit.hrl"). -register_cmd_test_() -> +register_command_test_() -> {setup, fun() -> {ok, InitState} = emqx_ctl:init([]), @@ -153,7 +153,7 @@ register_cmd_test_() -> ok = emqx_ctl:terminate(shutdown, State) end, fun(State = #state{seq = Seq}) -> - emqx_ctl:handle_cast({register_cmd, test0, {?MODULE, test0}, []}, State), + emqx_ctl:handle_cast({register_command, test0, {?MODULE, test0}, []}, State), [?_assertMatch([{{0,test0},{?MODULE, test0}, []}], ets:lookup(?TAB, {Seq,test0}))] end }. diff --git a/src/emqx_flow_control.erl b/src/emqx_flow_control.erl new file mode 100644 index 000000000..6effb1d53 --- /dev/null +++ b/src/emqx_flow_control.erl @@ -0,0 +1,70 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_flow_control). + +-behaviour(gen_server). + +%% API +-export([start_link/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-define(SERVER, ?MODULE). + +-record(state, {}). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +%% @doc Starts the server +-spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([]) -> + {ok, #state{}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + + + + diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index 3f926a0bb..06bd0eabe 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -1,24 +1,23 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_hooks). -behaviour(gen_server). -%% Start -export([start_link/0]). %% Hooks API @@ -41,7 +40,7 @@ -record(hook, {name :: atom(), callbacks = [] :: list(#callback{})}). --define(HOOK_TAB, mqtt_hook). +-define(TAB, ?MODULE). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). @@ -104,24 +103,25 @@ run_([], _Args, Acc) -> -spec(lookup(atom()) -> [#callback{}]). lookup(HookPoint) -> - case ets:lookup(?HOOK_TAB, HookPoint) of + case ets:lookup(?TAB, HookPoint) of [#hook{callbacks = Callbacks}] -> Callbacks; [] -> [] end. %%-------------------------------------------------------------------- -%% gen_server Callbacks +%% gen_server callbacks %%-------------------------------------------------------------------- init([]) -> - ets:new(?HOOK_TAB, [set, protected, named_table, {keypos, #hook.name}]), + _ = emqx_tables:new(?TAB, [set, protected, {keypos, #hook.name}, + {read_concurrency, true}]), {ok, #state{}}. handle_call({add, HookPoint, {Tag, Function}, InitArgs, Priority}, _From, State) -> Callback = #callback{tag = Tag, function = Function, init_args = InitArgs, priority = Priority}, {reply, - case ets:lookup(?HOOK_TAB, HookPoint) of + case ets:lookup(?TAB, HookPoint) of [#hook{callbacks = Callbacks}] -> case contain_(Tag, Function, Callbacks) of false -> @@ -135,7 +135,7 @@ handle_call({add, HookPoint, {Tag, Function}, InitArgs, Priority}, _From, State) handle_call({delete, HookPoint, {Tag, Function}}, _From, State) -> {reply, - case ets:lookup(?HOOK_TAB, HookPoint) of + case ets:lookup(?TAB, HookPoint) of [#hook{callbacks = Callbacks}] -> case contain_(Tag, Function, Callbacks) of true -> @@ -167,7 +167,7 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- insert_hook_(HookPoint, Callbacks) -> - ets:insert(?HOOK_TAB, #hook{name = HookPoint, callbacks = Callbacks}), ok. + ets:insert(?TAB, #hook{name = HookPoint, callbacks = Callbacks}), ok. add_callback_(Callback, Callbacks) -> lists:keymerge(#callback.priority, Callbacks, [Callback]). diff --git a/src/emqx_inflight.erl b/src/emqx_inflight.erl index 3bdc8b02a..73a60058a 100644 --- a/src/emqx_inflight.erl +++ b/src/emqx_inflight.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_inflight). diff --git a/src/emqx_json.erl b/src/emqx_json.erl index 907b6e76d..e3b4241d6 100644 --- a/src/emqx_json.erl +++ b/src/emqx_json.erl @@ -1,22 +1,23 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_json). --export([encode/1, encode/2, decode/1, decode/2]). +-export([encode/1, encode/2, safe_encode/1, safe_encode/2]). +-export([decode/1, decode/2, safe_decode/1, safe_decode/2]). -spec(encode(jsx:json_term()) -> jsx:json_text()). encode(Term) -> @@ -26,11 +27,41 @@ encode(Term) -> encode(Term, Opts) -> jsx:encode(Term, Opts). +-spec(safe_encode(jsx:json_term()) + -> {ok, jsx:json_text()} | {error, term()}). +safe_encode(Term) -> + safe_encode(Term, []). + +-spec(safe_encode(jsx:json_term(), jsx_to_json:config()) + -> {ok, jsx:json_text()} | {error, term()}). +safe_encode(Term, Opts) -> + try encode(Term, Opts) of + Json -> {ok, Json} + catch + error:Reason -> + {error, Reason} + end. + -spec(decode(jsx:json_text()) -> jsx:json_term()). -decode(JSON) -> - jsx:decode(JSON). +decode(Json) -> + jsx:decode(Json). -spec(decode(jsx:json_text(), jsx_to_json:config()) -> jsx:json_term()). -decode(JSON, Opts) -> - jsx:decode(JSON, Opts). +decode(Json, Opts) -> + jsx:decode(Json, Opts). + +-spec(safe_decode(jsx:json_text()) + -> {ok, jsx:json_term()} | {error, term()}). +safe_decode(Json) -> + safe_decode(Json, []). + +-spec(safe_decode(jsx:json_text(), jsx_to_json:config()) + -> {ok, jsx:json_term()} | {error, term()}). +safe_decode(Json, Opts) -> + try decode(Json, Opts) of + Term -> {ok, Term} + catch + error:Reason -> + {error, Reason} + end. diff --git a/src/emqx_keepalive.erl b/src/emqx_keepalive.erl index b1d8d87b0..ae38d28bc 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_keepalive). @@ -29,19 +29,21 @@ start(_, 0, _) -> {ok, #keepalive{}}; start(StatFun, TimeoutSec, TimeoutMsg) -> - case StatFun() of + case catch StatFun() of {ok, StatVal} -> {ok, #keepalive{statfun = StatFun, statval = StatVal, tsec = TimeoutSec, tmsg = TimeoutMsg, tref = timer(TimeoutSec, TimeoutMsg)}}; {error, Error} -> - {error, Error} + {error, Error}; + {'EXIT', Reason} -> + {error, Reason} end. -%% @doc Check keepalive, called when timeout. +%% @doc Check keepalive, called when timeout... -spec(check(keepalive()) -> {ok, keepalive()} | {error, term()}). check(KeepAlive = #keepalive{statfun = StatFun, statval = LastVal, repeat = Repeat}) -> - case StatFun() of + case catch StatFun() of {ok, NewVal} -> if NewVal =/= LastVal -> {ok, resume(KeepAlive#keepalive{statval = NewVal, repeat = 0})}; @@ -51,9 +53,12 @@ check(KeepAlive = #keepalive{statfun = StatFun, statval = LastVal, repeat = Repe {error, timeout} end; {error, Error} -> - {error, Error} + {error, Error}; + {'EXIT', Reason} -> + {error, Reason} end. +-spec(resume(keepalive()) -> keepalive()). resume(KeepAlive = #keepalive{tsec = TimeoutSec, tmsg = TimeoutMsg}) -> KeepAlive#keepalive{tref = timer(TimeoutSec, TimeoutMsg)}. @@ -64,6 +69,6 @@ cancel(#keepalive{tref = TRef}) when is_reference(TRef) -> cancel(_) -> ok. -timer(Sec, Msg) -> - erlang:send_after(timer:seconds(Sec), self(), Msg). +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 new file mode 100644 index 000000000..486c20705 --- /dev/null +++ b/src/emqx_kernel_sup.erl @@ -0,0 +1,43 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_kernel_sup). + +-behaviour(supervisor). + +-export([start_link/0]). + +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + {ok, {{one_for_one, 10, 100}, + [child_spec(emqx_pool, supervisor), + child_spec(emqx_alarm, worker), + child_spec(emqx_hooks, worker), + child_spec(emqx_stats, worker), + child_spec(emqx_metrics, worker), + child_spec(emqx_ctl, worker), + child_spec(emqx_tracer, worker)]}}. + +child_spec(M, worker) -> + {M, {M, start_link, []}, permanent, 5000, worker, [M]}; +child_spec(M, supervisor) -> + {M, {M, start_link, []}, + permanent, infinity, supervisor, [M]}. + diff --git a/src/emqx_log.erl b/src/emqx_logger.erl similarity index 60% rename from src/emqx_log.erl rename to src/emqx_logger.erl index 7f85b3309..0c5cf9240 100644 --- a/src/emqx_log.erl +++ b/src/emqx_logger.erl @@ -1,20 +1,20 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_log). +-module(emqx_logger). -compile({no_auto_import,[error/1]}). diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 3c732f676..b8b919ee9 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -50,7 +50,7 @@ get_flag(Flag, #message{flags = Flags}, Default) -> %% @doc Set flag -spec(set_flag(message_flag(), message()) -> message()). -set_flag(Flag, Msg = #message{flags = Flags}) -> +set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) -> Msg#message{flags = maps:put(Flag, true, Flags)}. %% @doc Unset flag diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index ba5ddf3c0..c356e62d7 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -1,55 +1,110 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_metrics). --behaviour(gen_server). +-include("emqx_mqtt.hrl"). -%% API Function Exports --export([start_link/0, create/1]). +-export([start_link/0]). --export([all/0, value/1, inc/1, inc/2, inc/3, dec/2, dec/3, set/2]). +-export([new/1, all/0]). + +-export([val/1, inc/1, inc/2, inc/3, dec/2, dec/3, set/2]). + +%% Received/sent metrics +-export([received/1, sent/1]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {tick}). +-record(state, {}). --define(TAB, metrics). +%% Bytes sent and received of Broker +-define(BYTES_METRICS, [ + {counter, 'bytes/received'}, % Total bytes received + {counter, 'bytes/sent'} % Total bytes sent +]). + +%% Packets sent and received of broker +-define(PACKET_METRICS, [ + {counter, 'packets/received'}, % All Packets received + {counter, 'packets/sent'}, % All Packets sent + {counter, 'packets/connect'}, % CONNECT Packets received + {counter, 'packets/connack'}, % CONNACK Packets sent + {counter, 'packets/publish/received'}, % PUBLISH packets received + {counter, 'packets/publish/sent'}, % PUBLISH packets sent + {counter, 'packets/puback/received'}, % PUBACK packets received + {counter, 'packets/puback/sent'}, % PUBACK packets sent + {counter, 'packets/puback/missed'}, % PUBACK packets missed + {counter, 'packets/pubrec/received'}, % PUBREC packets received + {counter, 'packets/pubrec/sent'}, % PUBREC packets sent + {counter, 'packets/pubrec/missed'}, % PUBREC packets missed + {counter, 'packets/pubrel/received'}, % PUBREL packets received + {counter, 'packets/pubrel/sent'}, % PUBREL packets sent + {counter, 'packets/pubrel/missed'}, % PUBREL packets missed + {counter, 'packets/pubcomp/received'}, % PUBCOMP packets received + {counter, 'packets/pubcomp/sent'}, % PUBCOMP packets sent + {counter, 'packets/pubcomp/missed'}, % PUBCOMP packets missed + {counter, 'packets/subscribe'}, % SUBSCRIBE Packets received + {counter, 'packets/suback'}, % SUBACK packets sent + {counter, 'packets/unsubscribe'}, % UNSUBSCRIBE Packets received + {counter, 'packets/unsuback'}, % UNSUBACK Packets sent + {counter, 'packets/pingreq'}, % PINGREQ packets received + {counter, 'packets/pingresp'}, % PINGRESP Packets sent + {counter, 'packets/disconnect'}, % DISCONNECT Packets received + {counter, 'packets/auth'} % Auth Packets received +]). + +%% Messages sent and received of broker +-define(MESSAGE_METRICS, [ + {counter, 'messages/received'}, % All Messages received + {counter, 'messages/sent'}, % All Messages sent + {counter, 'messages/qos0/received'}, % QoS0 Messages received + {counter, 'messages/qos0/sent'}, % QoS0 Messages sent + {counter, 'messages/qos1/received'}, % QoS1 Messages received + {counter, 'messages/qos1/sent'}, % QoS1 Messages sent + {counter, 'messages/qos2/received'}, % QoS2 Messages received + {counter, 'messages/qos2/sent'}, % QoS2 Messages sent + {counter, 'messages/qos2/dropped'}, % QoS2 Messages dropped + {gauge, 'messages/retained'}, % Messagea retained + {counter, 'messages/dropped'}, % Messages dropped + {counter, 'messages/forward'} % Messages forward +]). + +-define(TAB, ?MODULE). -define(SERVER, ?MODULE). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - %% @doc Start the metrics server -spec(start_link() -> {ok, pid()} | ignore | {error, term()}). start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -create({gauge, Name}) -> +%%-------------------------------------------------------------------- +%% Metrics API +%%-------------------------------------------------------------------- + +new({gauge, Name}) -> ets:insert(?TAB, {{Name, 0}, 0}); -create({counter, Name}) -> - Schedulers = lists:seq(1, erlang:system_info(schedulers)), +new({counter, Name}) -> + Schedulers = lists:seq(1, emqx_vm:schedulers()), ets:insert(?TAB, [{{Name, I}, 0} || I <- Schedulers]). - %% @doc Get all metrics -spec(all() -> [{atom(), non_neg_integer()}]). all() -> @@ -63,8 +118,8 @@ all() -> end, #{}, ?TAB)). %% @doc Get metric value --spec(value(atom()) -> non_neg_integer()). -value(Metric) -> +-spec(val(atom()) -> non_neg_integer()). +val(Metric) -> lists:sum(ets:select(?TAB, [{{{Metric, '_'}, '$1'}, [], ['$1']}])). %% @doc Increase counter @@ -84,9 +139,9 @@ inc(Metric, Val) when is_atom(Metric) -> %% @doc Increase metric value -spec(inc(counter | gauge, atom(), pos_integer()) -> pos_integer()). inc(gauge, Metric, Val) -> - ets:update_counter(?TAB, key(gauge, Metric), {2, Val}); + update_counter(key(gauge, Metric), {2, Val}); inc(counter, Metric, Val) -> - ets:update_counter(?TAB, key(counter, Metric), {2, Val}). + update_counter(key(counter, Metric), {2, Val}). %% @doc Decrease metric value -spec(dec(gauge, atom()) -> integer()). @@ -96,7 +151,7 @@ dec(gauge, Metric) -> %% @doc Decrease metric value -spec(dec(gauge, atom(), pos_integer()) -> integer()). dec(gauge, Metric, Val) -> - ets:update_counter(?TAB, key(gauge, Metric), {2, -Val}). + update_counter(key(gauge, Metric), {2, -Val}). %% @doc Set metric value set(Metric, Val) when is_atom(Metric) -> @@ -104,57 +159,120 @@ set(Metric, Val) when is_atom(Metric) -> set(gauge, Metric, Val) -> ets:insert(?TAB, {key(gauge, Metric), Val}). -%% @doc Metric Key +%% @doc Metric key key(gauge, Metric) -> {Metric, 0}; key(counter, Metric) -> {Metric, erlang:system_info(scheduler_id)}. +update_counter(Key, UpOp) -> + ets:update_counter(?TAB, Key, UpOp). + %%-------------------------------------------------------------------- -%% gen_server Callbacks +%% Receive/Sent metrics +%%-------------------------------------------------------------------- + +%% @doc Count packets received. +-spec(received(mqtt_packet()) -> ok). +received(Packet) -> + inc('packets/received'), + received1(Packet). +received1(?PUBLISH_PACKET(Qos, _PktId)) -> + inc('packets/publish/received'), + inc('messages/received'), + qos_received(Qos); +received1(?PACKET(Type)) -> + received2(Type). +received2(?CONNECT) -> + inc('packets/connect'); +received2(?PUBACK) -> + inc('packets/puback/received'); +received2(?PUBREC) -> + inc('packets/pubrec/received'); +received2(?PUBREL) -> + inc('packets/pubrel/received'); +received2(?PUBCOMP) -> + inc('packets/pubcomp/received'); +received2(?SUBSCRIBE) -> + inc('packets/subscribe'); +received2(?UNSUBSCRIBE) -> + inc('packets/unsubscribe'); +received2(?PINGREQ) -> + inc('packets/pingreq'); +received2(?DISCONNECT) -> + inc('packets/disconnect'); +received2(_) -> + ignore. +qos_received(?QOS_0) -> + inc('messages/qos0/received'); +qos_received(?QOS_1) -> + inc('messages/qos1/received'); +qos_received(?QOS_2) -> + inc('messages/qos2/received'). + +%% @doc Count packets received. Will not count $SYS PUBLISH. +-spec(sent(mqtt_packet()) -> ignore | non_neg_integer()). +sent(?PUBLISH_PACKET(_Qos, <<"$SYS/", _/binary>>, _, _)) -> + ignore; +sent(Packet) -> + inc('packets/sent'), + sent1(Packet). +sent1(?PUBLISH_PACKET(Qos, _PktId)) -> + inc('packets/publish/sent'), + inc('messages/sent'), + qos_sent(Qos); +sent1(?PACKET(Type)) -> + sent2(Type). +sent2(?CONNACK) -> + inc('packets/connack'); +sent2(?PUBACK) -> + inc('packets/puback/sent'); +sent2(?PUBREC) -> + inc('packets/pubrec/sent'); +sent2(?PUBREL) -> + inc('packets/pubrel/sent'); +sent2(?PUBCOMP) -> + inc('packets/pubcomp/sent'); +sent2(?SUBACK) -> + inc('packets/suback'); +sent2(?UNSUBACK) -> + inc('packets/unsuback'); +sent2(?PINGRESP) -> + inc('packets/pingresp'); +sent2(_Type) -> + ignore. +qos_sent(?QOS_0) -> + inc('messages/qos0/sent'); +qos_sent(?QOS_1) -> + inc('messages/qos1/sent'); +qos_sent(?QOS_2) -> + inc('messages/qos2/sent'). + +%%-------------------------------------------------------------------- +%% gen_server callbacks %%-------------------------------------------------------------------- init([]) -> - emqx_time:seed(), % Create metrics table - _ = ets:new(?TAB, [set, public, named_table, {write_concurrency, true}]), - % Tick to publish metrics - {ok, TRef} = timer:send_after(emqx_sys:sys_interval(), tick), - {ok, #state{tick = TRef}, hibernate}. + _ = emqx_tables:new(?TAB, [set, public, {write_concurrency, true}]), + lists:foreach(fun new/1, ?BYTES_METRICS ++ ?PACKET_METRICS ++ ?MESSAGE_METRICS), + {ok, #state{}, hibernate}. -handle_call(_Req, _From, State) -> - {reply, error, State}. +handle_call(Req, _From, State) -> + emqx_logger:error("[METRICS] Unexpected request: ~p", [Req]), + {reply, ignore, State}. -handle_cast(_Msg, State) -> +handle_cast(Msg, State) -> + emqx_logger:error("[METRICS] Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info(tick, State) -> - % publish metric message - [publish(Metric, Val) || {Metric, Val} <- all()], - {noreply, State, hibernate}; - -handle_info(_Info, State) -> +handle_info(Info, State) -> + emqx_logger:error("[METRICS] Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{tick = TRef}) -> - %%TODO: - timer:cancel(TRef). +terminate(_Reason, #state{}) -> + ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -%% TODO: the depencies are not right - -publish(Metric, Val) -> - Msg = emqx_message:make(metrics, metric_topic(Metric), bin(Val)), - emqx_broker:publish(emqx_message:set_flag(sys, Msg)). - -metric_topic(Metric) -> - emqx_topic:systop(list_to_binary(lists:concat(['metrics/', Metric]))). - -bin(I) when is_integer(I) -> list_to_binary(integer_to_list(I)). - diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index 0d3eecc5f..27c18f8c3 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -1,25 +1,26 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_misc). -export([merge_opts/2, start_timer/2, start_timer/3, cancel_timer/1, - proc_stats/0, proc_stats/1]). + proc_name/2, proc_stats/0, proc_stats/1]). %% @doc Merge Options +-spec(merge_opts(list(), list()) -> list()). merge_opts(Defaults, Options) -> lists:foldl( fun({Opt, Val}, Acc) -> @@ -51,6 +52,10 @@ cancel_timer(Timer) -> _ -> ok end. +-spec(proc_name(atom(), pos_integer()) -> atom()). +proc_name(Mod, Id) -> + list_to_atom(lists:concat([Mod, "_", Id])). + -spec(proc_stats() -> list()). proc_stats() -> proc_stats(self()). diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index 17ece8211..ca3f83e8f 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_presence). @@ -34,32 +34,32 @@ on_client_connected(ConnAck, Client = #client{client_id = ClientId, %%clean_sess = CleanSess, %%proto_ver = ProtoVer }, Env) -> - case catch emqx_json:encode([{clientid, ClientId}, - {username, Username}, - {ipaddress, iolist_to_binary(emqx_net:ntoa(IpAddr))}, - %%{clean_sess, CleanSess}, %%TODO:: fixme later - %%{protocol, ProtoVer}, - {connack, ConnAck}, - {ts, emqx_time:now_secs()}]) of - Payload when is_binary(Payload) -> + case emqx_json:safe_encode([{clientid, ClientId}, + {username, Username}, + {ipaddress, iolist_to_binary(emqx_net:ntoa(IpAddr))}, + %%{clean_sess, CleanSess}, %%TODO:: fixme later + %%{protocol, ProtoVer}, + {connack, ConnAck}, + {ts, emqx_time:now_secs()}]) of + {ok, Payload} -> Msg = message(qos(Env), topic(connected, ClientId), Payload), emqx:publish(emqx_message:set_flag(sys, Msg)); - {'EXIT', Reason} -> - emqx_log:error("[Presence Module] json error: ~p", [Reason]) + {error, Reason} -> + emqx_logger:error("[Presence Module] Json error: ~p", [Reason]) end, {ok, Client}. on_client_disconnected(Reason, #client{client_id = ClientId, username = Username}, Env) -> - case catch emqx_json:encode([{clientid, ClientId}, - {username, Username}, - {reason, reason(Reason)}, - {ts, emqx_time:now_secs()}]) of - Payload when is_binary(Payload) -> + case emqx_json:safe_encode([{clientid, ClientId}, + {username, Username}, + {reason, reason(Reason)}, + {ts, emqx_time:now_secs()}]) of + {ok, Payload} -> Msg = message(qos(Env), topic(disconnected, ClientId), Payload), emqx:publish(emqx_message:set_flag(sys, Msg)); - {'EXIT', Reason} -> - emqx_log:error("[Presence Module] json error: ~p", [Reason]) + {error, Reason} -> + emqx_logger:error("[Presence Module] Json error: ~p", [Reason]) end, ok. unload(_Env) -> @@ -67,13 +67,13 @@ unload(_Env) -> emqx:unhook('client.disconnected', fun ?MODULE:on_client_disconnected/3). message(Qos, Topic, Payload) -> - Msg = emqx_message:make(presence, Topic, iolist_to_binary(Payload)), + Msg = emqx_message:make(?MODULE, Topic, iolist_to_binary(Payload)), emqx_message:set_header(qos, Qos, Msg). topic(connected, ClientId) -> - emqx_topic:systop(list_to_binary(["clients/", ClientId, "/connected"])); + emqx_topic:systop(iolist_to_binary(["clients/", ClientId, "/connected"])); topic(disconnected, ClientId) -> - emqx_topic:systop(list_to_binary(["clients/", ClientId, "/disconnected"])). + emqx_topic:systop(iolist_to_binary(["clients/", ClientId, "/disconnected"])). qos(Env) -> proplists:get_value(qos, Env, 0). diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index b9c664afd..671fe7311 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -35,11 +35,11 @@ load(Rules0) -> emqx:hook('message.publish', fun ?MODULE:rewrite_publish/2, [Rules]). rewrite_subscribe(_ClientId, _Username, TopicTable, Rules) -> - emqx_log:info("Rewrite subscribe: ~p", [TopicTable]), + emqx_logger:info("Rewrite subscribe: ~p", [TopicTable]), {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. rewrite_unsubscribe(_ClientId, _Username, TopicTable, Rules) -> - emqx_log:info("Rewrite unsubscribe: ~p", [TopicTable]), + emqx_logger:info("Rewrite unsubscribe: ~p", [TopicTable]), {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. rewrite_publish(Message = #message{topic = Topic}, Rules) -> diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index ed3331d51..c6d6dd554 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%%=================================================================== -module(emqx_mod_subscription). diff --git a/src/emqx_modules.erl b/src/emqx_modules.erl index 31dbd3c4a..cd5523280 100644 --- a/src/emqx_modules.erl +++ b/src/emqx_modules.erl @@ -1,23 +1,24 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_modules). -export([load/0, unload/0]). +-spec(load() -> ok). load() -> lists:foreach( fun({Mod, Env}) -> @@ -25,6 +26,7 @@ load() -> io:format("Load ~s module successfully.~n", [Mod]) end, emqx_config:get_env(modules, [])). +-spec(unload() -> ok). unload() -> lists:foreach( fun({Mod, Env}) -> diff --git a/src/emqx_mqtt_metrics.erl b/src/emqx_mqtt_metrics.erl deleted file mode 100644 index 175990f0b..000000000 --- a/src/emqx_mqtt_metrics.erl +++ /dev/null @@ -1,159 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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_metrics). - --include("emqx_mqtt.hrl"). - --import(emqx_metrics, [inc/1]). - --export([init/0]). - -%% Received/Sent Metrics --export([received/1, sent/1]). - -%% Bytes sent and received of Broker --define(SYSTOP_BYTES, [ - {counter, 'bytes/received'}, % Total bytes received - {counter, 'bytes/sent'} % Total bytes sent -]). - -%% Packets sent and received of Broker --define(SYSTOP_PACKETS, [ - {counter, 'packets/received'}, % All Packets received - {counter, 'packets/sent'}, % All Packets sent - {counter, 'packets/connect'}, % CONNECT Packets received - {counter, 'packets/connack'}, % CONNACK Packets sent - {counter, 'packets/publish/received'}, % PUBLISH packets received - {counter, 'packets/publish/sent'}, % PUBLISH packets sent - {counter, 'packets/puback/received'}, % PUBACK packets received - {counter, 'packets/puback/sent'}, % PUBACK packets sent - {counter, 'packets/puback/missed'}, % PUBACK packets missed - {counter, 'packets/pubrec/received'}, % PUBREC packets received - {counter, 'packets/pubrec/sent'}, % PUBREC packets sent - {counter, 'packets/pubrec/missed'}, % PUBREC packets missed - {counter, 'packets/pubrel/received'}, % PUBREL packets received - {counter, 'packets/pubrel/sent'}, % PUBREL packets sent - {counter, 'packets/pubrel/missed'}, % PUBREL packets missed - {counter, 'packets/pubcomp/received'}, % PUBCOMP packets received - {counter, 'packets/pubcomp/sent'}, % PUBCOMP packets sent - {counter, 'packets/pubcomp/missed'}, % PUBCOMP packets missed - {counter, 'packets/subscribe'}, % SUBSCRIBE Packets received - {counter, 'packets/suback'}, % SUBACK packets sent - {counter, 'packets/unsubscribe'}, % UNSUBSCRIBE Packets received - {counter, 'packets/unsuback'}, % UNSUBACK Packets sent - {counter, 'packets/pingreq'}, % PINGREQ packets received - {counter, 'packets/pingresp'}, % PINGRESP Packets sent - {counter, 'packets/disconnect'} % DISCONNECT Packets received -]). - -%% Messages sent and received of broker --define(SYSTOP_MESSAGES, [ - {counter, 'messages/received'}, % All Messages received - {counter, 'messages/sent'}, % All Messages sent - {counter, 'messages/qos0/received'}, % QoS0 Messages received - {counter, 'messages/qos0/sent'}, % QoS0 Messages sent - {counter, 'messages/qos1/received'}, % QoS1 Messages received - {counter, 'messages/qos1/sent'}, % QoS1 Messages sent - {counter, 'messages/qos2/received'}, % QoS2 Messages received - {counter, 'messages/qos2/sent'}, % QoS2 Messages sent - {counter, 'messages/qos2/dropped'}, % QoS2 Messages dropped - {gauge, 'messages/retained'}, % Messagea retained - {counter, 'messages/dropped'}, % Messages dropped - {counter, 'messages/forward'} % Messages forward -]). - -% Init metrics -init() -> - lists:foreach(fun emqx_metrics:create/1, - ?SYSTOP_BYTES ++ ?SYSTOP_PACKETS ++ ?SYSTOP_MESSAGES). - -%% @doc Count packets received. --spec(received(mqtt_packet()) -> ok). -received(Packet) -> - inc('packets/received'), - received1(Packet). -received1(?PUBLISH_PACKET(Qos, _PktId)) -> - inc('packets/publish/received'), - inc('messages/received'), - qos_received(Qos); -received1(?PACKET(Type)) -> - received2(Type). -received2(?CONNECT) -> - inc('packets/connect'); -received2(?PUBACK) -> - inc('packets/puback/received'); -received2(?PUBREC) -> - inc('packets/pubrec/received'); -received2(?PUBREL) -> - inc('packets/pubrel/received'); -received2(?PUBCOMP) -> - inc('packets/pubcomp/received'); -received2(?SUBSCRIBE) -> - inc('packets/subscribe'); -received2(?UNSUBSCRIBE) -> - inc('packets/unsubscribe'); -received2(?PINGREQ) -> - inc('packets/pingreq'); -received2(?DISCONNECT) -> - inc('packets/disconnect'); -received2(_) -> - ignore. -qos_received(?QOS_0) -> - inc('messages/qos0/received'); -qos_received(?QOS_1) -> - inc('messages/qos1/received'); -qos_received(?QOS_2) -> - inc('messages/qos2/received'). - -%% @doc Count packets received. Will not count $SYS PUBLISH. --spec(sent(mqtt_packet()) -> ignore | non_neg_integer()). -sent(?PUBLISH_PACKET(_Qos, <<"$SYS/", _/binary>>, _, _)) -> - ignore; -sent(Packet) -> - inc('packets/sent'), - sent1(Packet). -sent1(?PUBLISH_PACKET(Qos, _PktId)) -> - inc('packets/publish/sent'), - inc('messages/sent'), - qos_sent(Qos); -sent1(?PACKET(Type)) -> - sent2(Type). -sent2(?CONNACK) -> - inc('packets/connack'); -sent2(?PUBACK) -> - inc('packets/puback/sent'); -sent2(?PUBREC) -> - inc('packets/pubrec/sent'); -sent2(?PUBREL) -> - inc('packets/pubrel/sent'); -sent2(?PUBCOMP) -> - inc('packets/pubcomp/sent'); -sent2(?SUBACK) -> - inc('packets/suback'); -sent2(?UNSUBACK) -> - inc('packets/unsuback'); -sent2(?PINGRESP) -> - inc('packets/pingresp'); -sent2(_Type) -> - ignore. -qos_sent(?QOS_0) -> - inc('messages/qos0/sent'); -qos_sent(?QOS_1) -> - inc('messages/qos1/sent'); -qos_sent(?QOS_2) -> - inc('messages/qos2/sent'). - diff --git a/src/emqx_parser.erl b/src/emqx_parser.erl index f72e8ea4e..27faaacc2 100644 --- a/src/emqx_parser.erl +++ b/src/emqx_parser.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_parser). diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index 3d40d1849..837ed8a0e 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%%=================================================================== -module(emqx_plugins). @@ -83,7 +83,7 @@ load_expand_plugin(PluginDir) -> end, Modules), case filelib:wildcard(Ebin ++ "/*.app") of [App|_] -> application:load(list_to_atom(filename:basename(App, ".app"))); - _ -> emqx_log:error("App file cannot be found."), + _ -> emqx_logger:error("App file cannot be found."), {error, load_app_fail} end. @@ -128,7 +128,7 @@ with_loaded_file(File, SuccFun) -> {ok, Names} -> SuccFun(Names); {error, Error} -> - emqx_log:error("[Plugins] Failed to read: ~p, error: ~p", [File, Error]), + emqx_logger:error("[Plugins] Failed to read: ~p, error: ~p", [File, Error]), {error, Error} end. @@ -136,7 +136,7 @@ load_plugins(Names, Persistent) -> Plugins = list(), NotFound = Names -- names(Plugins), case NotFound of [] -> ok; - NotFound -> emqx_log:error("[Plugins] Cannot find plugins: ~p", [NotFound]) + NotFound -> emqx_logger:error("[Plugins] Cannot find plugins: ~p", [NotFound]) end, NeedToLoad = Names -- NotFound -- names(started_app), [load_plugin(find_plugin(Name, Plugins), Persistent) || Name <- NeedToLoad]. @@ -185,12 +185,12 @@ plugin(CfgFile) -> load(PluginName) when is_atom(PluginName) -> case lists:member(PluginName, names(started_app)) of true -> - emqx_log:error("[Plugins] Plugin ~s is already started", [PluginName]), + emqx_logger:error("[Plugins] Plugin ~s is already started", [PluginName]), {error, already_started}; false -> case find_plugin(PluginName) of false -> - emqx_log:error("[Plugins] Plugin ~s not found", [PluginName]), + emqx_logger:error("[Plugins] Plugin ~s not found", [PluginName]), {error, not_found}; Plugin -> load_plugin(Plugin, true) @@ -218,12 +218,12 @@ load_app(App) -> start_app(App, SuccFun) -> case application:ensure_all_started(App) of {ok, Started} -> - emqx_log:info("Started Apps: ~p", [Started]), - emqx_log:info("Load plugin ~s successfully", [App]), + emqx_logger:info("Started Apps: ~p", [Started]), + emqx_logger:info("Load plugin ~s successfully", [App]), SuccFun(App), {ok, Started}; {error, {ErrApp, Reason}} -> - emqx_log:error("Load plugin ~s error, cannot start app ~s for ~p", [App, ErrApp, Reason]), + emqx_logger:error("Load plugin ~s error, cannot start app ~s for ~p", [App, ErrApp, Reason]), {error, {ErrApp, Reason}} end. @@ -240,10 +240,10 @@ unload(PluginName) when is_atom(PluginName) -> {true, true} -> unload_plugin(PluginName, true); {false, _} -> - emqx_log:error("Plugin ~s is not started", [PluginName]), + emqx_logger:error("Plugin ~s is not started", [PluginName]), {error, not_started}; {true, false} -> - emqx_log:error("~s is not a plugin, cannot unload it", [PluginName]), + emqx_logger:error("~s is not a plugin, cannot unload it", [PluginName]), {error, not_found} end. @@ -258,11 +258,11 @@ unload_plugin(App, Persistent) -> stop_app(App) -> case application:stop(App) of ok -> - emqx_log:info("Stop plugin ~s successfully", [App]), ok; + emqx_logger:info("Stop plugin ~s successfully", [App]), ok; {error, {not_started, App}} -> - emqx_log:error("Plugin ~s is not started", [App]), ok; + emqx_logger:error("Plugin ~s is not started", [App]), ok; {error, Reason} -> - emqx_log:error("Stop plugin ~s error: ~p", [App]), {error, Reason} + emqx_logger:error("Stop plugin ~s error: ~p", [App]), {error, Reason} end. %%-------------------------------------------------------------------- @@ -294,7 +294,7 @@ plugin_loaded(Name, true) -> ignore end; {error, Error} -> - emqx_log:error("Cannot read loaded plugins: ~p", [Error]) + emqx_logger:error("Cannot read loaded plugins: ~p", [Error]) end. plugin_unloaded(_Name, false) -> @@ -306,10 +306,10 @@ plugin_unloaded(Name, true) -> true -> write_loaded(lists:delete(Name, Names)); false -> - emqx_log:error("Cannot find ~s in loaded_file", [Name]) + emqx_logger:error("Cannot find ~s in loaded_file", [Name]) end; {error, Error} -> - emqx_log:error("Cannot read loaded_plugins: ~p", [Error]) + emqx_logger:error("Cannot read loaded_plugins: ~p", [Error]) end. read_loaded() -> @@ -328,7 +328,7 @@ write_loaded(AppNames) -> file:write(Fd, iolist_to_binary(io_lib:format("~s.~n", [Name]))) end, AppNames); {error, Error} -> - emqx_log:error("Open File ~p Error: ~p", [File, Error]), + emqx_logger:error("Open File ~p Error: ~p", [File, Error]), {error, Error} end. diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index 7e76e0a18..bcb4ae537 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -1,22 +1,24 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_pmon). --export([new/0, monitor/2, monitor/3, demonitor/2, find/2, erase/2]). +-export([new/0]). + +-export([monitor/2, monitor/3, demonitor/2, find/2, erase/2]). -compile({no_auto_import,[monitor/3]}). @@ -43,7 +45,8 @@ monitor(Pid, Val, PM = {?MODULE, [M]}) -> demonitor(Pid, PM = {?MODULE, [M]}) -> case maps:find(Pid, M) of {ok, {Ref, _Val}} -> - erlang:demonitor(Ref, [flush]), + %% Don't flush + _ = erlang:demonitor(Ref), {?MODULE, [maps:remove(Pid, M)]}; error -> PM diff --git a/src/emqx_pool.erl b/src/emqx_pool.erl new file mode 100644 index 000000000..43f4ef845 --- /dev/null +++ b/src/emqx_pool.erl @@ -0,0 +1,106 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_pool). + +-behaviour(gen_server). + +%% Start the pool supervisor +-export([start_link/0]). + +%% API Exports +-export([start_link/2, submit/1, async_submit/1]). + +%% gen_server Function Exports +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {pool, id}). + +-define(POOL, ?MODULE). + +%% @doc Start Pooler Supervisor. +start_link() -> + emqx_pool_sup:start_link(?POOL, random, {?MODULE, start_link, []}). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +-spec(start_link(atom(), pos_integer()) + -> {ok, pid()} | ignore | {error, term()}). +start_link(Pool, Id) -> + gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, + ?MODULE, [Pool, Id], []). + +%% @doc Submit work to the pool +-spec(submit(fun()) -> any()). +submit(Fun) -> + gen_server:call(worker(), {submit, Fun}, infinity). + +%% @doc Submit work to the pool asynchronously +-spec(async_submit(fun()) -> ok). +async_submit(Fun) -> + gen_server:cast(worker(), {async_submit, Fun}). + +worker() -> + gproc_pool:pick_worker(pool). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([Pool, Id]) -> + gproc_pool:connect_worker(Pool, {Pool, Id}), + {ok, #state{pool = Pool, id = Id}}. + +handle_call({submit, Fun}, _From, State) -> + {reply, catch run(Fun), State}; + +handle_call(Req, _From, State) -> + emqx_logger:error("[POOL] Unexpected request: ~p", [Req]), + {reply, ignore, State}. + +handle_cast({async_submit, Fun}, State) -> + try run(Fun) + catch _:Error -> + emqx_logger:error("[POOL] Error: ~p, ~p", [Error, erlang:get_stacktrace()]) + end, + {noreply, State}; + +handle_cast(Msg, State) -> + emqx_logger:error("[POOL] Unexpected msg: ~p", [Msg]), + {noreply, State}. + +handle_info(Info, State) -> + emqx_logger:error("[POOL] Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #state{pool = Pool, id = Id}) -> + gproc_pool:disconnect_worker(Pool, {Pool, Id}). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +run({M, F, A}) -> + erlang:apply(M, F, A); +run(Fun) when is_function(Fun) -> + Fun(). + diff --git a/src/emqx_pool_sup.erl b/src/emqx_pool_sup.erl index 789e00a75..29f656c73 100644 --- a/src/emqx_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_pool_sup). @@ -39,12 +39,11 @@ start_link(Pool, Type, MFA) -> start_link(Pool, Type, Schedulers, MFA). -spec(start_link(atom() | tuple(), atom(), pos_integer(), mfa()) -> {ok, pid()} | {error, term()}). +start_link(Pool, Type, Size, MFA) when is_atom(Pool) -> + supervisor:start_link({local, Pool}, ?MODULE, [Pool, Type, Size, MFA]); start_link(Pool, Type, Size, MFA) -> supervisor:start_link(?MODULE, [Pool, Type, Size, MFA]). -%% sup_name(Pool) when is_atom(Pool) -> -%% list_to_atom(atom_to_list(Pool) ++ "_pool_sup"). - init([Pool, Type, Size, {M, F, Args}]) -> ensure_pool(Pool, Type, [{size, Size}]), {ok, {{one_for_one, 10, 3600}, [ diff --git a/src/emqx_pooler.erl b/src/emqx_pooler.erl deleted file mode 100644 index 9bce4e2b5..000000000 --- a/src/emqx_pooler.erl +++ /dev/null @@ -1,98 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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_pooler). - --behaviour(gen_server). - -%% Start the pool supervisor --export([start_link/0]). - -%% API Exports --export([start_link/2, submit/1, async_submit/1]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {pool, id}). - -%% @doc Start Pooler Supervisor. -start_link() -> - emqx_pool_sup:start_link(pooler, random, {?MODULE, start_link, []}). - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - --spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). -start_link(Pool, Id) -> - gen_server:start_link({local, name(Id)}, ?MODULE, [Pool, Id], []). - -name(Id) -> list_to_atom(lists:concat([?MODULE, "_", Id])). - -%% @doc Submit work to pooler -submit(Fun) -> gen_server:call(worker(), {submit, Fun}, infinity). - -%% @doc Submit work to pooler asynchronously -async_submit(Fun) -> - gen_server:cast(worker(), {async_submit, Fun}). - -worker() -> - gproc_pool:pick_worker(pooler). - -%%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- - -init([Pool, Id]) -> - gproc_pool:connect_worker(Pool, {Pool, Id}), - {ok, #state{pool = Pool, id = Id}}. - -handle_call({submit, Fun}, _From, State) -> - {reply, catch run(Fun), State}; - -handle_call(_Req, _From, State) -> - {reply, ok, State}. - -handle_cast({async_submit, Fun}, State) -> - try run(Fun) - catch _:Error -> - emqx_log:error("Pooler Error: ~p, ~p", [Error, erlang:get_stacktrace()]) - end, - {noreply, State}; - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, #state{pool = Pool, id = Id}) -> - gproc_pool:disconnect_worker(Pool, {Pool, Id}). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -run({M, F, A}) -> - erlang:apply(M, F, A); -run(Fun) when is_function(Fun) -> - Fun(). - diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index a86c19459..f301378cf 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -57,8 +57,8 @@ -define(STATS_KEYS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(LOG(Level, Format, Args, State), - emqx_log:Level([{client, State#proto_state.client_id}], "Client(~s@~s): " ++ Format, - [State#proto_state.client_id, esockd_net:format(State#proto_state.peername) | Args])). + emqx_logger:Level([{client, State#proto_state.client_id}], "Client(~s@~s): " ++ Format, + [State#proto_state.client_id, esockd_net:format(State#proto_state.peername) | Args])). %% @doc Init protocol init(Peername, SendFun, Opts) -> @@ -220,7 +220,7 @@ process(?CONNECT_PACKET(Var), State0) -> {ok, Session} -> %% TODO:... SP = true, %% TODO:... %% TODO: Register the client - emqx_cm:reg(clientid(State2)), + emqx_cm:register_client(clientid(State2)), %%emqx_cm:reg(client(State2)), %% Start keepalive start_keepalive(KeepAlive, State2), @@ -362,7 +362,7 @@ send(Msg, State = #proto_state{client_id = ClientId, send(Packet = ?PACKET(Type), State = #proto_state{sendfun = SendFun, stats_data = Stats}) -> trace(send, Packet, State), - emqx_mqtt_metrics:sent(Packet), + emqx_metrics:sent(Packet), SendFun(Packet), {ok, State#proto_state{stats_data = inc_stats(send, Type, Stats)}}. @@ -398,15 +398,14 @@ stop_if_auth_failure(_RC, State) -> shutdown(_Error, #proto_state{client_id = undefined}) -> ignore; -shutdown(conflict, _State) -> - %% let it down +shutdown(conflict, _State = #proto_state{client_id = ClientId}) -> + emqx_cm:unregister_client(ClientId), ignore; -shutdown(mnesia_conflict, _State) -> - %% let it down - %% emqx_cm:unreg(ClientId); +shutdown(mnesia_conflict, _State = #proto_state{client_id = ClientId}) -> + emqx_cm:unregister_client(ClientId), ignore; - -shutdown(Error, State = #proto_state{will_msg = WillMsg}) -> +shutdown(Error, State = #proto_state{client_id = ClientId, + will_msg = WillMsg}) -> ?LOG(info, "Shutdown for ~p", [Error], State), Client = client(State), %% Auth failure not publish the will message @@ -415,11 +414,11 @@ shutdown(Error, State = #proto_state{will_msg = WillMsg}) -> false -> send_willmsg(Client, WillMsg) end, emqx_hooks:run('client.disconnected', [Error], Client), - %% let it down - %% emqx_cm:unreg(ClientId). + emqx_cm:unregister_client(ClientId), ok. -willmsg(Packet, State = #proto_state{mountpoint = MountPoint}) when is_record(Packet, mqtt_packet_connect) -> +willmsg(Packet, State = #proto_state{mountpoint = MountPoint}) + when is_record(Packet, mqtt_packet_connect) -> case emqx_packet:to_message(Packet) of undefined -> undefined; Msg -> mount(replvar(MountPoint, State), Msg) diff --git a/src/emqx_rate_limiter.erl b/src/emqx_rate_limiter.erl new file mode 100644 index 000000000..76eaf0c61 --- /dev/null +++ b/src/emqx_rate_limiter.erl @@ -0,0 +1,67 @@ +%%%------------------------------------------------------------------- +%%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%%% +%%% 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_rate_limiter). + +-behaviour(gen_server). + +%% API +-export([start_link/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-define(SERVER, ?MODULE). + +-record(state, {}). + +%%%=================================================================== +%%% API +%%%=================================================================== + +%% @doc Starts the server +-spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +init([]) -> + {ok, #state{}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 7d16d12d8..dffc91133 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -1,59 +1,53 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_router). -behaviour(gen_server). -include("emqx.hrl"). - -include_lib("ekka/include/ekka.hrl"). -%% Mnesia Bootstrap +%% Mnesia bootstrap -export([mnesia/1]). -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). -%% Start -export([start_link/2]). +%% Route APIs +-export([add_route/2, add_route/3, get_routes/1, del_route/2, del_route/3]). +-export([has_routes/1, match_routes/1, print_routes/1]). + %% Topics -export([topics/0]). -%% Route management APIs --export([add_route/2, add_route/3, get_routes/1, del_route/2, del_route/3]). - --export([has_routes/1, match_routes/1, print_routes/1]). - %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --type(group() :: binary()). - --type(destination() :: node() | {group(), node()} - | {cluster(), group(), node()}). +-type(destination() :: node() | {binary(), node()}). -record(state, {pool, id}). -define(ROUTE, emqx_route). %%-------------------------------------------------------------------- -%% Mnesia Bootstrap +%% Mnesia bootstrap %%-------------------------------------------------------------------- mnesia(boot) -> @@ -67,14 +61,17 @@ mnesia(copy) -> ok = ekka_mnesia:copy_table(?ROUTE). %%-------------------------------------------------------------------- -%% Start a router +%% Strat a Router %%-------------------------------------------------------------------- +-spec(start_link(atom(), pos_integer()) + -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> - gen_server:start_link(?MODULE, [Pool, Id], [{hibernate_after, 10000}]). + gen_server:start_link(emqx_misc:proc_name(?MODULE, Id), + ?MODULE, [Pool, Id], [{hibernate_after, 10000}]). %%-------------------------------------------------------------------- -%% Add/Del Routes +%% Route APIs %%-------------------------------------------------------------------- %% @doc Add a route @@ -105,40 +102,24 @@ del_route(From, Topic, Dest) when is_binary(Topic) -> has_routes(Topic) when is_binary(Topic) -> ets:member(?ROUTE, Topic). -%%-------------------------------------------------------------------- -%% Topics -%%-------------------------------------------------------------------- - --spec(topics() -> list(binary())). +%% @doc Get topics +-spec(topics() -> list(topic())). topics() -> mnesia:dirty_all_keys(?ROUTE). -%%-------------------------------------------------------------------- -%% Match routes -%%-------------------------------------------------------------------- - %% @doc Match routes %% Optimize: routing table will be replicated to all router nodes. --spec(match_routes(Topic:: topic()) -> [{topic(), binary() | node()}]). +-spec(match_routes(topic()) -> [route()]). match_routes(Topic) when is_binary(Topic) -> Matched = mnesia:ets(fun emqx_trie:match/1, [Topic]), - Routes = [ets:lookup(?ROUTE, To) || To <- [Topic | Matched]], - [{To, Dest} || #route{topic = To, dest = Dest} <- lists:append(Routes)]. - -%%-------------------------------------------------------------------- -%% Print routes -%%-------------------------------------------------------------------- + lists:append([get_routes(To) || To <- [Topic | Matched]]). %% @doc Print routes to a topic -spec(print_routes(topic()) -> ok). print_routes(Topic) -> - lists:foreach(fun({To, Dest}) -> + lists:foreach(fun(#route{topic = To, dest = Dest}) -> io:format("~s -> ~s~n", [To, Dest]) end, match_routes(Topic)). -%%-------------------------------------------------------------------- -%% Utility functions -%%-------------------------------------------------------------------- - cast(Router, Msg) -> gen_server:cast(Router, Msg). @@ -154,7 +135,7 @@ init([Pool, Id]) -> {ok, #state{pool = Pool, id = Id}}. handle_call(Req, _From, State) -> - emqx_log:error("[Router] Unexpected request: ~p", [Req]), + emqx_logger:error("[Router] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast({add_route, From, Route}, State) -> @@ -163,12 +144,12 @@ handle_cast({add_route, From, Route}, State) -> {noreply, State}; handle_cast({add_route, Route = #route{topic = Topic, dest = Dest}}, State) -> - case lists:member(Route, ets:lookup(?ROUTE, Topic)) of + case lists:member(Route, get_routes(Topic)) of true -> ok; false -> ok = emqx_router_helper:monitor(Dest), case emqx_topic:wildcard(Topic) of - true -> trans(fun add_trie_route/1, [Route]); + true -> log(trans(fun add_trie_route/1, [Route])); false -> add_direct_route(Route) end end, @@ -185,18 +166,18 @@ handle_cast({del_route, Route = #route{topic = Topic}}, State) -> true -> ok; false -> case emqx_topic:wildcard(Topic) of - true -> trans(fun del_trie_route/1, [Route]); + true -> log(trans(fun del_trie_route/1, [Route])); false -> del_direct_route(Route) end end, {noreply, State}; handle_cast(Msg, State) -> - emqx_log:error("[Router] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Router] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> - emqx_log:error("[Router] Unexpected info: ~p", [Info]), + emqx_logger:error("[Router] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> @@ -237,8 +218,10 @@ del_trie_route(Route = #route{topic = Topic}) -> trans(Fun, Args) -> case mnesia:transaction(Fun, Args) of {atomic, _} -> ok; - {aborted, Error} -> - emqx_log:error("[Router] Mnesia aborted: ~p", [Error]), - {error, Error} + {aborted, Error} -> {error, Error} end. +log(ok) -> ok; +log({error, Error}) -> + emqx_logger:error("[Router] Mnesia aborted: ~p", [Error]). + diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index 5a4de5f69..f4a19aca9 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_router_helper). @@ -20,72 +20,73 @@ -include("emqx.hrl"). -%% Mnesia Bootstrap +%% Mnesia bootstrap -export([mnesia/1]). -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). %% API --export([start_link/1, monitor/1]). +-export([start_link/0, monitor/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(routing_node, {name, ts}). +-record(routing_node, {name, const = unused}). --record(state, {nodes = [], stats_fun, stats_timer}). +-record(state, {nodes = []}). -compile({no_auto_import, [monitor/1]}). -define(SERVER, ?MODULE). --define(TAB, emqx_routing_node). +-define(ROUTE, emqx_route). +-define(ROUTING_NODE, emqx_routing_node). --define(LOCK, {?MODULE, clean_routes}). +-define(LOCK, {?MODULE, cleanup_routes}). %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- mnesia(boot) -> - ok = ekka_mnesia:create_table(?TAB, [ + ok = ekka_mnesia:create_table(?ROUTING_NODE, [ {type, set}, {ram_copies, [node()]}, {record_name, routing_node}, {attributes, record_info(fields, routing_node)}]); mnesia(copy) -> - ok = ekka_mnesia:copy_table(?TAB). + ok = ekka_mnesia:copy_table(?ROUTING_NODE). %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- %% @doc Starts the router helper --spec(start_link(fun()) -> {ok, pid()} | ignore | {error, any()}). -start_link(StatsFun) -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [StatsFun], []). +-spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %% @doc Monitor routing node --spec(monitor(node()) -> ok). -monitor({_Group, Node}) -> +-spec(monitor(node() | {binary(), node()}) -> ok). +monitor({_Group, Node}) -> monitor(Node); monitor(Node) when is_atom(Node) -> - case ekka:is_member(Node) orelse ets:member(?TAB, Node) of + case ekka:is_member(Node) + orelse ets:member(?ROUTING_NODE, Node) of true -> ok; - false -> - mnesia:dirty_write(?TAB, #routing_node{name = Node, ts = os:timestamp()}) + false -> mnesia:dirty_write(?ROUTING_NODE, #routing_node{name = Node}) end. %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- -init([StatsFun]) -> +init([]) -> ekka:monitor(membership), - mnesia:subscribe({table, ?TAB, simple}), + mnesia:subscribe({table, ?ROUTING_NODE, simple}), Nodes = lists:foldl( fun(Node, Acc) -> case ekka:is_member(Node) of @@ -93,21 +94,21 @@ init([StatsFun]) -> false -> _ = erlang:monitor_node(Node, true), [Node | Acc] end - end, [], mnesia:dirty_all_keys(?TAB)), - {ok, TRef} = timer:send_interval(timer:seconds(1), stats), - {ok, #state{nodes = Nodes, stats_fun = StatsFun, stats_timer = TRef}}. + end, [], mnesia:dirty_all_keys(?ROUTING_NODE)), + emqx_stats:update_interval(route_stats, stats_fun()), + {ok, #state{nodes = Nodes}}. handle_call(Req, _From, State) -> - emqx_log:error("[RouterHelper] Unexpected request: ~p", [Req]), + emqx_logger:error("[RouterHelper] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - emqx_log:error("[RouterHelper] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[RouterHelper] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info({mnesia_table_event, {write, #routing_node{name = Node}, _}}, State = #state{nodes = Nodes}) -> - emqx_log:info("[RouterHelper] New routing node: ~s", [Node]), + emqx_logger:info("[RouterHelper] New routing node: ~s", [Node]), case ekka:is_member(Node) orelse lists:member(Node, Nodes) of true -> {noreply, State}; false -> _ = erlang:monitor_node(Node, true), @@ -122,8 +123,8 @@ handle_info({nodedown, Node}, State = #state{nodes = Nodes}) -> fun() -> mnesia:transaction(fun cleanup_routes/1, [Node]) end), - mnesia:dirty_delete(?TAB, Node), - handle_info(stats, State#state{nodes = lists:delete(Node, Nodes)}); + mnesia:dirty_delete(?ROUTING_NODE, Node), + {noreply, State#state{nodes = lists:delete(Node, Nodes)}}; handle_info({membership, {mnesia, down, Node}}, State) -> handle_info({nodedown, Node}, State); @@ -131,18 +132,14 @@ handle_info({membership, {mnesia, down, Node}}, State) -> handle_info({membership, _Event}, State) -> {noreply, State}; -handle_info(stats, State = #state{stats_fun = StatsFun}) -> - ok = StatsFun(mnesia:table_info(emqx_route, size)), - {noreply, State, hibernate}; - handle_info(Info, State) -> - emqx_log:error("[RouteHelper] Unexpected info: ~p", [Info]), + emqx_logger:error("[RouteHelper] Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{stats_timer = TRef}) -> - timer:cancel(TRef), +terminate(_Reason, #state{}) -> ekka:unmonitor(membership), - mnesia:unsubscribe({table, ?TAB, simple}). + emqx_stats:cancel_update(route_stats), + mnesia:unsubscribe({table, ?ROUTING_NODE, simple}). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -151,9 +148,19 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- +stats_fun() -> + fun() -> + case ets:info(?ROUTE, size) of + undefined -> ok; + Size -> + emqx_stats:setstat('routes/count', 'routes/max', Size), + emqx_stats:setstat('topics/count', 'topics/max', Size) + end + end. + cleanup_routes(Node) -> Patterns = [#route{_ = '_', dest = Node}, #route{_ = '_', dest = {'_', Node}}], - [mnesia:delete_object(?TAB, R, write) - || Pat <- Patterns, R <- mnesia:match_object(?TAB, Pat, write)]. + [mnesia:delete_object(?ROUTE, Route, write) + || Pat <- Patterns, Route <- mnesia:match_object(?ROUTE, Pat, write)]. diff --git a/src/emqx_router_sup.erl b/src/emqx_router_sup.erl index 98d4eb68d..bc8daf4b4 100644 --- a/src/emqx_router_sup.erl +++ b/src/emqx_router_sup.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_router_sup). @@ -26,16 +26,14 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - StatsFun = emqx_stats:statsfun('routes/count', 'routes/max'), - %% Router helper - Helper = {router_helper, {emqx_router_helper, start_link, [StatsFun]}, + Helper = {router_helper, {emqx_router_helper, start_link, []}, permanent, 5000, worker, [emqx_router_helper]}, %% Router pool - PoolSup = emqx_pool_sup:spec(router_pool, - [router, hash, emqx_sys:schedulers(), - {emqx_router, start_link, []}]), + RouterPool = emqx_pool_sup:spec(emqx_router_pool, + [router, hash, emqx_vm:schedulers(), + {emqx_router, start_link, []}]), - {ok, {{one_for_all, 0, 3600}, [Helper, PoolSup]}}. + {ok, {{one_for_all, 0, 1}, [Helper, RouterPool]}}. diff --git a/src/emqx_rpc.erl b/src/emqx_rpc.erl index b133b2292..f7c13fa4e 100644 --- a/src/emqx_rpc.erl +++ b/src/emqx_rpc.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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). @@ -20,12 +20,14 @@ -export([multicall/4]). +-define(RPC, rpc). + call(Node, Mod, Fun, Args) -> - rpc:call(Node, Mod, Fun, Args). + ?RPC:call(Node, Mod, Fun, Args). multicall(Nodes, Mod, Fun, Args) -> - rpc:multicall(Nodes, Mod, Fun, Args). + ?RPC:multicall(Nodes, Mod, Fun, Args). cast(Node, Mod, Fun, Args) -> - rpc:cast(Node, Mod, Fun, Args). + ?RPC:cast(Node, Mod, Fun, Args). diff --git a/src/emqx_session.erl b/src/emqx_session.erl index f6f88de40..4d7ee4ce6 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -155,8 +155,8 @@ created_at]). -define(LOG(Level, Format, Args, State), - emqx_log:Level([{client, State#state.client_id}], - "Session(~s): " ++ Format, [State#state.client_id | Args])). + emqx_logger:Level([{client, State#state.client_id}], + "Session(~s): " ++ Format, [State#state.client_id | Args])). %% @doc Start a Session -spec(start_link(map()) -> {ok, pid()} | {error, term()}). @@ -296,7 +296,7 @@ init(#{clean_start := CleanStart, enable_stats = EnableStats, ignore_loop_deliver = IgnoreLoopDeliver, created_at = os:timestamp()}, - emqx_sm:register_session(#session{sid = ClientId, pid = self()}, info(State)), + emqx_sm:register_session(ClientId, info(State)), emqx_hooks:run('session.created', [ClientId, Username]), io:format("Session started: ~p~n", [self()]), {ok, emit_stats(State), hibernate}. @@ -342,7 +342,7 @@ handle_call(state, _From, State) -> reply(?record_to_proplist(state, State, ?STATE_KEYS), State); handle_call(Req, _From, State) -> - emqx_log:error("[Session] Unexpected request: ~p", [Req]), + emqx_logger:error("[Session] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast({subscribe, From, TopicTable, AckFun}, @@ -501,7 +501,7 @@ handle_cast({resume, ClientPid}, {noreply, emit_stats(dequeue(retry_delivery(true, State1)))}; handle_cast(Msg, State) -> - emqx_log:error("[Session] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Session] Unexpected msg: ~p", [Msg]), {noreply, State}. %% Ignore Messages delivered by self @@ -551,13 +551,13 @@ handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = ClientPid}) -> {noreply, State, hibernate}; handle_info(Info, State) -> - emqx_log:error("[Session] Unexpected info: ~p", [Info]), + emqx_logger:error("[Session] Unexpected info: ~p", [Info]), {noreply, State}. terminate(Reason, #state{client_id = ClientId, username = Username}) -> emqx_hooks:run('session.terminated', [ClientId, Username, Reason]), - emqx_sm:unregister_session(#session{sid = ClientId, pid = self()}). + emqx_sm:unregister_session(ClientId). code_change(_OldVsn, Session, _Extra) -> {ok, Session}. @@ -812,8 +812,7 @@ next_msg_id(State = #state{next_msg_id = Id}) -> emit_stats(State = #state{enable_stats = false}) -> State; emit_stats(State = #state{client_id = ClientId}) -> - Session = #session{sid = ClientId, pid = self()}, - emqx_sm_stats:set_session_stats(Session, stats(State)), + emqx_sm:set_session_stats(ClientId, stats(State)), State. inc_stats(Key) -> put(Key, get(Key) + 1). diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl index 6a19e29ef..c1941207b 100644 --- a/src/emqx_session_sup.erl +++ b/src/emqx_session_sup.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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). @@ -24,13 +24,12 @@ -export([init/1]). --spec(start_link() -> {ok, pid()}). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). --spec(start_session(session()) -> {ok, pid()}). -start_session(Session) -> - supervisor:start_child(?MODULE, [Session]). +-spec(start_session(map()) -> {ok, pid()}). +start_session(Attrs) -> + supervisor:start_child(?MODULE, [Attrs]). %%-------------------------------------------------------------------- %% Supervisor callbacks diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index a5a3a5d9f..f4f0ecf8e 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_shared_sub). @@ -20,13 +20,16 @@ -include("emqx.hrl"). -%% API +%% Mnesia bootstrap +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + -export([start_link/0]). -export([strategy/0]). - -export([subscribe/3, unsubscribe/3]). - -export([dispatch/3]). %% gen_server callbacks @@ -41,18 +44,26 @@ -record(shared_subscription, {group, topic, subpid}). +%%-------------------------------------------------------------------- +%% Mnesia bootstrap +%%-------------------------------------------------------------------- + +mnesia(boot) -> + ok = ekka_mnesia:create_table(?TAB, [ + {type, bag}, + {ram_copies, [node()]}, + {record_name, shared_subscription}, + {attributes, record_info(fields, shared_subscription)}]); + +mnesia(copy) -> + ok = ekka_mnesia:copy_table(?TAB). + %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- -spec(start_link() -> {ok, pid()} | ignore | {error, any()}). start_link() -> - ok = ekka_mnesia:create_table(?TAB, [ - {type, bag}, - {ram_copies, [node()]}, - {record_name, shared_subscription}, - {attributes, record_info(fields, shared_subscription)}]), - ok = ekka_mnesia:copy_table(?TAB), gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -spec(strategy() -> random | hash). @@ -62,24 +73,17 @@ strategy() -> subscribe(undefined, _Topic, _SubPid) -> ok; subscribe(Group, Topic, SubPid) when is_pid(SubPid) -> - mnesia:dirty_write(?TAB, r(Group, Topic, SubPid)), + mnesia:dirty_write(?TAB, record(Group, Topic, SubPid)), gen_server:cast(?SERVER, {monitor, SubPid}). unsubscribe(undefined, _Topic, _SubPid) -> ok; unsubscribe(Group, Topic, SubPid) when is_pid(SubPid) -> - mnesia:dirty_delete_object(?TAB, r(Group, Topic, SubPid)). + mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)). -r(Group, Topic, SubPid) -> +record(Group, Topic, SubPid) -> #shared_subscription{group = Group, topic = Topic, subpid = SubPid}. -dispatch({Cluster, Group}, Topic, Delivery) -> - case ekka:cluster_name() of - Cluster -> - dispatch(Group, Topic, Delivery); - _ -> Delivery - end; - %% TODO: ensure the delivery... dispatch(Group, Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> case pick(subscribers(Group, Topic)) of @@ -107,7 +111,7 @@ subscribers(Group, Topic) -> init([]) -> {atomic, PMon} = mnesia:transaction(fun init_monitors/0), mnesia:subscribe({table, ?TAB, simple}), - {ok, #state{pmon = PMon}}. + {ok, update_stats(#state{pmon = PMon})}. init_monitors() -> mnesia:foldl( @@ -116,36 +120,34 @@ init_monitors() -> end, emqx_pmon:new(), ?TAB). handle_call(Req, _From, State) -> - emqx_log:error("[Shared] Unexpected request: ~p", [Req]), + emqx_logger:error("[Shared] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast({monitor, SubPid}, State= #state{pmon = PMon}) -> - {noreply, State#state{pmon = PMon:monitor(SubPid)}}; + {noreply, update_stats(State#state{pmon = PMon:monitor(SubPid)})}; handle_cast(Msg, State) -> - emqx_log:error("[Shared] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Shared] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = PMon}) -> - emqx_log:info("Shared subscription created: ~p", [NewRecord]), #shared_subscription{subpid = SubPid} = NewRecord, - {noreply, State#state{pmon = PMon:monitor(SubPid)}}; + {noreply, update_stats(State#state{pmon = PMon:monitor(SubPid)})}; handle_info({mnesia_table_event, {delete_object, OldRecord, _}}, State = #state{pmon = PMon}) -> - emqx_log:info("Shared subscription deleted: ~p", [OldRecord]), #shared_subscription{subpid = SubPid} = OldRecord, - {noreply, State#state{pmon = PMon:demonitor(SubPid)}}; + {noreply, update_stats(State#state{pmon = PMon:demonitor(SubPid)})}; handle_info({mnesia_table_event, _Event}, State) -> {noreply, State}; handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) -> - emqx_log:info("Shared subscription down: ~p", [SubPid]), + emqx_logger:info("Shared subscription down: ~p", [SubPid]), mnesia:async_dirty(fun cleanup_down/1, [SubPid]), - {noreply, State#state{pmon = PMon:erase(SubPid)}}; + {noreply, update_stats(State#state{pmon = PMon:erase(SubPid)})}; handle_info(Info, State) -> - emqx_log:error("[Shared] Unexpected info: ~p", [Info]), + emqx_logger:error("[Shared] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> @@ -164,3 +166,8 @@ cleanup_down(SubPid) -> mnesia:delete_object(?TAB, Record) end, mnesia:match_object(Pat)). +update_stats(State) -> + emqx_stats:setstat('subscriptions/shared/count', + 'subscriptions/shared/max', + ets:info(?TAB, size)), State. + diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 85c4c4423..e2e5ca20c 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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). @@ -24,7 +24,8 @@ -export([open_session/1, lookup_session/1, close_session/1]). -export([resume_session/1, resume_session/2, discard_session/1, discard_session/2]). --export([register_session/2, unregister_session/1]). +-export([register_session/2, get_session_attrs/1, unregister_session/1]). +-export([get_session_stats/1, set_session_stats/2]). %% Internal functions for rpc -export([dispatch/3]). @@ -32,18 +33,22 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {pmon}). +-record(state, {session_pmon}). -define(SM, ?MODULE). +%% ETS Tables +-define(SESSION, emqx_session). +-define(SESSION_P, emqx_persistent_session). +-define(SESSION_ATTRS, emqx_session_attrs). +-define(SESSION_STATS, emqx_session_stats). + -spec(start_link() -> {ok, pid()} | ignore | {error, term()}). start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -%%-------------------------------------------------------------------- -%% Open Session -%%-------------------------------------------------------------------- + gen_server:start_link({local, ?SM}, ?MODULE, [], []). +%% @doc Open a session. +-spec(open_session(map()) -> {ok, pid()} | {error, term()}). open_session(Attrs = #{clean_start := true, client_id := ClientId, client_pid := ClientPid}) -> CleanStart = fun(_) -> @@ -66,81 +71,113 @@ open_session(Attrs = #{clean_start := false, end, emqx_sm_locker:trans(ClientId, ResumeStart). -%%-------------------------------------------------------------------- -%% Discard Session -%%-------------------------------------------------------------------- - +%% @doc Discard all the sessions identified by the ClientId. +-spec(discard_session(map()) -> ok). discard_session(ClientId) when is_binary(ClientId) -> discard_session(ClientId, self()). discard_session(ClientId, ClientPid) when is_binary(ClientId) -> lists:foreach( - fun(#session{pid = SessionPid}) -> + fun({_ClientId, SessionPid}) -> case catch emqx_session:discard(SessionPid, ClientPid) of {'EXIT', Error} -> - emqx_log:error("[SM] Failed to discard ~p: ~p", [SessionPid, Error]); + emqx_logger:error("[SM] Failed to discard ~p: ~p", [SessionPid, Error]); ok -> ok end end, lookup_session(ClientId)). -%%-------------------------------------------------------------------- -%% Resume Session -%%-------------------------------------------------------------------- - +%% @doc Try to resume a session. +-spec(resume_session(client_id()) -> {ok, pid()} | {error, term()}). resume_session(ClientId) -> resume_session(ClientId, self()). resume_session(ClientId, ClientPid) -> case lookup_session(ClientId) of [] -> {error, not_found}; - [#session{pid = SessionPid}] -> + [{_ClientId, SessionPid}] -> ok = emqx_session:resume(SessionPid, ClientPid), {ok, SessionPid}; Sessions -> - [#session{pid = SessionPid}|StaleSessions] = lists:reverse(Sessions), - emqx_log:error("[SM] More than one session found: ~p", [Sessions]), - lists:foreach(fun(#session{pid = Pid}) -> - catch emqx_session:discard(Pid, ClientPid) + [{_, SessionPid}|StaleSessions] = lists:reverse(Sessions), + emqx_logger:error("[SM] More than one session found: ~p", [Sessions]), + lists:foreach(fun({_, StalePid}) -> + catch emqx_session:discard(StalePid, ClientPid) end, StaleSessions), ok = emqx_session:resume(SessionPid, ClientPid), {ok, SessionPid} end. -%%-------------------------------------------------------------------- -%% Close a session -%%-------------------------------------------------------------------- - -close_session(#session{pid = SessionPid}) -> +%% @doc Close a session. +-spec(close_session({client_id(), pid()} | pid()) -> ok). +close_session({_ClientId, SessionPid}) -> + emqx_session:close(SessionPid); +close_session(SessionPid) when is_pid(SessionPid) -> emqx_session:close(SessionPid). -%%-------------------------------------------------------------------- -%% Create/Delete a session -%%-------------------------------------------------------------------- +%% @doc Register a session with attributes. +-spec(register_session(client_id() | {client_id(), pid()}, + list(emqx_session:attribute())) -> ok). +register_session(ClientId, Attrs) when is_binary(ClientId) -> + register_session({ClientId, self()}, Attrs); -register_session(Session, Attrs) when is_record(Session, session) -> - ets:insert(session, Session), - ets:insert(session_attrs, {Session, Attrs}), +register_session(Session = {ClientId, SessionPid}, Attrs) + when is_binary(ClientId), is_pid(SessionPid) -> + ets:insert(?SESSION, Session), + ets:insert(?SESSION_ATTRS, {Session, Attrs}), + case proplists:get_value(clean_start, Attrs, true) of + true -> ok; + false -> ets:insert(?SESSION_P, Session) + end, emqx_sm_registry:register_session(Session), - gen_server:cast(?MODULE, {registered, Session}). + notify({registered, ClientId, SessionPid}). -unregister_session(Session) when is_record(Session, session) -> +%% @doc Get session attrs +-spec(get_session_attrs({client_id(), pid()}) + -> list(emqx_session:attribute())). +get_session_attrs(Session = {ClientId, SessionPid}) + when is_binary(ClientId), is_pid(SessionPid) -> + safe_lookup_element(?SESSION_ATTRS, Session, []). + +%% @doc Unregister a session +-spec(unregister_session(client_id() | {client_id(), pid()}) -> ok). +unregister_session(ClientId) when is_binary(ClientId) -> + unregister_session({ClientId, self()}); + +unregister_session(Session = {ClientId, SessionPid}) + when is_binary(ClientId), is_pid(SessionPid) -> emqx_sm_registry:unregister_session(Session), - emqx_sm_stats:del_session_stats(Session), - ets:delete(session_attrs, Session), - ets:delete_object(session, Session), - gen_server:cast(?MODULE, {unregistered, Session}). + ets:delete(?SESSION_STATS, Session), + ets:delete(?SESSION_ATTRS, Session), + ets:delete_object(?SESSION_P, Session), + ets:delete_object(?SESSION, Session), + notify({unregistered, ClientId, SessionPid}). -%%-------------------------------------------------------------------- -%% Lookup a session from registry -%%-------------------------------------------------------------------- - +%% @doc Get session stats +-spec(get_session_stats({client_id(), pid()}) -> list(emqx_stats:stats())). +get_session_stats(Session = {ClientId, SessionPid}) + when is_binary(ClientId), is_pid(SessionPid) -> + safe_lookup_element(?SESSION_STATS, Session, []). + +%% @doc Set session stats +-spec(set_session_stats(client_id() | {client_id(), pid()}, + emqx_stats:stats()) -> ok). +set_session_stats(ClientId, Stats) when is_binary(ClientId) -> + set_session_stats({ClientId, self()}, Stats); + +set_session_stats(Session = {ClientId, SessionPid}, Stats) + when is_binary(ClientId), is_pid(SessionPid) -> + ets:insert(?SESSION_STATS, {Session, Stats}). + +%% @doc Lookup a session from registry +-spec(lookup_session(client_id()) -> list({client_id(), pid()})). lookup_session(ClientId) -> - emqx_sm_registry:lookup_session(ClientId). - -%%-------------------------------------------------------------------- -%% Dispatch by client Id -%%-------------------------------------------------------------------- + case emqx_sm_registry:is_enabled() of + true -> emqx_sm_registry:lookup_session(ClientId); + false -> ets:lookup(?SESSION, ClientId) + end. +%% @doc Dispatch a message to the session. +-spec(dispatch(client_id(), topic(), message()) -> any()). dispatch(ClientId, Topic, Msg) -> case lookup_session_pid(ClientId) of Pid when is_pid(Pid) -> @@ -149,60 +186,82 @@ dispatch(ClientId, Topic, Msg) -> emqx_hooks:run('message.dropped', [ClientId, Msg]) end. +%% @doc Lookup session pid. +-spec(lookup_session_pid(client_id()) -> pid() | undefined). lookup_session_pid(ClientId) -> - try ets:lookup_element(session, ClientId, #session.pid) - catch error:badarg -> - undefined + safe_lookup_element(?SESSION, ClientId, undefined). + +safe_lookup_element(Tab, Key, Default) -> + try ets:lookup_element(Tab, Key, 2) + catch + error:badarg -> Default end. +notify(Event) -> gen_server:cast(?SM, {notify, Event}). + %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- init([]) -> - _ = emqx_tables:create(session, [public, set, {keypos, 2}, - {read_concurrency, true}, - {write_concurrency, true}]), - _ = emqx_tables:create(session_attrs, [public, set, - {write_concurrency, true}]), - {ok, #state{pmon = emqx_pmon:new()}}. + TabOpts = [public, set, {write_concurrency, true}], + _ = emqx_tables:new(?SESSION, [{read_concurrency, true} | TabOpts]), + _ = emqx_tables:new(?SESSION_P, TabOpts), + _ = emqx_tables:new(?SESSION_ATTRS, TabOpts), + _ = emqx_tables:new(?SESSION_STATS, TabOpts), + emqx_stats:update_interval(sm_stats, stats_fun()), + {ok, #state{session_pmon = emqx_pmon:new()}}. handle_call(Req, _From, State) -> - emqx_log:error("[SM] Unexpected request: ~p", [Req]), + emqx_logger:error("[SM] Unexpected request: ~p", [Req]), {reply, ignore, State}. -handle_cast({registered, #session{sid = ClientId, pid = SessionPid}}, - State = #state{pmon = PMon}) -> - {noreply, State#state{pmon = PMon:monitor(SessionPid, ClientId)}}; +handle_cast({notify, {registered, ClientId, SessionPid}}, + State = #state{session_pmon = PMon}) -> + {noreply, State#state{session_pmon = PMon:monitor(SessionPid, ClientId)}}; -handle_cast({unregistered, #session{sid = _ClientId, pid = SessionPid}}, - State = #state{pmon = PMon}) -> - {noreply, State#state{pmon = PMon:erase(SessionPid)}}; +handle_cast({notify, {unregistered, _ClientId, SessionPid}}, + State = #state{session_pmon = PMon}) -> + {noreply, State#state{session_pmon = PMon:demonitor(SessionPid)}}; handle_cast(Msg, State) -> - emqx_log:error("[SM] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[SM] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info({'DOWN', _MRef, process, DownPid, _Reason}, - State = #state{pmon = PMon}) -> + State = #state{session_pmon = PMon}) -> case PMon:find(DownPid) of - {ok, ClientId} -> - case ets:lookup(session, ClientId) of - [] -> ok; - _ -> unregister_session(#session{sid = ClientId, pid = DownPid}) - end, - {noreply, State}; undefined -> - {noreply, State} + {noreply, State}; + ClientId -> + unregister_session({ClientId, DownPid}), + {noreply, State#state{session_pmon = PMon:erase(DownPid)}} end; handle_info(Info, State) -> - emqx_log:error("[SM] Unexpected info: ~p", [Info]), + emqx_logger:error("[SM] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> - ok. + emqx_stats:cancel_update(cm_stats). code_change(_OldVsn, State, _Extra) -> {ok, State}. +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +stats_fun() -> + fun () -> + safe_update_stats(?SESSION, 'sessions/count', 'sessions/max'), + safe_update_stats(?SESSION_P, 'sessions/persistent/count', + 'sessions/persistent/max') + end. + +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_locker.erl b/src/emqx_sm_locker.erl index 07770061b..50b1fe462 100644 --- a/src/emqx_sm_locker.erl +++ b/src/emqx_sm_locker.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_locker). @@ -32,8 +32,11 @@ start_link() -> trans(ClientId, Fun) -> trans(ClientId, Fun, undefined). --spec(trans(client_id(), fun(([node()]) -> any()), +-spec(trans(client_id() | undefined, + fun(([node()]) -> any()), ekka_locker:piggyback()) -> any()). +trans(undefined, Fun, _Piggyback) -> + Fun([]); trans(ClientId, Fun, Piggyback) -> case lock(ClientId, Piggyback) of {true, Nodes} -> diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index ad3597bcc..085403d79 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_registry). @@ -23,58 +23,73 @@ %% API -export([start_link/0]). +-export([is_enabled/0]). + -export([register_session/1, lookup_session/1, unregister_session/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --define(SERVER, ?MODULE). +-define(REGISTRY, ?MODULE). --define(TAB, session_registry). +-define(TAB, emqx_session_registry). -define(LOCK, {?MODULE, cleanup_sessions}). +-record(global_session, {sid, pid}). + -record(state, {}). +-type(session_pid() :: pid()). + +%% @doc Start the session manager. +-spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +start_link() -> + gen_server:start_link({local, ?REGISTRY}, ?MODULE, [], []). + +-spec(is_enabled() -> boolean()). +is_enabled() -> + ets:info(?TAB, name) =/= undefined. + +-spec(lookup_session(client_id()) -> list({client_id(), session_pid()})). +lookup_session(ClientId) -> + [{ClientId, SessionPid} || #global_session{pid = SessionPid} + <- mnesia:dirty_read(?TAB, ClientId)]. + +-spec(register_session({client_id(), session_pid()}) -> ok). +register_session({ClientId, SessionPid}) when is_binary(ClientId), + is_pid(SessionPid) -> + mnesia:dirty_write(?TAB, record(ClientId, SessionPid)). + +-spec(unregister_session({client_id(), session_pid()}) -> ok). +unregister_session({ClientId, SessionPid}) when is_binary(ClientId), + is_pid(SessionPid) -> + mnesia:dirty_delete_object(?TAB, record(ClientId, SessionPid)). + +record(ClientId, SessionPid) -> + #global_session{sid = ClientId, pid = SessionPid}. + %%-------------------------------------------------------------------- -%% API +%% gen_server callbacks %%-------------------------------------------------------------------- -start_link() -> +init([]) -> ok = ekka_mnesia:create_table(?TAB, [ {type, bag}, {ram_copies, [node()]}, - {record_name, session}, - {attributes, record_info(fields, session)}]), + {record_name, global_session}, + {attributes, record_info(fields, global_session)}]), ok = ekka_mnesia:copy_table(?TAB), - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). - --spec(lookup_session(client_id()) -> list(session())). -lookup_session(ClientId) -> - mnesia:dirty_read(?TAB, ClientId). - --spec(register_session(session()) -> ok). -register_session(Session) when is_record(Session, session) -> - mnesia:dirty_write(?TAB, Session). - --spec(unregister_session(session()) -> ok). -unregister_session(Session) when is_record(Session, session) -> - mnesia:dirty_delete_object(?TAB, Session). - -%%%=================================================================== -%%% gen_server callbacks -%%%=================================================================== - -init([]) -> ekka:monitor(membership), {ok, #state{}}. -handle_call(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. +handle_call(Req, _From, State) -> + emqx_logger:error("[Registry] Unexpected request: ~p", [Req]), + {reply, ignore, State}. -handle_cast(_Msg, State) -> +handle_cast(Msg, State) -> + emqx_logger:error("[Registry] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info({membership, {mnesia, down, Node}}, State) -> @@ -87,7 +102,8 @@ handle_info({membership, {mnesia, down, Node}}, State) -> handle_info({membership, _Event}, State) -> {noreply, State}; -handle_info(_Info, State) -> +handle_info(Info, State) -> + emqx_logger:error("[Registry] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> @@ -101,7 +117,7 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- cleanup_sessions(Node) -> - Pat = [{#session{pid = '$1', _ = '_'}, + Pat = [{#global_session{pid = '$1', _ = '_'}, [{'==', {node, '$1'}, Node}], ['$_']}], lists:foreach(fun(Session) -> mnesia:delete_object(?TAB, Session) diff --git a/src/emqx_sm_stats.erl b/src/emqx_sm_stats.erl deleted file mode 100644 index dab8d4a5d..000000000 --- a/src/emqx_sm_stats.erl +++ /dev/null @@ -1,72 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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_stats). - --behaviour(gen_statem). - --include("emqx.hrl"). - -%% API --export([start_link/0]). - --export([set_session_stats/2, get_session_stats/1, del_session_stats/1]). - -%% gen_statem callbacks --export([init/1, callback_mode/0, handle_event/4, terminate/3, code_change/4]). - --define(TAB, session_stats). - --record(state, {statsfun}). - -start_link() -> - gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []). - --spec(set_session_stats(session(), emqx_stats:stats()) -> true). -set_session_stats(Session, Stats) when is_record(Session, session) -> - ets:insert(?TAB, {Session, [{'$ts', emqx_time:now_secs()}|Stats]}). - --spec(get_session_stats(session()) -> emqx_stats:stats()). -get_session_stats(Session) -> - case ets:lookup(?TAB, Session) of - [{_, Stats}] -> Stats; - [] -> [] - end. - --spec(del_session_stats(session()) -> true). -del_session_stats(Session) -> - ets:delete(?TAB, Session). - -init([]) -> - _ = emqx_tables:create(?TAB, [public, {write_concurrency, true}]), - StatsFun = emqx_stats:statsfun('sessions/count', 'sessions/max'), - {ok, idle, #state{statsfun = StatsFun}, timer:seconds(1)}. - -callback_mode() -> handle_event_function. - -handle_event(timeout, _Timeout, idle, State = #state{statsfun = StatsFun}) -> - case ets:info(session, size) of - undefined -> ok; - Size -> StatsFun(Size) - end, - {next_state, idle, State, timer:seconds(1)}. - -terminate(_Reason, _StateName, _State) -> - ok. - -code_change(_OldVsn, StateName, State, _Extra) -> - {ok, StateName, State}. - diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index 9f2b44fa3..16e1b8715 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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). @@ -26,12 +26,17 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - Childs = [child(M) || M <- [emqx_sm_locker, - emqx_sm_registry, - emqx_sm_stats, - emqx_sm]], - {ok, {{one_for_all, 10, 3600}, Childs}}. + %% Session Locker + Locker = {locker, {emqx_sm_locker, start_link, []}, + permanent, 5000, worker, [emqx_sm_locker]}, -child(M) -> - {M, {M, start_link, []}, permanent, 5000, worker, [M]}. + %% Session Registry + Registry = {registry, {emqx_sm_registry, start_link, []}, + permanent, 5000, worker, [emqx_sm_registry]}, + + %% Session Manager + Manager = {manager, {emqx_sm, start_link, []}, + permanent, 5000, worker, [emqx_sm]}, + + {ok, {{one_for_rest, 10, 3600}, [Locker, Registry, Manager]}}. diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index 4aad2fb3d..0bcf2e5b8 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_stats). @@ -20,69 +20,78 @@ -include("emqx.hrl"). --export([start_link/0, stop/0]). +-export([start_link/0]). -%% Get all Stats +%% Get all stats -export([all/0]). -%% Statistics API. +%% Stats API. -export([statsfun/1, statsfun/2, getstats/0, getstat/1, setstat/2, setstat/3]). +-export([update_interval/2, update_interval/3, cancel_update/1]). + %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {tick}). +-record(update, {name, countdown, interval, func}). + +-record(state, {timer, updates :: #update{}}). -type(stats() :: list({atom(), non_neg_integer()})). -export_type([stats/0]). --define(STATS_TAB, stats). - -%% $SYS Topics for Clients --define(SYSTOP_CLIENTS, [ +%% Client stats +-define(CLIENT_STATS, [ 'clients/count', % clients connected current - 'clients/max' % max clients connected + 'clients/max' % maximum clients connected ]). -%% $SYS Topics for Sessions --define(SYSTOP_SESSIONS, [ +%% Session stats +-define(SESSION_STATS, [ 'sessions/count', - 'sessions/max' + 'sessions/max', + 'sessions/persistent/count', + 'sessions/persistent/max' ]). -%% $SYS Topics for Subscribers --define(SYSTOP_PUBSUB, [ - 'topics/count', % ... - 'topics/max', % ... - 'subscribers/count', % ... - 'subscribers/max', % ... - 'subscriptions/count', % ... - 'subscriptions/max', % ... - 'routes/count', % ... - 'routes/max' % ... +%% Subscribers, Subscriptions stats +-define(PUBSUB_STATS, [ + 'topics/count', + 'topics/max', + 'subscribers/count', + 'subscribers/max', + 'subscriptions/count', + 'subscriptions/max' ]). -%% $SYS Topic for retained --define(SYSTOP_RETAINED, [ +-define(ROUTE_STATS, [ + 'routes/count', + 'routes/max' +]). + +%% Retained stats +-define(RETAINED_STATS, [ 'retained/count', 'retained/max' ]). +-define(TAB, ?MODULE). +-define(SERVER, ?MODULE). + +%% @doc Start stats server +-spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- -%% @doc Start stats server --spec(start_link() -> {ok, pid()} | ignore | {error, term()}). -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -stop() -> - gen_server:call(?MODULE, stop). - -all() -> ets:tab2list(?STATS_TAB). +%% Get all stats. +-spec(all() -> stats()). +all() -> getstats(). %% @doc Generate stats fun -spec(statsfun(Stat :: atom()) -> fun()). @@ -94,71 +103,117 @@ statsfun(Stat, MaxStat) -> fun(Val) -> setstat(Stat, MaxStat, Val) end. %% @doc Get all statistics --spec(getstats() -> [{atom(), non_neg_integer()}]). +-spec(getstats() -> stats()). getstats() -> - lists:sort(ets:tab2list(?STATS_TAB)). + case ets:info(?TAB, name) of + undefined -> []; + _ -> ets:tab2list(?TAB) + end. %% @doc Get stats by name -spec(getstat(atom()) -> non_neg_integer() | undefined). getstat(Name) -> - case ets:lookup(?STATS_TAB, Name) of + case ets:lookup(?TAB, Name) of [{Name, Val}] -> Val; [] -> undefined end. -%% @doc Set broker stats +%% @doc Set stats -spec(setstat(Stat :: atom(), Val :: pos_integer()) -> boolean()). -setstat(Stat, Val) -> - ets:update_element(?STATS_TAB, Stat, {2, Val}). +setstat(Stat, Val) when is_integer(Val) -> + safe_update_element(Stat, Val). -%% @doc Set stats with max --spec(setstat(Stat :: atom(), MaxStat :: atom(), Val :: pos_integer()) -> boolean()). -setstat(Stat, MaxStat, Val) -> - gen_server:cast(?MODULE, {setstat, Stat, MaxStat, Val}). +%% @doc Set stats with max value. +-spec(setstat(Stat :: atom(), MaxStat :: atom(), + Val :: pos_integer()) -> boolean()). +setstat(Stat, MaxStat, Val) when is_integer(Val) -> + cast({setstat, Stat, MaxStat, Val}). + +-spec(update_interval(atom(), fun()) -> ok). +update_interval(Name, UpFun) -> + update_interval(Name, 1, UpFun). + +-spec(update_interval(atom(), pos_integer(), fun()) -> ok). +update_interval(Name, Secs, UpFun) when is_integer(Secs), Secs >= 1 -> + cast({update_interval, rec(Name, Secs, UpFun)}). + +-spec(cancel_update(atom()) -> ok). +cancel_update(Name) -> + cast({cancel_update, Name}). + +rec(Name, Secs, UpFun) -> + #update{name = Name, countdown = Secs, interval = Secs, func = UpFun}. + +cast(Msg) -> + gen_server:cast(?SERVER, Msg). %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- init([]) -> - emqx_time:seed(), - _ = emqx_tables:create(?STATS_TAB, [set, public, named_table, - {write_concurrency, true}]), - Topics = ?SYSTOP_CLIENTS ++ ?SYSTOP_SESSIONS ++ ?SYSTOP_PUBSUB ++ ?SYSTOP_RETAINED, - ets:insert(?STATS_TAB, [{Topic, 0} || Topic <- Topics]), - % Tick to publish stats - {ok, TRef} = timer:send_after(emqx_sys:sys_interval(), tick), - {ok, #state{tick = TRef}, hibernate}. + _ = emqx_tables:new(?TAB, [set, public, {write_concurrency, true}]), + Stats = lists:append([?CLIENT_STATS, ?SESSION_STATS, ?PUBSUB_STATS, + ?ROUTE_STATS, ?RETAINED_STATS]), + ets:insert(?TAB, [{Name, 0} || Name <- Stats]), + {ok, start_timer(#state{updates = []}), hibernate}. -handle_call(stop, _From, State) -> - {stop, normal, ok, State}; +start_timer(State) -> + State#state{timer = emqx_misc:start_timer(timer:seconds(1), tick)}. -handle_call(_Request, _From, State) -> - {reply, error, State}. +handle_call(Req, _From, State) -> + emqx_logger:error("[STATS] Unexpected request: ~p", [Req]), + {reply, ignore, State}. -%% atomic handle_cast({setstat, Stat, MaxStat, Val}, State) -> - MaxVal = ets:lookup_element(?STATS_TAB, MaxStat, 2), - if - Val > MaxVal -> - ets:update_element(?STATS_TAB, MaxStat, {2, Val}); - true -> ok + try ets:lookup_element(?TAB, MaxStat, 2) of + MaxVal when Val > MaxVal -> + ets:update_element(?TAB, MaxStat, {2, Val}); + _ -> ok + catch + error:badarg -> + ets:insert(?TAB, {MaxStat, Val}) end, - ets:update_element(?STATS_TAB, Stat, {2, Val}), + safe_update_element(Stat, Val), {noreply, State}; -handle_cast(_Msg, State) -> +handle_cast({update_interval, Update = #update{name = Name}}, + State = #state{updates = Updates}) -> + case lists:keyfind(Name, #update.name, Updates) of + #update{} -> + emqx_logger:error("[STATS]: Duplicated update: ~s", [Name]), + {noreply, State}; + false -> + {noreply, State#state{updates = [Update | Updates]}} + end; + +handle_cast({cancel_update, Name}, State = #state{updates = Updates}) -> + {noreply, State#state{updates = lists:keydelete(Name, #update.name, Updates)}}; + +handle_cast(Msg, State) -> + emqx_logger:error("[STATS] Unexpected msg: ~p", [Msg]), {noreply, State}. -%% Interval Tick. -handle_info(tick, State) -> - [publish(Stat, Val) || {Stat, Val} <- ets:tab2list(?STATS_TAB)], - {noreply, State, hibernate}; +handle_info({timeout, TRef, tick}, State = #state{timer = TRef, + updates = Updates}) -> + lists:foldl( + fun(Update = #update{name = Name, countdown = C, interval = I, + func = UpFun}, Acc) when C =< 0 -> + try UpFun() + catch _:Error -> + emqx_logger:error("[STATS] Update ~s error: ~p", [Name, Error]) + end, + [Update#update{countdown = I} | Acc]; + (Update = #update{countdown = C}, Acc) -> + [Update#update{countdown = C - 1} | Acc] + end, [], Updates), + {noreply, start_timer(State), hibernate}; -handle_info(_Info, State) -> +handle_info(Info, State) -> + emqx_logger:error("[STATS] Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{tick = TRef}) -> +terminate(_Reason, #state{timer = TRef}) -> timer:cancel(TRef). code_change(_OldVsn, State, _Extra) -> @@ -168,12 +223,10 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -publish(Stat, Val) -> - Msg = emqx_message:make(stats, stats_topic(Stat), bin(Val)), - emqx:publish(emqx_message:set_flag(sys, Msg)). - -stats_topic(Stat) -> - emqx_topic:systop(list_to_binary(lists:concat(['stats/', Stat]))). - -bin(I) when is_integer(I) -> list_to_binary(integer_to_list(I)). +safe_update_element(Key, Val) -> + try ets:update_element(?TAB, Key, {2, Val}) + catch + error:badarg -> + ets:insert_new(?TAB, {Key, Val}) + end. diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index ce2c438f0..61d48042e 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_sup). @@ -20,7 +20,6 @@ -export([start_link/0, start_child/1, start_child/2, stop_child/1]). -%% Supervisor callbacks -export([init/1]). -type(startchild_ret() :: {ok, supervisor:child()} @@ -29,20 +28,24 @@ -define(SUPERVISOR, ?MODULE). --define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). - start_link() -> supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + -spec(start_child(supervisor:child_spec()) -> startchild_ret()). start_child(ChildSpec) when is_tuple(ChildSpec) -> supervisor:start_child(?SUPERVISOR, ChildSpec). --spec(start_child(atom(), worker | supervisor) -> startchild_ret()). -start_child(Mod, Type) when Type == worker orelse Type == supervisor -> - start_child(?CHILD(Mod, Type)). +-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)). --spec(stop_child(supervisor:child_id()) -> ok | {error, any()}). +-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); @@ -54,24 +57,44 @@ stop_child(ChildId) -> %%-------------------------------------------------------------------- init([]) -> - {ok, {{one_for_all, 10, 3600}, - [?CHILD(emqx_ctl, worker), - ?CHILD(emqx_hooks, worker), - ?CHILD(emqx_stats, worker), - ?CHILD(emqx_metrics, worker), - ?CHILD(emqx_sys, worker), - ?CHILD(emqx_router_sup, supervisor), - ?CHILD(emqx_broker_sup, supervisor), - ?CHILD(emqx_pooler, supervisor), - ?CHILD(emqx_tracer_sup, supervisor), - ?CHILD(emqx_cm_sup, supervisor), - ?CHILD(emqx_sm_sup, supervisor), - ?CHILD(emqx_session_sup, supervisor), - ?CHILD(emqx_ws_connection_sup, supervisor), - ?CHILD(emqx_alarm, worker), - ?CHILD(emqx_mod_sup, supervisor), - ?CHILD(emqx_bridge_sup_sup, supervisor), - ?CHILD(emqx_access_control, worker), - ?CHILD(emqx_sysmon_sup, supervisor)] - }}. + %% Kernel Sup + KernelSup = supervisor_spec(emqx_kernel_sup), + %% Router Sup + RouterSup = supervisor_spec(emqx_router_sup), + %% Broker Sup + BrokerSup = supervisor_spec(emqx_broker_sup), + %% BridgeSup + BridgeSup = supervisor_spec(emqx_bridge_sup_sup), + %% AccessControl + AccessControl = worker_spec(emqx_access_control), + %% Session Manager + SMSup = supervisor_spec(emqx_sm_sup), + %% Session Sup + SessionSup = supervisor_spec(emqx_session_sup), + %% Connection Manager + CMSup = supervisor_spec(emqx_cm_sup), + %% WebSocket Connection Sup + WSConnSup = supervisor_spec(emqx_ws_connection_sup), + %% Sys Sup + SysSup = supervisor_spec(emqx_sys_sup), + {ok, {{one_for_all, 0, 1}, + [KernelSup, + RouterSup, + BrokerSup, + BridgeSup, + AccessControl, + SMSup, + SessionSup, + CMSup, + WSConnSup, + 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]}. diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl index b18cc195b..65f361b95 100644 --- a/src/emqx_sys.erl +++ b/src/emqx_sys.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%%=================================================================== -module(emqx_sys). @@ -22,8 +22,6 @@ -export([start_link/0]). --export([schedulers/0]). - -export([version/0, uptime/0, datetime/0, sysdescr/0, sys_interval/0]). -export([info/0]). @@ -31,33 +29,29 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {started_at, heartbeat, sys_ticker, version, sysdescr}). +-import(emqx_topic, [systop/1]). +-import(emqx_misc, [start_timer/2]). + +-record(state, {start_time, heartbeat, ticker, version, sysdescr}). -define(APP, emqx). +-define(SYS, ?MODULE). --define(SERVER, ?MODULE). - -%% $SYS Topics of Broker --define(SYSTOP_BROKERS, [ +-define(INFO_KEYS, [ version, % Broker version uptime, % Broker uptime datetime, % Broker local datetime sysdescr % Broker description ]). +-spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +start_link() -> + gen_server:start_link({local, ?SYS}, ?MODULE, [], []). + %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- --spec(start_link() -> {ok, pid()} | ignore | {error, any()}). -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). - -%% @doc Get schedulers --spec(schedulers() -> pos_integer()). -schedulers() -> - erlang:system_info(schedulers). - %% @doc Get sys version -spec(version() -> string()). version() -> @@ -70,7 +64,7 @@ sysdescr() -> %% @doc Get sys uptime -spec(uptime() -> string()). -uptime() -> gen_server:call(?SERVER, uptime). +uptime() -> gen_server:call(?SYS, uptime). %% @doc Get sys datetime -spec(datetime() -> string()). @@ -80,6 +74,8 @@ datetime() -> io_lib:format( "~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])). +%% @doc Get sys interval +-spec(sys_interval() -> pos_integer()). sys_interval() -> application:get_env(?APP, sys_interval, 60000). @@ -96,44 +92,49 @@ info() -> %%-------------------------------------------------------------------- init([]) -> - Tick = fun(I, M) -> - {ok, TRef} = timer:send_interval(I, M), TRef - end, - {ok, #state{started_at = os:timestamp(), - heartbeat = Tick(1000, heartbeat), - sys_ticker = Tick(sys_interval(), tick), - version = iolist_to_binary(version()), - sysdescr = iolist_to_binary(sysdescr())}, hibernate}. + State = #state{start_time = erlang:timestamp(), + version = iolist_to_binary(version()), + sysdescr = iolist_to_binary(sysdescr())}, + {ok, heartbeat(tick(State))}. + +heartbeat(State) -> + State#state{heartbeat = start_timer(timer:seconds(1), heartbeat)}. +tick(State) -> + State#state{ticker = start_timer(sys_interval(), tick)}. handle_call(uptime, _From, State) -> {reply, uptime(State), State}; handle_call(Req, _From, State) -> - emqx_log:error("[SYS] Unexpected request: ~p", [Req]), + emqx_logger:error("[SYS] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - emqx_log:error("[SYS] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[SYS] Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info(heartbeat, State) -> +handle_info({timeout, TRef, heartbeat}, State = #state{heartbeat = TRef}) -> publish(uptime, iolist_to_binary(uptime(State))), publish(datetime, iolist_to_binary(datetime())), - {noreply, State, hibernate}; + {noreply, heartbeat(State)}; -handle_info(tick, State = #state{version = Version, sysdescr = Descr}) -> - retain(brokers), - retain(version, Version), - retain(sysdescr, Descr), - {noreply, State, hibernate}; +handle_info({timeout, TRef, tick}, State = #state{ticker = TRef, + version = Version, + sysdescr = Descr}) -> + publish(version, Version), + publish(sysdescr, Descr), + publish(brokers, ekka_mnesia:running_nodes()), + publish(stats, emqx_stats:all()), + publish(metrics, emqx_metrics:all()), + {noreply, tick(State), hibernate}; handle_info(Info, State) -> - emqx_log:error("[SYS] Unexpected info: ~p", [Info]), + emqx_logger:error("[SYS] Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{heartbeat = Hb, sys_ticker = TRef}) -> - timer:cancel(Hb), - timer:cancel(TRef). +terminate(_Reason, #state{heartbeat = HBRef, ticker = TRef}) -> + emqx_misc:cancel_timer(HBRef), + emqx_misc:cancel_timer(TRef). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -142,24 +143,9 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -retain(brokers) -> - Payload = list_to_binary(string:join([atom_to_list(N) || - N <- ekka_mnesia:running_nodes()], ",")), - Msg = emqx_message:make(broker, <<"$SYS/brokers">>, Payload), - emqx:publish(emqx_message:set_flag(sys, emqx_message:set_flag(retain, Msg))). - -retain(Topic, Payload) when is_binary(Payload) -> - Msg = emqx_message:make(broker, emqx_topic:systop(Topic), Payload), - emqx:publish(emqx_message:set_flag(sys, emqx_message:set_flag(retain, Msg))). - -publish(Topic, Payload) when is_binary(Payload) -> - Msg = emqx_message:make(broker, emqx_topic:systop(Topic), Payload), - emqx:publish(emqx_message:set_flag(sys, Msg)). - -uptime(#state{started_at = Ts}) -> - Secs = timer:now_diff(os:timestamp(), Ts) div 1000000, +uptime(#state{start_time = Ts}) -> + Secs = timer:now_diff(erlang:timestamp(), Ts) div 1000000, lists:flatten(uptime(seconds, Secs)). - uptime(seconds, Secs) when Secs < 60 -> [integer_to_list(Secs), " seconds"]; uptime(seconds, Secs) -> @@ -175,3 +161,38 @@ uptime(hours, H) -> uptime(days, D) -> [integer_to_list(D), " days,"]. +publish(uptime, Uptime) -> + safe_publish(systop(uptime), [sys], Uptime); +publish(datetime, Datetime) -> + safe_publish(systop(datatype), [sys], Datetime); +publish(version, Version) -> + safe_publish(systop(version), [sys, retain], Version); +publish(sysdescr, Descr) -> + safe_publish(systop(sysdescr), [sys, retain], Descr); +publish(brokers, Nodes) -> + Payload = string:join([atom_to_list(N) || N <- Nodes], ","), + safe_publish(<<"$SYS/brokers">>, [sys, retain], Payload); +publish(stats, Stats) -> + [begin + Topic = systop(lists:concat(['stats/', Stat])), + safe_publish(Topic, [sys], integer_to_binary(Val)) + end || {Stat, Val} <- Stats, is_atom(Stat), is_integer(Val)]; +publish(metrics, Metrics) -> + [begin + Topic = systop(lists:concat(['metrics/', Metric])), + safe_publish(Topic, [sys], integer_to_binary(Val)) + end || {Metric, Val} <- Metrics, is_atom(Metric), is_integer(Val)]. + +safe_publish(Topic, Flags, Payload) -> + try do_publish(Topic, Flags, Payload) + catch + _:Error -> + emqx_logger:error("[SYS] Publish error: ~p", [Error]) + end. + +do_publish(Topic, Flags, Payload) -> + Msg0 = emqx_message:make(?SYS, Topic, iolist_to_binary(Payload)), + emqx_broker:publish(lists:foldl(fun(Flag, Msg) -> + emqx_message:set_flag(Flag, Msg) + end, Msg0, Flags)). + diff --git a/src/emqx_sysmon.erl b/src/emqx_sys_mon.erl similarity index 61% rename from src/emqx_sysmon.erl rename to src/emqx_sys_mon.erl index d0c3d9001..6e58f84bb 100644 --- a/src/emqx_sysmon.erl +++ b/src/emqx_sys_mon.erl @@ -1,20 +1,20 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_sysmon). +-module(emqx_sys_mon). -behavior(gen_server). @@ -23,31 +23,34 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {tickref, events = [], tracelog}). - -%%-define(LOG_FMT, [{formatter_config, [time, " ", message, "\n"]}]). +-record(state, {timer, events}). -define(LOG(Msg, ProcInfo), - emqx_log:warning([{sysmon, true}], "[SYSMON] ~s~n~p", [WarnMsg, ProcInfo])). + emqx_logger:warning([{sysmon, true}], + "[SYSMON] ~s~n~p", [WarnMsg, ProcInfo])). -define(LOG(Msg, ProcInfo, PortInfo), - emqx_log:warning([{sysmon, true}], "[SYSMON] ~s~n~p~n~p", [WarnMsg, ProcInfo, PortInfo])). + emqx_logger:warning([{sysmon, true}], + "[SYSMON] ~s~n~p~n~p", [WarnMsg, ProcInfo, PortInfo])). + +-define(SYSMON, ?MODULE). %% @doc Start system monitor --spec(start_link(Opts :: list(tuple())) -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link(Opts :: list(tuple())) + -> {ok, pid()} | ignore | {error, term()}). start_link(Opts) -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [Opts], []). + gen_server:start_link({local, ?SYSMON}, ?MODULE, [Opts], []). %%-------------------------------------------------------------------- -%% gen_server Callbacks +%% gen_server callbacks %%-------------------------------------------------------------------- init([Opts]) -> erlang:system_monitor(self(), parse_opt(Opts)), - {ok, TRef} = timer:send_interval(timer:seconds(1), reset), - %%TODO: don't trace for performance issue. - %%{ok, TraceLog} = start_tracelog(proplists:get_value(logfile, Opts)), - {ok, #state{tickref = TRef}}. + {ok, start_timer(#state{events = []})}. + +start_timer(State) -> + State#state{timer = emqx_misc:start_timer(timer:seconds(2), tick)}. parse_opt(Opts) -> parse_opt(Opts, []). @@ -75,53 +78,53 @@ parse_opt([_Opt|Opts], Acc) -> parse_opt(Opts, Acc). handle_call(Req, _From, State) -> - emqx_log:error("[SYSMON] Unexpected request: ~p", [Req]), + emqx_logger:error("[SYSMON] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - emqx_log:error("[SYSMON] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[SYSMON] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info({monitor, Pid, long_gc, Info}, State) -> suppress({long_gc, Pid}, fun() -> WarnMsg = io_lib:format("long_gc warning: pid = ~p, info: ~p", [Pid, Info]), ?LOG(WarnMsg, procinfo(Pid)), - publish(long_gc, WarnMsg) + safe_publish(long_gc, WarnMsg) end, State); handle_info({monitor, Pid, long_schedule, Info}, State) when is_pid(Pid) -> suppress({long_schedule, Pid}, fun() -> WarnMsg = io_lib:format("long_schedule warning: pid = ~p, info: ~p", [Pid, Info]), ?LOG(WarnMsg, procinfo(Pid)), - publish(long_schedule, WarnMsg) + safe_publish(long_schedule, WarnMsg) end, State); handle_info({monitor, Port, long_schedule, Info}, State) when is_port(Port) -> suppress({long_schedule, Port}, fun() -> WarnMsg = io_lib:format("long_schedule warning: port = ~p, info: ~p", [Port, Info]), ?LOG(WarnMsg, erlang:port_info(Port)), - publish(long_schedule, WarnMsg) + safe_publish(long_schedule, WarnMsg) end, State); handle_info({monitor, Pid, large_heap, Info}, State) -> suppress({large_heap, Pid}, fun() -> WarnMsg = io_lib:format("large_heap warning: pid = ~p, info: ~p", [Pid, Info]), ?LOG(WarnMsg, procinfo(Pid)), - publish(large_heap, WarnMsg) + safe_publish(large_heap, WarnMsg) end, State); handle_info({monitor, SusPid, busy_port, Port}, State) -> suppress({busy_port, Port}, fun() -> WarnMsg = io_lib:format("busy_port warning: suspid = ~p, port = ~p", [SusPid, Port]), ?LOG(WarnMsg, procinfo(SusPid), erlang:port_info(Port)), - publish(busy_port, WarnMsg) + safe_publish(busy_port, WarnMsg) end, State); handle_info({monitor, SusPid, busy_dist_port, Port}, State) -> suppress({busy_dist_port, Port}, fun() -> WarnMsg = io_lib:format("busy_dist_port warning: suspid = ~p, port = ~p", [SusPid, Port]), ?LOG(WarnMsg, procinfo(SusPid), erlang:port_info(Port)), - publish(busy_dist_port, WarnMsg) + safe_publish(busy_dist_port, WarnMsg) end, State); handle_info(reset, State) -> @@ -131,9 +134,8 @@ handle_info(Info, State) -> lager:error("[SYSMON] Unexpected Info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{tickref = TRef, tracelog = TraceLog}) -> - timer:cancel(TRef), - cancel_tracelog(TraceLog). +terminate(_Reason, #state{timer = TRef}) -> + emqx_misc:cancel_timer(TRef). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -152,20 +154,13 @@ procinfo(Pid) -> {Info, GcInfo} -> Info ++ GcInfo end. -publish(Sysmon, WarnMsg) -> - Msg = emqx_message:make(sysmon, topic(Sysmon), iolist_to_binary(WarnMsg)), - emqx:publish(emqx_message:set_flag(sys, Msg)). - -topic(Sysmon) -> - emqx_topic:systop(list_to_binary(lists:concat(['sysmon/', Sysmon]))). - -%% start_tracelog(undefined) -> -%% {ok, undefined}; -%% start_tracelog(LogFile) -> -%% lager:trace_file(LogFile, [{sysmon, true}], info, ?LOG_FMT). - -cancel_tracelog(undefined) -> - ok; -cancel_tracelog(TraceLog) -> - lager:stop_trace(TraceLog). +safe_publish(Event, WarnMsg) -> + try + Topic = emqx_topic:systop(lists:concat(['sysmon/', Event])), + Msg = emqx_message:make(?SYSMON, Topic, iolist_to_binary(WarnMsg)), + emqx_broker:publish(emqx_message:set_flag(sys, Msg)) + catch + _:Error -> + emqx_logger:error("[SYSMON] Publish error: ~p", [Error]) + end. diff --git a/src/emqx_sys_sup.erl b/src/emqx_sys_sup.erl new file mode 100644 index 000000000..745817469 --- /dev/null +++ b/src/emqx_sys_sup.erl @@ -0,0 +1,36 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%%=================================================================== + +-module(emqx_sys_sup). + +-behaviour(supervisor). + +-export([start_link/0]). + +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + Sys = {sys, {emqx_sys, start_link, []}, + permanent, 5000, worker, [emqx_sys]}, + + {ok, Env} = emqx_config:get_env(sysmon), + Sysmon = {sys_mon, {emqx_sysmon, start_link, [Env]}, + permanent, 5000, worker, [emqx_sys_mon]}, + {ok, {{one_for_one, 10, 100}, [Sys, Sysmon]}}. + diff --git a/src/emqx_sysmon_sup.erl b/src/emqx_sysmon_sup.erl deleted file mode 100644 index 0c4566bc3..000000000 --- a/src/emqx_sysmon_sup.erl +++ /dev/null @@ -1,35 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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_sysmon_sup). - --behaviour(supervisor). - -%% API --export([start_link/0]). - -%% Supervisor callbacks --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - {ok, Env} = emqx_config:get_env(sysmon), - Sysmon = {sysmon, {emqx_sysmon, start_link, [Env]}, - permanent, 5000, worker, [emqx_sysmon]}, - {ok, {{one_for_one, 10, 100}, [Sysmon]}}. - diff --git a/src/emqx_tables.erl b/src/emqx_tables.erl index 78993104c..c5f249592 100644 --- a/src/emqx_tables.erl +++ b/src/emqx_tables.erl @@ -1,27 +1,28 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_tables). --export([create/2]). +-export([new/2]). -create(Tab, Opts) -> +%% Create a named_table ets. +new(Tab, Opts) -> case ets:info(Tab, name) of undefined -> - ets:new(Tab, lists:usort([named_table|Opts])); + ets:new(Tab, lists:usort([named_table | Opts])); Tab -> Tab end. diff --git a/src/emqx_time.erl b/src/emqx_time.erl index cfcfadf63..1d1405900 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_time). diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index db7332be7..8cd7ce3b6 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_topic). @@ -143,11 +143,10 @@ word(<<"#">>) -> '#'; word(Bin) -> Bin. %% @doc '$SYS' Topic. -systop(Name) when is_atom(Name) -> - list_to_binary(lists:concat(["$SYS/brokers/", node(), "/", Name])); - +systop(Name) when is_atom(Name); is_list(Name) -> + iolist_to_binary(lists:concat(["$SYS/brokers/", node(), "/", Name])); systop(Name) when is_binary(Name) -> - list_to_binary(["$SYS/brokers/", atom_to_list(node()), "/", Name]). + iolist_to_binary(["$SYS/brokers/", atom_to_list(node()), "/", Name]). -spec(feed_var(binary(), binary(), binary()) -> binary()). feed_var(Var, Val, Topic) -> diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index 1d0211e78..5e68ed4c5 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_tracer). @@ -22,9 +22,7 @@ -export([start_link/0]). --export([trace/3]). - --export([start_trace/2, stop_trace/1, all_traces/0]). +-export([start_trace/2, lookup_traces/0, stop_trace/1]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -36,29 +34,15 @@ -define(OPTIONS, [{formatter_config, [time, " [",severity,"] ", message, "\n"]}]). +-define(TRACER, ?MODULE). + %%-------------------------------------------------------------------- %% Start the tracer %%-------------------------------------------------------------------- --spec(start_link() -> {ok, pid()}). +-spec(start_link() -> {ok, pid()} | ignore | {error, term()}). start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -%%-------------------------------------------------------------------- -%% Trace -%%-------------------------------------------------------------------- - -trace(publish, From, _Msg) when is_atom(From) -> - %% Dont' trace '$SYS' publish - ignore; -trace(publish, #client{client_id = ClientId, username = Username}, - #message{topic = Topic, payload = Payload}) -> - emqx_log:info([{client, ClientId}, {topic, Topic}], - "~s/~s PUBLISH to ~s: ~p", [ClientId, Username, Topic, Payload]); -trace(publish, From, #message{topic = Topic, payload = Payload}) - when is_binary(From); is_list(From) -> - emqx_log:info([{client, From}, {topic, Topic}], - "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). + gen_server:start_link({local, ?TRACER}, ?MODULE, [], []). %%-------------------------------------------------------------------- %% Start/Stop Trace @@ -68,11 +52,11 @@ trace(publish, From, #message{topic = Topic, payload = Payload}) -spec(start_trace(trace_who(), string()) -> ok | {error, term()}). start_trace({client, ClientId}, LogFile) -> start_trace({start_trace, {client, ClientId}, LogFile}); - start_trace({topic, Topic}, LogFile) -> start_trace({start_trace, {topic, Topic}, LogFile}). -start_trace(Req) -> gen_server:call(?MODULE, Req, infinity). +start_trace(Req) -> + gen_server:call(?MODULE, Req, infinity). %% @doc Stop tracing client or topic. -spec(stop_trace(trace_who()) -> ok | {error, term()}). @@ -81,25 +65,31 @@ stop_trace({client, ClientId}) -> stop_trace({topic, Topic}) -> gen_server:call(?MODULE, {stop_trace, {topic, Topic}}). -%% @doc Lookup all traces. --spec(all_traces() -> [{Who :: trace_who(), LogFile :: string()}]). -all_traces() -> - gen_server:call(?MODULE, all_traces). +%% @doc Lookup all traces +-spec(lookup_traces() -> [{Who :: trace_who(), LogFile :: string()}]). +lookup_traces() -> + gen_server:call(?TRACER, lookup_traces). %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- init([]) -> - {ok, #state{level = emqx_config:get_env(trace_level, debug), traces = #{}}}. + Level = emqx_config:get_env(trace_level, debug), + {ok, #state{level = Level, traces = #{}}}. -handle_call({start_trace, Who, LogFile}, _From, State = #state{level = Level, traces = Traces}) -> - case lager:trace_file(LogFile, [Who], Level, ?OPTIONS) of +handle_call({start_trace, Who, LogFile}, _From, + State = #state{level = Level, traces = Traces}) -> + case catch lager:trace_file(LogFile, [Who], Level, ?OPTIONS) of {ok, exists} -> - {reply, {error, existed}, State}; - {ok, Trace} -> + {reply, {error, alread_existed}, State}; + {ok, Trace} -> {reply, ok, State#state{traces = maps:put(Who, {Trace, LogFile}, Traces)}}; - {error, Error} -> + {error, Reason} -> + emqx_logger:error("[TRACER] trace error: ~p", [Reason]), + {reply, {error, Reason}, State}; + {'EXIT', Error} -> + emqx_logger:error("[TRACER] trace exit: ~p", [Error]), {reply, {error, Error}, State} end; @@ -112,23 +102,23 @@ handle_call({stop_trace, Who}, _From, State = #state{traces = Traces}) -> end, {reply, ok, State#state{traces = maps:remove(Who, Traces)}}; error -> - {reply, {error, not_found}, State} + {reply, {error, trance_not_found}, State} end; -handle_call(all_traces, _From, State = #state{traces = Traces}) -> +handle_call(lookup_traces, _From, State = #state{traces = Traces}) -> {reply, [{Who, LogFile} || {Who, {_Trace, LogFile}} <- maps:to_list(Traces)], State}; handle_call(Req, _From, State) -> - emqx_log:error("[TRACE] Unexpected Call: ~p", [Req]), + emqx_logger:error("[TRACER] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - emqx_log:error("[TRACE] Unexpected Cast: ~p", [Msg]), + emqx_logger:error("[TRACER] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> - emqx_log:error("[TRACE] Unexpected Info: ~p", [Info]), + emqx_logger:error("[TRACER] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> diff --git a/src/emqx_tracer_sup.erl b/src/emqx_tracer_sup.erl deleted file mode 100644 index bc5faa82c..000000000 --- a/src/emqx_tracer_sup.erl +++ /dev/null @@ -1,32 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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_tracer_sup). - --behaviour(supervisor). - --export([start_link/0]). - --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - Tracer = {tracer, {emqx_tracer, start_link, []}, - permanent, 5000, worker, [emqx_tracer]}, - {ok, {{one_for_one, 10, 3600}, [Tracer]}}. - diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index a1b67561d..595b38116 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_trie). @@ -24,39 +24,39 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). -%% Trie API +%% Trie APIs -export([insert/1, match/1, lookup/1, delete/1]). -%% Tables +%% Mnesia tables -define(TRIE, emqx_trie). -define(TRIE_NODE, emqx_trie_node). %%-------------------------------------------------------------------- -%% Mnesia Bootstrap +%% Mnesia bootstrap %%-------------------------------------------------------------------- %% @doc Create or replicate trie tables. -spec(mnesia(boot | copy) -> ok). mnesia(boot) -> - %% Trie Table + %% Trie table ok = ekka_mnesia:create_table(?TRIE, [ {ram_copies, [node()]}, {record_name, trie}, {attributes, record_info(fields, trie)}]), - %% Trie Node Table + %% Trie node table ok = ekka_mnesia:create_table(?TRIE_NODE, [ {ram_copies, [node()]}, {record_name, trie_node}, {attributes, record_info(fields, trie_node)}]); mnesia(copy) -> - %% Copy Trie Table + %% Copy trie table ok = ekka_mnesia:copy_table(?TRIE), - %% Copy Trie Node Table + %% Copy trie_node table ok = ekka_mnesia:copy_table(?TRIE_NODE). %%-------------------------------------------------------------------- -%% Trie API +%% Trie APIs %%-------------------------------------------------------------------- %% @doc Insert a topic into the trie @@ -110,7 +110,7 @@ add_path({Node, Word, Child}) -> [TrieNode = #trie_node{edge_count = Count}] -> case mnesia:wread({?TRIE, Edge}) of [] -> - write_trie_node(TrieNode#trie_node{edge_count = Count+1}), + write_trie_node(TrieNode#trie_node{edge_count = Count + 1}), write_trie(#trie{edge = Edge, node_id = Child}); [_] -> ok diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl index b2bdc2e54..e7ad63d61 100644 --- a/src/emqx_ws.erl +++ b/src/emqx_ws.erl @@ -26,8 +26,8 @@ -record(wsocket_state, {peername, client_pid, max_packet_size, parser}). -define(WSLOG(Level, Format, Args, State), - emqx_log:Level("WsClient(~s): " ++ Format, - [esockd_net:format(State#wsocket_state.peername) | Args])). + emqx_logger:Level("WsClient(~s): " ++ Format, + [esockd_net:format(State#wsocket_state.peername) | Args])). handle_request(Req) -> @@ -38,7 +38,7 @@ handle_request(Req) -> %%-------------------------------------------------------------------- handle_request('GET', "/mqtt", Req) -> - emqx_log:debug("WebSocket Connection from: ~s", [Req:get(peer)]), + emqx_logger:debug("WebSocket Connection from: ~s", [Req:get(peer)]), Upgrade = Req:get_header_value("Upgrade"), Proto = check_protocol_header(Req), case {is_websocket(Upgrade), Proto} of @@ -56,19 +56,19 @@ handle_request('GET', "/mqtt", Req) -> max_packet_size = PacketSize, client_pid = ClientPid}); {error, Reason} -> - emqx_log:error("Get peername with error ~s", [Reason]), + emqx_logger:error("Get peername with error ~s", [Reason]), Req:respond({400, [], <<"Bad Request">>}) end; {false, _} -> - emqx_log:error("Not WebSocket: Upgrade = ~s", [Upgrade]), + emqx_logger:error("Not WebSocket: Upgrade = ~s", [Upgrade]), Req:respond({400, [], <<"Bad Request">>}); {_, Proto} -> - emqx_log:error("WebSocket with error Protocol: ~s", [Proto]), + emqx_logger:error("WebSocket with error Protocol: ~s", [Proto]), Req:respond({400, [], <<"Bad WebSocket Protocol">>}) end; handle_request(Method, Path, Req) -> - emqx_log:error("Unexpected WS Request: ~s ~s", [Method, Path]), + emqx_logger:error("Unexpected WS Request: ~s ~s", [Method, Path]), Req:not_found(). is_websocket(Upgrade) -> diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 643f8d825..161104568 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -50,8 +50,8 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). -define(WSLOG(Level, Format, Args, State), - emqx_log:Level("WsClient(~s): " ++ Format, - [esockd_net:format(State#wsclient_state.peername) | Args])). + emqx_logger:Level("WsClient(~s): " ++ Format, + [esockd_net:format(State#wsclient_state.peername) | Args])). %% @doc Start WebSocket Client. start_link(Env, WsPid, Req, ReplyChannel) -> @@ -140,7 +140,7 @@ handle_call(Req, _From, State) -> reply({error, unexpected_request}, State). handle_cast({received, Packet}, State = #wsclient_state{proto_state = ProtoState}) -> - emqx_mqtt_metrics:received(Packet), + emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> {noreply, gc(State#wsclient_state{proto_state = ProtoState1}), hibernate}; @@ -290,7 +290,7 @@ emit_stats(undefined, State) -> State; emit_stats(ClientId, State) -> {reply, Stats, _, _} = handle_call(stats, undefined, State), - emqx_stats:set_client_stats(ClientId, Stats), + emqx_cm:set_client_stats(ClientId, Stats), State. wsock_stats(#wsclient_state{connection = Conn}) -> From 16426346bb9286c59204157f5663a6f69b1cdb5e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 24 Apr 2018 17:31:31 +0800 Subject: [PATCH 046/520] Fix whitespace --- src/emqx_session.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 4d7ee4ce6..3d2716099 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -59,7 +59,7 @@ %% %% QoS 1 and QoS 2 messages pending transmission to the Client. %% -%% QoS 2 messages which have been received from the Client, but have not +%% QoS 2 messages which have been received from the Client, but have not %% been completely acknowledged. %% %% Optionally, QoS 0 messages pending transmission to the Client. @@ -739,7 +739,7 @@ acked(pubrec, PacketId, State = #state{client_id = ClientId, case Inflight:lookup(PacketId) of {publish, Msg, _Ts} -> emqx_hooks:run('message.acked', [ClientId, Username], Msg), - State#state{inflight = Inflight:update(PacketId, {pubrel, PacketId, os:timestamp()})}; + State#state{inflight = Inflight:update(PacketId, {pubrel, PacketId, os:timestamp()})}; {pubrel, PacketId, _Ts} -> ?LOG(warning, "Duplicated PUBREC Packet: ~p", [PacketId], State), State From bbe56dabec6e868ceb772c80d626aeedcc23ac01 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 25 Apr 2018 15:37:30 +0800 Subject: [PATCH 047/520] Fix whitespace --- etc/emqx.conf | 6 +++--- etc/ssl_dist.conf | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 78aa90215..cf264a505 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -7,7 +7,7 @@ ##-------------------------------------------------------------------- ## Cluster name. -## +## ## Value: String cluster.name = emqxcluster @@ -297,7 +297,7 @@ log.console.level = error ## The file where console logs will be writed to, when 'log.console' is set as 'file'. ## -## Value: File Name +## Value: File Name ## log.console.file = {{ platform_log_dir }}/console.log ## Maximum file size for console log. @@ -425,7 +425,7 @@ mqtt.keepalive_backoff = 0.75 ## Force GC the MQTT connections. Value 0 will disable the Force GC. ## -## Value: Number >= 0 +## Value: Number >= 0 mqtt.conn.force_gc_count = 100 ##-------------------------------------------------------------------- diff --git a/etc/ssl_dist.conf b/etc/ssl_dist.conf index acdf0aa67..50b0e3279 100644 --- a/etc/ssl_dist.conf +++ b/etc/ssl_dist.conf @@ -1,6 +1,6 @@ %% The options in the {server, Opts} tuple are used when calling ssl:ssl_accept/3, %% and the options in the {client, Opts} tuple are used when calling ssl:connect/4. -%% +%% %% More information at: http://erlang.org/doc/apps/ssl/ssl_distribution.html [{server, [{certfile, "{{ platform_etc_dir }}/certs/cert.pem"}, From dc7804a2dc1c5fadeac8b2af7a43b973e40e1186 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 30 Apr 2018 12:27:39 +0800 Subject: [PATCH 048/520] Support MQTT Version 5.0 Client --- include/emqx_client.hrl | 37 + include/emqx_mqtt.hrl | 58 +- src/emqx_client.erl | 890 +++++++++++++++++- src/emqx_client_sock.erl | 172 ++++ src/emqx_inflight.erl | 4 +- ...qtt_props.erl => emqx_mqtt_properties.erl} | 0 src/emqx_packet.erl | 19 +- src/emqx_parser.erl | 42 +- 8 files changed, 1136 insertions(+), 86 deletions(-) create mode 100644 include/emqx_client.hrl create mode 100644 src/emqx_client_sock.erl rename src/{emqx_mqtt_props.erl => emqx_mqtt_properties.erl} (100%) diff --git a/include/emqx_client.hrl b/include/emqx_client.hrl new file mode 100644 index 000000000..98f6a0595 --- /dev/null +++ b/include/emqx_client.hrl @@ -0,0 +1,37 @@ + +%%-define(CLIENT_IN_BROKER, true). + +%% Default timeout +-define(DEFAULT_KEEPALIVE, 60000). +-define(DEFAULT_ACK_TIMEOUT, 20000). +-define(DEFAULT_CONNECT_TIMEOUT, 30000). +-define(DEFAULT_TCP_OPTIONS, + [binary, {packet, raw}, {active, false}, + {nodelay, true}, {reuseaddr, true}]). + +-ifdef(CLIENT_IN_BROKER). + +-define(LOG(Level, Msg), emqx_log:Level(Msg)). +-define(LOG(Level, Format, Args), emqx_log:Level(Format, Args)). + +-else. + +-define(LOG(Level, Msg), + (case Level of + debug -> error_logger:info_msg(Msg); + info -> error_logger:info_msg(Msg); + warning -> error_logger:warning_msg(Msg); + error -> error_logger:error_msg(Msg); + critical -> error_logger:error_msg(Msg) + end)). +-define(LOG(Level, Format, Args), + (case Level of + debug -> error_logger:info_msg(Format, Args); + info -> error_logger:info_msg(Format, Args); + warning -> error_logger:warning_msg(Format, Args); + error -> error_logger:error_msg(Format, Args); + critical -> error_logger:error_msg(Format, Args) + end)). + +-endif. + diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 65f6a1274..fb7201f02 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%%=================================================================== %%-------------------------------------------------------------------- %% MQTT SockOpts @@ -190,10 +190,10 @@ -type(mqtt_properties() :: undefined | map()). --type(mqtt_subopt() :: list({qos, mqtt_qos()} - | {retain_handling, boolean()} - | {keep_retain, boolean()} - | {no_local, boolean()})). +-type(mqtt_subopt() :: {qos, mqtt_qos()} + | {retain_handling, boolean()} + | {keep_retain, boolean()} + | {no_local, boolean()}). -record(mqtt_packet_connect, { client_id = <<>> :: mqtt_client_id(), @@ -323,15 +323,27 @@ packet_id = PacketId}, payload = Payload}). +-define(PUBACK_PACKET(PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, + variable = #mqtt_packet_puback{packet_id = PacketId}}). + -define(PUBACK_PACKET(Type, PacketId), #mqtt_packet{header = #mqtt_packet_header{type = Type}, variable = #mqtt_packet_puback{packet_id = PacketId}}). +-define(PUBREC_PACKET(PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, + variable = #mqtt_packet_puback{packet_id = PacketId}}). + -define(PUBREL_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, variable = #mqtt_packet_puback{packet_id = PacketId}}). --define(SUBSCRIBE_PACKET(PacketId, TopicFilters), +-define(PUBCOMP_PACKET(PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, + variable = #mqtt_packet_puback{packet_id = PacketId}}). + +-define(SUBSCRIBE_PACKET(PacketId, TopicFilters), #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, qos = ?QOS_1}, variable = #mqtt_packet_subscribe{packet_id = PacketId, topic_filters = TopicFilters}}). @@ -375,7 +387,7 @@ %% Topic that the message is published to topic :: binary(), %% Message QoS - qos = 0 :: mqtt_qos(), + qos = ?QOS0 :: mqtt_qos(), %% Message Flags flags = [] :: [retain | dup | sys], %% Retain flag @@ -384,8 +396,8 @@ dup = false :: boolean(), %% $SYS flag sys = false :: boolean(), - %% Headers - headers = [] :: list(), + %% Properties + properties = [] :: list(), %% Payload payload :: binary(), %% Timestamp diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 06399b1b5..fbfb92997 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -16,55 +16,879 @@ -module(emqx_client). --behaviour(gen_server). +-behaviour(gen_statem). +-include("emqx_mqtt.hrl"). +-include("emqx_client.hrl"). + +-export([start_link/0, start_link/1]). + +-export([subscribe/2, subscribe/3, unsubscribe/2]). +-export([publish/2, publish/3, publish/4, publish/5]). +-export([ping/1]). +-export([disconnect/1, disconnect/2]). +-export([puback/2, pubrec/2, pubrel/2, pubcomp/2]). +-export([subscriptions/1]). + +-export([initialized/3, waiting_for_connack/3, connected/3]). +-export([init/1, callback_mode/0, terminate/3, code_change/4]). + +-type(host() :: inet:ip_address() | inet:hostname()). + +-type(option() :: {name, atom()} + | {owner, pid()} + | {host, host()} + | {hosts, [{host(), inet:port_number()}]} + | {port, inet:port_number()} + | {tcp_opts, [gen_tcp:option()]} + | {ssl, boolean()} + | {ssl_opts, [ssl:ssl_option()]} + | {client_id, iodata()} + | {clean_start, boolean()} + | {username, iodata()} + | {password, iodata()} + | {proto_ver, v3 | v4 | v5} + | {keepalive, non_neg_integer()} + | {max_inflight, pos_integer()} + | {retry_interval, timeout()} + | {will_topic, iodata()} + | {will_payload, iodata()} + | {will_retain, boolean()} + | {will_qos, mqtt_qos() | mqtt_qos_name()} + | {connect_timeout, pos_integer()} + | {ack_timeout, pos_integer()} + | {force_ping, boolean()} + | {debug_mode, boolean()}). + +-export_type([option/0]). + +-record(state, {name :: atom(), + owner :: pid(), + host :: host(), + port :: inet:port_number(), + hosts :: [{host(), inet:port_number()}], + socket :: inet:socket(), + sock_opts :: [emqx_client_sock:option()], + receiver :: pid(), + client_id :: binary(), + clean_start :: boolean(), + username :: binary() | undefined, + password :: binary() | undefined, + proto_ver :: mqtt_vsn(), + proto_name :: iodata(), + keepalive :: non_neg_integer(), + keepalive_timer :: reference() | undefined, + force_ping :: boolean(), + will_flag :: boolean(), + will_msg :: mqtt_message(), + pending_calls :: list(), + subscribers :: list(), + subscriptions :: map(), + max_inflight :: infinity | pos_integer(), + inflight :: emqx_inflight:inflight(), + awaiting_rel :: map(), + properties :: list(), + auto_ack :: boolean(), + ack_timeout :: pos_integer(), + ack_timer :: reference(), + retry_interval :: pos_integer(), + retry_timer :: reference(), + connect_timeout :: pos_integer(), + last_packet_id :: mqtt_packet_id(), + debug_mode :: boolean()}). + +-record(call, {id, from, req, ts}). + +-type(client() :: pid() | atom()). + +-type(msgid() :: mqtt_packet_id()). + +-type(pubopt() :: {retain, boolean()} + | {qos, mqtt_qos()}). + +-type(subopt() :: mqtt_subopt()). + +-export_type([client/0, host/0, msgid/0, pubopt/0, subopt/0]). + +%%-------------------------------------------------------------------- %% API --export([start_link/0]). +%%-------------------------------------------------------------------- -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-spec(start_link() -> gen_statem:start_ret()). +start_link() -> start_link([]). --define(SERVER, ?MODULE). +-spec(start_link(map() | [option()]) -> gen_statem:start_ret()). +start_link(Options) when is_map(Options) -> + start_link(maps:to_list(Options)); +start_link(Options) when is_list(Options) -> + case start_client(with_owner(Options)) of + {ok, Client} -> + connect(Client); + Error -> Error + end. --record(state, {}). +start_client(Options) -> + case proplists:get_value(name, Options) of + undefined -> + gen_statem:start_link(?MODULE, [Options], []); + Name when is_atom(Name) -> + gen_statem:start_link({local, Name}, ?MODULE, [Options], []) + end. -%%%=================================================================== -%%% API -%%%=================================================================== +with_owner(Options) -> + case proplists:get_value(owner, Options) of + Owner when is_pid(Owner) -> Options; + undefined -> [{owner, self()} | Options] + end. -%% @doc Starts the server --spec(start_link() -> {ok, pid()} | ignore | {error, any()}). -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). +%% @private +%% @doc should be called with start_link +-spec(connect(client()) -> {ok, client()} | {error, term()}). +connect(Client) -> + gen_statem:call(Client, connect, infinity). -%%%=================================================================== -%%% gen_server callbacks -%%%=================================================================== +%% @doc Publish QoS0 message to broker. +-spec(publish(client(), iolist(), iodata()) -> ok | {error, term()}). +publish(Client, Topic, Payload) -> + publish(Client, #mqtt_message{topic = iolist_to_binary(Topic), + payload = iolist_to_binary(Payload)}). -init([]) -> - {ok, #state{}}. +%% @doc Publish message to broker with qos, retain options. +-spec(publish(client(), iolist(), iodata(), + mqtt_qos() | mqtt_qos_name() | [pubopt()]) + -> ok | {ok, msgid()} | {error, term()}). +publish(Client, Topic, Payload, QoS) when is_atom(QoS) -> + publish(Client, Topic, Payload, ?QOS_I(QoS)); +publish(Client, Topic, Payload, QoS) when ?IS_QOS(QoS) -> + publish(Client, Topic, Payload, [{qos, QoS}]); +publish(Client, Topic, Payload, Options) when is_list(Options) -> + publish(Client, Topic, [], Payload, Options). +publish(Client, Topic, Properties, Payload, Options) -> + ok = emqx_mqtt_properties:validate(Properties), + publish(Client, #mqtt_message{qos = pubopt(qos, Options), + retain = pubopt(retain, Options), + topic = iolist_to_binary(Topic), + properties = Properties, + payload = iolist_to_binary(Payload)}). -handle_call(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. +%% @doc Publish MQTT Message. +-spec(publish(client(), mqtt_message()) + -> ok | {ok, msgid()} | {error, term()}). +publish(Client, Msg) when is_record(Msg, mqtt_message) -> + gen_statem:call(Client, {publish, Msg}). -handle_cast(_Msg, State) -> - {noreply, State}. +pubopt(qos, Opts) -> + proplists:get_value(qos, Opts, ?QOS0); +pubopt(retain, Opts) -> + lists:member(retain, Opts) orelse proplists:get_bool(retain, Opts). -handle_info(_Info, State) -> - {noreply, State}. +-spec(subscribe(client(), binary() + | {binary(), mqtt_qos_name() | mqtt_qos()}) + -> {ok, mqtt_qos()} | {error, term()}). +subscribe(Client, Topic) when is_binary(Topic) -> + subscribe(Client, Topic, ?QOS_0); +subscribe(Client, {Topic, QoS}) when ?IS_QOS(QoS); is_atom(QoS) -> + subscribe(Client, Topic, ?QOS_I(QoS)); +subscribe(Client, Topics) when is_list(Topics) -> + case io_lib:printable_unicode_list(Topics) of + true -> subscribe(Client, [{Topics, ?QOS_0}]); + false -> Topics1 = [{iolist_to_binary(Topic), [{qos, ?QOS_I(QoS)}]} + || {Topic, QoS} <- Topics], + gen_statem:call(Client, {subscribe, Topics1}) + end. -terminate(_Reason, _State) -> - ok. +-spec(subscribe(client(), string() | binary(), + mqtt_qos_name() | mqtt_qos() | [subopt()]) + -> {ok, mqtt_qos()} | {error, term()}). +subscribe(Client, Topic, QoS) when is_atom(QoS) -> + subscribe(Client, Topic, ?QOS_I(QoS)); +subscribe(Client, Topic, QoS) when ?IS_QOS(QoS) -> + subscribe(Client, Topic, [{qos, QoS}]); +subscribe(Client, Topic, Options) -> + gen_statem:call(Client, {subscribe, iolist_to_binary(Topic), Options}). -code_change(_OldVsn, State, _Extra) -> - {ok, State}. +-spec(unsubscribe(client(), iolist()) -> ok | {error, term()}). +unsubscribe(Client, Topic) when is_binary(Topic) -> + unsubscribe(Client, [Topic]); +unsubscribe(Client, Topics) when is_list(Topics) -> + case io_lib:printable_unicode_list(Topics) of + true -> unsubscribe(Client, [Topics]); + false -> + Topics1 = [iolist_to_binary(Topic) || Topic <- Topics], + gen_statem:call(Client, {unsubscribe, Topics1}) + end. -%%%=================================================================== -%%% Internal functions -%%%=================================================================== +-spec(ping(client()) -> pong). +ping(Client) -> + gen_statem:call(Client, ping). +-spec(disconnect(client()) -> ok). +disconnect(C) -> + disconnect(C, []). +disconnect(Client, Props) -> + gen_statem:call(Client, {disconnect, Props}). +%%-------------------------------------------------------------------- +%% APIs for broker test cases. +%%-------------------------------------------------------------------- + +puback(Client, PacketId) when is_integer(PacketId) -> + gen_statem:cast(Client, {puback, PacketId}). + +pubrec(Client, PacketId) when is_integer(PacketId) -> + gen_statem:cast(Client, {pubrec, PacketId}). + +pubrel(Client, PacketId) when is_integer(PacketId) -> + gen_statem:cast(Client, {pubrel, PacketId}). + +pubcomp(Client, PacketId) when is_integer(PacketId) -> + gen_statem:cast(Client, {pubcomp, PacketId}). + +subscriptions(C) -> gen_statem:call(C, subscriptions). + +%%-------------------------------------------------------------------- +%% gen_statem callbacks +%%-------------------------------------------------------------------- + +init([Options]) -> + process_flag(trap_exit, true), + ClientId = case {proplists:get_value(proto_ver, Options, v4), + proplists:get_value(client_id, Options)} of + {v5, undefined} -> undefined; + {_ver, undefined} -> random_client_id(); + {_ver, Id} -> iolist_to_binary(Id) + end, + State = init(Options, #state{host = {127,0,0,1}, + port = 1883, + hosts = [], + sock_opts = [], + client_id = ClientId, + clean_start = true, + proto_ver = ?MQTT_PROTO_V4, + proto_name = <<"MQTT">>, + keepalive = ?DEFAULT_KEEPALIVE, + will_flag = false, + will_msg = #mqtt_message{}, + ack_timeout = ?DEFAULT_ACK_TIMEOUT, + connect_timeout = ?DEFAULT_CONNECT_TIMEOUT, + force_ping = false, + pending_calls = [], + subscribers = [], + subscriptions = #{}, + max_inflight = infinity, + inflight = emqx_inflight:new(0), + awaiting_rel = #{}, + auto_ack = true, + retry_interval = 0, + last_packet_id = 1, + debug_mode = false}), + %% Connect and Send ConnAck + {ok, initialized, State}. + +random_client_id() -> + rand:seed(exsplus, erlang:timestamp()), + I1 = rand:uniform(round(math:pow(2, 48))) - 1, + I2 = rand:uniform(round(math:pow(2, 32))) - 1, + {ok, Host} = inet:gethostname(), + iolist_to_binary(["emqx-client-", Host, "-", io_lib:format("~12.16.0b~8.16.0b", [I1, I2])]). + +init([], State) -> + State; +init([{name, Name} | Opts], State) -> + init(Opts, State#state{name = Name}); +init([{owner, Owner} | Opts], State) when is_pid(Owner) -> + link(Owner), + init(Opts, State#state{owner = Owner}); +init([{host, Host} | Opts], State) -> + init(Opts, State#state{host = Host}); +init([{port, Port} | Opts], State) -> + init(Opts, State#state{port = Port}); +init([{hosts, Hosts} | Opts], State) -> + Hosts1 = + lists:foldl(fun({Host, Port}, Acc) -> + [{Host, Port}|Acc]; + (Host, Acc) -> + [{Host, 1883}|Acc] + end, [], Hosts), + init(Opts, State#state{hosts = Hosts1}); +init([{tcp_opts, TcpOpts} | Opts], State = #state{sock_opts = SockOpts}) -> + init(Opts, State#state{sock_opts = emqx_misc:merge_opts(SockOpts, TcpOpts)}); +init([ssl | Opts], State = #state{sock_opts = SockOpts}) -> + ok = ssl:start(), + SockOpts1 = emqx_misc:merge_opts([{ssl_opts, []}], SockOpts), + init(Opts, State#state{sock_opts = SockOpts1}); +init([{ssl_opts, SslOpts} | Opts], State = #state{sock_opts = SockOpts}) -> + ok = ssl:start(), + SockOpts1 = emqx_misc:merge_opts(SockOpts, [{ssl_opts, SslOpts}]), + init(Opts, State#state{sock_opts = SockOpts1}); +init([{client_id, ClientId} | Opts], State) -> + init(Opts, State#state{client_id = iolist_to_binary(ClientId)}); +init([{clean_start, CleanStart} | Opts], State) when is_boolean(CleanStart) -> + init(Opts, State#state{clean_start = CleanStart}); +init([{useranme, Username} | Opts], State) -> + init(Opts, State#state{username = iolist_to_binary(Username)}); +init([{passwrod, Password} | Opts], State) -> + init(Opts, State#state{password = iolist_to_binary(Password)}); +init([{keepalive, Secs} | Opts], State) -> + init(Opts, State#state{keepalive = timer:seconds(Secs)}); +init([{proto_ver, v3} | Opts], State) -> + init(Opts, State#state{proto_ver = ?MQTT_PROTO_V3, + proto_name = <<"MQIsdp">>}); +init([{proto_ver, v4} | Opts], State) -> + init(Opts, State#state{proto_ver = ?MQTT_PROTO_V4, + proto_name = <<"MQTT">>}); +init([{proto_ver, v5} | Opts], State) -> + init(Opts, State#state{proto_ver = ?MQTT_PROTO_V5, + proto_name = <<"MQTT">>}); +init([{will_topic, Topic} | Opts], State = #state{will_msg = WillMsg}) -> + WillMsg1 = init_will_msg({topic, Topic}, WillMsg), + init(Opts, State#state{will_flag = true, will_msg = WillMsg1}); +init([{will_payload, Payload} | Opts], State = #state{will_msg = WillMsg}) -> + init(Opts, State#state{will_msg = init_will_msg({payload, Payload}, WillMsg)}); +init([{will_retain, Retain} | Opts], State = #state{will_msg = WillMsg}) -> + init(Opts, State#state{will_msg = init_will_msg({retain, Retain}, WillMsg)}); +init([{will_qos, QoS} | Opts], State = #state{will_msg = WillMsg}) -> + init(Opts, State#state{will_msg = init_will_msg({qos, QoS}, WillMsg)}); +init([{connect_timeout, Timeout}| Opts], State) -> + init(Opts, State#state{connect_timeout = timer:seconds(Timeout)}); +init([{ack_timeout, Timeout}| Opts], State) -> + init(Opts, State#state{ack_timeout = timer:seconds(Timeout)}); +init([force_ping | Opts], State) -> + init(Opts, State#state{force_ping = true}); +init([{force_ping, ForcePing} | Opts], State) when is_boolean(ForcePing) -> + init(Opts, State#state{force_ping = ForcePing}); +init([{max_inflight, infinity} | Opts], State) -> + init(Opts, State#state{max_inflight = infinity, + inflight = emqx_inflight:new(0)}); +init([{max_inflight, I} | Opts], State) when is_integer(I) -> + init(Opts, State#state{max_inflight = I, + inflight = emqx_inflight:new(I)}); +init([auto_ack | Opts], State) -> + init(Opts, State#state{auto_ack = true}); +init([{auto_ack, AutoAck} | Opts], State) when is_boolean(AutoAck) -> + init(Opts, State#state{auto_ack = AutoAck}); +init([{retry_interval, I} | Opts], State) -> + init(Opts, State#state{retry_interval = timer:seconds(I)}); +init([{debug_mode, Mode} | Opts], State) when is_boolean(Mode) -> + init(Opts, State#state{debug_mode = Mode}); +init([_Opt | Opts], State) -> + init(Opts, State). + +init_will_msg({topic, Topic}, WillMsg) -> + WillMsg#mqtt_message{topic = iolist_to_binary(Topic)}; +init_will_msg({payload, Payload}, WillMsg) -> + WillMsg#mqtt_message{payload = iolist_to_binary(Payload)}; +init_will_msg({retain, Retain}, WillMsg) when is_boolean(Retain) -> + WillMsg#mqtt_message{retain = Retain}; +init_will_msg({qos, QoS}, WillMsg) -> + WillMsg#mqtt_message{qos = ?QOS_I(QoS)}. + +callback_mode() -> state_functions. + +initialized({call, From}, connect, State = #state{connect_timeout = Timeout}) -> + case sock_connect(State) of + {ok, State1} -> + case mqtt_connect(State1) of + {ok, State2} -> + {next_state, waiting_for_connack, + add_call(new_call(connect, From), State2), [Timeout]}; + Err = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Err}]} + end; + Err = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Err}]} + end; + +initialized(EventType, EventContent, StateData) -> + handle_event(EventType, EventContent, StateData). + +sock_connect(State) -> + sock_connect(get_hosts(State), {error, no_hosts}, State). + +get_hosts(#state{hosts = [], host = Host, port = Port}) -> + [{Host, Port}]; +get_hosts(#state{hosts = Hosts}) -> Hosts. + +sock_connect([], Err, _State) -> + Err; +sock_connect([{Host, Port} | Hosts], _Err, State = #state{sock_opts = SockOpts}) -> + case emqx_client_sock:connect(self(), Host, Port, SockOpts) of + {ok, Socket, Receiver} -> + {ok, State#state{socket = Socket, receiver = Receiver}}; + Err = {error, _Reason} -> + sock_connect(Hosts, Err, State) + end. + +mqtt_connect(State = #state{client_id = ClientId, + clean_start = CleanStart, + username = Username, + password = Password, + proto_ver = ProtoVer, + proto_name = ProtoName, + keepalive = KeepAlive, + will_flag = WillFlag, + will_msg = WillMsg}) -> + #mqtt_message{qos = WillQos, + retain = WillRetain, + topic = WillTopic, + payload = WillPayload} = WillMsg, + send(?CONNECT_PACKET( + #mqtt_packet_connect{client_id = ClientId, + clean_start = CleanStart, + proto_ver = ProtoVer, + proto_name = ProtoName, + will_flag = WillFlag, + will_retain = WillRetain, + will_qos = WillQos, + keep_alive = KeepAlive, + will_topic = WillTopic, + will_msg = WillPayload, + username = Username, + password = Password}), State). + +waiting_for_connack(cast, ?CONNACK_PACKET(?CONNACK_ACCEPT, + _SessPresent, + _Properties), State) -> + case take_call(connect, State) of + {value, #call{from = From}, State1} -> + {next_state, connected, + ensure_keepalive_timer(ensure_ack_timer(State1)), + [{reply, From, {ok, self()}}]}; + false -> + io:format("Cannot find call: ~p~n", [State#state.pending_calls]), + {stop, {error, unexpected_connack}} + end; + +waiting_for_connack(cast, ?CONNACK_PACKET(ReasonCode, + _SessPresent, + _Properties), State) -> + reply_connack_error(emqx_packet:connack_error(ReasonCode), State); + +waiting_for_connack(timeout, _Timeout, State) -> + reply_connack_error(connack_timeout, State); + +waiting_for_connack(EventType, EventContent, StateData) -> + handle_event(EventType, EventContent, StateData). + +reply_connack_error(Reason, State) -> + Error = {error, Reason}, + case take_call(connect, State) of + {value, #call{from = From}, State1} -> + {stop_and_reply, Error, [{reply, From, Error}], State1}; + false -> {stop, Error} + end. + +connected({call, From}, subscriptions, State = #state{subscriptions = Subscriptions}) -> + {keep_state, State, [{reply, From, maps:to_list(Subscriptions)}]}; + +connected({call, From}, {publish, Msg = #mqtt_message{qos = ?QOS_0}}, State) -> + case send(Msg, State) of + {ok, NewState} -> + {keep_state, NewState, [{reply, From, ok}]}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, {publish, Msg = #mqtt_message{qos = Qos}}, + State = #state{inflight = Inflight, last_packet_id = PacketId}) + when (Qos =:= ?QOS_1); (Qos =:= ?QOS_2) -> + case Inflight:is_full() of + true -> + {keep_state, State, [{reply, From, {error, inflight_full}}]}; + false -> + Msg1 = Msg#mqtt_message{packet_id = PacketId}, + case send(Msg1, State) of + {ok, NewState} -> + Inflight1 = Inflight:insert(PacketId, {publish, Msg1, os:timestamp()}), + {keep_state, ensure_retry_timer(NewState#state{inflight = Inflight1}), + [{reply, From, {ok, PacketId}}]}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end + end; + +connected({call, From}, SubReq = {subscribe, Topic, SubOpts}, + State= #state{last_packet_id = PacketId, subscriptions = Subscriptions}) -> + %%TODO: handle qos... + QoS = proplists:get_value(qos, SubOpts, ?QOS_0), + case send(?SUBSCRIBE_PACKET(PacketId, [{Topic, QoS}]), State) of + {ok, NewState} -> + Call = new_call({subscribe, PacketId}, From, SubReq), + Subscriptions1 = maps:put(Topic, SubOpts, Subscriptions), + {keep_state, ensure_ack_timer(add_call(Call, NewState#state{subscriptions = Subscriptions1}))}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, SubReq = {subscribe, Topics}, + State= #state{last_packet_id = PacketId, subscriptions = Subscriptions}) -> + case send(?SUBSCRIBE_PACKET(PacketId, Topics), State) of + {ok, NewState} -> + Call = new_call({subscribe, PacketId}, From, SubReq), + Subscriptions1 = + lists:fold(fun({Topic, SubOpts}, Acc) -> + maps:put(Topic, SubOpts, Acc) + end, Subscriptions, Topics), + {keep_state, ensure_ack_timer(add_call(Call, NewState#state{subscriptions = Subscriptions1}))}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, UnsubReq = {unsubscribe, Topics}, + State = #state{last_packet_id = PacketId}) -> + case send(?UNSUBSCRIBE_PACKET(PacketId, Topics), State) of + {ok, NewState} -> + Call = new_call({unsubscribe, PacketId}, From, UnsubReq), + {keep_state, ensure_ack_timer(add_call(Call, NewState))}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, ping, State) -> + case send(?PACKET(?PINGREQ), State) of + {ok, NewState} -> + Call = new_call(ping, From), + {keep_state, ensure_ack_timer(add_call(Call, NewState))}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, disconnect, State) -> + case send(?PACKET(?DISCONNECT), State) of + {ok, NewState} -> + {stop_and_reply, normal, [{reply, From, ok}], NewState}; + Error = {error, _Reason} -> + {stop_and_reply, disconnected, [{reply, From, Error}]} + end; + +connected(cast, {puback, PacketId}, State) -> + send_puback(?PUBACK_PACKET(?PUBACK, PacketId), State); + +connected(cast, {pubrec, PacketId}, State) -> + send_puback(?PUBACK_PACKET(?PUBREC, PacketId), State); + +connected(cast, {pubrel, PacketId}, State) -> + send_puback(?PUBREL_PACKET(PacketId), State); + +connected(cast, {pubcomp, PacketId}, State) -> + send_puback(?PUBCOMP_PACKET(PacketId), State); + +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), State) -> + {keep_state, deliver_msg(packet_to_msg(Packet), State)}; + +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), + State = #state{auto_ack = AutoAck}) -> + _ = deliver_msg(packet_to_msg(Packet), State), + case AutoAck of + true -> send_puback(?PUBACK_PACKET(PacketId), State); + false -> {keep_state, State} + end; + +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), + State = #state{awaiting_rel = AwaitingRel}) -> + case send_puback(?PUBREC_PACKET(PacketId), State) of + {keep_state, NewState} -> + AwaitingRel1 = maps:put(PacketId, Packet, AwaitingRel), + {keep_state, NewState#state{awaiting_rel = AwaitingRel1}}; + Stop -> Stop + end; + +connected(cast, ?PUBACK_PACKET(PacketId), + State = #state{owner = Owner, inflight = Inflight}) -> + case Inflight:lookup(PacketId) of + {value, {publish, #mqtt_message{packet_id = PacketId}, _Ts}} -> + Owner ! {puback, PacketId}, + {keep_state, State#state{inflight = Inflight:delete(PacketId)}}; + none -> + ?LOG(warning, "Unexpected PUBACK: ~p", [PacketId]), + {keep_state, State} + end; + +connected(cast, ?PUBREC_PACKET(PacketId), State = #state{inflight = Inflight}) -> + send_puback(?PUBREL_PACKET(PacketId), + case Inflight:lookup(PacketId) of + {value, {publish, _Msg, _Ts}} -> + Inflight1 = Inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}), + State#state{inflight = Inflight1}; + {value, {pubrel, _Ref, _Ts}} -> + ?LOG(warning, "Duplicated PUBREC Packet: ~p", [PacketId]), + State; + none -> + ?LOG(warning, "Unexpected PUBREC Packet: ~p", [PacketId]), + State + end); + +%%TODO::... if auto_ack is false, should we take PacketId from the map? +connected(cast, ?PUBREL_PACKET(PacketId), State = #state{awaiting_rel = AwaitingRel, + auto_ack = AutoAck}) -> + case maps:take(PacketId, AwaitingRel) of + {Packet, AwaitingRel1} -> + NewState = deliver_msg(packet_to_msg(Packet), + State#state{awaiting_rel = AwaitingRel1}), + case AutoAck of + true -> send_puback(?PUBCOMP_PACKET(PacketId), NewState); + false -> {keep_state, NewState} + end; + error -> + ?LOG(warning, "Unexpected PUBREL: ~p", [PacketId]), + {keep_state, State} + end; + +connected(cast, ?PUBCOMP_PACKET(PacketId), State = #state{owner = Owner, inflight = Inflight}) -> + case Inflight:lookup(PacketId) of + {value, {pubrel, _PacketId, _Ts}} -> + Owner ! {pubcomp, PacketId}, + {keep_state, State#state{inflight = Inflight:delete(PacketId)}}; + none -> + ?LOG(warning, "Unexpected PUBCOMP Packet: ~p", [PacketId]), + {keep_state, State} + end; + +%%TODO: handle suback... +connected(cast, ?SUBACK_PACKET(PacketId, QosTable), + State = #state{subscriptions = Subscriptions}) -> + ?LOG(info, "SUBACK(~p) Received", [PacketId]), + case take_call({subscribe, PacketId}, State) of + {value, #call{from = From}, State1} -> + {keep_state, State1, [{reply, From, ok}]}; + false -> {keep_state, State} + end; + +%%TODO: handle unsuback... +connected(cast, ?UNSUBACK_PACKET(PacketId), + State = #state{subscriptions = Subscriptions}) -> + ?LOG(info, "UNSUBACK(~p) received", [PacketId]), + case take_call({unsubscribe, PacketId}, State) of + {value, #call{from = From, req = {_, Topics}}, State1} -> + {keep_state, State1#state{subscriptions = + lists:foldl(fun(Topic, Subs) -> + maps:remove(Topic, Subs) + end, Subscriptions, Topics)}, + [{reply, From, ok}]}; + false -> {keep_state, State} + end; + +%%TODO: handle PINGRESP... +connected(cast, ?PACKET(?PINGRESP), State = #state{pending_calls = []}) -> + {keep_state, State}; +connected(cast, ?PACKET(?PINGRESP), State) -> + case take_call(ping, State) of + {value, #call{from = From}, State1} -> + {keep_state, State1, [{reply, From, pong}]}; + false -> {keep_state, State} + end; + +connected(info, {timeout, _TRef, keepalive}, State = #state{force_ping = true}) -> + case send(?PACKET(?PINGREQ), State) of + {ok, NewState} -> + {keep_state, ensure_keepalive_timer(NewState)}; + Error -> {stop, Error} + end; + +connected(info, {timeout, TRef, keepalive}, + State = #state{socket = Socket, keepalive_timer = TRef}) -> + case should_ping(Socket) of + true -> + case send(?PACKET(?PINGREQ), State) of + {ok, NewState} -> + {keep_state, ensure_keepalive_timer(NewState)}; + Error -> {stop, Error} + end; + false -> + {keep_state, ensure_keepalive_timer(State)}; + {error, Reason} -> + {stop, {error, Reason}} + end; + +connected(info, {timeout, TRef, ack}, State = #state{ack_timer = TRef, + ack_timeout = Timeout, + pending_calls = Calls}) -> + NewState = State#state{ack_timer = undefined, + pending_calls = timeout_calls(Timeout, Calls)}, + {keep_state, ensure_ack_timer(NewState)}; + +connected(info, {timeout, TRef, retry}, State = #state{retry_timer = TRef, + inflight = Inflight}) -> + case Inflight:is_empty() of + true -> {keep_state, State#state{retry_timer = undefined}}; + false -> retry_send(State) + end; + +connected(EventType, EventContent, StateData) -> + handle_event(EventType, EventContent, StateData). + +should_ping(Sock) -> + case emqx_client_sock:getstat(Sock, [send_oct]) of + {ok, [{send_oct, Val}]} -> + OldVal = get(send_oct), put(send_oct, Val), + OldVal == undefined orelse OldVal == Val; + Err = {error, _Reason} -> + Err + end. + +handle_event(info, {'EXIT', Owner, Reason}, #state{owner = Owner}) -> + {stop, Reason}; + +handle_event(info, {'EXIT', Receiver, Reason}, #state{receiver = Receiver}) -> + {stop, Reason}; + +handle_event(info, {inet_reply, _Sock, ok}, State) -> + {keep_state, State}; + +handle_event(info, {inet_reply, _Sock, {error, Reason}}, State) -> + {stop, Reason, State}; + +handle_event(EventType, EventContent, State) -> + ?LOG(error, "Unexpected Event: (~p, ~p)", [EventType, EventContent]), + {keep_state, State}. + +%% Mandatory callback functions +terminate(_Reason, _State, #state{socket = undefined}) -> + ok; +terminate(_Reason, _State, #state{socket = Socket, + receiver = Receiver}) -> + emqx_client_sock:stop(Receiver), + emqx_client_sock:close(Socket). + +code_change(_Vsn, State, Data, _Extra) -> + {ok, State, Data}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +ensure_keepalive_timer(State = #state{keepalive = 0}) -> + State; +ensure_keepalive_timer(State = #state{keepalive = Keepalive}) -> + TRef = erlang:start_timer(Keepalive, self(), keepalive), + State#state{keepalive_timer = TRef}. + +new_call(Id, From) -> + new_call(Id, From, undefined). +new_call(Id, From, Req) -> + #call{id = Id, from = From, req = Req, ts = os:timestamp()}. + +add_call(Call, State = #state{pending_calls = Calls}) -> + State#state{pending_calls = [Call | Calls]}. + +take_call(Id, State = #state{pending_calls = Calls}) -> + case lists:keytake(Id, #call.id, Calls) of + {value, Call, Left} -> + {value, Call, State#state{pending_calls = Left}}; + false -> false + end. + +timeout_calls(Timeout, Calls) -> + timeout_calls(os:timestamp(), Timeout, Calls). +timeout_calls(Now, Timeout, Calls) -> + lists:foldl(fun(C = #call{from = From, ts = Ts}, Acc) -> + case (timer:now_diff(Now, Ts) div 1000) >= Timeout of + true -> gen_statem:reply(From, {error, ack_timeout}), + Acc; + false -> [C | Acc] + end + end, [], Calls). + +ensure_ack_timer(State = #state{ack_timer = undefined, + ack_timeout = Timeout, + pending_calls = Calls}) when length(Calls) > 0 -> + State#state{ack_timer = erlang:start_timer(Timeout, self(), ack)}; +ensure_ack_timer(State) -> State. + +ensure_retry_timer(State = #state{retry_interval = Interval}) -> + ensure_retry_timer(Interval, State). +ensure_retry_timer(Interval, State = #state{retry_timer = undefined}) when Interval > 0 -> + State#state{retry_timer = erlang:start_timer(Interval, self(), retry)}; +ensure_retry_timer(_Interval, State) -> + State. + +retry_send(State = #state{inflight = Inflight}) -> + SortFun = fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end, + Msgs = lists:sort(SortFun, Inflight:values()), + retry_send(Msgs, os:timestamp(), State). + +retry_send([], _Now, State) -> + {keep_state, ensure_retry_timer(State)}; +retry_send([{Type, Msg, Ts} | Msgs], Now, State = #state{retry_interval = Interval}) -> + Diff = timer:now_diff(Now, Ts) div 1000, %% micro -> ms + case (Diff >= Interval) of + true -> case retry_send(Type, Msg, Now, State) of + {ok, NewState} -> retry_send(Msgs, Now, NewState); + {error, Error} -> {stop, Error} + end; + false -> {keep_state, ensure_retry_timer(Interval - Diff, State)} + end. + +retry_send(publish, Msg = #mqtt_message{qos = QoS, packet_id = PacketId}, + Now, State = #state{inflight = Inflight}) -> + Msg1 = Msg#mqtt_message{dup = (QoS =:= ?QOS1)}, + case send(Msg1, State) of + {ok, NewState} -> + Inflight1 = Inflight:update(PacketId, {publish, Msg1, Now}), + {ok, NewState#state{inflight = Inflight1}}; + Error = {error, _Reason} -> + Error + end; +retry_send(pubrel, PacketId, Now, State = #state{inflight = Inflight}) -> + case send(?PUBREL_PACKET(PacketId), State) of + {ok, NewState} -> + Inflight1 = Inflight:update(PacketId, {pubrel, PacketId, Now}), + {ok, NewState#state{inflight = Inflight1}}; + Error = {error, _Reason} -> + Error + end. + +deliver_msg(#mqtt_message{packet_id = PacketId, + qos = QoS, + retain = Retain, + dup = Dup, + topic = Topic, + properties = Props, + payload = Payload}, + State = #state{owner = Owner}) -> + Metadata = #{mid => PacketId, qos => QoS, dup => Dup, + retain => Retain, properties => Props}, + Owner ! {publish, Topic, Metadata, Payload}, State. + +packet_to_msg(?PUBLISH_PACKET(QoS, Topic, PacketId, Payload)) -> + #mqtt_message{qos = QoS, packet_id = PacketId, + topic = Topic, payload = Payload}. + +msg_to_packet(#mqtt_message{packet_id = PacketId, + qos = Qos, + retain = Retain, + dup = Dup, + topic = Topic, + payload = Payload}) -> + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = Qos, + retain = Retain, + dup = Dup}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId}, + payload = Payload}. + +send_puback(Packet, State) -> + case send(Packet, State) of + {ok, NewState} -> {keep_state, NewState}; + {error, Reason} -> {stop, Reason} + end. + +send(Msg, State) when is_record(Msg, mqtt_message) -> + send(msg_to_packet(Msg), State); + +send(Packet, StateData = #state{socket = Socket}) + when is_record(Packet, mqtt_packet) -> + Data = emqx_serializer:serialize(Packet), + case emqx_client_sock:send(Socket, Data) of + ok -> {ok, next_msg_id(StateData)}; + {error, Reason} -> {error, Reason} + end. + +next_msg_id(State = #state{last_packet_id = 16#ffff}) -> + State#state{last_packet_id = 1}; + +next_msg_id(State = #state{last_packet_id = Id}) -> + State#state{last_packet_id = Id + 1}. diff --git a/src/emqx_client_sock.erl b/src/emqx_client_sock.erl new file mode 100644 index 000000000..febacd1d3 --- /dev/null +++ b/src/emqx_client_sock.erl @@ -0,0 +1,172 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%%=================================================================== + +-module(emqx_client_sock). + +-include("emqx_client.hrl"). + +-export([connect/4, connect/5, send/2, close/1, stop/1]). + +-export([sockname/1, setopts/2, getstat/2]). + +%% Internal export +-export([receiver/2, receiver_loop/3]). + +-record(ssl_socket, {tcp, ssl}). + +-type(socket() :: inet:socket() | #ssl_socket{}). + +-type(sockname() :: {inet:ip_address(), inet:port_number()}). + +-type(option() :: gen_tcp:connect_option() + | {ssl_options, [ssl:ssl_option()]}). + +-export_type([socket/0, option/0]). + +%%-------------------------------------------------------------------- +%% Socket API +%%-------------------------------------------------------------------- + +-spec(connect(pid(), inet:ip_address() | inet:hostname(), + inet:port_number(), [option()]) + -> {ok, socket()} | {error, term()}). +connect(ClientPid, Host, Port, SockOpts) -> + connect(ClientPid, Host, Port, SockOpts, ?DEFAULT_CONNECT_TIMEOUT). + +connect(ClientPid, Host, Port, SockOpts, Timeout) -> + case do_connect(Host, Port, SockOpts, Timeout) of + {ok, Sock} -> + Receiver = spawn_link(?MODULE, receiver, [ClientPid, Sock]), + ok = controlling_process(Sock, Receiver), + {ok, Sock, Receiver}; + Error -> + Error + end. + +do_connect(Host, Port, SockOpts, Timeout) -> + TcpOpts = emqx_misc:merge_opts(?DEFAULT_TCP_OPTIONS, + lists:keydelete(ssl_options, 1, SockOpts)), + case gen_tcp:connect(Host, Port, TcpOpts, Timeout) of + {ok, Sock} -> + case lists:keyfind(ssl_options, 1, SockOpts) of + {ssl_options, SslOpts} -> + ssl_upgrade(Sock, SslOpts, Timeout); + false -> {ok, Sock} + end; + {error, Reason} -> + {error, Reason} + end. + +ssl_upgrade(Sock, SslOpts, Timeout) -> + case ssl:connect(Sock, SslOpts, Timeout) of + {ok, SslSock} -> + {ok, #ssl_socket{tcp = Sock, ssl = SslSock}}; + {error, Reason} -> {error, Reason} + end. + +-spec(controlling_process(socket(), pid()) -> ok). +controlling_process(Sock, Pid) when is_port(Sock) -> + gen_tcp:controlling_process(Sock, Pid); +controlling_process(#ssl_socket{ssl = SslSock}, Pid) -> + ssl:controlling_process(SslSock, Pid). + +-spec(send(socket(), iodata()) -> ok | {error, einval | closed}). +send(Sock, Data) when is_port(Sock) -> + try erlang:port_command(Sock, Data) of + true -> ok + catch + error:badarg -> + {error, einval} + end; +send(#ssl_socket{ssl = SslSock}, Data) -> + ssl:send(SslSock, Data). + +-spec(close(socket()) -> ok). +close(Sock) when is_port(Sock) -> + gen_tcp:close(Sock); +close(#ssl_socket{ssl = SslSock}) -> + ssl:close(SslSock). + +-spec(stop(Receiver :: pid()) -> stop). +stop(Receiver) -> + Receiver ! stop. + +-spec(setopts(socket(), [gen_tcp:option() | ssl:socketoption()]) -> ok). +setopts(Sock, Opts) when is_port(Sock) -> + inet:setopts(Sock, Opts); +setopts(#ssl_socket{ssl = SslSock}, Opts) -> + ssl:setopts(SslSock, Opts). + +-spec(getstat(socket(), [atom()]) + -> {ok, [{atom(), integer()}]} | {error, term()}). +getstat(Sock, Options) when is_port(Sock) -> + inet:getstat(Sock, Options); +getstat(#ssl_socket{tcp = Sock}, Options) -> + inet:getstat(Sock, Options). + +-spec(sockname(socket()) -> {ok, sockname()} | {error, term()}). +sockname(Sock) when is_port(Sock) -> + inet:sockname(Sock); +sockname(#ssl_socket{ssl = SslSock}) -> + ssl:sockname(SslSock). + +%%-------------------------------------------------------------------- +%% Receiver +%%-------------------------------------------------------------------- + +receiver(ClientPid, Sock) -> + receiver_activate(ClientPid, Sock, emqx_parser:initial_state()). + +receiver_activate(ClientPid, Sock, ParseState) -> + setopts(Sock, [{active, once}]), + erlang:hibernate(?MODULE, receiver_loop, [ClientPid, Sock, ParseState]). + +receiver_loop(ClientPid, Sock, ParseState) -> + receive + {TcpOrSsL, _Sock, Data} when TcpOrSsL =:= tcp; + TcpOrSsL =:= ssl -> + case parse_received_bytes(ClientPid, Data, ParseState) of + {ok, NewParseState} -> + receiver_activate(ClientPid, Sock, NewParseState); + {error, Error} -> + exit({frame_error, Error}) + end; + {Error, _Sock, Reason} when Error =:= tcp_error; + Error =:= ssl_error -> + exit({Error, Reason}); + {Closed, _Sock} when Closed =:= tcp_closed; + Closed =:= ssl_closed -> + exit(Closed); + stop -> + close(Sock) + end. + +parse_received_bytes(_ClientPid, <<>>, ParseState) -> + {ok, ParseState}; + +parse_received_bytes(ClientPid, Data, ParseState) -> + io:format("RECV Data: ~p~n", [Data]), + case emqx_parser:parse(Data, ParseState) of + {more, ParseState1} -> + {ok, ParseState1}; + {ok, Packet, Rest} -> + io:format("RECV Packet: ~p~n", [Packet]), + gen_statem:cast(ClientPid, Packet), + parse_received_bytes(ClientPid, Rest, emqx_parser:initial_state()); + {error, Error} -> + {error, Error} + end. + diff --git a/src/emqx_inflight.erl b/src/emqx_inflight.erl index 73a60058a..10b3043b3 100644 --- a/src/emqx_inflight.erl +++ b/src/emqx_inflight.erl @@ -31,9 +31,9 @@ new(MaxSize) when MaxSize >= 0 -> contain(Key, {?MODULE, [_MaxSize, Tree]}) -> gb_trees:is_defined(Key, Tree). --spec(lookup(Key :: any(), inflight()) -> any()). +-spec(lookup(Key :: any(), inflight()) -> {value, any()} | none). lookup(Key, {?MODULE, [_MaxSize, Tree]}) -> - gb_trees:get(Key, Tree). + gb_trees:lookup(Key, Tree). -spec(insert(Key :: any(), Value :: any(), inflight()) -> inflight()). insert(Key, Value, {?MODULE, [MaxSize, Tree]}) -> diff --git a/src/emqx_mqtt_props.erl b/src/emqx_mqtt_properties.erl similarity index 100% rename from src/emqx_mqtt_props.erl rename to src/emqx_mqtt_properties.erl diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index f78c01362..fc23fb883 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -20,7 +20,7 @@ -include("emqx_mqtt.hrl"). --export([protocol_name/1, type_name/1, connack_name/1]). +-export([protocol_name/1, type_name/1, connack_error/1]). -export([format/1]). @@ -37,14 +37,15 @@ protocol_name(?MQTT_PROTO_V5) -> <<"MQTT">>. type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> lists:nth(Type, ?TYPE_NAMES). -%% @doc Connack Name --spec(connack_name(mqtt_connack()) -> atom()). -connack_name(?CONNACK_ACCEPT) -> 'CONNACK_ACCEPT'; -connack_name(?CONNACK_PROTO_VER) -> 'CONNACK_PROTO_VER'; -connack_name(?CONNACK_INVALID_ID) -> 'CONNACK_INVALID_ID'; -connack_name(?CONNACK_SERVER) -> 'CONNACK_SERVER'; -connack_name(?CONNACK_CREDENTIALS) -> 'CONNACK_CREDENTIALS'; -connack_name(?CONNACK_AUTH) -> 'CONNACK_AUTH'. +%% @doc Connack Error +-spec(connack_error(mqtt_connack()) -> atom()). +connack_error(?CONNACK_ACCEPT) -> 'CONNACK_ACCEPT'; +connack_error(?CONNACK_PROTO_VER) -> 'CONNACK_PROTO_VER'; +connack_error(?CONNACK_INVALID_ID) -> 'CONNACK_INVALID_ID'; +connack_error(?CONNACK_SERVER) -> 'CONNACK_SERVER'; +connack_error(?CONNACK_CREDENTIALS) -> 'CONNACK_CREDENTIALS'; +connack_error(?CONNACK_AUTH) -> 'CONNACK_AUTH'; +connack_error(_ReasonCode) -> 'CONNACK_UNKNOWN_ERR'. %% @doc From Message to Packet -spec(from_message(message()) -> mqtt_packet()). diff --git a/src/emqx_parser.erl b/src/emqx_parser.erl index 27faaacc2..5cb9aa20a 100644 --- a/src/emqx_parser.erl +++ b/src/emqx_parser.erl @@ -117,10 +117,10 @@ parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length, S false -> {error, protocol_header_corrupt} end; - %{?CONNACK, <>} -> - % <<_Reserved:7, SP:1, ReturnCode:8>> = FrameBin, - % wrap(Header, #mqtt_packet_connack{ack_flags = SP, - % return_code = ReturnCode }, Rest); + {?CONNACK, <>} -> + <<_Reserved:7, SP:1, ReasonCode:8>> = FrameBin, + wrap(Header, #mqtt_packet_connack{ack_flags = SP, + reason_code = ReasonCode}, Rest); {?PUBLISH, <>} -> {TopicName, Rest1} = parse_utf(FrameBin), {PacketId, Rest2} = case Qos of @@ -154,11 +154,11 @@ parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length, S wrap(Header, #mqtt_packet_subscribe{packet_id = PacketId, properties = Properties, topic_filters = TopicFilters}, Rest); - %{?SUBACK, <>} -> - % <> = FrameBin, - % {Properties, Rest2/binary>> = parse_properties(ProtoVer, Rest1), - % wrap(Header, #mqtt_packet_suback{packet_id = PacketId, properties = Properties, - % reason_codes = parse_qos(Rest1, [])}, Rest); + {?SUBACK, <>} -> + <> = FrameBin, + {Properties, Rest2} = parse_properties(Vsn, Rest1), + wrap(Header, #mqtt_packet_suback{packet_id = PacketId, properties = Properties, + reason_codes = parse_qos(Rest2, [])}, Rest); {?UNSUBSCRIBE, <>} -> %% 1 = Qos, <> = FrameBin, @@ -167,18 +167,17 @@ parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length, S wrap(Header, #mqtt_packet_unsubscribe{packet_id = PacketId, properties = Properties, topics = Topics}, Rest); - %{?UNSUBACK, <>} -> - % <> = FrameBin, - % {Properties, Rest2} = parse_properties(ProtoVer, Rest1), - % wrap(Header, #mqtt_packet_unsuback { - % packet_id = PacketId, - % properties = Properties }, Rest); + {?UNSUBACK, <>} -> + <> = FrameBin, + {Properties, _Rest2} = parse_properties(Vsn, Rest1), + wrap(Header, #mqtt_packet_unsuback{packet_id = PacketId, + properties = Properties}, Rest); {?PINGREQ, Rest} -> Length = 0, wrap(Header, Rest); - %{?PINGRESP, Rest} -> - % Length = 0, - % wrap(Header, Rest); + {?PINGRESP, Rest} -> + Length = 0, + wrap(Header, Rest); {?DISCONNECT, <>} -> if Vsn == ?MQTT_PROTO_V5 -> @@ -258,7 +257,7 @@ parse_property(<<16#16, Len:16/big, Val:Len/binary, Bin/binary>>, Props) -> parse_property(<<16#17, Val, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Request-Problem-Information' => Val}); %% 24: 'Will-Delay-Interval', Four Byte Integer; -parse_property(<<16#18, Val:32, Bin/binary>>, Props) -> +parse_property(<<16#18, Val:32, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Will-Delay-Interval' => Val}); %% 25: 'Request-Response-Information', Byte; parse_property(<<16#19, Val, Bin/binary>>, Props) -> @@ -327,6 +326,11 @@ parse_topics(?UNSUBSCRIBE = Sub, Bin, Topics) -> {Name, <>} = parse_utf(Bin), parse_topics(Sub, Rest, [Name | Topics]). +parse_qos(<<>>, Acc) -> + lists:reverse(Acc); +parse_qos(<>, Acc) -> + parse_qos(Rest, [QoS | Acc]). + parse_utf_pair(Bin) -> [{Name, Value} || <> <= Bin]. From 1740b067479db8223f33921443f618f0110a381b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 30 Apr 2018 12:33:02 +0800 Subject: [PATCH 049/520] Add MQTT Version 5.0 Spec --- docs/mqtt-v5.0.pdf | Bin 1880627 -> 1707553 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/mqtt-v5.0.pdf b/docs/mqtt-v5.0.pdf index cbcc0d029ec9270858555a05672ddf1b8aac0130..8f8690e08c98d7cc8ea096c4327f8ed96b22a288 100644 GIT binary patch delta 61 zcmV~$$q_>U006*2ki;2B96#N8t00dp!zVRRl0xhr*Y@AnpFk+`5KE+w PXtX-Lufb?K=cT*>l>HM` delta 174294 zcmeF4XMkN*)vyCfF(^gp%}Y}dNNz6@fdokCge3GA^v=v3dIplvg+qy;2nZq|MT&|q z7DP~xjs*b`5d{?x6qFZG0Rd?Nh3|RxIp>ziB$+^F=Fi8FnS0JXXYaLFUu*BR_Z62M z@WZEOoH%>2*^AG9-|Y9#USjr=vzMB^^z0AJ9zA=R*&m$!q1nsMUT*gCvsak?;n^RV zz2fYZX0JSZmD#J#UhPM#ud{4asbsR5zOI8tj~h37?6hMJYh=bwuJ7N-jONdb#`Ia4 zJl{{@a!O-n-w`vq8#6PxLbjUR96x<}->h(_mfYE?(Q{yZLf_FmmCGjgw(6Tci>78~ zD(QQZX7n9#n2$uc|ihz@*@X{%vYt=tRxJLRU=(977j@gldZ7Ht?#21e7LpgOW+AcAQxFv6VP#++uHcdxO{KEw^%Kg%H5T6xl}2ZYvn>Ym(Ri}`FyriEJtN7viWSe;wuv~ zMShRB%~YRFJ)&z?(26@AIQ?M196x!-Pi|YEc|c}tZ+EHQRW4M@)mph;?P?UOjcTD@ z?9KI7YUSQqZ>HbKrDRH(iKs*sX_V^7ZH}HbX=)G;?xr-KJfp9BY9lu=4MZ&BqtVu5 zr_HEOpLv*;qWc)IvlX|tZX9`FcVo(=2~8o71t+F!4DHzU@{pjE$wXuK+-q;Tu8qlt z{{SFYX?~u5#36_DgGOG6+dyfI$t5pA*fHgj7nw}1TpUwK?^jD>a>@OU!KYR)+9#83 zb~n(cj<;mA6cu~4UhJ@03Gbwjfsjdk)B3>8*3vr>@Fp3>KeyY3Vqz|n3c$odrm4Or z7Bi{dYQ7!6+|E~%rdBeka7>z7%_O4H?~NAQL}Mp!Juw3v8%NLL&vu7m`zNFywn{&2 z*Fl7M6#WRfaC9tW6RwKd{EkjFP{^fXAFF{vE*)2@1`4@U@KpnaTq?2)qXwcx4-BjZ z3i(t#=c5Mdm2$SLSm^047PHx&o?>^kyStRjcX#KjrChFF?^FYYti(;ZV_Fn+OTUrl zQ~6Iuo==2-Y;)xK)KG+x=TnogFe8r#9+l@c@{|YXGxDxNS68l3E7fcHY?rszsUB!GoU&+<-UAWI}UM%wMFq$-PCwY{PywH#4g}G>6=%9Ji zQCoPLx04+IqiOUCO;>L|G_UuR%C&5^R;)KFrQWV$HCxM;3cdMip{EYz`x(7BQb+Vh|PeY`C~fYf?y@4hW@Dh2pe2^Z{%A>!?>?EpEvPZsCF*p(OU3y( z--Td};}Nwx-`M*x)x_b>mCIvF!TZi+^5tsN5$_mc8^1(N_H409>r=;DF~cl~UxM|X z2V!ES*nyZRR|x7e9#y~$8fk7pnr~E62#PmuqEM)g$tMWNRf}Wt31SLG?qfJZgiS3 zwb~J&5N*VYk*7e&O>2D=s$B*6Crw~ z>0ot?s+>ve98c9U~jVyt8&uW>+D<)FCE^zRZH;y(#JxAZ2S%(q^-|7uy6YA`OLUdt=uT) zE9Blv)nWr@x~Is0rBX4*ZpR21|9GC%advm1@M0Rh}>UJ=bLt*yv;4FVF-3_^A{t<=$dff%rV) z#atQW(@DKtp^GeFGys8|A%FS9PrXz%c?dYgQms^>;WF=3?XXOnG)Pc{+e9!}CMjr^ zv63xzmFT9D<4&pERnC_iyjLja89yEKPOjWljK~~R_^;R~(pvbURLU1?B`%8qVYiiP zxxxs^Gx9p!G9F+7WMn6FOVSc0kQGt^ zmVYbUlSF)1tyHDI4OB_cR$vZV2Td?Zpjn~ZLvuBd9yg4sF4n;box~m5(R?~A(@{65 zhd|{j7%LU?zzpd^yAAt`=&%Y+z!W3l2BXd<4|E5b0c&W$4076WHDCfJgu+Fcv)m)~ zN<{`xW2E&`g|Ue_F>8(hs)v5S09XhZ4NQst^fSQ>)&1ls3m2`)#x8~~)sOF{vJq++T!pqzXtEDC02#t5V|99cXfAVpl@>ZBva z*F(dB1oBAyJvi$GAm_D!3V45#(0(lU`HFxd-Z3fQE%NUnT_l8oCUsE;0ExUqmT(OU zWfme%m?{R4_jsK|8+rkz4Dwx%c?p06Ng#MEzGZNnPnfhC^hOs0w4Zv!7i4choiwS- z-N--k13SpIRZ+<`fRNoF#hfaZgh!;cdZAZ9(HHQLm{8vFpNb5N_jy7^#eLd=`w{4F z?)5O7hz=A7ZXkAntDUE45Z%Qig)|E*hCxHf&koEEvw?@ylwd|x%$HzXfTO@fRc|En zjan(?P#ZZoBL~1JOeD7_->cFA*<67*{&UnkQAequ%Le%2{bE~QJ{)R6~HjH@)G)GDH0IkHNGq6Os0Xn;w-e>?~4}8%B*D{M!>;?VZpc#QH z0;SKDa^bn6ZSbsokT3QiqKLeIBRD#4Tw{YAGfh+|_Nt08L?8~j!S$6|f#-r$pwEjn z8iO5es#1qkNJ*ZVpfHdsJ!i}H0%Jv7%jnfEc?X(HYQ$4sLQu5}l_L7b(FF0q0Kp4E z^};~FUz9n6Sk)m8h6)UHm#YHAcss2iRuT;5k%tEN4 zO1V^v$~_26HxNoy*aP|$H5EJ}%`i3m4x~X2pc@mtj2h57~<)0y-dSz06A{8QV1eC&BWef!DREFIUnqrLV97eR0^?Jhd08F=C za0oOrnCPX|E*Vlx6WD|QIA zE)BG=RAEHTTa2-MF)A^CT1HBf4uwn-fO3ouBSs|`MR}DGYCLjyFsNAs3jh!&#Mb*D zqjf(3%D}J(@B?fKj$_QKiqR)6j76BoxM5JMG{eK#ZAY3k&_gG^2rm8{sv(EsbG*`l zCS#Jo*<8nR-ZDlB#wsLZ!vG{%kbnAv1YDJBm8g#St^{|OruiFaB!mD1ZP5vI!-G7B zDE2C7fRX?NAXT_sT5C-74&@O#2)Z9;)_B4D=u$e=(V=}LC1z$68v36OdPOztfwAxf zAmv&T08lQtl#UoZAJZ6M#FQflJV2jF2@T5&=xvz|6l)jYdYMkXP>a|x-)aRINWQBS zWdkZ?2^9*0z$dL!n>l7=!3ASXH0nq(QT;Wf+|ty5sBz*0HHfGV@c{GVU=KBw?@jD~4y`*!Bk<{{tk9lFpEMlsZgS7OBm z8RGA8j`<#7a)F68$2hvkKfjs{2YICfI_d!<`~`W+uMMK(VWI#%cC8?Df<~eoI*c6( z*dNl7pC^c{Vh4c>tA;U0w?chKjy=>TjCl?EM!XDIyrvbCUr`%AMr?(31NyueKkmYIWD)hj1U9rQe%ifabcVt@q*l^m{T4LA8@+ zNWYI9b+Z8;K#p;-G$|=IknyA|2%tNi>-T*Vl<+;Fc)#ysA2E3@(37!QRo1pk~Dz6PMZSQ5Ki;Gc0T?=g{5!vw?_;@NPup>-votzdcf&#McCu*1peY9cs9%c-{Ri{lyM6{v z3eq&Unkpj3_}{`52Z#YW5bAhfP!MZwa4wkVd8b{~_InH72$r9Bi$ntHx5WrL&Ktr! z@j#-QSyqP`2wfYn86C_EK&*5sPpDnvM1FKOaLdgXpt_-d3_^$g!!=H63V((WNqY~( z=I>Z-qIT0^EtJfd00Oa{_C4(`X*3;O`gcC0soE|FMudDoD7gJurm>iVqnA5cL2Vq2M{yBIG%QMA&nEB&C&ug8mO5=da1SMVUqic?bc9o{sLd zn88*e4?&aim-~&D#?)QMQ$l6qrW#A)5e3hQzc73B?ll-b9vl$U)_DQ}3(^3Y04BA9 zM$6nMiK!hi`USu=|Ch!8=Z5A$$obo3s+Nvk2o*gLiU+typ9#ju|8{Uakt$h?;3OLO zRYOPu+@_+!lVo5L!yGbMWJrA9@FOH&lGcpw#X*~458r5}7=8$Of-;NgZ%UkC&g>pE z*6Iyp&77)P*q<%{Z@!~7RB=Iy^q%rhq=QvMxsthxBNIHF)+6boZGNkc7EQznH#;Ej z#qZ$A1xKEQCD*}3V_lo+DZLEB@sQf24cF86tun(pFl6a>mfJV;Hg5mYmzEgQBfPX6g z;J+>$PD9UFo|OWw=?Q8AH&%}8FbBXhJx~+Jx|GjLUAd(s37p~M8qqZ{<#|xWbuWKn z#-m<`~ZY;0`0KI%0<;0dNK3hFjo$xJCX)w77v8=7uD|#U?(3va%14RN-~l z2b7z+LpyRpiU9WCiHB}( z)W~(n0oP#4t99Cap+=j?L_D7qSi$LI$oG2H%rMXOupVC>3Bw z0qV6qTuFnHC?Yd)YJ7VYLbPs8A z#Ds5PCSIyk$ku0JV{{zyL2D2J{^y7B@C+OrM(d=V0kztX+!<~8Svd&*5Fpac1hvkk zC_j(_m&Et0@;?aF^f6*$J;VcnqInewmccj;_?=)f!)0QMR6Ez;iR;)$AbitoBQJ_q z3FPx1+-_>F5nAL9-Quo5$};2yM6`(+vIxuoiIeFpkU$K*cD^6~1qDPB77Rma6^aT8 z43JPHjHnPL=|s>a{0~o?mIzS}30FFxb4Km_k22kb`ZjQNa*MeY28`gs5moI!G6}#= zCsmOG#9!l6B0I~q>e+8`ppaQ7PeCJGj(D1Afj1ZgZ}tKTXeW&iqWBG;0B#;8_F7E9 zJ^0uJUurd;H(67yP=A0y00>nF9rCCPE5b?#)I}AR>_eup|(t&o5s1F`N))@$-0W(#valg3s`zYUKg&1SiP>3GYu*gwKICBBTL9Z6e zWILd%F-%v3o!$U!FTlgzc(!^ru9^TJ{9^*1YZ5UkYT76H%KHskC4d7XYKcf*$ASSV z$W~o5CK(Z%Wkm1n6<5w|t8$bBlkj8+`RO)A58)Ko-~od_rtZN>oe`sgAf+P3`v@xv z2pt2cVjA(x0>1e_k9T-MA_{aW!(y2WSgA!M2U%8XYi)IDJ zgx-0kQMpF+2uo0_C=WX3w|)_$L^Ww24Zm7};xZ~G%^{M^ z2o@&-8b*)SrXdhQgQyYNhGy_r8K){4lgSSb3DBFcWBL>TbUz)Oylm#w5FP&pDb9ix zlsNyOva17pnrjW2kw3Zt3@}|4f!aC(=e#Z~3u0Y3rRj*Zz5sR84JAG_0U`jv3@6hL zQj#$aMQfApXx!DmVqivEpHjN;jHj>LzfU?246U8%DQ&z$^gO;R7VJO_G^SYLUYlc^d92?L=6>dO~QXO01dLFo) zZg?ozEnHBpomMn*k-OD4dnGfAxbqU(&ItPDI(AUprDG*iB1#<1-W{%G9NiW(1?;^?D&LCC`cl0KBGe$B0;Dv z`5ci1hwuWLC13|&vzP^aCp_se|4V-jIG_}$gPfrYHKbjPhSgsnj7H&hvfi?f#ugD} zE|+OGzu+I#)M-KY0k&v_w%|~{B1*6o-8iJn1qh6g0H#+*ugLr-FVYp^2Tk=dV1i7> z#1*4sbq5S2V+tQH2Ce4!P({3lNYbF=JN3n#%UUAg{2ug~drOpbg2EWnILMIB?gdO>VFpN&S>AG)nQAeeQMfUhY-5d)G-Qvm{2!>dG}V7LGW1`HfTf{tfMNF)D2(p8K=u08|b z@G%@9GAQ}D}&Fk@?F9D3mj%#MM8CMps05#1uniRfKRPZEtzbD9y z@pcxxI*Ss46uLo_Xci|7{f2i7(G;{E?s9FDSUBT&Jz4~g*Imhgm^m(WC*SB=gxCmh z@pd7TqFIBvF=ME6loz^2Z`uA-OIS_TpYQ>uoZb2?e3=vFTf-B@*{B1)YKf}f5m(dIS)10yXfUN^e&Cmfz#+Shz z%ZySbPE|Cu|Q za$LSCr^n(T)Lz>WlDGzXxI&`}j248XTkXkUt*^HXD%_ zrMgu}tKO?BHCr}9Fr%YH+Neus5kqu_G&Swy5dc^%4zrHF7mz8)6cz{?02B*JM@ z9suD46}SGsY{D&!2JRrs4neEjA~Id&jAvV^hHWfd2Z^hWXyOII8+bjugXCj|;|Llj z142SNxd^m&fj^j$VJVc52<0T$@^G(gn(vAj4-7#l&88KM8y4gR1POReB!Ie#-X6L@ zAV8ke$6<9V$iF+P;}bU<`vHER;j85jItZZKj@EmS6_7q8RP(!1tu zt{2b@sGzW}6TO0D$cp;Hw5@T1tx!G!Wc-W8v;Dw|3?J@^*OY251SC)XADtBDeqbLw zGJPJHuel=1Yv>-9(uHtKA)^y8qGXti!Y2xBsLn_`PFKO>F(@+tpr%MO$asIWqBqR? zGo3Ub9P#6ep@i&l0apqHuDUFh=Cz?yLh<}h>)GuZS!`VbBrU6KC9l?R& zLs&eDgW+m6hKX3=*%(-WD%(S{4{{OP6LizGi{_mx>8!A=T$ipG5{!d1E5lnG9&6woe1#0 z2d(k{WD3S7q=wtEc$g866`sKZ+#5)tcZ+?IRE!v&fOi5D!CdnkX;5NVQGG%}Q}teW z-vI$g)P!UpKp18WNMN!sa@=O=H@^-cSs={OOZ07bA*+^}5k>K5dh&Y&r9 zTWsKG^}eMQpiZWP0nLf%V(2^Ra2XXl`4kpz5tfrrfO2Nc&44>YJU~{QOqDOX21Xm^HP0#yFsozLPsY#VGaSxuJ!K1VJ%}fg7X2uuj(43}msR+T@cj&JF2N^ChG_JwXG6F=$lX}BbGZ+UTTr%fw@OfyC z*Ra9>5Jo6(r@3%lG}H2CgL$4FxCYC{*NU+t#^>Q0YGXF&&-^zd0B>O;js|e1xRzQ_ zn7cZmCZ&vQ9idvAfQt^41&tQTI|DPM6tB!cg==#nBpF~+7v<_TlX}K&<$DIpH5kBY zEwTR3HQeOC!UOWVJ|)+TgZ8CUn1;lKl2!xe!Cl1WIk3D=>0=Ny^;Kw}#miW~;2b7cgeA8Vc-hlmEcuqq%cAP<*A zmg1Udk?)=Rvyg)S8GtSx*WrD5fP{@AY8+|W-fQQ{8ejuOMBCV39As{q+cRZ^<)=Y% zT)f!t2aZq#`~9HB<>Lka&^|6~kK2qTC6ZMA#{1udGRdtE?6z96KI0e)vdi&i9sLTj zLmBl#*Mg=y1+a(3+_j+S-&Qf`{A)qmtHI|}3u+1Y;A=r6j0^OUV?ZwA<&-OO&lNEm z;e(QICL_*phXn+&l?(UH9U14ku0JfPhPFn*FoPbB#zrs7@41GjVQH--A`kOIH0@ur z7?c+cx6IWJS1l;xS1SdjiIUI=pi&UsVX6g=L(>iDQ~)%y7Ca=JD)#@28xF;wbQpAB7zqF{ z>a^pbss&~JS3qgPmAYGU@37T^Mx+k#`1Rk_%5;o(8feSJlU}Q7q=XdChmjMJ1i|+U z--9#b2276wK}9;#hxkd-xp}~Wg@{OHZhlz(L$_Ek9V{$5UTh4Ie!C5%UM(yYuFVmk zqV;Rh_5nbHNnAKnv@&IoZ)ePsibuU&I>Au-1J{^*mPwf>#Qwk(fyv-78W|2Lo6eF{vDUL5!@fRrsr8h9jXVhUKI=Pb}hEQ2amr9$3)7WHDe6=emch7L@hRFk8rc z=`x4KsEQ8;NdH#V3xkqYG-Vcxm7xiS$|NOzj$!qCs0EE|0PhyhkuZ7qYeA!Qoj#He z`(di3K0blY;V{&Ky8a?oKoo-pRJyJUBVni&Yc^OA4H$AbYC@xAquxbR3mSoX7%$dr zBvasWwIP{*iOo-1!)f9n6ds^LQLTYTklbL^3@FVnIun4(!W{cOs$4~dhos_4+kv|Y z!SsimipjJ8W&EJ>Z&YKFNqs<6V-)DZq@k<@rT!u4cp=3FH?`61P{2}S6i9+fCQnLm z#t1bY&U(McT2SS`iL#Iuvte{Rao=#%g0lXjRSfE4BT;U|>fb;k0R$Lmi%x(i$cyqE z(H)Dcf~35sT2S_XPq;bcfasu^`P2*}1-jL8y{TqGG6M8PSqsYipE^Fx&lw!b5yXvX zJbgWlQ=2&sR#Nd?M75x-e?q|~#h_$(NIn=uTobp$0f;Z2J;;- zwlKAzQF}2cvoyr7m_2+HyaMv{#;IZ=?ilI^QkZDOJJBmfWZzRQXoRZmbTH=f0ZB5D zEQmdxpqykz!!*+_?aXOLhinQkx&}hW3Gocsl~}P6rp4cz@W!M&yThLpl~wEhzq9!ffK1z>5(MXDz7f-}QO4=&&B_hi(qZguwL% zqUL?f0ujH&-}t2VQI8gY-%~AUtNw%OBxm_Zl8_opfC?SO-3`%i$3O6ok-wQR1CI0w z#@l?_pfm)Zxa;skxDU?K@16C87l{v?Bu(xjvg!9`RdIN*{~ZsQ2TXoKt~bo>28|Hz zVoH;)AOK>BuRv;|?fWJu;rj*Gf=2j$I23{UcHBne5F#P(rdONaBlanPBq~tX@39s% zB5Y2s5IL7kw=#>mT1$p%LD3$bgu|4{K-MWi4o%{HJ_Bgn!|r zAT6r7_@^oV7K4T<6tjQn4?0g@RcCyNYC$6>jFav#Fm%)(!t`5KdpJYrv(BENCH-u` zW^^zw09_)GDyM;JL0SJN{nJibf0=ADQ$aT@yCLFBvS3)jMOq80{D<_~dv3S|`oer_ z(tiOj&8_R(afSwMSwkEP9S|(0F~tIEL9PF27J~+AnoJtLT2R)X45CVP(F2xZZ(#W+ z?T)|^T;g`kO_)Vl3rhZreEfUSU;)5)T?^{|e`)?No<7$a80Yi1$yD!3MGt_vxp4{q z!*F(}@#%bpY`~D$g6jVo^TgUr_?#qkWz+D~f<`WlZ$k@_PRQ#cmPzIl52xda@N^4Y z-VMM9H5-;%J4ws*guk5|Mpw(kPQ1Ik!)adIOXl3=|Iiq!)~P}tbF$d5itFHb;em6l zXSe8UK_g*Fi$TK_G}pse3mWPlb@&;p7#QwaP}YBw>%%X%7<2?`K_e!Uo5i5gnFRv+ zxGoTg!-k=)1!exnI@PcW2pYJq#o0iMkgRosJHnzQy<&Y1Gz4T~DvoEYWeXGTuJQ}X zI@f`rv`Eru_Xbr93jdQZ3=0T&jb(`dphm#&hwY&Y`5{6iC4sw(z7~}H2a9G1(TLYT z{;=1Ay8fLBK{6T6`#SM3ytSZFdoifRbi-T=8uFiJYRLPVWI_3m=^4ISQ0gyR#h@cl z3+nnuQVdM4u%;b>a@lKG-AF=)c>z6s1j+?ELsJVHwHJdDMgWNG6o#!9G$P(_7K18) z7)}!-P8tkHEhza9Q_f~oVB2=e{U6ohLlUNT_l|0M3>zq>$$krBFE|RMOX{U`XBOyNik@Uac91UuNIX3 z7g%^l3Ld9}!MXBhSZhJaKapZiia|%978L#8Dh3^aT2S*J&0FeJyCzUJS}YQ)a7iKgl}}Q!Oa<7ufiu7<2?` zLCODFfa?67i)=|fg?0Ia{=-uX8nqXL;tR6^5oP3ROqLq}80jfNLNg9cEvWXVSq!QZ zHq5o4tbfGgO^QL`gyF6QW&K;L7}N>KVXg&r{bREj6xY`|o#CtnCI8hb1|5M~Q0yA1BTx$(wHJdD^9*+_DES{(wmAvYgUNAya9H6BBMwU~ zDEmJf`^Uwg%mfa1EhzpMgC+#A7<2?`L8-sQ>LkUWIH1E_3mWP1rc*8>Pzy@_%W68S z*~spaRvYG8P|m+G(abDhNH|y}#WF-v5oG=_#zS5U8uouc^EtascAE?%OD2*v-NR7} z%KSsC7<2?`LBsm*u*52qQsVIpZ!M_jZ#RoUN1zte{cq=6f*DfGgaTxSS?7XpxM*uZ zIsb^oB`&>K5FoCCBUMd`6cXt*lPz*l8mnWRu5$v-q7|3n5!14aAI$O_wuYzOC5`Xr zu9kKF&-K5d7!>YjYZNlT^piB~nvbEZ1&v(al@^1#P+*vAK{@}Ad99=v)af$(;o+TItO064<73wQ*Mg>pD0d#t z+j)v zb*@ozSoPTPlf6t%XdXMEZ$?jJhIf+VcE?U>bkFiSK7@Vr*sVC`wK09x%*;3&+j@Lv zT-YSV?J-pSh!5^GwPtzt_+pLsxLtng9>>HaT;I&D-7UHR^kdoA_k3ypF<(}{0xw2EH`G5T=+y5|qa zqnD&!wML(wSDTE!lAc_@h(>=zS2VxTm+HA5mT=dtPs>&F*&O@c6l>KS6$ssVc8Kaf zdTE)`e%=+rfyl{Yqod6?A3eHx25z8s%zOi#dfmF$e*Se}iTI@f3Y-^E-yF4d-d?#p zAMo1*uOIbWC-}A?9q3aV^by+uBv>1;K6?S+x1g`2NB*i0I$3Y)#!N*gJ-Ip+rK+M7 zb?{sB{O%x})s!C>4Y!RKhLoYFCUO^z-SNQb2X_ixPp!9ya+h3htq0$Z#R(18q<2@k zd$WyhywE~FwyVrr=#r7wQn5+IsFqaMBWO{gXKIO(jnz^u)k=$QN8a_?thbN9TjqI z(bK%vpXTi(k3JbI+Im;L(WrE})l?rMqbP=a+(*j1Jk^nz(i+z8u?29kvoq0(mdCX<{Ty8X;4{bmEMu$xk4t2A6R5^PVltgkvDp~ zv$b3=1%2I7rK`J}4cF?0u3V12{d&78*qlqlwOgi%;e31i<)ojs^X-Y=lAC+((sfX9 zmhLGVJk`JY7z|3TEDiE9qA$it{Oo+q?_*ZKQ$NH_B%Cn86{= zWGYy@O#E2nm)K>Y>6EQWGy0A=48uG%Gj`gH`t+HH)n_!OcOS!@t+=&ygcpNKAJ|xe0`1JEHVM zLZj5k@k~PF)E-2cR7>+AL2|0SGQ~up@F50_4+%xp9ig*S5cw+MgJQ~TtY}T1Nti9e zD#!Q^yM4t^(|^isWP6jx3u$jDv#~c;`c=Z3$ycScol^R6DSbF) zI@VvLqfWVw{95vU5`1L{jMF=5)2XPlNGJI!Wk^!!>7Df9fjBehfxfQjpwavAxc~QO z&4e=rX3=n__=$OD?0TdM>2mCz(O!Ggb8SpE{Kv;~wfTAa5r-U-%1Hu@WQd!n=Ekse zm;Y2?r7^6a3ZKcKHSg!MwJ}r$Z#Eq!<`!~!JnQBE#y_k6ms!9 z?Pw|0s$;6j{e6-DX7m>XIl)V_x8^uoBTiJ8PXfJzS}qy)p{3=2#eRB&pCY>X)-_uJ7D?Dha?iRq91X;P-+twAZ5*ym5N*NqY6 z6}gvmIX1afnrQ<1F!(4C=%{j$SRU_VPCZ>{m zyxB}W#kVjpxn|NP+#(rC2+#pVo^acL0b_Cdd0aaGwcmmz^UXkUyGUf@3EKSE@fdG5 zW2d-FJXR9P9Zz9W^Ql=)x-3*PoZ!>(6rLHA9G$m#NAoeLuwd@}Q|Y7g2+ZB(FDpZ+%SG zUp6)Ge0}fOOKUZ;@NO#eoUJkFy6DkG{XO0Jx^%JeZvpWxyq^#Owpw8QSAUa;MH|Rd zX!^;;kf&%hF}G1&piK=Sr!55g(?3TSXUWqDGoGoV^djMuc;bv?K6B*ptxiq}QEX`2 z03@AWOS1fF(<#GFh|I$k6PGHPlnZSS=uU*dctLHaipn_*YJKMdJITDnMm0*K54M9& zxFZBk1H+vJ)Co$Ox;jQlJlViPnE|oV?n$&^B?^-t30hKfA0{DMQr+47fbM5d9Vudx ztV_Cwa7WgqsRWV?i}6o+V2E4Ng3}wLk%!YH1)q}0pHH!7@!Pm5vV=)exjj~kvlv3z z3i+eXbd+DxO1rxfXZPP-Hi~0E+K9TdW9`SYiT_)0d@*FWBV*X}l#L+CvQ@KV%1l5& zX?&7e+x=qme!IUIbaOJmrdn&cmaR|Cmm-E+Y_~vddqWSn9DowRn7`7617UhStQy!Q z&iRfDoA_B#N7B{>TdXGGKk7@YEpFRiIqZML`iCLiVA+a7mNRz6oAg>=2?zq@hTNS0 zvICYn z)j0o<0%Yo5T=&FEc&Z-h&J!)DgJHV>rur>OVTBkA%UBg|iL&)F?h@qUv#?1gEm)B4 z$uaH)Wg%nu-aRzjzQ`42?z0lNgS>_PR>MgVtUq(@r&~_A63(4p<1;K+)0S+;mu#pU z8b}%fTrfttN^3*qdF*ct0Br8dnv1YTjvjDCDI0ggr?9`=_5(AZUBY%fOh9<9Q<$Bt!%>=+wg({Q|dDCmy?iw6*%2XJd=c$OL( zNaew%08G1{{Un2P{$4lqi1)S)yZVLwj$Pl#If!)3UpL{m8`Ch59{DQ%!%rb2U_6#7r8Ys32SR`il_kzD?EXi#W(!N zrkeqfbjRj|i(`}uAS3oYl*=HcAZ&Mn46)NvAqu-01a3}H3Ub1hlyE7$ptzwZ*p`6h zcpg09ZjqELvgbjSZ)9f9G(w>$ntVs`_CQE*@Z<7EH+T%Xu*~@ns>Jvd3%~Np z!;->M%s^X~l){)C9}3+F&!`SXF5QNZJt5h=O9%jqZHCyogcF2dOu=HqKDICPd!LgB z*gGs%aYP90EDE!^SC!>hi!PUKTz>XMjqk48q zVV6kP400Y2t2o1!B`7cV&2s}r&P{*=JaEocc1V}I2})x10PRp|=lNf7H=wuzT<8_A z)*(D$uUnakB5g+CL1r!mX0#r=#OZH5A65mUz}YtzvH-Z}LGTdhb1{3^T{uWb*jt-z zo|6G_j*91BweQ~F>uO%whD8~@8*qdT_u@sV7%I;L@tg=q5Vo!I4uj>lhn2V=Eao&k z*Qmc^*-4_ZyGYVaAlv3A=?!%6czja~D9FGCYakdG-+!uiwCU6;Xf? ziTtN3u7VAO*wafmvW+c%F%O(~=^bjzJib_bqJ};Hhh0vC$A(+sQ8PBlGLUfknG6T~ z?v2PD@ZgD`?icO;*zS|$Z>vW*h$(PCnt&6uIRzNQgLMr0(h7RUh2Z0cF;Y$%t%B`VmoNTCo9piYUrfE^zBI&xk~0Q0m8awAOzx z8jYPcnH9Uc@1I$XhapBl!tclup$o83uk-M98Bhk#~fh;XL#(6 zO!|sE?2w5$y zl>jb)W8ChU!Ci#7n|q$81g|h;&WWTwP85V!w2+*N&3HIFP)-ILImYkqKe8Dpa|fW{ z4Fd`W2DTIr9GinGa9>2s6d)=FaD4C+K&13U@p$w!r`=<6JUtlwjYEsYfHgGHoc#&k z)3rysFf70fn#cV=-0wr~){4Q)c>AG2{1?@MyFxiq9{1Qm_5=qYkDzmxBcd6MsIq}R z!<=Alp{<(|W7HvzB8t(%IJuVugoFB?s;1J0j_`kA61917I6X2EHb5R=Cy$Wz;AovL zHKN;;poX9@0*C5>+i8J#LB56roNmfsQCF^#6)>5Y(SjjPb>T_|^*CaQ1CTx7l=@RN z6BdBJ&}q0BJc;bX!)~%HIPP5raIhzO0)q=@F?x3W1y8uCAXi_2Etd81Fuoqci*!RC z&WJ*#c1uDmixyLAP-P5ZEc9YSO6(sv;>6Z)<`-*v3AH@i8XcvrgGt=Y8ud#Ygr-9+ z5zhs!E+=@G1!8U`<#7aEn#Kmr(L8K`qr{v5pJ8j%5<*yR zIDQwSz%Qad*e;c$Tkr^{`=PoOAE*Q}07G(6MldD*A4~+)3YIdDY^(p>q}tP;J(}91 zm~g)W8DLoy6#Hu+tf*joOd70E|K;8%@HJXV)2UZVbV7HE8;S|Y-Hcsz%b}76*}Mm; zp+9C8GvyiR;bC}E8i3XLqD3b=oE+xXeb_^kFd9Jz7lNW;-G2ZZMZ@3{^o3oI z;&4mPoyT++K@|YdVX%rG!|LAnY|*Cc#rJx>Zs6e=^42KeUZLk>x0DeBU@(wK1Uizw zlos_0`d`b4n8`N0VgWuCV5o+0{u7NOKERIyi4?2lbsA7Ra0VEISLAd+XQ&?9tHPvU zjSewmkVgfGy<`M#>fi>3#?KgL_y$YL`8$ly_zjPaQ2;(9z;>gbL>}%3&zZBq&u7#& zC?_!Ppby0hvWzCx>vt1BN2*F=gj}JFaLi<7#!C-wZGSlx~V)yuG#tSz2P{Q8>Eea*d7#*iyN|p2&j4gBHOIizS9cld2~p zrQqV^A}qFvF${|)S83ylGkfX4BY9ycz%ls5y`WosJxmP@iWDH@jGgo0Lokhx2ToKw zo|Pyzv8Os5=u_Ah?$A{U=jAa(txJPm@PE+ne5s}9FV2EwQ0H^_uAULi(ojG!dKHX_ z6Q=t_XfP(|dn6wDpcO`iDfZA_jky^UG#wlf&U%Mt`bM4ttujX;Q-1?QQci`7Q%P?+ zjUJQ*V!2a+^?)8t5FE{L-_yVwbcZPluB53aSP}FyX8_x2c+D9gRd5hMV%qdEa8AH2 zJcFyFPoV_h|EQmEERk3=8K_sEdA2!Sb05^hyNCT78B%jtd~Bp)nPEIK zPM3gyzlJkY?nYqY4Spk*XbeE+GYrXR^$+q#0FD0A0I3%AOtoQdJO#U}njnyFs8M_1 z9_|=w=%O1nBE=90iUYU@mLZuu$7#xtRU&B%3|OEl?5A}#A!Unoq>S@_WE}lhy>uX< zYgh{^hmC2VEEI!&!HI&u%@3k-Wgygsr+}D@lJ(d<2#kptj_APJ<3++vSUy^JKOIam zcRYjv5I`}`X{kW|3$$_KJ&5I?UA!6qCAM)_G`3)XPLwBdk3o9PXSIWeF~}!6+5#m1 z`rGOZFbqQ)Q{q44=X$!55dwKsWJYuB7M9q6;1#j|k&Y#*6wXD+AVNqF(VTv*TaqaU zVW)q3Fn%-n0zjxI(90--Mbl4$C~yz|Pz~q{IES%owvna~TakJbQdKpe0&l6#{y!RM z9%278U~=6T@N!eD5S6_4=5`voXKhA65lp49+@hdT6!PM-NAv*>#3%Oabg%(%rU#v@cg z9SC)2nBYia1;7QY>j8aGnz4W!^rH@|CY>F1;CCoZca%1WGkC)UmdcqFhCUSUu>Tsi zk2lfQ{MEdO4L2e|R6x5*MvX-t80IuWQi<{|sC4ffH|bXpF+k#w&3x4H#>rR=%$I(m z2B#Mf3kF9HXpMe!*qr}{Q8Wl-DBx7|KF|V50E)Ijno3_(VwylQQL#xizJc}R72iNr zb^)%96Wd@zix^-JUT2uvet-a7+C+%RDkwnkm`a=-@-H9+S;oquY$YhX4fD|jqrjmI z=RbfXUHUBJV^E+RJb-2bj$l32jZz)6%(ywG7eR9aN4beNWotxQY>8jOkU$6$7WiKx zRG!s~3w{tf4et%uL126mYj7WTJw|8mt~w-;whyHGiOc#p8^R(?NuTa zu4WRRH;qWzNm&3GJ$Hy7Sc}#n28x1)c?rrICy}sX*zBG_)5OQYs1Qff65b=Q`_IEY zATvK`622jJAPR;m@SGw{5mki)%82L`QUq^``&mX4yhKQ${zLh~DKZa$0v79!@zfZtnGKnO4H)PSfx;0b z7WP|4%H#>0AT#O=%9uKjCM}>nI54QxiGy2gT;O@uTo*G%C}&8kp$9tv1HqL>uuJ@d zkOg^Fe&JP+N!OlgkN2aS4O6)vxQA?MwNRyHbzBeEL8^HhZxA&MF=!I_LU+(u1tYlN zAB_kx;SVs(BhZ7Vz+W%b2Law@fPpEc@R}i!obY48|W=Q;47z~)c9`b#BRC(NA!qhz@k7H)R$t(cao71 z4R*x;LFNripa@TcZ-Y&;lrkmW4GBp&-;ex-#@#%I>xo_?Dil{r2zSI1P4K>c4ZfXL zz-SVq&1yU@Jvkk3P`8s|;TQ2Nk|h0MVkjy~!kK@J2KEAalVw;z8w^At&jB}}1Nj|X zXuieW;#$k#f!(I2AP2pPLA(dN1~XrZEStIm4Y?G)K|9iy<_-T3j@LpnDBK9d1Fq3c z!Z}a?(dQ6RBWv;n6$>()PB3AqPE)*kFfyK<1^08uT&h_ga@?W>dBNx9b)sVdL1h6U zxB&v~A3+sVG*O5mEf7>PYGCm(q*$yPXvzaP8@d$$V_=S%;#;BuFzIxHdr0Hc27j3M zp|N9D0i(7-HN?~dfRT(UOiJS;V}B$r7Ku2Iza&)P2gZrNNhT2e3l?Y%6k{}i7{v@@ z0!EM!{R9An2h?XWDg!10&#? z$uJHCCQuJbqx)bak!*^Vkq7^nG%R2m@k1)^n>U0H^@j-LVMpH+k#MEzzPOWnLR=np{hr7gh>S_=mTIt zEI>8#4gtM~2=WOg)C&p<*#sN|22}E&dgFL&Xg{KJ#0fVE6st}FCwT9yfq-j`2T&^j zq{Oht0u6g!Jjz7Ri9ke?4dXE)gEko>0)8|BCT zD~JR}*$+n(M9Q3+We=bx2}Acf>PW27RJaS1&^-#sR_p}35ymS|D7yLq#=VfiU`DqS zClt_8dz}Bm2%rqG(hkC+O-E}(KSK(GF>_$3SHAIFaXr)ZSr3_+-7*-cfJBGG=Y{<68KHz!t1)n+A;Va?tm5f zq(7&s7FazYxCYim1@W*s!Z4o%&vPGe`I7D#2yftDgCn&;L;E8i(;SHAEt)ejQ_OgP z;yNG=xqZESR54OuY~Z-{ZLVmD^Qb=PSDqM0E8E3g#&gxKkt>x!|q z2H7(1VXoA-;S$JX3-pBkC!43gP6JM$AT*30z>s@Sgf%Ef-NMB^`UEi|127UyyROou zQ>3_;ww$2HopTtt8sx$F1mS5B9U?}M69C~`bRa#Df5iO+CZ`Vx7Pb2tAjOS_(Yfe8 z28!3>Nx&cws)0N>1} zVmkyxr3tcV*AzO=w%FJ2WX$xdw+=U;fD}o_qwJ%lcovPXaZxTH8PJ4%SlFn+d$fZ5 zgF!|DrcB@9AM)7%g*)ay$!+PQ;36=Y$S4>otfedfJyb=Opp=OOJ!SLQdW)zbxL9sB zrKo@6q^m8No_13oGeC?RfOXVhSn=!VB^BbLmx2Ca9I%3y3KCL*?~9HH&7cfQqpGn~ zM*A?2niZvL8ck{88I&SWsEI;JEe zcps&LlM)h?s5V=3TQj(oH{mBh!`P$d^UnX$s(8oR8<{76&RY zA>$2z;=`i4P)4X2r3|%;T4Chah%kYtydXl(3PLg{qFZi^AkUyAiVg7}A`xDej?7qj z5Dg>tM~`1u%DF+52TnSQL3a~$>yDc(G(D~P5$*g7Hqd8G>N+^s0EL3Nfp-8~5!I>& zZ3F)sbOi4o+QIWY&mCYUu^xCIBSbf7E%X@z$~a9U>*e6Zpz8?1=^pnBNCPdta1hNkHfMm2#GMjNIiXoqlyxT+hBI!g)y`KuEGwgT2v z>DYfB7be6gOq(LHh9uf87ong^U}uC~AmCch3dVDXIq~2apjjOKIgKpr7mo^kXObTG z-0{ORvNXTV2CM)0-cVnEoBWHA0V#rHQb1j=5W9K>s|cN(oOS})+=uH{obh$`5@F&S zT9vtY0{|E?B*VnWoDPg9Rk{$BoNpkQr!Ao+kq?s-tp8Jin8Rg6N-mIsM#B#}4pT2Y z2I92ibdD9&-Jx+^GJP93fxmD8-30%J0ogf2;Xdrf_s|X;g-{kf!!RH*aT5cMobhI~ z=q|znkcfzhvcMdC2U|gO=!HwpQ!YR-KW1R1+p1LS`D+(z&is7>? z^IrrU1Q)Dqv6z3Zh<6CZwD30Z6C8R%{Iho4FaMksU0?=tQx) z-x&hv45z`ue2Qn7RO$GZTq8ov|3L^R1AW2SetmQvP1Fn|3CdWWpkb*V(;snk?$3M( z>jTshTstuZ$<$tT3@W%Ia^f0RL{+Jv$&YYNl3q=~ zwO%s*iSIiA;{RYcMHgpZa09TRlXPPmk!xKbd?Pw=Z%m~FrD+BL&8)PjsLd2{tv*By z)$Yi!HdDn1aFqb#k+H&#b7UUGR^5(O_f0&SIpnOGaFf zL7^EzNGKS_Ef`aZcOantu}~U=FA{%Iq8csTnxM5a0VruJBiVz`{kyTM#BSO)W$K=F zVBd5KU8x7&ZOV3h@b@@G2ao({3^d%i>vP5rZn$%emiT|c z!!>x9~`*i>x(Ez&nPU%Qs8LJL_-K;-6$^jsDaP1!E~XlFi26<#6X3 zt>qhW)JH> z`!RDUV2is+H_}P>1IdIcQeNp`mr%AAzkqW;rdCY=XakHqP;(c94#3*>|3EvmNznjo z7xto>7hve5u!~H`P}})5RDaD?Jjebsprpk|@1^=H)ZE7KwJN;^`qTy^{GV>WBKIx0 z`pY3%gPc71-m1Tde^9S3kjIH4bRy-HxExwOr<`z{2vE`j8Y=@_(S?GV+ao}A5!jjm z(e}l@fVvJlVo(m6NyF&GwyMACtpCDNVTW;xMxgquQbYfdsvD{5uR_W5zd;dG41^lN z+48SW8HS+O_$NjkBnk-${{k);Bb0W6KDeAFjd;uXT~jl}7DjzIWzHdmWeRvML=yyY zxLX7mw2yt1)nC+qwHudVul{2F6UZm+L4ZAAh!O6a9U|oh z>lyWhASK*aMI$Qk%e$@q3hO_duSp|)bu?xerA$1CJs`9~$V^u_sZNqXvGcD1HyIDi zuptA{BiRJLH67!E4W1`}TVVB<@vkcqw9)DKNgL8No$@=4(oD1^-y47$S|`^)fT4M} z)n9S^hpS*>iGh%>bOa{ehNb$8`KMr9MymRY{Ktq^f0aw*znSeJ0p;8T|Ax2vi~OGp zDPSOzPFesmo6HEozQa=eW&N9L{#{JL2j}XLCejmhKOEIxT@~g(%>0Z{^_TPiBU$}L z{e^QoBUb%|{F$Lr*^;LsY34d0h!G}_a6X2*`iu3iCeV}swf?YIe_{VHVIx=l74n}X zC`ku0?TiT?=ISr&|DBH_HOWN5@K=A~|Bh(&SJ?l+i8nKfI2&daNt6@y4^Q=1wH%FT z^%wDPSZp#<)nC;A;n*=DK0?)B*8e8?8s{{L{)V^uE3SXXxg4SDFV??04FV3rdSwz= za6bY0a8-Zd|1cAS{U4#~FYDh&vib}E$N3%7g9K7cK7o3EqmYNG`m5N@`b&L2C(H=M zO))q(MM#EOX3m;9ZKoxf;lnz>9DMI&`l71-V*LfVJR-Xhs{SJXMfPLls=ugz8qw;n zvig@9R2H0!Nc9)-KMQND%V5m}6VXh(5>KNKG{?hG{pI}cNLGIpEBN0dSp8L@{*7?Y zC73LU8UE@o>R(2*`V0J{*+#DVD+~XlSVye-3;d60^;eesH_pe1RezcP9LeghT!H#8 zR}GI)^_TOXu9$P3#Yj|th4t4XSpCKNcSb&P)nDv?Frw98(w}^vSsxOR!(aV{|2Lx5 zU#>s0z$UEQWU<+BSAP|mf5zn*vFfif^S>im{Z(N91!lNMton=kCsyN)T=f_E&k?Qu zV*g*3pN(Ag7x}Ldt^U&fjAZo}`L7YJ{v!Wj#?yJPP>(U()nDWvN3{Bj{bxtC`V0Dx zX!RHM4C z{$l;-h*p1*|7HGf=0R~i3jB&)xue;m>3uVNSHFR;dMq^iG||79ul$W?za z|23l3Upd!bjbQbc_J1U+znK3V(dsYfzvEi0k*WS_tN$8`>aTDDk_8!-RxmrZHLY>qd89PlLO0`B8decH0446Z>Wy>RbE?&*Zq6 zFrn{g&pb*`#G2BW*>}W@?#4`S#wR*Oq4;O!tQn2^p`(vpY~yK5Pup->cGQHAWH#A; zx25;EXX*pXZM*6AJ8!z-MmycN>xTEf|K2rD+HKX1R$aV4D4B7@|E&6?!kSvAvEwHLR&uWN=&{L>Sj~eb zC-lwe0UbP%9%Hvv-}G6H>9b~L#`*Tv<1^#J&J%9`;qqAPYFWp^mKSXNLj94Itz|xQ zYG~YFI94la9Z@!QR{c;9JP(yM+tz0ukQrO=)4M5EPv`kN` zN@9ewo%9a_Vi}E@|?FSfG}il-~cj^nkE^JS?^C z6taaooh(R>y>a|ndO8{SPd~POY9C}^xa6&Q;32SII*b+`nrFSX@G#erp;0*SEu1-- zHW2WTaCs+5Dye+EM?9o#3?2r3{yIAn^jGwQ)9``x>DabAl#^@;IA|(9I2HZdJb^F} zKYem=>SJNGNAsxNRC`3)f@)h?S<=f5t{Mh2!y@seOLDTD0fk7{a* zwfBQV;6m%)=E=k<1_J`y#CzcoxKR2x@J)({WPF?_Iv=kfuqY4gabORA1qa=a9=mPh z!2J)H6@T7tdIcZjus&Uk%&Zwl=wWQtcSzrisfS_f;EY;&-0{?%>N5}aMttbw=nHe#ItM;_SS zm^7n)3@vVlW7x^n3x(#(SA6J>J9Yf zdTfpLKYRLW`(E;TZt@!!EPHeI=f$&MUh;$Wi=!={-1v}Z|N30z#GggK{_W>h+k5=C zUYYQ@3y-P4{N^1e{p_flGXE2O>4JBT`oM`7-najj_pWutch+9~kz;kj^RI9D*XuXDuKLyrXTN{xt=C(okp29hPg%Ne{7Su_dEm{Zzx>9Ar;NG(iMN;9 z@Y0jkx$h^-zx334_bj*Vs8QX~{%5T>pv(oqN$42IFoQE~LAp|!#)>&THoH?XhkXS4 zwDsuzHcnwbj$jEpIi-CYfKKipRuSu+GX;K+H!Pm6!AeLuWzqz04;U_Y*rBU0IBL(Szg+nz&z!&h z`s@E_%MGVY===IBpSbPRb+=vVklE{>J$b^8ORsz6tX)sKKC^Cg@=E)zwCk)*Pu!%u z$-^rzaooNSZL`f@s~mCd6)!)%Qwwa4fKJ&@# z*MH=y)koF-I_|(j4j6UZK{G~Q{PO!Vqn0Q>@&1p^I41vvXvZDbKe6%64UYcat#>SO zef_5ClOA6E+;?{Q=KZVObM&Z8Zt`Ylj{V#JZMpg4qh7f8+-Tc>kIK9{{-(cQGVTLC z`7=Iz!1nbM&;Iq&_pjP})X5K?dg14%PF?+@Yb<@ll*J~0edbNS`{1Z)pTF_pIk$fB zfJs+=@VeW+I`+d`P5<0;KmW*#IcMLo<=U5>d+suO{eJXyyFI$xk@*uoGp2IrLm#`~ z;@rJIpLx=HSNtt=&W>OH_1C_8px#NW4)!n zwbkvJoiASVLwmjNoLG?BRdC_S?lz+N8Sdy;qF=(TN*> zuK2yF>;7i@7f;*q;m6ip@vw=9+_2*4zny#ens@`{DT7eIHnM{bO>`)+c=CkQ;kHR=MQi z2eV5)w&d0)Z@K*YAKr4=gIB+2i3_)U=(m4-XsLgldfbFjt86=d$>m4g@~9=Qp__xpg_wz@seMV`G-M{{?J9-ZJ{R*qqz8bB$^|I@ocJ|eW zf9kQNUjNy8PyT+n-6uV^*6XW&;ou!Fd*zIWwmWvh&D;IsV~>ArgHL?#5lVyzus+IiBR7u?me&1R#b7hhX# z_0)dT#8Fko=A074R&vO^9^x0*1yLr@&lYe^d4*x#w`6svD>f}$axZ4}we0K4z zJ~R4zPn@ylNgKbo_z9U+XPesr#pUi|8*Yw-znLp?ptbyEpr<#aqv%$o&2F2 zPG0LX%RRi@vL}7)fzdmk^6w||H!eB-ANB>dvJ;Fmc^;_FR9p?Y7A;pIdj*)9dYU&XjMi`R$1}tarpc*W8nPsyJhX zzi<7*x?ehHzo#~Rd9`0m{^WPQu=?dIKfCoqYrQ&Yy$6n-wd6%7-9K@e-!`88#E(W_ zz0K=aocZj5ujG&Y$nz6FeZ~7${mEByEBBu9Nd1L9Gb^8Y)fM;o{CS@_Z_iDm zKb&~|PtLpjtRuRQfB55k_4*TMzOmPTKKb^l z|GN8*b5>pd_%Cnz%X9BsXQvGsXFq-R>D!(6&3(W5jc?9;@sYo;bKA$y{pBXtZScs4 zHb3r{Pwn{fxEZS*QKdCF_M?mXq@ z4Wmze&kVn-2Tejw*UB7ZvM)p8-HlZ@2`4#+Aaqb&)el| zJM6pDAX;)AC z)c20v?fPBL{7&x`D_?cjZu?xd`!|l<@%bAbJ@J!MA0Pjzy+1YSwjGbZ=FM$hU1jwZ zKeWu5pZU=3cjeDqYxVt$m*#Wd+Nf~$FT2MyUg){B`?UkUT-vl$yXeA;uDs~2(q_fC zt}jMEy5#x~UqAXs2mR=*gOB)O_WUKj`15;vukKx|>+!DR|4@G5wLh+Wz%>s|f8d4R zy?uFM=eu{Abotb;&)o8vZ{5HDzBf$&!SwTA*!%I1J$~A;H(q+?pI5xCcI-Z{-2eAi z&wOyrXG&*PzTflM6^Fn6%C5(+@!viFd(4}gy>iX7%dNWV>a$nh_JifsFUdVy=vnQ} z)q11VcVG3=>~p2A)#t4Gg)=^J#?GtX)^qT+|9#|<=RWb;UPs<>?d^wu_1eddzUA=K zUb_4XS8jLmcIW(Zv2#!RR&nZ456*gU^n>LGPbjSS!wr8pr}x#~*YErG(=R+d;ol#6 z`uV3ny2&P+etDDg_bKjm>kl5f_NhHr-un;xZ2p6;J?HHG>l?mv{mu2AZaDJC_h0wt zLyrG-wB+@NU%SGi4?X-p*Kc#^zK3pe!^MYgcK5k=9e2hmU%FmS(ZBm3=Ay!fm6!=JzNwJ-eTxqYAe;qym5`j?{~IBe;oHk@(5;Ya`T zfWO@{^LvNi`sAI5jXr$uryhFlmAfAK!(LC^`inVdE%V?8_cm_Y;o@KIu}5W(Zy$Wh z!Pi`T;XcuK{`9dQ|LKK`UdX-Bcwv=s+pfLX+Fu;E_op^L=H_FcK4$Hg|MRC`-M{g# z4!-7(U)=8ebKigNkvkvzk3;|Z>sSB$!sE~X{I%^~edOr}pFjAyU%h(utH=L!|9{;5 z`k(&q^~e75=oSw~m%a1PQE!bp@F%}Lak-N&J!zT6cUhu7{)+J%j9=@gtN-)^KmGkr zzcgXT3Hb^C*y_z_t9>Ut^SMh$ulKoaW?%iGTYjB;;>GF_m%M%0k6&Ek$^F-Rc7?-N z`27mItb6FXSDv%!t_Msz`k|YzTI$?ifBcIduWWP50Xx5P^RuVr&d>d@`-F=pT>9a{ zmA&`=aL>~oo^$Q5@zLyx@&>Hrnvp zKC0;JSxZg|v zePgBn?03?SFaMtx$34E;ZR7sC&8bJ-b@LhfU2)A1rab)d(JSuu#Xt9)+L-?9|D3(j z*RJ2};0-t1=&dc!nzHjj(fzq=Pp$6o+43K=KhFGkYS+daoj>b0uU&H62}j;hoN)6F zr{2?h&qe<(KDo|Yr$4s++voo0@Y{DeVW+q6u6<$Vva4LR%IBB){(aY9c*~3z{{6GF zfA;C8pZxdH|9~DAT4mj39$fi`P4oNy=#X81^VbJ{aoqV2oZqwQ zoZIfX{i<7^z3j^EcG&0q)uK)Ix%Iif?{dW!zhC9MEAH^czOk?Geftqh+&S{l++oalk6y6*`6q1jxAXRW^N}Z?`Oamh?SADo|Jd{Sn~(j~>MyVU(nb3m zeEc6@*l3TjV>gLz9&^yX-Aml^^>6<5vR8ljhfjZR*7qN~`J@}h-+sj{pSbQj$9&_F zyGm=^F>RUe_Wj}g53Tv|H}5#`|Dta#ck!HyZ@J-(Ll1oTpdVa6>z~gY`t?JPy5Y3x zec!wOs;eIx|L1e=_}e4*-}$xY@3`@uDL>w!Z;!u!|C(EVamy{Q-}ypqpJ#U8Cm*}~nqT?oXXhOJk86Is>^5s2wAphf z-1y|@=bW?CqzfnQu=+W7-E`)Eclz14q8FZi>goHtpS=6gy>H$7thdWg{`m2$@4C8l z_w{F8v-$YVu6^##2Y&gpbKm#B>;LbYJMVwV36GY)`~2G%etZAvmwau;4S)NeJ8xR% znXT@>?7-Liwm9aeZ%z2;Hoy7e-D7V0^g%a2ckE3so&KxyzH!Gb|2g4DKfnGPFMa1v zzkmARwg0&HCqI6~rO&?n{3nn9)U$8SS>uv5E{OVezWtG7Hh%ICU;ER8eMkM}$-f+P zc;&4ZEr4J?>QJNf0w&)5HNz5h4lLx6AL|+#~tyuzVPC+$KU_fPnX-^qepD< z!knkx{`P-o{_dG)PMLZ8+fD!7u(Nd*~Z~+3|{#KKRZq6DHoi$AtfkUHk25y-z-|+Tz8ZoLW6~sY@4s zBJ;hKe~|yo24_9-(g~xEn6diRlP9dW*UjgCWAWvqn^#vdS}e2|LtAwosWF@(L1+zw*2vvZ{Bl>FMi>*yI=n3b)T5|(Sf7u zG|kc$(JUZqx5uIN{msSAGi}#DztNGk;~bGVzno^9*o!;9u~2D{g~-~)aeSHyNMiPR zMQu@>s~Li@ZE#UrG-P4h;G(u@$ilY4MQzcLg>8e2+M*!~+Xff4MMD<04K8YnhAeCw zT+|i~S=ctXs4W_@ux)TrTQp>0+u)+MXvo60!9{J+kcDl7i`t?g3)==4wM9b~whb<7 zi-s(08(h>D4O!SWxTq}}vaoG%QCl=*VcX!MwrI%0w!uYh(U660gNxdtAq(3E7qvx0 z7Pbv8YKw*}Y#UtE77bb0Hn^xQ8nUo$a8X+{sIV=bpXwLVEFf$fT-27E$6^akQ0X7G z4e=s@I8aO`emkaR;}rny)3Q`553sr>zbNKq2Qw}E|Ju73Fx}FsKJbkbL663OArV@^ zQo<;red?<^b<>v6P18W5blc59OOM97N@LRpP9KyOaeG5&4C5s#ts|o7kjKlD5HJ!% zp$G9cK4N45m3WVuX?-*x5*06De*d+%8U)8Bom!r><>Vi|XZ|`Nj>;ArLt+UtP z_qK;V=wSyvaq>Uf7JiF6(^j%QxUp*`XAi%@e*GREiPNJP$8!`zG7vnV$948{q#VJI z((J$geK1GL5&S64{`=ntbEF)>kJ9YF|9vn=$`Sl1&Hnq}2Xmwx!H?4HzyEzON6HcW zD9!%+-v@J~9Kny$?7#ngFh|M}{3y--``-t1q#VJI((J$geK1GL5&S64{`=ntbEF)> zkJ9YF|9vn=$`Sl1&Hnq}2Xmwx!H?4HzyEzON6HcWD9!%+-v@J~9Kny$?7#ngFh|M} z{3y--``-t1q#VJI((J$geK1GL5&S64{`=ntbEF)>kJ9YF|9vn=$`Sl1&Hnq}2Xmwx z!H?4HzyEzON6HcWD9!%+-v@J~9Kny$?7#ngFh|M}{3y--``-t1q#VJI((J$geK1GL z0sSbGM}NDmVQG)A?1|ouJnoCo<-AK=@Y#@i>A5KCwhApO>?DB^!6;8=1QOF?O8O< zl|IqivuK(teWJH#(KJ{3L~qZcX|D8%-kwF%TsTb&cVm&}8*Fgwl; zFwSYC;(F44$p3J*ON6p88V5r<$9oJ(ZMx#nPk!;m3#;-IER41f1ixtQ;=O`srM($u zyGa@5_0^@(+PRgL<SmM<%NNeiNOhjUuWlAU`K$xGUacVnC=uH7UD(dxxJ&)>R|0rh9=0m?J3p$D&$3c03+!J_qyn#c!>K>)JxQDt2uitYIoo%NVz=!`$E7fK zifQ|;oX}pywWZy<64kZ$EUw+G7f5BEOo8LJ+;=LkS4meX36qsi2~;yy7+rqC%*1RL zR3u87%s@cd)l_O8S74s660oVvK3UCo&*Iui`*#JZYpY~4y|}ir$0=sJk(sxJKevn8 zu36J61q}lVH>~Mg*7V$JN8xVEw% z1yXe<(m*Qhx*iv~hS@6tEeHkaQWR}GfOe^Zbc(j6(z_OJ5z&FU~h7T0bFtIsCEi2VL8jzj)gY7_mwG;j&I#hsf9RWBhWm4Cx?P=DqYP&QzLd#bQa*U8kWz@Oe_8szkRmtMOE|o=fRdcTCp5^4%#D zlxx+Q{ks(c!+)^aaDCXnNRFVQmXWobRVyj0I=#3y|F55by0Y({RCR5&?57vkR`w+{ zf6~>p_bjemWyiak+9y|C8;qwH*T(;)U0jFm^JM+1b} zZkBQQIqVC4fSHLQ{4Gxvl2N@)$Mm7lghJAL6xXJItfca!4wq8=8ITo0#A-}fDO^A` zm=WSb&&ng4Y6&Rs1FU?OhP;@{rBpGHMe$TEol0gOuxr%wA(bhN4ly@6-=VLc|J z-C=QUWq(#s|03R%+Q)OyO$$JM4ro{Aws4m2N6{A^ec{Mn

za0#mTw8fF>#kHG^?&Pa$?^#@1+W#u{4TZoz_=!;vxG>A?S77ZNf{Ij&F0gDV@I=B_ z-d9EuLm9Nzl4v$dNa{yoW;K-3LrH2Hcy3XFXnJw&CIuhEXp8-_gK#>0DTQ#6pdT6s zQVdds=H;42Ey>gz1p(WE9TKOjS($phcp_w^3s@{FX$TVX&EbRu_VnV~<{ygJFeC|Q zhcxgsbi6PN0Yeehh^Z_GItHOaH;pKYx3z|JIHe_AE>k6Jb_<`!{&Z>>(U7zdi88IY zwzBVFmHGO|4AT^x-URdrWz-DOHnC4_;FKZC6{@u|$$?A6i&leXfB^Ys&Dm;rYLHr8 zw^ZlAZ_nb|%0AQWQtwy&Nh=sAgjX4_;Y6w-bxV|Lw2(f!54=EV(TZ>dv-6g2(Sdaa zF(Sa+0}Z67<@KaB4krvo4T@_w8RueHju`+Y5F?>jd!Ue2(-@l?PPmKa(RK_JDvOaZ zS-=46@k8E$9hAkOR!bgY!IVr5!i7DHYd5Nn;!H+9XR=|p%lLg%#l?Wy`V+Gh&V{g2 z-6@Su2)=p;S?B1*eJX^6{8EiJazmbB;AI|$~E z@ooY9vhEhoZJ5Jq<_9X|k>}BRH?8!R&PTX5n+Bh6)lxGyz!H!s0*};Ls3npQ0{9!1 z1#5h<079GaWWy3I)BiP&e2jkBeqYW)ngJp9mN7)$)V^ zB33il^tUYbXon=Q;6`0#HB(r6&dlRaG5WCe48n)XzED5Bw**M3KZPG?B$!~{2r^5u zu3HcV>eOgniQ4V&Eoipk3NtF5pG*vTs@@iP5!so9`sYe`(|}yXeN(+ePk=vgi2U=C z)$%y&7BURsME-l?9x0FE_!pkXmI*P@-(WS-Yck%o7A1xQC3Dhb*i$Se2#fg;*n1V% zR`y3@Rx2YXKrs(fgA+Ogix?G#1H4;hOs7aPY`}H^(idrbD+Rfyo@&Ff2&9Ztk!)^q~v0im3)v(FLFzP5~cu#$6F1 zPB)sx@xsjGtbojW7T0bVSU`^Jg`PF!6Kfxaw?%|NtjN-$cEn}bX-NJbFIW~KIw-crzmc#z!{-0SlNrh!F2LXPQJy zJ$n_`ZboP~AJgHYW>GWN{T=qOfs|rgMA6QGU>>l?=1+O(IS2{tmWB-!-MB0Si-l$) zj>&zvVO6Lp#5jh8lR{iHW)bGfK*vUueOa0fK3hqyIPEW%{l=)sfbe!SU=}6O-~b%K z(TSk23;Y7+n0v%F5-a11>n$SD zP3<~IM4UrtVKivS_QMwYAVMjkDn=`Z8{q{Wc{h5zA}K9Dv6CjX$!57!#X-LPgI? z>u#hIFoPQeS-}k0qeDe%nT3ABto@F3NvUy!oU-5iS0p{~;#Hd-1pCAPH=;4Kej_WoG8|w+4L>^2luH9%G(8BZsN)ix| z$^SUjJb`*amZdo#FKL!EsDCXwR!Gq-D8$&JU`Hp2OoUi-#y)Ftm?-{=utLg&EMA-S zgxYaonFReW1{iV+4~t4aYknpL=$!$n5S z^AHx>ffa03ARWyCw;-&w3kk7|Rwd!?vaFj+P5d^|=P=Mvy4B=pf`+tTN&u#T*2DbG z9+Zhxx;9nQU$7b;L|?SSrFmMSr3nASd|^tE$3P8>i`&XgEw0@dG3@s}ga|7wWQ;Nx z#EkM7uZ6V-gBX?3WMNAhH+q{c3|@#8S4-A5k{)zo^^}SZGKPu9id;j!P!%laC?i=g z{4Z)Yq~gPaD8Wk_jZufkZMZI~_Bm}38@3plB%7-{Ya9j>+YtP5M2iVOfY>Y6!&~6D z0PNWn0XX^=CMe*dt_gbixA_a92Q>KZm5FA;67HNs`U~;8-9&ea$ zV4hVCKQyyA+GFkGhi1+*s>_fj#VoAMl>y>4m{cf0+27!Ya~~I6$)`DP=eIfD|!vxE>eIk156k$(*o2HEM+9JEt_;!HM?Kd9}Zj4KxB7u z?IzB$H5^KVwJQ7^umKPBZ>i9pQnyOM=4Od(K-ZQ-Zvi1?lGYH+x|1BvPs|27+Z7VNChps`aH?;0F0`cX4fH&j2S3(zt>Hl#Qqr1{1@aobr@a z0Fr=gJ&ByC)=?GxFJ4dxr&DYjs4ZqR0EiT;B2MWo6RNeHUR=A0-zzGT*4QwFh7m`3 zvbL1Z^b$qE0O3IRSrFBPSpY^9kRwx)5K{t^xS?21Vc8JWltK#zN!x=et;2$~9j1nn zkqs$(X!Wc#-UpEaWSozF3Y65b6os4(U$BDY6_K*^J%bD`t7+kI2-n=Nppo^6 z=OhTrb6i8;qqw&74_F859GGAzHWjx6DOjrVI>f^Bu%3t`A{9*V17koIK#krph};7Q ze?h`%LB){cfHPx5rQU(g^y1p||Bce#oq+&KdO0o@(h13^UZr7T6J}sBRvkYJ?pucF z1EAxt)U;xvmLOvD;a&_;%Ib>eLB6oDJ&SAGe+I~4XXG*kffzA0Q`X9^1=%OD-j=_J(0~P%heJ;E@qm3qn#NaL#fS_r zLBR&2a06lqQpdoL(dHwq#V`w%HC~vHPMfWTh_UFb%xVmjmSf)0ZD$c2ZcreDc@S5@0j6Mcmb43akbX@993)+IUu)aOsK-wu*5Q@1zo`#m@&P$w*4P-F=p%* z7H>o;XaQM+$%;j;hw17xHJUYsT)~8>{DIREWSe1sN7~0uW}fOUj4B3a+|76? zR=(y7;Ph>H2{bJ|IXn}rG#d{(T^p<$&BKQfQ#wW%(fmS^<`dc{IK7PHN4OYRWUAiwETf z$gDJ09RxHk$P>yi6)!rtHL>En0F?3J5vU<_?MJG|jTlub``poyOY4hjWq2T_ zNqau+chwxPb_mgz@X639V)3NL4^&)lN^$M@zcXudU4IxOCW8DTMS z$DmM3W==)~znLeV#TyX&2Y-XCIi^CC!Wm2=L`fS4HQ#0#Q|UKKdYqlkf7JM>Qnuef ziRKz%6Lk}~qmIX2*lr<(WWp<=Zs^-P@#}5ltI9G zue)Q&EGGUh<0gcP1KN5;E>REb$bklEF?byl97<8YXubqhG=O5bIaY*b4aJ*Qyf$$x zTDyeiNGIZgf;oE?*KR0`X)0QB1_X@RFk%ST8DC;8)DtBHv7E-SiP~ZA~6G`C^DcJ^J9VjS6eS$1Gva>os)u5cQhWAEV>O$@EaEBru3V* zD?B$B_o18T)((uyl*MRF!y2=j%$PoHWpNqC_m zC*Ei6ERdh5PK*e1W7C*Z&UMC-2q8ft`&`h(n%3zy4TEpq_r|zEY#_>3HX&jsYlwwF zwYBCnE1iS@peCuAe(e9@ZrlnqT^$7d5Xt2E~jx9u>9O8i*r3_R8aOo4jY3(de% zfHuee;wf(;i*s5vBeQB&5PQK)xSQz!+-V@No<#r=;;*+P{V|Fimiph!Y^PTaEmJQ|Fv z*)`GgzWK^5}Fu!5M`1B~cGw!Y6$#!!HESbAE?^dMiRk@b&v0W+aR#HT?2C;!rb zab#hHI#ZRfR$GkGSTf4<~RPEt!6j?=t~HJ#sS> z8Wz`Xj5vbaYJ?9UuObgjGadk(Iph*f%^)|pXmg~#Yk#{YGsE~GUfH*nD2xz9VSn4@ zHaY4(LN3Fa_%VM!IlVA(bI-qn7n_PK_X0bnd*($zpb4qQ1{l+S2fB5^y)r!9gw8RXN&0X^w_a~x4V+W@?6 z7|S!ew5C$^!q)Qg*6IrMVvxYk_018bV0nF^Q3-Ui+Hni$adge}D!(n*yI7j-s+$XQ zZ3(|K+jXoV<+Rdn6qFr!Hj24 z2Z*kEZSh5}ZzeJhOVKvX))h_17>|ndvg)FT#{S&KpQSfb%WRKda`0yCnch^>3 znCrO0c5T&#xiMSVO$9ZpF(OW;W}R=zZC66ins6WHkK3>PmsIA&! zMYpCcLz$n-Rtq8e3?=S2Bz9Uc^R!~-X~oRbikYVsGfyjKo>t5}t(bXQG4r%y=4r*u z(~6mYzbj@s)3f6#I=iP2VV4Wxh!r#Ew-e-i?&>{X!ZkHNcHW$r521RTX5Y8In_c$3 zXsIaxIW4{NT~B=Ft>5y#PyfNcTMK`F(oftaxIKRR`?O@oFTCxZuYSy{e{benKj&Y) z{d<1ojUQ~j@WSi=VD?{pg!R_(qw>>S{an(0H@=ss!5&!+`KK30C{O$kt zWsiRO!pA@B``>&2#$E4z*dtfIvzOrZ`19MImh5=O*Z$e>%)Rl!cYn=azu>hW_^M}r z-zWa$L%#MSzUG#PKKQ3@xcIPMg4^TIZ+lv@<2S$l(NF!}XWaMBXTRgi-g)yo-uwUl z_UI8G{ZU{4iH~{NxBlGiKhsNad;IxrPfK>Z?)Pv1r_cG7JD%|G)^7jcbKd@jAO5j7 zt$ywszVtu*((8Zgu9v>Om*DpJ^V^=5?707Xzvw$=Zhr35pZU43eAVsu{pBY;?o)s0 z+8=t!AN}+Xebm4H!e{gn+#Y{^+tZRAU;T#b-ty|7__Wvm@V!6qo}XR#`1_yvQ9pA2 zSN_4fKH=B?ulw)(gI_iIeCP{W}r0^Qg{UY&SQzGCSjN)Og&EdQFVZOXjk?lD+cogZAFa?Bp!EdyD;V zo7t6&xBu~Ind>0`1q6QSWW{mb!M*vMEa|=H=|70h1dZvGGY51{| zd4mQ8)er7Dpx(PTnjNj|Ot@)hH*)TFVr>n;$+x6Z7<}x2bpPIHw9+5UXe3$5;Jx_) zq(_Zoyn_+u;N#$9hpz|soU^3t z077SNhaEcyJ@|NWKw=|dF~b2mI63%sIdzc>{@WxrnnT=!7exg0@6FGq*lqsX`PRYQ zLC1-Y8N4^Q3)0{Z{5fhpc+q*(!9BJ!%1p#w4PF#+*Sj~j zJh#$+Z_e@A{u<9YaNA!x_r}O7WqHTpk}t0RVv=+9AtS0_z;4|ekZ~f;`!lYixRVoU zfZuuA0lm97ASbaR19Eb2xz8(vu;e`6F7PL;rH>!y&cVMv>9{#yG)@!`K|!5}982#e((HiWx$Xh@9nc;O5QFQld*UYh_>qA8J}x9sVs{@+VuS^G^uZ;L zV%Q0pI(ScXMgLx+$Au$xQ>yTRHm&n*v-OEmXjb{Nf+ds7DE{Cfu(N5%~R5(*X>fKYVK_KVx^iM8oI z?(VJ3Ee~j0lGwpuoQv=Gk(80cj*|vr(8=;ZDxO=J!hBXl#`L~jFvC6s-CH)mg8w!T zSeSKufAI0#4!FiMPP&8s024kq>_l9~puGjkez)vn{V!1n!v;myY`@qplOh=drsCdG zAN+NF1JNr)ET@3;-%fkPT_0HmbxaDo^q;tApR@9A4hlu}G~!g9(D^55~R0L`1g@AR{=hHB8KZ8(aeL2eXspakf7@aaRMy z!o7jq5HfwP&jZ|>!2UB?e*#AU{P0EbOMNJaxgS7gd3m2oLQcw%Jv}R-hkehg<$oU*gYvT`yY?a4MeIufdi}- zBRU|bW$Ira)KFnymb=$kT|4^U^0L_3{$Q3@1`=yI!I8c9Rs@>%XD1$V06~$`y~)x4 z5Z<2k0KdaBztnz7mL{=Z{ir+d+PrW^tdf#ZcYgV1j>aUi;MVi!FV2`lOXCC|5C*OSI8UDw%0@Y3+ z-z}?OyqR`aFKpg@aRw#UJSM`Wn55&I*QHdHB3g~;(ebU%-Q2iy_4@NKp5cD#=Dc0F ztL*P;?kN|}zvu;d$D-AZXRfZ@GjsN~0^h~ei<@rmj;RcV%ydB?ZDFzPkv4k`wJgMH zzZ+^9JauPhq#{Dbqu3c~JVcWt6?;0qF=eF9?xD8OXb&(!u7LQ~nM@WzkN;}XBllVW z=P%-*p2}|0{xw;xtL}Bv->!`oJJdG~q}yDFbS0|YPU3Nh zY`eBvBw;;x#_IDoXU^VmVRh?b#+EW`XKO{$#&x%R&SyS#^`7U>oJ~@S6|^89g6uN$ zGUg%mjfIWTXth$5^#U_%_JK(=Esk-oFviICLZqX`P!<@+3(}l@_D;;?Zd?GJad)Ax zM0SSP1Sq>>+6q+N`c711n_i49Om+g|&C?5*RkZEqwdVyDt`&tY%^i(1dFdp)6i8i> z(A3QYJttJU&E_h(f}9;mp(0tL(EG|xIL-i&K=fpQ>=(>QN~YvQ9S7()lM(tN^Cg-| zMxw-`Ck;lzsm0?<87xnU=xnaJB$dd3lA9t)Af=3Ot}t0jHsM&ZSsI+&Cn=PZELcfH zymv7+`nS|4)Pbb)PWZQ72FV{-6B;SkjPUTvT?!tlS#a=bY(_5QM>sJ5drsq=u2u`1+lc!O760p~})=Zb={_THU-s$WqDeP`MBCJ4q6f zL?wnzJF)s+exUpos}Eu_+srOXSy(9`lEukq$+wzRN!1aHCD~~<+?Rb!CIKEd0|QYR zKU^CuwYZ)qvx!Vsp6B~8faD|%MA-#N6w(eTxB+JX8!iVo)>1N${%_r*FCA+&wi>V` zyqTcECbhUugB7wr1UCZFq)gfYi9}v4*2}1nfeAQG+OMwMQ}7{~hmwCr8i|@oGSb9= z$;>-YCw~?63nV}Xq)coQGL=jxkXmR{w$_^JDkEzXT+|0fE{jRBcBIzIWmjX82C@>ks8Q0L|fbfJ;C!*hJ?YOcWF}& zOa+y1Te2KXrxH#|2?3kC+$QZ>SMHN2)l*)kib(?U;0iQ30W+D4be>rxLzT2REgwJ~`&VO2R+c2s2$}6! zjIHQDIDms6b2T>5!+x?vwKPeYlO!U$i?KJwkqAWvrAndZtSbG?_Cv_i- zmNF@>7ywFnsAShE97yIeMA;QR3+{853|D1g;`emJB_NInY4G_T|hZIj2bH%dWr3-JtEgim_G6 zK$-fo9vCG~5D%SfH}X_Lm@v#FzO5}n8I9!NN!~#?9gn1(b%ra~m4R<2RiOW8FIQ(6 zth}DEy_B{}N;|a}TS_LLP&GD#PZbQ=+6;WJVr){z8zCgac$1T~5(Ospsnkf=uuMuY z0f|ysf~c2MjqO0i*vP+=s>Wu4E?taG%Jh?}#)jbnVQRXB6{KiJwCivgXBoyR!;}n2 z(z}#^NygS=t;UA_ty?ldp`>iCCx2zX7+d@gMI6bjU$t5A!sDsNCUH8|CftQBg{#QY z8Np22CXuvLDClgVNv2{HD(M@($GQQeY@sl-@@JSV$$-&9O-*=`0y;6YWa$Rf==gM7 zS^2{Clbg;J@FHJrB=36?Z|z--jsH7IgJaoHHj-H{0wcNm2%4i+V@p!qcoFt3#wKNE zDO|}t7@Z|~wnBE;4yJjeYHVae(v0n2jIEI`i7s{$)!59V&Hjj2Mzf^iFw%O(*rY5D zqrobVy&4;>O5=oWQV+`BmOto2*{H$3VNOyxDp?5m@aiNojC4P~YHSB8#wKNUHcjAc zr8h<_$5xFE)lgpA{H>_Qwi*>CW``8KCyERItc*f;yR5V;3K+8kG-R$9jF*v<{#J72~=Za>0P=Qo0REE78$!N zcKmTxW2<@{u_Q_jjnR&T=$f&o)p7_jF0Q6zR#pg!V8p3kkp$b)xmq7?BLBiwGCZzP z)Cp43;*$K)Y-QXeR@esAs!eJ|ELqJ*akt8qyV=6>65=m$bI9e)Bth3&?TWEUnHg_X zYq6y|%kfoXOL9C(m6 z$p85Nq;EIA6at`?#Z8D|9JLx-=%0rz-Y>3WqKELIz)^QC4Tf7@Rr+TRhOq9EVV4sQ zA|6vUHkbv#mng>8B$x8ZRAW0(F*fv1M4U)9wz#iIdeEhC?^TRV5&&RFi5O(1$UVMl zY+Pm5nXECrJ&Un5Cs&OvTQfLDa`w{1r`ig>q_|&mhW5ew(U5t(YBQ!eJNG)Thp7-ZOGd4=(I@Z!HFMDXx<{jg1hRvLXWwA+6rK=RFOd=|8u z{fn_lna$9Ofm}l6IIFQG+xaDnvGKp@p4o?2Q9#XzB5Enf!3bq4vCF*zo`C&IL@|WftuCs<9oY7#sVWE>`8w@abc%##Vb{ zmnz0){wGz9?LfuY`2SFOwiAxM8e9BcJYi98Q+6VO7W9E$F*YT0-Q_1a{%UMkaQVHd z>2Pe1Vr<}#Q=dpRwwUSJTV-hMU5u^C-bd_oetSIOIIFQ8s2Cghe^S-h4pfYd{{@y% zm5;v~TP%tgWBV3kQ!+J<6;yca)!0h^yHqhYB_oK$*d++%c&o9|JuY30jsKYhjF3)= zr(>{)y&9YK zzjQIS=H#le9jF*vW8F`v8XJZ7(#6=CldHxSN3(r@Uax(Mu_+mg|3>^Ado{Kc&dKH; z$7VLRrx#;m|D04cwgVMoqyMqaPNW)J$eKpIe=#=apW^>JiE3;)gmTGZY~mm7O`k|L zwy1TNEXKzEv?UVzpB8ew)z}VHjIBW;Csd6syWy8C#>W3~Qq|a~-Zto4^Lpyht(>fC z*yJZvjV<1xI$612F*f#p?4qgV+_oEVAdJ&6>cr(#V~e?c$zp8$Ke*qCRAXb-U%D7u zb8^+#oEzdvxkNFx#tSD@jSc_1bTKym-;=7wcA#QxDcO5M)!6KBW@9(|oO}P&`L{ft zUX86HF1px?aTZ!3hN5EbyzAoTg_*NA-X-GbhRyZ!8;N{s!&AB0Y)J73)hXy57xJI~lXi(yJ>y^t44HEyb zM{H2S&8x)|iU11K&#yJlJ0k*UkJ@at2px2GYO^JASBsRwFWz&@=7lGnfBt>H_|OM^Ll4 zxtD+Jb8lMxs&9GwNBq)hHMi4hZl~4UPOG_{R&zV8=5|`m?X;TPX*IXgYHp|1-2R?d zb1Orqvwf~eHMb-XE3^Ez&Ydt$C3D|fANrtwd=fr{>Tzrr?y~P&H_k5mz7qLu`>{HJ zj1BxG4eFeA6JFU%_*b5E{;u;EZhOJ%dJ+(Ae(9a-n@_p0dM{te)9&W`eZl#-erGOT zcu_pR*WdTHhd$_G2L;0SJ^Wqw-@oyVKlAeM{LqI!^e^7_@Q-`x|Mc+YYaaKOPx#co zx&I@c^cx@l&%WXtANd8Z{EN{oe|g`>ylM7r=f36r5B})Y?{1#>yN|!?f&cu+OJDU< z&D-DcvPVAWx_|wF>tFU=_pZMGukQcqpZc;NpZQ;#Z@%wCU-pq-^u%H?+bqP zx|Lu2$dCDF_l|D($}hU>ja&co(kp-MowE=AjR)WGHBWr#KlrsLe$1C#{qrCGT~B=1 zJAUKJKa4J@}zl{lt6!)we$EY2Wzh(NF&DS6~0Wk9+8o9`&9F-gwoM zUwhT>Kk0+N`gwo&zS}?RTmJ0>Z-4g7fBwV%7V%CUw+W88iswDy`$e$7p_E$ar zAKv=zAN()Ry!m6Yxz2a$iT>m5A zzwpby^o~dW@^3xv@sI!RPkqU!-u!w0^l@jO^>1%|!ViAMbsv1)`#I<_}-J?$y^l_PR&C z_3F2N;J!(z3R+kUiRd#`#1maC*D5#o%b!j=#~Hd z1^?lFAO5>v{FnzG_QHp~`(ZbK^7B9WHQ)LfpZDBnzW7~l`oV{M+uJ|oUw+EclfU-4 z&-lQb9{7gQD@H%M{*~W-{i{B9{xw^_@T0eX!>@htb${~0Uwq@|zwjl$_gg>rke_~S z^G&DK-2VMtpZ6dC^DqC*OJ4EIuh{sE55D>5-|~Y$`M~$S=4nsA;}wtm^gDj?kN@!I zSO4>Of838e{OSMl{Mo;F&Rbse;cvPA-+%x5Yu@<_|HtF+|Bm-P{hIIiPxn3R6^FII zn>U?SbNhim{o!AK_ka14NB`<`KJ!yv^r{Em|0kdM($9Y2uRi$kul)G?&OhTV@45H! zzxylS^XuO?qHznF*cbw%3x?}?PR{l5F#W@*$fSthNMW03t91!fL9mVvcD zK*#?F$-dp|*XhxR!`}Mn;BQp&;XCxq?A$#<0f~cAOP)XFUcZcmSdvjcpMLI5W}Hv| zI+j7rHuUa}o%I}m(D~p#6jZk8R~SoP?|y*~_x#uEq+8BfLc9nV28dG{c9ck`kP>zlHQi+mXSs=DOeGCYj$ z>?H4&oMC)#CwX^we&Fml&A3Ca<9D``ckenE)t)SHbfL1KHi6%!`mM5czBDmT{`C)o^0~{b6csnxb1n_KAiVCci#Oa!Gt#T zcSxc-sx05B&rw%x=I{uT_)~7Hgzq>}J7(F;caG_$BEyssXE3c#aU}O+y%L0J8`$MiEMT{< zwkHHr+Hgjqs@*4x3PajK>c2(Nc6qoOXU;zJ!s^}kydWv@*W){Ul3T4a@z$qYKLBS| zeB9$T#X!>Wl)om=3#MA#_#>ye7p@seg|76~jN|{;s9PcgY-UCZnQH`CiG+85**ngq ztNF2ex|2S-XYX{|U-C(U9H$%aa)>wQ?|#d4#FGUJ z-XyoBxMkv^v-ci6;kdYPDGSZB?qD_8A{R(h_Fiv)nwig^S|mKe)1G1 zxvY1E4JC3v&jW#EeDzzF2q87qPBJdkapwG=e6r98;V=P?`V@MEOb(H(b^qY>85*K!(|Kz+Y zWVK{KnamhT#H6Kd#xh3EwNMEZlr1+9@@eQP?HiB*tTuVkE0trp8k$HJZ%K3zYA_RI zS7ao0i#5cQYNnRva>+1DZHY|4#GK}qM7xj(nFTbG%WNi}vyo@SL84)5^n^&r)SFO@ z;+k?5pmYQ!H&PbBf0?y6q^7A9dc%N;hORIBh z);hHgGLfX|o|RfJ=~bu`jXZX1mBlNee~BlS_$SC_g{9Mva5AVOpClX!l4U@KnH($~ zStS)cL1o~w;$}d}4~>nB=NaSHW~Ek_F-*rI_i@UEnu+FE7f348S{e>gNOv*Y%}APu z6ao-zX@FT(*a7%bk0wNo2+;`XCR{W#LkE_SjVC@T>0u%(b6+kZi$xqoqPT|kuHlKg z!;2%?Op2FFss!YT>pXw=6$(AARpey_7eSy%z@w za<{ZDUpTzUeduYpz~H+ksiXycTRjntvRH;FvaDf1iN_BpDMx0V+o?m9A%R$t;STE! zeGANwLk8q|a7DBAUYkP2zyKZ2YcJfy``RxKNM;l= zhyQP6rP{TZ)3%CAj6sH?lkFCzjyuf2vIG%EwIo=wR7tE|GlV`TEZ$z zkVMR-Qf9{Ze7nX5ZD8#Z6mnLMH0=f#@a;e>EZQVjnsi967jo$V(z#vPE6+&@FZp92 z5d}L81tIt=!f8Srhe`?;5*2G{Fd%f;a)GF;T7!P?tfFL{||jQJnFn zBv%n(QLB_I@CK%Bq^>~iz z78}K0!u!V~m(SD#D%matgT=E$cH@hc?!3mL{6qg$Q$0_g(a4eq1iz9 zVr-)(oGdR0s`rAyB>7#$arDAqKc>?)40#CJqK3q&CMl?FHQ<#7mt^ese!h#G2y%iV zD@TXXEs`}hOQ$k~tRYfO2eSCDgkOjs($vf=_d?|r1>(S2*%UJrnX7>mgh^gv5|M31 zx*0qJ8Csc7p4HEW;UqyzDVaiblVY`=z@_mL#T>J|@JKXoa5)GU6r)xcqwYwkNOaw$ z*^Q`K`8z@mb#{nbUYnoZ)m^U`><`CFQi zEJ$im0ALMjdNYT<+vMTPzT018I<4`E4@Ab-^Q6;le^Q{#nnyj| z{yy1^b{YV_@b=`q-0lEV>Kfn!LB~|A5L*WgG(PUiV4_V|nW2g2+e4}6Rk&d1d-Er7 zCOXIrNCORZcPG}^$lK?oppL(SQ{DHeYLeukOpcxJEn+u7bm)1?b8%JF-K>8Kplx@? zG6~~B5~){IKSd$qzd@jsvT(L>v)J`^2POk@LFcwQQof3IJ^tEn*%J*eIRxvgtiKn! z2J|aaG2GsMVajtL-0yiifO0(SbgPfD0n73!2(&t;^R%;#o@89&vJthCgb$iNYt8_g`M^(rO{SVDkl5qiB&Y4Z>gEqQO7id)|?F7 zfSvDRu*EvqYk;J5$1xUWWwUHY8)buD9)|(mZT-6;?EIz~cWVkg%R-90H{iJMl;sL{ zi^3|8Q_S}ubvY)w?xwU^^FN7(cZR+1Uzf%IcaJ}uFq+a1ub+Hw<{dR5YwyGui+)k| zf1I(fXkIo`%LY^wP)cNUvJ#m*LaltR)r>PKDaWEEvvrYKRHs%03ajKnjj4$@hWka; z*ym^Wz+|)8V`NumU>*MNH{yD>KPO9-qcXMPaA++iI(U4msKGKB%vcJ_rjA266{6zkUr zJO5v6uSXhW)uCAQ5X#f>-yk}W?f!()EJtvcEgM>$`{t;M(x3{-RIRD%|b`NZ_g=GoB$8Pl<+5XY9$yQf%L&G^M6n+^#7gUWN$`XV)9`_Res9TdcU)Y_-qo^G+#prO3y%mnuim ze~H&^|5Vfv=J;3#CO!k)yq1j2t*RMbIW{GV@$j0vPE|WDS-hb3ky4^0`m*oIZO^%& z$BG{4x|U+u^`>e%$++f*(s9(NxCmppq`06DaPpK z;yP~6(xq%uc|V!F5t{Fr8VC^|G!<49{Yrlny(R0Pd~>|EMridlhGbnEILZwKWczg; z=@`w=bvi`T-bb$FT%;xEl)3Al91GHclJ^mr>xei8w+>5H^^faVMHKl|^Wf)lBchf& zG1pYFsO1_+EiS(esG5#a7k6W>lh0gXJRQ=FT+7`Hb)%F9S0Jh-LI1gq<^=zf-7eC0 zErvcGlSD?U8PNZ-I<`YJNWljD%m5%XSfr-XpKDg(Sd%TX!d^14=*}z#&r>?e@O3DA zzr~1WB*`>mvRRA>4T}|DDKBFdD-)B=F*jVO+z2SDju}EZPHXj~m!xpU^la%lRNz`q z(VC-k7`kPue8?6Gm-Rllc^<=KAxn@AKG#S$hN<^~%Jr6CO^eyOvAQXmnB-c>$Ba{T zVvA**2E!4;$1Ok%2je#&%$3tJfW#Z4_rzXK0R`9qrxYLdys)AS*Jw+j23SAF4tsC1 znL!OVa&U(p6Lhh|Cl7gTE?O<5<(g6jU9tpJZ04Dj4f=q9RD7N@$6-%)0OAXZxvsr1 za@Z8?V&=Q%N0uv|PKlkBpk7i_s7BJI;B*S zh?$E~r?Gn!Su2QfD{9Z&T&FAv_m%67T$_gU{3$I29aEGI|DA`A8IhvnZp2@m@+a^! z9eO?Wh!9|Ey+F@^*mi>JWM|CYaEe1hD%xYvHLNSOoLjfMd$22M6$4Kbw9K9drz}G7 zQ8leC%*ADVw{CE>#e)-g7*L!n@SBGSe(Ml^8z6r$?_JnX9Ia{<1mu7p4^Ifu3THeP z0lEaeqgch5avesemW}tbB=A2Vly6sqHKP1uEg>CxP|;I&*>o>2IV$l0GWo{Qge~tu zTr&Zw;vw+gkLh>^0@rM_X%~As+>h@p8~FYX9pwM^zfeGo3OD1cO4XUvDNO@O1`B`@ zeP+zoLmdc8^I;?;L(DY8r%0B0))g6#M?D^)R!0ECh;8%|>Plj5|3UWnf7pL%(k@IaUFA4|1X-(05#lCIjP^d}^n zy;D$x<>h?WLC_cNK<@ZPD0Tr<j)^I4<$QARowDHyfsm}$peE3>qyb1 zz=&7dGl=VbuIAzZ0F+8~kkV7@9C>0e#dB4-KyYZ&bFlKB6sC54v8ee*xkt@KMr6gJ_gpbbjaufi zhnaq6&qL@Z8Inm!WSq@Q8Zh|T69ki06UKrpCHhZci`pEck5DnpJ1hyE8-L0nWt%kz zQQA`>QCCRJbcplkeH-bPCK3=w$XZ(jEX#9Fb}Tx0A4SApCdZwD7VNqablj8VPjDRG zLqc;hBTe#9c$G--UwB$%7wq<_4lx!r|6+c@%RG;JJjHm!KtL>z!l;ok>~X3j3yA00 zpw<)LFu4)^uXx~m6E+$ls@`@G3Rr@Gbe`#vI&7m8KA_s zd%B79d5s;&q6m*n2~k{8^EyQ{*+~S=82DLc9;H;X%GOvA1MrjAHC)qLVWK`wEt1sj zj@O_3!V@*-IvX$8Hmt%(yib&1s*u<6TM6X@s(uA)*DUA>pq7lj*|g5?H#nj;&$i~# zpEn>{bQbVrQedj)rDP1Ktndx0#tPAr@fPu*4q8jZ3f!>X_O`P|NUyeP;!TIh{*7K_ zmRM8dW6ZAn))XFzIRLtT5rK}G|Jhtsx07SfzkY~3{p3JjQM7LtM*I<*iv zf19~Mov;Rg^*SbLoS@+ihsYwmbn9(!pcQ3auET7Z*iq2Trmc7riI!c?@Ii%@fr_a0 zRm?WZimx}L5+Y{sqNve7FayFP`|;Uuv@TvHR$yun*DcSpDFM;(&=lPvTtf#qB(VeE zxSC1!=mDt&=gPV!L37T6M0tx*zaD<$UnS>UBVf;CnlGdO48D*)LxD&>&H*BCRgLTM z?zmeat!Tf7VqkXp%rN{h$OlCuXso-B{RUs8n;ldTvy`QtgokJ);*MDJj`KgL*IKUw zj=8v%!YUCRuFWq7L^!!cacw;ooaXR8&RERXJ0-TLpsus67{evl9d6@quy6wpG(atJ zI%7mRC1(P7qw^oA+MIc&u$^eENUiv@215!kx2PI~qVeEzp%80fP*x?%35E&=0d5r8 zARiwfD5i*(_j7!K@|Gi4Fv7A@T`=Dk&DlS?4l-aZ1stnGUu$m*`?xPjNYg%K;@rswakx(=Q|{+y^ym%!e6zx zRgfdZGG+L0kY-bSjbtzM!(L$}p6`VePJ~phb0#P<%wPbI$O#@8iy`Bv*fGK79EqjK zb~c6{kz+w=)1?g4n-{lWPmB_HV%h0Ks@X#<;*?rAhoR~MAb~b0l+Q63n+XPK?aVh0 zH3$eDwG4n3rKEc@Vh4T!g6B90aDzwGRt0(uHoN{2Wf-v`6ht7p1ry!4t654|R1wfX`FY>nl%Llm2^TXy8ZCnJ zbq+EhvrH?z)01cxWg7)Us9p{qAx-B$;yy`5W890C5_LHphAk1B79su&h8Ntm?mi5` zIwKTqE=|MqbgjP-u+`O-h9WajfF-E?LS0k0ESKOGgy|z#0eAUdyq-3~#$${-m0@jS z>oT~J3Cma`5ir!rsH3h^&#bCHK%~%tmJwYz4G&86!_2^V!o#zp7JGsu0WEe#m=6du z_aJT)v6vk$k_oWFLW)-FbO#;{WwtygXbmcQkeR0JTDTXajK4678c1NyF|Y_2coS|1 zR+t;Ck527QM0sji8xg4KjXC1{A-P2MMHkN*y@zc6u5u*hL$)IES z8lnyJ%Ap(%H6m6~E}jHzi-~~;L`SzA<#MP|A>V)`x|McfUo_!g%+KgORx7qoUdOHh z8Oy^rVq`>SBl&<7b=Sd^&Y&K=VUnz)nFpj?xN$tr*>0cK7)(`=WPRqS-G8@r;aBTZTm zNkQxJ4az5mTML?wVIeJDun1Vydbj_9X|z;njc})oJ4n10Jgt+9@PQqqysUYrBHLz; zn;32!2_WZuW2oI11-tAX*9k|BBQ&tc#!EEgsb@%JbJiBN>xC#F(PbmovU;)pSTR1# zxgg6DpI&=RFOoiI-iGgo;aIG=?}{>ligp&=-VdavkVWT#eA_3Ya&6t~NM4y+Au^qSNGK!DC^}K0VR%N@ad!%SqtO7O zF@Ug&&V@|-(@eBs-y6Blpb-opOFIbQkwkX~0oW4lS%-!gmKz9+Pf$e!#X%d#UiUr< z@^^dwC$0pf6*|k_P5fUu^%g%r3m&LqbV3O#Q~X{eqNe$_szJ`gK&o6WVLZ-8m`Mf# zt)iHjL9ig2l_dc?hZP|%>(W9krJ5&$5e*k;taY?Mdo{YRhW!nc2G};71Ig{uOC(hd);W4R^L+k;6wU*HzV;T)i7C?=vUzq2&X|D$Jrwuj9km2!Mye7qL9z;#dA zzY(0gVMm0e-nvJ06?cISZ8VN*W*V$f8M?KRYxpO+gI!Lak4Q?4y_#3O6~=sdOq`7%Qg+)s|GToOAW*{+kq64)y!$lS-!nHtR<_ZDG1?zdx0oNrGu2T;Xo+E zuZDx=Io*P;b!JTsyFwLWYSmiP>p>3E0dssJ(J-PrMYV}l9G_|(+=~B)L&4I;WgZWB z_JVT0i$dZ$;AdhI<#@k;sZ7o6|r}z9K;?Pi7fju0b4|^rbFRv|<6`@my~d zHO~p>ZtI55qcvZV!Uj+?3~G12@Th2U%f}nc7p?>TuW;oU*rDUhI64o-5XdN7UWkt` z&hpxb(*R}4br#DCyBj4mPN`V7nPHeVgG&dYg$S*L90a1!2TdB8>-a?Y7#5s=XXd&= zdV4>6KXDsF1g50PQU}K_Du3`a1kByt2 zLgaf@y929&U93MXxn?bTp1r@sQTU$;M8k7k-@^e88|NRQ!)9YFe%l;uL@SWD{X&L=YepGB z1(c4inX8LtW@fZFd(Fz?_`%BJHB0rN*G@u!_ZwM)f!lr-#x)BjMn=0Fj;sxwZL4`q ziI?4pq1^?coyggv1kNsR2hKLv7NolY^de`IS7CeFy~x?)37qABiq{FT$L*a>9?A(G zQRmMQLJYvMS{TV3oiop=hd2NR5S;>pMCw8D*hkd5Vx$oi*+9*CWt0>fRD1ovS?A9+ zA7v~$goeYO4iq_y0USf%Y-5Jm>t%){AGz~W36UhN)dG@t|JQ&+dFEYB`X`%QZoVYU%<~EOZ>$CuC*(5a9b^;k7$%t zx!qxWGbB*dL)w~lqHkpBV*FAA+YU=mMAGD%tm!&VAYBZm7)Pqvj;*8WSR6^LsC*D3 zzO1NVPxyIzIFfz|@uF8AOW>^dv#9t$Qy?OFfYN9oV}yoLlRwPDs1#+TMO`G96W(Hg zum?y4YG|HmGN3a*iSFB)8KxPiwyK{4#Kwsj77CO zu7!eR|0`=9WQt0k8HXc`Ih@~RRkaNiRam_kwsmBXq7>t344Le-dI;oM(!!xj2hKK! zj+`xqTlNKZ6%E~({acI}8_j;LYmB{q*Zt`EIkVAPakTP9`uAZYXE_3`y^T-2y-WZz zkE$P)pDvrtQTUOA0s95c^1o&~F67sl^3g<*5nXlP$XV9+F$B)?zmRZo#z!kGbFU`_ z_llf7p1|3hzr+fx8v~6d8q$og%-Y3Ri=h3|5 zv<_EF{9w7lVg}%5w!CP~!^qj=37if6hs7eR1VIuJS&Hb`vAm0eu`zczSLoU-6EwjF zvs5`_hVA8jR#=YGw49CSNh{%own@T5^Bum>3!HWSNcY7+ieYdE6@6bia`t!vXB%sX zam2wD4;j;~ejgxm*7C$Q&EXzvpZ%CHo-xFTA5O3X1kN^`o7Dj47F{itTLwVd)p2fQ z$rZSLzXlp_9YZObAKo8>HmvwCfwT7Cvh&0p3;QC=T9&KVmZ#^srSlzOG=^B*`+w!c z>@=66dp3E|c@DGf89BQ%&{E0B8t8c0fwT6%LOvygXu(iD znBP5*4?41evuomcoTlR+z?~jA+s$7C*)`i8S-U+HqfH03lO88#+bn@I(Lp9aifE_1 z+pV$dvXJt;6x8uosJ@#&K?*d{-^*YazhD)|U3qS1W!-j>v)(y|z}cPno7S8*fb1=_ zKT)r{zu|H~XeEt$Jgn^6cd&?O8U*)!4ubvX@=vr~0BR0$b{5Z0w>l{noh`s^`@Xm% zZ#>FATHtImW|1U+cs|F#cvWM~#xW7^?9O1izj!lt$h17kE%{z{R94|X*K2t|YF0UE zbdbo|ZYW3l-QoX~IbNz^uI`Ag+MU_`?qvkdj_Lmr-A}UG-Cc1|U=1y&JWSwh;;%5} zmUUiMShUq+volE*&}BX$EcQ`NH%$Bd99dUB@HNoQ{Wu=#8VJ zFU(r{-%*sdKi!$r^uMG=+Ict9FpSGmOuWs7OBXhrMB(ibIGgp~8Ly9P^f5jXI}{F= z21}DGO};(6P>NSJ&P!w7KB3UHMac*h^Hw(2TU9f>lCvsmdg+)64s!nV(2=t_5TJo0 z2F^CIUTAVT3sB0GeJVh#t^e3(@eJYwOc`@wM&-CnVk_HC@7lBGBC+})7C!>x!oLR( zoVEX+rfk&vH^&!p6e`R!4cc5`(a>4(NLn93XQ;7ZqRG1^Zd|kFa_lqLHpz4T-1koM z#pp7iYB^r)oaV^2lU>F*4xF|AJ~wFoqRWe%joEedz*+Qv$jDje6OSQqR{kxkN8WRM z0r6!-&bF9+)WBKzk5qIW%Nw%sY926h7Is)$I)AX|(1EjU{0X)R2eKa*dp+hfP7a-d zUhI6=wh?-fv)Ocjy5*RN2Iq~KX7*A;0(n0M8}-!j66l61rUlN*zjMgQS!4%sk>`;D zXB*>Z*i!$(2QMdbHa_tj3Cw{vlS7P$37if7=YT4eF@$&-k+Tj99Yf%(`~$^A;J+X9 zA*IN_oVc;u7gN)&aJGzqdUxb3i`>3nj(B(UbC|$c{@-}G4iPzvqs7~Q#K2kmzZei- z9(Nkgrv`JV{UDLE6gOP;sDZP|zbnq*5RtR-%pW~)R{yBpiA(tJD3^6UXyoki1kN^* zSUF)92^AJ{;K*6TiltycM+%(ff54-%_nY&nHlk>32aB9#FS2!x8aUe=I&!uIcEbK3 zVYcZp+Cv1+I)9n?gB&}&%*a`08=jCOezxu)@ZkbyqyHZ+ayI_l(s+*)IE(+$01p#6 z8<%MOokt6tZE~j4nB$uQO>ca)j z%D;Wc$XV<^2FeiwXOn+nJ+hk+y1dBQM60qHC~bHK!#Eb64QZAhaT#UO^K42a6e~wh zrU%Z3|4<~Hg7f)hMb73xs-NhM845h&4ai~Na4>w7UQ@f{ZtW8|tAEIc_N}|G{mvQO zgGA2S@roaSR*-n4Y+jQjoD!E8INKaLa+czeljnR z@4>manGO{=%l>wFk_?IqMI#4~oaNVz+C>LAO5kkr&(mY-6sQ?t_-Wl6@PZf$XV`qV*99pvvvNzjkM*b zxtz$^oQywu;B0f~$k{sieB{8{M;H>xp3oXz>asLcYOF_av&tVoDypsmwzJCU<{ zL+Ek>XXW1_$89!Rg*wMa67vIP`^d%<%^=$@`77H{?dH=sa@I9@X}{!1fwQT~J@^>(5`<*laH3R#y^uT-|r2 zo56M&;cL^_4cE{5EsqSMo($uy?zL@QoKX zH*2Ef5a*|^-qSpH=By*nieSt4YAbAhWqE65mW-XB-{2Y-=`mWE*sP zM%L{7=BCUQvr8Lma~oS6!YjMcuB|^l#;3DwUG3DiZV6|it~NDteDn63*Pd6!!l`cK zTicbk*>3HvGtCp8aOTYB-5Y5dY&zt=V}{ebUP*d@wx7ww0&S)9c144|G7wCwwA~gv z?}k0C&~|gsO55$qwmqJGi~|vBdUv@Gy6F9xx#b;9y!%CW-L;Etx46)@KzF$&To3)t z%s4M}4dz*NNT^=3vNHaA!As-&yRf+CE?sS?CvE*xWi}H+^Pw#yXCY%wUl~tu?)S29{GQx+g2@uTMOZbztV) z^7!G>th5!|pS4ZJTRXTXx6k07^IXGwk@MX!#_i?&$Kd8dcvSC2Ch_1NcjfTjQvbch zIYwjeJ%;2lx0RcJ5uguzKfR zn-|VRS-9=aFW(FcUB*jqJ%9eV}ZI zrLC2 Date: Mon, 30 Apr 2018 12:33:29 +0800 Subject: [PATCH 050/520] Add validate/1 function --- src/emqx_mqtt_properties.erl | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/emqx_mqtt_properties.erl b/src/emqx_mqtt_properties.erl index 7e4633657..5df1569c6 100644 --- a/src/emqx_mqtt_properties.erl +++ b/src/emqx_mqtt_properties.erl @@ -1,22 +1,22 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_props). +-module(emqx_mqtt_properties). --export([name/1, id/1]). +-export([name/1, id/1, validate/1]). %%-------------------------------------------------------------------- %% Property id to name @@ -109,3 +109,5 @@ id('Wildcard-Subscription-Available') -> 16#28; id('Subscription-Identifier-Available') -> 16#29; id('Shared-Subscription-Available') -> 16#2A. +%%TODO: +validate(Props) when is_list(Props) -> ok. From 1fe28a7aef1ab047f63b8e53d746e5afda47865a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 30 Apr 2018 12:36:17 +0800 Subject: [PATCH 051/520] Update merge_opts/2 function --- src/emqx_misc.erl | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index 27c18f8c3..b61a06c60 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -23,17 +23,11 @@ -spec(merge_opts(list(), list()) -> list()). merge_opts(Defaults, Options) -> lists:foldl( - fun({Opt, Val}, Acc) -> - case lists:keymember(Opt, 1, Acc) of - true -> lists:keyreplace(Opt, 1, Acc, {Opt, Val}); - false -> [{Opt, Val}|Acc] - end; - (Opt, Acc) -> - case lists:member(Opt, Acc) of - true -> Acc; - false -> [Opt | Acc] - end - end, Defaults, Options). + fun({Opt, Val}, Acc) -> + lists:keystore(Opt, 1, Acc, {Opt, Val}); + (Opt, Acc) -> + lists:usort([Opt | Acc]) + end, Defaults, Options). -spec(start_timer(integer(), term()) -> reference()). start_timer(Interval, Msg) -> From 31bc0918737ccca440262a13cdb464e195e6968c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 6 May 2018 17:45:18 +0800 Subject: [PATCH 052/520] Implement MQTT Version 5.0 client --- docs/mqtt-v5.0.pdf | Bin 1707553 -> 2557522 bytes include/emqx_client.hrl | 37 -- include/emqx_mqtt.hrl | 265 ++++++++--- src/emqx_broker.erl | 2 +- src/emqx_client.erl | 794 ++++++++++++++++++++------------- src/emqx_client_sock.erl | 100 +---- src/emqx_connection.erl | 73 +-- src/emqx_mod_subscription.erl | 8 +- src/emqx_mqtt_properties.erl | 117 +++-- src/emqx_mqtt_rscode.erl | 115 ----- src/emqx_packet.erl | 68 ++- src/emqx_parser.erl | 442 +++++++++--------- src/emqx_protocol.erl | 84 ++-- src/emqx_reason_codes.erl | 110 +++++ src/emqx_router.erl | 2 +- src/emqx_serializer.erl | 356 ++++++++------- src/emqx_session.erl | 11 +- src/emqx_sm_sup.erl | 2 +- test/emqx_access_SUITE.erl | 10 +- test/emqx_protocol_SUITE.erl | 60 +-- test/emqx_serializer_SUITE.erl | 91 ++++ 21 files changed, 1510 insertions(+), 1237 deletions(-) delete mode 100644 include/emqx_client.hrl delete mode 100644 src/emqx_mqtt_rscode.erl create mode 100644 src/emqx_reason_codes.erl create mode 100644 test/emqx_serializer_SUITE.erl diff --git a/docs/mqtt-v5.0.pdf b/docs/mqtt-v5.0.pdf index 8f8690e08c98d7cc8ea096c4327f8ed96b22a288..1fa8883d0517edaf5e2df9010b9601a223a4d1e4 100644 GIT binary patch delta 855995 zcmdR%2Yi%O)`!6cqGG|0NGu>&Akz~?ks?i$s;pv5nFtC5Au5PEqQPDl+gh-#9Z@V` zLB#@sy<r~J?VoO|EPbyp2}`P{1-o>JYg zdX4HetJkVtyLz4KM%9h0*R5_+yqw`y=nDk)tgsuQN3mL zR`+hbUDHe|RZv(^S$b5H4jr1bA2@box}bgEk|WavP53n`J)){0%J=Cpv|SWzPPVW$xzDyzaL$=oOVrOStwbgmrZQC`|pyHt*-;!>jvQboB>x{s2xiS?Ma{TWUt~ljM-Zn2>aVnDgWVvv~sqDBiwWNzA z;&sIC;d>a171Sn!6tc$t+;xzR{0 zk7+sa_$2Q{HRADE-Un*LD~{x)$%@A@d9P5s%x`jTEM&pFd@CnjBpJ=SRbG3^Xx@iv z#N(*Es%pf`6esfo%c(CJ`wbS>j@@|V4>(`ECg7F}6AyOC)IsJ*VNFEmjagV*TR~ZQ zWRAoObAzrU-|vgvOxF7nHPj=ozeFN$%H<*_5_w^2^oPWGo#rG?)S!I5?ia5c9hsG4 zd{Io7%VIUs@LAq+wWO&rUf$c1c`vPzCa*s(kSNH6+*n6}A)bDx{}tEVH?P0qSUyr~ zB#so<09}p5nMh$xJmeHuToY1kB`W zj&N6W(KS7~s(?fC&fh;>7T)I_zjNiN^7JT=$OBI20lv%Mvc|9-_VQ|-3OYn9q80I? zWFlFXN+uJjcoM`(MxuqW2qq}ZB;v^=ZwjM@$&{ybD~R*6lh@1#)_`M6t3sIc9y;P^ zzwFev_wGGPMh_`yUs0AQDNV*w$)e(9Nl|G!UX(71mBcF|6{+H6MR7$zZOd>;y&!Lz zZd64DnMkZAo9#2Es{4SdlBzU?Q4QDVJF2p5KpGt&Vm1U;zx@mb9H%br|Vz;y~xOhG{kPF?r7-$HKI)=enb+7BF6GGg>di^a0B zXr~Jwbxj{Pv@G4fd*{049Cy^N*&DXsA&LnyiGqUme>>=4Ze852F#JJ)NUG-bh+~Hh zTR|FqA$tv+uN?@;pMnAm+b)^NzK$g0?a*uX^P)t%Nbd7GgV%v7GeL8OHMgtpRh_eR zG;eOXJgA`N@H%BH;l$O;^pB3-PQOGuc8n=LPvQx$xEFj8g*eHR+HPZFc6F6sX-nFVv#(q3InkkuCvlG zkjde|e}n;i6jQ4G{{jQWk+O6$SspDdj>pT(N@69+WU90%mMBW3iy%VLx-d|i0aAYk z96xYE877yN%zr{+yKqQlQ z67ejAJ_=0YCN<5a{D;MUm`{nh4@nd_R4y;&Pp69K!<;v-AfS$#)o%`TGQSITf}V2c zi#mVARr22EfCn6#shiq!I8#%n1XQWvmRW9)r6{pHzpOjLs0fueL0wE6SkM8kkw~Vq zM59w*LNJ!eD=&BF8tz$xa5ZJ#N@Lo8hcwAcr+opLQ6)5PGqIYIu?FAZAj3xjc(r&= zNntFSEGbDuV#Q_gigGAac`{N`ktUM(DiK=&-~!vtYaxekH8i_6zQr>6Otl=~@+q$U za)68F{B8wZ=W-Z12>8q%!mD6{6;Lhk>a63`MYT*_^b6EFFL>5`e+br)BF}X6v;7e( zp37Zz$B?Nh7a2q%)-D>0rcgjZ3*P&(pDj7A_5#)1i?#Shb^~olu4vFGqI?e!+h#XIo zV9MkCp}37CB5`tKJj$y?JjHS4@pPg%UXqCyB}$TIQ1WyAFD3H-m17isc;X{01{Nj>2M5 zV^tF*Elwry0wqodRZ=o~A-bzdT@)|Lp154KS&iVlS+9?#JWdm#sT$|i9}6Gs>rrocy{N#j4SQk0{uiF{R=bw1oHl1ve`U>JJAZ=5EaaTyOj%BT!X0MI^b?16PyHu(>q0_5`57- zA5jaqkU&Iu8h&U;4bb-k$Bpb_fG5VS;^1KfWya(5&Ja{AMVCp0z*GUCxEX+z#R0C9 z%tCxTov9U%dU2XB~A^gKn z7%V@_JPw6M$SnS>Fu=ecP%}x3p&_sYH!KDJ7=K00 zxYOP($xZoLqYr#Gua|P8B`DcX&$6#|fm2)9J=;Gf=B zsmel36vs>T6ln;*O9)EY0Vy5(W0gcCLu`|5HKLA%10uCBf!9}XI}e5(;9g-|K&XG> z(F#OBl2l0)weQFLXCmF1!f|F-{_jVz;=iaumb0mWmEuMsW|*Ia1p+&K#}F9 z9>ygkqcRVYjNs=Ex)3$<$IAgbsYD@%76Q`dPkn$d2u)G0JX(=V(|Ltcail|(un`5Z z6mbMnq!RdYNwgvpOEDIC4%#C~u#^I$2Ve#yGwJB<5w4B;;$>JpscrZ~8)JfytfZL5 zA?HWD1XnW4?rjYqZFY^$x|pNRjgGBISU?#WVg5kqmYyo zCJwNn_IR`uk?Hb?>S(A0?I9Lm9py@@j4Tm129n$o*w(Y~kO-e4Hdij^Q-+Wc*SVPN z)N!joz!RT^{cw6YbcYLRKeVK1cqGa#M_XyWD~ncOvN(C82_SjPF5A$A;6HbjX-oFsUcWc<|9^R{ex<)M!WvfeeM9!Er7&^|EBz53gD4r=| zrO>lEtr*xO&>1g;0;ny=Ijt2yepKuyR5o}4zJqScK#WKTTE>ZjL;#?I>oP-v>9Cc| z7>&pxis2tbA{Q|2b9q9{Q?R;dW(8D2hN9p?VFW7Q7b2=g;8T!5h{Mvj9qcxV8^{|8^?C)^a*1X86Kj0+z_2~tHlGGpgsSOCY6 zd<9AKAKw!V)_6D-=`Wijl^yc|r5p=4V%)@8p!#L_vl+oWLA>FA(Na`f%B6r@RRfx$ zF94E2;y0J%W*SkHC`N8;TrMmLb!1)&@1RtbP$s!Gig!?e6i$awe4fS=q+}-YaxxWq z63~(vIZHW^Log9U?bccWh71_5IvE5JxRM<^H| z!wAF_yr%}jCqmfcEyY|JMaRhv=&~YKm;t@vZq~eJHGGq>3HqS2H>o-vd>A&10!4i+ zgb%=HxGBUe$#8>?xI_uoW^B?aM29JW8gvRmdM*N7psiexKmo#aVzMHB zCD5TOz92rRzs-4E5^G_W3dU1}z0e0YGO$2C0F-i^&#@8K6xqyZ>2NMV#f3mbm@da5 zm`Dgt5G7zahjT9A630oo1}-V7qt1lb6oLQvbDE@r-Bb2FK!5lj?(Bus0tWKp055K* zw#e#ce|eGTU~$A}kZI&3*Wr#OM26l`Larx;iU$04N|bLf1FRy%QAMgS!&s9z1@VY- zO7KD$YtkARjp2+*G@*f1S{cgroxuZ=g>+2X!DEPp5bwg4ggoTVzz+15c&LLQtfDG0 zp*iuXAZ8}5nvhjjf$yTQVrd8?wdltz_%k}fp$xD<(0EnBEg;(1*ZNi}Nn;65A*1VN zVhLwcqL2yc8Q~QKV4!(4rvN2VHPmG6?(UBb)po@X*a#1^?K>{-1AHf#_S&BoA$Q2XvKhp{Z#-+&9$b{GM z6TDVNMk0IwuUL#@xflt_O9w(p_lPv3wH&pbAg*ogz^!Rc5x`daDbg4;hs%;vsTma8 zyTLLue33RnK_!WXX_YU6N$@WOvBzZz#f5lHM>N#Gp^`TeEGq)?lOG6H{hAw?q&Wj8 zVsI9u*2Q8*36=*vba|{2NoI(77!GwRClcM9h|7_b@B`(?ASe}RF30~11y+@Vh#ZE_ zfM9&VMCEbTi2ygQ31IUR)q|OEN?wC`;=TYC*dW}R>!iuQTv!=NoZK8QWkk{q6opaxOa;b~2y92uG(u4b1-;a201cQ5nR3Vm6F#2+%lQVGNa3hn`UNiXc^PK4 ziv%2@C4Mp3a`2Fh2`W&33XZBz;sDwaaJUCfNiyLZ*&J2`E=@CDgszoJ=2N?(s23`M z_rZ|-BdC@ExiBD|l3Q{OGY$$e$Cw}F5s*FdZUy;Y;MLL?!DP2RZMbaihXxaleKZ-ojHkeRL+7C zxC*M`B(wpc;1#DLroCYuRktc8=DOAkV#z_N%DU?jBGbt&TAvC^>s0Lri@AK=rxM_@YvG6xNz zdP48dz_ySnM#3U?`B%;7r z3rxqy19*h^093WI7vnoLDJq!yc~4a1ki-JEhKgfQn{yD+;E;j;qZu(Y&E4e9)CDQn zU5wW?SD{=Ok3u7!^wzf{JOXaXZGHy*Mfi*$&L0lZ6IM zD|o4+BHD!%0*k=aJ1yQTB(ySVGgOEuxDY0QX-HCH%>Oa4;A$Wf&VqF~jhP=pi+a|Q zL>rQNbr|mn=-N0^#f+2SBB4TCvh@Q{1Y^+cG!JZ#${06Am*9x0N7sYM^Q{0y{{upS z9q>032=dymg5m4ggoM2yClmlT{2TU%u);6R1$=-^X>g)5qXiE`v-HaFe#;nqoKNWg zTSKEaxHaxt&U-#4Sh*|tkCI7;<03c`d5rO+IFXKwU>(Dp1dUBcN8A!8{?N zYd8iWF|h?rqXKM)q{43CQpAQcg+eq_7U+_bcqQc17f9j%nua10C5p;?&mSBTRYJ`0 zc~F~cI~k3ypnArL$|<$@KHDSaT#}PTsznvikJ-Xe21J^-sT07 z41yZ*-~+CzEfAf1gdBrn0U}-oAHd8s>lhL*;zb!ofvrFpVwDT%EOJ&n4;)Z_)rQY= zI7yW&M->kj1Q$aONCF07Lp+i*Fn9S2)D=gC4eERh(%%xZlDJtV0I|7 zoKs~*p_{F4xV%A?Ejoy9#CBA+D#)DNp(J$ioM@GXS&9+64g+ly?nFd|^S@vi2A61} zT4090r0gG8vH?Ln0+mu`Sv_OJ7bH?HFQC){Eie`Ofvk~~_(Z-dAK+X|ID?p`E3Qgl zGBBGHB0Sj2=wS`E+&72_2(ih~f>xh${*hS9k}kx&Q| z6FQs!wFDrWRQ}Qln7J^HV8pyi=TQwV1;%>lt9}^U7W}e zNZwqD{}3(P^KkBuq2c0m28p>0b|9jSeC|O?pow50@+FgRfG5sExG)Q33NhM+4!)Bw zFiz-oI?F>vQ3t?#K0zp^CGZliiXzPl@L>kP*+hypv_usCPcH}C!QQ11C`%yDF1)~y zoNLF&!~}R)|Ao{{;~k{IEe+oA+M1E*u{t6M)rg713+U2i!clO4;gIYo)nWkF-{fMt zv4SQxwff1j1DL9S%*kjEby<$+Da1rTLWaR*sPj>~bln(W)DjV1128z8g{m4T35fte z=R0(%IOH3GoBpd3hXXJ?IwDtNAe(WZKHt%RvhKkt;iNWFLd;W`V?+tHw8NUd5NxyT zG_kqd#M&9eK_ifMLXxOp+(4Vm7q_)xL8#OJ%sMz3&sR>T>UbQx|ntGu>xQIz3QoXoiB>=Wxa zlstzUsdN%0qn7X3-*6g%i*vr~8WYbH^wYsre0Y z=UA@7t%YHS9VRolxl~;-Z0-YO%zwdG89Nw_g^2^#X(7FIGz*`2GpQNpq~Q#nTh#$Y zct5_6X1KLTXNChO?d(BfFb9Z$*u)jWLB3G9{=#28$%ZDNPd=spNup8JIxn2mj)fuT zEVW#SRUBNBQbHB0VI3b=!UM^2c8`sFR>7ArszxF6CkBjQeug&SG>O5{9?m47Ic zuv4WNLL)nM0ia_{rtmZxsXprZ2Wl3<7lqn>BYuM469w3c5Jcb9>tso|04|{u5&<$g zP~I6qwc!FjSuhP|{ld9Z!jTZi|4BBwHQq-Ft0uV*ra*zr8nONpWk8<^KrDve+K%K- zj2Um?HGMrJ95dixj9fwumT`D!(E>oI0IdP4IxVVZa+!QbUPUi9>|G;@hv_{8X}0zY zO=%J^`G6;DP79)BumTtlC=gjzB7DOO|2(_36?+BB3 zVz^m^@DuX`SttLS4pE$0n`Ni?DAvWpu@&Q#WZBCimjjNHQc6e0Y|RWIL!HhI>p_gn zv5D{euF$Ap2(E-)Eb45$GTz`DqNLsf8f367d>(0>X0@t^*9Ko8@C__u6+=58WRn&4 zgY19{&S~F+S_wb3??-5W1kU*y4_G>6Jm|1k>cGUi=$W%>w@?rg4(t|OW@-iB zVa*i3F$$Sdx?!%Pgdxr_AD?g)5e%Tk-S8J<2j+lstdE`05!~@3*1zhhKnzyIcM%qC z(g8tTP7kttBwP%G;-p&LbzY2}V<927F^O=wKAyp7vz?JJjfvtdV!1L-N&~}y6hso)(3mMGG+lsNJVY7r9LDb-{a)ru6f$P9oLWzA zqbMkzqw#$SDX15!13Vo6u-tHTfH3DsGEg!g%!NQlmv4ZD=nHq_m9b`Dz{wtqJb;?# zkGZFvOasoW4iKRsT}Cd0d!Qy;rFbisa%mRj8Dn>{Ue-seUQNr1P%-&k{gQS>gD@UU z2z@gJ6IW0!UDUfE4)eb_2e!obiErz+Y;6{Zl^BmsXcrMogeE=#K%)UP!fj!H2(Md) za)S5F9RbYvg_&N~r$-VqLWJuFPzyp>O_L)6Dn;d=xB?Ca0tD-2{SAqt;hHW+3uAIBQ^$}Qa|_G`Eg`Z& zOibW2=q}q~(FTr89RU8@@x~PmLNhz~9!{`n3~9jUy&UZYM54mMy*Sxd!4DWXy5L9= z&MW{7F(CyS(vZX;b_~i9QYDAQXh2Y@1P+HpF#69XEXHe9i!}jWS3a`%fLqFJozHMx zs7@s?6LNwqA?d6jf^cOzTv(cqP&)6>7{3Tj2pFn%^{;a!jE7*eMT-FukZF77e>e$H z<(6POiC5;Lf6LhIU8yXP78f@(2_yZM3%VSG{tyD%JLaz=LU_L7C`XJb2~Mj&rz|Wv zK?Tlr7KAW06Og1>|71^w+7`i`hJcp|I=r{Bg0FJAZU!mEJfJw1Hvw5v(fP9@>0*== z|Kkx9mAEQx39-bM7Ou)E_yR3xg3_@|&m28}nSTSh2|P?p#M=@#+8GJC4_9Pv4=<)T z;{=fp*6vu;hCzvVPCTG-Ll+y%C@9f27IM*az|x{uR{I1o$KdcNkch(jng6wUPD~Tk zuKdTkgc~n~TNfi|Vols<~gqZ%u=sAhi$MdWu!6x;3>EE4eakpdGM85PkV+!ydy#dU60oZ}TQMw3&cY2!Gq9;i>#Q%_sk=N>kRs_l=#Ym@7xNR8R8k@^` z9KONDYviXRjz%PyhMVIR1?YftX-pWrgDc^3V4>mZf^@_{0!IyevHmmrWRp!OQHC$^ zd0VNj^^>KMhOwh-{=yG80bRM14f&*ZpzB7?aRl$A~E{Cv!xQ z(ujq)`CObWJFcnR&JFU7xrS~c*cjHof;JS&w2Fyrb3Qmoa+P4nbZxHDc5 zyaHEFpVG@`#J1DGNizbD8_x5!{*PCH$?|5LP}VGAWi)yh`l0{G$cSR`FF2d71AQd* z;-};~e86SK)(FyYD*@0DEr^3QQ>5%fK2c~Of`)<%S|fYl^>&QSfUf!>D}XPy<{b{} zgb*J7qZ&;<)_DPfRV)6>90m6Td>JmL)usd_tk`_UjphH;O;&rlAIijJ`P77O=ce(k zZ7^}bK)!eC0Rs^wE^>qaIg!9IBEC1_V_sOBE9!ET9I;HY$ z5KBOXQ%q)W8b!iT-~>h$prI#mRU0Q%`vjMLFiRBhhR6n18M`dd#*@J(C6|N9j5&|X zle9&T@Do9CT`J0$^^dkeWw=x@v(ACt!%C*m7r=7~W)ldil$g`y2;`H363~GO7|R&V z96(!zSO=alg;TUD-r8E$NFbmYhlZyhF!1d2pJrs2Oz<@-bKL^rfkk1jT%N?rxU6gd z6D%gf_1iK>#HUgqVj3_;JTZve&*(Udd&_=tZcgL&02sQ)VG)ff8K?>Ue|Rpi8|WvO z$4~fa#~086tMNyC*Jogy>9<$psyS#e83MCgFPN?O9rbl|A+h`w6T!@;yOrsn6h>!9qezDPC}Zc zm_Q{qqeIt+*8yx1%L0{94NznDLp|sOAIG!p3-+6b~2%IOF2=P|#ROa80y`Pm3ka3kMR$G1P(qq*(t8NP_*uJ3)e5kd9p> z2N2158Aw>IgSUc@s)6{HB|71(DzmFu0AukG?Ln#;WngXMD#+tQ3TP37f}Oe**MdVK zKAs@{QI|`3)mhMm$wuyK360iNE&}>|0~Ih}qvwNZjL%9rJpV!T7}0h%U4r4t8Et0m zr#UeNS`vN;FqDT;G)LeBJbwdY0}8V4flG(vf+HfQ<+IZ)mXgl;BNF10j&(FN!>t%A z?2zctLtsuE<6;i)FC^1F35IH!1*%?;14u)`ZEajpz?vrzgIO|!rvGIS zh|{if@y4LMbw8gX0GC0l0FM~p7^R<t*ttQASfqXgtOPt8BNzpd7n7|<`Mwow9>o_2@QeqMaXbVjM0a`F4YdB1PI3HbN zuoV*o*TUM^!DraBYX%^sh;7P%W;tye8a1|c!`fK@7ssP z2$%{XkE(GQWu|E6JM+7XY8V2ygOCE*&f@9Y!nKReoOqOwf)NNErtQ2MUVw7|Vz54a zPNNb9kQMxe`cJ%f$uUW(ax+*i zhu|~ZmWV-EM3DC}de@CufcWU9euw>CdP>3J(o@`yejav11_6@r3c<%^!7gDGe&YfI zsvoS4Jvl)lO?TK&$!h`-h;PxVzR*l4VlfQJ3K-)nCJpWa-@wf5KfqLxn2JJe!dd&8 z%1A5W5O07XH~`Ee=Nl)05N4$7gV%$S=mYv;ZbbC3%nDUAAP6hy!O)!RH8mSSI;-tW z?7<~i{|7Kgn8eJFqi7TKyCOOhmXCSnisAySK!U z^$`#X2HBA3_PQ$P`L{K=eP^aX&)zmkvQxG>Q;g&wnsF~8KM`#P%yxTdNB_MMFPy?G7QFx z`oo;B#W?tq?Rw~_(j3Y>Qd}Y9$5m`JF>)!S96!itRb*IMei|E2IJvaK`BGuY6k=IT zh!Y>CV9Ww9VA%@V@@Pmm!cvkTE;q;c#6It2&AK#$ie@+z3jw!W!usDTAn^F@krSzCb@-6RRCIXjsM9$g0bZm!Oc|x9{>oyR!Df4ohAa@eh}-= zrN%yR{Y+i|7B4h(ZC+v7|;>_?hkm%i+C? zDU==n!NDNYc!ceAyB7i}%E3#VreFZX3s-Q2<~@$2{?$9BM)pe>j{5Nt7r`1oCjVO& zaRG{AZIACWUPy+QVm!hoKoOSRAQGMq7WhB{--j{NV8KURb_;Xlc4kMaeUmgAL@xYG ztvWQT3m@EfDVoQ@idp}FgE$99Cqqc#5{xYeCQc@}KNLY2itwxz{b_bLH^Go`CDn^2 zxC!`YwhaCfH~|sw1SJ#==VOdS5H%tGXB(q9!T$f?I~Yhu3uTJYP84xM_}mjno4E@H z(iAL<58+}KrqT{3*Mt*{V-m(<*&Cr(MX<5ygW23Cz_q%rIiKfGR^|fIrf@?qD7T@h zkQdO72{+E}83o0_{OCY|cm+D@?U>8nurcrY02~{8SZ8MNL;Ta`vfXVGkjmBJ_&*yF zbZL2QC1=0e<_T6tBPv_h)MUgpqrw$=Z~sB z3xK2zBlLWCdc!#xw@;1B{R}-d!1J$4XseK1#|c#aC{I_k#bjpZf&hj^r8jhW9^Fh{ z+4bhVXvytSKA>bb6k{0?#yy{s8F+6~3)2~>c#_(6>n0GD{sSjlm!n2($^jBjY|48* znxyG$A4Cfm60xBbyr%`}$m{$?2yVOLUnjLz{H)e{?4SS34JfK&<*{=1p{~12VAzi` zyM1A#INGi-x2I{mIMObW%^8zYEG)NV`|t@J%7ot&0%gs%en&y+gj!ZP-mCJ%NntTXJ|TQRP%%VDOTZ4UDr{;tAp4vkDvhZIqFmqXScNpkETNNt?@a19o1 zZ?c^;a88fWNA>%Vb%8xl%ATrakOjG63v7!7qrlKfTRi$YRb!+3j>HP%et(@eK4H4TR1cBV`r= z^m0jyDGCS&-1td?lMJ9C0iEMy?0g?ox-#1wnuR4$uDXH@avq69b_@Q=H&h)q@`B+v z@}e^9NZylfh)jqZ-zvLv%2|PVX=dT`|FO*>N2p;_t>-V#SoEJ@n_w+IphVnI#K06J zF?|CT4PHBI1PjHU%sG=wEoiby9HIz6aXG9$!Lh-2SaX>@Ue3ySbg;cKL#@flcmW4x zkUU(7JjsR3NxFp{2v1y5BX}Hasp&crDo8TzF=JmA%N(jLC6FW!?uD?h49V! zh-GGIFVRTTq-ZkX{(E#y>Z&w5;;c%wd9bZj*~&%%{WS!y&C7J=oJP@&reQ21o1+yG1P83%T-7ZgVJ|f<_2e{LE4!H0Js*Z*A&^23gAc>^(dTM*{_T zEd!^1glp2t;=TXlE{OYkwmIayfS@{Oom~#Wzbs?4L4)mV?c&0AT5u;gIq_jpFT0?n zf&BJcn2$hE!4zdWc?5LhjQEVW0r$cuybi_HZ^F^&V!8|{wpoJxwS~~)8hV$%Ft|Wr zZO>5V8fN-{h2J0E<}jaIezVJ=>#wC`+9pq5U9e*{Z=wmG!_Ht*9*3tmSM5)iN?|}d zZ2>uXkEhgvR^y$k(1+z4XCbh@*QoP1LobOhg1X>T;vP5_MDlY}58;JbYzRr?YN`>H zR1;?ZYnwx^s%dkkaaJ$4%OUfxs2pA$RE`ZXL}>lyFj^?Kw@w{~8C*r?0;c6Y;}&lL zs2Hhw^AUM!RH-8x0wr-}NN=cT^CM?*C@SHe{AB%o?qjewe}HV;PD1uQj>JuMEek{b z^3Ctv<}eh}7z_^1wJfE0X*ZtxuRs~wu4*OY$^hm?kO_K# zdl3nyf$)dv0Gd3`S-2SR&unw()RHhqqYyo^oPf;XT#oBsa4?$y_?lG+QD7y-pHNkZ z@h)EbliM6ptb|8wxQW;R!q~Y%5BNW{KcrK=-BQL!`B(Kehc*!fC?}Deg{F?fNpVj0 zUocMbao1Ydq_eiPJfnoBOk@ItxHB`@9?L$3Z0nNt7rOF-tVG1)?a2e%x)$(kk0sl0 z$Zc~dtaB~NspA&fz)pJktiO`S(KYZw+=QgVVazAH+5#rzvx7AF=sP*TPNyc!uuy;L z8!Nrdq0gGxtq!wXToc^#eCHqO4$w74enNw(48RzPXsNE!Z4Sv7_G6jbwv&qhF8`rT zO8+P8Zm8JlDW-L>hI6EL7@-nECh?!`q*9^s9qCBMEaSmP=|QM|LfBP&P7s0)q2)`a z6Na!h10w5jJ3p6zTSNo3wAL~}s6~cOXpPJafvRG5nM?=LX*K>hJ*s7yD)~d(9EyK9 zl6`!ahL}+8!?OMi-$z224Ymb}!c?bIUknoa(!w@m%mj>B)^BGSzr{0Lfe6tQOH7<@bEM8^f<$eG#2#a0@%b~(iV8G+RwVF&fcPh=f2H24a5Gdnf z;eDykA2>UP^tPlaF|yBwQXYYmuEvA&@SV?%2)PR6f(e{e^%=ojlzI>i5heOG*H&-~ zZTt!!aOIVB#F-4pDF(tF@HNcp{<{ccX@PjaZ2+Rcl zD3x!S!XPU!%S3QlR_Vm`Y;$PzGDtWyniSYwEXc$u@yCq_43Uq-;NAgT1PLYIBoP$6 zDz-V~vd|pn^3ba56YbqY==}FrnEvB`i$UO4;Kl7!7qdtNo7CHsWi8~4f-?^q27^G( zjbLY=|K~P`j5W73%tQvF=XW^-|6Q8GR4!JeuYr@Sj%^PA<1UBXAGKo_@f}D8mk6G~ z(0IjDhZ1h=Yke#Eq_HG1r2{KI04AL6+=RXZc%Wn_e59{7AShV4HX)6XL+yZr@}=Iz zPX?Z1{@p1_pa-HA=7*dH;Rakr7bG>&rY2o;Gc!Nm@P>0lat&mc%jE*1*ni&U&;>N| z3i+=KD1cNPnt2K4pOlf22p_;JLB_#cjD+N+aYq5kPBh38P|;2hkY~SuTW1^pR{PG~ z!*jSUIhB}GY;Wf?EG``7b3jR+VODju5B)EKEX2P(E=wpbM7-K-w*C#3ypdp85s;t! zK(JEXgOnjja|YnW=qyOBi^Yr;w>b=x>v)Oq>cLD%C9g@e0J;+er z_rZ|-BN=jO0}M!~Z=_4}0tH*Olom~!@{{Y8i^xRfe;q&^w zwTam{%od?MtK2`a%^??eku>z3Af;7J{lFeTu~BQ6L-`;5KmIy; z*Oh?Jur&}(VCG)LpG~2*5oQNcZ z8<`DI5X{YBBz1v*JZ#we_y2sqG1VP=psP4QMnPv+N&&E zc1o8GJ1vm!bvl_NJq+()NWRVqjIn4Yu+Z_6Aqh3bQhJHaq-4LgA|8=lQK1UgZ z>p2;Xv%rTa$l)-QWPG3P5py!h$s!f+7aTELI0_)O2mW8SIpjFLCm+EHsS$ko!dip< zM~E@<6%8o9goFj#NS(oIGaQNw{eOa!wbfvP@ga~nKje~b@tmO%h5v1vL$1NuFh^hn zhRLyTEiT0VgYt0VnF#j*0O}pU2#hB|5Y?b1!NLi4FaE?fhYZ38tGR>KKX(w`R6z!e z5Aq8j_&i8Q@g?>v9}K|ddt}Ezz$h*dkM5UZY6kSSa@!n|e8IQyEIYa*0xaWgO4@9*u-1z@VJFoy)nWOq4a{)4g$_#q%| zuaR10^{aZDLz%PB_Oqf{zp@xH1nGZ+$L3^LixXhHHZQ?+U_Q7Ap6~j^@a%=X9ulH- z6gklX2(}=A4-c%c%OUH(WkgyZo+Tp5?&#BF4rG*sqaiuspW5b-awN=` zTB&1jhKL^!=i&Jys#u@D^T{C;i;}5XA#{`ptRW_B@D9(QGbT&4B&6aC^igIA`1vRg z{!Lb+Y$1(dKrIv|8?xIRVg>@*$mbR?4tfX%BK-#WIIbzd)C`OQ z7thGp!6!R_{Z73zdJ7~(bO++_@DFl2D#{d4$D%Q;I7eG78bk{w4lkfj`DzdwQD4I_CU zb%6F^RIbEz&6l-VuqkxoPi%8Y=u!8e1>1H`X}C6H|{29p= zr1vqv)wj(d)JBRESfuSYqA$2T>!1FTZ4NP*Jpou!DD=$$AO^ge=MU_jy7s}2C737U z1rsII*@s=B=b$yg2hnnUn_U1Zf8rMs!a%|dDuZ*4C@!Y=kj$xY@T<_224(>pfSx8E z!07b6geUBBNdF(D*yCTV+Z@{3pmK1M!CoD4fQZz=$&AYn-ONQ8_i>lNZ;{aLdMv$H z%{GT>v))|}`S+*6H6P&tcS)PfPgtFkjsXc=hI~a3+P!m;63Q|_Q8J|piiYdRkU3k$ z(zwyf306T3okO~~C?z*5{ zZJR@GV*5#^3weM`0La80{V%5n*~*0+;QXvj2&mpF-{#N;1{1uX0KU#paX_L$|3?*8 zE%Ke!xXmFZ7GX@>G64D?bU&ShXD8n3a6qsZ)>kai)IIe388O+TKWu6-E9sj z+rb%ppQ$r?#+Iv?1YX6zKPSec6gplMUc@HR079byG{S9Re@L%ehH`@UEVzOr(;RW8 zPp&9KCUpt+t{*@x@LQ{Ca)iYKpCK*cqD#79p}qhQIgS25T%2h^3m6VJt1~JBgH_=u z8+iji{?X-bF^h{i6_#utPSD<`;+3v|MYH$0m^gd0K3F?c)*^m$(AQE6LYbIBZSD?S%A zT#(25BOkJ&S7q>#mOwe&1If6v^ArRMun}?=>L*j!ngdD^lS3>fT}tbdivM++Lx2jj zB=PuIM%bqvQC)LD{R0;v8H%7z4k{H(M1UcF6C#!ke}0=oC->^z<&fupsK>YoP!slD z0pn4-&;-;RN|wEWfO;?a6JVuNOkc!RAf^~jG}Bq8P-D0< z$W>IecR6JJg}}h=xH#OOv_M6JHk|bX{{T?S=0Cg5Aam%|Jb3n+YbZ*!P^u*oKrAVV_4<-;zAJpVvywtHot*EN660%Kbn zz}bXJXUB5Wwuy^}u#YokD3OdpFp=Fjhmha|Cs*wLTHkY3j&MQ)b)#9=kcn~JCJ=Bk z0bm#Mzbk%3KS_wr13XYt$p?r8ns==70S#PY^+&fkgmEGk{DDAsiEFkEY5&LNQg~=~ zp9z=Rtd?yKsr+zrK-iv-L^r?7AoE{Dv2xUk07n7w6y4Vw~>KAVL# zZN$^Z|I^zX;$a{NxFXnd4}zOln1TK$^ix0uRoxT8bqk=8tK1-6J`uJmwmJNdyBw1L z@K6`nJ2!=I!u=qZVJ>tPZgWVCfH(Nyin|2G&Aq_!ZR^&!3Tg{(@}|N#M3!O+OYw_S2keqyP8UY;(x$GKoj*hwkEtHM<^-Q_^fE{T@Ly82i-%58}gHQl+)S~<)=K?sVB0c zLc|%^`IqM2G7`kf|p~$GKe&dHw@3z~VrD!E>$60tjO-4x;h} z6;{DEhqzn)yBuaLq45oF=SmKrZ&)4M9MWV8%?qL*1Hke_Zov`5^N-{vr&-)%%2|Iz zLSDCpbEzfWN}x$n3?AAikUDXUt2w}GN)tOD6eC#4CA+AXT7t@&C;(hphjyZx@1(Btgl+fpjUczkR+{wap>uVogmc%Tx!IO3f~Z?!V3| zFETO}I>9BVI;f4S!#^AeU`GzI*MCb zKOv}C4e!Hi1Ugj*W@gC``u{7oIsA{i9J2qzU$M=heWlQTw?X8T9c-JLnEnqgCmrTt zBRYZD$ab#zkOqOIoC#Fs$Nuy-hpzjgv*gnSb$2<0|FiBRSAMR(Z4SX#$R>n~Nm%QD ztAIf3*?;%1*yfPdoh@e_ukJ2~^gsWKZ4Q09q~2W)Ga>ZIx~q4aL&&yi=eN5YGXHOh z&5}bYC?2W9P<K7{Ey;M@j(p%u4Ew>jh<_3v`X_<>mH!Rp=S(0vqqOy@Vd9P<1lTpWhZ0X{sq znzlLQ%JuJZ$of|&4_tVNZ_=4`p%nU{z3Kypmdic?`)9W~bV{<`T@Jy2J(BjzYTf1# z)#v_FY}Vf8Ftb;aCg~C7ZVl*@fNU?A-%PMuq`-aq`WG~5-z`=UiRWLZ3KF^3D{L{C zs&#Vr0jUDtdv^B$MFmCK*DIXCrkjZ}3v+k~n@xt8s9Xd2=-#b0x$f_X) z$*7tbI;<)^s-S(hVI@`RuIaMM@-&|g96K`2K8<}!k4l$S@qWOurB&ghZk3~kdltXK zF%jN$?p!&h02Yu{Vx~#^{^`+`$BrsXkM`4?%2m|ps!{2Z;Z4Rg++|?nfo%pBHt0N{ zVAoy;G#)f(z>6F9*sa(8yR~V%-?RPNJiXS_%}+aE%U!lyqh!>SQy)2U%5|siyI0?% z3mR;5{zk(!Ztge-X~c+1Wgl43fo>dSbuz^^nC(0DRVw{LQ}%a! zGML=^&XuFeQ3s#o4BWm;<%p{Eh^o;A9Xz{hr-BY)V`P^&iJ#0ez_ok~gJPaZd-yMR z<4s`{Q1p~;1#y1XXmCJP$?%bU5cY!XQ8Ic+LHl%JNkvJ-UGF2srB(`w(nxV(F$E=g zESJB0l`Dw#@u_V;TYZ@%?XG)Y9iA+>Wa*+NHF&#O9vTn=#mNe{vO>d`|!cC&Dm3A(8lo`a|4FA(&Q^YXfS z2>z3m{}1RPQw*jDW6yCVFkfOIbhJx@~YlL*y)+qJ<>yu96~Jmx+f`g)W}LHMnTo6 zV`Ug!Du-2$8Zfe?EUmlb)A4})N=6^;2f3KO?6Hj+Hp_5$y)>mx9x6Lx%eA|-U8_OE zyBajCmm>t9%!T8BK-%D;`H|NkEsQ!(2J&M^!g`@2Svz&*Vr%79_a8Ku_;0F9j zUUjKl=&e%K{qH1|%;S$iQA>5Hd@^3Hx>UVU6?`CTp7uSvbm?3&I$d6XE2OfKP-lki*6+u^Wv)|uaV^L5raBeU$`Qyy=*v`_i_Grr42 zdhGMnvSpF0mmS{Zv;$sPy6p28mW?c&IR4oA%a%1S=)KN5k4;S7STHQpU~sb?POab1 z^EfuR;CJC)@aVi!vk4?s3viYv5M3@?<#2~OrpktK9a9B&%wP4tnkrvntdyl%1?2eu zZmJL*IiJr~bt{+bLj>iOnln`nG3!zpgJ*d`T9fRfMR@l91u=ehviut%Fz1xnzk6ps z{@-|e$3bKNw%`1D^B11~?%O9;e$(NF8^=ysICj#$x4m6;WxV@7y{;d>>CXG~*m~pZ zGo5?Q-RPygx9iz+&Cb`oc1I`jIZ=MVb#8Z&R- zdZT8|&MIwKu=DQQJ^$w8f1C5lGlLs1+qz)y1|!$lYv$H_pLIcGRMov}f4StFe}0ge za%0IY8{GNS*YUy2<}B-TW};2eq%|6R*!aFRPy4WKzl{!Gr@;;TpLN#04KfY(X!Nh^ z=dK&sXhFfgGdFLvZs%RQJ#pQmceUQ2(bC7`lZG8L<@wfqpK94)_tQ@~<^H}6E*~VU*^lk_xNh0w z4I7Tn3}0uh>ZP+DJ#qYQB~RV->W`PtIpy=_9hY`I=axBLiw?vZg#@A&HJ3u>fi@E^?qg9{%57em#tgWVDHFU zYhV7>-p@9g+M~4k%9ko9x7?`FOQ-Mk^d7CAeQnLsAuTs+G5xilrk^yhSJgis-tFR7 z7JYQa+Jk?6?2^a6s%Y1@Y4NrFJKys0fg3$h-1WZx%_e6K+3xgh`u_0uTfX>yqgba7 zTetk6^Mbwa`KDc~XRBK;du`D5vGZbAjgFsr_8CK-XnpXmJ3fEa$L(M3e)}2y?ijM< zuJ-p1?|k{~t)9GVx3@0Xh#!66&rQg>yu~SA2NM@hnMz= ztUEh?W&GFME}wJWv3>4&bMtM6cA9w3-oLcjYi{OWect|Qt^41&cAF)qHvFLB>-QeJ z{?D`Snzdum%O_U9Q`mXt=9f1ZP_Wh}AGByZ?bMNbpA?@LZIt=-wn(Ff1x>3@J)&UQ z;ID6<@Km?`;=3NYcat9*Tz}pEzsx^wcJuolD_HmGc}sg`?z^PXcFphW*?hO{XT}$8 z*W`)QUV7@2BZid>ZCsf?w{^FF_t_(}-Nzj}98*y=;Hp;7Z?n_G6Rz0mhntu7+3niB zH{AY%7LA6zAKUVT%^E%PNym4VO+D@E*-ei)yzGp-7QC@=W{Vl)Pk6QC!OPA`e0KOh zT5Ng7ftmAq6>c*%Gxm&&PMQ2#;p{ERF=`TL| zaB=0i$FBA5_$A%DPwn*89Up#H`D@2X>&!g(;AQtdFn(O??ZymV_eDny z`P-=%ZrtIH(`GHWz7~xG=BD!#K;CyX0G?+W5+aIv)_fsMtiKeRYCKQ#-H|n<2GyVG;?U*^`0NU z@3m{+JAVJn$VMF+{CY&=8K)ebZZf0MH%F{HxuEf>-M8F0`uKT`xBu|WU9UfP$o5Ol z+^Y1Pq8$!9v*RXbcirKv9$Vktd~TPsFN}V)Y5y}0={jZmt9xu1J*7kU))!8nb7c8e z6S_sNeWUL$ZPtCY^~J59Zk#@C)IP(zO}M_aU)x)DeYN4hwzq9^?B=hX`&rjL_uk;T z*_nQe`<*l7l$*MbzIMV(Uk};u-gVz<`1yHFJ3V#X@z;-jY1Svbz8(8*qUef@^S zs$Q!4N7byV@2WnpI;|>JwaZ$YpIX%E%J=tfy#279Hr?p9#`7AVRNeORCKp}Orsu0W zJh;gdXKnRl{}0E0IQ+vlU#++2o)0&Ec=Lyw?Rm+`Tb$JP*R_|7`?gu;+m2iISU-8U5|E%>JZF$s|BcdIS{PFQQbJ{#MWX>ngT|RTnDO1n?YV!qKE;zn*bmAHlHyg0g zfZYca4~V@|@JjcJHSQ_8XXATXj&3qKGP>F5mfvi%-uQh^omJiHl=M#f7C&>qr8n<- z&gAj~TOHWw^)|1c`1%R2Z~OW|n@rlVQ*?*5FS@(O%{{00$V@DcA9V4s&Np3n{KVVm zOrJYoSW&WPa+hf%r*)V%de{YTeEeq5MYk>bV$s`gJo9GdT^EeLws^;)$wk{2Hz{g& z%=)8u9sTv_t&dq}^v5G7E!^g#UuMsGCOv1fXD@l?wK<6=?|XK)Cx_2&@z|kr-hKSn zzc+efhbI<1Iq}&6kBz?a$n!5dZ_pW+J$~gg}5|Mf5n}bRbF)X zDf3V5c>1z7%XV8@+^}iGF5^dze`1Z#*Qi{3?x{;o9r)N2r?xnCarGM$&aK{lLazxA zPWX7jPu06kXf~nGx*az;Y~zbIJ*L^*O^0qewr#IXU)ywpjrZL3tya6YZ{4P1r%PME zUXa{=|BdEtx?!{S8$Y_s()~L3f4ocSotduZJUMyC%l>&u`Pt>CeRRvFH(We^;N*cf zzS?Z)RrlUhRsHQTpPszc#phnp`|{oo^uA`-UI!j}>CWR0Id1Q!yWHGri`@pb{bAsw zPVe>DtLGz~PVPLu%VP&_H{iN`K3LFt>VEHC*(CMuyWbAzw_l|H)Lvgr*rey&gWC@H z^oo~{OmCGwd(;D8H`}b+W}`2u%3N~bC2JpW&}&D$*5tJ#k19{>ao@v79eUK!pKblw zF`q4bZT?Yb4*l`yZC)93+uWNzz2zTwj_tGF?di#vOnL35@45^=bdOFy|NZ-&T8zEA z@zp0>ebAvj-#y~po!{-b=f)2=czD1G3zl?VvUJH#-#xWK^#*IKzw-u%H9vdnb+$UN z`SV-$Y(Am+)rloTCJ))>$P+WOe!TLmX>*%Syz-_Y1J--x5PWLKSn8hWJ(1BHf7ADy zb-o$9UgDgA=UjQ|-}Y_(Oy_5AKJb?VH@$uL+s7Wg!M|I){%&&pWII~7cMeYvTQKa- zH?LbX`SYf4p7+){Z+CxtdQpQ$yPVObY5NwfT9mc8bB_Kgp zc<8kIUwvrT2e!^UwE6vyjGQ=X->QF)I`X(5j=gjAvX4tX>-SOQiwEY8K4amU?YC`z z+o0Q1@4fx*LvM|H>8WQIp83MXcV1CBZSj{CbDw)V`NNN2{kYGPci(t?Myn(Co!Ig9 z`A~`-&f9d+;^u}kLdBvIE^|)Zp>^2X!S$50qFa7hAGe7y_ zjx`2PAKv23i_V*S{#F;fcK)85?9uYbW`kN>@MPurmHQoWTFW;dKCAy(<2Kpp@83>* zdg3EHJ+xD5c-gy~KYYjW4?X++RiDS&e%$uR{*hZResxOxgL}`t_pBD(_SpaPuE(wO zMP^a2E%tk%*EMgYr{11C?edK;`l@h1Z`Lh$&~gi zojc~%;Y0u3Z|V(=E|_xZgiAKKr1@p3Z5Qlu>F7moe|7DG(>6POvxR-Wyk^cGQ&0T+ z317e2_M8FFU9!m~vu-}-=860F8T7!QclW$u>`hayxOw+GPq?G?gB3Gge_+}}``<8S z%A`9VxwheJ7ryc3_mdaDxpeeR3lDzbnQJ<}Hs__wk8gNLj~QQnas9GcA7u7TUiRP> z@hhJ{eDg<7T-4;JUL#^J&vAu ze(lz2kEwg!e$vA`+*tNi#k0>n@cfI<&z-W{q3uVEeeZ;qKY#VtPo_Qh`MBesUHs02 zM^{Z>FzLOAr)~TBq}zW;ES}z>Z|22!Zhw8;zpi`hiCr#uwPoUmp_y;*`~K+f4sZTq ziyn~T1gvv9o~dhW1G`{T|l z?LPYOyJzgW?SkgFpV;cx+m1Qpq+e!Nf7*5N(U;9@-@5I@#q*zh^q{MszM|r)c9S-~ z>eed{m^6Ic(MzW-yZVKPZv7#$c+kgpcf9N5qVM~Cw%}iVPS0F(!oocl-n{vTo4?+1 zVdjIS{l7WjyQAk_zu?0+`oH?})6?Hs{LcKNGeef%|InhT9q#%2jI$5l?a+ybJ@ox6 zA20mmgjbiG(0lJgX3n18VCze7xp1Qfd(CSw^OLn3JkYYi^)KA{#;`7Ho?N;^-$td! zcWpTKf~QZde08th$=A0zWA>giFMD~PX2Z^U`P(r|iZiEon!Wi=TYr4irQKdXu>Tf6 zH5}aW=h*p!uDa)t#Sd&T=F+kAe(bl#m`>MkbVFg!>K&inZg#8Z+dkH6?oPAX@3!L( zJMOamPOaNtSMWg3{rmOnxz9m|?lbtcy`Ss-Zm%bLy>iGihrE5rQ(fov=ric>xA%Un z?_)h4zkGi6a~BVJaK!hsPJHXb2hw*QaOJUgJeS$-?W%`Hj_cDX);+OP&m#}pV)H?} zr|#Y3!|`7~IcDP-2c3BTNwbpA{M@s0<8QWD|NV8#Z@%!f9*=#r(fNJfE8O_DL+GSWd2D>a ztPvMYzGK|(KYaN8Ss%Aqxb?yZK00GjpG~gyLr2>N}m1l z?VBHdVe4zQIB&-pBU`?9;@odW9Xo8%0SkZp$5~g+8@$uI?|riWS&tnsE&fgU_g}yL z_v1fZyu-paE!(s#+v{J?9e&V{-_H8--?z5^SfTwr*W$*GA}<%G0n&s#s$WZ~BH z`h1a?)at$VCtma8&1-jCyU~OZ>m9z?t(!mB?3_)fUvl9^JN9^F;%664nK<{78=ro2 z&NI6d?tJ1~@oUE%bZgh*{U%)h**_~LPno>kyl z(rd)yUzBe+XV(_F2;6t-I%S+^wM9g}v5o{B_ggHaMoqeH-3$ z-CNazyM5Ai=dSB^zo+uX3p&l3Jp1Gee%|=eOV8YI`&Q?Sc;&q>uGr+7gRgphTG{Sb zpM2R7r=NIJ=DiU=PoBBw+&6#z;lRcJp0m~5UoU-O+TlN)GN9A9kNrAfgMEHz`f%}# z!=4>*{ehp2iC;gg&rfHS-SO1VyHxb~rNgWP+n#jrolk$Y{*Aj&t*9K{w!^{`n(uY> z4p$EvTJ^w2*9;zc$2)`4Q-&TseZ)62Hhy{1ix-_7yJ7V4^Ck^` z^WAZo%Eb>&9G6=5-qK0G{_sP`H|AfttZ;F^{pY{8bn^IfA8T>J#7R#!9p2*nu~&S# z>vum*x~NP0ox5Lo*n3-zTl(1(YmM3OicQDf5WlhG@ITDoZIp35rQ?C@>tZ@c&2>A3dKubpW> z;qYr8>NThJ?yr3F(gl&LD$Xr^xwKp4_`A!e9z5~#p1*!{+OVdJo?WzI@u=eG9@uEg zfnN;Sw%?>9XN)~@$%!q0d}i%k*M6kQfsGGpa?+-I?Rx!g(VZXc(!0mxL*MCh;sMtW z?0>-BgLeMgQ3ou!@Z8D0&zOA9qI(ZHea8L2#3uZ*-|QEq=~Fj6?S;D*pY>eH$xolW%is||XSP`9$8~x&YS-_< z)0(c`sQQ-b(#~f*@!74PelTU@Q5U^2XHdU+*Y7*M^;2V>JnE@+pDg=u;U=eVvd6@& zC+;$Bvtcjv=(+!SJ=S|`>e!o)eDL4x7OsDD@0;I!{-7!2X55`xyuo{0T-5BED<@5F z|LBm+tnm+?`tfPgn>0FmcGHtOoZ9x%c3n-bTlcWD`V9NW zf76UcEpPj{?cwWR8qeH+$E7cSyll|<=YPNd%*?H8jx2BA;NA}Bjc(E4rz0C) zSUoqf|CZx+kFN3lZEN>^eDekeEn4Hu``(-XL!&h&M!W94&M$kfGh~f@w>-StCy+h#h-RnFvY~VlR)N zh`k~9?y9V~mbI=G?1i=WE~wZm_QHEzXJ#NnATmSp$NQrsQ=aFPyL|3;&Uv<8>CnEj z&h0ViAK!I){MfNQkJz=%jjJY4uG^;TwUb|%^ZZMt+kXGp`8E4bA2hT3F>QWpeZyJL zOyA<+?`L28O}F~mXtTfXx8e9n=M3ty-i;lHoHA|hYQIkEuy=<$&)D~jU$*Oc{jS%K zo_XrbAscUWQ@cYpJ@v$02fVu8Pq$CJefAsAjk)O1?Z$mI`@D}no3q3FAACG~&Pubc zopax#UvF~48WkIUvTP5s8OyBNcIVw+c%pj!SzC5`xYJAL9dzBS>+Vce-(mO8NKYmF0L+KZ4-Z&^ZVvD+X&3y;nJ!`8uPkyl82RD6M{rR?s4jI?$ zhhaZm_QM9hb@}D-5jW56`rU?Yx-5VFE_x`#AK&%S z<4=oT+y2QRYY*A|ox{ic*6+rFZ(e@+(Vu>~X6oX}dsV)1^lw`|yVbiB|9GPPY3(;z z;6AyR6alT&=($j>ho>)9P;KXJznee zPM0tC{H)U_cilgE%-0|OG4O#yj{9!eOS^aPa_=u^Y~F9pE!g%oqx@f$>56OTUhrx3 z&P)GYwtM+;pY40=?{ELH+flcT82-zBFP$BKX2of59`b3Q(W5Tw{?Nx)E;r|ZwT4YU z_Pn+;@BebSNqv|7q)pBHy=Qjrx$cjpJ;rn#-tUTQTW!16Hosgv_4KK)m7aC?fP-d? znAv`2{L9DZR{!_bbzZMqyY-w&hdh4!`#rv0<&zITY5&O@D?E5o@2!`wUSW&qk}WSd ze!`41KiKlx6>54<-t?uFx;*x7&7sw6o_OeIn|)fk*ZAMh*<`J4uK4%btuLE<*}wN) z=k2xMKH{jmzJL1L9ln|JP49NkuTa0jSDj*OJibDo)izpVrH(^8?z8&WQ>LDC#+2CP zkEi^(@wfwiykp;c?>~FOvl|Q_u<7~}UcB+U4W|r#<%WZQdgYcwE_-F;m!dbPb-Q8q zwTE1@;kD0Q^U$!;>eolTJnYyP{`u4$k9~3PQIiJT{n2sLj{p3I)GZ?p`eKbQ_E`JV z%PKBgf4@8S{qpSzZy#~w`A6;l!&=|&|NZK3jQH}xTR-||bU;bJomV++<(pT$c;(4g zojmD)Gxj{|Kc{T^?q*%5b-laCgFRNATzl!h{~5X2fPeh3)tfVSxP9)MQHSeietF+P z?;rKzL(gqCXVewx`^RVA9&PdUkNVto8nZTV8PZGJ|I9JGt_klm2_%7Nr-~cROSH zAH62*`N)W!R*#)=&A^KvzWlw3*Z1#z^^RBdskx~vx#u#em49nLr~MK4ZE^f5XZ8E( z#GST#dEFge=$EaW1`!hX$w{=R+m~_BZzgM@Pb#$j^&%1h;`~G?M)BBD;^r!omd1iz3fK{%$ z{OR5Pe%y`!`0T~C_P=Dn-Y0H0;@QG`e7mKZlOr;QY@HtRJ+~C!5^3 z^^u1^I^z71^)(%i`Qom7zFqF6N56k+-?t7Q6Bwq^E!uK)A5Qw`rdHN zbD!+`+ROiW^}3gLyz;Qh*Iw;;=+*Df+GX;4ANBrU^xYX!$e`t8M4rath>DjWWKdciNxa@EH9C_zvbfBp2AzneSh;@jSsG^8fo|##h&V zvElDG?D74-SFF79frkb~zx;mZ$g*)y&F%WtK0if2&F#KfpEWLc?xrJe>9ohCul4M+ zX0-p_KV9>`=-5+Ei+%ZJ>yB+|E;@egeGZtnZtGSLwmad@hky8C%F{pmaOX>wZpVUgvpP_xR_Myz;5EOlGz8f=p# z*j#mZehD^P8}8L_c>TDYE_|Y6>y?_RhF#upB~KycoM=(Y{PODOOI*(>s+$ z6E$Bwr>JsH)O_`vqRKf@^VM^TD(6JaSI;S`oD(%)J*TL0PSkw$oTAD(QS;SviYn(s z%~#JUs+$6E$Bwr>JsH)O_`vqRKf@^VM^TD(6JaSI;S`oD(%)J*TL0PSkw$ zoTAD(QS;SviYn(s%~#JUs+$6E$Bwr>JsH)O_`vqRKf@^VM^TD(6JaSI;S` zoD(%)J*TL0PSkw$oTAD(QS;SviYn(s%~#JUs+M716`B2V`Fp4uZa5v?e4D8LpV&hcxTSn7v>B};v&%4#kb#j4$Ahk_)fNjWEk;f66)UQ?SV(CxYI?6&QMJWF zN{dm`d&P>XEf!K*jGEpnR#a`VkkVq*^j@)|YKw)G7Ne&3iWOB`ETps;HN97?sM=y7 zrNyY}y<$bx77HmYMosS(E2_3wNNF)@daqbfwZ%e8i&4{i#fqve7E)S_n%*l`RBf@4 z(qh!~Ua_KTi-nXHqo((Y6;)d-q_h|{y;rQL+F~K4#i;4MVnx*!3n?u|P45*esAhk_)fNjWEk;f66)UQ?SV(CxYI?6&QMJWFN{dm`d&P>XEf!K*jGEpnR#a`V zkkVq*^j@)|YKw)G79-9dUvQFPs1dbv$s@1g5j8pYS(eZdKUh>F09WQEh zJYI{JJn||YQKRGWTD;_uSMi7%9go-IC6By{N7U$eycREcj>l{9l1E;}BWiR!UW=DJ@+uxtqvP>fyyTHr@rW87 zkJsWQkGzUU)aZD;7B6|^RXn0b$K$nl$s@1g5j8pYS(eZdKUh>GRctnkk z$7}JDM_$DvYIHnai^2n=rM2(Ke zYw?mtUd1D7bUa>*mpt++9#Ny?@mjp(kyr7E8Xb?<;w6u~ibvGwc)S)bdE`|*qDIH# zwRp)Rui_CkIv%gZOCEU@kEqe{cr9M?$g6lnjgH4_@sdYg#UpBTJYI{JJn||YQKRGW zTD;_uSMi7%9go-IC6By{N7U$eycREcZB zj;tTZlUaET5+7!tdbQv&X7S2JJ}|Q)cN*V5CFL2Sm1K!mk|kP6mQ*EKB9%nh#!Ip- z^1$B=d3lkcDlf8Iz{*OqTbOu!aCmYeUuB;1#EWcyWo5bB%W}8##62IgSW?EJhuIgI z2{E;bX#QuJLkl?>G53Hqo?x53K9*}LmK#7U*Fr4&h_A9(ZY1&CUGdyq@$3M20B-L7 z@Em5x6!8?)Y%k%Ny?z#+XB%GRF3&znn6Gl(WgpPXC%K7dA4bcgN^|qgKIJuk$5(Fm z;6C)qzo-Q9D}i17X!!WiirjX*1!^&H?C8UMQ14KlwLG?J)VMKKW7DH*d?s9fKFT~K zE<7x2Avtq^+5KoecshC+&t~3puf0o3Qk6UcHOf2~{;`2XGl$CB8&wk!;NYP8_xf>(KWNzGhIbdLp(Z4D3o zT~Ij<3YxnNats{3>u4ymwd#|RjD>Gd@J3fhS@mo1DDGHEhNUQF!>4ldi0y1cy|zh~ zBr>AGHH&*%cTlX{h!*>_?!2di2OZ2%=r10sy~szdhKXixZ_MdMGdfy`(~ZS0_t@$T z8YfCK>XLg>bt0?yJas$!q-xj?Wqnk$Xq?ExfDhPQ)N`w |*@y;w9(WD%voL#!L4 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 z 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

P`|91cYwY|=E)P)J|2;)n>1~NM z)B<31VXvgMK*#KLSFXkLrR%s z`DavQWcw=U^geiHWN494&W%T~o#kj!WxYGeJn0WyhJ$`q+kdCy9$5g0y;sR;ghC~B zBPWFSpRA+*)8!=Hhz8_vT!42&i($#94*t&F(jxE*W`YfqvyU|u$K-xNfsTiS^acF0 zZp;pUf+^AA!@>)*cIL|%hDbitgy94+%E;*)v!bXR$Jb%lW=)Df6J+^c1V$#tYK#_b zI5O4{QgSwKWGkeQj5Ys-fm}?lVGj3@_86G=@(CX-hwPvh4<|>hcoa2`KRSnOIr=o%7BT55Z4e%#PZk` zhYsu5so+ws&tVO|BUVv7Bw@CoMy};xk|J$Wiq%lO?-6LSMX*wNN%A$TAvY!cV*d}6 zJd7(Z6D>xaZQq=HLad{R0@L)jab{>-@J=U`r<_T0&zV)=-civ!WWp;)v}Gn#`Y_l= zJb8|oBFeU#Q%wqv< zcsu5r(VD#QoYHl-O}vtiL^n$zE$1yi1zh24QQC`FhnlA!=(G{;Z~f1BO$xP@#?4jXJ)t3#4{3A-swTX#t@umZ%*@5cq)SeO28NnV zE5a?w$yr>SNs31dxQ3%z+HlxBsV+#8Nd&LOI%05~Cx*EpIGYExP9Sj6|&G-W+R3p({HnzL+5plhOn_WZY$#fxuz~ zRU%(6O$`1QAH+ib9uR1T5eXVDcSEvcn3hpyWVFr+^FdBq_2A(&B9K!rLCQ}9D1;Y- z82~;SzfoQxjnAQjhX|fq586Rh1t2fOEqcB=d~T zb3`>rwL}ZC1DfT(EoOzbA}3h_-1Y%E;MS`AznQ}6sQs96lL%RYYN!?MXrWyZ!s%F& z1*CPn4$_nh-4?t!oJ*9@EMYts4c^V2>7D78`~+W9n-V$M;}MG)#*UXf0+_PJW;6p< zO&J+-G(SKN9phwOSS>PforfOlm_1-JXN&&?;6@nRG{Q~9NbM9{3Vb`2X=idafXr>a zL&O^@z=QgoQN`p4mD!X7;>(G)|H~k{8T5lK76-{8Kk`D~CD7BF1skqoAW$?9WNSeK zQ+3(lMg5Wg;$8J(=Cho>#9jak2oaxyUFkIJn!7bO9CcgWg0FE(1{ful7EDKEJ4G`* zU+fwI7E6{hFeF53|2MzLCDuyu*&?3IBG)M+Gl!VuU^wpy!*bSMp)ZO?1qC0Y^{x#O z1Zp~)(uA~!rWOQsa4~+qFp!X1f_6&d5D! zC|oetEC$!5p@w3m=SM7D2V+|w8bU0Au+0T)DsA$FKKz}3eRLwxx)DfPVDAN*pHZTL zG+TQ?cuptsaz+D%>CwLD6cnh{G;6Tu8EzQPfFhV<@}jPbniLC{%4vdL9=43O&KkV> ze~vUhCIo0?<4WDS&WK9=2t3qaE=tCh>mXCCbi|Firg|gc;zGSo;kXWE?JPRt2jqGX zWrAc&XfGv?c2-YW|EW2?48SZCZ$)Yguw`bKm?IZqm(*glvNHy-X^^hNwt4wJt0s83 zo>MuM7bLXUi?yFBj(?&K18h=Uv*Xjqw~0QC=QG%*$(TzAarBWJSdbRSiNXp0ylJ&@JUZn>+7JlSTOlEHs$=<2vTV zQe7t(7$mN7H`olyhf}Z#b_6n50ehaZf;O^}P^P$(nd}52DA+kt*ej$FM6Aevpz0ZT zCeRCr25!s|E4t?S7D5@;-0VIYFbd5{VOTP)bbwz5Pn*ofk2`4Q;-6gcP4~9tzc(J5 zzXn#%UC*40lN z+y9Sbm<tS*M z=X?!<3!8G`Sq+oS*JqEjf6`PNPH6HVq8rw05t4qWp`{()5`z5vY`wycK*>C-V`#`D z8b)fN*ob!F0nNUjme%D;W_q+WpZ|Qh(}@qh7S#Ih4SW9z)PlDE;jn%;^xpbk*}uc) zqo-4uv)P$Bu`r?eaV&?fSwQ2rcd{q4sn-uD7E8_bE>9GbMQt0kpwoDeA;pxcxMYr9 zwQ{5Qwh$?^`~Yh~lYi^QpkpS7TMMfGbw2r8tPZ>uEZ3#q z69kxyu9;j+(n2LnNg5R)!hU`*nJ2IheMJs-TMJ75_rJLq^a|91TK^Gc zl3>GaUP`VZI7BY9deQ9lI-Gc7rpi>cx^j^vHD^Lq6 z|42C`Y_f0$=$Fa}U9`kNM7?-7TxJnb&rrnz5^ACE#dlMn!pu3MxoN!@O0jKgVdgV{ z$9wF|9Md@Ef`;VDAQ1kG%Fsq|jd%u;=wN8IqWlFAX)Ui7|Dy$ko-!O<$CgDMxT*{d z)L|mMZ=kM=a^++a#&zTy-}SqobY*#?7S#G5Gb&X&k`8|BZ+^jW4!#zY{>$iOP7VcO z-~|{t+vS8XV8JJI8a^L2lk&+w-nUv%^}m5I#{+P9=2(p=D(v*b*AFKsE@y&gS_dB-=ui7M9F7rcN)ipf*0anGLFK*utOZT~!>)K%f|f~n$hDx>e=ULp!DABMs zFUvoOTF}M$ubUzgpt0=-FUx6me^W#QeuSPrz*^89;vd_GvEv=0X=t?}m=m3Zmk|@qfSlnfZtv@eBS3s}W_7XxLU@d6l?JI~E%+zq{mbU+ZT>t}H95?0>XT3|c6efxsX?v|7;X;Qw`Q7{X`s z%;<;xs0CftKWTcisNIT;1FQwzKL5k51!ezko$L0iq8%Stk?Hm}*Y9kf|N5iN*xvQu zWdI6lu^BG$EemGBO;cr8y;Qy-1Eb}4%d@^k_R0JMtp&Y!{u?7ZoPp7Q*P}}H5+K}y zrrCkHoFEewyKF5e|EC!5kjaZk=&<`|qjGpglCSGaCRjv;+xD*(6#eJdj=%!5HfvXk zjr?C|!G61cV6~tZ<9`cSF9UXo+6BaxN_zgPHBY;(1x^0Z=5sOVqVwXG;7~!r*5Tx8 zwh$v9X#EN3wKQ{&wV*rBza^6ELr5DIx!B)Y&|&`-aB4M2i$*dlhB_WW#N6cax`m|z zHn(Wk5HrhL=ef;BrBJm#1K3!(PI5q1A%6{#N+Kj7y&g5`hK^Fn8&D3l0vt z7F7O8ht^fJRsmH*!=cuKPSj7uHqn1a&K-6wXzO1RK7*mIKrLwWkLJ*O#YBf+3p#Bs z2E78cp!WY9i%3Q>s5})lP!gsvEUIup0GHvUTBs%0`&EXqB6Is)UOB2U`pJ|Fw5-J&q;GeZb%MQxst6b*9L= zk;X#6N~>85GN8qdV5}|pp)b|;8q$m~I}#+n@T>dxk8|N+W>qz-nq>Q7rgpoE$vh`- zkr9y*@z03=Zw7-V41=nCNi1mH|6Lleg&=T+PVogW@`U&ldIef|HY{lFe@>7Y#VXMsmf*5j&>d%xsK)_r@)fY4iT_L_ zXC^;^m%`^0hHLsSg9Ro3!d?e3=vTmk?vUIJ2BrHrec}O^!-C3xv0TDErQ(v7qqJRP3BKsUBFtrLmy7e`yATI(l9j3kv@t zQo{kvG*ry{g|VRGfA(O|_`Kng2w-e0C`47)qGqDfHy#y9?2k#$u!Vpqz0~UDn za#&FJzwE)FUjYls{u>*8j`{KY!~*B=8=uhNbRvLb7Yx1|h*n&f6AF%f5R&yJSWx;O z=gSBN9gz>0#Da?dO8g(k1`Yo!U_q1r!{IA|hM?1^FqW@j>T6e=8w(2m(=_NDenBkg zt|Hb)Flf1)%K3CLENJjAF*hcj$9*&x$ATKa1Lz0_O$4mZakO0u3u^tHx-}Ru=vTmk z?&4~hz@S_&BH4Y86EVUdQ9En`XT*Y9|2-J=D_}w4KX`pcFsNn7*e{6%&HZaR0Wdv^ ze!_mj-c7#QzX%reMgO&#^Km73H&ytS*!_}{yLujA( z4}a;SoTFfwQ^w-T_Pd^P|IYbt*Hcc?G2iWa%trcrwqH?aczk>fxtk)%euKYS2TObJ z!}Xd6F3k@~PQU-x_dox~zdT-j``u5EkCoT>SKk&o^uN6O^Z&m3_7Nz1BYeXhB%fGu zTvB)QxcMahe0BZq{o_rW?^XiSJ;-!Ub-a6i^i%HQVf0h(-+A;mQ~uv^^fy!X-@pFx zho2vR`1#LQzs^v8``jTyX);WxUdk> zd)aLdalo~f)7mefxVt8ybglAOuX)3i*!3AC@smK}B8j~2IUw=wxU9143C8?#R@wE0 zTUPC`_qAF>M-DRK?kaEbGHvk^2VHCWQumMR{mqo6JH4N&y3+m1|2xvp?La%xmC+hv1fB*S+-~asX=f@OIMEv`oe*EzLW9sehMlx<* znXNw#+zzc)uMTErpDj2^pRca$=BulRt$z-xO+=HC51a0HKmGWx|6*f&FIV|b?{+`@ z`Cs1s^!USv|4{VT%6|L!Z-4sm_=n&9Rh#PkX3QA;s%+CY6WyS{+x0hF_i{blUtM9R z-)!C5u$BMY7n^Tb;qSLwU)()>161oSfo2>F!g>bD9w7Ve67YT$kUjF>e`I|k0LF_Y zd}5W!rIc~OsSe>4kgi4vTr>#If_-4wB1;=xucAR|460Q9d>7mQQ|I|HT;Z3|1G? z5Wz7`<<1HVqnLr#2y-JEGf@zL&TahSbm*bk*8|yipjFOz!woot`^`Oog>cEe&c-aN zQOp~2Y~a<26-{t{j_mn9Gw=k0)+{AdZMfR!MIiew$IFu-*ZO_x+ei?um0ExmyQVMhtd~LQQA&;>tV@$R7RcG)Q)&gCG<0`x+p7;y)=R zxzlUF0Em6dh@uiQ#}yVJ@t?-$)<>U%gmc%=@V##)m=@a5fmZ|BgMYx85ciiN*;^Ra zaX@zWKb{E5o?U`ca_4Ut(Rgi>P%gX4tl0%Kqsmqm+M7#H-GAt@fDgIh+5Z7Bvh8aB zM^OYYRi34wA-;AS7KaEuSIn>mU2w&=FwRWw7`=14!E+X(7)fSM^ z^vAI&nk3vaEPbfwV=f+#%yH;4yZry*a=AJvw}4ux5;nR0APij)qPq;)MZ;%f8)U53 zfihaUma4#{f-;3IiJP<+-0lw zNb9*?C=o*btxE<94?0{o6dY@+Bp&unK=%0mz8uM(VB%c9>Q-!kkS06fNuNiuzYWN~I~kH4mqYW}YFPq=8d420JL-m*Ek|xji%KK5 zK39@PSXKV#YzzA_M`KfJyJhb#Z?KGF6-!Xzzz48`ntQQ$a-e7 z!&KZ(o(M1ibTI&W3KhdpHUh`7hUC1-c-6-b3-cSEp3lQRkA@W9ADsB}K=$1Ke;OqF z+kouBf1d`){x%?c{2%39$P}4!8=#yEUzXK4d+0bId+=Z3$str`_;5OOMw%rY-P+lj zLx)O1QIF`G?u#5)OGXa_8qQP)M_cxH!KI>f0M6bz)R8)_r*T%ud9MIhRU?ZyJ%2oqUHs=X17C<_ z*Zv!T?Cw9Nix2(&G9>%>j?x>B2eOMlA2bp^;8--Gis1!cnALb*i0af!Kz8vrCqlBn z4ai>me)6nwA{y!0tegE3$;G2mn%rR`($U{=&ar`IFYFr~c3dsHx|Ffs* zK+%O075bK}#{0Qg{`J&*cTVuR!}Z8?$;;QBg{|f8p_Gh1hp15+%JFQt_2e~|jm-1);?&3RA^+uo z{ZGut{^u=7c2B9ywK(fP4>cDG(?BcI8btleh zd_0gn@mE>Bq>{?2;)c|E#&)v;m_!wQOIBkRtSkmP_#BYE?w`(8`?*qzl`hnf+Y4B= zXvWMEZ^~+XJdi#3mz?l(NksfWIYe+k8GqcWF_tIY=4-PW9}i@Qzhvc33~Zg)PL$Qy z^c@LghkuU)I_?!)Cz%=&g!rXdjgJSiqrWp`HD<60mOR01pGj(*_)}&Q&UV`nH$%7# z$zPh)_;?_@{10*L3&9Jqp@f7ZesNY~17bAdjw%cIOF;I@|K|k6L8M`qh1l^4G~iS> z7rBTh)(}V1n3=O7Nr$9pVqP%Cxo9NbxHhZt@j!O*4{1~vj|Z{`e{%Y)#6xU)JF#D<&uV-;ke&ZCu@0i3tV8mM#&qrgCB>5Po3~~)_NpU+?7OpMHKt4R zD<(oPezmap=YZ_Hgb*g#z)7{)-HLR#P{vl`QAj|8%l|7XZ*%%FKBkRAT9^!w9hHO9f*s&GtF zW9R=fWHrX`j|8&Ie^5bn2_hEVWHM zcW23JJVidye=0I6_sX&lJ^^Ik<@h-T9P@UGgb^4vhEBdYtMTzb_FcvOiQh>Cw2p*q zNyEhPO=;GW9p#l-jh+3S=EvADrZ{m!#Bqi3Hc5@?UnF%54yVj&e0)-4@lUyv^~70? zX?sT|H5UJ0QLHQ(I#E%sF&;!A8KYa@lGWINsGr9G*(?4>!{V6}*L$L@#>WHMcjyhX zIB`~E7NaRhE%u}eXNXBRM@#uG) zapC3t+&slAImM^Hl*olvtd^yKuH9j85 z&i;3Xti~A%5=h2!X$lN#@S|6l*} zZ~pH0zy0nv0iwVA`AEGP0gW|oqTCJ|X z*=&cuZ&&MYt~X=(di~AKSiVH30@?2Nm~g1*7IRv7+o7Ea4S)6RAOHLR0CxozU2iu9 zeLZ;P9yoh|GfqbWlxX_)G9DAn5~+I9i*Zm`g|6OtAvcQ+vFfAe?y z`TuY0t6vKNCUfk@f;8BV=R&Ee*1LyWBm6DHLktd6??i zP^#?-I!V8JH@ba2}K@{i{&o z{2w`mN^SEbO!aIi)qVU~Iua8_1a~y;q$r7QjtSP^O3r{%t^0RB4^urGN;UL<1Wfg8 zC{_30#7mS!Zx_k8+@e&iKQloNf#=V+94)^^saF2Y&%;#taOwzfa+E6jTTCA@0U=eeVFJ>i(^P1HKGXErZ}WP^vq4!a|2m zC{qywxji7plSswr1%bnz3#FR-7mt9cT8{IeRQcb7os97>nJ2UUu^OE%-*Ssm4gThm z`IgJf7H!g5H-X3e{|Vv}{&<;u%LyE#VI_d9P^!tlE{+TbV0#sJ#^6x++u0k8a;t)@RQljPJ>b{{XdAqp>c`b<|cy@J$b(6GBb{C<^8#T=m6##`|{Ko z6-JmUAvBaGPHFo76ev~x2j*b9+@+qG--|{eb!&OT`sqQYE zZ<$VYfbCk)KQm0!@EpB!?a!A%pgf(sde9Hm8ozrE>*VEo!5k5Hs2fPUUNHK|J#NW#;GtbIXXwuh%NuS{V zr+M+g;?14zN}Kh6zT#)Tc`Hg){)vm`TW*GNPWU))+W(`Vo3ywGy8 zeXZA^RD-{X&uy^F)dfCh&9}^uspElK=%r(w`-g(Fx>&yD{VWZC9Yg=8%(rau4p6FD ze`nl_rT-lRQw^X4os?;cus$71mH#Ua z*sMSAH!9o!w~8H&9g=5=m69!psjNan2_z=SlcH4lKlwc_mv6aEw~YT^G~Y5tdXR3J z{I}P02z7BSMMR__MIzVVe8hwki@oI``EWLrD*I1-{};=*+@@Q0|0V!XUH0b^+={~E zYH`sgp|e4RfE;FpUF1wC)!l{jE%PnWxES@F^vU3j{L47~XJM*mL#fh#KQT6dKH_u< zOa}c&ew0o7oS#B)iVV$qoU83&2r(Zw@iL9O{yCN(i0OiWa(hBPlNfjF+{5ig zJKQ9|qFfzS7XptVramX$`}QvP@38uae!5J)Kpl&Tmu_6ZugGTcPxs{ z$biNVa5j``?*BLfrfNyfgHqjHINx%FXxv;=y5;0wJ_4qCHk2y-M+J7XXhg2P59*Y7 zjg(Wu6u}eDL+@QA#uQTcPh;xgT``-hH!sh$m`%KvTu=Qhnv z9f{l)p4d-!^5{Rxc!W=+qMQw-3jc2E9KM3V-V_vUqxqP>F$)i!&#~02}0*^hX8ZI*Q=p+zDbglBFQZ z!nJF{W4L&9U5<65Lng3BCJ$LEL%eeNQK4NblFe>jMP27^7vZfW{e@9+|;=6!Go&cyeU9;k(^PmWS`|Dpgq+e!yA0;&fqP!%vW$<`CzPZc>AO4a=< zIq8|xPZ7bINkV{ZR`;;$h6}GP4Ti*GkPIEy7DYX?Mo`^6m~OJ#=TEu20u9w|%`Q(D z)Ig{S-|>zQmX{a7=f@ms?6@3nwCnfoIGF0$P^!UST{7P?qK&K5W=GUkc1Qi7T#0f@ z2&zB}m;0DdEXX{&n@08ANqshyYR>=JLh{mC{YXaCC?>*W7-&pLj_ z@SZZS=`TO&bJ&QCSM(OhoE=yKQx`BC`vR( zFqjd$4S16*u+7ebQiZ>-RAHb~)=aJ>!GX-Ohwk&HZapxh|D&+4+i?+oxOR|Fqyn{2`aRM%?-wB`P_E z*v8FB?j$-JN_Dt@IZni|yhQCJmtb5i%yK>tz8Wg^nNX_Xe@x4GK-jLGoJ3%Bg4{q_ zJ6piG&u2raCjY__Fx9i6RK?%Ls*MI>e5BV9PNf*fFOZBwG7ZI&<@iAaAso~ig_Y3C zQJba9ehC2|{zSz*EXk)F#hcP+{QpdE2Qbyop;X6gtqn?bN)>l(j*6;4FrUb95-&}EoRN_?+KS)_R{M|IM%uD*a?;oxL?fipOs=xbwb2X-b zJ$$0_y$+DpZ^3TmKmE2+D_?#4xBvX^e?9({&-KS&Pk;RFVJ6f9`EHrXcTOVxY!2zWX*q_`PdRz#(aV2#IQsi3sq{rg&!Wu#76pK> z^OFIugynNIx{@m=?(L^`^vguU&*O`0l}8e3h;NT`9+A-S*9%Z|2ahgvbdkhBvR{IZ zuEm%rcj~?T&n@WtZn15f?oYezH7T%bNuTXK`C|r-NkM!cB>dF0DndiVIl z$4si(n26irzL3%SeggRK`QKqi>!TA|-)A%I2Fh4oAyCUW6o31la=6Ed$2$k++XGgc z&HVQd|N2k=bjYJ?;fFVDR^RXl)!hT)Siga@sK3eFp*;JKg7uB_ZGCajk!d5mirl9? zlKGi-S-;7OPIPfsazx(m-~IRxKcd9nX3a*Kak%-U$1!`(NGn|CYx_iDAd{bs%1rdfY)zhMZ!&0P%#@0p0>GoxA($1nN6kGNRj zm;B$q^dz{sAgK^WW$#i(9<48SFLdRfcKgX<`#$q5Xe6o`$=WK#v1Vc?XwlL)9c8*-45g^qs`M;YJrW0q3 zNwLN!__NNTWoU}kW5xcGBG%U<@_)Mr4l2`f4EeqpG2=->_qmzoW4yJTCscSQgR=Yw zF6e;lAU)I*P-vNS;-fm$FVBlVarn*y3=xtna7^*K#}N9;{NM58z2~nK)X!;XWXD~s z@Js&hC-Z+>;am%_VHQs&*K5YHLe8>G#o-fcRrwm@9=Dc(jZ4s4Ne5Ut;=7;9V^4`& zIAKC8P3~FdIN~5;X!`#)|2J!sGpV0n@_(1tt)yQplfUHu#_-Xn}oUU;291ShL zsdT{gaT;Xg_J32QLE)2pDGV!_StAU>B{9A=Lt>f}8?+mUEejmw!nzNxuOg1@i=@&@ zIx)_W%z((GjDOH^KI+ckG>?DM;X2Rf$4rh(q?s8|9a2UG=?2l3E~0-Q;(hXG$d9H{ zFDR-vD+Uq=fOu9>n)OX;s;k4HAYDmRjlo5u8NPe@UxU%Ps*}!6_)8I0SrPFEnZWAQ zqPdq}`6b^z=YL4hW8#U*@i#PJymOc6ZXE>FoK4^7=0;;;NM^>Dw& zae!5Y(pbmu(bV{tMKUw=14oYi^A>UJs_zY~4kqqGol47*IJMf_NyW2B_R-F5l+@rQt`!+B6iu1FZc(#n$Qh`ME2a{g@dPfAB(uq|tie z`=J9ByE1hlM~uX#24$5c94}5Z(hzl0n|&1P7(Sy}oWT9B*3UjR6708*({_D0hFKZ6 zA!3Y_%n`*7FAnK*cs}5O(lnk!$@5@&K_>fcn)a-}Ms6S7KUF;;CW+NO6nsCPC;mF& zO%2ojU#!~`4W8uNCsDq`{=c=Kli74SAXXPlXijeE)5Ky;ujoV;Y+|*|aL`6ces4>N z**ty%Mnl7X@0-d^EF)_c4M7Z4XByT)-Uy=iG*$ z(`iI(6bS~3$a_5&msi1n=+nL?BZbFdL8!V66H{|fyt8Jvd$0UIr+9Hcgd9)gJr?@6~ zmxt@%ufO!J89E*c7}gdGPB7iD8!S_}Kx6G`kY4lthC3CRntRZIsYKj}eqL3g#R`K8_bV z<@oWq-~!1c`V=S36NZ1c!|O(FmVUCX_#cy*Q-KTU?SjQ^N&5~?`og~CvwsE0HP1wy z6)~=d?ZN&RiA)#P9l!@iCkGQ6PS1=-C3|>Xb-vhaH4xnX6uFjqG3|}U$0o*YmAuQr zpL+nS&E!Us$BdL?)xd5JbU$T~WB*6tCvY+Yi8jmFgG9@S5{Z`+u?K8Z za?bMDEnel5fAK?~b8L&f)aS^!z$xtPsH*<)Ij5okfl7?|97$`Z<+J^-pc+gBt{3TX zy*f(W6!w=S)Hgyhe4qW`aeX!pX8O#9BbuNylAI`C-96VIA;H~D3b4aA22m&IXE zOrGTo@WZYAFM=Pv#$tM4h{6*hoR0XZZH2w0V;O-XgZjzgqyHKVcplMe@RneS>&9Y4 zXjm-rgCH`4rErLE&u4$Hq&!z4%K9Dtr$VhBup))iq*%r$7 zwXqEK0})^w)fFg}FfCn~az})ay~bx@Wg}s0#o{^{o8NrT{;WC2=m5O)LJT9TuZPw} z53|a-mpfoD=V0erJ44esc}GKLs_;2a6;*`Y_#7}uW+R7~PzV1A>c~EMXWQ9HI-CsI z&cC63LTpNOS0tPy)P$kWb?Z!MnnHJoIlOZA1`986&$WeV;Nsy6dlj_9#sGhlSU;Ia)P8wAG4s);p zfqrzz-tjpsMkYHTDf>;QqLfe{L#}bF(2G%sPWfEwi8`52Bec%G4^3Hw&|_Ml!g^1U zpV^Fd$^U0U^e9Fa^v$=0zBP!x^-yk99nVfcUtaK-DL47C@MI%e;q=EMAeYd0G^-q@ zd=97kWbwdCED3Sui%*!y12~Bearm|WJ8!9U=I}_kbJgAnRe0n0R^L6BMDp0 zDrbL0ugeh)0oQ2bv@;t%M}5n?@;3v`@P8X9R}BPiIqV-xaVP#bFW6-Iqwnby1<^{Q zvuOM_6f(3hF;EdEaTa-D3{y#5ox6%-5gK#^a~QF0zhryenSB4D_8dMR>y1jx8+FA} z-do;l@qgG!Ok`$8M{*lsvKeEzO~K~C^}O-cVpVH4l`r#dzWr{f8<``h{7SF zxLX=C15|`VxPL%FDy-k5QiUGK)JI5~PJPBQOm?;qmTa06i5pvAwiyk{ilUzCtc1r~nf$>%r%Yx8pOcgZqxOEO%{>3;t=rtc5#YP2L+kPXGYwj$7zKiub330GCYg3j zb2$}SRxBs`AV&BYUYS`6o0M%_#(1ON*wjIqi6~Q_&(!{BS!U`tn}JRGKCE#^8^Psl zFa-fWB*lkbo8IUvL}nVq`SXCwE9`_wKpY`!tyZAD!R~Xv#!;uc#56+WibXI!+fv+j z2r&ZN;WeDWkPOHh>3<(oB@_G?o)+1Kx;>g37qaF07Ubv$7Yt)|c36=OhbdqS=e4B8N&_yt-_;Z-%z4nv& z8b>oYOxV7R`8*QZ%Y2UG1>Z(h7>Ngn5=<5K2B+iuN9730Y1b$)RkM~`oP3|dYId~k z<~!)l5CA%h%3?a5DDcz={Qn(BN&YboiZhRsgssBnu{~%#5i4lJdPCP`jch%~5^NRo z(6E1_7a1ki6#1Cr7t6l3a7{x9u$E4Uvf%s-)($9c7?Ea()*t(b$k7|4$F;pEdT{x0 zLN;DQA2QCcWFpf~uFS^o%vsRdxiBWA8T_haoCb144^ll|5(<}hlv;~<1zcf z_c>v2CLF}eJ`R(V;1f7u$-EvL!RO>cq1ep`N$VzoI;q zNep+zHc>!sVT!1vV5Li@H?k@d=B22e)jQ-9VM|Ey<#My-i?C;=3X6vK>2{QOOKT28 zS566eOVl{KE14Z4f1F@GMbpDBJgBMs6i+q^4s3-a`#X?)KMZ5uW42_<Ws-FazY3C zqR%)gc1&`8GO>d{8%K}GvC#aXr3}aG4S#I5NLRF#+x@XObVi!e3KJ`&1@rhG{yHR- z$1xbr1ig$wCQJh7D|+>26PS#ak?zX92;VmezQ?RgxA}T_O6!sQ$vjn;6@q)bv8ngv|Y=Tt+_?q#2AeL^TKPQE#Eu; zMJ}q4i%Me8AScnTb@yO)tRSIib9psNPt)4V4z{|Q@}|g4JHQf@zfaTDEz9M01mx!- zR1w;xZ|CTfzfp1Q%lHERho&94Z4*ahFNuJmPDUN|nQ)A{lbFcdiKY36up*$ATU`JGO*azQaL6&^t{;H$~?dqzVB5=gO}ro*FQ* zdWk4wl8B;aCNSq5uoCSN!X<1o>7<)o9WBA@WvUjNW6ha`s7W_P@|RmWOZTkWr_4bZSa%wYobXn+4GTz|;I<(J>S|pqz_V z*K$T-K*He+Zf!AQv~FP&ikKTm=z1i24qiC{n?#hB)z?AzbL11=WYTNG2o2icgMK;{ zpq5N#K+-hZT!)|wVjX>px2bWBHfTMbvGa-H;RH>|u#lE6RAgG;n?J;8SlN`82y;re zOa6Weo#yTY1sJLu76@0bjSgfZ>2C}PrtTry9$t`6n7p`;ttlO_JZE-F952y~pNk>M zAVYjfB@>vwl1;h4RLyr^tl3JdSG8a3rbE9k;T{5(7XR z(BT-PCeKEA#?t(VP?_ zUUe}nH#0C^auE>}11)Yb*SCYRr)ma>a}p)nEU{r_|61gNF;zKqqFl@(b}tgqtJ${d zg^Y=r!ZNvp@$i)zN;-nBe98!-?M1b+BU<$!6`H7o&z{Nh~$(@BSnGS3l~- znlqJpIJ`Jl{R|^cNOi0ltY3&H>z(sJNi>ZUul9Mgg!qUvb2x`t)D;8qF28GOvYq`^ zTN&E|ws~)0gqjg_9|3+}mQ%W-EDb>*&s$eC7Wxc@m;9zVtN5?O}VwB0Mm>93D__ zEk(3j@+C_(Y+C{=zM^RV&yKX1n57D%bux-(v&Ef=wHphg(lo^0V!(PEa5 zHJB}213sJNoUgNuGvKH^s|Wl)2evdA8()m&<%siw+>{(kXBKDh{nR>o0S2U2n6?9# z2C^3-v;r-NcMSR1lX~WJY$9w7$TUH@(VCgAsja>@*tPuUW`Ne33Rn#LS5*kTN2m1J zIK1Cyl;tyY(4c^rMh!-u+Z1#kBw4N;4Pq%_3TZIh!e??aXMUe!V-2J7y~U7!ifB|2 zwJ|@T>zq(>oXDks=tO(bV4^ZkW20F3oM@v=T5V3RGHb7ai+`5ZslW8J?(0f|0BK~@;sUQ9*_wV}`0Frn^)9~6--*!*DP zA>+^gct1(_@h_o0fAgEq<^SG1dF{bNha(SUcSgkS|2n{MPT8@SSkT}kbz~}Xa8qbC#wAA}vMjn;c2ENI<7hDD%$s>ZppY}pU<}HHJB8htU$FBZX`7gxTpf6fGzV;6KZic&FG; zpO%J^oY8$pjXC(wD4ke-b_CDO* z+YwB_CgsgQDG@dfCNbF21!y`1Q7oEuzGn-IgDDJZI-KU<9gCPTI;0n zx6G5NBqL&oV?46i#Q)W{imMHOqBLpU$h?Gf%Zb4fMR}v3N820nHj|Yp&+)?oWoauM zF*+4Zof7K&6OI~IP7&P^9PD~pE!G<<^%H)WkfJ*8n~U>dL3c|qDBcjIaGc7-bw!Fh zYK<(qY(izj$(l#9=hVyK6w8Jg%O07nU}0F}433IQ@r@?QHvGY!nCPtoXJbsEe5Ny| zD~s*tJXlcvufpYpbt1wTW9|5-bv@9v;`_Kv!MvM+4^&8Pq zzHw6T3pY`JI7OFcRK#q@<+SC*VEtoAK;coRhADICU=NKqbZjsk%Mz4$&5~fr{67w7 z6pqM_9IxXI#fwIx6otzYF>cM})lmZ@56~5|G{@*KQ(L^B^8}aoLOmvphZkdYY`QT! z(|9t{@dde=ZG>K84GF5sc#$oBs8u74!!feLk}ieOIAgH@*8q}#eAxdQv;;mS*nrNE z18GjI`CUtt4(bdq51yczp`NWMxTAe${}_r^kX0_YXVO zMJXJJEfg2!BAQO57+V+GI1?5$@xL<|G=eh{U2_qf38bmNQGE`gvHQddRA$zEGyJ62 zAt2q1>Sp9}KK*m_i~h?H6)&)}NFybaIC z4|Mqb+?DbD?el{U4F5j90Bg04jKt^sb`PngW#JCsb{)1a~Icr#v)1s(8j zPCjv!7!sa6zP5aPG&d zpg0v_gl$D79&|ohY7ER~rG(ETLv2aP98v6<;*jKD>;W_)rJ1oDW@DI$!Esuo{cpyM z{Wi7#=lweb5qoJQ8mse1dT+7ICtfnYjvc2gAeJu&G;-4{;+ARLnN{Sd_tB%VU?=J2 zHm|qN_{aH$FEw_u@0_7)Hyz@C>h+h!g0laF)jFH7TTgRf+UPrD54$ltj!-wMZk!_~ zJAc0s3(Eg78i(+1>oav>|C?q!tts+ksu-+bIvo355<{TRD6EcdIlkLVIhe3=%?#>^ zx8@|6_u~JN-KTu`UpVaYi=P%)P~)$|BF;rZ zE!L9Fs_gpMa{><%FmnzpsPT_Zca0Y(Cq6O9vQfra>{RHRFiSHMyH_!u;9cVR;33wr zsPI{`_(uopXlwv9Xv9M|@|W55?-N*1;}?EKj@0Ncj|JubvidrJD;gVlW~`Zk3t>Tn zf0)6bCM)8Yb6JP#{O9Nx8Jo!~?LX0AKA#Q?O8$8v8-{>@xRXtUNyuDiBsy;51BPSA zS+JlxF^pQHpD`>ObwvSLgT%Gu=ot{wIkBL|ADmxe+3bBIzAzS)|A!tE*DDWS;&kI& zH2E>WUjhq?|3`&mYR=h?lQ>(zC**b!EGYl4c$X0jirg-Z1*QKWKT2h^z^{M>-6b%X zUC}>`N=|?F!zHnx?0>OUvF!7MGBGeCUK9(;|6}MQ7}N$#SY^(=m%@Vb|L(z{Ov>68 zm6r`@O=1^0GZvKnkCHutK@)DkS`($4FfE=Rj02CKIZF6ZjPF)Em{*$`;!^?#nVu>do-%M&EU5U~Js1=t5`bHV zCq~5_yF(3G9L9lo`_RRSv7qpGdobu%z=D?jX99zM1uQ81n~<8sz(o1DJQmdbXL~T{ zSHObue{%+pV9>9C1>G&dpkDzCD*i+O$p{9u6qm+=?t;gQY8^1>5Z!T6Ea+|t26gy~ z6Xudw(A^RY8q3dxv7pYsdoU>jayeFZG2`>*Wy5eym^#IOX3 z=c>R$PFU91^I<`E2{xO+pkDzCn)qKQufYrU6|kV}Ulk@#{rn6B(>9HTn4_Sa%qPNv ziht$*9KoPWku=TEb%Z~6=t#Cnv}Vhfpfu4jao5Fx7SKuzTl36TSng9;Q1X8U7H~fB z011`8AQrUZzsg0F_^!)iL3c|qXk0BpLtGFG8vZkbL0SI82?G$|3ryJg*|DI`pBeE0 z2K@?HQ0K2b81yS(LHR%9=e1|(fnNa&%KwR>j9^gbUM^-4O_#%hI{)p#phE7&d_)}) z_rNTB0W4_r{}~MWnJn9zF;#Sv7j&6$^qDN%i_F@G4{zi@-Ge!gN}ydCWheM&rOK8y zd39C)47sDp<)i#HBGekFnR&GgN~ zwJkJ#;dg3tRkmfD>-FLVs}-g*zG1UUKJ4iYZXw=(L+Fp+?6m5}jr~`YpT$EmNzboy zI$V6C+WgICh5o08*6YPL?v7b}b8|aCvt6yL3(3E(3ncIUt96*o^g3}6`)_#a7vJd5 z)P?Q+^`Z+jsQot&Nx(k6jc&dFX1m&6Y2Sb80z>fh)#}<*FCLO8eQaoDw(-9C8Eka^ zMjY$@8%p~A8;<1tH>))Q-d^7htF&7s;$i=-H@o!X8se@kTk%kJWeWSU+XX=k?^{XC z-ql-22Gsa7{IMbzY*+F2dNCPeJtvF4-f{xQdQMDTz5DBZ*=}kW1iq`tG}M)C!@Kj} zzSHaLCBY00ubpGMdhj=G*={Y|t1T4q*FSs{zSA_!OwpGmzkE~A8NV;9-I@7cjD!~2 zReZSZ%Qn}2!(2$+vt(dfM#e76(msLbk0oO=o~-0&WNt)>xn;D z^rkw4IpL6*!TrpzOWwAfQMP1o!?U+EhxXZ9m)>?|t6^4+f1ZBl&oKFeh6TT28C_XK zV^a@ivn%7+@5>?w=V!Ga^w@UQPED>l>K=A9XlH@9vZIA~F#CG$%3CyC?cTZnv~T!; zo$G0EIc{6)9^RU}{YZv-J;CW;-$5*3Sa$mN#C$ma*4-4|ucGVD(^ZY!A5ienL1fg; z7MNZ)VnXcTeY=cK_(M0k`)$;O!-maZ*Fj>sn?X>eZu%Xo4jN9oj{3X)g)m56!|HZr z8?wIbpx~*#Y}<8av)YdJcKhkyxJ#*hc(d+C2WmBl?#0d#KH15Qht;Z^!Jy}t)PH@w zqY5UlxM|oer+0@HQ?KWG59{k~SI-SsEiFLkKD2EJpjxkXO~Z^K9km);)9U~^?TPML zS3&e;-Sl(0&D%KoS5dI=t#!NZ%9hH@SZ~`YhHBB04rxJIwM##r^d?+;o)AQPK$~DEw7Na1Yx~Mr=0M?aZ)0Y*<_ld%d|9ZqiJ3%34mStfO>2 zwoc9a*5;kM<3p`w1a2-(Ge(>@UkY|_0D8loi{5Z!j$dDaoM z?aZn>sk-3cy4}XgcH=p4IScDfaHuFmux*4tqifhfvKt7;PB*JCCq*;Hzn}iK7|ga2 zhNx~mEppeHEzPrimSv}%L0G(l8Pq=Zdq2zLxii6JwIdM~?FfBHT*^91&rQy4J#*FS zm;#^NKhQP;-_z~wyX~eu{djoW%8kgOt~1orZXowU-G`ap>wghg9U&=e6;$=0#F|-9 z)*eFCgKFvO;lax?`k#vU=(2DqYo(of0tDN7Yy~Y5-mf~*yP;Eegh_#O|6=n(h22zd zz;jzd+Q7%Ool({eL|M1(Ho+MU?aun29Q)Z496 z*eaRIV2=(erXH+lU)Hi^^|&#*dL3IX zR1WLzl-f-z-}@m|qmKYar$z=Hb9;SHQ)$X}3R^v$ZS6s%Kmx+=I<0ImxIzC@(V4kv$mnAGlg0DpA1OO#DkjF~%%Wp7P6x$~ggvdRV-vC#i=pIyg50s`wh zC{VSX1upw``WbCnrBxdcy^Cj~|0(QrcNeSod_h?&qSfnEcV%%u9O#KD>pSTF|86@4 z1ln#B&eZE}7GRdT5kK@C&S|?N>YTub_wDt++-K2k6J@OqryguxJ35EimPt9$cY_0} zJCq0XKWc2};n+F|cT-58>39VdqCG2a9hlqWd&)XGVl{C8NHY@JcVmdaYFT3rr$1%4 zjfp*FjK&A=OBwaHE1PY>lm3^At14TQ9c$WUQr2wQ)T4io z?;G{MDqNUFViQamG;Y^0%S~4%VMbSW+nf*9xwv&!JO`<`X8#az);^qw*uKnkHI2BJ zqd5y`1fZu~PO4r zZA`bya-W}PGyn7S!vHqI#JT}Pw~f&B8VRUgd@D_7WRC6z7je8;lC+=jL!b;EGX+by`5}_ z#y?O07PD?gr>vVoWgXYsI>5BVwRM0wly>PLu+d_+wC`9=uWl-)tg!>7UTcy~S=VW0 z?TJ~vjp9Ya)$Ydqi_My(VoUwX7DwiR!=5hrOd6K%lGXv{Y(s9Biq)gka`EfKOt6SZXaB}u*%wVVd}MK zz?8{<)KGN_8x<)PMW%O+v}W4WgJJ9HwW|Ai<;Jf`hnV$0g{?<374JGC3`V(~EtiNi zI)0u-)lFxVEzbVo3%gWo4GdMbJy<8xnr!5aj93xIt^Sv?mOG}b6*!@`s`Mu&7}+c7C+-QLDTTg8*@iok2zfLgh0^uHLnTKP^da|mx9c3^8vG%U+> z(Y6sHY$@$(2IA=fgg+&ZQ#@_)~QnR6QxSsre^oGBCC(Jl$_Zae+_UmeXT zTbSks{SN}Ljn`YJtYH>wrp?y8ma^9Old{$&ugu!Dgf9Pz3z(Xbum^N&r>rAPaEZpD zoJQOe*uBrK!F^dt+06ee0Y#~_s+1SH8LfMm%dju&=z}-8+d0aXVms~YHIsNZHGpAV zBf0CpZP-mR{(+&TQh?x8)s&(|0&$G zM3}P17MvHl&bjH#THIDMgKK3iLsHhL^Yp(IwrdO(+8UgBBzYCunaN#=eHj~j(?Nkc zjb9+|+g|V5X;pY|K(M~4*msa#(GVB4jt1B^Ea;`Lhq(^*X8q5@SZ?=lXKYfhCoaL4 zE#sm(w(%YYf?*!cmh&IqU}Idavi1<7-V;+z@^(|r3EmQ%sIvA9=zWY*jsBOymibV4 z?YnknSXB>4qSLvxt*h7Rj_R%FXD9tHT6L>{q^uuZK)+ohZGd7r)2Z{6t4_+g{|-2y z-A>9nstyNkM@)rl4N%@krD-~-tQACr3-@-T|0!H``&e14WUANGjIzC6Hyl#cYu&o+ z4eoTB<-w@`DQu&jQ?YfSS6Ro90yS%AhKZ)tq{LnITh{$QRM}>_*m7DchKJE+S5FLl zSB52ZWn_6*R$;u6|FZUWK>cl2cosW|9=D9Z+IDl)Tl+fG?G zGfXetR4eQFN6axTU)b(QiLNl+={nwJx?^0)G|<;`GVh)hm!mB6KPezR0o;y?1%`9*O zwv?_O7fx4?@o-V5eQ2XDM6Sx(t*BmS=}uXz*C}iLzj zz{5P-y6%-Rm9+Yw!p;=0tP?uQSeKjGaz<)ZIQ3eSf-uuqB{Ivk0zP7o<;tQh!- zvX*9)b%RUU(x066^DqZr*Kp@POTEc)wCFVGL;Eb>eM1Y3lYGWYN|4J&KaHTA?` zclEZd(u&Jm%fH%IWLRo>01HT~l6!d7s8R;P`rSX z5WS65^uA7YSLTYpg8{pLPiT8bNWvu#*U2&=nZ4W|Sar%;#Z#Fmp@T;9Y$s8bF<&+I zKRIYxC>GZ-WJa@0$~t4Dvd-?Sth4(n+bgJ({+Gg5*GpN;KaMI)N>bLI zBX$ZqyKl-`KCi4h)hO%KtCY10h%(VEhf9(&CYs2fJquRYo&A+z1Jl|lj|#BDrZYSy z9eoJ-Z?;n~7zwL9oCR3e_LM{>RzyV2|0DqwcAPL}tu03xRiJIeBy$i(*RIa=L#0>D{0H>G7vuxwPur@Bj3FAFnuW?%WLahaZ3Z`3l#|Eo`)m0|HXrZbf22rm-f#Yy*W>ZSUtYafX{z%- zweb6&9{=qscQFb6^YM>=c>MJJ&;NWCJ4J4i87_MJ^Uv>o`nm3UvLFG!|LQOP;&1-` HyI=i3@)qP% delta 62 zcmV~$xfOy?006MZp#m!RB^}!_21QCn_zgQSi4&3hj}V<<+eKoD)Wg$DCRcd-_$pOu PjaH{O7)@rsYgx-1lcW error_logger:info_msg(Msg); - info -> error_logger:info_msg(Msg); - warning -> error_logger:warning_msg(Msg); - error -> error_logger:error_msg(Msg); - critical -> error_logger:error_msg(Msg) - end)). --define(LOG(Level, Format, Args), - (case Level of - debug -> error_logger:info_msg(Format, Args); - info -> error_logger:info_msg(Format, Args); - warning -> error_logger:warning_msg(Format, Args); - error -> error_logger:error_msg(Format, Args); - critical -> error_logger:error_msg(Format, Args) - end)). - --endif. - diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index fb7201f02..793c9d501 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -22,7 +22,7 @@ {backlog, 512}, {nodelay, true}]). %%-------------------------------------------------------------------- -%% MQTT Protocol Version and Levels +%% MQTT Protocol Version and Names %%-------------------------------------------------------------------- -define(MQTT_PROTO_V3, 3). @@ -34,10 +34,10 @@ {?MQTT_PROTO_V4, <<"MQTT">>}, {?MQTT_PROTO_V5, <<"MQTT">>}]). --type(mqtt_vsn() :: ?MQTT_PROTO_V3 | ?MQTT_PROTO_V4 | ?MQTT_PROTO_V5). +-type(mqtt_version() :: ?MQTT_PROTO_V3 | ?MQTT_PROTO_V4 | ?MQTT_PROTO_V5). %%-------------------------------------------------------------------- -%% MQTT QoS Level +%% MQTT QoS Levels %%-------------------------------------------------------------------- -define(QOS_0, 0). %% At most once @@ -71,8 +71,13 @@ end) end). +-define(IS_QOS_NAME(I), + (I =:= qos0; I =:= at_most_once; + I =:= qos1; I =:= at_least_once; + I =:= qos2; I =:= exactly_once)). + %%-------------------------------------------------------------------- -%% Max ClientId Length. Why 1024? +%% Maximum ClientId Length. Why 1024? %%-------------------------------------------------------------------- -define(MAX_CLIENTID_LEN, 1024). @@ -87,8 +92,8 @@ username :: binary() | undefined, peername :: {inet:ip_address(), inet:port_number()}, clean_sess :: boolean(), - proto_ver :: 3 | 4, - keepalive = 0, + proto_ver :: mqtt_version(), + keepalive = 0 :: non_neg_integer(), will_topic :: undefined | binary(), mountpoint :: undefined | binary(), connected_at :: erlang:timestamp(), @@ -119,39 +124,76 @@ -define(AUTH, 15). %% Authentication exchange -define(TYPE_NAMES, [ - 'CONNECT', - 'CONNACK', - 'PUBLISH', - 'PUBACK', - 'PUBREC', - 'PUBREL', - 'PUBCOMP', - 'SUBSCRIBE', - 'SUBACK', - 'UNSUBSCRIBE', - 'UNSUBACK', - 'PINGREQ', - 'PINGRESP', - 'DISCONNECT', - 'AUTH']). + 'CONNECT', + 'CONNACK', + 'PUBLISH', + 'PUBACK', + 'PUBREC', + 'PUBREL', + 'PUBCOMP', + 'SUBSCRIBE', + 'SUBACK', + 'UNSUBSCRIBE', + 'UNSUBACK', + 'PINGREQ', + 'PINGRESP', + 'DISCONNECT', + 'AUTH']). -type(mqtt_packet_type() :: ?RESERVED..?AUTH). %%-------------------------------------------------------------------- -%% MQTT Connect Return Codes +%% MQTT Reason Codes %%-------------------------------------------------------------------- --define(CONNACK_ACCEPT, 0). %% Connection accepted --define(CONNACK_PROTO_VER, 1). %% Unacceptable protocol version --define(CONNACK_INVALID_ID, 2). %% Client Identifier is correct UTF-8 but not allowed by the Server --define(CONNACK_SERVER, 3). %% Server unavailable --define(CONNACK_CREDENTIALS, 4). %% Username or password is malformed --define(CONNACK_AUTH, 5). %% Client is not authorized to connect - --type(mqtt_connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH). +-define(RC_SUCCESS, 16#00). +-define(RC_NORMAL_DISCONNECTION, 16#00). +-define(RC_GRANTED_QOS_0, 16#00). +-define(RC_GRANTED_QOS_1, 16#01). +-define(RC_GRANTED_QOS_2, 16#02). +-define(RC_DISCONNECT_WITH_WILL_MESSAGE, 16#04). +-define(RC_NO_MATCHING_SUBSCRIBERS, 16#10). +-define(RC_NO_SUBSCRIPTION_EXISTED, 16#11). +-define(RC_CONTINUE_AUTHENTICATION, 16#18). +-define(RC_RE_AUTHENTICATE, 16#19). +-define(RC_UNSPECIFIED_ERROR, 16#80). +-define(RC_MALFORMED_PACKET, 16#81). +-define(RC_PROTOCOL_ERROR, 16#82). +-define(RC_IMPLEMENTATION_SPECIFIC_ERROR, 16#83). +-define(RC_UNSUPPORTED_PROTOCOL_VERSION, 16#84). +-define(RC_CLIENT_IDENTIFIER_NOT_VALID, 16#85). +-define(RC_BAD_USER_NAME_OR_PASSWORD, 16#86). +-define(RC_NOT_AUTHORIZED, 16#87). +-define(RC_SERVER_UNAVAILABLE, 16#88). +-define(RC_SERVER_BUSY, 16#89). +-define(RC_BANNED, 16#8A). +-define(RC_SERVER_SHUTTING_DOWN, 16#8B). +-define(RC_BAD_AUTHENTICATION_METHOD, 16#8C). +-define(RC_KEEP_ALIVE_TIMEOUT, 16#8D). +-define(RC_SESSION_TAKEN_OVER, 16#8E). +-define(RC_TOPIC_FILTER_INVALID, 16#8F). +-define(RC_TOPIC_NAME_INVALID, 16#90). +-define(RC_PACKET_IDENTIFIER_IN_USE, 16#91). +-define(RC_PACKET_IDENTIFIER_NOT_FOUND, 16#92). +-define(RC_RECEIVE_MAXIMUM_EXCEEDED, 16#93). +-define(RC_TOPIC_ALIAS_INVALID, 16#94). +-define(RC_PACKET_TOO_LARGE, 16#95). +-define(RC_MESSAGE_RATE_TOO_HIGH, 16#96). +-define(RC_QUOTA_EXCEEDED, 16#97). +-define(RC_ADMINISTRATIVE_ACTION, 16#98). +-define(RC_PAYLOAD_FORMAT_INVALID, 16#99). +-define(RC_RETAIN_NOT_SUPPORTED, 16#9A). +-define(RC_QOS_NOT_SUPPORTED, 16#9B). +-define(RC_USE_ANOTHER_SERVER, 16#9C). +-define(RC_SERVER_MOVED, 16#9D). +-define(RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED, 16#9E). +-define(RC_CONNECTION_RATE_EXCEEDED, 16#9F). +-define(RC_MAXIMUM_CONNECT_TIME, 16#A0). +-define(RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED, 16#A1). +-define(RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED, 16#A2). %%-------------------------------------------------------------------- -%% Max MQTT Packet Length +%% Maximum MQTT Packet Length %%-------------------------------------------------------------------- -define(MAX_PACKET_SIZE, 16#fffffff). @@ -184,44 +226,43 @@ -type(mqtt_username() :: binary() | undefined). --type(mqtt_packet_id() :: 1..16#ffff | undefined). +-type(mqtt_packet_id() :: 1..16#FFFF | undefined). --type(mqtt_reason_code() :: 1..16#ff | undefined). +-type(mqtt_reason_code() :: 0..16#FF | undefined). --type(mqtt_properties() :: undefined | map()). +-type(mqtt_properties() :: #{atom() => term()} | undefined). --type(mqtt_subopt() :: {qos, mqtt_qos()} - | {retain_handling, boolean()} - | {keep_retain, boolean()} - | {no_local, boolean()}). +%% nl: no local, rap: retain as publish, rh: retain handling +-record(mqtt_subopts, {rh = 0, rap = 0, nl = 0, qos = ?QOS_0}). + +-type(mqtt_subopts() :: #mqtt_subopts{}). -record(mqtt_packet_connect, - { client_id = <<>> :: mqtt_client_id(), - proto_ver = ?MQTT_PROTO_V4 :: mqtt_vsn(), - proto_name = <<"MQTT">> :: binary(), - will_retain = false :: boolean(), - will_qos = ?QOS_1 :: mqtt_qos(), - will_flag = false :: boolean(), - clean_sess = false :: boolean(), - clean_start = true :: boolean(), - keep_alive = 60 :: non_neg_integer(), - will_props = undefined :: undefined | map(), - will_topic = undefined :: undefined | binary(), - will_msg = undefined :: undefined | binary(), - username = undefined :: undefined | binary(), - password = undefined :: undefined | binary(), - is_bridge = false :: boolean(), - properties = undefined :: mqtt_properties() %% MQTT Version 5.0 + { proto_name = <<"MQTT">> :: binary(), + proto_ver = ?MQTT_PROTO_V4 :: mqtt_version(), + is_bridge = false :: boolean(), + clean_start = true :: boolean(), + will_flag = false :: boolean(), + will_qos = ?QOS_1 :: mqtt_qos(), + will_retain = false :: boolean(), + keepalive = 0 :: non_neg_integer(), + properties = undefined :: mqtt_properties(), + client_id = <<>> :: mqtt_client_id(), + will_props = undefined :: undefined | map(), + will_topic = undefined :: undefined | binary(), + will_payload = undefined :: undefined | binary(), + username = undefined :: undefined | binary(), + password = undefined :: undefined | binary() }). -record(mqtt_packet_connack, - { ack_flags = ?RESERVED :: 0 | 1, - reason_code :: mqtt_connack(), - properties :: map() + { ack_flags :: 0 | 1, + reason_code :: mqtt_reason_code(), + properties :: mqtt_properties() }). -record(mqtt_packet_publish, - { topic_name :: binary(), + { topic_name :: mqtt_topic(), packet_id :: mqtt_packet_id(), properties :: mqtt_properties() }). @@ -235,13 +276,7 @@ -record(mqtt_packet_subscribe, { packet_id :: mqtt_packet_id(), properties :: mqtt_properties(), - topic_filters :: list({binary(), mqtt_subopt()}) - }). - --record(mqtt_packet_unsubscribe, - { packet_id :: mqtt_packet_id(), - properties :: mqtt_properties(), - topics :: list(binary()) + topic_filters :: [{mqtt_topic(), mqtt_subopts()}] }). -record(mqtt_packet_suback, @@ -250,9 +285,15 @@ reason_codes :: list(mqtt_reason_code()) }). +-record(mqtt_packet_unsubscribe, + { packet_id :: mqtt_packet_id(), + properties :: mqtt_properties(), + topic_filters :: [mqtt_topic()] + }). + -record(mqtt_packet_unsuback, - { packet_id :: mqtt_packet_id(), - properties :: mqtt_properties(), + { packet_id :: mqtt_packet_id(), + properties :: mqtt_properties(), reason_codes :: list(mqtt_reason_code()) }). @@ -311,6 +352,19 @@ reason_code = ReasonCode, properties = Properties}}). +-define(AUTH_PACKET(), + #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, + variable = #mqtt_packet_auth{reason_code = 0}}). + +-define(AUTH_PACKET(ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, + variable = #mqtt_packet_auth{reason_code = ReasonCode}}). + +-define(AUTH_PACKET(ReasonCode, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, + variable = #mqtt_packet_auth{reason_code = ReasonCode, + properties = Properties}}). + -define(PUBLISH_PACKET(Qos, PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = Qos}, @@ -323,31 +377,64 @@ packet_id = PacketId}, payload = Payload}). +-define(PUBLISH_PACKET(Header, Topic, PacketId, Properties, Payload), + #mqtt_packet{header = Header = #mqtt_packet_header{type = ?PUBLISH}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId, + properties = Properties}, + payload = Payload}). + -define(PUBACK_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, variable = #mqtt_packet_puback{packet_id = PacketId}}). --define(PUBACK_PACKET(Type, PacketId), - #mqtt_packet{header = #mqtt_packet_header{type = Type}, - variable = #mqtt_packet_puback{packet_id = PacketId}}). +-define(PUBACK_PACKET(PacketId, ReasonCode, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties}}). -define(PUBREC_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, variable = #mqtt_packet_puback{packet_id = PacketId}}). +-define(PUBREC_PACKET(PacketId, ReasonCode, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties}}). + -define(PUBREL_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, variable = #mqtt_packet_puback{packet_id = PacketId}}). +-define(PUBREL_PACKET(PacketId, ReasonCode, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties}}). + -define(PUBCOMP_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, variable = #mqtt_packet_puback{packet_id = PacketId}}). +-define(PUBCOMP_PACKET(PacketId, ReasonCode, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties}}). + -define(SUBSCRIBE_PACKET(PacketId, TopicFilters), #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, qos = ?QOS_1}, variable = #mqtt_packet_subscribe{packet_id = PacketId, topic_filters = TopicFilters}}). +-define(SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), + #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, qos = ?QOS_1}, + variable = #mqtt_packet_subscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}}). + -define(SUBACK_PACKET(PacketId, ReasonCodes), #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, variable = #mqtt_packet_suback{packet_id = PacketId, @@ -358,14 +445,39 @@ variable = #mqtt_packet_suback{packet_id = PacketId, properties = Properties, reason_codes = ReasonCodes}}). --define(UNSUBSCRIBE_PACKET(PacketId, Topics), +-define(UNSUBSCRIBE_PACKET(PacketId, TopicFilters), #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, qos = ?QOS_1}, - variable = #mqtt_packet_unsubscribe{packet_id = PacketId, - topics = Topics}}). + variable = #mqtt_packet_unsubscribe{packet_id = PacketId, + topic_filters = TopicFilters}}). + +-define(UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), + #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, qos = ?QOS_1}, + variable = #mqtt_packet_unsubscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}}). + -define(UNSUBACK_PACKET(PacketId), - #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, + #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, variable = #mqtt_packet_unsuback{packet_id = PacketId}}). +-define(UNSUBACK_PACKET(PacketId, Properties, ReasonCodes), + #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, + variable = #mqtt_packet_unsuback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes}}). + +-define(DISCONNECT_PACKET(), + #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}}). + +-define(DISCONNECT_PACKET(ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, + variable = #mqtt_packet_disconnect{reason_code = ReasonCode}}). + +-define(DISCONNECT_PACKET(ReasonCode, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, + variable = #mqtt_packet_disconnect{reason_code = ReasonCode, + properties = Properties}}). + -define(PACKET(Type), #mqtt_packet{header = #mqtt_packet_header{type = Type}}). @@ -406,6 +518,11 @@ -type(mqtt_message() :: #mqtt_message{}). +-define(WILL_MSG(Qos, Retain, Topic, Props, Payload), + #mqtt_message{qos = WillQos, retain = WillRetain, + topic = WillTopic, properties = Props, + payload = WillPayload}). + %%-------------------------------------------------------------------- %% MQTT Delivery %%-------------------------------------------------------------------- diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index e40f181cb..57db2defe 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -56,7 +56,7 @@ -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> - gen_server:start_link(emqx_misc:proc_name(?MODULE, Id), + gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, [Pool, Id], [{hibernate_after, 2000}]). %%-------------------------------------------------------------------- diff --git a/src/emqx_client.erl b/src/emqx_client.erl index fbfb92997..ef640fe43 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -19,19 +19,23 @@ -behaviour(gen_statem). -include("emqx_mqtt.hrl"). --include("emqx_client.hrl"). -export([start_link/0, start_link/1]). --export([subscribe/2, subscribe/3, unsubscribe/2]). +-export([subscribe/2, subscribe/3, subscribe/4]). -export([publish/2, publish/3, publish/4, publish/5]). +-export([unsubscribe/2, unsubscribe/3]). -export([ping/1]). --export([disconnect/1, disconnect/2]). --export([puback/2, pubrec/2, pubrel/2, pubcomp/2]). +-export([disconnect/1, disconnect/2, disconnect/3]). +-export([puback/2, puback/3, puback/4]). +-export([pubrec/2, pubrec/3, pubrec/4]). +-export([pubrel/2, pubrel/3, pubrel/4]). +-export([pubcomp/2, pubcomp/3, pubcomp/4]). -export([subscriptions/1]). +-export([info/1]). -export([initialized/3, waiting_for_connack/3, connected/3]). --export([init/1, callback_mode/0, terminate/3, code_change/4]). +-export([init/1, callback_mode/0, handle_event/4, terminate/3, code_change/4]). -type(host() :: inet:ip_address() | inet:hostname()). @@ -43,6 +47,8 @@ | {tcp_opts, [gen_tcp:option()]} | {ssl, boolean()} | {ssl_opts, [ssl:ssl_option()]} + | {connect_timeout, pos_integer()} + | {bridge_mode, boolean()} | {client_id, iodata()} | {clean_start, boolean()} | {username, iodata()} @@ -55,12 +61,13 @@ | {will_payload, iodata()} | {will_retain, boolean()} | {will_qos, mqtt_qos() | mqtt_qos_name()} - | {connect_timeout, pos_integer()} + | {will_props, mqtt_properties()} + | {auto_ack, boolean()} | {ack_timeout, pos_integer()} | {force_ping, boolean()} - | {debug_mode, boolean()}). + | {properties, mqtt_properties()}). --export_type([option/0]). +-export_type([host/0, option/0]). -record(state, {name :: atom(), owner :: pid(), @@ -69,46 +76,69 @@ hosts :: [{host(), inet:port_number()}], socket :: inet:socket(), sock_opts :: [emqx_client_sock:option()], - receiver :: pid(), + connect_timeout :: pos_integer(), + bridge_mode :: boolean(), client_id :: binary(), clean_start :: boolean(), + session_present :: boolean(), username :: binary() | undefined, password :: binary() | undefined, - proto_ver :: mqtt_vsn(), + proto_ver :: mqtt_version(), proto_name :: iodata(), keepalive :: non_neg_integer(), keepalive_timer :: reference() | undefined, + expiry_interval :: pos_integer(), force_ping :: boolean(), will_flag :: boolean(), will_msg :: mqtt_message(), + properties :: mqtt_properties(), pending_calls :: list(), - subscribers :: list(), subscriptions :: map(), max_inflight :: infinity | pos_integer(), inflight :: emqx_inflight:inflight(), awaiting_rel :: map(), - properties :: list(), auto_ack :: boolean(), ack_timeout :: pos_integer(), ack_timer :: reference(), retry_interval :: pos_integer(), retry_timer :: reference(), - connect_timeout :: pos_integer(), last_packet_id :: mqtt_packet_id(), - debug_mode :: boolean()}). + parse_state :: emqx_parser:state()}). -record(call, {id, from, req, ts}). -type(client() :: pid() | atom()). --type(msgid() :: mqtt_packet_id()). +-type(topic() :: mqtt_topic()). --type(pubopt() :: {retain, boolean()} - | {qos, mqtt_qos()}). +-type(payload() :: iodata()). --type(subopt() :: mqtt_subopt()). +-type(packet_id() :: mqtt_packet_id()). --export_type([client/0, host/0, msgid/0, pubopt/0, subopt/0]). +-type(properties() :: mqtt_properties()). + +-type(qos() :: mqtt_qos_name() | mqtt_qos()). + +-type(pubopt() :: {retain, boolean()} | {qos, qos()}). + +-type(subopt() :: {rh, 0 | 1 | 2} + | {rap, boolean()} + | {nl, boolean()} + | {qos, qos()}). + +-type(reason_code() :: mqtt_reason_code()). + +-type(subscribe_ret() :: {ok, properties(), [reason_code()]} | {error, term()}). + +-export_type([client/0, topic/0, qos/0, properties/0, payload/0, + packet_id/0, pubopt/0, subopt/0, reason_code/0]). + +%% Default timeout +-define(DEFAULT_KEEPALIVE, 60000). +-define(DEFAULT_ACK_TIMEOUT, 30000). +-define(DEFAULT_CONNECT_TIMEOUT, 60000). + +-define(PROPERTY(Name, Val), #state{properties = #{Name := Val}}). %%-------------------------------------------------------------------- %% API @@ -121,6 +151,8 @@ start_link() -> start_link([]). start_link(Options) when is_map(Options) -> start_link(maps:to_list(Options)); start_link(Options) when is_list(Options) -> + ok = emqx_mqtt_properties:validate( + proplists:get_value(properties, Options, #{})), case start_client(with_owner(Options)) of {ok, Client} -> connect(Client); @@ -142,110 +174,183 @@ with_owner(Options) -> end. %% @private -%% @doc should be called with start_link --spec(connect(client()) -> {ok, client()} | {error, term()}). +-spec(connect(client()) -> {ok, client(), properties()} | {error, term()}). connect(Client) -> gen_statem:call(Client, connect, infinity). -%% @doc Publish QoS0 message to broker. --spec(publish(client(), iolist(), iodata()) -> ok | {error, term()}). -publish(Client, Topic, Payload) -> - publish(Client, #mqtt_message{topic = iolist_to_binary(Topic), +-spec(subscribe(client(), topic() | {topic(), qos() | [subopt()]}) + -> subscribe_ret()). +subscribe(Client, Topic) when is_binary(Topic) -> + subscribe(Client, {Topic, ?QOS_0}); +subscribe(Client, {Topic, QoS}) when is_binary(Topic), is_atom(QoS) -> + subscribe(Client, {Topic, ?QOS_I(QoS)}); +subscribe(Client, {Topic, QoS}) when is_binary(Topic), ?IS_QOS(QoS) -> + subscribe(Client, [{Topic, ?QOS_I(QoS)}]); +subscribe(Client, Topics) when is_list(Topics) -> + subscribe(Client, #{}, lists:map( + fun({Topic, QoS}) when is_binary(Topic), is_atom(QoS) -> + {Topic, [{qos, ?QOS_I(QoS)}]}; + ({Topic, QoS}) when is_binary(Topic), ?IS_QOS(QoS) -> + {Topic, [{qos, ?QOS_I(QoS)}]}; + ({Topic, Opts}) when is_binary(Topic), is_list(Opts) -> + {Topic, Opts} + end, Topics)). + +-spec(subscribe(client(), topic(), qos() | [subopt()]) -> + subscribe_ret(); + (client(), properties(), [{topic(), qos() | [subopt()]}]) -> + subscribe_ret()). +subscribe(Client, Topic, QoS) when is_binary(Topic), is_atom(QoS) -> + subscribe(Client, Topic, ?QOS_I(QoS)); +subscribe(Client, Topic, QoS) when is_binary(Topic), ?IS_QOS(QoS) -> + subscribe(Client, Topic, [{qos, QoS}]); +subscribe(Client, Topic, Opts) when is_binary(Topic), is_list(Opts) -> + subscribe(Client, #{}, [{Topic, Opts}]); +subscribe(Client, Properties, Topics) when is_map(Properties), is_list(Topics) -> + Topics1 = [{Topic, parse_subopt(Opts)} || {Topic, Opts} <- Topics], + gen_statem:call(Client, {subscribe, Properties, Topics1}). + +-spec(subscribe(client(), properties(), topic(), qos() | [subopt()]) + -> subscribe_ret()). +subscribe(Client, Properties, Topic, QoS) + when is_map(Properties), is_binary(Topic), is_atom(QoS) -> + subscribe(Client, Properties, Topic, ?QOS_I(QoS)); +subscribe(Client, Properties, Topic, QoS) + when is_map(Properties), is_binary(Topic), ?IS_QOS(QoS) -> + subscribe(Client, Properties, Topic, [{qos, QoS}]); +subscribe(Client, Properties, Topic, Opts) + when is_map(Properties), is_binary(Topic), is_list(Opts) -> + subscribe(Client, Properties, [{Topic, Opts}]). + +parse_subopt(Opts) -> + parse_subopt(Opts, #mqtt_subopts{}). + +parse_subopt([], Rec) -> + Rec; +parse_subopt([{rh, I} | Opts], Rec) when I >= 0, I =< 2 -> + parse_subopt(Opts, Rec#mqtt_subopts{rh = I}); +parse_subopt([{rap, true} | Opts], Rec) -> + parse_subopt(Opts, Rec#mqtt_subopts{rap =1}); +parse_subopt([{rap, false} | Opts], Rec) -> + parse_subopt(Opts, Rec#mqtt_subopts{rap = 0}); +parse_subopt([{nl, true} | Opts], Rec) -> + parse_subopt(Opts, Rec#mqtt_subopts{nl = 1}); +parse_subopt([{nl, false} | Opts], Rec) -> + parse_subopt(Opts, Rec#mqtt_subopts{nl = 0}); +parse_subopt([{qos, QoS} | Opts], Rec) -> + parse_subopt(Opts, Rec#mqtt_subopts{qos = ?QOS_I(QoS)}). + +-spec(publish(client(), topic(), payload()) -> ok | {error, term()}). +publish(Client, Topic, Payload) when is_binary(Topic) -> + publish(Client, #mqtt_message{topic = Topic, qos = ?QOS_0, payload = iolist_to_binary(Payload)}). -%% @doc Publish message to broker with qos, retain options. --spec(publish(client(), iolist(), iodata(), - mqtt_qos() | mqtt_qos_name() | [pubopt()]) - -> ok | {ok, msgid()} | {error, term()}). -publish(Client, Topic, Payload, QoS) when is_atom(QoS) -> - publish(Client, Topic, Payload, ?QOS_I(QoS)); -publish(Client, Topic, Payload, QoS) when ?IS_QOS(QoS) -> +-spec(publish(client(), topic(), payload(), qos() | [pubopt()]) + -> ok | {ok, packet_id()} | {error, term()}). +publish(Client, Topic, Payload, QoS) when is_binary(Topic), is_atom(QoS) -> + publish(Client, Topic, Payload, [{qos, ?QOS_I(QoS)}]); +publish(Client, Topic, Payload, QoS) when is_binary(Topic), ?IS_QOS(QoS) -> publish(Client, Topic, Payload, [{qos, QoS}]); -publish(Client, Topic, Payload, Options) when is_list(Options) -> - publish(Client, Topic, [], Payload, Options). -publish(Client, Topic, Properties, Payload, Options) -> +publish(Client, Topic, Payload, Opts) when is_binary(Topic), is_list(Opts) -> + publish(Client, Topic, #{}, Payload, Opts). + +%% MQTT Version 5.0 +-spec(publish(client(), topic(), properties(), payload(), [pubopt()]) + -> ok | {ok, packet_id()} | {error, term()}). +publish(Client, Topic, Properties, Payload, Opts) + when is_binary(Topic), is_map(Properties), is_list(Opts) -> ok = emqx_mqtt_properties:validate(Properties), - publish(Client, #mqtt_message{qos = pubopt(qos, Options), - retain = pubopt(retain, Options), - topic = iolist_to_binary(Topic), + Retain = proplists:get_bool(retain, Opts), + QoS = ?QOS_I(proplists:get_value(qos, Opts, ?QOS_0)), + publish(Client, #mqtt_message{topic = Topic, + qos = QoS, + retain = Retain, properties = Properties, payload = iolist_to_binary(Payload)}). -%% @doc Publish MQTT Message. -spec(publish(client(), mqtt_message()) - -> ok | {ok, msgid()} | {error, term()}). + -> ok | {ok, packet_id()} | {error, term()}). publish(Client, Msg) when is_record(Msg, mqtt_message) -> gen_statem:call(Client, {publish, Msg}). -pubopt(qos, Opts) -> - proplists:get_value(qos, Opts, ?QOS0); -pubopt(retain, Opts) -> - lists:member(retain, Opts) orelse proplists:get_bool(retain, Opts). - --spec(subscribe(client(), binary() - | {binary(), mqtt_qos_name() | mqtt_qos()}) - -> {ok, mqtt_qos()} | {error, term()}). -subscribe(Client, Topic) when is_binary(Topic) -> - subscribe(Client, Topic, ?QOS_0); -subscribe(Client, {Topic, QoS}) when ?IS_QOS(QoS); is_atom(QoS) -> - subscribe(Client, Topic, ?QOS_I(QoS)); -subscribe(Client, Topics) when is_list(Topics) -> - case io_lib:printable_unicode_list(Topics) of - true -> subscribe(Client, [{Topics, ?QOS_0}]); - false -> Topics1 = [{iolist_to_binary(Topic), [{qos, ?QOS_I(QoS)}]} - || {Topic, QoS} <- Topics], - gen_statem:call(Client, {subscribe, Topics1}) - end. - --spec(subscribe(client(), string() | binary(), - mqtt_qos_name() | mqtt_qos() | [subopt()]) - -> {ok, mqtt_qos()} | {error, term()}). -subscribe(Client, Topic, QoS) when is_atom(QoS) -> - subscribe(Client, Topic, ?QOS_I(QoS)); -subscribe(Client, Topic, QoS) when ?IS_QOS(QoS) -> - subscribe(Client, Topic, [{qos, QoS}]); -subscribe(Client, Topic, Options) -> - gen_statem:call(Client, {subscribe, iolist_to_binary(Topic), Options}). - --spec(unsubscribe(client(), iolist()) -> ok | {error, term()}). +-spec(unsubscribe(client(), topic() | [topic()]) -> subscribe_ret()). unsubscribe(Client, Topic) when is_binary(Topic) -> unsubscribe(Client, [Topic]); unsubscribe(Client, Topics) when is_list(Topics) -> - case io_lib:printable_unicode_list(Topics) of - true -> unsubscribe(Client, [Topics]); - false -> - Topics1 = [iolist_to_binary(Topic) || Topic <- Topics], - gen_statem:call(Client, {unsubscribe, Topics1}) - end. + unsubscribe(Client, #{}, Topics). + +%% MQTT Version 5.0 +-spec(unsubscribe(client(), properties(), topic() | [topic()]) -> subscribe_ret()). +unsubscribe(Client, Properties, Topic) when is_map(Properties), is_binary(Topic) -> + unsubscribe(Client, Properties, [Topic]); +unsubscribe(Client, Properties, Topics) when is_map(Properties), is_list(Topics) -> + gen_statem:call(Client, {unsubscribe, Properties, Topics}). -spec(ping(client()) -> pong). ping(Client) -> gen_statem:call(Client, ping). -spec(disconnect(client()) -> ok). -disconnect(C) -> - disconnect(C, []). +disconnect(Client) -> + disconnect(Client, ?RC_SUCCESS). -disconnect(Client, Props) -> - gen_statem:call(Client, {disconnect, Props}). +-spec(disconnect(client(), reason_code()) -> ok). +disconnect(Client, ReasonCode) -> + disconnect(Client, ReasonCode, #{}). + +-spec(disconnect(client(), reason_code(), properties()) -> ok). +disconnect(Client, ReasonCode, Properties) -> + gen_statem:call(Client, {disconnect, ReasonCode, Properties}). %%-------------------------------------------------------------------- -%% APIs for broker test cases. +%% For test cases. %%-------------------------------------------------------------------- puback(Client, PacketId) when is_integer(PacketId) -> - gen_statem:cast(Client, {puback, PacketId}). + puback(Client, PacketId, ?RC_SUCCESS). +puback(Client, PacketId, ReasonCode) when is_integer(PacketId), + is_integer(ReasonCode) -> + puback(Client, PacketId, ReasonCode, #{}). +puback(Client, PacketId, ReasonCode, Properties) when is_integer(PacketId), + is_integer(ReasonCode), + is_map(Properties) -> + gen_statem:cast(Client, {puback, PacketId, ReasonCode, Properties}). pubrec(Client, PacketId) when is_integer(PacketId) -> - gen_statem:cast(Client, {pubrec, PacketId}). + pubrec(Client, PacketId, ?RC_SUCCESS). +pubrec(Client, PacketId, ReasonCode) when is_integer(PacketId), + is_integer(ReasonCode) -> + pubrec(Client, PacketId, ReasonCode, #{}). +pubrec(Client, PacketId, ReasonCode, Properties) when is_integer(PacketId), + is_integer(ReasonCode), + is_map(Properties) -> + gen_statem:cast(Client, {pubrec, PacketId, ReasonCode, Properties}). pubrel(Client, PacketId) when is_integer(PacketId) -> - gen_statem:cast(Client, {pubrel, PacketId}). + pubrel(Client, PacketId, ?RC_SUCCESS). +pubrel(Client, PacketId, ReasonCode) when is_integer(PacketId), + is_integer(ReasonCode) -> + pubrel(Client, PacketId, ReasonCode, #{}). +pubrel(Client, PacketId, ReasonCode, Properties) when is_integer(PacketId), + is_integer(ReasonCode), + is_map(Properties) -> + gen_statem:cast(Client, {pubrel, PacketId, ReasonCode, Properties}). pubcomp(Client, PacketId) when is_integer(PacketId) -> - gen_statem:cast(Client, {pubcomp, PacketId}). + pubcomp(Client, PacketId, ?RC_SUCCESS). +pubcomp(Client, PacketId, ReasonCode) when is_integer(PacketId), + is_integer(ReasonCode) -> + pubcomp(Client, PacketId, ReasonCode, #{}). +pubcomp(Client, PacketId, ReasonCode, Properties) when is_integer(PacketId), + is_integer(ReasonCode), + is_map(Properties) -> + gen_statem:cast(Client, {pubcomp, PacketId, ReasonCode, Properties}). -subscriptions(C) -> gen_statem:call(C, subscriptions). +subscriptions(Client) -> + gen_statem:call(Client, subscriptions). + +info(Client) -> + gen_statem:call(Client, info). %%-------------------------------------------------------------------- %% gen_statem callbacks @@ -255,7 +360,7 @@ init([Options]) -> process_flag(trap_exit, true), ClientId = case {proplists:get_value(proto_ver, Options, v4), proplists:get_value(client_id, Options)} of - {v5, undefined} -> undefined; + {v5, undefined} -> <<>>; {_ver, undefined} -> random_client_id(); {_ver, Id} -> iolist_to_binary(Id) end, @@ -263,28 +368,27 @@ init([Options]) -> port = 1883, hosts = [], sock_opts = [], + bridge_mode = false, client_id = ClientId, clean_start = true, proto_ver = ?MQTT_PROTO_V4, proto_name = <<"MQTT">>, keepalive = ?DEFAULT_KEEPALIVE, + force_ping = false, will_flag = false, will_msg = #mqtt_message{}, - ack_timeout = ?DEFAULT_ACK_TIMEOUT, - connect_timeout = ?DEFAULT_CONNECT_TIMEOUT, - force_ping = false, pending_calls = [], - subscribers = [], subscriptions = #{}, max_inflight = infinity, inflight = emqx_inflight:new(0), awaiting_rel = #{}, + properties = #{}, auto_ack = true, + ack_timeout = ?DEFAULT_ACK_TIMEOUT, retry_interval = 0, - last_packet_id = 1, - debug_mode = false}), - %% Connect and Send ConnAck - {ok, initialized, State}. + connect_timeout = ?DEFAULT_CONNECT_TIMEOUT, + last_packet_id = 1}), + {ok, initialized, init_parse_state(State)}. random_client_id() -> rand:seed(exsplus, erlang:timestamp()), @@ -344,12 +448,14 @@ init([{proto_ver, v5} | Opts], State) -> init([{will_topic, Topic} | Opts], State = #state{will_msg = WillMsg}) -> WillMsg1 = init_will_msg({topic, Topic}, WillMsg), init(Opts, State#state{will_flag = true, will_msg = WillMsg1}); +init([{will_props, Properties} | Opts], State = #state{will_msg = WillMsg}) -> + init(Opts, State#state{will_msg = init_will_msg({props, Properties}, WillMsg)}); init([{will_payload, Payload} | Opts], State = #state{will_msg = WillMsg}) -> - init(Opts, State#state{will_msg = init_will_msg({payload, Payload}, WillMsg)}); + init(Opts, State#state{will_msg = init_will_msg({payload, Payload}, WillMsg)}); init([{will_retain, Retain} | Opts], State = #state{will_msg = WillMsg}) -> - init(Opts, State#state{will_msg = init_will_msg({retain, Retain}, WillMsg)}); + init(Opts, State#state{will_msg = init_will_msg({retain, Retain}, WillMsg)}); init([{will_qos, QoS} | Opts], State = #state{will_msg = WillMsg}) -> - init(Opts, State#state{will_msg = init_will_msg({qos, QoS}, WillMsg)}); + init(Opts, State#state{will_msg = init_will_msg({qos, QoS}, WillMsg)}); init([{connect_timeout, Timeout}| Opts], State) -> init(Opts, State#state{connect_timeout = timer:seconds(Timeout)}); init([{ack_timeout, Timeout}| Opts], State) -> @@ -358,25 +464,29 @@ init([force_ping | Opts], State) -> init(Opts, State#state{force_ping = true}); init([{force_ping, ForcePing} | Opts], State) when is_boolean(ForcePing) -> init(Opts, State#state{force_ping = ForcePing}); +init([{properties, Properties} | Opts], State = #state{properties = InitProps}) -> + init(Opts, State#state{properties = maps:merge(InitProps, Properties)}); init([{max_inflight, infinity} | Opts], State) -> init(Opts, State#state{max_inflight = infinity, - inflight = emqx_inflight:new(0)}); + inflight = emqx_inflight:new(0)}); init([{max_inflight, I} | Opts], State) when is_integer(I) -> init(Opts, State#state{max_inflight = I, - inflight = emqx_inflight:new(I)}); + inflight = emqx_inflight:new(I)}); init([auto_ack | Opts], State) -> init(Opts, State#state{auto_ack = true}); init([{auto_ack, AutoAck} | Opts], State) when is_boolean(AutoAck) -> init(Opts, State#state{auto_ack = AutoAck}); init([{retry_interval, I} | Opts], State) -> init(Opts, State#state{retry_interval = timer:seconds(I)}); -init([{debug_mode, Mode} | Opts], State) when is_boolean(Mode) -> - init(Opts, State#state{debug_mode = Mode}); +init([{bridge_mode, Mode} | Opts], State) when is_boolean(Mode) -> + init(Opts, State#state{bridge_mode = Mode}); init([_Opt | Opts], State) -> init(Opts, State). init_will_msg({topic, Topic}, WillMsg) -> WillMsg#mqtt_message{topic = iolist_to_binary(Topic)}; +init_will_msg({props, Properties}, WillMsg) -> + WillMsg#mqtt_message{properties = Properties}; init_will_msg({payload, Payload}, WillMsg) -> WillMsg#mqtt_message{payload = iolist_to_binary(Payload)}; init_will_msg({retain, Retain}, WillMsg) when is_boolean(Retain) -> @@ -384,104 +494,123 @@ init_will_msg({retain, Retain}, WillMsg) when is_boolean(Retain) -> init_will_msg({qos, QoS}, WillMsg) -> WillMsg#mqtt_message{qos = ?QOS_I(QoS)}. +init_parse_state(State = #state{proto_ver = Ver, properties = Properties}) -> + Size = maps:get('Maximum-Packet-Size', Properties, ?MAX_PACKET_SIZE), + State#state{parse_state = emqx_parser:initial_state([{max_len, Size}, + {version, Ver}])}. + callback_mode() -> state_functions. -initialized({call, From}, connect, State = #state{connect_timeout = Timeout}) -> - case sock_connect(State) of - {ok, State1} -> - case mqtt_connect(State1) of - {ok, State2} -> +initialized({call, From}, connect, State = #state{sock_opts = SockOpts, + connect_timeout = Timeout}) -> + case sock_connect(hosts(State), SockOpts, Timeout) of + {ok, Sock} -> + case mqtt_connect(run_sock(State#state{socket = Sock})) of + {ok, NewState} -> {next_state, waiting_for_connack, - add_call(new_call(connect, From), State2), [Timeout]}; - Err = {error, Reason} -> - {stop_and_reply, Reason, [{reply, From, Err}]} + add_call(new_call(connect, From), NewState), [Timeout]}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} end; - Err = {error, Reason} -> - {stop_and_reply, Reason, [{reply, From, Err}]} + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} end; -initialized(EventType, EventContent, StateData) -> - handle_event(EventType, EventContent, StateData). +initialized(EventType, EventContent, State) -> + handle_event(EventType, EventContent, initialized, State). -sock_connect(State) -> - sock_connect(get_hosts(State), {error, no_hosts}, State). - -get_hosts(#state{hosts = [], host = Host, port = Port}) -> - [{Host, Port}]; -get_hosts(#state{hosts = Hosts}) -> Hosts. - -sock_connect([], Err, _State) -> - Err; -sock_connect([{Host, Port} | Hosts], _Err, State = #state{sock_opts = SockOpts}) -> - case emqx_client_sock:connect(self(), Host, Port, SockOpts) of - {ok, Socket, Receiver} -> - {ok, State#state{socket = Socket, receiver = Receiver}}; - Err = {error, _Reason} -> - sock_connect(Hosts, Err, State) - end. - -mqtt_connect(State = #state{client_id = ClientId, - clean_start = CleanStart, - username = Username, - password = Password, - proto_ver = ProtoVer, - proto_name = ProtoName, - keepalive = KeepAlive, - will_flag = WillFlag, - will_msg = WillMsg}) -> - #mqtt_message{qos = WillQos, - retain = WillRetain, - topic = WillTopic, - payload = WillPayload} = WillMsg, +mqtt_connect(State = #state{client_id = ClientId, + clean_start = CleanStart, + bridge_mode = IsBridge, + username = Username, + password = Password, + proto_ver = ProtoVer, + proto_name = ProtoName, + keepalive = KeepAlive, + will_flag = WillFlag, + will_msg = WillMsg, + properties = Properties}) -> + ?WILL_MSG(WillQos, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, + ConnProps = emqx_mqtt_properties:filter(?CONNECT, maps:to_list(Properties)), send(?CONNECT_PACKET( - #mqtt_packet_connect{client_id = ClientId, - clean_start = CleanStart, - proto_ver = ProtoVer, - proto_name = ProtoName, - will_flag = WillFlag, - will_retain = WillRetain, - will_qos = WillQos, - keep_alive = KeepAlive, - will_topic = WillTopic, - will_msg = WillPayload, - username = Username, - password = Password}), State). + #mqtt_packet_connect{proto_ver = ProtoVer, + proto_name = ProtoName, + is_bridge = IsBridge, + clean_start = CleanStart, + will_flag = WillFlag, + will_qos = WillQos, + will_retain = WillRetain, + keepalive = KeepAlive, + properties = ConnProps, + client_id = ClientId, + will_props = WillProps, + will_topic = WillTopic, + will_payload = WillPayload, + username = Username, + password = Password}), State). -waiting_for_connack(cast, ?CONNACK_PACKET(?CONNACK_ACCEPT, - _SessPresent, - _Properties), State) -> +waiting_for_connack(cast, ?CONNACK_PACKET(?RC_SUCCESS, + SessPresent, + Properties), + State = #state{properties = AllProps}) -> case take_call(connect, State) of {value, #call{from = From}, State1} -> - {next_state, connected, - ensure_keepalive_timer(ensure_ack_timer(State1)), - [{reply, From, {ok, self()}}]}; + AllProps1 = case Properties of + undefined -> AllProps; + _ -> maps:merge(AllProps, Properties) + end, + Reply = {ok, self(), Properties}, + State2 = State1#state{properties = AllProps1, + session_present = SessPresent}, + {next_state, connected, ensure_keepalive_timer(State2), + [{reply, From, Reply}]}; false -> - io:format("Cannot find call: ~p~n", [State#state.pending_calls]), - {stop, {error, unexpected_connack}} + {stop, bad_connack} end; waiting_for_connack(cast, ?CONNACK_PACKET(ReasonCode, _SessPresent, - _Properties), State) -> - reply_connack_error(emqx_packet:connack_error(ReasonCode), State); + Properties), State) -> + Reason = emqx_reason_codes:name(ReasonCode), + case take_call(connect, State) of + {value, #call{from = From}, _State} -> + Reply = {error, {Reason, Properties}}, + {stop_and_reply, Reason, [{reply, From, Reply}]}; + false -> {stop, connack_error} + end; waiting_for_connack(timeout, _Timeout, State) -> - reply_connack_error(connack_timeout, State); - -waiting_for_connack(EventType, EventContent, StateData) -> - handle_event(EventType, EventContent, StateData). - -reply_connack_error(Reason, State) -> - Error = {error, Reason}, case take_call(connect, State) of - {value, #call{from = From}, State1} -> - {stop_and_reply, Error, [{reply, From, Error}], State1}; - false -> {stop, Error} - end. + {value, #call{from = From}, _State} -> + Reply = {error, connack_timeout}, + {stop_and_reply, connack_timeout, [{reply, From, Reply}]}; + false -> {stop, connack_timeout} + end; + +waiting_for_connack(EventType, EventContent, State) -> + handle_event(EventType, EventContent, waiting_for_connack, State). connected({call, From}, subscriptions, State = #state{subscriptions = Subscriptions}) -> {keep_state, State, [{reply, From, maps:to_list(Subscriptions)}]}; +connected({call, From}, info, State) -> + Info = lists:zip(record_info(fields, state), tl(tuple_to_list(State))), + {keep_state, State, [{reply, From, Info}]}; + +connected({call, From}, SubReq = {subscribe, Properties, Topics}, + State = #state{last_packet_id = PacketId, subscriptions = Subscriptions}) -> + case send(?SUBSCRIBE_PACKET(PacketId, Properties, Topics), State) of + {ok, NewState} -> + Call = new_call({subscribe, PacketId}, From, SubReq), + Subscriptions1 = + lists:foldl(fun({Topic, Opts}, Acc) -> + maps:put(Topic, Opts, Acc) + end, Subscriptions, Topics), + {keep_state, ensure_ack_timer(add_call(Call,NewState#state{subscriptions = Subscriptions1}))}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + connected({call, From}, {publish, Msg = #mqtt_message{qos = ?QOS_0}}, State) -> case send(Msg, State) of {ok, NewState} -> @@ -508,36 +637,9 @@ connected({call, From}, {publish, Msg = #mqtt_message{qos = Qos}}, end end; -connected({call, From}, SubReq = {subscribe, Topic, SubOpts}, - State= #state{last_packet_id = PacketId, subscriptions = Subscriptions}) -> - %%TODO: handle qos... - QoS = proplists:get_value(qos, SubOpts, ?QOS_0), - case send(?SUBSCRIBE_PACKET(PacketId, [{Topic, QoS}]), State) of - {ok, NewState} -> - Call = new_call({subscribe, PacketId}, From, SubReq), - Subscriptions1 = maps:put(Topic, SubOpts, Subscriptions), - {keep_state, ensure_ack_timer(add_call(Call, NewState#state{subscriptions = Subscriptions1}))}; - Error = {error, Reason} -> - {stop_and_reply, Reason, [{reply, From, Error}]} - end; - -connected({call, From}, SubReq = {subscribe, Topics}, - State= #state{last_packet_id = PacketId, subscriptions = Subscriptions}) -> - case send(?SUBSCRIBE_PACKET(PacketId, Topics), State) of - {ok, NewState} -> - Call = new_call({subscribe, PacketId}, From, SubReq), - Subscriptions1 = - lists:fold(fun({Topic, SubOpts}, Acc) -> - maps:put(Topic, SubOpts, Acc) - end, Subscriptions, Topics), - {keep_state, ensure_ack_timer(add_call(Call, NewState#state{subscriptions = Subscriptions1}))}; - Error = {error, Reason} -> - {stop_and_reply, Reason, [{reply, From, Error}]} - end; - -connected({call, From}, UnsubReq = {unsubscribe, Topics}, +connected({call, From}, UnsubReq = {unsubscribe, Properties, Topics}, State = #state{last_packet_id = PacketId}) -> - case send(?UNSUBSCRIBE_PACKET(PacketId, Topics), State) of + case send(?UNSUBSCRIBE_PACKET(PacketId, Properties, Topics), State) of {ok, NewState} -> Call = new_call({unsubscribe, PacketId}, From, UnsubReq), {keep_state, ensure_ack_timer(add_call(Call, NewState))}; @@ -554,25 +656,25 @@ connected({call, From}, ping, State) -> {stop_and_reply, Reason, [{reply, From, Error}]} end; -connected({call, From}, disconnect, State) -> - case send(?PACKET(?DISCONNECT), State) of +connected({call, From}, {disconnect, ReasonCode, Properties}, State) -> + case send(?DISCONNECT_PACKET(ReasonCode, Properties), State) of {ok, NewState} -> {stop_and_reply, normal, [{reply, From, ok}], NewState}; - Error = {error, _Reason} -> - {stop_and_reply, disconnected, [{reply, From, Error}]} + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} end; -connected(cast, {puback, PacketId}, State) -> - send_puback(?PUBACK_PACKET(?PUBACK, PacketId), State); +connected(cast, {puback, PacketId, ReasonCode, Properties}, State) -> + send_puback(?PUBACK_PACKET(PacketId, ReasonCode, Properties), State); -connected(cast, {pubrec, PacketId}, State) -> - send_puback(?PUBACK_PACKET(?PUBREC, PacketId), State); +connected(cast, {pubrec, PacketId, ReasonCode, Properties}, State) -> + send_puback(?PUBREC_PACKET(PacketId, ReasonCode, Properties), State); -connected(cast, {pubrel, PacketId}, State) -> - send_puback(?PUBREL_PACKET(PacketId), State); +connected(cast, {pubrel, PacketId, ReasonCode, Properties}, State) -> + send_puback(?PUBREL_PACKET(PacketId, ReasonCode, Properties), State); -connected(cast, {pubcomp, PacketId}, State) -> - send_puback(?PUBCOMP_PACKET(PacketId), State); +connected(cast, {pubcomp, PacketId, ReasonCode, Properties}, State) -> + send_puback(?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), State); connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), State) -> {keep_state, deliver_msg(packet_to_msg(Packet), State)}; @@ -594,14 +696,16 @@ connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), Stop -> Stop end; -connected(cast, ?PUBACK_PACKET(PacketId), +connected(cast, ?PUBACK_PACKET(PacketId, ReasonCode, Properties), State = #state{owner = Owner, inflight = Inflight}) -> case Inflight:lookup(PacketId) of {value, {publish, #mqtt_message{packet_id = PacketId}, _Ts}} -> - Owner ! {puback, PacketId}, + Owner ! {puback, #{packet_id => PacketId, + reason_code => ReasonCode, + properties => Properties}}, {keep_state, State#state{inflight = Inflight:delete(PacketId)}}; none -> - ?LOG(warning, "Unexpected PUBACK: ~p", [PacketId]), + emqx_logger:warning("Unexpected PUBACK: ~p", [PacketId]), {keep_state, State} end; @@ -612,16 +716,16 @@ connected(cast, ?PUBREC_PACKET(PacketId), State = #state{inflight = Inflight}) - Inflight1 = Inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}), State#state{inflight = Inflight1}; {value, {pubrel, _Ref, _Ts}} -> - ?LOG(warning, "Duplicated PUBREC Packet: ~p", [PacketId]), + emqx_logger:warning("Duplicated PUBREC Packet: ~p", [PacketId]), State; none -> - ?LOG(warning, "Unexpected PUBREC Packet: ~p", [PacketId]), + emqx_logger:warning("Unexpected PUBREC Packet: ~p", [PacketId]), State end); %%TODO::... if auto_ack is false, should we take PacketId from the map? -connected(cast, ?PUBREL_PACKET(PacketId), State = #state{awaiting_rel = AwaitingRel, - auto_ack = AutoAck}) -> +connected(cast, ?PUBREL_PACKET(PacketId), + State = #state{awaiting_rel = AwaitingRel, auto_ack = AutoAck}) -> case maps:take(PacketId, AwaitingRel) of {Packet, AwaitingRel1} -> NewState = deliver_msg(packet_to_msg(Packet), @@ -631,54 +735,60 @@ connected(cast, ?PUBREL_PACKET(PacketId), State = #state{awaiting_rel = Awaiting false -> {keep_state, NewState} end; error -> - ?LOG(warning, "Unexpected PUBREL: ~p", [PacketId]), + emqx_logger:warning("Unexpected PUBREL: ~p", [PacketId]), {keep_state, State} end; -connected(cast, ?PUBCOMP_PACKET(PacketId), State = #state{owner = Owner, inflight = Inflight}) -> +connected(cast, ?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), + State = #state{owner = Owner, inflight = Inflight}) -> case Inflight:lookup(PacketId) of {value, {pubrel, _PacketId, _Ts}} -> - Owner ! {pubcomp, PacketId}, + Owner ! {puback, #{packet_id => PacketId, + reason_code => ReasonCode, + properties => Properties}}, {keep_state, State#state{inflight = Inflight:delete(PacketId)}}; none -> - ?LOG(warning, "Unexpected PUBCOMP Packet: ~p", [PacketId]), + emqx_logger:warning("Unexpected PUBCOMP Packet: ~p", [PacketId]), {keep_state, State} end; -%%TODO: handle suback... -connected(cast, ?SUBACK_PACKET(PacketId, QosTable), - State = #state{subscriptions = Subscriptions}) -> - ?LOG(info, "SUBACK(~p) Received", [PacketId]), +connected(cast, ?SUBACK_PACKET(PacketId, Properties, ReasonCodes), + State = #state{subscriptions = _Subscriptions}) -> case take_call({subscribe, PacketId}, State) of - {value, #call{from = From}, State1} -> - {keep_state, State1, [{reply, From, ok}]}; + {value, #call{from = From}, NewState} -> + %%TODO: Merge reason codes to subscriptions? + Reply = {ok, Properties, ReasonCodes}, + {keep_state, NewState, [{reply, From, Reply}]}; false -> {keep_state, State} end; -%%TODO: handle unsuback... -connected(cast, ?UNSUBACK_PACKET(PacketId), +connected(cast, ?UNSUBACK_PACKET(PacketId, Properties, ReasonCodes), State = #state{subscriptions = Subscriptions}) -> - ?LOG(info, "UNSUBACK(~p) received", [PacketId]), case take_call({unsubscribe, PacketId}, State) of - {value, #call{from = From, req = {_, Topics}}, State1} -> - {keep_state, State1#state{subscriptions = - lists:foldl(fun(Topic, Subs) -> - maps:remove(Topic, Subs) - end, Subscriptions, Topics)}, - [{reply, From, ok}]}; + {value, #call{from = From, req = {_, Topics}}, NewState} -> + Subscriptions1 = + lists:foldl(fun(Topic, Acc) -> + maps:remove(Topic, Acc) + end, Subscriptions, Topics), + {keep_state, NewState#state{subscriptions = Subscriptions1}, + [{reply, From, {ok, Properties, ReasonCodes}}]}; false -> {keep_state, State} end; -%%TODO: handle PINGRESP... connected(cast, ?PACKET(?PINGRESP), State = #state{pending_calls = []}) -> {keep_state, State}; connected(cast, ?PACKET(?PINGRESP), State) -> case take_call(ping, State) of - {value, #call{from = From}, State1} -> - {keep_state, State1, [{reply, From, pong}]}; + {value, #call{from = From}, NewState} -> + {keep_state, NewState, [{reply, From, pong}]}; false -> {keep_state, State} end; +connected(cast, ?DISCONNECT_PACKET(ReasonCode, Properties), + State = #state{owner = Owner}) -> + Owner ! {disconnected, ReasonCode, Properties}, + {stop, disconnected, State}; + connected(info, {timeout, _TRef, keepalive}, State = #state{force_ping = true}) -> case send(?PACKET(?PINGREQ), State) of {ok, NewState} -> @@ -687,68 +797,77 @@ connected(info, {timeout, _TRef, keepalive}, State = #state{force_ping = true}) end; connected(info, {timeout, TRef, keepalive}, - State = #state{socket = Socket, keepalive_timer = TRef}) -> - case should_ping(Socket) of + State = #state{socket = Sock, keepalive_timer = TRef}) -> + case should_ping(Sock) of true -> case send(?PACKET(?PINGREQ), State) of {ok, NewState} -> - {keep_state, ensure_keepalive_timer(NewState)}; + {keep_state, ensure_keepalive_timer(NewState), [hibernate]}; Error -> {stop, Error} end; false -> - {keep_state, ensure_keepalive_timer(State)}; + {keep_state, ensure_keepalive_timer(State), [hibernate]}; {error, Reason} -> - {stop, {error, Reason}} + {stop, Reason} end; -connected(info, {timeout, TRef, ack}, State = #state{ack_timer = TRef, - ack_timeout = Timeout, +connected(info, {timeout, TRef, ack}, State = #state{ack_timer = TRef, + ack_timeout = Timeout, pending_calls = Calls}) -> NewState = State#state{ack_timer = undefined, pending_calls = timeout_calls(Timeout, Calls)}, {keep_state, ensure_ack_timer(NewState)}; connected(info, {timeout, TRef, retry}, State = #state{retry_timer = TRef, - inflight = Inflight}) -> + inflight = Inflight}) -> case Inflight:is_empty() of true -> {keep_state, State#state{retry_timer = undefined}}; false -> retry_send(State) end; -connected(EventType, EventContent, StateData) -> - handle_event(EventType, EventContent, StateData). +connected(EventType, EventContent, Data) -> + handle_event(EventType, EventContent, connected, Data). should_ping(Sock) -> case emqx_client_sock:getstat(Sock, [send_oct]) of {ok, [{send_oct, Val}]} -> OldVal = get(send_oct), put(send_oct, Val), OldVal == undefined orelse OldVal == Val; - Err = {error, _Reason} -> - Err + Error = {error, _Reason} -> + Error end. -handle_event(info, {'EXIT', Owner, Reason}, #state{owner = Owner}) -> - {stop, Reason}; +handle_event(info, {TcpOrSsL, _Sock, Data}, _StateName, State) + when TcpOrSsL =:= tcp; TcpOrSsL =:= ssl -> + emqx_logger:debug("RECV Data: ~p", [Data]), + receive_loop(Data, run_sock(State)); -handle_event(info, {'EXIT', Receiver, Reason}, #state{receiver = Receiver}) -> - {stop, Reason}; - -handle_event(info, {inet_reply, _Sock, ok}, State) -> - {keep_state, State}; - -handle_event(info, {inet_reply, _Sock, {error, Reason}}, State) -> +handle_event(info, {Error, _Sock, Reason}, _StateName, State) + when Error =:= tcp_error; Error =:= ssl_error -> {stop, Reason, State}; -handle_event(EventType, EventContent, State) -> - ?LOG(error, "Unexpected Event: (~p, ~p)", [EventType, EventContent]), - {keep_state, State}. +handle_event(info, {Closed, _Sock}, _StateName, State) + when Closed =:= tcp_closed; Closed =:= ssl_closed -> + {stop, Closed, State}; + +handle_event(info, {'EXIT', Owner, Reason}, _, #state{owner = Owner}) -> + {stop, Reason}; + +handle_event(info, {inet_reply, _Sock, ok}, _, State) -> + {keep_state, State}; + +handle_event(info, {inet_reply, _Sock, {error, Reason}}, _, State) -> + {stop, Reason, State}; + +handle_event(EventType, EventContent, StateName, StateData) -> + emqx_logger:error("State: ~s, Unexpected Event: (~p, ~p)", + [StateName, EventType, EventContent]), + {keep_state, StateData}. %% Mandatory callback functions terminate(_Reason, _State, #state{socket = undefined}) -> ok; -terminate(_Reason, _State, #state{socket = Socket, - receiver = Receiver}) -> - emqx_client_sock:stop(Receiver), +terminate(_Reason, _State, #state{socket = Socket}) -> emqx_client_sock:close(Socket). code_change(_Vsn, State, Data, _Extra) -> @@ -758,24 +877,27 @@ code_change(_Vsn, State, Data, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- +ensure_keepalive_timer(State = ?PROPERTY('Server-Keep-Alive', Secs)) -> + ensure_keepalive_timer(timer:seconds(Secs), State); ensure_keepalive_timer(State = #state{keepalive = 0}) -> State; -ensure_keepalive_timer(State = #state{keepalive = Keepalive}) -> - TRef = erlang:start_timer(Keepalive, self(), keepalive), - State#state{keepalive_timer = TRef}. +ensure_keepalive_timer(State = #state{keepalive = I}) -> + ensure_keepalive_timer(I, State). +ensure_keepalive_timer(I, State) when is_integer(I) -> + State#state{keepalive_timer = erlang:start_timer(I, self(), keepalive)}. new_call(Id, From) -> new_call(Id, From, undefined). new_call(Id, From, Req) -> #call{id = Id, from = From, req = Req, ts = os:timestamp()}. -add_call(Call, State = #state{pending_calls = Calls}) -> - State#state{pending_calls = [Call | Calls]}. +add_call(Call, Data = #state{pending_calls = Calls}) -> + Data#state{pending_calls = [Call | Calls]}. -take_call(Id, State = #state{pending_calls = Calls}) -> +take_call(Id, Data = #state{pending_calls = Calls}) -> case lists:keytake(Id, #call.id, Calls) of {value, Call, Left} -> - {value, Call, State#state{pending_calls = Left}}; + {value, Call, Data#state{pending_calls = Left}}; false -> false end. @@ -790,15 +912,16 @@ timeout_calls(Now, Timeout, Calls) -> end end, [], Calls). -ensure_ack_timer(State = #state{ack_timer = undefined, - ack_timeout = Timeout, +ensure_ack_timer(State = #state{ack_timer = undefined, + ack_timeout = Timeout, pending_calls = Calls}) when length(Calls) > 0 -> State#state{ack_timer = erlang:start_timer(Timeout, self(), ack)}; ensure_ack_timer(State) -> State. ensure_retry_timer(State = #state{retry_interval = Interval}) -> ensure_retry_timer(Interval, State). -ensure_retry_timer(Interval, State = #state{retry_timer = undefined}) when Interval > 0 -> +ensure_retry_timer(Interval, State = #state{retry_timer = undefined}) + when Interval > 0 -> State#state{retry_timer = erlang:start_timer(Interval, self(), retry)}; ensure_retry_timer(_Interval, State) -> State. @@ -806,7 +929,7 @@ ensure_retry_timer(_Interval, State) -> retry_send(State = #state{inflight = Inflight}) -> SortFun = fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end, Msgs = lists:sort(SortFun, Inflight:values()), - retry_send(Msgs, os:timestamp(), State). + retry_send(Msgs, os:timestamp(), State ). retry_send([], _Now, State) -> {keep_state, ensure_retry_timer(State)}; @@ -839,36 +962,61 @@ retry_send(pubrel, PacketId, Now, State = #state{inflight = Inflight}) -> Error end. -deliver_msg(#mqtt_message{packet_id = PacketId, - qos = QoS, - retain = Retain, +deliver_msg(#mqtt_message{qos = QoS, dup = Dup, + retain = Retain, topic = Topic, - properties = Props, + packet_id = PacketId, + properties = Properties, payload = Payload}, State = #state{owner = Owner}) -> - Metadata = #{mid => PacketId, qos => QoS, dup => Dup, - retain => Retain, properties => Props}, - Owner ! {publish, Topic, Metadata, Payload}, State. + Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain, + packet_id => PacketId, topic => Topic, + properties => Properties, payload => Payload}}, + State. -packet_to_msg(?PUBLISH_PACKET(QoS, Topic, PacketId, Payload)) -> - #mqtt_message{qos = QoS, packet_id = PacketId, - topic = Topic, payload = Payload}. +packet_to_msg(?PUBLISH_PACKET(Header, Topic, PacketId, Properties, Payload)) -> + #mqtt_packet_header{qos = QoS, retain = R, dup = Dup} = Header, + #mqtt_message{qos = QoS, retain = R, dup = Dup, + packet_id = PacketId, topic = Topic, + properties = Properties, payload = Payload}. -msg_to_packet(#mqtt_message{packet_id = PacketId, - qos = Qos, - retain = Retain, - dup = Dup, - topic = Topic, - payload = Payload}) -> +msg_to_packet(#mqtt_message{qos = Qos, + dup = Dup, + retain = Retain, + topic = Topic, + packet_id = PacketId, + properties = Properties, + payload = Payload}) -> #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = Qos, retain = Retain, dup = Dup}, variable = #mqtt_packet_publish{topic_name = Topic, - packet_id = PacketId}, + packet_id = PacketId, + properties = Properties}, payload = Payload}. + +%%-------------------------------------------------------------------- +%% Socket Connect/Send + +sock_connect(Hosts, SockOpts, Timeout) -> + sock_connect(Hosts, SockOpts, Timeout, {error, no_hosts}). + +sock_connect([], _SockOpts, _Timeout, LastErr) -> + LastErr; +sock_connect([{Host, Port} | Hosts], SockOpts, Timeout, _LastErr) -> + case emqx_client_sock:connect(Host, Port, SockOpts, Timeout) of + {ok, Socket} -> {ok, Socket}; + Err = {error, _Reason} -> + sock_connect(Hosts, SockOpts, Timeout, Err) + end. + +hosts(#state{hosts = [], host = Host, port = Port}) -> + [{Host, Port}]; +hosts(#state{hosts = Hosts}) -> Hosts. + send_puback(Packet, State) -> case send(Packet, State) of {ok, NewState} -> {keep_state, NewState}; @@ -878,17 +1026,43 @@ send_puback(Packet, State) -> send(Msg, State) when is_record(Msg, mqtt_message) -> send(msg_to_packet(Msg), State); -send(Packet, StateData = #state{socket = Socket}) +send(Packet, State = #state{socket = Sock, proto_ver = Ver}) when is_record(Packet, mqtt_packet) -> - Data = emqx_serializer:serialize(Packet), - case emqx_client_sock:send(Socket, Data) of - ok -> {ok, next_msg_id(StateData)}; - {error, Reason} -> {error, Reason} + Data = emqx_serializer:serialize(Packet, [{version, Ver}]), + emqx_logger:debug("SEND Data: ~p", [Data]), + case emqx_client_sock:send(Sock, Data) of + ok -> {ok, next_packet_id(State)}; + Error -> Error end. -next_msg_id(State = #state{last_packet_id = 16#ffff}) -> +run_sock(State = #state{socket = Sock}) -> + emqx_client_sock:setopts(Sock, [{active, once}]), State. + +%%-------------------------------------------------------------------- +%% Receive Loop + +receive_loop(<<>>, State) -> + {keep_state, State}; + +receive_loop(Bytes, State = #state{parse_state = ParseState}) -> + case catch emqx_parser:parse(Bytes, ParseState) of + {ok, Packet, Rest} -> + ok = gen_statem:cast(self(), Packet), + receive_loop(Rest, init_parse_state(State)); + {more, NewParseState} -> + {keep_state, State#state{parse_state = NewParseState}}; + {error, Reason} -> + {stop, Reason}; + {'EXIT', Error} -> + {stop, Error} + end. + +%%-------------------------------------------------------------------- +%% Next packet id + +next_packet_id(State = #state{last_packet_id = 16#ffff}) -> State#state{last_packet_id = 1}; -next_msg_id(State = #state{last_packet_id = Id}) -> +next_packet_id(State = #state{last_packet_id = Id}) -> State#state{last_packet_id = Id + 1}. diff --git a/src/emqx_client_sock.erl b/src/emqx_client_sock.erl index febacd1d3..a7bc5aa15 100644 --- a/src/emqx_client_sock.erl +++ b/src/emqx_client_sock.erl @@ -16,15 +16,10 @@ -module(emqx_client_sock). --include("emqx_client.hrl"). - --export([connect/4, connect/5, send/2, close/1, stop/1]). +-export([connect/4, send/2, close/1]). -export([sockname/1, setopts/2, getstat/2]). -%% Internal export --export([receiver/2, receiver_loop/3]). - -record(ssl_socket, {tcp, ssl}). -type(socket() :: inet:socket() | #ssl_socket{}). @@ -32,37 +27,23 @@ -type(sockname() :: {inet:ip_address(), inet:port_number()}). -type(option() :: gen_tcp:connect_option() - | {ssl_options, [ssl:ssl_option()]}). + | {ssl_opts, [ssl:ssl_option()]}). -export_type([socket/0, option/0]). -%%-------------------------------------------------------------------- -%% Socket API -%%-------------------------------------------------------------------- +-define(DEFAULT_TCP_OPTIONS, [binary, {packet, raw}, {active, false}, + {nodelay, true}, {reuseaddr, true}]). --spec(connect(pid(), inet:ip_address() | inet:hostname(), - inet:port_number(), [option()]) +-spec(connect(inet:ip_address() | inet:hostname(), + inet:port_number(), [option()], timeout()) -> {ok, socket()} | {error, term()}). -connect(ClientPid, Host, Port, SockOpts) -> - connect(ClientPid, Host, Port, SockOpts, ?DEFAULT_CONNECT_TIMEOUT). - -connect(ClientPid, Host, Port, SockOpts, Timeout) -> - case do_connect(Host, Port, SockOpts, Timeout) of - {ok, Sock} -> - Receiver = spawn_link(?MODULE, receiver, [ClientPid, Sock]), - ok = controlling_process(Sock, Receiver), - {ok, Sock, Receiver}; - Error -> - Error - end. - -do_connect(Host, Port, SockOpts, Timeout) -> +connect(Host, Port, SockOpts, Timeout) -> TcpOpts = emqx_misc:merge_opts(?DEFAULT_TCP_OPTIONS, - lists:keydelete(ssl_options, 1, SockOpts)), + lists:keydelete(ssl_opts, 1, SockOpts)), case gen_tcp:connect(Host, Port, TcpOpts, Timeout) of {ok, Sock} -> - case lists:keyfind(ssl_options, 1, SockOpts) of - {ssl_options, SslOpts} -> + case lists:keyfind(ssl_opts, 1, SockOpts) of + {ssl_opts, SslOpts} -> ssl_upgrade(Sock, SslOpts, Timeout); false -> {ok, Sock} end; @@ -73,23 +54,17 @@ do_connect(Host, Port, SockOpts, Timeout) -> ssl_upgrade(Sock, SslOpts, Timeout) -> case ssl:connect(Sock, SslOpts, Timeout) of {ok, SslSock} -> + ok = ssl:controlling_process(SslSock, self()), {ok, #ssl_socket{tcp = Sock, ssl = SslSock}}; {error, Reason} -> {error, Reason} end. --spec(controlling_process(socket(), pid()) -> ok). -controlling_process(Sock, Pid) when is_port(Sock) -> - gen_tcp:controlling_process(Sock, Pid); -controlling_process(#ssl_socket{ssl = SslSock}, Pid) -> - ssl:controlling_process(SslSock, Pid). - -spec(send(socket(), iodata()) -> ok | {error, einval | closed}). send(Sock, Data) when is_port(Sock) -> try erlang:port_command(Sock, Data) of true -> ok catch - error:badarg -> - {error, einval} + error:badarg -> {error, einval} end; send(#ssl_socket{ssl = SslSock}, Data) -> ssl:send(SslSock, Data). @@ -100,10 +75,6 @@ close(Sock) when is_port(Sock) -> close(#ssl_socket{ssl = SslSock}) -> ssl:close(SslSock). --spec(stop(Receiver :: pid()) -> stop). -stop(Receiver) -> - Receiver ! stop. - -spec(setopts(socket(), [gen_tcp:option() | ssl:socketoption()]) -> ok). setopts(Sock, Opts) when is_port(Sock) -> inet:setopts(Sock, Opts); @@ -123,50 +94,3 @@ sockname(Sock) when is_port(Sock) -> sockname(#ssl_socket{ssl = SslSock}) -> ssl:sockname(SslSock). -%%-------------------------------------------------------------------- -%% Receiver -%%-------------------------------------------------------------------- - -receiver(ClientPid, Sock) -> - receiver_activate(ClientPid, Sock, emqx_parser:initial_state()). - -receiver_activate(ClientPid, Sock, ParseState) -> - setopts(Sock, [{active, once}]), - erlang:hibernate(?MODULE, receiver_loop, [ClientPid, Sock, ParseState]). - -receiver_loop(ClientPid, Sock, ParseState) -> - receive - {TcpOrSsL, _Sock, Data} when TcpOrSsL =:= tcp; - TcpOrSsL =:= ssl -> - case parse_received_bytes(ClientPid, Data, ParseState) of - {ok, NewParseState} -> - receiver_activate(ClientPid, Sock, NewParseState); - {error, Error} -> - exit({frame_error, Error}) - end; - {Error, _Sock, Reason} when Error =:= tcp_error; - Error =:= ssl_error -> - exit({Error, Reason}); - {Closed, _Sock} when Closed =:= tcp_closed; - Closed =:= ssl_closed -> - exit(Closed); - stop -> - close(Sock) - end. - -parse_received_bytes(_ClientPid, <<>>, ParseState) -> - {ok, ParseState}; - -parse_received_bytes(ClientPid, Data, ParseState) -> - io:format("RECV Data: ~p~n", [Data]), - case emqx_parser:parse(Data, ParseState) of - {more, ParseState1} -> - {ok, ParseState1}; - {ok, Packet, Rest} -> - io:format("RECV Packet: ~p~n", [Packet]), - gen_statem:cast(ClientPid, Packet), - parse_received_bytes(ClientPid, Rest, emqx_parser:initial_state()); - {error, Error} -> - {error, Error} - end. - diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index c034e4701..930424d38 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%%=================================================================== -module(emqx_connection). @@ -49,7 +49,7 @@ %% Unused fields: connname, peerhost, peerport -record(state, {connection, peername, conn_state, await_recv, - rate_limit, packet_size, parser, proto_state, + rate_limit, max_packet_size, proto_state, parse_state, keepalive, enable_stats, idle_timeout, force_gc_count}). -define(INFO_KEYS, [peername, conn_state, await_recv]). @@ -109,24 +109,22 @@ do_init(Conn, Env, Peername) -> SendFun = send_fun(Conn, Peername), RateLimit = get_value(rate_limit, Conn:opts()), PacketSize = get_value(max_packet_size, Env, ?MAX_PACKET_SIZE), - Parser = emqx_parser:initial_state(PacketSize), ProtoState = emqx_protocol:init(Conn, Peername, SendFun, Env), EnableStats = get_value(client_enable_stats, Env, false), IdleTimout = get_value(client_idle_timeout, Env, 30000), ForceGcCount = emqx_gc:conn_max_gc_count(), - State = run_socket(#state{connection = Conn, - peername = Peername, - await_recv = false, - conn_state = running, - rate_limit = RateLimit, - packet_size = PacketSize, - parser = Parser, - proto_state = ProtoState, - enable_stats = EnableStats, - idle_timeout = IdleTimout, - force_gc_count = ForceGcCount}), + State = run_socket(#state{connection = Conn, + peername = Peername, + await_recv = false, + conn_state = running, + rate_limit = RateLimit, + max_packet_size = PacketSize, + proto_state = ProtoState, + enable_stats = EnableStats, + idle_timeout = IdleTimout, + force_gc_count = ForceGcCount}), gen_server:enter_loop(?MODULE, [{hibernate_after, 10000}], - State, self(), IdleTimout). + init_parse_state(State), self(), IdleTimout). send_fun(Conn, Peername) -> Self = self(), @@ -143,6 +141,11 @@ send_fun(Conn, Peername) -> end end. +init_parse_state(State = #state{max_packet_size = Size, proto_state = ProtoState}) -> + emqx_parser:initial_state([{max_len, Size}, + {ver, emqx_protocol:get(proto_ver, ProtoState)}]), + State. + handle_pre_hibernate(State) -> {hibernate, emqx_gc:reset_conn_gc_count(#state.force_gc_count, emit_stats(State))}. @@ -308,19 +311,17 @@ code_change(_OldVsn, State, _Extra) -> received(<<>>, State) -> {noreply, gc(State)}; -received(Bytes, State = #state{parser = Parser, - packet_size = PacketSize, +received(Bytes, State = #state{parse_state = ParseState, proto_state = ProtoState, idle_timeout = IdleTimeout}) -> - case catch emqx_parser:parse(Bytes, Parser) of - {more, NewParser} -> - {noreply, run_socket(State#state{parser = NewParser}), IdleTimeout}; + case catch emqx_parser:parse(Bytes, ParseState) of + {more, NewParseState} -> + {noreply, State#state{parse_state = NewParseState}, IdleTimeout}; {ok, Packet, Rest} -> emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> - received(Rest, State#state{parser = emqx_parser:initial_state(PacketSize), - proto_state = ProtoState1}); + received(Rest, init_parse_state(State#state{proto_state = ProtoState1})); {error, Error} -> ?LOG(error, "Protocol error - ~p", [Error], State), shutdown(Error, State); diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index c6d6dd554..d7669b274 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -33,10 +33,10 @@ load(Topics) -> emqx:hook('client.connected', fun ?MODULE:on_client_connected/3, [Topics]). -on_client_connected(?CONNACK_ACCEPT, Client = #client{client_id = ClientId, - client_pid = ClientPid, - username = Username}, Topics) -> - +on_client_connected(RC, Client = #client{client_id = ClientId, + client_pid = ClientPid, + username = Username}, Topics) + when RC < 16#80 -> Replace = fun(Topic) -> rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) end, TopicTable = [{Replace(Topic), Qos} || {Topic, Qos} <- Topics], ClientPid ! {subscribe, TopicTable}, diff --git a/src/emqx_mqtt_properties.erl b/src/emqx_mqtt_properties.erl index 5df1569c6..8ef559a37 100644 --- a/src/emqx_mqtt_properties.erl +++ b/src/emqx_mqtt_properties.erl @@ -16,71 +16,67 @@ -module(emqx_mqtt_properties). --export([name/1, id/1, validate/1]). +-include("emqx_mqtt.hrl"). -%%-------------------------------------------------------------------- -%% Property id to name -%%-------------------------------------------------------------------- +-export([id/1, name/1, filter/2, validate/1]). + +-define(PROPS_TABLE, + #{16#01 => {'Payload-Format-Indicator', 'Byte', [?PUBLISH]}, + 16#02 => {'Message-Expiry-Interval', 'Four-Byte-Integer', [?PUBLISH]}, + 16#03 => {'Content-Type', 'UTF8-Encoded-String', [?PUBLISH]}, + 16#08 => {'Response-Topic', 'UTF8-Encoded-String', [?PUBLISH]}, + 16#09 => {'Correlation-Data', 'Binary-Data', [?PUBLISH]}, + 16#0B => {'Subscription-Identifier', 'Variable-Byte-Integer', [?PUBLISH, ?SUBSCRIBE]}, + 16#11 => {'Session-Expiry-Interval', 'Four-Byte-Integer', [?CONNECT, ?CONNACK, ?DISCONNECT]}, + 16#12 => {'Assigned-Client-Identifier', 'UTF8-Encoded-String', [?CONNACK]}, + 16#13 => {'Server-Keep-Alive', 'Two-Byte-Integer', [?CONNACK]}, + 16#15 => {'Authentication-Method', 'UTF8-Encoded-String', [?CONNECT, ?CONNACK, ?AUTH]}, + 16#16 => {'Authentication-Data', 'Binary-Data', [?CONNECT, ?CONNACK, ?AUTH]}, + 16#17 => {'Request-Problem-Information', 'Byte', [?CONNECT]}, + 16#18 => {'Will-Delay-Interval', 'Four-Byte-Integer', ['WILL']}, + 16#19 => {'Request-Response-Information', 'Byte', [?CONNECT]}, + 16#1A => {'Response-Information', 'UTF8-Encoded-String', [?CONNACK]}, + 16#1C => {'Server-Reference', 'UTF8-Encoded-String', [?CONNACK, ?DISCONNECT]}, + 16#1F => {'Reason-String', 'UTF8-Encoded-String', 'ALL'}, + 16#21 => {'Receive-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]}, + 16#22 => {'Topic-Alias-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]}, + 16#23 => {'Topic-Alias', 'Two-Byte-Integer', [?PUBLISH]}, + 16#24 => {'Maximum-QoS', 'Byte', [?CONNACK]}, + 16#25 => {'Retain-Available', 'Byte', [?CONNACK]}, + 16#26 => {'User-Property', 'UTF8-String-Pair', 'ALL'}, + 16#27 => {'Maximum-Packet-Size', 'Four-Byte-Integer', [?CONNECT, ?CONNACK]}, + 16#28 => {'Wildcard-Subscription-Available', 'Byte', [?CONNACK]}, + 16#29 => {'Subscription-Identifier-Available', 'Byte', [?CONNACK]}, + 16#2A => {'Shared-Subscription-Available', 'Byte', [?CONNACK]}}). -%% 01: Byte; PUBLISH, Will Properties name(16#01) -> 'Payload-Format-Indicator'; -%% 02: Four Byte Integer; PUBLISH, Will Properties name(16#02) -> 'Message-Expiry-Interval'; -%% 03: UTF-8 Encoded String; PUBLISH, Will Properties name(16#03) -> 'Content-Type'; -%% 08: UTF-8 Encoded String; PUBLISH, Will Properties name(16#08) -> 'Response-Topic'; -%% 09: Binary Data; PUBLISH, Will Properties name(16#09) -> 'Correlation-Data'; -%% 11: Variable Byte Integer; PUBLISH, SUBSCRIBE name(16#0B) -> 'Subscription-Identifier'; -%% 17: Four Byte Integer; CONNECT, CONNACK, DISCONNECT name(16#11) -> 'Session-Expiry-Interval'; -%% 18: UTF-8 Encoded String; CONNACK name(16#12) -> 'Assigned-Client-Identifier'; -%% 19: Two Byte Integer; CONNACK name(16#13) -> 'Server-Keep-Alive'; -%% 21: UTF-8 Encoded String; CONNECT, CONNACK, AUTH name(16#15) -> 'Authentication-Method'; -%% 22: Binary Data; CONNECT, CONNACK, AUTH name(16#16) -> 'Authentication-Data'; -%% 23: Byte; CONNECT name(16#17) -> 'Request-Problem-Information'; -%% 24: Four Byte Integer; Will Properties name(16#18) -> 'Will-Delay-Interval'; -%% 25: Byte; CONNECT name(16#19) -> 'Request-Response-Information'; -%% 26: UTF-8 Encoded String; CONNACK -name(16#1A) -> 'Response Information'; -%% 28: UTF-8 Encoded String; CONNACK, DISCONNECT +name(16#1A) -> 'Response-Information'; name(16#1C) -> 'Server-Reference'; -%% 31: UTF-8 Encoded String; CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBACK, UNSUBACK, DISCONNECT, AUTH name(16#1F) -> 'Reason-String'; -%% 33: Two Byte Integer; CONNECT, CONNACK name(16#21) -> 'Receive-Maximum'; -%% 34: Two Byte Integer; CONNECT, CONNACK name(16#22) -> 'Topic-Alias-Maximum'; -%% 35: Two Byte Integer; PUBLISH -name(16#23) -> 'Topic Alias'; -%% 36: Byte; CONNACK +name(16#23) -> 'Topic-Alias'; name(16#24) -> 'Maximum-QoS'; -%% 37: Byte; CONNACK name(16#25) -> 'Retain-Available'; -%% 38: UTF-8 String Pair; ALL name(16#26) -> 'User-Property'; -%% 39: Four Byte Integer; CONNECT, CONNACK name(16#27) -> 'Maximum-Packet-Size'; -%% 40: Byte; CONNACK name(16#28) -> 'Wildcard-Subscription-Available'; -%% 41: Byte; CONNACK name(16#29) -> 'Subscription-Identifier-Available'; -%% 42: Byte; CONNACK name(16#2A) -> 'Shared-Subscription-Available'. -%%-------------------------------------------------------------------- -%% Property name to id -%%-------------------------------------------------------------------- - id('Payload-Format-Indicator') -> 16#01; id('Message-Expiry-Interval') -> 16#02; id('Content-Type') -> 16#03; @@ -91,16 +87,16 @@ id('Session-Expiry-Interval') -> 16#11; id('Assigned-Client-Identifier') -> 16#12; id('Server-Keep-Alive') -> 16#13; id('Authentication-Method') -> 16#15; -id('Authentication Data') -> 16#16; +id('Authentication-Data') -> 16#16; id('Request-Problem-Information') -> 16#17; id('Will-Delay-Interval') -> 16#18; id('Request-Response-Information') -> 16#19; -id('Response Information') -> 16#1A; +id('Response-Information') -> 16#1A; id('Server-Reference') -> 16#1C; id('Reason-String') -> 16#1F; id('Receive-Maximum') -> 16#21; id('Topic-Alias-Maximum') -> 16#22; -id('Topic Alias') -> 16#23; +id('Topic-Alias') -> 16#23; id('Maximum-QoS') -> 16#24; id('Retain-Available') -> 16#25; id('User-Property') -> 16#26; @@ -109,5 +105,42 @@ id('Wildcard-Subscription-Available') -> 16#28; id('Subscription-Identifier-Available') -> 16#29; id('Shared-Subscription-Available') -> 16#2A. -%%TODO: -validate(Props) when is_list(Props) -> ok. +filter(Packet, Props) when ?CONNECT =< Packet, Packet =< ?AUTH -> + Fun = fun(Name) -> + case maps:find(id(Name), ?PROPS_TABLE) of + {ok, {Name, _Type, 'ALL'}} -> + true; + {ok, {Name, _Type, Packets}} -> + lists:member(Packet, Packets); + error -> false + end + end, + [Prop || Prop = {Name, _} <- Props, Fun(Name)]. + +validate(Props) when is_map(Props) -> + lists:foreach(fun validate_prop/1, maps:to_list(Props)). + +validate_prop(Prop = {Name, Val}) -> + case maps:find(id(Name), ?PROPS_TABLE) of + {ok, {Name, Type, _}} -> + validate_value(Type, Val) + orelse error(bad_property, Prop); + error -> + error({bad_property, Prop}) + end. + +validate_value('Byte', Val) -> + is_integer(Val); +validate_value('Two-Byte-Integer', Val) -> + is_integer(Val); +validate_value('Four-Byte-Integer', Val) -> + is_integer(Val); +validate_value('Variable-Byte-Integer', Val) -> + is_integer(Val); +validate_value('UTF8-Encoded-String', Val) -> + is_binary(Val); +validate_value('Binary-Data', Val) -> + is_binary(Val); +validate_value('User-Property', Val) -> + is_tuple(Val) orelse is_list(Val). + diff --git a/src/emqx_mqtt_rscode.erl b/src/emqx_mqtt_rscode.erl deleted file mode 100644 index 5bc7c0210..000000000 --- a/src/emqx_mqtt_rscode.erl +++ /dev/null @@ -1,115 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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_rscode). - --export([value/1]). - -%%-------------------------------------------------------------------- -%% Reason code to name -%%-------------------------------------------------------------------- - -%% 00: Success; CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK, AUTH -value('Success') -> 16#00; -%% 00: Normal disconnection; DISCONNECT -value('Normal-Disconnection') -> 16#00; -%% 00: Granted QoS 0; SUBACK -value('Granted-QoS0') -> 16#00; -%% 01: Granted QoS 1; SUBACK -value('Granted-QoS1') -> 16#01; -%% 02: Granted QoS 2; SUBACK -value('Granted-QoS2') -> 16#02; -%% 04: Disconnect with Will Message; DISCONNECT -value('Disconnect-With-Will-Message') -> 16#04; -%% 16: No matching subscribers; PUBACK, PUBREC -value('No-Matching-Subscribers') -> 16#10; -%% 17: No subscription existed; UNSUBACK -value('No-Subscription-Existed') -> 16#11; -%% 24: Continue authentication; AUTH -value('Continue-Authentication') -> 16#18; -%% 25: Re-Authenticate; AUTH -value('Re-Authenticate') -> 16#19; -%% 128: Unspecified error; CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT -value('Unspecified-Error') -> 16#80; -%% 129: Malformed Packet; CONNACK, DISCONNECT -value('Malformed-Packet') -> 16#81; -%% 130: Protocol Error; CONNACK, DISCONNECT -value('Protocol-Error') -> 16#82; -%% 131: Implementation specific error; CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT -value('Implementation-Specific-Error') -> 16#83; -%% 132: Unsupported Protocol Version; CONNACK -value('Unsupported-Protocol-Version') -> 16#84; -%% 133: Client Identifier not valid; CONNACK -value('Client-Identifier-not-Valid') -> 16#85; -%% 134: Bad User Name or Password; CONNACK -value('Bad-Username-or-Password') -> 16#86; -%% 135: Not authorized; CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT -value('Not-Authorized') -> 16#87; -%% 136: Server unavailable; CONNACK -value('Server-Unavailable') -> 16#88; -%% 137: Server busy; CONNACK, DISCONNECT -value('Server-Busy') -> 16#89; -%% 138: Banned; CONNACK -value('Banned') -> 16#8A; -%% 139: Server shutting down; DISCONNECT -value('Server-Shutting-Down') -> 16#8B; -%% 140: Bad authentication method; CONNACK, DISCONNECT -value('Bad-Authentication-Method') -> 16#8C; -%% 141: Keep Alive timeout; DISCONNECT -value('Keep-Alive-Timeout') -> 16#8D; -%% 142: Session taken over; DISCONNECT -value('Session-Taken-Over') -> 16#8E; -%% 143: Topic Filter invalid; SUBACK, UNSUBACK, DISCONNECT -value('Topic-Filter-Invalid') -> 16#8F; -%% 144: Topic Name invalid; CONNACK, PUBACK, PUBREC, DISCONNECT -value('Topic-Name-Invalid') -> 16#90; -%% 145: Packet Identifier in use; PUBACK, PUBREC, SUBACK, UNSUBACK -value('Packet-Identifier-Inuse') -> 16#91; -%% 146: Packet Identifier not found; PUBREL, PUBCOMP -value('Packet-Identifier-Not-Found') -> 16#92; -%% 147: Receive Maximum exceeded; DISCONNECT -value('Receive-Maximum-Exceeded') -> 16#93; -%% 148: Topic Alias invalid; DISCONNECT -value('Topic-Alias-Invalid') -> 16#94; -%% 149: Packet too large; CONNACK, DISCONNECT -value('Packet-Too-Large') -> 16#95; -%% 150: Message rate too high; DISCONNECT -value('Message-Rate-Too-High') -> 16#96; -%% 151: Quota exceeded; CONNACK, PUBACK, PUBREC, SUBACK, DISCONNECT -value('Quota-Exceeded') -> 16#97; -%% 152: Administrative action; DISCONNECT -value('Administrative-Action') -> 16#98; -%% 153: Payload format invalid; CONNACK, PUBACK, PUBREC, DISCONNECT -value('Payload-Format-Invalid') -> 16#99; -%% 154: Retain not supported; CONNACK, DISCONNECT -value('Retain-Not-Supported') -> 16#9A; -%% 155: QoS not supported; CONNACK, DISCONNECT -value('QoS-Not-Supported') -> 16#9B; -%% 156: Use another server; CONNACK, DISCONNECT -value('Use-Another-Server') -> 16#9C; -%% 157: Server moved; CONNACK, DISCONNECT -value('Server-Moved') -> 16#9D; -%% 158: Shared Subscriptions not supported; SUBACK, DISCONNECT -value('Shared-Subscriptions-Not-Supported') -> 16#9E; -%% 159: Connection rate exceeded; CONNACK, DISCONNECT -value('Connection-Rate-Exceeded') -> 16#9F; -%% 160: Maximum connect time; DISCONNECT -value('Maximum-Connect-Time') -> 16#A0; -%% 161: Subscription Identifiers not supported; SUBACK, DISCONNECT -value('Subscription-Identifiers-Not-Supported') -> 16#A1; -%% 162: Wildcard-Subscriptions-Not-Supported; SUBACK, DISCONNECT -value('Wildcard-Subscriptions-Not-Supported') -> 16#A2. - diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index fc23fb883..655bdd504 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -20,14 +20,14 @@ -include("emqx_mqtt.hrl"). --export([protocol_name/1, type_name/1, connack_error/1]). +-export([protocol_name/1, type_name/1]). -export([format/1]). -export([to_message/1, from_message/1]). %% @doc Protocol name of version --spec(protocol_name(mqtt_vsn()) -> binary()). +-spec(protocol_name(mqtt_version()) -> binary()). protocol_name(?MQTT_PROTO_V3) -> <<"MQIsdp">>; protocol_name(?MQTT_PROTO_V4) -> <<"MQTT">>; protocol_name(?MQTT_PROTO_V5) -> <<"MQTT">>. @@ -37,16 +37,6 @@ protocol_name(?MQTT_PROTO_V5) -> <<"MQTT">>. type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> lists:nth(Type, ?TYPE_NAMES). -%% @doc Connack Error --spec(connack_error(mqtt_connack()) -> atom()). -connack_error(?CONNACK_ACCEPT) -> 'CONNACK_ACCEPT'; -connack_error(?CONNACK_PROTO_VER) -> 'CONNACK_PROTO_VER'; -connack_error(?CONNACK_INVALID_ID) -> 'CONNACK_INVALID_ID'; -connack_error(?CONNACK_SERVER) -> 'CONNACK_SERVER'; -connack_error(?CONNACK_CREDENTIALS) -> 'CONNACK_CREDENTIALS'; -connack_error(?CONNACK_AUTH) -> 'CONNACK_AUTH'; -connack_error(_ReasonCode) -> 'CONNACK_UNKNOWN_ERR'. - %% @doc From Message to Packet -spec(from_message(message()) -> mqtt_packet()). from_message(Msg = #message{qos = Qos, @@ -68,7 +58,7 @@ from_message(Msg = #message{qos = Qos, to_message(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, retain = Retain, qos = Qos, - dup = Dup}, + dup = Dup}, variable = #mqtt_packet_publish{topic_name = Topic, packet_id = PacketId, properties = Props}, @@ -80,11 +70,11 @@ to_message(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, properties = Props}; to_message(#mqtt_packet_connect{will_flag = false}) -> undefined; -to_message(#mqtt_packet_connect{will_retain = Retain, - will_qos = Qos, - will_topic = Topic, - will_props = Props, - will_msg = Payload}) -> +to_message(#mqtt_packet_connect{will_retain = Retain, + will_qos = Qos, + will_topic = Topic, + will_props = Props, + will_payload = Payload}) -> Msg = emqx_message:make(undefined, Topic, Payload), Msg#message{flags = #{retain => Retain}, headers = #{qos => Qos}, @@ -99,7 +89,7 @@ format_header(#mqtt_packet_header{type = Type, dup = Dup, qos = QoS, retain = Retain}, S) -> - S1 = if + S1 = if S == undefined -> <<>>; true -> [", ", S] end, @@ -113,23 +103,23 @@ format_variable(Variable, Payload) -> io_lib:format("~s, Payload=~p", [format_variable(Variable), Payload]). format_variable(#mqtt_packet_connect{ - proto_ver = ProtoVer, - proto_name = ProtoName, - will_retain = WillRetain, - will_qos = WillQoS, - will_flag = WillFlag, - clean_sess = CleanSess, - keep_alive = KeepAlive, - client_id = ClientId, - will_topic = WillTopic, - will_msg = WillMsg, - username = Username, - password = Password}) -> - Format = "ClientId=~s, ProtoName=~s, ProtoVsn=~p, CleanSess=~s, KeepAlive=~p, Username=~s, Password=~s", - Args = [ClientId, ProtoName, ProtoVer, CleanSess, KeepAlive, Username, format_password(Password)], - {Format1, Args1} = if - WillFlag -> { Format ++ ", Will(Q~p, R~p, Topic=~s, Msg=~s)", - Args ++ [WillQoS, i(WillRetain), WillTopic, WillMsg] }; + proto_ver = ProtoVer, + proto_name = ProtoName, + will_retain = WillRetain, + will_qos = WillQoS, + will_flag = WillFlag, + clean_start = CleanStart, + keepalive = KeepAlive, + client_id = ClientId, + will_topic = WillTopic, + will_payload = WillPayload, + username = Username, + password = Password}) -> + Format = "ClientId=~s, ProtoName=~s, ProtoVsn=~p, CleanStart=~s, KeepAlive=~p, Username=~s, Password=~s", + Args = [ClientId, ProtoName, ProtoVer, CleanStart, KeepAlive, Username, format_password(Password)], + {Format1, Args1} = if + WillFlag -> { Format ++ ", Will(Q~p, R~p, Topic=~s, Payload=~p)", + Args ++ [WillQoS, i(WillRetain), WillTopic, WillPayload] }; true -> {Format, Args} end, io_lib:format(Format1, Args1); @@ -149,9 +139,9 @@ format_variable(#mqtt_packet_subscribe{packet_id = PacketId, topic_filters = TopicFilters}) -> io_lib:format("PacketId=~p, TopicFilters=~p", [PacketId, TopicFilters]); -format_variable(#mqtt_packet_unsubscribe{packet_id = PacketId, - topics = Topics}) -> - io_lib:format("PacketId=~p, Topics=~p", [PacketId, Topics]); +format_variable(#mqtt_packet_unsubscribe{packet_id = PacketId, + topic_filters = Topics}) -> + io_lib:format("PacketId=~p, TopicFilters=~p", [PacketId, Topics]); format_variable(#mqtt_packet_suback{packet_id = PacketId, reason_codes = ReasonCodes}) -> diff --git a/src/emqx_parser.erl b/src/emqx_parser.erl index 5cb9aa20a..a7c6707f3 100644 --- a/src/emqx_parser.erl +++ b/src/emqx_parser.erl @@ -20,180 +20,198 @@ -include("emqx_mqtt.hrl"). -%% API -export([initial_state/0, initial_state/1, parse/2]). -type(max_packet_size() :: 1..?MAX_PACKET_SIZE). --type(state() :: #{maxlen := max_packet_size(), vsn := mqtt_vsn()}). +-type(option() :: {max_len, max_packet_size()} + | {version, mqtt_version()}). --spec(initial_state() -> {none, state()}). -initial_state() -> - initial_state(?MAX_PACKET_SIZE). +-type(state() :: {none, map()} | {more, fun()}). + +-export_type([option/0, state/0]). %% @doc Initialize a parser --spec(initial_state(max_packet_size()) -> {none, state()}). -initial_state(MaxSize) -> - {none, #{maxlen => MaxSize, vsn => ?MQTT_PROTO_V4}}. +-spec(initial_state() -> {none, map()}). +initial_state() -> initial_state([]). + +-spec(initial_state([option()]) -> {none, map()}). +initial_state(Options) when is_list(Options) -> + {none, parse_opt(Options, #{max_len => ?MAX_PACKET_SIZE, + version => ?MQTT_PROTO_V4})}. + +parse_opt([], Map) -> + Map; +parse_opt([{version, Ver}|Opts], Map) -> + parse_opt(Opts, Map#{version := Ver}); +parse_opt([{max_len, Len}|Opts], Map) -> + parse_opt(Opts, Map#{max_len := Len}); +parse_opt([_|Opts], Map) -> + parse_opt(Opts, Map). %% @doc Parse MQTT Packet --spec(parse(binary(), {none, state()} | fun()) - -> {ok, mqtt_packet()} | {error, term()} | {more, fun()}). -parse(<<>>, {none, State}) -> - {more, fun(Bin) -> parse(Bin, {none, State}) end}; -parse(<>, {none, State}) -> +-spec(parse(binary(), {none, map()} | fun()) + -> {ok, mqtt_packet()} | {error, term()} | {more, fun()}). +parse(<<>>, {none, Options}) -> + {more, fun(Bin) -> parse(Bin, {none, Options}) end}; +parse(<>, {none, Options}) -> parse_remaining_len(Rest, #mqtt_packet_header{type = Type, dup = bool(Dup), qos = fixqos(Type, QoS), - retain = bool(Retain)}, State); + retain = bool(Retain)}, Options); parse(Bin, Cont) -> Cont(Bin). -parse_remaining_len(<<>>, Header, State) -> - {more, fun(Bin) -> parse_remaining_len(Bin, Header, State) end}; -parse_remaining_len(Rest, Header, State) -> - parse_remaining_len(Rest, Header, 1, 0, State). +parse_remaining_len(<<>>, Header, Options) -> + {more, fun(Bin) -> parse_remaining_len(Bin, Header, Options) end}; +parse_remaining_len(Rest, Header, Options) -> + parse_remaining_len(Rest, Header, 1, 0, Options). -parse_remaining_len(_Bin, _Header, _Multiplier, Length, #{maxlen := MaxLen}) +parse_remaining_len(_Bin, _Header, _Multiplier, Length, #{max_len := MaxLen}) when Length > MaxLen -> - {error, invalid_mqtt_frame_len}; -parse_remaining_len(<<>>, Header, Multiplier, Length, State) -> - {more, fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length, State) end}; + {error, mqtt_frame_too_long}; +parse_remaining_len(<<>>, Header, Multiplier, Length, Options) -> + {more, fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length, Options) end}; %% Optimize: match PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK... -parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0, State) -> - parse_frame(Rest, Header, 2, State); +parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0, Options) -> + parse_frame(Rest, Header, 2, Options); %% optimize: match PINGREQ... -parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, State) -> - parse_frame(Rest, Header, 0, State); -parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value, State) -> - parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier, State); -parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value, State = #{maxlen := MaxLen}) -> +parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, Options) -> + parse_frame(Rest, Header, 0, Options); +parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value, Options) -> + parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier, Options); +parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value, + Options = #{max_len := MaxLen}) -> FrameLen = Value + Len * Multiplier, if - FrameLen > MaxLen -> {error, invalid_mqtt_frame_len}; - true -> parse_frame(Rest, Header, FrameLen, State) + FrameLen > MaxLen -> error(mqtt_frame_too_long); + true -> parse_frame(Rest, Header, FrameLen, Options) end. -parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length, State = #{vsn := Vsn}) -> - case {Type, Bin} of - {?CONNECT, <>} -> - {ProtoName, Rest1} = parse_utf(FrameBin), - %% Fix mosquitto bridge: 0x83, 0x84 - <> = Rest1, - <> = Rest2, - {Properties, Rest4} = parse_properties(ProtoVer, Rest3), - {ClientId, Rest5} = parse_utf(Rest4), - {WillProps, Rest6} = parse_will_props(Rest5, ProtoVer, WillFlag), - {WillTopic, Rest7} = parse_utf(Rest6, WillFlag), - {WillMsg, Rest8} = parse_msg(Rest7, WillFlag), - {UserName, Rest9} = parse_utf(Rest8, UsernameFlag), - {PasssWord, <<>>} = parse_utf(Rest9, PasswordFlag), - case protocol_name_approved(ProtoVer, ProtoName) of - true -> - wrap(Header, - #mqtt_packet_connect{ - proto_ver = ProtoVer, - proto_name = ProtoName, - will_retain = bool(WillRetain), - will_qos = WillQos, - will_flag = bool(WillFlag), - clean_sess = bool(CleanSess), - keep_alive = KeepAlive, - client_id = ClientId, - will_props = WillProps, - will_topic = WillTopic, - will_msg = WillMsg, - username = UserName, - password = PasssWord, - is_bridge = (BridgeTag =:= 8), - properties = Properties}, Rest); - false -> - {error, protocol_header_corrupt} +parse_frame(Bin, Header, 0, _Options) -> + wrap(Header, Bin); + +parse_frame(Bin, Header, Length, Options) -> + case Bin of + <> -> + case parse_packet(Header, FrameBin, Options) of + {Variable, Payload} -> + wrap(Header, Variable, Payload, Rest); + Variable -> + wrap(Header, Variable, Rest) end; - {?CONNACK, <>} -> - <<_Reserved:7, SP:1, ReasonCode:8>> = FrameBin, - wrap(Header, #mqtt_packet_connack{ack_flags = SP, - reason_code = ReasonCode}, Rest); - {?PUBLISH, <>} -> - {TopicName, Rest1} = parse_utf(FrameBin), - {PacketId, Rest2} = case Qos of - 0 -> {undefined, Rest1}; - _ -> <> = Rest1, - {Id, R} - end, - {Properties, Payload} = parse_properties(Vsn, Rest2), - wrap(fixdup(Header), #mqtt_packet_publish{topic_name = TopicName, - packet_id = PacketId, - properties = Properties}, - Payload, Rest); - {PubAck, <>} - when PubAck == ?PUBACK; PubAck == ?PUBREC; PubAck == ?PUBREL; PubAck == ?PUBCOMP -> - <> = FrameBin, - case Vsn == ?MQTT_PROTO_V5 of - true -> - <> = Rest1, - {Properties, Rest3} = parse_properties(Vsn, Rest2), - wrap(Header, #mqtt_packet_puback{packet_id = PacketId, - reason_code = ReasonCode, - properties = Properties}, Rest3); - false -> - wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest) - end; - {?SUBSCRIBE, <>} -> - %% 1 = Qos, - <> = FrameBin, - {Properties, Rest2} = parse_properties(Vsn, Rest1), - TopicFilters = parse_topics(?SUBSCRIBE, Rest2, []), - wrap(Header, #mqtt_packet_subscribe{packet_id = PacketId, - properties = Properties, - topic_filters = TopicFilters}, Rest); - {?SUBACK, <>} -> - <> = FrameBin, - {Properties, Rest2} = parse_properties(Vsn, Rest1), - wrap(Header, #mqtt_packet_suback{packet_id = PacketId, properties = Properties, - reason_codes = parse_qos(Rest2, [])}, Rest); - {?UNSUBSCRIBE, <>} -> - %% 1 = Qos, - <> = FrameBin, - {Properties, Rest2} = parse_properties(Vsn, Rest1), - Topics = parse_topics(?UNSUBSCRIBE, Rest2, []), - wrap(Header, #mqtt_packet_unsubscribe{packet_id = PacketId, - properties = Properties, - topics = Topics}, Rest); - {?UNSUBACK, <>} -> - <> = FrameBin, - {Properties, _Rest2} = parse_properties(Vsn, Rest1), - wrap(Header, #mqtt_packet_unsuback{packet_id = PacketId, - properties = Properties}, Rest); - {?PINGREQ, Rest} -> - Length = 0, - wrap(Header, Rest); - {?PINGRESP, Rest} -> - Length = 0, - wrap(Header, Rest); - {?DISCONNECT, <>} -> - if - Vsn == ?MQTT_PROTO_V5 -> - <> = FrameBin, - {Properties, Rest2} = parse_properties(Vsn, Rest1), - wrap(Header, #mqtt_packet_disconnect{reason_code = ReasonCode, - properties = Properties}, Rest2); - true -> - Length = 0, wrap(Header, Rest) - end; - {_, TooShortBin} -> + TooShortBin -> {more, fun(BinMore) -> - parse_frame(<>, Header, Length, State) + parse_frame(<>, Header, Length, Options) end} end. +parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) -> + {ProtoName, Rest} = parse_utf8_string(FrameBin), + <> = Rest, + <> = Rest1, + case protocol_name_approved(ProtoVer, ProtoName) of + true -> ok; + false -> error(protocol_name_unapproved) + end, + {Properties, Rest3} = parse_properties(Rest2, ProtoVer), + {ClientId, Rest4} = parse_utf8_string(Rest3), + ConnPacket = #mqtt_packet_connect{proto_name = ProtoName, + proto_ver = ProtoVer, + is_bridge = (BridgeTag =:= 8), + clean_start = bool(CleanStart), + will_flag = bool(WillFlag), + will_qos = WillQos, + will_retain = bool(WillRetain), + keepalive = KeepAlive, + properties = Properties, + client_id = ClientId}, + {ConnPacket1, Rest5} = parse_will_message(ConnPacket, Rest4), + {Username, Rest6} = parse_utf8_string(Rest5, bool(UsernameFlag)), + {Passsword, <<>>} = parse_utf8_string(Rest6, bool(PasswordFlag)), + ConnPacket1#mqtt_packet_connect{username = Username, password = Passsword}; + +parse_packet(#mqtt_packet_header{type = ?CONNACK}, + <>, #{version := Ver}) -> + {Properties, <<>>} = parse_properties(Rest, Ver), + #mqtt_packet_connack{ack_flags = AckFlags, + reason_code = ReasonCode, + properties = Properties}; + +parse_packet(#mqtt_packet_header{type = ?PUBLISH, qos = QoS}, Bin, + #{version := Ver}) -> + {TopicName, Rest} = parse_utf8_string(Bin), + {PacketId, Rest1} = case QoS of + ?QOS_0 -> {undefined, Rest}; + _ -> parse_packet_id(Rest) + end, + {Properties, Payload} = parse_properties(Rest1, Ver), + {#mqtt_packet_publish{topic_name = TopicName, + packet_id = PacketId, + properties = Properties}, Payload}; + +parse_packet(#mqtt_packet_header{type = PubAck}, <>, _Options) + when ?PUBACK =< PubAck, PubAck =< ?PUBCOMP -> + #mqtt_packet_puback{packet_id = PacketId, reason_code = 0}; +parse_packet(#mqtt_packet_header{type = PubAck}, <>, + #{version := Ver = ?MQTT_PROTO_V5}) + when ?PUBACK =< PubAck, PubAck =< ?PUBCOMP -> + {Properties, <<>>} = parse_properties(Rest, Ver), + #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties}; + +parse_packet(#mqtt_packet_header{type = ?SUBSCRIBE}, <>, + #{version := Ver}) -> + {Properties, Rest1} = parse_properties(Rest, Ver), + TopicFilters = parse_topic_filters(subscribe, Rest1), + #mqtt_packet_subscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}; + +parse_packet(#mqtt_packet_header{type = ?SUBACK}, <>, + #{version := Ver}) -> + {Properties, Rest1} = parse_properties(Rest, Ver), + #mqtt_packet_suback{packet_id = PacketId, + properties = Properties, + reason_codes = parse_reason_codes(Rest1)}; + +parse_packet(#mqtt_packet_header{type = ?UNSUBSCRIBE}, <>, + #{version := Ver}) -> + {Properties, Rest1} = parse_properties(Rest, Ver), + TopicFilters = parse_topic_filters(unsubscribe, Rest1), + #mqtt_packet_unsubscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}; + +parse_packet(#mqtt_packet_header{type = ?UNSUBACK}, <>, _Options) -> + #mqtt_packet_unsuback{packet_id = PacketId}; +parse_packet(#mqtt_packet_header{type = ?UNSUBACK}, <>, + #{version := Ver}) -> + {Properties, Rest1} = parse_properties(Rest, Ver), + ReasonCodes = parse_reason_codes(Rest1), + #mqtt_packet_unsuback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes}; + +parse_packet(#mqtt_packet_header{type = ?DISCONNECT}, <>, + #{version := ?MQTT_PROTO_V5}) -> + {Properties, <<>>} = parse_properties(Rest, ?MQTT_PROTO_V5), + #mqtt_packet_disconnect{reason_code = ReasonCode, + properties = Properties}; + +parse_packet(#mqtt_packet_header{type = ?AUTH}, <>, + #{version := ?MQTT_PROTO_V5}) -> + {Properties, <<>>} = parse_properties(Rest, ?MQTT_PROTO_V5), + #mqtt_packet_auth{reason_code = ReasonCode, properties = Properties}. + wrap(Header, Variable, Payload, Rest) -> {ok, #mqtt_packet{header = Header, variable = Variable, payload = Payload}, Rest}. wrap(Header, Variable, Rest) -> @@ -201,111 +219,100 @@ wrap(Header, Variable, Rest) -> wrap(Header, Rest) -> {ok, #mqtt_packet{header = Header}, Rest}. -parse_will_props(Bin, ProtoVer = ?MQTT_PROTO_V5, 1) -> - parse_properties(ProtoVer, Bin); -parse_will_props(Bin, _ProtoVer, _WillFlag) -> - {#{}, Bin}. +protocol_name_approved(Ver, Name) -> + lists:member({Ver, Name}, ?PROTOCOL_NAMES). -parse_properties(?MQTT_PROTO_V5, Bin) -> +parse_will_message(Packet = #mqtt_packet_connect{will_flag = true, + proto_ver = Ver}, Bin) -> + {Props, Rest} = parse_properties(Bin, Ver), + {Topic, Rest1} = parse_utf8_string(Rest), + {Payload, Rest2} = parse_binary_data(Rest1), + {Packet#mqtt_packet_connect{will_props = Props, + will_topic = Topic, + will_payload = Payload}, Rest2}; +parse_will_message(Packet, Bin) -> + {Packet, Bin}. + +parse_packet_id(<>) -> + {PacketId, Rest}. + +parse_properties(Bin, Ver) when Ver =/= ?MQTT_PROTO_V5 -> + {undefined, Bin}; +parse_properties(<<0, Rest/binary>>, ?MQTT_PROTO_V5) -> + {#{}, Rest}; +parse_properties(Bin, ?MQTT_PROTO_V5) -> {Len, Rest} = parse_variable_byte_integer(Bin), <> = Rest, - {parse_property(PropsBin, #{}), Rest1}; -parse_properties(_MQTT_PROTO_V3, Bin) -> - {#{}, Bin}. %% No properties. + {parse_property(PropsBin, #{}), Rest1}. parse_property(<<>>, Props) -> Props; -%% 01: 'Payload-Format-Indicator', Byte; parse_property(<<16#01, Val, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Payload-Format-Indicator' => Val}); -%% 02: 'Message-Expiry-Interval', Four Byte Integer; parse_property(<<16#02, Val:32/big, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Message-Expiry-Interval' => Val}); -%% 03: 'Content-Type', UTF-8 Encoded String; parse_property(<<16#03, Bin/binary>>, Props) -> - {Val, Rest} = parse_utf(Bin), + {Val, Rest} = parse_utf8_string(Bin), parse_property(Rest, Props#{'Content-Type' => Val}); -%% 08: 'Response-Topic', UTF-8 Encoded String; parse_property(<<16#08, Bin/binary>>, Props) -> - {Val, Rest} = parse_utf(Bin), + {Val, Rest} = parse_utf8_string(Bin), parse_property(Rest, Props#{'Response-Topic' => Val}); -%% 09: 'Correlation-Data', Binary Data; parse_property(<<16#09, Len:16/big, Val:Len/binary, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Correlation-Data' => Val}); -%% 11: 'Subscription-Identifier', Variable Byte Integer; parse_property(<<16#0B, Bin/binary>>, Props) -> {Val, Rest} = parse_variable_byte_integer(Bin), parse_property(Rest, Props#{'Subscription-Identifier' => Val}); -%% 17: 'Session-Expiry-Interval', Four Byte Integer; parse_property(<<16#11, Val:32/big, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Session-Expiry-Interval' => Val}); -%% 18: 'Assigned-Client-Identifier', UTF-8 Encoded String; parse_property(<<16#12, Bin/binary>>, Props) -> - {Val, Rest} = parse_utf(Bin), + {Val, Rest} = parse_utf8_string(Bin), parse_property(Rest, Props#{'Assigned-Client-Identifier' => Val}); -%% 19: 'Server-Keep-Alive', Two Byte Integer; parse_property(<<16#13, Val:16, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Server-Keep-Alive' => Val}); -%% 21: 'Authentication-Method', UTF-8 Encoded String; parse_property(<<16#15, Bin/binary>>, Props) -> - {Val, Rest} = parse_utf(Bin), + {Val, Rest} = parse_utf8_string(Bin), parse_property(Rest, Props#{'Authentication-Method' => Val}); -%% 22: 'Authentication-Data', Binary Data; parse_property(<<16#16, Len:16/big, Val:Len/binary, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Authentication-Data' => Val}); -%% 23: 'Request-Problem-Information', Byte; parse_property(<<16#17, Val, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Request-Problem-Information' => Val}); -%% 24: 'Will-Delay-Interval', Four Byte Integer; parse_property(<<16#18, Val:32, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Will-Delay-Interval' => Val}); -%% 25: 'Request-Response-Information', Byte; parse_property(<<16#19, Val, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Request-Response-Information' => Val}); -%% 26: 'Response Information', UTF-8 Encoded String; parse_property(<<16#1A, Bin/binary>>, Props) -> - {Val, Rest} = parse_utf(Bin), + {Val, Rest} = parse_utf8_string(Bin), parse_property(Rest, Props#{'Response-Information' => Val}); -%% 28: 'Server-Reference', UTF-8 Encoded String; parse_property(<<16#1C, Bin/binary>>, Props) -> - {Val, Rest} = parse_utf(Bin), + {Val, Rest} = parse_utf8_string(Bin), parse_property(Rest, Props#{'Server-Reference' => Val}); -%% 31: 'Reason-String', UTF-8 Encoded String; parse_property(<<16#1F, Bin/binary>>, Props) -> - {Val, Rest} = parse_utf(Bin), + {Val, Rest} = parse_utf8_string(Bin), parse_property(Rest, Props#{'Reason-String' => Val}); -%% 33: 'Receive-Maximum', Two Byte Integer; parse_property(<<16#21, Val:16/big, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Receive-Maximum' => Val}); -%% 34: 'Topic-Alias-Maximum', Two Byte Integer; parse_property(<<16#22, Val:16/big, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Topic-Alias-Maximum' => Val}); -%% 35: 'Topic-Alias', Two Byte Integer; parse_property(<<16#23, Val:16/big, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Topic-Alias' => Val}); -%% 36: 'Maximum-QoS', Byte; parse_property(<<16#24, Val, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Maximum-QoS' => Val}); -%% 37: 'Retain-Available', Byte; parse_property(<<16#25, Val, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Retain-Available' => Val}); -%% 38: 'User-Property', UTF-8 String Pair; parse_property(<<16#26, Bin/binary>>, Props) -> - {Pair, Rest} = parse_utf_pair(Bin), - parse_property(Rest, case maps:find('User-Property', Props) of - {ok, UserProps} -> Props#{'User-Property' := [Pair | UserProps]}; - error -> Props#{'User-Property' := [Pair]} - end); -%% 39: 'Maximum-Packet-Size', Four Byte Integer; + {Pair, Rest} = parse_utf8_pair(Bin), + case maps:find('User-Property', Props) of + {ok, UserProps} -> + parse_property(Rest,Props#{'User-Property' := [Pair|UserProps]}); + error -> + parse_property(Rest, Props#{'User-Property' => [Pair]}) + end; parse_property(<<16#27, Val:32, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Maximum-Packet-Size' => Val}); -%% 40: 'Wildcard-Subscription-Available', Byte; parse_property(<<16#28, Val, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Wildcard-Subscription-Available' => Val}); -%% 41: 'Subscription-Identifier-Available', Byte; parse_property(<<16#29, Val, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Subscription-Identifier-Available' => Val}); -%% 42: 'Shared-Subscription-Available', Byte; parse_property(<<16#2A, Val, Bin/binary>>, Props) -> parse_property(Bin, Props#{'Shared-Subscription-Available' => Val}). @@ -316,53 +323,36 @@ parse_variable_byte_integer(<<1:1, Len:7, Rest/binary>>, Multiplier, Value) -> parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) -> {Value + Len * Multiplier, Rest}. -parse_topics(_Packet, <<>>, Topics) -> - lists:reverse(Topics); -parse_topics(?SUBSCRIBE = Sub, Bin, Topics) -> - {Name, <<_Reserved:2, RetainHandling:2, KeepRetain:1, NoLocal:1, QoS:2, Rest/binary>>} = parse_utf(Bin), - SubOpts = [{qos, QoS}, {retain_handling, RetainHandling}, {keep_retain, KeepRetain}, {no_local, NoLocal}], - parse_topics(Sub, Rest, [{Name, SubOpts}| Topics]); -parse_topics(?UNSUBSCRIBE = Sub, Bin, Topics) -> - {Name, <>} = parse_utf(Bin), - parse_topics(Sub, Rest, [Name | Topics]). +parse_topic_filters(subscribe, Bin) -> + [{Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} + || <> <= Bin]; -parse_qos(<<>>, Acc) -> - lists:reverse(Acc); -parse_qos(<>, Acc) -> - parse_qos(Rest, [QoS | Acc]). +parse_topic_filters(unsubscribe, Bin) -> + [Topic || <> <= Bin]. -parse_utf_pair(Bin) -> +parse_reason_codes(Bin) -> + [Code || <> <= Bin]. + +parse_utf8_pair(Bin) -> [{Name, Value} || <> <= Bin]. -parse_utf(Bin, 0) -> +parse_utf8_string(Bin, false) -> {undefined, Bin}; -parse_utf(Bin, _) -> - parse_utf(Bin). +parse_utf8_string(Bin, true) -> + parse_utf8_string(Bin). -parse_utf(<>) -> +parse_utf8_string(<>) -> {Str, Rest}. -parse_msg(Bin, 0) -> - {undefined, Bin}; -parse_msg(<>, _) -> - {Msg, Rest}. +parse_binary_data(<>) -> + {Data, Rest}. bool(0) -> false; bool(1) -> true. -protocol_name_approved(Ver, Name) -> - lists:member({Ver, Name}, ?PROTOCOL_NAMES). - %% Fix Issue#575 fixqos(?PUBREL, 0) -> 1; fixqos(?SUBSCRIBE, 0) -> 1; fixqos(?UNSUBSCRIBE, 0) -> 1; fixqos(_Type, QoS) -> QoS. -%% Fix Issue#1319 -fixdup(Header = #mqtt_packet_header{qos = ?QOS0, dup = true}) -> - Header#mqtt_packet_header{dup = false}; -fixdup(Header = #mqtt_packet_header{qos = ?QOS2, dup = true}) -> - Header#mqtt_packet_header{dup = false}; -fixdup(Header) -> Header. - diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index f301378cf..370ab4739 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -25,7 +25,7 @@ -import(proplists, [get_value/2, get_value/3]). %% API --export([init/3, init/4, info/1, stats/1, clientid/1, client/1, session/1]). +-export([init/3, init/4, get/2, info/1, stats/1, clientid/1, client/1, session/1]). -export([subscribe/2, unsubscribe/2, pubrel/2, shutdown/2]). @@ -43,14 +43,14 @@ %% Protocol State %% ws_initial_headers: Headers from first HTTP request for WebSocket Client. -record(proto_state, {peername, sendfun, connected = false, client_id, client_pid, - clean_sess, proto_ver, proto_name, username, is_superuser, + clean_start, proto_ver, proto_name, username, is_superuser, will_msg, keepalive, keepalive_backoff, max_clientid_len, session, stats_data, mountpoint, ws_initial_headers, peercert_username, is_bridge, connected_at}). -type(proto_state() :: #proto_state{}). --define(INFO_KEYS, [client_id, username, clean_sess, proto_ver, proto_name, +-define(INFO_KEYS, [client_id, username, clean_start, proto_ver, proto_name, keepalive, will_msg, ws_initial_headers, mountpoint, peercert_username, connected_at]). @@ -98,6 +98,12 @@ repl_username_with_peercert(State = #proto_state{peercert_username = undefined}) repl_username_with_peercert(State = #proto_state{peercert_username = PeerCert}) -> State#proto_state{username = PeerCert}. +%%TODO:: +get(proto_ver, #proto_state{proto_ver = Ver}) -> + Ver; +get(_, _ProtoState) -> + undefined. + info(ProtoState) -> ?record_to_proplist(proto_state, ProtoState, ?INFO_KEYS). @@ -111,7 +117,7 @@ client(#proto_state{client_id = ClientId, client_pid = ClientPid, peername = Peername, username = Username, - clean_sess = CleanSess, + clean_start = CleanStart, proto_ver = ProtoVer, keepalive = Keepalive, will_msg = WillMsg, @@ -133,7 +139,7 @@ session(#proto_state{session = Session}) -> %% CONNECT – Client requests a connection to a Server -%% A Client can only send the CONNECT Packet once over a Network Connection. +%% A Client can only send the CONNECT Packet once over a Network Connection. -spec(received(mqtt_packet(), proto_state()) -> {ok, proto_state()} | {error, term()}). received(Packet = ?PACKET(?CONNECT), State = #proto_state{connected = false, stats_data = Stats}) -> @@ -188,8 +194,8 @@ process(?CONNECT_PACKET(Var), State0) -> proto_name = ProtoName, username = Username, password = Password, - clean_sess = CleanSess, - keep_alive = KeepAlive, + clean_start= CleanStart, + keepalive = KeepAlive, client_id = ClientId, is_bridge = IsBridge} = Var, @@ -198,7 +204,7 @@ process(?CONNECT_PACKET(Var), State0) -> proto_name = ProtoName, username = Username, client_id = ClientId, - clean_sess = CleanSess, + clean_start = CleanStart, keepalive = KeepAlive, will_msg = willmsg(Var, State0), is_bridge = IsBridge, @@ -206,14 +212,14 @@ process(?CONNECT_PACKET(Var), State0) -> {ReturnCode1, SessPresent, State3} = case validate_connect(Var, State1) of - ?CONNACK_ACCEPT -> + ?RC_SUCCESS -> case authenticate(client(State1), Password) of {ok, IsSuperuser} -> %% Generate clientId if null State2 = maybe_set_clientid(State1), %% Start session - case emqx_sm:open_session(#{clean_start => CleanSess, + case emqx_sm:open_session(#{clean_start => CleanStart, client_id => clientid(State2), username => Username, client_pid => self()}) of @@ -227,13 +233,13 @@ process(?CONNECT_PACKET(Var), State0) -> %% Emit Stats self() ! emit_stats, %% ACCEPT - {?CONNACK_ACCEPT, SP, State2#proto_state{session = Session, is_superuser = IsSuperuser}}; + {?RC_SUCCESS, SP, State2#proto_state{session = Session, is_superuser = IsSuperuser}}; {error, Error} -> {stop, {shutdown, Error}, State2} end; {error, Reason}-> ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], State1), - {?CONNACK_CREDENTIALS, false, State1} + {?RC_BAD_USER_NAME_OR_PASSWORD, false, State1} end; ReturnCode -> {ReturnCode, false, State1} @@ -252,19 +258,19 @@ process(Packet = ?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload), State = #pro end, {ok, State}; -process(?PUBACK_PACKET(?PUBACK, PacketId), State = #proto_state{session = Session}) -> +process(?PUBACK_PACKET(PacketId), State = #proto_state{session = Session}) -> emqx_session:puback(Session, PacketId), {ok, State}; -process(?PUBACK_PACKET(?PUBREC, PacketId), State = #proto_state{session = Session}) -> +process(?PUBREC_PACKET(PacketId), State = #proto_state{session = Session}) -> emqx_session:pubrec(Session, PacketId), send(?PUBREL_PACKET(PacketId), State); -process(?PUBACK_PACKET(?PUBREL, PacketId), State = #proto_state{session = Session}) -> +process(?PUBREL_PACKET(PacketId), State = #proto_state{session = Session}) -> emqx_session:pubrel(Session, PacketId), - send(?PUBACK_PACKET(?PUBCOMP, PacketId), State); + send(?PUBCOMP_PACKET(PacketId), State); -process(?PUBACK_PACKET(?PUBCOMP, PacketId), State = #proto_state{session = Session})-> +process(?PUBCOMP_PACKET(PacketId), State = #proto_state{session = Session})-> emqx_session:pubcomp(Session, PacketId), {ok, State}; %% Protect from empty topic table @@ -346,7 +352,10 @@ with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), Msg1 = Msg#message{from = #client{client_id = ClientId, username = Username}}, case emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)) of ok -> - send(?PUBACK_PACKET(Type, PacketId), State); + case Type of + ?PUBACK -> send(?PUBACK_PACKET(PacketId), State); + ?PUBREC -> send(?PUBREC_PACKET(PacketId), State) + end; {error, Error} -> ?LOG(error, "PUBLISH ~p error: ~p", [PacketId, Error], State) end. @@ -390,11 +399,10 @@ inc_stats(Type, PktPos, PktCnt, MsgPos, MsgCnt, Stats) -> false -> Stats1 end. -stop_if_auth_failure(RC, State) when RC == ?CONNACK_CREDENTIALS; RC == ?CONNACK_AUTH -> - {stop, {shutdown, auth_failure}, State}; - -stop_if_auth_failure(_RC, State) -> - {ok, State}. +stop_if_auth_failure(?RC_SUCCESS, State) -> + {ok, State}; +stop_if_auth_failure(RC, State) when RC =/= ?RC_SUCCESS -> + {stop, {shutdown, auth_failure}, State}. shutdown(_Error, #proto_state{client_id = undefined}) -> ignore; @@ -450,15 +458,13 @@ start_keepalive(Sec, #proto_state{keepalive_backoff = Backoff}) when Sec > 0 -> validate_connect(Connect = #mqtt_packet_connect{}, ProtoState) -> case validate_protocol(Connect) of - true -> + true -> case validate_clientid(Connect, ProtoState) of - true -> - ?CONNACK_ACCEPT; - false -> - ?CONNACK_INVALID_ID + true -> ?RC_SUCCESS; + false -> ?RC_CLIENT_IDENTIFIER_NOT_VALID end; - false -> - ?CONNACK_PROTO_VER + false -> + ?RC_UNSUPPORTED_PROTOCOL_VERSION end. validate_protocol(#mqtt_packet_connect{proto_ver = Ver, proto_name = Name}) -> @@ -469,10 +475,10 @@ validate_clientid(#mqtt_packet_connect{client_id = ClientId}, when (byte_size(ClientId) >= 1) andalso (byte_size(ClientId) =< MaxLen) -> true; -%% Issue#599: Null clientId and clean_sess = false -validate_clientid(#mqtt_packet_connect{client_id = ClientId, - clean_sess = CleanSess}, _ProtoState) - when byte_size(ClientId) == 0 andalso (not CleanSess) -> +%% Issue#599: Null clientId and clean_start = false +validate_clientid(#mqtt_packet_connect{client_id = ClientId, + clean_start = CleanStart}, _ProtoState) + when byte_size(ClientId) == 0 andalso (not CleanStart) -> false; %% MQTT3.1.1 allow null clientId. @@ -481,10 +487,10 @@ validate_clientid(#mqtt_packet_connect{proto_ver =?MQTT_PROTO_V4, when byte_size(ClientId) =:= 0 -> true; -validate_clientid(#mqtt_packet_connect{proto_ver = ProtoVer, - clean_sess = CleanSess}, ProtoState) -> - ?LOG(warning, "Invalid clientId. ProtoVer: ~p, CleanSess: ~s", - [ProtoVer, CleanSess], ProtoState), +validate_clientid(#mqtt_packet_connect{proto_ver = ProtoVer, + clean_start = CleanStart}, ProtoState) -> + ?LOG(warning, "Invalid clientId. ProtoVer: ~p, CleanStart: ~s", + [ProtoVer, CleanStart], ProtoState), false. validate_packet(?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload)) -> @@ -499,7 +505,7 @@ validate_packet(?SUBSCRIBE_PACKET(_PacketId, TopicTable)) -> validate_packet(?UNSUBSCRIBE_PACKET(_PacketId, Topics)) -> validate_topics(filter, Topics); -validate_packet(_Packet) -> +validate_packet(_Packet) -> ok. validate_topics(_Type, []) -> diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl new file mode 100644 index 000000000..029914f48 --- /dev/null +++ b/src/emqx_reason_codes.erl @@ -0,0 +1,110 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_reason_codes). + +-export([name/1, text/1]). + +name(16#00) -> success; +name(16#01) -> granted_qos1; +name(16#02) -> granted_qos2; +name(16#04) -> disconnect_with_will_message; +name(16#10) -> no_matching_subscribers; +name(16#11) -> no_subscription_existed; +name(16#18) -> continue_authentication; +name(16#19) -> re_authenticate; +name(16#80) -> unspecified_error; +name(16#81) -> malformed_Packet; +name(16#82) -> protocol_error; +name(16#83) -> implementation_specific_error; +name(16#84) -> unsupported_protocol_version; +name(16#85) -> client_identifier_not_valid; +name(16#86) -> bad_username_or_password; +name(16#87) -> not_authorized; +name(16#88) -> server_unavailable; +name(16#89) -> server_busy; +name(16#8A) -> banned; +name(16#8B) -> server_shutting_down; +name(16#8C) -> bad_authentication_method; +name(16#8D) -> keepalive_timeout; +name(16#8E) -> session_taken_over; +name(16#8F) -> topic_filter_invalid; +name(16#90) -> topic_name_invalid; +name(16#91) -> packet_identifier_inuse; +name(16#92) -> packet_identifier_not_found; +name(16#93) -> receive_maximum_exceeded; +name(16#94) -> topic_alias_invalid; +name(16#95) -> packet_too_large; +name(16#96) -> message_rate_too_high; +name(16#97) -> quota_exceeded; +name(16#98) -> administrative_action; +name(16#99) -> payload_format_invalid; +name(16#9A) -> retain_not_supported; +name(16#9B) -> qos_not_supported; +name(16#9C) -> use_another_server; +name(16#9D) -> server_moved; +name(16#9E) -> shared_subscriptions_not_supported; +name(16#9F) -> connection_rate_exceeded; +name(16#A0) -> maximum_connect_time; +name(16#A1) -> subscription_identifiers_not_supported; +name(16#A2) -> wildcard_subscriptions_not_supported; +name(Code) -> list_to_atom("unkown_" ++ integer_to_list(Code)). + +text(16#00) -> <<"Success">>; +text(16#01) -> <<"Granted QoS 1">>; +text(16#02) -> <<"Granted QoS 2">>; +text(16#04) -> <<"Disconnect with Will Message">>; +text(16#10) -> <<"No matching subscribers">>; +text(16#11) -> <<"No subscription existed">>; +text(16#18) -> <<"Continue authentication">>; +text(16#19) -> <<"Re-authenticate">>; +text(16#80) -> <<"Unspecified error">>; +text(16#81) -> <<"Malformed Packet">>; +text(16#82) -> <<"Protocol Error">>; +text(16#83) -> <<"Implementation specific error">>; +text(16#84) -> <<"Unsupported Protocol Version">>; +text(16#85) -> <<"Client Identifier not valid">>; +text(16#86) -> <<"Bad User Name or Password">>; +text(16#87) -> <<"Not authorized">>; +text(16#88) -> <<"Server unavailable">>; +text(16#89) -> <<"Server busy">>; +text(16#8A) -> <<"Banned">>; +text(16#8B) -> <<"Server shutting down">>; +text(16#8C) -> <<"Bad authentication method">>; +text(16#8D) -> <<"Keep Alive timeout">>; +text(16#8E) -> <<"Session taken over">>; +text(16#8F) -> <<"Topic Filter invalid">>; +text(16#90) -> <<"Topic Name invalid">>; +text(16#91) -> <<"Packet Identifier in use">>; +text(16#92) -> <<"Packet Identifier not found">>; +text(16#93) -> <<"Receive Maximum exceeded">>; +text(16#94) -> <<"Topic Alias invalid">>; +text(16#95) -> <<"Packet too large">>; +text(16#96) -> <<"Message rate too high">>; +text(16#97) -> <<"Quota exceeded">>; +text(16#98) -> <<"Administrative action">>; +text(16#99) -> <<"Payload format invalid">>; +text(16#9A) -> <<"Retain not supported">>; +text(16#9B) -> <<"QoS not supported">>; +text(16#9C) -> <<"Use another server">>; +text(16#9D) -> <<"Server moved">>; +text(16#9E) -> <<"Shared Subscriptions not supported">>; +text(16#9F) -> <<"Connection rate exceeded">>; +text(16#A0) -> <<"Maximum connect time">>; +text(16#A1) -> <<"Subscription Identifiers not supported">>; +text(16#A2) -> <<"Wildcard Subscriptions not supported">>; +text(Code) -> iolist_to_binary(["Unkown", integer_to_list(Code)]). + diff --git a/src/emqx_router.erl b/src/emqx_router.erl index dffc91133..c79482bd2 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -67,7 +67,7 @@ mnesia(copy) -> -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> - gen_server:start_link(emqx_misc:proc_name(?MODULE, Id), + gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, [Pool, Id], [{hibernate_after, 10000}]). %%-------------------------------------------------------------------- diff --git a/src/emqx_serializer.erl b/src/emqx_serializer.erl index 0450a462f..670213674 100644 --- a/src/emqx_serializer.erl +++ b/src/emqx_serializer.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_serializer). @@ -20,224 +20,266 @@ -include("emqx_mqtt.hrl"). -%% API --export([serialize/1]). +-type(option() :: {version, mqtt_version()}). -%% @doc Serialise MQTT Packet --spec(serialize(mqtt_packet()) -> iolist()). -serialize(#mqtt_packet{header = Header = #mqtt_packet_header{type = Type}, +-export_type([option/0]). + +-export([serialize/1, serialize/2]). + +-spec(serialize(mqtt_packet()) -> iodata()). +serialize(Packet) -> serialize(Packet, []). + +-spec(serialize(mqtt_packet(), [option()]) -> iodata()). +serialize(#mqtt_packet{header = Header, variable = Variable, - payload = Payload}) -> - serialize_header(Header, - serialize_variable(Type, Variable, - serialize_payload(Payload))). + payload = Payload}, Opts) when is_list(Opts) -> + Opts1 = parse_opt(Opts, #{version => ?MQTT_PROTO_V4}), + serialize(Header, serialize_variable(Variable, Opts1), serialize_payload(Payload)). -serialize_header(#mqtt_packet_header{type = Type, - dup = Dup, - qos = Qos, - retain = Retain}, - {VariableBin, PayloadBin}) - when ?CONNECT =< Type andalso Type =< ?DISCONNECT -> - Len = byte_size(VariableBin) + byte_size(PayloadBin), +parse_opt([], Map) -> + Map; +parse_opt([{version, Ver}|Opts], Map) -> + parse_opt(Opts, Map#{version := Ver}); +parse_opt([_|Opts], Map) -> + parse_opt(Opts, Map). + +serialize(#mqtt_packet_header{type = Type, + dup = Dup, + qos = Qos, + retain = Retain}, VariableData, PayloadData) + when ?CONNECT =< Type andalso Type =< ?AUTH -> + Len = iolist_size(VariableData) + iolist_size(PayloadData), true = (Len =< ?MAX_PACKET_SIZE), [<>, - serialize_len(Len), VariableBin, PayloadBin]. + serialize_remaining_len(Len), VariableData, PayloadData]. -serialize_variable(?CONNECT, #mqtt_packet_connect{client_id = ClientId, - proto_ver = ProtoVer, - proto_name = ProtoName, - will_retain = WillRetain, - will_qos = WillQos, - will_flag = WillFlag, - clean_sess = CleanSess, - keep_alive = KeepAlive, - will_topic = WillTopic, - will_msg = WillMsg, - username = Username, - password = Password}, undefined) -> - VariableBin = <<(byte_size(ProtoName)):16/big-unsigned-integer, - ProtoName/binary, - ProtoVer:8, - (opt(Username)):1, - (opt(Password)):1, - (opt(WillRetain)):1, - WillQos:2, - (opt(WillFlag)):1, - (opt(CleanSess)):1, - 0:1, - KeepAlive:16/big-unsigned-integer>>, - PayloadBin = serialize_utf(ClientId), - PayloadBin1 = case WillFlag of - true -> <>; - false -> PayloadBin - end, - UserPasswd = << <<(serialize_utf(B))/binary>> || B <- [Username, Password], B =/= undefined >>, - {VariableBin, <>}; +serialize_variable(#mqtt_packet_connect{proto_name = ProtoName, + proto_ver = ProtoVer, + is_bridge = IsBridge, + clean_start = CleanStart, + will_flag = WillFlag, + will_qos = WillQos, + will_retain = WillRetain, + keepalive = KeepAlive, + properties = Properties, + client_id = ClientId, + will_props = WillProps, + will_topic = WillTopic, + will_payload = WillPayload, + username = Username, + password = Password}, _Opts) -> + [serialize_binary_data(ProtoName), + <<(case IsBridge of + true -> 16#80 + ProtoVer; + false -> ProtoVer + end):8, + (opt(Username)):1, + (opt(Password)):1, + (opt(WillRetain)):1, + WillQos:2, + (opt(WillFlag)):1, + (opt(CleanStart)):1, + 0:1, + KeepAlive:16/big-unsigned-integer>>, + serialize_properties(Properties, ProtoVer), + serialize_utf8_string(ClientId), + case WillFlag of + true -> [serialize_properties(WillProps, ProtoVer), + serialize_utf8_string(WillTopic), + serialize_binary_data(WillPayload)]; + false -> <<>> + end, + serialize_utf8_string(Username, true), + serialize_utf8_string(Password, true)]; -serialize_variable(?CONNACK, #mqtt_packet_connack{ack_flags = AckFlags, - reason_code = ReasonCode, - properties = Properties}, undefined) -> - PropsBin = serialize_properties(Properties), - {<>, <<>>}; +serialize_variable(#mqtt_packet_connack{ack_flags = AckFlags, + reason_code = ReasonCode, + properties = Properties}, #{version := Ver}) -> + [AckFlags, ReasonCode, serialize_properties(Properties, Ver)]; -serialize_variable(?SUBSCRIBE, #mqtt_packet_subscribe{packet_id = PacketId, - topic_filters = TopicFilters}, undefined) -> - {<>, serialize_topics(TopicFilters)}; +serialize_variable(#mqtt_packet_publish{topic_name = TopicName, + packet_id = PacketId, + properties = Properties}, #{version := Ver}) -> + [serialize_utf8_string(TopicName), + if + PacketId =:= undefined -> <<>>; + true -> <> + end, + serialize_properties(Properties, Ver)]; -serialize_variable(?SUBACK, #mqtt_packet_suback{packet_id = PacketId, - properties = Properties, - reason_codes = ReasonCodes}, undefined) -> - io:format("SubAck ReasonCodes: ~p~n", [ReasonCodes]), - {<>, << <> || Code <- ReasonCodes >>}; +serialize_variable(#mqtt_packet_puback{packet_id = PacketId}, #{version := 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}) -> + [<>, ReasonCode, + serialize_properties(Properties, ?MQTT_PROTO_V5)]; -serialize_variable(?UNSUBSCRIBE, #mqtt_packet_unsubscribe{packet_id = PacketId, - topics = Topics }, undefined) -> - {<>, serialize_topics(Topics)}; +serialize_variable(#mqtt_packet_subscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}, + #{version := Ver}) -> + [<>, serialize_properties(Properties, Ver), + serialize_topic_filters(subscribe, TopicFilters, Ver)]; -serialize_variable(?UNSUBACK, #mqtt_packet_unsuback{packet_id = PacketId, - properties = Properties, - reason_codes = ReasonCodes}, undefined) -> - {<>, << <> || Code <- ReasonCodes >>}; +serialize_variable(#mqtt_packet_suback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes}, + #{version := Ver}) -> + [<>, serialize_properties(Properties, Ver), + << <> || Code <- ReasonCodes >>]; -serialize_variable(?PUBLISH, #mqtt_packet_publish{topic_name = TopicName, - packet_id = PacketId }, PayloadBin) -> - TopicBin = serialize_utf(TopicName), - PacketIdBin = if - PacketId =:= undefined -> <<>>; - true -> <> - end, - {<>, PayloadBin}; +serialize_variable(#mqtt_packet_unsubscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}, + #{version := Ver}) -> + [<>, serialize_properties(Properties, Ver), + serialize_topic_filters(unsubscribe, TopicFilters, Ver)]; -serialize_variable(PubAck, #mqtt_packet_puback{packet_id = PacketId}, _Payload) - when PubAck =:= ?PUBACK; PubAck =:= ?PUBREC; PubAck =:= ?PUBREL; PubAck =:= ?PUBCOMP -> - {<>, <<>>}; +serialize_variable(#mqtt_packet_unsuback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes}, + #{version := Ver}) -> + [<>, serialize_properties(Properties, Ver), + << <> || Code <- ReasonCodes >>]; -serialize_variable(?PINGREQ, undefined, undefined) -> - {<<>>, <<>>}; +serialize_variable(#mqtt_packet_disconnect{}, #{version := Ver}) + when Ver == ?MQTT_PROTO_V3; Ver == ?MQTT_PROTO_V4 -> + <<>>; -serialize_variable(?PINGRESP, undefined, undefined) -> - {<<>>, <<>>}; +serialize_variable(#mqtt_packet_disconnect{reason_code = ReasonCode, + properties = Properties}, + #{version := Ver = ?MQTT_PROTO_V5}) -> + [ReasonCode, serialize_properties(Properties, Ver)]; +serialize_variable(#mqtt_packet_disconnect{}, _Ver) -> + <<>>; -serialize_variable(?DISCONNECT, #mqtt_packet_disconnect{reason_code = ReasonCode, - properties = Properties}, undefined) -> - {<>, <<>>}; +serialize_variable(#mqtt_packet_auth{reason_code = ReasonCode, + properties = Properties}, + #{version := Ver = ?MQTT_PROTO_V5}) -> + [ReasonCode, serialize_properties(Properties, Ver)]; -serialize_variable(?AUTH, #mqtt_packet_auth{reason_code = ReasonCode, - properties = Properties}, undefined) -> - {<>, <<>>}. +serialize_variable(PacketId, ?MQTT_PROTO_V3) when is_integer(PacketId) -> + <>; +serialize_variable(PacketId, ?MQTT_PROTO_V4) when is_integer(PacketId) -> + <>; +serialize_variable(undefined, _Ver) -> + <<>>. serialize_payload(undefined) -> - undefined; -serialize_payload(Bin) when is_binary(Bin) -> + <<>>; +serialize_payload(Bin) when is_binary(Bin); is_list(Bin) -> Bin. -serialize_properties(undefined) -> +serialize_properties(_Props, Ver) when Ver =/= ?MQTT_PROTO_V5 -> <<>>; -serialize_properties(Props) -> - << <<(serialize_property(Prop, Val))/binary>> || {Prop, Val} <- maps:to_list(Props) >>. +serialize_properties(Props, ?MQTT_PROTO_V5) -> + serialize_properties(Props). -%% 01: Byte; +serialize_properties(undefined) -> + <<0>>; +serialize_properties(Props) when map_size(Props) == 0 -> + <<0>>; +serialize_properties(Props) when is_map(Props) -> + Bin = << <<(serialize_property(Prop, Val))/binary>> || {Prop, Val} <- maps:to_list(Props) >>, + [serialize_variable_byte_integer(byte_size(Bin)), Bin]. + +%% Ignore undefined +serialize_property(_, undefined) -> + <<>>; serialize_property('Payload-Format-Indicator', Val) -> <<16#01, Val>>; -%% 02: Four Byte Integer; serialize_property('Message-Expiry-Interval', Val) -> <<16#02, Val:32/big>>; -%% 03: UTF-8 Encoded String; serialize_property('Content-Type', Val) -> - <<16#03, (serialize_utf(Val))/binary>>; -%% 08: UTF-8 Encoded String; + <<16#03, (serialize_utf8_string(Val))/binary>>; serialize_property('Response-Topic', Val) -> - <<16#08, (serialize_utf(Val))/binary>>; -%% 09: Binary Data; + <<16#08, (serialize_utf8_string(Val))/binary>>; serialize_property('Correlation-Data', Val) -> - <<16#09, (iolist_size(Val)):16, Val/binary>>; -%% 11: Variable Byte Integer; + <<16#09, (byte_size(Val)):16, Val/binary>>; serialize_property('Subscription-Identifier', Val) -> <<16#0B, (serialize_variable_byte_integer(Val))/binary>>; -%% 17: Four Byte Integer; serialize_property('Session-Expiry-Interval', Val) -> <<16#11, Val:32/big>>; -%% 18: UTF-8 Encoded String; serialize_property('Assigned-Client-Identifier', Val) -> - <<16#12, (serialize_utf(Val))/binary>>; -%% 19: Two Byte Integer; + <<16#12, (serialize_utf8_string(Val))/binary>>; serialize_property('Server-Keep-Alive', Val) -> <<16#13, Val:16/big>>; -%% 21: UTF-8 Encoded String; serialize_property('Authentication-Method', Val) -> - <<16#15, (serialize_utf(Val))/binary>>; -%% 22: Binary Data; + <<16#15, (serialize_utf8_string(Val))/binary>>; serialize_property('Authentication-Data', Val) -> <<16#16, (iolist_size(Val)):16, Val/binary>>; -%% 23: Byte; serialize_property('Request-Problem-Information', Val) -> <<16#17, Val>>; -%% 24: Four Byte Integer; serialize_property('Will-Delay-Interval', Val) -> <<16#18, Val:32/big>>; -%% 25: Byte; serialize_property('Request-Response-Information', Val) -> <<16#19, Val>>; -%% 26: UTF-8 Encoded String; serialize_property('Response-Information', Val) -> - <<16#1A, (serialize_utf(Val))/binary>>; -%% 28: UTF-8 Encoded String; + <<16#1A, (serialize_utf8_string(Val))/binary>>; serialize_property('Server-Reference', Val) -> - <<16#1C, (serialize_utf(Val))/binary>>; -%% 31: UTF-8 Encoded String; + <<16#1C, (serialize_utf8_string(Val))/binary>>; serialize_property('Reason-String', Val) -> - <<16#1F, (serialize_utf(Val))/binary>>; -%% 33: Two Byte Integer; + <<16#1F, (serialize_utf8_string(Val))/binary>>; serialize_property('Receive-Maximum', Val) -> <<16#21, Val:16/big>>; -%% 34: Two Byte Integer; serialize_property('Topic-Alias-Maximum', Val) -> <<16#22, Val:16/big>>; -%% 35: Two Byte Integer; serialize_property('Topic-Alias', Val) -> <<16#23, Val:16/big>>; -%% 36: Byte; serialize_property('Maximum-QoS', Val) -> <<16#24, Val>>; -%% 37: Byte; serialize_property('Retain-Available', Val) -> <<16#25, Val>>; -%% 38: UTF-8 String Pair; -serialize_property('User-Property', Val) -> - <<16#26, (serialize_utf_pair(Val))/binary>>; -%% 39: Four Byte Integer; +serialize_property('User-Property', {Key, Val}) -> + <<16#26, (serialize_utf8_pair({Key, Val}))/binary>>; +serialize_property('User-Property', Props) when is_list(Props) -> + << <<(serialize_property('User-Property', {Key, Val}))/binary>> + || {Key, Val} <- Props >>; serialize_property('Maximum-Packet-Size', Val) -> <<16#27, Val:32/big>>; -%% 40: Byte; serialize_property('Wildcard-Subscription-Available', Val) -> <<16#28, Val>>; -%% 41: Byte; serialize_property('Subscription-Identifier-Available', Val) -> <<16#29, Val>>; -%% 42: Byte; serialize_property('Shared-Subscription-Available', Val) -> <<16#2A, Val>>. -serialize_topics([{_Topic, _Qos}|_] = Topics) -> - << <<(serialize_utf(Topic))/binary, ?RESERVED:6, Qos:2>> || {Topic, Qos} <- Topics >>; +serialize_topic_filters(subscribe, TopicFilters, ?MQTT_PROTO_V5) -> + << <<(serialize_utf8_string(Topic))/binary, ?RESERVED:2, Rh:2, (opt(Rap)):1, (opt(Nl)):1, Qos:2>> + || {Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = Qos}} <- TopicFilters >>; -serialize_topics([H|_] = Topics) when is_binary(H) -> - << <<(serialize_utf(Topic))/binary>> || Topic <- Topics >>. +serialize_topic_filters(subscribe, TopicFilters, _Ver) -> + << <<(serialize_utf8_string(Topic))/binary, ?RESERVED:6, Qos:2>> + || {Topic, #mqtt_subopts{qos = Qos}} <- TopicFilters >>; -serialize_utf_pair({Name, Value}) -> - << <<(serialize_utf(S))/binary, (serialize_utf(S))/binary>> || S <- [Name, Value] >>. +serialize_topic_filters(unsubscribe, TopicFilters, _Ver) -> + << <<(serialize_utf8_string(Topic))/binary>> || Topic <- TopicFilters >>. -serialize_utf(String) -> +serialize_utf8_pair({Name, Value}) -> + << <<(serialize_utf8_string(S))/binary, + (serialize_utf8_string(S))/binary>> || S <- [Name, Value] >>. + +serialize_binary_data(Bin) -> + [<<(byte_size(Bin)):16/big-unsigned-integer>>, Bin]. + +serialize_utf8_string(undefined, false) -> + error(utf8_string_undefined); +serialize_utf8_string(undefined, true) -> + <<>>; +serialize_utf8_string(String, _AllowNull) -> + serialize_utf8_string(String). + +serialize_utf8_string(String) -> StringBin = unicode:characters_to_binary(String), Len = byte_size(StringBin), true = (Len =< 16#ffff), <>. -serialize_len(I) -> - serialize_variable_byte_integer(I). %%TODO: refactor later. +serialize_remaining_len(I) -> + serialize_variable_byte_integer(I). serialize_variable_byte_integer(N) when N =< ?LOWBITS -> <<0:1, N:7>>; diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 3d2716099..1dbe1e83a 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -725,10 +725,10 @@ acked(puback, PacketId, State = #state{client_id = ClientId, username = Username, inflight = Inflight}) -> case Inflight:lookup(PacketId) of - {publish, Msg, _Ts} -> + {value, {publish, Msg, _Ts}} -> emqx_hooks:run('message.acked', [ClientId, Username], Msg), State#state{inflight = Inflight:delete(PacketId)}; - _ -> + none -> ?LOG(warning, "Duplicated PUBACK Packet: ~p", [PacketId], State), State end; @@ -737,11 +737,14 @@ acked(pubrec, PacketId, State = #state{client_id = ClientId, username = Username, inflight = Inflight}) -> case Inflight:lookup(PacketId) of - {publish, Msg, _Ts} -> + {value, {publish, Msg, _Ts}} -> emqx_hooks:run('message.acked', [ClientId, Username], Msg), State#state{inflight = Inflight:update(PacketId, {pubrel, PacketId, os:timestamp()})}; - {pubrel, PacketId, _Ts} -> + {value, {pubrel, PacketId, _Ts}} -> ?LOG(warning, "Duplicated PUBREC Packet: ~p", [PacketId], State), + State; + none -> + ?LOG(warning, "Unexpected PUBREC Packet: ~p", [PacketId], State), State end; diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index 16e1b8715..b01f13e41 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -38,5 +38,5 @@ init([]) -> Manager = {manager, {emqx_sm, start_link, []}, permanent, 5000, worker, [emqx_sm]}, - {ok, {{one_for_rest, 10, 3600}, [Locker, Registry, Manager]}}. + {ok, {{rest_for_one, 10, 3600}, [Locker, Registry, Manager]}}. diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index ac79daa3c..aee73b873 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -118,8 +118,8 @@ unregister_mod(_) -> [] = ?AC:lookup_mods(auth). check_acl(_) -> - User1 = #mqtt_client{client_id = <<"client1">>, username = <<"testuser">>}, - User2 = #mqtt_client{client_id = <<"client2">>, username = <<"xyz">>}, + User1 = #client{client_id = <<"client1">>, username = <<"testuser">>}, + User2 = #client{client_id = <<"client2">>, username = <<"xyz">>}, allow = ?AC:check_acl(User1, subscribe, <<"users/testuser/1">>), allow = ?AC:check_acl(User1, subscribe, <<"clients/client1">>), allow = ?AC:check_acl(User1, subscribe, <<"clients/client1/x/y">>), @@ -158,8 +158,8 @@ compile_rule(_) -> {deny, all} = compile({deny, all}). match_rule(_) -> - User = #mqtt_client{peername = {{127,0,0,1}, 2948}, client_id = <<"testClient">>, username = <<"TestUser">>}, - User2 = #mqtt_client{peername = {{192,168,0,10}, 3028}, client_id = <<"testClient">>, username = <<"TestUser">>}, + User = #client{peername = {{127,0,0,1}, 2948}, client_id = <<"testClient">>, username = <<"TestUser">>}, + User2 = #client{peername = {{192,168,0,10}, 3028}, client_id = <<"testClient">>, username = <<"TestUser">>}, {matched, allow} = match(User, <<"Test/Topic">>, {allow, all}), {matched, deny} = match(User, <<"Test/Topic">>, {deny, all}), @@ -169,7 +169,7 @@ match_rule(_) -> 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(#mqtt_client{username = <<"user2">>}, <<"users/user2/abc/def">>, + {matched, allow} = match(#client{username = <<"user2">>}, <<"users/user2/abc/def">>, compile({allow, all, subscribe, ["users/%u/#"]})), {matched, deny} = match(User, <<"d/e/f">>, compile({deny, all, subscribe, ["$SYS/#", "#"]})), Rule = compile({allow, {'and', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, <<"Topic">>}), diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl index fac287f74..0d86beea0 100644 --- a/test/emqx_protocol_SUITE.erl +++ b/test/emqx_protocol_SUITE.erl @@ -87,7 +87,7 @@ parse_connect(_) -> keep_alive = 60}}, <<>>} = emqx_parser:parse(V31ConnBin, Parser), %% CONNECT(Q0, R0, D0, ClientId=mosqpub/10451-iMac.loca, ProtoName=MQTT, ProtoVsn=4, CleanSess=true, KeepAlive=60, Username=undefined, Password=undefined) V311ConnBin = <<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>>, - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT, + {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT, dup = false, qos = 0, retain = false}, @@ -160,7 +160,7 @@ parse_publish(_) -> variable = #mqtt_packet_publish{topic_name = <<"a/b/c">>, packet_id = 1}, payload = <<"hahah">> }, <<>>} = emqx_parser:parse(PubBin, Parser), - + %PUBLISH(Qos=0, Retain=false, Dup=false, TopicName=xxx/yyy, PacketId=undefined, Payload=<<"hello">>) %DISCONNECT(Qos=0, Retain=false, Dup=false) PubBin1 = <<48,14,0,7,120,120,120,47,121,121,121,104,101,108,108,111,224,0>>, @@ -244,62 +244,6 @@ parse_disconnect(_) -> qos = 0, retain = false}}, <<>>} = emqx_parser:parse(Bin, Parser). -%%-------------------------------------------------------------------- -%% Serialize Cases -%%-------------------------------------------------------------------- - -serialize_connect(_) -> - serialize(?CONNECT_PACKET(#mqtt_packet_connect{})), - serialize(?CONNECT_PACKET(#mqtt_packet_connect{ - client_id = <<"clientId">>, - will_qos = ?QOS1, - will_flag = true, - will_retain = true, - will_topic = <<"will">>, - will_msg = <<"haha">>, - clean_sess = true})). - -serialize_connack(_) -> - ConnAck = #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, - variable = #mqtt_packet_connack{ack_flags = 0, return_code = 0}}, - ?assertEqual(<<32,2,0,0>>, iolist_to_binary(serialize(ConnAck))). - -serialize_publish(_) -> - serialize(?PUBLISH_PACKET(?QOS_0, <<"Topic">>, undefined, <<"Payload">>)), - serialize(?PUBLISH_PACKET(?QOS_1, <<"Topic">>, 938, <<"Payload">>)), - serialize(?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 99, long_payload())). - -serialize_puback(_) -> - serialize(?PUBACK_PACKET(?PUBACK, 10384)). - -serialize_pubrel(_) -> - serialize(?PUBREL_PACKET(10384)). - -serialize_subscribe(_) -> - TopicTable = [{<<"TopicQos0">>, ?QOS_0}, {<<"TopicQos1">>, ?QOS_1}, {<<"TopicQos2">>, ?QOS_2}], - serialize(?SUBSCRIBE_PACKET(10, TopicTable)). - -serialize_suback(_) -> - serialize(?SUBACK_PACKET(10, [?QOS_0, ?QOS_1, 128])). - -serialize_unsubscribe(_) -> - serialize(?UNSUBSCRIBE_PACKET(10, [<<"Topic1">>, <<"Topic2">>])). - -serialize_unsuback(_) -> - serialize(?UNSUBACK_PACKET(10)). - -serialize_pingreq(_) -> - serialize(?PACKET(?PINGREQ)). - -serialize_pingresp(_) -> - serialize(?PACKET(?PINGRESP)). - -serialize_disconnect(_) -> - serialize(?PACKET(?DISCONNECT)). - -long_payload() -> - iolist_to_binary(["payload." || _I <- lists:seq(1, 100)]). - %%-------------------------------------------------------------------- %% Packet Cases %%-------------------------------------------------------------------- diff --git a/test/emqx_serializer_SUITE.erl b/test/emqx_serializer_SUITE.erl new file mode 100644 index 000000000..d0a34fd20 --- /dev/null +++ b/test/emqx_serializer_SUITE.erl @@ -0,0 +1,91 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_serializer_SUITE). + +-compile(export_all). + +-include("emqx_mqtt.hrl"). + +-include_lib("eunit/include/eunit.hrl"). + +-import(emqx_serializer, [serialize/1]). + +all() -> + [serialize_connect, + serialize_connack, + serialize_publish, + serialize_puback, + serialize_pubrel, + serialize_subscribe, + serialize_suback, + serialize_unsubscribe, + serialize_unsuback, + serialize_pingreq, + serialize_pingresp, + serialize_disconnect]. + +serialize_connect(_) -> + serialize(?CONNECT_PACKET(#mqtt_packet_connect{})), + serialize(?CONNECT_PACKET(#mqtt_packet_connect{ + client_id = <<"clientId">>, + will_qos = ?QOS1, + will_flag = true, + will_retain = true, + will_topic = <<"will">>, + will_payload = <<"haha">>, + clean_sess = true})). + +serialize_connack(_) -> + ConnAck = #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, + variable = #mqtt_packet_connack{ack_flags = 0, return_code = 0}}, + ?assertEqual(<<32,2,0,0>>, iolist_to_binary(serialize(ConnAck))). + +serialize_publish(_) -> + serialize(?PUBLISH_PACKET(?QOS_0, <<"Topic">>, undefined, <<"Payload">>)), + serialize(?PUBLISH_PACKET(?QOS_1, <<"Topic">>, 938, <<"Payload">>)), + serialize(?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 99, long_payload())). + +serialize_puback(_) -> + serialize(?PUBACK_PACKET(?PUBACK, 10384)). + +serialize_pubrel(_) -> + serialize(?PUBREL_PACKET(10384)). + +serialize_subscribe(_) -> + TopicTable = [{<<"TopicQos0">>, ?QOS_0}, {<<"TopicQos1">>, ?QOS_1}, {<<"TopicQos2">>, ?QOS_2}], + serialize(?SUBSCRIBE_PACKET(10, TopicTable)). + +serialize_suback(_) -> + serialize(?SUBACK_PACKET(10, [?QOS_0, ?QOS_1, 128])). + +serialize_unsubscribe(_) -> + serialize(?UNSUBSCRIBE_PACKET(10, [<<"Topic1">>, <<"Topic2">>])). + +serialize_unsuback(_) -> + serialize(?UNSUBACK_PACKET(10)). + +serialize_pingreq(_) -> + serialize(?PACKET(?PINGREQ)). + +serialize_pingresp(_) -> + serialize(?PACKET(?PINGRESP)). + +serialize_disconnect(_) -> + serialize(?PACKET(?DISCONNECT)). + +long_payload() -> + iolist_to_binary(["payload." || _I <- lists:seq(1, 100)]). From c11e8f453b222121a8883462516bbe4c8c6500e9 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 9 May 2018 00:42:41 +0800 Subject: [PATCH 053/520] Add test cases for MQTT 5.0 frame --- src/{emqx_parser.erl => emqx_frame.erl} | 382 ++++++++++++++++++--- src/emqx_serializer.erl | 294 ---------------- test/emqx_frame_SUITE.erl | 430 ++++++++++++++++++++++++ 3 files changed, 763 insertions(+), 343 deletions(-) rename src/{emqx_parser.erl => emqx_frame.erl} (51%) delete mode 100644 src/emqx_serializer.erl create mode 100644 test/emqx_frame_SUITE.erl diff --git a/src/emqx_parser.erl b/src/emqx_frame.erl similarity index 51% rename from src/emqx_parser.erl rename to src/emqx_frame.erl index a7c6707f3..7f01ee706 100644 --- a/src/emqx_parser.erl +++ b/src/emqx_frame.erl @@ -14,44 +14,50 @@ %%% limitations under the License. %%%=================================================================== --module(emqx_parser). +-module(emqx_frame). -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([initial_state/0, initial_state/1, parse/2]). +-type(options() :: #{max_packet_size => 1..?MAX_PACKET_SIZE, + version => mqtt_version()}). --type(max_packet_size() :: 1..?MAX_PACKET_SIZE). +-type(parse_state() :: {none, options()} | cont_fun(binary())). --type(option() :: {max_len, max_packet_size()} - | {version, mqtt_version()}). +-type(cont_fun(Bin) :: fun((Bin) -> {ok, mqtt_packet(), binary()} + | {more, cont_fun(Bin)})). --type(state() :: {none, map()} | {more, fun()}). +-export_type([options/0, parse_state/0]). --export_type([option/0, state/0]). +-export([initial_state/0, initial_state/1]). +-export([parse/2]). +-export([serialize/1, serialize/2]). -%% @doc Initialize a parser --spec(initial_state() -> {none, map()}). -initial_state() -> initial_state([]). +-define(DEFAULT_OPTIONS, #{max_packet_size => ?MAX_PACKET_SIZE, + version => ?MQTT_PROTO_V4}). --spec(initial_state([option()]) -> {none, map()}). -initial_state(Options) when is_list(Options) -> - {none, parse_opt(Options, #{max_len => ?MAX_PACKET_SIZE, - version => ?MQTT_PROTO_V4})}. +%%-------------------------------------------------------------------- +%% Init parse state +%%-------------------------------------------------------------------- -parse_opt([], Map) -> - Map; -parse_opt([{version, Ver}|Opts], Map) -> - parse_opt(Opts, Map#{version := Ver}); -parse_opt([{max_len, Len}|Opts], Map) -> - parse_opt(Opts, Map#{max_len := Len}); -parse_opt([_|Opts], Map) -> - parse_opt(Opts, Map). +-spec(initial_state() -> {none, options()}). +initial_state() -> + initial_state(#{}). -%% @doc Parse MQTT Packet --spec(parse(binary(), {none, map()} | fun()) - -> {ok, mqtt_packet()} | {error, term()} | {more, fun()}). +-spec(initial_state(options()) -> {none, options()}). +initial_state(Options) when is_map(Options) -> + {none, merge_opts(Options)}. + +merge_opts(Options) -> + maps:merge(?DEFAULT_OPTIONS, Options). + +%%-------------------------------------------------------------------- +%% Parse MQTT Frame +%%-------------------------------------------------------------------- + +-spec(parse(binary(), parse_state()) + -> {ok, mqtt_packet(), binary()} | {more, cont_fun(binary())}). parse(<<>>, {none, Options}) -> {more, fun(Bin) -> parse(Bin, {none, Options}) end}; parse(<>, {none, Options}) -> @@ -59,31 +65,33 @@ parse(<>, {none, Options}) -> dup = bool(Dup), qos = fixqos(Type, QoS), retain = bool(Retain)}, Options); -parse(Bin, Cont) -> Cont(Bin). +parse(Bin, Cont) when is_binary(Bin), is_function(Cont) -> + Cont(Bin). parse_remaining_len(<<>>, Header, Options) -> {more, fun(Bin) -> parse_remaining_len(Bin, Header, Options) end}; parse_remaining_len(Rest, Header, Options) -> parse_remaining_len(Rest, Header, 1, 0, Options). -parse_remaining_len(_Bin, _Header, _Multiplier, Length, #{max_len := MaxLen}) - when Length > MaxLen -> - {error, mqtt_frame_too_long}; +parse_remaining_len(_Bin, _Header, _Multiplier, Length, + #{max_packet_size := MaxSize}) + when Length > MaxSize -> + error(mqtt_frame_too_large); parse_remaining_len(<<>>, Header, Multiplier, Length, Options) -> {more, fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length, Options) end}; -%% Optimize: match PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK... -parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0, Options) -> - parse_frame(Rest, Header, 2, Options); -%% optimize: match PINGREQ... +%% Match PINGREQ. parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, Options) -> parse_frame(Rest, Header, 0, Options); +%% Match PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK... +parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0, Options) -> + parse_frame(Rest, Header, 2, Options); parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value, Options) -> parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier, Options); parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value, - Options = #{max_len := MaxLen}) -> + Options = #{max_packet_size:= MaxSize}) -> FrameLen = Value + Len * Multiplier, if - FrameLen > MaxLen -> error(mqtt_frame_too_long); + FrameLen > MaxSize -> error(mqtt_frame_too_large); true -> parse_frame(Rest, Header, FrameLen, Options) end. @@ -105,6 +113,13 @@ parse_frame(Bin, Header, Length, Options) -> end} end. +wrap(Header, Variable, Payload, Rest) -> + {ok, #mqtt_packet{header = Header, variable = Variable, payload = Payload}, Rest}. +wrap(Header, Variable, Rest) -> + {ok, #mqtt_packet{header = Header, variable = Variable}, Rest}. +wrap(Header, Rest) -> + {ok, #mqtt_packet{header = Header}, Rest}. + parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) -> {ProtoName, Rest} = parse_utf8_string(FrameBin), <> = Rest, @@ -117,7 +132,7 @@ parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) -> _Reserved : 1, KeepAlive : 16/big, Rest2/binary>> = Rest1, - case protocol_name_approved(ProtoVer, ProtoName) of + case protocol_approved(ProtoVer, ProtoName) of true -> ok; false -> error(protocol_name_unapproved) end, @@ -212,16 +227,6 @@ parse_packet(#mqtt_packet_header{type = ?AUTH}, <>, {Properties, <<>>} = parse_properties(Rest, ?MQTT_PROTO_V5), #mqtt_packet_auth{reason_code = ReasonCode, properties = Properties}. -wrap(Header, Variable, Payload, Rest) -> - {ok, #mqtt_packet{header = Header, variable = Variable, payload = Payload}, Rest}. -wrap(Header, Variable, Rest) -> - {ok, #mqtt_packet{header = Header, variable = Variable}, Rest}. -wrap(Header, Rest) -> - {ok, #mqtt_packet{header = Header}, Rest}. - -protocol_name_approved(Ver, Name) -> - lists:member({Ver, Name}, ?PROTOCOL_NAMES). - parse_will_message(Packet = #mqtt_packet_connect{will_flag = true, proto_ver = Ver}, Bin) -> {Props, Rest} = parse_properties(Bin, Ver), @@ -233,6 +238,9 @@ parse_will_message(Packet = #mqtt_packet_connect{will_flag = true, parse_will_message(Packet, Bin) -> {Packet, Bin}. +protocol_approved(Ver, Name) -> + lists:member({Ver, Name}, ?PROTOCOL_NAMES). + parse_packet_id(<>) -> {PacketId, Rest}. @@ -333,8 +341,9 @@ parse_topic_filters(unsubscribe, Bin) -> parse_reason_codes(Bin) -> [Code || <> <= Bin]. -parse_utf8_pair(Bin) -> - [{Name, Value} || <> <= Bin]. +parse_utf8_pair(<>) -> + {{Key, Val}, Rest}. parse_utf8_string(Bin, false) -> {undefined, Bin}; @@ -347,10 +356,285 @@ parse_utf8_string(<>) -> parse_binary_data(<>) -> {Data, Rest}. +%%-------------------------------------------------------------------- +%% Serialize MQTT Packet +%%-------------------------------------------------------------------- + +-spec(serialize(mqtt_packet()) -> iodata()). +serialize(Packet) -> + serialize(Packet, ?DEFAULT_OPTIONS). + +-spec(serialize(mqtt_packet(), options()) -> 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)). + +serialize(#mqtt_packet_header{type = Type, + dup = Dup, + qos = QoS, + retain = Retain}, VariableBin, PayloadBin) + when ?CONNECT =< Type andalso Type =< ?AUTH -> + Len = iolist_size(VariableBin) + iolist_size(PayloadBin), + true = (Len =< ?MAX_PACKET_SIZE), + [<>, + serialize_remaining_len(Len), VariableBin, PayloadBin]. + +serialize_variable(#mqtt_packet_connect{ + proto_name = ProtoName, + proto_ver = ProtoVer, + is_bridge = IsBridge, + clean_start = CleanStart, + will_flag = WillFlag, + will_qos = WillQos, + will_retain = WillRetain, + keepalive = KeepAlive, + properties = Properties, + client_id = ClientId, + will_props = WillProps, + will_topic = WillTopic, + will_payload = WillPayload, + username = Username, + password = Password}, _Options) -> + [serialize_binary_data(ProtoName), + <<(case IsBridge of + true -> 16#80 + ProtoVer; + false -> ProtoVer + end):8, + (flag(Username)):1, + (flag(Password)):1, + (flag(WillRetain)):1, + WillQos:2, + (flag(WillFlag)):1, + (flag(CleanStart)):1, + 0:1, + KeepAlive:16/big-unsigned-integer>>, + serialize_properties(Properties, ProtoVer), + serialize_utf8_string(ClientId), + case WillFlag of + true -> [serialize_properties(WillProps, ProtoVer), + serialize_utf8_string(WillTopic), + serialize_binary_data(WillPayload)]; + false -> <<>> + end, + serialize_utf8_string(Username, true), + serialize_utf8_string(Password, true)]; + +serialize_variable(#mqtt_packet_connack{ack_flags = AckFlags, + reason_code = ReasonCode, + properties = Properties}, + #{version := Ver}) -> + [AckFlags, ReasonCode, serialize_properties(Properties, Ver)]; + +serialize_variable(#mqtt_packet_publish{topic_name = TopicName, + packet_id = PacketId, + properties = Properties}, + #{version := Ver}) -> + [serialize_utf8_string(TopicName), + if + PacketId =:= undefined -> <<>>; + true -> <> + end, + serialize_properties(Properties, Ver)]; + +serialize_variable(#mqtt_packet_puback{packet_id = PacketId}, + #{version := 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}) -> + [<>, ReasonCode, + serialize_properties(Properties, ?MQTT_PROTO_V5)]; + +serialize_variable(#mqtt_packet_subscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}, + #{version := 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}) -> + [<>, serialize_properties(Properties, Ver), + serialize_reason_codes(ReasonCodes)]; + +serialize_variable(#mqtt_packet_unsubscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}, + #{version := 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}) -> + [<>, serialize_properties(Properties, Ver), + serialize_reason_codes(ReasonCodes)]; + +serialize_variable(#mqtt_packet_disconnect{}, #{version := 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}) -> + [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}) -> + [ReasonCode, serialize_properties(Properties, Ver)]; + +serialize_variable(PacketId, ?MQTT_PROTO_V3) when is_integer(PacketId) -> + <>; +serialize_variable(PacketId, ?MQTT_PROTO_V4) when is_integer(PacketId) -> + <>; +serialize_variable(undefined, _Ver) -> + <<>>. + +serialize_payload(undefined) -> <<>>; +serialize_payload(Bin) -> Bin. + +serialize_properties(_Props, Ver) when Ver =/= ?MQTT_PROTO_V5 -> + <<>>; +serialize_properties(Props, ?MQTT_PROTO_V5) -> + serialize_properties(Props). + +serialize_properties(undefined) -> + <<0>>; +serialize_properties(Props) when map_size(Props) == 0 -> + <<0>>; +serialize_properties(Props) when is_map(Props) -> + Bin = << <<(serialize_property(Prop, Val))/binary>> || {Prop, Val} <- maps:to_list(Props) >>, + [serialize_variable_byte_integer(byte_size(Bin)), Bin]. + +serialize_property(_, undefined) -> + <<>>; +serialize_property('Payload-Format-Indicator', Val) -> + <<16#01, Val>>; +serialize_property('Message-Expiry-Interval', Val) -> + <<16#02, Val:32/big>>; +serialize_property('Content-Type', Val) -> + <<16#03, (serialize_utf8_string(Val))/binary>>; +serialize_property('Response-Topic', Val) -> + <<16#08, (serialize_utf8_string(Val))/binary>>; +serialize_property('Correlation-Data', Val) -> + <<16#09, (byte_size(Val)):16, Val/binary>>; +serialize_property('Subscription-Identifier', Val) -> + <<16#0B, (serialize_variable_byte_integer(Val))/binary>>; +serialize_property('Session-Expiry-Interval', Val) -> + <<16#11, Val:32/big>>; +serialize_property('Assigned-Client-Identifier', Val) -> + <<16#12, (serialize_utf8_string(Val))/binary>>; +serialize_property('Server-Keep-Alive', Val) -> + <<16#13, Val:16/big>>; +serialize_property('Authentication-Method', Val) -> + <<16#15, (serialize_utf8_string(Val))/binary>>; +serialize_property('Authentication-Data', Val) -> + <<16#16, (iolist_size(Val)):16, Val/binary>>; +serialize_property('Request-Problem-Information', Val) -> + <<16#17, Val>>; +serialize_property('Will-Delay-Interval', Val) -> + <<16#18, Val:32/big>>; +serialize_property('Request-Response-Information', Val) -> + <<16#19, Val>>; +serialize_property('Response-Information', Val) -> + <<16#1A, (serialize_utf8_string(Val))/binary>>; +serialize_property('Server-Reference', Val) -> + <<16#1C, (serialize_utf8_string(Val))/binary>>; +serialize_property('Reason-String', Val) -> + <<16#1F, (serialize_utf8_string(Val))/binary>>; +serialize_property('Receive-Maximum', Val) -> + <<16#21, Val:16/big>>; +serialize_property('Topic-Alias-Maximum', Val) -> + <<16#22, Val:16/big>>; +serialize_property('Topic-Alias', Val) -> + <<16#23, Val:16/big>>; +serialize_property('Maximum-QoS', Val) -> + <<16#24, Val>>; +serialize_property('Retain-Available', Val) -> + <<16#25, Val>>; +serialize_property('User-Property', {Key, Val}) -> + <<16#26, (serialize_utf8_pair({Key, Val}))/binary>>; +serialize_property('User-Property', Props) when is_list(Props) -> + << <<(serialize_property('User-Property', {Key, Val}))/binary>> + || {Key, Val} <- Props >>; +serialize_property('Maximum-Packet-Size', Val) -> + <<16#27, Val:32/big>>; +serialize_property('Wildcard-Subscription-Available', Val) -> + <<16#28, Val>>; +serialize_property('Subscription-Identifier-Available', Val) -> + <<16#29, Val>>; +serialize_property('Shared-Subscription-Available', Val) -> + <<16#2A, Val>>. + +serialize_topic_filters(subscribe, TopicFilters, ?MQTT_PROTO_V5) -> + << <<(serialize_utf8_string(Topic))/binary, (serialize_subopts(SubOpts)) >> + || {Topic, SubOpts} <- TopicFilters >>; + +serialize_topic_filters(subscribe, TopicFilters, _Ver) -> + << <<(serialize_utf8_string(Topic))/binary, ?RESERVED:6, QoS:2>> + || {Topic, #mqtt_subopts{qos = QoS}} <- TopicFilters >>; + +serialize_topic_filters(unsubscribe, TopicFilters, _Ver) -> + << <<(serialize_utf8_string(Topic))/binary>> || Topic <- TopicFilters >>. + +serialize_subopts(#mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}) -> + <>. + +serialize_reason_codes(undefined) -> + <<>>; +serialize_reason_codes(ReasonCodes) when is_list(ReasonCodes) -> + << <> || Code <- ReasonCodes >>. + +serialize_utf8_pair({Name, Value}) -> + << (serialize_utf8_string(Name))/binary, (serialize_utf8_string(Value))/binary >>. + +serialize_binary_data(Bin) -> + [<<(byte_size(Bin)):16/big-unsigned-integer>>, Bin]. + +serialize_utf8_string(undefined, false) -> + error(utf8_string_undefined); +serialize_utf8_string(undefined, true) -> + <<>>; +serialize_utf8_string(String, _AllowNull) -> + serialize_utf8_string(String). + +serialize_utf8_string(String) -> + StringBin = unicode:characters_to_binary(String), + Len = byte_size(StringBin), + true = (Len =< 16#ffff), + <>. + +serialize_remaining_len(I) -> + serialize_variable_byte_integer(I). + +serialize_variable_byte_integer(N) when N =< ?LOWBITS -> + <<0:1, N:7>>; +serialize_variable_byte_integer(N) -> + <<1:1, (N rem ?HIGHBIT):7, (serialize_variable_byte_integer(N div ?HIGHBIT))/binary>>. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + bool(0) -> false; bool(1) -> true. -%% Fix Issue#575 +flag(undefined) -> ?RESERVED; +flag(false) -> 0; +flag(true) -> 1; +flag(X) when is_integer(X) -> X; +flag(B) when is_binary(B) -> 1. + fixqos(?PUBREL, 0) -> 1; fixqos(?SUBSCRIBE, 0) -> 1; fixqos(?UNSUBSCRIBE, 0) -> 1; diff --git a/src/emqx_serializer.erl b/src/emqx_serializer.erl deleted file mode 100644 index 670213674..000000000 --- a/src/emqx_serializer.erl +++ /dev/null @@ -1,294 +0,0 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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_serializer). - --include("emqx.hrl"). - --include("emqx_mqtt.hrl"). - --type(option() :: {version, mqtt_version()}). - --export_type([option/0]). - --export([serialize/1, serialize/2]). - --spec(serialize(mqtt_packet()) -> iodata()). -serialize(Packet) -> serialize(Packet, []). - --spec(serialize(mqtt_packet(), [option()]) -> iodata()). -serialize(#mqtt_packet{header = Header, - variable = Variable, - payload = Payload}, Opts) when is_list(Opts) -> - Opts1 = parse_opt(Opts, #{version => ?MQTT_PROTO_V4}), - serialize(Header, serialize_variable(Variable, Opts1), serialize_payload(Payload)). - -parse_opt([], Map) -> - Map; -parse_opt([{version, Ver}|Opts], Map) -> - parse_opt(Opts, Map#{version := Ver}); -parse_opt([_|Opts], Map) -> - parse_opt(Opts, Map). - -serialize(#mqtt_packet_header{type = Type, - dup = Dup, - qos = Qos, - retain = Retain}, VariableData, PayloadData) - when ?CONNECT =< Type andalso Type =< ?AUTH -> - Len = iolist_size(VariableData) + iolist_size(PayloadData), - true = (Len =< ?MAX_PACKET_SIZE), - [<>, - serialize_remaining_len(Len), VariableData, PayloadData]. - -serialize_variable(#mqtt_packet_connect{proto_name = ProtoName, - proto_ver = ProtoVer, - is_bridge = IsBridge, - clean_start = CleanStart, - will_flag = WillFlag, - will_qos = WillQos, - will_retain = WillRetain, - keepalive = KeepAlive, - properties = Properties, - client_id = ClientId, - will_props = WillProps, - will_topic = WillTopic, - will_payload = WillPayload, - username = Username, - password = Password}, _Opts) -> - [serialize_binary_data(ProtoName), - <<(case IsBridge of - true -> 16#80 + ProtoVer; - false -> ProtoVer - end):8, - (opt(Username)):1, - (opt(Password)):1, - (opt(WillRetain)):1, - WillQos:2, - (opt(WillFlag)):1, - (opt(CleanStart)):1, - 0:1, - KeepAlive:16/big-unsigned-integer>>, - serialize_properties(Properties, ProtoVer), - serialize_utf8_string(ClientId), - case WillFlag of - true -> [serialize_properties(WillProps, ProtoVer), - serialize_utf8_string(WillTopic), - serialize_binary_data(WillPayload)]; - false -> <<>> - end, - serialize_utf8_string(Username, true), - serialize_utf8_string(Password, true)]; - -serialize_variable(#mqtt_packet_connack{ack_flags = AckFlags, - reason_code = ReasonCode, - properties = Properties}, #{version := Ver}) -> - [AckFlags, ReasonCode, serialize_properties(Properties, Ver)]; - -serialize_variable(#mqtt_packet_publish{topic_name = TopicName, - packet_id = PacketId, - properties = Properties}, #{version := Ver}) -> - [serialize_utf8_string(TopicName), - if - PacketId =:= undefined -> <<>>; - true -> <> - end, - serialize_properties(Properties, Ver)]; - -serialize_variable(#mqtt_packet_puback{packet_id = PacketId}, #{version := 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}) -> - [<>, ReasonCode, - serialize_properties(Properties, ?MQTT_PROTO_V5)]; - -serialize_variable(#mqtt_packet_subscribe{packet_id = PacketId, - properties = Properties, - topic_filters = TopicFilters}, - #{version := 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}) -> - [<>, serialize_properties(Properties, Ver), - << <> || Code <- ReasonCodes >>]; - -serialize_variable(#mqtt_packet_unsubscribe{packet_id = PacketId, - properties = Properties, - topic_filters = TopicFilters}, - #{version := 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}) -> - [<>, serialize_properties(Properties, Ver), - << <> || Code <- ReasonCodes >>]; - -serialize_variable(#mqtt_packet_disconnect{}, #{version := 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}) -> - [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}) -> - [ReasonCode, serialize_properties(Properties, Ver)]; - -serialize_variable(PacketId, ?MQTT_PROTO_V3) when is_integer(PacketId) -> - <>; -serialize_variable(PacketId, ?MQTT_PROTO_V4) when is_integer(PacketId) -> - <>; -serialize_variable(undefined, _Ver) -> - <<>>. - -serialize_payload(undefined) -> - <<>>; -serialize_payload(Bin) when is_binary(Bin); is_list(Bin) -> - Bin. - -serialize_properties(_Props, Ver) when Ver =/= ?MQTT_PROTO_V5 -> - <<>>; -serialize_properties(Props, ?MQTT_PROTO_V5) -> - serialize_properties(Props). - -serialize_properties(undefined) -> - <<0>>; -serialize_properties(Props) when map_size(Props) == 0 -> - <<0>>; -serialize_properties(Props) when is_map(Props) -> - Bin = << <<(serialize_property(Prop, Val))/binary>> || {Prop, Val} <- maps:to_list(Props) >>, - [serialize_variable_byte_integer(byte_size(Bin)), Bin]. - -%% Ignore undefined -serialize_property(_, undefined) -> - <<>>; -serialize_property('Payload-Format-Indicator', Val) -> - <<16#01, Val>>; -serialize_property('Message-Expiry-Interval', Val) -> - <<16#02, Val:32/big>>; -serialize_property('Content-Type', Val) -> - <<16#03, (serialize_utf8_string(Val))/binary>>; -serialize_property('Response-Topic', Val) -> - <<16#08, (serialize_utf8_string(Val))/binary>>; -serialize_property('Correlation-Data', Val) -> - <<16#09, (byte_size(Val)):16, Val/binary>>; -serialize_property('Subscription-Identifier', Val) -> - <<16#0B, (serialize_variable_byte_integer(Val))/binary>>; -serialize_property('Session-Expiry-Interval', Val) -> - <<16#11, Val:32/big>>; -serialize_property('Assigned-Client-Identifier', Val) -> - <<16#12, (serialize_utf8_string(Val))/binary>>; -serialize_property('Server-Keep-Alive', Val) -> - <<16#13, Val:16/big>>; -serialize_property('Authentication-Method', Val) -> - <<16#15, (serialize_utf8_string(Val))/binary>>; -serialize_property('Authentication-Data', Val) -> - <<16#16, (iolist_size(Val)):16, Val/binary>>; -serialize_property('Request-Problem-Information', Val) -> - <<16#17, Val>>; -serialize_property('Will-Delay-Interval', Val) -> - <<16#18, Val:32/big>>; -serialize_property('Request-Response-Information', Val) -> - <<16#19, Val>>; -serialize_property('Response-Information', Val) -> - <<16#1A, (serialize_utf8_string(Val))/binary>>; -serialize_property('Server-Reference', Val) -> - <<16#1C, (serialize_utf8_string(Val))/binary>>; -serialize_property('Reason-String', Val) -> - <<16#1F, (serialize_utf8_string(Val))/binary>>; -serialize_property('Receive-Maximum', Val) -> - <<16#21, Val:16/big>>; -serialize_property('Topic-Alias-Maximum', Val) -> - <<16#22, Val:16/big>>; -serialize_property('Topic-Alias', Val) -> - <<16#23, Val:16/big>>; -serialize_property('Maximum-QoS', Val) -> - <<16#24, Val>>; -serialize_property('Retain-Available', Val) -> - <<16#25, Val>>; -serialize_property('User-Property', {Key, Val}) -> - <<16#26, (serialize_utf8_pair({Key, Val}))/binary>>; -serialize_property('User-Property', Props) when is_list(Props) -> - << <<(serialize_property('User-Property', {Key, Val}))/binary>> - || {Key, Val} <- Props >>; -serialize_property('Maximum-Packet-Size', Val) -> - <<16#27, Val:32/big>>; -serialize_property('Wildcard-Subscription-Available', Val) -> - <<16#28, Val>>; -serialize_property('Subscription-Identifier-Available', Val) -> - <<16#29, Val>>; -serialize_property('Shared-Subscription-Available', Val) -> - <<16#2A, Val>>. - -serialize_topic_filters(subscribe, TopicFilters, ?MQTT_PROTO_V5) -> - << <<(serialize_utf8_string(Topic))/binary, ?RESERVED:2, Rh:2, (opt(Rap)):1, (opt(Nl)):1, Qos:2>> - || {Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = Qos}} <- TopicFilters >>; - -serialize_topic_filters(subscribe, TopicFilters, _Ver) -> - << <<(serialize_utf8_string(Topic))/binary, ?RESERVED:6, Qos:2>> - || {Topic, #mqtt_subopts{qos = Qos}} <- TopicFilters >>; - -serialize_topic_filters(unsubscribe, TopicFilters, _Ver) -> - << <<(serialize_utf8_string(Topic))/binary>> || Topic <- TopicFilters >>. - -serialize_utf8_pair({Name, Value}) -> - << <<(serialize_utf8_string(S))/binary, - (serialize_utf8_string(S))/binary>> || S <- [Name, Value] >>. - -serialize_binary_data(Bin) -> - [<<(byte_size(Bin)):16/big-unsigned-integer>>, Bin]. - -serialize_utf8_string(undefined, false) -> - error(utf8_string_undefined); -serialize_utf8_string(undefined, true) -> - <<>>; -serialize_utf8_string(String, _AllowNull) -> - serialize_utf8_string(String). - -serialize_utf8_string(String) -> - StringBin = unicode:characters_to_binary(String), - Len = byte_size(StringBin), - true = (Len =< 16#ffff), - <>. - -serialize_remaining_len(I) -> - serialize_variable_byte_integer(I). - -serialize_variable_byte_integer(N) when N =< ?LOWBITS -> - <<0:1, N:7>>; -serialize_variable_byte_integer(N) -> - <<1:1, (N rem ?HIGHBIT):7, (serialize_variable_byte_integer(N div ?HIGHBIT))/binary>>. - -opt(undefined) -> ?RESERVED; -opt(false) -> 0; -opt(true) -> 1; -opt(X) when is_integer(X) -> X; -opt(B) when is_binary(B) -> 1. - diff --git a/test/emqx_frame_SUITE.erl b/test/emqx_frame_SUITE.erl new file mode 100644 index 000000000..c177fda8e --- /dev/null +++ b/test/emqx_frame_SUITE.erl @@ -0,0 +1,430 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_frame_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx_mqtt.hrl"). + +-include_lib("eunit/include/eunit.hrl"). + +-import(emqx_frame, [serialize/1, serialize/2]). + +all() -> + [{group, connect}, + {group, connack}, + {group, publish}, + {group, puback}, + {group, subscribe}, + {group, suback}, + {group, unsubscribe}, + {group, unsuback}, + {group, ping}, + {group, disconnect}, + {group, auth}]. + +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]}, + {connack, [parallel], + [serialize_parse_connack, + serialize_parse_connack_v5]}, + {publish, [parallel], + [serialize_parse_qos0_publish, + serialize_parse_qos1_publish, + serialize_parse_qos2_publish, + 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]}, + {subscribe, [parallel], + [serialize_parse_subscribe, + serialize_parse_subscribe_v5]}, + {suback, [parallel], + [serialize_parse_suback, + serialize_parse_suback_v5]}, + {unsubscribe, [parallel], + [serialize_parse_unsubscribe, + serialize_parse_unsubscribe_v5]}, + {unsuback, [parallel], + [serialize_parse_unsuback, + serialize_parse_unsuback_v5]}, + {ping, [parallel], + [serialize_parse_pingreq, + serialize_parse_pingresp]}, + {disconnect, [parallel], + [serialize_parse_disconnect, + serialize_parse_disconnect_v5]}, + {auth, [parallel], + [serialize_parse_auth_v5]}]. + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +init_per_group(_Group, Config) -> + Config. + +end_per_group(_Group, _Config) -> + ok. + +serialize_parse_connect(_) -> + Packet1 = ?CONNECT_PACKET(#mqtt_packet_connect{}), + ?assertEqual({ok, Packet1, <<>>}, parse_serialize(Packet1)), + Packet2 = ?CONNECT_PACKET(#mqtt_packet_connect{ + client_id = <<"clientId">>, + will_qos = ?QOS1, + will_flag = true, + will_retain = true, + will_topic = <<"will">>, + will_payload = <<"bye">>, + clean_start = true}), + ?assertEqual({ok, Packet2, <<>>}, parse_serialize(Packet2)). + +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>>, + Packet = ?CONNECT_PACKET( + #mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V3, + proto_name = <<"MQIsdp">>, + client_id = <<"mosqpub/10451-iMac.loca">>, + clean_start = true, + keepalive = 60}), + ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + +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, + proto_name = <<"MQTT">>, + client_id = <<"mosqpub/10451-iMac.loca">>, + clean_start = true, + keepalive = 60}), + ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), + ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + +serialize_parse_v5_connect(_) -> + Props = #{'Session-Expiry-Interval' => 60, + 'Receive-Maximum' => 100, + 'Maximum-QoS' => ?QOS_2, + 'Retain-Available' => 1, + 'Maximum-Packet-Size' => 1024, + 'Topic-Alias-Maximum' => 10, + 'Request-Response-Information' => 1, + 'Request-Problem-Information' => 1, + 'Authentication-Method' => <<"oauth2">>, + 'Authentication-Data' => <<"33kx93k">>}, + + WillProps = #{'Will-Delay-Interval' => 60, + 'Payload-Format-Indicator' => 1, + 'Message-Expiry-Interval' => 60, + 'Content-Type' => <<"text/json">>, + 'Response-Topic' => <<"topic">>, + 'Correlation-Data' => <<"correlateid">>, + 'User-Property' => [{<<"k">>, <<"v">>}]}, + Packet = ?CONNECT_PACKET( + #mqtt_packet_connect{proto_name = <<"MQTT">>, + proto_ver = ?MQTT_PROTO_V5, + is_bridge = false, + clean_start = true, + client_id = <<>>, + will_flag = true, + will_qos = ?QOS_1, + will_retain = false, + keepalive = 60, + properties = Props, + will_props = WillProps, + will_topic = <<"topic">>, + will_payload = <<>>, + username = <<"device:1">>, + password = <<"passwd">>}), + ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + +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, + proto_name = <<"MQTT">>, + client_id = <<>>, + clean_start = true, + keepalive = 60}), + ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), + ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + +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, + 112,117,98,108,105,99>>, + Packet = #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, + variable = #mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V3, + proto_name = <<"MQIsdp">>, + client_id = <<"mosqpub/10452-iMac.loca">>, + clean_start = true, + keepalive = 60, + will_retain = false, + will_qos = ?QOS_1, + will_flag = true, + will_topic = <<"/will">>, + will_payload = <<"willmsg">>, + username = <<"test">>, + password = <<"public">>}}, + ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), + ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + +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, + 67,58,50,57,58,50,66,58,55,55,58,53,50,47,115,116,97,116,101,0,1,48>>, + Topic = <<"$SYS/broker/connection/C_00:0C:29:2B:77:52/state">>, + Packet = #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, + variable = #mqtt_packet_connect{client_id = <<"C_00:0C:29:2B:77:52">>, + proto_ver = 16#03, + proto_name = <<"MQIsdp">>, + is_bridge = true, + will_retain = true, + will_qos = ?QOS_1, + will_flag = true, + clean_start = false, + keepalive = 60, + will_topic = Topic, + will_payload = <<"0">>}}, + ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), + ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + +serialize_parse_connack(_) -> + Packet = ?CONNACK_PACKET(?RC_SUCCESS), + ?assertEqual(<<32,2,0,0>>, iolist_to_binary(serialize(Packet))), + ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + +serialize_parse_connack_v5(_) -> + Props = #{'Session-Expiry-Interval' => 60, + 'Receive-Maximum' => 100, + 'Maximum-QoS' => ?QOS_2, + 'Retain-Available' => 1, + 'Maximum-Packet-Size' => 1024, + 'Assigned-Client-Identifier' => <<"id">>, + 'Topic-Alias-Maximum' => 10, + 'Reason-String' => <<>>, + 'Wildcard-Subscription-Available' => 1, + 'Subscription-Identifier-Available' => 1, + 'Shared-Subscription-Available' => 1, + 'Server-Keep-Alive' => 60, + 'Response-Information' => <<"response">>, + 'Server-Reference' => <<"192.168.1.10">>, + 'Authentication-Method' => <<"oauth2">>, + 'Authentication-Data' => <<"33kx93k">>}, + Packet = ?CONNACK_PACKET(?RC_SUCCESS, 0, Props), + ?assertEqual({ok, Packet, <<>>}, + parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +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, + qos = ?QOS_0, + retain = false}, + variable = #mqtt_packet_publish{topic_name = <<"xxx/yyy">>, + packet_id = undefined}, + payload = <<"hello">>}, + ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), + ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + +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, + qos = ?QOS_1, + retain = false}, + variable = #mqtt_packet_publish{topic_name = <<"a/b/c">>, + packet_id = 1}, + payload = <<"haha">>}, + ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), + ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + +serialize_parse_qos2_publish(_) -> + Packet = ?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 1, payload()), + ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + +serialize_parse_publish_v5(_) -> + Props = #{'Payload-Format-Indicator' => 1, + 'Message-Expiry-Interval' => 60, + 'Topic-Alias' => 16#AB, + 'Response-Topic' => <<"reply">>, + 'Correlation-Data' => <<"correlation-id">>, + 'Subscription-Identifier' => 1, + 'Content-Type' => <<"text/json">>}, + Packet = ?PUBLISH_PACKET(#mqtt_packet_header{type = ?PUBLISH}, + <<"$share/group/topic">>, 1, Props, + <<"payload">>), + ?assertEqual({ok, Packet, <<>>}, + parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_puback(_) -> + Packet = ?PUBACK_PACKET(1), + ?assertEqual(<<64,2,0,1>>, iolist_to_binary(serialize(Packet))), + ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + +serialize_parse_puback_v5(_) -> + Packet = ?PUBACK_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), + ?assertEqual({ok, Packet, <<>>}, + parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_pubrec(_) -> + Packet = ?PUBREC_PACKET(1), + ?assertEqual(<<5:4,0:4,2,0,1>>, iolist_to_binary(serialize(Packet))), + ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + +serialize_parse_pubrec_v5(_) -> + Packet = ?PUBREC_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), + ?assertEqual({ok, Packet, <<>>}, + parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_pubrel(_) -> + Packet = ?PUBREL_PACKET(1), + ?assertEqual(<<6:4,2:4,2,0,1>>, iolist_to_binary(serialize(Packet))), + ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + +serialize_parse_pubrel_v5(_) -> + Packet = ?PUBREL_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), + ?assertEqual({ok, Packet, <<>>}, + parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_pubcomp(_) -> + Packet = ?PUBCOMP_PACKET(1), + ?assertEqual(<<7:4,0:4,2,0,1>>, iolist_to_binary(serialize(Packet))), + ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + +serialize_parse_pubcomp_v5(_) -> + Packet = ?PUBCOMP_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), + ?assertEqual({ok, Packet, <<>>}, + parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +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>>, + TopicFilters = [{<<"TopicA">>, #mqtt_subopts{qos = 2}}], + Packet = ?SUBSCRIBE_PACKET(2, TopicFilters), + ?assertEqual(Bin, serialize(Packet)), + ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + +serialize_parse_subscribe_v5(_) -> + TopicFilters = [{<<"TopicQos0">>, #mqtt_subopts{rh = 1, qos = ?QOS_0}}, + {<<"TopicQos1">>, #mqtt_subopts{rh = 1, qos =?QOS_1}}], + Packet = ?SUBSCRIBE_PACKET(1, #{'Subscription-Identifier' => 16#FFFFFFF}, + TopicFilters), + ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + +serialize_parse_suback(_) -> + Packet = ?SUBACK_PACKET(10, [?QOS_0, ?QOS_1, 128]), + ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + +serialize_parse_suback_v5(_) -> + Packet = ?SUBACK_PACKET(1, #{'Reason-String' => <<"success">>, + 'User-Property' => [{<<"key">>, <<"value">>}]}, + [?QOS_0, ?QOS_1, 128]), + ?assertEqual({ok, Packet, <<>>}, + parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + + +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, iolist_to_binary(serialize(Packet))), + ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + +serialize_parse_unsubscribe_v5(_) -> + Props = #{'User-Property' => [{<<"key">>, <<"val">>}]}, + Packet = ?UNSUBSCRIBE_PACKET(10, Props, [<<"Topic1">>, <<"Topic2">>]), + ?assertEqual({ok, Packet, <<>>}, + parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_unsuback(_) -> + Packet = ?UNSUBACK_PACKET(10), + ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + +serialize_parse_unsuback_v5(_) -> + Packet = ?UNSUBACK_PACKET(10, #{'Reason-String' => <<"Not authorized">>, + 'User-Property' => [{<<"key">>, <<"val">>}]}, + [16#87, 16#87, 16#87]), + ?assertEqual({ok, Packet, <<>>}, + parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_pingreq(_) -> + PingReq = ?PACKET(?PINGREQ), + ?assertEqual({ok, PingReq, <<>>}, parse_serialize(PingReq)). + +serialize_parse_pingresp(_) -> + PingResp = ?PACKET(?PINGRESP), + ?assertEqual({ok, PingResp, <<>>}, parse_serialize(PingResp)). + +parse_disconnect(_) -> + ?assertEqual({ok, ?DISCONNECT_PACKET(?RC_SUCCESS), <<>>}, parse(<<224, 0>>)). + +serialize_parse_disconnect(_) -> + Packet = ?PACKET(?DISCONNECT), + ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + +serialize_parse_disconnect_v5(_) -> + Packet = ?DISCONNECT_PACKET(?RC_SUCCESS, + #{'Session-Expiry-Interval' => 60, + 'Reason-String' => <<"server_moved">>, + 'Server-Reference' => <<"192.168.1.10">>}), + ?assertEqual({ok, Packet, <<>>}, + parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_auth_v5(_) -> + Packet = ?AUTH_PACKET(?RC_SUCCESS, + #{'Authentication-Method' => <<"oauth2">>, + 'Authentication-Data' => <<"3zekkd">>, + 'Reason-String' => <<"success">>, + 'User-Property' => [{<<"key">>, <<"val">>}]}), + ?assertEqual({ok, Packet, <<>>}, + parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +parse_serialize(Packet) -> + parse(iolist_to_binary(serialize(Packet))). + +parse_serialize(Packet, Opts) when is_map(Opts) -> + parse(iolist_to_binary(serialize(Packet, Opts)), Opts). + +parse(Bin) -> + parse(Bin, #{}). + +parse(Bin, Opts) when is_map(Opts) -> + emqx_frame:parse(Bin, emqx_frame:initial_state(Opts)). + +payload() -> + iolist_to_binary(["payload." || _I <- lists:seq(1, 1000)]). + From bf253ab9b3c3720872af7916ad081540012ba1ac Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 10 May 2018 22:03:59 +0800 Subject: [PATCH 054/520] Support batch delete --- src/emqx_router.erl | 105 +++++++++++++++++++++++++-------- test/emqx_serializer_SUITE.erl | 91 ---------------------------- 2 files changed, 80 insertions(+), 116 deletions(-) delete mode 100644 test/emqx_serializer_SUITE.erl diff --git a/src/emqx_router.erl b/src/emqx_router.erl index c79482bd2..56f94eb08 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -30,7 +30,9 @@ -export([start_link/2]). %% Route APIs --export([add_route/2, add_route/3, get_routes/1, del_route/2, del_route/3]). +-export([add_route/1, add_route/2, add_route/3]). +-export([get_routes/1]). +-export([del_route/1, del_route/2, del_route/3]). -export([has_routes/1, match_routes/1, print_routes/1]). %% Topics @@ -42,10 +44,17 @@ -type(destination() :: node() | {binary(), node()}). --record(state, {pool, id}). +-record(batch, {enabled, timer, pending}). + +-record(state, {pool, id, batch :: #batch{}}). -define(ROUTE, emqx_route). +-define(BATCH(Enabled), #batch{enabled = Enabled}). + +-define(BATCH(Enabled, Pending), + #batch{enabled = Enabled, pending = Pending}). + %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- @@ -68,34 +77,45 @@ mnesia(copy) -> -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, - ?MODULE, [Pool, Id], [{hibernate_after, 10000}]). + ?MODULE, [Pool, Id], [{hibernate_after, 2000}]). %%-------------------------------------------------------------------- %% Route APIs %%-------------------------------------------------------------------- -%% @doc Add a route +-spec(add_route(topic() | route()) -> ok). +add_route(Topic) when is_binary(Topic) -> + add_route(#route{topic = Topic, dest = node()}); +add_route(Route = #route{topic = Topic}) -> + cast(pick(Topic), {add_route, Route}). + -spec(add_route(topic(), destination()) -> ok). add_route(Topic, Dest) when is_binary(Topic) -> - cast(pick(Topic), {add_route, #route{topic = Topic, dest = Dest}}). + add_route(#route{topic = Topic, dest = Dest}). -spec(add_route({pid(), reference()}, topic(), destination()) -> ok). add_route(From, Topic, Dest) when is_binary(Topic) -> - cast(pick(Topic), {add_route, From, #route{topic = Topic, dest = Dest}}). + Route = #route{topic = Topic, dest = Dest}, + cast(pick(Topic), {add_route, From, Route}). -%% @doc Get routes -spec(get_routes(topic()) -> [route()]). get_routes(Topic) -> ets:lookup(?ROUTE, Topic). -%% @doc Delete a route +-spec(del_route(topic() | route()) -> ok). +del_route(Topic) when is_binary(Topic) -> + del_route(#route{topic = Topic, dest = node()}); +del_route(Route = #route{topic = Topic}) -> + cast(pick(Topic), {del_route, Route}). + -spec(del_route(topic(), destination()) -> ok). del_route(Topic, Dest) when is_binary(Topic) -> - cast(pick(Topic), {del_route, #route{topic = Topic, dest = Dest}}). + del_route(#route{topic = Topic, dest = Dest}). -spec(del_route({pid(), reference()}, topic(), destination()) -> ok). del_route(From, Topic, Dest) when is_binary(Topic) -> - cast(pick(Topic), {del_route, From, #route{topic = Topic, dest = Dest}}). + Route = #route{topic = Topic, dest = Dest}, + cast(pick(Topic), {del_route, From, Route}). %% @doc Has routes? -spec(has_routes(topic()) -> boolean()). @@ -131,17 +151,20 @@ pick(Topic) -> %%-------------------------------------------------------------------- init([Pool, Id]) -> + rand:seed(exsplus, erlang:timestamp()), gproc_pool:connect_worker(Pool, {Pool, Id}), - {ok, #state{pool = Pool, id = Id}}. + Batch = #batch{enabled = emqx_config:get_env(route_batch_delete, false), + pending = sets:new()}, + {ok, ensure_batch_timer(#state{pool = Pool, id = Id, batch = Batch})}. handle_call(Req, _From, State) -> emqx_logger:error("[Router] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast({add_route, From, Route}, State) -> - _ = handle_cast({add_route, Route}, State), + {noreply, NewState} = handle_cast({add_route, Route}, State), gen_server:reply(From, ok), - {noreply, State}; + {noreply, NewState}; handle_cast({add_route, Route = #route{topic = Topic, dest = Dest}}, State) -> case lists:member(Route, get_routes(Topic)) of @@ -156,31 +179,36 @@ handle_cast({add_route, Route = #route{topic = Topic, dest = Dest}}, State) -> {noreply, State}; handle_cast({del_route, From, Route}, State) -> - _ = handle_cast({del_route, Route}, State), + {noreply, NewState} = handle_cast({del_route, Route}, State), gen_server:reply(From, ok), - {noreply, State}; + {noreply, NewState}; handle_cast({del_route, Route = #route{topic = Topic}}, State) -> %% Confirm if there are still subscribers... - case ets:member(emqx_subscriber, Topic) of - true -> ok; - false -> - case emqx_topic:wildcard(Topic) of - true -> log(trans(fun del_trie_route/1, [Route])); - false -> del_direct_route(Route) - end - end, - {noreply, State}; + {noreply, case ets:member(emqx_subscriber, Topic) of + true -> State; + false -> + case emqx_topic:wildcard(Topic) of + true -> log(trans(fun del_trie_route/1, [Route])), + State; + false -> del_direct_route(Route, State) + end + end}; handle_cast(Msg, State) -> emqx_logger:error("[Router] Unexpected msg: ~p", [Msg]), {noreply, State}. +handle_info({timeout, _TRef, batch_delete}, State = #state{batch = Batch}) -> + _ = del_direct_routes(Batch#batch.pending), + {noreply, ensure_batch_timer(State#state{batch = ?BATCH(true, sets:new())}), hibernate}; + handle_info(Info, State) -> emqx_logger:error("[Router] Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{pool = Pool, id = Id}) -> +terminate(_Reason, #state{pool = Pool, id = Id, batch = Batch}) -> + _ = cacel_batch_timer(Batch), gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> @@ -190,6 +218,17 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- +ensure_batch_timer(State = #state{batch = #batch{enabled = false}}) -> + State; +ensure_batch_timer(State = #state{batch = Batch}) -> + TRef = erlang:start_timer(50 + rand:uniform(50), self(), batch_delete), + State#state{batch = Batch#batch{timer = TRef}}. + +cacel_batch_timer(#batch{enabled = false}) -> + ok; +cacel_batch_timer(#batch{enabled = true, timer = TRef}) -> + erlang:cancel_timer(TRef). + add_direct_route(Route) -> mnesia:async_dirty(fun mnesia:write/3, [?ROUTE, Route, sticky_write]). @@ -200,9 +239,25 @@ add_trie_route(Route = #route{topic = Topic}) -> end, mnesia:write(?ROUTE, Route, sticky_write). +del_direct_route(Route, State = #state{batch = ?BATCH(false)}) -> + del_direct_route(Route), State; +del_direct_route(Route, State = #state{batch = Batch = ?BATCH(true, Pending)}) -> + State#state{batch = Batch#batch{pending = sets:add_element(Route, Pending)}}. + del_direct_route(Route) -> mnesia:async_dirty(fun mnesia:delete_object/3, [?ROUTE, Route, sticky_write]). +del_direct_routes([]) -> + ok; +del_direct_routes(Routes) -> + DelFun = fun(R = #route{topic = Topic}) -> + case ets:member(emqx_subscriber, Topic) of + true -> ok; + false -> mnesia:delete_object(?ROUTE, R, sticky_write) + end + end, + mnesia:async_dirty(fun lists:foreach/2, [DelFun, Routes]). + del_trie_route(Route = #route{topic = Topic}) -> case mnesia:wread({?ROUTE, Topic}) of [Route] -> %% Remove route and trie diff --git a/test/emqx_serializer_SUITE.erl b/test/emqx_serializer_SUITE.erl deleted file mode 100644 index d0a34fd20..000000000 --- a/test/emqx_serializer_SUITE.erl +++ /dev/null @@ -1,91 +0,0 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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_serializer_SUITE). - --compile(export_all). - --include("emqx_mqtt.hrl"). - --include_lib("eunit/include/eunit.hrl"). - --import(emqx_serializer, [serialize/1]). - -all() -> - [serialize_connect, - serialize_connack, - serialize_publish, - serialize_puback, - serialize_pubrel, - serialize_subscribe, - serialize_suback, - serialize_unsubscribe, - serialize_unsuback, - serialize_pingreq, - serialize_pingresp, - serialize_disconnect]. - -serialize_connect(_) -> - serialize(?CONNECT_PACKET(#mqtt_packet_connect{})), - serialize(?CONNECT_PACKET(#mqtt_packet_connect{ - client_id = <<"clientId">>, - will_qos = ?QOS1, - will_flag = true, - will_retain = true, - will_topic = <<"will">>, - will_payload = <<"haha">>, - clean_sess = true})). - -serialize_connack(_) -> - ConnAck = #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, - variable = #mqtt_packet_connack{ack_flags = 0, return_code = 0}}, - ?assertEqual(<<32,2,0,0>>, iolist_to_binary(serialize(ConnAck))). - -serialize_publish(_) -> - serialize(?PUBLISH_PACKET(?QOS_0, <<"Topic">>, undefined, <<"Payload">>)), - serialize(?PUBLISH_PACKET(?QOS_1, <<"Topic">>, 938, <<"Payload">>)), - serialize(?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 99, long_payload())). - -serialize_puback(_) -> - serialize(?PUBACK_PACKET(?PUBACK, 10384)). - -serialize_pubrel(_) -> - serialize(?PUBREL_PACKET(10384)). - -serialize_subscribe(_) -> - TopicTable = [{<<"TopicQos0">>, ?QOS_0}, {<<"TopicQos1">>, ?QOS_1}, {<<"TopicQos2">>, ?QOS_2}], - serialize(?SUBSCRIBE_PACKET(10, TopicTable)). - -serialize_suback(_) -> - serialize(?SUBACK_PACKET(10, [?QOS_0, ?QOS_1, 128])). - -serialize_unsubscribe(_) -> - serialize(?UNSUBSCRIBE_PACKET(10, [<<"Topic1">>, <<"Topic2">>])). - -serialize_unsuback(_) -> - serialize(?UNSUBACK_PACKET(10)). - -serialize_pingreq(_) -> - serialize(?PACKET(?PINGREQ)). - -serialize_pingresp(_) -> - serialize(?PACKET(?PINGRESP)). - -serialize_disconnect(_) -> - serialize(?PACKET(?DISCONNECT)). - -long_payload() -> - iolist_to_binary(["payload." || _I <- lists:seq(1, 100)]). From bffdd2ba7441e6462919ec70244474d7f9188e9f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 22 May 2018 13:01:19 +0800 Subject: [PATCH 055/520] Upgrade esockd and add more test cases --- include/emqx_mqtt.hrl | 33 +-- src/emqx_access_control.erl | 2 +- src/emqx_broker.erl | 37 ++-- src/emqx_broker_sup.erl | 21 +- src/emqx_client.erl | 145 +++++++------ src/emqx_config.erl | 8 +- src/emqx_connection.erl | 93 ++++---- src/emqx_ctl.erl | 15 +- src/emqx_frame.erl | 10 +- src/emqx_inflight.erl | 56 ++--- src/emqx_protocol.erl | 37 ++-- src/emqx_router.erl | 2 - src/emqx_router_helper.erl | 5 +- src/emqx_session.erl | 70 +++--- src/emqx_sys_sup.erl | 2 +- src/emqx_ws_connection.erl | 64 +++--- test/emqx_SUITE.erl | 1 + test/emqx_SUITE_data/loaded_plugins | 0 test/emqx_access_SUITE.erl | 1 + test/emqx_base62_SUITE.erl | 1 + test/emqx_broker_SUITE.erl | 8 +- test/emqx_client_SUITE.erl | 41 ++++ test/emqx_ct_broker_helpers.erl | 39 ++-- test/emqx_ct_helpers.erl | 24 +++ test/emqx_frame_SUITE.erl | 9 +- test/emqx_guid_SUITE.erl | 1 + test/emqx_inflight_SUITE.erl | 75 +++---- test/emqx_lib_SUITE.erl | 1 + test/emqx_misc_SUITE.erl | 1 + test/emqx_mod_SUITE.erl | 3 +- test/emqx_mqtt_compat_SUITE.erl | 220 +++++++++++++++++++ test/emqx_mqueue_SUITE.erl | 2 + test/emqx_net_SUITE.erl | 1 + test/emqx_pqueue_SUITE.erl | 2 + test/emqx_protocol_SUITE.erl | 320 ---------------------------- test/emqx_protocol_SUITE.erl.bk | 147 +++++++++++++ test/emqx_router_SUITE.erl | 168 +++++---------- test/emqx_time_SUITE.erl | 1 + test/emqx_topic_SUITE.erl | 1 + test/emqx_trie_SUITE.erl | 43 ++-- test/emqx_vm_SUITE.erl | 1 + 41 files changed, 892 insertions(+), 819 deletions(-) create mode 100644 test/emqx_SUITE_data/loaded_plugins create mode 100644 test/emqx_client_SUITE.erl create mode 100644 test/emqx_ct_helpers.erl create mode 100644 test/emqx_mqtt_compat_SUITE.erl delete mode 100644 test/emqx_protocol_SUITE.erl create mode 100644 test/emqx_protocol_SUITE.erl.bk diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 793c9d501..4dae631f4 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -243,7 +243,7 @@ is_bridge = false :: boolean(), clean_start = true :: boolean(), will_flag = false :: boolean(), - will_qos = ?QOS_1 :: mqtt_qos(), + will_qos = ?QOS_0 :: mqtt_qos(), will_retain = false :: boolean(), keepalive = 0 :: non_neg_integer(), properties = undefined :: mqtt_properties(), @@ -339,7 +339,8 @@ -define(CONNACK_PACKET(ReasonCode), #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, - variable = #mqtt_packet_connack{reason_code = ReasonCode}}). + variable = #mqtt_packet_connack{ack_flags = 0, + reason_code = ReasonCode}}). -define(CONNACK_PACKET(ReasonCode, SessPresent), #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, @@ -348,9 +349,9 @@ -define(CONNACK_PACKET(ReasonCode, SessPresent, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, - variable = #mqtt_packet_connack{ack_flags = SessPresent, + variable = #mqtt_packet_connack{ack_flags = SessPresent, reason_code = ReasonCode, - properties = Properties}}). + properties = Properties}}). -define(AUTH_PACKET(), #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, @@ -370,15 +371,16 @@ qos = Qos}, variable = #mqtt_packet_publish{packet_id = PacketId}}). --define(PUBLISH_PACKET(Qos, Topic, PacketId, Payload), +-define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - qos = Qos}, + qos = QoS}, variable = #mqtt_packet_publish{topic_name = Topic, packet_id = PacketId}, payload = Payload}). --define(PUBLISH_PACKET(Header, Topic, PacketId, Properties, Payload), - #mqtt_packet{header = Header = #mqtt_packet_header{type = ?PUBLISH}, +-define(PUBLISH_PACKET(QoS, Topic, PacketId, Properties, Payload), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = QoS}, variable = #mqtt_packet_publish{topic_name = Topic, packet_id = PacketId, properties = Properties}, @@ -386,7 +388,8 @@ -define(PUBACK_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, - variable = #mqtt_packet_puback{packet_id = PacketId}}). + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = 0}}). -define(PUBACK_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, @@ -396,7 +399,8 @@ -define(PUBREC_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, - variable = #mqtt_packet_puback{packet_id = PacketId}}). + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = 0}}). -define(PUBREC_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, @@ -406,7 +410,8 @@ -define(PUBREL_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, - variable = #mqtt_packet_puback{packet_id = PacketId}}). + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = 0}}). -define(PUBREL_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, @@ -416,7 +421,8 @@ -define(PUBCOMP_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, - variable = #mqtt_packet_puback{packet_id = PacketId}}). + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = 0}}). -define(PUBCOMP_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, @@ -467,7 +473,8 @@ reason_codes = ReasonCodes}}). -define(DISCONNECT_PACKET(), - #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}}). + #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, + variable = #mqtt_packet_disconnect{reason_code = 0}}). -define(DISCONNECT_PACKET(ReasonCode), #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 85713b7cc..aa51c0df0 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -114,7 +114,7 @@ tab_key(acl) -> acl_modules. %% @doc Stop access control server. stop() -> - gen_server:call(?MODULE, stop). + gen_server:stop(?MODULE, normal, infinity). %%-------------------------------------------------------------------- %% gen_server callbacks diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 57db2defe..aaacb086d 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -22,16 +22,11 @@ -export([start_link/2]). --export([subscribe/1, subscribe/2, subscribe/3, unsubscribe/1, unsubscribe/2]). - +-export([subscribe/1, subscribe/2, subscribe/3, subscribe/4]). -export([publish/1, publish/2]). - +-export([unsubscribe/1, unsubscribe/2]). -export([dispatch/2, dispatch/3]). - -export([subscriptions/1, subscribers/1, subscribed/2]). - --export([topics/0]). - -export([get_subopts/2, set_subopts/3]). %% gen_server Function Exports @@ -41,7 +36,6 @@ -record(state, {pool, id, submon}). -define(BROKER, ?MODULE). - -define(TIMEOUT, 120000). %% ETS tables @@ -104,7 +98,7 @@ unsubscribe(Topic, Subscriber, Timeout) -> -spec(publish(message()) -> delivery() | stopped). publish(Msg = #message{from = From}) -> %% Hook to trace? - trace(public, From, Msg), + trace(publish, From, Msg), case emqx_hooks:run('message.publish', [], Msg) of {ok, Msg1 = #message{topic = Topic}} -> publish(Topic, Msg1); @@ -226,15 +220,20 @@ subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), is_pid(SubPid) -> ets:member(?SUBOPTION, {Topic, {SubId, SubPid}}). -topics() -> emqx_router:topics(). - +-spec(get_subopts(topic(), subscriber()) -> [suboption()]). get_subopts(Topic, Subscriber) when is_binary(Topic) -> try ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2) catch error:badarg -> [] end. +-spec(set_subopts(topic(), subscriber(), [suboption()]) -> boolean()). set_subopts(Topic, Subscriber, Opts) when is_binary(Topic), is_list(Opts) -> - gen_server:call(pick(Subscriber), {set_subopts, Topic, Subscriber, Opts}). + case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of + [{_, OldOpts}] -> + Opts1 = lists:usort(lists:umerge(Opts, OldOpts)), + ets:insert(?SUBOPTION, {{Topic, Subscriber}, Opts1}); + [] -> false + end. with_subpid(SubPid) when is_pid(SubPid) -> SubPid; @@ -267,18 +266,8 @@ init([Pool, Id]) -> gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #state{pool = Pool, id = Id, submon = emqx_pmon:new()}}. -handle_call({set_subopts, Topic, Subscriber, Opts}, _From, State) -> - case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of - [{_, OldOpts}] -> - Opts1 = lists:usort(lists:umerge(Opts, OldOpts)), - ets:insert(?SUBOPTION, {{Topic, Subscriber}, Opts1}), - {reply, ok, State}; - [] -> - {reply, {error, not_found}, State} - end; - -handle_call(Request, _From, State) -> - emqx_logger:error("[Broker] Unexpected request: ~p", [Request]), +handle_call(Req, _From, State) -> + emqx_logger:error("[Broker] Unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast({From, {subscribe, Topic, Subscriber, Options}}, State) -> diff --git a/src/emqx_broker_sup.erl b/src/emqx_broker_sup.erl index c16b5c63d..034d87711 100644 --- a/src/emqx_broker_sup.erl +++ b/src/emqx_broker_sup.erl @@ -22,11 +22,7 @@ -export([init/1]). --import(lists, [foreach/2]). - --define(TAB_OPTS, [public, - {read_concurrency, true}, - {write_concurrency, true}]). +-define(TAB_OPTS, [public, {read_concurrency, true}, {write_concurrency, true}]). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). @@ -37,9 +33,9 @@ start_link() -> init([]) -> %% Create the pubsub tables - foreach(fun create_tab/1, [subscription, subscriber, suboption]), + lists:foreach(fun create_tab/1, [subscription, subscriber, suboption]), - %% Shared subscription + %% Shared Subscription SharedSub = {shared_sub, {emqx_shared_sub, start_link, []}, permanent, 5000, worker, [emqx_shared_sub]}, @@ -47,13 +43,12 @@ init([]) -> Helper = {broker_helper, {emqx_broker_helper, start_link, []}, permanent, 5000, worker, [emqx_broker_helper]}, - %% Broker pool - PoolArgs = [broker, hash, emqx_vm:schedulers() * 2, - {emqx_broker, start_link, []}], + %% Broker Pool + BrokerPool = emqx_pool_sup:spec(emqx_broker_pool, + [broker, hash, emqx_vm:schedulers() * 2, + {emqx_broker, start_link, []}]), - PoolSup = emqx_pool_sup:spec(eqmx_broker_pool, PoolArgs), - - {ok, {{one_for_all, 0, 1}, [SharedSub, Helper, PoolSup]}}. + {ok, {{one_for_all, 0, 1}, [SharedSub, Helper, BrokerPool]}}. %%-------------------------------------------------------------------- %% Create tables diff --git a/src/emqx_client.erl b/src/emqx_client.erl index ef640fe43..277d7ecd8 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -32,7 +32,9 @@ -export([pubrel/2, pubrel/3, pubrel/4]). -export([pubcomp/2, pubcomp/3, pubcomp/4]). -export([subscriptions/1]). --export([info/1]). +-export([info/1, stop/1]). +%% For test cases +-export([pause/1, resume/1]). -export([initialized/3, waiting_for_connack/3, connected/3]). -export([init/1, callback_mode/0, handle_event/4, terminate/3, code_change/4]). @@ -60,12 +62,12 @@ | {will_topic, iodata()} | {will_payload, iodata()} | {will_retain, boolean()} - | {will_qos, mqtt_qos() | mqtt_qos_name()} - | {will_props, mqtt_properties()} + | {will_qos, qos()} + | {will_props, properties()} | {auto_ack, boolean()} | {ack_timeout, pos_integer()} | {force_ping, boolean()} - | {properties, mqtt_properties()}). + | {properties, properties()}). -export_type([host/0, option/0]). @@ -80,18 +82,17 @@ bridge_mode :: boolean(), client_id :: binary(), clean_start :: boolean(), - session_present :: boolean(), username :: binary() | undefined, password :: binary() | undefined, proto_ver :: mqtt_version(), proto_name :: iodata(), keepalive :: non_neg_integer(), keepalive_timer :: reference() | undefined, - expiry_interval :: pos_integer(), force_ping :: boolean(), + paused :: boolean(), will_flag :: boolean(), will_msg :: mqtt_message(), - properties :: mqtt_properties(), + properties :: properties(), pending_calls :: list(), subscriptions :: map(), max_inflight :: infinity | pos_integer(), @@ -102,8 +103,9 @@ ack_timer :: reference(), retry_interval :: pos_integer(), retry_timer :: reference(), - last_packet_id :: mqtt_packet_id(), - parse_state :: emqx_parser:state()}). + session_present :: boolean(), + last_packet_id :: packet_id(), + parse_state :: emqx_frame:state()}). -record(call, {id, from, req, ts}). @@ -254,7 +256,6 @@ publish(Client, Topic, Payload, QoS) when is_binary(Topic), ?IS_QOS(QoS) -> publish(Client, Topic, Payload, Opts) when is_binary(Topic), is_list(Opts) -> publish(Client, Topic, #{}, Payload, Opts). -%% MQTT Version 5.0 -spec(publish(client(), topic(), properties(), payload(), [pubopt()]) -> ok | {ok, packet_id()} | {error, term()}). publish(Client, Topic, Properties, Payload, Opts) @@ -262,9 +263,9 @@ publish(Client, Topic, Properties, Payload, Opts) ok = emqx_mqtt_properties:validate(Properties), Retain = proplists:get_bool(retain, Opts), QoS = ?QOS_I(proplists:get_value(qos, Opts, ?QOS_0)), - publish(Client, #mqtt_message{topic = Topic, - qos = QoS, + publish(Client, #mqtt_message{qos = QoS, retain = Retain, + topic = Topic, properties = Properties, payload = iolist_to_binary(Payload)}). @@ -279,7 +280,6 @@ unsubscribe(Client, Topic) when is_binary(Topic) -> unsubscribe(Client, Topics) when is_list(Topics) -> unsubscribe(Client, #{}, Topics). -%% MQTT Version 5.0 -spec(unsubscribe(client(), properties(), topic() | [topic()]) -> subscribe_ret()). unsubscribe(Client, Properties, Topic) when is_map(Properties), is_binary(Topic) -> unsubscribe(Client, Properties, [Topic]); @@ -303,47 +303,43 @@ disconnect(Client, ReasonCode, Properties) -> gen_statem:call(Client, {disconnect, ReasonCode, Properties}). %%-------------------------------------------------------------------- -%% For test cases. +%% For test cases %%-------------------------------------------------------------------- puback(Client, PacketId) when is_integer(PacketId) -> puback(Client, PacketId, ?RC_SUCCESS). -puback(Client, PacketId, ReasonCode) when is_integer(PacketId), - is_integer(ReasonCode) -> +puback(Client, PacketId, ReasonCode) + when is_integer(PacketId), is_integer(ReasonCode) -> puback(Client, PacketId, ReasonCode, #{}). -puback(Client, PacketId, ReasonCode, Properties) when is_integer(PacketId), - is_integer(ReasonCode), - is_map(Properties) -> +puback(Client, PacketId, ReasonCode, Properties) + when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> gen_statem:cast(Client, {puback, PacketId, ReasonCode, Properties}). pubrec(Client, PacketId) when is_integer(PacketId) -> pubrec(Client, PacketId, ?RC_SUCCESS). -pubrec(Client, PacketId, ReasonCode) when is_integer(PacketId), - is_integer(ReasonCode) -> +pubrec(Client, PacketId, ReasonCode) + when is_integer(PacketId), is_integer(ReasonCode) -> pubrec(Client, PacketId, ReasonCode, #{}). -pubrec(Client, PacketId, ReasonCode, Properties) when is_integer(PacketId), - is_integer(ReasonCode), - is_map(Properties) -> +pubrec(Client, PacketId, ReasonCode, Properties) + when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> gen_statem:cast(Client, {pubrec, PacketId, ReasonCode, Properties}). pubrel(Client, PacketId) when is_integer(PacketId) -> pubrel(Client, PacketId, ?RC_SUCCESS). -pubrel(Client, PacketId, ReasonCode) when is_integer(PacketId), - is_integer(ReasonCode) -> +pubrel(Client, PacketId, ReasonCode) + when is_integer(PacketId), is_integer(ReasonCode) -> pubrel(Client, PacketId, ReasonCode, #{}). -pubrel(Client, PacketId, ReasonCode, Properties) when is_integer(PacketId), - is_integer(ReasonCode), - is_map(Properties) -> +pubrel(Client, PacketId, ReasonCode, Properties) + when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> gen_statem:cast(Client, {pubrel, PacketId, ReasonCode, Properties}). pubcomp(Client, PacketId) when is_integer(PacketId) -> pubcomp(Client, PacketId, ?RC_SUCCESS). -pubcomp(Client, PacketId, ReasonCode) when is_integer(PacketId), - is_integer(ReasonCode) -> +pubcomp(Client, PacketId, ReasonCode) + when is_integer(PacketId), is_integer(ReasonCode) -> pubcomp(Client, PacketId, ReasonCode, #{}). -pubcomp(Client, PacketId, ReasonCode, Properties) when is_integer(PacketId), - is_integer(ReasonCode), - is_map(Properties) -> +pubcomp(Client, PacketId, ReasonCode, Properties) + when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> gen_statem:cast(Client, {pubcomp, PacketId, ReasonCode, Properties}). subscriptions(Client) -> @@ -352,6 +348,15 @@ subscriptions(Client) -> info(Client) -> gen_statem:call(Client, info). +stop(Client) -> + gen_statem:call(Client, stop). + +pause(Client) -> + gen_statem:call(Client, pause). + +resume(Client) -> + gen_statem:call(Client, resume). + %%-------------------------------------------------------------------- %% gen_statem callbacks %%-------------------------------------------------------------------- @@ -375,6 +380,7 @@ init([Options]) -> proto_name = <<"MQTT">>, keepalive = ?DEFAULT_KEEPALIVE, force_ping = false, + paused = false, will_flag = false, will_msg = #mqtt_message{}, pending_calls = [], @@ -496,8 +502,8 @@ init_will_msg({qos, QoS}, WillMsg) -> init_parse_state(State = #state{proto_ver = Ver, properties = Properties}) -> Size = maps:get('Maximum-Packet-Size', Properties, ?MAX_PACKET_SIZE), - State#state{parse_state = emqx_parser:initial_state([{max_len, Size}, - {version, Ver}])}. + State#state{parse_state = emqx_frame:initial_state( + #{max_packet_size => Size, version => Ver})}. callback_mode() -> state_functions. @@ -519,17 +525,17 @@ initialized({call, From}, connect, State = #state{sock_opts = SockOpts, initialized(EventType, EventContent, State) -> handle_event(EventType, EventContent, initialized, State). -mqtt_connect(State = #state{client_id = ClientId, - clean_start = CleanStart, - bridge_mode = IsBridge, - username = Username, - password = Password, - proto_ver = ProtoVer, - proto_name = ProtoName, - keepalive = KeepAlive, - will_flag = WillFlag, - will_msg = WillMsg, - properties = Properties}) -> +mqtt_connect(State = #state{client_id = ClientId, + clean_start = CleanStart, + bridge_mode = IsBridge, + username = Username, + password = Password, + proto_ver = ProtoVer, + proto_name = ProtoName, + keepalive = KeepAlive, + will_flag = WillFlag, + will_msg = WillMsg, + properties = Properties}) -> ?WILL_MSG(WillQos, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, ConnProps = emqx_mqtt_properties:filter(?CONNECT, maps:to_list(Properties)), send(?CONNECT_PACKET( @@ -597,6 +603,15 @@ connected({call, From}, info, State) -> Info = lists:zip(record_info(fields, state), tl(tuple_to_list(State))), {keep_state, State, [{reply, From, Info}]}; +connected({call, From}, pause, State) -> + {keep_state, State#state{paused = true}, [{reply, From, ok}]}; + +connected({call, From}, resume, State) -> + {keep_state, State#state{paused = false}, [{reply, From, ok}]}; + +connected({call, From}, stop, _State) -> + {stop_and_reply, normal, [{reply, From, ok}]}; + connected({call, From}, SubReq = {subscribe, Properties, Topics}, State = #state{last_packet_id = PacketId, subscriptions = Subscriptions}) -> case send(?SUBSCRIBE_PACKET(PacketId, Properties, Topics), State) of @@ -622,14 +637,14 @@ connected({call, From}, {publish, Msg = #mqtt_message{qos = ?QOS_0}}, State) -> connected({call, From}, {publish, Msg = #mqtt_message{qos = Qos}}, State = #state{inflight = Inflight, last_packet_id = PacketId}) when (Qos =:= ?QOS_1); (Qos =:= ?QOS_2) -> - case Inflight:is_full() of + case emqx_inflight:is_full(Inflight) of true -> {keep_state, State, [{reply, From, {error, inflight_full}}]}; false -> Msg1 = Msg#mqtt_message{packet_id = PacketId}, case send(Msg1, State) of {ok, NewState} -> - Inflight1 = Inflight:insert(PacketId, {publish, Msg1, os:timestamp()}), + Inflight1 = emqx_inflight:insert(PacketId, {publish, Msg1, os:timestamp()}, Inflight), {keep_state, ensure_retry_timer(NewState#state{inflight = Inflight1}), [{reply, From, {ok, PacketId}}]}; Error = {error, Reason} -> @@ -679,8 +694,12 @@ connected(cast, {pubcomp, PacketId, ReasonCode, Properties}, State) -> connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), State) -> {keep_state, deliver_msg(packet_to_msg(Packet), State)}; +connected(cast, ?PUBLISH_PACKET(_QoS, _PacketId), State = #state{paused = true}) -> + {keep_state, State}; + connected(cast, Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), State = #state{auto_ack = AutoAck}) -> + _ = deliver_msg(packet_to_msg(Packet), State), case AutoAck of true -> send_puback(?PUBACK_PACKET(PacketId), State); @@ -698,12 +717,12 @@ connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), connected(cast, ?PUBACK_PACKET(PacketId, ReasonCode, Properties), State = #state{owner = Owner, inflight = Inflight}) -> - case Inflight:lookup(PacketId) of + case emqx_inflight:lookup(PacketId, Inflight) of {value, {publish, #mqtt_message{packet_id = PacketId}, _Ts}} -> Owner ! {puback, #{packet_id => PacketId, reason_code => ReasonCode, properties => Properties}}, - {keep_state, State#state{inflight = Inflight:delete(PacketId)}}; + {keep_state, State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}}; none -> emqx_logger:warning("Unexpected PUBACK: ~p", [PacketId]), {keep_state, State} @@ -711,9 +730,9 @@ connected(cast, ?PUBACK_PACKET(PacketId, ReasonCode, Properties), connected(cast, ?PUBREC_PACKET(PacketId), State = #state{inflight = Inflight}) -> send_puback(?PUBREL_PACKET(PacketId), - case Inflight:lookup(PacketId) of + case emqx_inflight:lookup(PacketId, Inflight) of {value, {publish, _Msg, _Ts}} -> - Inflight1 = Inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}), + Inflight1 = emqx_inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight), State#state{inflight = Inflight1}; {value, {pubrel, _Ref, _Ts}} -> emqx_logger:warning("Duplicated PUBREC Packet: ~p", [PacketId]), @@ -741,12 +760,12 @@ connected(cast, ?PUBREL_PACKET(PacketId), connected(cast, ?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), State = #state{owner = Owner, inflight = Inflight}) -> - case Inflight:lookup(PacketId) of + case emqx_inflight:lookup(PacketId, Inflight) of {value, {pubrel, _PacketId, _Ts}} -> Owner ! {puback, #{packet_id => PacketId, reason_code => ReasonCode, properties => Properties}}, - {keep_state, State#state{inflight = Inflight:delete(PacketId)}}; + {keep_state, State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}}; none -> emqx_logger:warning("Unexpected PUBCOMP Packet: ~p", [PacketId]), {keep_state, State} @@ -797,8 +816,8 @@ connected(info, {timeout, _TRef, keepalive}, State = #state{force_ping = true}) end; connected(info, {timeout, TRef, keepalive}, - State = #state{socket = Sock, keepalive_timer = TRef}) -> - case should_ping(Sock) of + State = #state{socket = Sock, paused = Paused, keepalive_timer = TRef}) -> + case (not Paused) andalso should_ping(Sock) of true -> case send(?PACKET(?PINGREQ), State) of {ok, NewState} -> @@ -820,7 +839,7 @@ connected(info, {timeout, TRef, ack}, State = #state{ack_timer = TRef, connected(info, {timeout, TRef, retry}, State = #state{retry_timer = TRef, inflight = Inflight}) -> - case Inflight:is_empty() of + case emqx_inflight:is_empty(Inflight) of true -> {keep_state, State#state{retry_timer = undefined}}; false -> retry_send(State) end; @@ -928,7 +947,7 @@ ensure_retry_timer(_Interval, State) -> retry_send(State = #state{inflight = Inflight}) -> SortFun = fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end, - Msgs = lists:sort(SortFun, Inflight:values()), + Msgs = lists:sort(SortFun, emqx_inflight:values(Inflight)), retry_send(Msgs, os:timestamp(), State ). retry_send([], _Now, State) -> @@ -948,7 +967,7 @@ retry_send(publish, Msg = #mqtt_message{qos = QoS, packet_id = PacketId}, Msg1 = Msg#mqtt_message{dup = (QoS =:= ?QOS1)}, case send(Msg1, State) of {ok, NewState} -> - Inflight1 = Inflight:update(PacketId, {publish, Msg1, Now}), + Inflight1 = emqx_inflight:update(PacketId, {publish, Msg1, Now}, Inflight), {ok, NewState#state{inflight = Inflight1}}; Error = {error, _Reason} -> Error @@ -956,7 +975,7 @@ retry_send(publish, Msg = #mqtt_message{qos = QoS, packet_id = PacketId}, retry_send(pubrel, PacketId, Now, State = #state{inflight = Inflight}) -> case send(?PUBREL_PACKET(PacketId), State) of {ok, NewState} -> - Inflight1 = Inflight:update(PacketId, {pubrel, PacketId, Now}), + Inflight1 = emqx_inflight:update(PacketId, {pubrel, PacketId, Now}, Inflight), {ok, NewState#state{inflight = Inflight1}}; Error = {error, _Reason} -> Error @@ -1028,7 +1047,7 @@ send(Msg, State) when is_record(Msg, mqtt_message) -> send(Packet, State = #state{socket = Sock, proto_ver = Ver}) when is_record(Packet, mqtt_packet) -> - Data = emqx_serializer:serialize(Packet, [{version, Ver}]), + Data = emqx_frame:serialize(Packet, #{version => Ver}), emqx_logger:debug("SEND Data: ~p", [Data]), case emqx_client_sock:send(Sock, Data) of ok -> {ok, next_packet_id(State)}; @@ -1045,7 +1064,7 @@ receive_loop(<<>>, State) -> {keep_state, State}; receive_loop(Bytes, State = #state{parse_state = ParseState}) -> - case catch emqx_parser:parse(Bytes, ParseState) of + case catch emqx_frame:parse(Bytes, ParseState) of {ok, Packet, Rest} -> ok = gen_statem:cast(self(), Packet), receive_loop(Rest, init_parse_state(State)); diff --git a/src/emqx_config.erl b/src/emqx_config.erl index a1ea69394..ef1d8ec9f 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -17,7 +17,7 @@ %% @doc Hot Configuration %% %% TODO: How to persist the configuration? -%% +%% %% 1. Store in mnesia database? %% 2. Store in dets? %% 3. Store in data/app.config? @@ -27,6 +27,8 @@ -export([get_env/1, get_env/2]). +-export([populate/1]). + -export([read/1, write/2, dump/2, reload/1, get/2, get/3, set/3]). -type(env() :: {atom(), term()}). @@ -42,6 +44,10 @@ get_env(Key, Default) -> get_env(Key) -> application:get_env(?APP, Key). +%% TODO: +populate(_App) -> + ok. + %% @doc Read the configuration of an application. -spec(read(atom()) -> {ok, list(env())} | {error, term()}). read(App) -> diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 930424d38..fe0518072 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -27,7 +27,7 @@ -import(proplists, [get_value/2, get_value/3]). %% API Function Exports --export([start_link/2]). +-export([start_link/3]). %% Management and Monitor API -export([info/1, stats/1, kick/1, clean_acl_cache/2]). @@ -44,11 +44,8 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]). -%% TODO: How to emit stats? --export([handle_pre_hibernate/1]). - %% Unused fields: connname, peerhost, peerport --record(state, {connection, peername, conn_state, await_recv, +-record(state, {transport, socket, peername, conn_state, await_recv, rate_limit, max_packet_size, proto_state, parse_state, keepalive, enable_stats, idle_timeout, force_gc_count}). @@ -60,8 +57,8 @@ emqx_logger:Level("Client(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). -start_link(Conn, Env) -> - {ok, proc_lib:spawn_link(?MODULE, init, [[Conn, Env]])}. +start_link(Transport, Sock, Env) -> + {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock, Env]])}. info(CPid) -> gen_server:call(CPid, info). @@ -72,11 +69,11 @@ stats(CPid) -> kick(CPid) -> gen_server:call(CPid, kick). -set_rate_limit(Cpid, Rl) -> - gen_server:call(Cpid, {set_rate_limit, Rl}). +set_rate_limit(CPid, Rl) -> + gen_server:call(CPid, {set_rate_limit, Rl}). -get_rate_limit(Cpid) -> - gen_server:call(Cpid, get_rate_limit). +get_rate_limit(CPid) -> + gen_server:call(CPid, get_rate_limit). subscribe(CPid, TopicTable) -> CPid ! {subscribe, TopicTable}. @@ -94,26 +91,25 @@ clean_acl_cache(CPid, Topic) -> %% gen_server Callbacks %%-------------------------------------------------------------------- -init([Conn0, Env]) -> - {ok, Conn} = Conn0:wait(), - case Conn:peername() of - {ok, Peername} -> do_init(Conn, Env, Peername); - {error, enotconn} -> Conn:fast_close(), - exit(normal); - {error, Reason} -> Conn:fast_close(), - exit({shutdown, Reason}) +init([Transport, Sock, Env]) -> + case Transport:wait(Sock) of + {ok, NewSock} -> + {ok, Peername} = Transport:ensure_ok_or_exit(peername, [NewSock]), + do_init(Transport, Sock, Peername, Env); + {error, Reason} -> + {stop, Reason} end. -do_init(Conn, Env, Peername) -> - %% Send Fun - SendFun = send_fun(Conn, Peername), - RateLimit = get_value(rate_limit, Conn:opts()), +do_init(Transport, Sock, Peername, Env) -> + RateLimit = get_value(rate_limit, Env), PacketSize = get_value(max_packet_size, Env, ?MAX_PACKET_SIZE), - ProtoState = emqx_protocol:init(Conn, Peername, SendFun, Env), + SendFun = send_fun(Transport, Sock, Peername), + ProtoState = emqx_protocol:init(Transport, Sock, Peername, SendFun, Env), EnableStats = get_value(client_enable_stats, Env, false), IdleTimout = get_value(client_idle_timeout, Env, 30000), ForceGcCount = emqx_gc:conn_max_gc_count(), - State = run_socket(#state{connection = Conn, + State = run_socket(#state{transport = Transport, + socket = Sock, peername = Peername, await_recv = false, conn_state = running, @@ -123,18 +119,17 @@ do_init(Conn, Env, Peername) -> enable_stats = EnableStats, idle_timeout = IdleTimout, force_gc_count = ForceGcCount}), - gen_server:enter_loop(?MODULE, [{hibernate_after, 10000}], + gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], init_parse_state(State), self(), IdleTimout). -send_fun(Conn, Peername) -> +send_fun(Transport, Sock, Peername) -> Self = self(), fun(Packet) -> - Data = emqx_serializer:serialize(Packet), + Data = emqx_frame:serialize(Packet), ?LOG(debug, "SEND ~p", [Data], #state{peername = Peername}), emqx_metrics:inc('bytes/sent', iolist_size(Data)), - try Conn:async_send(Data) of + try Transport:async_send(Sock, Data) of ok -> ok; - true -> ok; %% Compatible with esockd 4.x {error, Reason} -> Self ! {shutdown, Reason} catch error:Error -> Self ! {shutdown, Error} @@ -142,12 +137,9 @@ send_fun(Conn, Peername) -> end. init_parse_state(State = #state{max_packet_size = Size, proto_state = ProtoState}) -> - emqx_parser:initial_state([{max_len, Size}, - {ver, emqx_protocol:get(proto_ver, ProtoState)}]), - State. - -handle_pre_hibernate(State) -> - {hibernate, emqx_gc:reset_conn_gc_count(#state.force_gc_count, emit_stats(State))}. + Version = emqx_protocol:get(proto_ver, ProtoState), + State#state{parse_state = emqx_frame:initial_state( + #{max_packet_size => Size, version => Version})}. handle_call(info, From, State = #state{proto_state = ProtoState}) -> ProtoInfo = emqx_protocol:info(ProtoState), @@ -252,12 +244,13 @@ handle_info({inet_reply, _Sock, ok}, State) -> handle_info({inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); -handle_info({keepalive, start, Interval}, State = #state{connection = Conn}) -> +handle_info({keepalive, start, Interval}, + State = #state{transport = Transport, socket = Sock}) -> ?LOG(debug, "Keepalive at the interval of ~p", [Interval], State), StatFun = fun() -> - case Conn:getstat([recv_oct]) of + case Transport:getstat(Sock, [recv_oct]) of {ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct}; - {error, Error} -> {error, Error} + Error -> Error end end, case emqx_keepalive:start(StatFun, Interval, {keepalive, check}) of @@ -284,12 +277,13 @@ handle_info(Info, State) -> ?LOG(error, "Unexpected Info: ~p", [Info], State), {noreply, State}. -terminate(Reason, State = #state{connection = Conn, +terminate(Reason, State = #state{transport = Transport, + socket = Sock, keepalive = KeepAlive, proto_state = ProtoState}) -> ?LOG(debug, "Terminated for ~p", [Reason], State), - Conn:fast_close(), + Transport:fast_close(Sock), emqx_keepalive:cancel(KeepAlive), case {ProtoState, Reason} of {undefined, _} -> @@ -314,7 +308,7 @@ received(<<>>, State) -> received(Bytes, State = #state{parse_state = ParseState, proto_state = ProtoState, idle_timeout = IdleTimeout}) -> - case catch emqx_parser:parse(Bytes, ParseState) of + case catch emqx_frame:parse(Bytes, ParseState) of {more, NewParseState} -> {noreply, State#state{parse_state = NewParseState}, IdleTimeout}; {ok, Packet, Rest} -> @@ -355,8 +349,8 @@ run_socket(State = #state{conn_state = blocked}) -> State; run_socket(State = #state{await_recv = true}) -> State; -run_socket(State = #state{connection = Conn}) -> - Conn:async_recv(0, infinity), +run_socket(State = #state{transport = Transport, socket = Sock}) -> + Transport:async_recv(Sock, 0, infinity), State#state{await_recv = true}. with_proto(Fun, State = #state{proto_state = ProtoState}) -> @@ -375,8 +369,11 @@ emit_stats(ClientId, State) -> emqx_cm:set_client_stats(ClientId, Stats), State. -sock_stats(#state{connection = Conn}) -> - case Conn:getstat(?SOCK_STATS) of {ok, Ss} -> Ss; {error, _} -> [] end. +sock_stats(#state{transport = Transport, socket = Sock}) -> + case Transport:getstat(Sock, ?SOCK_STATS) of + {ok, Ss} -> Ss; + _Error -> [] + end. reply(Reply, State) -> {reply, Reply, State, hibernate}. @@ -387,7 +384,7 @@ shutdown(Reason, State) -> stop(Reason, State) -> {stop, Reason, State}. -gc(State = #state{connection = Conn}) -> - Cb = fun() -> Conn:gc(), emit_stats(State) end, +gc(State = #state{transport = Transport, socket = Sock}) -> + Cb = fun() -> Transport:gc(Sock), emit_stats(State) end, emqx_gc:maybe_force_gc(#state.force_gc_count, State, Cb). diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index 805982e0e..a2228564a 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -40,24 +40,21 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -%% @doc Register a command -spec(register_command(cmd(), {module(), atom()}) -> ok). register_command(Cmd, MF) when is_atom(Cmd) -> register_command(Cmd, MF, []). -%% @doc Register a command with options -spec(register_command(cmd(), {module(), atom()}, list()) -> ok). register_command(Cmd, MF, Opts) when is_atom(Cmd) -> cast({register_command, Cmd, MF, Opts}). -%% @doc Unregister a command -spec(unregister_command(cmd()) -> ok). unregister_command(Cmd) when is_atom(Cmd) -> cast({unregister_command, Cmd}). -cast(Msg) -> gen_server:cast(?SERVER, Msg). +cast(Msg) -> + gen_server:cast(?SERVER, Msg). -%% @doc Run a command -spec(run_command(cmd(), [string()]) -> ok | {error, term()}). run_command(help, []) -> usage(); @@ -68,7 +65,7 @@ run_command(Cmd, Args) when is_atom(Cmd) -> _ -> ok catch _:Reason -> - emqx_logger:error("[CTL] Cmd error:~p, stacktrace:~p", + emqx_logger:error("[CTL] CMD Error:~p, Stacktrace:~p", [Reason, erlang:get_stacktrace()]), {error, Reason} end; @@ -76,7 +73,6 @@ run_command(Cmd, Args) when is_atom(Cmd) -> usage(), {error, cmd_not_found} end. -%% @doc Lookup a command -spec(lookup_command(cmd()) -> [{module(), atom()}]). lookup_command(Cmd) when is_atom(Cmd) -> case ets:match(?TAB, {{'_', Cmd}, '$1', '_'}) of @@ -84,7 +80,6 @@ lookup_command(Cmd) when is_atom(Cmd) -> [] -> [] end. -%% @doc Usage usage() -> io:format("Usage: ~s~n", [?MODULE]), [begin io:format("~80..-s~n", [""]), Mod:Cmd(usage) end @@ -144,7 +139,7 @@ next_seq(State = #state{seq = Seq}) -> -include_lib("eunit/include/eunit.hrl"). register_command_test_() -> - {setup, + {setup, fun() -> {ok, InitState} = emqx_ctl:init([]), InitState @@ -152,7 +147,7 @@ register_command_test_() -> fun(State) -> ok = emqx_ctl:terminate(shutdown, State) end, - fun(State = #state{seq = Seq}) -> + fun(State = #state{seq = Seq}) -> emqx_ctl:handle_cast({register_command, test0, {?MODULE, test0}, []}, State), [?_assertMatch([{{0,test0},{?MODULE, test0}, []}], ets:lookup(?TAB, {Seq,test0}))] end diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 7f01ee706..61f8f9e50 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -167,6 +167,7 @@ parse_packet(#mqtt_packet_header{type = ?PUBLISH, qos = QoS}, Bin, ?QOS_0 -> {undefined, Rest}; _ -> parse_packet_id(Rest) end, + io:format("Rest1: ~p~n", [Rest1]), {Properties, Payload} = parse_properties(Rest1, Ver), {#mqtt_packet_publish{topic_name = TopicName, packet_id = PacketId, @@ -577,8 +578,10 @@ serialize_property('Shared-Subscription-Available', Val) -> <<16#2A, Val>>. serialize_topic_filters(subscribe, TopicFilters, ?MQTT_PROTO_V5) -> - << <<(serialize_utf8_string(Topic))/binary, (serialize_subopts(SubOpts)) >> - || {Topic, SubOpts} <- TopicFilters >>; + << <<(serialize_utf8_string(Topic))/binary, + ?RESERVED:2, Rh:2, (flag(Rap)):1,(flag(Nl)):1, QoS:2 >> + || {Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} + <- TopicFilters >>; serialize_topic_filters(subscribe, TopicFilters, _Ver) -> << <<(serialize_utf8_string(Topic))/binary, ?RESERVED:6, QoS:2>> @@ -587,9 +590,6 @@ serialize_topic_filters(subscribe, TopicFilters, _Ver) -> serialize_topic_filters(unsubscribe, TopicFilters, _Ver) -> << <<(serialize_utf8_string(Topic))/binary>> || Topic <- TopicFilters >>. -serialize_subopts(#mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}) -> - <>. - serialize_reason_codes(undefined) -> <<>>; serialize_reason_codes(ReasonCodes) when is_list(ReasonCodes) -> diff --git a/src/emqx_inflight.erl b/src/emqx_inflight.erl index 10b3043b3..e2d77d95b 100644 --- a/src/emqx_inflight.erl +++ b/src/emqx_inflight.erl @@ -19,72 +19,72 @@ -export([new/1, contain/2, lookup/2, insert/3, update/3, delete/2, values/1, to_list/1, size/1, max_size/1, is_full/1, is_empty/1, window/1]). --type(inflight() :: {?MODULE, list()}). +-type(inflight() :: {max_size, gb_trees:tree()}). -export_type([inflight/0]). -spec(new(non_neg_integer()) -> inflight()). new(MaxSize) when MaxSize >= 0 -> - {?MODULE, [MaxSize, gb_trees:empty()]}. + {MaxSize, gb_trees:empty()}. --spec(contain(Key :: any(), inflight()) -> boolean()). -contain(Key, {?MODULE, [_MaxSize, Tree]}) -> +-spec(contain(Key :: term(), inflight()) -> boolean()). +contain(Key, {_MaxSize, Tree}) -> gb_trees:is_defined(Key, Tree). --spec(lookup(Key :: any(), inflight()) -> {value, any()} | none). -lookup(Key, {?MODULE, [_MaxSize, Tree]}) -> +-spec(lookup(Key :: term(), inflight()) -> {value, term()} | none). +lookup(Key, {_MaxSize, Tree}) -> gb_trees:lookup(Key, Tree). --spec(insert(Key :: any(), Value :: any(), inflight()) -> inflight()). -insert(Key, Value, {?MODULE, [MaxSize, Tree]}) -> - {?MODULE, [MaxSize, gb_trees:insert(Key, Value, Tree)]}. +-spec(insert(Key :: term(), Value :: term(), inflight()) -> inflight()). +insert(Key, Value, {MaxSize, Tree}) -> + {MaxSize, gb_trees:insert(Key, Value, Tree)}. --spec(delete(Key :: any(), inflight()) -> inflight()). -delete(Key, {?MODULE, [MaxSize, Tree]}) -> - {?MODULE, [MaxSize, gb_trees:delete(Key, Tree)]}. +-spec(delete(Key :: term(), inflight()) -> inflight()). +delete(Key, {MaxSize, Tree}) -> + {MaxSize, gb_trees:delete(Key, Tree)}. --spec(update(Key :: any(), Val :: any(), inflight()) -> inflight()). -update(Key, Val, {?MODULE, [MaxSize, Tree]}) -> - {?MODULE, [MaxSize, gb_trees:update(Key, Val, Tree)]}. +-spec(update(Key :: term(), Val :: term(), inflight()) -> inflight()). +update(Key, Val, {MaxSize, Tree}) -> + {MaxSize, gb_trees:update(Key, Val, Tree)}. -spec(is_full(inflight()) -> boolean()). -is_full({?MODULE, [0, _Tree]}) -> +is_full({0, _Tree}) -> false; -is_full({?MODULE, [MaxSize, Tree]}) -> +is_full({MaxSize, Tree}) -> MaxSize =< gb_trees:size(Tree). -spec(is_empty(inflight()) -> boolean()). -is_empty({?MODULE, [_MaxSize, Tree]}) -> +is_empty({_MaxSize, Tree}) -> gb_trees:is_empty(Tree). --spec(smallest(inflight()) -> {K :: any(), V :: any()}). -smallest({?MODULE, [_MaxSize, Tree]}) -> +-spec(smallest(inflight()) -> {K :: term(), V :: term()}). +smallest({_MaxSize, Tree}) -> gb_trees:smallest(Tree). --spec(largest(inflight()) -> {K :: any(), V :: any()}). -largest({?MODULE, [_MaxSize, Tree]}) -> +-spec(largest(inflight()) -> {K :: term(), V :: term()}). +largest({_MaxSize, Tree}) -> gb_trees:largest(Tree). -spec(values(inflight()) -> list()). -values({?MODULE, [_MaxSize, Tree]}) -> +values({_MaxSize, Tree}) -> gb_trees:values(Tree). --spec(to_list(inflight()) -> list({K :: any(), V :: any()})). -to_list({?MODULE, [_MaxSize, Tree]}) -> +-spec(to_list(inflight()) -> list({K :: term(), V :: term()})). +to_list({_MaxSize, Tree}) -> gb_trees:to_list(Tree). -spec(window(inflight()) -> list()). -window(Inflight = {?MODULE, [_MaxSize, Tree]}) -> +window(Inflight = {_MaxSize, Tree}) -> case gb_trees:is_empty(Tree) of true -> []; false -> [Key || {Key, _Val} <- [smallest(Inflight), largest(Inflight)]] end. -spec(size(inflight()) -> non_neg_integer()). -size({?MODULE, [_MaxSize, Tree]}) -> +size({_MaxSize, Tree}) -> gb_trees:size(Tree). -spec(max_size(inflight()) -> non_neg_integer()). -max_size({?MODULE, [MaxSize, _Tree]}) -> +max_size({MaxSize, _Tree}) -> MaxSize. diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 370ab4739..30fbb1848 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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). @@ -25,7 +25,7 @@ -import(proplists, [get_value/2, get_value/3]). %% API --export([init/3, init/4, get/2, info/1, stats/1, clientid/1, client/1, session/1]). +-export([init/3, init/5, get/2, info/1, stats/1, clientid/1, client/1, session/1]). -export([subscribe/2, unsubscribe/2, pubrel/2, shutdown/2]). @@ -76,8 +76,9 @@ init(Peername, SendFun, Opts) -> keepalive_backoff = Backoff, stats_data = #proto_stats{enable_stats = EnableStats}}. -init(Conn, Peername, SendFun, Opts) -> - enrich_opt(Conn:opts(), Conn, init(Peername, SendFun, Opts)). +init(_Transport, _Sock, Peername, SendFun, Opts) -> + init(Peername, SendFun, Opts). + %%enrich_opt(Conn:opts(), Conn, ). enrich_opt([], _Conn, State) -> State; diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 56f94eb08..7aff3a446 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -117,12 +117,10 @@ del_route(From, Topic, Dest) when is_binary(Topic) -> Route = #route{topic = Topic, dest = Dest}, cast(pick(Topic), {del_route, From, Route}). -%% @doc Has routes? -spec(has_routes(topic()) -> boolean()). has_routes(Topic) when is_binary(Topic) -> ets:member(?ROUTE, Topic). -%% @doc Get topics -spec(topics() -> list(topic())). topics() -> mnesia:dirty_all_keys(?ROUTE). diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index f4a19aca9..0606f70fd 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -40,7 +40,6 @@ -compile({no_auto_import, [monitor/1]}). -define(SERVER, ?MODULE). - -define(ROUTE, emqx_route). -define(ROUTING_NODE, emqx_routing_node). @@ -96,7 +95,7 @@ init([]) -> end end, [], mnesia:dirty_all_keys(?ROUTING_NODE)), emqx_stats:update_interval(route_stats, stats_fun()), - {ok, #state{nodes = Nodes}}. + {ok, #state{nodes = Nodes}, hibernate}. handle_call(Req, _From, State) -> emqx_logger:error("[RouterHelper] Unexpected request: ~p", [Req]), @@ -124,7 +123,7 @@ handle_info({nodedown, Node}, State = #state{nodes = Nodes}) -> mnesia:transaction(fun cleanup_routes/1, [Node]) end), mnesia:dirty_delete(?ROUTING_NODE, Node), - {noreply, State#state{nodes = lists:delete(Node, Nodes)}}; + {noreply, State#state{nodes = lists:delete(Node, Nodes)}, hibernate}; handle_info({membership, {mnesia, down, Node}}, State) -> handle_info({nodedown, Node}, State); diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 1dbe1e83a..83d53abc6 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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). @@ -246,7 +246,7 @@ stats(#state{max_subscriptions = MaxSubscriptions, [{max_subscriptions, MaxSubscriptions}, {subscriptions, maps:size(Subscriptions)}, {max_inflight, MaxInflight}, - {inflight_len, Inflight:size()}, + {inflight_len, emqx_inflight:size(Inflight)}, {max_mqueue, ?MQueue:max_len(MQueue)}, {mqueue_len, ?MQueue:len(MQueue)}, {mqueue_dropped, ?MQueue:dropped(MQueue)}, @@ -405,12 +405,12 @@ handle_cast({unsubscribe, From, TopicTable}, %% PUBACK: handle_cast({puback, PacketId}, State = #state{inflight = Inflight}) -> {noreply, - case Inflight:contain(PacketId) of + case emqx_inflight:contain(PacketId, Inflight) of true -> dequeue(acked(puback, PacketId, State)); false -> ?LOG(warning, "PUBACK ~p missed inflight: ~p", - [PacketId, Inflight:window()], State), + [PacketId, emqx_inflight:window(Inflight)], State), emqx_metrics:inc('packets/puback/missed'), State end, hibernate}; @@ -418,12 +418,12 @@ handle_cast({puback, PacketId}, State = #state{inflight = Inflight}) -> %% PUBREC: handle_cast({pubrec, PacketId}, State = #state{inflight = Inflight}) -> {noreply, - case Inflight:contain(PacketId) of + case emqx_inflight:contain(PacketId, Inflight) of true -> acked(pubrec, PacketId, State); false -> ?LOG(warning, "PUBREC ~p missed inflight: ~p", - [PacketId, Inflight:window()], State), + [PacketId, emqx_inflight:window(Inflight)], State), emqx_metrics:inc('packets/pubrec/missed'), State end, hibernate}; @@ -446,12 +446,12 @@ handle_cast({pubrel, PacketId}, State = #state{awaiting_rel = AwaitingRel}) -> %% PUBCOMP: handle_cast({pubcomp, PacketId}, State = #state{inflight = Inflight}) -> {noreply, - case Inflight:contain(PacketId) of + case emqx_inflight:contain(PacketId, Inflight) of true -> dequeue(acked(pubcomp, PacketId, State)); false -> ?LOG(warning, "The PUBCOMP ~p is not inflight: ~p", - [PacketId, Inflight:window()], State), + [PacketId, emqx_inflight:window(Inflight)], State), emqx_metrics:inc('packets/pubcomp/missed'), State end, hibernate}; @@ -581,11 +581,11 @@ kick(ClientId, OldPid, Pid) -> %%-------------------------------------------------------------------- %% Redeliver at once if Force is true - retry_delivery(Force, State = #state{inflight = Inflight}) -> - case Inflight:is_empty() of + case emqx_inflight:is_empty(Inflight) of true -> State; - false -> Msgs = lists:sort(sortfun(inflight), Inflight:values()), + false -> Msgs = lists:sort(sortfun(inflight), + emqx_inflight:values(Inflight)), retry_delivery(Force, Msgs, os:timestamp(), State) end. @@ -601,11 +601,11 @@ retry_delivery(Force, [{Type, Msg, Ts} | Msgs], Now, case {Type, Msg} of {publish, Msg = #message{headers = #{packet_id := PacketId}}} -> redeliver(Msg, State), - Inflight1 = Inflight:update(PacketId, {publish, Msg, Now}), + Inflight1 = emqx_inflight:update(PacketId, {publish, Msg, Now}, Inflight), retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}); {pubrel, PacketId} -> redeliver({pubrel, PacketId}, State), - Inflight1 = Inflight:update(PacketId, {pubrel, PacketId, Now}), + Inflight1 = emqx_inflight:update(PacketId, {pubrel, PacketId, Now}, Inflight), retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}) end; true -> @@ -678,7 +678,7 @@ dispatch(Msg = #message{qos = ?QOS0}, State) -> dispatch(Msg = #message{qos = QoS}, State = #state{next_msg_id = MsgId, inflight = Inflight}) when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 -> - case Inflight:is_full() of + case emqx_inflight:is_full(Inflight) of true -> enqueue_msg(Msg, State); false -> @@ -719,15 +719,15 @@ await(Msg = #message{headers = #{packet_id := PacketId}}, true -> State#state{retry_timer = start_timer(Interval, retry_delivery)}; false -> State end, - State1#state{inflight = Inflight:insert(PacketId, {publish, Msg, os:timestamp()})}. + State1#state{inflight = emqx_inflight:insert(PacketId, {publish, Msg, os:timestamp()}, Inflight)}. acked(puback, PacketId, State = #state{client_id = ClientId, username = Username, inflight = Inflight}) -> - case Inflight:lookup(PacketId) of + case emqx_inflight:lookup(PacketId, Inflight) of {value, {publish, Msg, _Ts}} -> emqx_hooks:run('message.acked', [ClientId, Username], Msg), - State#state{inflight = Inflight:delete(PacketId)}; + State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}; none -> ?LOG(warning, "Duplicated PUBACK Packet: ~p", [PacketId], State), State @@ -736,10 +736,10 @@ acked(puback, PacketId, State = #state{client_id = ClientId, acked(pubrec, PacketId, State = #state{client_id = ClientId, username = Username, inflight = Inflight}) -> - case Inflight:lookup(PacketId) of + case emqx_inflight:lookup(PacketId, Inflight) of {value, {publish, Msg, _Ts}} -> emqx_hooks:run('message.acked', [ClientId, Username], Msg), - State#state{inflight = Inflight:update(PacketId, {pubrel, PacketId, os:timestamp()})}; + State#state{inflight = emqx_inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight)}; {value, {pubrel, PacketId, _Ts}} -> ?LOG(warning, "Duplicated PUBREC Packet: ~p", [PacketId], State), State; @@ -749,7 +749,7 @@ acked(pubrec, PacketId, State = #state{client_id = ClientId, end; acked(pubcomp, PacketId, State = #state{inflight = Inflight}) -> - State#state{inflight = Inflight:delete(PacketId)}. + State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}. %%-------------------------------------------------------------------- %% Dequeue @@ -760,7 +760,7 @@ dequeue(State = #state{client_pid = undefined}) -> State; dequeue(State = #state{inflight = Inflight}) -> - case Inflight:is_full() of + case emqx_inflight:is_full(Inflight) of true -> State; false -> dequeue2(State) end. diff --git a/src/emqx_sys_sup.erl b/src/emqx_sys_sup.erl index 745817469..69d6f4cb5 100644 --- a/src/emqx_sys_sup.erl +++ b/src/emqx_sys_sup.erl @@ -30,7 +30,7 @@ init([]) -> permanent, 5000, worker, [emqx_sys]}, {ok, Env} = emqx_config:get_env(sysmon), - Sysmon = {sys_mon, {emqx_sysmon, start_link, [Env]}, + Sysmon = {sys_mon, {emqx_sys_mon, start_link, [Env]}, permanent, 5000, worker, [emqx_sys_mon]}, {ok, {{one_for_one, 10, 100}, [Sys, Sysmon]}}. diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 161104568..83a55c21e 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -1,18 +1,18 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_ws_connection). @@ -44,8 +44,9 @@ -export([handle_pre_hibernate/1]). %% WebSocket Client State --record(wsclient_state, {ws_pid, peername, connection, proto_state, keepalive, - enable_stats, force_gc_count}). +-record(wsclient_state, {ws_pid, transport, socket, peername, + proto_state, keepalive, enable_stats, + force_gc_count}). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). @@ -85,27 +86,29 @@ clean_acl_cache(CPid, Topic) -> init([Env, WsPid, Req, ReplyChannel]) -> process_flag(trap_exit, true), - Conn = Req:get(connection), true = link(WsPid), - case Req:get(peername) of + Transport = mochiweb_request:get(transport, Req), + Sock = mochiweb_request:get(socket, Req), + case mochiweb_request:get(peername, Req) of {ok, Peername} -> Headers = mochiweb_headers:to_list(mochiweb_request:get(headers, Req)), - ProtoState = emqx_protocol:init(Conn, Peername, send_fun(ReplyChannel), + ProtoState = emqx_protocol:init(Transport, Sock, Peername, send_fun(ReplyChannel), [{ws_initial_headers, Headers} | Env]), IdleTimeout = get_value(client_idle_timeout, Env, 30000), EnableStats = get_value(client_enable_stats, Env, false), ForceGcCount = emqx_gc:conn_max_gc_count(), - {ok, #wsclient_state{connection = Conn, + {ok, #wsclient_state{transport = Transport, + socket = Sock, ws_pid = WsPid, peername = Peername, proto_state = ProtoState, enable_stats = EnableStats, force_gc_count = ForceGcCount}, IdleTimeout, {backoff, 2000, 2000, 20000}, ?MODULE}; - {error, enotconn} -> Conn:fast_close(), + {error, enotconn} -> Transport:fast_close(Sock), exit(WsPid, normal), exit(normal); - {error, Reason} -> Conn:fast_close(), + {error, Reason} -> Transport:fast_close(Sock), exit(WsPid, normal), exit({shutdown, Reason}) end. @@ -205,9 +208,10 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> handle_info({shutdown, Reason}, State) -> shutdown(Reason, State); -handle_info({keepalive, start, Interval}, State = #wsclient_state{connection = Conn}) -> +handle_info({keepalive, start, Interval}, + State = #wsclient_state{transport = Transport, socket =Sock}) -> ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State), - case emqx_keepalive:start(stat_fun(Conn), Interval, {keepalive, check}) of + case emqx_keepalive:start(stat_fun(Transport, Sock), Interval, {keepalive, check}) of {ok, KeepAlive} -> {noreply, State#wsclient_state{keepalive = KeepAlive}, hibernate}; {error, Error} -> @@ -265,7 +269,7 @@ code_change(_OldVsn, State, _Extra) -> send_fun(ReplyChannel) -> Self = self(), fun(Packet) -> - Data = emqx_serializer:serialize(Packet), + Data = emqx_frame:serialize(Packet), emqx_metrics:inc('bytes/sent', iolist_size(Data)), case ReplyChannel({binary, Data}) of ok -> ok; @@ -273,9 +277,9 @@ send_fun(ReplyChannel) -> end end. -stat_fun(Conn) -> +stat_fun(Transport, Sock) -> fun() -> - case Conn:getstat([recv_oct]) of + case Transport:getstat(Sock, [recv_oct]) of {ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct}; {error, Error} -> {error, Error} end @@ -293,8 +297,8 @@ emit_stats(ClientId, State) -> emqx_cm:set_client_stats(ClientId, Stats), State. -wsock_stats(#wsclient_state{connection = Conn}) -> - case Conn:getstat(?SOCK_STATS) of +wsock_stats(#wsclient_state{transport = Transport, socket = Sock}) -> + case Transport:getstat(Sock, ?SOCK_STATS) of {ok, Ss} -> Ss; {error, _} -> [] end. diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index b47b23942..e3e0bbb38 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -17,6 +17,7 @@ -module(emqx_SUITE). -compile(export_all). +-compile(nowarn_export_all). -include_lib("emqttc/include/emqttc_packet.hrl"). diff --git a/test/emqx_SUITE_data/loaded_plugins b/test/emqx_SUITE_data/loaded_plugins new file mode 100644 index 000000000..e69de29bb diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index aee73b873..270d55f57 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -17,6 +17,7 @@ -module(emqx_access_SUITE). -compile(export_all). +-compile(nowarn_export_all). -include("emqx.hrl"). diff --git a/test/emqx_base62_SUITE.erl b/test/emqx_base62_SUITE.erl index 6baf724d1..e0cb0e26a 100644 --- a/test/emqx_base62_SUITE.erl +++ b/test/emqx_base62_SUITE.erl @@ -21,6 +21,7 @@ -define(BASE62, emqx_base62). -compile(export_all). +-compile(nowarn_export_all). all() -> [t_base62_encode]. diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index e0ec205f7..e77d689d4 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -16,6 +16,7 @@ -module(emqx_broker_SUITE). -compile(export_all). +-compile(nowarn_export_all). -define(APP, emqx). @@ -24,6 +25,7 @@ -include_lib("common_test/include/ct.hrl"). -include("emqx.hrl"). +-include("emqx_mqtt.hrl"). all() -> [ @@ -56,7 +58,7 @@ init_per_suite(Config) -> end_per_suite(Config) -> emqx_ct_broker_helpers:run_teardown_steps(). - + %%-------------------------------------------------------------------- %% PubSub Test %%-------------------------------------------------------------------- @@ -147,7 +149,7 @@ start_session(_) -> {ok, ClientPid} = emqx_mock_client:start_link(<<"clientId">>), {ok, SessPid} = emqx_mock_client:start_session(ClientPid), Message = emqx_message:make(<<"clientId">>, 2, <<"topic">>, <<"hello">>), - Message1 = Message#mqtt_message{pktid = 1}, + Message1 = Message#mqtt_message{packet_id = 1}, emqx_session:publish(SessPid, Message1), emqx_session:pubrel(SessPid, 1), emqx_session:subscribe(SessPid, [{<<"topic/session">>, [{qos, 2}]}]), @@ -228,7 +230,7 @@ hook_fun7(arg, initArg) -> any. hook_fun8(arg, initArg) -> stop. set_alarms(_) -> - AlarmTest = #mqtt_alarm{id = <<"1">>, severity = error, title="alarm title", summary="alarm summary"}, + AlarmTest = #alarm{id = <<"1">>, severity = error, title="alarm title", summary="alarm summary"}, emqx_alarm:set_alarm(AlarmTest), Alarms = emqx_alarm:get_alarms(), ?assertEqual(1, length(Alarms)), diff --git a/test/emqx_client_SUITE.erl b/test/emqx_client_SUITE.erl new file mode 100644 index 000000000..7b2d5aaae --- /dev/null +++ b/test/emqx_client_SUITE.erl @@ -0,0 +1,41 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%%% +%%% Licensed under the Apache License, Version 2.0 (the "License"); +%%% you may not use this file except in compliance with the License. +%%% You may obtain a copy of the License at +%%% +%%% http://www.apache.org/licenses/LICENSE-2.0 +%%% +%%% Unless required by applicable law or agreed to in writing, software +%%% distributed under the License is distributed on an "AS IS" BASIS, +%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%%% See the License for the specific language governing permissions and +%%% limitations under the License. +%%%=================================================================== + +-module(emqx_client_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx_mqtt.hrl"). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> []. + +groups() -> []. + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +init_per_group(_Group, Config) -> + Config. + +end_per_group(_Group, _Config) -> + ok. + diff --git a/test/emqx_ct_broker_helpers.erl b/test/emqx_ct_broker_helpers.erl index 3dfe84fcc..a62297a49 100644 --- a/test/emqx_ct_broker_helpers.erl +++ b/test/emqx_ct_broker_helpers.erl @@ -1,22 +1,23 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_ct_broker_helpers). -compile(export_all). +-compile(nowarn_export_all). -define(APP, emqx). @@ -57,13 +58,11 @@ local_path(Components) -> set_app_env({App, Lists}) -> lists:foreach(fun({acl_file, _Var}) -> - application:set_env(App, acl_file, local_path(["etc", "acl.conf"])); - ({license_file, _Var}) -> - application:set_env(App, license_file, local_path(["etc", "emqx.lic"])); + application:set_env(App, acl_file, local_path(["etc", "acl.conf"])); ({plugins_loaded_file, _Var}) -> - application:set_env(App, plugins_loaded_file, local_path(["test", "emqx_SUITE_data","loaded_plugins"])); + application:set_env(App, plugins_loaded_file, local_path(["test", "emqx_SUITE_data","loaded_plugins"])); ({Par, Var}) -> - application:set_env(App, Par, Var) + application:set_env(App, Par, Var) end, Lists). change_opts(SslType) -> diff --git a/test/emqx_ct_helpers.erl b/test/emqx_ct_helpers.erl new file mode 100644 index 000000000..c1618eccd --- /dev/null +++ b/test/emqx_ct_helpers.erl @@ -0,0 +1,24 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_ct_helpers). + +-export([ensure_mnesia_stopped/0]). + +ensure_mnesia_stopped() -> + ekka_mnesia:ensure_stopped(), + ekka_mnesia:delete_schema(). + diff --git a/test/emqx_frame_SUITE.erl b/test/emqx_frame_SUITE.erl index c177fda8e..c4ec83024 100644 --- a/test/emqx_frame_SUITE.erl +++ b/test/emqx_frame_SUITE.erl @@ -284,9 +284,7 @@ serialize_parse_publish_v5(_) -> 'Correlation-Data' => <<"correlation-id">>, 'Subscription-Identifier' => 1, 'Content-Type' => <<"text/json">>}, - Packet = ?PUBLISH_PACKET(#mqtt_packet_header{type = ?PUBLISH}, - <<"$share/group/topic">>, 1, Props, - <<"payload">>), + Packet = ?PUBLISH_PACKET(?QOS_1, <<"$share/group/topic">>, 1, Props, <<"payload">>), ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). @@ -335,7 +333,7 @@ serialize_parse_subscribe(_) -> Bin = <<130,11,0,2,0,6,84,111,112,105,99,65,2>>, TopicFilters = [{<<"TopicA">>, #mqtt_subopts{qos = 2}}], Packet = ?SUBSCRIBE_PACKET(2, TopicFilters), - ?assertEqual(Bin, serialize(Packet)), + ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), ?assertEqual({ok, Packet, <<>>}, parse(Bin)). serialize_parse_subscribe_v5(_) -> @@ -343,7 +341,8 @@ serialize_parse_subscribe_v5(_) -> {<<"TopicQos1">>, #mqtt_subopts{rh = 1, qos =?QOS_1}}], Packet = ?SUBSCRIBE_PACKET(1, #{'Subscription-Identifier' => 16#FFFFFFF}, TopicFilters), - ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + ?assertEqual({ok, Packet, <<>>}, + parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). serialize_parse_suback(_) -> Packet = ?SUBACK_PACKET(10, [?QOS_0, ?QOS_1, 128]), diff --git a/test/emqx_guid_SUITE.erl b/test/emqx_guid_SUITE.erl index a6978bac8..0dee1da48 100644 --- a/test/emqx_guid_SUITE.erl +++ b/test/emqx_guid_SUITE.erl @@ -19,6 +19,7 @@ -include_lib("eunit/include/eunit.hrl"). -compile(export_all). +-compile(nowarn_export_all). all() -> [t_guid_gen, t_guid_hexstr, t_guid_base62]. diff --git a/test/emqx_inflight_SUITE.erl b/test/emqx_inflight_SUITE.erl index 903d5c7b5..de3accc06 100644 --- a/test/emqx_inflight_SUITE.erl +++ b/test/emqx_inflight_SUITE.erl @@ -1,61 +1,62 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_inflight_SUITE). -include_lib("eunit/include/eunit.hrl"). -%% CT -compile(export_all). +-compile(nowarn_export_all). -all() -> [t_contain, t_lookup, t_insert, t_update, t_delete, t_window, - t_is_full, t_is_empty]. +-import(emqx_inflight, [new/1, contain/2, insert/3, lookup/2, update/3, + delete/2, is_empty/1, is_full/1]). + +all() -> + [t_contain, t_lookup, t_insert, t_update, t_delete, t_window, + t_is_full, t_is_empty]. t_contain(_) -> - Inflight = emqx_inflight:new(0), - ?assertNot(Inflight:contain(k)), - Inflight1 = Inflight:insert(k, v), - ?assert(Inflight1:contain(k)). + ?assertNot(contain(k, new(0))), + ?assert(contain(k, insert(k, v, new(0)))). t_lookup(_) -> - Inflight = (emqx_inflight:new(0)):insert(k, v), - ?assertEqual(v, Inflight:lookup(k)). + Inflight = insert(k, v, new(0)), + ?assertEqual({value, v}, lookup(k, Inflight)), + ?assertEqual(none, lookup(x, Inflight)). t_insert(_) -> - Inflight = ((emqx_inflight:new(0)):insert(k1, v1)):insert(k2, v2), - ?assertEqual(v2, Inflight:lookup(k2)). + Inflight = insert(k2, v2, insert(k1, v1, new(0))), + ?assertEqual({value, v1}, lookup(k1, Inflight)), + ?assertEqual({value, v2}, lookup(k2, Inflight)). t_update(_) -> - Inflight = ((emqx_inflight:new(0)):insert(k, v1)):update(k, v2), - ?assertEqual(v2, Inflight:lookup(k)). + Inflight = update(k, v2, insert(k, v1, new(0))), + ?assertEqual({value, v2}, lookup(k, Inflight)). t_delete(_) -> - Inflight = ((emqx_inflight:new(0)):insert(k, v1)):delete(k), - ?assert(Inflight:is_empty()). + ?assert(is_empty(delete(k, insert(k, v1, new(0))))). t_window(_) -> - ?assertEqual([], (emqx_inflight:new(10)):window()), - Inflight = ((emqx_inflight:new(0)):insert(1, 1)):insert(2, 2), - ?assertEqual([1, 2], Inflight:window()). + ?assertEqual([], emqx_inflight:window(new(10))), + Inflight = insert(2, 2, insert(1, 1, new(0))), + ?assertEqual([1, 2], emqx_inflight:window(Inflight)). t_is_full(_) -> - Inflight = ((emqx_inflight:new(1)):insert(k, v1)), - ?assert(Inflight:is_full()). + ?assert(is_full(insert(k, v1, new(1)))). t_is_empty(_) -> - Inflight = ((emqx_inflight:new(1)):insert(k, v1)), - ?assertNot(Inflight:is_empty()). + ?assertNot(is_empty(insert(k, v1, new(1)))). diff --git a/test/emqx_lib_SUITE.erl b/test/emqx_lib_SUITE.erl index 0b24fb059..2cd24bb63 100644 --- a/test/emqx_lib_SUITE.erl +++ b/test/emqx_lib_SUITE.erl @@ -19,6 +19,7 @@ -include_lib("eunit/include/eunit.hrl"). -compile(export_all). +-compile(nowarn_export_all). -define(SOCKOPTS, [ binary, diff --git a/test/emqx_misc_SUITE.erl b/test/emqx_misc_SUITE.erl index e1283a9d4..4b7ec74f6 100644 --- a/test/emqx_misc_SUITE.erl +++ b/test/emqx_misc_SUITE.erl @@ -19,6 +19,7 @@ -include_lib("eunit/include/eunit.hrl"). -compile(export_all). +-compile(nowarn_export_all). -define(SOCKOPTS, [binary, {packet, raw}, diff --git a/test/emqx_mod_SUITE.erl b/test/emqx_mod_SUITE.erl index e87bddecc..963d39c45 100644 --- a/test/emqx_mod_SUITE.erl +++ b/test/emqx_mod_SUITE.erl @@ -17,10 +17,11 @@ -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_mqtt_compat_SUITE.erl b/test/emqx_mqtt_compat_SUITE.erl new file mode 100644 index 000000000..d27c094ea --- /dev/null +++ b/test/emqx_mqtt_compat_SUITE.erl @@ -0,0 +1,220 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_compat_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-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(WILD_TOPICS, [<<"TopicA/+">>, <<"+/C">>, <<"#">>, <<"/#">>, <<"/+">>, + <<"+/+">>, <<"TopicA/#">>]). + +all() -> + [basic_test, + retained_message_test, + will_message_test, + zero_length_clientid_test, + offline_message_queueing_test, + overlapping_subscriptions_test, + keepalive_test, + redelivery_on_reconnect_test, + subscribe_failure_test, + dollar_topics_test]. + +init_per_suite(Config) -> + emqx_ct_broker_helpers:run_setup_steps(), + Config. + +end_per_suite(_Config) -> + emqx_ct_broker_helpers:run_teardown_steps(). + +receive_messages(Count) -> + receive_messages(Count, []). + +receive_messages(0, Msgs) -> + Msgs; +receive_messages(Count, Msgs) -> + receive + {public, Msg} -> + receive_messages(Count-1, [Msg|Msgs]); + _Other -> + receive_messages(Count, Msgs) + after 10 -> + Msgs + end. + +basic_test(_Config) -> + Topic = nth(1, ?TOPICS), + ct:print("Basic test starting"), + {ok, C, _} = emqx_client:start_link(), + {ok, _, [0]} = emqx_client:subscribe(C, Topic, qos2), + ok = emqx_client:publish(C, Topic, <<"qos 0">>), + {ok, _} = emqx_client:publish(C, Topic, <<"qos 1">>, 1), + {ok, _} = emqx_client:publish(C, Topic, <<"qos 2">>, 2), + ok = emqx_client:disconnect(C), + ?assertEqual(3, length(receive_messages(3))). + +retained_message_test(_Config) -> + ct:print("Retained message test starting"), + + %% Retained messages + {ok, C1, _} = emqx_client:start_link([{clean_start, true}]), + ok = emqx_client:publish(C1, nth(1, ?TOPICS), <<"qos 0">>, [{qos, 0}, {retain, true}]), + {ok, _} = emqx_client:publish(C1, nth(3, ?TOPICS), <<"qos 1">>, [{qos, 1}, {retain, true}]), + {ok, _} = emqx_client:publish(C1, nth(4, ?TOPICS), <<"qos 2">>, [{qos, 2}, {retain, true}]), + timer:sleep(10), + {ok, #{}, [0]} = emqx_client:subscribe(C1, nth(6, ?WILD_TOPICS), 2), + ok = emqx_client:disconnect(C1), + ?assertEqual(3, length(receive_messages(10))), + + %% Clear retained messages + {ok, C2, _} = emqx_client:start_link([{clean_start, true}]), + ok = emqx_client:publish(C2, nth(2, ?TOPICS), <<"">>, [{qos, 0}, {retain, true}]), + {ok, _} = emqx_client:publish(C2, nth(3, ?TOPICS), <<"">>, [{qos, 1}, {retain, true}]), + {ok, _} = emqx_client:publish(C2, nth(4, ?TOPICS), <<"">>, [{qos, 2}, {retain, true}]), + timer:sleep(10), %% wait for QoS 2 exchange to be completed + {ok, _, [0]} = emqx_client:subscribe(C2, nth(6, ?WILD_TOPICS), 2), + timer:sleep(10), + ok = emqx_client:disconnect(), + ?assertEqual(0, length(receive_messages(3))). + +will_message_test(_Config) -> + {ok, C1, _} = emqx_client:start_link([{clean_start, true}, + {will_topic = nth(3, ?TOPICS)}, + {will_payload, <<"client disconnected">>}, + {keepalive, 2}]), + {ok, C2, _} = emqx_client:start_link(), + {ok, _, [2]} = emqx_client:subscribe(C2, nth(3, ?TOPICS), 2), + timer:sleep(10), + ok = emqx_client:stop(C1), + timer:sleep(5), + ok = emqx_client:disconnect(C2), + ?assertEqual(1, length(receive_messages(1))), + ct:print("Will message test succeeded"). + +zero_length_clientid_test(_Config) -> + ct:print("Zero length clientid test starting"), + {error, _} = emqx_client:start_link([{clean_start, false}, + {client_id, <<>>}]), + {ok, _, _} = emqx_client:start_link([{clean_start, true}, + {client_id, <<>>}]), + ct:print("Zero length clientid test succeeded"). + +offline_message_queueing_test(_) -> + {ok, C1, _} = emqx_client:start_link([{clean_start, false}, + {client_id, <<"c1">>}]), + {ok, _, [2]} = emqx_client:subscribe(C1, nth(6, ?WILD_TOPICS), 2), + ok = emqx_client:disconnect(C1), + {ok, C2, _} = emqx_client:start_link([{clean_start, true}, + {client_id, <<"c2">>}]), + + ok = emqx_client:publish(C2, nth(2, ?TOPICS), <<"qos 0">>, 0), + {ok, _} = emqx_client:publish(C2, nth(3, ?TOPICS), <<"qos 1">>, 1), + {ok, _} = emqx_client:publish(C2, nth(4, ?TOPICS), <<"qos 2">>, 2), + timer:sleep(10), + emqx_client:disconnect(C2), + {ok, C3, _} = emqx_client:start_link([{clean_start, false}, + {client_id, <<"c1">>}]), + timer:sleep(10), + emqx_client:disconnect(C3), + ?assertEqual(3, length(receive_messages(3))). + +overlapping_subscriptions_test(_) -> + {ok, C, _} = emqx_client:start_link([]), + {ok, _, [2, 1]} = emqx_client:subscribe(C, [{nth(7, ?WILD_TOPICS), 2}, + {nth(1, ?WILD_TOPICS), 1}]), + timer:sleep(10), + {ok, _} = emqx_client:publish(C, nth(4, ?TOPICS), <<"overlapping topic filters">>, 2), + time:sleep(10), + emqx_client:disconnect(C), + Num = receive_messages(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."); + Num == 2 -> + ct:print("This server is publishing one message per each + matching overlapping subscription."); + true -> ok + end. + +keepalive_test(_) -> + ct:print("Keepalive test starting"), + {ok, C1, _} = emqx_client:start_link([{clean_start, true}, + {keepalive, 5}, + {will_topic, nth(5, ?TOPICS)}, + {will_payload, <<"keepalive expiry">>}]), + ok = emqx_client:pause(C1), + + {ok, C2, _} = emqx_client:start_link([{clean_start, true}, + {keepalive, 0}]), + {ok, _, [2]} = emqx_client:subscribe(C2, nth(5, ?TOPICS), 2), + timer:sleep(15000), + ok = emqx_client:disconnect(C2), + ?assertEqual(1, length(receive_messages(1))), + ct:print("Keepalive test succeeded"). + +redelivery_on_reconnect_test(_) -> + ct:print("Redelivery on reconnect test starting"), + {ok, C1, _} = emqx_client:start_link([{clean_start, false}, + {client_id, <<"c">>}]), + {ok, _, [2]} = emqx_client:subscribe(C1, nth(7, ?WILD_TOPICS), 2), + timer:sleep(10), + ok = emqx_client:pause(C1), + {ok, _} = emqx_client:publish(C1, nth(2, ?TOPICS), <<>>, + [{qos, 1}, {retain, false}]), + {ok, _} = emqx_client:publish(C1, nth(4, ?TOPICS), <<>>, + [{qos, 2}, {retain, false}]), + time:sleep(10), + ok = emqx_client:disconnect(C1), + ?assertEqual(0, length(receive_messages(2))), + {ok, C2, _} = emqx_client:start_link([{clean_start, false}, + {client_id, <<"c">>}]), + timer:sleep(10), + ok = emqx_client:disconnect(C2), + ?assertEqual(2, length(receive_messages(2))). + +subscribe_failure_test(_) -> + ct:print("Subscribe failure test starting"), + {ok, C, _} = emqx_client:start_link([]), + {ok, _, [16#80]} = emqx_client:subscribe(C, <<"$SYS/#">>, 2), + timer:sleep(10), + ct:print("Subscribe failure test succeeded"). + +dollar_topics_test(_) -> + ct:print("$ topics test starting"), + {ok, C, _} = emqx_client:start_link([{clean_start, true}, + {keepalive, 0}]), + {ok, _, [2]} = emqx_client:subscribe(C, nth(6, ?WILD_TOPICS), 2), + {ok, _} = emqx_client:publish(C, <<"$", (nth(2, ?TOPICS))>>, + <<"">>, [{qos, 1}, {retain, false}]), + timer:sleep(10), + ?assertEqual(0, length(receive_messages(1))), + ok = emqx_client:disconnect(C), + ct:print("$ topics test succeeded"). + diff --git a/test/emqx_mqueue_SUITE.erl b/test/emqx_mqueue_SUITE.erl index 3123ab94f..d174d980e 100644 --- a/test/emqx_mqueue_SUITE.erl +++ b/test/emqx_mqueue_SUITE.erl @@ -17,8 +17,10 @@ -module(emqx_mqueue_SUITE). -compile(export_all). +-compile(nowarn_export_all). -include("emqx.hrl"). +-include("emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). diff --git a/test/emqx_net_SUITE.erl b/test/emqx_net_SUITE.erl index a729ad1a7..34f3d54e2 100644 --- a/test/emqx_net_SUITE.erl +++ b/test/emqx_net_SUITE.erl @@ -18,6 +18,7 @@ %% CT -compile(export_all). +-compile(nowarn_export_all). all() -> [{group, keepalive}]. diff --git a/test/emqx_pqueue_SUITE.erl b/test/emqx_pqueue_SUITE.erl index c798bf100..55b2dc01b 100644 --- a/test/emqx_pqueue_SUITE.erl +++ b/test/emqx_pqueue_SUITE.erl @@ -16,9 +16,11 @@ -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). diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl deleted file mode 100644 index 0d86beea0..000000000 --- a/test/emqx_protocol_SUITE.erl +++ /dev/null @@ -1,320 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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_SUITE). - --compile(export_all). - --include("emqx.hrl"). - --include("emqx_mqtt.hrl"). - --include_lib("eunit/include/eunit.hrl"). - --import(emqx_serializer, [serialize/1]). - -all() -> - [{group, parser}, - {group, serializer}, - {group, packet}, - {group, message}]. - -groups() -> - [{parser, [], - [parse_connect, - parse_bridge, - parse_publish, - parse_puback, - parse_pubrec, - parse_pubrel, - parse_pubcomp, - parse_subscribe, - parse_unsubscribe, - parse_pingreq, - parse_disconnect]}, - {serializer, [], - [serialize_connect, - serialize_connack, - serialize_publish, - serialize_puback, - serialize_pubrel, - serialize_subscribe, - serialize_suback, - serialize_unsubscribe, - serialize_unsuback, - serialize_pingreq, - serialize_pingresp, - serialize_disconnect]}, - {packet, [], - [packet_proto_name, - packet_type_name, - packet_connack_name, - packet_format]}, - {message, [], - [message_make, - message_from_packet, - message_flag]}]. - -%%-------------------------------------------------------------------- -%% Parse Cases -%%-------------------------------------------------------------------- - -parse_connect(_) -> - Parser = emqx_parser:initial_state(), - %% CONNECT(Q0, R0, D0, ClientId=mosqpub/10451-iMac.loca, ProtoName=MQIsdp, ProtoVsn=3, CleanSess=true, KeepAlive=60, Username=undefined, Password=undefined) - V31ConnBin = <<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>>, - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT, - dup = false, - qos = 0, - retain = false}, - variable = #mqtt_packet_connect{proto_ver = 3, - proto_name = <<"MQIsdp">>, - client_id = <<"mosqpub/10451-iMac.loca">>, - clean_sess = true, - keep_alive = 60}}, <<>>} = emqx_parser:parse(V31ConnBin, Parser), - %% CONNECT(Q0, R0, D0, ClientId=mosqpub/10451-iMac.loca, ProtoName=MQTT, ProtoVsn=4, CleanSess=true, KeepAlive=60, Username=undefined, Password=undefined) - V311ConnBin = <<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>>, - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT, - dup = false, - qos = 0, - retain = false}, - variable = #mqtt_packet_connect{proto_ver = 4, - proto_name = <<"MQTT">>, - client_id = <<"mosqpub/10451-iMac.loca">>, - clean_sess = true, - keep_alive = 60 } }, <<>>} = emqx_parser:parse(V311ConnBin, Parser), - - %% CONNECT(Qos=0, Retain=false, Dup=false, ClientId="", ProtoName=MQTT, ProtoVsn=4, CleanSess=true, KeepAlive=60) - V311ConnWithoutClientId = <<16,12,0,4,77,81,84,84,4,2,0,60,0,0>>, - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT, - dup = false, - qos = 0, - retain = false}, - variable = #mqtt_packet_connect{proto_ver = 4, - proto_name = <<"MQTT">>, - client_id = <<>>, - clean_sess = true, - keep_alive = 60 } }, <<>>} = emqx_parser:parse(V311ConnWithoutClientId, Parser), - %%CONNECT(Q0, R0, D0, ClientId=mosqpub/10452-iMac.loca, ProtoName=MQIsdp, ProtoVsn=3, CleanSess=true, KeepAlive=60, - %% Username=test, Password=******, Will(Qos=1, Retain=false, Topic=/will, Msg=willmsg)) - ConnBinWithWill = <<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,112,117,98,108,105,99>>, - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT, - dup = false, - qos = 0, - retain = false}, - variable = #mqtt_packet_connect{proto_ver = 3, - proto_name = <<"MQIsdp">>, - client_id = <<"mosqpub/10452-iMac.loca">>, - clean_sess = true, - keep_alive = 60, - will_retain = false, - will_qos = 1, - will_flag = true, - will_topic = <<"/will">>, - will_msg = <<"willmsg">>, - username = <<"test">>, - password = <<"public">>}}, <<>>} = emqx_parser:parse(ConnBinWithWill, Parser), - ok. - -parse_bridge(_) -> - Parser = emqx_parser:initial_state(), - Data = <<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, - 67,58,50,57,58,50,66,58,55,55,58,53,50,47,115,116,97,116,101,0,1,48>>, - - %% CONNECT(Q0, R0, D0, ClientId=C_00:0C:29:2B:77:52, ProtoName=MQIsdp, ProtoVsn=131, CleanSess=false, KeepAlive=60, - %% Username=undefined, Password=undefined, Will(Q1, R1, Topic=$SYS/broker/connection/C_00:0C:29:2B:77:52/state, Msg=0)) - {ok, #mqtt_packet{variable = Variable}, <<>>} = emqx_parser:parse(Data, Parser), - #mqtt_packet_connect{client_id = <<"C_00:0C:29:2B:77:52">>, - proto_ver = 16#03, - proto_name = <<"MQIsdp">>, - will_retain = true, - will_qos = 1, - will_flag = true, - clean_sess = false, - keep_alive = 60, - will_topic = <<"$SYS/broker/connection/C_00:0C:29:2B:77:52/state">>, - will_msg = <<"0">>} = Variable. - -parse_publish(_) -> - Parser = emqx_parser:initial_state(), - %%PUBLISH(Qos=1, Retain=false, Dup=false, TopicName=a/b/c, PacketId=1, Payload=<<"hahah">>) - PubBin = <<50,14,0,5,97,47,98,47,99,0,1,104,97,104,97,104>>, - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - dup = false, - qos = 1, - retain = false}, - variable = #mqtt_packet_publish{topic_name = <<"a/b/c">>, - packet_id = 1}, - payload = <<"hahah">> }, <<>>} = emqx_parser:parse(PubBin, Parser), - - %PUBLISH(Qos=0, Retain=false, Dup=false, TopicName=xxx/yyy, PacketId=undefined, Payload=<<"hello">>) - %DISCONNECT(Qos=0, Retain=false, Dup=false) - PubBin1 = <<48,14,0,7,120,120,120,47,121,121,121,104,101,108,108,111,224,0>>, - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - dup = false, - qos = 0, - retain = false}, - variable = #mqtt_packet_publish{topic_name = <<"xxx/yyy">>, - packet_id = undefined}, - payload = <<"hello">> }, <<224,0>>} = emqx_parser:parse(PubBin1, Parser), - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT, - dup = false, - qos = 0, - retain = false}}, <<>>} = emqx_parser:parse(<<224, 0>>, Parser). - -parse_puback(_) -> - Parser = emqx_parser:initial_state(), - %%PUBACK(Qos=0, Retain=false, Dup=false, PacketId=1) - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK, - dup = false, - qos = 0, - retain = false}}, <<>>} = emqx_parser:parse(<<64,2,0,1>>, Parser). -parse_pubrec(_) -> - Parser = emqx_parser:initial_state(), - %%PUBREC(Qos=0, Retain=false, Dup=false, PacketId=1) - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC, - dup = false, - qos = 0, - retain = false}}, <<>>} = emqx_parser:parse(<<5:4,0:4,2,0,1>>, Parser). - -parse_pubrel(_) -> - Parser = emqx_parser:initial_state(), - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, - dup = false, - qos = 1, - retain = false}}, <<>>} = emqx_parser:parse(<<6:4,2:4,2,0,1>>, Parser). - -parse_pubcomp(_) -> - Parser = emqx_parser:initial_state(), - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP, - dup = false, - qos = 0, - retain = false}}, <<>>} = emqx_parser:parse(<<7:4,0:4,2,0,1>>, Parser). - -parse_subscribe(_) -> - Parser = emqx_parser:initial_state(), - %% SUBSCRIBE(Q1, R0, D0, PacketId=2, TopicTable=[{<<"TopicA">>,2}]) - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, - dup = false, - qos = 1, - retain = false}, - variable = #mqtt_packet_subscribe{packet_id = 2, - topic_table = [{<<"TopicA">>,2}]} }, <<>>} - = emqx_parser:parse(<<130,11,0,2,0,6,84,111,112,105,99,65,2>>, Parser). - -parse_unsubscribe(_) -> - Parser = emqx_parser:initial_state(), - %% UNSUBSCRIBE(Q1, R0, D0, PacketId=2, TopicTable=[<<"TopicA">>]) - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, - dup = false, - qos = 1, - retain = false}, - variable = #mqtt_packet_unsubscribe{packet_id = 2, - topics = [<<"TopicA">>]}}, <<>>} - = emqx_parser:parse(<<162,10,0,2,0,6,84,111,112,105,99,65>>, Parser). - -parse_pingreq(_) -> - Parser = emqx_parser:initial_state(), - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?PINGREQ, - dup = false, - qos = 0, - retain = false}}, <<>>} - = emqx_parser:parse(<>, Parser). - -parse_disconnect(_) -> - Parser = emqx_parser:initial_state(), - %DISCONNECT(Qos=0, Retain=false, Dup=false) - Bin = <<224, 0>>, - {ok, #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT, - dup = false, - qos = 0, - retain = false}}, <<>>} = emqx_parser:parse(Bin, Parser). - -%%-------------------------------------------------------------------- -%% Packet Cases -%%-------------------------------------------------------------------- - -packet_proto_name(_) -> - ?assertEqual(<<"MQIsdp">>, emqx_packet:protocol_name(3)), - ?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(4)). - -packet_type_name(_) -> - ?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT)), - ?assertEqual('UNSUBSCRIBE', emqx_packet:type_name(?UNSUBSCRIBE)). - -packet_connack_name(_) -> - ?assertEqual('CONNACK_ACCEPT', emqx_packet:connack_name(?CONNACK_ACCEPT)), - ?assertEqual('CONNACK_PROTO_VER', emqx_packet:connack_name(?CONNACK_PROTO_VER)), - ?assertEqual('CONNACK_INVALID_ID', emqx_packet:connack_name(?CONNACK_INVALID_ID)), - ?assertEqual('CONNACK_SERVER', emqx_packet:connack_name(?CONNACK_SERVER)), - ?assertEqual('CONNACK_CREDENTIALS', emqx_packet:connack_name(?CONNACK_CREDENTIALS)), - ?assertEqual('CONNACK_AUTH', emqx_packet:connack_name(?CONNACK_AUTH)). - -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))]), - io:format("~s", [emqx_packet:format(?PUBLISH_PACKET(?QOS_2, <<"topic">>, 10, <<"payload">>))]), - io:format("~s", [emqx_packet:format(?PUBACK_PACKET(?PUBACK, 98))]), - io:format("~s", [emqx_packet:format(?PUBREL_PACKET(99))]), - io:format("~s", [emqx_packet:format(?SUBSCRIBE_PACKET(15, [{<<"topic">>, ?QOS0}, {<<"topic1">>, ?QOS1}]))]), - io:format("~s", [emqx_packet:format(?SUBACK_PACKET(40, [?QOS0, ?QOS1]))]), - io:format("~s", [emqx_packet:format(?UNSUBSCRIBE_PACKET(89, [<<"t">>, <<"t2">>]))]), - io:format("~s", [emqx_packet:format(?UNSUBACK_PACKET(90))]). - -%%-------------------------------------------------------------------- -%% Message Cases -%%-------------------------------------------------------------------- - -message_make(_) -> - Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), - ?assertEqual(0, Msg#mqtt_message.qos), - Msg1 = emqx_message:make(<<"clientid">>, qos2, <<"topic">>, <<"payload">>), - ?assert(is_binary(Msg1#mqtt_message.id)), - ?assertEqual(2, Msg1#mqtt_message.qos). - -message_from_packet(_) -> - Msg = emqx_message:from_packet(?PUBLISH_PACKET(1, <<"topic">>, 10, <<"payload">>)), - ?assertEqual(1, Msg#mqtt_message.qos), - ?assertEqual(10, Msg#mqtt_message.pktid), - ?assertEqual(<<"topic">>, Msg#mqtt_message.topic), - WillMsg = emqx_message:from_packet(#mqtt_packet_connect{will_flag = true, - will_topic = <<"WillTopic">>, - will_msg = <<"WillMsg">>}), - ?assertEqual(<<"WillTopic">>, WillMsg#mqtt_message.topic), - ?assertEqual(<<"WillMsg">>, WillMsg#mqtt_message.payload), - - Msg2 = emqx_message:from_packet(<<"username">>, <<"clientid">>, - ?PUBLISH_PACKET(1, <<"topic">>, 20, <<"payload">>)), - ?assertEqual({<<"clientid">>, <<"username">>}, Msg2#mqtt_message.from), - io:format("~s", [emqx_message:format(Msg2)]). - -message_flag(_) -> - Pkt = ?PUBLISH_PACKET(1, <<"t">>, 2, <<"payload">>), - Msg2 = emqx_message:from_packet(<<"clientid">>, Pkt), - Msg3 = emqx_message:set_flag(retain, Msg2), - Msg4 = emqx_message:set_flag(dup, Msg3), - ?assert(Msg4#mqtt_message.dup), - ?assert(Msg4#mqtt_message.retain), - Msg5 = emqx_message:set_flag(Msg4), - Msg6 = emqx_message:unset_flag(dup, Msg5), - Msg7 = emqx_message:unset_flag(retain, Msg6), - ?assertNot(Msg7#mqtt_message.dup), - ?assertNot(Msg7#mqtt_message.retain), - emqx_message:unset_flag(Msg7), - emqx_message:to_packet(Msg7). - diff --git a/test/emqx_protocol_SUITE.erl.bk b/test/emqx_protocol_SUITE.erl.bk new file mode 100644 index 000000000..f2d6d306a --- /dev/null +++ b/test/emqx_protocol_SUITE.erl.bk @@ -0,0 +1,147 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx.hrl"). + +-include("emqx_mqtt.hrl"). + +-include_lib("eunit/include/eunit.hrl"). + +-import(emqx_serializer, [serialize/1]). + +all() -> + [{group, parser}, + {group, serializer}, + {group, packet}, + {group, message}]. + +groups() -> + [{parser, [], + [parse_connect, + parse_bridge, + parse_publish, + parse_puback, + parse_pubrec, + parse_pubrel, + parse_pubcomp, + parse_subscribe, + parse_unsubscribe, + parse_pingreq, + parse_disconnect]}, + {serializer, [], + [serialize_connect, + serialize_connack, + serialize_publish, + serialize_puback, + serialize_pubrel, + serialize_subscribe, + serialize_suback, + serialize_unsubscribe, + serialize_unsuback, + serialize_pingreq, + serialize_pingresp, + serialize_disconnect]}, + {packet, [], + [packet_proto_name, + packet_type_name, + packet_connack_name, + packet_format]}, + {message, [], + [message_make, + message_from_packet, + message_flag]}]. + + + +%%-------------------------------------------------------------------- +%% Packet Cases +%%-------------------------------------------------------------------- + +packet_proto_name(_) -> + ?assertEqual(<<"MQIsdp">>, emqx_packet:protocol_name(3)), + ?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(4)). + +packet_type_name(_) -> + ?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT)), + ?assertEqual('UNSUBSCRIBE', emqx_packet:type_name(?UNSUBSCRIBE)). + +packet_connack_name(_) -> + ?assertEqual('CONNACK_ACCEPT', emqx_packet:connack_name(?CONNACK_ACCEPT)), + ?assertEqual('CONNACK_PROTO_VER', emqx_packet:connack_name(?CONNACK_PROTO_VER)), + ?assertEqual('CONNACK_INVALID_ID', emqx_packet:connack_name(?CONNACK_INVALID_ID)), + ?assertEqual('CONNACK_SERVER', emqx_packet:connack_name(?CONNACK_SERVER)), + ?assertEqual('CONNACK_CREDENTIALS', emqx_packet:connack_name(?CONNACK_CREDENTIALS)), + ?assertEqual('CONNACK_AUTH', emqx_packet:connack_name(?CONNACK_AUTH)). + +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))]), + io:format("~s", [emqx_packet:format(?PUBLISH_PACKET(?QOS_2, <<"topic">>, 10, <<"payload">>))]), + io:format("~s", [emqx_packet:format(?PUBACK_PACKET(?PUBACK, 98))]), + io:format("~s", [emqx_packet:format(?PUBREL_PACKET(99))]), + io:format("~s", [emqx_packet:format(?SUBSCRIBE_PACKET(15, [{<<"topic">>, ?QOS0}, {<<"topic1">>, ?QOS1}]))]), + io:format("~s", [emqx_packet:format(?SUBACK_PACKET(40, [?QOS0, ?QOS1]))]), + io:format("~s", [emqx_packet:format(?UNSUBSCRIBE_PACKET(89, [<<"t">>, <<"t2">>]))]), + io:format("~s", [emqx_packet:format(?UNSUBACK_PACKET(90))]). + +%%-------------------------------------------------------------------- +%% Message Cases +%%-------------------------------------------------------------------- + +message_make(_) -> + Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), + ?assertEqual(0, Msg#mqtt_message.qos), + Msg1 = emqx_message:make(<<"clientid">>, qos2, <<"topic">>, <<"payload">>), + ?assert(is_binary(Msg1#mqtt_message.id)), + ?assertEqual(2, Msg1#mqtt_message.qos). + +message_from_packet(_) -> + Msg = emqx_message:from_packet(?PUBLISH_PACKET(1, <<"topic">>, 10, <<"payload">>)), + ?assertEqual(1, Msg#mqtt_message.qos), + ?assertEqual(10, Msg#mqtt_message.pktid), + ?assertEqual(<<"topic">>, Msg#mqtt_message.topic), + WillMsg = emqx_message:from_packet(#mqtt_packet_connect{will_flag = true, + will_topic = <<"WillTopic">>, + will_msg = <<"WillMsg">>}), + ?assertEqual(<<"WillTopic">>, WillMsg#mqtt_message.topic), + ?assertEqual(<<"WillMsg">>, WillMsg#mqtt_message.payload), + + Msg2 = emqx_message:from_packet(<<"username">>, <<"clientid">>, + ?PUBLISH_PACKET(1, <<"topic">>, 20, <<"payload">>)), + ?assertEqual({<<"clientid">>, <<"username">>}, Msg2#mqtt_message.from), + io:format("~s", [emqx_message:format(Msg2)]). + +message_flag(_) -> + Pkt = ?PUBLISH_PACKET(1, <<"t">>, 2, <<"payload">>), + Msg2 = emqx_message:from_packet(<<"clientid">>, Pkt), + Msg3 = emqx_message:set_flag(retain, Msg2), + Msg4 = emqx_message:set_flag(dup, Msg3), + ?assert(Msg4#mqtt_message.dup), + ?assert(Msg4#mqtt_message.retain), + Msg5 = emqx_message:set_flag(Msg4), + Msg6 = emqx_message:unset_flag(dup, Msg5), + Msg7 = emqx_message:unset_flag(retain, Msg6), + ?assertNot(Msg7#mqtt_message.dup), + ?assertNot(Msg7#mqtt_message.retain), + emqx_message:unset_flag(Msg7), + emqx_message:to_packet(Msg7). + diff --git a/test/emqx_router_SUITE.erl b/test/emqx_router_SUITE.erl index c46ae27c0..17da6d97d 100644 --- a/test/emqx_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -1,57 +1,44 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_router_SUITE). -include("emqx.hrl"). - -include_lib("eunit/include/eunit.hrl"). -compile(export_all). +-compile(nowarn_export_all). -define(R, emqx_router). +-define(TABS, [emqx_route, emqx_trie, emqx_trie_node]). all() -> - [{group, route}, - {group, local_route}]. + [{group, route}]. groups() -> [{route, [sequence], - [t_get_topics, - t_add_del_route, - t_match_route, - t_print, - t_has_route, - t_unused]}, - {local_route, [sequence], - [t_get_local_topics, - t_add_del_local_route, - t_match_local_route]}]. + [add_del_route, + match_routes]}]. init_per_suite(Config) -> - ekka:start(), - ekka_mnesia:ensure_started(), - {ok, _} = emqx_router_sup:start_link(), + emqx_ct_broker_helpers:run_setup_steps(), Config. end_per_suite(_Config) -> - emqx_router:stop(), - ekka:stop(), - ekka_mnesia:ensure_stopped(), - ekka_mnesia:delete_schema(). + emqx_ct_broker_helpers:run_teardown_steps(). init_per_testcase(_TestCase, Config) -> Config. @@ -59,87 +46,49 @@ init_per_testcase(_TestCase, Config) -> end_per_testcase(_TestCase, _Config) -> clear_tables(). -t_get_topics(_) -> - ?R:add_route(<<"a/b/c">>), - ?R:add_route(<<"a/b/c">>), - ?R:add_route(<<"a/+/b">>), +add_del_route(_) -> + From = {self(), make_ref()}, + ?R:add_route(From, <<"a/b/c">>, node()), + ?R:add_route(From, <<"a/b/c">>, node()), + ?R:add_route(From, <<"a/+/b">>, node()), ?assertEqual([<<"a/+/b">>, <<"a/b/c">>], lists:sort(?R:topics())), - ?R:del_route(<<"a/b/c">>), - ?R:del_route(<<"a/+/b">>), + ?R:del_route(From, <<"a/b/c">>, node()), + ?R:del_route(From, <<"a/+/b">>, node()), ?assertEqual([], lists:sort(?R:topics())). -t_add_del_route(_) -> - %%Node = node(), - ?R:add_route(<<"a/b/c">>), - ?R:add_route(<<"a/+/b">>), - ?R:del_route(<<"a/b/c">>), - ?R:del_route(<<"a/+/b">>). +match_routes(_) -> + From = {self(), make_ref()}, + ?R:add_route(From, <<"a/b/c">>, node()), + ?R:add_route(From, <<"a/+/c">>, node()), + ?R:add_route(From, <<"a/b/#">>, node()), + ?R:add_route(From, <<"#">>, node()), + ?assertEqual([#route{topic = <<"#">>, dest = node()}, + #route{topic = <<"a/+/c">>, dest = node()}, + #route{topic = <<"a/b/#">>, dest = node()}, + #route{topic = <<"a/b/c">>, dest = node()}], + lists:sort(?R:match_routes(<<"a/b/c">>))). -t_match_route(_) -> - Node = node(), - ?R:add_route(<<"a/b/c">>), - ?R:add_route(<<"a/+/c">>), - ?R:add_route(<<"a/b/#">>), - ?R:add_route(<<"#">>), - ?assertEqual([#route{topic = <<"#">>, node = Node}, - #route{topic = <<"a/+/c">>, node = Node}, - #route{topic = <<"a/b/#">>, node = Node}, - #route{topic = <<"a/b/c">>, node = Node}], - lists:sort(?R:match(<<"a/b/c">>))). - -t_has_route(_) -> - ?R:add_route(<<"devices/+/messages">>), - ?assert(?R:has_route(<<"devices/+/messages">>)). - -t_get_local_topics(_) -> - ?R:add_local_route(<<"a/b/c">>), - ?R:add_local_route(<<"x/+/y">>), - ?R:add_local_route(<<"z/#">>), - ?assertEqual([<<"z/#">>, <<"x/+/y">>, <<"a/b/c">>], ?R:local_topics()), - ?R:del_local_route(<<"x/+/y">>), - ?R:del_local_route(<<"z/#">>), - ?assertEqual([<<"a/b/c">>], ?R:local_topics()). - -t_add_del_local_route(_) -> - Node = node(), - ?R:add_local_route(<<"a/b/c">>), - ?R:add_local_route(<<"x/+/y">>), - ?R:add_local_route(<<"z/#">>), - ?assertEqual([{<<"a/b/c">>, Node}, - {<<"x/+/y">>, Node}, - {<<"z/#">>, Node}], - lists:sort(?R:get_local_routes())), - ?R:del_local_route(<<"x/+/y">>), - ?R:del_local_route(<<"z/#">>), - ?assertEqual([{<<"a/b/c">>, Node}], lists:sort(?R:get_local_routes())). - -t_match_local_route(_) -> - ?R:add_local_route(<<"$SYS/#">>), - ?R:add_local_route(<<"a/b/c">>), - ?R:add_local_route(<<"a/+/c">>), - ?R:add_local_route(<<"a/b/#">>), - ?R:add_local_route(<<"#">>), - Matched = [Topic || #route{topic = {local, Topic}} <- ?R:match_local(<<"a/b/c">>)], - ?assertEqual([<<"#">>, <<"a/+/c">>, <<"a/b/#">>, <<"a/b/c">>], lists:sort(Matched)). +has_routes(_) -> + From = {self(), make_ref()}, + ?R:add_route(From, <<"devices/+/messages">>, node()), + ?assert(?R:has_routes(<<"devices/+/messages">>)). clear_tables() -> - ?R:clean_local_routes(), - lists:foreach(fun mnesia:clear_table/1, [route, trie, trie_node]). + lists:foreach(fun mnesia:clear_table/1, ?TABS). router_add_del(_) -> - %% Add ?R:add_route(<<"#">>), ?R:add_route(<<"a/b/c">>), ?R:add_route(<<"+/#">>), Routes = [R1, R2 | _] = [ - #route{topic = <<"#">>, node = node()}, - #route{topic = <<"+/#">>, node = node()}, - #route{topic = <<"a/b/c">>, node = node()}], - Routes = lists:sort(?R:match(<<"a/b/c">>)), + #route{topic = <<"#">>, dest = node()}, + #route{topic = <<"+/#">>, dest = node()}, + #route{topic = <<"a/b/c">>, dest = node()}], + ?assertEqual(Routes, lists:sort(?R:match_routes(<<"a/b/c">>))), %% Batch Add lists:foreach(fun(R) -> ?R:add_route(R) end, Routes), - Routes = lists:sort(?R:match(<<"a/b/c">>)), + ?assertEqual(Routes, lists:sort(?R:match_routes(<<"a/b/c">>))), %% Del ?R:del_route(<<"a/b/c">>), @@ -147,25 +96,10 @@ router_add_del(_) -> {atomic, []} = mnesia:transaction(fun emqx_trie:lookup/1, [<<"a/b/c">>]), %% Batch Del - R3 = #route{topic = <<"#">>, node = 'a@127.0.0.1'}, + R3 = #route{topic = <<"#">>, dest = 'a@127.0.0.1'}, ?R:add_route(R3), ?R:del_route(R1), ?R:del_route(R2), ?R:del_route(R3), [] = lists:sort(?R:match(<<"a/b/c">>)). -t_print(_) -> - Routes = [#route{topic = <<"a/b/c">>, node = node()}, - #route{topic = <<"#">>, node = node()}, - #route{topic = <<"+/#">>, node = node()}], - lists:foreach(fun(R) -> ?R:add_route(R) end, Routes), - ?R:print(<<"a/b/c">>), - ?R:del_route(<<"+/#">>), - ?R:del_route(<<"a/b/c">>), - ?R:del_route(<<"#">>). - -t_unused(_) -> - gen_server:call(?R, bad_call), - gen_server:cast(?R, bad_msg), - ?R ! bad_info. - diff --git a/test/emqx_time_SUITE.erl b/test/emqx_time_SUITE.erl index 9a0514e74..8ff8a4437 100644 --- a/test/emqx_time_SUITE.erl +++ b/test/emqx_time_SUITE.erl @@ -19,6 +19,7 @@ -include_lib("eunit/include/eunit.hrl"). -compile(export_all). +-compile(nowarn_export_all). all() -> [t_time_now_to]. diff --git a/test/emqx_topic_SUITE.erl b/test/emqx_topic_SUITE.erl index a60921ada..87fb2c906 100644 --- a/test/emqx_topic_SUITE.erl +++ b/test/emqx_topic_SUITE.erl @@ -20,6 +20,7 @@ %% CT -compile(export_all). +-compile(nowarn_export_all). -import(emqx_topic, [wildcard/1, match/2, validate/1, triples/1, join/1, words/1, systop/1, feed_var/3, parse/1, parse/2]). diff --git a/test/emqx_trie_SUITE.erl b/test/emqx_trie_SUITE.erl index 87eb52fea..98d14e7e1 100644 --- a/test/emqx_trie_SUITE.erl +++ b/test/emqx_trie_SUITE.erl @@ -1,39 +1,40 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_trie_SUITE). -compile(export_all). +-compile(nowarn_export_all). -include("emqx.hrl"). +-include_lib("eunit/include/eunit.hrl"). -define(TRIE, emqx_trie). - --include_lib("eunit/include/eunit.hrl"). +-define(TRIE_TABS, [emqx_trie, emqx_trie_node]). all() -> [t_insert, t_match, t_match2, t_match3, t_delete, t_delete2, t_delete3]. init_per_suite(Config) -> - ekka_mnesia:ensure_started(), - ?TRIE:mnesia(boot), - ?TRIE:mnesia(copy), + application:load(emqx), + ok = ekka:start(), Config. end_per_suite(_Config) -> + ekka:stop(), ekka_mnesia:ensure_stopped(), ekka_mnesia:delete_schema(). @@ -131,5 +132,5 @@ t_delete3(_) -> end). clear_tables() -> - lists:foreach(fun mnesia:clear_table/1, [emqx_trie, emqx_trie_node]). + lists:foreach(fun mnesia:clear_table/1, ?TRIE_TABS). diff --git a/test/emqx_vm_SUITE.erl b/test/emqx_vm_SUITE.erl index 07f6a22cc..1f5c4b2b0 100644 --- a/test/emqx_vm_SUITE.erl +++ b/test/emqx_vm_SUITE.erl @@ -17,6 +17,7 @@ -module(emqx_vm_SUITE). -compile(export_all). +-compile(nowarn_export_all). -include_lib("common_test/include/ct.hrl"). From 055de617fc92bb82e4f3bbf8746202413ed94682 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 31 May 2018 21:57:17 +0800 Subject: [PATCH 056/520] Add emqx_message:make/4 and fix whitespace --- src/emqx_message.erl | 40 ++++++++++++++++++++++------------------ src/emqx_topic.erl | 2 +- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/emqx_message.erl b/src/emqx_message.erl index b8b919ee9..04d9c1d9c 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -1,24 +1,25 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_message). -include("emqx.hrl"). +-include("emqx_mqtt.hrl"). --export([make/3]). +-export([make/3, make/4]). -export([get_flag/2, get_flag/3, set_flag/2, unset_flag/2]). @@ -29,8 +30,11 @@ %% Create a default message -spec(make(atom() | client(), topic(), payload()) -> message()). make(From, Topic, Payload) when is_atom(From); is_record(From, client) -> + make(From, ?QOS_0, Topic, Payload). + +make(From, QoS, Topic, Payload) when is_atom(From); is_record(From, client) -> #message{id = msgid(), - qos = 0, + qos = ?QOS_I(QoS), from = From, sender = self(), flags = #{}, @@ -55,7 +59,7 @@ set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) -> %% @doc Unset flag -spec(unset_flag(message_flag(), message()) -> message()). -unset_flag(Flag, Msg = #message{flags = Flags}) -> +unset_flag(Flag, Msg = #message{flags = Flags}) -> Msg#message{flags = maps:remove(Flag, Flags)}. %% @doc Get header diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index 8cd7ce3b6..a642b5951 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -44,7 +44,7 @@ -spec(wildcard(topic() | words()) -> true | false). wildcard(Topic) when is_binary(Topic) -> wildcard(words(Topic)); -wildcard([]) -> +wildcard([]) -> false; wildcard(['#'|_]) -> true; From 5d45d40db5175fae99d52e3a074de3a79d37af68 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 12 Jun 2018 10:04:01 +0800 Subject: [PATCH 057/520] Rename emqx_mqtt module to emqx_listeners --- src/{emqx_mqtt.erl => emqx_listeners.erl} | 70 ++++++++++------------- 1 file changed, 29 insertions(+), 41 deletions(-) rename src/{emqx_mqtt.erl => emqx_listeners.erl} (65%) diff --git a/src/emqx_mqtt.erl b/src/emqx_listeners.erl similarity index 65% rename from src/emqx_mqtt.erl rename to src/emqx_listeners.erl index d7a7281cf..d63a6d620 100644 --- a/src/emqx_mqtt.erl +++ b/src/emqx_listeners.erl @@ -1,45 +1,36 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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). +-module(emqx_listeners). -include("emqx_mqtt.hrl"). --export([bootstrap/0, shutdown/0]). - --export([start_listeners/0, start_listener/1]). --export([stop_listeners/0, stop_listener/1]). --export([restart_listeners/0, restart_listener/1]). --export([listeners/0]). +-export([start/0, restart/0, stop/0]). +-export([start_listener/1, stop_listener/1, restart_listener/1]). +-export([all/0]). -type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}). -bootstrap() -> - start_listeners(). - -shutdown() -> - stop_listeners(). - %%-------------------------------------------------------------------- %% Start/Stop Listeners %%-------------------------------------------------------------------- %% @doc Start Listeners. --spec(start_listeners() -> ok). -start_listeners() -> +-spec(start() -> ok). +start() -> lists:foreach(fun start_listener/1, emqx_config:get_env(listeners, [])). %% Start mqtt listener @@ -65,7 +56,7 @@ start_listener(Proto, ListenOn, Opts) -> MFArgs = {emqx_connection, start_link, [Env]}, {ok, _} = esockd:open(Proto, ListenOn, merge_sockopts(Opts), MFArgs). -listeners() -> +all() -> [Listener || Listener = {{Proto, _}, _Pid} <- esockd:listeners(), is_mqtt(Proto)]. is_mqtt('mqtt:tcp') -> true; @@ -75,8 +66,8 @@ is_mqtt('mqtt:wss') -> true; is_mqtt(_Proto) -> false. %% @doc Stop Listeners --spec(stop_listeners() -> ok). -stop_listeners() -> +-spec(stop() -> ok). +stop() -> lists:foreach(fun stop_listener/1, emqx_config:get_env(listeners, [])). -spec(stop_listener(listener()) -> ok | {error, any()}). @@ -88,16 +79,13 @@ stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> mochiweb:stop_http('mqtt:ws', ListenOn); stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> mochiweb:stop_http('mqtt:wss', ListenOn); -% stop_listener({Proto, ListenOn, _Opts}) when Proto == api -> -% mochiweb:stop_http('mqtt:api', ListenOn); stop_listener({Proto, ListenOn, _Opts}) -> esockd:close(Proto, ListenOn). %% @doc Restart Listeners --spec(restart_listeners() -> ok). -restart_listeners() -> - lists:foreach(fun restart_listener/1, - emqx_config:get_env(listeners, [])). +-spec(restart() -> ok). +restart() -> + lists:foreach(fun restart_listener/1, emqx_config:get_env(listeners, [])). -spec(restart_listener(listener()) -> any()). restart_listener({tcp, ListenOn, _Opts}) -> @@ -108,13 +96,13 @@ restart_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> mochiweb:restart_http('mqtt:ws', ListenOn); restart_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> mochiweb:restart_http('mqtt:wss', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == api -> - mochiweb:restart_http('mqtt:api', ListenOn); restart_listener({Proto, ListenOn, _Opts}) -> esockd:reopen(Proto, ListenOn). merge_sockopts(Options) -> + %%TODO: tcp_options? SockOpts = emqx_misc:merge_opts( ?MQTT_SOCKOPTS, proplists:get_value(sockopts, Options, [])), emqx_misc:merge_opts(Options, [{sockopts, SockOpts}]). + From b733a3bcfb89fb9b2cfcfbd4922035177d176bfd Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 12 Jun 2018 12:10:53 +0800 Subject: [PATCH 058/520] Throw mqtt_frame_too_large exception if the sent frame is too large --- src/emqx_frame.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 61f8f9e50..ae2ab010d 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -378,7 +378,7 @@ serialize(#mqtt_packet_header{type = Type, retain = Retain}, VariableBin, PayloadBin) when ?CONNECT =< Type andalso Type =< ?AUTH -> Len = iolist_size(VariableBin) + iolist_size(PayloadBin), - true = (Len =< ?MAX_PACKET_SIZE), + (Len =< ?MAX_PACKET_SIZE) orelse error(mqtt_frame_too_large), [<>, serialize_remaining_len(Len), VariableBin, PayloadBin]. From 1de94b68588df8f61f398db79bd166b0091c7e30 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jun 2018 22:40:30 +0800 Subject: [PATCH 059/520] Register the default ACL module in emqx_access_control --- Makefile | 17 ++++++----- src/emqx_access_control.erl | 15 +++++++++- src/emqx_app.erl | 58 ++++++++++++++----------------------- src/emqx_broker.erl | 15 ++++++++-- src/emqx_message.erl | 11 +++---- 5 files changed, 63 insertions(+), 53 deletions(-) diff --git a/Makefile b/Makefile index 0718648f8..42eaffc57 100644 --- a/Makefile +++ b/Makefile @@ -14,9 +14,9 @@ dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_getopt = git https://github.com/jcomellas/getopt v0.8.2 dep_lager = git https://github.com/basho/lager master dep_lager_syslog = git https://github.com/basho/lager_syslog -dep_esockd = git https://github.com/emqtt/esockd v5.2.1 -dep_ekka = git https://github.com/emqtt/ekka v0.2.2 -dep_mochiweb = git https://github.com/emqtt/mochiweb v4.2.2 +dep_esockd = git https://github.com/emqtt/esockd emqx30 +dep_ekka = git https://github.com/emqtt/ekka develop +dep_mochiweb = git https://github.com/emqtt/mochiweb emqx30 dep_pbkdf2 = git https://github.com/emqtt/pbkdf2 2.0.1 dep_bcrypt = git https://github.com/smarkets/erlang-bcrypt master dep_clique = git https://github.com/emqtt/clique @@ -29,16 +29,19 @@ ERLC_OPTS += +'{parse_transform, lager_transform}' BUILD_DEPS = cuttlefish dep_cuttlefish = git https://github.com/emqtt/cuttlefish -TEST_DEPS = emqttc -dep_emqttc = git https://github.com/emqtt/emqttc +TEST_DEPS = emqx_ct_helplers +dep_emqx_ct_helplers = git git@github.com:emqx/emqx_ct_helpers TEST_ERLC_OPTS += +debug_info TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose -CT_SUITES = emqx emqx_broker emqx_mod emqx_lib emqx_topic emqx_trie emqx_mqueue emqx_inflight \ - emqx_vm emqx_net emqx_protocol emqx_access emqx_router +CT_SUITES = emqx_inflight +## emqx_trie emqx_router emqx_frame emqx_mqtt_compat + +#CT_SUITES = emqx emqx_broker emqx_mod emqx_lib emqx_topic emqx_mqueue emqx_inflight \ +# emqx_vm emqx_net emqx_protocol emqx_access emqx_router CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqxct@127.0.0.1 diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index aa51c0df0..869ef2649 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -42,7 +42,20 @@ %% @doc Start access control server. -spec(start_link() -> {ok, pid()} | ignore | {error, term()}). start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + case gen_server:start_link({local, ?SERVER}, ?MODULE, [], []) of + {ok, Pid} -> + ok = register_default_mod(), + {ok, Pid}; + {error, Reason} -> + {error, Reason} + end. + +register_default_mod() -> + case emqx_config:get_env(acl_file) of + {ok, File} -> + emqx_access_control:register_mod(acl, emqx_acl_internal, [File]); + undefined -> ok + end. %% @doc Authenticate Client. -spec(auth(Client :: client(), Password :: password()) diff --git a/src/emqx_app.erl b/src/emqx_app.erl index f7a51fc7f..54926f2a7 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -1,25 +1,23 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%-------------------------------------------------------------------- +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_app). -behaviour(application). --include("emqx_mqtt.hrl"). - %% Application callbacks -export([start/2, stop/1]). @@ -33,9 +31,10 @@ start(_Type, _Args) -> print_banner(), ekka:start(), {ok, Sup} = emqx_sup:start_link(), - %%TODO: fixme later - ok = register_acl_mod(), emqx_modules:load(), + emqx_plugins:init(), + emqx_plugins:load(), + emqx_listeners:start(), start_autocluster(), register(emqx, self()), print_vsn(), @@ -43,8 +42,8 @@ start(_Type, _Args) -> -spec(stop(State :: term()) -> term()). stop(_State) -> - emqx_modules:unload(), - catch emqx_mqtt:shutdown(). + emqx_listeners:stop(), + emqx_modules:unload(). %%-------------------------------------------------------------------- %% Print Banner @@ -57,16 +56,6 @@ print_vsn() -> {ok, Vsn} = application:get_key(vsn), io:format("~s ~s is running now!~n", [?APP, Vsn]). -%%-------------------------------------------------------------------- -%% Register default ACL File -%%-------------------------------------------------------------------- - -register_acl_mod() -> - case emqx_config:get_env(acl_file) of - {ok, File} -> emqx_access_control:register_mod(acl, emqx_acl_internal, [File]); - undefined -> ok - end. - %%-------------------------------------------------------------------- %% Autocluster %%-------------------------------------------------------------------- @@ -74,10 +63,5 @@ register_acl_mod() -> start_autocluster() -> ekka:callback(prepare, fun emqx:shutdown/1), ekka:callback(reboot, fun emqx:reboot/0), - ekka:autocluster(?APP, fun after_autocluster/0). - -after_autocluster() -> - emqx_plugins:init(), - emqx_plugins:load(), - emqx_mqtt:bootstrap(). + ekka:autocluster(?APP). diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index aaacb086d..5a6c9d954 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -28,6 +28,7 @@ -export([dispatch/2, dispatch/3]). -export([subscriptions/1, subscribers/1, subscribed/2]). -export([get_subopts/2, set_subopts/3]). +-export([topics/0]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -95,13 +96,17 @@ unsubscribe(Topic, Subscriber, Timeout) -> %% Publish %%-------------------------------------------------------------------- +-spec(publish(topic(), payload()) -> delivery() | stopped). +publish(Topic, Payload) when is_binary(Topic), is_binary(Payload) -> + publish(emqx_message:make(Topic, Payload)). + -spec(publish(message()) -> delivery() | stopped). publish(Msg = #message{from = From}) -> %% Hook to trace? trace(publish, From, Msg), case emqx_hooks:run('message.publish', [], Msg) of {ok, Msg1 = #message{topic = Topic}} -> - publish(Topic, Msg1); + route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1)); {stop, Msg1} -> emqx_logger:warning("Stop publishing: ~s", [emqx_message:format(Msg1)]), stopped @@ -123,8 +128,9 @@ trace(public, From, #message{topic = Topic, payload = Payload}) emqx_logger:info([{client, From}, {topic, Topic}], "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). -publish(Topic, Msg) -> - route(aggre(emqx_router:match_routes(Topic)), delivery(Msg)). +%%-------------------------------------------------------------------- +%% Route +%%-------------------------------------------------------------------- route([], Delivery = #delivery{message = Msg}) -> emqx_hooks:run('message.dropped', [undefined, Msg]), @@ -258,6 +264,9 @@ pick(SubId) when is_binary(SubId) -> pick({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> pick(SubId). +-spec(topics() -> [topic()]). +topics() -> emqx_router:topics(). + %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 04d9c1d9c..79adf6ad1 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -19,15 +19,16 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([make/3, make/4]). - +-export([make/2, make/3, make/4]). -export([get_flag/2, get_flag/3, set_flag/2, unset_flag/2]). - -export([get_header/2, get_header/3, set_header/3]). - -export([get_user_property/2, get_user_property/3, set_user_property/3]). -%% Create a default message +-spec(make(topic(), payload()) -> message()). +make(Topic, Payload) -> + make(undefined, Topic, Payload). + +%% Create a message -spec(make(atom() | client(), topic(), payload()) -> message()). make(From, Topic, Payload) when is_atom(From); is_record(From, client) -> make(From, ?QOS_0, Topic, Payload). From 385c7cd3e64d035229fcec2fa6f8042bda92f7e8 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 29 Jun 2018 12:26:30 +0800 Subject: [PATCH 060/520] Remove 'tuple call' and be compatible with Erlang/OTP R21 --- src/emqx_cm.erl | 22 +++++++++------------- src/emqx_pmon.erl | 10 ++++------ src/emqx_shared_sub.erl | 18 +++++++----------- src/emqx_sm.erl | 22 +++++++++------------- 4 files changed, 29 insertions(+), 43 deletions(-) diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index a7e7f6356..9e3edf0b7 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -133,26 +133,22 @@ handle_call(Req, _From, State) -> emqx_logger:error("[CM] Unexpected request: ~p", [Req]), {reply, ignore, State}. -handle_cast({notify, {registered, ClientId, Pid}}, - State = #state{client_pmon = PMon}) -> - {noreply, State#state{client_pmon = PMon:monitor(Pid, ClientId)}}; +handle_cast({notify, {registered, ClientId, Pid}}, State = #state{client_pmon = PMon}) -> + {noreply, State#state{client_pmon = emqx_pmon:monitor(Pid, ClientId, PMon)}}; -handle_cast({notify, {unregistered, _ClientId, Pid}}, - State = #state{client_pmon = PMon}) -> - {noreply, State#state{client_pmon = PMon:demonitor(Pid)}}; +handle_cast({notify, {unregistered, _ClientId, Pid}}, State = #state{client_pmon = PMon}) -> + {noreply, State#state{client_pmon = emqx_pmon:demonitor(Pid, PMon)}}; handle_cast(Msg, State) -> emqx_logger:error("[CM] Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, DownPid, _Reason}, - State = #state{client_pmon = PMon}) -> - case PMon:find(DownPid) of - undefined -> - {noreply, State}; - ClientId -> +handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{client_pmon = PMon}) -> + case emqx_pmon:find(DownPid, PMon) of + undefined -> {noreply, State}; + ClientId -> unregister_client({ClientId, DownPid}), - {noreply, State#state{client_pmon = PMon:erase(DownPid)}} + {noreply, State#state{client_pmon = emqx_pmon:erase(DownPid, PMon)}} end; handle_info(Info, State) -> diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index bcb4ae537..6af403a2d 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -35,10 +35,9 @@ monitor(Pid, PM) -> monitor(Pid, Val, PM = {?MODULE, [M]}) -> case maps:is_key(Pid, M) of - true -> PM; - false -> - Ref = erlang:monitor(process, Pid), - {?MODULE, [maps:put(Pid, {Ref, Val}, M)]} + true -> PM; + false -> Ref = erlang:monitor(process, Pid), + {?MODULE, [maps:put(Pid, {Ref, Val}, M)]} end. -spec(demonitor(pid(), pmon()) -> pmon()). @@ -48,8 +47,7 @@ demonitor(Pid, PM = {?MODULE, [M]}) -> %% Don't flush _ = erlang:demonitor(Ref), {?MODULE, [maps:remove(Pid, M)]}; - error -> - PM + error -> PM end. -spec(find(pid(), pmon()) -> undefined | term()). diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index f4f0ecf8e..6825ca8d0 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -116,7 +116,7 @@ init([]) -> init_monitors() -> mnesia:foldl( fun(#shared_subscription{subpid = SubPid}, Mon) -> - Mon:monitor(SubPid) + emqx_pmon:monitor(SubPid, Mon) end, emqx_pmon:new(), ?TAB). handle_call(Req, _From, State) -> @@ -124,7 +124,7 @@ handle_call(Req, _From, State) -> {reply, ignore, State}. handle_cast({monitor, SubPid}, State= #state{pmon = PMon}) -> - {noreply, update_stats(State#state{pmon = PMon:monitor(SubPid)})}; + {noreply, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})}; handle_cast(Msg, State) -> emqx_logger:error("[Shared] Unexpected msg: ~p", [Msg]), @@ -132,11 +132,11 @@ handle_cast(Msg, State) -> handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = PMon}) -> #shared_subscription{subpid = SubPid} = NewRecord, - {noreply, update_stats(State#state{pmon = PMon:monitor(SubPid)})}; + {noreply, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})}; handle_info({mnesia_table_event, {delete_object, OldRecord, _}}, State = #state{pmon = PMon}) -> #shared_subscription{subpid = SubPid} = OldRecord, - {noreply, update_stats(State#state{pmon = PMon:demonitor(SubPid)})}; + {noreply, update_stats(State#state{pmon = emqx_pmon:demonitor(SubPid, PMon)})}; handle_info({mnesia_table_event, _Event}, State) -> {noreply, State}; @@ -144,7 +144,7 @@ handle_info({mnesia_table_event, _Event}, State) -> handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) -> emqx_logger:info("Shared subscription down: ~p", [SubPid]), mnesia:async_dirty(fun cleanup_down/1, [SubPid]), - {noreply, update_stats(State#state{pmon = PMon:erase(SubPid)})}; + {noreply, update_stats(State#state{pmon = emqx_pmon:erase(SubPid, PMon)})}; handle_info(Info, State) -> emqx_logger:error("[Shared] Unexpected info: ~p", [Info]), @@ -162,12 +162,8 @@ code_change(_OldVsn, State, _Extra) -> cleanup_down(SubPid) -> Pat = #shared_subscription{_ = '_', subpid = SubPid}, - lists:foreach(fun(Record) -> - mnesia:delete_object(?TAB, Record) - end, mnesia:match_object(Pat)). + lists:foreach(fun(Record) -> mnesia:delete_object(?TAB, Record) end, mnesia:match_object(Pat)). update_stats(State) -> - emqx_stats:setstat('subscriptions/shared/count', - 'subscriptions/shared/max', - ets:info(?TAB, size)), State. + emqx_stats:setstat('subscriptions/shared/count', 'subscriptions/shared/max', ets:info(?TAB, size)), State. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index e2e5ca20c..50ce9cb75 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -216,26 +216,22 @@ handle_call(Req, _From, State) -> emqx_logger:error("[SM] Unexpected request: ~p", [Req]), {reply, ignore, State}. -handle_cast({notify, {registered, ClientId, SessionPid}}, - State = #state{session_pmon = PMon}) -> - {noreply, State#state{session_pmon = PMon:monitor(SessionPid, ClientId)}}; +handle_cast({notify, {registered, ClientId, SessionPid}}, State = #state{session_pmon = PMon}) -> + {noreply, State#state{session_pmon = emqx_pmon:monitor(SessionPid, ClientId, PMon)}}; -handle_cast({notify, {unregistered, _ClientId, SessionPid}}, - State = #state{session_pmon = PMon}) -> - {noreply, State#state{session_pmon = PMon:demonitor(SessionPid)}}; +handle_cast({notify, {unregistered, _ClientId, SessionPid}}, State = #state{session_pmon = PMon}) -> + {noreply, State#state{session_pmon = emqx_pmon:demonitor(SessionPid, PMon)}}; handle_cast(Msg, State) -> emqx_logger:error("[SM] Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, DownPid, _Reason}, - State = #state{session_pmon = PMon}) -> - case PMon:find(DownPid) of - undefined -> - {noreply, State}; - ClientId -> +handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{session_pmon = PMon}) -> + case emqx_pmon:find(DownPid, PMon) of + undefined -> {noreply, State}; + ClientId -> unregister_session({ClientId, DownPid}), - {noreply, State#state{session_pmon = PMon:erase(DownPid)}} + {noreply, State#state{session_pmon = emqx_pmon:erase(DownPid, PMon)}} end; handle_info(Info, State) -> From 6f405dc12834b705d373ca22c22e9c0fcea1ec95 Mon Sep 17 00:00:00 2001 From: chenyy Date: Fri, 29 Jun 2018 15:10:12 +0800 Subject: [PATCH 061/520] =?UTF-8?q?remove=20=3FMODULE=20because=20we=20don?= =?UTF-8?q?=E2=80=99t=20need=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/emqx_pmon.erl | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index 6af403a2d..f6b4611ec 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -20,45 +20,45 @@ -export([monitor/2, monitor/3, demonitor/2, find/2, erase/2]). --compile({no_auto_import,[monitor/3]}). - --type(pmon() :: {?MODULE, map()}). +-compile({no_auto_import,[monitor/3]}). +-type(pmon() :: map()). -export_type([pmon/0]). +-spec(new() -> pmon()). new() -> - {?MODULE, [maps:new()]}. + maps:new(). -spec(monitor(pid(), pmon()) -> pmon()). monitor(Pid, PM) -> monitor(Pid, undefined, PM). -monitor(Pid, Val, PM = {?MODULE, [M]}) -> - case maps:is_key(Pid, M) of +monitor(Pid, Val, PM) -> + case maps:is_key(Pid, PM) of true -> PM; false -> Ref = erlang:monitor(process, Pid), - {?MODULE, [maps:put(Pid, {Ref, Val}, M)]} + maps:put(Pid, {Ref, Val}, PM) end. -spec(demonitor(pid(), pmon()) -> pmon()). -demonitor(Pid, PM = {?MODULE, [M]}) -> - case maps:find(Pid, M) of +demonitor(Pid, PM) -> + case maps:find(Pid, PM) of {ok, {Ref, _Val}} -> %% Don't flush _ = erlang:demonitor(Ref), - {?MODULE, [maps:remove(Pid, M)]}; + maps:remove(Pid, PM); error -> PM end. -spec(find(pid(), pmon()) -> undefined | term()). -find(Pid, {?MODULE, [M]}) -> - case maps:find(Pid, M) of +find(Pid, PM) -> + case maps:find(Pid, PM) of {ok, {_Ref, Val}} -> Val; error -> undefined end. -spec(erase(pid(), pmon()) -> pmon()). -erase(Pid, {?MODULE, [M]}) -> - {?MODULE, [maps:remove(Pid, M)]}. +erase(Pid, PM) -> + maps:remove(Pid, PM). From 62aa072f2fcd567cf8e009d8324af596470d9dfe Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 18 Jul 2018 13:43:17 +0800 Subject: [PATCH 062/520] Upgrade for MQTT Version 5.0 and Erlang/OTP R21 --- src/emqx.app.src | 2 +- src/emqx.erl | 56 ++--- src/emqx_access_control.erl | 28 ++- src/emqx_access_rule.erl | 13 +- src/emqx_acl_internal.erl | 28 ++- src/emqx_acl_mod.erl | 4 +- src/{emqx_alarm.erl => emqx_alarm_mgr.erl} | 110 +++++----- src/emqx_app.erl | 31 ++- src/emqx_auth_mod.erl | 4 +- src/emqx_banned.erl | 32 ++- src/emqx_base62.erl | 4 +- src/emqx_boot.erl | 63 ------ src/emqx_bridge.erl | 73 +++---- src/emqx_bridge_sup.erl | 11 +- src/emqx_bridge_sup_sup.erl | 49 ++--- src/emqx_broker.erl | 130 ++++++------ src/emqx_broker_helper.erl | 46 ++-- src/emqx_broker_sup.erl | 42 ++-- src/emqx_cli.erl | 28 ++- src/emqx_client.erl | 50 +++-- src/emqx_client_sock.erl | 31 ++- src/emqx_cm.erl | 89 ++++---- src/emqx_cm_sup.erl | 39 ++-- src/emqx_config.erl | 6 +- src/emqx_connection.erl | 34 ++- src/emqx_ctl.erl | 60 +++--- src/emqx_flapping.erl | 5 +- src/emqx_flow_control.erl | 28 ++- src/emqx_frame.erl | 62 +++--- src/emqx_gc.erl | 9 +- src/emqx_gen_mod.erl | 4 +- src/emqx_guid.erl | 8 +- src/emqx_hooks.erl | 28 ++- src/emqx_inflight.erl | 69 +++--- src/emqx_json.erl | 28 ++- src/emqx_keepalive.erl | 28 ++- src/emqx_kernel_sup.erl | 44 ++-- src/emqx_lager_backend.erl | 4 +- src/emqx_listeners.erl | 149 +++++++------ src/emqx_logger.erl | 38 ++-- src/emqx_message.erl | 61 +++--- src/emqx_metrics.erl | 60 +++--- src/emqx_misc.erl | 33 ++- src/emqx_mod_presence.erl | 36 ++-- src/emqx_mod_rewrite.erl | 4 +- src/emqx_mod_subscription.erl | 32 ++- src/emqx_mod_sup.erl | 18 +- src/emqx_modules.erl | 28 ++- src/emqx_mqtt_properties.erl | 29 ++- src/emqx_mqueue.erl | 11 +- src/emqx_net.erl | 232 --------------------- src/emqx_packet.erl | 28 +-- src/emqx_pmon.erl | 71 +++---- src/emqx_pool.erl | 78 +++---- src/emqx_pool_sup.erl | 36 ++-- src/emqx_protocol.erl | 13 +- src/emqx_reason_codes.erl | 33 ++- src/emqx_router.erl | 89 ++++---- src/emqx_router_helper.erl | 63 +++--- src/emqx_router_sup.erl | 30 ++- src/emqx_rpc.erl | 32 ++- src/emqx_session_sup.erl | 36 ++-- src/emqx_shared_sub.erl | 69 +++--- src/emqx_sm.erl | 72 +++---- src/emqx_sm_locker.erl | 39 ++-- src/emqx_sm_registry.erl | 60 +++--- src/emqx_sm_sup.erl | 60 +++--- src/emqx_stats.erl | 70 +++---- src/emqx_sys.erl | 104 ++++----- src/emqx_sys_mon.erl | 138 ++++++------ src/emqx_sys_sup.erl | 47 +++-- src/emqx_tables.erl | 28 ++- src/emqx_time.erl | 28 ++- src/emqx_topic.erl | 53 ++--- src/emqx_tracer.erl | 82 ++++---- src/emqx_trie.erl | 49 ++--- 76 files changed, 1476 insertions(+), 2043 deletions(-) rename src/{emqx_alarm.erl => emqx_alarm_mgr.erl} (52%) delete mode 100644 src/emqx_boot.erl delete mode 100644 src/emqx_net.erl diff --git a/src/emqx.app.src b/src/emqx.app.src index 8798b6eab..f3ccb8fa3 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -6,6 +6,6 @@ {applications,[kernel,stdlib,gproc,lager,esockd,mochiweb,lager_syslog,pbkdf2,bcrypt,clique,jsx]}, {env,[]}, {mod,{emqx_app,[]}}, - {maintainers,["Feng Lee "]}, + {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 7d6e89427..a539bbd42 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -1,42 +1,36 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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). -include("emqx.hrl"). -%% Start/Stop Application +%% Start/Stop the application -export([start/0, is_running/1, stop/0]). %% PubSub API --export([subscribe/1, subscribe/2, subscribe/3, publish/1, - unsubscribe/1, unsubscribe/2]). +-export([subscribe/1, subscribe/2, subscribe/3]). +-export([publish/1]). +-export([unsubscribe/1, unsubscribe/2]). %% PubSub management API -export([topics/0, subscriptions/1, subscribers/1, subscribed/2]). - -%% Get/Set suboptions -export([get_subopts/2, set_subopts/3]). %% Hooks API -export([hook/4, hook/3, unhook/2, run_hooks/2, run_hooks/3]). -%% Debug API --export([dump/0]). - %% Shutdown and reboot -export([shutdown/0, shutdown/1, reboot/0]). @@ -76,16 +70,16 @@ is_running(Node) -> subscribe(Topic) -> emqx_broker:subscribe(iolist_to_binary(Topic)). --spec(subscribe(topic() | iodata(), subscriber() | string()) -> ok | {error, term()}). +-spec(subscribe(topic() | string(), subscriber() | string()) -> ok | {error, term()}). subscribe(Topic, Subscriber) -> emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber)). --spec(subscribe(topic() | iodata(), subscriber() | string(), [suboption()]) -> ok | {error, term()}). +-spec(subscribe(topic() | string(), subscriber() | string(), [suboption()]) -> ok | {error, term()}). subscribe(Topic, Subscriber, Options) -> emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). %% @doc Publish Message --spec(publish(message()) -> {ok, delivery()} | ignore). +-spec(publish(message()) -> {ok, delivery()} | {error, term()}). publish(Msg) -> emqx_broker:publish(Msg). @@ -118,7 +112,7 @@ subscribers(Topic) -> -spec(subscriptions(subscriber() | string()) -> [{topic(), list(suboption())}]). subscriptions(Subscriber) -> - emqx_broker:subscriptions(Subscriber). + emqx_broker:subscriptions(list_to_subid(Subscriber)). -spec(subscribed(topic() | string(), subscriber()) -> boolean()). subscribed(Topic, Subscriber) -> @@ -170,16 +164,10 @@ shutdown() -> shutdown(normal). shutdown(Reason) -> - emqx_logger:error("EMQ shutdown for ~s", [Reason]), + emqx_logger:error("emqx shutdown for ~s", [Reason]), emqx_plugins:unload(), lists:foreach(fun application:stop/1, [emqx, ekka, mochiweb, esockd, gproc]). reboot() -> lists:foreach(fun application:start/1, [gproc, esockd, mochiweb, ekka, emqx]). -%%-------------------------------------------------------------------- -%% Debug -%%-------------------------------------------------------------------- - -dump() -> lists:append([emqx_broker:dump(), emqx_router:dump()]). - diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 869ef2649..75e49fe07 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_access_control). diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 7eb4f2dc8..91c601db4 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). @@ -73,8 +71,7 @@ compile(topic, Topic) -> end. 'pattern?'(Words) -> - lists:member(<<"%u">>, Words) - orelse lists:member(<<"%c">>, Words). + lists:member(<<"%u">>, Words) orelse lists:member(<<"%c">>, Words). bin(L) when is_list(L) -> list_to_binary(L); @@ -99,7 +96,7 @@ match_who(_Client, {user, all}) -> true; match_who(_Client, {client, all}) -> true; -match_who(#client{client_id = ClientId}, {client, ClientId}) -> +match_who(#client{id = ClientId}, {client, ClientId}) -> true; match_who(#client{username = Username}, {user, Username}) -> true; @@ -137,9 +134,9 @@ feed_var(Client, Pattern) -> feed_var(Client, Pattern, []). feed_var(_Client, [], Acc) -> lists:reverse(Acc); -feed_var(Client = #client{client_id = undefined}, [<<"%c">>|Words], Acc) -> +feed_var(Client = #client{id = undefined}, [<<"%c">>|Words], Acc) -> feed_var(Client, Words, [<<"%c">>|Acc]); -feed_var(Client = #client{client_id = ClientId}, [<<"%c">>|Words], Acc) -> +feed_var(Client = #client{id = ClientId}, [<<"%c">>|Words], Acc) -> feed_var(Client, Words, [ClientId |Acc]); feed_var(Client = #client{username = undefined}, [<<"%u">>|Words], Acc) -> feed_var(Client, Words, [<<"%u">>|Acc]); diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index 3f2824fbc..65a3199ae 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_acl_internal). diff --git a/src/emqx_acl_mod.erl b/src/emqx_acl_mod.erl index 217b2ecd6..85844b042 100644 --- a/src/emqx_acl_mod.erl +++ b/src/emqx_acl_mod.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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_mod). diff --git a/src/emqx_alarm.erl b/src/emqx_alarm_mgr.erl similarity index 52% rename from src/emqx_alarm.erl rename to src/emqx_alarm_mgr.erl index 2b5990db4..f6901c325 100644 --- a/src/emqx_alarm.erl +++ b/src/emqx_alarm_mgr.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,29 +11,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_alarm). +-module(emqx_alarm_mgr). -behaviour(gen_event). -include("emqx.hrl"). --define(ALARM_MGR, ?MODULE). - -%% API Function Exports --export([start_link/0, alarm_fun/0, get_alarms/0, - set_alarm/1, clear_alarm/1, - add_alarm_handler/1, add_alarm_handler/2, - delete_alarm_handler/1]). +-export([start_link/0]). +-export([alarm_fun/0, get_alarms/0, set_alarm/1, clear_alarm/1]). +-export([add_alarm_handler/1, add_alarm_handler/2, delete_alarm_handler/1]). %% gen_event callbacks --export([init/1, handle_event/2, handle_call/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2, + code_change/3]). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- +-define(ALARM_MGR, ?MODULE). + +-record(state, {alarms}). start_link() -> start_with(fun(Pid) -> gen_event:add_handler(Pid, ?MODULE, []) end). @@ -75,70 +69,72 @@ add_alarm_handler(Module, Args) when is_atom(Module) -> delete_alarm_handler(Module) when is_atom(Module) -> gen_event:delete_handler(?ALARM_MGR, Module, []). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Default Alarm handler -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -init(_) -> {ok, []}. +init(_) -> {ok, #state{alarms = []}}. -handle_event({set_alarm, Alarm = #alarm{id = AlarmId, - severity = Severity, - title = Title, - summary = Summary}}, Alarms)-> - TS = os:timestamp(), - case catch emqx_json:encode([{id, AlarmId}, - {severity, Severity}, - {title, iolist_to_binary(Title)}, - {summary, iolist_to_binary(Summary)}, - {ts, emqx_time:now_secs(TS)}]) of - {'EXIT', Reason} -> - emqx_logger:error("[Alarm] Failed to encode set_alarm: ~p", [Reason]); - JSON -> - emqx_broker:publish(alarm_msg(alert, AlarmId, JSON)) +handle_event({set_alarm, Alarm = #alarm{timestamp = undefined}}, State)-> + handle_event({set_alarm, Alarm#alarm{timestamp = os:timestamp()}}, State); + +handle_event({set_alarm, Alarm = #alarm{id = AlarmId}}, State = #state{alarms = Alarms}) -> + case encode_alarm(Alarm) of + {ok, Json} -> + emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json)); + {error, Reason} -> + emqx_logger:error("[AlarmMgr] Failed to encode alarm: ~p", [Reason]) end, - {ok, [Alarm#alarm{timestamp = TS} | Alarms]}; + {ok, State#state{alarms = [Alarm|Alarms]}}; -handle_event({clear_alarm, AlarmId}, Alarms) -> - case catch emqx_json:encode([{id, AlarmId}, {ts, emqx_time:now_secs()}]) of - {'EXIT', Reason} -> - emqx_logger:error("[Alarm] Failed to encode clear_alarm: ~p", [Reason]); - JSON -> - emqx_broker:publish(alarm_msg(clear, AlarmId, JSON)) +handle_event({clear_alarm, AlarmId}, State = #state{alarms = Alarms}) -> + case emqx_json:safe_encode([{id, AlarmId}, {ts, emqx_time:now_secs()}]) of + {ok, Json} -> + emqx_broker:safe_publish(alarm_msg(clear, AlarmId, Json)); + {error, Reason} -> + emqx_logger:error("[AlarmMgr] Failed to encode clear: ~p", [Reason]) end, - {ok, lists:keydelete(AlarmId, 2, Alarms), hibernate}; + {ok, State#state{alarms = lists:keydelete(AlarmId, 2, Alarms)}, hibernate}; -handle_event(_, Alarms)-> - {ok, Alarms}. +handle_event(Event, State)-> + error_logger:error("[AlarmMgr] unexpected event: ~p", [Event]), + {ok, State}. -handle_info(_, Alarms) -> - {ok, Alarms}. +handle_info(Info, State) -> + error_logger:error("[AlarmMgr] unexpected info: ~p", [Info]), + {ok, State}. -handle_call(get_alarms, Alarms) -> - {ok, Alarms, Alarms}; +handle_call(get_alarms, State = #state{alarms = Alarms}) -> + {ok, Alarms, State}; -handle_call(_Query, Alarms) -> - {ok, {error, bad_query}, Alarms}. - -terminate(swap, Alarms) -> - {?MODULE, Alarms}; +handle_call(Req, State) -> + error_logger:error("[AlarmMgr] unexpected call: ~p", [Req]), + {ok, ignored, State}. +terminate(swap, State) -> + {?MODULE, State}; terminate(_, _) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ + +encode_alarm(#alarm{id = AlarmId, severity = Severity, title = Title, + summary = Summary, timestamp = Ts}) -> + emqx_json:safe_encode([{id, AlarmId}, {severity, Severity}, + {title, iolist_to_binary(Title)}, + {summary, iolist_to_binary(Summary)}, + {ts, emqx_time:now_secs(Ts)}]). alarm_msg(Type, AlarmId, Json) -> - Msg = emqx_message:make(alarm, topic(Type, AlarmId), iolist_to_binary(Json)), - emqx_message:set_flag(sys, Msg). + emqx_message:make(?ALARM_MGR, #{sys => true, qos => 0}, topic(Type, AlarmId), Json). topic(alert, AlarmId) -> emqx_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>); - topic(clear, AlarmId) -> emqx_topic:systop(<<"alarms/", AlarmId/binary, "/clear">>). diff --git a/src/emqx_app.erl b/src/emqx_app.erl index 54926f2a7..7a5426bae 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -1,24 +1,21 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_app). -behaviour(application). -%% Application callbacks -export([start/2, stop/1]). -define(APP, emqx). @@ -34,7 +31,7 @@ start(_Type, _Args) -> emqx_modules:load(), emqx_plugins:init(), emqx_plugins:load(), - emqx_listeners:start(), + emqx_listeners:start_all(), start_autocluster(), register(emqx, self()), print_vsn(), diff --git a/src/emqx_auth_mod.erl b/src/emqx_auth_mod.erl index 620584488..a5c3844a9 100644 --- a/src/emqx_auth_mod.erl +++ b/src/emqx_auth_mod.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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_mod). diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index ff5250f27..e2f03643e 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_banned). @@ -70,9 +68,7 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -spec(check(client()) -> boolean()). -check(#client{client_id = ClientId, - username = Username, - peername = {IPAddr, _}}) -> +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}). diff --git a/src/emqx_base62.erl b/src/emqx_base62.erl index 3d0d01969..929089f1b 100644 --- a/src/emqx_base62.erl +++ b/src/emqx_base62.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All Rights Reserved. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/src/emqx_boot.erl b/src/emqx_boot.erl deleted file mode 100644 index dadc4c4cd..000000000 --- a/src/emqx_boot.erl +++ /dev/null @@ -1,63 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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_boot). - --export([apply_module_attributes/1, all_module_attributes/1]). - -%% only {F, Args}... -apply_module_attributes(Name) -> - [{Module, [apply(Module, F, Args) || {F, Args} <- Attrs]} || - {_App, Module, Attrs} <- all_module_attributes(Name)]. - -%% Copy from rabbit_misc.erl -all_module_attributes(Name) -> - Targets = - lists:usort( - lists:append( - [[{App, Module} || Module <- Modules] || - {App, _, _} <- ignore_lib_apps(application:loaded_applications()), - {ok, Modules} <- [application:get_key(App, modules)]])), - lists:foldl( - fun ({App, Module}, Acc) -> - case lists:append([Atts || {N, Atts} <- module_attributes(Module), - N =:= Name]) of - [] -> Acc; - Atts -> [{App, Module, Atts} | Acc] - end - end, [], Targets). - -%% Copy from rabbit_misc.erl -module_attributes(Module) -> - case catch Module:module_info(attributes) of - {'EXIT', {undef, [{Module, module_info, [attributes], []} | _]}} -> - []; - {'EXIT', Reason} -> - exit(Reason); - V -> - V - end. - -ignore_lib_apps(Apps) -> - LibApps = [kernel, stdlib, sasl, appmon, eldap, erts, - syntax_tools, ssl, crypto, mnesia, os_mon, - inets, goldrush, lager, gproc, runtime_tools, - snmp, otp_mibs, public_key, asn1, ssh, hipe, - common_test, observer, webtool, xmerl, tools, - test_server, compiler, debugger, eunit, et, - wx], - [App || App = {Name, _, _} <- Apps, not lists:member(Name, LibApps)]. - diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index f45e4045c..ee2aa1535 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,22 +11,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_bridge). -behaviour(gen_server). -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). -%% API Function Exports -export([start_link/5]). -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -define(PING_DOWN_INTERVAL, 1000). @@ -49,36 +44,31 @@ -export_type([option/0]). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - %% @doc Start a bridge --spec(start_link(any(), pos_integer(), atom(), binary(), [option()]) -> - {ok, pid()} | ignore | {error, term()}). +-spec(start_link(term(), pos_integer(), atom(), binary(), [option()]) + -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id, Node, Topic, Options) -> - gen_server:start_link(?MODULE, [Pool, Id, Node, Topic, Options], []). + gen_server:start_link(?MODULE, [Pool, Id, Node, Topic, Options], [{hibernate_after, 5000}]). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([Pool, Id, Node, Topic, Options]) -> process_flag(trap_exit, true), - gproc_pool:connect_worker(Pool, {Pool, Id}), + true = gproc_pool:connect_worker(Pool, {Pool, Id}), case net_kernel:connect_node(Node) of - true -> + true -> true = erlang:monitor_node(Node, true), Share = iolist_to_binary(["$bridge:", atom_to_list(Node), ":", Topic]), - %% TODO:: local??? - emqx_broker:subscribe(Topic, self(), [local, {share, Share}, {qos, ?QOS_0}]), + emqx_broker:subscribe(Topic, self(), [{share, Share}, {qos, ?QOS_0}]), State = parse_opts(Options, #state{node = Node, subtopic = Topic}), + %%TODO: queue.... MQueue = emqx_mqueue:new(qname(Node, Topic), [{max_len, State#state.max_queue_len}], emqx_alarm:alarm_fun()), - {ok, State#state{pool = Pool, id = Id, mqueue = MQueue}, - hibernate, {backoff, 1000, 1000, 10000}}; - false -> + {ok, State#state{pool = Pool, id = Id, mqueue = MQueue}}; + false -> {stop, {cannot_connect_node, Node}} end. @@ -103,44 +93,41 @@ qname(Node, Topic) -> iolist_to_binary(["Bridge:", Node, ":", Topic]). handle_call(Req, _From, State) -> - emqx_logger:error("[Bridge] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[Bridge] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast(Msg, State) -> - emqx_logger:error("[Bridge] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Bridge] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({dispatch, _Topic, Msg}, State = #state{mqueue = MQ, status = down}) -> - {noreply, State#state{mqueue = emqx_mqueue:in(Msg, MQ)}}; +handle_info({dispatch, _Topic, Msg}, State = #state{mqueue = Q, status = down}) -> + %% TODO: how to drop??? + {noreply, State#state{mqueue = emqx_mqueue:in(Msg, Q)}}; handle_info({dispatch, _Topic, Msg}, State = #state{node = Node, status = up}) -> - emqx_rpc:cast(Node, emqx, publish, [transform(Msg, State)]), - {noreply, State, hibernate}; + ok = emqx_rpc:cast(Node, emqx_broker, publish, [transform(Msg, State)]), + {noreply, State}; handle_info({nodedown, Node}, State = #state{node = Node, ping_down_interval = Interval}) -> - emqx_logger:warning("[Bridge] Node Down: ~s", [Node]), + emqx_logger:warning("[Bridge] node down: ~s", [Node]), erlang:send_after(Interval, self(), ping_down_node), {noreply, State#state{status = down}, hibernate}; handle_info({nodeup, Node}, State = #state{node = Node}) -> %% TODO: Really fast?? case emqx:is_running(Node) of - true -> - emqx_logger:warning("[Bridge] Node up: ~s", [Node]), - {noreply, dequeue(State#state{status = up})}; - false -> - self() ! {nodedown, Node}, - {noreply, State#state{status = down}} + true -> emqx_logger:warning("[Bridge] Node up: ~s", [Node]), + {noreply, dequeue(State#state{status = up})}; + false -> self() ! {nodedown, Node}, + {noreply, State#state{status = down}} end; handle_info(ping_down_node, State = #state{node = Node, ping_down_interval = Interval}) -> Self = self(), spawn_link(fun() -> case net_kernel:connect_node(Node) of - true -> %%TODO: this is not right... fixme later - Self ! {nodeup, Node}; - false -> - erlang:send_after(Interval, Self, ping_down_node) + true -> Self ! {nodeup, Node}; + false -> erlang:send_after(Interval, Self, ping_down_node) end end), {noreply, State}; @@ -149,7 +136,7 @@ handle_info({'EXIT', _Pid, normal}, State) -> {noreply, State}; handle_info(Info, State) -> - emqx_logger:error("[Bridge] Unexpected info: ~p", [Info]), + emqx_logger:error("[Bridge] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> diff --git a/src/emqx_bridge_sup.erl b/src/emqx_bridge_sup.erl index 4f389c0dc..e7ba21310 100644 --- a/src/emqx_bridge_sup.erl +++ b/src/emqx_bridge_sup.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,16 +11,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_bridge_sup). +-include("emqx.hrl"). + -export([start_link/3]). - -%% @doc Start bridge pool supervisor --spec(start_link(atom(), binary(), [emqx_bridge:option()]) - -> {ok, pid()} | {error, term()}). +-spec(start_link(node(), topic(), [emqx_bridge:option()]) -> {ok, pid()} | {error, term()}). start_link(Node, Topic, Options) -> MFA = {emqx_bridge, start_link, [Node, Topic, Options]}, emqx_pool_sup:start_link({bridge, Node, Topic}, random, MFA). diff --git a/src/emqx_bridge_sup_sup.erl b/src/emqx_bridge_sup_sup.erl index 14fecaecc..4b34bedd9 100644 --- a/src/emqx_bridge_sup_sup.erl +++ b/src/emqx_bridge_sup_sup.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_bridge_sup_sup). @@ -23,6 +21,7 @@ -export([start_link/0, bridges/0]). -export([start_bridge/2, start_bridge/3, stop_bridge/2]). +%% Supervisor callbacks -export([init/1]). -define(CHILD_ID(Node, Topic), {bridge_sup, Node, Topic}). @@ -30,10 +29,6 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - %% @doc List all bridges -spec(bridges() -> [{node(), topic(), pid()}]). bridges() -> @@ -50,8 +45,7 @@ start_bridge(Node, Topic) when is_atom(Node), is_binary(Topic) -> start_bridge(Node, _Topic, _Options) when Node =:= node() -> {error, bridge_to_self}; start_bridge(Node, Topic, Options) when is_atom(Node), is_binary(Topic) -> - {ok, BridgeEnv} = emqx_config:get_env(bridge), - Options1 = emqx_misc:merge_opts(BridgeEnv, Options), + Options1 = emqx_misc:merge_opts(emqx_config:get_env(bridge, []), Options), supervisor:start_child(?MODULE, bridge_spec(Node, Topic, Options1)). %% @doc Stop a bridge @@ -63,15 +57,18 @@ stop_bridge(Node, Topic) when is_atom(Node), is_binary(Topic) -> Error -> Error end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Supervisor callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([]) -> {ok, {{one_for_one, 10, 3600}, []}}. bridge_spec(Node, Topic, Options) -> - {?CHILD_ID(Node, Topic), - {emqx_bridge_sup, start_link, [Node, Topic, Options]}, - permanent, infinity, supervisor, [emqx_bridge_sup]}. + #{id => ?CHILD_ID(Node, Topic), + start => {emqx_bridge_sup, start_link, [Node, Topic, Options]}, + restart => permanent, + shutdown => infinity, + type => supervisor, + modules => [emqx_bridge_sup]}. diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 5a6c9d954..43cc81155 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_broker). @@ -21,18 +19,17 @@ -include("emqx.hrl"). -export([start_link/2]). - -export([subscribe/1, subscribe/2, subscribe/3, subscribe/4]). --export([publish/1, publish/2]). +-export([publish/1, publish/2, safe_publish/1]). -export([unsubscribe/1, unsubscribe/2]). -export([dispatch/2, dispatch/3]). -export([subscriptions/1, subscribers/1, subscribed/2]). -export([get_subopts/2, set_subopts/3]). -export([topics/0]). -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(state, {pool, id, submon}). @@ -44,19 +41,14 @@ -define(SUBSCRIBER, emqx_subscriber). -define(SUBSCRIPTION, emqx_subscription). -%%-------------------------------------------------------------------- -%% Start a broker -%%-------------------------------------------------------------------- - --spec(start_link(atom(), pos_integer()) - -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, [Pool, Id], [{hibernate_after, 2000}]). -%%-------------------------------------------------------------------- -%% Subscriber/Unsubscribe -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ +%% Subscribe/Unsubscribe +%%------------------------------------------------------------------------------ -spec(subscribe(topic()) -> ok | {error, term()}). subscribe(Topic) when is_binary(Topic) -> @@ -85,52 +77,60 @@ unsubscribe(Topic) when is_binary(Topic) -> unsubscribe(Topic, Subscriber) when is_binary(Topic) -> unsubscribe(Topic, Subscriber, ?TIMEOUT). --spec(unsubscribe(topic(), subscriber(), timeout()) - -> ok | {error, term()}). +-spec(unsubscribe(topic(), subscriber(), timeout()) -> ok | {error, term()}). unsubscribe(Topic, Subscriber, Timeout) -> {Topic1, _} = emqx_topic:parse(Topic), UnsubReq = {unsubscribe, Topic1, with_subpid(Subscriber)}, async_call(pick(Subscriber), UnsubReq, Timeout). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Publish -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -spec(publish(topic(), payload()) -> delivery() | stopped). publish(Topic, Payload) when is_binary(Topic), is_binary(Payload) -> publish(emqx_message:make(Topic, Payload)). --spec(publish(message()) -> delivery() | stopped). +-spec(publish(message()) -> {ok, delivery()} | {error, stopped}). publish(Msg = #message{from = From}) -> %% Hook to trace? - trace(publish, From, Msg), + _ = trace(publish, From, Msg), case emqx_hooks:run('message.publish', [], Msg) of {ok, Msg1 = #message{topic = Topic}} -> - route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1)); + {ok, route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1))}; {stop, Msg1} -> emqx_logger:warning("Stop publishing: ~s", [emqx_message:format(Msg1)]), - stopped + {error, stopped} end. -%%-------------------------------------------------------------------- +%% called internally +safe_publish(Msg) -> + try + publish(Msg) + catch + _:Error:Stacktrace -> + emqx_logger:error("[Broker] publish error: ~p~n~p~n~p", [Error, Msg, Stacktrace]) + end. + +%%------------------------------------------------------------------------------ %% Trace -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ trace(publish, From, _Msg) when is_atom(From) -> %% Dont' trace '$SYS' publish ignore; -trace(public, #client{client_id = ClientId, username = Username}, +trace(public, #client{id = ClientId, username = Username}, #message{topic = Topic, payload = Payload}) -> emqx_logger:info([{client, ClientId}, {topic, Topic}], - "~s/~s PUBLISH to ~s: ~p", [ClientId, Username, Topic, Payload]); + "~s/~s PUBLISH to ~s: ~p", [Username, ClientId, Topic, Payload]); trace(public, From, #message{topic = Topic, payload = Payload}) when is_binary(From); is_list(From) -> emqx_logger:info([{client, From}, {topic, Topic}], "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Route -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ route([], Delivery = #delivery{message = Msg}) -> emqx_hooks:run('message.dropped', [undefined, Msg]), @@ -193,7 +193,7 @@ dispatch({SubId, SubPid}, Topic, Msg) when is_binary(SubId), is_pid(SubPid) -> dispatch(SubId, Topic, Msg) when is_binary(SubId) -> emqx_sm:dispatch(SubId, Topic, Msg); dispatch({share, _Group, _Sub}, _Topic, _Msg) -> - ignore. + ignored. dropped(<<"$SYS/", _/binary>>) -> ok; @@ -201,16 +201,16 @@ dropped(_Topic) -> emqx_metrics:inc('messages/dropped'). delivery(Msg) -> - #delivery{message = Msg, flows = []}. + #delivery{node = node(), message = Msg, flows = []}. subscribers(Topic) -> try ets:lookup_element(?SUBSCRIBER, Topic, 2) catch error:badarg -> [] end. subscriptions(Subscriber) -> lists:map(fun({_, {share, _Group, Topic}}) -> - subscription(Topic, Subscriber); + subscription(Topic, Subscriber); ({_, Topic}) -> - subscription(Topic, Subscriber) + subscription(Topic, Subscriber) end, ets:lookup(?SUBSCRIPTION, Subscriber)). subscription(Topic, Subscriber) -> @@ -221,9 +221,7 @@ subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> ets:member(?SUBOPTION, {Topic, SubPid}); subscribed(Topic, SubId) when is_binary(Topic), is_binary(SubId) -> length(ets:match_object(?SUBOPTION, {{Topic, {SubId, '_'}}, '_'}, 1)) == 1; -subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), - is_binary(SubId), - is_pid(SubPid) -> +subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), is_binary(SubId), is_pid(SubPid) -> ets:member(?SUBOPTION, {Topic, {SubId, SubPid}}). -spec(get_subopts(topic(), subscriber()) -> [suboption()]). @@ -262,26 +260,26 @@ pick(SubPid) when is_pid(SubPid) -> pick(SubId) when is_binary(SubId) -> gproc_pool:pick_worker(broker, SubId); pick({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> - pick(SubId). + pick(SubPid). -spec(topics() -> [topic()]). topics() -> emqx_router:topics(). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([Pool, Id]) -> - gproc_pool:connect_worker(Pool, {Pool, Id}), + true = gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #state{pool = Pool, id = Id, submon = emqx_pmon:new()}}. handle_call(Req, _From, State) -> - emqx_logger:error("[Broker] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[Broker] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast({From, {subscribe, Topic, Subscriber, Options}}, State) -> case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of - [] -> + [] -> Group = proplists:get_value(share, Options), true = do_subscribe(Group, Topic, Subscriber, Options), emqx_shared_sub:subscribe(Group, Topic, subpid(Subscriber)), @@ -307,11 +305,10 @@ handle_cast({From, {unsubscribe, Topic, Subscriber}}, State) -> {noreply, State}; handle_cast(Msg, State) -> - emqx_logger:error("[Broker] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Broker] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, SubPid, _Reason}, - State = #state{submon = SubMon}) -> +handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{submon = SubMon}) -> Subscriber = case SubMon:find(SubPid) of undefined -> SubPid; SubId -> {SubId, SubPid} @@ -321,7 +318,8 @@ handle_info({'DOWN', _MRef, process, SubPid, _Reason}, ({_, Topic}) -> Topic end, ets:lookup(?SUBSCRIPTION, Subscriber)), - lists:foreach(fun(Topic) -> + lists:foreach( + fun(Topic) -> case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of [{_, Options}] -> Group = proplists:get_value(share, Options), @@ -336,18 +334,18 @@ handle_info({'DOWN', _MRef, process, SubPid, _Reason}, {noreply, demonitor_subscriber(SubPid, State)}; handle_info(Info, State) -> - emqx_logger:error("[Broker] Unexpected info: ~p", [Info]), + emqx_logger:error("[Broker] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> - gproc_pool:disconnect_worker(Pool, {Pool, Id}). + true = gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- -%% Internal Functions -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ do_subscribe(Group, Topic, Subscriber, Options) -> ets:insert(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index f461c44c7..975b2bf0d 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_broker_helper). @@ -20,8 +18,8 @@ -export([start_link/0]). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -define(HELPER, ?MODULE). @@ -31,24 +29,24 @@ start_link() -> gen_server:start_link({local, ?HELPER}, ?MODULE, [], []). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([]) -> emqx_stats:update_interval(broker_stats, stats_fun()), {ok, #state{}, hibernate}. handle_call(Req, _From, State) -> - emqx_logger:error("[BrokerHelper] Unexpected request: ~p", [Req]), + emqx_logger:error("[BrokerHelper] unexpected call: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - emqx_logger:error("[BrokerHelper] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[BrokerHelper] unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> - emqx_logger:error("[BrokerHelper] Unexpected info: ~p", [Info]), + emqx_logger:error("[BrokerHelper] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{}) -> @@ -57,9 +55,9 @@ terminate(_Reason, #state{}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ stats_fun() -> fun() -> diff --git a/src/emqx_broker_sup.erl b/src/emqx_broker_sup.erl index 034d87711..51f6e72aa 100644 --- a/src/emqx_broker_sup.erl +++ b/src/emqx_broker_sup.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_broker_sup). @@ -27,15 +25,15 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Supervisor callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([]) -> %% Create the pubsub tables - lists:foreach(fun create_tab/1, [subscription, subscriber, suboption]), + ok = lists:foreach(fun create_tab/1, [subscription, subscriber, suboption]), - %% Shared Subscription + %% Shared subscription SharedSub = {shared_sub, {emqx_shared_sub, start_link, []}, permanent, 5000, worker, [emqx_shared_sub]}, @@ -43,16 +41,16 @@ init([]) -> Helper = {broker_helper, {emqx_broker_helper, start_link, []}, permanent, 5000, worker, [emqx_broker_helper]}, - %% Broker Pool + %% Broker pool BrokerPool = emqx_pool_sup:spec(emqx_broker_pool, [broker, hash, emqx_vm:schedulers() * 2, {emqx_broker, start_link, []}]), {ok, {{one_for_all, 0, 1}, [SharedSub, Helper, BrokerPool]}}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Create tables -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ create_tab(suboption) -> %% Suboption: {Topic, Sub} -> [{qos, 1}] diff --git a/src/emqx_cli.erl b/src/emqx_cli.erl index ffbacb35e..4463f2b27 100644 --- a/src/emqx_cli.erl +++ b/src/emqx_cli.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_cli). diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 277d7ecd8..87b1e2bf3 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. -module(emqx_client). @@ -142,9 +140,9 @@ -define(PROPERTY(Name, Val), #state{properties = #{Name := Val}}). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -spec(start_link() -> gen_statem:start_ret()). start_link() -> start_link([]). @@ -302,9 +300,9 @@ disconnect(Client, ReasonCode) -> disconnect(Client, ReasonCode, Properties) -> gen_statem:call(Client, {disconnect, ReasonCode, Properties}). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% For test cases -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ puback(Client, PacketId) when is_integer(PacketId) -> puback(Client, PacketId, ?RC_SUCCESS). @@ -357,9 +355,9 @@ pause(Client) -> resume(Client) -> gen_statem:call(Client, resume). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_statem callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([Options]) -> process_flag(trap_exit, true), @@ -892,9 +890,9 @@ terminate(_Reason, _State, #state{socket = Socket}) -> code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ ensure_keepalive_timer(State = ?PROPERTY('Server-Keep-Alive', Secs)) -> ensure_keepalive_timer(timer:seconds(Secs), State); @@ -1017,7 +1015,7 @@ msg_to_packet(#mqtt_message{qos = Qos, payload = Payload}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Socket Connect/Send sock_connect(Hosts, SockOpts, Timeout) -> @@ -1057,7 +1055,7 @@ send(Packet, State = #state{socket = Sock, proto_ver = Ver}) run_sock(State = #state{socket = Sock}) -> emqx_client_sock:setopts(Sock, [{active, once}]), State. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Receive Loop receive_loop(<<>>, State) -> @@ -1076,7 +1074,7 @@ receive_loop(Bytes, State = #state{parse_state = ParseState}) -> {stop, Error} end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Next packet id next_packet_id(State = #state{last_packet_id = 16#ffff}) -> diff --git a/src/emqx_client_sock.erl b/src/emqx_client_sock.erl index a7bc5aa15..dc19a8d91 100644 --- a/src/emqx_client_sock.erl +++ b/src/emqx_client_sock.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. -module(emqx_client_sock). @@ -26,8 +24,7 @@ -type(sockname() :: {inet:ip_address(), inet:port_number()}). --type(option() :: gen_tcp:connect_option() - | {ssl_opts, [ssl:ssl_option()]}). +-type(option() :: gen_tcp:connect_option() | {ssl_opts, [ssl:ssl_option()]}). -export_type([socket/0, option/0]). diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 9e3edf0b7..0adb07603 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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). @@ -22,20 +20,18 @@ -export([start_link/0]). --export([lookup_client/1, register_client/1, register_client/2, - unregister_client/1]). +-export([lookup_client/1]). +-export([register_client/1, register_client/2, unregister_client/1]). -export([get_client_attrs/1, lookup_client_pid/1]). - -export([get_client_stats/1, set_client_stats/2]). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(state, {client_pmon}). -define(CM, ?MODULE). - %% ETS Tables. -define(CLIENT, emqx_client). -define(CLIENT_ATTRS, emqx_client_attrs). @@ -56,22 +52,20 @@ lookup_client(ClientId) when is_binary(ClientId) -> register_client(ClientId) when is_binary(ClientId) -> register_client({ClientId, self()}); -register_client({ClientId, ClientPid}) when is_binary(ClientId), - is_pid(ClientPid) -> +register_client({ClientId, ClientPid}) when is_binary(ClientId), is_pid(ClientPid) -> register_client({ClientId, ClientPid}, []). -spec(register_client({client_id(), pid()}, list()) -> ok). -register_client({ClientId, ClientPid}, Attrs) when is_binary(ClientId), - is_pid(ClientPid) -> - ets:insert(?CLIENT, {ClientId, ClientPid}), - ets:insert(?CLIENT_ATTRS, {{ClientId, ClientPid}, Attrs}), +register_client(CObj = {ClientId, ClientPid}, Attrs) when is_binary(ClientId), is_pid(ClientPid) -> + _ = ets:insert(?CLIENT, CObj), + _ = ets:insert(?CLIENT_ATTRS, {CObj, Attrs}), notify({registered, ClientId, ClientPid}). %% @doc Get client attrs -spec(get_client_attrs({client_id(), pid()}) -> list()). -get_client_attrs({ClientId, ClientPid}) when is_binary(ClientId), - is_pid(ClientPid) -> - try ets:lookup_element(?CLIENT_ATTRS, {ClientId, ClientPid}, 2) +get_client_attrs(CObj = {ClientId, ClientPid}) when is_binary(ClientId), is_pid(ClientPid) -> + try + ets:lookup_element(?CLIENT_ATTRS, CObj, 2) catch error:badarg -> [] end. @@ -81,11 +75,10 @@ get_client_attrs({ClientId, ClientPid}) when is_binary(ClientId), unregister_client(ClientId) when is_binary(ClientId) -> unregister_client({ClientId, self()}); -unregister_client({ClientId, ClientPid}) when is_binary(ClientId), - is_pid(ClientPid) -> - ets:delete(?CLIENT_STATS, {ClientId, ClientPid}), - ets:delete(?CLIENT_ATTRS, {ClientId, ClientPid}), - ets:delete_object(?CLIENT, {ClientId, ClientPid}), +unregister_client(CObj = {ClientId, ClientPid}) when is_binary(ClientId), is_pid(ClientPid) -> + _ = ets:delete(?CLIENT_STATS, CObj), + _ = ets:delete(?CLIENT_ATTRS, CObj), + _ = ets:delete_object(?CLIENT, CObj), notify({unregistered, ClientId, ClientPid}). %% @doc Lookup client pid @@ -98,9 +91,8 @@ lookup_client_pid(ClientId) when is_binary(ClientId) -> %% @doc Get client stats -spec(get_client_stats({client_id(), pid()}) -> list(emqx_stats:stats())). -get_client_stats({ClientId, ClientPid}) when is_binary(ClientId), - is_pid(ClientPid) -> - try ets:lookup_element(?CLIENT_STATS, {ClientId, ClientPid}, 2) +get_client_stats(CObj = {ClientId, ClientPid}) when is_binary(ClientId), is_pid(ClientPid) -> + try ets:lookup_element(?CLIENT_STATS, CObj, 2) catch error:badarg -> [] end. @@ -110,16 +102,15 @@ get_client_stats({ClientId, ClientPid}) when is_binary(ClientId), set_client_stats(ClientId, Stats) when is_binary(ClientId) -> set_client_stats({ClientId, self()}, Stats); -set_client_stats({ClientId, ClientPid}, Stats) when is_binary(ClientId), - is_pid(ClientPid) -> - ets:insert(?CLIENT_STATS, {{ClientId, ClientPid}, Stats}). +set_client_stats(CObj = {ClientId, ClientPid}, Stats) when is_binary(ClientId), is_pid(ClientPid) -> + ets:insert(?CLIENT_STATS, {CObj, Stats}). notify(Msg) -> gen_server:cast(?CM, {notify, Msg}). -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% gen_server callbacks -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- init([]) -> TabOpts = [public, set, {write_concurrency, true}], @@ -130,8 +121,8 @@ init([]) -> {ok, #state{client_pmon = emqx_pmon:new()}}. handle_call(Req, _From, State) -> - emqx_logger:error("[CM] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[CM] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast({notify, {registered, ClientId, Pid}}, State = #state{client_pmon = PMon}) -> {noreply, State#state{client_pmon = emqx_pmon:monitor(Pid, ClientId, PMon)}}; @@ -140,7 +131,7 @@ handle_cast({notify, {unregistered, _ClientId, Pid}}, State = #state{client_pmon {noreply, State#state{client_pmon = emqx_pmon:demonitor(Pid, PMon)}}; handle_cast(Msg, State) -> - emqx_logger:error("[CM] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[CM] unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{client_pmon = PMon}) -> @@ -152,7 +143,7 @@ handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{client_pm end; handle_info(Info, State) -> - emqx_logger:error("[CM] Unexpected info: ~p", [Info]), + emqx_logger:error("[CM] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State = #state{}) -> @@ -161,9 +152,9 @@ terminate(_Reason, _State = #state{}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% Internal functions -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- update_client_stats() -> case ets:info(?CLIENT, size) of diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index 9b98e0c97..231822ba5 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -1,18 +1,17 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_sup). @@ -26,7 +25,11 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - CM = {emqx_cm, {emqx_cm, start_link, []}, - permanent, 5000, worker, [emqx_cm]}, - {ok, {{one_for_all, 10, 3600}, [CM]}}. + {ok, {{one_for_all, 10, 3600}, + [#{id => manager, + start => {emqx_cm, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_cm]}]}}. diff --git a/src/emqx_config.erl b/src/emqx_config.erl index ef1d8ec9f..d1456f644 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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 %% @@ -35,7 +33,7 @@ -define(APP, emqx). --spec(get_env(Key :: atom(), Default :: any()) -> undefined | any()). +-spec(get_env(Key :: atom(), Default :: term()) -> term()). get_env(Key, Default) -> application:get_env(?APP, Key, Default). diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index fe0518072..544646701 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. -module(emqx_connection). @@ -41,8 +39,8 @@ -export([session/1]). %% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - code_change/3, terminate/2]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, + terminate/2]). %% Unused fields: connname, peerhost, peerport -record(state, {transport, socket, peername, conn_state, await_recv, @@ -88,7 +86,7 @@ clean_acl_cache(CPid, Topic) -> gen_server:call(CPid, {clean_acl_cache, Topic}). %%-------------------------------------------------------------------- -%% gen_server Callbacks +%% gen_server callbacks %%-------------------------------------------------------------------- init([Transport, Sock, Env]) -> diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index a2228564a..46d97e757 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. -module(emqx_ctl). @@ -22,9 +20,8 @@ -export([register_command/2, register_command/3, unregister_command/1]). -export([run_command/2, lookup_command/1]). -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(state, {seq = 0}). @@ -33,10 +30,6 @@ -define(SERVER, ?MODULE). -define(TAB, emqx_command). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). @@ -64,9 +57,8 @@ run_command(Cmd, Args) when is_atom(Cmd) -> try Mod:Fun(Args) of _ -> ok catch - _:Reason -> - emqx_logger:error("[CTL] CMD Error:~p, Stacktrace:~p", - [Reason, erlang:get_stacktrace()]), + _:Reason:Stacktrace -> + emqx_logger:error("[CTL] CMD Error:~p, Stacktrace:~p", [Reason, Stacktrace]), {error, Reason} end; [] -> @@ -83,19 +75,19 @@ lookup_command(Cmd) when is_atom(Cmd) -> usage() -> io:format("Usage: ~s~n", [?MODULE]), [begin io:format("~80..-s~n", [""]), Mod:Cmd(usage) end - || {_, {Mod, Cmd}, _} <- ets:tab2list(?TAB)]. + || {_, {Mod, Cmd}, _} <- ets:tab2list(?TAB)]. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([]) -> _ = emqx_tables:new(?TAB, [ordered_set, protected]), {ok, #state{seq = 0}}. handle_call(Req, _From, State) -> - emqx_logger:error("Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast({register_command, Cmd, MF, Opts}, State = #state{seq = Seq}) -> case ets:match(?TAB, {{'$1', Cmd}, '_', '_'}) of @@ -111,11 +103,11 @@ handle_cast({unregister_command, Cmd}, State) -> noreply(State); handle_cast(Msg, State) -> - emqx_logger:error("Unexpected msg: ~p", [Msg]), + emqx_logger:error("Unexpected cast: ~p", [Msg]), noreply(State). handle_info(Info, State) -> - emqx_logger:error("Unexpected info: ~p", [Info]), + emqx_logger:error("unexpected info: ~p", [Info]), noreply(State). terminate(_Reason, _State) -> @@ -124,9 +116,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal Function -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ noreply(State) -> {noreply, State, hibernate}. diff --git a/src/emqx_flapping.erl b/src/emqx_flapping.erl index bf19c50ee..56dac9110 100644 --- a/src/emqx_flapping.erl +++ b/src/emqx_flapping.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,8 +11,8 @@ %% 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 TODO: %% 1. Flapping Detection %% 2. Conflict Detection? -module(emqx_flapping). diff --git a/src/emqx_flow_control.erl b/src/emqx_flow_control.erl index 6effb1d53..e5042e9a7 100644 --- a/src/emqx_flow_control.erl +++ b/src/emqx_flow_control.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_flow_control). diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index ae2ab010d..0a261db6e 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -1,25 +1,26 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_frame). -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). +-export([initial_state/0, initial_state/1]). +-export([parse/2]). +-export([serialize/1, serialize/2]). + -type(options() :: #{max_packet_size => 1..?MAX_PACKET_SIZE, version => mqtt_version()}). @@ -30,16 +31,11 @@ -export_type([options/0, parse_state/0]). --export([initial_state/0, initial_state/1]). --export([parse/2]). --export([serialize/1, serialize/2]). +-define(DEFAULT_OPTIONS, #{max_packet_size => ?MAX_PACKET_SIZE, version => ?MQTT_PROTO_V4}). --define(DEFAULT_OPTIONS, #{max_packet_size => ?MAX_PACKET_SIZE, - version => ?MQTT_PROTO_V4}). - -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Init parse state -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -spec(initial_state() -> {none, options()}). initial_state() -> @@ -52,12 +48,11 @@ initial_state(Options) when is_map(Options) -> merge_opts(Options) -> maps:merge(?DEFAULT_OPTIONS, Options). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Parse MQTT Frame -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ --spec(parse(binary(), parse_state()) - -> {ok, mqtt_packet(), binary()} | {more, cont_fun(binary())}). +-spec(parse(binary(), parse_state()) -> {ok, mqtt_packet(), binary()} | {more, cont_fun(binary())}). parse(<<>>, {none, Options}) -> {more, fun(Bin) -> parse(Bin, {none, Options}) end}; parse(<>, {none, Options}) -> @@ -357,9 +352,9 @@ parse_utf8_string(<>) -> parse_binary_data(<>) -> {Data, Rest}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Serialize MQTT Packet -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -spec(serialize(mqtt_packet()) -> iodata()). serialize(Packet) -> @@ -369,8 +364,7 @@ serialize(Packet) -> 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)). + serialize(Header, serialize_variable(Variable, merge_opts(Options)), serialize_payload(Payload)). serialize(#mqtt_packet_header{type = Type, dup = Dup, @@ -622,10 +616,6 @@ serialize_variable_byte_integer(N) when N =< ?LOWBITS -> serialize_variable_byte_integer(N) -> <<1:1, (N rem ?HIGHBIT):7, (serialize_variable_byte_integer(N div ?HIGHBIT))/binary>>. -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - bool(0) -> false; bool(1) -> true. diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 4901cce5e..2cc0b2a1a 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,12 +11,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. -%%-------------------------------------------------------------------- %% GC Utility functions. -module(emqx_gc). +%% Memory: (10, 100, 1000) +%% + +%%-record + -export([conn_max_gc_count/0, reset_conn_gc_count/2, maybe_force_gc/2, maybe_force_gc/3]). diff --git a/src/emqx_gen_mod.erl b/src/emqx_gen_mod.erl index 184b77fb8..42466fdd9 100644 --- a/src/emqx_gen_mod.erl +++ b/src/emqx_gen_mod.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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 02941baa3..43855c734 100644 --- a/src/emqx_guid.erl +++ b/src/emqx_guid.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,15 +11,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. -%%-------------------------------------------------------------------- %% @doc Generate global unique id for mqtt message. %% %% -------------------------------------------------------- -%% | Timestamp | NodeID + PID | Sequence | +%% | Timestamp | NodeID + PID | Sequence | %% |<------- 64bits ------->|<--- 48bits --->|<- 16bits ->| %% -------------------------------------------------------- -%% +%% %% 1. Timestamp: erlang:system_time if Erlang >= R18, otherwise os:timestamp %% 2. NodeId: encode node() to 2 bytes integer %% 3. Pid: encode pid to 4 bytes integer diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index 06bd0eabe..0aa3e274c 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_hooks). diff --git a/src/emqx_inflight.erl b/src/emqx_inflight.erl index e2d77d95b..3353983d8 100644 --- a/src/emqx_inflight.erl +++ b/src/emqx_inflight.erl @@ -1,90 +1,89 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_inflight). -export([new/1, contain/2, lookup/2, insert/3, update/3, delete/2, values/1, to_list/1, size/1, max_size/1, is_full/1, is_empty/1, window/1]). --type(inflight() :: {max_size, gb_trees:tree()}). +-type(max_size() :: pos_integer()). +-type(inflight() :: {?MODULE, max_size(), gb_trees:tree()}). -export_type([inflight/0]). -spec(new(non_neg_integer()) -> inflight()). new(MaxSize) when MaxSize >= 0 -> - {MaxSize, gb_trees:empty()}. + {?MODULE, MaxSize, gb_trees:empty()}. -spec(contain(Key :: term(), inflight()) -> boolean()). -contain(Key, {_MaxSize, Tree}) -> +contain(Key, {?MODULE, _MaxSize, Tree}) -> gb_trees:is_defined(Key, Tree). -spec(lookup(Key :: term(), inflight()) -> {value, term()} | none). -lookup(Key, {_MaxSize, Tree}) -> +lookup(Key, {?MODULE, _MaxSize, Tree}) -> gb_trees:lookup(Key, Tree). -spec(insert(Key :: term(), Value :: term(), inflight()) -> inflight()). -insert(Key, Value, {MaxSize, Tree}) -> - {MaxSize, gb_trees:insert(Key, Value, Tree)}. +insert(Key, Value, {?MODULE, MaxSize, Tree}) -> + {?MODULE, MaxSize, gb_trees:insert(Key, Value, Tree)}. -spec(delete(Key :: term(), inflight()) -> inflight()). -delete(Key, {MaxSize, Tree}) -> - {MaxSize, gb_trees:delete(Key, Tree)}. +delete(Key, {?MODULE, MaxSize, Tree}) -> + {?MODULE, MaxSize, gb_trees:delete(Key, Tree)}. -spec(update(Key :: term(), Val :: term(), inflight()) -> inflight()). -update(Key, Val, {MaxSize, Tree}) -> - {MaxSize, gb_trees:update(Key, Val, Tree)}. +update(Key, Val, {?MODULE, MaxSize, Tree}) -> + {?MODULE, MaxSize, gb_trees:update(Key, Val, Tree)}. -spec(is_full(inflight()) -> boolean()). -is_full({0, _Tree}) -> +is_full({?MODULE, 0, _Tree}) -> false; -is_full({MaxSize, Tree}) -> +is_full({?MODULE, MaxSize, Tree}) -> MaxSize =< gb_trees:size(Tree). -spec(is_empty(inflight()) -> boolean()). -is_empty({_MaxSize, Tree}) -> +is_empty({?MODULE, _MaxSize, Tree}) -> gb_trees:is_empty(Tree). -spec(smallest(inflight()) -> {K :: term(), V :: term()}). -smallest({_MaxSize, Tree}) -> +smallest({?MODULE, _MaxSize, Tree}) -> gb_trees:smallest(Tree). -spec(largest(inflight()) -> {K :: term(), V :: term()}). -largest({_MaxSize, Tree}) -> +largest({?MODULE, _MaxSize, Tree}) -> gb_trees:largest(Tree). -spec(values(inflight()) -> list()). -values({_MaxSize, Tree}) -> +values({?MODULE, _MaxSize, Tree}) -> gb_trees:values(Tree). -spec(to_list(inflight()) -> list({K :: term(), V :: term()})). -to_list({_MaxSize, Tree}) -> +to_list({?MODULE, _MaxSize, Tree}) -> gb_trees:to_list(Tree). -spec(window(inflight()) -> list()). -window(Inflight = {_MaxSize, Tree}) -> +window(Inflight = {?MODULE, _MaxSize, Tree}) -> case gb_trees:is_empty(Tree) of true -> []; false -> [Key || {Key, _Val} <- [smallest(Inflight), largest(Inflight)]] end. -spec(size(inflight()) -> non_neg_integer()). -size({_MaxSize, Tree}) -> +size({?MODULE, _MaxSize, Tree}) -> gb_trees:size(Tree). -spec(max_size(inflight()) -> non_neg_integer()). -max_size({MaxSize, _Tree}) -> +max_size({?MODULE, MaxSize, _Tree}) -> MaxSize. diff --git a/src/emqx_json.erl b/src/emqx_json.erl index e3b4241d6..9c0740b33 100644 --- a/src/emqx_json.erl +++ b/src/emqx_json.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_json). diff --git a/src/emqx_keepalive.erl b/src/emqx_keepalive.erl index ae38d28bc..25740b099 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_keepalive). diff --git a/src/emqx_kernel_sup.erl b/src/emqx_kernel_sup.erl index 486c20705..96dde598c 100644 --- a/src/emqx_kernel_sup.erl +++ b/src/emqx_kernel_sup.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_kernel_sup). @@ -36,8 +34,18 @@ init([]) -> child_spec(emqx_tracer, worker)]}}. child_spec(M, worker) -> - {M, {M, start_link, []}, permanent, 5000, worker, [M]}; + #{id => M, + start => {M, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [M]}; child_spec(M, supervisor) -> - {M, {M, start_link, []}, - permanent, infinity, supervisor, [M]}. + #{id => M, + start => {M, start_link, []}, + restart => permanent, + shutdown => infinity, + type => supervisor, + modules => [M]}. + diff --git a/src/emqx_lager_backend.erl b/src/emqx_lager_backend.erl index a307d5d8b..b002ff7af 100644 --- a/src/emqx_lager_backend.erl +++ b/src/emqx_lager_backend.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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_lager_backend). diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index d63a6d620..02de39123 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -1,97 +1,62 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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 start/stop MQTT listeners. -module(emqx_listeners). -include("emqx_mqtt.hrl"). --export([start/0, restart/0, stop/0]). +-export([start_all/0, restart_all/0, stop_all/0]). -export([start_listener/1, stop_listener/1, restart_listener/1]). --export([all/0]). -type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}). -%%-------------------------------------------------------------------- -%% Start/Stop Listeners -%%-------------------------------------------------------------------- - -%% @doc Start Listeners. --spec(start() -> ok). -start() -> +%% @doc Start all listeners +-spec(start_all() -> ok). +start_all() -> lists:foreach(fun start_listener/1, emqx_config:get_env(listeners, [])). -%% Start mqtt listener --spec(start_listener(listener()) -> {ok, pid()} | {error, any()}). -start_listener({tcp, ListenOn, Opts}) -> - start_listener('mqtt:tcp', ListenOn, Opts); +%% Start MQTT/TCP listener +-spec(start_listener(listener()) -> {ok, pid()} | {error, term()}). +start_listener({tcp, ListenOn, Options}) -> + start_mqtt_listener('mqtt:tcp', ListenOn, Options); +%% Start MQTT/TLS listener +start_listener({Proto, ListenOn, Options}) when Proto == ssl; Proto == tls -> + start_mqtt_listener('mqtt:tls', ListenOn, Options); +%% Start MQTT/WS listener +start_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> + start_http_listener('mqtt:ws', ListenOn, Options); +%% Start MQTT/WSS listener +start_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss -> + start_http_listener('mqtt:wss', ListenOn, Options). -%% Start mqtt(SSL) listener -start_listener({ssl, ListenOn, Opts}) -> - start_listener('mqtt:ssl', ListenOn, Opts); +start_mqtt_listener(Name, ListenOn, Options) -> + {ok, _} = esockd:open(Name, ListenOn, merge_sockopts(Options), {emqx_connection, start_link, []}). -%% Start http listener -start_listener({Proto, ListenOn, Opts}) when Proto == http; Proto == ws -> - {ok, _} = mochiweb:start_http('mqtt:ws', ListenOn, Opts, {emqx_ws, handle_request, []}); +start_http_listener(Name, ListenOn, Options) -> + {ok, _} = mochiweb:start_http(Name, ListenOn, Options, {emqx_ws, handle_request, []}). -%% Start https listener -start_listener({Proto, ListenOn, Opts}) when Proto == https; Proto == wss -> - {ok, _} = mochiweb:start_http('mqtt:wss', ListenOn, Opts, {emqx_ws, handle_request, []}). - -start_listener(Proto, ListenOn, Opts) -> - Env = lists:append(emqx_config:get_env(client, []), - emqx_config:get_env(protocol, [])), - MFArgs = {emqx_connection, start_link, [Env]}, - {ok, _} = esockd:open(Proto, ListenOn, merge_sockopts(Opts), MFArgs). - -all() -> - [Listener || Listener = {{Proto, _}, _Pid} <- esockd:listeners(), is_mqtt(Proto)]. - -is_mqtt('mqtt:tcp') -> true; -is_mqtt('mqtt:ssl') -> true; -is_mqtt('mqtt:ws') -> true; -is_mqtt('mqtt:wss') -> true; -is_mqtt(_Proto) -> false. - -%% @doc Stop Listeners --spec(stop() -> ok). -stop() -> - lists:foreach(fun stop_listener/1, emqx_config:get_env(listeners, [])). - --spec(stop_listener(listener()) -> ok | {error, any()}). -stop_listener({tcp, ListenOn, _Opts}) -> - esockd:close('mqtt:tcp', ListenOn); -stop_listener({ssl, ListenOn, _Opts}) -> - esockd:close('mqtt:ssl', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> - mochiweb:stop_http('mqtt:ws', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> - mochiweb:stop_http('mqtt:wss', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) -> - esockd:close(Proto, ListenOn). - -%% @doc Restart Listeners --spec(restart() -> ok). -restart() -> +%% @doc Restart all listeners +-spec(restart_all() -> ok). +restart_all() -> lists:foreach(fun restart_listener/1, emqx_config:get_env(listeners, [])). -spec(restart_listener(listener()) -> any()). restart_listener({tcp, ListenOn, _Opts}) -> esockd:reopen('mqtt:tcp', ListenOn); -restart_listener({ssl, ListenOn, _Opts}) -> - esockd:reopen('mqtt:ssl', ListenOn); +restart_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> + esockd:reopen('mqtt:tls', ListenOn); restart_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> mochiweb:restart_http('mqtt:ws', ListenOn); restart_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> @@ -99,10 +64,36 @@ restart_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> restart_listener({Proto, ListenOn, _Opts}) -> esockd:reopen(Proto, ListenOn). +%% @doc Stop all listeners +-spec(stop_all() -> ok). +stop_all() -> + lists:foreach(fun stop_listener/1, emqx_config:get_env(listeners, [])). + +-spec(stop_listener(listener()) -> ok | {error, any()}). +stop_listener({tcp, ListenOn, _Opts}) -> + esockd:close('mqtt:tcp', ListenOn); +stop_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> + esockd:close('mqtt:tls', ListenOn); +stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> + mochiweb:stop_http('mqtt:ws', ListenOn); +stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> + mochiweb:stop_http('mqtt:wss', ListenOn); +stop_listener({Proto, ListenOn, _Opts}) -> + esockd:close(Proto, ListenOn). + merge_sockopts(Options) -> - %%TODO: tcp_options? - SockOpts = emqx_misc:merge_opts( - ?MQTT_SOCKOPTS, proplists:get_value(sockopts, Options, [])), - emqx_misc:merge_opts(Options, [{sockopts, SockOpts}]). + case lists:keytake(tcp_options, 1, Options) of + {value, {tcp_options, TcpOpts}, Options1} -> + [{tcp_options, emqx_misc:merge_opts(?MQTT_SOCKOPTS, TcpOpts)} | Options1]; + false -> + [{tcp_options, ?MQTT_SOCKOPTS} | Options] + end. +%% all() -> +%% [Listener || Listener = {{Proto, _}, _Pid} <- esockd:listeners(), is_mqtt(Proto)]. +%%is_mqtt('mqtt:tcp') -> true; +%%is_mqtt('mqtt:tls') -> true; +%%is_mqtt('mqtt:ws') -> true; +%%is_mqtt('mqtt:wss') -> true; +%%is_mqtt(_Proto) -> false. diff --git a/src/emqx_logger.erl b/src/emqx_logger.erl index 0c5cf9240..2af5cd6b7 100644 --- a/src/emqx_logger.erl +++ b/src/emqx_logger.erl @@ -1,28 +1,26 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. -module(emqx_logger). -compile({no_auto_import,[error/1]}). --export([debug/1, debug/2, debug/3, - info/1, info/2, info/3, - warning/1, warning/2, warning/3, - error/1, error/2, error/3, - critical/1, critical/2, critical/3]). +-export([debug/1, debug/2, debug/3]). +-export([info/1, info/2, info/3]). +-export([warning/1, warning/2, warning/3]). +-export([error/1, error/2, error/3]). +-export([critical/1, critical/2, critical/3]). debug(Msg) -> lager:debug(Msg). diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 79adf6ad1..3a96f75a6 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -1,45 +1,46 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_message). -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([make/2, make/3, make/4]). --export([get_flag/2, get_flag/3, set_flag/2, unset_flag/2]). +-export([new/2, new/3, new/4, new/5]). +-export([get_flag/2, get_flag/3, set_flag/2, set_flag/3, unset_flag/2]). -export([get_header/2, get_header/3, set_header/3]). -export([get_user_property/2, get_user_property/3, set_user_property/3]). --spec(make(topic(), payload()) -> message()). -make(Topic, Payload) -> - make(undefined, Topic, Payload). +-spec(new(topic(), payload()) -> message()). +new(Topic, Payload) -> + new(undefined, Topic, Payload). -%% Create a message --spec(make(atom() | client(), topic(), payload()) -> message()). -make(From, Topic, Payload) when is_atom(From); is_record(From, client) -> - make(From, ?QOS_0, Topic, Payload). +-spec(new(atom() | client(), topic(), payload()) -> message()). +new(From, Topic, Payload) when is_atom(From); is_record(From, client) -> + new(From, #{qos => ?QOS0}, Topic, Payload). -make(From, QoS, Topic, Payload) when is_atom(From); is_record(From, client) -> +-spec(new(atom() | client(), message_flags(), topic(), payload()) -> message()). +new(From, Flags, Topic, Payload) when is_atom(From); is_record(From, client) -> + new(From, Flags, #{}, Topic, Payload). + +-spec(new(atom() | client(), message_flags(), message_headers(), topic(), payload()) -> message()). +new(From, Flags, Headers, Topic, Payload) when is_atom(From); is_record(From, client) -> #message{id = msgid(), - qos = ?QOS_I(QoS), from = From, sender = self(), - flags = #{}, - headers = #{}, + flags = Flags, + headers = Headers, topic = Topic, properties = #{}, payload = Payload, @@ -58,6 +59,10 @@ get_flag(Flag, #message{flags = Flags}, Default) -> set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) -> Msg#message{flags = maps:put(Flag, true, Flags)}. +-spec(set_flag(message_flag(), boolean() | integer(), message()) -> message()). +set_flag(Flag, Val, Msg = #message{flags = Flags}) when is_atom(Flag) -> + Msg#message{flags = maps:put(Flag, Val, Flags)}. + %% @doc Unset flag -spec(unset_flag(message_flag(), message()) -> message()). unset_flag(Flag, Msg = #message{flags = Flags}) -> diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index c356e62d7..519b96fe4 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -1,35 +1,30 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_metrics). -include("emqx_mqtt.hrl"). -export([start_link/0]). - -export([new/1, all/0]). - -export([val/1, inc/1, inc/2, inc/3, dec/2, dec/3, set/2]). - %% Received/sent metrics -export([received/1, sent/1]). -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(state, {}). @@ -86,7 +81,6 @@ ]). -define(TAB, ?MODULE). - -define(SERVER, ?MODULE). %% @doc Start the metrics server @@ -94,9 +88,9 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Metrics API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ new({gauge, Name}) -> ets:insert(?TAB, {{Name, 0}, 0}); @@ -168,9 +162,9 @@ key(counter, Metric) -> update_counter(Key, UpOp) -> ets:update_counter(?TAB, Key, UpOp). -%%-------------------------------------------------------------------- -%% Receive/Sent metrics -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- +%% Received/Sent metrics +%%----------------------------------------------------------------------------- %% @doc Count packets received. -spec(received(mqtt_packet()) -> ok). @@ -248,9 +242,9 @@ qos_sent(?QOS_1) -> qos_sent(?QOS_2) -> inc('messages/qos2/sent'). -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% gen_server callbacks -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- init([]) -> % Create metrics table @@ -259,15 +253,15 @@ init([]) -> {ok, #state{}, hibernate}. handle_call(Req, _From, State) -> - emqx_logger:error("[METRICS] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[Metrics] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast(Msg, State) -> - emqx_logger:error("[METRICS] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Metrics] unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> - emqx_logger:error("[METRICS] Unexpected info: ~p", [Info]), + emqx_logger:error("[Metrics] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{}) -> diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index b61a06c60..e2b60a156 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -1,25 +1,23 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_misc). -export([merge_opts/2, start_timer/2, start_timer/3, cancel_timer/1, proc_name/2, proc_stats/0, proc_stats/1]). -%% @doc Merge Options +%% @doc Merge options -spec(merge_opts(list(), list()) -> list()). merge_opts(Defaults, Options) -> lists:foldl( @@ -42,7 +40,8 @@ cancel_timer(undefined) -> ok; cancel_timer(Timer) -> case catch erlang:cancel_timer(Timer) of - false -> receive {timeout, Timer, _} -> ok after 0 -> ok end; + false -> + receive {timeout, Timer, _} -> ok after 0 -> ok end; _ -> ok end. diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index ca3f83e8f..e41bd0587 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_presence). @@ -21,14 +19,13 @@ -include("emqx.hrl"). -export([load/1, unload/1]). - -export([on_client_connected/3, on_client_disconnected/3]). load(Env) -> emqx:hook('client.connected', fun ?MODULE:on_client_connected/3, [Env]), emqx:hook('client.disconnected', fun ?MODULE:on_client_disconnected/3, [Env]). -on_client_connected(ConnAck, Client = #client{client_id = ClientId, +on_client_connected(ConnAck, Client = #client{id = ClientId, username = Username, peername = {IpAddr, _} %%clean_sess = CleanSess, @@ -36,7 +33,7 @@ on_client_connected(ConnAck, Client = #client{client_id = ClientId, }, Env) -> case emqx_json:safe_encode([{clientid, ClientId}, {username, Username}, - {ipaddress, iolist_to_binary(emqx_net:ntoa(IpAddr))}, + {ipaddress, iolist_to_binary(esockd_net:ntoa(IpAddr))}, %%{clean_sess, CleanSess}, %%TODO:: fixme later %%{protocol, ProtoVer}, {connack, ConnAck}, @@ -49,8 +46,7 @@ on_client_connected(ConnAck, Client = #client{client_id = ClientId, end, {ok, Client}. -on_client_disconnected(Reason, #client{client_id = ClientId, - username = Username}, Env) -> +on_client_disconnected(Reason, #client{id = ClientId, username = Username}, Env) -> case emqx_json:safe_encode([{clientid, ClientId}, {username, Username}, {reason, reason(Reason)}, diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index 671fe7311..8ddd07f6c 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index d7669b274..6db5e30f3 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. -module(emqx_mod_subscription). @@ -33,9 +31,7 @@ load(Topics) -> emqx:hook('client.connected', fun ?MODULE:on_client_connected/3, [Topics]). -on_client_connected(RC, Client = #client{client_id = ClientId, - client_pid = ClientPid, - username = Username}, Topics) +on_client_connected(RC, Client = #client{id = ClientId, pid = ClientPid, username = Username}, Topics) when RC < 16#80 -> Replace = fun(Topic) -> rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) end, TopicTable = [{Replace(Topic), Qos} || {Topic, Qos} <- Topics], diff --git a/src/emqx_mod_sup.erl b/src/emqx_mod_sup.erl index d45a33810..ca5a3d61c 100644 --- a/src/emqx_mod_sup.erl +++ b/src/emqx_mod_sup.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,27 +11,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_mod_sup). -behaviour(supervisor). --include("emqx.hrl"). - -%% API -export([start_link/0, start_child/1, start_child/2, stop_child/1]). - -%% Supervisor callbacks -export([init/1]). %% Helper macro for declaring children of supervisor -define(CHILD(Mod, Type), {Mod, {Mod, start_link, []}, permanent, 5000, Type, [Mod]}). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). @@ -49,10 +38,9 @@ stop_child(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 cd5523280..09e732f68 100644 --- a/src/emqx_modules.erl +++ b/src/emqx_modules.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_modules). diff --git a/src/emqx_mqtt_properties.erl b/src/emqx_mqtt_properties.erl index 8ef559a37..4634d5bdc 100644 --- a/src/emqx_mqtt_properties.erl +++ b/src/emqx_mqtt_properties.erl @@ -1,19 +1,18 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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 MQTT5 Properties -module(emqx_mqtt_properties). -include("emqx_mqtt.hrl"). diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index 4f46526ed..e6bd16540 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,8 +11,8 @@ %% 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. -%%-------------------------------------------------------------------- +%% TODO: should be a bound queue. %% @doc A Simple in-memory message queue. %% %% Notice that MQTT is not an enterprise messaging queue. MQTT assume that client @@ -155,7 +154,7 @@ stats(#mqueue{type = Type, q = Q, max_len = MaxLen, len = Len, dropped = Dropped %% @doc Enqueue a message. -spec(in(message(), mqueue()) -> mqueue()). -in(#message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> +in(#message{flags = #{qos := ?QOS_0}}, MQ = #mqueue{qos0 = false}) -> MQ; in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}; @@ -167,8 +166,8 @@ in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len}) -> maybe_set_alarm(MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}); in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, - priorities = Priorities, - max_len = 0}) -> + priorities = Priorities, + max_len = 0}) -> case lists:keysearch(Topic, 1, Priorities) of {value, {_, Pri}} -> MQ#mqueue{q = ?PQUEUE:in(Msg, Pri, Q)}; diff --git a/src/emqx_net.erl b/src/emqx_net.erl deleted file mode 100644 index ea5bb2462..000000000 --- a/src/emqx_net.erl +++ /dev/null @@ -1,232 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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_net). - --include_lib("kernel/include/inet.hrl"). - --export([tcp_name/3, tcp_host/1, getopts/2, setopts/2, getaddr/2, - port_to_listeners/1]). - --export([peername/1, sockname/1, format/2, format/1, ntoa/1, - connection_string/2]). - --define(FIRST_TEST_BIND_PORT, 10000). - -%%-------------------------------------------------------------------- - -%% inet_parse:address takes care of ip string, like "0.0.0.0" -%% inet:getaddr returns immediately for ip tuple {0,0,0,0}, -%% and runs 'inet_gethost' port process for dns lookups. -%% On Windows inet:getaddr runs dns resolver for ip string, which may fail. -getaddr(Host, Family) -> - case inet_parse:address(Host) of - {ok, IPAddress} -> [{IPAddress, resolve_family(IPAddress, Family)}]; - {error, _} -> gethostaddr(Host, Family) - end. - -gethostaddr(Host, auto) -> - Lookups = [{Family, inet:getaddr(Host, Family)} || Family <- [inet, inet6]], - case [{IP, Family} || {Family, {ok, IP}} <- Lookups] of - [] -> host_lookup_error(Host, Lookups); - IPs -> IPs - end; - -gethostaddr(Host, Family) -> - case inet:getaddr(Host, Family) of - {ok, IPAddress} -> [{IPAddress, Family}]; - {error, Reason} -> host_lookup_error(Host, Reason) - end. - -host_lookup_error(Host, Reason) -> - error_logger:error_msg("invalid host ~p - ~p~n", [Host, Reason]), - throw({error, {invalid_host, Host, Reason}}). - -resolve_family({_,_,_,_}, auto) -> inet; -resolve_family({_,_,_,_,_,_,_,_}, auto) -> inet6; -resolve_family(IP, auto) -> throw({error, {strange_family, IP}}); -resolve_family(_, F) -> F. - -%%-------------------------------------------------------------------- - -%% There are three kinds of machine (for our purposes). -%% -%% * Those which treat IPv4 addresses as a special kind of IPv6 address -%% ("Single stack") -%% - Linux by default, Windows Vista and later -%% - We also treat any (hypothetical?) IPv6-only machine the same way -%% * Those which consider IPv6 and IPv4 to be completely separate things -%% ("Dual stack") -%% - OpenBSD, Windows XP / 2003, Linux if so configured -%% * Those which do not support IPv6. -%% - Ancient/weird OSes, Linux if so configured -%% -%% How to reconfigure Linux to test this: -%% Single stack (default): -%% echo 0 > /proc/sys/net/ipv6/bindv6only -%% Dual stack: -%% echo 1 > /proc/sys/net/ipv6/bindv6only -%% IPv4 only: -%% add ipv6.disable=1 to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub then -%% sudo update-grub && sudo reboot -%% -%% This matters in (and only in) the case where the sysadmin (or the -%% app descriptor) has only supplied a port and we wish to bind to -%% "all addresses". This means different things depending on whether -%% we're single or dual stack. On single stack binding to "::" -%% implicitly includes all IPv4 addresses, and subsequently attempting -%% to bind to "0.0.0.0" will fail. On dual stack, binding to "::" will -%% only bind to IPv6 addresses, and we need another listener bound to -%% "0.0.0.0" for IPv4. Finally, on IPv4-only systems we of course only -%% want to bind to "0.0.0.0". -%% -%% Unfortunately it seems there is no way to detect single vs dual stack -%% apart from attempting to bind to the port. -port_to_listeners(Port) -> - IPv4 = {"0.0.0.0", Port, inet}, - IPv6 = {"::", Port, inet6}, - case ipv6_status(?FIRST_TEST_BIND_PORT) of - single_stack -> [IPv6]; - ipv6_only -> [IPv6]; - dual_stack -> [IPv6, IPv4]; - ipv4_only -> [IPv4] - end. - -ipv6_status(TestPort) -> - IPv4 = [inet, {ip, {0,0,0,0}}], - IPv6 = [inet6, {ip, {0,0,0,0,0,0,0,0}}], - case gen_tcp:listen(TestPort, IPv6) of - {ok, LSock6} -> - case gen_tcp:listen(TestPort, IPv4) of - {ok, LSock4} -> - %% Dual stack - gen_tcp:close(LSock6), - gen_tcp:close(LSock4), - dual_stack; - %% Checking the error here would only let us - %% distinguish single stack IPv6 / IPv4 vs IPv6 only, - %% which we figure out below anyway. - {error, _} -> - gen_tcp:close(LSock6), - case gen_tcp:listen(TestPort, IPv4) of - %% Single stack - {ok, LSock4} -> gen_tcp:close(LSock4), - single_stack; - %% IPv6-only machine. Welcome to the future. - {error, eafnosupport} -> ipv6_only; %% Linux - {error, eprotonosupport}-> ipv6_only; %% FreeBSD - %% Dual stack machine with something already - %% on IPv4. - {error, _} -> ipv6_status(TestPort + 1) - end - end; - %% IPv4-only machine. Welcome to the 90s. - {error, eafnosupport} -> %% Linux - ipv4_only; - {error, eprotonosupport} -> %% FreeBSD - ipv4_only; - %% Port in use - {error, _} -> - ipv6_status(TestPort + 1) - end. - -tcp_name(Prefix, IPAddress, Port) - when is_atom(Prefix) andalso is_number(Port) -> - list_to_atom( - lists:flatten( - io_lib:format( - "~w_~s:~w", [Prefix, inet_parse:ntoa(IPAddress), Port]))). - -connection_string(Sock, Direction) -> - case socket_ends(Sock, Direction) of - {ok, {FromAddress, FromPort, ToAddress, ToPort}} -> - {ok, lists:flatten( - io_lib:format( - "~s:~p -> ~s:~p", - [maybe_ntoab(FromAddress), FromPort, - maybe_ntoab(ToAddress), ToPort]))}; - Error -> - Error - end. - -socket_ends(Sock, Direction) -> - {From, To} = sock_funs(Direction), - case {From(Sock), To(Sock)} of - {{ok, {FromAddress, FromPort}}, {ok, {ToAddress, ToPort}}} -> - {ok, {rdns(FromAddress), FromPort, - rdns(ToAddress), ToPort}}; - {{error, _Reason} = Error, _} -> - Error; - {_, {error, _Reason} = Error} -> - Error - end. - -maybe_ntoab(Addr) when is_tuple(Addr) -> ntoab(Addr); -maybe_ntoab(Host) -> Host. - -rdns(Addr) -> Addr. - -sock_funs(inbound) -> {fun peername/1, fun sockname/1}; -sock_funs(outbound) -> {fun sockname/1, fun peername/1}. - -getopts(Sock, Options) when is_port(Sock) -> - inet:getopts(Sock, Options). - -setopts(Sock, Options) when is_port(Sock) -> - inet:setopts(Sock, Options). - -sockname(Sock) when is_port(Sock) -> inet:sockname(Sock). - -peername(Sock) when is_port(Sock) -> inet:peername(Sock). - -format(sockname, SockName) -> - format(SockName); -format(peername, PeerName) -> - format(PeerName). -format({Addr, Port}) -> - lists:flatten(io_lib:format("~s:~p", [maybe_ntoab(Addr), Port])). - -ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> - inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}); -ntoa(IP) -> - inet_parse:ntoa(IP). - -ntoab(IP) -> - Str = ntoa(IP), - case string:str(Str, ":") of - 0 -> Str; - _ -> "[" ++ Str ++ "]" - end. - -tcp_host({0,0,0,0}) -> - hostname(); - -tcp_host({0,0,0,0,0,0,0,0}) -> - hostname(); - -tcp_host(IPAddress) -> - case inet:gethostbyaddr(IPAddress) of - {ok, #hostent{h_name = Name}} -> Name; - {error, _Reason} -> ntoa(IPAddress) - end. - -hostname() -> - {ok, Hostname} = inet:gethostname(), - case inet:gethostbyname(Hostname) of - {ok, #hostent{h_name = Name}} -> Name; - {error, _Reason} -> Hostname - end. - diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 655bdd504..dc88d59d7 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). @@ -21,9 +19,7 @@ -include("emqx_mqtt.hrl"). -export([protocol_name/1, type_name/1]). - -export([format/1]). - -export([to_message/1, from_message/1]). %% @doc Protocol name of version @@ -39,9 +35,8 @@ type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> %% @doc From Message to Packet -spec(from_message(message()) -> mqtt_packet()). -from_message(Msg = #message{qos = Qos, - topic = Topic, - payload = Payload}) -> +from_message(Msg = #message{topic = Topic, payload = Payload}) -> + Qos = emqx_message:get_flag(qos, Msg, 0), Dup = emqx_message:get_flag(dup, Msg, false), Retain = emqx_message:get_flag(retain, Msg, false), PacketId = emqx_message:get_header(packet_id, Msg), @@ -61,13 +56,12 @@ to_message(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, dup = Dup}, variable = #mqtt_packet_publish{topic_name = Topic, packet_id = PacketId, - properties = Props}, + properties = Properties}, payload = Payload}) -> - Msg = emqx_message:make(undefined, Topic, Payload), - Msg#message{qos = Qos, - flags = #{dup => Dup, retain => Retain}, - headers = #{packet_id => PacketId}, - properties = Props}; + Flags = #{dup => Dup, retain => Retain, qos => Qos}, + Msg = emqx_message:new(undefined, Flags, #{packet_id => PacketId}, Topic, Payload), + Msg#message{properties = Properties}; + to_message(#mqtt_packet_connect{will_flag = false}) -> undefined; to_message(#mqtt_packet_connect{will_retain = Retain, @@ -75,10 +69,8 @@ to_message(#mqtt_packet_connect{will_retain = Retain, will_topic = Topic, will_props = Props, will_payload = Payload}) -> - Msg = emqx_message:make(undefined, Topic, Payload), - Msg#message{flags = #{retain => Retain}, - headers = #{qos => Qos}, - properties = Props}. + Msg = emqx_message:new(undefined, #{qos => Qos, retain => Retain}, Topic, Payload), + Msg#message{properties = Props}. %% @doc Format packet -spec(format(mqtt_packet()) -> iolist()). diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index f6b4611ec..8b421f20a 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -1,57 +1,52 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_pmon). -export([new/0]). - -export([monitor/2, monitor/3, demonitor/2, find/2, erase/2]). +-compile({no_auto_import,[monitor/3]}). --compile({no_auto_import,[monitor/3]}). --type(pmon() :: map()). - +-type(pmon() :: {?MODULE, map()}). -export_type([pmon/0]). -spec(new() -> pmon()). -new() -> - maps:new(). +new() -> {?MODULE, maps:new()}. -spec(monitor(pid(), pmon()) -> pmon()). monitor(Pid, PM) -> monitor(Pid, undefined, PM). -monitor(Pid, Val, PM) -> - 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, {?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}. -spec(demonitor(pid(), pmon()) -> pmon()). -demonitor(Pid, PM) -> - case maps:find(Pid, PM) of - {ok, {Ref, _Val}} -> - %% Don't flush - _ = erlang:demonitor(Ref), - maps:remove(Pid, PM); - error -> PM - end. +demonitor(Pid, {?MODULE, PM}) -> + {?MODULE, case maps:find(Pid, PM) of + {ok, {Ref, _Val}} -> + %% Don't flush + _ = erlang:demonitor(Ref), + maps:remove(Pid, PM); + error -> PM + end}. -spec(find(pid(), pmon()) -> undefined | term()). -find(Pid, PM) -> +find(Pid, {?MODULE, PM}) -> case maps:find(Pid, PM) of {ok, {_Ref, Val}} -> Val; @@ -59,6 +54,6 @@ find(Pid, PM) -> end. -spec(erase(pid(), pmon()) -> pmon()). -erase(Pid, PM) -> - maps:remove(Pid, PM). +erase(Pid, {?MODULE, PM}) -> + {?MODULE, maps:remove(Pid, PM)}. diff --git a/src/emqx_pool.erl b/src/emqx_pool.erl index 43f4ef845..8d927ddd9 100644 --- a/src/emqx_pool.erl +++ b/src/emqx_pool.erl @@ -1,50 +1,40 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_pool). -behaviour(gen_server). -%% Start the pool supervisor --export([start_link/0]). +-export([start_link/0, start_link/2]). +-export([submit/1, async_submit/1]). -%% API Exports --export([start_link/2, submit/1, async_submit/1]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(state, {pool, id}). -define(POOL, ?MODULE). -%% @doc Start Pooler Supervisor. +%% @doc Start pooler supervisor. start_link() -> emqx_pool_sup:start_link(?POOL, random, {?MODULE, start_link, []}). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - --spec(start_link(atom(), pos_integer()) - -> {ok, pid()} | ignore | {error, term()}). +%% @doc Start pool +-spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> - gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, - ?MODULE, [Pool, Id], []). + gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, [Pool, Id], []). %% @doc Submit work to the pool -spec(submit(fun()) -> any()). @@ -59,45 +49,45 @@ async_submit(Fun) -> worker() -> gproc_pool:pick_worker(pool). -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% gen_server callbacks -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- init([Pool, Id]) -> - gproc_pool:connect_worker(Pool, {Pool, Id}), + true = gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #state{pool = Pool, id = Id}}. handle_call({submit, Fun}, _From, State) -> {reply, catch run(Fun), State}; handle_call(Req, _From, State) -> - emqx_logger:error("[POOL] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[Pool] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast({async_submit, Fun}, State) -> try run(Fun) - catch _:Error -> - emqx_logger:error("[POOL] Error: ~p, ~p", [Error, erlang:get_stacktrace()]) + catch _:Error:Stacktrace -> + emqx_logger:error("[Pool] error: ~p, ~p", [Error, Stacktrace]) end, {noreply, State}; handle_cast(Msg, State) -> - emqx_logger:error("[POOL] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Pool] unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> - emqx_logger:error("[POOL] Unexpected info: ~p", [Info]), + emqx_logger:error("[Pool] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> - gproc_pool:disconnect_worker(Pool, {Pool, Id}). + true = gproc_pool:disconnect_worker(Pool, {Pool, 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 29f656c73..efcde5d2f 100644 --- a/src/emqx_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -1,27 +1,23 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_pool_sup). -behaviour(supervisor). -%% API -export([spec/1, spec/2, start_link/3, start_link/4]). -%% Supervisor callbacks -export([init/1]). -spec(spec(list()) -> supervisor:child_spec()). @@ -35,8 +31,7 @@ spec(ChildId, Args) -> -spec(start_link(atom() | tuple(), atom(), mfa()) -> {ok, pid()} | {error, term()}). start_link(Pool, Type, MFA) -> - Schedulers = erlang:system_info(schedulers), - start_link(Pool, Type, Schedulers, MFA). + start_link(Pool, Type, emqx_vm:schedulers(schedulers), MFA). -spec(start_link(atom() | tuple(), atom(), pos_integer(), mfa()) -> {ok, pid()} | {error, term()}). start_link(Pool, Type, Size, MFA) when is_atom(Pool) -> @@ -49,8 +44,7 @@ init([Pool, Type, Size, {M, F, Args}]) -> {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]} + {{M, I}, {M, F, [Pool, I | Args]}, transient, 5000, worker, [M]} end || I <- lists:seq(1, Size)]}}. ensure_pool(Pool, Type, Opts) -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 30fbb1848..c932089e7 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -129,11 +129,10 @@ client(#proto_state{client_id = ClientId, WillMsg =:= undefined -> undefined; true -> WillMsg#message.topic end, - #client{client_id = ClientId, - client_pid = ClientPid, - username = Username, - peername = Peername, - mountpoint = MountPoint}. + #client{id = ClientId, + pid = ClientPid, + username = Username, + peername = Peername}. session(#proto_state{session = Session}) -> Session. @@ -334,7 +333,7 @@ publish(Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), mountpoint = MountPoint, session = Session}) -> Msg = emqx_packet:to_message(Packet), - Msg1 = Msg#message{from = #client{client_id = ClientId, username = Username}}, + Msg1 = Msg#message{from = #client{id = ClientId, username = Username}}, emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)); publish(Packet = ?PUBLISH_PACKET(?QOS_1, _PacketId), State) -> @@ -350,7 +349,7 @@ with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), session = Session}) -> %% TODO: ... Msg = emqx_packet:to_message(Packet), - Msg1 = Msg#message{from = #client{client_id = ClientId, username = Username}}, + Msg1 = Msg#message{from = #client{id = ClientId, username = Username}}, case emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)) of ok -> case Type of diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index 029914f48..335be3bd4 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -1,19 +1,18 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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 MQTT5 reason codes -module(emqx_reason_codes). -export([name/1, text/1]). @@ -61,7 +60,7 @@ name(16#9F) -> connection_rate_exceeded; name(16#A0) -> maximum_connect_time; name(16#A1) -> subscription_identifiers_not_supported; name(16#A2) -> wildcard_subscriptions_not_supported; -name(Code) -> list_to_atom("unkown_" ++ integer_to_list(Code)). +name(Code) -> list_to_atom("unkown_reason_code" ++ integer_to_list(Code)). text(16#00) -> <<"Success">>; text(16#01) -> <<"Granted QoS 1">>; @@ -106,5 +105,5 @@ text(16#9F) -> <<"Connection rate exceeded">>; text(16#A0) -> <<"Maximum connect time">>; text(16#A1) -> <<"Subscription Identifiers not supported">>; text(16#A2) -> <<"Wildcard Subscriptions not supported">>; -text(Code) -> iolist_to_binary(["Unkown", integer_to_list(Code)]). +text(Code) -> iolist_to_binary(["Unkown Reason Code:", integer_to_list(Code)]). diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 7aff3a446..8f6375720 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_router). @@ -34,13 +32,11 @@ -export([get_routes/1]). -export([del_route/1, del_route/2, del_route/3]). -export([has_routes/1, match_routes/1, print_routes/1]). - -%% Topics -export([topics/0]). %% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -type(destination() :: node() | {binary(), node()}). @@ -49,15 +45,12 @@ -record(state, {pool, id, batch :: #batch{}}). -define(ROUTE, emqx_route). - -define(BATCH(Enabled), #batch{enabled = Enabled}). +-define(BATCH(Enabled, Pending), #batch{enabled = Enabled, pending = Pending}). --define(BATCH(Enabled, Pending), - #batch{enabled = Enabled, pending = Pending}). - -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% Mnesia bootstrap -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- mnesia(boot) -> ok = ekka_mnesia:create_table(?ROUTE, [ @@ -65,23 +58,21 @@ mnesia(boot) -> {ram_copies, [node()]}, {record_name, route}, {attributes, record_info(fields, route)}]); - mnesia(copy) -> ok = ekka_mnesia:copy_table(?ROUTE). -%%-------------------------------------------------------------------- -%% Strat a Router -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ +%% Strat a router +%%------------------------------------------------------------------------------ --spec(start_link(atom(), pos_integer()) - -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, [Pool, Id], [{hibernate_after, 2000}]). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Route APIs -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -spec(add_route(topic() | route()) -> ok). add_route(Topic) when is_binary(Topic) -> @@ -95,8 +86,7 @@ add_route(Topic, Dest) when is_binary(Topic) -> -spec(add_route({pid(), reference()}, topic(), destination()) -> ok). add_route(From, Topic, Dest) when is_binary(Topic) -> - Route = #route{topic = Topic, dest = Dest}, - cast(pick(Topic), {add_route, From, Route}). + cast(pick(Topic), {add_route, From, #route{topic = Topic, dest = Dest}}). -spec(get_routes(topic()) -> [route()]). get_routes(Topic) -> @@ -114,8 +104,7 @@ del_route(Topic, Dest) when is_binary(Topic) -> -spec(del_route({pid(), reference()}, topic(), destination()) -> ok). del_route(From, Topic, Dest) when is_binary(Topic) -> - Route = #route{topic = Topic, dest = Dest}, - cast(pick(Topic), {del_route, From, Route}). + cast(pick(Topic), {del_route, From, #route{topic = Topic, dest = Dest}}). -spec(has_routes(topic()) -> boolean()). has_routes(Topic) when is_binary(Topic) -> @@ -144,9 +133,9 @@ cast(Router, Msg) -> pick(Topic) -> gproc_pool:pick_worker(router, Topic). -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% gen_server callbacks -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- init([Pool, Id]) -> rand:seed(exsplus, erlang:timestamp()), @@ -156,12 +145,12 @@ init([Pool, Id]) -> {ok, ensure_batch_timer(#state{pool = Pool, id = Id, batch = Batch})}. handle_call(Req, _From, State) -> - emqx_logger:error("[Router] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[Router] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast({add_route, From, Route}, State) -> {noreply, NewState} = handle_cast({add_route, Route}, State), - gen_server:reply(From, ok), + _ = gen_server:reply(From, ok), {noreply, NewState}; handle_cast({add_route, Route = #route{topic = Topic, dest = Dest}}, State) -> @@ -178,7 +167,7 @@ handle_cast({add_route, Route = #route{topic = Topic, dest = Dest}}, State) -> handle_cast({del_route, From, Route}, State) -> {noreply, NewState} = handle_cast({del_route, Route}, State), - gen_server:reply(From, ok), + _ = gen_server:reply(From, ok), {noreply, NewState}; handle_cast({del_route, Route = #route{topic = Topic}}, State) -> @@ -194,7 +183,7 @@ handle_cast({del_route, Route = #route{topic = Topic}}, State) -> end}; handle_cast(Msg, State) -> - emqx_logger:error("[Router] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Router] unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({timeout, _TRef, batch_delete}, State = #state{batch = Batch}) -> @@ -202,7 +191,7 @@ handle_info({timeout, _TRef, batch_delete}, State = #state{batch = Batch}) -> {noreply, ensure_batch_timer(State#state{batch = ?BATCH(true, sets:new())}), hibernate}; handle_info(Info, State) -> - emqx_logger:error("[Router] Unexpected info: ~p", [Info]), + emqx_logger:error("[Router] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id, batch = Batch}) -> @@ -212,9 +201,9 @@ terminate(_Reason, #state{pool = Pool, id = Id, batch = Batch}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% Internal functions -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- ensure_batch_timer(State = #state{batch = #batch{enabled = false}}) -> State; @@ -225,7 +214,7 @@ ensure_batch_timer(State = #state{batch = Batch}) -> cacel_batch_timer(#batch{enabled = false}) -> ok; cacel_batch_timer(#batch{enabled = true, timer = TRef}) -> - erlang:cancel_timer(TRef). + catch erlang:cancel_timer(TRef). add_direct_route(Route) -> mnesia:async_dirty(fun mnesia:write/3, [?ROUTE, Route, sticky_write]). @@ -275,6 +264,6 @@ trans(Fun, Args) -> end. log(ok) -> ok; -log({error, Error}) -> - emqx_logger:error("[Router] Mnesia aborted: ~p", [Error]). +log({error, Reason}) -> + emqx_logger:error("[Router] mnesia aborted: ~p", [Reason]). diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index 0606f70fd..d0d0e8075 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_router_helper). @@ -30,11 +28,10 @@ -export([start_link/0, monitor/1]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(routing_node, {name, const = unused}). - -record(state, {nodes = []}). -compile({no_auto_import, [monitor/1]}). @@ -42,12 +39,11 @@ -define(SERVER, ?MODULE). -define(ROUTE, emqx_route). -define(ROUTING_NODE, emqx_routing_node). - -define(LOCK, {?MODULE, cleanup_routes}). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Mnesia bootstrap -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ mnesia(boot) -> ok = ekka_mnesia:create_table(?ROUTING_NODE, [ @@ -59,9 +55,9 @@ mnesia(boot) -> mnesia(copy) -> ok = ekka_mnesia:copy_table(?ROUTING_NODE). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% @doc Starts the router helper -spec(start_link() -> {ok, pid()} | ignore | {error, any()}). @@ -79,9 +75,9 @@ monitor(Node) when is_atom(Node) -> false -> mnesia:dirty_write(?ROUTING_NODE, #routing_node{name = Node}) end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([]) -> ekka:monitor(membership), @@ -98,16 +94,15 @@ init([]) -> {ok, #state{nodes = Nodes}, hibernate}. handle_call(Req, _From, State) -> - emqx_logger:error("[RouterHelper] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[RouterHelper] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast(Msg, State) -> - emqx_logger:error("[RouterHelper] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[RouterHelper] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({mnesia_table_event, {write, #routing_node{name = Node}, _}}, - State = #state{nodes = Nodes}) -> - emqx_logger:info("[RouterHelper] New routing node: ~s", [Node]), +handle_info({mnesia_table_event, {write, #routing_node{name = Node}, _}}, State = #state{nodes = Nodes}) -> + emqx_logger:info("[RouterHelper] write routing node: ~s", [Node]), case ekka:is_member(Node) orelse lists:member(Node, Nodes) of true -> {noreply, State}; false -> _ = erlang:monitor_node(Node, true), @@ -132,7 +127,7 @@ handle_info({membership, _Event}, State) -> {noreply, State}; handle_info(Info, State) -> - emqx_logger:error("[RouteHelper] Unexpected info: ~p", [Info]), + emqx_logger:error("[RouteHelper] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{}) -> @@ -143,9 +138,9 @@ terminate(_Reason, #state{}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ stats_fun() -> fun() -> diff --git a/src/emqx_router_sup.erl b/src/emqx_router_sup.erl index bc8daf4b4..004b88bb8 100644 --- a/src/emqx_router_sup.erl +++ b/src/emqx_router_sup.erl @@ -1,25 +1,22 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_router_sup). -behaviour(supervisor). -export([start_link/0]). - -export([init/1]). start_link() -> @@ -34,6 +31,5 @@ init([]) -> RouterPool = emqx_pool_sup:spec(emqx_router_pool, [router, hash, emqx_vm:schedulers(), {emqx_router, start_link, []}]), - {ok, {{one_for_all, 0, 1}, [Helper, RouterPool]}}. diff --git a/src/emqx_rpc.erl b/src/emqx_rpc.erl index f7c13fa4e..d4433bf6c 100644 --- a/src/emqx_rpc.erl +++ b/src/emqx_rpc.erl @@ -1,26 +1,24 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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 wrap gen_rpc? -module(emqx_rpc). -export([call/4, cast/4]). - -export([multicall/4]). --define(RPC, rpc). +-define(RPC, gen_rpc). call(Node, Mod, Fun, Args) -> ?RPC:call(Node, Mod, Fun, Args). diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl index c1941207b..644e33f37 100644 --- a/src/emqx_session_sup.erl +++ b/src/emqx_session_sup.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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). @@ -37,6 +35,10 @@ start_session(Attrs) -> init([]) -> {ok, {{simple_one_for_one, 0, 1}, - [{session, {emqx_session, start_link, []}, - temporary, 5000, worker, [emqx_session]}]}}. + [#{id => session, + start => {emqx_session, start_link, []}, + restart => temporary, + shutdown => 5000, + type => worker, + modules => [emqx_session]}]}}. diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 6825ca8d0..9380f38f7 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_shared_sub). @@ -33,20 +31,18 @@ -export([dispatch/3]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -define(SERVER, ?MODULE). - -define(TAB, emqx_shared_subscription). -record(state, {pmon}). - -record(shared_subscription, {group, topic, subpid}). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Mnesia bootstrap -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ @@ -58,15 +54,15 @@ mnesia(boot) -> mnesia(copy) -> ok = ekka_mnesia:copy_table(?TAB). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ --spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +-spec(start_link() -> {ok, pid()} | ignore | {error, term()}). start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). --spec(strategy() -> random | hash). +-spec(strategy() -> round_robin | random | hash). strategy() -> emqx_config:get_env(shared_subscription_strategy, random). @@ -84,7 +80,7 @@ unsubscribe(Group, Topic, SubPid) when is_pid(SubPid) -> record(Group, Topic, SubPid) -> #shared_subscription{group = Group, topic = Topic, subpid = SubPid}. -%% TODO: ensure the delivery... +%% TODO: dispatch strategy, ensure the delivery... dispatch(Group, Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> case pick(subscribers(Group, Topic)) of false -> Delivery; @@ -97,16 +93,15 @@ pick([]) -> pick([SubPid]) -> SubPid; pick(SubPids) -> - X = abs(erlang:monotonic_time() - bxor erlang:unique_integer()), + X = abs(erlang:monotonic_time() bxor erlang:unique_integer()), lists:nth((X rem length(SubPids)) + 1, SubPids). subscribers(Group, Topic) -> ets:select(?TAB, [{{shared_subscription, Group, Topic, '$1'}, [], ['$1']}]). -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% gen_server callbacks -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- init([]) -> {atomic, PMon} = mnesia:transaction(fun init_monitors/0), @@ -120,14 +115,14 @@ init_monitors() -> end, emqx_pmon:new(), ?TAB). handle_call(Req, _From, State) -> - emqx_logger:error("[Shared] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[SharedSub] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast({monitor, SubPid}, State= #state{pmon = PMon}) -> {noreply, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})}; handle_cast(Msg, State) -> - emqx_logger:error("[Shared] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[SharedSub] unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = PMon}) -> @@ -142,12 +137,12 @@ handle_info({mnesia_table_event, _Event}, State) -> {noreply, State}; handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) -> - emqx_logger:info("Shared subscription down: ~p", [SubPid]), + emqx_logger:info("[SharedSub] shared subscriber down: ~p", [SubPid]), mnesia:async_dirty(fun cleanup_down/1, [SubPid]), {noreply, update_stats(State#state{pmon = emqx_pmon:erase(SubPid, PMon)})}; handle_info(Info, State) -> - emqx_logger:error("[Shared] Unexpected info: ~p", [Info]), + emqx_logger:error("[SharedSub] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> @@ -161,8 +156,8 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- cleanup_down(SubPid) -> - Pat = #shared_subscription{_ = '_', subpid = SubPid}, - lists:foreach(fun(Record) -> mnesia:delete_object(?TAB, Record) end, mnesia:match_object(Pat)). + lists:foreach(fun(Record) -> mnesia:delete_object(?TAB, Record) end, + mnesia:match_object(#shared_subscription{_ = '_', subpid = SubPid})). update_stats(State) -> emqx_stats:setstat('subscriptions/shared/count', 'subscriptions/shared/max', ets:info(?TAB, size)), State. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 50ce9cb75..de16a2b0f 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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). @@ -30,8 +28,8 @@ %% Internal functions for rpc -export([dispatch/3]). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, {session_pmon}). @@ -50,7 +48,8 @@ start_link() -> %% @doc Open a session. -spec(open_session(map()) -> {ok, pid()} | {error, term()}). open_session(Attrs = #{clean_start := true, - client_id := ClientId, client_pid := ClientPid}) -> + client_id := ClientId, + client_pid := ClientPid}) -> CleanStart = fun(_) -> ok = discard_session(ClientId, ClientPid), emqx_session_sup:start_session(Attrs) @@ -58,7 +57,8 @@ open_session(Attrs = #{clean_start := true, emqx_sm_locker:trans(ClientId, CleanStart); open_session(Attrs = #{clean_start := false, - client_id := ClientId, client_pid := ClientPid}) -> + client_id := ClientId, + client_pid := ClientPid}) -> ResumeStart = fun(_) -> case resume_session(ClientId, ClientPid) of {ok, SessionPid} -> @@ -72,7 +72,7 @@ open_session(Attrs = #{clean_start := false, emqx_sm_locker:trans(ClientId, ResumeStart). %% @doc Discard all the sessions identified by the ClientId. --spec(discard_session(map()) -> ok). +-spec(discard_session(client_id()) -> ok). discard_session(ClientId) when is_binary(ClientId) -> discard_session(ClientId, self()). @@ -80,8 +80,8 @@ discard_session(ClientId, ClientPid) when is_binary(ClientId) -> lists:foreach( fun({_ClientId, SessionPid}) -> case catch emqx_session:discard(SessionPid, ClientPid) of - {'EXIT', Error} -> - emqx_logger:error("[SM] Failed to discard ~p: ~p", [SessionPid, Error]); + {Err, Reason} when Err =:= 'EXIT'; Err =:= error -> + emqx_logger:error("[SM] Failed to discard ~p: ~p", [SessionPid, Reason]); ok -> ok end end, lookup_session(ClientId)). @@ -126,7 +126,7 @@ register_session(Session = {ClientId, SessionPid}, Attrs) ets:insert(?SESSION_ATTRS, {Session, Attrs}), case proplists:get_value(clean_start, Attrs, true) of true -> ok; - false -> ets:insert(?SESSION_P, Session) + false -> ets:insert(?SESSION_P, Session) end, emqx_sm_registry:register_session(Session), notify({registered, ClientId, SessionPid}). @@ -197,11 +197,12 @@ safe_lookup_element(Tab, Key, Default) -> error:badarg -> Default end. -notify(Event) -> gen_server:cast(?SM, {notify, Event}). +notify(Event) -> + gen_server:cast(?SM, {notify, Event}). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([]) -> TabOpts = [public, set, {write_concurrency, true}], @@ -213,8 +214,8 @@ init([]) -> {ok, #state{session_pmon = emqx_pmon:new()}}. handle_call(Req, _From, State) -> - emqx_logger:error("[SM] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[SM] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast({notify, {registered, ClientId, SessionPid}}, State = #state{session_pmon = PMon}) -> {noreply, State#state{session_pmon = emqx_pmon:monitor(SessionPid, ClientId, PMon)}}; @@ -223,7 +224,7 @@ handle_cast({notify, {unregistered, _ClientId, SessionPid}}, State = #state{sess {noreply, State#state{session_pmon = emqx_pmon:demonitor(SessionPid, PMon)}}; handle_cast(Msg, State) -> - emqx_logger:error("[SM] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[SM] unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{session_pmon = PMon}) -> @@ -235,24 +236,23 @@ handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{session_p end; handle_info(Info, State) -> - emqx_logger:error("[SM] Unexpected info: ~p", [Info]), + emqx_logger:error("[SM] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> - emqx_stats:cancel_update(cm_stats). + emqx_stats:cancel_update(sm_stats). code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ stats_fun() -> - fun () -> + fun() -> safe_update_stats(?SESSION, 'sessions/count', 'sessions/max'), - safe_update_stats(?SESSION_P, 'sessions/persistent/count', - 'sessions/persistent/max') + safe_update_stats(?SESSION_P, 'sessions/persistent/count', 'sessions/persistent/max') end. safe_update_stats(Tab, Stat, MaxStat) -> diff --git a/src/emqx_sm_locker.erl b/src/emqx_sm_locker.erl index 50b1fe462..050e6276c 100644 --- a/src/emqx_sm_locker.erl +++ b/src/emqx_sm_locker.erl @@ -1,27 +1,23 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_locker). -include("emqx.hrl"). -export([start_link/0]). - -export([trans/2, trans/3]). - -export([lock/1, lock/2, unlock/1]). -spec(start_link() -> {ok, pid()} | ignore | {error, term()}). @@ -32,9 +28,7 @@ start_link() -> trans(ClientId, Fun) -> trans(ClientId, Fun, undefined). --spec(trans(client_id() | undefined, - fun(([node()]) -> any()), - ekka_locker:piggyback()) -> any()). +-spec(trans(client_id() | undefined, fun(([node()]) -> any()), ekka_locker:piggyback()) -> any()). trans(undefined, Fun, _Piggyback) -> Fun([]); trans(ClientId, Fun, Piggyback) -> @@ -49,8 +43,7 @@ trans(ClientId, Fun, Piggyback) -> lock(ClientId) -> ekka_locker:aquire(?MODULE, ClientId, strategy()). --spec(lock(client_id(), ekka_locker:piggyback()) - -> ekka_locker:lock_result()). +-spec(lock(client_id(), ekka_locker:piggyback()) -> ekka_locker:lock_result()). lock(ClientId, Piggyback) -> ekka_locker:aquire(?MODULE, ClientId, strategy(), Piggyback). @@ -60,5 +53,5 @@ unlock(ClientId) -> -spec(strategy() -> local | one | quorum | all). strategy() -> - application:get_env(emqx, session_locking_strategy, quorum). + emqx_config:get_env(session_locking_strategy, quorum). diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index 085403d79..4a9be13c0 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_registry). @@ -20,25 +18,17 @@ -include("emqx.hrl"). -%% API -export([start_link/0]). - -export([is_enabled/0]). - -export([register_session/1, lookup_session/1, unregister_session/1]). - %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(REGISTRY, ?MODULE). - -define(TAB, emqx_session_registry). - -define(LOCK, {?MODULE, cleanup_sessions}). -record(global_session, {sid, pid}). - -record(state, {}). -type(session_pid() :: pid()). @@ -58,21 +48,19 @@ lookup_session(ClientId) -> <- mnesia:dirty_read(?TAB, ClientId)]. -spec(register_session({client_id(), session_pid()}) -> ok). -register_session({ClientId, SessionPid}) when is_binary(ClientId), - is_pid(SessionPid) -> +register_session({ClientId, SessionPid}) when is_binary(ClientId), is_pid(SessionPid) -> mnesia:dirty_write(?TAB, record(ClientId, SessionPid)). -spec(unregister_session({client_id(), session_pid()}) -> ok). -unregister_session({ClientId, SessionPid}) when is_binary(ClientId), - is_pid(SessionPid) -> +unregister_session({ClientId, SessionPid}) when is_binary(ClientId), is_pid(SessionPid) -> mnesia:dirty_delete_object(?TAB, record(ClientId, SessionPid)). record(ClientId, SessionPid) -> #global_session{sid = ClientId, pid = SessionPid}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([]) -> ok = ekka_mnesia:create_table(?TAB, [ @@ -85,11 +73,11 @@ init([]) -> {ok, #state{}}. handle_call(Req, _From, State) -> - emqx_logger:error("[Registry] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[Registry] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast(Msg, State) -> - emqx_logger:error("[Registry] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Registry] unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({membership, {mnesia, down, Node}}, State) -> @@ -103,7 +91,7 @@ handle_info({membership, _Event}, State) -> {noreply, State}; handle_info(Info, State) -> - emqx_logger:error("[Registry] Unexpected info: ~p", [Info]), + emqx_logger:error("[Registry] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> @@ -112,9 +100,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ cleanup_sessions(Node) -> Pat = [{#global_session{pid = '$1', _ = '_'}, diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index b01f13e41..0be9facb0 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -1,18 +1,17 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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). @@ -26,17 +25,26 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - %% Session Locker - Locker = {locker, {emqx_sm_locker, start_link, []}, - permanent, 5000, worker, [emqx_sm_locker]}, - - %% Session Registry - Registry = {registry, {emqx_sm_registry, start_link, []}, - permanent, 5000, worker, [emqx_sm_registry]}, - + %% 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 = {manager, {emqx_sm, start_link, []}, - permanent, 5000, worker, [emqx_sm]}, - + Manager = #{id => manager, + start => {emqx_sm, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_sm]}, {ok, {{rest_for_one, 10, 3600}, [Locker, Registry, Manager]}}. diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index 0bcf2e5b8..479021bf0 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_stats). @@ -27,15 +25,13 @@ %% Stats API. -export([statsfun/1, statsfun/2, getstats/0, getstat/1, setstat/2, setstat/3]). - -export([update_interval/2, update_interval/3, cancel_update/1]). -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(update, {name, countdown, interval, func}). - -record(state, {timer, updates :: #update{}}). -type(stats() :: list({atom(), non_neg_integer()})). @@ -85,10 +81,6 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - %% Get all stats. -spec(all() -> stats()). all() -> getstats(). @@ -97,7 +89,7 @@ all() -> getstats(). -spec(statsfun(Stat :: atom()) -> fun()). statsfun(Stat) -> fun(Val) -> setstat(Stat, Val) end. - + -spec(statsfun(Stat :: atom(), MaxStat :: atom()) -> fun()). statsfun(Stat, MaxStat) -> fun(Val) -> setstat(Stat, MaxStat, Val) end. @@ -147,23 +139,23 @@ rec(Name, Secs, UpFun) -> cast(Msg) -> gen_server:cast(?SERVER, Msg). -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% gen_server callbacks -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- init([]) -> _ = emqx_tables:new(?TAB, [set, public, {write_concurrency, true}]), Stats = lists:append([?CLIENT_STATS, ?SESSION_STATS, ?PUBSUB_STATS, ?ROUTE_STATS, ?RETAINED_STATS]), - ets:insert(?TAB, [{Name, 0} || Name <- Stats]), + true = ets:insert(?TAB, [{Name, 0} || Name <- Stats]), {ok, start_timer(#state{updates = []}), hibernate}. start_timer(State) -> State#state{timer = emqx_misc:start_timer(timer:seconds(1), tick)}. handle_call(Req, _From, State) -> - emqx_logger:error("[STATS] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[Stats] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast({setstat, Stat, MaxStat, Val}, State) -> try ets:lookup_element(?TAB, MaxStat, 2) of @@ -177,11 +169,10 @@ 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{} -> - emqx_logger:error("[STATS]: Duplicated update: ~s", [Name]), + emqx_logger:error("[Stats]: duplicated update: ~s", [Name]), {noreply, State}; false -> {noreply, State#state{updates = [Update | Updates]}} @@ -191,17 +182,16 @@ handle_cast({cancel_update, Name}, State = #state{updates = Updates}) -> {noreply, State#state{updates = lists:keydelete(Name, #update.name, Updates)}}; handle_cast(Msg, State) -> - emqx_logger:error("[STATS] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Stats] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({timeout, TRef, tick}, State = #state{timer = TRef, - updates = Updates}) -> +handle_info({timeout, TRef, tick}, State = #state{timer= TRef, updates = Updates}) -> lists:foldl( fun(Update = #update{name = Name, countdown = C, interval = I, func = UpFun}, Acc) when C =< 0 -> try UpFun() catch _:Error -> - emqx_logger:error("[STATS] Update ~s error: ~p", [Name, Error]) + emqx_logger:error("[Stats] update ~s error: ~p", [Name, Error]) end, [Update#update{countdown = I} | Acc]; (Update = #update{countdown = C}, Acc) -> @@ -210,7 +200,7 @@ handle_info({timeout, TRef, tick}, State = #state{timer = TRef, {noreply, start_timer(State), hibernate}; handle_info(Info, State) -> - emqx_logger:error("[STATS] Unexpected info: ~p", [Info]), + emqx_logger:error("[Stats] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{timer = TRef}) -> @@ -219,9 +209,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}) diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl index 65f361b95..8644f8801 100644 --- a/src/emqx_sys.erl +++ b/src/emqx_sys.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. -module(emqx_sys). @@ -21,13 +19,10 @@ -include("emqx.hrl"). -export([start_link/0]). - -export([version/0, uptime/0, datetime/0, sysdescr/0, sys_interval/0]). - -export([info/0]). - --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -import(emqx_topic, [systop/1]). -import(emqx_misc, [start_timer/2]). @@ -48,10 +43,6 @@ start_link() -> gen_server:start_link({local, ?SYS}, ?MODULE, [], []). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - %% @doc Get sys version -spec(version() -> string()). version() -> @@ -64,7 +55,8 @@ sysdescr() -> %% @doc Get sys uptime -spec(uptime() -> string()). -uptime() -> gen_server:call(?SYS, uptime). +uptime() -> + gen_server:call(?SYS, uptime). %% @doc Get sys datetime -spec(datetime() -> string()). @@ -87,9 +79,9 @@ info() -> {uptime, uptime()}, {datetime, datetime()}]. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([]) -> State = #state{start_time = erlang:timestamp(), @@ -106,11 +98,11 @@ handle_call(uptime, _From, State) -> {reply, uptime(State), State}; handle_call(Req, _From, State) -> - emqx_logger:error("[SYS] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[SYS] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast(Msg, State) -> - emqx_logger:error("[SYS] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[SYS] unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({timeout, TRef, heartbeat}, State = #state{heartbeat = TRef}) -> @@ -118,9 +110,7 @@ handle_info({timeout, TRef, heartbeat}, State = #state{heartbeat = TRef}) -> publish(datetime, iolist_to_binary(datetime())), {noreply, heartbeat(State)}; -handle_info({timeout, TRef, tick}, State = #state{ticker = TRef, - version = Version, - sysdescr = Descr}) -> +handle_info({timeout, TRef, tick}, State = #state{ticker = TRef, version = Version, sysdescr = Descr}) -> publish(version, Version), publish(sysdescr, Descr), publish(brokers, ekka_mnesia:running_nodes()), @@ -129,19 +119,18 @@ handle_info({timeout, TRef, tick}, State = #state{ticker = TRef, {noreply, tick(State), hibernate}; handle_info(Info, State) -> - emqx_logger:error("[SYS] Unexpected info: ~p", [Info]), + emqx_logger:error("[SYS] unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{heartbeat = HBRef, ticker = TRef}) -> - emqx_misc:cancel_timer(HBRef), - emqx_misc:cancel_timer(TRef). +terminate(_Reason, #state{heartbeat = TRef1, ticker = TRef2}) -> + lists:foreach(fun emqx_misc:cancel_timer/1, [TRef1, TRef2]). code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% Internal functions -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- uptime(#state{start_time = Ts}) -> Secs = timer:now_diff(erlang:timestamp(), Ts) div 1000000, @@ -162,37 +151,26 @@ uptime(days, D) -> [integer_to_list(D), " days,"]. publish(uptime, Uptime) -> - safe_publish(systop(uptime), [sys], Uptime); + safe_publish(systop(uptime), Uptime); publish(datetime, Datetime) -> - safe_publish(systop(datatype), [sys], Datetime); + safe_publish(systop(datatype), Datetime); publish(version, Version) -> - safe_publish(systop(version), [sys, retain], Version); + safe_publish(systop(version), #{retain => true}, Version); publish(sysdescr, Descr) -> - safe_publish(systop(sysdescr), [sys, retain], Descr); + safe_publish(systop(sysdescr), #{retain => true}, Descr); publish(brokers, Nodes) -> Payload = string:join([atom_to_list(N) || N <- Nodes], ","), - safe_publish(<<"$SYS/brokers">>, [sys, retain], Payload); + safe_publish(<<"$SYS/brokers">>, #{retain => true}, Payload); publish(stats, Stats) -> - [begin - Topic = systop(lists:concat(['stats/', Stat])), - safe_publish(Topic, [sys], integer_to_binary(Val)) - end || {Stat, Val} <- Stats, is_atom(Stat), is_integer(Val)]; + [safe_publish(systop(lists:concat(['stats/', Stat])), integer_to_binary(Val)) + || {Stat, Val} <- Stats, is_atom(Stat), is_integer(Val)]; publish(metrics, Metrics) -> - [begin - Topic = systop(lists:concat(['metrics/', Metric])), - safe_publish(Topic, [sys], integer_to_binary(Val)) - end || {Metric, Val} <- Metrics, is_atom(Metric), is_integer(Val)]. + [safe_publish(systop(lists:concat(['metrics/', Metric])), integer_to_binary(Val)) + || {Metric, Val} <- Metrics, is_atom(Metric), is_integer(Val)]. +safe_publish(Topic, Payload) -> + safe_publish(Topic, #{}, Payload). safe_publish(Topic, Flags, Payload) -> - try do_publish(Topic, Flags, Payload) - catch - _:Error -> - emqx_logger:error("[SYS] Publish error: ~p", [Error]) - end. - -do_publish(Topic, Flags, Payload) -> - Msg0 = emqx_message:make(?SYS, Topic, iolist_to_binary(Payload)), - emqx_broker:publish(lists:foldl(fun(Flag, Msg) -> - emqx_message:set_flag(Flag, Msg) - end, Msg0, Flags)). + Flags1 = maps:merge(#{sys => true, qos => 0}, Flags), + emqx_broker:safe_publish(emqx_message:new(?SYS, Flags1, Topic, iolist_to_binary(Payload))). diff --git a/src/emqx_sys_mon.erl b/src/emqx_sys_mon.erl index 6e58f84bb..af6d39138 100644 --- a/src/emqx_sys_mon.erl +++ b/src/emqx_sys_mon.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. -module(emqx_sys_mon). @@ -20,30 +18,25 @@ -export([start_link/1]). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(state, {timer, events}). --define(LOG(Msg, ProcInfo), - emqx_logger:warning([{sysmon, true}], - "[SYSMON] ~s~n~p", [WarnMsg, ProcInfo])). - --define(LOG(Msg, ProcInfo, PortInfo), - emqx_logger:warning([{sysmon, true}], - "[SYSMON] ~s~n~p~n~p", [WarnMsg, ProcInfo, PortInfo])). - -define(SYSMON, ?MODULE). +-define(LOG(Msg, ProcInfo), + emqx_logger:warning([{sysmon, true}], "[SYSMON] ~s~n~p", [WarnMsg, ProcInfo])). +-define(LOG(Msg, ProcInfo, PortInfo), + emqx_logger:warning([{sysmon, true}], "[SYSMON] ~s~n~p~n~p", [WarnMsg, ProcInfo, PortInfo])). %% @doc Start system monitor --spec(start_link(Opts :: list(tuple())) - -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link(Opts :: list(tuple())) -> {ok, pid()} | ignore | {error, term()}). start_link(Opts) -> gen_server:start_link({local, ?SYSMON}, ?MODULE, [Opts], []). -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% gen_server callbacks -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- init([Opts]) -> erlang:system_monitor(self(), parse_opt(Opts)), @@ -78,60 +71,66 @@ parse_opt([_Opt|Opts], Acc) -> parse_opt(Opts, Acc). handle_call(Req, _From, State) -> - emqx_logger:error("[SYSMON] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[SYSMON] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast(Msg, State) -> - emqx_logger:error("[SYSMON] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[SYSMON] unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({monitor, Pid, long_gc, Info}, State) -> - suppress({long_gc, Pid}, fun() -> - WarnMsg = io_lib:format("long_gc warning: pid = ~p, info: ~p", [Pid, Info]), - ?LOG(WarnMsg, procinfo(Pid)), - safe_publish(long_gc, WarnMsg) - end, State); + suppress({long_gc, Pid}, + fun() -> + WarnMsg = io_lib:format("long_gc warning: pid = ~p, info: ~p", [Pid, Info]), + ?LOG(WarnMsg, procinfo(Pid)), + safe_publish(long_gc, WarnMsg) + end, State); handle_info({monitor, Pid, long_schedule, Info}, State) when is_pid(Pid) -> - suppress({long_schedule, Pid}, fun() -> - WarnMsg = io_lib:format("long_schedule warning: pid = ~p, info: ~p", [Pid, Info]), - ?LOG(WarnMsg, procinfo(Pid)), - safe_publish(long_schedule, WarnMsg) - end, State); + suppress({long_schedule, Pid}, + fun() -> + WarnMsg = io_lib:format("long_schedule warning: pid = ~p, info: ~p", [Pid, Info]), + ?LOG(WarnMsg, procinfo(Pid)), + safe_publish(long_schedule, WarnMsg) + end, State); handle_info({monitor, Port, long_schedule, Info}, State) when is_port(Port) -> - suppress({long_schedule, Port}, fun() -> - WarnMsg = io_lib:format("long_schedule warning: port = ~p, info: ~p", [Port, Info]), - ?LOG(WarnMsg, erlang:port_info(Port)), - safe_publish(long_schedule, WarnMsg) - end, State); + suppress({long_schedule, Port}, + fun() -> + WarnMsg = io_lib:format("long_schedule warning: port = ~p, info: ~p", [Port, Info]), + ?LOG(WarnMsg, erlang:port_info(Port)), + safe_publish(long_schedule, WarnMsg) + end, State); handle_info({monitor, Pid, large_heap, Info}, State) -> - suppress({large_heap, Pid}, fun() -> - WarnMsg = io_lib:format("large_heap warning: pid = ~p, info: ~p", [Pid, Info]), - ?LOG(WarnMsg, procinfo(Pid)), - safe_publish(large_heap, WarnMsg) - end, State); + suppress({large_heap, Pid}, + fun() -> + WarnMsg = io_lib:format("large_heap warning: pid = ~p, info: ~p", [Pid, Info]), + ?LOG(WarnMsg, procinfo(Pid)), + safe_publish(large_heap, WarnMsg) + end, State); handle_info({monitor, SusPid, busy_port, Port}, State) -> - suppress({busy_port, Port}, fun() -> - WarnMsg = io_lib:format("busy_port warning: suspid = ~p, port = ~p", [SusPid, Port]), - ?LOG(WarnMsg, procinfo(SusPid), erlang:port_info(Port)), - safe_publish(busy_port, WarnMsg) - end, State); + suppress({busy_port, Port}, + fun() -> + WarnMsg = io_lib:format("busy_port warning: suspid = ~p, port = ~p", [SusPid, Port]), + ?LOG(WarnMsg, procinfo(SusPid), erlang:port_info(Port)), + safe_publish(busy_port, WarnMsg) + end, State); handle_info({monitor, SusPid, busy_dist_port, Port}, State) -> - suppress({busy_dist_port, Port}, fun() -> - WarnMsg = io_lib:format("busy_dist_port warning: suspid = ~p, port = ~p", [SusPid, Port]), - ?LOG(WarnMsg, procinfo(SusPid), erlang:port_info(Port)), - safe_publish(busy_dist_port, WarnMsg) - end, State); + suppress({busy_dist_port, Port}, + fun() -> + WarnMsg = io_lib:format("busy_dist_port warning: suspid = ~p, port = ~p", [SusPid, Port]), + ?LOG(WarnMsg, procinfo(SusPid), erlang:port_info(Port)), + safe_publish(busy_dist_port, WarnMsg) + end, State); handle_info(reset, State) -> {noreply, State#state{events = []}, hibernate}; handle_info(Info, State) -> - lager:error("[SYSMON] Unexpected Info: ~p", [Info]), + lager:error("[SYSMON] unexpected Info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{timer = TRef}) -> @@ -155,12 +154,9 @@ procinfo(Pid) -> end. safe_publish(Event, WarnMsg) -> - try - Topic = emqx_topic:systop(lists:concat(['sysmon/', Event])), - Msg = emqx_message:make(?SYSMON, Topic, iolist_to_binary(WarnMsg)), - emqx_broker:publish(emqx_message:set_flag(sys, Msg)) - catch - _:Error -> - emqx_logger:error("[SYSMON] Publish error: ~p", [Error]) - end. + Topic = emqx_topic:systop(lists:concat(['sysmon/', Event])), + emqx_broker:safe_publish(sysmon_msg(Topic, iolist_to_binary(WarnMsg))). + +sysmon_msg(Topic, Payload) -> + emqx_message:new(?SYSMON, #{sys => true, qos => 0}, Topic, Payload). diff --git a/src/emqx_sys_sup.erl b/src/emqx_sys_sup.erl index 69d6f4cb5..0e56e5fc5 100644 --- a/src/emqx_sys_sup.erl +++ b/src/emqx_sys_sup.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. -module(emqx_sys_sup). @@ -26,11 +24,18 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - Sys = {sys, {emqx_sys, start_link, []}, - permanent, 5000, worker, [emqx_sys]}, - - {ok, Env} = emqx_config:get_env(sysmon), - Sysmon = {sys_mon, {emqx_sys_mon, start_link, [Env]}, - permanent, 5000, worker, [emqx_sys_mon]}, + Sys = #{id => sys, + start => {emqx_sys, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_sys]}, + Sysmon = #{id => sys_mon, + start => {emqx_sys_mon, start_link, + [emqx_config:get_env(sysmon, [])]}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_sys_mon]}, {ok, {{one_for_one, 10, 100}, [Sys, Sysmon]}}. diff --git a/src/emqx_tables.erl b/src/emqx_tables.erl index c5f249592..330c87d9c 100644 --- a/src/emqx_tables.erl +++ b/src/emqx_tables.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_tables). diff --git a/src/emqx_time.erl b/src/emqx_time.erl index 1d1405900..2e69638dc 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_time). diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index a642b5951..43ab8e0df 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -1,39 +1,31 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_topic). -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). -import(lists, [reverse/1]). -export([match/2, validate/1, triples/1, words/1, wildcard/1]). - -export([join/1, feed_var/3, systop/1]). - -export([parse/1, parse/2]). +-type(word() :: '' | '+' | '#' | binary()). +-type(words() :: list(word())). -type(option() :: {qos, mqtt_qos()} | {share, '$queue' | binary()}). - --type(word() :: '' | '+' | '#' | binary()). - --type(words() :: list(word())). - -type(triple() :: {root | binary(), word(), binary()}). -export_type([option/0, word/0, triple/0]). @@ -110,14 +102,13 @@ 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, []). triples([], _Parent, Acc) -> reverse(Acc); - triples([W|Words], Parent, Acc) -> Node = join(Parent, W), triples(Words, Node, [{Parent, W, Node}|Acc]). @@ -166,9 +157,9 @@ join([W]) -> join(Words) -> {_, Bin} = lists:foldr(fun(W, {true, Tail}) -> - {false, <>}; + {false, <>}; (W, {false, Tail}) -> - {false, <>} + {false, <>} end, {true, <<>>}, [bin(W) || W <- Words]), Bin. @@ -176,24 +167,16 @@ join(Words) -> parse(Topic) when is_binary(Topic) -> parse(Topic, []). -parse(Topic = <<"$fastlane/", Topic1/binary>>, Options) -> - case lists:member(fastlane, Options) of - true -> error({invalid_topic, Topic}); - false -> parse(Topic1, [fastlane | Options]) - end; - parse(Topic = <<"$queue/", Topic1/binary>>, Options) -> case lists:keyfind(share, 1, Options) of {share, _} -> error({invalid_topic, Topic}); false -> parse(Topic1, [{share, '$queue'} | Options]) end; - parse(Topic = <<"$share/", Topic1/binary>>, Options) -> case lists:keyfind(share, 1, Options) of {share, _} -> error({invalid_topic, Topic}); false -> [Group, Topic2] = binary:split(Topic1, <<"/">>), {Topic2, [{share, Group} | Options]} end; - parse(Topic, Options) -> {Topic, Options}. diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index 5e68ed4c5..0f16c858c 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_tracer). @@ -21,32 +19,29 @@ -include("emqx.hrl"). -export([start_link/0]). - -export([start_trace/2, lookup_traces/0, stop_trace/1]). -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(state, {level, traces}). -type(trace_who() :: {client | topic, binary()}). +-define(TRACER, ?MODULE). -define(OPTIONS, [{formatter_config, [time, " [",severity,"] ", message, "\n"]}]). --define(TRACER, ?MODULE). - -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Start the tracer -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -spec(start_link() -> {ok, pid()} | ignore | {error, term()}). start_link() -> gen_server:start_link({local, ?TRACER}, ?MODULE, [], []). -%%-------------------------------------------------------------------- -%% Start/Stop Trace -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ +%% Start/Stop trace +%%------------------------------------------------------------------------------ %% @doc Start to trace client or topic. -spec(start_trace(trace_who(), string()) -> ok | {error, term()}). @@ -55,8 +50,7 @@ start_trace({client, ClientId}, LogFile) -> start_trace({topic, Topic}, LogFile) -> start_trace({start_trace, {topic, Topic}, LogFile}). -start_trace(Req) -> - gen_server:call(?MODULE, Req, infinity). +start_trace(Req) -> gen_server:call(?MODULE, Req, infinity). %% @doc Stop tracing client or topic. -spec(stop_trace(trace_who()) -> ok | {error, term()}). @@ -70,26 +64,24 @@ stop_trace({topic, Topic}) -> lookup_traces() -> gen_server:call(?TRACER, lookup_traces). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([]) -> - Level = emqx_config:get_env(trace_level, debug), - {ok, #state{level = Level, traces = #{}}}. + {ok, #state{level = emqx_config:get_env(trace_level, debug), traces = #{}}}. -handle_call({start_trace, Who, LogFile}, _From, - State = #state{level = Level, traces = Traces}) -> +handle_call({start_trace, Who, LogFile}, _From, State = #state{level = Level, traces = Traces}) -> case catch lager:trace_file(LogFile, [Who], Level, ?OPTIONS) of {ok, exists} -> - {reply, {error, alread_existed}, State}; + {reply, {error, already_existed}, State}; {ok, Trace} -> {reply, ok, State#state{traces = maps:put(Who, {Trace, LogFile}, Traces)}}; {error, Reason} -> - emqx_logger:error("[TRACER] trace error: ~p", [Reason]), + emqx_logger:error("[Tracer] trace error: ~p", [Reason]), {reply, {error, Reason}, State}; {'EXIT', Error} -> - emqx_logger:error("[TRACER] trace exit: ~p", [Error]), + emqx_logger:error("[Tracer] trace exit: ~p", [Error]), {reply, {error, Error}, State} end; @@ -98,27 +90,27 @@ handle_call({stop_trace, Who}, _From, State = #state{traces = Traces}) -> {ok, {Trace, _LogFile}} -> case lager:stop_trace(Trace) of ok -> ok; - {error, Error} -> lager:error("Stop trace ~p error: ~p", [Who, Error]) + {error, Error} -> + emqx_logger:error("[Tracer] stop trace ~p error: ~p", [Who, Error]) end, {reply, ok, State#state{traces = maps:remove(Who, Traces)}}; error -> - {reply, {error, trance_not_found}, State} + {reply, {error, not_found}, State} end; handle_call(lookup_traces, _From, State = #state{traces = Traces}) -> - {reply, [{Who, LogFile} || {Who, {_Trace, LogFile}} - <- maps:to_list(Traces)], State}; + {reply, [{Who, LogFile} || {Who, {_Trace, LogFile}} <- maps:to_list(Traces)], State}; handle_call(Req, _From, State) -> - emqx_logger:error("[TRACER] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[Tracer] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast(Msg, State) -> - emqx_logger:error("[TRACER] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Tracer] unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> - emqx_logger:error("[TRACER] Unexpected info: ~p", [Info]), + emqx_logger:error("[Tracer] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index 595b38116..43e36c060 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_trie). @@ -31,9 +29,9 @@ -define(TRIE, emqx_trie). -define(TRIE_NODE, emqx_trie_node). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Mnesia bootstrap -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% @doc Create or replicate trie tables. -spec(mnesia(boot | copy) -> ok). @@ -55,9 +53,9 @@ mnesia(copy) -> %% Copy trie_node table ok = ekka_mnesia:copy_table(?TRIE_NODE). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Trie APIs -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% @doc Insert a topic into the trie -spec(insert(Topic :: topic()) -> ok). @@ -94,13 +92,12 @@ delete(Topic) when is_binary(Topic) -> delete_path(lists:reverse(emqx_topic:triples(Topic))); [TrieNode] -> write_trie_node(TrieNode#trie_node{topic = undefined}); - [] -> - ok + [] -> ok end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% @private %% @doc Add a path to the trie. @@ -112,8 +109,7 @@ add_path({Node, Word, Child}) -> [] -> write_trie_node(TrieNode#trie_node{edge_count = Count + 1}), write_trie(#trie{edge = Edge, node_id = Child}); - [_] -> - ok + [_] -> ok end; [] -> write_trie_node(#trie_node{node_id = Node, edge_count = 1}), @@ -145,8 +141,7 @@ match_node(NodeId, [W|Words], ResAcc) -> case mnesia:read(?TRIE, #trie_edge{node_id = NodeId, word = '#'}) of [#trie{node_id = ChildId}] -> mnesia:read(?TRIE_NODE, ChildId) ++ ResAcc; - [] -> - ResAcc + [] -> ResAcc end. %% @private From b3adcc8971a0a00bc47b73a3174cb041bc24c1c3 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 18 Jul 2018 14:24:35 +0800 Subject: [PATCH 063/520] Add RPC Args --- etc/emqx.conf | 38 +++++++++++++++++++++++++++ priv/emqx.schema | 67 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index cf264a505..5aa4bf3ea 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -137,6 +137,11 @@ cluster.autoclean = 5m ## Value: String ## cluster.k8s.app_name = emq +## Kubernates Namespace +## +## Value: String +## cluster.k8s.namespace = default + ##-------------------------------------------------------------------- ## Node Args ##-------------------------------------------------------------------- @@ -270,6 +275,39 @@ node.dist_net_ticktime = 60 node.dist_listen_min = 6369 node.dist_listen_max = 6369 +##-------------------------------------------------------------------- +## RPC Args +##-------------------------------------------------------------------- + +## TCP server port. +rpc.tcp_server_port = 5369 + +## Default TCP port for outgoing connections +rpc.tcp_client_port = 5369 + +## Client connect timeout +rpc.connect_timeout = 5000 + +## Client and Server send timeout +rpc.send_timeout = 5000 + +## Authentication timeout +rpc.authentication_timeout = 5000 + +## Default receive timeout for call() functions +rpc.call_receive_timeout = 15000 + +## Socket keepalive configuration +rpc.socket_keepalive_idle = 900 + +## Seconds between probes +rpc.socket_keepalive_interval = 75 + +## Probes lost to close the connection +rpc.socket_keepalive_count = 9 + +## TODO: sndbuf, rcvbuf and buffer + ##-------------------------------------------------------------------- ## Log ##-------------------------------------------------------------------- diff --git a/priv/emqx.schema b/priv/emqx.schema index 8aeeb0ebf..b4187397a 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -124,6 +124,10 @@ {datatype, string} ]}. +{mapping, "cluster.k8s.namespace", "ekka.cluster_discovery", [ + {datatype, string} +]}. + {translation, "ekka.cluster_discovery", fun(Conf) -> Strategy = cuttlefish:conf_get("cluster.discovery", Conf), Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, @@ -152,7 +156,8 @@ [{apiserver, cuttlefish:conf_get("cluster.k8s.apiserver", Conf)}, {service_name, cuttlefish:conf_get("cluster.k8s.service_name", Conf)}, {address_type, cuttlefish:conf_get("cluster.k8s.address_type", Conf, ip)}, - {app_name, cuttlefish:conf_get("cluster.k8s.app_name", Conf)}]; + {app_name, cuttlefish:conf_get("cluster.k8s.app_name", Conf)}, + {namespace, cuttlefish:conf_get("cluster.k8s.namespace", Conf)}]; (manual) -> [ ] end, @@ -170,7 +175,7 @@ end}. %% @doc The erlang distributed protocol {mapping, "node.proto_dist", "vm_args.-proto_dist", [ - {default, "inet_tcp"}, + %{default, "inet_tcp"}, {datatype, {enum, [inet_tcp, inet6_tcp, inet_tls]}}, hidden ]}. @@ -315,6 +320,64 @@ end}. hidden ]}. +%%-------------------------------------------------------------------- +%% RPC Args +%%-------------------------------------------------------------------- + +%% RPC server port. +{mapping, "rpc.tcp_server_port", "gen_rpc.tcp_server_port", [ + {default, 5369}, + {datatype, integer} +]}. + +%% Default TCP port for outgoing connections +{mapping, "rpc.tcp_client_port", "gen_rpc.tcp_client_port", [ + {default, 5369}, + {datatype, integer} +]}. + +%% Client connect timeout +{mapping, "rpc.connect_timeout", "gen_rpc.connect_timeout", [ + {default, 5000}, + {datatype, integer} +]}. + +%% Client and Server send timeout +{mapping, "rpc.send_timeout", "gen_rpc.send_timeout", [ + {default, 5000}, + {datatype, integer} +]}. + +%% Authentication timeout +{mapping, "rpc.authentication_timeout", "gen_rpc.authentication_timeout", [ + {default, 5000}, + {datatype, integer} +]}. + +%% Default receive timeout for call() functions +{mapping, "rpc.call_receive_timeout", "gen_rpc.call_receive_timeout", [ + {default, 15000}, + {datatype, integer} +]}. + +%% Socket keepalive configuration +{mapping, "rpc.socket_keepalive_idle", "gen_rpc.socket_keepalive_idle", [ + {default, 7200}, + {datatype, integer} +]}. + +%% Seconds between probes +{mapping, "rpc.socket_keepalive_interval", "gen_rpc.socket_keepalive_interval", [ + {default, 75}, + {datatype, integer} +]}. + +%% Probes lost to close the connection +{mapping, "rpc.socket_keepalive_count", "gen_rpc.socket_keepalive_count", [ + {default, 9}, + {datatype, integer} +]}. + %%-------------------------------------------------------------------- %% Log %%-------------------------------------------------------------------- From b110a154efb50123c1442ee2131ac9f89f78d9b5 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 18 Jul 2018 14:27:11 +0800 Subject: [PATCH 064/520] Upgrade header files for EMQ X 3.0 --- Makefile | 10 ++--- TODO | 5 ++- docs/mqtt-v5.0.pdf | Bin 2557522 -> 2605289 bytes include/emqx.hrl | 99 +++++++++++++++++++----------------------- include/emqx_misc.hrl | 13 ++++++ include/emqx_mqtt.hrl | 48 +++++++------------- 6 files changed, 81 insertions(+), 94 deletions(-) diff --git a/Makefile b/Makefile index 42eaffc57..349baab95 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,7 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker PROJECT_VERSION = 3.0 -NO_AUTOPATCH = gen_rpc cuttlefish - -DEPS = goldrush gproc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx canal_lock +DEPS = goldrush gproc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx dep_goldrush = git https://github.com/basho/goldrush 0.1.9 dep_gproc = git https://github.com/uwiger/gproc 0.7.0 @@ -19,9 +17,9 @@ dep_ekka = git https://github.com/emqtt/ekka develop dep_mochiweb = git https://github.com/emqtt/mochiweb emqx30 dep_pbkdf2 = git https://github.com/emqtt/pbkdf2 2.0.1 dep_bcrypt = git https://github.com/smarkets/erlang-bcrypt master -dep_clique = git https://github.com/emqtt/clique -dep_clique = git https://github.com/emqtt/clique -dep_canal_lock = git https://github.com/emqx/canal-lock +dep_clique = git https://github.com/emqx/clique + +NO_AUTOPATCH = gen_rpc cuttlefish ERLC_OPTS += +debug_info ERLC_OPTS += +'{parse_transform, lager_transform}' diff --git a/TODO b/TODO index f748eab3f..055d2b8d4 100644 --- a/TODO +++ b/TODO @@ -1,2 +1,5 @@ + 1. Update the README.md -2. Update the documentation +2. Update the Documentation +3. Shared subscription strategy and dispatch strategy + diff --git a/docs/mqtt-v5.0.pdf b/docs/mqtt-v5.0.pdf index 1fa8883d0517edaf5e2df9010b9601a223a4d1e4..6e5cd42056adbbc99e718a7f8700aa15491751fa 100644 GIT binary patch delta 39013 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 C7|8qp delta 89 zcmV~$*A0SD006)|3t|NNAw3L!W=dL diff --git a/include/emqx.hrl b/include/emqx.hrl index 9ee90b186..f0b2b46d1 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -1,30 +1,28 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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. %%-------------------------------------------------------------------- %% Banner %%-------------------------------------------------------------------- --define(COPYRIGHT, "Copyright (c) 2013-2018 EMQ Inc."). +-define(COPYRIGHT, "Copyright (c) 2018 EMQ Technologies Co., Ltd"). -define(LICENSE_MESSAGE, "Licensed under the Apache License, Version 2.0"). -define(PROTOCOL_VERSION, "MQTT/5.0"). --define(ERTS_MINIMUM_REQUIRED, "9.2"). +-define(ERTS_MINIMUM_REQUIRED, "10.0"). %%-------------------------------------------------------------------- %% Topics' prefix: $SYS | $queue | $share @@ -52,11 +50,9 @@ | {share, binary()} | {atom(), term()}). --record(subscription, - { subid :: binary() | atom(), - topic :: topic(), - subopts :: list(suboption()) - }). +-record(subscription, {subid :: binary() | atom(), + topic :: topic(), + subopts :: list(suboption())}). -type(subscription() :: #subscription{}). @@ -76,29 +72,20 @@ -type(mountpoint() :: binary()). --type(connector() :: atom()). +-type(zone() :: undefined | atom()). --type(zone() :: atom()). - --record(client, - { client_id :: client_id(), - client_pid :: pid(), - zone :: zone(), - node :: node(), - username :: username(), - peername :: peername(), - protocol :: protocol(), - connector :: connector(), - mountpoint :: mountpoint(), - attributes :: #{atom() => term()} - }). +-record(client, {id :: client_id(), + pid :: pid(), + zone :: zone(), + peername :: peername(), + username :: username(), + protocol :: protocol(), + attributes :: #{atom() => term()}, + connected_at :: erlang:timestamp()}). -type(client() :: #client{}). --record(session, - { sid :: client_id(), - pid :: pid() - }). +-record(session, {sid :: client_id(), pid :: pid()}). -type(session() :: #session{}). @@ -108,11 +95,12 @@ -type(message_id() :: binary() | undefined). --type(message_flag() :: sys | dup | retain | atom()). +-type(message_flag() :: sys | qos | dup | retain | atom()). --type(message_flags() :: #{message_flag() => boolean()}). +-type(message_flags() :: #{message_flag() => boolean() | integer()}). --type(message_headers() :: #{packet_id => pos_integer(), +-type(message_headers() :: #{protocol => protocol(), + packet_id => pos_integer(), priority => non_neg_integer(), ttl => pos_integer(), atom() => term()}). @@ -121,13 +109,13 @@ %% See 'Application Message' in MQTT Version 5.0 -record(message, - { id :: message_id(), %% Global unique id - qos :: qos(), %% Message QoS + { id :: message_id(), %% Message guid + qos :: qos(), %% Message qos from :: atom() | client(), %% Message from sender :: pid(), %% The pid of the sender/publisher flags :: message_flags(), %% Message flags headers :: message_headers(), %% Message headers - topic :: binary(), %% Message topic + topic :: topic(), %% Message topic properties :: map(), %% Message user properties payload :: payload(), %% Message payload timestamp :: erlang:timestamp() %% Timestamp @@ -136,8 +124,9 @@ -type(message() :: #message{}). -record(delivery, - { message :: message(), - flows :: list() + { node :: node(), %% The node that created the delivery + message :: message(), %% The message delivered + flows :: list() %% The message flow path }). -type(delivery() :: #delivery{}). @@ -219,12 +208,12 @@ %%-------------------------------------------------------------------- -record(command, - { name, - action, - args = [], - opts = [], - usage, - descr + { name :: atom(), + action :: atom(), + args = [] :: list(), + opts = [] :: list(), + usage :: string(), + descr :: string() }). -type(command() :: #command{}). diff --git a/include/emqx_misc.hrl b/include/emqx_misc.hrl index f5ec9fbb2..e904b71e3 100644 --- a/include/emqx_misc.hrl +++ b/include/emqx_misc.hrl @@ -1,3 +1,16 @@ +%% Copyright (c) 2018 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. -define(record_to_map(Def, Rec), maps:from_list(?record_to_proplist(Def, Rec))). diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 4dae631f4..f5bb13604 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. %%-------------------------------------------------------------------- %% MQTT SockOpts @@ -77,7 +75,7 @@ I =:= qos2; I =:= exactly_once)). %%-------------------------------------------------------------------- -%% Maximum ClientId Length. Why 1024? +%% Maximum ClientId Length. %%-------------------------------------------------------------------- -define(MAX_CLIENTID_LEN, 1024). @@ -199,7 +197,7 @@ -define(MAX_PACKET_SIZE, 16#fffffff). %%-------------------------------------------------------------------- -%% MQTT Parser and Serializer +%% MQTT Frame Mask %%-------------------------------------------------------------------- -define(HIGHBIT, 2#10000000). @@ -526,19 +524,5 @@ -type(mqtt_message() :: #mqtt_message{}). -define(WILL_MSG(Qos, Retain, Topic, Props, Payload), - #mqtt_message{qos = WillQos, retain = WillRetain, - topic = WillTopic, properties = Props, - payload = WillPayload}). - -%%-------------------------------------------------------------------- -%% MQTT Delivery -%%-------------------------------------------------------------------- - --record(mqtt_delivery, - { sender :: pid(), - message :: mqtt_message(), - flows :: list() - }). - --type(mqtt_delivery() :: #mqtt_delivery{}). + #mqtt_message{qos = Qos, retain = Retain, topic = Topic, properties = Props, payload = Payload}). From ca4cdfe4ee71c5bf676e441f86acc947068adaf6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 18 Jul 2018 23:29:20 +0800 Subject: [PATCH 065/520] Move the passwd_hash/2 function to emqx-passwd project --- src/emqx_auth_mod.erl | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/src/emqx_auth_mod.erl b/src/emqx_auth_mod.erl index a5c3844a9..65298ef9b 100644 --- a/src/emqx_auth_mod.erl +++ b/src/emqx_auth_mod.erl @@ -16,10 +16,6 @@ -include("emqx.hrl"). --export([passwd_hash/2]). - --type(hash_type() :: plain | md5 | sha | sha256 | pbkdf2 | bcrypt). - %%-------------------------------------------------------------------- %% Authentication behavihour %%-------------------------------------------------------------------- @@ -46,33 +42,3 @@ behaviour_info(_Other) -> -endif. -%% @doc Password Hash --spec(passwd_hash(hash_type(), binary() | tuple()) -> binary()). -passwd_hash(plain, Password) -> - Password; -passwd_hash(md5, Password) -> - hexstring(crypto:hash(md5, Password)); -passwd_hash(sha, Password) -> - hexstring(crypto:hash(sha, Password)); -passwd_hash(sha256, Password) -> - hexstring(crypto:hash(sha256, Password)); -passwd_hash(pbkdf2, {Salt, Password, Macfun, Iterations, Dklen}) -> - case pbkdf2:pbkdf2(Macfun, Password, Salt, Iterations, Dklen) of - {ok, Hexstring} -> pbkdf2:to_hex(Hexstring); - {error, Error} -> - emqx_logger:error("[AuthMod] PasswdHash with pbkdf2 error:~p", [Error]), <<>> - end; -passwd_hash(bcrypt, {Salt, Password}) -> - case bcrypt:hashpw(Password, Salt) of - {ok, HashPassword} -> list_to_binary(HashPassword); - {error, Error}-> - emqx_logger:error("[AuthMod] PasswdHash with bcrypt error:~p", [Error]), <<>> - end. - -hexstring(<>) -> - iolist_to_binary(io_lib:format("~32.16.0b", [X])); -hexstring(<>) -> - iolist_to_binary(io_lib:format("~40.16.0b", [X])); -hexstring(<>) -> - iolist_to_binary(io_lib:format("~64.16.0b", [X])). - From 6c58bbab2bdff16c36d1194ba1e5fc23987e29a1 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 18 Jul 2018 23:29:38 +0800 Subject: [PATCH 066/520] Remove emqx_lager_backend module --- src/emqx_lager_backend.erl | 84 -------------------------------------- 1 file changed, 84 deletions(-) delete mode 100644 src/emqx_lager_backend.erl diff --git a/src/emqx_lager_backend.erl b/src/emqx_lager_backend.erl deleted file mode 100644 index b002ff7af..000000000 --- a/src/emqx_lager_backend.erl +++ /dev/null @@ -1,84 +0,0 @@ -%% Copyright (c) 2018 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_lager_backend). - --behaviour(gen_event). - --include_lib("lager/include/lager.hrl"). - --export([init/1, handle_call/2, handle_event/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {level :: {'mask', integer()}, - formatter :: atom(), - format_config :: any()}). - --define(DEFAULT_FORMAT, [time, " ", pid, " [",severity, "] ", message]). - -init([Level]) when is_atom(Level) -> - init(Level); - -init(Level) when is_atom(Level) -> - init([Level,{lager_default_formatter, ?DEFAULT_FORMAT}]); - -init([Level,{Formatter, FormatterConfig}]) when is_atom(Formatter) -> - Levels = lager_util:config_to_mask(Level), - {ok, #state{level = Levels, formatter = Formatter, - format_config = FormatterConfig}}. - -handle_call(get_loglevel, #state{level = Level} = State) -> - {ok, Level, State}; - -handle_call({set_loglevel, Level}, State) -> - try lager_util:config_to_mask(Level) of - Levels -> {ok, ok, State#state{level = Levels}} - catch - _:_ -> {ok, {error, bad_log_level}, State} - end; - -handle_call(_Request, State) -> - {ok, ok, State}. - -handle_event({log, Message}, State = #state{level = L}) -> - case lager_util:is_loggable(Message, L, ?MODULE) of - true -> - publish_log(Message, State); - false -> - {ok, State} - end; - -handle_event(_Event, State) -> - {ok, State}. - -handle_info(_Info, State) -> - {ok, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -publish_log(Message, State = #state{formatter = Formatter, - format_config = FormatConfig}) -> - Severity = lager_msg:severity(Message), - Payload = Formatter:format(Message, FormatConfig), - Msg = emqx_message:make(log, topic(Severity), iolist_to_binary(Payload)), - emqx:publish(emqx_message:set_flag(sys, Msg)), - {ok, State}. - -topic(Severity) -> - emqx_topic:systop(list_to_binary(lists:concat(['logs/', Severity]))). - From 1735786ffbcde26b28e11f4e2d43154de0c11980 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 18 Jul 2018 23:30:16 +0800 Subject: [PATCH 067/520] Remove pbkdf2 lager_syslog bcrypt deps --- Makefile | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 349baab95..38051db1c 100644 --- a/Makefile +++ b/Makefile @@ -4,20 +4,16 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker PROJECT_VERSION = 3.0 -DEPS = goldrush gproc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx +DEPS = jsx gproc gen_rpc lager ekka esockd mochiweb clique -dep_goldrush = git https://github.com/basho/goldrush 0.1.9 -dep_gproc = git https://github.com/uwiger/gproc 0.7.0 -dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 -dep_getopt = git https://github.com/jcomellas/getopt v0.8.2 -dep_lager = git https://github.com/basho/lager master -dep_lager_syslog = git https://github.com/basho/lager_syslog -dep_esockd = git https://github.com/emqtt/esockd emqx30 -dep_ekka = git https://github.com/emqtt/ekka develop -dep_mochiweb = git https://github.com/emqtt/mochiweb emqx30 -dep_pbkdf2 = git https://github.com/emqtt/pbkdf2 2.0.1 -dep_bcrypt = git https://github.com/smarkets/erlang-bcrypt master -dep_clique = git https://github.com/emqx/clique +dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 +dep_gproc = git https://github.com/uwiger/gproc 0.8.0 +dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.1.1 +dep_lager = git https://github.com/erlang-lager/lager 3.6.4 +dep_esockd = git https://github.com/emqx/esockd emqx30 +dep_ekka = git https://github.com/emqx/ekka emqx30 +dep_mochiweb = git https://github.com/emqtt/mochiweb emqx30 +dep_clique = git https://github.com/emqx/clique NO_AUTOPATCH = gen_rpc cuttlefish @@ -25,7 +21,7 @@ ERLC_OPTS += +debug_info ERLC_OPTS += +'{parse_transform, lager_transform}' BUILD_DEPS = cuttlefish -dep_cuttlefish = git https://github.com/emqtt/cuttlefish +dep_cuttlefish = git https://github.com/emqx/cuttlefish emqx30 TEST_DEPS = emqx_ct_helplers dep_emqx_ct_helplers = git git@github.com:emqx/emqx_ct_helpers @@ -47,8 +43,7 @@ COVER = true PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets public_key ssl lager compiler mnesia DIALYZER_DIRS := ebin/ -DIALYZER_OPTS := --verbose --statistics -Werror_handling \ - -Wrace_conditions #-Wunmatched_returns +DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wunmatched_returns include erlang.mk From 7ee54aac289e8db632d07d405c6cc3dee497bb6b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 18 Jul 2018 23:41:00 +0800 Subject: [PATCH 068/520] Add 'qos' field to message record --- TODO | 4 +- etc/emqx.conf | 10 +-- priv/emqx.schema | 42 ++++------ src/emqx.app.src | 2 +- src/emqx_alarm_mgr.erl | 2 +- src/emqx_connection.erl | 44 ++++------ src/emqx_listeners.erl | 22 ++--- src/emqx_message.erl | 1 + src/emqx_mqueue.erl | 4 +- src/emqx_protocol.erl | 74 +++++++---------- src/emqx_session.erl | 180 ++++++++++++++++++---------------------- src/emqx_sup.erl | 32 ++++--- src/emqx_sys.erl | 2 +- src/emqx_sys_mon.erl | 2 +- 14 files changed, 183 insertions(+), 238 deletions(-) diff --git a/TODO b/TODO index 055d2b8d4..87e6dea16 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,7 @@ 1. Update the README.md 2. Update the Documentation -3. Shared subscription strategy and dispatch strategy +3. Shared subscription and dispatch strategy +4. Remove lager syslog: + dep_lager_syslog = git https://github.com/basho/lager_syslog diff --git a/etc/emqx.conf b/etc/emqx.conf index 5aa4bf3ea..7101c3a7b 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -702,25 +702,25 @@ listener.tcp.external.acceptors = 16 ## Value: Number listener.tcp.external.max_clients = 102400 -## TODO: Zone of the external MQTT/TCP listener belonged to. +## Zone of the external MQTT/TCP listener belonged to. ## ## Value: String -## listener.tcp.external.zone = external +listener.tcp.external.zone = devicebound ## Mountpoint of the MQTT/TCP Listener. All the topics of this ## listener will be prefixed with the mount point if this option ## is enabled. -## Notice that EMQ X supports wildcard mount:%c clientid, %u username +## Notice that supports wildcard mount:%c clientid, %u username ## ## Value: String -## listener.tcp.external.mountpoint = external/ +listener.tcp.external.mountpoint = devicebound/ ## Rate limit for the external MQTT/TCP connections. ## Format is 'burst,rate'. ## ## Value: burst,rate ## Unit: KB/sec -## listener.tcp.external.rate_limit = 100,10 +listener.tcp.external.rate_limit = 100,10 ## The access control rules for the MQTT/TCP listener. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index b4187397a..fe43f8f53 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1409,21 +1409,18 @@ end}. MountPoint = fun(undefined) -> undefined; (S) -> list_to_binary(S) end, - ConnOpts = fun(Prefix) -> - Filter([{zone, Atom(cuttlefish:conf_get(Prefix ++ ".zone", Conf, undefined))}, - {rate_limit, cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined)}, - {proxy_protocol, cuttlefish:conf_get(Prefix ++ ".proxy_protocol", Conf, undefined)}, - {proxy_protocol_timeout, cuttlefish:conf_get(Prefix ++ ".proxy_protocol_timeout", Conf, undefined)}, - {mountpoint, MountPoint(cuttlefish:conf_get(Prefix ++ ".mountpoint", Conf, undefined))}, - {peer_cert_as_username, cuttlefish:conf_get(Prefix ++ ".peer_cert_as_username", Conf, undefined)}, - {proxy_port_header, cuttlefish:conf_get(Prefix ++ ".proxy_port_header", Conf, undefined)}, - {proxy_address_header, cuttlefish:conf_get(Prefix ++ ".proxy_address_header", Conf, undefined)}]) - end, - LisOpts = fun(Prefix) -> Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, {max_clients, cuttlefish:conf_get(Prefix ++ ".max_clients", Conf)}, - {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)} | AccOpts(Prefix)]) + {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, + {zone, Atom(cuttlefish:conf_get(Prefix ++ ".zone", Conf, undefined))}, + {rate_limit, cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined)}, + {proxy_protocol, cuttlefish:conf_get(Prefix ++ ".proxy_protocol", Conf, undefined)}, + {proxy_protocol_timeout, cuttlefish:conf_get(Prefix ++ ".proxy_protocol_timeout", Conf, undefined)}, + {mountpoint, MountPoint(cuttlefish:conf_get(Prefix ++ ".mountpoint", Conf, undefined))}, + {peer_cert_as_username, cuttlefish:conf_get(Prefix ++ ".peer_cert_as_username", Conf, undefined)}, + {proxy_port_header, cuttlefish:conf_get(Prefix ++ ".proxy_port_header", Conf, undefined)}, + {proxy_address_header, cuttlefish:conf_get(Prefix ++ ".proxy_address_header", Conf, undefined)} | AccOpts(Prefix)]) end, TcpOpts = fun(Prefix) -> Filter([{backlog, cuttlefish:conf_get(Prefix ++ ".backlog", Conf, undefined)}, @@ -1460,11 +1457,9 @@ end}. TcpListeners = fun(Type, Name) -> Prefix = string:join(["listener", Type, Name], "."), case cuttlefish:conf_get(Prefix, Conf, undefined) of - undefined -> - []; - ListenOn -> - [{Atom(Type), ListenOn, [{connopts, ConnOpts(Prefix)}, - {sockopts, TcpOpts(Prefix)} | LisOpts(Prefix)]}] + undefined -> []; + ListenOn -> + [{Atom(Type), ListenOn, [{tcp_options, TcpOpts(Prefix)} | LisOpts(Prefix)]}] end end, @@ -1474,9 +1469,8 @@ end}. undefined -> []; ListenOn -> - [{Atom(Type), ListenOn, [{connopts, ConnOpts(Prefix)}, - {sockopts, TcpOpts(Prefix)}, - {sslopts, SslOpts(Prefix)} | LisOpts(Prefix)]}] + [{Atom(Type), ListenOn, [{tcp_options, TcpOpts(Prefix)}, + {ssl_options, SslOpts(Prefix)} | LisOpts(Prefix)]}] end end, @@ -1486,12 +1480,8 @@ end}. undefined -> []; ListenOn -> - SslOpts1 = case SslOpts(Prefix) of - [] -> []; - SslOpts0 -> [{sslopts, SslOpts0}] - end, - [{Atom(Type), ListenOn, [{connopts, ConnOpts(Prefix)}, - {sockopts, TcpOpts(Prefix)}| LisOpts(Prefix)] ++ SslOpts1}] + SslOpts1 = case SslOpts(Prefix) of [] -> []; SslOpts0 -> [{ssl_options, SslOpts0}] end, + [{Atom(Type), ListenOn, [{tcp_options, TcpOpts(Prefix)}|LisOpts(Prefix)] ++ SslOpts1}] end end, diff --git a/src/emqx.app.src b/src/emqx.app.src index f3ccb8fa3..36807e47b 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -3,7 +3,7 @@ {vsn,"3.0"}, {modules,[]}, {registered,[emqx_sup]}, - {applications,[kernel,stdlib,gproc,lager,esockd,mochiweb,lager_syslog,pbkdf2,bcrypt,clique,jsx]}, + {applications,[kernel, stdlib,jsx,gproc,gen_rpc,lager,ekka,esockd,mochiweb]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, diff --git a/src/emqx_alarm_mgr.erl b/src/emqx_alarm_mgr.erl index f6901c325..e041341e2 100644 --- a/src/emqx_alarm_mgr.erl +++ b/src/emqx_alarm_mgr.erl @@ -131,7 +131,7 @@ encode_alarm(#alarm{id = AlarmId, severity = Severity, title = Title, {ts, emqx_time:now_secs(Ts)}]). alarm_msg(Type, AlarmId, Json) -> - emqx_message:make(?ALARM_MGR, #{sys => true, qos => 0}, topic(Type, AlarmId), Json). + emqx_message:make(?ALARM_MGR, #{sys => true}, topic(Type, AlarmId), Json). topic(alert, AlarmId) -> emqx_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>); diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 544646701..cd71b1af4 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -17,24 +17,17 @@ -behaviour(gen_server). -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). - -include("emqx_misc.hrl"). -import(proplists, [get_value/2, get_value/3]). -%% API Function Exports -export([start_link/3]). - %% Management and Monitor API -export([info/1, stats/1, kick/1, clean_acl_cache/2]). - -export([set_rate_limit/2, get_rate_limit/1]). - %% SUB/UNSUB Asynchronously. Called by plugins. -export([subscribe/2, unsubscribe/2]). - %% Get the session proc? -export([session/1]). @@ -48,15 +41,12 @@ keepalive, enable_stats, idle_timeout, force_gc_count}). -define(INFO_KEYS, [peername, conn_state, await_recv]). - -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). - -define(LOG(Level, Format, Args, State), - emqx_logger:Level("Client(~s): " ++ Format, - [esockd_net:format(State#state.peername) | Args])). + emqx_logger:Level("Conn(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). -start_link(Transport, Sock, Env) -> - {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock, Env]])}. +start_link(Transport, Sock, Options) -> + {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock, Options]])}. info(CPid) -> gen_server:call(CPid, info). @@ -85,26 +75,27 @@ session(CPid) -> clean_acl_cache(CPid, Topic) -> gen_server:call(CPid, {clean_acl_cache, Topic}). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -init([Transport, Sock, Env]) -> +init([Transport, Sock, Options]) -> case Transport:wait(Sock) of {ok, NewSock} -> {ok, Peername} = Transport:ensure_ok_or_exit(peername, [NewSock]), - do_init(Transport, Sock, Peername, Env); + do_init(Transport, Sock, Peername, Options); {error, Reason} -> {stop, Reason} end. -do_init(Transport, Sock, Peername, Env) -> - RateLimit = get_value(rate_limit, Env), - PacketSize = get_value(max_packet_size, Env, ?MAX_PACKET_SIZE), +do_init(Transport, Sock, Peername, Options) -> + io:format("Options: ~p~n", [Options]), + RateLimit = get_value(rate_limit, Options), + PacketSize = get_value(max_packet_size, Options, ?MAX_PACKET_SIZE), SendFun = send_fun(Transport, Sock, Peername), - ProtoState = emqx_protocol:init(Transport, Sock, Peername, SendFun, Env), - EnableStats = get_value(client_enable_stats, Env, false), - IdleTimout = get_value(client_idle_timeout, Env, 30000), + ProtoState = emqx_protocol:init(Transport, Sock, Peername, SendFun, Options), + EnableStats = get_value(client_enable_stats, Options, false), + IdleTimout = get_value(client_idle_timeout, Options, 30000), ForceGcCount = emqx_gc:conn_max_gc_count(), State = run_socket(#state{transport = Transport, socket = Sock, @@ -136,8 +127,7 @@ send_fun(Transport, Sock, Peername) -> init_parse_state(State = #state{max_packet_size = Size, proto_state = ProtoState}) -> Version = emqx_protocol:get(proto_ver, ProtoState), - State#state{parse_state = emqx_frame:initial_state( - #{max_packet_size => Size, version => Version})}. + State#state{parse_state = emqx_frame:initial_state(#{max_packet_size => Size, version => Version})}. handle_call(info, From, State = #state{proto_state = ProtoState}) -> ProtoInfo = emqx_protocol:info(ProtoState), @@ -194,10 +184,6 @@ handle_info({suback, PacketId, GrantedQos}, State) -> emqx_protocol:send(Packet, ProtoState) end, State); -%% Fastlane -handle_info({dispatch, _Topic, Msg}, State) -> - handle_info({deliver, emqx_message:set_flag(qos, ?QOS_0, Msg)}, State); - handle_info({deliver, Message}, State) -> with_proto( fun(ProtoState) -> diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 02de39123..c9697f0ca 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -42,10 +42,14 @@ start_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss -> start_http_listener('mqtt:wss', ListenOn, Options). start_mqtt_listener(Name, ListenOn, Options) -> - {ok, _} = esockd:open(Name, ListenOn, merge_sockopts(Options), {emqx_connection, start_link, []}). + SockOpts = esockd:parse_opt(Options), + MFA = {emqx_connection, start_link, [Options -- SockOpts]}, + {ok, _} = esockd:open(Name, ListenOn, merge_default(SockOpts), MFA). start_http_listener(Name, ListenOn, Options) -> - {ok, _} = mochiweb:start_http(Name, ListenOn, Options, {emqx_ws, handle_request, []}). + SockOpts = esockd:parse_opt(Options), + MFA = {emqx_ws, handle_request, [Options -- SockOpts]}, + {ok, _} = mochiweb:start_http(Name, ListenOn, SockOpts, MFA). %% @doc Restart all listeners -spec(restart_all() -> ok). @@ -56,7 +60,7 @@ restart_all() -> restart_listener({tcp, ListenOn, _Opts}) -> esockd:reopen('mqtt:tcp', ListenOn); restart_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> - esockd:reopen('mqtt:tls', ListenOn); + esockd:reopen('mqtt:ssl', ListenOn); restart_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> mochiweb:restart_http('mqtt:ws', ListenOn); restart_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> @@ -73,7 +77,7 @@ stop_all() -> stop_listener({tcp, ListenOn, _Opts}) -> esockd:close('mqtt:tcp', ListenOn); stop_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> - esockd:close('mqtt:tls', ListenOn); + esockd:close('mqtt:ssl', ListenOn); stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> mochiweb:stop_http('mqtt:ws', ListenOn); stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> @@ -81,7 +85,7 @@ stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> stop_listener({Proto, ListenOn, _Opts}) -> esockd:close(Proto, ListenOn). -merge_sockopts(Options) -> +merge_default(Options) -> case lists:keytake(tcp_options, 1, Options) of {value, {tcp_options, TcpOpts}, Options1} -> [{tcp_options, emqx_misc:merge_opts(?MQTT_SOCKOPTS, TcpOpts)} | Options1]; @@ -89,11 +93,3 @@ merge_sockopts(Options) -> [{tcp_options, ?MQTT_SOCKOPTS} | Options] end. -%% all() -> -%% [Listener || Listener = {{Proto, _}, _Pid} <- esockd:listeners(), is_mqtt(Proto)]. -%%is_mqtt('mqtt:tcp') -> true; -%%is_mqtt('mqtt:tls') -> true; -%%is_mqtt('mqtt:ws') -> true; -%%is_mqtt('mqtt:wss') -> true; -%%is_mqtt(_Proto) -> false. - diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 3a96f75a6..77dbfbf82 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -37,6 +37,7 @@ new(From, Flags, Topic, Payload) when is_atom(From); is_record(From, client) -> -spec(new(atom() | client(), message_flags(), message_headers(), topic(), payload()) -> message()). new(From, Flags, Headers, Topic, Payload) when is_atom(From); is_record(From, client) -> #message{id = msgid(), + qos = ?QOS0, from = From, sender = self(), flags = Flags, diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index e6bd16540..db2f30cb6 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -21,7 +21,7 @@ %% This module implements a simple in-memory queue for MQTT persistent session. %% %% If the broker restarted or crashed, all the messages queued will be gone. -%% +%% %% Concept of Message Queue and Inflight Window: %% %% |<----------------- Max Len ----------------->| @@ -154,7 +154,7 @@ stats(#mqueue{type = Type, q = Q, max_len = MaxLen, len = Len, dropped = Dropped %% @doc Enqueue a message. -spec(in(message(), mqueue()) -> mqueue()). -in(#message{flags = #{qos := ?QOS_0}}, MQ = #mqueue{qos0 = false}) -> +in(#message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> MQ; in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}; diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index c932089e7..b2e9965e8 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -1,44 +1,36 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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). -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). - -include("emqx_misc.hrl"). -import(proplists, [get_value/2, get_value/3]). %% API -export([init/3, init/5, get/2, info/1, stats/1, clientid/1, client/1, session/1]). - -export([subscribe/2, unsubscribe/2, pubrel/2, shutdown/2]). - -export([received/2, send/2]). - -export([process/2]). -ifdef(TEST). -compile(export_all). -endif. --record(proto_stats, {enable_stats = false, recv_pkt = 0, recv_msg = 0, - send_pkt = 0, send_msg = 0}). +-record(proto_stats, {enable_stats = false, recv_pkt = 0, recv_msg = 0, send_pkt = 0, send_msg = 0}). %% Protocol State %% ws_initial_headers: Headers from first HTTP request for WebSocket Client. @@ -76,23 +68,22 @@ init(Peername, SendFun, Opts) -> keepalive_backoff = Backoff, stats_data = #proto_stats{enable_stats = EnableStats}}. -init(_Transport, _Sock, Peername, SendFun, Opts) -> - init(Peername, SendFun, Opts). - %%enrich_opt(Conn:opts(), Conn, ). +init(_Transport, _Sock, Peername, SendFun, Options) -> + enrich_opt(Options, init(Peername, SendFun, Options)). -enrich_opt([], _Conn, State) -> +enrich_opt([], State) -> State; -enrich_opt([{mountpoint, MountPoint} | ConnOpts], Conn, State) -> - enrich_opt(ConnOpts, Conn, State#proto_state{mountpoint = MountPoint}); -enrich_opt([{peer_cert_as_username, N} | ConnOpts], Conn, State) -> - enrich_opt(ConnOpts, Conn, State#proto_state{peercert_username = peercert_username(N, Conn)}); -enrich_opt([_ | ConnOpts], Conn, State) -> - enrich_opt(ConnOpts, Conn, State). +enrich_opt([{mountpoint, MountPoint} | ConnOpts], State) -> + enrich_opt(ConnOpts, State#proto_state{mountpoint = MountPoint}); +%%enrich_opt([{peer_cert_as_username, N} | ConnOpts], State) -> +%% enrich_opt(ConnOpts, State#proto_state{peercert_username = peercert_username(N, Conn)}); +enrich_opt([_ | ConnOpts], State) -> + enrich_opt(ConnOpts, State). -peercert_username(cn, Conn) -> - Conn:peer_cert_common_name(); -peercert_username(dn, Conn) -> - Conn:peer_cert_subject(). +%%peercert_username(cn, Conn) -> +%% Conn:peer_cert_common_name(); +%%peercert_username(dn, Conn) -> +%% Conn:peer_cert_subject(). repl_username_with_peercert(State = #proto_state{peercert_username = undefined}) -> State; @@ -122,17 +113,14 @@ client(#proto_state{client_id = ClientId, proto_ver = ProtoVer, keepalive = Keepalive, will_msg = WillMsg, - ws_initial_headers = WsInitialHeaders, - mountpoint = MountPoint, - connected_at = Time}) -> + ws_initial_headers = _WsInitialHeaders, + mountpoint = _MountPoint, + connected_at = _Time}) -> WillTopic = if WillMsg =:= undefined -> undefined; true -> WillMsg#message.topic end, - #client{id = ClientId, - pid = ClientPid, - username = Username, - peername = Peername}. + #client{id = ClientId, pid = ClientPid, username = Username, peername = Peername}. session(#proto_state{session = Session}) -> Session. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 83d53abc6..607a3644f 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -1,48 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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). - --behaviour(gen_server). - --include("emqx.hrl"). - --include("emqx_mqtt.hrl"). - --include("emqx_misc.hrl"). - --import(emqx_misc, [start_timer/2]). - --import(proplists, [get_value/2, get_value/3]). - -%% Session API --export([start_link/1, resume/2, discard/2]). - -%% Management and Monitor API --export([state/1, info/1, stats/1]). - -%% PubSub API --export([subscribe/2, subscribe/3, publish/2, puback/2, pubrec/2, - pubrel/2, pubcomp/2, unsubscribe/2]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --define(MQueue, emqx_mqueue). +%% Copyright (c) 2018 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. %% A stateful interaction between a Client and a Server. Some Sessions %% last only as long as the Network Connection, others can span multiple @@ -66,6 +34,32 @@ %% %% If the session is currently disconnected, the time at which the Session state %% will be deleted. +-module(emqx_session). + +-behaviour(gen_server). + +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). +-include("emqx_misc.hrl"). + +-import(emqx_misc, [start_timer/2]). +-import(proplists, [get_value/2, get_value/3]). + +%% Session API +-export([start_link/1, resume/2, discard/2]). +%% Management and Monitor API +-export([state/1, info/1, stats/1]). +%% PubSub API +-export([subscribe/2, subscribe/3]). +-export([publish/2, puback/2, pubrec/2, pubrel/2, pubcomp/2]). +-export([unsubscribe/2]). + +%% gen_server Function Exports +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). + +-define(MQueue, emqx_mqueue). + -record(state, { %% Clean Start Flag clean_start = false :: boolean(), @@ -145,9 +139,7 @@ }). -define(TIMEOUT, 60000). - -define(INFO_KEYS, [clean_start, client_id, username, client_pid, binding, created_at]). - -define(STATE_KEYS, [clean_start, client_id, username, binding, client_pid, old_client_pid, next_msg_id, max_subscriptions, subscriptions, upgrade_qos, inflight, max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, @@ -169,71 +161,71 @@ start_link(Attrs) -> %% @doc Subscribe topics -spec(subscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok). -subscribe(SessionPid, TopicTable) -> %%TODO: the ack function??... - gen_server:cast(SessionPid, {subscribe, self(), TopicTable, fun(_) -> ok end}). +subscribe(SPid, TopicTable) -> %%TODO: the ack function??... + gen_server:cast(SPid, {subscribe, self(), TopicTable, fun(_) -> ok end}). -spec(subscribe(pid(), mqtt_packet_id(), [{binary(), [emqx_topic:option()]}]) -> ok). -subscribe(SessionPid, PacketId, TopicTable) -> %%TODO: the ack function??... +subscribe(SPid, PacketId, TopicTable) -> %%TODO: the ack function??... From = self(), AckFun = fun(GrantedQos) -> From ! {suback, PacketId, GrantedQos} end, - gen_server:cast(SessionPid, {subscribe, From, TopicTable, AckFun}). + gen_server:cast(SPid, {subscribe, From, TopicTable, AckFun}). %% @doc Publish Message --spec(publish(pid(), message()) -> ok | {error, term()}). -publish(_SessionPid, Msg = #message{qos = ?QOS_0}) -> +-spec(publish(pid(), message()) -> {ok, delivery()} | {error, term()}). +publish(_SPid, Msg = #message{qos = ?QOS_0}) -> %% Publish QoS0 Directly - emqx_broker:publish(Msg), ok; + emqx_broker:publish(Msg); -publish(_SessionPid, Msg = #message{qos = ?QOS_1}) -> +publish(_SPid, Msg = #message{qos = ?QOS_1}) -> %% Publish QoS1 message directly for client will PubAck automatically - emqx_broker:publish(Msg), ok; + emqx_broker:publish(Msg); -publish(SessionPid, Msg = #message{qos = ?QOS_2}) -> +publish(SPid, Msg = #message{qos = ?QOS_2}) -> %% Publish QoS2 to Session - gen_server:call(SessionPid, {publish, Msg}, ?TIMEOUT). + gen_server:call(SPid, {publish, Msg}, infinity). %% @doc PubAck Message -spec(puback(pid(), mqtt_packet_id()) -> ok). -puback(SessionPid, PacketId) -> - gen_server:cast(SessionPid, {puback, PacketId}). +puback(SPid, PacketId) -> + gen_server:cast(SPid, {puback, PacketId}). -spec(pubrec(pid(), mqtt_packet_id()) -> ok). -pubrec(SessionPid, PacketId) -> - gen_server:cast(SessionPid, {pubrec, PacketId}). +pubrec(SPid, PacketId) -> + gen_server:cast(SPid, {pubrec, PacketId}). -spec(pubrel(pid(), mqtt_packet_id()) -> ok). -pubrel(SessionPid, PacketId) -> - gen_server:cast(SessionPid, {pubrel, PacketId}). +pubrel(SPid, PacketId) -> + gen_server:cast(SPid, {pubrel, PacketId}). -spec(pubcomp(pid(), mqtt_packet_id()) -> ok). -pubcomp(SessionPid, PacketId) -> - gen_server:cast(SessionPid, {pubcomp, PacketId}). +pubcomp(SPid, PacketId) -> + gen_server:cast(SPid, {pubcomp, PacketId}). %% @doc Unsubscribe the topics -spec(unsubscribe(pid(), [{binary(), [suboption()]}]) -> ok). -unsubscribe(SessionPid, TopicTable) -> - gen_server:cast(SessionPid, {unsubscribe, self(), TopicTable}). +unsubscribe(SPid, TopicTable) -> + gen_server:cast(SPid, {unsubscribe, self(), TopicTable}). %% @doc Resume the session -spec(resume(pid(), pid()) -> ok). -resume(SessionPid, ClientPid) -> - gen_server:cast(SessionPid, {resume, ClientPid}). +resume(SPid, ClientPid) -> + gen_server:cast(SPid, {resume, ClientPid}). %% @doc Get session state -state(SessionPid) when is_pid(SessionPid) -> - gen_server:call(SessionPid, state). +state(SPid) when is_pid(SPid) -> + gen_server:call(SPid, state). %% @doc Get session info -spec(info(pid() | #state{}) -> list(tuple())). -info(SessionPid) when is_pid(SessionPid) -> - gen_server:call(SessionPid, info); +info(SPid) when is_pid(SPid) -> + gen_server:call(SPid, info); info(State) when is_record(State, state) -> ?record_to_proplist(state, State, ?INFO_KEYS). -spec(stats(pid() | #state{}) -> list({atom(), non_neg_integer()})). -stats(SessionPid) when is_pid(SessionPid) -> - gen_server:call(SessionPid, stats); +stats(SPid) when is_pid(SPid) -> + gen_server:call(SPid, stats); stats(#state{max_subscriptions = MaxSubscriptions, subscriptions = Subscriptions, @@ -257,8 +249,8 @@ stats(#state{max_subscriptions = MaxSubscriptions, %% @doc Discard the session -spec(discard(pid(), client_id()) -> ok). -discard(SessionPid, ClientId) -> - gen_server:call(SessionPid, {discard, ClientId}). +discard(SPid, ClientId) -> + gen_server:call(SPid, {discard, ClientId}). %%-------------------------------------------------------------------- %% gen_server Callbacks @@ -342,41 +334,34 @@ handle_call(state, _From, State) -> reply(?record_to_proplist(state, State, ?STATE_KEYS), State); handle_call(Req, _From, State) -> - emqx_logger:error("[Session] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[Session] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast({subscribe, From, TopicTable, AckFun}, - State = #state{client_id = ClientId, - username = Username, - subscriptions = Subscriptions}) -> + State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> ?LOG(info, "Subscribe ~p", [TopicTable], State), {GrantedQos, Subscriptions1} = lists:foldl(fun({Topic, Opts}, {QosAcc, SubMap}) -> - io:format("SubOpts: ~p~n", [Opts]), - Fastlane = lists:member(fastlane, Opts), - NewQos = if Fastlane == true -> ?QOS_0; true -> get_value(qos, Opts) end, + NewQos = get_value(qos, Opts), SubMap1 = case maps:find(Topic, SubMap) of {ok, NewQos} -> ?LOG(warning, "Duplicated subscribe: ~s, qos = ~w", [Topic, NewQos], State), SubMap; {ok, OldQos} -> - emqx_broker:setopts(Topic, ClientId, [{qos, NewQos}]), + %% TODO:.... + emqx_broker:set_subopts(Topic, ClientId, [{qos, NewQos}]), emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), - ?LOG(warning, "Duplicated subscribe ~s, old_qos=~w, new_qos=~w", - [Topic, OldQos, NewQos], State), + ?LOG(warning, "Duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, NewQos], State), maps:put(Topic, NewQos, SubMap); error -> - case Fastlane of - true -> emqx:subscribe(Topic, From, Opts); - false -> emqx:subscribe(Topic, ClientId, Opts) - end, + %% TODO:.... + emqx:subscribe(Topic, ClientId, Opts), emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), maps:put(Topic, NewQos, SubMap) end, {[NewQos|QosAcc], SubMap1} end, {[], Subscriptions}, TopicTable), - io:format("GrantedQos: ~p~n", [GrantedQos]), AckFun(lists:reverse(GrantedQos)), {noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate}; @@ -501,7 +486,7 @@ handle_cast({resume, ClientPid}, {noreply, emit_stats(dequeue(retry_delivery(true, State1)))}; handle_cast(Msg, State) -> - emqx_logger:error("[Session] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Session] unexpected cast: ~p", [Msg]), {noreply, State}. %% Ignore Messages delivered by self @@ -546,16 +531,15 @@ handle_info({'EXIT', Pid, _Reason}, State = #state{old_client_pid = Pid}) -> handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = ClientPid}) -> - ?LOG(error, "Unexpected EXIT: client_pid=~p, exit_pid=~p, reason=~p", + ?LOG(error, "unexpected EXIT: client_pid=~p, exit_pid=~p, reason=~p", [ClientPid, Pid, Reason], State), {noreply, State, hibernate}; handle_info(Info, State) -> - emqx_logger:error("[Session] Unexpected info: ~p", [Info]), + emqx_logger:error("[Session] unexpected info: ~p", [Info]), {noreply, State}. terminate(Reason, #state{client_id = ClientId, username = Username}) -> - emqx_hooks:run('session.terminated', [ClientId, Username, Reason]), emqx_sm:unregister_session(ClientId). diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index 61d48042e..3df468f1e 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_sup). @@ -74,7 +72,7 @@ init([]) -> %% Connection Manager CMSup = supervisor_spec(emqx_cm_sup), %% WebSocket Connection Sup - WSConnSup = supervisor_spec(emqx_ws_connection_sup), + %% WSConnSup = supervisor_spec(emqx_ws_connection_sup), %% Sys Sup SysSup = supervisor_spec(emqx_sys_sup), {ok, {{one_for_all, 0, 1}, @@ -86,7 +84,7 @@ init([]) -> SMSup, SessionSup, CMSup, - WSConnSup, + %%WSConnSup, SysSup]}}. %%-------------------------------------------------------------------- diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl index 8644f8801..667ef0f1a 100644 --- a/src/emqx_sys.erl +++ b/src/emqx_sys.erl @@ -171,6 +171,6 @@ publish(metrics, Metrics) -> safe_publish(Topic, Payload) -> safe_publish(Topic, #{}, Payload). safe_publish(Topic, Flags, Payload) -> - Flags1 = maps:merge(#{sys => true, qos => 0}, Flags), + Flags1 = maps:merge(#{sys => true}, Flags), emqx_broker:safe_publish(emqx_message:new(?SYS, Flags1, Topic, iolist_to_binary(Payload))). diff --git a/src/emqx_sys_mon.erl b/src/emqx_sys_mon.erl index af6d39138..435dfaaee 100644 --- a/src/emqx_sys_mon.erl +++ b/src/emqx_sys_mon.erl @@ -158,5 +158,5 @@ safe_publish(Event, WarnMsg) -> emqx_broker:safe_publish(sysmon_msg(Topic, iolist_to_binary(WarnMsg))). sysmon_msg(Topic, Payload) -> - emqx_message:new(?SYSMON, #{sys => true, qos => 0}, Topic, Payload). + emqx_message:new(?SYSMON, #{sys => true}, Topic, Payload). From e2a34ec98d3c377d0b2cf6f29bbdf81d5fdb50b7 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 19 Jul 2018 09:34:49 +0800 Subject: [PATCH 069/520] Comment lager syslog to be compatible with Erlang/OTP R21 --- etc/emqx.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 7101c3a7b..b95e8b1f1 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -391,12 +391,12 @@ log.crash.file = {{ platform_log_dir }}/crash.log ## Enable syslog. ## ## Values: on | off -log.syslog = on +## log.syslog = on ## Sets the severity level for syslog. ## ## Value: debug | info | notice | warning | error | critical | alert | emergency -log.syslog.level = error +## log.syslog.level = error ##-------------------------------------------------------------------- ## Allow Anonymous Authentication and Default ACL From cbbc231210df5604719db57887b080637fd0e030 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 19 Jul 2018 10:08:37 +0800 Subject: [PATCH 070/520] Comment log.syslog.* to fix building issue --- etc/emqx.conf | 2 +- priv/emqx.schema | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index b95e8b1f1..d13348908 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -391,7 +391,7 @@ log.crash.file = {{ platform_log_dir }}/crash.log ## Enable syslog. ## ## Values: on | off -## log.syslog = on +log.syslog = on ## Sets the severity level for syslog. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index fe43f8f53..a3d2cfa32 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -442,7 +442,7 @@ end}. ]}. {mapping, "log.syslog", "lager.handlers", [ - {default, off}, + %%{default, off}, {datatype, flag} ]}. @@ -456,10 +456,10 @@ end}. {datatype, {enum, [daemon, local0, local1, local2, local3, local4, local5, local6, local7]}} ]}. -{mapping, "log.syslog.level", "lager.handlers", [ - {default, error}, - {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency]}} -]}. +%%{mapping, "log.syslog.level", "lager.handlers", [ +%% {default, error}, +%% {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency]}} +%%]}. {mapping, "log.error.redirect", "lager.error_logger_redirect", [ {default, on}, @@ -511,13 +511,14 @@ end}. both -> [ConsoleHandler, ConsoleFileHandler]; _ -> [] end, - SyslogHandler = case cuttlefish:conf_get("log.syslog", Conf) of - false -> []; - true -> [{lager_syslog_backend, - [cuttlefish:conf_get("log.syslog.identity", Conf), - cuttlefish:conf_get("log.syslog.facility", Conf), - cuttlefish:conf_get("log.syslog.level", Conf)]}] - end, + SyslogHandler = [], + %%case cuttlefish:conf_get("log.syslog", Conf, false) of + %% false -> []; + %% true -> [{lager_syslog_backend, + %% [cuttlefish:conf_get("log.syslog.identity", Conf), + %% cuttlefish:conf_get("log.syslog.facility", Conf), + %% cuttlefish:conf_get("log.syslog.level", Conf)]}] + %%end, ConsoleHandlers ++ ErrorHandler ++ InfoHandler ++ SyslogHandler end }. From 429703341583a2fe3149582e23e6c63004a6c2a6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 19 Jul 2018 10:19:25 +0800 Subject: [PATCH 071/520] Fix undefined emqx_vm:schedulers/1 --- src/emqx.app.src | 2 +- src/emqx_pool_sup.erl | 4 ++-- src/emqx_sys_sup.erl | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/emqx.app.src b/src/emqx.app.src index 36807e47b..7d0dd0c4d 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -3,7 +3,7 @@ {vsn,"3.0"}, {modules,[]}, {registered,[emqx_sup]}, - {applications,[kernel, stdlib,jsx,gproc,gen_rpc,lager,ekka,esockd,mochiweb]}, + {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,mochiweb]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, diff --git a/src/emqx_pool_sup.erl b/src/emqx_pool_sup.erl index efcde5d2f..b71c15f1e 100644 --- a/src/emqx_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -27,11 +27,11 @@ spec(Args) -> -spec(spec(any(), list()) -> supervisor:child_spec()). spec(ChildId, Args) -> {ChildId, {?MODULE, start_link, Args}, - transient, infinity, supervisor, [?MODULE]}. + transient, infinity, supervisor, [?MODULE]}. -spec(start_link(atom() | tuple(), atom(), mfa()) -> {ok, pid()} | {error, term()}). start_link(Pool, Type, MFA) -> - start_link(Pool, Type, emqx_vm:schedulers(schedulers), MFA). + start_link(Pool, Type, emqx_vm:schedulers(), MFA). -spec(start_link(atom() | tuple(), atom(), pos_integer(), mfa()) -> {ok, pid()} | {error, term()}). start_link(Pool, Type, Size, MFA) when is_atom(Pool) -> diff --git a/src/emqx_sys_sup.erl b/src/emqx_sys_sup.erl index 0e56e5fc5..b450ddd66 100644 --- a/src/emqx_sys_sup.erl +++ b/src/emqx_sys_sup.erl @@ -31,8 +31,7 @@ init([]) -> type => worker, modules => [emqx_sys]}, Sysmon = #{id => sys_mon, - start => {emqx_sys_mon, start_link, - [emqx_config:get_env(sysmon, [])]}, + start => {emqx_sys_mon, start_link, [emqx_config:get_env(sysmon, [])]}, restart => permanent, shutdown => 5000, type => worker, From eeeed35e2a46ecbeee958c551c34351d7af10835 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 19 Jul 2018 10:46:05 +0800 Subject: [PATCH 072/520] Remove the alarm_fun arg from emqx_mqueue:new/3 --- src/emqx_bridge.erl | 4 +--- src/emqx_mqueue.erl | 42 +++++++++--------------------------------- src/emqx_session.erl | 2 +- 3 files changed, 11 insertions(+), 37 deletions(-) diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index ee2aa1535..517b32dcd 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -64,9 +64,7 @@ init([Pool, Id, Node, Topic, Options]) -> emqx_broker:subscribe(Topic, self(), [{share, Share}, {qos, ?QOS_0}]), State = parse_opts(Options, #state{node = Node, subtopic = Topic}), %%TODO: queue.... - MQueue = emqx_mqueue:new(qname(Node, Topic), - [{max_len, State#state.max_queue_len}], - emqx_alarm:alarm_fun()), + MQueue = emqx_mqueue:new(qname(Node, Topic), [{max_len, State#state.max_queue_len}]), {ok, State#state{pool = Pool, id = Id, mqueue = MQueue}}; false -> {stop, {cannot_connect_node, Node}} diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index db2f30cb6..43bb8654a 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -5,8 +5,7 @@ %% 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 +%%%% 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 @@ -43,14 +42,13 @@ -module(emqx_mqueue). %% TODO: XYZ -%% -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -import(proplists, [get_value/3]). --export([new/3, type/1, name/1, is_empty/1, len/1, max_len/1, in/2, out/1, +-export([new/2, type/1, name/1, is_empty/1, len/1, max_len/1, in/2, out/1, dropped/1, stats/1]). -define(LOW_WM, 0.2). @@ -79,24 +77,22 @@ %% len of simple queue len = 0, max_len = 0, low_wm = ?LOW_WM, high_wm = ?HIGH_WM, - qos0 = false, dropped = 0, - alarm_fun}). + qos0 = false, dropped = 0}). -type(mqueue() :: #mqueue{}). -export_type([mqueue/0, priority/0, option/0]). -%% @doc New Queue. --spec(new(iolist(), list(option()), fun()) -> mqueue()). -new(Name, Opts, AlarmFun) -> +%% @doc New queue. +-spec(new(iolist(), list(option())) -> mqueue()). +new(Name, Opts) -> Type = get_value(type, Opts, simple), MaxLen = get_value(max_length, Opts, 0), init_q(#mqueue{type = Type, name = iolist_to_binary(Name), len = 0, max_len = MaxLen, low_wm = low_wm(MaxLen, Opts), high_wm = high_wm(MaxLen, Opts), - qos0 = get_value(store_qos0, Opts, false), - alarm_fun = AlarmFun}, Opts). + qos0 = get_value(store_qos0, Opts, false)}, Opts). init_q(MQ = #mqueue{type = simple}, _Opts) -> MQ#mqueue{q = queue:new()}; @@ -163,7 +159,7 @@ in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = MaxLen, dropped {{value, _Old}, Q2} = queue:out(Q), MQ#mqueue{q = queue:in(Msg, Q2), dropped = Dropped +1}; in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len}) -> - maybe_set_alarm(MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}); + MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}; in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, priorities = Priorities, @@ -199,28 +195,8 @@ out(MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> {R, MQ#mqueue{q = Q2, len = Len - 1}}; out(MQ = #mqueue{type = simple, q = Q, len = Len}) -> {R, Q2} = queue:out(Q), - {R, maybe_clear_alarm(MQ#mqueue{q = Q2, len = Len - 1})}; + {R, MQ#mqueue{q = Q2, len = Len - 1}}; out(MQ = #mqueue{type = priority, q = Q}) -> {R, Q2} = ?PQUEUE:out(Q), {R, MQ#mqueue{q = Q2}}. -maybe_set_alarm(MQ = #mqueue{high_wm = undefined}) -> - MQ; -maybe_set_alarm(MQ = #mqueue{name = Name, len = Len, high_wm = HighWM, alarm_fun = AlarmFun}) - when Len > HighWM -> - Alarm = #alarm{id = iolist_to_binary(["queue_high_watermark.", Name]), - severity = warning, - title = io_lib:format("Queue ~s high-water mark", [Name]), - summary = io_lib:format("queue len ~p > high_watermark ~p", [Len, HighWM])}, - MQ#mqueue{alarm_fun = AlarmFun(alert, Alarm)}; -maybe_set_alarm(MQ) -> - MQ. - -maybe_clear_alarm(MQ = #mqueue{low_wm = undefined}) -> - MQ; -maybe_clear_alarm(MQ = #mqueue{name = Name, len = Len, low_wm = LowWM, alarm_fun = AlarmFun}) - when Len < LowWM -> - MQ#mqueue{alarm_fun = AlarmFun(clear, list_to_binary(["queue_high_watermark.", Name]))}; -maybe_clear_alarm(MQ) -> - MQ. - diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 607a3644f..02c7152b9 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -268,7 +268,7 @@ init(#{clean_start := CleanStart, MaxInflight = get_value(max_inflight, Env, 0), EnableStats = get_value(enable_stats, Env, false), IgnoreLoopDeliver = get_value(ignore_loop_deliver, Env, false), - MQueue = ?MQueue:new(ClientId, QEnv, emqx_alarm:alarm_fun()), + MQueue = ?MQueue:new(ClientId, QEnv), State = #state{clean_start = CleanStart, binding = binding(ClientPid), client_id = ClientId, From 83dee0e34009caf1b64df20d6ddfb098a39b4f87 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 19 Jul 2018 10:53:41 +0800 Subject: [PATCH 073/520] Rename emqx_alarm to emqx_alarm_mgr --- src/emqx_kernel_sup.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_kernel_sup.erl b/src/emqx_kernel_sup.erl index 96dde598c..25e96b930 100644 --- a/src/emqx_kernel_sup.erl +++ b/src/emqx_kernel_sup.erl @@ -26,7 +26,7 @@ start_link() -> init([]) -> {ok, {{one_for_one, 10, 100}, [child_spec(emqx_pool, supervisor), - child_spec(emqx_alarm, worker), + child_spec(emqx_alarm_mgr, worker), child_spec(emqx_hooks, worker), child_spec(emqx_stats, worker), child_spec(emqx_metrics, worker), From 7d0cba94273f81c69cea7cb8ae3f00d20869e590 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 6 Aug 2018 16:33:10 +0800 Subject: [PATCH 074/520] Add MQTT 5.0 supports for connection, protocol and session modules --- docs/mqtt-v5.0.pdf | Bin 2605289 -> 2895576 bytes etc/emqx.conf | 38 ++- etc/zone.conf | 126 +++++++++ include/emqx.hrl | 154 +++++------ include/emqx_mqtt.hrl | 109 +++----- priv/emqx.schema | 21 +- src/emqx.erl | 8 +- src/emqx_alarm_mgr.erl | 6 +- src/emqx_bridge.erl | 4 +- src/emqx_broker.erl | 326 ++++++++++++----------- src/emqx_broker_helper.erl | 6 +- src/emqx_client.erl | 109 ++++---- src/emqx_connection.erl | 365 ++++++++++++++------------ src/emqx_frame.erl | 13 +- src/emqx_kernel_sup.erl | 1 + src/emqx_listeners.erl | 2 +- src/emqx_message.erl | 63 ++--- src/emqx_metrics.erl | 10 +- src/emqx_mod_presence.erl | 12 +- src/emqx_mod_subscription.erl | 2 +- src/emqx_mqtt_properties.erl | 25 +- src/emqx_mqueue.erl | 2 +- src/emqx_packet.erl | 57 ++-- src/emqx_protocol.erl | 471 +++++++++++++++++----------------- src/emqx_router.erl | 6 +- src/emqx_session.erl | 415 +++++++++++++++--------------- src/emqx_sm.erl | 2 +- src/emqx_sm_registry.erl | 3 +- src/emqx_sys.erl | 6 +- src/emqx_sys_mon.erl | 6 +- src/emqx_topic.erl | 24 +- src/emqx_tracer.erl | 12 +- src/emqx_vm.erl | 14 +- src/emqx_ws_connection.erl | 43 ++-- src/emqx_zone.erl | 78 ++++++ 35 files changed, 1382 insertions(+), 1157 deletions(-) create mode 100644 etc/zone.conf create mode 100644 src/emqx_zone.erl diff --git a/docs/mqtt-v5.0.pdf b/docs/mqtt-v5.0.pdf index 6e5cd42056adbbc99e718a7f8700aa15491751fa..5b7e403f86ee30f1518b3400acdcc06f77243f1b 100644 GIT binary patch delta 139568 zcmeF)YpkW`bsu(ch9a#ANr{@_?F@&rXU?2Clw{59`=z89k-CvGba!nlYG9qqK8_um z6lFOvkgD4P1}$0xv6#Sm9lL3yqF?$!iIv8sU?+%NG*FTvsbwH>9VdbKC4!tdk&L7P zY$53H|GfL%XPsjk7AOB0Y{XhTY7jOHiTesahbL;G_ z+i$(%)*ZL*y!BnTUU}=>tv_<>u3LBCy64ubZoT@}y|-R->-?&mUGx3+IxyR~!c`d{09-yPMJd$#^!^}$zd{l(h`)ol-M z_21c_%{txA`#R@tKKbmk&wcdAJN-eoyS($x`QZ=mJ@wpA=3Z}k@6FFW`_cKc-WyJv zTW|l!b3gInywk5feb3gz)xWx9_kXH>>!GbbfAQw~p8M$DN9UdXq}w@n?ujSPotr;< z_}u=FoEwd+uiddbI2v>ZFVoj>v#(xvHd)?Vbv9bw_};z!AMf0JzX>cmJJr>&%ycp6 zj+dVt&$MZEeWufw>AktP>b(KlAKZK1%#^F|dT{IYi$Sje?rIRd299fk==DzxqStM} z(hQ>4!0yx_PQ!bW2A|un?s#zP4F=IPbv%d$oa=*V;I&%wUa#9gY&D4H#y2yFbbCCA z2B^n_XrQ__h<*dFwLz?4*bJiIfbg3cL}q0WKlH%XcQ5*bego;%DEbXp*GJI+Y<(2X zm#a}UF#KjlQP86L+nq=5u72&FtxiVLfctnP4Q$s((m-u}B+Zwrku-q(CPp$Xi&DMf z!L1vAVlZf+z1HKP0q%N_%_y242MsX3nI0#FAb)r;Xdu0s@t}e8djAbr*ZXfKxaz+F z!OQhu-S) zt-8lob-$Pl568p3et&O&u-D%^8XX)B21kcSNBz;^{BSxu-0Z$!BPR9uCq)Qjgw-BxEfYhIo;?<@s?NA(aoqotVgqef6amS2sER9U>Q<5mqQKb8%R zmK@BTRYS{OM@zoa87*0|A5Du(j_FPfmdcS?f`-7m?)k7gW$W*md% zq(@8S@=i03VNHb{c+re;x!|4Aas@i0Q8VjN)AXnr+NgZ>_Ku}k3hB;h+`K++rZjHG zJ#L0HUJmJxcN#ez!C!s+=N{QU>sR%myLO*`q88a%)HP&T4(8m=bz~XUJ)OI`ju|g~ z?)rtZgYK}jieS2s<*wbgYc`#4Hrt^2iDv8Jo@{pSUtT=_POSw5HNw#umhdFCmH_o@ z2?oEGaPVshh!fMEt_C<;4zOUy<^W%gNNVs?NY>~5-lw+D-|Pdcr}7a8NnL| zy?n4af*O7&7k}I=-7yfVPyfFcFYJIpx@tzyklK2`{ch85zqg!u5!0Lf)Id)Ieddj| z6y9vKp|KN>y4sJp?pDw5U%PfTa{FiQ-hJ@QU{a9fwH}2(9FJ?J=~Lsf zZe?8042p=Z-*$5~fPQ~^WBf7$>l@^3dU{~Ps`r^r8Q7>9*myOtu#k*-Qa$Y|%YwT8oV#`}Iw;IB#ZxJe^ zm0(%k8o5=y^oBB|=%tlso!=dSAa|BK6ir1~qDw(idh{Q|Eb z;Cmjs{KowI|bs=f!bp>Fi*%1bAsmE9a`=S1vx7H)_!5*KGOjxLX$I_F-q> z;g>I7s6KGVrF1*)HSLc3&AWQeLY``OJS^{$x#Q~jU%C2tJ|CZaw=_9fH92jXoULBF z>T}5ax$9k~{;MCm`bMox`m4wHSD(4Gl}Sv%0siFIu(T2yc=C=*SM$PM)%Sh*;yriZ zkchcxb*0sJf92xMd@v~=JUW;(QsIN0KahsKea!#!_BW)gvr*dDRr949$=a{g&!skb z?&eB2E(T2)V$qGKE8Tcn>&Crte>k4Zr?c5`Hagmy9S(-0>2yXa?f2)0dq?w=x-kvv zWd99nH~lw6a@>D|u%-Xm$~2hOros17569E#_;9>`IGzvpdh@~PX#enFI2#W7qan_H z!Za9^2_N$zYUgP_G$to&{#d|KzrDsEhrRBK53cjaVSjjI`ZD}++OQ$etNA0~)%=kt zqDH)CP`JwRux9=06P>M_Pt!P$aRCQ!ICO7-D-Ta|=-zOAqrber&Y`O>J#}>e#elCK z0O&OXhzj}WpqL291Neq;dCj1(x8t{$!>zvj*RS8b=#NH|QG`|*-m{%^l;++!7kMxa zZm%8;jvq{;$u|$Mo#Q9746S)G8je;^raiA6KcTs$n??GW7DvSDBid6lg6VLA!yZ4G z9X}bzDp)dfeW)+mAuheAVw$t$O2WJ)&`U61{lQJoRsXH15t~ z@SMEIH@4PF{T_^4gF$mI2Gz#5!y%!zd2f0=;Kgz@z0u2bJdPQ-@$op~b>rS73c}{S zkoLyCX}m2b?=5=M=#eLH_GT|LYAkKzy~z+qIdyM*3Nn-7_!I=)E6@u3JE+1x{m}%2 zI^Ezjw)BYsP8ju@Gnr0?QMFIJ=iX$Ejy&s+rl@i4UivRL*D-f_Z+vP=rjzMpbKR%Y zUTlv1Z#9wWl+JhRUQm7Wy~X%tJ|2hBZZtSq=OLaM^mG!&v+>?ETF&OZ80s7MW>|+? z=D#-T+369^x}!C4`9jCD-l-*VFGktPPGzjZgxRV0hT~1doOP$C$aB`6of@=z z5pn*vI0iqco}$lLZ#ddq;~7T3iE!?%bN_nK!_$-#4trwIkqma1X5F~4PQC|cPPI21uFE>__p#^6V6pMyxH~$gaWA{e0jdc&3 zkL&B?dy{T6m_;)?FZQDPZ@kFxIejk%@Wy+^qq%W!7+2lKz2Zh(ySD&_39aeWi{rQp z)?UneE2|Mf#hEN z#T$c}PEL1%6`p!;wn;fujV`4s^5>Yu+$?UQ&3mV3JY)QCb~5X4ifHW(HrcX!!{g;# z)S@>xH)+A~1~Da`EJI{2vprx0Y&vp4hb=XW1IJD3ft$3DONKi|#xnT^lw?7a2eZ#y?F zp%$+lt{lb9ZfkrEwVUpEx0SJ3ZM|x%b9*0^x9)XQlPCuMmauKwKvTcZass8h|Lxau7bZe2Z# z1e*Eusw+SD$oVrvdr1#$_1_rB&8hE-KCs#Sa@X_R&8B;?LN(NR;M3+=mSvgb57k>t zCjyefAvPw-tG~Q=JV_jNd2iWE;eF-9Q&9okYH{c8;q85%xayzXx%cfv;t~ccerrID*?f4rmH}8%ekHh6AUhQ}^uNQvf+PhxyWXGq~ zhxV>rTTH3L`-l4nllV18N7L~>ZM`?%7jSSiJsNNL{R+LsV|BdET9kq^pS5k7;mS?F z+LIry0Jhqx8QN3Zzx&c_LYR~4GjG25H_l>!)sMa9BERqmGJNJOacK`H-Rf_@<>Fh* z>|gcPiyz4Q^*$7Gd-5s%>|cEI_3<0)h2I$6YI9(Hbj!`4wVlXe_2l2$x$i7IDZ6}U z3w-P>KVz52`qfUl{hH3={j&G)4tqMm zzj}Do>>Fdg2=d8JK>r55O6+zzy~%9t{&;nNeFVMg-aoxNI6FjlU)|YyomGL8Uz_M4QO1Wq0OM)@Nnqg$z(Vm?+qac`W%h+#s}j* z&-Gx;AkxQpd_W*+-`9(|2Jk*c>A~>OGcSw}e6=U|Va}y6;o;`#)VW9p&Fqhl_=D_@ znT`#_3=CbE7+B3Gwp2J{3_cxl*r%QGcw%xhTc->H-)Pr>>}YE9Xkqlx`KIZZY@d|| z&8-Y*PEVeV4|Q;$fq^*>-5Hi{K?QU)4?ci0Jn)`LjCo~s&|6K&=HzgX3AzUsbHg4C z#=h%~;uzc;E;7g-hZI%HOtV0)Cs~eq)>MIV|DfE@8wTg^qg)SWU^f;6b^tDc+>}q6 z#Y*~hM-av5XIh}zne*|o}z$)KCAbS zFwF(8`xs8tuTLhy9$f?vb!3>jnFQy05cUwx9eO^2I|t>9{xAlR#k9TQ&>3n8}-4G>1YapkD$H9 z9?#ti{>-UinJ?tfjv;{*9JTsWo*lT$X7x+}9cQY3RNk@1f)advW`Qv>z%p+PN=z4nSoP4AAmE6gOG%UV#DX zUij;1U;yL8QN}%=STRFIrbenW%}hgjgP~Q|7NF7PP&F)o>Rki3RJxm4p2Nu=Lhlau z@vgjS?&Ezt&I{pmAakUTQI>?2G8xau3tLcFyIVfSjsR|7V}MjJcOMcMd3J0)^G=O4*@e~cwrhK2mAe@>*64ISGc6P69)zupAAlA4ATX0;E%8!k{DvB zJxFDZAXSLWs{k=Xc3oq)2w;NP06iTC=|U;C*g%8~2_ga$29IxP&1|tf!|}a74t&Fo zM$$6~+8r-25g*Qj&)II!nC3tO5sCyudYLaA=muV%+G7puu-Cnitwk73x|R>u1;I3o zJrHKovAIW-T3iwa(b3p~e1(_Tvz?gccyA%j0Z=ml%}mTZqqD|9HvqUuMiO+i7c|At zp%8%2Gk`)Pmch5w_3=oFBzz!|p1j!*XU)x-_7(gD@gJ`oFf%Opnf&6}EZ$9uE zO5{~LY8dYxtO=_nZy{2c8&+arhLi@XgXZ>Nn3-ny@EI!h_q|Cn;C!JCLkF#W!^=Aq zvN8mV7aRzu$@(85q(i_XY|^0rg;NeSC)fbmby0+Yz<;`>?jlp6q$Mh}5%jc(;GuEs z8R)_Xz{DTu7>;NWLgf~MeFR0tXvSn`VlH&7#JlQ6BUqd|pz zjTStI z&_R^7X~Jl06SOQr*AW`T$P`LMUA1mT36wxa)B<1_D=IgPeN9;$^QR;5U*LA(4gr+6 z!uOyMHsul*M81*gzQdAe4m8d~tJ=ef4vbQOcgVmD%^9V@TewUsFx&7x^c|LBfdZ#P z+yHGGc(fm%TCP1X48sCI0RCU=9oLV1FUc=tL+K z|071C_?vIQlYpTv!~q1Ti2Q&Q0}i%X+l;}wLVu)X_>@qq?~Xaa2pEEEOiUOyU~}39 z|1l{IAvHZU-V8MB70riE5kxDb2c>AkWMc>qgQe?WG!_ZA2Ttx=E*p=PS&BspPOiLq!(_%GC=3K^RQ6Y(2i?ok~0Mz~Hj3fv6 z&BOKR%llwB_k=c#qj2%V1`^+J!=C$SFnndW2wN~(Fra2LpC(1i!6cBBQTR5XEOQ74 zrweJ+Xn=MQsplKUn8pNzp{r=bI0hoMq_Jf22izn4RWBfD$@a?|v0X|P1_F7kV6;ONL>aJT8ip0_hwSX|pryU=HG@L=5xx)(URe9XC=bR? zO@WeO|A8iHyE`n*_I+#aM%wrwe zNylyQA@cwQbP%>maR(btm9P-&b_6}Gcb3N#+%grb0-13N9GU(Xnpv1@KFCa_<-h2U zJOntLVOa0bdvJLgs|~Txa*RF}LX2=Q4L3t6z9(cN4T=V#RV^3p5pxuTaltP9CFBf< zTJKqZyoePRMsCG)K>8QvX~Y1WIdEx&?ScpeEi?}$$qEg03D972IGUx)#RC8B-dYfv%9t^}eA6;8-*mVOfC51i?w@)A$FT z5k7$|0dx@}K@t!TCr8f6EV@i6KnF&B00v>I76CH|v9Wd2N|Z7bZl!!>eXP78n+SNI zPa9>Z_b93$ETWX)b}$P;hJRo_dL4)U=SUtO592U|C}>OuydbhK>Uj2+Lq=GMH)C!@ zp95x1TTcy|fDa7}(=L7{?}SqxY9~69(H8Z?+PD{Y27DtE!DV4$A&R)|f+k_1v{?c& z+9d-iJRyh)5uq)xMLdOlW*}jxeNTh9%rC+QlLj+tY@X^OfQnAFPvAxgiTARoZ}0{N z#r`86eB#Z3B-$Dz(riI8x-XDL6^Alto}yux3{8I+Qp^(rB*}5LqC*ly8p>CuL&XYW z6+^_($PEy~bf7e4C`&#bEwBn?VXaVr^@|TEni+~WAVLR9=A&38r2*qVh|CDwAUn@0 zQtM;St_!6^MTH3=HeI?$!Q*xGPFOh_i9v3mpftoR9>@4B;JFSD5D0ea@6b{_9|f&k z7qmp;*lf6B_!|r$b~Mg1z?vdtBN-#gVpYZNoT6@|WwySscQ071=27Gjccxa=PYb?x*iD`IH7Fn1*RQHMdg=>Ht z#ZLQJyca?v3zQr^G~y5DgyFE>bPFYjzu+%-3LKuhRiKjfx2RglERkCD#>||UDVj8T zLLH{$3A;MzN3Mi-z!}tH?O_9+0!5D&SS8B#|1eQYU^r!#WNh4UoJU0$7=@jhoqywu zK}P9}A{D~R^2G?#LKN(xS$Q68n;0W2cH|AJJOtXPYD2Z1@6Q5a=#0rf`oS|IJi8&4U& z3v2~I3ev=f>2(0dj3R0vq({0v0Hdf}>D^lR*bgXW83>nf$p~d+YAN-Dt`QC9oqI+Q z_htw+>^Epzz9Z6z>;-K$`UzxMtH*^LMikY%>bCAka0?fFB%5T2au`f zo*`6J6lYUBeh3-limC#iqU6Nm5dty^ixtg@$+jRKEC^&_Tx47^L$bp7e!LYt8VCjP zz=P}%uoD0^sus5euEtvgew@r85V4iUnC^w&`4|l1FNtd_QX#H8$_otwJi&(kACDFt zp7I2ikOFiJu!kIwH#tTQnqQPL#7$5i|~MRMh^n9_+x-VC_l$mc__Oyc?DakeGGhkP$U>!i=jdMtlSM2&c7#M7|w2vk2|8$?^DT zGI6{`wbHGI0R!;&L+kih+y zH&*0!^k*z9E?kslIk6;*lf48B6ZSBP|IaLNBS?wgSsCUSjLp5mLNJS%%Dgvz!G!p( z;IEyvXu&jp8eKS2l&+Bu5gLoZ1n>t?Dr^j{M+%@Q)3HcdZ1BtSF1UsYV~GH^PYrm{ z;(7jIYnThHxBeOo!)5KoR~#&krE^dq6qnjTL_v`m;1&Ouk;Z3Epani468`|K$`h^8 zD?Pytv;PoVhT;kGj40R~wVQ*$JfnmMbBmc3eT9&TUI$ank;5r+ytq z1_wqPg!Z>(v%p19GC3m&fFl1wpTS@{8Rd~NMbbsZggTZ07w{%&hTTRTkG7bt8Vomj zf~BFT0DZgxp#>xeypg9dXpoE{8dnin%oxK(mJ!q75KdzME0`R69RIUM5vSqOnP=@8 zjx85uf+7K{%5GUv(X2Q~zz&obVH$~GP8nFlc_yMojz^=&P6vAzB{15dZKRZ&P=680 zU#}RgCyKYXp;%U|xKN^;QA&Z-eT*mDm0?ycP&9WlM2x(ocAKkC@sh&6bR2btX@_AO zB1T=*C#x5(5a*tr7u2JegZ#R{3}ZDF{|BEZvSU;dClP{s=_In--H=u+LoC=Z{Rtt) zJS$3Sgt7-v1n>=)%K^d#hT*V~I7n^)u?+aX8B#b}_??cSP9!i2N;eT0@%`w}D&h@u zAM4wrxXw@k0EV5wQdAVh(qO`f(n@%Mp+rFqBjRu@qAeOA!xVSPqz)Ed4qbB!8v@)0 zRwBh1--5Wyt9v z3cB&x{{vWKYf=O5oxsrDbCwdzEk88}Mh*XF|{7|PMQ4l1=!zt$}uTa(y_aMc>Onqeu z%p`kZk%Pt(X<$LJ!hjtl(eZ&U3j^~4If=^>yM~4bSO)e-$N~II6X7dn74SIC1`)IA zX7zCa%|OTSZy?DIU({2K+qVYpUeTlpFpPxLK1>!!3^%kEj4I(ED8g4(Afg%TTcF4= z&zfu7e(_OQa5hUhq_Q*tYxup{#o#0fNN(Os{GNX_2na8*kD?2cl;&p;+T?ykRoUgY zsH9Asy2b`!Lm*{eS=#94bI;gwbNKZ_^2rEhY$7?zunKmriJSks`QK1{DH@G3n92L;7EwqS0T? z7FK3v;6T(wP6YSUc>DvgeY1B9kE}l%&ss+wvh8#huU_0Lxr2RUT-tLlK{R1NWhdD{ zqf_!JN2{i3?}>j1DO8ViAiedM*G zhUn6DEFmstY;N1-ef~!7)8voIcC8SgOCwP*%gmBlLI02V9>N^--C`7MjeI~ zljA7M1V-^2A##&p2SI04ViM?JApRqO%HB21&y0)8Q?S#l;5L**jaE3Z00)$cxDt6d zX?($OB#1dPuCx=^dqQh6ys|m%SI9PSgD0@UrnTurIvJe4;{Oe6GRlw^<``}k(r}fX zkH|y+hDa?AnH7t`=P@E;`#~dk5%R>05O8dOKr9>eWQIjd1dN4+*u_Z+BOrVb8T@eA z*8zrK2#d0Zl)vL~4`kAxT#OggqoyQmH)tj7a0B9C~if$np%=atGG9CpM_?bU3 zB*S6}_sk^UIv4NeIuOVjhtAUhdcfC;Q`R7}4A9I+Q9)f>B7;X4X&5tu zg7_zJyVi!ri)6HP7*d{tkY${lnM|supWl7-wsG~s-@Saf`qH1e_R8x(_Qc7wibl|c z&?ahl8Jv2@rSl0+P5kn=BRKV?H(eJV>umV*9ii>Nu>H{4Vc!{pHa;~*ailA)u|$d# zi%Z}FHL#6O?X~f#WJz|~+xS$>KBA>I5Zm}vjGn4PM~RMwQNxk=-rD%oHa?a2opsm7 zr^Ydst#%FrRMN($w(+TUVA!#gW2Q(OpIY3doab$PDtn55s>B7f@u~5<#3$Rvr^cA~ zr+ALq_|$k0`MbEc5el~npA27f8=smTiZ(tq`weY;Y8#*0#;3ONscn3!9U>dPZG37Q zpW4Q!w(+TLd})b-n>jqO+OeeCk%uT9L>Dq?^C90hxrN_1|MBO3 z;v)xh70{(ys=jvTrH8ApzVY(Y$^81o*LJQ~|KQy2tFOt}p|4~oUZw$F6+Uk2e+>uK zhtFO5k>sgA>>b#F?N6OLJlZ?h-02*f2652neQfu_eU8>G+dj@TJS;`wE_f+$Sw(9E>ZQ9bDUAp|tAUNmz%GcNJ2kNC-oLYR-}xFmCx?|Xn9tO3`gV-!v%hj( zu3pKapZxW$q2(*Zdz&7g`LUig8I}aW5b2W_E?sC4FPY|-h_qT}LE^`VXR1)VX>~oW zyJi9?-T!*`zj}>e^W_-c|IWQ1|C2^q{nSrh zc}w*(e`eRYgyFb>vrz>BE;!wE8qObo)5Qy=-P<00q zKC}D3+!a>w>KoTCETOP0uzc=j0|h5NR!~S1_1i97I$wVYfaB`PS6*rWxGc%M_UQ=# zjt6B14>;v}dIoxV;m_`#fAJSTw)+V%k%T_byWrUBsduYaz2@Qj>bX|G_BubyG|FJI zn)M}!IgYYcgHh3<($b0tE{jaBr9w~rhJxIwf#|TuvOc+k4y8VMueUd!i`{ZG*P#c8 z^Tf#a505aU96A<3zrh?Q-IM({=(Xg*%lhH#{ja!S>EH2$Ql4Fb@uvUY-soU2Sq2rv zoAvhR`%WtMrbjY0OlF6(&HiEFae4UT@~O-IQ)Z{0?Xo<4Q@?zf3VyblzxoVyefFcC z#2oYe+0j6C_IXcnsPWW!e`jYDo|=y~`wuu9^sDQ$J0G^}bUoYpsz}2z8Lg@`9Or_c zHIy|OR9}DN_J4P~^R|QPzkOo+jb|Oqum08(+wTf0-}gti|C+_0Kyeko^GjA}wYuNy zVBpJ)rx6Zo<7sNNpS!slPgAXZEg?K6keZU$oTXVd`$0&vEUR#W*e7)=`V6l38`sX4 zT=Zw5WA!)Ry8VO!PJ7i)y=nVG^}jy3eFGLxt51D(8*_$#rHEwEuN0BgV~R-biXdF| zSTu*p%OsS?E`88LRpr3XuG;c``1dUb~E#ou^p```>9C&z~9fmr}&Z z=H-j4vIpfw>SF!UPE608w9Glqy{<#}dpQp&0qfz!`P;qylN_bg z!`a{{&U-I=TBqQZe!F5Hqv}hayMEtOb@7rKWevYxLy+n&MUr1jfs@IVme*<7DYzwD zaXzlFmGchFd96Vkhu6%lC~MBdEgxVE-QkUyQ*_u`$CbO5WzCTv(c6onI zSD;6f(hmN8@-N~Zw!VLl#_5(Le>v*x zXlHV1CeN18%3KSu%#lS}is$9jIFTlkT#o$Ztg-iUeBCKZr#td*j=bi0dlEs*us~e@ z{M&Zle2=e1;>naz4%t^f_PNeKum0iNcmK=k_P@3DnwvQxos*Le1NbF1gM-{TrdaOh zY;?{Iis;iwe!uvmPw&3zwPzu5^-F*6(wkFE27dj~-AiW)-Rj}Le@VS(#k{L`{ryXC zRtSVi^xKbI%B?~5rN6NKaP@!v{#B(yOrd(|_pjbpeSP`CsA*$deds5)Zk+c z4M){`f8ff6cX*J+Ut-6%uw-g$luGTgKP-!3Sex(I1q(GdRm<)i&M#O&JvtXFYwNWujj;&Uhw*0QK5=X=8S0BIp zBi(}|?Bbw1kj-K??Cu>o20k2j=hK077Kc(-Y_O3*wB`G%5C8VX?|BF~(a~&wa)wf|(wQ6$4`oHPdw?_Aw zZg_RmW3e(Zvu;qy;ldUI`98dC#X_iNau7+evb$FLn25S|^4 zxLoQ+8_|5+=nMb#e`SG;R>}M5CsJmyWAX|N7Q_5BRYZf9n!xY4g?QsksvT6hFjGkY5{Uf9MwYJWb>m_8tZ*SGZ zJGSgXlV5uM!%KTry0i`0G7paJRnW@nHzuOXn6JguJuj2M=BL#f1SkF4T$MTH^c-}u zX0B?fneS4MSO?$a#C#1Xrh2hi;nHOC;>Ukx_r<%YUSHjN_+s_>$F^TV8QL&bN2iQc zS#_3!FPhoPSQYbEjI|qwhey1+vfA#=`;u&`+BLH2+3yba52s{q?i7Z ziKKA2QewP}%ZCJynM*Njw+H&nJV`su)wU zO-U)0gklt2e&!TXlB~kZa96?|`4MwWNJKwaVwg9NVnJ6^~p}IXdzj)fqEPO#r!Xk{8WP9q0lTaP)l&i@kgut|V8H0O?wt z$)n`*DJh8ROd*c`NV&v8m>F~HynSgQCSZ4JkQ=W?4!Y+|G( zdqp|^Ad^@sgXK5$ElF8uhn^=jtxPyf4e30!v#1*ic;Ro|VDh^yUz86rIguo3k$MVN z#A->Nl%!$tjRp_XpyX;v#a+_$Yc@%HGtlHrOIk;Z zbdP}_rVxQ*4iO;0y|(V4Rk%~|z^IbvGHJ%)g3%|pRVW_!3Lo@5+0*=MY2b$?$}%L~ zTHS8Y-g~Z#0KzZzU+5qWl4Kc7Noe9{GGHZ-d+9KwXcZ{|Ns(j{GJlb2$&m_?EC3Fl z$(I~=;8g?59TFfD>w z($7jstLA&y1Z|V+DJiL}xj*Tsq?&?jrNweLZ-(VSNYl>hKOnbVc~p0hlzZl-fa*`( z(dlhtQm`Q{1fQBexF^AuJaL{?@BLTT-cWs_y86BH7i0KNmxL!Z?`mxIT`AjXq{$1C zoK^Xwq2@WfC1hcTtlX=wN}{dh8@L(V=efTfS`hQ@tUH^^Tu|=IS|* z0D44|^wH(8$iUV2(P5m4Po6g{rv56&TYyYU2uqH%$+G%W zw6{NVggZdB;9uL z`gZk~{?g97s>{E1<(*aMf86;KRrhaS-r7pu%6iGx1_-q$dsX*sor^*Bi`A$8>CV=c zT#~64Q(z-cTQtM^_o({B+0MfcWA;g5mwOS4$R&RY><_BXe`@EUi>c7jAgXaL8z6#4 z-RiRsT)%9*RL4-f38&6Z^a-m$h)eldSTv>;{U^)<)+c$U0uj|`fA8vr>OEh$e3#q- z-(nL#Tz$`X?_9piNSCZ(*sGrZ`Ku2;jq{K+I;FhO&@f+v1MD@<1kJ2!k_8hYMe<>< z^&(KLlHHS5Q(bxC>g(^0!ulVv15U>;T)waR{kL~6S6~0`oj+H-`oHh|`K;yche$gr zPjColFH5xiyQI)8l9T@zXDyX(AzAE^@}5+#sm+u{jRZ>)D7-cp+|Z=!e)Yu%uRpjO zse|zH#u5?o%)51t-qvzQB{wH|@7jjap+@B!xp^sbG<=^I1~lGZ6eru1-pH zpa*Nb557sYX|}K?axC*!$pH!EO-#Vw$l;u(6{PtCt1ua)N!dob1%Imy2PmD|M@ zrBrD)**vq;kc@>%49RKaI@Bfwi3U=3D=9_gdCUTuyQZOhijf6YDb5rz9hEiYn;ghd z%WPXovZoY zE5J8^uo|W%;YYM6YCCOtOV;uTLIZyU{Ggng5 zW$;-U4L5?NWP8vtFVF+Ssj~a18i(twS*li~oN(lR&?%|L`5P=)(xgLI`X8tOu}>_K zG^RE(OHSV+Uk+#pQ3K+Zl2TdPR3E`#V=q~lBR5^As6>*)r(Pe_@xS0RB^+KrzbIL* z`MWS1IKo`Wb+Un{u1m5Z$4W3ItT*T#{)h4U9e-Df3MgeZR?;)onm|5`%#A2zDgH?X z0V;CG2eza%$d-ctp^_v6mZBA2W-U2S0#fksTn6)`W;S4p2O)qUIp0A{rr(fkyh0)Q zu(M1@p4Wi4%(gHcQdVP8q*ALbd0ZLE@V32SZ71AwVQpD%iL#g*jk&2r`Oa&5&Zgr^pWaSNMy-)WJAh%5xn?Z;J;Y zOO#oA*{ri9Zt5_47W>Ycp=@gv%`Ps5Qc%kPQ*4V;iZfzYt{JSJXFB@we(ZMyt)xoS zWsL=Y@qZvq6@%1a00X2Hazy~~rIn6y9T!df1deB;hNOr_gwcj!dng{bF>=~3B$asB zJGCn!93b*TE^o}o04=liH#eGBmA8^$rTS4xM&;0PeSu(!gR;=MEu?kJ-VBBf{s6e_bxw(9sPxs1G2 zGS`z}v~tuTM|a#NfSdQ>0wTnguoxW#l`T@-YAI0U3!I=XM1+AsOiiE`V~svX`IqMz zi-G@~K#(jxv0MXSe6f5#7A45%gQ!r2D@8(D0%Qx+@nA~G&Dsyplixqd`jVhMg(FjR zD5=RUBIcxpV)!QYa&m#YUbvkEJY<-3O{udOu&81X6e2RBxN87)M~9#}2OW@-3qwft zp9~luw0yyQ&O*YAtsk_2sK$s7AZpJo3Md9?Q)DoR2md7(1pct!K!q|y>4Qp1`l>-52%!XHs`XOukQ~zt>L5ihVM{yX7+`^Lb}B#2y<{stVUU#nkD0hDBDU$3qla# z|9L@?C{&qUo6Hec=YpUhxDHc+I6f?xRNl@;K-`j1Ga#z@lYxdCDUjG!k)>>1D{MkF z*EeRUSdx2Y!jLga8fcP#WhG@T_ex$MLIEC{S{OVaD;0IY*I^!{LY5I=HEHfq*~8Uv zGpn9mpYYLQCYmRIvO$(A5qX|^0EQLO9ht-BJy@hVTg)ycBy|R~UW)^UKOhXqN|}~l zVJ?J+dBsD|k!r^1$+HM1?9i}O?l8b?HlYs50SmMgPap__WsofxO7wzl3Kl~?d{Ids##gjA zA1E9L*u{&2eOOoQC;D9wMsi|Z8w_S;`wV@j0tzB3axu6uw-E8#8nVUVFvoaOcoXOM zm;=Ne?}XYg2m{Ovl(e!|v|C-pGfJtzt*oYJ=@u9PF^Cl_A=GSjQ>`l-3+@J8qUnWP z!)aok#8|^O%A9dk!-$}YA1PQA_aE9rb$DV*YDT9IEb~_?J4Fw&*zhsy30*?`z@=!2 z(c41S8L(PQLKk_WAnZ03ui#PqU4BJ`MIx9Nk&`+nVY*Zn>^FNwWM5>6l2U0TbHJg9 zT^vVUCSXi$2i5star~GtrzoXVv8dz#pB=>*EkKE#6yH9IHXMi-QzaxsIf1UU$PlJg zk8lsOI!J*c%N>!-{)f-Hj}8~Fd-7ZuzdzAm@kMAAY&Bm5lY)8KF4CYyjx>mNlls45 z9vMz_fLH-WT+kS~Y9rwGO%_I{DPj^CKyRl188IMcPDU2XF4hf7Kw1P3z8$3tBAFLW zAy=9R%@~x~$A5-e{L&5K8x9x#W_Iu@>TL>A6Vm=Jii>%Z723%+3z~Q_4C7&w=gp{4|~S|=(t`+o#u~1Kq-2S@xTY} z1w4UCg}G$=kOB(1Kn*^`B0?!(Q5=FrZ$jcl7t*FC6AckHC9b_AbxsPfyxfWcUw!z>&V7V+c7fy7isDVZ zw3fK0E^0+AOu1|_!i%UM#Ia#?|NFu=S9NF|YpmtE(8`pvYZP;J9-o9kWLH?XCKMmM<|9uctj!iIYZy^%az9IA9_VZt2ZP%a ziB=L@D|O`K-R4@JX)d$`Y#kq_;oH-%|I-%wQ#NpS<>#>QhheysG-$e|`1!Rp%pH zcka-EAVjbyfg4#GI-ZUwnM`F%%W-jz27s50M!0r^Wda?t(ZGCg&SeBwwOS zC4_VnuR@IQJdYDE5htz@H!2GqwbJL5nGS6e=@vZwF61b+sL^$R;gRx-oRR&HJ zhU&fd?mVb6x36Qx^IYmZyA2U_fqYJOFa$l1kDrgr@WW_hofs1?#NZ4=z)Xs#GT%eT zvN2($5{?r0jBBG``Sr4LMo(if-bLiI8^|OkpVm-EmKJ*}gD%#N>v$NnPV2+;wyUx@)eHYs^vCnF=nq2yo&U zi_%yMdjd|}^B8!a}T}0MqK$u>}TiyH9oeR|mp5MLy%)we+t+Q&4Rv&)n z&b2e+ZV8PQJ?r$>7j_a%+C)}=umh9+!-GkiXxG{7#Vfyc?Kj(SQl24d|ZZ*l(Y+7l;Hzlg!&^F#j;Ab2D+YQsrUQlhRH3T-%PjJP(O zG}dd(Um8wqlwKQ7+J=)dc<71sAO0LRC+uJx(0=Pb+3#q6v-z;xHk`B#CuKoU>)UYB zSe^cB!%3Nw*-?NNF}Y}Dw6hcOs%&YO5p;(v*YJ+6?j*u-wN;iP;4@q@sl zHk`B#CvC$?6L8grleXccZ8&KgPTGc(w&A30IBD#yHk`B#CvC$?+i=o0oU{!mZNo{0 z%7}z$!%5q4(l(s54JU2GN!xJJHk`B#CvC$?|1gG=)+tTLUD=Ax?RF1UsO zPp+CA+D&HeajAB2oV$D69i^Vr_f@Zc_s;!}=gjRO?(H8FHsiQKTi-gnG zMpEptvI=V*H(8A9j#r6jmqkp*%l!RKnU}E=1;-ut=LZAlv~qN3Dna;Yu;+xLdJ1zlE6EvBC|dsKr=ehNg$qW?C5c zw<7~==5(AGyDq{q?l)_we-gxFG03e4`HCzL7*-%cCbh8TG89HPRDtU8;-m=#^wIBFI0Ju zLL2KjZ5PAz}={g{maL^q8YLnU~yF|%wAVkB#ngr5YDyLq=hB`iw!yaIj6D5v83rCPp(ov}9 zz6_0yOy&GiPWP3w7kOWln1gAKoLhaTJrZ4?6r+#k0G;z>ZaY+vg@6w}^>3ThwoZMT zlO4%JloO5)1(ri$g549foG52uRY|qdCbdn_ZpomrmfSRe!&GvDE)3=mM^fA5npmx9 z&f^>>wZ-Ye0Uc>bKC(6n8HZ@2kTJ@z+Z?aQRSBv#scjpDO!hA0tg-(#sjZ`f$GVDe zz#LC*liC7ANymbBI7%$|xJ_!CeBR}3SbcWCjY5u^k>hbF$Px55scoCowoPh_M3ZBliDVsBf;T+60}i8+oZN_QrqO4Ym?fxNo|vwrcG+wCbeyo z+O|n;llDmZJ{wGJQrkAEZJX4#O=|1BvTSB;QrkAEZJX4#O={aFwfzoGYAZvt_?MK- z5NG7SWS4A{+KT;YliDWHb(_>y=v`8=wn=UOP$#vO5B%OoI~Pi@X~JXQG436Q$9^lp zr2XzP{6T08^{#Z^ye+~rQ{~Geakk<$<=~b( zD$%q}GI;rrQ$j(pTwGYX>hgUm2qjDEUP`$L?@Yo+hvv1Bw9e8qUy~*^$)}QxN~&dl z3(A#zDyh6}e@HSdytnGSVe9@R4=?3-bSO|!jHdy&(WLf@5~N8BknGH2C9SvLspKah zUXXL@-q)#W<)5~!*^6HYy%oX|@TL?_o6|OtkqPHaA*9F-dAotL6q?|3pTlcqbAa-` zGP+K2cs5v`w>fPC zG4N8*C`A#S4oz97Vtxv)Nuj3XK28>7wCLYMR%>!0v!_a4WTy!M->Jd`=PkTYS7ypK zJR>3DIVnu#3}Q}&rIM3_$*xlv*YYHBGo>nNo6|N9jhvoW8rVVCl)ox#oDY^2>ExEV!lTBh@8SIJOinOcgs^FK51w zoV86=J9S)LL*Y~}O=U`~Io08EYBN<{duqG6p{|?mY=J2wms)~S4rFT#M@VsaRU>Va zacnwho@%|R1b3KfuC7&cRAw&QXsKb?=Co~d+O|1u+nlz_11mR-myj9be9c0Ka9H}ogCv81Px~Ny23Y00s!Y7!T#p(>Z!zC2f$FxgzN>!M>t)Ql= zg7!ObHQWquRLxnJh0)UUC^fle_^8sF=iJaKAfD3my!+xmdxJ$vTT8PepM_eS+H}5O zl9BAaxK>+Isb06oeB0V#V9HeMx4QMXBA(|NhMEGMF|gWb(uf%06bh{aR-4nd&1u`_ zv~6?R(n9gbHm7Zy)3(iN+vc>5A-fk3{iqi;j&i2LJZD|vXX?yi%w1ZQbZ@bDDRSBH znF+kOoKnEEM6U4bDPliPLb5&g4rIxbAFlfR8@3)267}k@U%qy3d_*BrIe65oKJ`Rr zuqgVUd}(7k8xz_8r^Vi$Q|Rhl>0!DpfjanA>6&3ohm^O9^*c1^F~ufB8Y)a4ccN*O zY-?>asYF@wQ+)C9-NzPj)TY?Ca3m|I7G8pqcta$P^N91k_|lc2r&f!#WEb$QQ|m1t z$cl#Y4)=!l^GPtlb=(W_{t9#w*aU;z;koF#W>4@`#5mwE8TJz8B*P4MRgYgphslo@ zzew);ug9CrbYKy<&OU&&m)SH@=uo_m0mzrO{mp&my+pO6CuB=4 z$#DDAWuDZMleVAG)MQd!CBq#g3F}%iTxG0}bJC7_v%{l)uZzCt`+NJn?r1Wb@9)p{ zdWUl3?r$c;?N8Q{;SQQiuBGEalfw15<0gUY`{pH~>tH<#?^v$~cA;jo$$>KhgQLA+ zm*Udjn;lK}#+kN$p?Y0vOg8?qD8wldd)z=>1epmI&Z{NP>PE0V* ztIz(6?M}CgQ4`RI!^vU)U?P$5VYk13ba;3uiE)2=xB&tHA;X_AE~BbL+%h^MoSTTc0e9`!o$pAG*={AL@sED{z(B7)nvWv zY)dW~8zDBRM8)u#=5NPU60kG#Tp7Gl^l{F_O@@^rmog!gC>#FXX^xb*7Q{g;C4`z8 zTzhw^G=g+A)e~ofMKO{`{K#QdY8q;PHeSdjA{1!O;e254VP?OMS6X)*3=@?KnU2E7 z8BKzFjCqg0*I?W^)VU+{iSg#9cWrLY=d^LlT=rRLZOHa zi^T*RE$o~1vpcEugvc3dGv@ubnnlS-z)pPj35d7aX(p>%!eMyd-3_|zL$n;NwZvA| zxlQ3awsc)*i(z6I5mftj=MFfW;T1(DUK7dL#*3l7+icU>AMn>WrlPv{dKiOQ+v_qv@JF+9e2Qzl$)DZ3L3D9tU%bBV+Zn=$HZsV3kYTD;% z;d zZ>c`@^vp%bx9O`bYR@or{|rAt6zC`_x*R+?{DLl+qh*RVANmd z?#oftgrp}OOdGe{jebq#|3e?QJgi=N!z1UV!5voj{^?zzx5ESgf9uht!$FBAJu@D@ z`02M_{YS4jFXU5<66=;R%~91eeDq`H~ggq@m@!UNX+I2WWEL29r8EBp!-Q)CQB%M{_!=O@eFlxD6(?eclF>I%6o0V2&xa!K8vB zrpy!(6~qXYlS1AGleS54OY$UcL_2S7Fln+d^ZZSD3X_}aD2b|gy!P>t1kv?M07a5Z z2&%KKG?nu~+>szPt|000rI;A)EePQpNmhU?VNN`c$sFxUWRks1_MX~W1FlG0pQ5Lb zq~JTv%yqqQt4fSU!sUcxCAoCIPy)`e-6OYYe1@0*W=`(PVIW9PiX-Vvj*?n6*`@o7 zq~UV*+6^v=vaWj)SUc)F5m;w)OoR4qoj<}6CXyRbu=(~RO-)*a#C!>#>)I6&9B3v< zfZYa@w!x%rFln0vw+$w3gGsv%nY6*AZ7^vYOu9$;>Qc@Q1g|R+QWUoCxW90YVM?`< z{yx?g^YbXmS57ZTSL%jr`IEjhR?at209{La<662M8`Dzl*zBK|IQJt#De{x?Wkgv! zqH-ozJ>RG}xu}RUq&V(V>u8iJFbSZW>Osm*$0ulSBJmyn(4)-wNjz4{w~Eixspvi- z{397x>9*!!{N3fS4@43O^3$x(OTtU%0PKo)2Xr7MbR_E(>+gvs^LIaQ`8UZum1!IA zy;8K*QL2uNs*k;8XZv=i0gkHA|An3F_ZUi@W-A$2_p1+I*}2cjkE9ezX{RXOa>y3F zm}+XysxDJMlR8KOKbdQCmc`i*%}D!`JvS3eK2a$_Ek$z6s(O-gDkfMGyp}UBDM%%1 zqHOA?{BHscsp$Yk~nYN8x?BPDb9PuQD4O z2a{g)-g|c*T!h4(6_TSDr8@}jG*Pmyp*W;p(ljOuTXF)k(vhZ?a?}kI!G#=#4MV_8 zY_x_=xW+isx6Oj&HcLgNl-o?Ks6=M2Q8dGS?mBrX7ML`S(gu^(z?mm_lNz3wT!aE$ua|K$tXK)65BTUP@R5TK3`^(9d(aW*>>lL{HFXm z{3H9YO*-4;oh0&mN8R~szq^qWRW$1-AK7{8R65;J^2sJ$ZW+U<$#J_H!{}tz*72z7 zy{_}@&Hik3FgP5{_vW+Ses4}LIm0&-*EX9R?aQTl?ujSPotr;jbT%{xV(esCwzHOZ}(gTK;zQP(Azj<%g@!Keqjf zoI>2lFg$KRxB}XE1;+Pxygx`<+jm=ifcV;z7q2d&D~$GfNBt?yr+c_hpXkX0Jm(wV zpGhLR0lrZ(;RZ^NKYs4!+n#&&qZ*ww>+;s0{OH@?|IxjV&U3R#srv)(-v5z03-Kh` ze6zmq`N(qz@1K`XtE)FV32Yu!zuN6wGp^CJ+PiD_?OWqfFs@#YKi)ZBzj61IZ@Io* zee9{L7bOTT^Q~W+4Q{BN>3^Nhpx3>@^H_f4mdE$|!yBes?w@G0`qWpq2WJDFdhGz` zXYSs85ORF!j!PGk0Q$;pR|n~@nf?bmgW2@Pq~CRNv(xeKjX~eZ3|V}K-N}tUy{|uT zf7rhpBax|{Nn$1`QIOPJzrqnpc_|fRIHMdH{n&BlJd#(q|S>2 zlct+BCmq6r|g&0lk?m?heK?8L|s>_kM0-GEthLK)V#;Z z* zkoUnz6~&ka%X8+d9KJEV02|nN)#U*J@HU5U9qgQ-=Qf8gvP5U!IXQfBvsB=~e8Ny< zTR4cNi=g5;NT|AVPK?qvhi`25Y@q?x9A}O#->a_t?$s?DbLB*21VyZd*dEwNBHHmC z`+0aCF9x>aIszog;%%?tSb!TxLk?Qxc@a3tm{0^y>V&{HZHtUwwVTrh-gDg~k=K7Y zeo$DT-pZz7jw3LRGa$-x^o9H};sm&fXJoeSeesY7hHprN!K-#xy|9*hF!N|*KOE!iT3Fxg2cX3HswVECFDp5lfZiW zH>v8FEgU-$xQXJV!Uy(cEKQ|GLF@~(7Pa8Fm#aJJQz@t z@bDxrB*=?zUJywV-f{>F9kB(*6P=(kA%GAx2(#~6h@Us0Mlu}B$D8tZT>0J)ZphEe z$1mrx=WPz(HixfBBxu}*UE|$_Eg#{ENE?4l63F>jj+Mc5fd-v>Le%QFqv)hLcft{U zQ$2H(3JoWqHep@Z01odN)?u@`Q2Ze&QyB5cMoZejDe&aP%ep>7l6_IG*)>WmT2Bl1 z^nD+bH!^1tR=}N#ML3a_pj)Cr?w9DeUM2x-Aqg18t8$#oc(S4qH+niM+up)*9Vv?( z5t3$b5@{QonPL_;wGtkTnYB55+pz04?D{)2?AoDE3PBEa&hak?NU;x^sSV*ahc6zO z?e$csjbbDm3CHbYy~3{Q;N4tP>xE=xBa{|Sop zWZ88acAXQ?M|5-R5sS{!_gquIT_<`iNi$&%wE=2ay9Ckfli}d+N!PVQD@su$@*=`F zH(j+Dfjvr%9{b8W$0M<)>P zk!vbP%u&}+A?2$ViAj%R>_6+U>oem?_3=kbsOz|T>8Y!I5yz9`h~sZ1nAGurI%;*& zfAPw1UHi>{Zx-J+p0tf8MTBiUX~K>9PDBj~SsN#|n*Sj+brS!aT1(0@6$ye#lCEkf zc|TR*vjt9sv0B=AQaVH%PnvU60YtPPB9Ej>`}x%ApsmZMa}?m_k5pWQzH zj`&bXJQ|R~OcMdy$x9L|J!P8+*(f%S5kS5~&8LC{!x)n0T7*F)^U!Hi*VU(gZ|8yP zg}Zk8A0l-(t5uMkiKQosqr`Eo^4&$VvSd7YG8l`*gwT2*?V&S|ptfatjvg4%?zQ@? zr#7mu{@W|pA1N}}@vr)R8&4`tzGa9PGoDE*4v%||P=IaXWwVAQvdmIsQ_oRcK9?xj z?3eO~TVx6CJy5NEu$(bg`oP*p`{ki8Z>FOt^}6#PwW-|An@N}x<;r1Tiqiwpnd-*# zcbxN7^~tRIsXIEo?I9)>O`G1g_L9W2Tu5|!{Y|R({;O+msM>f^!5clQaT`xc+=X4S z3Pd{PkFe_oe-j%{*>B@X530|9YUiPgu~ut(ZR1ISV##}ktdlm4ZyQhA#*-%8r$}{N zf_4y}Sh%Wf@HU<_=R^;njhH_%&b)kTPSet~&=OBbV zKd~>WekB){J`?Ub9ov$y|04+33wE0l@s$|)nS_$IkFp`>WuB#M35nK{wpI+3;9nv8 zdE3^ZO+LGKWw%G!(qx&nl_)}2fKHBQ5ZidtoO(;RYmQ%vtu0A5 zBcXHDMPvrS?Rkz-(h2NpBy$Rx?^uf%5CN`}y`2Q(x=+~C4@6#248&P$;K(+6eUNTA zsqX#h&V}j&&+p!U=3rdC@OLj?uDNWxr?$__m^v9$Pk#C0 z1J#=z?VPLr@$`m63MZ^u0_aJgq)_ z?$Y;HuYUK={WoXbBN|Kp@aV{2!}(nN%6Mxb|%CV16X&`d& zp2~o{f9=}Y@wB&0^gAtSWZxkuewDfQTZ!wP4oe4;(7x(7uRU~TJgr{%!p`6sXE47# z%~xOBdw8oP5)S%q+a9@dxzcGWIo^9%)>mLRe$^=yI0RTTvq+sUAtRPk4u-Qa``%~aAVjzoy&JD z$M0-;e?6D)th)D4@5<$?*?M_qgObblj3Hln;`#+oh9`T@lMg?BxdsK5de)@EUSh}W zR5I*Y1FWZjV$$s{b1F}|y^>S8dob*d4iBgE!~N;O(czKfD^5O4bL?%lKR+03V$v-6 zu$%6clsJK$&N(ixo0V{oySY=kAFa9{m+rq$365E}ceFp-KV*!I51pqyI64}1=iQ-1 z?t7aloy&Y{xZm7n3-mjkn?LY@Ki27tXEy@(WK`x$FRS#-xIm+R8(FMUD~npZ;q*m$ zSq$3o{lWCcNS5R@a&nmx1b=&hURkFAujE|D`rfy5-}M>}$HS5i`t-1d)lYq7_u*%1 z7@Z!J^UBgPpB~g;+Pfk8esfThZq@rtr+=o8wZ3QP`dNobYSi%{vHPzz+GE=L17T$f z!JKbu3i_H=&p&?op(Q;M9VfA%x5>mD>|V!$UiHFnTzl6Y=yB4U7Bu+L>8!i&7|+px z0v#qRbdzQ3%smX~4GyP!C;8c=*PKQ;&g?zuHHf~J*&A6eJ6@&np7g8U_SX4@zQZKy z84G(X?{#DzXnxcz#GM+a{ivtqM@@8oG+!;nNN2Q^Ab!-8;YaiNwE4Vn^u_x+o;BYu zIap`3WL%-tdOB`3-z{m~o#kg^h#J|@887M3k9sNms3Gph>g8pFB!sWhzf<Av4|-*4LKH|-3{px>zb!4kxT*z)^9)6k%4 zNS5+4#Wi}I^!nAuf9{cO1lb#u1njS^QScahdl=Da3hWQNg)y9Q3T5%8?fViDdt>%; zj9}KgF?<=CgSezxK2Ki${Cnle?FY{~g;wpo8M}sqwR*F7YWrOE?3XXzRi1tPbJx3P zymRkkS08^}D)5-nH#^p|d?+V~t>2(|&G8+3E&w`rbH#F(V09-fcd!zEen$A+-0Mz- zYES43Y9H)R_H&kcJ{3FZMBZ$AxWRHK)onXl=c}*&&i480ADr8L^^TLz%O0B%G$UG# z0Gws+e^>R(Z{NP>PO%SpzxwRI*uMOu)1%p}chIN3^bSF5F4B82IG70-cQmU9A5RoO z;HX-HYK_kE{8}Mer}JTrIBV(psg*^93QKRW1+PrH{%0ly7wZ+U97|MN$FKtA<0Tpr z2&cu2bm~PJsF$FAdo8j5s5iYq@E69tE@e#kM(?yTV0DTz5PP>4GJ~ZuU?wlU;gR#_ zt!0LqjslK%S=>?{;~gF}aQa|}B4Vnb-&kfe|CJo*IV6RpZ1{O_iaSV z|8ey~^@G2#{m@xXycfSVfB5USzlQd&#Dchc&KAir#D!JQ|Cz1t-z#T01$W04sIZH$ zC}%qz;wRaoN-#8%>)-uiMI>eOFvbYCq6YmX0JWGMCB#2RetgG6$oIz_5h&iTe*SH{ zZ@woch$Jfx-t!xdsvrAY=bu;q@a?-l`Z~3)coC5acc?p#pc2}}s1PC*Cz_Mq(vtpMEvIOVcxgUK* zfCLkSS&@l>C$?)RMx zHrjb_b$4}jb#+yBpYDJB^Q*6iYR025+j}VZXc9(?G(WyFTcrH_%8!r#`ONLp=JF99 z@5A=d!v|bE!{Z@**Rpx|;4!Mh|2tO4+6HZpH@^Pz>G9bI>mR(pajv`yt#HH2%GCg&%ct`xnjQ$DGAZ-G&Rs$&x?f^H=VX;6}#BsBSv^To~Fl z$5-E5U)&05v{&oZ*F6|GLI&15t7d98YrtO@M=@4Aj;@k1H)-ELxw zwqa|ZFUuyuU$mx~paEX$^xg2zAn`=u2#FbnDb!PV%p?-AC=FK)lgM?>t@nSb>AXYFb*2r4Z1Y? zv?t$M25rjT!07QETK~#n@^{O{1ExRv&HD7|->%lbyFty{N$`@^3BjkVohVIoZm4mP zK^b=)4u_(~oBZ{ik6+v|l84Dkhe{8lkdB}J{cEe6RQ&um@0~vV{h!x=4BsmqZ>(;g zW-*_#{D_!IQha#ld!v+7sK8hf#!~Lh+ba^xTB;IYBtG(hOul?6lHH74ki>(N*IOeYC-s!$#Njt#jBNN)DrNWR(jo$rXY{EL$;y(^hKJNS9;z zijIxq2yOX?{!iiu+-rsLZJ=X07a|p6`-B;euN%``J{Y0&QnsgefO{1o@nU%43t`a3 zHCKl1B3c(Zq7=ifX(Q2pRhn&TB70F+nA*txC>FL|Fg~+%%!Y&uGNx(OFXHFDTI(Ix zYnfQNH%n&(ALGRVaj%#W-yGTtz)so z77a&P@(jI$-jgmxYDEfjaiCymLab#K9XC)K(_n{5Kl(}gIl&Q`7%9wVk7H(3b`7Rb z>9(!el=xOynC{~WV4KIhm*ZR$cUdE8TUvFPk5}}F0F{}<(P+W;o0fb4_K$zF62>Ji zDr)v&Osi?dnBlYaNKe)Ek=4>!f{9u>+>a}5oPALR;R?ud*`#7l!(c#E3i&b<#}vm?@~DA z9Y3j$<3-l+AvQ5ax_(&8^W~j0IAgKjBo*?Mj$__CVnNfRIG(R%`;@a%bt7>}krJZ8 zCVpZ(V=#4Q9Z?xuH}+jGGFMBwPQBzNFNPL zH2t5A4`9Trb%i|Wc@1W+RJz=lCk1An6p4V!Po#ae{dUQ0HP?7pBON}pt`U5DV)LEb zWK4l^^SzBdS8REOXpKmLthy~QVCI0timH0VhQAFR*tifC{vX%2*?u=3u!6g!zkBhg0OvTxXy%N`75 zSKqL_<+gC0E6icLV6}4P9(UX`r1cS1jSMdSA9l;;F_@X)u1jY7!OV5EAB-!+rD5iSH4GN}iMj0L|8ZZn1ss8?*9z9g{#@NKn-enaptE2F>{6NO11!@9 zRE*WyZLy2m2+SBLG*g6-%*IvmBUOOj6cO zY&ll-amsvIoJRi-LW^(%TO7r~PXxnnwF)`)WEiB5S?78-LDZ&GlIMu6F!NKfz3a?& z?>e*PQXicWV6ix5`OnjdquSN#6uJQBQXnIs%-|84VuGp5MdG$8hH3Es0*uUDHBOjn zU5N0@nA}E}vwBjNh}7u1*aolZdc7`UJd`Oo5q zQ2l;Vs%*em;iOp+va5|QVqmRHi{-fJT(nOLmTglNYuECcy)+CKiP1`gU5l&Yn7ZVo zHHg`2IMpcY+98%lSosIwqo)B0@O7>oF-kIz{s0PgH7c37OI{T?cJPg9;!O4bV8+&iz;qr5rfWVJ5rTH4 zBlGLnv|!EtAMP$E@{N$aOT+lM-7%XHJJK zjgFmW!?;r0F>M3;%dI9}AydK%bGx8pI;`VS3$B1NlKphhZ zH?CZ91V;+*@Dp{ugUJ-z@J5oaB_#F21jqWnh?*S}n>w6B*O)$Dm$XLWfgV-DeIiLC zOh-0sVS4cYs18{hp-;OA;Jrc&y_L1}JZ)eem9A$!P_bgEHE|HuTm3q)R?~HX8H1cB z)_q0jHaP|i`Ogjrp>UkWtA+$jGu;%fHdCP|6^5zsj^s1bQXdP#(nhpT;kGqJ0y7OJ zS8a|(qfqVVUa^av#k^ly=3kBB$(;Hq5Sb8#ISQW!K9_iRH>*)U? z{^9gsEiRZ=twztyH^76_+z>kymfQ@i>fCD~{=~|rnn}KM+M}Bp3?EvVVcl+<*}*^^^Ysf{^W~ delta 1643 zcmZuxJ#Q324Amv%2unz~gz#~MupAsqLE-Uy*wfKKg+QX_572;+s9BI)Q?e2TNc;c< z2#LQ!gM^fdCK3%bbGwIbZhhUzAZgOzt-?|5nBK2czoo*SFiZhqWn90RZSddBidu0m`RkVLdEv0>#CR zVRnlvwzRlH#8A@zf{f8G?z$%Ioy}wQtTTta$UHHEejGV#*5yU8kQbcQix4ur^E|WM zxXnX2)QOl4+x+=e?bq`PmW=A;Pw2xCTIF)G3iWv2qut%jq@GbZyd&NJ$ CZc)zw diff --git a/etc/emqx.conf b/etc/emqx.conf index d13348908..555a6d63b 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -702,10 +702,15 @@ listener.tcp.external.acceptors = 16 ## Value: Number listener.tcp.external.max_clients = 102400 +## Maximum connection per second. +## +## Value: Number +listener.tcp.external.max_conn_rate = 1000 + ## Zone of the external MQTT/TCP listener belonged to. ## ## Value: String -listener.tcp.external.zone = devicebound +listener.tcp.external.zone = external ## Mountpoint of the MQTT/TCP Listener. All the topics of this ## listener will be prefixed with the mount point if this option @@ -831,10 +836,10 @@ listener.tcp.internal.acceptors = 4 ## Value: Number listener.tcp.internal.max_clients = 102400 -## TODO: Zone of the internal MQTT/TCP listener belonged to. +## Zone of the internal MQTT/TCP listener belonged to. ## ## Value: String -## listener.tcp.internal.zone = internal +listener.tcp.internal.zone = internal ## Mountpoint of the MQTT/TCP Listener. ## @@ -932,10 +937,15 @@ listener.ssl.external.acceptors = 16 ## Value: Number listener.ssl.external.max_clients = 102400 -## TODO: Zone of the external MQTT/SSL listener belonged to. +## Maximum MQTT/SSL connections per second. +## +## Value: Number +listener.ssl.external.max_conn_rate = 1000 + +## Zone of the external MQTT/SSL listener belonged to. ## ## Value: String -## listener.ssl.external.zone = external +listener.ssl.external.zone = external ## Mountpoint of the MQTT/SSL Listener. ## @@ -1166,10 +1176,15 @@ listener.ws.external.acceptors = 4 ## Value: Number listener.ws.external.max_clients = 102400 -## TODO: Zone of the external MQTT/WebSocket listener belonged to. +## Maximum MQTT/WebSocket connections per second. +## +## Value: Number +listener.ws.external.max_conn_rate = 1000 + +## Zone of the external MQTT/WebSocket listener belonged to. ## ## Value: String -## listener.ws.external.zone = external +listener.ws.external.zone = external ## Mountpoint of the MQTT/WebSocket Listener. ## @@ -1294,10 +1309,15 @@ listener.wss.external.acceptors = 4 ## Value: Number listener.wss.external.max_clients = 64 -## TODO: Zone of the external MQTT/WebSocket/SSL listener belonged to. +## Maximum MQTT/WebSocket/SSL connections per second. +## +## Value: Number +listener.wss.external.max_conn_rate = 1000 + +## Zone of the external MQTT/WebSocket/SSL listener belonged to. ## ## Value: String -## listener.wss.external.zone = external +listener.wss.external.zone = external ## Mountpoint of the MQTT/WebSocket/SSL Listener. ## diff --git a/etc/zone.conf b/etc/zone.conf new file mode 100644 index 000000000..5c546fe46 --- /dev/null +++ b/etc/zone.conf @@ -0,0 +1,126 @@ + +## Limits and Capabilities + +##-------------------------------------------------------------------- +## Connection + +zone.${name}.idle_timeout = 30s + +zone.${name}.rate_limit = 10,100 + +## 10 messages per second, with a bucket 100 messages. +zone.${name}.publish_limit = 10,100 + +## Enable stats +zone.${name}.enable_stats = on + +## zone.${name}.shutdown_policy = ??? + +##-------------------------------------------------------------------- +## Protocol + +## Capabilities: + +## Maximum length of MQTT clientId allowed. +## +## Value: Number [23-65535] +zone.${name}.max_clientid_len = 1024 + +## Maximum MQTT packet size allowed. +## +## Value: Bytes +## +## Default: 64K +zone.${name}.max_packet_size = 64K +zone.${name}.max_topic_alias = 0 +zone.${name}.max_qos_allowed = 2 +zone.${name}.retain_available = on +zone.${name}.wildcard_subscription = on +zone.${name}.shared_subscription = off + +## The backoff for MQTT keepalive timeout. +## EMQ will kick a MQTT connection out until 'Keepalive * backoff * 2' timeout. +## +## Value: Float > 0.5 +zone.${name}.keepalive_backoff = 0.75 + +##-------------------------------------------------------------------- +## Authentication + +zone.${name}.allow_anonymous = true + +##-------------------------------------------------------------------- +## Session + +zone.${name}.max_subscriptions = 0 +zone.${name}.upgrade_qos = off + +## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. +## +## Value: Number +zone.${name}.max_inflight = 32 + +## Retry interval for QoS1/2 message delivering. +## +## Value: Duration +zone.${name}.retry_interval = 20s + +## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. +## +## Value: Number +zone.${name}.max_awaiting_rel = 100 + +## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. +## +## Value: Duration +zone.${name}.await_rel_timeout = 30s + +## Whether to ignore loop delivery of messages. +## +## Value: true | false +## +## Default: false +zone.${name}.ignore_loop_deliver = false + +## Max session expiration time. +## +## Value: Duration +## -d: day +## -h: hour +## -m: minute +## -s: second +## +## Default: 2h, 2 hours +zone.${name}.session_expiry_interval = 2h + +##-------------------------------------------------------------------- +## Queue + +## Message queue type. +## +## Value: simple | priority +zone.${name}.mqueue_type = simple + +## Topic priority. Default is 0. +## +## Value: Number [0-255] +## +## zone.${name}.mqueue_priority = topic/1=10,topic/2=8 + +## Maximum queue length. Enqueued messages when persistent client disconnected, +## or inflight window is full. 0 means no limit. +## +## Value: Number >= 0 +zone.${name}.max_mqueue_len = 100 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.${name}.mqueue_store_qos0 = true + +##-------------------------------------------------------------------- +## General + +zone.${name}.enable_stats = on + + diff --git a/include/emqx.hrl b/include/emqx.hrl index f0b2b46d1..7340cf835 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -24,42 +24,51 @@ -define(ERTS_MINIMUM_REQUIRED, "10.0"). +%%-------------------------------------------------------------------- +%% PubSub +%%-------------------------------------------------------------------- + +-type(pubsub() :: publish | subscribe). + +-define(PS(I), (I =:= publish orelse I =:= subscribe)). + %%-------------------------------------------------------------------- %% Topics' prefix: $SYS | $queue | $share %%-------------------------------------------------------------------- -%% System Topic +%% System topic -define(SYSTOP, <<"$SYS/">>). -%% Queue Topic +%% Queue topic -define(QUEUE, <<"$queue/">>). -%% Shared Topic +%% Shared topic -define(SHARE, <<"$share/">>). %%-------------------------------------------------------------------- %% Topic, subscription and subscriber %%-------------------------------------------------------------------- --type(qos() :: integer()). - -type(topic() :: binary()). --type(suboption() :: {qos, qos()} - | {share, '$queue'} - | {share, binary()} - | {atom(), term()}). +-type(subid() :: binary() | atom()). --record(subscription, {subid :: binary() | atom(), - topic :: topic(), - subopts :: list(suboption())}). +-type(subopts() :: #{qos => integer(), share => '$queue' | binary(), atom() => term()}). + +-record(subscription, { + topic :: topic(), + subid :: subid(), + subopts :: subopts() + }). -type(subscription() :: #subscription{}). --type(subscriber() :: binary() | pid() | {binary(), pid()}). +-type(subscriber() :: {pid(), subid()}). + +-type(topic_table() :: [{topic(), subopts()}]). %%-------------------------------------------------------------------- -%% Client and session +%% Client and Session %%-------------------------------------------------------------------- -type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). @@ -70,18 +79,19 @@ -type(username() :: binary() | atom()). --type(mountpoint() :: binary()). +-type(zone() :: atom()). --type(zone() :: undefined | atom()). - --record(client, {id :: client_id(), - pid :: pid(), - zone :: zone(), - peername :: peername(), - username :: username(), - protocol :: protocol(), - attributes :: #{atom() => term()}, - connected_at :: erlang:timestamp()}). +-record(client, { + id :: client_id(), + pid :: pid(), + zone :: zone(), + protocol :: protocol(), + peername :: peername(), + peercert :: nossl | binary(), + username :: username(), + clean_start :: boolean(), + attributes :: map() + }). -type(client() :: #client{}). @@ -90,63 +100,53 @@ -type(session() :: #session{}). %%-------------------------------------------------------------------- -%% Message and delivery +%% Payload, Message and Delivery %%-------------------------------------------------------------------- --type(message_id() :: binary() | undefined). +-type(qos() :: integer()). --type(message_flag() :: sys | qos | dup | retain | atom()). +-type(payload() :: binary() | iodata()). --type(message_flags() :: #{message_flag() => boolean() | integer()}). - --type(message_headers() :: #{protocol => protocol(), - packet_id => pos_integer(), - priority => non_neg_integer(), - ttl => pos_integer(), - atom() => term()}). - --type(payload() :: binary()). +-type(message_flag() :: dup | sys | retain | atom()). %% See 'Application Message' in MQTT Version 5.0 --record(message, - { id :: message_id(), %% Message guid - qos :: qos(), %% Message qos - from :: atom() | client(), %% Message from - sender :: pid(), %% The pid of the sender/publisher - flags :: message_flags(), %% Message flags - headers :: message_headers(), %% Message headers - topic :: topic(), %% Message topic - properties :: map(), %% Message user properties - payload :: payload(), %% Message payload - timestamp :: erlang:timestamp() %% Timestamp +-record(message, { + %% Global unique message ID + id :: binary() | pos_integer(), + %% Message QoS + qos = 0 :: qos(), + %% Message from + from :: atom() | client_id(), + %% Message flags + flags :: #{message_flag() => boolean()}, + %% Message headers, or MQTT 5.0 Properties + headers = #{} :: map(), + %% Topic that the message is published to + topic :: topic(), + %% Message Payload + payload :: binary(), + %% Timestamp + timestamp :: erlang:timestamp() }). -type(message() :: #message{}). --record(delivery, - { node :: node(), %% The node that created the delivery +-record(delivery, { + sender :: pid(), %% Sender of the delivery message :: message(), %% The message delivered - flows :: list() %% The message flow path + flows :: list() %% The dispatch path of message }). -type(delivery() :: #delivery{}). -%%-------------------------------------------------------------------- -%% PubSub -%%-------------------------------------------------------------------- - --type(pubsub() :: publish | subscribe). - --define(PS(I), (I =:= publish orelse I =:= subscribe)). - %%-------------------------------------------------------------------- %% Route %%-------------------------------------------------------------------- --record(route, - { topic :: topic(), - dest :: node() | {binary(), node()} - }). +-record(route, { + topic :: topic(), + dest :: node() | {binary(), node()} + }). -type(route() :: #route{}). @@ -156,20 +156,20 @@ -type(trie_node_id() :: binary() | atom()). --record(trie_node, - { node_id :: trie_node_id(), +-record(trie_node, { + node_id :: trie_node_id(), edge_count = 0 :: non_neg_integer(), topic :: topic() | undefined, flags :: list(atom()) }). --record(trie_edge, - { node_id :: trie_node_id(), +-record(trie_edge, { + node_id :: trie_node_id(), word :: binary() | atom() }). --record(trie, - { edge :: #trie_edge{}, +-record(trie, { + edge :: #trie_edge{}, node_id :: trie_node_id() }). @@ -177,11 +177,11 @@ %% Alarm %%-------------------------------------------------------------------- --record(alarm, - { id :: binary(), +-record(alarm, { + id :: binary(), severity :: notice | warning | error | critical, - title :: iolist() | binary(), - summary :: iolist() | binary(), + title :: iolist(), + summary :: iolist(), timestamp :: erlang:timestamp() }). @@ -191,8 +191,8 @@ %% Plugin %%-------------------------------------------------------------------- --record(plugin, - { name :: atom(), +-record(plugin, { + name :: atom(), version :: string(), dir :: string(), descr :: string(), @@ -207,8 +207,8 @@ %% Command %%-------------------------------------------------------------------- --record(command, - { name :: atom(), +-record(command, { + name :: atom(), action :: atom(), args = [] :: list(), opts = [] :: list(), diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index f5bb13604..35a8a5082 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -83,13 +83,12 @@ %%-------------------------------------------------------------------- %% MQTT Client %%-------------------------------------------------------------------- - --record(mqtt_client, - { client_id :: binary() | undefined, +-record(mqtt_client, { + client_id :: binary() | undefined, client_pid :: pid(), username :: binary() | undefined, peername :: {inet:ip_address(), inet:port_number()}, - clean_sess :: boolean(), + clean_start :: boolean(), proto_ver :: mqtt_version(), keepalive = 0 :: non_neg_integer(), will_topic :: undefined | binary(), @@ -207,8 +206,8 @@ %% MQTT Packet Fixed Header %%-------------------------------------------------------------------- --record(mqtt_packet_header, - { type = ?RESERVED :: mqtt_packet_type(), +-record(mqtt_packet_header, { + type = ?RESERVED :: mqtt_packet_type(), dup = false :: boolean(), qos = ?QOS_0 :: mqtt_qos(), retain = false :: boolean() @@ -235,8 +234,8 @@ -type(mqtt_subopts() :: #mqtt_subopts{}). --record(mqtt_packet_connect, - { proto_name = <<"MQTT">> :: binary(), +-record(mqtt_packet_connect, { + proto_name = <<"MQTT">> :: binary(), proto_ver = ?MQTT_PROTO_V4 :: mqtt_version(), is_bridge = false :: boolean(), clean_start = true :: boolean(), @@ -253,55 +252,55 @@ password = undefined :: undefined | binary() }). --record(mqtt_packet_connack, - { ack_flags :: 0 | 1, +-record(mqtt_packet_connack, { + ack_flags :: 0 | 1, reason_code :: mqtt_reason_code(), properties :: mqtt_properties() }). --record(mqtt_packet_publish, - { topic_name :: mqtt_topic(), +-record(mqtt_packet_publish, { + topic_name :: mqtt_topic(), packet_id :: mqtt_packet_id(), properties :: mqtt_properties() }). --record(mqtt_packet_puback, - { packet_id :: mqtt_packet_id(), +-record(mqtt_packet_puback, { + packet_id :: mqtt_packet_id(), reason_code :: mqtt_reason_code(), properties :: mqtt_properties() }). --record(mqtt_packet_subscribe, - { packet_id :: mqtt_packet_id(), +-record(mqtt_packet_subscribe, { + packet_id :: mqtt_packet_id(), properties :: mqtt_properties(), topic_filters :: [{mqtt_topic(), mqtt_subopts()}] }). --record(mqtt_packet_suback, - { packet_id :: mqtt_packet_id(), +-record(mqtt_packet_suback, { + packet_id :: mqtt_packet_id(), properties :: mqtt_properties(), reason_codes :: list(mqtt_reason_code()) }). --record(mqtt_packet_unsubscribe, - { packet_id :: mqtt_packet_id(), +-record(mqtt_packet_unsubscribe, { + packet_id :: mqtt_packet_id(), properties :: mqtt_properties(), topic_filters :: [mqtt_topic()] }). --record(mqtt_packet_unsuback, - { packet_id :: mqtt_packet_id(), +-record(mqtt_packet_unsuback, { + packet_id :: mqtt_packet_id(), properties :: mqtt_properties(), reason_codes :: list(mqtt_reason_code()) }). --record(mqtt_packet_disconnect, - { reason_code :: mqtt_reason_code(), +-record(mqtt_packet_disconnect, { + reason_code :: mqtt_reason_code(), properties :: mqtt_properties() }). --record(mqtt_packet_auth, - { reason_code :: mqtt_reason_code(), +-record(mqtt_packet_auth, { + reason_code :: mqtt_reason_code(), properties :: mqtt_properties() }). @@ -309,8 +308,8 @@ %% MQTT Control Packet %%-------------------------------------------------------------------- --record(mqtt_packet, - { header :: #mqtt_packet_header{}, +-record(mqtt_packet, { + header :: #mqtt_packet_header{}, variable :: #mqtt_packet_connect{} | #mqtt_packet_connack{} | #mqtt_packet_publish{} @@ -364,9 +363,12 @@ variable = #mqtt_packet_auth{reason_code = ReasonCode, properties = Properties}}). --define(PUBLISH_PACKET(Qos, PacketId), +-define(PUBLISH_PACKET(QoS), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = QoS}}). + +-define(PUBLISH_PACKET(QoS, PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - qos = Qos}, + qos = QoS}, variable = #mqtt_packet_publish{packet_id = PacketId}}). -define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload), @@ -396,7 +398,7 @@ properties = Properties}}). -define(PUBREC_PACKET(PacketId), - #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = 0}}). @@ -464,6 +466,11 @@ #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, variable = #mqtt_packet_unsuback{packet_id = PacketId}}). +-define(UNSUBACK_PACKET(PacketId, ReasonCodes), + #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, + variable = #mqtt_packet_unsuback{packet_id = PacketId, + reason_codes = ReasonCodes}}). + -define(UNSUBACK_PACKET(PacketId, Properties, ReasonCodes), #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, variable = #mqtt_packet_unsuback{packet_id = PacketId, @@ -486,43 +493,3 @@ -define(PACKET(Type), #mqtt_packet{header = #mqtt_packet_header{type = Type}}). -%%-------------------------------------------------------------------- -%% MQTT Message -%%-------------------------------------------------------------------- - --type(mqtt_msg_id() :: binary() | undefined). - --type(mqtt_msg_from() :: atom() | {binary(), undefined | binary()}). - --record(mqtt_message, - { %% Global unique message ID - id :: mqtt_msg_id(), - %% PacketId - packet_id :: mqtt_packet_id(), - %% ClientId and Username - from :: mqtt_msg_from(), - %% Topic that the message is published to - topic :: binary(), - %% Message QoS - qos = ?QOS0 :: mqtt_qos(), - %% Message Flags - flags = [] :: [retain | dup | sys], - %% Retain flag - retain = false :: boolean(), - %% Dup flag - dup = false :: boolean(), - %% $SYS flag - sys = false :: boolean(), - %% Properties - properties = [] :: list(), - %% Payload - payload :: binary(), - %% Timestamp - timestamp :: erlang:timestamp() - }). - --type(mqtt_message() :: #mqtt_message{}). - --define(WILL_MSG(Qos, Retain, Topic, Props, Payload), - #mqtt_message{qos = Qos, retain = Retain, topic = Topic, properties = Props, payload = Payload}). - diff --git a/priv/emqx.schema b/priv/emqx.schema index a3d2cfa32..7ff5fd0a3 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -933,6 +933,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.tcp.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.tcp.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1024,6 +1028,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.ssl.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.ssl.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1165,8 +1173,8 @@ end}. {datatype, integer} ]}. -{mapping, "listener.ws.$name.rate_limit", "emqx.listeners", [ - {datatype, string} +{mapping, "listener.ws.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} ]}. {mapping, "listener.ws.$name.zone", "emqx.listeners", [ @@ -1261,6 +1269,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.wss.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.wss.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1404,7 +1416,7 @@ end}. AccOpts = fun(Prefix) -> case cuttlefish_variable:filter_by_prefix(Prefix ++ ".access", Conf) of [] -> []; - Rules -> [{access, [Access(Rule) || {_, Rule} <- Rules]}] + Rules -> [{access_rules, [Access(Rule) || {_, Rule} <- Rules]}] end end, @@ -1413,9 +1425,10 @@ end}. LisOpts = fun(Prefix) -> Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, {max_clients, cuttlefish:conf_get(Prefix ++ ".max_clients", Conf)}, + {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, {zone, Atom(cuttlefish:conf_get(Prefix ++ ".zone", Conf, undefined))}, - {rate_limit, cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined)}, + %%{rate_limit, cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined)}, {proxy_protocol, cuttlefish:conf_get(Prefix ++ ".proxy_protocol", Conf, undefined)}, {proxy_protocol_timeout, cuttlefish:conf_get(Prefix ++ ".proxy_protocol_timeout", Conf, undefined)}, {mountpoint, MountPoint(cuttlefish:conf_get(Prefix ++ ".mountpoint", Conf, undefined))}, diff --git a/src/emqx.erl b/src/emqx.erl index a539bbd42..cbe37d12e 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -74,7 +74,7 @@ subscribe(Topic) -> subscribe(Topic, Subscriber) -> emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber)). --spec(subscribe(topic() | string(), subscriber() | string(), [suboption()]) -> ok | {error, term()}). +-spec(subscribe(topic() | string(), subscriber() | string(), subopts()) -> ok | {error, term()}). subscribe(Topic, Subscriber, Options) -> emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). @@ -95,11 +95,11 @@ unsubscribe(Topic, Subscriber) -> %% PubSub management API %%-------------------------------------------------------------------- --spec(get_subopts(topic() | string(), subscriber()) -> [suboption()]). +-spec(get_subopts(topic() | string(), subscriber()) -> subopts()). get_subopts(Topic, Subscriber) -> emqx_broker:get_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber)). --spec(set_subopts(topic() | string(), subscriber(), [suboption()]) -> ok). +-spec(set_subopts(topic() | string(), subscriber(), subopts()) -> ok). set_subopts(Topic, Subscriber, Options) when is_list(Options) -> emqx_broker:set_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). @@ -110,7 +110,7 @@ topics() -> emqx_router:topics(). subscribers(Topic) -> emqx_broker:subscribers(iolist_to_binary(Topic)). --spec(subscriptions(subscriber() | string()) -> [{topic(), list(suboption())}]). +-spec(subscriptions(subscriber() | string()) -> [{topic(), subopts()}]). subscriptions(Subscriber) -> emqx_broker:subscriptions(list_to_subid(Subscriber)). diff --git a/src/emqx_alarm_mgr.erl b/src/emqx_alarm_mgr.erl index e041341e2..41da4e705 100644 --- a/src/emqx_alarm_mgr.erl +++ b/src/emqx_alarm_mgr.erl @@ -81,7 +81,7 @@ handle_event({set_alarm, Alarm = #alarm{timestamp = undefined}}, State)-> handle_event({set_alarm, Alarm = #alarm{id = AlarmId}}, State = #state{alarms = Alarms}) -> case encode_alarm(Alarm) of {ok, Json} -> - emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json)); + ok = emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json)); {error, Reason} -> emqx_logger:error("[AlarmMgr] Failed to encode alarm: ~p", [Reason]) end, @@ -131,7 +131,9 @@ encode_alarm(#alarm{id = AlarmId, severity = Severity, title = Title, {ts, emqx_time:now_secs(Ts)}]). alarm_msg(Type, AlarmId, Json) -> - emqx_message:make(?ALARM_MGR, #{sys => true}, topic(Type, AlarmId), Json). + Msg = emqx_message:make(?ALARM_MGR, topic(Type, AlarmId), Json), + emqx_message:set_headers(#{'Content-Type' => <<"application/json">>}, + emqx_message:set_flags(#{sys => true}, Msg)). topic(alert, AlarmId) -> emqx_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>); diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index 517b32dcd..eef5d249b 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -72,8 +72,8 @@ init([Pool, Id, Node, Topic, Options]) -> parse_opts([], State) -> State; -parse_opts([{qos, Qos} | Opts], State) -> - parse_opts(Opts, State#state{qos = Qos}); +parse_opts([{qos, QoS} | Opts], State) -> + parse_opts(Opts, State#state{qos = QoS}); parse_opts([{topic_suffix, Suffix} | Opts], State) -> parse_opts(Opts, State#state{topic_suffix= Suffix}); parse_opts([{topic_prefix, Prefix} | Opts], State) -> diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 43cc81155..9d332a5f2 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -20,8 +20,10 @@ -export([start_link/2]). -export([subscribe/1, subscribe/2, subscribe/3, subscribe/4]). --export([publish/1, publish/2, safe_publish/1]). --export([unsubscribe/1, unsubscribe/2]). +-export([multi_subscribe/1, multi_subscribe/2, multi_subscribe/3]). +-export([publish/1, safe_publish/1]). +-export([unsubscribe/1, unsubscribe/2, unsubscribe/3]). +-export([multi_unsubscribe/1, multi_unsubscribe/2, multi_unsubscribe/3]). -export([dispatch/2, dispatch/3]). -export([subscriptions/1, subscribers/1, subscribed/2]). -export([get_subopts/2, set_subopts/3]). @@ -31,102 +33,141 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {pool, id, submon}). +-record(state, {pool, id, submap, submon}). +-record(subscribe, {topic, subpid, subid, subopts = #{}}). +-record(unsubscribe, {topic, subpid, subid}). +%% The default request timeout +-define(TIMEOUT, 60000). -define(BROKER, ?MODULE). --define(TIMEOUT, 120000). %% ETS tables -define(SUBOPTION, emqx_suboption). -define(SUBSCRIBER, emqx_subscriber). -define(SUBSCRIPTION, emqx_subscription). +-define(is_subid(Id), (is_binary(Id) orelse is_atom(Id))). + -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> - gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, - ?MODULE, [Pool, Id], [{hibernate_after, 2000}]). + gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, + [Pool, Id], [{hibernate_after, 2000}]). %%------------------------------------------------------------------------------ -%% Subscribe/Unsubscribe +%% Subscribe %%------------------------------------------------------------------------------ --spec(subscribe(topic()) -> ok | {error, term()}). +-spec(subscribe(topic()) -> ok). subscribe(Topic) when is_binary(Topic) -> subscribe(Topic, self()). --spec(subscribe(topic(), subscriber()) -> ok | {error, term()}). -subscribe(Topic, Subscriber) when is_binary(Topic) -> - subscribe(Topic, Subscriber, []). +-spec(subscribe(topic(), pid() | subid()) -> ok). +subscribe(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> + subscribe(Topic, SubPid, undefined); +subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> + subscribe(Topic, self(), SubId). --spec(subscribe(topic(), subscriber(), [suboption()]) -> ok | {error, term()}). -subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> - subscribe(Topic, Subscriber, Options, ?TIMEOUT). +-spec(subscribe(topic(), pid() | subid(), subid() | subopts()) -> ok). +subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> + subscribe(Topic, SubPid, SubId, []); +subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> + subscribe(Topic, SubPid, SubId, []); +subscribe(Topic, SubPid, SubOpts) when is_binary(Topic), is_pid(SubPid), is_map(SubOpts) -> + subscribe(Topic, SubPid, undefined, SubOpts); +subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map(SubOpts) -> + subscribe(Topic, self(), SubId, SubOpts). --spec(subscribe(topic(), subscriber(), [suboption()], timeout()) - -> ok | {error, term()}). -subscribe(Topic, Subscriber, Options, Timeout) -> - {Topic1, Options1} = emqx_topic:parse(Topic, Options), - SubReq = {subscribe, Topic1, with_subpid(Subscriber), Options1}, - async_call(pick(Subscriber), SubReq, Timeout). +-spec(subscribe(topic(), pid(), subid(), subopts()) -> ok). +subscribe(Topic, SubPid, SubId, SubOpts) when is_binary(Topic), is_pid(SubPid), + ?is_subid(SubId), is_map(SubOpts) -> + Broker = pick(SubPid), + SubReq = #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}, + wait_for_reply(async_call(Broker, SubReq), ?TIMEOUT). --spec(unsubscribe(topic()) -> ok | {error, term()}). +-spec(multi_subscribe(topic_table()) -> ok). +multi_subscribe(TopicTable) when is_list(TopicTable) -> + multi_subscribe(TopicTable, self()). + +-spec(multi_subscribe(topic_table(), pid() | subid()) -> ok). +multi_subscribe(TopicTable, SubPid) when is_pid(SubPid) -> + multi_subscribe(TopicTable, SubPid, undefined); +multi_subscribe(TopicTable, SubId) when ?is_subid(SubId) -> + multi_subscribe(TopicTable, self(), SubId). + +-spec(multi_subscribe(topic_table(), pid(), subid()) -> ok). +multi_subscribe(TopicTable, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) -> + Broker = pick(SubPid), + SubReq = fun(Topic, SubOpts) -> + #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts} + end, + wait_for_replies([async_call(Broker, SubReq(Topic, SubOpts)) + || {Topic, SubOpts} <- TopicTable], ?TIMEOUT). + +%%------------------------------------------------------------------------------ +%% Unsubscribe +%%------------------------------------------------------------------------------ + +-spec(unsubscribe(topic()) -> ok). unsubscribe(Topic) when is_binary(Topic) -> unsubscribe(Topic, self()). --spec(unsubscribe(topic(), subscriber()) -> ok | {error, term()}). -unsubscribe(Topic, Subscriber) when is_binary(Topic) -> - unsubscribe(Topic, Subscriber, ?TIMEOUT). +-spec(unsubscribe(topic(), pid() | subid()) -> ok). +unsubscribe(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> + unsubscribe(Topic, SubPid, undefined); +unsubscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> + unsubscribe(Topic, self(), SubId). --spec(unsubscribe(topic(), subscriber(), timeout()) -> ok | {error, term()}). -unsubscribe(Topic, Subscriber, Timeout) -> - {Topic1, _} = emqx_topic:parse(Topic), - UnsubReq = {unsubscribe, Topic1, with_subpid(Subscriber)}, - async_call(pick(Subscriber), UnsubReq, Timeout). +-spec(unsubscribe(topic(), pid(), subid()) -> ok). +unsubscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> + Broker = pick(SubPid), + UnsubReq = #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId}, + wait_for_reply(async_call(Broker, UnsubReq), ?TIMEOUT). + +-spec(multi_unsubscribe([topic()]) -> ok). +multi_unsubscribe(Topics) -> + multi_unsubscribe(Topics, self()). + +-spec(multi_unsubscribe([topic()], pid() | subid()) -> ok). +multi_unsubscribe(Topics, SubPid) when is_pid(SubPid) -> + multi_unsubscribe(Topics, SubPid, undefined); +multi_unsubscribe(Topics, SubId) when ?is_subid(SubId) -> + multi_unsubscribe(Topics, self(), SubId). + +-spec(multi_unsubscribe([topic()], pid(), subid()) -> ok). +multi_unsubscribe(Topics, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) -> + Broker = pick(SubPid), + UnsubReq = fun(Topic) -> + #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId} + end, + wait_for_replies([async_call(Broker, UnsubReq(Topic)) || Topic <- Topics], ?TIMEOUT). %%------------------------------------------------------------------------------ %% Publish %%------------------------------------------------------------------------------ --spec(publish(topic(), payload()) -> delivery() | stopped). -publish(Topic, Payload) when is_binary(Topic), is_binary(Payload) -> - publish(emqx_message:make(Topic, Payload)). - --spec(publish(message()) -> {ok, delivery()} | {error, stopped}). -publish(Msg = #message{from = From}) -> - %% Hook to trace? - _ = trace(publish, From, Msg), +-spec(publish(message()) -> delivery()). +publish(Msg) when is_record(Msg, message) -> + _ = emqx_tracer:trace(publish, Msg), case emqx_hooks:run('message.publish', [], Msg) of {ok, Msg1 = #message{topic = Topic}} -> - {ok, route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1))}; + route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1)); {stop, Msg1} -> - emqx_logger:warning("Stop publishing: ~s", [emqx_message:format(Msg1)]), - {error, stopped} + emqx_logger:warning("Stop publishing: ~p", [Msg]), delivery(Msg1) end. -%% called internally -safe_publish(Msg) -> +%% Called internally +safe_publish(Msg) when is_record(Msg, message) -> try publish(Msg) catch _:Error:Stacktrace -> emqx_logger:error("[Broker] publish error: ~p~n~p~n~p", [Error, Msg, Stacktrace]) + after + ok end. -%%------------------------------------------------------------------------------ -%% Trace -%%------------------------------------------------------------------------------ - -trace(publish, From, _Msg) when is_atom(From) -> - %% Dont' trace '$SYS' publish - ignore; -trace(public, #client{id = ClientId, username = Username}, - #message{topic = Topic, payload = Payload}) -> - emqx_logger:info([{client, ClientId}, {topic, Topic}], - "~s/~s PUBLISH to ~s: ~p", [Username, ClientId, Topic, Payload]); -trace(public, From, #message{topic = Topic, payload = Payload}) - when is_binary(From); is_list(From) -> - emqx_logger:info([{client, From}, {topic, Topic}], - "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). +delivery(Msg) -> + #delivery{sender = self(), message = Msg, flows = []}. %%------------------------------------------------------------------------------ %% Route @@ -186,12 +227,8 @@ dispatch(Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> Delivery#delivery{flows = [{dispatch, Topic, Count}|Flows]} end. -dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> +dispatch({SubPid, _SubId}, Topic, Msg) when is_pid(SubPid) -> SubPid ! {dispatch, Topic, Msg}; -dispatch({SubId, SubPid}, Topic, Msg) when is_binary(SubId), is_pid(SubPid) -> - SubPid ! {dispatch, Topic, Msg}; -dispatch(SubId, Topic, Msg) when is_binary(SubId) -> - emqx_sm:dispatch(SubId, Topic, Msg); dispatch({share, _Group, _Sub}, _Topic, _Msg) -> ignored. @@ -200,12 +237,11 @@ dropped(<<"$SYS/", _/binary>>) -> dropped(_Topic) -> emqx_metrics:inc('messages/dropped'). -delivery(Msg) -> - #delivery{node = node(), message = Msg, flows = []}. - +-spec(subscribers(topic()) -> [subscriber()]). subscribers(Topic) -> try ets:lookup_element(?SUBSCRIBER, Topic, 2) catch error:badarg -> [] end. +-spec(subscriptions(subscriber()) -> [{topic(), subopts()}]). subscriptions(Subscriber) -> lists:map(fun({_, {share, _Group, Topic}}) -> subscription(Topic, Subscriber); @@ -216,51 +252,49 @@ subscriptions(Subscriber) -> subscription(Topic, Subscriber) -> {Topic, ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2)}. --spec(subscribed(topic(), subscriber()) -> boolean()). +-spec(subscribed(topic(), pid() | subid() | subscriber()) -> boolean()). subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> - ets:member(?SUBOPTION, {Topic, SubPid}); -subscribed(Topic, SubId) when is_binary(Topic), is_binary(SubId) -> - length(ets:match_object(?SUBOPTION, {{Topic, {SubId, '_'}}, '_'}, 1)) == 1; -subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), is_binary(SubId), is_pid(SubPid) -> - ets:member(?SUBOPTION, {Topic, {SubId, SubPid}}). + length(ets:match_object(?SUBOPTION, {{Topic, {SubPid, '_'}}, '_'}, 1)) == 1; +subscribed(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> + length(ets:match_object(?SUBOPTION, {{Topic, {'_', SubId}}, '_'}, 1)) == 1; +subscribed(Topic, {SubPid, SubId}) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> + ets:member(?SUBOPTION, {Topic, {SubPid, SubId}}). --spec(get_subopts(topic(), subscriber()) -> [suboption()]). +-spec(get_subopts(topic(), subscriber()) -> subopts()). get_subopts(Topic, Subscriber) when is_binary(Topic) -> try ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2) catch error:badarg -> [] end. --spec(set_subopts(topic(), subscriber(), [suboption()]) -> boolean()). -set_subopts(Topic, Subscriber, Opts) when is_binary(Topic), is_list(Opts) -> +-spec(set_subopts(topic(), subscriber(), subopts()) -> boolean()). +set_subopts(Topic, Subscriber, Opts) when is_binary(Topic), is_map(Opts) -> case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of [{_, OldOpts}] -> - Opts1 = lists:usort(lists:umerge(Opts, OldOpts)), - ets:insert(?SUBOPTION, {{Topic, Subscriber}, Opts1}); + ets:insert(?SUBOPTION, {{Topic, Subscriber}, maps:merge(OldOpts, Opts)}); [] -> false end. -with_subpid(SubPid) when is_pid(SubPid) -> - SubPid; -with_subpid(SubId) when is_binary(SubId) -> - {SubId, self()}; -with_subpid({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> - {SubId, SubPid}. - -async_call(Broker, Msg, Timeout) -> +async_call(Broker, Req) -> From = {self(), Tag = make_ref()}, - ok = gen_server:cast(Broker, {From, Msg}), + ok = gen_server:cast(Broker, {From, Req}), + Tag. + +wait_for_replies(Tags, Timeout) -> + lists:foreach( + fun(Tag) -> + wait_for_reply(Tag, Timeout) + end, Tags). + +wait_for_reply(Tag, Timeout) -> receive {Tag, Reply} -> Reply after Timeout -> - {error, timeout} + exit(timeout) end. +%% Pick a broker pick(SubPid) when is_pid(SubPid) -> - gproc_pool:pick_worker(broker, SubPid); -pick(SubId) when is_binary(SubId) -> - gproc_pool:pick_worker(broker, SubId); -pick({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> - pick(SubPid). + gproc_pool:pick_worker(broker, SubPid). -spec(topics() -> [topic()]). topics() -> emqx_router:topics(). @@ -271,33 +305,35 @@ topics() -> emqx_router:topics(). init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), - {ok, #state{pool = Pool, id = Id, submon = emqx_pmon:new()}}. + {ok, #state{pool = Pool, id = Id, submap = #{}, submon = emqx_pmon:new()}}. handle_call(Req, _From, State) -> emqx_logger:error("[Broker] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({From, {subscribe, Topic, Subscriber, Options}}, State) -> - case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of - [] -> - Group = proplists:get_value(share, Options), - true = do_subscribe(Group, Topic, Subscriber, Options), - emqx_shared_sub:subscribe(Group, Topic, subpid(Subscriber)), - emqx_router:add_route(From, Topic, destination(Options)), +handle_cast({From, #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}}, State) -> + Subscriber = {SubPid, SubId}, + case ets:member(?SUBOPTION, {Topic, Subscriber}) of + false -> + Group = maps:get(share, SubOpts, undefined), + true = do_subscribe(Group, Topic, Subscriber, SubOpts), + emqx_shared_sub:subscribe(Group, Topic, SubPid), + emqx_router:add_route(From, Topic, dest(Group)), {noreply, monitor_subscriber(Subscriber, State)}; - [_] -> + true -> gen_server:reply(From, ok), {noreply, State} end; -handle_cast({From, {unsubscribe, Topic, Subscriber}}, State) -> +handle_cast({From, #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId}}, State) -> + Subscriber = {SubPid, SubId}, case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of - [{_, Options}] -> - Group = proplists:get_value(share, Options), + [{_, SubOpts}] -> + Group = maps:get(share, SubOpts, undefined), true = do_unsubscribe(Group, Topic, Subscriber), - emqx_shared_sub:unsubscribe(Group, Topic, subpid(Subscriber)), + emqx_shared_sub:unsubscribe(Group, Topic, SubPid), case ets:member(?SUBSCRIBER, Topic) of - false -> emqx_router:del_route(From, Topic, destination(Options)); + false -> emqx_router:del_route(From, Topic, dest(Group)); true -> gen_server:reply(From, ok) end; [] -> gen_server:reply(From, ok) @@ -308,37 +344,22 @@ handle_cast(Msg, State) -> emqx_logger:error("[Broker] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{submon = SubMon}) -> - Subscriber = case SubMon:find(SubPid) of - undefined -> SubPid; - SubId -> {SubId, SubPid} - end, - Topics = lists:map(fun({_, {share, _, Topic}}) -> - Topic; - ({_, Topic}) -> - Topic - end, ets:lookup(?SUBSCRIPTION, Subscriber)), - lists:foreach( - fun(Topic) -> - case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of - [{_, Options}] -> - Group = proplists:get_value(share, Options), - true = do_unsubscribe(Group, Topic, Subscriber), - case ets:member(?SUBSCRIBER, Topic) of - false -> emqx_router:del_route(Topic, destination(Options)); - true -> ok - end; - [] -> ok - end - end, Topics), - {noreply, demonitor_subscriber(SubPid, State)}; +handle_info({'DOWN', _MRef, process, SubPid, Reason}, State = #state{submap = SubMap}) -> + case maps:find(SubPid, SubMap) of + {ok, SubIds} -> + lists:foreach(fun(SubId) -> subscriber_down({SubPid, SubId}) end, SubIds), + {noreply, demonitor_subscriber(SubPid, State)}; + error -> + emqx_logger:error("unexpected 'DOWN': ~p, reason: ~p", [SubPid, Reason]), + {noreply, State} + end; handle_info(Info, State) -> emqx_logger:error("[Broker] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> - true = gproc_pool:disconnect_worker(Pool, {Pool, Id}). + gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -347,35 +368,44 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -do_subscribe(Group, Topic, Subscriber, Options) -> +do_subscribe(Group, Topic, Subscriber, SubOpts) -> ets:insert(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), ets:insert(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}), - ets:insert(?SUBOPTION, {{Topic, Subscriber}, Options}). + ets:insert(?SUBOPTION, {{Topic, Subscriber}, SubOpts}). do_unsubscribe(Group, Topic, Subscriber) -> ets:delete_object(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), ets:delete_object(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}), ets:delete(?SUBOPTION, {Topic, Subscriber}). -monitor_subscriber(SubPid, State = #state{submon = SubMon}) when is_pid(SubPid) -> - State#state{submon = SubMon:monitor(SubPid)}; +subscriber_down(Subscriber) -> + Topics = lists:map(fun({_, {share, _, Topic}}) -> + Topic; + ({_, Topic}) -> + Topic + end, ets:lookup(?SUBSCRIPTION, Subscriber)), + lists:foreach(fun(Topic) -> + case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of + [{_, SubOpts}] -> + Group = maps:get(share, SubOpts, undefined), + true = do_unsubscribe(Group, Topic, Subscriber), + ets:member(?SUBSCRIBER, Topic) + orelse emqx_router:del_route(Topic, dest(Group)); + [] -> ok + end + end, Topics). -monitor_subscriber({SubId, SubPid}, State = #state{submon = SubMon}) -> - State#state{submon = SubMon:monitor(SubPid, SubId)}. +monitor_subscriber({SubPid, SubId}, State = #state{submap = SubMap, submon = SubMon}) -> + UpFun = fun(SubIds) -> lists:usort([SubId|SubIds]) end, + State#state{submap = maps:update_with(SubPid, UpFun, [SubId], SubMap), + submon = emqx_pmon:monitor(SubPid, SubMon)}. -demonitor_subscriber(SubPid, State = #state{submon = SubMon}) -> - State#state{submon = SubMon:demonitor(SubPid)}. +demonitor_subscriber(SubPid, State = #state{submap = SubMap, submon = SubMon}) -> + State#state{submap = maps:remove(SubPid, SubMap), + submon = emqx_pmon:demonitor(SubPid, SubMon)}. -destination(Options) -> - case proplists:get_value(share, Options) of - undefined -> node(); - Group -> {Group, node()} - end. - -subpid(SubPid) when is_pid(SubPid) -> - SubPid; -subpid({_SubId, SubPid}) when is_pid(SubPid) -> - SubPid. +dest(undefined) -> node(); +dest(Group) -> {Group, node()}. shared(undefined, Name) -> Name; shared(Group, Name) -> {share, Group, Name}. diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index 975b2bf0d..fecf98a7b 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -17,9 +17,7 @@ -behaviour(gen_server). -export([start_link/0]). - --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, - code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(HELPER, ?MODULE). @@ -39,7 +37,7 @@ init([]) -> handle_call(Req, _From, State) -> emqx_logger:error("[BrokerHelper] unexpected call: ~p", [Req]), - {reply, ignore, State}. + {reply, ignored, State}. handle_cast(Msg, State) -> emqx_logger:error("[BrokerHelper] unexpected cast: ~p", [Msg]), diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 87b1e2bf3..8c742fbfd 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -69,6 +69,11 @@ -export_type([host/0, option/0]). +-record(mqtt_msg, {qos = ?QOS0, retain = false, dup = false, + packet_id, topic, props, payload}). + +-type(mqtt_msg() :: #mqtt_msg{}). + -record(state, {name :: atom(), owner :: pid(), host :: host(), @@ -89,7 +94,7 @@ force_ping :: boolean(), paused :: boolean(), will_flag :: boolean(), - will_msg :: mqtt_message(), + will_msg :: mqtt_msg(), properties :: properties(), pending_calls :: list(), subscriptions :: map(), @@ -140,6 +145,9 @@ -define(PROPERTY(Name, Val), #state{properties = #{Name := Val}}). +-define(WILL_MSG(QoS, Retain, Topic, Props, Payload), + #mqtt_msg{qos = QoS, retain = Retain, topic = Topic, props = Props, payload = Payload}). + %%------------------------------------------------------------------------------ %% API %%------------------------------------------------------------------------------ @@ -242,8 +250,7 @@ parse_subopt([{qos, QoS} | Opts], Rec) -> -spec(publish(client(), topic(), payload()) -> ok | {error, term()}). publish(Client, Topic, Payload) when is_binary(Topic) -> - publish(Client, #mqtt_message{topic = Topic, qos = ?QOS_0, - payload = iolist_to_binary(Payload)}). + publish(Client, #mqtt_msg{topic = Topic, qos = ?QOS_0, payload = iolist_to_binary(Payload)}). -spec(publish(client(), topic(), payload(), qos() | [pubopt()]) -> ok | {ok, packet_id()} | {error, term()}). @@ -261,15 +268,14 @@ publish(Client, Topic, Properties, Payload, Opts) ok = emqx_mqtt_properties:validate(Properties), Retain = proplists:get_bool(retain, Opts), QoS = ?QOS_I(proplists:get_value(qos, Opts, ?QOS_0)), - publish(Client, #mqtt_message{qos = QoS, - retain = Retain, - topic = Topic, - properties = Properties, - payload = iolist_to_binary(Payload)}). + publish(Client, #mqtt_msg{qos = QoS, + retain = Retain, + topic = Topic, + props = Properties, + payload = iolist_to_binary(Payload)}). --spec(publish(client(), mqtt_message()) - -> ok | {ok, packet_id()} | {error, term()}). -publish(Client, Msg) when is_record(Msg, mqtt_message) -> +-spec(publish(client(), #mqtt_msg{}) -> ok | {ok, packet_id()} | {error, term()}). +publish(Client, Msg) when is_record(Msg, mqtt_msg) -> gen_statem:call(Client, {publish, Msg}). -spec(unsubscribe(client(), topic() | [topic()]) -> subscribe_ret()). @@ -380,7 +386,7 @@ init([Options]) -> force_ping = false, paused = false, will_flag = false, - will_msg = #mqtt_message{}, + will_msg = #mqtt_msg{}, pending_calls = [], subscriptions = #{}, max_inflight = infinity, @@ -488,15 +494,15 @@ init([_Opt | Opts], State) -> init(Opts, State). init_will_msg({topic, Topic}, WillMsg) -> - WillMsg#mqtt_message{topic = iolist_to_binary(Topic)}; -init_will_msg({props, Properties}, WillMsg) -> - WillMsg#mqtt_message{properties = Properties}; + WillMsg#mqtt_msg{topic = iolist_to_binary(Topic)}; +init_will_msg({props, Props}, WillMsg) -> + WillMsg#mqtt_msg{props = Props}; init_will_msg({payload, Payload}, WillMsg) -> - WillMsg#mqtt_message{payload = iolist_to_binary(Payload)}; + WillMsg#mqtt_msg{payload = iolist_to_binary(Payload)}; init_will_msg({retain, Retain}, WillMsg) when is_boolean(Retain) -> - WillMsg#mqtt_message{retain = Retain}; + WillMsg#mqtt_msg{retain = Retain}; init_will_msg({qos, QoS}, WillMsg) -> - WillMsg#mqtt_message{qos = ?QOS_I(QoS)}. + WillMsg#mqtt_msg{qos = ?QOS_I(QoS)}. init_parse_state(State = #state{proto_ver = Ver, properties = Properties}) -> Size = maps:get('Maximum-Packet-Size', Properties, ?MAX_PACKET_SIZE), @@ -534,15 +540,16 @@ mqtt_connect(State = #state{client_id = ClientId, will_flag = WillFlag, will_msg = WillMsg, properties = Properties}) -> - ?WILL_MSG(WillQos, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, - ConnProps = emqx_mqtt_properties:filter(?CONNECT, maps:to_list(Properties)), + ?WILL_MSG(WillQoS, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, + ConnProps = emqx_mqtt_properties:filter(?CONNECT, Properties), + io:format("ConnProps: ~p~n", [ConnProps]), send(?CONNECT_PACKET( #mqtt_packet_connect{proto_ver = ProtoVer, proto_name = ProtoName, is_bridge = IsBridge, clean_start = CleanStart, will_flag = WillFlag, - will_qos = WillQos, + will_qos = WillQoS, will_retain = WillRetain, keepalive = KeepAlive, properties = ConnProps, @@ -624,7 +631,7 @@ connected({call, From}, SubReq = {subscribe, Properties, Topics}, {stop_and_reply, Reason, [{reply, From, Error}]} end; -connected({call, From}, {publish, Msg = #mqtt_message{qos = ?QOS_0}}, State) -> +connected({call, From}, {publish, Msg = #mqtt_msg{qos = ?QOS_0}}, State) -> case send(Msg, State) of {ok, NewState} -> {keep_state, NewState, [{reply, From, ok}]}; @@ -632,14 +639,14 @@ connected({call, From}, {publish, Msg = #mqtt_message{qos = ?QOS_0}}, State) -> {stop_and_reply, Reason, [{reply, From, Error}]} end; -connected({call, From}, {publish, Msg = #mqtt_message{qos = Qos}}, +connected({call, From}, {publish, Msg = #mqtt_msg{qos = QoS}}, State = #state{inflight = Inflight, last_packet_id = PacketId}) - when (Qos =:= ?QOS_1); (Qos =:= ?QOS_2) -> + when (QoS =:= ?QOS_1); (QoS =:= ?QOS_2) -> case emqx_inflight:is_full(Inflight) of true -> {keep_state, State, [{reply, From, {error, inflight_full}}]}; false -> - Msg1 = Msg#mqtt_message{packet_id = PacketId}, + Msg1 = Msg#mqtt_msg{packet_id = PacketId}, case send(Msg1, State) of {ok, NewState} -> Inflight1 = emqx_inflight:insert(PacketId, {publish, Msg1, os:timestamp()}, Inflight), @@ -690,7 +697,7 @@ connected(cast, {pubcomp, PacketId, ReasonCode, Properties}, State) -> send_puback(?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), State); connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), State) -> - {keep_state, deliver_msg(packet_to_msg(Packet), State)}; + {keep_state, deliver(packet_to_msg(Packet), State)}; connected(cast, ?PUBLISH_PACKET(_QoS, _PacketId), State = #state{paused = true}) -> {keep_state, State}; @@ -698,7 +705,7 @@ connected(cast, ?PUBLISH_PACKET(_QoS, _PacketId), State = #state{paused = true}) connected(cast, Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), State = #state{auto_ack = AutoAck}) -> - _ = deliver_msg(packet_to_msg(Packet), State), + _ = deliver(packet_to_msg(Packet), State), case AutoAck of true -> send_puback(?PUBACK_PACKET(PacketId), State); false -> {keep_state, State} @@ -716,7 +723,7 @@ connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), connected(cast, ?PUBACK_PACKET(PacketId, ReasonCode, Properties), State = #state{owner = Owner, inflight = Inflight}) -> case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, #mqtt_message{packet_id = PacketId}, _Ts}} -> + {value, {publish, #mqtt_msg{packet_id = PacketId}, _Ts}} -> Owner ! {puback, #{packet_id => PacketId, reason_code => ReasonCode, properties => Properties}}, @@ -745,8 +752,7 @@ connected(cast, ?PUBREL_PACKET(PacketId), State = #state{awaiting_rel = AwaitingRel, auto_ack = AutoAck}) -> case maps:take(PacketId, AwaitingRel) of {Packet, AwaitingRel1} -> - NewState = deliver_msg(packet_to_msg(Packet), - State#state{awaiting_rel = AwaitingRel1}), + NewState = deliver(packet_to_msg(Packet), State#state{awaiting_rel = AwaitingRel1}), case AutoAck of true -> send_puback(?PUBCOMP_PACKET(PacketId), NewState); false -> {keep_state, NewState} @@ -960,9 +966,9 @@ retry_send([{Type, Msg, Ts} | Msgs], Now, State = #state{retry_interval = Interv false -> {keep_state, ensure_retry_timer(Interval - Diff, State)} end. -retry_send(publish, Msg = #mqtt_message{qos = QoS, packet_id = PacketId}, +retry_send(publish, Msg = #mqtt_msg{qos = QoS, packet_id = PacketId}, Now, State = #state{inflight = Inflight}) -> - Msg1 = Msg#mqtt_message{dup = (QoS =:= ?QOS1)}, + Msg1 = Msg#mqtt_msg{dup = (QoS =:= ?QOS1)}, case send(Msg1, State) of {ok, NewState} -> Inflight1 = emqx_inflight:update(PacketId, {publish, Msg1, Now}, Inflight), @@ -979,42 +985,29 @@ retry_send(pubrel, PacketId, Now, State = #state{inflight = Inflight}) -> Error end. -deliver_msg(#mqtt_message{qos = QoS, - dup = Dup, - retain = Retain, - topic = Topic, - packet_id = PacketId, - properties = Properties, - payload = Payload}, - State = #state{owner = Owner}) -> - Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain, - packet_id => PacketId, topic => Topic, - properties => Properties, payload => Payload}}, +deliver(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}, + State = #state{owner = Owner}) -> + Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain, packet_id => PacketId, + topic => Topic, properties => Props, payload => Payload}}, State. -packet_to_msg(?PUBLISH_PACKET(Header, Topic, PacketId, Properties, Payload)) -> +packet_to_msg(?PUBLISH_PACKET(Header, Topic, PacketId, Props, Payload)) -> #mqtt_packet_header{qos = QoS, retain = R, dup = Dup} = Header, - #mqtt_message{qos = QoS, retain = R, dup = Dup, - packet_id = PacketId, topic = Topic, - properties = Properties, payload = Payload}. + #mqtt_msg{qos = QoS, retain = R, dup = Dup, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}. -msg_to_packet(#mqtt_message{qos = Qos, - dup = Dup, - retain = Retain, - topic = Topic, - packet_id = PacketId, - properties = Properties, - payload = Payload}) -> +msg_to_packet(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}) -> #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - qos = Qos, + qos = QoS, retain = Retain, dup = Dup}, variable = #mqtt_packet_publish{topic_name = Topic, packet_id = PacketId, - properties = Properties}, + properties = Props}, payload = Payload}. - %%------------------------------------------------------------------------------ %% Socket Connect/Send @@ -1040,7 +1033,7 @@ send_puback(Packet, State) -> {error, Reason} -> {stop, Reason} end. -send(Msg, State) when is_record(Msg, mqtt_message) -> +send(Msg, State) when is_record(Msg, mqtt_msg) -> send(msg_to_packet(Msg), State); send(Packet, State = #state{socket = Sock, proto_ver = Ver}) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index cd71b1af4..7dc70e6ce 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -20,33 +20,51 @@ -include("emqx_mqtt.hrl"). -include("emqx_misc.hrl"). --import(proplists, [get_value/2, get_value/3]). - -export([start_link/3]). -%% Management and Monitor API --export([info/1, stats/1, kick/1, clean_acl_cache/2]). --export([set_rate_limit/2, get_rate_limit/1]). -%% SUB/UNSUB Asynchronously. Called by plugins. --export([subscribe/2, unsubscribe/2]). -%% Get the session proc? --export([session/1]). -%% gen_server Function Exports +-export([info/1, stats/1, kick/1]). +-export([get_session/1]). +-export([clean_acl_cache/1]). +-export([get_rate_limit/1, set_rate_limit/2]). +-export([get_pub_limit/1, set_pub_limit/2]). + +%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]). -%% Unused fields: connname, peerhost, peerport --record(state, {transport, socket, peername, conn_state, await_recv, - rate_limit, max_packet_size, proto_state, parse_state, - keepalive, enable_stats, idle_timeout, force_gc_count}). +-record(state, { + transport, %% Network transport module + socket, %% TCP or SSL Socket + peername, %% Peername of the socket + sockname, %% Sockname of the socket + conn_state, %% Connection state: running | blocked + await_recv, %% Awaiting recv + incoming, %% Incoming bytes and packets + pub_limit, %% Publish rate limit + rate_limit, %% Throughput rate limit + limit_timer, %% Rate limit timer + proto_state, %% MQTT protocol state + parse_state, %% MQTT parse state + keepalive, %% MQTT keepalive timer + enable_stats, %% Enable stats + stats_timer, %% Stats timer + idle_timeout %% Connection idle timeout + }). + +-define(INFO_KEYS, [peername, sockname, conn_state, await_recv, rate_limit, pub_limit]). --define(INFO_KEYS, [peername, conn_state, await_recv]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). --define(LOG(Level, Format, Args, State), - emqx_logger:Level("Conn(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). -start_link(Transport, Sock, Options) -> - {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock, Options]])}. +-define(LOG(Level, Format, Args, State), + emqx_logger:Level("Conn(~s): " ++ Format, + [esockd_net:format(State#state.peername) | Args])). + +start_link(Transport, Socket, Options) -> + {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Socket, Options]])}. + +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ info(CPid) -> gen_server:call(CPid, info). @@ -57,152 +75,151 @@ stats(CPid) -> kick(CPid) -> gen_server:call(CPid, kick). -set_rate_limit(CPid, Rl) -> - gen_server:call(CPid, {set_rate_limit, Rl}). +get_session(CPid) -> + gen_server:call(CPid, session, infinity). + +clean_acl_cache(CPid) -> + gen_server:call(CPid, clean_acl_cache). get_rate_limit(CPid) -> gen_server:call(CPid, get_rate_limit). -subscribe(CPid, TopicTable) -> - CPid ! {subscribe, TopicTable}. +set_rate_limit(CPid, Rl = {_Rate, _Burst}) -> + gen_server:call(CPid, {set_rate_limit, Rl}). -unsubscribe(CPid, Topics) -> - CPid ! {unsubscribe, Topics}. +get_pub_limit(CPid) -> + gen_server:call(CPid, get_pub_limit). -session(CPid) -> - gen_server:call(CPid, session, infinity). - -clean_acl_cache(CPid, Topic) -> - gen_server:call(CPid, {clean_acl_cache, Topic}). +set_pub_limit(CPid, Rl = {_Rate, _Burst}) -> + gen_server:call(CPid, {set_pub_limit, Rl}). %%------------------------------------------------------------------------------ %% gen_server callbacks %%------------------------------------------------------------------------------ -init([Transport, Sock, Options]) -> - case Transport:wait(Sock) of - {ok, NewSock} -> - {ok, Peername} = Transport:ensure_ok_or_exit(peername, [NewSock]), - do_init(Transport, Sock, Peername, Options); +init([Transport, RawSocket, Options]) -> + case Transport:wait(RawSocket) of + {ok, Socket} -> + {ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]), + {ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]), + Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]), + Zone = proplists:get_value(zone, Options), + RateLimit = init_rate_limit(emqx_zone:get_env(Zone, rate_limit)), + PubLimit = init_rate_limit(emqx_zone:get_env(Zone, publish_limit)), + EnableStats = emqx_zone:get_env(Zone, enable_stats, false), + IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), + SendFun = send_fun(Transport, Socket, Peername), + ProtoState = emqx_protocol:init(#{zone => Zone, + peername => Peername, + sockname => Sockname, + peercert => Peercert, + sendfun => SendFun}, Options), + ParseState = emqx_protocol:parser(ProtoState), + State = run_socket(#state{transport = Transport, + socket = Socket, + peername = Peername, + await_recv = false, + conn_state = running, + rate_limit = RateLimit, + pub_limit = PubLimit, + proto_state = ProtoState, + parse_state = ParseState, + enable_stats = EnableStats, + idle_timeout = IdleTimout}), + gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], + State, self(), IdleTimout); {error, Reason} -> {stop, Reason} end. -do_init(Transport, Sock, Peername, Options) -> - io:format("Options: ~p~n", [Options]), - RateLimit = get_value(rate_limit, Options), - PacketSize = get_value(max_packet_size, Options, ?MAX_PACKET_SIZE), - SendFun = send_fun(Transport, Sock, Peername), - ProtoState = emqx_protocol:init(Transport, Sock, Peername, SendFun, Options), - EnableStats = get_value(client_enable_stats, Options, false), - IdleTimout = get_value(client_idle_timeout, Options, 30000), - ForceGcCount = emqx_gc:conn_max_gc_count(), - State = run_socket(#state{transport = Transport, - socket = Sock, - peername = Peername, - await_recv = false, - conn_state = running, - rate_limit = RateLimit, - max_packet_size = PacketSize, - proto_state = ProtoState, - enable_stats = EnableStats, - idle_timeout = IdleTimout, - force_gc_count = ForceGcCount}), - gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], - init_parse_state(State), self(), IdleTimout). +init_rate_limit(undefined) -> + undefined; +init_rate_limit({Rate, Burst}) -> + esockd_rate_limit:new(Rate, Burst). -send_fun(Transport, Sock, Peername) -> - Self = self(), - fun(Packet) -> - Data = emqx_frame:serialize(Packet), - ?LOG(debug, "SEND ~p", [Data], #state{peername = Peername}), - emqx_metrics:inc('bytes/sent', iolist_size(Data)), - try Transport:async_send(Sock, Data) of - ok -> ok; - {error, Reason} -> Self ! {shutdown, Reason} +send_fun(Transport, Socket, Peername) -> + fun(Data) -> + try Transport:async_send(Socket, Data) of + ok -> + ?LOG(debug, "SEND ~p", [Data], #state{peername = Peername}), + emqx_metrics:inc('bytes/sent', iolist_size(Data)), ok; + Error -> Error catch - error:Error -> Self ! {shutdown, Error} + error:Error -> {error, Error} end end. -init_parse_state(State = #state{max_packet_size = Size, proto_state = ProtoState}) -> - Version = emqx_protocol:get(proto_ver, ProtoState), - State#state{parse_state = emqx_frame:initial_state(#{max_packet_size => Size, version => Version})}. +handle_call(info, From, State = #state{transport = Transport, socket = Socket, proto_state = ProtoState}) -> + ProtoInfo = emqx_protocol:info(ProtoState), + ConnInfo = [{socktype, Transport:type(Socket)} + | ?record_to_proplist(state, State, ?INFO_KEYS)], + StatsInfo = element(2, handle_call(stats, From, State)), + {reply, lists:append([ConnInfo, StatsInfo, ProtoInfo]), State}; -handle_call(info, From, State = #state{proto_state = ProtoState}) -> - ProtoInfo = emqx_protocol:info(ProtoState), - ClientInfo = ?record_to_proplist(state, State, ?INFO_KEYS), - {reply, Stats, _, _} = handle_call(stats, From, State), - reply(lists:append([ClientInfo, ProtoInfo, Stats]), State); - -handle_call(stats, _From, State = #state{proto_state = ProtoState}) -> - reply(lists:append([emqx_misc:proc_stats(), - emqx_protocol:stats(ProtoState), - sock_stats(State)]), State); +handle_call(stats, _From, State = #state{transport = Transport, socket = Sock, proto_state = ProtoState}) -> + ProcStats = emqx_misc:proc_stats(), + ProtoStats = emqx_protocol:stats(ProtoState), + SockStats = case Transport:getstat(Sock, ?SOCK_STATS) of + {ok, Ss} -> Ss; + {error, _} -> [] + end, + {reply, lists:append([ProcStats, ProtoStats, SockStats]), State}; handle_call(kick, _From, State) -> {stop, {shutdown, kick}, ok, State}; -handle_call({set_rate_limit, Rl}, _From, State) -> - reply(ok, State#state{rate_limit = Rl}); +handle_call(get_session, _From, State = #state{proto_state = ProtoState}) -> + {reply, emqx_protocol:session(ProtoState), State}; + +handle_call(clean_acl_cache, _From, State = #state{proto_state = ProtoState}) -> + {reply, ok, State#state{proto_state = emqx_protocol:clean_acl_cache(ProtoState)}}; handle_call(get_rate_limit, _From, State = #state{rate_limit = Rl}) -> - reply(Rl, State); + {reply, esockd_rate_limit:info(Rl), State}; -handle_call(session, _From, State = #state{proto_state = ProtoState}) -> - reply(emqx_protocol:session(ProtoState), State); +handle_call({set_rate_limit, {Rate, Burst}}, _From, State) -> + {reply, ok, State#state{rate_limit = esockd_rate_limit:new(Rate, Burst)}}; -handle_call({clean_acl_cache, Topic}, _From, State) -> - erase({acl, publish, Topic}), - reply(ok, State); +handle_call(get_publish_limit, _From, State = #state{pub_limit = Rl}) -> + {reply, esockd_rate_limit:info(Rl), State}; + +handle_call({set_publish_limit, {Rate, Burst}}, _From, State) -> + {reply, ok, State#state{pub_limit = esockd_rate_limit:new(Rate, Burst)}}; handle_call(Req, _From, State) -> - ?LOG(error, "Unexpected Call: ~p", [Req], State), - {reply, ignore, State}. + ?LOG(error, "unexpected call: ~p", [Req], State), + {reply, ignored, State}. handle_cast(Msg, State) -> - ?LOG(error, "Unexpected Cast: ~p", [Msg], State), + ?LOG(error, "unexpected cast: ~p", [Msg], State), {noreply, State}. -handle_info({subscribe, TopicTable}, State) -> +handle_info(SubReq = {subscribe, _TopicTable}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:subscribe(TopicTable, ProtoState) + emqx_protocol:process(SubReq, ProtoState) end, State); -handle_info({unsubscribe, Topics}, State) -> +handle_info(UnsubReq = {unsubscribe, _Topics}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:unsubscribe(Topics, ProtoState) + emqx_protocol:process(UnsubReq, ProtoState) end, State); -%% Asynchronous SUBACK -handle_info({suback, PacketId, GrantedQos}, State) -> +handle_info({deliver, PubOrAck}, State) -> with_proto( fun(ProtoState) -> - Packet = ?SUBACK_PACKET(PacketId, GrantedQos), - emqx_protocol:send(Packet, ProtoState) - end, State); + emqx_protocol:deliver(PubOrAck, ProtoState) + end, maybe_gc(ensure_stats_timer(State))); -handle_info({deliver, Message}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:send(Message, ProtoState) - end, State); - -handle_info({redeliver, {?PUBREL, PacketId}}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:pubrel(PacketId, ProtoState) - end, State); - -handle_info(emit_stats, State) -> - {noreply, emit_stats(State), hibernate}; +handle_info(emit_stats, State = #state{proto_state = ProtoState}) -> + Stats = element(2, handle_call(stats, undefined, State)), + emqx_cm:set_client_stats(emqx_protocol:clientid(ProtoState), Stats), + {noreply, State = #state{stats_timer = undefined}, hibernate}; handle_info(timeout, State) -> shutdown(idle_timeout, State); -%% Fix issue #535 handle_info({shutdown, Error}, State) -> shutdown(Error, State); @@ -211,25 +228,25 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> shutdown(conflict, State); handle_info(activate_sock, State) -> - {noreply, run_socket(State#state{conn_state = running})}; + {noreply, run_socket(State#state{conn_state = running, limit_timer = undefined})}; handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) -> Size = iolist_size(Data), ?LOG(debug, "RECV ~p", [Data], State), emqx_metrics:inc('bytes/received', Size), - received(Data, rate_limit(Size, State#state{await_recv = false})); + Incoming = #{bytes => Size, packets => 0}, + handle_packet(Data, State#state{await_recv = false, incoming = Incoming}); handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) -> shutdown(Reason, State); handle_info({inet_reply, _Sock, ok}, State) -> - {noreply, gc(State)}; %% Tune GC + {noreply, State}; handle_info({inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); -handle_info({keepalive, start, Interval}, - State = #state{transport = Transport, socket = Sock}) -> +handle_info({keepalive, start, Interval}, State = #state{transport = Transport, socket = Sock}) -> ?LOG(debug, "Keepalive at the interval of ~p", [Interval], State), StatFun = fun() -> case Transport:getstat(Sock, [recv_oct]) of @@ -258,20 +275,18 @@ handle_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> end; handle_info(Info, State) -> - ?LOG(error, "Unexpected Info: ~p", [Info], State), + ?LOG(error, "unexpected info: ~p", [Info], State), {noreply, State}. terminate(Reason, State = #state{transport = Transport, socket = Sock, keepalive = KeepAlive, proto_state = ProtoState}) -> - ?LOG(debug, "Terminated for ~p", [Reason], State), Transport:fast_close(Sock), emqx_keepalive:cancel(KeepAlive), case {ProtoState, Reason} of - {undefined, _} -> - ok; + {undefined, _} -> ok; {_, {shutdown, Error}} -> emqx_protocol:shutdown(Error, ProtoState); {_, Reason} -> @@ -281,25 +296,29 @@ terminate(Reason, State = #state{transport = Transport, code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -%% Receive and Parse TCP Data -received(<<>>, State) -> - {noreply, gc(State)}; +%% Receive and parse TCP data +handle_packet(<<>>, State) -> + {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; -received(Bytes, State = #state{parse_state = ParseState, - proto_state = ProtoState, - idle_timeout = IdleTimeout}) -> +handle_packet(Bytes, State = #state{incoming = Incoming, + parse_state = ParseState, + proto_state = ProtoState, + idle_timeout = IdleTimeout}) -> case catch emqx_frame:parse(Bytes, ParseState) of {more, NewParseState} -> {noreply, State#state{parse_state = NewParseState}, IdleTimeout}; - {ok, Packet, Rest} -> + {ok, Packet = ?PACKET(Type), Rest} -> emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> - received(Rest, init_parse_state(State#state{proto_state = ProtoState1})); + ParseState1 = emqx_protocol:parser(ProtoState1), + handle_packet(Rest, State#state{incoming = count_packets(Type, Incoming), + proto_state = ProtoState1, + parse_state = ParseState1}); {error, Error} -> ?LOG(error, "Protocol error - ~p", [Error], State), shutdown(Error, State); @@ -312,22 +331,33 @@ received(Bytes, State = #state{parse_state = ParseState, ?LOG(error, "Framing error - ~p", [Error], State), shutdown(Error, State); {'EXIT', Reason} -> - ?LOG(error, "Parser failed for ~p", [Reason], State), - ?LOG(error, "Error data: ~p", [Bytes], State), - shutdown(parser_error, State) + ?LOG(error, "Parse failed for ~p~nError data:~p", [Reason, Bytes], State), + shutdown(parse_error, State) end. -rate_limit(_Size, State = #state{rate_limit = undefined}) -> +count_packets(?PUBLISH, Incoming = #{packets := Num}) -> + Incoming#{packets := Num + 1}; +count_packets(?SUBSCRIBE, Incoming = #{packets := Num}) -> + Incoming#{packets := Num + 1}; +count_packets(_Type, Incoming) -> + Incoming. + +ensure_rate_limit(State = #state{rate_limit = Rl, pub_limit = Pl, + incoming = #{bytes := Bytes, packets := Pkts}}) -> + ensure_rate_limit([{Pl, #state.pub_limit, Pkts}, {Rl, #state.rate_limit, Bytes}], State). + +ensure_rate_limit([], State) -> run_socket(State); -rate_limit(Size, State = #state{rate_limit = Rl}) -> - case Rl:check(Size) of - {0, Rl1} -> - run_socket(State#state{conn_state = running, rate_limit = Rl1}); - {Pause, Rl1} -> - ?LOG(warning, "Rate limiter pause for ~p", [Pause], State), - erlang:send_after(Pause, self(), activate_sock), - State#state{conn_state = blocked, rate_limit = Rl1} - end. +ensure_rate_limit([{undefined, _Pos, _Num}|Limiters], State) -> + ensure_rate_limit(Limiters, State); +ensure_rate_limit([{Rl, Pos, Num}|Limiters], State) -> + case esockd_rate_limit:check(Num, Rl) of + {0, Rl1} -> + ensure_rate_limit(Limiters, setelement(Pos, State, Rl1)); + {Pause, Rl1} -> + TRef = erlang:send_after(Pause, self(), activate_sock), + setelement(Pos, State#state{conn_state = blocked, limit_timer = TRef}, Rl1) + end. run_socket(State = #state{conn_state = blocked}) -> State; @@ -338,29 +368,21 @@ run_socket(State = #state{transport = Transport, socket = Sock}) -> State#state{await_recv = true}. with_proto(Fun, State = #state{proto_state = ProtoState}) -> - {ok, ProtoState1} = Fun(ProtoState), - {noreply, State#state{proto_state = ProtoState1}}. - -emit_stats(State = #state{proto_state = ProtoState}) -> - emit_stats(emqx_protocol:clientid(ProtoState), State). - -emit_stats(_ClientId, State = #state{enable_stats = false}) -> - State; -emit_stats(undefined, State) -> - State; -emit_stats(ClientId, State) -> - {reply, Stats, _, _} = handle_call(stats, undefined, State), - emqx_cm:set_client_stats(ClientId, Stats), - State. - -sock_stats(#state{transport = Transport, socket = Sock}) -> - case Transport:getstat(Sock, ?SOCK_STATS) of - {ok, Ss} -> Ss; - _Error -> [] + case Fun(ProtoState) of + {ok, ProtoState1} -> + {noreply, State#state{proto_state = ProtoState1}}; + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, ProtoState1} -> + shutdown(Reason, State#state{proto_state = ProtoState1}) end. -reply(Reply, State) -> - {reply, Reply, State, hibernate}. +ensure_stats_timer(State = #state{enable_stats = true, + stats_timer = undefined, + idle_timeout = IdleTimeout}) -> + State#state{stats_timer = erlang:send_after(IdleTimeout, self(), emit_stats)}; +ensure_stats_timer(State) -> + State. shutdown(Reason, State) -> stop({shutdown, Reason}, State). @@ -368,7 +390,8 @@ shutdown(Reason, State) -> stop(Reason, State) -> {stop, Reason, State}. -gc(State = #state{transport = Transport, socket = Sock}) -> - Cb = fun() -> Transport:gc(Sock), emit_stats(State) end, - emqx_gc:maybe_force_gc(#state.force_gc_count, State, Cb). +maybe_gc(State) -> + State. %% TODO:... + %%Cb = fun() -> Transport:gc(Sock), end, + %%emqx_gc:maybe_force_gc(#state.force_gc_count, State, Cb). diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 0a261db6e..7385b7116 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -121,7 +121,7 @@ parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) -> < is_bridge = (BridgeTag =:= 8), clean_start = bool(CleanStart), will_flag = bool(WillFlag), - will_qos = WillQos, + will_qos = WillQoS, will_retain = bool(WillRetain), keepalive = KeepAlive, properties = Properties, @@ -242,6 +242,9 @@ parse_packet_id(<>) -> parse_properties(Bin, Ver) when Ver =/= ?MQTT_PROTO_V5 -> {undefined, Bin}; +%% TODO: version mess? +parse_properties(<<>>, ?MQTT_PROTO_V5) -> + {#{}, <<>>}; parse_properties(<<0, Rest/binary>>, ?MQTT_PROTO_V5) -> {#{}, Rest}; parse_properties(Bin, ?MQTT_PROTO_V5) -> @@ -328,7 +331,7 @@ parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) -> {Value + Len * Multiplier, Rest}. parse_topic_filters(subscribe, Bin) -> - [{Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} + [{Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} || <> <= Bin]; parse_topic_filters(unsubscribe, Bin) -> @@ -382,7 +385,7 @@ serialize_variable(#mqtt_packet_connect{ is_bridge = IsBridge, clean_start = CleanStart, will_flag = WillFlag, - will_qos = WillQos, + will_qos = WillQoS, will_retain = WillRetain, keepalive = KeepAlive, properties = Properties, @@ -400,7 +403,7 @@ serialize_variable(#mqtt_packet_connect{ (flag(Username)):1, (flag(Password)):1, (flag(WillRetain)):1, - WillQos:2, + WillQoS:2, (flag(WillFlag)):1, (flag(CleanStart)):1, 0:1, diff --git a/src/emqx_kernel_sup.erl b/src/emqx_kernel_sup.erl index 25e96b930..40ec7cfd7 100644 --- a/src/emqx_kernel_sup.erl +++ b/src/emqx_kernel_sup.erl @@ -31,6 +31,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(M, worker) -> diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index c9697f0ca..9e8445414 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -33,7 +33,7 @@ start_listener({tcp, ListenOn, Options}) -> start_mqtt_listener('mqtt:tcp', ListenOn, Options); %% Start MQTT/TLS listener start_listener({Proto, ListenOn, Options}) when Proto == ssl; Proto == tls -> - start_mqtt_listener('mqtt:tls', ListenOn, Options); + start_mqtt_listener('mqtt:ssl', ListenOn, Options); %% Start MQTT/WS listener start_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> start_http_listener('mqtt:ws', ListenOn, Options); diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 77dbfbf82..ae8670942 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -17,45 +17,43 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([new/2, new/3, new/4, new/5]). +-export([make/2, make/3, make/4]). +-export([set_flags/2]). -export([get_flag/2, get_flag/3, set_flag/2, set_flag/3, unset_flag/2]). +-export([set_headers/2]). -export([get_header/2, get_header/3, set_header/3]). --export([get_user_property/2, get_user_property/3, set_user_property/3]). --spec(new(topic(), payload()) -> message()). -new(Topic, Payload) -> - new(undefined, Topic, Payload). +-spec(make(topic(), payload()) -> message()). +make(Topic, Payload) -> + make(undefined, Topic, Payload). --spec(new(atom() | client(), topic(), payload()) -> message()). -new(From, Topic, Payload) when is_atom(From); is_record(From, client) -> - new(From, #{qos => ?QOS0}, Topic, Payload). +-spec(make(atom() | client_id(), topic(), payload()) -> message()). +make(From, Topic, Payload) -> + make(From, ?QOS0, Topic, Payload). --spec(new(atom() | client(), message_flags(), topic(), payload()) -> message()). -new(From, Flags, Topic, Payload) when is_atom(From); is_record(From, client) -> - new(From, Flags, #{}, Topic, Payload). - --spec(new(atom() | client(), message_flags(), message_headers(), topic(), payload()) -> message()). -new(From, Flags, Headers, Topic, Payload) when is_atom(From); is_record(From, client) -> - #message{id = msgid(), - qos = ?QOS0, +-spec(make(atom() | client_id(), qos(), topic(), payload()) -> message()). +make(From, QoS, Topic, Payload) -> + #message{id = msgid(QoS), + qos = QoS, from = From, - sender = self(), - flags = Flags, - headers = Headers, + flags = #{dup => false}, topic = Topic, - properties = #{}, payload = Payload, timestamp = os:timestamp()}. -msgid() -> emqx_guid:gen(). +msgid(?QOS0) -> undefined; +msgid(_QoS) -> emqx_guid:gen(). + +set_flags(Flags, Msg = #message{flags = undefined}) when is_map(Flags) -> + Msg#message{flags = Flags}; +set_flags(New, Msg = #message{flags = Old}) when is_map(New) -> + Msg#message{flags = maps:merge(Old, New)}. -%% @doc Get flag get_flag(Flag, Msg) -> get_flag(Flag, Msg, false). get_flag(Flag, #message{flags = Flags}, Default) -> maps:get(Flag, Flags, Default). -%% @doc Set flag -spec(set_flag(message_flag(), message()) -> message()). set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) -> Msg#message{flags = maps:put(Flag, true, Flags)}. @@ -64,27 +62,22 @@ set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) -> set_flag(Flag, Val, Msg = #message{flags = Flags}) when is_atom(Flag) -> Msg#message{flags = maps:put(Flag, Val, Flags)}. -%% @doc Unset flag -spec(unset_flag(message_flag(), message()) -> message()). unset_flag(Flag, Msg = #message{flags = Flags}) -> Msg#message{flags = maps:remove(Flag, Flags)}. -%% @doc Get header +set_headers(Headers, Msg = #message{headers = undefined}) when is_map(Headers) -> + Msg#message{headers = Headers}; +set_headers(New, Msg = #message{headers = Old}) when is_map(New) -> + Msg#message{headers = maps:merge(Old, New)}. + get_header(Hdr, Msg) -> get_header(Hdr, Msg, undefined). get_header(Hdr, #message{headers = Headers}, Default) -> maps:get(Hdr, Headers, Default). -%% @doc Set header +set_header(Hdr, Val, Msg = #message{headers = undefined}) -> + Msg#message{headers = #{Hdr => Val}}; set_header(Hdr, Val, Msg = #message{headers = Headers}) -> Msg#message{headers = maps:put(Hdr, Val, Headers)}. -%% @doc Get user property -get_user_property(Key, Msg) -> - get_user_property(Key, Msg, undefined). -get_user_property(Key, #message{properties = Props}, Default) -> - maps:get(Key, Props, Default). - -set_user_property(Key, Val, Msg = #message{properties = Props}) -> - Msg#message{properties = maps:put(Key, Val, Props)}. - diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 519b96fe4..506ff2c0d 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -171,10 +171,10 @@ update_counter(Key, UpOp) -> received(Packet) -> inc('packets/received'), received1(Packet). -received1(?PUBLISH_PACKET(Qos, _PktId)) -> +received1(?PUBLISH_PACKET(QoS, _PktId)) -> inc('packets/publish/received'), inc('messages/received'), - qos_received(Qos); + qos_received(QoS); received1(?PACKET(Type)) -> received2(Type). received2(?CONNECT) -> @@ -206,15 +206,15 @@ qos_received(?QOS_2) -> %% @doc Count packets received. Will not count $SYS PUBLISH. -spec(sent(mqtt_packet()) -> ignore | non_neg_integer()). -sent(?PUBLISH_PACKET(_Qos, <<"$SYS/", _/binary>>, _, _)) -> +sent(?PUBLISH_PACKET(_QoS, <<"$SYS/", _/binary>>, _, _)) -> ignore; sent(Packet) -> inc('packets/sent'), sent1(Packet). -sent1(?PUBLISH_PACKET(Qos, _PktId)) -> +sent1(?PUBLISH_PACKET(QoS, _PktId)) -> inc('packets/publish/sent'), inc('messages/sent'), - qos_sent(Qos); + qos_sent(QoS); sent1(?PACKET(Type)) -> sent2(Type). sent2(?CONNACK) -> diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index e41bd0587..ef70dc28d 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -39,8 +39,7 @@ on_client_connected(ConnAck, Client = #client{id = ClientId, {connack, ConnAck}, {ts, emqx_time:now_secs()}]) of {ok, Payload} -> - Msg = message(qos(Env), topic(connected, ClientId), Payload), - emqx:publish(emqx_message:set_flag(sys, Msg)); + emqx:publish(message(qos(Env), topic(connected, ClientId), Payload)); {error, Reason} -> emqx_logger:error("[Presence Module] Json error: ~p", [Reason]) end, @@ -52,8 +51,7 @@ on_client_disconnected(Reason, #client{id = ClientId, username = Username}, Env) {reason, reason(Reason)}, {ts, emqx_time:now_secs()}]) of {ok, Payload} -> - Msg = message(qos(Env), topic(disconnected, ClientId), Payload), - emqx:publish(emqx_message:set_flag(sys, Msg)); + emqx_broker:publish(message(qos(Env), topic(disconnected, ClientId), Payload)); {error, Reason} -> emqx_logger:error("[Presence Module] Json error: ~p", [Reason]) end, ok. @@ -62,9 +60,9 @@ unload(_Env) -> emqx:unhook('client.connected', fun ?MODULE:on_client_connected/3), emqx:unhook('client.disconnected', fun ?MODULE:on_client_disconnected/3). -message(Qos, Topic, Payload) -> - Msg = emqx_message:make(?MODULE, Topic, iolist_to_binary(Payload)), - emqx_message:set_header(qos, Qos, Msg). +message(QoS, Topic, Payload) -> + Msg = emqx_message:make(?MODULE, QoS, Topic, iolist_to_binary(Payload)), + emqx_message:set_flags(#{sys => true}, Msg). topic(connected, ClientId) -> emqx_topic:systop(iolist_to_binary(["clients/", ClientId, "/connected"])); diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index 6db5e30f3..978b46a3b 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -34,7 +34,7 @@ load(Topics) -> on_client_connected(RC, Client = #client{id = ClientId, pid = ClientPid, username = Username}, Topics) when RC < 16#80 -> Replace = fun(Topic) -> rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) end, - TopicTable = [{Replace(Topic), Qos} || {Topic, Qos} <- Topics], + TopicTable = [{Replace(Topic), QoS} || {Topic, QoS} <- Topics], ClientPid ! {subscribe, TopicTable}, {ok, Client}; diff --git a/src/emqx_mqtt_properties.erl b/src/emqx_mqtt_properties.erl index 4634d5bdc..643156013 100644 --- a/src/emqx_mqtt_properties.erl +++ b/src/emqx_mqtt_properties.erl @@ -104,17 +104,20 @@ id('Wildcard-Subscription-Available') -> 16#28; id('Subscription-Identifier-Available') -> 16#29; id('Shared-Subscription-Available') -> 16#2A. -filter(Packet, Props) when ?CONNECT =< Packet, Packet =< ?AUTH -> - Fun = fun(Name) -> - case maps:find(id(Name), ?PROPS_TABLE) of - {ok, {Name, _Type, 'ALL'}} -> - true; - {ok, {Name, _Type, Packets}} -> - lists:member(Packet, Packets); - error -> false - end - end, - [Prop || Prop = {Name, _} <- Props, Fun(Name)]. +filter(PacketType, Props) when is_map(Props) -> + maps:from_list(filter(PacketType, maps:to_list(Props))); + +filter(PacketType, Props) when ?CONNECT =< PacketType, PacketType =< ?AUTH, is_list(Props) -> + Filter = fun(Name) -> + case maps:find(id(Name), ?PROPS_TABLE) of + {ok, {Name, _Type, 'ALL'}} -> + true; + {ok, {Name, _Type, AllowedTypes}} -> + lists:member(PacketType, AllowedTypes); + error -> false + end + end, + [Prop || Prop = {Name, _} <- Props, Filter(Name)]. validate(Props) when is_map(Props) -> lists:foreach(fun validate_prop/1, maps:to_list(Props)). diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index 43bb8654a..31811583f 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -150,7 +150,7 @@ stats(#mqueue{type = Type, q = Q, max_len = MaxLen, len = Len, dropped = Dropped %% @doc Enqueue a message. -spec(in(message(), mqueue()) -> mqueue()). -in(#message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> +in(#message{flags = #{qos := ?QOS_0}}, MQ = #mqueue{qos0 = false}) -> MQ; in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}; diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index dc88d59d7..8baa6f088 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -15,12 +15,11 @@ -module(emqx_packet). -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). -export([protocol_name/1, type_name/1]). -export([format/1]). --export([to_message/1, from_message/1]). +-export([to_message/2, from_message/2]). %% @doc Protocol name of version -spec(protocol_name(mqtt_version()) -> binary()). @@ -34,43 +33,40 @@ type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> lists:nth(Type, ?TYPE_NAMES). %% @doc From Message to Packet --spec(from_message(message()) -> mqtt_packet()). -from_message(Msg = #message{topic = Topic, payload = Payload}) -> - Qos = emqx_message:get_flag(qos, Msg, 0), +-spec(from_message(mqtt_packet_id(), message()) -> mqtt_packet()). +from_message(PacketId, Msg = #message{qos = QoS, topic = Topic, payload = Payload}) -> Dup = emqx_message:get_flag(dup, Msg, false), Retain = emqx_message:get_flag(retain, Msg, false), - PacketId = emqx_message:get_header(packet_id, Msg), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - qos = Qos, + qos = QoS, retain = Retain, dup = Dup}, variable = #mqtt_packet_publish{topic_name = Topic, - packet_id = PacketId}, + packet_id = PacketId, + properties = #{}}, %%TODO: payload = Payload}. %% @doc Message from Packet --spec(to_message(mqtt_packet()) -> message()). -to_message(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - retain = Retain, - qos = Qos, - dup = Dup}, - variable = #mqtt_packet_publish{topic_name = Topic, - packet_id = PacketId, - properties = Properties}, - payload = Payload}) -> - Flags = #{dup => Dup, retain => Retain, qos => Qos}, - Msg = emqx_message:new(undefined, Flags, #{packet_id => PacketId}, Topic, Payload), - Msg#message{properties = Properties}; +-spec(to_message(client_id(), mqtt_packet()) -> message()). +to_message(ClientId, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + retain = Retain, + qos = QoS, + dup = Dup}, + variable = #mqtt_packet_publish{topic_name = Topic, + properties = Props}, + payload = Payload}) -> + Msg = emqx_message:make(ClientId, QoS, Topic, Payload), + Msg#message{flags = #{dup => Dup, retain => Retain}, headers = Props}; -to_message(#mqtt_packet_connect{will_flag = false}) -> +to_message(_ClientId, #mqtt_packet_connect{will_flag = false}) -> undefined; -to_message(#mqtt_packet_connect{will_retain = Retain, - will_qos = Qos, - will_topic = Topic, - will_props = Props, - will_payload = Payload}) -> - Msg = emqx_message:new(undefined, #{qos => Qos, retain => Retain}, Topic, Payload), - Msg#message{properties = Props}. +to_message(ClientId, #mqtt_packet_connect{will_retain = Retain, + will_qos = QoS, + will_topic = Topic, + will_props = Props, + will_payload = Payload}) -> + Msg = emqx_message:make(ClientId, QoS, Topic, Payload), + Msg#message{flags = #{qos => QoS, retain => Retain}, headers = Props}. %% @doc Format packet -spec(format(mqtt_packet()) -> iolist()). @@ -110,8 +106,8 @@ format_variable(#mqtt_packet_connect{ Format = "ClientId=~s, ProtoName=~s, ProtoVsn=~p, CleanStart=~s, KeepAlive=~p, Username=~s, Password=~s", Args = [ClientId, ProtoName, ProtoVer, CleanStart, KeepAlive, Username, format_password(Password)], {Format1, Args1} = if - WillFlag -> { Format ++ ", Will(Q~p, R~p, Topic=~s, Payload=~p)", - Args ++ [WillQoS, i(WillRetain), WillTopic, WillPayload] }; + WillFlag -> {Format ++ ", Will(Q~p, R~p, Topic=~s, Payload=~p)", + Args ++ [WillQoS, i(WillRetain), WillTopic, WillPayload]}; true -> {Format, Args} end, io_lib:format(Format1, Args1); @@ -153,3 +149,4 @@ format_password(_Password) -> '******'. i(true) -> 1; i(false) -> 0; i(I) when is_integer(I) -> I. + diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index b2e9965e8..30b2c0294 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -18,108 +18,86 @@ -include("emqx_mqtt.hrl"). -include("emqx_misc.hrl"). --import(proplists, [get_value/2, get_value/3]). - -%% API --export([init/3, init/5, get/2, info/1, stats/1, clientid/1, client/1, session/1]). --export([subscribe/2, unsubscribe/2, pubrel/2, shutdown/2]). --export([received/2, send/2]). --export([process/2]). +-export([init/2, info/1, stats/1, clientid/1, session/1]). +-export([parser/1]). +-export([received/2, process/2, deliver/2, send/2]). +-export([shutdown/2]). -ifdef(TEST). -compile(export_all). -endif. --record(proto_stats, {enable_stats = false, recv_pkt = 0, recv_msg = 0, send_pkt = 0, send_msg = 0}). +-define(CAPABILITIES, [{max_packet_size, ?MAX_PACKET_SIZE}, + {max_clientid_len, ?MAX_CLIENTID_LEN}, + {max_topic_alias, 0}, + {max_qos_allowed, ?QOS2}, + {retain_available, true}, + {shared_subscription, true}, + {wildcard_subscription, true}]). -%% Protocol State -%% ws_initial_headers: Headers from first HTTP request for WebSocket Client. --record(proto_state, {peername, sendfun, connected = false, client_id, client_pid, - clean_start, proto_ver, proto_name, username, is_superuser, - will_msg, keepalive, keepalive_backoff, max_clientid_len, - session, stats_data, mountpoint, ws_initial_headers, - peercert_username, is_bridge, connected_at}). +-record(proto_state, {sockprops, capabilities, connected, client_id, client_pid, + clean_start, proto_ver, proto_name, username, connprops, + is_superuser, will_msg, keepalive, keepalive_backoff, session, + recv_pkt = 0, recv_msg = 0, send_pkt = 0, send_msg = 0, + mountpoint, is_bridge, connected_at}). --type(proto_state() :: #proto_state{}). - --define(INFO_KEYS, [client_id, username, clean_start, proto_ver, proto_name, - keepalive, will_msg, ws_initial_headers, mountpoint, - peercert_username, connected_at]). +-define(INFO_KEYS, [capabilities, connected, client_id, clean_start, username, proto_ver, proto_name, + keepalive, will_msg, mountpoint, is_bridge, connected_at]). -define(STATS_KEYS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(LOG(Level, Format, Args, State), - emqx_logger:Level([{client, State#proto_state.client_id}], "Client(~s@~s): " ++ Format, - [State#proto_state.client_id, esockd_net:format(State#proto_state.peername) | Args])). + emqx_logger:Level([{client, State#proto_state.client_id}], "Client(~s@~s): " ++ Format, + [State#proto_state.client_id, + esockd_net:format(maps:get(peername, State#proto_state.sockprops)) | Args])). -%% @doc Init protocol -init(Peername, SendFun, Opts) -> - Backoff = get_value(keepalive_backoff, Opts, 0.75), - EnableStats = get_value(client_enable_stats, Opts, false), - MaxLen = get_value(max_clientid_len, Opts, ?MAX_CLIENTID_LEN), - WsInitialHeaders = get_value(ws_initial_headers, Opts), - #proto_state{peername = Peername, - sendfun = SendFun, - max_clientid_len = MaxLen, - is_superuser = false, +-type(proto_state() :: #proto_state{}). + +-export_type([proto_state/0]). + +init(SockProps = #{zone := Zone, peercert := Peercert}, Options) -> + MountPoint = emqx_zone:get_env(Zone, mountpoint), + Backoff = emqx_zone:get_env(Zone, keepalive_backoff, 0.75), + Username = case proplists:get_value(peer_cert_as_username, Options) of + cn -> esockd_peercert:common_name(Peercert); + dn -> esockd_peercert:subject(Peercert); + _ -> undefined + end, + #proto_state{sockprops = SockProps, + capabilities = capabilities(Zone), + connected = false, + clean_start = true, client_pid = self(), - peercert_username = undefined, - ws_initial_headers = WsInitialHeaders, + proto_ver = ?MQTT_PROTO_V4, + proto_name = <<"MQTT">>, + username = Username, + is_superuser = false, keepalive_backoff = Backoff, - stats_data = #proto_stats{enable_stats = EnableStats}}. + mountpoint = MountPoint, + is_bridge = false, + recv_pkt = 0, + recv_msg = 0, + send_pkt = 0, + send_msg = 0}. -init(_Transport, _Sock, Peername, SendFun, Options) -> - enrich_opt(Options, init(Peername, SendFun, Options)). +capabilities(Zone) -> + Capabilities = emqx_zone:get_env(Zone, mqtt_capabilities, []), + maps:from_list(lists:ukeymerge(1, ?CAPABILITIES, Capabilities)). -enrich_opt([], State) -> - State; -enrich_opt([{mountpoint, MountPoint} | ConnOpts], State) -> - enrich_opt(ConnOpts, State#proto_state{mountpoint = MountPoint}); -%%enrich_opt([{peer_cert_as_username, N} | ConnOpts], State) -> -%% enrich_opt(ConnOpts, State#proto_state{peercert_username = peercert_username(N, Conn)}); -enrich_opt([_ | ConnOpts], State) -> - enrich_opt(ConnOpts, State). - -%%peercert_username(cn, Conn) -> -%% Conn:peer_cert_common_name(); -%%peercert_username(dn, Conn) -> -%% Conn:peer_cert_subject(). - -repl_username_with_peercert(State = #proto_state{peercert_username = undefined}) -> - State; -repl_username_with_peercert(State = #proto_state{peercert_username = PeerCert}) -> - State#proto_state{username = PeerCert}. - -%%TODO:: -get(proto_ver, #proto_state{proto_ver = Ver}) -> - Ver; -get(_, _ProtoState) -> - undefined. +parser(#proto_state{capabilities = #{max_packet_size := Size}, proto_ver = Ver}) -> + emqx_frame:initial_state(#{max_packet_size => Size, version => Ver}). info(ProtoState) -> ?record_to_proplist(proto_state, ProtoState, ?INFO_KEYS). -stats(#proto_state{stats_data = Stats}) -> - tl(?record_to_proplist(proto_stats, Stats)). +stats(ProtoState) -> + ?record_to_proplist(proto_state, ProtoState, ?STATS_KEYS). clientid(#proto_state{client_id = ClientId}) -> ClientId. -client(#proto_state{client_id = ClientId, - client_pid = ClientPid, - peername = Peername, - username = Username, - clean_start = CleanStart, - proto_ver = ProtoVer, - keepalive = Keepalive, - will_msg = WillMsg, - ws_initial_headers = _WsInitialHeaders, - mountpoint = _MountPoint, - connected_at = _Time}) -> - WillTopic = if - WillMsg =:= undefined -> undefined; - true -> WillMsg#message.topic - end, +client(#proto_state{sockprops = #{peername := Peername}, + client_id = ClientId, client_pid = ClientPid, username = Username}) -> #client{id = ClientId, pid = ClientPid, username = Username, peername = Peername}. session(#proto_state{session = Session}) -> @@ -129,117 +107,93 @@ session(#proto_state{session = Session}) -> %% A Client can only send the CONNECT Packet once over a Network Connection. -spec(received(mqtt_packet(), proto_state()) -> {ok, proto_state()} | {error, term()}). -received(Packet = ?PACKET(?CONNECT), - State = #proto_state{connected = false, stats_data = Stats}) -> - trace(recv, Packet, State), Stats1 = inc_stats(recv, ?CONNECT, Stats), - process(Packet, State#proto_state{connected = true, stats_data = Stats1}); +received(Packet = ?PACKET(?CONNECT), ProtoState = #proto_state{connected = false}) -> + trace(recv, Packet, ProtoState), + process(Packet, inc_stats(recv, ?CONNECT, ProtoState#proto_state{connected = true})); received(?PACKET(?CONNECT), State = #proto_state{connected = true}) -> {error, protocol_bad_connect, State}; %% Received other packets when CONNECT not arrived. -received(_Packet, State = #proto_state{connected = false}) -> - {error, protocol_not_connected, State}; +received(_Packet, ProtoState = #proto_state{connected = false}) -> + {error, protocol_not_connected, ProtoState}; -received(Packet = ?PACKET(Type), State = #proto_state{stats_data = Stats}) -> - trace(recv, Packet, State), Stats1 = inc_stats(recv, Type, Stats), +received(Packet = ?PACKET(Type), ProtoState) -> + trace(recv, Packet, ProtoState), case validate_packet(Packet) of ok -> - process(Packet, State#proto_state{stats_data = Stats1}); + process(Packet, inc_stats(recv, Type, ProtoState)); {error, Reason} -> - {error, Reason, State} + {error, Reason, ProtoState} end. -subscribe(RawTopicTable, ProtoState = #proto_state{client_id = ClientId, - username = Username, - session = Session}) -> - TopicTable = parse_topic_table(RawTopicTable), - case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of - {ok, TopicTable1} -> - emqx_session:subscribe(Session, TopicTable1); - {stop, _} -> - ok - end, - {ok, ProtoState}. +process(?CONNECT_PACKET(Var), ProtoState = #proto_state{username = Username0, client_pid = ClientPid}) -> + #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} = Var, + ProtoState1 = ProtoState#proto_state{proto_ver = ProtoVer, + proto_name = ProtoName, + username = if Username0 == undefined -> + Username; + true -> Username0 + end, %% TODO: fixme later. + client_id = ClientId, + clean_start = CleanStart, + keepalive = Keepalive, + connprops = ConnProps, + will_msg = willmsg(Var, ProtoState), + is_bridge = IsBridge, + connected_at = os:timestamp()}, -unsubscribe(RawTopics, ProtoState = #proto_state{client_id = ClientId, - username = Username, - session = Session}) -> - case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of - {ok, TopicTable} -> - emqx_session:unsubscribe(Session, TopicTable); - {stop, _} -> - ok - end, - {ok, ProtoState}. - -%% @doc Send PUBREL -pubrel(PacketId, State) -> send(?PUBREL_PACKET(PacketId), State). - -process(?CONNECT_PACKET(Var), State0) -> - - #mqtt_packet_connect{proto_ver = ProtoVer, - proto_name = ProtoName, - username = Username, - password = Password, - clean_start= CleanStart, - keepalive = KeepAlive, - client_id = ClientId, - is_bridge = IsBridge} = Var, - - State1 = repl_username_with_peercert( - State0#proto_state{proto_ver = ProtoVer, - proto_name = ProtoName, - username = Username, - client_id = ClientId, - clean_start = CleanStart, - keepalive = KeepAlive, - will_msg = willmsg(Var, State0), - is_bridge = IsBridge, - connected_at = os:timestamp()}), - - {ReturnCode1, SessPresent, State3} = - case validate_connect(Var, State1) of + {ReturnCode1, SessPresent, ProtoState3} = + case validate_connect(Var, ProtoState1) of ?RC_SUCCESS -> - case authenticate(client(State1), Password) of + case authenticate(client(ProtoState1), Password) of {ok, IsSuperuser} -> %% Generate clientId if null - State2 = maybe_set_clientid(State1), - - %% Start session + ProtoState2 = maybe_set_clientid(ProtoState1), + %% Open session case emqx_sm:open_session(#{clean_start => CleanStart, - client_id => clientid(State2), + client_id => clientid(ProtoState2), username => Username, - client_pid => self()}) of + client_pid => ClientPid}) of {ok, Session} -> %% TODO:... SP = true, %% TODO:... %% TODO: Register the client - emqx_cm:register_client(clientid(State2)), + emqx_cm:register_client(clientid(ProtoState2)), %%emqx_cm:reg(client(State2)), %% Start keepalive - start_keepalive(KeepAlive, State2), + start_keepalive(Keepalive, ProtoState2), %% Emit Stats - self() ! emit_stats, + %% self() ! emit_stats, %% ACCEPT - {?RC_SUCCESS, SP, State2#proto_state{session = Session, is_superuser = IsSuperuser}}; + {?RC_SUCCESS, SP, ProtoState2#proto_state{session = Session, is_superuser = IsSuperuser}}; {error, Error} -> - {stop, {shutdown, Error}, State2} + ?LOG(error, "Failed to open session: ~p", [Error], ProtoState2), + {?RC_UNSPECIFIED_ERROR, false, ProtoState2} %% TODO: the error reason??? end; {error, Reason}-> - ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], State1), - {?RC_BAD_USER_NAME_OR_PASSWORD, false, State1} + ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], ProtoState1), + {?RC_BAD_USER_NAME_OR_PASSWORD, false, ProtoState1} end; ReturnCode -> - {ReturnCode, false, State1} + {ReturnCode, false, ProtoState1} end, %% Run hooks - emqx_hooks:run('client.connected', [ReturnCode1], client(State3)), + emqx_hooks:run('client.connected', [ReturnCode1], client(ProtoState3)), %%TODO: Send Connack - send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), State3), + send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), ProtoState3), %% stop if authentication failure - stop_if_auth_failure(ReturnCode1, State3); + stop_if_auth_failure(ReturnCode1, ProtoState3); -process(Packet = ?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload), State = #proto_state{is_superuser = IsSuper}) -> +process(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId, _Payload), + State = #proto_state{is_superuser = IsSuper}) -> case IsSuper orelse allow == check_acl(publish, Topic, client(State)) of true -> publish(Packet, State); false -> ?LOG(error, "Cannot publish to ~s for ACL Deny", [Topic], State) @@ -266,36 +220,49 @@ process(?SUBSCRIBE_PACKET(PacketId, []), State) -> send(?SUBACK_PACKET(PacketId, []), State); %% TODO: refactor later... -process(?SUBSCRIBE_PACKET(PacketId, RawTopicTable), - State = #proto_state{client_id = ClientId, - username = Username, - is_superuser = IsSuperuser, - mountpoint = MountPoint, - session = Session}) -> - Client = client(State), TopicTable = parse_topic_table(RawTopicTable), +process(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), State) -> + #proto_state{client_id = ClientId, + username = Username, + is_superuser = IsSuperuser, + mountpoint = MountPoint, + session = Session} = State, + Client = client(State), + TopicFilters = parse_topic_filters(RawTopicFilters), AllowDenies = if IsSuperuser -> []; - true -> [check_acl(subscribe, Topic, Client) || {Topic, _Opts} <- TopicTable] + true -> [check_acl(subscribe, Topic, Client) || {Topic, _Opts} <- TopicFilters] end, case lists:member(deny, AllowDenies) of true -> - ?LOG(error, "Cannot SUBSCRIBE ~p for ACL Deny", [TopicTable], State), - send(?SUBACK_PACKET(PacketId, [16#80 || _ <- TopicTable]), State); + ?LOG(error, "Cannot SUBSCRIBE ~p for ACL Deny", [TopicFilters], State), + send(?SUBACK_PACKET(PacketId, [?RC_NOT_AUTHORIZED || _ <- TopicFilters]), State); false -> - case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of - {ok, TopicTable1} -> - emqx_session:subscribe(Session, PacketId, mount(replvar(MountPoint, State), TopicTable1)), + case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicFilters) of + {ok, TopicFilters1} -> + ok = emqx_session:subscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicFilters1)}), {ok, State}; {stop, _} -> {ok, State} end end; +process({subscribe, RawTopicTable}, + State = #proto_state{client_id = ClientId, + username = Username, + session = Session}) -> + TopicTable = parse_topic_filters(RawTopicTable), + case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of + {ok, TopicTable1} -> + emqx_session:subscribe(Session, TopicTable1); + {stop, _} -> ok + end, + {ok, State}; + %% Protect from empty topic list process(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> send(?UNSUBACK_PACKET(PacketId), State); -process(?UNSUBSCRIBE_PACKET(PacketId, RawTopics), +process(?UNSUBSCRIBE_PACKET(PacketId, _Properties, RawTopics), State = #proto_state{client_id = ClientId, username = Username, mountpoint = MountPoint, @@ -308,84 +275,106 @@ process(?UNSUBSCRIBE_PACKET(PacketId, RawTopics), end, send(?UNSUBACK_PACKET(PacketId), State); -process(?PACKET(?PINGREQ), State) -> - send(?PACKET(?PINGRESP), State); +process({unsubscribe, RawTopics}, State = #proto_state{client_id = ClientId, + username = Username, + session = Session}) -> + case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of + {ok, TopicTable} -> + emqx_session:unsubscribe(Session, TopicTable); + {stop, _} -> ok + end, + {ok, State}; -process(?PACKET(?DISCONNECT), State) -> +process(?PACKET(?PINGREQ), ProtoState) -> + send(?PACKET(?PINGRESP), ProtoState); + +process(?PACKET(?DISCONNECT), ProtoState) -> % Clean willmsg - {stop, normal, State#proto_state{will_msg = undefined}}. + {stop, normal, ProtoState#proto_state{will_msg = undefined}}. -publish(Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), +deliver({publish, PacketId, Msg}, + State = #proto_state{client_id = ClientId, + username = Username, + mountpoint = MountPoint, + is_bridge = IsBridge}) -> + emqx_hooks:run('message.delivered', [ClientId], + emqx_message:set_header(username, Username, Msg)), + Msg1 = unmount(MountPoint, clean_retain(IsBridge, Msg)), + send(emqx_packet:from_message(PacketId, Msg1), State); + +deliver({pubrel, PacketId}, State) -> + send(?PUBREL_PACKET(PacketId), State); + +deliver({suback, PacketId, ReasonCodes}, ProtoState) -> + send(?SUBACK_PACKET(PacketId, ReasonCodes), ProtoState); + +deliver({unsuback, PacketId, ReasonCodes}, ProtoState) -> + send(?UNSUBACK_PACKET(PacketId, ReasonCodes), ProtoState). + +publish(Packet = ?PUBLISH_PACKET(?QOS_0, PacketId), State = #proto_state{client_id = ClientId, username = Username, mountpoint = MountPoint, session = Session}) -> - Msg = emqx_packet:to_message(Packet), - Msg1 = Msg#message{from = #client{id = ClientId, username = Username}}, - emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)); + Msg = emqx_message:set_header(username, Username, + emqx_packet:to_message(ClientId, Packet)), + emqx_session:publish(Session, PacketId, mount(replvar(MountPoint, State), Msg)); -publish(Packet = ?PUBLISH_PACKET(?QOS_1, _PacketId), State) -> +publish(Packet = ?PUBLISH_PACKET(?QOS_1), State) -> with_puback(?PUBACK, Packet, State); -publish(Packet = ?PUBLISH_PACKET(?QOS_2, _PacketId), State) -> +publish(Packet = ?PUBLISH_PACKET(?QOS_2), State) -> with_puback(?PUBREC, Packet, State). -with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), +with_puback(Type, Packet = ?PUBLISH_PACKET(_QoS, PacketId), State = #proto_state{client_id = ClientId, username = Username, mountpoint = MountPoint, session = Session}) -> - %% TODO: ... - Msg = emqx_packet:to_message(Packet), - Msg1 = Msg#message{from = #client{id = ClientId, username = Username}}, - case emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)) of - ok -> - case Type of - ?PUBACK -> send(?PUBACK_PACKET(PacketId), State); - ?PUBREC -> send(?PUBREC_PACKET(PacketId), State) - end; + Msg = emqx_message:set_header(username, Username, + emqx_packet:to_message(ClientId, Packet)), + case emqx_session:publish(Session, PacketId, mount(replvar(MountPoint, State), Msg)) of {error, Error} -> - ?LOG(error, "PUBLISH ~p error: ~p", [PacketId, Error], State) + ?LOG(error, "PUBLISH ~p error: ~p", [PacketId, Error], State); + _Delivery -> send({Type, PacketId}, State) %% TODO: end. --spec(send(message() | mqtt_packet(), proto_state()) -> {ok, proto_state()}). -send(Msg, State = #proto_state{client_id = ClientId, - username = Username, - mountpoint = MountPoint, - is_bridge = IsBridge}) - when is_record(Msg, message) -> - emqx_hooks:run('message.delivered', [ClientId, Username], Msg), - send(emqx_packet:from_message(unmount(MountPoint, clean_retain(IsBridge, Msg))), State); +-spec(send({mqtt_packet_type(), mqtt_packet_id()} | + {mqtt_packet_id(), message()} | + mqtt_packet(), proto_state()) -> {ok, proto_state()}). +send({?PUBACK, PacketId}, State) -> + send(?PUBACK_PACKET(PacketId), State); -send(Packet = ?PACKET(Type), State = #proto_state{sendfun = SendFun, stats_data = Stats}) -> - trace(send, Packet, State), - emqx_metrics:sent(Packet), - SendFun(Packet), - {ok, State#proto_state{stats_data = inc_stats(send, Type, Stats)}}. +send({?PUBREC, PacketId}, State) -> + send(?PUBREC_PACKET(PacketId), State); + +send(Packet = ?PACKET(Type), ProtoState = #proto_state{proto_ver = Ver, + sockprops = #{sendfun := SendFun}}) -> + Data = emqx_frame:serialize(Packet, #{version => Ver}), + case SendFun(Data) of + ok -> emqx_metrics:sent(Packet), + trace(send, Packet, ProtoState), + {ok, inc_stats(send, Type, ProtoState)}; + {error, Reason} -> + {error, Reason} + end. trace(recv, Packet, ProtoState) -> - ?LOG(info, "RECV ~s", [emqx_packet:format(Packet)], ProtoState); + ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)], ProtoState); trace(send, Packet, ProtoState) -> - ?LOG(info, "SEND ~s", [emqx_packet:format(Packet)], ProtoState). + ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)], ProtoState). -inc_stats(_Direct, _Type, Stats = #proto_stats{enable_stats = false}) -> - Stats; - -inc_stats(recv, Type, Stats) -> - #proto_stats{recv_pkt = PktCnt, recv_msg = MsgCnt} = Stats, - inc_stats(Type, #proto_stats.recv_pkt, PktCnt, #proto_stats.recv_msg, MsgCnt, Stats); - -inc_stats(send, Type, Stats) -> - #proto_stats{send_pkt = PktCnt, send_msg = MsgCnt} = Stats, - inc_stats(Type, #proto_stats.send_pkt, PktCnt, #proto_stats.send_msg, MsgCnt, Stats). - -inc_stats(Type, PktPos, PktCnt, MsgPos, MsgCnt, Stats) -> - Stats1 = setelement(PktPos, Stats, PktCnt + 1), - case Type =:= ?PUBLISH of - true -> setelement(MsgPos, Stats1, MsgCnt + 1); - false -> Stats1 - end. +inc_stats(recv, Type, ProtoState = #proto_state{recv_pkt = PktCnt, recv_msg = MsgCnt}) -> + ProtoState#proto_state{recv_pkt = PktCnt + 1, + recv_msg = if Type =:= ?PUBLISH -> MsgCnt + 1; + true -> MsgCnt + end}; +inc_stats(send, Type, ProtoState = #proto_state{send_pkt = PktCnt, send_msg = MsgCnt}) -> + ProtoState#proto_state{send_pkt = PktCnt + 1, + send_msg = if Type =:= ?PUBLISH -> MsgCnt + 1; + true -> MsgCnt + end}. stop_if_auth_failure(?RC_SUCCESS, State) -> {ok, State}; @@ -403,19 +392,18 @@ shutdown(mnesia_conflict, _State = #proto_state{client_id = ClientId}) -> shutdown(Error, State = #proto_state{client_id = ClientId, will_msg = WillMsg}) -> ?LOG(info, "Shutdown for ~p", [Error], State), - Client = client(State), %% Auth failure not publish the will message case Error =:= auth_failure of true -> ok; - false -> send_willmsg(Client, WillMsg) + false -> send_willmsg(ClientId, WillMsg) end, - emqx_hooks:run('client.disconnected', [Error], Client), + emqx_hooks:run('client.disconnected', [Error], client(State)), emqx_cm:unregister_client(ClientId), ok. -willmsg(Packet, State = #proto_state{mountpoint = MountPoint}) +willmsg(Packet, State = #proto_state{client_id = ClientId, mountpoint = MountPoint}) when is_record(Packet, mqtt_packet_connect) -> - case emqx_packet:to_message(Packet) of + case emqx_packet:to_message(ClientId, Packet) of undefined -> undefined; Msg -> mount(replvar(MountPoint, State), Msg) end. @@ -430,10 +418,10 @@ maybe_set_clientid(State = #proto_state{client_id = NullId}) maybe_set_clientid(State) -> State. -send_willmsg(_Client, undefined) -> +send_willmsg(_ClientId, undefined) -> ignore; -send_willmsg(Client, WillMsg) -> - emqx_broker:publish(WillMsg#message{from = Client}). +send_willmsg(ClientId, WillMsg) -> + emqx_broker:publish(WillMsg#message{from = ClientId}). start_keepalive(0, _State) -> ignore; @@ -459,7 +447,7 @@ validate_protocol(#mqtt_packet_connect{proto_ver = Ver, proto_name = Name}) -> lists:member({Ver, Name}, ?PROTOCOL_NAMES). validate_clientid(#mqtt_packet_connect{client_id = ClientId}, - #proto_state{max_clientid_len = MaxLen}) + #proto_state{capabilities = #{max_clientid_len := MaxLen}}) when (byte_size(ClientId) >= 1) andalso (byte_size(ClientId) =< MaxLen) -> true; @@ -481,7 +469,7 @@ validate_clientid(#mqtt_packet_connect{proto_ver = ProtoVer, [ProtoVer, CleanStart], ProtoState), false. -validate_packet(?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload)) -> +validate_packet(?PUBLISH_PACKET(_QoS, Topic, _PacketId, _Payload)) -> case emqx_topic:validate({name, Topic}) of true -> ok; false -> {error, badtopic} @@ -501,11 +489,11 @@ validate_topics(_Type, []) -> validate_topics(Type, TopicTable = [{_Topic, _SubOpts}|_]) when Type =:= name orelse Type =:= filter -> - Valid = fun(Topic, Qos) -> - emqx_topic:validate({Type, Topic}) and validate_qos(Qos) + Valid = fun(Topic, QoS) -> + emqx_topic:validate({Type, Topic}) and validate_qos(QoS) end, case [Topic || {Topic, SubOpts} <- TopicTable, - not Valid(Topic, proplists:get_value(qos, SubOpts))] of + not Valid(Topic, SubOpts#mqtt_subopts.qos)] of [] -> ok; _ -> {error, badtopic} end; @@ -518,17 +506,16 @@ validate_topics(Type, Topics = [Topic0|_]) when is_binary(Topic0) -> validate_qos(undefined) -> true; -validate_qos(Qos) when ?IS_QOS(Qos) -> +validate_qos(QoS) when ?IS_QOS(QoS) -> true; validate_qos(_) -> false. -parse_topic_table(TopicTable) -> - lists:map(fun({Topic0, SubOpts}) -> - {Topic, Opts} = emqx_topic:parse(Topic0), - %%TODO: - {Topic, lists:usort(lists:umerge(Opts, SubOpts))} - end, TopicTable). +parse_topic_filters(TopicFilters) -> + [begin + {Topic, Opts} = emqx_topic:parse(RawTopic), + {Topic, maps:merge(?record_to_map(mqtt_subopts, SubOpts), Opts)} + end || {RawTopic, SubOpts} <- TopicFilters]. parse_topics(Topics) -> [emqx_topic:parse(Topic) || Topic <- Topics]. diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 8f6375720..85a6a63ad 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -33,10 +33,8 @@ -export([del_route/1, del_route/2, del_route/3]). -export([has_routes/1, match_routes/1, print_routes/1]). -export([topics/0]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, - code_change/3]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -type(destination() :: node() | {binary(), node()}). diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 02c7152b9..75e927a9c 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -11,29 +11,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. - +%% +%% @doc %% A stateful interaction between a Client and a Server. Some Sessions %% last only as long as the Network Connection, others can span multiple %% consecutive Network Connections between a Client and a Server. %% -%% The Session state in the Server consists of: +%% The Session State in the Server consists of: %% -%% The existence of a Session, even if the rest of the Session state is empty. +%% The existence of a Session, even if the rest of the Session State is empty. %% -%% The Client’s subscriptions. +%% The Clients subscriptions, including any Subscription Identifiers. %% %% QoS 1 and QoS 2 messages which have been sent to the Client, but have not %% been completely acknowledged. %% -%% QoS 1 and QoS 2 messages pending transmission to the Client. +%% QoS 1 and QoS 2 messages pending transmission to the Client and OPTIONALLY +%% QoS 0 messages pending transmission to the Client. %% -%% QoS 2 messages which have been received from the Client, but have not -%% been completely acknowledged. +%% QoS 2 messages which have been received from the Client, but have not been +%% completely acknowledged.The Will Message and the Will Delay Interval %% -%% Optionally, QoS 0 messages pending transmission to the Client. -%% -%% If the session is currently disconnected, the time at which the Session state -%% will be deleted. +%% If the Session is currently not connected, the time at which the Session +%% will end and Session State will be discarded. +%% @end -module(emqx_session). -behaviour(gen_server). @@ -42,26 +43,20 @@ -include("emqx_mqtt.hrl"). -include("emqx_misc.hrl"). --import(emqx_misc, [start_timer/2]). --import(proplists, [get_value/2, get_value/3]). - -%% Session API --export([start_link/1, resume/2, discard/2]). -%% Management and Monitor API --export([state/1, info/1, stats/1]). -%% PubSub API --export([subscribe/2, subscribe/3]). --export([publish/2, puback/2, pubrec/2, pubrel/2, pubcomp/2]). +-export([start_link/1, close/1]). +-export([info/1, stats/1]). +-export([resume/2, discard/2]). +-export([subscribe/2]).%%, subscribe/3]). +-export([publish/3]). +-export([puback/2, pubrec/2, pubrel/2, pubcomp/2]). -export([unsubscribe/2]). -%% gen_server Function Exports +%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --define(MQueue, emqx_mqueue). - --record(state, - { %% Clean Start Flag +-record(state, { + %% Clean Start Flag clean_start = false :: boolean(), %% Client Binding: local | remote @@ -73,21 +68,25 @@ %% Username username :: binary() | undefined, - %% Client Pid binding with session + %% Client pid binding with session client_pid :: pid(), - %% Old Client Pid that has been kickout + %% Old client Pid that has been kickout old_client_pid :: pid(), - %% Next message id of the session - next_msg_id = 1 :: mqtt_packet_id(), + %% Pending sub/unsub requests + requests :: map(), + %% Next packet id of the session + next_pkt_id = 1 :: mqtt_packet_id(), + + %% Max subscriptions max_subscriptions :: non_neg_integer(), - %% Client’s subscriptions. + %% Client’s Subscriptions. subscriptions :: map(), - %% Upgrade Qos? + %% Upgrade QoS? upgrade_qos = false :: boolean(), %% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked. @@ -106,18 +105,18 @@ %% QoS 1 and QoS 2 messages pending transmission to the Client. %% %% Optionally, QoS 0 messages pending transmission to the Client. - mqueue :: ?MQueue:mqueue(), + mqueue :: emqx_mqueue:mqueue(), %% Client -> Broker: Inflight QoS2 messages received from client and waiting for pubrel. awaiting_rel :: map(), - %% Max Packets that Awaiting PUBREL + %% Max Packets Awaiting PUBREL max_awaiting_rel = 100 :: non_neg_integer(), - %% Awaiting PUBREL timeout + %% Awaiting PUBREL Timeout await_rel_timeout = 20000 :: timeout(), - %% Awaiting PUBREL timer + %% Awaiting PUBREL Timer await_rel_timer :: reference() | undefined, %% Session Expiry Interval @@ -135,64 +134,63 @@ %% Ignore loop deliver? ignore_loop_deliver = false :: boolean(), + %% Created at created_at :: erlang:timestamp() }). -define(TIMEOUT, 60000). -define(INFO_KEYS, [clean_start, client_id, username, client_pid, binding, created_at]). -define(STATE_KEYS, [clean_start, client_id, username, binding, client_pid, old_client_pid, - next_msg_id, max_subscriptions, subscriptions, upgrade_qos, inflight, + next_pkt_id, max_subscriptions, subscriptions, upgrade_qos, inflight, max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, await_rel_timeout, expiry_interval, enable_stats, force_gc_count, created_at]). -define(LOG(Level, Format, Args, State), - emqx_logger:Level([{client, State#state.client_id}], - "Session(~s): " ++ Format, [State#state.client_id | Args])). + emqx_logger:Level([{client, State#state.client_id}], + "Session(~s): " ++ Format, [State#state.client_id | Args])). -%% @doc Start a Session --spec(start_link(map()) -> {ok, pid()} | {error, term()}). +%% @doc Start a session +-spec(start_link(Attrs :: map()) -> {ok, pid()} | {error, term()}). start_link(Attrs) -> gen_server:start_link(?MODULE, Attrs, [{hibernate_after, 10000}]). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% PubSub API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -%% @doc Subscribe topics --spec(subscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok). -subscribe(SPid, TopicTable) -> %%TODO: the ack function??... - gen_server:cast(SPid, {subscribe, self(), TopicTable, fun(_) -> ok end}). +%% for mqtt 5.0 +-spec(subscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). +subscribe(SPid, SubReq = {PacketId, Props, TopicFilters}) -> + gen_server:cast(SPid, {subscribe, self(), SubReq}). --spec(subscribe(pid(), mqtt_packet_id(), [{binary(), [emqx_topic:option()]}]) -> ok). -subscribe(SPid, PacketId, TopicTable) -> %%TODO: the ack function??... - From = self(), - AckFun = fun(GrantedQos) -> From ! {suback, PacketId, GrantedQos} end, - gen_server:cast(SPid, {subscribe, From, TopicTable, AckFun}). - -%% @doc Publish Message --spec(publish(pid(), message()) -> {ok, delivery()} | {error, term()}). -publish(_SPid, Msg = #message{qos = ?QOS_0}) -> - %% Publish QoS0 Directly +-spec(publish(pid(), mqtt_packet_id(), message()) -> {ok, delivery()} | {error, term()}). +publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_0}) -> + %% Publish QoS0 message to broker directly emqx_broker:publish(Msg); -publish(_SPid, Msg = #message{qos = ?QOS_1}) -> - %% Publish QoS1 message directly for client will PubAck automatically +publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_1}) -> + %% Publish QoS1 message to broker directly emqx_broker:publish(Msg); -publish(SPid, Msg = #message{qos = ?QOS_2}) -> - %% Publish QoS2 to Session - gen_server:call(SPid, {publish, Msg}, infinity). +publish(SPid, PacketId, Msg = #message{qos = ?QOS_2}) -> + %% Publish QoS2 message to session + gen_server:call(SPid, {publish, PacketId, Msg}, infinity). -%% @doc PubAck Message -spec(puback(pid(), mqtt_packet_id()) -> ok). puback(SPid, PacketId) -> gen_server:cast(SPid, {puback, PacketId}). +puback(SPid, PacketId, {ReasonCode, Props}) -> + gen_server:cast(SPid, {puback, PacketId, {ReasonCode, Props}}). + -spec(pubrec(pid(), mqtt_packet_id()) -> ok). pubrec(SPid, PacketId) -> gen_server:cast(SPid, {pubrec, PacketId}). +pubrec(SPid, PacketId, {ReasonCode, Props}) -> + gen_server:cast(SPid, {pubrec, PacketId, {ReasonCode, Props}}). + -spec(pubrel(pid(), mqtt_packet_id()) -> ok). pubrel(SPid, PacketId) -> gen_server:cast(SPid, {pubrel, PacketId}). @@ -201,20 +199,14 @@ pubrel(SPid, PacketId) -> pubcomp(SPid, PacketId) -> gen_server:cast(SPid, {pubcomp, PacketId}). -%% @doc Unsubscribe the topics --spec(unsubscribe(pid(), [{binary(), [suboption()]}]) -> ok). -unsubscribe(SPid, TopicTable) -> - gen_server:cast(SPid, {unsubscribe, self(), TopicTable}). +-spec(unsubscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). +unsubscribe(SPid, UnsubReq = {PacketId, Properties, TopicFilters}) -> + gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). -%% @doc Resume the session -spec(resume(pid(), pid()) -> ok). resume(SPid, ClientPid) -> gen_server:cast(SPid, {resume, ClientPid}). -%% @doc Get session state -state(SPid) when is_pid(SPid) -> - gen_server:call(SPid, state). - %% @doc Get session info -spec(info(pid() | #state{}) -> list(tuple())). info(SPid) when is_pid(SPid) -> @@ -239,9 +231,9 @@ stats(#state{max_subscriptions = MaxSubscriptions, {subscriptions, maps:size(Subscriptions)}, {max_inflight, MaxInflight}, {inflight_len, emqx_inflight:size(Inflight)}, - {max_mqueue, ?MQueue:max_len(MQueue)}, - {mqueue_len, ?MQueue:len(MQueue)}, - {mqueue_dropped, ?MQueue:dropped(MQueue)}, + {max_mqueue, emqx_mqueue:max_len(MQueue)}, + {mqueue_len, emqx_mqueue:len(MQueue)}, + {mqueue_dropped, emqx_mqueue:dropped(MQueue)}, {max_awaiting_rel, MaxAwaitingRel}, {awaiting_rel_len, maps:size(AwaitingRel)}, {deliver_msg, get(deliver_msg)}, @@ -250,41 +242,42 @@ stats(#state{max_subscriptions = MaxSubscriptions, %% @doc Discard the session -spec(discard(pid(), client_id()) -> ok). discard(SPid, ClientId) -> - gen_server:call(SPid, {discard, ClientId}). + gen_server:call(SPid, {discard, ClientId}, infinity). -%%-------------------------------------------------------------------- -%% gen_server Callbacks -%%-------------------------------------------------------------------- +-spec(close(pid()) -> ok). +close(SPid) -> + gen_server:call(SPid, close, infinity). -init(#{clean_start := CleanStart, - client_id := ClientId, - username := Username, - client_pid := ClientPid}) -> +%%------------------------------------------------------------------------------ +%% gen_server callbacks +%%------------------------------------------------------------------------------ + +init(#{clean_start := CleanStart, client_id := ClientId, username := Username, client_pid := ClientPid}) -> process_flag(trap_exit, true), true = link(ClientPid), init_stats([deliver_msg, enqueue_msg]), {ok, Env} = emqx_config:get_env(session), {ok, QEnv} = emqx_config:get_env(mqueue), - MaxInflight = get_value(max_inflight, Env, 0), - EnableStats = get_value(enable_stats, Env, false), - IgnoreLoopDeliver = get_value(ignore_loop_deliver, Env, false), - MQueue = ?MQueue:new(ClientId, QEnv), + MaxInflight = proplists:get_value(max_inflight, Env, 0), + EnableStats = proplists:get_value(enable_stats, Env, false), + IgnoreLoopDeliver = proplists:get_value(ignore_loop_deliver, Env, false), + MQueue = emqx_mqueue:new(ClientId, QEnv), State = #state{clean_start = CleanStart, binding = binding(ClientPid), client_id = ClientId, client_pid = ClientPid, username = Username, subscriptions = #{}, - max_subscriptions = get_value(max_subscriptions, Env, 0), - upgrade_qos = get_value(upgrade_qos, Env, false), + max_subscriptions = proplists:get_value(max_subscriptions, Env, 0), + upgrade_qos = proplists:get_value(upgrade_qos, Env, false), max_inflight = MaxInflight, inflight = emqx_inflight:new(MaxInflight), mqueue = MQueue, - retry_interval = get_value(retry_interval, Env), + retry_interval = proplists:get_value(retry_interval, Env), awaiting_rel = #{}, - await_rel_timeout = get_value(await_rel_timeout, Env), - max_awaiting_rel = get_value(max_awaiting_rel, Env), - expiry_interval = get_value(expiry_interval, Env), + await_rel_timeout = proplists:get_value(await_rel_timeout, Env), + max_awaiting_rel = proplists:get_value(max_awaiting_rel, Env), + expiry_interval = proplists:get_value(expiry_interval, Env), enable_stats = EnableStats, ignore_loop_deliver = IgnoreLoopDeliver, created_at = os:timestamp()}, @@ -307,19 +300,19 @@ handle_call({discard, ClientPid}, _From, State = #state{client_pid = OldClientPi ?LOG(warning, " ~p kickout ~p", [ClientPid, OldClientPid], State), {stop, {shutdown, conflict}, ok, State}; -handle_call({publish, Msg = #message{qos = ?QOS_2, headers = #{packet_id := PacketId}}}, _From, +handle_call({publish, PacketId, Msg = #message{qos = ?QOS_2}}, _From, State = #state{awaiting_rel = AwaitingRel, await_rel_timer = Timer, await_rel_timeout = Timeout}) -> case is_awaiting_full(State) of false -> State1 = case Timer == undefined of - true -> State#state{await_rel_timer = start_timer(Timeout, check_awaiting_rel)}; + true -> State#state{await_rel_timer = emqx_misc:start_timer(Timeout, check_awaiting_rel)}; false -> State end, reply(ok, State1#state{awaiting_rel = maps:put(PacketId, Msg, AwaitingRel)}); true -> - ?LOG(warning, "Dropped Qos2 Message for too many awaiting_rel: ~p", [Msg], State), + ?LOG(warning, "Dropped QoS2 Message for too many awaiting_rel: ~p", [Msg], State), emqx_metrics:inc('messages/qos2/dropped'), reply({error, dropped}, State) end; @@ -330,62 +323,53 @@ handle_call(info, _From, State) -> handle_call(stats, _From, State) -> reply(stats(State), State); -handle_call(state, _From, State) -> - reply(?record_to_proplist(state, State, ?STATE_KEYS), State); +handle_call(close, _From, State) -> + {stop, normal, State}; handle_call(Req, _From, State) -> emqx_logger:error("[Session] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({subscribe, From, TopicTable, AckFun}, +handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> - ?LOG(info, "Subscribe ~p", [TopicTable], State), - {GrantedQos, Subscriptions1} = - lists:foldl(fun({Topic, Opts}, {QosAcc, SubMap}) -> - NewQos = get_value(qos, Opts), - SubMap1 = - case maps:find(Topic, SubMap) of - {ok, NewQos} -> - ?LOG(warning, "Duplicated subscribe: ~s, qos = ~w", [Topic, NewQos], State), - SubMap; - {ok, OldQos} -> - %% TODO:.... - emqx_broker:set_subopts(Topic, ClientId, [{qos, NewQos}]), - emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), - ?LOG(warning, "Duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, NewQos], State), - maps:put(Topic, NewQos, SubMap); - error -> - %% TODO:.... - emqx:subscribe(Topic, ClientId, Opts), - emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), - maps:put(Topic, NewQos, SubMap) - end, - {[NewQos|QosAcc], SubMap1} - end, {[], Subscriptions}, TopicTable), - AckFun(lists:reverse(GrantedQos)), - {noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate}; + ?LOG(info, "Subscribe ~p", [TopicFilters], State), + {ReasonCodes, Subscriptions1} = + lists:foldl(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) -> + {[QoS|RcAcc], + case maps:find(Topic, SubMap) of + {ok, SubOpts} -> + ?LOG(warning, "Duplicated subscribe: ~s, subopts: ~p", [Topic, SubOpts], State), + SubMap; + {ok, OldOpts} -> + emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), + emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, SubOpts}), + ?LOG(warning, "Duplicated subscribe ~s, old_opts: ~p, new_opts: ~p", [Topic, OldOpts, SubOpts], State), + maps:put(Topic, SubOpts, SubMap); + error -> + emqx_broker:subscribe(Topic, ClientId, SubOpts), + emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, SubOpts}), + maps:put(Topic, SubOpts, SubMap) + end} + end, {[], Subscriptions}, TopicFilters), + suback(From, PacketId, lists:reverse(ReasonCodes)), + {noreply, emit_stats(State#state{subscriptions = Subscriptions1})}; -handle_cast({unsubscribe, From, TopicTable}, - State = #state{client_id = ClientId, - username = Username, - subscriptions = Subscriptions}) -> - ?LOG(info, "Unsubscribe ~p", [TopicTable], State), - Subscriptions1 = - lists:foldl(fun({Topic, Opts}, SubMap) -> - Fastlane = lists:member(fastlane, Opts), +handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, + State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> + ?LOG(info, "Unsubscribe ~p", [TopicFilters], State), + {ReasonCodes, Subscriptions1} = + lists:foldl(fun(Topic, {RcAcc, SubMap}) -> case maps:find(Topic, SubMap) of - {ok, _Qos} -> - case Fastlane of - true -> emqx:unsubscribe(Topic, From); - false -> emqx:unsubscribe(Topic, ClientId) - end, - emqx_hooks:run('session.unsubscribed', [ClientId, Username], {Topic, Opts}), - maps:remove(Topic, SubMap); + {ok, SubOpts} -> + emqx_broker:unsubscribe(Topic, ClientId), + emqx_hooks:run('session.unsubscribed', [ClientId, Username], {Topic, SubOpts}), + {[?RC_SUCCESS|RcAcc], maps:remove(Topic, SubMap)}; error -> - SubMap + {[?RC_NO_SUBSCRIPTION_EXISTED|RcAcc], SubMap} end - end, Subscriptions, TopicTable), - {noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate}; + end, {[], Subscriptions}, TopicFilters), + unsuback(From, PacketId, lists:reverse(ReasonCodes)), + {noreply, emit_stats(State#state{subscriptions = Subscriptions1})}; %% PUBACK: handle_cast({puback, PacketId}, State = #state{inflight = Inflight}) -> @@ -490,12 +474,12 @@ handle_cast(Msg, State) -> {noreply, State}. %% Ignore Messages delivered by self -handle_info({dispatch, _Topic, #message{from = {ClientId, _}}}, +handle_info({dispatch, _Topic, #message{from = ClientId}}, State = #state{client_id = ClientId, ignore_loop_deliver = true}) -> {noreply, State}; %% Dispatch Message -handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, message) -> +handle_info({dispatch, Topic, Msg}, State) -> {noreply, gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))}; %% Do nothing if the client has been disconnected. @@ -521,7 +505,7 @@ handle_info({'EXIT', ClientPid, Reason}, client_pid = ClientPid, expiry_interval = Interval}) -> ?LOG(info, "Client ~p EXIT for ~p", [ClientPid, Reason], State), - ExpireTimer = start_timer(Interval, expired), + ExpireTimer = emqx_misc:start_timer(Interval, expired), State1 = State#state{client_pid = undefined, expiry_timer = ExpireTimer}, {noreply, emit_stats(State1), hibernate}; @@ -543,12 +527,25 @@ terminate(Reason, #state{client_id = ClientId, username = Username}) -> emqx_hooks:run('session.terminated', [ClientId, Username, Reason]), emqx_sm:unregister_session(ClientId). -code_change(_OldVsn, Session, _Extra) -> - {ok, Session}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +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 client -%%-------------------------------------------------------------------- kick(_ClientId, undefined, _Pid) -> ignore; @@ -560,32 +557,32 @@ kick(ClientId, OldPid, Pid) -> %% Clean noproc receive {'EXIT', OldPid, _} -> ok after 0 -> ok end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Replay or Retry Delivery -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -%% Redeliver at once if Force is true +%% Redeliver at once if force is true retry_delivery(Force, State = #state{inflight = Inflight}) -> case emqx_inflight:is_empty(Inflight) of - true -> State; - false -> Msgs = lists:sort(sortfun(inflight), - emqx_inflight:values(Inflight)), - retry_delivery(Force, Msgs, os:timestamp(), State) + true -> + State; + false -> + Msgs = lists:sort(sortfun(inflight), emqx_inflight:values(Inflight)), + retry_delivery(Force, Msgs, os:timestamp(), State) end. retry_delivery(_Force, [], _Now, State = #state{retry_interval = Interval}) -> - State#state{retry_timer = start_timer(Interval, retry_delivery)}; + State#state{retry_timer = emqx_misc:start_timer(Interval, retry_delivery)}; -retry_delivery(Force, [{Type, Msg, Ts} | Msgs], Now, - State = #state{inflight = Inflight, - retry_interval = Interval}) -> +retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now, + State = #state{inflight = Inflight, retry_interval = Interval}) -> Diff = timer:now_diff(Now, Ts) div 1000, %% micro -> ms if Force orelse (Diff >= Interval) -> - case {Type, Msg} of - {publish, Msg = #message{headers = #{packet_id := PacketId}}} -> - redeliver(Msg, State), - Inflight1 = emqx_inflight:update(PacketId, {publish, Msg, Now}, Inflight), + case {Type, Msg0} of + {publish, {PacketId, Msg}} -> + redeliver({PacketId, Msg}, State), + Inflight1 = emqx_inflight:update(PacketId, {publish, {PacketId, Msg}, Now}, Inflight), retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}); {pubrel, PacketId} -> redeliver({pubrel, PacketId}, State), @@ -593,12 +590,12 @@ retry_delivery(Force, [{Type, Msg, Ts} | Msgs], Now, retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}) end; true -> - State#state{retry_timer = start_timer(Interval - Diff, retry_delivery)} + State#state{retry_timer = emqx_misc:start_timer(Interval - Diff, retry_delivery)} end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Expire Awaiting Rel -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) -> case maps:size(AwaitingRel) of @@ -619,12 +616,12 @@ expire_awaiting_rel([{PacketId, Msg = #message{timestamp = TS}} | Msgs], emqx_metrics:inc('messages/qos2/dropped'), expire_awaiting_rel(Msgs, Now, State#state{awaiting_rel = maps:remove(PacketId, AwaitingRel)}); Diff -> - State#state{await_rel_timer = start_timer(Timeout - Diff, check_awaiting_rel)} + State#state{await_rel_timer = emqx_misc:start_timer(Timeout - Diff, check_awaiting_rel)} end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Sort Inflight, AwaitingRel -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ sortfun(inflight) -> fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end; @@ -635,18 +632,18 @@ sortfun(awaiting_rel) -> Ts1 < Ts2 end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Check awaiting rel -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ is_awaiting_full(#state{max_awaiting_rel = 0}) -> false; is_awaiting_full(#state{awaiting_rel = AwaitingRel, max_awaiting_rel = MaxLen}) -> maps:size(AwaitingRel) >= MaxLen. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Dispatch Messages -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Enqueue message if the client has been disconnected dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) -> @@ -657,53 +654,50 @@ dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) -> %% Deliver qos0 message directly to client dispatch(Msg = #message{qos = ?QOS0}, State) -> - deliver(Msg, State), State; + deliver(undefined, Msg, State), State; -dispatch(Msg = #message{qos = QoS}, - State = #state{next_msg_id = MsgId, inflight = Inflight}) +dispatch(Msg = #message{qos = QoS}, State = #state{next_pkt_id = PacketId, inflight = Inflight}) when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 -> case emqx_inflight:is_full(Inflight) of true -> enqueue_msg(Msg, State); false -> - Msg1 = emqx_message:set_header(packet_id, MsgId, Msg), - deliver(Msg1, State), - await(Msg1, next_msg_id(State)) + deliver(PacketId, Msg, State), + await(PacketId, Msg, next_pkt_id(State)) end. enqueue_msg(Msg, State = #state{mqueue = Q}) -> inc_stats(enqueue_msg), - State#state{mqueue = ?MQueue:in(Msg, Q)}. + State#state{mqueue = emqx_mqueue:in(Msg, Q)}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Deliver -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -redeliver(Msg = #message{qos = QoS}, State) -> - deliver(if QoS =:= ?QOS2 -> Msg; true -> emqx_message:set_flag(dup, Msg) end, State); +redeliver({PacketId, Msg = #message{qos = QoS}}, State) -> + deliver(PacketId, if QoS =:= ?QOS2 -> Msg; true -> emqx_message:set_flag(dup, Msg) end, State); redeliver({pubrel, PacketId}, #state{client_pid = Pid}) -> - Pid ! {redeliver, {?PUBREL, PacketId}}. + Pid ! {deliver, {pubrel, PacketId}}. -deliver(Msg, #state{client_pid = Pid, binding = local}) -> - inc_stats(deliver_msg), Pid ! {deliver, Msg}; -deliver(Msg, #state{client_pid = Pid, binding = remote}) -> - inc_stats(deliver_msg), emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, Msg}]). +deliver(PacketId, Msg, #state{client_pid = Pid, binding = local}) -> + inc_stats(deliver_msg), Pid ! {deliver, {publish, PacketId, Msg}}; +deliver(PacketId, Msg, #state{client_pid = Pid, binding = remote}) -> + inc_stats(deliver_msg), emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, PacketId, Msg}]). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Awaiting ACK for QoS1/QoS2 Messages -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -await(Msg = #message{headers = #{packet_id := PacketId}}, - State = #state{inflight = Inflight, - retry_timer = RetryTimer, - retry_interval = Interval}) -> +await(PacketId, Msg, State = #state{inflight = Inflight, + retry_timer = RetryTimer, + retry_interval = Interval}) -> %% Start retry timer if the Inflight is still empty State1 = case RetryTimer == undefined of - true -> State#state{retry_timer = start_timer(Interval, retry_delivery)}; + true -> State#state{retry_timer = emqx_misc:start_timer(Interval, retry_delivery)}; false -> State end, - State1#state{inflight = emqx_inflight:insert(PacketId, {publish, Msg, os:timestamp()}, Inflight)}. + State1#state{inflight = emqx_inflight:insert(PacketId, {publish, {PacketId, Msg}, os:timestamp()}, Inflight)}. acked(puback, PacketId, State = #state{client_id = ClientId, username = Username, @@ -735,9 +729,9 @@ acked(pubrec, PacketId, State = #state{client_id = ClientId, 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{client_pid = undefined}) -> @@ -750,7 +744,7 @@ dequeue(State = #state{inflight = Inflight}) -> end. dequeue2(State = #state{mqueue = Q}) -> - case ?MQueue:out(Q) of + case emqx_mqueue:out(Q) of {empty, _Q} -> State; {{value, Msg}, Q1} -> @@ -758,9 +752,8 @@ dequeue2(State = #state{mqueue = Q}) -> dequeue(dispatch(Msg, State#state{mqueue = Q1})) end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Tune QoS -%%-------------------------------------------------------------------- tune_qos(Topic, Msg = #message{qos = PubQoS}, #state{subscriptions = SubMap, upgrade_qos = UpgradeQoS}) -> @@ -775,26 +768,23 @@ tune_qos(Topic, Msg = #message{qos = PubQoS}, Msg end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Reset Dup -%%-------------------------------------------------------------------- reset_dup(Msg) -> emqx_message:unset_flag(dup, Msg). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Next Msg Id -%%-------------------------------------------------------------------- -next_msg_id(State = #state{next_msg_id = 16#FFFF}) -> - State#state{next_msg_id = 1}; +next_pkt_id(State = #state{next_pkt_id = 16#FFFF}) -> + State#state{next_pkt_id = 1}; -next_msg_id(State = #state{next_msg_id = Id}) -> - State#state{next_msg_id = Id + 1}. +next_pkt_id(State = #state{next_pkt_id = Id}) -> + State#state{next_pkt_id = Id + 1}. %%-------------------------------------------------------------------- %% Emit session stats -%%-------------------------------------------------------------------- emit_stats(State = #state{enable_stats = false}) -> State; @@ -806,7 +796,6 @@ inc_stats(Key) -> put(Key, get(Key) + 1). %%-------------------------------------------------------------------- %% Helper functions -%%-------------------------------------------------------------------- reply(Reply, State) -> {reply, Reply, State, hibernate}. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index de16a2b0f..00f4bff3d 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -46,7 +46,7 @@ start_link() -> gen_server:start_link({local, ?SM}, ?MODULE, [], []). %% @doc Open a session. --spec(open_session(map()) -> {ok, pid()} | {error, term()}). +-spec(open_session(map()) -> {ok, pid(), boolean()} | {error, term()}). open_session(Attrs = #{clean_start := true, client_id := ClientId, client_pid := ClientPid}) -> diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index 4a9be13c0..701b9ae4e 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -22,7 +22,8 @@ -export([is_enabled/0]). -export([register_session/1, lookup_session/1, unregister_session/1]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -define(REGISTRY, ?MODULE). -define(TAB, emqx_session_registry). diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl index 667ef0f1a..57dc41703 100644 --- a/src/emqx_sys.erl +++ b/src/emqx_sys.erl @@ -171,6 +171,8 @@ publish(metrics, Metrics) -> safe_publish(Topic, Payload) -> safe_publish(Topic, #{}, Payload). safe_publish(Topic, Flags, Payload) -> - Flags1 = maps:merge(#{sys => true}, Flags), - emqx_broker:safe_publish(emqx_message:new(?SYS, Flags1, Topic, iolist_to_binary(Payload))). + emqx_broker:safe_publish( + emqx_message:set_flags( + maps:merge(#{sys => true}, Flags), + emqx_message:make(?SYS, Topic, iolist_to_binary(Payload)))). diff --git a/src/emqx_sys_mon.erl b/src/emqx_sys_mon.erl index 435dfaaee..b42d96aa4 100644 --- a/src/emqx_sys_mon.erl +++ b/src/emqx_sys_mon.erl @@ -43,7 +43,7 @@ init([Opts]) -> {ok, start_timer(#state{events = []})}. start_timer(State) -> - State#state{timer = emqx_misc:start_timer(timer:seconds(2), tick)}. + State#state{timer = emqx_misc:start_timer(timer:seconds(2), reset)}. parse_opt(Opts) -> parse_opt(Opts, []). @@ -126,7 +126,7 @@ handle_info({monitor, SusPid, busy_dist_port, Port}, State) -> safe_publish(busy_dist_port, WarnMsg) end, State); -handle_info(reset, State) -> +handle_info({timeout, _Ref, reset}, State) -> {noreply, State#state{events = []}, hibernate}; handle_info(Info, State) -> @@ -158,5 +158,5 @@ safe_publish(Event, WarnMsg) -> emqx_broker:safe_publish(sysmon_msg(Topic, iolist_to_binary(WarnMsg))). sysmon_msg(Topic, Payload) -> - emqx_message:new(?SYSMON, #{sys => true}, Topic, Payload). + emqx_message:make(?SYSMON, #{sys => true}, Topic, Payload). diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index 43ab8e0df..3bf42f6ac 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -25,10 +25,9 @@ -type(word() :: '' | '+' | '#' | binary()). -type(words() :: list(word())). --type(option() :: {qos, mqtt_qos()} | {share, '$queue' | binary()}). -type(triple() :: {root | binary(), word(), binary()}). --export_type([option/0, word/0, triple/0]). +-export_type([word/0, triple/0]). -define(MAX_TOPIC_LEN, 4096). @@ -163,20 +162,21 @@ join(Words) -> end, {true, <<>>}, [bin(W) || W <- Words]), Bin. --spec(parse(topic()) -> {topic(), [option()]}). +-spec(parse(topic()) -> {topic(), #{}}). parse(Topic) when is_binary(Topic) -> - parse(Topic, []). + parse(Topic, #{}). parse(Topic = <<"$queue/", Topic1/binary>>, Options) -> - case lists:keyfind(share, 1, Options) of - {share, _} -> error({invalid_topic, Topic}); - false -> parse(Topic1, [{share, '$queue'} | Options]) + case maps:find(share, Options) of + {ok, _} -> error({invalid_topic, Topic}); + error -> parse(Topic1, maps:put(share, '$queue', Options)) end; parse(Topic = <<"$share/", Topic1/binary>>, Options) -> - case lists:keyfind(share, 1, Options) of - {share, _} -> error({invalid_topic, Topic}); - false -> [Group, Topic2] = binary:split(Topic1, <<"/">>), - {Topic2, [{share, Group} | Options]} + case maps:find(share, Options) of + {ok, _} -> error({invalid_topic, Topic}); + error -> [Group, Topic2] = binary:split(Topic1, <<"/">>), + {Topic2, maps:put(share, Group, Options)} end; -parse(Topic, Options) -> {Topic, Options}. +parse(Topic, Options) -> + {Topic, Options}. diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index 0f16c858c..65e6f6378 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -19,6 +19,7 @@ -include("emqx.hrl"). -export([start_link/0]). +-export([trace/2]). -export([start_trace/2, lookup_traces/0, stop_trace/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, @@ -31,14 +32,17 @@ -define(TRACER, ?MODULE). -define(OPTIONS, [{formatter_config, [time, " [",severity,"] ", message, "\n"]}]). -%%------------------------------------------------------------------------------ -%% Start the tracer -%%------------------------------------------------------------------------------ - -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 + ignore; +trace(publish, #message{from = From, topic = Topic, payload = Payload}) + when is_binary(From); is_atom(From) -> + emqx_logger:info([{client, From}, {topic, Topic}], "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). + %%------------------------------------------------------------------------------ %% Start/Stop trace %%------------------------------------------------------------------------------ diff --git a/src/emqx_vm.erl b/src/emqx_vm.erl index c367ddbc4..0f3a4eaa4 100644 --- a/src/emqx_vm.erl +++ b/src/emqx_vm.erl @@ -17,21 +17,15 @@ -module(emqx_vm). -export([schedulers/0]). - -export([microsecs/0]). - -export([loads/0, get_system_info/0, get_system_info/1, mem_info/0, scheduler_usage/1]). - -export([get_memory/0]). - -export([get_process_list/0, get_process_info/0, get_process_info/1, get_process_gc/0, get_process_gc/1, get_process_group_leader_info/1, get_process_limit/0]). - -export([get_ets_list/0, get_ets_info/0, get_ets_info/1, get_ets_object/0, get_ets_object/1]). - -export([get_port_types/0, get_port_info/0, get_port_info/1]). -define(UTIL_ALLOCATORS, [temp_alloc, @@ -204,13 +198,13 @@ mem_info() -> [{total_memory, proplists:get_value(total_memory, Dataset)}, {used_memory, proplists:get_value(total_memory, Dataset) - proplists:get_value(free_memory, Dataset)}]. -ftos(F) -> +ftos(F) -> [S] = io_lib:format("~.2f", [F]), S. -%%%% erlang vm scheduler_usage fun copied from recon +%%%% erlang vm scheduler_usage fun copied from recon scheduler_usage(Interval) when is_integer(Interval) -> %% We start and stop the scheduler_wall_time system flag - %% if it wasn't in place already. Usually setting the flag + %% if it wasn't in place already. Usually setting the flag %% should have a CPU impact(make it higher) only when under low usage. FormerFlag = erlang:system_flag(scheduler_wall_time, true), First = erlang:statistics(scheduler_wall_time), @@ -300,7 +294,7 @@ get_process_group_leader_info(LeaderPid) when is_pid(LeaderPid) -> [{Key, Value}|| {Key, Value} <- process_info(LeaderPid), lists:member(Key, ?PROCESS_INFO)]. get_process_limit() -> - erlang:system_info(process_limit). + erlang:system_info(process_limit). get_ets_list() -> ets:all(). diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 83a55c21e..dadc2cc57 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_ws_connection). @@ -157,8 +155,8 @@ handle_cast({received, Packet}, State = #wsclient_state{proto_state = ProtoState end; handle_cast(Msg, State) -> - ?WSLOG(error, "Unexpected Msg: ~p", [Msg], State), - {noreply, State, hibernate}. + ?WSLOG(error, "unexpected msg: ~p", [Msg], State), + {noreply, State}. handle_info({subscribe, TopicTable}, State) -> with_proto( @@ -172,10 +170,17 @@ handle_info({unsubscribe, Topics}, State) -> emqx_protocol:unsubscribe(Topics, ProtoState) end, State); -handle_info({suback, PacketId, GrantedQos}, State) -> +handle_info({suback, PacketId, ReasonCodes}, State) -> with_proto( fun(ProtoState) -> - Packet = ?SUBACK_PACKET(PacketId, GrantedQos), + Packet = ?SUBACK_PACKET(PacketId, ReasonCodes), + emqx_protocol:send(Packet, ProtoState) + end, State); + +handle_info({unsuback, PacketId, ReasonCodes}, State) -> + with_proto( + fun(ProtoState) -> + Packet = ?UNSUBACK_PACKET(PacketId, ReasonCodes), emqx_protocol:send(Packet, ProtoState) end, State); diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl new file mode 100644 index 000000000..0d874a38b --- /dev/null +++ b/src/emqx_zone.erl @@ -0,0 +1,78 @@ +%% Copyright (c) 2018 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_zone). + +-behaviour(gen_server). + +-export([start_link/0]). +-export([get_env/2, get_env/3]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). + +-record(state, {timer}). +-define(TAB, ?MODULE). +-define(SERVER, ?MODULE). + +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +get_env(Zone, Par) -> + get_env(Zone, Par, undefined). + +get_env(Zone, Par, Def) -> + try ets:lookup_element(?TAB, {Zone, Par}, 2) catch error:badarg -> Def end. + +%%------------------------------------------------------------------------------ +%% gen_server callbacks +%%------------------------------------------------------------------------------ + +init([]) -> + _ = emqx_tables:new(?TAB, [set, {read_concurrency, true}]), + {ok, element(2, handle_info(reload, #state{}))}. + +handle_call(Req, _From, State) -> + emqx_logger:error("[Zone] unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Msg, State) -> + emqx_logger:error("[Zone] unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(reload, State) -> + lists:foreach( + fun({Zone, Options}) -> + [ets:insert(?TAB, {{Zone, Par}, Val}) || {Par, Val} <- Options] + end, emqx_config:get_env(zones, [])), + {noreply, ensure_reload_timer(State), hibernate}; + +handle_info(Info, State) -> + emqx_logger:error("[Zone] unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +ensure_reload_timer(State) -> + State#state{timer = erlang:send_after(5000, self(), reload)}. + From 645c971a07210f4a38feb01ea5fb4fe198e43388 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 6 Aug 2018 17:09:14 +0800 Subject: [PATCH 075/520] Fix QoS tuning --- src/emqx_session.erl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 75e927a9c..03bcae3f2 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -758,14 +758,12 @@ dequeue2(State = #state{mqueue = Q}) -> tune_qos(Topic, Msg = #message{qos = PubQoS}, #state{subscriptions = SubMap, upgrade_qos = UpgradeQoS}) -> case maps:find(Topic, SubMap) of - {ok, SubQoS} when UpgradeQoS andalso (SubQoS > PubQoS) -> + {ok, #{qos := SubQoS}} when UpgradeQoS andalso (SubQoS > PubQoS) -> Msg#message{qos = SubQoS}; - {ok, SubQoS} when (not UpgradeQoS) andalso (SubQoS < PubQoS) -> + {ok, #{qos := SubQoS}} when (not UpgradeQoS) andalso (SubQoS < PubQoS) -> Msg#message{qos = SubQoS}; - {ok, _} -> - Msg; - error -> - Msg + {ok, _} -> Msg; + error -> Msg end. %%------------------------------------------------------------------------------ From 79481db659cb752efe33eb9b3f82e82389a5d36e Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Mon, 6 Aug 2018 23:45:27 +0800 Subject: [PATCH 076/520] Enhance base62 encode/decode Funs --- src/emqx_base62.erl | 109 ++++++++++++++++++++++++++----------- src/emqx_guid.erl | 5 +- test/emqx_base62_SUITE.erl | 12 ++-- 3 files changed, 86 insertions(+), 40 deletions(-) diff --git a/src/emqx_base62.erl b/src/emqx_base62.erl index 929089f1b..0649900c2 100644 --- a/src/emqx_base62.erl +++ b/src/emqx_base62.erl @@ -16,41 +16,86 @@ -export([encode/1, decode/1]). -%% @doc Encode an integer to base62 string --spec(encode(non_neg_integer()) -> binary()). -encode(I) when is_integer(I) andalso I > 0 -> - list_to_binary(encode(I, [])). -encode(I, Acc) when I < 62 -> - [char(I) | Acc]; -encode(I, Acc) -> - encode(I div 62, [char(I rem 62) | Acc]). +%% @doc Encode any data to base62 binary +-spec encode(string() + | integer() + | binary()) -> float(). +encode(I) when is_integer(I) -> + encode(integer_to_binary(I)); +encode(S) when is_list(S)-> + encode(list_to_binary(S)); +encode(B) when is_binary(B) -> + encode(B, <<>>). -char(I) when I < 10 -> - $0 + I; - -char(I) when I < 36 -> - $A + I - 10; - -char(I) when I < 62 -> - $a + I - 36. - -%% @doc Decode base62 string to an integer --spec(decode(string() | binary()) -> integer()). +%% @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(binary_to_list(B)); -decode(S) when is_list(S) -> - decode(S, 0). + decode(B, <<>>). -decode([], I) -> - I; -decode([C|S], I) -> - decode(S, I * 62 + byte(C)). +%% encode_base62(<>, Acc) -> +%% encode_byte_group(H, Acc); +%% encode_base62(<>, Acc) -> +%% encode_byte_group(H, Acc); -byte(C) when $0 =< C andalso C =< $9 -> - C - $0; -byte(C) when $A =< C andalso C =< $Z -> - C - $A + 10; -byte(C) when $a =< C andalso C =< $z -> - C - $a + 36. +encode(<>, Acc) -> + CharList = [encode_char(Index1), encode_char(Index2), encode_char(Index3), encode_char(Index4)], + NewAcc = <>, + encode(Rest, NewAcc); +encode(<>, Acc) -> + CharList = [encode_char(Index1), encode_char(Index2), encode_char(Index3)], + NewAcc = <>, + encode(<<>>, NewAcc); +encode(<>, Acc) -> + CharList = [encode_char(Index1), encode_char(Index2)], + NewAcc = <>, + encode(<<>>, NewAcc); +encode(<<>>, Acc) -> + Acc. + +decode(<>, Acc) + when bit_size(Rest) >= 8-> + case Head == $9 of + true -> + <> = Rest, + DecodeChar = decode_char(9, Head1), + <<_:2, RestBit:6>> = <>, + NewAcc = <>, + decode(Rest1, NewAcc); + false -> + DecodeChar = decode_char(Head), + <<_:2, RestBit:6>> = <>, + NewAcc = <>, + decode(Rest, NewAcc) + end; +decode(<>, Acc) -> + DecodeChar = decode_char(Head), + LeftBitSize = bit_size(Acc) rem 8, + RightBitSize = 8 - LeftBitSize, + <<_:LeftBitSize, RestBit:RightBitSize>> = <>, + NewAcc = <>, + decode(Rest, NewAcc); +decode(<<>>, Acc) -> + Acc. + + +encode_char(I) when I < 26 -> + $A + I; +encode_char(I) when I < 52 -> + $a + I - 26; +encode_char(I) when I < 61 -> + $0 + I - 52; +encode_char(I) -> + [$9, $A + I - 61]. + +decode_char(I) when I >= $a andalso I =< $z -> + I + 26 - $a; +decode_char(I) when I >= $0 andalso I =< $8-> + I + 52 - $0; +decode_char(I) when I >= $A andalso I =< $Z-> + I - $A. + +decode_char(9, I) -> + I + 61 - $A. diff --git a/src/emqx_guid.erl b/src/emqx_guid.erl index 43855c734..8fe0c7b6e 100644 --- a/src/emqx_guid.erl +++ b/src/emqx_guid.erl @@ -126,8 +126,9 @@ from_hexstr(S) -> I = list_to_integer(binary_to_list(S), 16), <>. to_base62(<>) -> - emqx_base62:encode(I). + binary_to_list(emqx_base62:encode(I)). from_base62(S) -> - I = emqx_base62:decode(S), <>. + I = binary_to_integer(emqx_base62:decode(S)), + <>. diff --git a/test/emqx_base62_SUITE.erl b/test/emqx_base62_SUITE.erl index e0cb0e26a..cf4bc9756 100644 --- a/test/emqx_base62_SUITE.erl +++ b/test/emqx_base62_SUITE.erl @@ -26,11 +26,11 @@ all() -> [t_base62_encode]. t_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)), + <<"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)), - Y = ?BASE62:decode(?BASE62:encode(Y)). + X = erlang:binary_to_integer(?BASE62:decode(?BASE62:encode(X))), + Y = erlang:binary_to_integer(?BASE62:decode(?BASE62:encode(Y))). From 96d251ec3cb224e8e0712fb0083987e38f039d78 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Tue, 7 Aug 2018 10:27:04 +0800 Subject: [PATCH 077/520] Add encode and decode options and test suites --- src/emqx_base62.erl | 24 ++++++++++++++++++------ src/emqx_guid.erl | 4 ++-- test/emqx_base62_SUITE.erl | 7 +++++-- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/emqx_base62.erl b/src/emqx_base62.erl index 0649900c2..690115ec2 100644 --- a/src/emqx_base62.erl +++ b/src/emqx_base62.erl @@ -14,8 +14,10 @@ -module(emqx_base62). --export([encode/1, decode/1]). - +-export([encode/1, + encode/2, + decode/1, + decode/2]). %% @doc Encode any data to base62 binary -spec encode(string() @@ -28,18 +30,23 @@ encode(S) when is_list(S)-> encode(B) when is_binary(B) -> encode(B, <<>>). +%% encode(D, string) -> +%% 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, <<>>). -%% encode_base62(<>, Acc) -> -%% encode_byte_group(H, Acc); -%% encode_base62(<>, Acc) -> -%% encode_byte_group(H, Acc); +%%==================================================================== +%% Internal 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 = <>, @@ -55,6 +62,10 @@ 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 @@ -99,3 +110,4 @@ decode_char(I) when I >= $A andalso I =< $Z-> decode_char(9, I) -> I + 61 - $A. + diff --git a/src/emqx_guid.erl b/src/emqx_guid.erl index 8fe0c7b6e..fa9139ebd 100644 --- a/src/emqx_guid.erl +++ b/src/emqx_guid.erl @@ -126,9 +126,9 @@ from_hexstr(S) -> I = list_to_integer(binary_to_list(S), 16), <>. to_base62(<>) -> - binary_to_list(emqx_base62:encode(I)). + emqx_base62:encode(I). from_base62(S) -> - I = binary_to_integer(emqx_base62:decode(S)), + I = emqx_base62:decode(S, integer), <>. diff --git a/test/emqx_base62_SUITE.erl b/test/emqx_base62_SUITE.erl index cf4bc9756..820c7ec32 100644 --- a/test/emqx_base62_SUITE.erl +++ b/test/emqx_base62_SUITE.erl @@ -32,5 +32,8 @@ t_base62_encode(_) -> <<"65535">> = ?BASE62:decode(?BASE62:encode(<<"65535">>)), <> = emqx_guid:gen(), <> = emqx_guid:gen(), - X = erlang:binary_to_integer(?BASE62:decode(?BASE62:encode(X))), - Y = erlang:binary_to_integer(?BASE62:decode(?BASE62:encode(Y))). + 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). + From 288e03c91427a24c5b72daf016e05cf9104f03c8 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Tue, 7 Aug 2018 10:31:57 +0800 Subject: [PATCH 078/520] Update OTP version for travis-CI --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c625470d0..b2d01f3fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: erlang otp_release: - - 20.0 - - 20.1 + - 21.0 + - 21.0.4 script: - make From 8418be0a5be1a386ccd7aab5df1559c2b0376d88 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 7 Aug 2018 11:00:04 +0800 Subject: [PATCH 079/520] Use the new emqx_session:unsubscribe/2 API --- src/emqx_protocol.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 30b2c0294..85fe35e52 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -262,14 +262,14 @@ process({subscribe, RawTopicTable}, process(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> send(?UNSUBACK_PACKET(PacketId), State); -process(?UNSUBSCRIBE_PACKET(PacketId, _Properties, RawTopics), +process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopics), State = #proto_state{client_id = ClientId, username = Username, mountpoint = MountPoint, session = Session}) -> case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of {ok, TopicTable} -> - emqx_session:unsubscribe(Session, mount(replvar(MountPoint, State), TopicTable)); + emqx_session:unsubscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicTable)}); {stop, _} -> ok end, @@ -280,7 +280,7 @@ process({unsubscribe, RawTopics}, State = #proto_state{client_id = ClientId, session = Session}) -> case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of {ok, TopicTable} -> - emqx_session:unsubscribe(Session, TopicTable); + emqx_session:unsubscribe(Session, {undefined, #{}, TopicTable}); {stop, _} -> ok end, {ok, State}; From 4d9e03a8038e71f46138eca3987b098514359f1e Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 8 Aug 2018 18:36:14 +0800 Subject: [PATCH 080/520] Refactor websocket conn using cowboy --- Makefile | 4 +- etc/emqx.conf | 99 +----------------- include/emqx.hrl | 14 +-- priv/emqx.schema | 185 +-------------------------------- src/emqx.app.src | 2 +- src/emqx_cli.erl | 4 +- src/emqx_cm.erl | 2 +- src/emqx_ctl.erl | 16 ++- src/emqx_listeners.erl | 24 +++-- src/emqx_protocol.erl | 8 +- src/emqx_shared_sub.erl | 22 ++-- src/emqx_sm.erl | 2 +- src/emqx_sup.erl | 4 +- src/emqx_vm.erl | 2 +- src/emqx_ws.erl | 135 +++++++++++------------- src/emqx_ws_connection.erl | 154 +++++++++++---------------- src/emqx_ws_connection_sup.erl | 8 +- 17 files changed, 194 insertions(+), 491 deletions(-) diff --git a/Makefile b/Makefile index 38051db1c..509794bbd 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker PROJECT_VERSION = 3.0 -DEPS = jsx gproc gen_rpc lager ekka esockd mochiweb clique +DEPS = jsx gproc gen_rpc lager ekka esockd minirest clique dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 @@ -12,7 +12,7 @@ dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.1.1 dep_lager = git https://github.com/erlang-lager/lager 3.6.4 dep_esockd = git https://github.com/emqx/esockd emqx30 dep_ekka = git https://github.com/emqx/ekka emqx30 -dep_mochiweb = git https://github.com/emqtt/mochiweb emqx30 +dep_minirest = git https://github.com/emqx/minirest emqx30 dep_clique = git https://github.com/emqx/clique NO_AUTOPATCH = gen_rpc cuttlefish diff --git a/etc/emqx.conf b/etc/emqx.conf index 555a6d63b..9db1761bc 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1176,57 +1176,11 @@ listener.ws.external.acceptors = 4 ## Value: Number listener.ws.external.max_clients = 102400 -## Maximum MQTT/WebSocket connections per second. -## -## Value: Number -listener.ws.external.max_conn_rate = 1000 - ## Zone of the external MQTT/WebSocket listener belonged to. ## ## Value: String listener.ws.external.zone = external -## Mountpoint of the MQTT/WebSocket Listener. -## -## See: listener.tcp..mountpoint -## -## Value: String -## listener.ws.external.mountpoint = external/ - -## The access control for the MQTT/WebSocket listener. -## -## See: listener.tcp..access -## -## Value: ACL Rule -listener.ws.external.access.1 = allow all - -## Use X-Forwarded-For header for real source IP if the EMQ cluster is -## deployed behind NGINX or HAProxy. -## -## Value: String -## listener.ws.external.proxy_address_header = X-Forwarded-For - -## Use X-Forwarded-Port header for real source port if the EMQ cluster is -## deployed behind NGINX or HAProxy. -## -## Value: String -## listener.ws.external.proxy_port_header = X-Forwarded-Port - -## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind -## HAProxy or Nginx. -## -## See: listener.tcp..proxy_protocol -## -## Value: on | off -## listener.ws.external.proxy_protocol = on - -## Sets the timeout for proxy protocol. -## -## See: listener.tcp..proxy_protocol_timeout -## -## Value: Duration -## listener.ws.external.proxy_protocol_timeout = 3s - ## The TCP backlog of external MQTT/WebSocket Listener. ## ## See: listener.tcp..backlog @@ -1283,11 +1237,6 @@ listener.ws.external.send_timeout_close = on ## Value: true | false listener.ws.external.nodelay = true -## The SO_REUSEADDR flag for MQTT/WebSocket Listener. -## -## Value: true | false -listener.ws.external.reuseaddr = true - ##-------------------------------------------------------------------- ## External WebSocket/SSL listener for MQTT Protocol @@ -1309,11 +1258,6 @@ listener.wss.external.acceptors = 4 ## Value: Number listener.wss.external.max_clients = 64 -## Maximum MQTT/WebSocket/SSL connections per second. -## -## Value: Number -listener.wss.external.max_conn_rate = 1000 - ## Zone of the external MQTT/WebSocket/SSL listener belonged to. ## ## Value: String @@ -1326,37 +1270,6 @@ listener.wss.external.zone = external ## Value: String ## listener.wss.external.mountpoint = inbound/ -## The access control rules for the MQTT/WebSocket/SSL listener. -## -## See: listener.tcp..access. -## -## Value: ACL Rule -listener.wss.external.access.1 = allow all - -## See: listener.ws.external.proxy_address_header -## -## Value: String -## listener.wss.external.proxy_address_header = X-Forwarded-For - -## See: listener.ws.external.proxy_port_header -## -## Value: String -## listener.wss.external.proxy_port_header = X-Forwarded-Port - -## Enable the Proxy Protocol V1/2 support. -## -## See: listener.tcp..proxy_protocol -## -## Value: on | off -## listener.wss.external.proxy_protocol = on - -## Sets the timeout for proxy protocol. -## -## See: listener.tcp..proxy_protocol_timeout -## -## Value: Duration -## listener.wss.external.proxy_protocol_timeout = 3s - ## TLS versions only to protect from POODLE attack. ## ## See: listener.ssl..tls_versions @@ -1364,13 +1277,6 @@ listener.wss.external.access.1 = allow all ## Value: String, seperated by ',' ## listener.wss.external.tls_versions = tlsv1.2,tlsv1.1,tlsv1 -## TLS Handshake timeout. -## -## See: listener.ssl..handshake_timeout -## -## Value: Duration -listener.wss.external.handshake_timeout = 15s - ## Path to the file containing the user's private PEM-encoded key. ## ## See: listener.ssl..keyfile @@ -1481,10 +1387,7 @@ listener.wss.external.send_timeout_close = on ## Value: true | false ## listener.wss.external.nodelay = true -## The SO_REUSEADDR flag for WebSocket/SSL listener. -## -## Value: true | false -listener.wss.external.reuseaddr = true +listener.wss.external.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA ##------------------------------------------------------------------- ## System Monitor diff --git a/include/emqx.hrl b/include/emqx.hrl index 7340cf835..3b42713ff 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -192,13 +192,13 @@ %%-------------------------------------------------------------------- -record(plugin, { - name :: atom(), - version :: string(), - dir :: string(), - descr :: string(), - vendor :: string(), - active :: boolean(), - info :: map() + name :: atom(), + version :: string(), + dir :: string(), + descr :: string(), + vendor :: string(), + active = false :: boolean(), + info :: map() }). -type(plugin() :: #plugin{}). diff --git a/priv/emqx.schema b/priv/emqx.schema index 7ff5fd0a3..7db77f1fb 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -497,7 +497,7 @@ end}. ConsoleLogLevel = cuttlefish:conf_get("log.console.level", Conf), ConsoleLogFile = cuttlefish:conf_get("log.console.file", Conf), - ConsoleHandler = {lager_console_backend, [ConsoleLogLevel]}, + ConsoleHandler = {lager_console_backend, [{level, ConsoleLogLevel}]}, ConsoleFileHandler = {lager_file_backend, [{file, ConsoleLogFile}, {level, ConsoleLogLevel}, {size, cuttlefish:conf_get("log.console.size", Conf)}, @@ -1173,10 +1173,6 @@ end}. {datatype, integer} ]}. -{mapping, "listener.ws.$name.max_conn_rate", "emqx.listeners", [ - {datatype, integer} -]}. - {mapping, "listener.ws.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1185,28 +1181,6 @@ end}. {datatype, string} ]}. -{mapping, "listener.ws.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ws.$name.proxy_address_header", "emqx.listeners", [ - {datatype, string}, - hidden -]}. - -{mapping, "listener.ws.$name.proxy_port_header", "emqx.listeners", [ - {datatype, string}, - hidden -]}. - -{mapping, "listener.ws.$name.proxy_protocol", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.ws.$name.proxy_protocol_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - {mapping, "listener.ws.$name.backlog", "emqx.listeners", [ {default, 1024}, {datatype, integer} @@ -1247,11 +1221,6 @@ end}. hidden ]}. -{mapping, "listener.ws.$name.reuseaddr", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - %%-------------------------------------------------------------------- %% MQTT/WebSocket/SSL Listeners @@ -1269,10 +1238,6 @@ end}. {datatype, integer} ]}. -{mapping, "listener.wss.$name.max_conn_rate", "emqx.listeners", [ - {datatype, integer} -]}. - {mapping, "listener.wss.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1281,32 +1246,6 @@ end}. {datatype, string} ]}. -{mapping, "listener.wss.$name.rate_limit", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.proxy_address_header", "emqx.listeners", [ - {datatype, string}, - hidden -]}. - -{mapping, "listener.wss.$name.proxy_port_header", "emqx.listeners", [ - {datatype, string}, - hidden -]}. - -{mapping, "listener.wss.$name.proxy_protocol", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.wss.$name.proxy_protocol_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - {mapping, "listener.wss.$name.backlog", "emqx.listeners", [ {default, 1024}, {datatype, integer} @@ -1347,11 +1286,6 @@ end}. hidden ]}. -{mapping, "listener.wss.$name.reuseaddr", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - {mapping, "listener.wss.$name.tls_versions", "emqx.listeners", [ {datatype, string} ]}. @@ -1360,11 +1294,6 @@ end}. {datatype, string} ]}. -{mapping, "listener.wss.$name.handshake_timeout", "emqx.listeners", [ - {default, "15s"}, - {datatype, {duration, ms}} -]}. - {mapping, "listener.wss.$name.keyfile", "emqx.listeners", [ {datatype, string} ]}. @@ -1444,7 +1373,7 @@ end}. {sndbuf, cuttlefish:conf_get(Prefix ++ ".sndbuf", Conf, undefined)}, {buffer, cuttlefish:conf_get(Prefix ++ ".buffer", Conf, undefined)}, {nodelay, cuttlefish:conf_get(Prefix ++ ".nodelay", Conf, true)}, - {reuseaddr, cuttlefish:conf_get(Prefix ++ ".reuseaddr", Conf, true)}]) + {reuseaddr, cuttlefish:conf_get(Prefix ++ ".reuseaddr", Conf, undefined)}]) end, SplitFun = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end, @@ -1488,17 +1417,6 @@ end}. end end, - ApiListeners = fun(Type, Name) -> - Prefix = string:join(["listener", Type, Name], "."), - case cuttlefish:conf_get(Prefix, Conf, undefined) of - undefined -> - []; - ListenOn -> - SslOpts1 = case SslOpts(Prefix) of [] -> []; SslOpts0 -> [{ssl_options, SslOpts0}] end, - [{Atom(Type), ListenOn, [{tcp_options, TcpOpts(Prefix)}|LisOpts(Prefix)] ++ SslOpts1}] - end - end, - lists:flatten([TcpListeners(Type, Name) || {["listener", Type, Name], ListenOn} <- cuttlefish_variable:filter_by_prefix("listener.tcp", Conf) @@ -1506,106 +1424,9 @@ end}. ++ [SslListeners(Type, Name) || {["listener", Type, Name], ListenOn} <- cuttlefish_variable:filter_by_prefix("listener.ssl", Conf) - ++ cuttlefish_variable:filter_by_prefix("listener.wss", Conf)] - ++ - [ApiListeners(Type, Name) || {["listener", Type, Name], ListenOn} - <- cuttlefish_variable:filter_by_prefix("listener.api", Conf)]) + ++ cuttlefish_variable:filter_by_prefix("listener.wss", Conf)]) end}. -%%-------------------------------------------------------------------- -%% MQTT REST API Listeners - -{mapping, "listener.api.$name", "emqx.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "listener.api.$name.acceptors", "emqx.listeners", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "listener.api.$name.max_clients", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.api.$name.rate_limit", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.api.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.api.$name.backlog", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.api.$name.send_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "listener.api.$name.send_timeout_close", "emqx.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "listener.api.$name.recbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.api.$name.sndbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.api.$name.buffer", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.api.$name.tune_buffer", "emqx.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "listener.api.$name.nodelay", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.api.$name.reuseaddr", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.api.$name.handshake_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - -{mapping, "listener.api.$name.keyfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.api.$name.certfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.api.$name.cacertfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.api.$name.verify", "emqx.listeners", [ - {datatype, atom} -]}. - -{mapping, "listener.api.$name.fail_if_no_peer_cert", "emqx.listeners", [ - {datatype, {enum, [true, false]}} -]}. - %%-------------------------------------------------------------------- %% System Monitor %%-------------------------------------------------------------------- diff --git a/src/emqx.app.src b/src/emqx.app.src index 7d0dd0c4d..b7a195c8b 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -3,7 +3,7 @@ {vsn,"3.0"}, {modules,[]}, {registered,[emqx_sup]}, - {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,mochiweb]}, + {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,minirest]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, diff --git a/src/emqx_cli.erl b/src/emqx_cli.erl index 4463f2b27..6be9093b5 100644 --- a/src/emqx_cli.erl +++ b/src/emqx_cli.erl @@ -14,7 +14,7 @@ -module(emqx_cli). --export([print/1, print/2, usage/1]). +-export([print/1, print/2, usage/1, usage/2]). print(Msg) -> io:format(Msg). @@ -28,3 +28,5 @@ usage(CmdList) -> io:format("~-48s# ~s~n", [Cmd, Descr]) end, CmdList). +usage(Format, Args) -> + usage([{Format, Args}]). \ No newline at end of file diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 0adb07603..f05d63a29 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -84,7 +84,7 @@ unregister_client(CObj = {ClientId, ClientPid}) when is_binary(ClientId), is_pid %% @doc Lookup client pid -spec(lookup_client_pid(client_id()) -> pid() | undefined). lookup_client_pid(ClientId) when is_binary(ClientId) -> - case lookup_client_pid(ClientId) of + case ets:lookup(?CLIENT, ClientId) of [] -> undefined; [{_, Pid}] -> Pid end. diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index 46d97e757..c16bd23cb 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -18,7 +18,7 @@ -export([start_link/0]). -export([register_command/2, register_command/3, unregister_command/1]). --export([run_command/2, lookup_command/1]). +-export([run_command/1, run_command/2, lookup_command/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -48,7 +48,21 @@ unregister_command(Cmd) when is_atom(Cmd) -> cast(Msg) -> gen_server:cast(?SERVER, Msg). +run_command([]) -> + run_command(help, []); +run_command([Cmd | Args]) -> + run_command(list_to_atom(Cmd), Args). + -spec(run_command(cmd(), [string()]) -> ok | {error, term()}). +run_command(set, []) -> + emqx_mgmt_cli_cfg:set_usage(), ok; + +run_command(set, Args) -> + emqx_mgmt_cli_cfg:run(["config" | Args]), ok; + +run_command(show, Args) -> + emqx_mgmt_cli_cfg:run(["config" | Args]), ok; + run_command(help, []) -> usage(); run_command(Cmd, Args) when is_atom(Cmd) -> diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 9e8445414..257820fb1 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -36,21 +36,31 @@ start_listener({Proto, ListenOn, Options}) when Proto == ssl; Proto == tls -> start_mqtt_listener('mqtt:ssl', ListenOn, Options); %% Start MQTT/WS listener start_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> - start_http_listener('mqtt:ws', ListenOn, Options); + Dispatch = [{"/mqtt", emqx_ws, []}], + NumAcceptors = proplists:get_value(acceptors, Options, 4), + MaxConnections = proplists:get_value(max_clients, Options, 1024), + TcpOptions = proplists:get_value(tcp_options, Options, []), + Options1 = [{port, ListenOn}, + {num_acceptors, NumAcceptors}, + {max_connections, MaxConnections} | TcpOptions], + minirest:start_http(Proto, Options1, Dispatch); %% Start MQTT/WSS listener start_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss -> - start_http_listener('mqtt:wss', ListenOn, Options). + Dispatch = [{"/mqtt", emqx_ws, []}], + NumAcceptors = proplists:get_value(acceptors, Options, 4), + MaxConnections = proplists:get_value(max_clients, Options, 1024), + TcpOptions = proplists:get_value(tcp_options, Options, []), + SslOptions = proplists:get_value(ssl_options, Options, []), + Options1 = [{port, ListenOn}, + {num_acceptors, NumAcceptors}, + {max_connections, MaxConnections} | TcpOptions ++ SslOptions], + minirest:start_https(Proto, Options1, Dispatch). start_mqtt_listener(Name, ListenOn, Options) -> SockOpts = esockd:parse_opt(Options), MFA = {emqx_connection, start_link, [Options -- SockOpts]}, {ok, _} = esockd:open(Name, ListenOn, merge_default(SockOpts), MFA). -start_http_listener(Name, ListenOn, Options) -> - SockOpts = esockd:parse_opt(Options), - MFA = {emqx_ws, handle_request, [Options -- SockOpts]}, - {ok, _} = mochiweb:start_http(Name, ListenOn, SockOpts, MFA). - %% @doc Restart all listeners -spec(restart_all() -> ok). restart_all() -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 30b2c0294..58c2279c9 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -352,11 +352,11 @@ send(Packet = ?PACKET(Type), ProtoState = #proto_state{proto_ver = Ver, sockprops = #{sendfun := SendFun}}) -> Data = emqx_frame:serialize(Packet, #{version => Ver}), case SendFun(Data) of - ok -> emqx_metrics:sent(Packet), - trace(send, Packet, ProtoState), - {ok, inc_stats(send, Type, ProtoState)}; {error, Reason} -> - {error, Reason} + {error, Reason}; + _ -> emqx_metrics:sent(Packet), + trace(send, Packet, ProtoState), + {ok, inc_stats(send, Type, ProtoState)} end. trace(recv, Packet, ProtoState) -> diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 9380f38f7..2e4772f05 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -38,7 +38,7 @@ -define(TAB, emqx_shared_subscription). -record(state, {pmon}). --record(shared_subscription, {group, topic, subpid}). +-record(emqx_shared_subscription, {group, topic, subpid}). %%------------------------------------------------------------------------------ %% Mnesia bootstrap @@ -48,8 +48,8 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ {type, bag}, {ram_copies, [node()]}, - {record_name, shared_subscription}, - {attributes, record_info(fields, shared_subscription)}]); + {record_name, emqx_shared_subscription}, + {attributes, record_info(fields, emqx_shared_subscription)}]); mnesia(copy) -> ok = ekka_mnesia:copy_table(?TAB). @@ -78,7 +78,7 @@ unsubscribe(Group, Topic, SubPid) when is_pid(SubPid) -> mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)). record(Group, Topic, SubPid) -> - #shared_subscription{group = Group, topic = Topic, subpid = SubPid}. + #emqx_shared_subscription{group = Group, topic = Topic, subpid = SubPid}. %% TODO: dispatch strategy, ensure the delivery... dispatch(Group, Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> @@ -110,7 +110,7 @@ init([]) -> init_monitors() -> mnesia:foldl( - fun(#shared_subscription{subpid = SubPid}, Mon) -> + fun(#emqx_shared_subscription{subpid = SubPid}, Mon) -> emqx_pmon:monitor(SubPid, Mon) end, emqx_pmon:new(), ?TAB). @@ -126,11 +126,11 @@ handle_cast(Msg, State) -> {noreply, State}. handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = PMon}) -> - #shared_subscription{subpid = SubPid} = NewRecord, + #emqx_shared_subscription{subpid = SubPid} = NewRecord, {noreply, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})}; handle_info({mnesia_table_event, {delete_object, OldRecord, _}}, State = #state{pmon = PMon}) -> - #shared_subscription{subpid = SubPid} = OldRecord, + #emqx_shared_subscription{subpid = SubPid} = OldRecord, {noreply, update_stats(State#state{pmon = emqx_pmon:demonitor(SubPid, PMon)})}; handle_info({mnesia_table_event, _Event}, State) -> @@ -138,7 +138,7 @@ handle_info({mnesia_table_event, _Event}, State) -> handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) -> emqx_logger:info("[SharedSub] shared subscriber down: ~p", [SubPid]), - mnesia:async_dirty(fun cleanup_down/1, [SubPid]), + cleanup_down(SubPid), {noreply, update_stats(State#state{pmon = emqx_pmon:erase(SubPid, PMon)})}; handle_info(Info, State) -> @@ -156,8 +156,10 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- cleanup_down(SubPid) -> - lists:foreach(fun(Record) -> mnesia:delete_object(?TAB, Record) end, - mnesia:match_object(#shared_subscription{_ = '_', subpid = SubPid})). + lists:foreach( + fun(Record) -> + mnesia:dirty_delete_object(?TAB, Record) + end,mnesia:dirty_match_object(#emqx_shared_subscription{_ = '_', subpid = SubPid})). update_stats(State) -> emqx_stats:setstat('subscriptions/shared/count', 'subscriptions/shared/max', ets:info(?TAB, size)), State. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 00f4bff3d..afa2d6b06 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -20,7 +20,7 @@ -export([start_link/0]). --export([open_session/1, lookup_session/1, close_session/1]). +-export([open_session/1, lookup_session/1, close_session/1, lookup_session_pid/1]). -export([resume_session/1, resume_session/2, discard_session/1, discard_session/2]). -export([register_session/2, get_session_attrs/1, unregister_session/1]). -export([get_session_stats/1, set_session_stats/2]). diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index 3df468f1e..b3378ba91 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -72,7 +72,7 @@ init([]) -> %% Connection Manager CMSup = supervisor_spec(emqx_cm_sup), %% WebSocket Connection Sup - %% WSConnSup = supervisor_spec(emqx_ws_connection_sup), + WSConnSup = supervisor_spec(emqx_ws_connection_sup), %% Sys Sup SysSup = supervisor_spec(emqx_sys_sup), {ok, {{one_for_all, 0, 1}, @@ -84,7 +84,7 @@ init([]) -> SMSup, SessionSup, CMSup, - %%WSConnSup, + WSConnSup, SysSup]}}. %%-------------------------------------------------------------------- diff --git a/src/emqx_vm.erl b/src/emqx_vm.erl index 0f3a4eaa4..bf6388232 100644 --- a/src/emqx_vm.erl +++ b/src/emqx_vm.erl @@ -199,7 +199,7 @@ mem_info() -> {used_memory, proplists:get_value(total_memory, Dataset) - proplists:get_value(free_memory, Dataset)}]. ftos(F) -> - [S] = io_lib:format("~.2f", [F]), S. + S = io_lib:format("~.2f", [F]), S. %%%% erlang vm scheduler_usage fun copied from recon scheduler_usage(Interval) when is_integer(Interval) -> diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl index e7ad63d61..b84c603af 100644 --- a/src/emqx_ws.erl +++ b/src/emqx_ws.erl @@ -20,100 +20,83 @@ -import(proplists, [get_value/3]). --export([handle_request/1, ws_loop/3]). - %% WebSocket Loop State --record(wsocket_state, {peername, client_pid, max_packet_size, parser}). +-record(wsocket_state, {req, peername, client_pid, max_packet_size, parser}). -define(WSLOG(Level, Format, Args, State), - emqx_logger:Level("WsClient(~s): " ++ Format, - [esockd_net:format(State#wsocket_state.peername) | Args])). + lager:Level("WsClient(~s): " ++ Format, + [esockd_net:format(State#wsocket_state.peername) | Args])). +-export([init/2]). +-export([websocket_init/1]). +-export([websocket_handle/2]). +-export([websocket_info/2]). -handle_request(Req) -> - handle_request(Req:get(method), Req:get(path), Req). - -%%-------------------------------------------------------------------- -%% MQTT Over WebSocket -%%-------------------------------------------------------------------- - -handle_request('GET', "/mqtt", Req) -> - emqx_logger:debug("WebSocket Connection from: ~s", [Req:get(peer)]), - Upgrade = Req:get_header_value("Upgrade"), - Proto = check_protocol_header(Req), - case {is_websocket(Upgrade), Proto} of - {true, "mqtt" ++ _Vsn} -> - case Req:get(peername) of - {ok, Peername} -> - {ok, ProtoEnv} = emqx_config:get_env(protocol), - PacketSize = get_value(max_packet_size, ProtoEnv, ?MAX_PACKET_SIZE), - Parser = emqx_parser:initial_state(PacketSize), - %% Upgrade WebSocket. - {ReentryWs, ReplyChannel} = mochiweb_websocket:upgrade_connection(Req, fun ?MODULE:ws_loop/3), - {ok, ClientPid} = emqx_ws_conn_sup:start_connection(self(), Req, ReplyChannel), - ReentryWs(#wsocket_state{peername = Peername, - parser = Parser, - max_packet_size = PacketSize, - client_pid = ClientPid}); - {error, Reason} -> - emqx_logger:error("Get peername with error ~s", [Reason]), - Req:respond({400, [], <<"Bad Request">>}) - end; - {false, _} -> - emqx_logger:error("Not WebSocket: Upgrade = ~s", [Upgrade]), - Req:respond({400, [], <<"Bad Request">>}); - {_, Proto} -> - emqx_logger:error("WebSocket with error Protocol: ~s", [Proto]), - Req:respond({400, [], <<"Bad WebSocket Protocol">>}) - end; - -handle_request(Method, Path, Req) -> - emqx_logger:error("Unexpected WS Request: ~s ~s", [Method, Path]), - Req:not_found(). - -is_websocket(Upgrade) -> - (not emqx_config:get_env(websocket_check_upgrade_header, true)) orelse - (Upgrade =/= undefined andalso string:to_lower(Upgrade) =:= "websocket"). - -check_protocol_header(Req) -> - case emqx_config:get_env(websocket_protocol_header, false) of - true -> get_protocol_header(Req); - false -> "mqtt-v3.1.1" +init(Req0, State) -> + case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req0) of + undefined -> + {cowboy_websocket, Req0, #wsocket_state{}}; + Subprotocols -> + case lists:member(<<"mqtt">>, Subprotocols) of + true -> + Peername = cowboy_req:peer(Req0), + Req = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt">>, Req0), + {cowboy_websocket, Req, #wsocket_state{req = Req, peername = Peername}, #{idle_timeout => 86400000}}; + false -> + Req = cowboy_req:reply(400, Req0), + {ok, Req, #wsocket_state{}} + end end. -get_protocol_header(Req) -> - case Req:get_header_value("EMQ-WebSocket-Protocol") of - undefined -> Req:get_header_value("Sec-WebSocket-Protocol"); - Proto -> Proto +websocket_init(State = #wsocket_state{req = Req}) -> + case emqx_ws_connection_sup:start_connection(self(), Req) of + {ok, ClientPid} -> + {ok, ProtoEnv} = emqx_config:get_env(protocol), + PacketSize = get_value(max_packet_size, ProtoEnv, ?MAX_PACKET_SIZE), + Parser = emqx_frame:initial_state(#{max_packet_size => PacketSize}), + NewState = State#wsocket_state{parser = Parser, + max_packet_size = PacketSize, + client_pid = ClientPid}, + {ok, NewState}; + Error -> + ?WSLOG(error, "Start client fail: ~p", [Error], State), + {stop, State} end. -%%-------------------------------------------------------------------- -%% Receive Loop -%%-------------------------------------------------------------------- +websocket_handle({binary, <<>>}, State) -> + {ok, State}; +websocket_handle({binary, [<<>>]}, State) -> + {ok, State}; -%% @doc WebSocket frame receive loop. -ws_loop(<<>>, State, _ReplyChannel) -> - State; -ws_loop([<<>>], State, _ReplyChannel) -> - State; -ws_loop(Data, State = #wsocket_state{client_pid = ClientPid, parser = Parser}, ReplyChannel) -> +websocket_handle({binary, Data}, State = #wsocket_state{client_pid = ClientPid, parser = Parser}) -> ?WSLOG(debug, "RECV ~p", [Data], State), - emqx_metrics:inc('bytes/received', iolist_size(Data)), - case catch emqx_parser:parse(iolist_to_binary(Data), Parser) of + BinSize = iolist_size(Data), + emqx_metrics:inc('bytes/received', BinSize), + case catch emqx_frame:parse(iolist_to_binary(Data), Parser) of {more, NewParser} -> - State#wsocket_state{parser = NewParser}; + {ok, State#wsocket_state{parser = NewParser}}; {ok, Packet, Rest} -> - gen_server:cast(ClientPid, {received, Packet}), - ws_loop(Rest, reset_parser(State), ReplyChannel); + gen_server:cast(ClientPid, {received, Packet, BinSize}), + websocket_handle({binary, Rest}, reset_parser(State)); {error, Error} -> ?WSLOG(error, "Frame error: ~p", [Error], State), - exit({shutdown, Error}); + {stop, State}; {'EXIT', Reason} -> ?WSLOG(error, "Frame error: ~p", [Reason], State), ?WSLOG(error, "Error data: ~p", [Data], State), - exit({shutdown, parser_error}) + {stop, State} end. -reset_parser(State = #wsocket_state{max_packet_size = PacketSize}) -> - State#wsocket_state{parser = emqx_parser:initial_state(PacketSize)}. +websocket_info({binary, Data}, State) -> + {reply, {binary, Data}, State}; + +websocket_info({'EXIT', _Pid, {shutdown, kick}}, State) -> + {stop, State}; + +websocket_info(_Info, State) -> + {ok, State}. + +reset_parser(State = #wsocket_state{max_packet_size = PacketSize}) -> + State#wsocket_state{parser = emqx_frame:initial_state(#{max_packet_size => PacketSize})}. + diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index dadc2cc57..93a289636 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -23,7 +23,7 @@ -import(proplists, [get_value/2, get_value/3]). %% API Exports --export([start_link/4]). +-export([start_link/3]). %% Management and Monitor API -export([info/1, stats/1, kick/1, clean_acl_cache/2]). @@ -38,13 +38,15 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% TODO: remove ... --export([handle_pre_hibernate/1]). - %% WebSocket Client State --record(wsclient_state, {ws_pid, transport, socket, peername, - proto_state, keepalive, enable_stats, - force_gc_count}). +-record(wsclient_state, {ws_pid, peername, proto_state, keepalive, + enable_stats, force_gc_count}). + +%% recv_oct +%% Number of bytes received by the socket. + +%% recv_cnt +%% Number of packets received by the socket. -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). @@ -53,8 +55,8 @@ [esockd_net:format(State#wsclient_state.peername) | Args])). %% @doc Start WebSocket Client. -start_link(Env, WsPid, Req, ReplyChannel) -> - gen_server:start_link(?MODULE, [Env, WsPid, Req, ReplyChannel], +start_link(Env, WsPid, Req) -> + gen_server:start_link(?MODULE, [Env, WsPid, Req], [[{hibernate_after, 10000}]]). info(CPid) -> @@ -82,38 +84,29 @@ clean_acl_cache(CPid, Topic) -> %% gen_server Callbacks %%-------------------------------------------------------------------- -init([Env, WsPid, Req, ReplyChannel]) -> +init([Options, WsPid, Req]) -> + init_stas(), process_flag(trap_exit, true), true = link(WsPid), - Transport = mochiweb_request:get(transport, Req), - Sock = mochiweb_request:get(socket, Req), - case mochiweb_request:get(peername, Req) of - {ok, Peername} -> - Headers = mochiweb_headers:to_list(mochiweb_request:get(headers, Req)), - ProtoState = emqx_protocol:init(Transport, Sock, Peername, send_fun(ReplyChannel), - [{ws_initial_headers, Headers} | Env]), - IdleTimeout = get_value(client_idle_timeout, Env, 30000), - EnableStats = get_value(client_enable_stats, Env, false), - ForceGcCount = emqx_gc:conn_max_gc_count(), - {ok, #wsclient_state{transport = Transport, - socket = Sock, - ws_pid = WsPid, - peername = Peername, - proto_state = ProtoState, - enable_stats = EnableStats, - force_gc_count = ForceGcCount}, - IdleTimeout, {backoff, 2000, 2000, 20000}, ?MODULE}; - {error, enotconn} -> Transport:fast_close(Sock), - exit(WsPid, normal), - exit(normal); - {error, Reason} -> Transport:fast_close(Sock), - exit(WsPid, normal), - exit({shutdown, Reason}) - end. - -handle_pre_hibernate(State = #wsclient_state{ws_pid = WsPid}) -> - erlang:garbage_collect(WsPid), - {hibernate, emqx_gc:reset_conn_gc_count(#wsclient_state.force_gc_count, emit_stats(State))}. + Peername = cowboy_req:peer(Req), + Headers = cowboy_req:headers(Req), + Sockname = cowboy_req:sock(Req), + Peercert = cowboy_req:cert(Req), + Zone = proplists:get_value(zone, Options), + ProtoState = emqx_protocol:init(#{zone => Zone, + peername => Peername, + sockname => Sockname, + peercert => Peercert, + sendfun => send_fun(WsPid)}, + [{ws_initial_headers, Headers} | Options]), + IdleTimeout = get_value(client_idle_timeout, Options, 30000), + EnableStats = get_value(client_enable_stats, Options, false), + ForceGcCount = emqx_gc:conn_max_gc_count(), + {ok, #wsclient_state{ws_pid = WsPid, + peername = Peername, + proto_state = ProtoState, + enable_stats = EnableStats, + force_gc_count = ForceGcCount}, IdleTimeout}. handle_call(info, From, State = #wsclient_state{peername = Peername, proto_state = ProtoState}) -> @@ -123,7 +116,7 @@ handle_call(info, From, State = #wsclient_state{peername = Peername, handle_call(stats, _From, State = #wsclient_state{proto_state = ProtoState}) -> reply(lists:append([emqx_misc:proc_stats(), - wsock_stats(State), + wsock_stats(), emqx_protocol:stats(ProtoState)]), State); handle_call(kick, _From, State) -> @@ -140,7 +133,9 @@ handle_call(Req, _From, State) -> ?WSLOG(error, "Unexpected request: ~p", [Req], State), reply({error, unexpected_request}, State). -handle_cast({received, Packet}, State = #wsclient_state{proto_state = ProtoState}) -> +handle_cast({received, Packet, BinSize}, State = #wsclient_state{proto_state = ProtoState}) -> + put(recv_oct, get(recv_oct) + BinSize), + put(recv_cnt, get(recv_cnt) + 1), emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> @@ -158,48 +153,24 @@ handle_cast(Msg, State) -> ?WSLOG(error, "unexpected msg: ~p", [Msg], State), {noreply, State}. -handle_info({subscribe, TopicTable}, State) -> +handle_info(SubReq ={subscribe, _TopicTable}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:subscribe(TopicTable, ProtoState) + emqx_protocol:process(SubReq, ProtoState) end, State); -handle_info({unsubscribe, Topics}, State) -> +handle_info(UnsubReq = {unsubscribe, _Topics}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:unsubscribe(Topics, ProtoState) + emqx_protocol:process(UnsubReq, ProtoState) end, State); -handle_info({suback, PacketId, ReasonCodes}, State) -> +handle_info({deliver, PubOrAck}, State) -> with_proto( fun(ProtoState) -> - Packet = ?SUBACK_PACKET(PacketId, ReasonCodes), - emqx_protocol:send(Packet, ProtoState) - end, State); - -handle_info({unsuback, PacketId, ReasonCodes}, State) -> - with_proto( - fun(ProtoState) -> - Packet = ?UNSUBACK_PACKET(PacketId, ReasonCodes), - emqx_protocol:send(Packet, ProtoState) - end, State); - -%% Fastlane -handle_info({dispatch, _Topic, Message}, State) -> - handle_info({deliver, Message#message{qos = ?QOS_0}}, State); - -handle_info({deliver, Message}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:send(Message, ProtoState) + emqx_protocol:deliver(PubOrAck, ProtoState) end, gc(State)); -handle_info({redeliver, {?PUBREL, PacketId}}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:pubrel(PacketId, ProtoState) - end, State); - handle_info(emit_stats, State) -> {noreply, emit_stats(State), hibernate}; @@ -213,10 +184,9 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> handle_info({shutdown, Reason}, State) -> shutdown(Reason, State); -handle_info({keepalive, start, Interval}, - State = #wsclient_state{transport = Transport, socket =Sock}) -> +handle_info({keepalive, start, Interval}, State) -> ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State), - case emqx_keepalive:start(stat_fun(Transport, Sock), Interval, {keepalive, check}) of + case emqx_keepalive:start(stat_fun(), Interval, {keepalive, check}) of {ok, KeepAlive} -> {noreply, State#wsclient_state{keepalive = KeepAlive}, hibernate}; {error, Error} -> @@ -271,23 +241,18 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -send_fun(ReplyChannel) -> - Self = self(), - fun(Packet) -> - Data = emqx_frame:serialize(Packet), - emqx_metrics:inc('bytes/sent', iolist_size(Data)), - case ReplyChannel({binary, Data}) of - ok -> ok; - {error, Reason} -> Self ! {shutdown, Reason} - end +send_fun(WsPid) -> + fun(Data) -> + BinSize = iolist_size(Data), + emqx_metrics:inc('bytes/sent', BinSize), + put(send_oct, get(send_oct) + BinSize), + put(send_cnt, get(send_cnt) + 1), + WsPid ! {binary, iolist_to_binary(Data)} end. -stat_fun(Transport, Sock) -> +stat_fun() -> fun() -> - case Transport:getstat(Sock, [recv_oct]) of - {ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct}; - {error, Error} -> {error, Error} - end + {ok, get(recv_oct)} end. emit_stats(State = #wsclient_state{proto_state = ProtoState}) -> @@ -302,11 +267,8 @@ emit_stats(ClientId, State) -> emqx_cm:set_client_stats(ClientId, Stats), State. -wsock_stats(#wsclient_state{transport = Transport, socket = Sock}) -> - case Transport:getstat(Sock, ?SOCK_STATS) of - {ok, Ss} -> Ss; - {error, _} -> [] - end. +wsock_stats() -> + [{Key, get(Key)}|| Key <- ?SOCK_STATS]. with_proto(Fun, State = #wsclient_state{proto_state = ProtoState}) -> {ok, ProtoState1} = Fun(ProtoState), @@ -325,3 +287,9 @@ gc(State) -> Cb = fun() -> emit_stats(State) end, emqx_gc:maybe_force_gc(#wsclient_state.force_gc_count, State, Cb). +init_stas() -> + put(recv_oct, 0), + put(recv_cnt, 0), + put(send_oct, 0), + put(send_cnt, 0). + diff --git a/src/emqx_ws_connection_sup.erl b/src/emqx_ws_connection_sup.erl index b58e7c956..f627abfbb 100644 --- a/src/emqx_ws_connection_sup.erl +++ b/src/emqx_ws_connection_sup.erl @@ -18,7 +18,7 @@ -behavior(supervisor). --export([start_link/0, start_connection/3]). +-export([start_link/0, start_connection/2]). -export([init/1]). @@ -27,9 +27,9 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% @doc Start a MQTT/WebSocket Connection. --spec(start_connection(pid(), mochiweb_request:request(), fun()) -> {ok, pid()}). -start_connection(WsPid, Req, ReplyChannel) -> - supervisor:start_child(?MODULE, [WsPid, Req, ReplyChannel]). +-spec(start_connection(pid(), mochiweb_request:request()) -> {ok, pid()}). +start_connection(WsPid, Req) -> + supervisor:start_child(?MODULE, [WsPid, Req]). %%-------------------------------------------------------------------- %% Supervisor callbacks From b5a1960b6342bfaa9eacc6dba081f366deb7c56c Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 8 Aug 2018 18:42:11 +0800 Subject: [PATCH 081/520] Stop emqx_ws --- src/emqx_ws.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl index b84c603af..d7f6dc6e8 100644 --- a/src/emqx_ws.erl +++ b/src/emqx_ws.erl @@ -32,7 +32,7 @@ -export([websocket_handle/2]). -export([websocket_info/2]). -init(Req0, State) -> +init(Req0, _State) -> case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req0) of undefined -> {cowboy_websocket, Req0, #wsocket_state{}}; @@ -90,7 +90,8 @@ websocket_handle({binary, Data}, State = #wsocket_state{client_pid = ClientPid, websocket_info({binary, Data}, State) -> {reply, {binary, Data}, State}; -websocket_info({'EXIT', _Pid, {shutdown, kick}}, State) -> +websocket_info({'EXIT', Pid, Reason}, State = #wsocket_state{client_pid = Pid}) -> + ?WSLOG(debug, "EXIT: ~p", [Reason], State), {stop, State}; websocket_info(_Info, State) -> From 4cf181503080fa31169bb449b5128518bb14305b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 8 Aug 2018 19:23:32 +0800 Subject: [PATCH 082/520] Add more configurations for EMQ X R3.0 --- etc/emqx.conf | 928 ++++++++++++++++++++++++++++------------------- priv/emqx.schema | 874 ++++++++++++++++++++++---------------------- 2 files changed, 973 insertions(+), 829 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 555a6d63b..9458e4fed 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -9,7 +9,7 @@ ## Cluster name. ## ## Value: String -cluster.name = emqxcluster +cluster.name = emqxcl ## Cluster auto-discovery strategy. ## @@ -48,7 +48,7 @@ cluster.autoclean = 5m ## Node list of the cluster. ## ## Value: String -## cluster.static.seeds = emq1@127.0.0.1,emq2@127.0.0.1 +## cluster.static.seeds = emqx1@127.0.0.1,emqx2@127.0.0.1 ##-------------------------------------------------------------------- ## Cluster using IP Multicast. @@ -91,7 +91,7 @@ cluster.autoclean = 5m ## The App name is used to build 'node.name' with IP address. ## ## Value: String -## cluster.dns.app = emq +## cluster.dns.app = emqx ##-------------------------------------------------------------------- ## Cluster using etcd @@ -105,7 +105,7 @@ cluster.autoclean = 5m ## will create a path in etcd: v2/keys/// ## ## Value: String -## cluster.etcd.prefix = emqcl +## cluster.etcd.prefix = emqxcl ## The TTL for node's path in etcd. ## @@ -125,7 +125,7 @@ cluster.autoclean = 5m ## The service name helps lookup EMQ nodes in the cluster. ## ## Value: String -## cluster.k8s.service_name = emq +## cluster.k8s.service_name = emqx ## The address type is used to extract host from k8s service. ## @@ -135,7 +135,7 @@ cluster.autoclean = 5m ## The app name helps build 'node.name'. ## ## Value: String -## cluster.k8s.app_name = emq +## cluster.k8s.app_name = emqx ## Kubernates Namespace ## @@ -143,7 +143,7 @@ cluster.autoclean = 5m ## cluster.k8s.namespace = default ##-------------------------------------------------------------------- -## Node Args +## Node ##-------------------------------------------------------------------- ## Node name. @@ -276,38 +276,54 @@ node.dist_listen_min = 6369 node.dist_listen_max = 6369 ##-------------------------------------------------------------------- -## RPC Args +## RPC ##-------------------------------------------------------------------- -## TCP server port. +## TCP server port for RPC. +## +## Value: Port [1024-65535] rpc.tcp_server_port = 5369 -## Default TCP port for outgoing connections +## TCP port for outgoing RPC connections. +## +## Value: Port [1024-65535] rpc.tcp_client_port = 5369 -## Client connect timeout +## RCP Client connect timeout. +## +## Value: Seconds rpc.connect_timeout = 5000 -## Client and Server send timeout +## TCP send timeout of RPC client and server. +## +## Value: Seconds rpc.send_timeout = 5000 ## Authentication timeout +## +## Value: Seconds rpc.authentication_timeout = 5000 ## Default receive timeout for call() functions +## +## Value: Seconds rpc.call_receive_timeout = 15000 -## Socket keepalive configuration +## Socket idle keepalive. +## +## Value: Seconds rpc.socket_keepalive_idle = 900 -## Seconds between probes +## TCP Keepalive probes interval. +## +## Value: Integer rpc.socket_keepalive_interval = 75 ## Probes lost to close the connection +## +## Value: Integer rpc.socket_keepalive_count = 9 -## TODO: sndbuf, rcvbuf and buffer - ##-------------------------------------------------------------------- ## Log ##-------------------------------------------------------------------- @@ -399,283 +415,35 @@ log.syslog = on ## log.syslog.level = error ##-------------------------------------------------------------------- -## Allow Anonymous Authentication and Default ACL +## Authentication/Access Control ##-------------------------------------------------------------------- -## Allow Anonymous Authentication. -## -## Notice: Disable the option for production deployment. +## Allow anonymous authentication by default if no auth plugins loaded. +## Notice: Disable the option in production deployment! ## ## Value: true | false -mqtt.allow_anonymous = true - -## Default behaviour when ACL nomatch. -## -## Value: allow | deny -mqtt.acl_nomatch = allow +allow_anonymous = true ## Default ACL File. ## ## Value: File Name -mqtt.acl_file = {{ platform_etc_dir }}/acl.conf +acl_file = {{ platform_etc_dir }}/acl.conf -## Whether to cache ACL for publish messages. -## -## Value: true | false -mqtt.cache_acl = true - -##-------------------------------------------------------------------- -## MQTT Protocol -##-------------------------------------------------------------------- - -## Maximum length of MQTT clientId allowed. -## -## Value: Number [23-65535] -mqtt.max_clientid_len = 1024 - -## Maximum MQTT packet size allowed. -## -## Value: Bytes -## -## Default: 64K -mqtt.max_packet_size = 64KB - -## Check if the websocket protocol header is valid. -## Turn off the option when developing WeChat App. +## Whether to enable ACL cache for publish. ## ## Value: on | off -mqtt.websocket_protocol_header = on +enable_acl_cache = on -## Check Websocket Upgrade Header. -## -## Value: on | off -mqtt.websocket_check_upgrade_header = on - -## The backoff for MQTT keepalive timeout. -## EMQ will kick a MQTT connection out until 'Keepalive * backoff * 2' timeout. -## -## Value: Float > 0.5 -mqtt.keepalive_backoff = 0.75 - -##-------------------------------------------------------------------- -## MQTT Connection -##-------------------------------------------------------------------- - -## Force GC the MQTT connections. Value 0 will disable the Force GC. -## -## Value: Number >= 0 -mqtt.conn.force_gc_count = 100 - -##-------------------------------------------------------------------- -## MQTT Client -##-------------------------------------------------------------------- - -## MQTT client idle timeout, specified in seconds. +## The ACL cache age. ## ## Value: Duration -mqtt.client.idle_timeout = 30s +## Default: 5 minute +acl_cache_age = 5m -## TODO: Maximum publish rate of MQTT messages per second. +## The ACL cache size, 0 means no limit. ## -## Value: Number -## mqtt.client.max_publish_rate = 5 - -## Enable per client statistics. -## -## Value: on | off -mqtt.client.enable_stats = off - -##-------------------------------------------------------------------- -## MQTT Session -##-------------------------------------------------------------------- - -## Maximum number of subscriptions allowed, 0 means no limit. -## -## Value: Number -mqtt.session.max_subscriptions = 0 - -## Force to upgrade QoS according to subscription. -## -## Value: on | off -mqtt.session.upgrade_qos = off - -## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. -## -## Value: Number -mqtt.session.max_inflight = 32 - -## Retry interval for QoS1/2 message delivering. -## -## Value: Duration -mqtt.session.retry_interval = 20s - -## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. -## -## Value: Number -mqtt.session.max_awaiting_rel = 1000 - -## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. -## -## Value: Duration -mqtt.session.await_rel_timeout = 30s - -## Enable per session statistics. -## -## Value: on | off -mqtt.session.enable_stats = on - -## Session expiration time. -## -## Value: Duration -## -d: day -## -h: hour -## -m: minute -## -s: second -## -## Default: 2h, 2 hours -mqtt.session.expiry_interval = 2h - -## Whether to ignore loop delivery of messages. -## -## Value: true | false -## -## Default: false -mqtt.session.ignore_loop_deliver = false - -##-------------------------------------------------------------------- -## MQTT Message Queue -##-------------------------------------------------------------------- - -## Message queue type. -## -## Value: simple | priority -mqtt.mqueue.type = simple - -## Topic priority. Default is 0. -## -## Value: Number [0-255] -## -## mqtt.mqueue.priority = topic/1=10,topic/2=8 - -## Maximum queue length. Enqueued messages when persistent client disconnected, -## or inflight window is full. 0 means no limit. -## -## Value: Number >= 0 -mqtt.mqueue.max_length = 1000 - -## Low-water mark of queued messages. -## -## Value: Percent -mqtt.mqueue.low_watermark = 20% - -## High-water mark of queued messages. -## -## Value: Percent -mqtt.mqueue.high_watermark = 60% - -## Whether to enqueue Qos0 messages. -## -## Value: false | true -mqtt.mqueue.store_qos0 = true - -##-------------------------------------------------------------------- -## MQTT Broker and PubSub -##-------------------------------------------------------------------- - -## System interval of publishing $SYS messages. -## -## Value: Duration -## -## Default: 1m, 1 minute -mqtt.broker.sys_interval = 1m - -## The PubSub pool size. Default value should be same as scheduler numbers. -## -## Value: Number > 1 -mqtt.pubsub.pool_size = 8 - -## TODO: Subscribe asynchronously. -## -## Value: true | false -mqtt.pubsub.async = true - -##-------------------------------------------------------------------- -## MQTT Bridge -##-------------------------------------------------------------------- - -## The pending message queue size of bridge. -## -## Value: Number -mqtt.bridge.max_queue_len = 10000 - -## Ping interval of bridge node. -## -## Value: Duration -## -## Default: 1s, 1 second -mqtt.bridge.ping_down_interval = 1s - -##------------------------------------------------------------------- -## Plugins -##------------------------------------------------------------------- - -## The etc dir for plugins' config. -## -## Value: Folder -mqtt.plugins.etc_dir ={{ platform_etc_dir }}/plugins/ - -## The file to store loaded plugin names. -## -## Value: File -mqtt.plugins.loaded_file = {{ platform_data_dir }}/loaded_plugins - -## File to store loaded plugin names. -mqtt.plugins.expand_plugins_dir = {{ platform_plugins_dir }}/ - -##-------------------------------------------------------------------- -## Modules -##-------------------------------------------------------------------- - -##-------------------------------------------------------------------- -## Presence Module - -## Enable Presence Module. -## -## Value: on | off -module.presence = on - -## Sets the QoS for presence MQTT message. -## -## Value: 0 | 1 | 2 -module.presence.qos = 1 - -##-------------------------------------------------------------------- -## Subscription Module - -## Enable Subscription Module. -## -## Value: on | off -module.subscription = off - -## Subscribe the Topics automatically when client connected. -## module.subscription.1.topic = $client/%c -## Qos of the subscription: 0 | 1 | 2 -## module.subscription.1.qos = 1 - -## module.subscription.2.topic = $user/%u -## module.subscription.2.qos = 1 - -##-------------------------------------------------------------------- -## Rewrite Module - -## Enable Rewrite Module. -## -## Value: on | off -module.rewrite = off - -## {rewrite, Topic, Re, Dest} -## module.rewrite.rule.1 = x/# ^x/y/(.+)$ z/y/$1 -## module.rewrite.rule.2 = y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2 +## Value: Integer +acl_cache_size = 0 ##-------------------------------------------------------------------- ## Listeners @@ -684,7 +452,7 @@ module.rewrite = off ##-------------------------------------------------------------------- ## MQTT/TCP - External TCP Listener for MQTT Protocol -## listener.tcp. is the IP address and port that the MQTT/TCP +## listener.tcp.$name is the IP address and port that the MQTT/TCP ## listener will bind. ## ## Value: IP:Port | Port @@ -695,37 +463,40 @@ listener.tcp.external = 0.0.0.0:1883 ## The acceptor pool for external MQTT/TCP listener. ## ## Value: Number -listener.tcp.external.acceptors = 16 +listener.tcp.external.acceptors = 8 ## Maximum number of concurrent MQTT/TCP connections. ## ## Value: Number -listener.tcp.external.max_clients = 102400 +listener.tcp.external.max_connections = 1024000 -## Maximum connection per second. +## Maximum external connections per second. ## ## Value: Number listener.tcp.external.max_conn_rate = 1000 ## Zone of the external MQTT/TCP listener belonged to. ## +## See: zone.$name.* +## ## Value: String listener.tcp.external.zone = external -## Mountpoint of the MQTT/TCP Listener. All the topics of this -## listener will be prefixed with the mount point if this option -## is enabled. -## Notice that supports wildcard mount:%c clientid, %u username +## Mountpoint of the MQTT/TCP Listener. All the topics will be prefixed +## with the mountpoint path if this option is enabled. +## +## Variables in mountpoint path: +## - %c: clientid +## - %u: username ## ## Value: String -listener.tcp.external.mountpoint = devicebound/ +## listener.tcp.external.mountpoint = devicebound/ -## Rate limit for the external MQTT/TCP connections. -## Format is 'burst,rate'. +## Rate limit for the external MQTT/TCP connections. Format is 'rate,burst'. ## -## Value: burst,rate -## Unit: KB/sec -listener.tcp.external.rate_limit = 100,10 +## Value: rate,burst +## Unit: Bps +## listener.tcp.external.rate_limit = 1024,4096 ## The access control rules for the MQTT/TCP listener. ## @@ -736,7 +507,7 @@ listener.tcp.external.rate_limit = 100,10 ## Example: allow 192.168.0.0/24 listener.tcp.external.access.1 = allow all -## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed +## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed ## behind HAProxy or Nginx. ## ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ @@ -744,18 +515,17 @@ listener.tcp.external.access.1 = allow all ## Value: on | off ## listener.tcp.external.proxy_protocol = on -## Sets the timeout for proxy protocol. EMQ will close the TCP connection +## Sets the timeout for proxy protocol. EMQ X will close the TCP connection ## if no proxy protocol packet recevied within the timeout. ## ## Value: Duration ## listener.tcp.external.proxy_protocol_timeout = 3s ## Enable the option for X.509 certificate based authentication. -## EMQ will Use the PP2_SUBTYPE_SSL_CN field in Proxy Protocol V2 -## as MQTT username. +## EMQX will use the common name of certificate as MQTT username. ## -## Value: cn -## listener.tcp.external.peer_cert_as_username = cn +## Value: boolean +## listener.tcp.external.peer_cert_as_username = true ## The TCP backlog defines the maximum length that the queue of pending ## connections can grow to. @@ -778,14 +548,14 @@ listener.tcp.external.send_timeout_close = on ## See: http://erlang.org/doc/man/inet.html ## ## Value: Bytes -## listener.tcp.external.recbuf = 4KB +## listener.tcp.external.recbuf = 2KB ## The TCP send buffer(os kernel) for MQTT connections. ## ## See: http://erlang.org/doc/man/inet.html ## ## Value: Bytes -## listener.tcp.external.sndbuf = 4KB +## listener.tcp.external.sndbuf = 2KB ## The size of the user-level software buffer used by the driver. ## Not to be confused with options sndbuf and recbuf, which correspond @@ -797,7 +567,7 @@ listener.tcp.external.send_timeout_close = on ## See: http://erlang.org/doc/man/inet.html ## ## Value: Bytes -## listener.tcp.external.buffer = 4KB +## listener.tcp.external.buffer = 2KB ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. ## @@ -834,7 +604,12 @@ listener.tcp.internal.acceptors = 4 ## Maximum number of concurrent MQTT/TCP connections. ## ## Value: Number -listener.tcp.internal.max_clients = 102400 +listener.tcp.internal.max_connections = 10240000 + +## Maximum internal connections per second. +## +## Value: Number +listener.tcp.internal.max_conn_rate = 1000 ## Zone of the internal MQTT/TCP listener belonged to. ## @@ -843,42 +618,43 @@ listener.tcp.internal.zone = internal ## Mountpoint of the MQTT/TCP Listener. ## -## See: listener.tcp..mountpoint +## See: listener.tcp.$name.mountpoint ## ## Value: String ## listener.tcp.internal.mountpoint = internal/ ## Rate limit for the internal MQTT/TCP connections. ## -## See: listener.tcp..rate_limit +## See: listener.tcp.$name.rate_limit ## -## Value: burst,rate -## listener.tcp.internal.rate_limit = 1000,100 +## Value: rate,burst +## Unit: Bps +## listener.tcp.internal.rate_limit = 1000000,2000000 ## The TCP backlog of internal MQTT/TCP Listener. ## -## See: listener.tcp..backlog +## See: listener.tcp.$name.backlog ## ## Value: Number >= 0 listener.tcp.internal.backlog = 512 ## The TCP send timeout for internal MQTT connections. ## -## See: listener.tcp..send_timeout +## See: listener.tcp.$name.send_timeout ## ## Value: Duration listener.tcp.internal.send_timeout = 5s ## Close the MQTT/TCP connection if send timeout. ## -## See: listener.tcp..send_timeout_close +## See: listener.tcp.$name.send_timeout_close ## ## Value: on | off listener.tcp.external.send_timeout_close = on ## The TCP receive buffer(os kernel) for internal MQTT connections. ## -## See: listener.tcp..recbuf +## See: listener.tcp.$name.recbuf ## ## Value: Bytes ## listener.tcp.internal.recbuf = 16KB @@ -892,21 +668,21 @@ listener.tcp.external.send_timeout_close = on ## The size of the user-level software buffer used by the driver. ## -## See: listener.tcp..buffer +## See: listener.tcp.$name.buffer ## ## Value: Bytes ## listener.tcp.internal.buffer = 16KB ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. ## -## See: listener.tcp..tune_buffer +## See: listener.tcp.$name.tune_buffer ## ## Value: on | off ## listener.tcp.internal.tune_buffer = off ## The TCP_NODELAY flag for internal MQTT connections. ## -## See: listener.tcp..nodelay +## See: listener.tcp.$name.nodelay ## ## Value: true | false listener.tcp.internal.nodelay = false @@ -919,7 +695,7 @@ listener.tcp.internal.reuseaddr = true ##-------------------------------------------------------------------- ## MQTT/SSL - External SSL Listener for MQTT Protocol -## listener.ssl. is the IP address and port that the MQTT/SSL +## listener.ssl.$name is the IP address and port that the MQTT/SSL ## listener will bind. ## ## Value: IP:Port | Port @@ -935,12 +711,12 @@ listener.ssl.external.acceptors = 16 ## Maximum number of concurrent MQTT/SSL connections. ## ## Value: Number -listener.ssl.external.max_clients = 102400 +listener.ssl.external.max_connections = 102400 ## Maximum MQTT/SSL connections per second. ## ## Value: Number -listener.ssl.external.max_conn_rate = 1000 +listener.ssl.external.max_conn_rate = 500 ## Zone of the external MQTT/SSL listener belonged to. ## @@ -950,31 +726,32 @@ listener.ssl.external.zone = external ## Mountpoint of the MQTT/SSL Listener. ## ## Value: String -## listener.ssl.external.mountpoint = inbound/ +## listener.ssl.external.mountpoint = devicebound/ ## The access control rules for the MQTT/SSL listener. ## -## See: listener.tcp..access +## See: listener.tcp.$name.access ## ## Value: ACL Rule listener.ssl.external.access.1 = allow all ## Rate limit for the external MQTT/SSL connections. ## -## Value: burst,rate -## listener.ssl.external.rate_limit = 100,10 +## Value: rate,burst +## Unit: Bps +## listener.ssl.external.rate_limit = 1024,4096 ## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind ## HAProxy or Nginx. ## -## See: listener.tcp..proxy_protocol +## See: listener.tcp.$name.proxy_protocol ## ## Value: on | off ## listener.ssl.external.proxy_protocol = on ## Sets the timeout for proxy protocol. ## -## See: listener.tcp..proxy_protocol_timeout +## See: listener.tcp.$name.proxy_protocol_timeout ## ## Value: Duration ## listener.ssl.external.proxy_protocol_timeout = 3s @@ -1088,64 +865,64 @@ listener.ssl.external.certfile = {{ platform_etc_dir }}/certs/cert.pem ## Value: on | off ## listener.ssl.external.honor_cipher_order = on -## Use the CN or DN value from the client certificate as a username. +## Use the CN field from the client certificate as a username. ## Notice that 'verify' should be set as 'verify_peer'. ## -## Value: cn | dn +## Value: boolean ## listener.ssl.external.peer_cert_as_username = cn ## TCP backlog for the SSL connection. ## -## See listener.tcp..backlog +## See listener.tcp.$name.backlog ## ## Value: Number >= 0 ## listener.ssl.external.backlog = 1024 ## The TCP send timeout for the SSL connection. ## -## See listener.tcp..send_timeout +## See listener.tcp.$name.send_timeout ## ## Value: Duration ## listener.ssl.external.send_timeout = 15s ## Close the SSL connection if send timeout. ## -## See: listener.tcp..send_timeout_close +## See: listener.tcp.$name.send_timeout_close ## ## Value: on | off ## listener.ssl.external.send_timeout_close = on ## The TCP receive buffer(os kernel) for the SSL connections. ## -## See: listener.tcp..recbuf +## See: listener.tcp.$name.recbuf ## ## Value: Bytes ## listener.ssl.external.recbuf = 4KB ## The TCP send buffer(os kernel) for internal MQTT connections. ## -## See: listener.tcp..sndbuf +## See: listener.tcp.$name.sndbuf ## ## Value: Bytes ## listener.ssl.external.sndbuf = 4KB ## The size of the user-level software buffer used by the driver. ## -## See: listener.tcp..buffer +## See: listener.tcp.$name.buffer ## ## Value: Bytes ## listener.ssl.external.buffer = 4KB ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. ## -## See: listener.tcp..tune_buffer +## See: listener.tcp.$name.tune_buffer ## ## Value: on | off ## listener.ssl.external.tune_buffer = off ## The TCP_NODELAY flag for SSL connections. ## -## See: listener.tcp..nodelay +## See: listener.tcp.$name.nodelay ## ## Value: true | false ## listener.ssl.external.nodelay = true @@ -1156,9 +933,9 @@ listener.ssl.external.certfile = {{ platform_etc_dir }}/certs/cert.pem listener.ssl.external.reuseaddr = true ##-------------------------------------------------------------------- -## External WebSocket Listener for MQTT Protocol +## External WebSocket listener for MQTT protocol -## listener.ws. is the IP address and port that the MQTT/WebSocket +## listener.ws.$name is the IP address and port that the MQTT/WebSocket ## listener will bind. ## ## Value: IP:Port | Port @@ -1174,13 +951,19 @@ listener.ws.external.acceptors = 4 ## Maximum number of concurrent MQTT/WebSocket connections. ## ## Value: Number -listener.ws.external.max_clients = 102400 +listener.ws.external.max_connections = 102400 ## Maximum MQTT/WebSocket connections per second. ## ## Value: Number listener.ws.external.max_conn_rate = 1000 +## Rate limit for the MQTT/WebSocket connections. +## +## Value: rate,burst +## Unit: Bps +## listener.ws.external.rate_limit = 1024,4096 + ## Zone of the external MQTT/WebSocket listener belonged to. ## ## Value: String @@ -1188,25 +971,30 @@ listener.ws.external.zone = external ## Mountpoint of the MQTT/WebSocket Listener. ## -## See: listener.tcp..mountpoint +## See: listener.tcp.$name.mountpoint ## ## Value: String -## listener.ws.external.mountpoint = external/ +## listener.ws.external.mountpoint = devicebound/ ## The access control for the MQTT/WebSocket listener. ## -## See: listener.tcp..access +## See: listener.tcp.$name.access ## ## Value: ACL Rule listener.ws.external.access.1 = allow all -## Use X-Forwarded-For header for real source IP if the EMQ cluster is +## Verify if the protocol header is valid. Turn off for WeChat MiniApp. +## +## Value: on | off +listener.ws.external.verify_protocol_header = on + +## Use X-Forwarded-For header for real source IP if the EMQ X cluster is ## deployed behind NGINX or HAProxy. ## ## Value: String ## listener.ws.external.proxy_address_header = X-Forwarded-For -## Use X-Forwarded-Port header for real source port if the EMQ cluster is +## Use X-Forwarded-Port header for real source port if the EMQ X cluster is ## deployed behind NGINX or HAProxy. ## ## Value: String @@ -1215,70 +1003,70 @@ listener.ws.external.access.1 = allow all ## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind ## HAProxy or Nginx. ## -## See: listener.tcp..proxy_protocol +## See: listener.tcp.$name.proxy_protocol ## ## Value: on | off ## listener.ws.external.proxy_protocol = on ## Sets the timeout for proxy protocol. ## -## See: listener.tcp..proxy_protocol_timeout +## See: listener.tcp.$name.proxy_protocol_timeout ## ## Value: Duration ## listener.ws.external.proxy_protocol_timeout = 3s ## The TCP backlog of external MQTT/WebSocket Listener. ## -## See: listener.tcp..backlog +## See: listener.tcp.$name.backlog ## ## Value: Number >= 0 listener.ws.external.backlog = 1024 ## The TCP send timeout for external MQTT/WebSocket connections. ## -## See: listener.tcp..send_timeout +## See: listener.tcp.$name.send_timeout ## ## Value: Duration listener.ws.external.send_timeout = 15s ## Close the MQTT/WebSocket connection if send timeout. ## -## See: listener.tcp..send_timeout_close +## See: listener.tcp.$name.send_timeout_close ## ## Value: on | off listener.ws.external.send_timeout_close = on ## The TCP receive buffer(os kernel) for external MQTT/WebSocket connections. ## -## See: listener.tcp..recbuf +## See: listener.tcp.$name.recbuf ## ## Value: Bytes -## listener.ws.external.recbuf = 4KB +## listener.ws.external.recbuf = 2KB ## The TCP send buffer(os kernel) for external MQTT/WebSocket connections. ## -## See: listener.tcp..sndbuf +## See: listener.tcp.$name.sndbuf ## ## Value: Bytes -## listener.ws.external.sndbuf = 4KB +## listener.ws.external.sndbuf = 2KB ## The size of the user-level software buffer used by the driver. ## -## See: listener.tcp..buffer +## See: listener.tcp.$name.buffer ## ## Value: Bytes -## listener.ws.external.buffer = 4KB +## listener.ws.external.buffer = 2KB ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. ## -## See: listener.tcp..tune_buffer +## See: listener.tcp.$name.tune_buffer ## ## Value: on | off ## listener.ws.external.tune_buffer = off ## The TCP_NODELAY flag for external MQTT/WebSocket connections. ## -## See: listener.tcp..nodelay +## See: listener.tcp.$name.nodelay ## ## Value: true | false listener.ws.external.nodelay = true @@ -1291,7 +1079,7 @@ listener.ws.external.reuseaddr = true ##-------------------------------------------------------------------- ## External WebSocket/SSL listener for MQTT Protocol -## listener.wss. is the IP address and port that the MQTT/WebSocket/SSL +## listener.wss.$name is the IP address and port that the MQTT/WebSocket/SSL ## listener will bind. ## ## Value: IP:Port | Port @@ -1307,13 +1095,21 @@ listener.wss.external.acceptors = 4 ## Maximum number of concurrent MQTT/Webwocket/SSL connections. ## ## Value: Number -listener.wss.external.max_clients = 64 +listener.wss.external.max_connections = 16 ## Maximum MQTT/WebSocket/SSL connections per second. ## +## See: listener.tcp.$name.max_conn_rate +## ## Value: Number listener.wss.external.max_conn_rate = 1000 +## Rate limit for the MQTT/WebSocket/SSL connections. +## +## Value: rate,burst +## Unit: Bps +## listener.wss.external.rate_limit = 1024,4096 + ## Zone of the external MQTT/WebSocket/SSL listener belonged to. ## ## Value: String @@ -1321,18 +1117,23 @@ listener.wss.external.zone = external ## Mountpoint of the MQTT/WebSocket/SSL Listener. ## -## See: listener.tcp..mountpoint +## See: listener.tcp.$name.mountpoint ## ## Value: String -## listener.wss.external.mountpoint = inbound/ +## listener.wss.external.mountpoint = devicebound/ ## The access control rules for the MQTT/WebSocket/SSL listener. ## -## See: listener.tcp..access. +## See: listener.tcp.$name.access. ## ## Value: ACL Rule listener.wss.external.access.1 = allow all +## See: listener.ws.external.verify_protocol_header +## +## Value: on | off +listener.wss.external.verify_protocol_header = on + ## See: listener.ws.external.proxy_address_header ## ## Value: String @@ -1345,138 +1146,138 @@ listener.wss.external.access.1 = allow all ## Enable the Proxy Protocol V1/2 support. ## -## See: listener.tcp..proxy_protocol +## See: listener.tcp.$name.proxy_protocol ## ## Value: on | off ## listener.wss.external.proxy_protocol = on ## Sets the timeout for proxy protocol. ## -## See: listener.tcp..proxy_protocol_timeout +## See: listener.tcp.$name.proxy_protocol_timeout ## ## Value: Duration ## listener.wss.external.proxy_protocol_timeout = 3s ## TLS versions only to protect from POODLE attack. ## -## See: listener.ssl..tls_versions +## See: listener.ssl.$name.tls_versions ## ## Value: String, seperated by ',' ## listener.wss.external.tls_versions = tlsv1.2,tlsv1.1,tlsv1 ## TLS Handshake timeout. ## -## See: listener.ssl..handshake_timeout +## See: listener.ssl.$name.handshake_timeout ## ## Value: Duration listener.wss.external.handshake_timeout = 15s ## Path to the file containing the user's private PEM-encoded key. ## -## See: listener.ssl..keyfile +## See: listener.ssl.$name.keyfile ## ## Value: File listener.wss.external.keyfile = {{ platform_etc_dir }}/certs/key.pem ## Path to a file containing the user certificate. ## -## See: listener.ssl..certfile +## See: listener.ssl.$name.certfile ## ## Value: File listener.wss.external.certfile = {{ platform_etc_dir }}/certs/cert.pem ## Path to the file containing PEM-encoded CA certificates. ## -## See: listener.ssl..cacert +## See: listener.ssl.$name.cacert ## ## Value: File ## listener.wss.external.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem -## See: listener.ssl..dhfile +## See: listener.ssl.$name.dhfile ## ## Value: File ## listener.ssl.external.dhfile = {{ platform_etc_dir }}/certs/dh-params.pem -## See: listener.ssl..vefify +## See: listener.ssl.$name.vefify ## ## Value: vefify_peer | verify_none ## listener.wss.external.verify = verify_peer -## See: listener.ssl..fail_if_no_peer_cert +## See: listener.ssl.$name.fail_if_no_peer_cert ## ## Value: false | true ## listener.wss.external.fail_if_no_peer_cert = true -## See: listener.ssl..ciphers +## See: listener.ssl.$name.ciphers ## ## Value: Ciphers ## listener.wss.external.ciphers = -## See: listener.ssl..secure_renegotiate +## See: listener.ssl.$name.secure_renegotiate ## ## Value: on | off ## listener.wss.external.secure_renegotiate = off -## See: listener.ssl..reuse_sessions +## See: listener.ssl.$name.reuse_sessions ## ## Value: on | off ## listener.wss.external.reuse_sessions = on -## See: listener.ssl..honor_cipher_order +## See: listener.ssl.$name.honor_cipher_order ## ## Value: on | off ## listener.wss.external.honor_cipher_order = on -## See: listener.ssl..peer_cert_as_username +## See: listener.ssl.$name.peer_cert_as_username ## ## Value: cn | dn ## listener.wss.external.peer_cert_as_username = cn ## TCP backlog for the WebSocket/SSL connection. ## -## See: listener.tcp..backlog +## See: listener.tcp.$name.backlog ## ## Value: Number >= 0 listener.wss.external.backlog = 1024 ## The TCP send timeout for the WebSocket/SSL connection. ## -## See: listener.tcp..send_timeout +## See: listener.tcp.$name.send_timeout ## ## Value: Duration listener.wss.external.send_timeout = 15s ## Close the WebSocket/SSL connection if send timeout. ## -## See: listener.tcp..send_timeout_close +## See: listener.tcp.$name.send_timeout_close ## ## Value: on | off listener.wss.external.send_timeout_close = on ## The TCP receive buffer(os kernel) for the WebSocket/SSL connections. ## -## See: listener.tcp..recbuf +## See: listener.tcp.$name.recbuf ## ## Value: Bytes ## listener.wss.external.recbuf = 4KB ## The TCP send buffer(os kernel) for the WebSocket/SSL connections. ## -## See: listener.tcp..sndbuf +## See: listener.tcp.$name.sndbuf ## ## Value: Bytes ## listener.wss.external.sndbuf = 4KB ## The size of the user-level software buffer used by the driver. ## -## See: listener.tcp..buffer +## See: listener.tcp.$name.buffer ## ## Value: Bytes ## listener.wss.external.buffer = 4KB ## The TCP_NODELAY flag for WebSocket/SSL connections. ## -## See: listener.tcp..nodelay +## See: listener.tcp.$name.nodelay ## ## Value: true | false ## listener.wss.external.nodelay = true @@ -1486,9 +1287,376 @@ listener.wss.external.send_timeout_close = on ## Value: true | false listener.wss.external.reuseaddr = true +##-------------------------------------------------------------------- +## Zones +##-------------------------------------------------------------------- + +##-------------------------------------------------------------------- +## External Zone + +## Idle timeout of the external MQTT connections. +## +## Value: duration +zone.external.idle_timeout = 15s + +## Publish limit for the external MQTT connections. +## +## Value: rate,burst +## Default: 10 messages per second, and 100 messages burst. +## zone.external.publish_limit = 10,100 + +## Enable ACL check. +## +## Value: Flag +zone.external.enable_acl = on + +## Enable per connection statistics. +## +## Value: on | off +zone.external.enable_stats = on + +## Maximum MQTT packet size allowed. +## +## Value: Bytes +## Default: 64KB +zone.external.max_packet_size = 1MB + +## Maximum length of MQTT clientId allowed. +## +## Value: Number [23-65535] +zone.external.max_clientid_len = 65535 + +## Maximum topic levels allowed. 0 means no limit. +## +## Value: Number +zone.external.max_topic_levels = 0 + +## Maximum QoS allowed. +## +## Value: 0 | 1 | 2 +zone.external.max_qos_allowed = 2 + +## Maximum Topic Alias, 0 means no limit. +## +## Value: 0-65535 +zone.external.max_topic_alias = 0 + +## Whether the Server supports retained messages. +## +## Value: boolean +zone.external.retain_available = true + +## Whether the Server supports Wildcard Subscriptions +## +## Value: boolean +zone.external.wildcard_subscription = true + +## Whether the Server supports Shared Subscriptions +## +## Value: boolean +zone.external.shared_subscription = true + +## The backoff for MQTT keepalive timeout. The broker will kick a connection out +## until 'Keepalive * backoff * 2' timeout. +## +## Value: Float > 0.5 +zone.external.keepalive_backoff = 0.75 + +## Maximum number of subscriptions allowed, 0 means no limit. +## +## Value: Number +zone.external.max_subscriptions = 0 + +## Force to upgrade QoS according to subscription. +## +## Value: on | off +zone.external.upgrade_qos = off + +## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. +## +## Value: Number +zone.external.max_inflight = 32 + +## Retry interval for QoS1/2 message delivering. +## +## Value: Duration +zone.external.retry_interval = 20s + +## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. +## +## Value: Number +zone.external.max_awaiting_rel = 100 + +## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. +## +## Value: Duration +zone.external.await_rel_timeout = 60s + +## Whether to ignore loop delivery of messages. +## +## Value: true | false +## Default: false +zone.external.ignore_loop_deliver = false + +## Default session expiry interval for MQTT V3.1.1 connections. +## +## Value: Duration +## -d: day +## -h: hour +## -m: minute +## -s: second +## +## Default: 2h, 2 hours +zone.external.session_expiry_interval = 2h + +## Maximum queue length. Enqueued messages when persistent client disconnected, +## or inflight window is full. 0 means no limit. +## +## Value: Number >= 0 +zone.external.max_mqueue_len = 1000 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.external.mqueue_store_qos0 = true + +##-------------------------------------------------------------------- +## Internal Zone + +## Enable per connection stats. +## +## Value: Flag +zone.internal.enable_stats = on + +## Enable ACL check. +## +## Value: Flag +zone.internal.enable_acl = off + +## See zone.$name.wildcard_subscription. +## +## Value: boolean +zone.internal.wildcard_subscription = true + +## See zone.$name.shared_subscription. +## +## Value: boolean +zone.internal.shared_subscription = true + +## See zone.$name.max_subscriptions. +## +## Value: Integer +zone.internal.max_subscriptions = 0 + +## See zone.$name.max_inflight +## +## Value: Number +zone.internal.max_inflight = 32 + +## See zone.$name.max_awaiting_rel +## +## Value: Number +zone.internal.max_awaiting_rel = 100 + +## See zone.$name.max_mqueue_len +## +## Value: Number >= 0 +zone.internal.max_mqueue_len = 1000 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.internal.mqueue_store_qos0 = true + +##-------------------------------------------------------------------- +## Bridges +##-------------------------------------------------------------------- + +## Bridge Type. +## +## Value: local | remote +bridge.name.type = local + +## Bridge address: node name for local bridge, host:port for remote. +## +## Value: String +## Example: emqx@127.0.0.1, 127.0.0.1:1883 +bridge.name.address = emqx@127.0.0.1 + +## Protocol version of the bridge. +## +## Value: Enum +## - mqtt5 +## - mqtt4 +## - mqtt3 +bridge.name.proto_ver = mqtt4 + +## The ClientId of a remote bridge. +## +## Value: String +bridge.name.client_id = bridge:$name + +## The Clean start flag of a remote bridge. +## +## Value: boolean +bridge.name.clean_start = false + +## The username for a remote bridge. +## +## Value: String +bridge.name.username = user + +## The password for a remote bridge. +## +## Value: String +bridge.name.password = passwd + +## Mountpoint of the bridge. +## +## Value: String +bridge.name.mountpoint = bridge/$name/ + +## PEM-encoded CA certificates of the bridge. +## +## Value: File +bridge.name.cacertfile = cacert.pem + +## SSL Certfile of the bridge. +## +## Value: File +bridge.name.certfile = cert.pem + +## SSL Keyfile of the bridge. +## +## Value: File +bridge.name.keyfile = key.pem + +## SSL Ciphers used by the bridge. +## +## Value: String +bridge.name.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384 + +## TLS versions used by the bridge. +## +## Value: String +bridge.name.tls_versions = tlsv1.2,tlsv1.1,tlsv1 + +## The pending message queue of a bridge. +## +## Value: Number +bridge.name.max_pending_messages = 10000 + +## Ping interval of a down bridge. +## +## Value: Duration +## Default: 10 seconds +bridge.name.keepalive = 10s + +## Subscriptions of the bridge. +## +## Default: 10 seconds +bridge.name.subscription.1.topic = topic1/ +bridge.name.subscription.1.qos = 2 +## bridge.name.subscription.2.topic = topic2/ +## bridge.name.subscription.2.qos = 2 + +##-------------------------------------------------------------------- +## Modules +##-------------------------------------------------------------------- + +##-------------------------------------------------------------------- +## Presence Module + +## Enable Presence Module. +## +## Value: on | off +module.presence = on + +## Sets the QoS for presence MQTT message. +## +## Value: 0 | 1 | 2 +module.presence.qos = 1 + +##-------------------------------------------------------------------- +## Subscription Module + +## Enable Subscription Module. +## +## Value: on | off +module.subscription = off + +## Subscribe the Topics automatically when client connected. +## module.subscription.1.topic = $client/%c +## Qos of the subscription: 0 | 1 | 2 +## module.subscription.1.qos = 1 + +## module.subscription.2.topic = $user/%u +## module.subscription.2.qos = 1 + +##-------------------------------------------------------------------- +## Rewrite Module + +## Enable Rewrite Module. +## +## Value: on | off +module.rewrite = off + +## {rewrite, Topic, Re, Dest} +## module.rewrite.rule.1 = x/# ^x/y/(.+)$ z/y/$1 +## module.rewrite.rule.2 = y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2 + ##------------------------------------------------------------------- +## Plugins +##------------------------------------------------------------------- + +## The etc dir for plugins' config. +## +## Value: Folder +plugins.etc_dir ={{ platform_etc_dir }}/plugins/ + +## The file to store loaded plugin names. +## +## Value: File +plugins.loaded_file = {{ platform_data_dir }}/loaded_plugins + +## File to store loaded plugin names. +plugins.expand_plugins_dir = {{ platform_plugins_dir }}/ + +##-------------------------------------------------------------------- +## Broker +##-------------------------------------------------------------------- + +## System interval of publishing $SYS messages. +## +## Value: Duration +## Default: 1m, 1 minute +broker.sys_interval = 1m + +## Session locking strategy in a cluster. +## +## Value: Enum +## - local +## - one +## - quorum +## - all +broker.session_locking_strategy = quorum + +## Dispatch strategy for shared subscription +## +## Value: Enum +## - random +## - round_robbin +## - hash +broker.shared_subscription_strategy = random + +## Enable batch clean for deleted routes. +## +## Value: Flag +broker.route_batch_clean = on + +##-------------------------------------------------------------------- ## System Monitor -##------------------------------------------------------------------- +##-------------------------------------------------------------------- ## Enable Long GC monitoring. ## Notice: don't enable the monitor in production for: diff --git a/priv/emqx.schema b/priv/emqx.schema index 7ff5fd0a3..3a3267930 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -165,10 +165,10 @@ end}. %%-------------------------------------------------------------------- -%% Erlang Node +%% Node %%-------------------------------------------------------------------- -%% @doc Erlang node name +%% @doc Node name {mapping, "node.name", "vm_args.-name", [ {default, "emqx@127.0.0.1"} ]}. @@ -321,7 +321,7 @@ end}. ]}. %%-------------------------------------------------------------------- -%% RPC Args +%% RPC %%-------------------------------------------------------------------- %% RPC server port. @@ -550,368 +550,39 @@ end}. ]}. %%-------------------------------------------------------------------- -%% Allow Anonymous and Default ACL +%% Authentication/ACL %%-------------------------------------------------------------------- -%% @doc Allow Anonymous -{mapping, "mqtt.allow_anonymous", "emqx.allow_anonymous", [ +%% @doc Allow anonymous authentication. +{mapping, "allow_anonymous", "emqx.allow_anonymous", [ {default, false}, {datatype, {enum, [true, false]}} ]}. -%% @doc ACL nomatch -{mapping, "mqtt.acl_nomatch", "emqx.acl_nomatch", [ - {default, allow}, - {datatype, {enum, [allow, deny]}} -]}. - -%% @doc Default ACL File -{mapping, "mqtt.acl_file", "emqx.acl_file", [ +%% @doc Default ACL file. +{mapping, "acl_file", "emqx.acl_file", [ {datatype, string}, hidden ]}. -%% @doc Cache ACL for PUBLISH -{mapping, "mqtt.cache_acl", "emqx.cache_acl", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -%%-------------------------------------------------------------------- -%% MQTT Protocol -%%-------------------------------------------------------------------- - -%% @doc Set the Max ClientId Length Allowed. -{mapping, "mqtt.max_clientid_len", "emqx.protocol", [ - {default, 1024}, - {datatype, integer} -]}. - -%% @doc Max Packet Size Allowed, 64K by default. -{mapping, "mqtt.max_packet_size", "emqx.protocol", [ - {default, "64KB"}, - {datatype, bytesize} -]}. - -%% @doc Keepalive backoff -{mapping, "mqtt.keepalive_backoff", "emqx.protocol", [ - {default, 1.25}, - {datatype, float} -]}. - -{translation, "emqx.protocol", fun(Conf) -> - [{max_clientid_len, cuttlefish:conf_get("mqtt.max_clientid_len", Conf)}, - {max_packet_size, cuttlefish:conf_get("mqtt.max_packet_size", Conf)}, - {keepalive_backoff, cuttlefish:conf_get("mqtt.keepalive_backoff", Conf)}] -end}. - -{mapping, "mqtt.websocket_protocol_header", "emqx.websocket_protocol_header", [ +%% @doc Enable ACL cache for publish. +{mapping, "enable_acl_cache", "emqx.enable_acl_cache", [ {default, on}, {datatype, flag} ]}. -{mapping, "mqtt.websocket_check_upgrade_header", "emqx.websocket_check_upgrade_header", [ - {default, on}, - {datatype, flag} -]}. - -%%-------------------------------------------------------------------- -%% MQTT Connection -%%-------------------------------------------------------------------- - -%% @doc Force the client to GC: integer -{mapping, "mqtt.conn.force_gc_count", "emqx.conn_force_gc_count", [ - {datatype, integer} -]}. - -%%-------------------------------------------------------------------- -%% MQTT Client -%%-------------------------------------------------------------------- - -%% @doc Max Publish Rate of Message -{mapping, "mqtt.client.max_publish_rate", "emqx.client", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Client Idle Timeout. -{mapping, "mqtt.client.idle_timeout", "emqx.client", [ - {default, "30s"}, +%% @doc ACL cache age. +{mapping, "acl_cache_age", "emqx.acl_cache_age", [ + {default, "5m"}, {datatype, {duration, ms}} ]}. -%% @doc Enable Stats of Client. -{mapping, "mqtt.client.enable_stats", "emqx.client", [ - {default, off}, - {datatype, flag} -]}. - -{translation, "emqx.client", fun(Conf) -> - [{max_publish_rate, cuttlefish:conf_get("mqtt.client.max_publish_rate", Conf)}, - {client_idle_timeout, cuttlefish:conf_get("mqtt.client.idle_timeout", Conf)}, - {client_enable_stats, cuttlefish:conf_get("mqtt.client.enable_stats", Conf)}] -end}. - -%%-------------------------------------------------------------------- -%% MQTT Session -%%-------------------------------------------------------------------- - -%% @doc Max Number of Subscriptions Allowed -{mapping, "mqtt.session.max_subscriptions", "emqx.session", [ +%% @doc ACL cache size. +{mapping, "acl_cache_size", "emqx.acl_cache_size", [ {default, 0}, {datatype, integer} ]}. -%% @doc Upgrade QoS? -{mapping, "mqtt.session.upgrade_qos", "emqx.session", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Max number of QoS 1 and 2 messages that can be “inflight” at one time. -%% 0 means no limit -{mapping, "mqtt.session.max_inflight", "emqx.session", [ - {default, 100}, - {datatype, integer} -]}. - -%% @doc Retry interval for redelivering QoS1/2 messages. -{mapping, "mqtt.session.retry_interval", "emqx.session", [ - {default, "20s"}, - {datatype, {duration, ms}} -]}. - -%% @doc Max Packets that Awaiting PUBREL, 0 means no limit -{mapping, "mqtt.session.max_awaiting_rel", "emqx.session", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Awaiting PUBREL Timeout -{mapping, "mqtt.session.await_rel_timeout", "emqx.session", [ - {default, "20s"}, - {datatype, {duration, ms}} -]}. - -%% @doc Enable Stats -{mapping, "mqtt.session.enable_stats", "emqx.session", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Session Expiry Interval -{mapping, "mqtt.session.expiry_interval", "emqx.session", [ - {default, "2h"}, - {datatype, {duration, ms}} -]}. - -%% @doc Ignore message from self publish -{mapping, "mqtt.session.ignore_loop_deliver", "emqx.session", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx.session", fun(Conf) -> - [{max_subscriptions, cuttlefish:conf_get("mqtt.session.max_subscriptions", Conf)}, - {upgrade_qos, cuttlefish:conf_get("mqtt.session.upgrade_qos", Conf)}, - {max_inflight, cuttlefish:conf_get("mqtt.session.max_inflight", Conf)}, - {retry_interval, cuttlefish:conf_get("mqtt.session.retry_interval", Conf)}, - {max_awaiting_rel, cuttlefish:conf_get("mqtt.session.max_awaiting_rel", Conf)}, - {await_rel_timeout, cuttlefish:conf_get("mqtt.session.await_rel_timeout", Conf)}, - {enable_stats, cuttlefish:conf_get("mqtt.session.enable_stats", Conf)}, - {expiry_interval, cuttlefish:conf_get("mqtt.session.expiry_interval", Conf)}, - {ignore_loop_deliver, cuttlefish:conf_get("mqtt.session.ignore_loop_deliver", Conf)}] -end}. - -%%-------------------------------------------------------------------- -%% MQTT MQueue -%%-------------------------------------------------------------------- - -%% @doc Type: simple | priority -{mapping, "mqtt.mqueue.type", "emqx.mqueue", [ - {default, simple}, - {datatype, atom} -]}. - -%% @doc Topic Priority: 0~255, Default is 0 -{mapping, "mqtt.mqueue.priority", "emqx.mqueue", [ - {default, ""}, - {datatype, string} -]}. - -%% @doc Max queue length. Enqueued messages when persistent client disconnected, or inflight window is full. 0 means no limit. -{mapping, "mqtt.mqueue.max_length", "emqx.mqueue", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Low-water mark of queued messages -{mapping, "mqtt.mqueue.low_watermark", "emqx.mqueue", [ - {default, "20%"}, - {datatype, {percent, float}} -]}. - -%% @doc High-water mark of queued messages -{mapping, "mqtt.mqueue.high_watermark", "emqx.mqueue", [ - {default, "60%"}, - {datatype, {percent, float}} -]}. - -%% @doc Queue Qos0 messages? -{mapping, "mqtt.mqueue.store_qos0", "emqx.mqueue", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx.mqueue", fun(Conf) -> - Opts = [{type, cuttlefish:conf_get("mqtt.mqueue.type", Conf, simple)}, - {max_length, cuttlefish:conf_get("mqtt.mqueue.max_length", Conf)}, - {low_watermark, cuttlefish:conf_get("mqtt.mqueue.low_watermark", Conf)}, - {high_watermark, cuttlefish:conf_get("mqtt.mqueue.high_watermark", Conf)}, - {store_qos0, cuttlefish:conf_get("mqtt.mqueue.store_qos0", Conf)}], - case cuttlefish:conf_get("mqtt.mqueue.priority", Conf) of - undefined -> Opts; - V -> [{priority, - [begin [T, P] = string:tokens(S, "="), - {T, list_to_integer(P)} - end || S <- string:tokens(V, ",")]} | Opts] - end -end}. - -%%-------------------------------------------------------------------- -%% MQTT Broker -%%-------------------------------------------------------------------- - -{mapping, "mqtt.broker.sys_interval", "emqx.broker_sys_interval", [ - {datatype, {duration, ms}}, - {default, "1m"} -]}. - -%%-------------------------------------------------------------------- -%% MQTT PubSub -%%-------------------------------------------------------------------- - -{mapping, "mqtt.pubsub.pool_size", "emqx.pubsub", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "mqtt.pubsub.async", "emqx.pubsub", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx.pubsub", fun(Conf) -> - [{pool_size, cuttlefish:conf_get("mqtt.pubsub.pool_size", Conf)}, - {async, cuttlefish:conf_get("mqtt.pubsub.async", Conf)}] -end}. - -%%-------------------------------------------------------------------- -%% MQTT Bridge -%%-------------------------------------------------------------------- - -{mapping, "mqtt.bridge.max_queue_len", "emqx.bridge", [ - {default, 10000}, - {datatype, integer} -]}. - -{mapping, "mqtt.bridge.ping_down_interval", "emqx.bridge", [ - {datatype, {duration, ms}}, - {default, "1s"} -]}. - -{translation, "emqx.bridge", fun(Conf) -> - [{max_queue_len, cuttlefish:conf_get("mqtt.bridge.max_queue_len", Conf)}, - {ping_down_interval, cuttlefish:conf_get("mqtt.bridge.ping_down_interval", Conf)}] -end}. - -%%------------------------------------------------------------------- -%% Plugins -%%------------------------------------------------------------------- - -{mapping, "mqtt.plugins.etc_dir", "emqx.plugins_etc_dir", [ - {datatype, string} -]}. - -{mapping, "mqtt.plugins.loaded_file", "emqx.plugins_loaded_file", [ - {datatype, string} -]}. - -{mapping, "mqtt.plugins.expand_plugins_dir", "emqx.expand_plugins_dir", [ - {datatype, string} -]}. - -%%-------------------------------------------------------------------- -%% Modules -%%-------------------------------------------------------------------- - -{mapping, "module.presence", "emqx.modules", [ - {default, off}, - {datatype, flag} -]}. - -{mapping, "module.presence.qos", "emqx.modules", [ - {default, 1}, - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -{mapping, "module.subscription", "emqx.modules", [ - {default, off}, - {datatype, flag} -]}. - -{mapping, "module.subscription.$id.topic", "emqx.modules", [ - {datatype, string} -]}. - -{mapping, "module.subscription.$id.qos", "emqx.modules", [ - {default, 1}, - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -{mapping, "module.rewrite", "emqx.modules", [ - {default, off}, - {datatype, flag} -]}. - -{mapping, "module.rewrite.rule.$id", "emqx.modules", [ - {datatype, string} -]}. - -{translation, "emqx.modules", fun(Conf) -> - Subscriptions = fun() -> - List = cuttlefish_variable:filter_by_prefix("module.subscription", Conf), - QosList = [Qos || {_, Qos} <- lists:sort([{I, Qos} || {[_,"subscription", I,"qos"], Qos} <- List])], - TopicList = [iolist_to_binary(Topic) || {_, Topic} <- - lists:sort([{I, Topic} || {[_,"subscription", I, "topic"], Topic} <- List])], - lists:zip(TopicList, QosList) - end, - Rewrites = fun() -> - Rules = cuttlefish_variable:filter_by_prefix("module.rewrite.rule", Conf), - lists:map(fun({[_, "rewrite", "rule", I], Rule}) -> - [Topic, Re, Dest] = string:tokens(Rule, " "), - {rewrite, list_to_binary(Topic), list_to_binary(Re), list_to_binary(Dest)} - end, Rules) - end, - lists:append([ - case cuttlefish:conf_get("module.presence", Conf) of %% Presence - true -> [{emqx_mod_presence, [{qos, cuttlefish:conf_get("module.presence.qos", Conf, 1)}]}]; - false -> [] - end, - case cuttlefish:conf_get("module.subscription", Conf) of %% Subscription - true -> [{emqx_mod_subscription, Subscriptions()}]; - false -> [] - end, - case cuttlefish:conf_get("module.rewrite", Conf) of %% Rewrite - true -> [{emqx_mod_rewrite, Rewrites()}]; - false -> [] - end - ]) -end}. - - %%-------------------------------------------------------------------- %% Listeners %%-------------------------------------------------------------------- @@ -928,7 +599,7 @@ end}. {datatype, integer} ]}. -{mapping, "listener.tcp.$name.max_clients", "emqx.listeners", [ +{mapping, "listener.tcp.$name.max_connections", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. @@ -963,7 +634,8 @@ end}. ]}. {mapping, "listener.tcp.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn, dn]}} + {default, false}, + {datatype, {enum, [true, false]}} ]}. {mapping, "listener.tcp.$name.backlog", "emqx.listeners", [ @@ -1023,7 +695,7 @@ end}. {datatype, integer} ]}. -{mapping, "listener.ssl.$name.max_clients", "emqx.listeners", [ +{mapping, "listener.ssl.$name.max_connections", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. @@ -1168,7 +840,7 @@ end}. {datatype, integer} ]}. -{mapping, "listener.ws.$name.max_clients", "emqx.listeners", [ +{mapping, "listener.ws.$name.max_connections", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. @@ -1185,10 +857,20 @@ end}. {datatype, string} ]}. +{mapping, "listener.ws.$name.rate_limit", "emqx.listeners", [ + {default, undefined}, + {datatype, string} +]}. + {mapping, "listener.ws.$name.access.$id", "emqx.listeners", [ {datatype, string} ]}. +{mapping, "listener.ws.$name.verify_protocol_header", "emqx.listeners", [ + {default, on}, + {datatype, flag} +]}. + {mapping, "listener.ws.$name.proxy_address_header", "emqx.listeners", [ {datatype, string}, hidden @@ -1264,7 +946,7 @@ end}. {datatype, integer} ]}. -{mapping, "listener.wss.$name.max_clients", "emqx.listeners", [ +{mapping, "listener.wss.$name.max_connections", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. @@ -1285,6 +967,11 @@ end}. {datatype, string} ]}. +{mapping, "listener.wss.$name.verify_protocol_header", "emqx.listeners", [ + {default, on}, + {datatype, flag} +]}. + {mapping, "listener.wss.$name.access.$id", "emqx.listeners", [ {datatype, string} ]}. @@ -1422,16 +1109,23 @@ end}. MountPoint = fun(undefined) -> undefined; (S) -> list_to_binary(S) end, + Ratelimit = fun(undefined) -> + undefined; + (S) -> + list_to_tuple([list_to_integer(Token) || Token <- string:tokens(S, ",")]) + end, + LisOpts = fun(Prefix) -> Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, - {max_clients, cuttlefish:conf_get(Prefix ++ ".max_clients", Conf)}, + {max_connections, cuttlefish:conf_get(Prefix ++ ".max_connections", Conf)}, {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, {zone, Atom(cuttlefish:conf_get(Prefix ++ ".zone", Conf, undefined))}, - %%{rate_limit, cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined)}, + {rate_limit, Ratelimit(cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined))}, {proxy_protocol, cuttlefish:conf_get(Prefix ++ ".proxy_protocol", Conf, undefined)}, {proxy_protocol_timeout, cuttlefish:conf_get(Prefix ++ ".proxy_protocol_timeout", Conf, undefined)}, {mountpoint, MountPoint(cuttlefish:conf_get(Prefix ++ ".mountpoint", Conf, undefined))}, + {verify_protocol_header, cuttlefish:conf_get(Prefix ++ ".verify_protocol_header", Conf, undefined)}, {peer_cert_as_username, cuttlefish:conf_get(Prefix ++ ".peer_cert_as_username", Conf, undefined)}, {proxy_port_header, cuttlefish:conf_get(Prefix ++ ".proxy_port_header", Conf, undefined)}, {proxy_address_header, cuttlefish:conf_get(Prefix ++ ".proxy_address_header", Conf, undefined)} | AccOpts(Prefix)]) @@ -1488,124 +1182,406 @@ end}. end end, - ApiListeners = fun(Type, Name) -> - Prefix = string:join(["listener", Type, Name], "."), - case cuttlefish:conf_get(Prefix, Conf, undefined) of - undefined -> - []; - ListenOn -> - SslOpts1 = case SslOpts(Prefix) of [] -> []; SslOpts0 -> [{ssl_options, SslOpts0}] end, - [{Atom(Type), ListenOn, [{tcp_options, TcpOpts(Prefix)}|LisOpts(Prefix)] ++ SslOpts1}] - end - end, - - lists:flatten([TcpListeners(Type, Name) || {["listener", Type, Name], ListenOn} <- cuttlefish_variable:filter_by_prefix("listener.tcp", Conf) ++ cuttlefish_variable:filter_by_prefix("listener.ws", Conf)] ++ [SslListeners(Type, Name) || {["listener", Type, Name], ListenOn} <- cuttlefish_variable:filter_by_prefix("listener.ssl", Conf) - ++ cuttlefish_variable:filter_by_prefix("listener.wss", Conf)] - ++ - [ApiListeners(Type, Name) || {["listener", Type, Name], ListenOn} - <- cuttlefish_variable:filter_by_prefix("listener.api", Conf)]) + ++ cuttlefish_variable:filter_by_prefix("listener.wss", Conf)]) end}. %%-------------------------------------------------------------------- -%% MQTT REST API Listeners +%% Zones +%%-------------------------------------------------------------------- -{mapping, "listener.api.$name", "emqx.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "listener.api.$name.acceptors", "emqx.listeners", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "listener.api.$name.max_clients", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.api.$name.rate_limit", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.api.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.api.$name.backlog", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.api.$name.send_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "listener.api.$name.send_timeout_close", "emqx.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "listener.api.$name.recbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.api.$name.sndbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.api.$name.buffer", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.api.$name.tune_buffer", "emqx.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "listener.api.$name.nodelay", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.api.$name.reuseaddr", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.api.$name.handshake_timeout", "emqx.listeners", [ +%% @doc Idle timeout of the MQTT connection. +{mapping, "zone.$name.idle_timeout", "emqx.zones", [ + {default, "15s"}, {datatype, {duration, ms}} ]}. -{mapping, "listener.api.$name.keyfile", "emqx.listeners", [ +%% @doc Enable ACL check. +{mapping, "zone.$name.enable_acl", "emqx.zones", [ + {default, off}, + {datatype, flag} +]}. + +%% @doc Enable per connection statistics. +{mapping, "zone.$name.enable_stats", "emqx.zones", [ + {default, off}, + {datatype, flag} +]}. + +%% @doc Publish limit of the MQTT connections. +{mapping, "zone.$name.publish_limit", "emqx.zones", [ + {default, undefined}, {datatype, string} ]}. -{mapping, "listener.api.$name.certfile", "emqx.listeners", [ - {datatype, string} +%% @doc Max Packet Size Allowed, 64K by default. +{mapping, "zone.$name.max_packet_size", "emqx.zones", [ + {default, "64KB"}, + {datatype, bytesize} ]}. -{mapping, "listener.api.$name.cacertfile", "emqx.listeners", [ - {datatype, string} +%% @doc Set the Max ClientId Length Allowed. +{mapping, "zone.$name.max_clientid_len", "emqx.zones", [ + {default, 65535}, + {datatype, integer} ]}. -{mapping, "listener.api.$name.verify", "emqx.listeners", [ - {datatype, atom} +%% @doc Set the Maximum topic levels. +{mapping, "zone.$name.max_topic_levels", "emqx.zones", [ + {default, 0}, + {datatype, integer} ]}. -{mapping, "listener.api.$name.fail_if_no_peer_cert", "emqx.listeners", [ +%% @doc Set the Maximum QoS allowed. +{mapping, "zone.$name.max_qos_allowed", "emqx.zones", [ + {default, 2}, + {datatype, integer}, + {validators, ["range:0-2"]} +]}. + +%% @doc Set the Maximum topic alias. +{mapping, "zone.$name.max_topic_alias", "emqx.zones", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Whether the server supports retained messages. +{mapping, "zone.$name.retain_available", "emqx.zones", [ + {default, true}, {datatype, {enum, [true, false]}} ]}. +%% @doc Whether the Server supports Wildcard Subscriptions. +{mapping, "zone.$name.wildcard_subscription", "emqx.zones", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +%% @doc Whether the Server supports Shared Subscriptions. +{mapping, "zone.$name.shared_subscription", "emqx.zones", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +%% @doc Keepalive backoff +{mapping, "zone.$name.keepalive_backoff", "emqx.zones", [ + {default, 0.75}, + {datatype, float} +]}. + +%% @doc Max Number of Subscriptions Allowed. +{mapping, "zone.$name.max_subscriptions", "emqx.zones", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Upgrade QoS according to subscription? +{mapping, "zone.$name.upgrade_qos", "emqx.zones", [ + {default, off}, + {datatype, flag} +]}. + +%% @doc Max number of QoS 1 and 2 messages that can be “inflight” at one time. +%% 0 means no limit +{mapping, "zone.$name.max_inflight", "emqx.zones", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Retry interval for redelivering QoS1/2 messages. +{mapping, "zone.$name.retry_interval", "emqx.zones", [ + {default, "20s"}, + {datatype, {duration, ms}} +]}. + +%% @doc Max Packets that Awaiting PUBREL, 0 means no limit +{mapping, "zone.$name.max_awaiting_rel", "emqx.zones", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Awaiting PUBREL timeout +{mapping, "zone.$name.await_rel_timeout", "emqx.zones", [ + {default, "60s"}, + {datatype, {duration, ms}} +]}. + +%% @doc Ignore message from self publish +{mapping, "zone.$name.ignore_loop_deliver", "emqx.zones", [ + {default, false}, + {datatype, {enum, [true, false]}} +]}. + +%% @doc Session Expiry Interval +{mapping, "zone.$name.session_expiry_interval", "emqx.zones", [ + {default, "2h"}, + {datatype, {duration, ms}} +]}. + +%% @doc Max queue length. Enqueued messages when persistent client +%% disconnected, or inflight window is full. 0 means no limit. +{mapping, "zone.$name.max_mqueue_len", "emqx.zones", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Queue Qos0 messages? +{mapping, "zone.$name.mqueue_store_qos0", "emqx.zones", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +{translation, "emqx.zones", fun(Conf) -> + maps:to_list( + lists:foldl( + fun({["zone", Name, Opt], Val}, Acc) -> + ZName = list_to_atom(Name), + case maps:find(ZName, Acc) of + {ok, Opts} -> + maps:put(ZName, [{list_to_atom(Opt), Val} | Opts], Acc); + error -> + maps:put(ZName, [{list_to_atom(Opt), Val}], Acc) + end + end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("zone.", Conf)))) +end}. + +%%-------------------------------------------------------------------- +%% Bridges +%%-------------------------------------------------------------------- + +{mapping, "bridge.$name.type", "emqx.bridges", [ + {default, local}, + {datatype, {enum, [local,remote]}} +]}. + +{mapping, "bridge.$name.address", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.proto_ver", "emqx.bridges", [ + {datatype, {enum, [mqtt3, mqtt4, mqtt5]}} +]}. + +{mapping, "bridge.$name.client_id", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.clean_start", "emqx.bridges", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +{mapping, "bridge.$name.username", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.password", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.mountpoint", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.cacertfile", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.certfile", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.keyfile", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.ciphers", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.max_pending_messages", "emqx.bridges", [ + {default, 10000}, + {datatype, integer} +]}. + +{mapping, "bridge.$name.keepalive", "emqx.bridges", [ + {default, "10s"}, + {datatype, {duration, s}} +]}. + +{mapping, "bridge.$name.tls_versions", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.subscription.$id.topic", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.subscription.$id.qos", "emqx.bridges", [ + {datatype, integer} +]}. + +{translation, "emqx.bridges", fun(Conf) -> + + Split = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end, + + IsSsl = fun(cacertfile) -> true; + (certfile) -> true; + (keyfile) -> true; + (ciphers) -> true; + (tls_versions) -> true; + (_Opt) -> false + end, + + Parse = fun(tls_versions, Vers) -> + {versions, [list_to_atom(S) || S <- Split(Vers)]}; + (ciphers, Ciphers) -> + {ciphers, Split(Ciphers)}; + (Opt, Val) -> + {Opt, Val} + end, + + Merge = fun(Opt, Val, Opts) -> + case IsSsl(Opt) of + true -> + SslOpts = [Parse(Opt, Val)|proplists:get_value(ssl_opts, Opts, [])], + lists:ukeymerge(1, [{ssl_opts, SslOpts}], Opts); + false -> + [{Opt, Val}|Opts] + end + end, + Subscriptions = fun(Name) -> + Configs = cuttlefish_variable:filter_by_prefix("bridge." ++ Name ++ ".subscription", Conf), + lists:zip([Topic || {_, Topic} <- lists:sort([{I, Topic} || {[_, _, "subscription", I, "topic"], Topic} <- Configs])], + [QoS || {_, QoS} <- lists:sort([{I, QoS} || {[_, _, "subscription", I, "qos"], QoS} <- Configs])]) + end, + + maps:to_list( + lists:foldl( + fun({["bridge", Name, Opt], Val}, Acc) -> + maps:update_with(list_to_atom(Name), + fun(Opts) -> + Merge(list_to_atom(Opt), Val, Opts) + end, [{subscriptions, Subscriptions(Name)}], Acc); + (_, Acc) -> Acc + end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("bridge.", Conf)))) +end}. + +%%-------------------------------------------------------------------- +%% Modules +%%-------------------------------------------------------------------- + +{mapping, "module.presence", "emqx.modules", [ + {default, off}, + {datatype, flag} +]}. + +{mapping, "module.presence.qos", "emqx.modules", [ + {default, 1}, + {datatype, integer}, + {validators, ["range:0-2"]} +]}. + +{mapping, "module.subscription", "emqx.modules", [ + {default, off}, + {datatype, flag} +]}. + +{mapping, "module.subscription.$id.topic", "emqx.modules", [ + {datatype, string} +]}. + +{mapping, "module.subscription.$id.qos", "emqx.modules", [ + {default, 1}, + {datatype, integer}, + {validators, ["range:0-2"]} +]}. + +{mapping, "module.rewrite", "emqx.modules", [ + {default, off}, + {datatype, flag} +]}. + +{mapping, "module.rewrite.rule.$id", "emqx.modules", [ + {datatype, string} +]}. + +{translation, "emqx.modules", fun(Conf) -> + Subscriptions = fun() -> + List = cuttlefish_variable:filter_by_prefix("module.subscription", Conf), + QosList = [Qos || {_, Qos} <- lists:sort([{I, Qos} || {[_,"subscription", I,"qos"], Qos} <- List])], + TopicList = [iolist_to_binary(Topic) || {_, Topic} <- + lists:sort([{I, Topic} || {[_,"subscription", I, "topic"], Topic} <- List])], + lists:zip(TopicList, QosList) + end, + Rewrites = fun() -> + Rules = cuttlefish_variable:filter_by_prefix("module.rewrite.rule", Conf), + lists:map(fun({[_, "rewrite", "rule", I], Rule}) -> + [Topic, Re, Dest] = string:tokens(Rule, " "), + {rewrite, list_to_binary(Topic), list_to_binary(Re), list_to_binary(Dest)} + end, Rules) + end, + lists:append([ + case cuttlefish:conf_get("module.presence", Conf) of %% Presence + true -> [{emqx_mod_presence, [{qos, cuttlefish:conf_get("module.presence.qos", Conf, 1)}]}]; + false -> [] + end, + case cuttlefish:conf_get("module.subscription", Conf) of %% Subscription + true -> [{emqx_mod_subscription, Subscriptions()}]; + false -> [] + end, + case cuttlefish:conf_get("module.rewrite", Conf) of %% Rewrite + true -> [{emqx_mod_rewrite, Rewrites()}]; + false -> [] + end + ]) +end}. + +%%------------------------------------------------------------------- +%% Plugins +%%------------------------------------------------------------------- + +{mapping, "plugins.etc_dir", "emqx.plugins_etc_dir", [ + {datatype, string} +]}. + +{mapping, "plugins.loaded_file", "emqx.plugins_loaded_file", [ + {datatype, string} +]}. + +{mapping, "plugins.expand_plugins_dir", "emqx.expand_plugins_dir", [ + {datatype, string} +]}. + +%%-------------------------------------------------------------------- +%% Broker +%%-------------------------------------------------------------------- + +{mapping, "broker.sys_interval", "emqx.broker_sys_interval", [ + {datatype, {duration, ms}}, + {default, "1m"} +]}. + +{mapping, "broker.session_locking_strategy", "emqx.session_locking_strategy", [ + {default, quorum}, + {datatype, {enum, [local,one,quorum,all]}} +]}. + +{mapping, "broker.shared_subscription_strategy", "emqx.shared_subscription_strategy", [ + {default, random}, + {datatype, {enum, [random, round_robbin, hash]}} +]}. + +{mapping, "broker.route_batch_clean", "emqx.route_batch_clean", [ + {default, on}, + {datatype, flag} +]}. + %%-------------------------------------------------------------------- %% System Monitor %%-------------------------------------------------------------------- @@ -1642,10 +1618,10 @@ end}. ]}. {translation, "emqx.sysmon", fun(Conf) -> - [{long_gc, cuttlefish:conf_get("sysmon.long_gc", Conf)}, - {long_schedule, cuttlefish:conf_get("sysmon.long_schedule", Conf)}, - {large_heap, cuttlefish:conf_get("sysmon.large_heap", Conf)}, - {busy_port, cuttlefish:conf_get("sysmon.busy_port", Conf)}, - {busy_dist_port, cuttlefish:conf_get("sysmon.busy_dist_port", Conf)}] + [{long_gc, cuttlefish:conf_get("sysmon.long_gc", Conf)}, + {long_schedule, cuttlefish:conf_get("sysmon.long_schedule", Conf)}, + {large_heap, cuttlefish:conf_get("sysmon.large_heap", Conf)}, + {busy_port, cuttlefish:conf_get("sysmon.busy_port", Conf)}, + {busy_dist_port, cuttlefish:conf_get("sysmon.busy_dist_port", Conf)}] end}. From 4005d58166f1edabc45715b13229a05d91b4c316 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 8 Aug 2018 19:31:25 +0800 Subject: [PATCH 083/520] Move the 'rate_limit' option from zone to listener --- src/emqx_connection.erl | 17 +++++++++-------- src/emqx_frame.erl | 1 - src/emqx_protocol.erl | 1 + 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 7dc70e6ce..89233fb43 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -41,7 +41,7 @@ await_recv, %% Awaiting recv incoming, %% Incoming bytes and packets pub_limit, %% Publish rate limit - rate_limit, %% Throughput rate limit + rate_limit, %% Traffic rate limit limit_timer, %% Rate limit timer proto_state, %% MQTT protocol state parse_state, %% MQTT parse state @@ -56,7 +56,7 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). -define(LOG(Level, Format, Args, State), - emqx_logger:Level("Conn(~s): " ++ Format, + emqx_logger:Level("Client(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). start_link(Transport, Socket, Options) -> @@ -100,11 +100,12 @@ set_pub_limit(CPid, Rl = {_Rate, _Burst}) -> init([Transport, RawSocket, Options]) -> case Transport:wait(RawSocket) of {ok, Socket} -> + io:format("Options: ~p~n", [Options]), {ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]), {ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]), Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]), Zone = proplists:get_value(zone, Options), - RateLimit = init_rate_limit(emqx_zone:get_env(Zone, rate_limit)), + RateLimit = init_rate_limit(proplists:get_value(rate_limit, Options)), PubLimit = init_rate_limit(emqx_zone:get_env(Zone, publish_limit)), EnableStats = emqx_zone:get_env(Zone, enable_stats, false), IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), @@ -194,16 +195,16 @@ handle_cast(Msg, State) -> ?LOG(error, "unexpected cast: ~p", [Msg], State), {noreply, State}. -handle_info(SubReq = {subscribe, _TopicTable}, State) -> +handle_info(Sub = {subscribe, _TopicTable}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:process(SubReq, ProtoState) + emqx_protocol:process(Sub, ProtoState) end, State); -handle_info(UnsubReq = {unsubscribe, _Topics}, State) -> +handle_info(Unsub = {unsubscribe, _Topics}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:process(UnsubReq, ProtoState) + emqx_protocol:process(Unsub, ProtoState) end, State); handle_info({deliver, PubOrAck}, State) -> @@ -300,7 +301,7 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -%% Receive and parse TCP data +%% Receive and parse data handle_packet(<<>>, State) -> {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 7385b7116..10498afcf 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -162,7 +162,6 @@ parse_packet(#mqtt_packet_header{type = ?PUBLISH, qos = QoS}, Bin, ?QOS_0 -> {undefined, Rest}; _ -> parse_packet_id(Rest) end, - io:format("Rest1: ~p~n", [Rest1]), {Properties, Payload} = parse_properties(Rest1, Ver), {#mqtt_packet_publish{topic_name = TopicName, packet_id = PacketId, diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 85fe35e52..9a13c6538 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -19,6 +19,7 @@ -include("emqx_misc.hrl"). -export([init/2, info/1, stats/1, clientid/1, session/1]). +%%-export([capabilities/1]). -export([parser/1]). -export([received/2, process/2, deliver/2, send/2]). -export([shutdown/2]). From 854132d0c3c4d7433ead3238bafab792bdb66537 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 9 Aug 2018 09:13:30 +0800 Subject: [PATCH 084/520] Add max_conn_rate, handshake_timeout options for wss listeners --- priv/emqx.schema | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/priv/emqx.schema b/priv/emqx.schema index 7787a28f9..425c0f4f3 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -845,6 +845,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.ws.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.ws.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -942,6 +946,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.wss.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.wss.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -981,6 +989,11 @@ end}. {datatype, {duration, ms}} ]}. +{mapping, "listener.wss.$name.handshake_timeout", "emqx.listeners", [ + {default, "15s"}, + {datatype, {duration, ms}} +]}. + {mapping, "listener.wss.$name.backlog", "emqx.listeners", [ {default, 1024}, {datatype, integer} From 18116ac3b5be93669e251d55edefc5e9c22c7162 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 9 Aug 2018 09:32:56 +0800 Subject: [PATCH 085/520] Remove etc/zone.conf --- etc/zone.conf | 126 -------------------------------------------------- 1 file changed, 126 deletions(-) delete mode 100644 etc/zone.conf diff --git a/etc/zone.conf b/etc/zone.conf deleted file mode 100644 index 5c546fe46..000000000 --- a/etc/zone.conf +++ /dev/null @@ -1,126 +0,0 @@ - -## Limits and Capabilities - -##-------------------------------------------------------------------- -## Connection - -zone.${name}.idle_timeout = 30s - -zone.${name}.rate_limit = 10,100 - -## 10 messages per second, with a bucket 100 messages. -zone.${name}.publish_limit = 10,100 - -## Enable stats -zone.${name}.enable_stats = on - -## zone.${name}.shutdown_policy = ??? - -##-------------------------------------------------------------------- -## Protocol - -## Capabilities: - -## Maximum length of MQTT clientId allowed. -## -## Value: Number [23-65535] -zone.${name}.max_clientid_len = 1024 - -## Maximum MQTT packet size allowed. -## -## Value: Bytes -## -## Default: 64K -zone.${name}.max_packet_size = 64K -zone.${name}.max_topic_alias = 0 -zone.${name}.max_qos_allowed = 2 -zone.${name}.retain_available = on -zone.${name}.wildcard_subscription = on -zone.${name}.shared_subscription = off - -## The backoff for MQTT keepalive timeout. -## EMQ will kick a MQTT connection out until 'Keepalive * backoff * 2' timeout. -## -## Value: Float > 0.5 -zone.${name}.keepalive_backoff = 0.75 - -##-------------------------------------------------------------------- -## Authentication - -zone.${name}.allow_anonymous = true - -##-------------------------------------------------------------------- -## Session - -zone.${name}.max_subscriptions = 0 -zone.${name}.upgrade_qos = off - -## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. -## -## Value: Number -zone.${name}.max_inflight = 32 - -## Retry interval for QoS1/2 message delivering. -## -## Value: Duration -zone.${name}.retry_interval = 20s - -## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. -## -## Value: Number -zone.${name}.max_awaiting_rel = 100 - -## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. -## -## Value: Duration -zone.${name}.await_rel_timeout = 30s - -## Whether to ignore loop delivery of messages. -## -## Value: true | false -## -## Default: false -zone.${name}.ignore_loop_deliver = false - -## Max session expiration time. -## -## Value: Duration -## -d: day -## -h: hour -## -m: minute -## -s: second -## -## Default: 2h, 2 hours -zone.${name}.session_expiry_interval = 2h - -##-------------------------------------------------------------------- -## Queue - -## Message queue type. -## -## Value: simple | priority -zone.${name}.mqueue_type = simple - -## Topic priority. Default is 0. -## -## Value: Number [0-255] -## -## zone.${name}.mqueue_priority = topic/1=10,topic/2=8 - -## Maximum queue length. Enqueued messages when persistent client disconnected, -## or inflight window is full. 0 means no limit. -## -## Value: Number >= 0 -zone.${name}.max_mqueue_len = 100 - -## Whether to enqueue Qos0 messages. -## -## Value: false | true -zone.${name}.mqueue_store_qos0 = true - -##-------------------------------------------------------------------- -## General - -zone.${name}.enable_stats = on - - From 3ac4be84e401761028c78ad5c448b38ee9d132c1 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 9 Aug 2018 14:26:39 +0800 Subject: [PATCH 086/520] Remove 'listener.wss.external.handshake_timeout' for cowboy does not support this option --- etc/acl.conf | 1 + etc/emqx.conf | 7 ------- priv/emqx.schema | 8 ++++---- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/etc/acl.conf b/etc/acl.conf index 2560bf80d..fb85f3f20 100644 --- a/etc/acl.conf +++ b/etc/acl.conf @@ -24,3 +24,4 @@ {deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. +{allow, all}. diff --git a/etc/emqx.conf b/etc/emqx.conf index 6566bc365..78e0a9f1a 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1160,13 +1160,6 @@ listener.wss.external.verify_protocol_header = on ## Value: String, seperated by ',' ## listener.wss.external.tls_versions = tlsv1.2,tlsv1.1,tlsv1 -## TLS Handshake timeout. -## -## See: listener.ssl.$name.handshake_timeout -## -## Value: Duration -listener.wss.external.handshake_timeout = 15s - ## Path to the file containing the user's private PEM-encoded key. ## ## See: listener.ssl.$name.keyfile diff --git a/priv/emqx.schema b/priv/emqx.schema index 425c0f4f3..d138a4f50 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -989,10 +989,10 @@ end}. {datatype, {duration, ms}} ]}. -{mapping, "listener.wss.$name.handshake_timeout", "emqx.listeners", [ - {default, "15s"}, - {datatype, {duration, ms}} -]}. +%%{mapping, "listener.wss.$name.handshake_timeout", "emqx.listeners", [ +%% {default, "15s"}, +%% {datatype, {duration, ms}} +%%]}. {mapping, "listener.wss.$name.backlog", "emqx.listeners", [ {default, 1024}, From 919eb9fa1e37b9784c677706a904bc1ebe71abc4 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 9 Aug 2018 14:27:49 +0800 Subject: [PATCH 087/520] Use cowboy to replace minirest --- src/emqx_app.erl | 2 +- src/emqx_listeners.erl | 66 +++++++++++++++++++++++------------------- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/emqx_app.erl b/src/emqx_app.erl index 7a5426bae..d5ca8f6ae 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -31,7 +31,7 @@ start(_Type, _Args) -> emqx_modules:load(), emqx_plugins:init(), emqx_plugins:load(), - emqx_listeners:start_all(), + emqx_listeners:start(), start_autocluster(), register(emqx, self()), print_vsn(), diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 257820fb1..78fd5db80 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -12,75 +12,83 @@ %% See the License for the specific language governing permissions and %% limitations under the License. -%% @doc start/stop MQTT listeners. +%% @doc Start/Stop MQTT listeners. -module(emqx_listeners). -include("emqx_mqtt.hrl"). --export([start_all/0, restart_all/0, stop_all/0]). +-export([start/0, restart/0, stop/0]). -export([start_listener/1, stop_listener/1, restart_listener/1]). -type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}). %% @doc Start all listeners --spec(start_all() -> ok). -start_all() -> +-spec(start() -> ok). +start() -> lists:foreach(fun start_listener/1, emqx_config:get_env(listeners, [])). %% Start MQTT/TCP listener -spec(start_listener(listener()) -> {ok, pid()} | {error, term()}). start_listener({tcp, ListenOn, Options}) -> start_mqtt_listener('mqtt:tcp', ListenOn, Options); + %% Start MQTT/TLS listener start_listener({Proto, ListenOn, Options}) when Proto == ssl; Proto == tls -> start_mqtt_listener('mqtt:ssl', ListenOn, Options); + %% Start MQTT/WS listener start_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> - Dispatch = [{"/mqtt", emqx_ws, []}], + Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws, []}]}]), NumAcceptors = proplists:get_value(acceptors, Options, 4), - MaxConnections = proplists:get_value(max_clients, Options, 1024), + MaxConnections = proplists:get_value(max_connections, Options, 1024), TcpOptions = proplists:get_value(tcp_options, Options, []), - Options1 = [{port, ListenOn}, - {num_acceptors, NumAcceptors}, - {max_connections, MaxConnections} | TcpOptions], - minirest:start_http(Proto, Options1, Dispatch); + RanchOpts = [{num_acceptors, NumAcceptors}, + {max_connections, MaxConnections} | TcpOptions], + cowboy:start_clear('mqtt:ws', with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}); + %% Start MQTT/WSS listener start_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss -> - Dispatch = [{"/mqtt", emqx_ws, []}], + Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws, []}]}]), NumAcceptors = proplists:get_value(acceptors, Options, 4), MaxConnections = proplists:get_value(max_clients, Options, 1024), TcpOptions = proplists:get_value(tcp_options, Options, []), SslOptions = proplists:get_value(ssl_options, Options, []), - Options1 = [{port, ListenOn}, - {num_acceptors, NumAcceptors}, - {max_connections, MaxConnections} | TcpOptions ++ SslOptions], - minirest:start_https(Proto, Options1, Dispatch). + RanchOpts = [{num_acceptors, NumAcceptors}, + {max_connections, MaxConnections} | TcpOptions ++ SslOptions], + cowboy:start_tls('mqtt:wss', with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}). start_mqtt_listener(Name, ListenOn, Options) -> SockOpts = esockd:parse_opt(Options), MFA = {emqx_connection, start_link, [Options -- SockOpts]}, {ok, _} = esockd:open(Name, ListenOn, merge_default(SockOpts), MFA). +with_port(Port, Opts) when is_integer(Port) -> + [{port, Port}|Opts]; +with_port({Addr, Port}, Opts) -> + [{ip, Addr}, {port, Port}|Opts]. + %% @doc Restart all listeners --spec(restart_all() -> ok). -restart_all() -> +-spec(restart() -> ok). +restart() -> lists:foreach(fun restart_listener/1, emqx_config:get_env(listeners, [])). -spec(restart_listener(listener()) -> any()). -restart_listener({tcp, ListenOn, _Opts}) -> +restart_listener({tcp, ListenOn, _Options}) -> esockd:reopen('mqtt:tcp', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> +restart_listener({Proto, ListenOn, _Options}) when Proto == ssl; Proto == tls -> esockd:reopen('mqtt:ssl', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> - mochiweb:restart_http('mqtt:ws', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> - mochiweb:restart_http('mqtt:wss', ListenOn); +restart_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> + cowboy:stop_listener('mqtt:ws'), + start_listener({Proto, ListenOn, Options}); +restart_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss -> + cowboy:stop_listener('mqtt:wss'), + start_listener({Proto, ListenOn, Options}); restart_listener({Proto, ListenOn, _Opts}) -> esockd:reopen(Proto, ListenOn). %% @doc Stop all listeners --spec(stop_all() -> ok). -stop_all() -> +-spec(stop() -> ok). +stop() -> lists:foreach(fun stop_listener/1, emqx_config:get_env(listeners, [])). -spec(stop_listener(listener()) -> ok | {error, any()}). @@ -88,10 +96,10 @@ stop_listener({tcp, ListenOn, _Opts}) -> esockd:close('mqtt:tcp', ListenOn); stop_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> esockd:close('mqtt:ssl', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> - mochiweb:stop_http('mqtt:ws', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> - mochiweb:stop_http('mqtt:wss', ListenOn); +stop_listener({Proto, _ListenOn, _Opts}) when Proto == http; Proto == ws -> + cowboy:stop_listener('mqtt:ws'); +stop_listener({Proto, _ListenOn, _Opts}) when Proto == https; Proto == wss -> + cowboy:stop_listener('mqtt:wss'); stop_listener({Proto, ListenOn, _Opts}) -> esockd:close(Proto, ListenOn). From 09b55352601542991fb8c86333ef3e9de6ea3d7c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 9 Aug 2018 15:17:42 +0800 Subject: [PATCH 088/520] Depends on cowboy 2.4.0 --- Makefile | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 509794bbd..7b1cc1cdd 100644 --- a/Makefile +++ b/Makefile @@ -4,16 +4,16 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker PROJECT_VERSION = 3.0 -DEPS = jsx gproc gen_rpc lager ekka esockd minirest clique +DEPS = jsx gproc gen_rpc lager ekka esockd cowboy clique -dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 -dep_gproc = git https://github.com/uwiger/gproc 0.8.0 -dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.1.1 -dep_lager = git https://github.com/erlang-lager/lager 3.6.4 -dep_esockd = git https://github.com/emqx/esockd emqx30 -dep_ekka = git https://github.com/emqx/ekka emqx30 -dep_minirest = git https://github.com/emqx/minirest emqx30 -dep_clique = git https://github.com/emqx/clique +dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 +dep_gproc = git https://github.com/uwiger/gproc 0.8.0 +dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.1.1 +dep_lager = git https://github.com/erlang-lager/lager 3.6.4 +dep_esockd = git https://github.com/emqx/esockd emqx30 +dep_ekka = git https://github.com/emqx/ekka emqx30 +dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 +dep_clique = git https://github.com/emqx/clique NO_AUTOPATCH = gen_rpc cuttlefish From d516b8c24193dfbc17875ba24b76f19b0226d8a6 Mon Sep 17 00:00:00 2001 From: turtled Date: Thu, 9 Aug 2018 15:19:45 +0800 Subject: [PATCH 089/520] mochiweb -> cowboy --- src/emqx.erl | 4 ++-- src/emqx_ws_connection_sup.erl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqx.erl b/src/emqx.erl index cbe37d12e..475428fd4 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -166,8 +166,8 @@ shutdown() -> shutdown(Reason) -> emqx_logger:error("emqx shutdown for ~s", [Reason]), emqx_plugins:unload(), - lists:foreach(fun application:stop/1, [emqx, ekka, mochiweb, esockd, gproc]). + lists:foreach(fun application:stop/1, [emqx, ekka, cowboy, esockd, gproc]). reboot() -> - lists:foreach(fun application:start/1, [gproc, esockd, mochiweb, ekka, emqx]). + lists:foreach(fun application:start/1, [gproc, esockd, cowboy, ekka, emqx]). diff --git a/src/emqx_ws_connection_sup.erl b/src/emqx_ws_connection_sup.erl index f627abfbb..1216eeb75 100644 --- a/src/emqx_ws_connection_sup.erl +++ b/src/emqx_ws_connection_sup.erl @@ -27,7 +27,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% @doc Start a MQTT/WebSocket Connection. --spec(start_connection(pid(), mochiweb_request:request()) -> {ok, pid()}). +-spec(start_connection(pid(), cowboy_req:req()) -> {ok, pid()}). start_connection(WsPid, Req) -> supervisor:start_child(?MODULE, [WsPid, Req]). From d9004d4cfb55019825315cb4cad3cbaf669349e6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 10 Aug 2018 12:43:32 +0800 Subject: [PATCH 090/520] Add MQTT section in configuration file --- etc/emqx.conf | 68 +++++++++++++++++++++++++++++++-------- priv/emqx.schema | 84 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 121 insertions(+), 31 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 78e0a9f1a..83ddcf6a8 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -440,10 +440,50 @@ enable_acl_cache = on ## Default: 5 minute acl_cache_age = 5m -## The ACL cache size, 0 means no limit. +##-------------------------------------------------------------------- +## MQTT +##-------------------------------------------------------------------- + +## Maximum MQTT packet size allowed. ## -## Value: Integer -acl_cache_size = 0 +## Value: Bytes +## Default: 1MB +mqtt.max_packet_size = 1MB + +## Maximum length of MQTT clientId allowed. +## +## Value: Number [23-65535] +mqtt.max_clientid_len = 65535 + +## Maximum topic levels allowed. 0 means no limit. +## +## Value: Number +mqtt.max_topic_levels = 0 + +## Maximum QoS allowed. +## +## Value: 0 | 1 | 2 +mqtt.max_qos_allowed = 2 + +## Maximum Topic Alias, 0 means no limit. +## +## Value: 0-65535 +mqtt.max_topic_alias = 0 + +## Whether the Server supports MQTT retained messages. +## +## Value: boolean +mqtt.retain_available = true + +## Whether the Server supports MQTT Wildcard Subscriptions +## +## Value: boolean +mqtt.wildcard_subscription = true + +## Whether the Server supports MQTT Shared Subscriptions +## +## Value: boolean +mqtt.shared_subscription = true ##-------------------------------------------------------------------- ## Listeners @@ -1303,43 +1343,43 @@ zone.external.enable_stats = on ## Maximum MQTT packet size allowed. ## ## Value: Bytes -## Default: 64KB -zone.external.max_packet_size = 1MB +## Default: 1MB +## zone.external.max_packet_size = 64KB ## Maximum length of MQTT clientId allowed. ## ## Value: Number [23-65535] -zone.external.max_clientid_len = 65535 +## zone.external.max_clientid_len = 1024 ## Maximum topic levels allowed. 0 means no limit. ## ## Value: Number -zone.external.max_topic_levels = 0 +## zone.external.max_topic_levels = 7 ## Maximum QoS allowed. ## ## Value: 0 | 1 | 2 -zone.external.max_qos_allowed = 2 +## zone.external.max_qos_allowed = 2 ## Maximum Topic Alias, 0 means no limit. ## ## Value: 0-65535 -zone.external.max_topic_alias = 0 +## zone.external.max_topic_alias = 0 ## Whether the Server supports retained messages. ## ## Value: boolean -zone.external.retain_available = true +## zone.external.retain_available = true ## Whether the Server supports Wildcard Subscriptions ## ## Value: boolean -zone.external.wildcard_subscription = true +## zone.external.wildcard_subscription = false ## Whether the Server supports Shared Subscriptions ## ## Value: boolean -zone.external.shared_subscription = true +## zone.external.shared_subscription = false ## The backoff for MQTT keepalive timeout. The broker will kick a connection out ## until 'Keepalive * backoff * 2' timeout. @@ -1421,12 +1461,12 @@ zone.internal.enable_acl = off ## See zone.$name.wildcard_subscription. ## ## Value: boolean -zone.internal.wildcard_subscription = true +## zone.internal.wildcard_subscription = true ## See zone.$name.shared_subscription. ## ## Value: boolean -zone.internal.shared_subscription = true +## zone.internal.shared_subscription = true ## See zone.$name.max_subscriptions. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index d138a4f50..a81d21799 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -578,11 +578,64 @@ end}. ]}. %% @doc ACL cache size. -{mapping, "acl_cache_size", "emqx.acl_cache_size", [ +%% {mapping, "acl_cache_size", "emqx.acl_cache_size", [ +%% {default, 0}, +%% {datatype, integer} +%% ]}. + +%%-------------------------------------------------------------------- +%% MQTT +%%-------------------------------------------------------------------- + +%% @doc Max Packet Size Allowed, 1MB by default. +{mapping, "mqtt.max_packet_size", "emqx.max_packet_size", [ + {default, "1MB"}, + {datatype, bytesize} +]}. + +%% @doc Set the Max ClientId Length Allowed. +{mapping, "mqtt.max_clientid_len", "emqx.max_clientid_len", [ + {default, 65535}, + {datatype, integer} +]}. + +%% @doc Set the Maximum topic levels. +{mapping, "mqtt.max_topic_levels", "emqx.max_topic_levels", [ {default, 0}, {datatype, integer} ]}. +%% @doc Set the Maximum QoS allowed. +{mapping, "mqtt.max_qos_allowed", "emqx.max_qos_allowed", [ + {default, 2}, + {datatype, integer}, + {validators, ["range:0-2"]} +]}. + +%% @doc Set the Maximum topic alias. +{mapping, "mqtt.max_topic_alias", "emqx.max_topic_alias", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Whether the server supports MQTT retained messages. +{mapping, "mqtt.retain_available", "emqx.mqtt_retain_available", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +%% @doc Whether the Server supports MQTT Wildcard Subscriptions. +{mapping, "mqtt.wildcard_subscription", "emqx.mqtt_wildcard_subscription", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +%% @doc Whether the Server supports MQTT Shared Subscriptions. +{mapping, "mqtt.shared_subscription", "emqx.mqtt_shared_subscription", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + %%-------------------------------------------------------------------- %% Listeners %%-------------------------------------------------------------------- @@ -1211,50 +1264,42 @@ end}. %% @doc Max Packet Size Allowed, 64K by default. {mapping, "zone.$name.max_packet_size", "emqx.zones", [ - {default, "64KB"}, {datatype, bytesize} ]}. %% @doc Set the Max ClientId Length Allowed. {mapping, "zone.$name.max_clientid_len", "emqx.zones", [ - {default, 65535}, {datatype, integer} ]}. %% @doc Set the Maximum topic levels. {mapping, "zone.$name.max_topic_levels", "emqx.zones", [ - {default, 0}, {datatype, integer} ]}. %% @doc Set the Maximum QoS allowed. {mapping, "zone.$name.max_qos_allowed", "emqx.zones", [ - {default, 2}, {datatype, integer}, {validators, ["range:0-2"]} ]}. %% @doc Set the Maximum topic alias. {mapping, "zone.$name.max_topic_alias", "emqx.zones", [ - {default, 0}, {datatype, integer} ]}. %% @doc Whether the server supports retained messages. {mapping, "zone.$name.retain_available", "emqx.zones", [ - {default, true}, {datatype, {enum, [true, false]}} ]}. %% @doc Whether the Server supports Wildcard Subscriptions. {mapping, "zone.$name.wildcard_subscription", "emqx.zones", [ - {default, true}, {datatype, {enum, [true, false]}} ]}. %% @doc Whether the Server supports Shared Subscriptions. {mapping, "zone.$name.shared_subscription", "emqx.zones", [ - {default, true}, {datatype, {enum, [true, false]}} ]}. @@ -1327,16 +1372,21 @@ end}. ]}. {translation, "emqx.zones", fun(Conf) -> + Mapping = fun(retain_available, Val) -> + {mqtt_retain_available, Val}; + (wildcard_subscription, Val) -> + {mqtt_wildcard_subscription, Val}; + (shared_subscription, Val) -> + {mqtt_shared_subscription, Val}; + (Opt, Val) -> {Opt, Val} + end, maps:to_list( lists:foldl( - fun({["zone", Name, Opt], Val}, Acc) -> - ZName = list_to_atom(Name), - case maps:find(ZName, Acc) of - {ok, Opts} -> - maps:put(ZName, [{list_to_atom(Opt), Val} | Opts], Acc); - error -> - maps:put(ZName, [{list_to_atom(Opt), Val}], Acc) - end + fun({["zone", Name, Opt], Val}, Zones) -> + maps:update_with(list_to_atom(Name), + fun(Opts) -> + [Mapping(list_to_atom(Opt), Val)|Opts] + end, [], Zones) end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("zone.", Conf)))) end}. From 6ab489a9b5f2e50cc141acbaeabee76624398076 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 10 Aug 2018 15:38:08 +0800 Subject: [PATCH 091/520] Change the MAX_CLIENTID_LEN to 65535 --- include/emqx_mqtt.hrl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 35a8a5082..007de4dd1 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -78,7 +78,7 @@ %% Maximum ClientId Length. %%-------------------------------------------------------------------- --define(MAX_CLIENTID_LEN, 1024). +-define(MAX_CLIENTID_LEN, 65535). %%-------------------------------------------------------------------- %% MQTT Client From bc8302dae9e4802a0a8090da941747f9f569235c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 10 Aug 2018 15:39:24 +0800 Subject: [PATCH 092/520] Change default value of max_mqueue_len to 1000 --- priv/emqx.schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index a81d21799..44b5e4bc1 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1361,7 +1361,7 @@ end}. %% @doc Max queue length. Enqueued messages when persistent client %% disconnected, or inflight window is full. 0 means no limit. {mapping, "zone.$name.max_mqueue_len", "emqx.zones", [ - {default, 0}, + {default, 1000}, {datatype, integer} ]}. From 3d05954d5b2b400e7545e1ee6719edac3e1e570b Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sat, 11 Aug 2018 16:17:39 +0800 Subject: [PATCH 093/520] Optimize emqx_time module --- src/emqx_time.erl | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/emqx_time.erl b/src/emqx_time.erl index 2e69638dc..40c71faa6 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -14,23 +14,13 @@ -module(emqx_time). --export([seed/0, now_secs/0, now_secs/1, now_ms/0, now_ms/1, ts_from_ms/1]). +-export([seed/0, now_secs/0, now_ms/0]). seed() -> rand:seed(exsplus, erlang:timestamp()). now_ms() -> - now_ms(os:timestamp()). - -now_ms({MegaSecs, Secs, MicroSecs}) -> - (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). + erlang:system_time(millisecond). now_secs() -> - now_secs(os:timestamp()). - -now_secs({MegaSecs, Secs, _MicroSecs}) -> - MegaSecs * 1000000 + Secs. - -ts_from_ms(Ms) -> - {Ms div 1000000, Ms rem 1000000, 0}. - + erlang:system_time(second). From f80cd2d98686f78af522fd7006ea4ecd6181efaf Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 11 Aug 2018 17:57:19 +0800 Subject: [PATCH 094/520] Improve the MQTT over Websocket connection --- priv/emqx.schema | 18 +- src/emqx_access_control.erl | 14 +- src/emqx_config.erl | 10 +- src/emqx_connection.erl | 88 +++---- src/emqx_gc.erl | 4 +- src/emqx_listeners.erl | 2 +- src/emqx_mqueue.erl | 58 ++--- src/emqx_packet.erl | 2 +- src/emqx_plugins.erl | 51 ++-- src/emqx_protocol.erl | 46 +--- src/emqx_router.erl | 1 - src/emqx_session.erl | 85 +++--- src/emqx_sup.erl | 3 - src/emqx_time.erl | 2 +- src/emqx_ws.erl | 103 -------- src/emqx_ws_connection.erl | 459 ++++++++++++++++----------------- src/emqx_ws_connection_sup.erl | 44 ---- src/emqx_zone.erl | 26 +- 18 files changed, 407 insertions(+), 609 deletions(-) delete mode 100644 src/emqx_ws.erl delete mode 100644 src/emqx_ws_connection_sup.erl diff --git a/priv/emqx.schema b/priv/emqx.schema index 44b5e4bc1..c502a8d24 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1372,21 +1372,21 @@ end}. ]}. {translation, "emqx.zones", fun(Conf) -> - Mapping = fun(retain_available, Val) -> + Mapping = fun("retain_available", Val) -> {mqtt_retain_available, Val}; - (wildcard_subscription, Val) -> + ("wildcard_subscription", Val) -> {mqtt_wildcard_subscription, Val}; - (shared_subscription, Val) -> + ("shared_subscription", Val) -> {mqtt_shared_subscription, Val}; - (Opt, Val) -> {Opt, Val} + (Opt, Val) -> + {list_to_atom(Opt), Val} end, maps:to_list( lists:foldl( fun({["zone", Name, Opt], Val}, Zones) -> maps:update_with(list_to_atom(Name), - fun(Opts) -> - [Mapping(list_to_atom(Opt), Val)|Opts] - end, [], Zones) + fun(Opts) -> [Mapping(Opt, Val)|Opts] end, + [Mapping(Opt, Val)], Zones) end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("zone.", Conf)))) end}. @@ -1507,9 +1507,11 @@ end}. maps:update_with(list_to_atom(Name), fun(Opts) -> Merge(list_to_atom(Opt), Val, Opts) - end, [{subscriptions, Subscriptions(Name)}], Acc); + end, [{list_to_atom(Opt), Val}, + {subscriptions, Subscriptions(Name)}], Acc); (_, Acc) -> Acc end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("bridge.", Conf)))) + end}. %%-------------------------------------------------------------------- diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 75e49fe07..2b7630f1e 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -22,6 +22,8 @@ -export([start_link/0, auth/2, check_acl/3, reload_acl/0, lookup_mods/1, register_mod/3, register_mod/4, unregister_mod/2, stop/0]). +-export([clean_acl_cache/1, clean_acl_cache/2]). + %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -50,9 +52,9 @@ start_link() -> register_default_mod() -> case emqx_config:get_env(acl_file) of - {ok, File} -> - emqx_access_control:register_mod(acl, emqx_acl_internal, [File]); - undefined -> ok + undefined -> ok; + File -> + emqx_access_control:register_mod(acl, emqx_acl_internal, [File]) end. %% @doc Authenticate Client. @@ -127,6 +129,12 @@ tab_key(acl) -> acl_modules. stop() -> gen_server:stop(?MODULE, normal, infinity). +%%TODO: Support ACL cache... +clean_acl_cache(_ClientId) -> + ok. +clean_acl_cache(_ClientId, _Topic) -> + ok. + %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- diff --git a/src/emqx_config.erl b/src/emqx_config.erl index d1456f644..2b96f88fc 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -33,15 +33,15 @@ -define(APP, emqx). +%% @doc Get environment +-spec(get_env(Key :: atom()) -> term() | undefined). +get_env(Key) -> + get_env(Key, undefined). + -spec(get_env(Key :: atom(), Default :: term()) -> term()). get_env(Key, Default) -> application:get_env(?APP, Key, Default). -%% @doc Get environment --spec(get_env(Key :: atom()) -> {ok, any()} | undefined). -get_env(Key) -> - application:get_env(?APP, Key). - %% TODO: populate(_App) -> ok. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 89233fb43..38c100297 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -21,9 +21,8 @@ -include("emqx_misc.hrl"). -export([start_link/3]). - -export([info/1, stats/1, kick/1]). --export([get_session/1]). +-export([session/1]). -export([clean_acl_cache/1]). -export([get_rate_limit/1, set_rate_limit/2]). -export([get_pub_limit/1, set_pub_limit/2]). @@ -44,7 +43,7 @@ rate_limit, %% Traffic rate limit limit_timer, %% Rate limit timer proto_state, %% MQTT protocol state - parse_state, %% MQTT parse state + parser_state, %% MQTT parser state keepalive, %% MQTT keepalive timer enable_stats, %% Enable stats stats_timer, %% Stats timer @@ -75,7 +74,7 @@ stats(CPid) -> kick(CPid) -> gen_server:call(CPid, kick). -get_session(CPid) -> +session(CPid) -> gen_server:call(CPid, session, infinity). clean_acl_cache(CPid) -> @@ -100,22 +99,20 @@ set_pub_limit(CPid, Rl = {_Rate, _Burst}) -> init([Transport, RawSocket, Options]) -> case Transport:wait(RawSocket) of {ok, Socket} -> - io:format("Options: ~p~n", [Options]), + Zone = proplists:get_value(zone, Options), {ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]), {ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]), Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]), - Zone = proplists:get_value(zone, Options), - RateLimit = init_rate_limit(proplists:get_value(rate_limit, Options)), - PubLimit = init_rate_limit(emqx_zone:get_env(Zone, publish_limit)), - EnableStats = emqx_zone:get_env(Zone, enable_stats, false), - IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), + PubLimit = rate_limit(emqx_zone:env(Zone, publish_limit)), + RateLimit = rate_limit(proplists:get_value(rate_limit, Options)), + EnableStats = emqx_zone:env(Zone, enable_stats, true), + IdleTimout = emqx_zone:env(Zone, idle_timeout, 30000), SendFun = send_fun(Transport, Socket, Peername), - ProtoState = emqx_protocol:init(#{zone => Zone, - peername => Peername, + ProtoState = emqx_protocol:init(#{peername => Peername, sockname => Sockname, peercert => Peercert, sendfun => SendFun}, Options), - ParseState = emqx_protocol:parser(ProtoState), + ParserState = emqx_protocol:parser(ProtoState), State = run_socket(#state{transport = Transport, socket = Socket, peername = Peername, @@ -124,7 +121,7 @@ init([Transport, RawSocket, Options]) -> rate_limit = RateLimit, pub_limit = PubLimit, proto_state = ProtoState, - parse_state = ParseState, + parser_state = ParserState, enable_stats = EnableStats, idle_timeout = IdleTimout}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], @@ -133,9 +130,9 @@ init([Transport, RawSocket, Options]) -> {stop, Reason} end. -init_rate_limit(undefined) -> +rate_limit(undefined) -> undefined; -init_rate_limit({Rate, Burst}) -> +rate_limit({Rate, Burst}) -> esockd_rate_limit:new(Rate, Burst). send_fun(Transport, Socket, Peername) -> @@ -152,8 +149,7 @@ send_fun(Transport, Socket, Peername) -> handle_call(info, From, State = #state{transport = Transport, socket = Socket, proto_state = ProtoState}) -> ProtoInfo = emqx_protocol:info(ProtoState), - ConnInfo = [{socktype, Transport:type(Socket)} - | ?record_to_proplist(state, State, ?INFO_KEYS)], + ConnInfo = [{socktype, Transport:type(Socket)} | ?record_to_proplist(state, State, ?INFO_KEYS)], StatsInfo = element(2, handle_call(stats, From, State)), {reply, lists:append([ConnInfo, StatsInfo, ProtoInfo]), State}; @@ -169,7 +165,7 @@ handle_call(stats, _From, State = #state{transport = Transport, socket = Sock, p handle_call(kick, _From, State) -> {stop, {shutdown, kick}, ok, State}; -handle_call(get_session, _From, State = #state{proto_state = ProtoState}) -> +handle_call(session, _From, State = #state{proto_state = ProtoState}) -> {reply, emqx_protocol:session(ProtoState), State}; handle_call(clean_acl_cache, _From, State = #state{proto_state = ProtoState}) -> @@ -195,28 +191,20 @@ handle_cast(Msg, State) -> ?LOG(error, "unexpected cast: ~p", [Msg], State), {noreply, State}. -handle_info(Sub = {subscribe, _TopicTable}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:process(Sub, ProtoState) - end, State); - -handle_info(Unsub = {unsubscribe, _Topics}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:process(Unsub, ProtoState) - end, State); - -handle_info({deliver, PubOrAck}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:deliver(PubOrAck, ProtoState) - end, maybe_gc(ensure_stats_timer(State))); +handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> + case emqx_protocol:deliver(PubOrAck, ProtoState) of + {ok, ProtoState1} -> + {noreply, maybe_gc(ensure_stats_timer(State#state{proto_state = ProtoState1}))}; + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, ProtoState1} -> + shutdown(Reason, State#state{proto_state = ProtoState1}) + end; handle_info(emit_stats, State = #state{proto_state = ProtoState}) -> Stats = element(2, handle_call(stats, undefined, State)), emqx_cm:set_client_stats(emqx_protocol:clientid(ProtoState), Stats), - {noreply, State = #state{stats_timer = undefined}, hibernate}; + {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info(timeout, State) -> shutdown(idle_timeout, State); @@ -306,20 +294,20 @@ handle_packet(<<>>, State) -> {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; handle_packet(Bytes, State = #state{incoming = Incoming, - parse_state = ParseState, + parser_state = ParserState, proto_state = ProtoState, idle_timeout = IdleTimeout}) -> - case catch emqx_frame:parse(Bytes, ParseState) of - {more, NewParseState} -> - {noreply, State#state{parse_state = NewParseState}, IdleTimeout}; + case catch emqx_frame:parse(Bytes, ParserState) of + {more, NewParserState} -> + {noreply, State#state{parser_state = NewParserState}, IdleTimeout}; {ok, Packet = ?PACKET(Type), Rest} -> emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> - ParseState1 = emqx_protocol:parser(ProtoState1), - handle_packet(Rest, State#state{incoming = count_packets(Type, Incoming), - proto_state = ProtoState1, - parse_state = ParseState1}); + ParserState1 = emqx_protocol:parser(ProtoState1), + handle_packet(Rest, State#state{incoming = count_packets(Type, Incoming), + proto_state = ProtoState1, + parser_state = ParserState1}); {error, Error} -> ?LOG(error, "Protocol error - ~p", [Error], State), shutdown(Error, State); @@ -368,16 +356,6 @@ run_socket(State = #state{transport = Transport, socket = Sock}) -> Transport:async_recv(Sock, 0, infinity), State#state{await_recv = true}. -with_proto(Fun, State = #state{proto_state = ProtoState}) -> - case Fun(ProtoState) of - {ok, ProtoState1} -> - {noreply, State#state{proto_state = ProtoState1}}; - {error, Reason} -> - shutdown(Reason, State); - {error, Reason, ProtoState1} -> - shutdown(Reason, State#state{proto_state = ProtoState1}) - end. - ensure_stats_timer(State = #state{enable_stats = true, stats_timer = undefined, idle_timeout = IdleTimeout}) -> diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 2cc0b2a1a..6b1d43207 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -27,8 +27,8 @@ -spec(conn_max_gc_count() -> integer()). conn_max_gc_count() -> case emqx_config:get_env(conn_force_gc_count) of - {ok, I} when I > 0 -> I + rand:uniform(I); - {ok, I} when I =< 0 -> undefined; + I when is_integer(I), I > 0 -> I + rand:uniform(I); + I when is_integer(I), I =< 0 -> undefined; undefined -> undefined end. diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 78fd5db80..084ffe7c2 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -38,7 +38,7 @@ start_listener({Proto, ListenOn, Options}) when Proto == ssl; Proto == tls -> %% Start MQTT/WS listener start_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> - Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws, []}]}]), + Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws_connection, Options}]}]), NumAcceptors = proplists:get_value(acceptors, Options, 4), MaxConnections = proplists:get_value(max_connections, Options, 1024), TcpOptions = proplists:get_value(tcp_options, Options, []), diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index 31811583f..458c301fc 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -39,32 +39,25 @@ %% %% @end +%% TODO: ... -module(emqx_mqueue). -%% TODO: XYZ -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). -import(proplists, [get_value/3]). --export([new/2, type/1, name/1, is_empty/1, len/1, max_len/1, in/2, out/1, - dropped/1, stats/1]). - --define(LOW_WM, 0.2). - --define(HIGH_WM, 0.6). +-export([new/2, type/1, name/1, is_empty/1, len/1, max_len/1, in/2, out/1]). +-export([dropped/1, stats/1]). -define(PQUEUE, emqx_pqueue). -type(priority() :: {iolist(), pos_integer()}). --type(option() :: {type, simple | priority} - | {max_length, non_neg_integer()} %% Max queue length - | {priority, list(priority())} - | {low_watermark, float()} %% Low watermark - | {high_watermark, float()} %% High watermark - | {store_qos0, boolean()}). %% Queue Qos0? +-type(options() :: #{type => simple | priority, + max_len => non_neg_integer(), + priority => list(priority()), + store_qos0 => boolean()}). -type(stat() :: {max_len, non_neg_integer()} | {len, non_neg_integer()} @@ -76,29 +69,22 @@ pseq = 0, priorities = [], %% len of simple queue len = 0, max_len = 0, - low_wm = ?LOW_WM, high_wm = ?HIGH_WM, qos0 = false, dropped = 0}). -type(mqueue() :: #mqueue{}). --export_type([mqueue/0, priority/0, option/0]). +-export_type([mqueue/0, priority/0, options/0]). -%% @doc New queue. --spec(new(iolist(), list(option())) -> mqueue()). -new(Name, Opts) -> - Type = get_value(type, Opts, simple), - MaxLen = get_value(max_length, Opts, 0), +-spec(new(iolist(), options()) -> mqueue()). +new(Name, #{type := Type, max_len := MaxLen, store_qos0 := StoreQos0}) -> init_q(#mqueue{type = Type, name = iolist_to_binary(Name), - len = 0, max_len = MaxLen, - low_wm = low_wm(MaxLen, Opts), - high_wm = high_wm(MaxLen, Opts), - qos0 = get_value(store_qos0, Opts, false)}, Opts). + len = 0, max_len = MaxLen, qos0 = StoreQos0}). -init_q(MQ = #mqueue{type = simple}, _Opts) -> +init_q(MQ = #mqueue{type = simple}) -> MQ#mqueue{q = queue:new()}; -init_q(MQ = #mqueue{type = priority}, Opts) -> - Priorities = get_value(priority, Opts, []), - init_p(Priorities, MQ#mqueue{q = ?PQUEUE:new()}). +init_q(MQ = #mqueue{type = priority}) -> + %%Priorities = get_value(priority, Opts, []), + init_p([], MQ#mqueue{q = ?PQUEUE:new()}). init_p([], MQ) -> MQ; @@ -110,16 +96,6 @@ insert_p(Topic, P, MQ = #mqueue{priorities = Tab, pseq = Seq}) -> <> = <>, {PInt, MQ#mqueue{priorities = [{Topic, PInt} | Tab], pseq = Seq + 1}}. -low_wm(0, _Opts) -> - undefined; -low_wm(MaxLen, Opts) -> - round(MaxLen * get_value(low_watermark, Opts, ?LOW_WM)). - -high_wm(0, _Opts) -> - undefined; -high_wm(MaxLen, Opts) -> - round(MaxLen * get_value(high_watermark, Opts, ?HIGH_WM)). - -spec(name(mqueue()) -> iolist()). name(#mqueue{name = Name}) -> Name. @@ -172,8 +148,8 @@ in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, MQ1#mqueue{q = ?PQUEUE:in(Msg, Pri, Q)} end; in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, - priorities = Priorities, - max_len = MaxLen}) -> + priorities = Priorities, + max_len = MaxLen}) -> case lists:keysearch(Topic, 1, Priorities) of {value, {_, Pri}} -> case ?PQUEUE:plen(Pri, Q) >= MaxLen of diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 8baa6f088..65f125f68 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -114,7 +114,7 @@ format_variable(#mqtt_packet_connect{ format_variable(#mqtt_packet_connack{ack_flags = AckFlags, reason_code = ReasonCode}) -> - io_lib:format("AckFlags=~p, RetainCode=~p", [AckFlags, ReasonCode]); + io_lib:format("AckFlags=~p, ReasonCode=~p", [AckFlags, ReasonCode]); format_variable(#mqtt_packet_publish{topic_name = TopicName, packet_id = PacketId}) -> diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index 837ed8a0e..0c03e827e 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -32,12 +32,11 @@ -spec(init() -> ok). init() -> case emqx_config:get_env(plugins_etc_dir) of - {ok, PluginsEtc} -> + undefined -> ok; + PluginsEtc -> CfgFiles = [filename:join(PluginsEtc, File) || - File <- filelib:wildcard("*.config", PluginsEtc)], - lists:foreach(fun init_config/1, CfgFiles); - undefined -> - ok + File <- filelib:wildcard("*.config", PluginsEtc)], + lists:foreach(fun init_config/1, CfgFiles) end. init_config(CfgFile) -> @@ -51,25 +50,24 @@ init_config(CfgFile) -> load() -> load_expand_plugins(), case emqx_config:get_env(plugins_loaded_file) of - {ok, File} -> + undefined -> %% No plugins available + ignore; + File -> ensure_file(File), - with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end); - undefined -> - %% No plugins available - ignore + with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end) end. load_expand_plugins() -> case emqx_config:get_env(expand_plugins_dir) of - {ok, Dir} -> + undefined -> ok; + Dir -> PluginsDir = filelib:wildcard("*", Dir), lists:foreach(fun(PluginDir) -> case filelib:is_dir(Dir ++ PluginDir) of true -> load_expand_plugin(Dir ++ PluginDir); false -> ok end - end, PluginsDir); - _ -> ok + end, PluginsDir) end. load_expand_plugin(PluginDir) -> @@ -102,7 +100,8 @@ init_expand_plugin_config(PluginDir) -> get_expand_plugin_config() -> case emqx_config:get_env(expand_plugins_dir) of - {ok, Dir} -> + undefined -> ok; + Dir -> PluginsDir = filelib:wildcard("*", Dir), lists:foldl(fun(PluginDir, Acc) -> case filelib:is_dir(Dir ++ PluginDir) of @@ -115,11 +114,9 @@ get_expand_plugin_config() -> false -> Acc end - end, [], PluginsDir); - _ -> ok + end, [], PluginsDir) end. - ensure_file(File) -> case filelib:is_file(File) of false -> write_loaded([]); true -> ok end. @@ -145,10 +142,10 @@ load_plugins(Names, Persistent) -> -spec(unload() -> list() | {error, term()}). unload() -> case emqx_config:get_env(plugins_loaded_file) of - {ok, File} -> - with_loaded_file(File, fun stop_plugins/1); undefined -> - ignore + ignore; + File -> + with_loaded_file(File, fun stop_plugins/1) end. %% stop plugins @@ -159,7 +156,9 @@ stop_plugins(Names) -> -spec(list() -> [plugin()]). list() -> case emqx_config:get_env(plugins_etc_dir) of - {ok, PluginsEtc} -> + undefined -> + []; + PluginsEtc -> CfgFiles = filelib:wildcard("*.{conf,config}", PluginsEtc) ++ get_expand_plugin_config(), Plugins = [plugin(CfgFile) || CfgFile <- CfgFiles], StartedApps = names(started_app), @@ -168,9 +167,7 @@ list() -> true -> Plugin#plugin{active = true}; false -> Plugin end - end, Plugins); - undefined -> - [] + end, Plugins) end. plugin(CfgFile) -> @@ -314,14 +311,14 @@ plugin_unloaded(Name, true) -> read_loaded() -> case emqx_config:get_env(plugins_loaded_file) of - {ok, File} -> read_loaded(File); - undefined -> {error, not_found} + undefined -> {error, not_found}; + File -> read_loaded(File) end. read_loaded(File) -> file:consult(File). write_loaded(AppNames) -> - {ok, File} = emqx_config:get_env(plugins_loaded_file), + File = emqx_config:get_env(plugins_loaded_file), case file:open(File, [binary, write]) of {ok, Fd} -> lists:foreach(fun(Name) -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 47a2c16e4..705674000 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -36,7 +36,7 @@ {shared_subscription, true}, {wildcard_subscription, true}]). --record(proto_state, {sockprops, capabilities, connected, client_id, client_pid, +-record(proto_state, {zone, sockprops, capabilities, connected, client_id, client_pid, clean_start, proto_ver, proto_name, username, connprops, is_superuser, will_msg, keepalive, keepalive_backoff, session, recv_pkt = 0, recv_msg = 0, send_pkt = 0, send_msg = 0, @@ -56,15 +56,17 @@ -export_type([proto_state/0]). -init(SockProps = #{zone := Zone, peercert := Peercert}, Options) -> - MountPoint = emqx_zone:get_env(Zone, mountpoint), - Backoff = emqx_zone:get_env(Zone, keepalive_backoff, 0.75), +init(SockProps = #{peercert := Peercert}, Options) -> + Zone = proplists:get_value(zone, Options), + MountPoint = emqx_zone:env(Zone, mountpoint), + Backoff = emqx_zone:env(Zone, keepalive_backoff, 0.75), Username = case proplists:get_value(peer_cert_as_username, Options) of cn -> esockd_peercert:common_name(Peercert); dn -> esockd_peercert:subject(Peercert); _ -> undefined end, - #proto_state{sockprops = SockProps, + #proto_state{zone = Zone, + sockprops = SockProps, capabilities = capabilities(Zone), connected = false, clean_start = true, @@ -82,7 +84,7 @@ init(SockProps = #{zone := Zone, peercert := Peercert}, Options) -> send_msg = 0}. capabilities(Zone) -> - Capabilities = emqx_zone:get_env(Zone, mqtt_capabilities, []), + Capabilities = emqx_zone:env(Zone, mqtt_capabilities, []), maps:from_list(lists:ukeymerge(1, ?CAPABILITIES, Capabilities)). parser(#proto_state{capabilities = #{max_packet_size := Size}, proto_ver = Ver}) -> @@ -128,7 +130,9 @@ received(Packet = ?PACKET(Type), ProtoState) -> {error, Reason, ProtoState} end. -process(?CONNECT_PACKET(Var), ProtoState = #proto_state{username = Username0, client_pid = ClientPid}) -> +process(?CONNECT_PACKET(Var), ProtoState = #proto_state{zone = Zone, + username = Username0, + client_pid = ClientPid}) -> #mqtt_packet_connect{proto_name = ProtoName, proto_ver = ProtoVer, is_bridge = IsBridge, @@ -160,7 +164,8 @@ process(?CONNECT_PACKET(Var), ProtoState = #proto_state{username = Username0, cl %% Generate clientId if null ProtoState2 = maybe_set_clientid(ProtoState1), %% Open session - case emqx_sm:open_session(#{clean_start => CleanStart, + case emqx_sm:open_session(#{zone => Zone, + clean_start => CleanStart, client_id => clientid(ProtoState2), username => Username, client_pid => ClientPid}) of @@ -242,23 +247,10 @@ process(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), State) -> {ok, TopicFilters1} -> ok = emqx_session:subscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicFilters1)}), {ok, State}; - {stop, _} -> - {ok, State} + {stop, _} -> {ok, State} end end; -process({subscribe, RawTopicTable}, - State = #proto_state{client_id = ClientId, - username = Username, - session = Session}) -> - TopicTable = parse_topic_filters(RawTopicTable), - case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of - {ok, TopicTable1} -> - emqx_session:subscribe(Session, TopicTable1); - {stop, _} -> ok - end, - {ok, State}; - %% Protect from empty topic list process(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> send(?UNSUBACK_PACKET(PacketId), State); @@ -276,16 +268,6 @@ process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopics), end, send(?UNSUBACK_PACKET(PacketId), State); -process({unsubscribe, RawTopics}, State = #proto_state{client_id = ClientId, - username = Username, - session = Session}) -> - case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of - {ok, TopicTable} -> - emqx_session:unsubscribe(Session, {undefined, #{}, TopicTable}); - {stop, _} -> ok - end, - {ok, State}; - process(?PACKET(?PINGREQ), ProtoState) -> send(?PACKET(?PINGRESP), ProtoState); diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 85a6a63ad..863214617 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -39,7 +39,6 @@ -type(destination() :: node() | {binary(), node()}). -record(batch, {enabled, timer, pending}). - -record(state, {pool, id, batch :: #batch{}}). -define(ROUTE, emqx_route). diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 03bcae3f2..1253de5cc 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -48,7 +48,9 @@ -export([resume/2, discard/2]). -export([subscribe/2]).%%, subscribe/3]). -export([publish/3]). --export([puback/2, pubrec/2, pubrel/2, pubcomp/2]). +-export([puback/2, puback/3]). +-export([pubrec/2, pubrec/3]). +-export([pubrel/2, pubcomp/2]). -export([unsubscribe/2]). %% gen_server callbacks @@ -139,7 +141,11 @@ }). -define(TIMEOUT, 60000). + +-define(DEFAULT_SUBOPTS, #{rh => 0, rap => 0, nl => 0, qos => ?QOS_0}). + -define(INFO_KEYS, [clean_start, client_id, username, client_pid, binding, created_at]). + -define(STATE_KEYS, [clean_start, client_id, username, binding, client_pid, old_client_pid, next_pkt_id, max_subscriptions, subscriptions, upgrade_qos, inflight, max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, @@ -151,16 +157,21 @@ "Session(~s): " ++ Format, [State#state.client_id | Args])). %% @doc Start a session --spec(start_link(Attrs :: map()) -> {ok, pid()} | {error, term()}). -start_link(Attrs) -> - gen_server:start_link(?MODULE, Attrs, [{hibernate_after, 10000}]). +-spec(start_link(SessAttrs :: map()) -> {ok, pid()} | {error, term()}). +start_link(SessAttrs) -> + gen_server:start_link(?MODULE, SessAttrs, [{hibernate_after, 30000}]). %%------------------------------------------------------------------------------ %% PubSub API %%------------------------------------------------------------------------------ +-spec(subscribe(pid(), list({topic(), map()}) | + {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). +%% internal call +subscribe(SPid, TopicFilters) when is_list(TopicFilters) -> + %%TODO: Parse the topic filters? + subscribe(SPid, {undefined, #{}, TopicFilters}); %% for mqtt 5.0 --spec(subscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). subscribe(SPid, SubReq = {PacketId, Props, TopicFilters}) -> gen_server:cast(SPid, {subscribe, self(), SubReq}). @@ -200,6 +211,9 @@ pubcomp(SPid, PacketId) -> gen_server:cast(SPid, {pubcomp, PacketId}). -spec(unsubscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). +unsubscribe(SPid, TopicFilters) when is_list(TopicFilters) -> + %%TODO: Parse the topic filters? + unsubscribe(SPid, {undefined, #{}, TopicFilters}); unsubscribe(SPid, UnsubReq = {PacketId, Properties, TopicFilters}) -> gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). @@ -252,40 +266,43 @@ close(SPid) -> %% gen_server callbacks %%------------------------------------------------------------------------------ -init(#{clean_start := CleanStart, client_id := ClientId, username := Username, client_pid := ClientPid}) -> +init(#{zone := Zone, + client_id := ClientId, + client_pid := ClientPid, + clean_start := CleanStart, + username := Username}) -> process_flag(trap_exit, true), true = link(ClientPid), init_stats([deliver_msg, enqueue_msg]), - {ok, Env} = emqx_config:get_env(session), - {ok, QEnv} = emqx_config:get_env(mqueue), - MaxInflight = proplists:get_value(max_inflight, Env, 0), - EnableStats = proplists:get_value(enable_stats, Env, false), - IgnoreLoopDeliver = proplists:get_value(ignore_loop_deliver, Env, false), - MQueue = emqx_mqueue:new(ClientId, QEnv), + MaxInflight = emqx_zone:env(Zone, max_inflight), State = #state{clean_start = CleanStart, binding = binding(ClientPid), client_id = ClientId, client_pid = ClientPid, username = Username, subscriptions = #{}, - max_subscriptions = proplists:get_value(max_subscriptions, Env, 0), - upgrade_qos = proplists:get_value(upgrade_qos, Env, false), + max_subscriptions = emqx_zone:env(Zone, max_subscriptions, 0), + upgrade_qos = emqx_zone:env(Zone, upgrade_qos, false), max_inflight = MaxInflight, inflight = emqx_inflight:new(MaxInflight), - mqueue = MQueue, - retry_interval = proplists:get_value(retry_interval, Env), + mqueue = init_mqueue(Zone, ClientId), + retry_interval = emqx_zone:env(Zone, retry_interval, 0), awaiting_rel = #{}, - await_rel_timeout = proplists:get_value(await_rel_timeout, Env), - max_awaiting_rel = proplists:get_value(max_awaiting_rel, Env), - expiry_interval = proplists:get_value(expiry_interval, Env), - enable_stats = EnableStats, - ignore_loop_deliver = IgnoreLoopDeliver, + await_rel_timeout = emqx_zone:env(Zone, await_rel_timeout), + max_awaiting_rel = emqx_zone:env(Zone, max_awaiting_rel), + expiry_interval = emqx_zone:env(Zone, session_expiry_interval), + enable_stats = emqx_zone:env(Zone, enable_stats, true), + ignore_loop_deliver = emqx_zone:env(Zone, ignore_loop_deliver, true), created_at = os:timestamp()}, emqx_sm:register_session(ClientId, info(State)), - emqx_hooks:run('session.created', [ClientId, Username]), - io:format("Session started: ~p~n", [self()]), + emqx_hooks:run('session.created', [ClientId]), {ok, emit_stats(State), hibernate}. +init_mqueue(Zone, ClientId) -> + emqx_mqueue:new(ClientId, #{type => simple, + max_len => emqx_zone:env(Zone, max_mqueue_len), + store_qos0 => emqx_zone:env(Zone, mqueue_store_qos0)}). + init_stats(Keys) -> lists:foreach(fun(K) -> put(K, 0) end, Keys). @@ -331,7 +348,7 @@ handle_call(Req, _From, State) -> {reply, ignored, State}. handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, - State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> + State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> ?LOG(info, "Subscribe ~p", [TopicFilters], State), {ReasonCodes, Subscriptions1} = lists:foldl(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) -> @@ -342,12 +359,12 @@ handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, SubMap; {ok, OldOpts} -> emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), - emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, SubOpts}), + emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), ?LOG(warning, "Duplicated subscribe ~s, old_opts: ~p, new_opts: ~p", [Topic, OldOpts, SubOpts], State), maps:put(Topic, SubOpts, SubMap); error -> emqx_broker:subscribe(Topic, ClientId, SubOpts), - emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, SubOpts}), + emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), maps:put(Topic, SubOpts, SubMap) end} end, {[], Subscriptions}, TopicFilters), @@ -355,14 +372,14 @@ handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, {noreply, emit_stats(State#state{subscriptions = Subscriptions1})}; handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, - State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> + State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> ?LOG(info, "Unsubscribe ~p", [TopicFilters], State), {ReasonCodes, Subscriptions1} = lists:foldl(fun(Topic, {RcAcc, SubMap}) -> case maps:find(Topic, SubMap) of {ok, SubOpts} -> emqx_broker:unsubscribe(Topic, ClientId), - emqx_hooks:run('session.unsubscribed', [ClientId, Username], {Topic, SubOpts}), + emqx_hooks:run('session.unsubscribed', [ClientId, Topic, SubOpts]), {[?RC_SUCCESS|RcAcc], maps:remove(Topic, SubMap)}; error -> {[?RC_NO_SUBSCRIPTION_EXISTED|RcAcc], SubMap} @@ -473,13 +490,18 @@ handle_cast(Msg, State) -> emqx_logger:error("[Session] unexpected cast: ~p", [Msg]), {noreply, State}. -%% Ignore Messages delivered by self +handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> + {noreply, lists:foldl(fun(Msg, NewState) -> + element(2, handle_info({dispatch, Topic, Msg}, NewState)) + end, State, Msgs)}; + +%% Ignore messages delivered by self handle_info({dispatch, _Topic, #message{from = ClientId}}, State = #state{client_id = ClientId, ignore_loop_deliver = true}) -> {noreply, State}; %% Dispatch Message -handle_info({dispatch, Topic, Msg}, State) -> +handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, message) -> {noreply, gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))}; %% Do nothing if the client has been disconnected. @@ -510,11 +532,10 @@ handle_info({'EXIT', ClientPid, Reason}, {noreply, emit_stats(State1), hibernate}; handle_info({'EXIT', Pid, _Reason}, State = #state{old_client_pid = Pid}) -> - %%ignore + %% ignore {noreply, State, hibernate}; handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = ClientPid}) -> - ?LOG(error, "unexpected EXIT: client_pid=~p, exit_pid=~p, reason=~p", [ClientPid, Pid, Reason], State), {noreply, State, hibernate}; diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index b3378ba91..563244232 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -71,8 +71,6 @@ init([]) -> SessionSup = supervisor_spec(emqx_session_sup), %% Connection Manager CMSup = supervisor_spec(emqx_cm_sup), - %% WebSocket Connection Sup - WSConnSup = supervisor_spec(emqx_ws_connection_sup), %% Sys Sup SysSup = supervisor_spec(emqx_sys_sup), {ok, {{one_for_all, 0, 1}, @@ -84,7 +82,6 @@ init([]) -> SMSup, SessionSup, CMSup, - WSConnSup, SysSup]}}. %%-------------------------------------------------------------------- diff --git a/src/emqx_time.erl b/src/emqx_time.erl index 2e69638dc..623c4a543 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -20,7 +20,7 @@ seed() -> rand:seed(exsplus, erlang:timestamp()). now_ms() -> - now_ms(os:timestamp()). + os:system_time(milli_seconds). now_ms({MegaSecs, Secs, MicroSecs}) -> (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl deleted file mode 100644 index d7f6dc6e8..000000000 --- a/src/emqx_ws.erl +++ /dev/null @@ -1,103 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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_ws). - --include("emqx_mqtt.hrl"). - --import(proplists, [get_value/3]). - -%% WebSocket Loop State --record(wsocket_state, {req, peername, client_pid, max_packet_size, parser}). - --define(WSLOG(Level, Format, Args, State), - lager:Level("WsClient(~s): " ++ Format, - [esockd_net:format(State#wsocket_state.peername) | Args])). - --export([init/2]). --export([websocket_init/1]). --export([websocket_handle/2]). --export([websocket_info/2]). - -init(Req0, _State) -> - case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req0) of - undefined -> - {cowboy_websocket, Req0, #wsocket_state{}}; - Subprotocols -> - case lists:member(<<"mqtt">>, Subprotocols) of - true -> - Peername = cowboy_req:peer(Req0), - Req = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt">>, Req0), - {cowboy_websocket, Req, #wsocket_state{req = Req, peername = Peername}, #{idle_timeout => 86400000}}; - false -> - Req = cowboy_req:reply(400, Req0), - {ok, Req, #wsocket_state{}} - end - end. - -websocket_init(State = #wsocket_state{req = Req}) -> - case emqx_ws_connection_sup:start_connection(self(), Req) of - {ok, ClientPid} -> - {ok, ProtoEnv} = emqx_config:get_env(protocol), - PacketSize = get_value(max_packet_size, ProtoEnv, ?MAX_PACKET_SIZE), - Parser = emqx_frame:initial_state(#{max_packet_size => PacketSize}), - NewState = State#wsocket_state{parser = Parser, - max_packet_size = PacketSize, - client_pid = ClientPid}, - {ok, NewState}; - Error -> - ?WSLOG(error, "Start client fail: ~p", [Error], State), - {stop, State} - end. - -websocket_handle({binary, <<>>}, State) -> - {ok, State}; -websocket_handle({binary, [<<>>]}, State) -> - {ok, State}; - -websocket_handle({binary, Data}, State = #wsocket_state{client_pid = ClientPid, parser = Parser}) -> - ?WSLOG(debug, "RECV ~p", [Data], State), - BinSize = iolist_size(Data), - emqx_metrics:inc('bytes/received', BinSize), - case catch emqx_frame:parse(iolist_to_binary(Data), Parser) of - {more, NewParser} -> - {ok, State#wsocket_state{parser = NewParser}}; - {ok, Packet, Rest} -> - gen_server:cast(ClientPid, {received, Packet, BinSize}), - websocket_handle({binary, Rest}, reset_parser(State)); - {error, Error} -> - ?WSLOG(error, "Frame error: ~p", [Error], State), - {stop, State}; - {'EXIT', Reason} -> - ?WSLOG(error, "Frame error: ~p", [Reason], State), - ?WSLOG(error, "Error data: ~p", [Data], State), - {stop, State} - end. - -websocket_info({binary, Data}, State) -> - {reply, {binary, Data}, State}; - -websocket_info({'EXIT', Pid, Reason}, State = #wsocket_state{client_pid = Pid}) -> - ?WSLOG(debug, "EXIT: ~p", [Reason], State), - {stop, State}; - -websocket_info(_Info, State) -> - {ok, State}. - -reset_parser(State = #wsocket_state{max_packet_size = PacketSize}) -> - State#wsocket_state{parser = emqx_frame:initial_state(#{max_packet_size => PacketSize})}. - - diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 93a289636..5932b9ef7 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -14,232 +14,111 @@ -module(emqx_ws_connection). --behaviour(gen_server). - -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). +-include("emqx_misc.hrl"). --import(proplists, [get_value/2, get_value/3]). - -%% API Exports --export([start_link/3]). - -%% Management and Monitor API --export([info/1, stats/1, kick/1, clean_acl_cache/2]). - -%% SUB/UNSUB Asynchronously --export([subscribe/2, unsubscribe/2]). - -%% Get the session proc? +-export([info/1]). +-export([stats/1]). +-export([kick/1]). -export([session/1]). -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +%% websocket callbacks +-export([init/2]). +-export([websocket_init/1]). +-export([websocket_handle/2]). +-export([websocket_info/2]). +-export([terminate/3]). -%% WebSocket Client State --record(wsclient_state, {ws_pid, peername, proto_state, keepalive, - enable_stats, force_gc_count}). +-record(state, { + request, + options, + peername, + sockname, + proto_state, + parser_state, + keepalive, + enable_stats, + stats_timer, + idle_timeout, + shutdown_reason + }). -%% recv_oct -%% Number of bytes received by the socket. +-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -%% recv_cnt -%% Number of packets received by the socket. - --define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). +-define(INFO_KEYS, [peername, sockname]). -define(WSLOG(Level, Format, Args, State), - emqx_logger:Level("WsClient(~s): " ++ Format, - [esockd_net:format(State#wsclient_state.peername) | Args])). + lager:Level("WsClient(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). -%% @doc Start WebSocket Client. -start_link(Env, WsPid, Req) -> - gen_server:start_link(?MODULE, [Env, WsPid, Req], - [[{hibernate_after, 10000}]]). +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ -info(CPid) -> - gen_server:call(CPid, info). +info(WSPid) -> + call(WSPid, info). -stats(CPid) -> - gen_server:call(CPid, stats). +stats(WSPid) -> + call(WSPid, stats). -kick(CPid) -> - gen_server:call(CPid, kick). +kick(WSPid) -> + call(WSPid, kick). -subscribe(CPid, TopicTable) -> - CPid ! {subscribe, TopicTable}. +session(WSPid) -> + call(WSPid, session). -unsubscribe(CPid, Topics) -> - CPid ! {unsubscribe, Topics}. - -session(CPid) -> - gen_server:call(CPid, session). - -clean_acl_cache(CPid, Topic) -> - gen_server:call(CPid, {clean_acl_cache, Topic}). - -%%-------------------------------------------------------------------- -%% gen_server Callbacks -%%-------------------------------------------------------------------- - -init([Options, WsPid, Req]) -> - init_stas(), - process_flag(trap_exit, true), - true = link(WsPid), - Peername = cowboy_req:peer(Req), - Headers = cowboy_req:headers(Req), - Sockname = cowboy_req:sock(Req), - Peercert = cowboy_req:cert(Req), - Zone = proplists:get_value(zone, Options), - ProtoState = emqx_protocol:init(#{zone => Zone, - peername => Peername, - sockname => Sockname, - peercert => Peercert, - sendfun => send_fun(WsPid)}, - [{ws_initial_headers, Headers} | Options]), - IdleTimeout = get_value(client_idle_timeout, Options, 30000), - EnableStats = get_value(client_enable_stats, Options, false), - ForceGcCount = emqx_gc:conn_max_gc_count(), - {ok, #wsclient_state{ws_pid = WsPid, - peername = Peername, - proto_state = ProtoState, - enable_stats = EnableStats, - force_gc_count = ForceGcCount}, IdleTimeout}. - -handle_call(info, From, State = #wsclient_state{peername = Peername, - proto_state = ProtoState}) -> - Info = [{websocket, true}, {peername, Peername} | emqx_protocol:info(ProtoState)], - {reply, Stats, _, _} = handle_call(stats, From, State), - reply(lists:append(Info, Stats), State); - -handle_call(stats, _From, State = #wsclient_state{proto_state = ProtoState}) -> - reply(lists:append([emqx_misc:proc_stats(), - wsock_stats(), - emqx_protocol:stats(ProtoState)]), State); - -handle_call(kick, _From, State) -> - {stop, {shutdown, kick}, ok, State}; - -handle_call(session, _From, State = #wsclient_state{proto_state = ProtoState}) -> - reply(emqx_protocol:session(ProtoState), State); - -handle_call({clean_acl_cache, Topic}, _From, State) -> - erase({acl, publish, Topic}), - reply(ok, State); - -handle_call(Req, _From, State) -> - ?WSLOG(error, "Unexpected request: ~p", [Req], State), - reply({error, unexpected_request}, State). - -handle_cast({received, Packet, BinSize}, State = #wsclient_state{proto_state = ProtoState}) -> - put(recv_oct, get(recv_oct) + BinSize), - put(recv_cnt, get(recv_cnt) + 1), - emqx_metrics:received(Packet), - case emqx_protocol:received(Packet, ProtoState) of - {ok, ProtoState1} -> - {noreply, gc(State#wsclient_state{proto_state = ProtoState1}), hibernate}; - {error, Error} -> - ?WSLOG(error, "Protocol error - ~p", [Error], State), - shutdown(Error, State); - {error, Error, ProtoState1} -> - shutdown(Error, State#wsclient_state{proto_state = ProtoState1}); - {stop, Reason, ProtoState1} -> - stop(Reason, State#wsclient_state{proto_state = ProtoState1}) - end; - -handle_cast(Msg, State) -> - ?WSLOG(error, "unexpected msg: ~p", [Msg], State), - {noreply, State}. - -handle_info(SubReq ={subscribe, _TopicTable}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:process(SubReq, ProtoState) - end, State); - -handle_info(UnsubReq = {unsubscribe, _Topics}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:process(UnsubReq, ProtoState) - end, State); - -handle_info({deliver, PubOrAck}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:deliver(PubOrAck, ProtoState) - end, gc(State)); - -handle_info(emit_stats, State) -> - {noreply, emit_stats(State), hibernate}; - -handle_info(timeout, State) -> - shutdown(idle_timeout, State); - -handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> - ?WSLOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State), - shutdown(conflict, State); - -handle_info({shutdown, Reason}, State) -> - shutdown(Reason, State); - -handle_info({keepalive, start, Interval}, State) -> - ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State), - case emqx_keepalive:start(stat_fun(), Interval, {keepalive, check}) of - {ok, KeepAlive} -> - {noreply, State#wsclient_state{keepalive = KeepAlive}, hibernate}; - {error, Error} -> - ?WSLOG(warning, "Keepalive error - ~p", [Error], State), - shutdown(Error, State) - end; - -handle_info({keepalive, check}, State = #wsclient_state{keepalive = KeepAlive}) -> - case emqx_keepalive:check(KeepAlive) of - {ok, KeepAlive1} -> - {noreply, emit_stats(State#wsclient_state{keepalive = KeepAlive1}), hibernate}; - {error, timeout} -> - ?WSLOG(debug, "Keepalive Timeout!", [], State), - shutdown(keepalive_timeout, State); - {error, Error} -> - ?WSLOG(warning, "Keepalive error - ~p", [Error], State), - shutdown(keepalive_error, State) - end; - -handle_info({'EXIT', WsPid, normal}, State = #wsclient_state{ws_pid = WsPid}) -> - stop(normal, State); - -handle_info({'EXIT', WsPid, Reason}, State = #wsclient_state{ws_pid = WsPid}) -> - ?WSLOG(error, "shutdown: ~p",[Reason], State), - shutdown(Reason, State); - -%% The session process exited unexpectedly. -handle_info({'EXIT', Pid, Reason}, State = #wsclient_state{proto_state = ProtoState}) -> - case emqx_protocol:session(ProtoState) of - Pid -> stop(Reason, State); - _ -> ?WSLOG(error, "Unexpected EXIT: ~p, Reason: ~p", [Pid, Reason], State), - {noreply, State, hibernate} - end; - -handle_info(Info, State) -> - ?WSLOG(error, "Unexpected Info: ~p", [Info], State), - {noreply, State, hibernate}. - -terminate(Reason, #wsclient_state{proto_state = ProtoState, keepalive = KeepAlive}) -> - emqx_keepalive:cancel(KeepAlive), - case Reason of - {shutdown, Error} -> - emqx_protocol:shutdown(Error, ProtoState); - _ -> - emqx_protocol:shutdown(Reason, ProtoState) +call(WSPid, Req) -> + Mref = erlang:monitor(process, WSPid), + WSPid ! {call, {self(), Mref}, Req}, + receive + {Mref, Reply} -> + erlang:demonitor(Mref, [flush]), + Reply; + {'DOWN', Mref, _, _, Reason} -> + exit(Reason) + after 5000 -> + erlang:demonitor(Mref, [flush]), + exit(timeout) end. -code_change(_OldVsn, State, _Extra) -> - {ok, State}. +%%------------------------------------------------------------------------------ +%% WebSocket callbacks +%%------------------------------------------------------------------------------ -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- +init(Req, Opts) -> + io:format("Opts: ~p~n", [Opts]), + case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of + undefined -> + {cowboy_websocket, Req, #state{}}; + Subprotocols -> + case lists:member(<<"mqtt">>, Subprotocols) of + true -> + Resp = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt">>, Req), + {cowboy_websocket, Resp, #state{request = Req, options = Opts}, #{idle_timeout => 86400000}}; + false -> + {ok, cowboy_req:reply(400, Req), #state{}} + end + end. + +websocket_init(#state{request = Req, options = Options}) -> + Peername = cowboy_req:peer(Req), + Sockname = cowboy_req:sock(Req), + Peercert = cowboy_req:cert(Req), + ProtoState = emqx_protocol:init(#{peername => Peername, + sockname => Sockname, + peercert => Peercert, + sendfun => send_fun(self())}, Options), + ParserState = emqx_protocol:parser(ProtoState), + Zone = proplists:get_value(zone, Options), + EnableStats = emqx_zone:env(Zone, enable_stats, true), + IdleTimout = emqx_zone:env(Zone, idle_timeout, 30000), + lists:foreach(fun(Stat) -> put(Stat, 0) end, ?SOCK_STATS), + {ok, #state{peername = Peername, + sockname = Sockname, + parser_state = ParserState, + proto_state = ProtoState, + enable_stats = EnableStats, + idle_timeout = IdleTimout}}. send_fun(WsPid) -> fun(Data) -> @@ -251,45 +130,143 @@ send_fun(WsPid) -> end. stat_fun() -> - fun() -> - {ok, get(recv_oct)} + fun() -> {ok, get(recv_oct)} end. + +websocket_handle({binary, <<>>}, State) -> + {ok, State}; +websocket_handle({binary, [<<>>]}, State) -> + {ok, State}; +websocket_handle({binary, Data}, State = #state{parser_state = ParserState, + proto_state = ProtoState}) -> + BinSize = iolist_size(Data), + put(recv_oct, get(recv_oct) + BinSize), + ?WSLOG(debug, "RECV ~p", [Data], State), + emqx_metrics:inc('bytes/received', BinSize), + case catch emqx_frame:parse(iolist_to_binary(Data), ParserState) of + {more, NewParserState} -> + {ok, State#state{parser_state = NewParserState}}; + {ok, Packet, Rest} -> + emqx_metrics:received(Packet), + put(recv_cnt, get(recv_cnt) + 1), + case emqx_protocol:received(Packet, ProtoState) of + {ok, ProtoState1} -> + websocket_handle({binary, Rest}, reset_parser(State#state{proto_state = ProtoState1})); + {error, Error} -> + ?WSLOG(error, "Protocol error - ~p", [Error], State), + {stop, State}; + {error, Error, ProtoState1} -> + shutdown(Error, State#state{proto_state = ProtoState1}); + {stop, Reason, ProtoState1} -> + shutdown(Reason, State#state{proto_state = ProtoState1}) + end; + {error, Error} -> + ?WSLOG(error, "Frame error: ~p", [Error], State), + {stop, State}; + {'EXIT', Reason} -> + ?WSLOG(error, "Frame error:~p~nFrame data: ~p", [Reason, Data], State), + {stop, State} end. -emit_stats(State = #wsclient_state{proto_state = ProtoState}) -> - emit_stats(emqx_protocol:clientid(ProtoState), State). +websocket_info({call, From, info}, State = #state{peername = Peername, + sockname = Sockname, + proto_state = ProtoState}) -> + ProtoInfo = emqx_protocol:info(ProtoState), + ConnInfo = [{socktype, websocket}, {conn_state, running}, + {peername, Peername}, {sockname, Sockname}], + gen_server:reply(From, lists:append([ConnInfo, ProtoInfo])), + {ok, State}; -emit_stats(_ClientId, State = #wsclient_state{enable_stats = false}) -> - State; -emit_stats(undefined, State) -> - State; -emit_stats(ClientId, State) -> - {reply, Stats, _, _} = handle_call(stats, undefined, State), - emqx_cm:set_client_stats(ClientId, Stats), +websocket_info({call, From, stats}, State = #state{proto_state = ProtoState}) -> + Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(), emqx_protocol:stats(ProtoState)]), + gen_server:reply(From, Stats), + {ok, State}; + +websocket_info({call, From, kick}, State) -> + gen_server:reply(From, ok), + shutdown(kick, State); + +websocket_info({call, From, session}, State = #state{proto_state = ProtoState}) -> + gen_server:reply(From, emqx_protocol:session(ProtoState)), + {ok, State}; + +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})}; + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, ProtoState1} -> + shutdown(Reason, State#state{proto_state = ProtoState1}) + end; + +websocket_info(emit_stats, State = #state{proto_state = ProtoState}) -> + Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(), + emqx_protocol:stats(ProtoState)]), + emqx_cm:set_client_stats(emqx_protocol:clientid(ProtoState), Stats), + {ok, State#state{stats_timer = undefined}, hibernate}; + +websocket_info({keepalive, start, Interval}, State) -> + ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State), + case emqx_keepalive:start(stat_fun(), Interval, {keepalive, check}) of + {ok, KeepAlive} -> + {ok, State#state{keepalive = KeepAlive}}; + {error, Error} -> + ?WSLOG(warning, "Keepalive error - ~p", [Error], State), + shutdown(Error, State) + end; + +websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> + case emqx_keepalive:check(KeepAlive) of + {ok, KeepAlive1} -> + {ok, State#state{keepalive = KeepAlive1}}; + {error, timeout} -> + ?WSLOG(debug, "Keepalive Timeout!", [], State), + shutdown(keepalive_timeout, State); + {error, Error} -> + ?WSLOG(warning, "Keepalive error - ~p", [Error], State), + shutdown(keepalive_error, State) + end; + +websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) -> + ?WSLOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State), + shutdown(conflict, State); + +websocket_info({binary, Data}, State) -> + {reply, {binary, Data}, State}; + +websocket_info({shutdown, Reason}, State) -> + shutdown(Reason, State); + +websocket_info(Info, State) -> + ?WSLOG(error, "unexpected info: ~p", [Info], State), + {ok, State}. + +terminate(SockError, _Req, #state{keepalive = Keepalive, + proto_state = ProtoState, + shutdown_reason = Reason}) -> + emqx_keepalive:cancel(Keepalive), + io:format("Websocket shutdown for ~p, sockerror: ~p~n", [Reason, SockError]), + case Reason of + undefined -> + ok; + %%emqx_protocol:shutdown(SockError, ProtoState); + _ -> + ok%%emqx_protocol:shutdown(Reason, ProtoState) + end. + +reset_parser(State = #state{proto_state = ProtoState}) -> + State#state{parser_state = emqx_protocol:parser(ProtoState)}. + +ensure_stats_timer(State = #state{enable_stats = true, + stats_timer = undefined, + idle_timeout = Timeout}) -> + State#state{stats_timer = erlang:send_after(Timeout, self(), emit_stats)}; +ensure_stats_timer(State) -> State. -wsock_stats() -> - [{Key, get(Key)}|| Key <- ?SOCK_STATS]. - -with_proto(Fun, State = #wsclient_state{proto_state = ProtoState}) -> - {ok, ProtoState1} = Fun(ProtoState), - {noreply, State#wsclient_state{proto_state = ProtoState1}, hibernate}. - -reply(Reply, State) -> - {reply, Reply, State, hibernate}. - shutdown(Reason, State) -> - stop({shutdown, Reason}, State). + {stop, State#state{shutdown_reason = Reason}}. -stop(Reason, State) -> - {stop, Reason, State}. - -gc(State) -> - Cb = fun() -> emit_stats(State) end, - emqx_gc:maybe_force_gc(#wsclient_state.force_gc_count, State, Cb). - -init_stas() -> - put(recv_oct, 0), - put(recv_cnt, 0), - put(send_oct, 0), - put(send_cnt, 0). +wsock_stats() -> + [{Key, get(Key)} || Key <- ?SOCK_STATS]. diff --git a/src/emqx_ws_connection_sup.erl b/src/emqx_ws_connection_sup.erl deleted file mode 100644 index 1216eeb75..000000000 --- a/src/emqx_ws_connection_sup.erl +++ /dev/null @@ -1,44 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. 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_ws_connection_sup). - --behavior(supervisor). - --export([start_link/0, start_connection/2]). - --export([init/1]). - --spec(start_link() -> {ok, pid()}). -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -%% @doc Start a MQTT/WebSocket Connection. --spec(start_connection(pid(), cowboy_req:req()) -> {ok, pid()}). -start_connection(WsPid, Req) -> - supervisor:start_child(?MODULE, [WsPid, Req]). - -%%-------------------------------------------------------------------- -%% Supervisor callbacks -%%-------------------------------------------------------------------- - -init([]) -> - %%TODO: Cannot upgrade the environments, Use zone? - Env = lists:append(emqx_config:get_env(client, []), emqx_config:get_env(protocol, [])), - {ok, {{simple_one_for_one, 0, 1}, - [{ws_connection, {emqx_ws_connection, start_link, [Env]}, - temporary, 5000, worker, [emqx_ws_connection]}]}}. - diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl index 0d874a38b..830f08b89 100644 --- a/src/emqx_zone.erl +++ b/src/emqx_zone.erl @@ -17,24 +17,32 @@ -behaviour(gen_server). -export([start_link/0]). --export([get_env/2, get_env/3]). + +-export([env/2, env/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, {timer}). + -define(TAB, ?MODULE). --define(SERVER, ?MODULE). start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -get_env(Zone, Par) -> - get_env(Zone, Par, undefined). +env(undefined, Par) -> + emqx_config:get_env(Par); +env(Zone, Par) -> + env(Zone, Par, undefined). -get_env(Zone, Par, Def) -> - try ets:lookup_element(?TAB, {Zone, Par}, 2) catch error:badarg -> Def end. +env(undefined, Par, Default) -> + emqx_config:get_env(Par, Default); +env(Zone, Par, Default) -> + try ets:lookup_element(?TAB, {Zone, Par}, 2) + catch error:badarg -> + emqx_config:get_env(Par, Default) + end. %%------------------------------------------------------------------------------ %% gen_server callbacks @@ -54,8 +62,8 @@ handle_cast(Msg, State) -> handle_info(reload, State) -> lists:foreach( - fun({Zone, Options}) -> - [ets:insert(?TAB, {{Zone, Par}, Val}) || {Par, Val} <- Options] + fun({Zone, Opts}) -> + [ets:insert(?TAB, {{Zone, Par}, Val}) || {Par, Val} <- Opts] end, emqx_config:get_env(zones, [])), {noreply, ensure_reload_timer(State), hibernate}; From c9d604ed02aff0fa40aa5f4dc5e3029986eb551d Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 13 Aug 2018 16:49:53 +0800 Subject: [PATCH 095/520] Fix the badmatch error of packet_to_msg/1 --- src/emqx_client.erl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 8c742fbfd..695b9d10c 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -992,8 +992,14 @@ deliver(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, topic => Topic, properties => Props, payload => Payload}}, State. -packet_to_msg(?PUBLISH_PACKET(Header, Topic, PacketId, Props, Payload)) -> - #mqtt_packet_header{qos = QoS, retain = R, dup = Dup} = Header, +packet_to_msg(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + dup = Dup, + qos = QoS, + retain = R}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId, + properties = Props}, + payload = Payload}) -> #mqtt_msg{qos = QoS, retain = R, dup = Dup, packet_id = PacketId, topic = Topic, props = Props, payload = Payload}. From 22e8b07a3d41602180774178a3d0b06ded7d16ec Mon Sep 17 00:00:00 2001 From: turtled Date: Sun, 19 Aug 2018 20:31:44 +0800 Subject: [PATCH 096/520] Receive/send messages by bridge --- etc/emqx.conf | 241 ++++++++++++++++++++++++++++++------- priv/emqx.schema | 27 ++++- src/emqx_bridge1.erl | 254 +++++++++++++++++++++++++++++++++++++++ src/emqx_bridge1_sup.erl | 45 +++++++ src/emqx_broker.erl | 4 +- src/emqx_message.erl | 4 +- src/emqx_sup.erl | 2 + src/emqx_time.erl | 4 +- 8 files changed, 529 insertions(+), 52 deletions(-) create mode 100644 src/emqx_bridge1.erl create mode 100644 src/emqx_bridge1_sup.erl diff --git a/etc/emqx.conf b/etc/emqx.conf index 83ddcf6a8..6b31c63c3 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1497,16 +1497,20 @@ zone.internal.mqueue_store_qos0 = true ## Bridges ##-------------------------------------------------------------------- -## Bridge Type. +##-------------------------------------------------------------------- +## Bridges to edge +##-------------------------------------------------------------------- +## Bridge type. ## -## Value: local | remote -bridge.name.type = local +## Value: Enum +## Example: out | in +bridge.edge.type = in ## Bridge address: node name for local bridge, host:port for remote. ## ## Value: String ## Example: emqx@127.0.0.1, 127.0.0.1:1883 -bridge.name.address = emqx@127.0.0.1 +bridge.edge.address = 127.0.0.1:1883 ## Protocol version of the bridge. ## @@ -1514,76 +1518,221 @@ bridge.name.address = emqx@127.0.0.1 ## - mqtt5 ## - mqtt4 ## - mqtt3 -bridge.name.proto_ver = mqtt4 +bridge.edge.proto_ver = mqtt4 ## The ClientId of a remote bridge. ## ## Value: String -bridge.name.client_id = bridge:$name +bridge.edge.client_id = bridge_edge ## The Clean start flag of a remote bridge. ## ## Value: boolean -bridge.name.clean_start = false +bridge.edge.clean_start = false ## The username for a remote bridge. ## ## Value: String -bridge.name.username = user +bridge.edge.username = user ## The password for a remote bridge. ## ## Value: String -bridge.name.password = passwd +bridge.edge.password = passwd ## Mountpoint of the bridge. ## ## Value: String -bridge.name.mountpoint = bridge/$name/ - -## PEM-encoded CA certificates of the bridge. -## -## Value: File -bridge.name.cacertfile = cacert.pem - -## SSL Certfile of the bridge. -## -## Value: File -bridge.name.certfile = cert.pem - -## SSL Keyfile of the bridge. -## -## Value: File -bridge.name.keyfile = key.pem - -## SSL Ciphers used by the bridge. -## -## Value: String -bridge.name.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384 - -## TLS versions used by the bridge. -## -## Value: String -bridge.name.tls_versions = tlsv1.2,tlsv1.1,tlsv1 - -## The pending message queue of a bridge. -## -## Value: Number -bridge.name.max_pending_messages = 10000 +## bridge.edge.mountpoint = bridge/edge/ ## Ping interval of a down bridge. ## ## Value: Duration ## Default: 10 seconds -bridge.name.keepalive = 10s +bridge.edge.keepalive = 10s -## Subscriptions of the bridge. +## Subscriptions of the bridge topic. ## +## Value: String +bridge.edge.subscription.1.topic = # + +## Subscriptions of the bridge qos. +## +## Value: Number +bridge.edge.subscription.1.qos = 1 + +## The pending message queue of a bridge. +## +## Value: Number +bridge.edge.max_pending_messages = 10000 + +## Start type of the bridge. +## +## Value: enum +## manual +## auto +bridge.edge.start_type = manual + +## Bridge reconnect count. +## +## Value: Number +bridge.edge.reconnect_count = 10 + +## Bridge reconnect time. +## +## Value: Duration +## Default: 30 seconds +bridge.edge.reconnect_time = 30s + +## PEM-encoded CA certificates of the bridge. +## +## Value: File +## bridge.edge.cacertfile = cacert.pem + +## SSL Certfile of the bridge. +## +## Value: File +## bridge.edge.certfile = cert.pem + +## SSL Keyfile of the bridge. +## +## Value: File +## bridge.edge.keyfile = key.pem + +## SSL Ciphers used by the bridge. +## +## Value: String +## bridge.edge.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384 + +## TLS versions used by the bridge. +## +## Value: String +## bridge.edge.tls_versions = tlsv1.2,tlsv1.1,tlsv1 + + +##-------------------------------------------------------------------- +## Bridges to cloud +##-------------------------------------------------------------------- +## Bridge type. +## +## Value: Enum +## Example: out | in +bridge.cloud.type = out + +## Bridge address: node name for local bridge, host:port for remote. +## +## Value: String +## Example: emqx@127.0.0.1, 127.0.0.1:1883 +bridge.cloud.address = 127.0.0.1:1883 + +## Protocol version of the bridge. +## +## Value: Enum +## - mqtt5 +## - mqtt4 +## - mqtt3 +bridge.cloud.proto_ver = mqtt4 + +## The ClientId of a remote bridge. +## +## Value: String +bridge.cloud.client_id = bridge_cloud + +## The Clean start flag of a remote bridge. +## +## Value: boolean +bridge.cloud.clean_start = false + +## The username for a remote bridge. +## +## Value: String +bridge.cloud.username = user + +## The password for a remote bridge. +## +## Value: String +bridge.cloud.password = passwd + +## Mountpoint of the bridge. +## +## Value: String +bridge.cloud.mountpoint = bridge/edge/${node}/ + +## Ping interval of a down bridge. +## +## Value: Duration ## Default: 10 seconds -bridge.name.subscription.1.topic = topic1/ -bridge.name.subscription.1.qos = 2 -## bridge.name.subscription.2.topic = topic2/ -## bridge.name.subscription.2.qos = 2 +bridge.cloud.keepalive = 10s + +## Forward message topics +## +## Value: String +## Example: topic1/#,topic2/# +bridge.cloud.forward_rule = # + +## Subscriptions of the bridge topic. +## +## Value: String +bridge.cloud.subscription.1.topic = $share/cmd/topic1 + +## Subscriptions of the bridge qos. +## +## Value: Number +bridge.cloud.subscription.1.qos = 1 + +## Bridge store message type. +## +## Value: Enum +## Example: memory | disk +bridge.cloud.store_type = memory + +## The pending message queue of a bridge. +## +## Value: Number +bridge.cloud.max_pending_messages = 10000 + +## Start type of the bridge. +## +## Value: enum +## manual +## auto +bridge.cloud.start_type = manual + +## Bridge reconnect count. +## +## Value: Number +bridge.cloud.reconnect_count = 10 + +## Bridge reconnect time. +## +## Value: Duration +## Default: 30 seconds +bridge.cloud.reconnect_time = 30s + +## PEM-encoded CA certificates of the bridge. +## +## Value: File +## bridge.cloud.cacertfile = cacert.pem + +## SSL Certfile of the bridge. +## +## Value: File +## bridge.cloud.certfile = cert.pem + +## SSL Keyfile of the bridge. +## +## Value: File +## bridge.cloud.keyfile = key.pem + +## SSL Ciphers used by the bridge. +## +## Value: String +## bridge.cloud.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384 + +## TLS versions used by the bridge. +## +## Value: String +## bridge.cloud.tls_versions = tlsv1.2,tlsv1.1,tlsv1 ##-------------------------------------------------------------------- ## Modules diff --git a/priv/emqx.schema b/priv/emqx.schema index c502a8d24..1626b645f 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1395,8 +1395,11 @@ end}. %%-------------------------------------------------------------------- {mapping, "bridge.$name.type", "emqx.bridges", [ - {default, local}, - {datatype, {enum, [local,remote]}} + {datatype, {enum, [in, out]}} +]}. + +{mapping, "bridge.$name.store_type", "emqx.bridges", [ + {datatype, {enum, [memory, disk]}} ]}. {mapping, "bridge.$name.address", "emqx.bridges", [ @@ -1428,6 +1431,10 @@ end}. {datatype, string} ]}. +{mapping, "bridge.$name.forward_rule", "emqx.bridges", [ + {datatype, string} +]}. + {mapping, "bridge.$name.cacertfile", "emqx.bridges", [ {datatype, string} ]}. @@ -1466,6 +1473,22 @@ end}. {datatype, integer} ]}. +{mapping, "bridge.$name.start_type", "emqx.bridges", [ + {datatype, {enum, [manual, auto]}}, + {default, auto} +]}. + +{mapping, "bridge.$name.reconnect_count", "emqx.bridges", [ + {default, 10}, + {datatype, integer} +]}. + +{mapping, "bridge.$name.reconnect_time", "emqx.bridges", [ + {default, "30s"}, + {datatype, {duration, s}} +]}. + + {translation, "emqx.bridges", fun(Conf) -> Split = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end, diff --git a/src/emqx_bridge1.erl b/src/emqx_bridge1.erl new file mode 100644 index 000000000..139711932 --- /dev/null +++ b/src/emqx_bridge1.erl @@ -0,0 +1,254 @@ +%% Copyright (c) 2018 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_bridge1). + +-behaviour(gen_server). + +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). + + -import(proplists, [get_value/2, get_value/3]). + +-export([start_link/2, start_bridge/1, stop_bridge/1, status/1]). + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). + +-record(state, {client_pid, options, reconnect_time, reconnect_count, + def_reconnect_count, type, mountpoint, queue, store_type, + max_pending_messages}). + +-record(mqtt_msg, {qos = ?QOS0, retain = false, dup = false, + packet_id, topic, props, payload}). + +start_link(Name, Options) -> + gen_server:start_link({local, name(Name)}, ?MODULE, [Options], []). + +start_bridge(Name) -> + gen_server:call(name(Name), start_bridge). + +stop_bridge(Name) -> + gen_server:call(name(Name), stop_bridge). + +status(Pid) -> + gen_server:call(Pid, status). + +%%------------------------------------------------------------------------------ +%% gen_server callbacks +%%------------------------------------------------------------------------------ + +init([Options]) -> + process_flag(trap_exit, true), + case get_value(start_type, Options, manual) of + manual -> ok; + auto -> erlang:send_after(1000, self(), start) + end, + ReconnectCount = get_value(reconnect_count, Options, 10), + ReconnectTime = get_value(reconnect_time, Options, 30000), + MaxPendingMsg = get_value(max_pending_messages, Options, 10000), + Mountpoint = format_mountpoint(get_value(mountpoint, Options)), + StoreType = get_value(store_type, Options, memory), + Type = get_value(type, Options, in), + Queue = [], + {ok, #state{type = Type, + mountpoint = Mountpoint, + queue = Queue, + store_type = StoreType, + options = Options, + reconnect_count = ReconnectCount, + reconnect_time = ReconnectTime, + def_reconnect_count = ReconnectCount, + max_pending_messages = MaxPendingMsg}}. + +handle_call(start_bridge, _From, State = #state{client_pid = undefined}) -> + {noreply, NewState} = handle_info(start, State), + {reply, <<"start bridge successfully">>, NewState}; + +handle_call(start_bridge, _From, State) -> + {reply, <<"bridge already started">>, State}; + +handle_call(stop_bridge, _From, State = #state{client_pid = undefined}) -> + {reply, <<"bridge not started">>, State}; + +handle_call(stop_bridge, _From, State = #state{client_pid = Pid}) -> + emqx_client:disconnect(Pid), + {reply, <<"stop bridge successfully">>, State}; + +handle_call(status, _From, State = #state{client_pid = undefined}) -> + {reply, <<"Stopped">>, State}; +handle_call(status, _From, State = #state{client_pid = _Pid})-> + {reply, <<"Running">>, State}; + +handle_call(Req, _From, State) -> + emqx_logger:error("[Bridge] unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Msg, State) -> + emqx_logger:error("[Bridge] unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(start, State = #state{reconnect_count = 0}) -> + {noreply, State}; + +%%---------------------------------------------------------------- +%% start in message bridge +%%---------------------------------------------------------------- +handle_info(start, State = #state{options = Options, + client_pid = undefined, + reconnect_time = ReconnectTime, + reconnect_count = ReconnectCount, + type = in}) -> + case emqx_client:start_link([{owner, self()}|options(Options)]) of + {ok, ClientPid, _} -> + Subs = get_value(subscriptions, Options, []), + [emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs], + {noreply, State#state{client_pid = ClientPid}}; + {error,_} -> + erlang:send_after(ReconnectTime, self(), start), + {noreply, State = #state{reconnect_count = ReconnectCount-1}} + end; + +%%---------------------------------------------------------------- +%% start out message bridge +%%---------------------------------------------------------------- +handle_info(start, State = #state{options = Options, + client_pid = undefined, + reconnect_time = ReconnectTime, + reconnect_count = ReconnectCount, + type = out}) -> + case emqx_client:start_link([{owner, self()}|options(Options)]) of + {ok, ClientPid, _} -> + Subs = get_value(subscriptions, Options, []), + [emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs], + ForwardRules = string:tokens(get_value(forward_rule, Options, ""), ","), + [emqx_broker:subscribe(i2b(Topic)) || Topic <- ForwardRules, emqx_topic:validate({filter, i2b(Topic)})], + {noreply, State#state{client_pid = ClientPid}}; + {error,_} -> + erlang:send_after(ReconnectTime, self(), start), + {noreply, State = #state{reconnect_count = ReconnectCount-1}} + end; + +%%---------------------------------------------------------------- +%% received local node message +%%---------------------------------------------------------------- +handle_info({dispatch, _, #message{topic = Topic, payload = Payload, flags = #{retain := Retain}}}, + State = #state{client_pid = Pid, mountpoint = Mountpoint, queue = Queue, + store_type = StoreType, max_pending_messages = MaxPendingMsg}) -> + Msg = #mqtt_msg{qos = 1, + retain = Retain, + topic = mountpoint(Mountpoint, Topic), + payload = Payload}, + case emqx_client:publish(Pid, Msg) of + {ok, PkgId} -> + {noreply, State#state{queue = store(StoreType, {PkgId, Msg}, Queue, MaxPendingMsg)}}; + {error, Reason} -> + emqx_logger:error("Publish fail:~p", [Reason]), + {noreply, State} + end; + +%%---------------------------------------------------------------- +%% received remote node message +%%---------------------------------------------------------------- +handle_info({publish, #{qos := QoS, dup := Dup, retain := Retain, topic := Topic, + properties := Props, payload := Payload}}, State) -> + NewMsg0 = emqx_message:make(bridge, QoS, Topic, Payload), + NewMsg1 = emqx_message:set_headers(Props, emqx_message:set_flags(#{dup => Dup, retain=> Retain}, NewMsg0)), + emqx_broker:publish(NewMsg1), + {noreply, State}; + +%%---------------------------------------------------------------- +%% received remote puback message +%%---------------------------------------------------------------- +handle_info({puback, #{packet_id := PkgId}}, State = #state{queue = Queue, store_type = StoreType}) -> + % lists:keydelete(PkgId, 1, Queue) + {noreply, State#state{queue = delete(StoreType, PkgId, Queue)}}; + +handle_info({'EXIT', Pid, normal}, State = #state{client_pid = Pid}) -> + {noreply, State#state{client_pid = undefined}}; + +handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = Pid, + reconnect_time = ReconnectTime, + def_reconnect_count = DefReconnectCount}) -> + lager:warning("emqx bridge stop reason:~p", [Reason]), + erlang:send_after(ReconnectTime, self(), start), + {noreply, State#state{client_pid = undefined, reconnect_count = DefReconnectCount}}; + +handle_info(Info, State) -> + emqx_logger:error("[Bridge] unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #state{}) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +proto_ver(mqtt3) -> v3; +proto_ver(mqtt4) -> v4; +proto_ver(mqtt5) -> v5. +address(Address) -> + case string:tokens(Address, ":") of + [Host] -> {Host, 1883}; + [Host, Port] -> {Host, list_to_integer(Port)} + end. +options(Options) -> + options(Options, []). +options([], Acc) -> + Acc; +options([{username, Username}| Options], Acc) -> + options(Options, [{username, Username}|Acc]); +options([{proto_ver, ProtoVer}| Options], Acc) -> + options(Options, [{proto_ver, proto_ver(ProtoVer)}|Acc]); +options([{password, Password}| Options], Acc) -> + options(Options, [{password, Password}|Acc]); +options([{keepalive, Keepalive}| Options], Acc) -> + options(Options, [{keepalive, Keepalive}|Acc]); +options([{client_id, ClientId}| Options], Acc) -> + options(Options, [{client_id, ClientId}|Acc]); +options([{clean_start, CleanStart}| Options], Acc) -> + options(Options, [{clean_start, CleanStart}|Acc]); +options([{address, Address}| Options], Acc) -> + {Host, Port} = address(Address), + options(Options, [{host, Host}, {port, Port}|Acc]); +options([_Option | Options], Acc) -> + options(Options, Acc). + +name(Id) -> + list_to_atom(lists:concat([?MODULE, "_", Id])). + +i2b(L) -> iolist_to_binary(L). + +mountpoint(undefined, Topic) -> + Topic; +mountpoint(Prefix, Topic) -> + <>. + +format_mountpoint(undefined) -> + undefined; +format_mountpoint(Prefix) -> + binary:replace(i2b(Prefix), <<"${node}">>, atom_to_binary(node(), utf8)). + +store(memory, Data, Queue, MaxPendingMsg) when length(Queue) =< MaxPendingMsg -> + [Data | Queue]; +store(memory, _Data, Queue, _MaxPendingMsg) -> + lager:error("Beyond max pending messages"), + Queue; +store(disk, Data, Queue, _MaxPendingMsg)-> + [Data | Queue]. + +delete(memory, PkgId, Queue) -> + lists:keydelete(PkgId, 1, Queue); +delete(disk, PkgId, Queue) -> + lists:keydelete(PkgId, 1, Queue). \ No newline at end of file diff --git a/src/emqx_bridge1_sup.erl b/src/emqx_bridge1_sup.erl new file mode 100644 index 000000000..444c7cfb5 --- /dev/null +++ b/src/emqx_bridge1_sup.erl @@ -0,0 +1,45 @@ +%% Copyright (c) 2018 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_bridge1_sup). + +-behavior(supervisor). + +-include("emqx.hrl"). + +-export([start_link/0, bridges/0]). + +%% Supervisor callbacks +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% @doc List all bridges +-spec(bridges() -> [{node(), topic(), pid()}]). +bridges() -> + [{Name, emqx_bridge1:status(Pid)} || {Name, Pid, _, _} <- supervisor:which_children(?MODULE)]. + +init([]) -> + BridgesOpts = emqx_config:get_env(bridges, []), + Bridges = [spec(Opts)|| Opts <- BridgesOpts], + {ok, {{one_for_one, 10, 100}, Bridges}}. + +spec({Id, Options})-> + #{id => Id, + start => {emqx_bridge1, start_link, [Id, Options]}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_bridge1]}. \ No newline at end of file diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 9d332a5f2..7015590d8 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -69,9 +69,9 @@ subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> -spec(subscribe(topic(), pid() | subid(), subid() | subopts()) -> ok). subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - subscribe(Topic, SubPid, SubId, []); + subscribe(Topic, SubPid, SubId, #{}); subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - subscribe(Topic, SubPid, SubId, []); + subscribe(Topic, SubPid, SubId, #{}); subscribe(Topic, SubPid, SubOpts) when is_binary(Topic), is_pid(SubPid), is_map(SubOpts) -> subscribe(Topic, SubPid, undefined, SubOpts); subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map(SubOpts) -> diff --git a/src/emqx_message.erl b/src/emqx_message.erl index ae8670942..da762703e 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -69,7 +69,9 @@ unset_flag(Flag, Msg = #message{flags = Flags}) -> set_headers(Headers, Msg = #message{headers = undefined}) when is_map(Headers) -> Msg#message{headers = Headers}; set_headers(New, Msg = #message{headers = Old}) when is_map(New) -> - Msg#message{headers = maps:merge(Old, New)}. + Msg#message{headers = maps:merge(Old, New)}; +set_headers(_, Msg) -> + Msg. get_header(Hdr, Msg) -> get_header(Hdr, Msg, undefined). diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index 563244232..cddfea8b5 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -63,6 +63,7 @@ init([]) -> BrokerSup = supervisor_spec(emqx_broker_sup), %% BridgeSup BridgeSup = supervisor_spec(emqx_bridge_sup_sup), + BridgeSup1 = supervisor_spec(emqx_bridge1_sup), %% AccessControl AccessControl = worker_spec(emqx_access_control), %% Session Manager @@ -78,6 +79,7 @@ init([]) -> RouterSup, BrokerSup, BridgeSup, + BridgeSup1, AccessControl, SMSup, SessionSup, diff --git a/src/emqx_time.erl b/src/emqx_time.erl index 97ea4b573..0d74168c4 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -14,7 +14,7 @@ -module(emqx_time). --export([seed/0, now_secs/0, now_ms/0]). +-export([seed/0, now_secs/0, now_ms/0, now_ms/1]). seed() -> rand:seed(exsplus, erlang:timestamp()). @@ -25,3 +25,5 @@ now_secs() -> now_ms() -> erlang:system_time(millisecond). +now_ms({MegaSecs, Secs, MicroSecs}) -> + (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). \ No newline at end of file From d4176461ff6ae736e08a2efd0c466ef300d8d34c Mon Sep 17 00:00:00 2001 From: Petr Gotthard Date: Mon, 20 Aug 2018 11:58:19 +0200 Subject: [PATCH 097/520] Send client_pid to distinguish multiple clients When a controlling process starts multiple clients that make multiple subscriptions it may be desirable to identify from which client a message is comming from. The topic id may not be sufficient. --- src/emqx_client.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 695b9d10c..27f8be353 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -989,7 +989,8 @@ deliver(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, topic = Topic, props = Props, payload = Payload}, State = #state{owner = Owner}) -> Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain, packet_id => PacketId, - topic => Topic, properties => Props, payload => Payload}}, + topic => Topic, properties => Props, payload => Payload, + client_pid => self()}}, State. packet_to_msg(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, From 36647b641fb1b5e1b3f600ee974e8578df5240a2 Mon Sep 17 00:00:00 2001 From: turtled Date: Fri, 24 Aug 2018 11:38:54 +0800 Subject: [PATCH 098/520] Fix select emqx_shared_subscription fail --- src/emqx_bridge1.erl | 2 +- src/emqx_shared_sub.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_bridge1.erl b/src/emqx_bridge1.erl index 139711932..cfad74803 100644 --- a/src/emqx_bridge1.erl +++ b/src/emqx_bridge1.erl @@ -19,7 +19,7 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). - -import(proplists, [get_value/2, get_value/3]). +-import(proplists, [get_value/2, get_value/3]). -export([start_link/2, start_bridge/1, stop_bridge/1, status/1]). diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 2e4772f05..5194de9d4 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -97,7 +97,7 @@ pick(SubPids) -> lists:nth((X rem length(SubPids)) + 1, SubPids). subscribers(Group, Topic) -> - ets:select(?TAB, [{{shared_subscription, Group, Topic, '$1'}, [], ['$1']}]). + ets:select(?TAB, [{{emqx_shared_subscription, Group, Topic, '$1'}, [], ['$1']}]). %%----------------------------------------------------------------------------- %% gen_server callbacks From 0f052ce352597ef09453d66a333563a481c64193 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 24 Aug 2018 18:39:59 +0800 Subject: [PATCH 099/520] Upgrade connection, protocol and session modules for MQTT 5.0 --- docs/mqtt-v5.0.pdf | Bin 2895576 -> 3215041 bytes etc/emqx.conf | 373 +++--- include/emqx.hrl | 25 +- include/emqx_mqtt.hrl | 37 +- priv/emqx.schema | 330 +++--- src/emqx.app.src | 2 +- src/emqx_access_control.erl | 49 +- src/emqx_broker.erl | 4 +- src/emqx_client.erl | 30 +- src/emqx_connection.erl | 214 ++-- src/emqx_frame.erl | 9 +- src/emqx_mqtt_caps.erl | 139 +++ src/emqx_packet.erl | 46 +- src/emqx_pmon.erl | 6 +- src/emqx_protocol.erl | 1030 +++++++++-------- src/emqx_session.erl | 358 +++--- src/emqx_sm.erl | 73 +- src/emqx_topic.erl | 66 +- src/emqx_ws_connection.erl | 8 +- src/emqx_zone.erl | 38 +- .../emqx_mqtt_caps_SUITE.erl | 16 +- test/emqx_topic_SUITE.erl | 28 +- 22 files changed, 1640 insertions(+), 1241 deletions(-) create mode 100644 src/emqx_mqtt_caps.erl rename include/emqx_misc.hrl => test/emqx_mqtt_caps_SUITE.erl (59%) diff --git a/docs/mqtt-v5.0.pdf b/docs/mqtt-v5.0.pdf index 5b7e403f86ee30f1518b3400acdcc06f77243f1b..c3d0c97258e0c619fc4d44a0b80250f7def82b96 100644 GIT binary patch delta 31475 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 0.5 +zone.external.keepalive_backoff = 0.75 + +## Maximum number of subscriptions allowed, 0 means no limit. +## +## Value: Number +zone.external.max_subscriptions = 0 + +## Force to upgrade QoS according to subscription. +## +## Value: on | off +zone.external.upgrade_qos = off + +## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. +## +## Value: Number +zone.external.max_inflight = 32 + +## Retry interval for QoS1/2 message delivering. +## +## Value: Duration +zone.external.retry_interval = 20s + +## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. +## +## Value: Number +zone.external.max_awaiting_rel = 100 + +## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. +## +## Value: Duration +zone.external.await_rel_timeout = 60s + +## Whether to ignore loop delivery of messages. +## +## Value: true | false +## Default: false +zone.external.ignore_loop_deliver = false + +## Default session expiry interval for MQTT V3.1.1 connections. +## +## Value: Duration +## -d: day +## -h: hour +## -m: minute +## -s: second +## +## Default: 2h, 2 hours +zone.external.session_expiry_interval = 2h + +## Maximum queue length. Enqueued messages when persistent client disconnected, +## or inflight window is full. 0 means no limit. +## +## Value: Number >= 0 +zone.external.max_mqueue_len = 1000 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.external.mqueue_store_qos0 = true + +##-------------------------------------------------------------------- +## Internal Zone + +zone.internal.allow_anonymous = true + +## Enable per connection stats. +## +## Value: Flag +zone.internal.enable_stats = on + +## Enable ACL check. +## +## Value: Flag +zone.internal.enable_acl = off + +## See zone.$name.wildcard_subscription. +## +## Value: boolean +## zone.internal.wildcard_subscription = true + +## See zone.$name.shared_subscription. +## +## Value: boolean +## zone.internal.shared_subscription = true + +## See zone.$name.max_subscriptions. +## +## Value: Integer +zone.internal.max_subscriptions = 0 + +## See zone.$name.max_inflight +## +## Value: Number +zone.internal.max_inflight = 32 + +## See zone.$name.max_awaiting_rel +## +## Value: Number +zone.internal.max_awaiting_rel = 100 + +## See zone.$name.max_mqueue_len +## +## Value: Number >= 0 +zone.internal.max_mqueue_len = 1000 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.internal.mqueue_store_qos0 = true + ##-------------------------------------------------------------------- ## Listeners ##-------------------------------------------------------------------- @@ -1312,187 +1500,6 @@ listener.wss.external.send_timeout_close = on listener.wss.external.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA -##-------------------------------------------------------------------- -## Zones -##-------------------------------------------------------------------- - -##-------------------------------------------------------------------- -## External Zone - -## Idle timeout of the external MQTT connections. -## -## Value: duration -zone.external.idle_timeout = 15s - -## Publish limit for the external MQTT connections. -## -## Value: rate,burst -## Default: 10 messages per second, and 100 messages burst. -## zone.external.publish_limit = 10,100 - -## Enable ACL check. -## -## Value: Flag -zone.external.enable_acl = on - -## Enable per connection statistics. -## -## Value: on | off -zone.external.enable_stats = on - -## Maximum MQTT packet size allowed. -## -## Value: Bytes -## Default: 1MB -## zone.external.max_packet_size = 64KB - -## Maximum length of MQTT clientId allowed. -## -## Value: Number [23-65535] -## zone.external.max_clientid_len = 1024 - -## Maximum topic levels allowed. 0 means no limit. -## -## Value: Number -## zone.external.max_topic_levels = 7 - -## Maximum QoS allowed. -## -## Value: 0 | 1 | 2 -## zone.external.max_qos_allowed = 2 - -## Maximum Topic Alias, 0 means no limit. -## -## Value: 0-65535 -## zone.external.max_topic_alias = 0 - -## Whether the Server supports retained messages. -## -## Value: boolean -## zone.external.retain_available = true - -## Whether the Server supports Wildcard Subscriptions -## -## Value: boolean -## zone.external.wildcard_subscription = false - -## Whether the Server supports Shared Subscriptions -## -## Value: boolean -## zone.external.shared_subscription = false - -## The backoff for MQTT keepalive timeout. The broker will kick a connection out -## until 'Keepalive * backoff * 2' timeout. -## -## Value: Float > 0.5 -zone.external.keepalive_backoff = 0.75 - -## Maximum number of subscriptions allowed, 0 means no limit. -## -## Value: Number -zone.external.max_subscriptions = 0 - -## Force to upgrade QoS according to subscription. -## -## Value: on | off -zone.external.upgrade_qos = off - -## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. -## -## Value: Number -zone.external.max_inflight = 32 - -## Retry interval for QoS1/2 message delivering. -## -## Value: Duration -zone.external.retry_interval = 20s - -## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. -## -## Value: Number -zone.external.max_awaiting_rel = 100 - -## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. -## -## Value: Duration -zone.external.await_rel_timeout = 60s - -## Whether to ignore loop delivery of messages. -## -## Value: true | false -## Default: false -zone.external.ignore_loop_deliver = false - -## Default session expiry interval for MQTT V3.1.1 connections. -## -## Value: Duration -## -d: day -## -h: hour -## -m: minute -## -s: second -## -## Default: 2h, 2 hours -zone.external.session_expiry_interval = 2h - -## Maximum queue length. Enqueued messages when persistent client disconnected, -## or inflight window is full. 0 means no limit. -## -## Value: Number >= 0 -zone.external.max_mqueue_len = 1000 - -## Whether to enqueue Qos0 messages. -## -## Value: false | true -zone.external.mqueue_store_qos0 = true - -##-------------------------------------------------------------------- -## Internal Zone - -## Enable per connection stats. -## -## Value: Flag -zone.internal.enable_stats = on - -## Enable ACL check. -## -## Value: Flag -zone.internal.enable_acl = off - -## See zone.$name.wildcard_subscription. -## -## Value: boolean -## zone.internal.wildcard_subscription = true - -## See zone.$name.shared_subscription. -## -## Value: boolean -## zone.internal.shared_subscription = true - -## See zone.$name.max_subscriptions. -## -## Value: Integer -zone.internal.max_subscriptions = 0 - -## See zone.$name.max_inflight -## -## Value: Number -zone.internal.max_inflight = 32 - -## See zone.$name.max_awaiting_rel -## -## Value: Number -zone.internal.max_awaiting_rel = 100 - -## See zone.$name.max_mqueue_len -## -## Value: Number >= 0 -zone.internal.max_mqueue_len = 1000 - -## Whether to enqueue Qos0 messages. -## -## Value: false | true -zone.internal.mqueue_store_qos0 = true - ##-------------------------------------------------------------------- ## Bridges ##-------------------------------------------------------------------- diff --git a/include/emqx.hrl b/include/emqx.hrl index 3b42713ff..0372f547e 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -53,7 +53,9 @@ -type(subid() :: binary() | atom()). --type(subopts() :: #{qos => integer(), share => '$queue' | binary(), atom() => term()}). +-type(subopts() :: #{qos => integer(), + share => '$queue' | binary(), + atom() => term()}). -record(subscription, { topic :: topic(), @@ -82,20 +84,21 @@ -type(zone() :: atom()). -record(client, { - id :: client_id(), - pid :: pid(), - zone :: zone(), - protocol :: protocol(), - peername :: peername(), - peercert :: nossl | binary(), - username :: username(), - clean_start :: boolean(), - attributes :: map() + id :: client_id(), + pid :: pid(), + zone :: zone(), + peername :: peername(), + username :: username(), + protocol :: protocol(), + attributes :: map() }). -type(client() :: #client{}). --record(session, {sid :: client_id(), pid :: pid()}). +-record(session, { + sid :: client_id(), + pid :: pid() + }). -type(session() :: #session{}). diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 007de4dd1..e429aa4a3 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -229,10 +229,17 @@ -type(mqtt_properties() :: #{atom() => term()} | undefined). -%% nl: no local, rap: retain as publish, rh: retain handling --record(mqtt_subopts, {rh = 0, rap = 0, nl = 0, qos = ?QOS_0}). +-type(mqtt_subopts() :: #{atom() => term()}). --type(mqtt_subopts() :: #mqtt_subopts{}). +-define(DEFAULT_SUBOPTS, #{rh => 0, %% Retain Handling + rap => 0, %% Retain as Publish + nl => 0, %% No Local + qos => ?QOS_0, + rc => 0, %% Reason Code + subid => 0 %% Subscription-Identifier + }). + +-type(mqtt_topic_filters() :: [{mqtt_topic(), mqtt_subopts()}]). -record(mqtt_packet_connect, { proto_name = <<"MQTT">> :: binary(), @@ -273,7 +280,7 @@ -record(mqtt_packet_subscribe, { packet_id :: mqtt_packet_id(), properties :: mqtt_properties(), - topic_filters :: [{mqtt_topic(), mqtt_subopts()}] + topic_filters :: mqtt_topic_filters() }). -record(mqtt_packet_suback, { @@ -391,6 +398,11 @@ variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = 0}}). +-define(PUBACK_PACKET(PacketId, ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode}}). + -define(PUBACK_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, variable = #mqtt_packet_puback{packet_id = PacketId, @@ -399,8 +411,13 @@ -define(PUBREC_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, - variable = #mqtt_packet_puback{packet_id = PacketId, - reason_code = 0}}). + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = 0}}). + +-define(PUBREC_PACKET(PacketId, ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode}}). -define(PUBREC_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, @@ -412,6 +429,10 @@ #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = 0}}). +-define(PUBREL_PACKET(PacketId, ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode}}). -define(PUBREL_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, @@ -423,6 +444,10 @@ #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = 0}}). +-define(PUBCOMP_PACKET(PacketId, ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode}}). -define(PUBCOMP_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, diff --git a/priv/emqx.schema b/priv/emqx.schema index c502a8d24..b35136d70 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -559,6 +559,12 @@ end}. {datatype, {enum, [true, false]}} ]}. +%% @doc ACL nomatch. +{mapping, "acl_nomatch", "emqx.acl_nomatch", [ + {default, deny}, + {datatype, {enum, [allow, deny]}} +]}. + %% @doc Default ACL file. {mapping, "acl_file", "emqx.acl_file", [ {datatype, string}, @@ -584,7 +590,7 @@ end}. %% ]}. %%-------------------------------------------------------------------- -%% MQTT +%% MQTT Protocol %%-------------------------------------------------------------------- %% @doc Max Packet Size Allowed, 1MB by default. @@ -636,6 +642,172 @@ end}. {datatype, {enum, [true, false]}} ]}. +%%-------------------------------------------------------------------- +%% Zones +%%-------------------------------------------------------------------- + +%% @doc Idle timeout of the MQTT connection. +{mapping, "zone.$name.idle_timeout", "emqx.zones", [ + {default, "15s"}, + {datatype, {duration, ms}} +]}. + +{mapping, "zone.$name.allow_anonymous", "emqx.zones", [ + {default, false}, + {datatype, {enum, [true, false]}} +]}. + +{mapping, "zone.$name.acl_nomatch", "emqx.zones", [ + {default, deny}, + {datatype, {enum, [allow, deny]}} +]}. + +%% @doc Enable ACL check. +{mapping, "zone.$name.enable_acl", "emqx.zones", [ + {default, off}, + {datatype, flag} +]}. + +%% @doc Enable per connection statistics. +{mapping, "zone.$name.enable_stats", "emqx.zones", [ + {default, off}, + {datatype, flag} +]}. + +%% @doc Publish limit of the MQTT connections. +{mapping, "zone.$name.publish_limit", "emqx.zones", [ + {default, undefined}, + {datatype, string} +]}. + +%% @doc Max Packet Size Allowed, 64K by default. +{mapping, "zone.$name.max_packet_size", "emqx.zones", [ + {datatype, bytesize} +]}. + +%% @doc Set the Max ClientId Length Allowed. +{mapping, "zone.$name.max_clientid_len", "emqx.zones", [ + {datatype, integer} +]}. + +%% @doc Set the Maximum topic levels. +{mapping, "zone.$name.max_topic_levels", "emqx.zones", [ + {datatype, integer} +]}. + +%% @doc Set the Maximum QoS allowed. +{mapping, "zone.$name.max_qos_allowed", "emqx.zones", [ + {datatype, integer}, + {validators, ["range:0-2"]} +]}. + +%% @doc Set the Maximum topic alias. +{mapping, "zone.$name.max_topic_alias", "emqx.zones", [ + {datatype, integer} +]}. + +%% @doc Whether the server supports retained messages. +{mapping, "zone.$name.retain_available", "emqx.zones", [ + {datatype, {enum, [true, false]}} +]}. + +%% @doc Whether the Server supports Wildcard Subscriptions. +{mapping, "zone.$name.wildcard_subscription", "emqx.zones", [ + {datatype, {enum, [true, false]}} +]}. + +%% @doc Whether the Server supports Shared Subscriptions. +{mapping, "zone.$name.shared_subscription", "emqx.zones", [ + {datatype, {enum, [true, false]}} +]}. + +%% @doc Keepalive backoff +{mapping, "zone.$name.keepalive_backoff", "emqx.zones", [ + {default, 0.75}, + {datatype, float} +]}. + +%% @doc Max Number of Subscriptions Allowed. +{mapping, "zone.$name.max_subscriptions", "emqx.zones", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Upgrade QoS according to subscription? +{mapping, "zone.$name.upgrade_qos", "emqx.zones", [ + {default, off}, + {datatype, flag} +]}. + +%% @doc Max number of QoS 1 and 2 messages that can be “inflight” at one time. +%% 0 means no limit +{mapping, "zone.$name.max_inflight", "emqx.zones", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Retry interval for redelivering QoS1/2 messages. +{mapping, "zone.$name.retry_interval", "emqx.zones", [ + {default, "20s"}, + {datatype, {duration, ms}} +]}. + +%% @doc Max Packets that Awaiting PUBREL, 0 means no limit +{mapping, "zone.$name.max_awaiting_rel", "emqx.zones", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Awaiting PUBREL timeout +{mapping, "zone.$name.await_rel_timeout", "emqx.zones", [ + {default, "60s"}, + {datatype, {duration, ms}} +]}. + +%% @doc Ignore loop delivery of messages +{mapping, "zone.$name.ignore_loop_deliver", "emqx.zones", [ + {default, false}, + {datatype, {enum, [true, false]}} +]}. + +%% @doc Session Expiry Interval +{mapping, "zone.$name.session_expiry_interval", "emqx.zones", [ + {default, "2h"}, + {datatype, {duration, ms}} +]}. + +%% @doc Max queue length. Enqueued messages when persistent client +%% disconnected, or inflight window is full. 0 means no limit. +{mapping, "zone.$name.max_mqueue_len", "emqx.zones", [ + {default, 1000}, + {datatype, integer} +]}. + +%% @doc Queue Qos0 messages? +{mapping, "zone.$name.mqueue_store_qos0", "emqx.zones", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +{translation, "emqx.zones", fun(Conf) -> + Mapping = fun("retain_available", Val) -> + {mqtt_retain_available, Val}; + ("wildcard_subscription", Val) -> + {mqtt_wildcard_subscription, Val}; + ("shared_subscription", Val) -> + {mqtt_shared_subscription, Val}; + (Opt, Val) -> + {list_to_atom(Opt), Val} + end, + maps:to_list( + lists:foldl( + fun({["zone", Name, Opt], Val}, Zones) -> + maps:update_with(list_to_atom(Name), + fun(Opts) -> [Mapping(Opt, Val)|Opts] end, + [Mapping(Opt, Val)], Zones) + end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("zone.", Conf)))) +end}. + %%-------------------------------------------------------------------- %% Listeners %%-------------------------------------------------------------------- @@ -1234,162 +1406,6 @@ end}. ++ cuttlefish_variable:filter_by_prefix("listener.wss", Conf)]) end}. -%%-------------------------------------------------------------------- -%% Zones -%%-------------------------------------------------------------------- - -%% @doc Idle timeout of the MQTT connection. -{mapping, "zone.$name.idle_timeout", "emqx.zones", [ - {default, "15s"}, - {datatype, {duration, ms}} -]}. - -%% @doc Enable ACL check. -{mapping, "zone.$name.enable_acl", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Enable per connection statistics. -{mapping, "zone.$name.enable_stats", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Publish limit of the MQTT connections. -{mapping, "zone.$name.publish_limit", "emqx.zones", [ - {default, undefined}, - {datatype, string} -]}. - -%% @doc Max Packet Size Allowed, 64K by default. -{mapping, "zone.$name.max_packet_size", "emqx.zones", [ - {datatype, bytesize} -]}. - -%% @doc Set the Max ClientId Length Allowed. -{mapping, "zone.$name.max_clientid_len", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Set the Maximum topic levels. -{mapping, "zone.$name.max_topic_levels", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Set the Maximum QoS allowed. -{mapping, "zone.$name.max_qos_allowed", "emqx.zones", [ - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -%% @doc Set the Maximum topic alias. -{mapping, "zone.$name.max_topic_alias", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Whether the server supports retained messages. -{mapping, "zone.$name.retain_available", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether the Server supports Wildcard Subscriptions. -{mapping, "zone.$name.wildcard_subscription", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether the Server supports Shared Subscriptions. -{mapping, "zone.$name.shared_subscription", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Keepalive backoff -{mapping, "zone.$name.keepalive_backoff", "emqx.zones", [ - {default, 0.75}, - {datatype, float} -]}. - -%% @doc Max Number of Subscriptions Allowed. -{mapping, "zone.$name.max_subscriptions", "emqx.zones", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Upgrade QoS according to subscription? -{mapping, "zone.$name.upgrade_qos", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Max number of QoS 1 and 2 messages that can be “inflight” at one time. -%% 0 means no limit -{mapping, "zone.$name.max_inflight", "emqx.zones", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Retry interval for redelivering QoS1/2 messages. -{mapping, "zone.$name.retry_interval", "emqx.zones", [ - {default, "20s"}, - {datatype, {duration, ms}} -]}. - -%% @doc Max Packets that Awaiting PUBREL, 0 means no limit -{mapping, "zone.$name.max_awaiting_rel", "emqx.zones", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Awaiting PUBREL timeout -{mapping, "zone.$name.await_rel_timeout", "emqx.zones", [ - {default, "60s"}, - {datatype, {duration, ms}} -]}. - -%% @doc Ignore message from self publish -{mapping, "zone.$name.ignore_loop_deliver", "emqx.zones", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Session Expiry Interval -{mapping, "zone.$name.session_expiry_interval", "emqx.zones", [ - {default, "2h"}, - {datatype, {duration, ms}} -]}. - -%% @doc Max queue length. Enqueued messages when persistent client -%% disconnected, or inflight window is full. 0 means no limit. -{mapping, "zone.$name.max_mqueue_len", "emqx.zones", [ - {default, 1000}, - {datatype, integer} -]}. - -%% @doc Queue Qos0 messages? -{mapping, "zone.$name.mqueue_store_qos0", "emqx.zones", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx.zones", fun(Conf) -> - Mapping = fun("retain_available", Val) -> - {mqtt_retain_available, Val}; - ("wildcard_subscription", Val) -> - {mqtt_wildcard_subscription, Val}; - ("shared_subscription", Val) -> - {mqtt_shared_subscription, Val}; - (Opt, Val) -> - {list_to_atom(Opt), Val} - end, - maps:to_list( - lists:foldl( - fun({["zone", Name, Opt], Val}, Zones) -> - maps:update_with(list_to_atom(Name), - fun(Opts) -> [Mapping(Opt, Val)|Opts] end, - [Mapping(Opt, Val)], Zones) - end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("zone.", Conf)))) -end}. - %%-------------------------------------------------------------------- %% Bridges %%-------------------------------------------------------------------- diff --git a/src/emqx.app.src b/src/emqx.app.src index b7a195c8b..d963e3662 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -3,7 +3,7 @@ {vsn,"3.0"}, {modules,[]}, {registered,[emqx_sup]}, - {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,minirest]}, + {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,cowboy]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 2b7630f1e..f43309088 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -18,15 +18,16 @@ -include("emqx.hrl"). -%% API Function Exports --export([start_link/0, auth/2, check_acl/3, reload_acl/0, lookup_mods/1, - register_mod/3, register_mod/4, unregister_mod/2, stop/0]). - +-export([start_link/0]). +-export([authenticate/2]). +-export([check_acl/3, reload_acl/0, lookup_mods/1]). -export([clean_acl_cache/1, clean_acl_cache/2]). +-export([register_mod/3, register_mod/4, unregister_mod/2]). +-export([stop/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -define(TAB, ?MODULE). -define(SERVER, ?MODULE). @@ -35,9 +36,9 @@ -record(state, {}). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% @doc Start access control server. -spec(start_link() -> {ok, pid()} | ignore | {error, term()}). @@ -58,34 +59,34 @@ register_default_mod() -> end. %% @doc Authenticate Client. --spec(auth(Client :: client(), Password :: password()) +-spec(authenticate(Client :: client(), Password :: password()) -> ok | {ok, boolean()} | {error, term()}). -auth(Client, Password) when is_record(Client, client) -> - auth(Client, Password, lookup_mods(auth)). -auth(_Client, _Password, []) -> - case emqx_config:get_env(allow_anonymous, false) of +authenticate(Client, Password) when is_record(Client, client) -> + authenticate(Client, Password, lookup_mods(auth)). + +authenticate(#client{zone = Zone}, _Password, []) -> + case emqx_zone:get_env(Zone, allow_anonymous, false) of true -> ok; false -> {error, "No auth module to check!"} end; -auth(Client, Password, [{Mod, State, _Seq} | Mods]) -> + +authenticate(Client, Password, [{Mod, State, _Seq} | Mods]) -> case catch Mod:check(Client, Password, State) of ok -> ok; {ok, IsSuper} -> {ok, IsSuper}; - ignore -> auth(Client, Password, Mods); + ignore -> authenticate(Client, Password, Mods); {error, Reason} -> {error, Reason}; {'EXIT', Error} -> {error, Error} end. %% @doc Check ACL --spec(check_acl(Client, PubSub, Topic) -> allow | deny when - Client :: client(), - PubSub :: pubsub(), - Topic :: topic()). +-spec(check_acl(client(), pubsub(), topic()) -> allow | deny). check_acl(Client, PubSub, Topic) when ?PS(PubSub) -> check_acl(Client, PubSub, Topic, lookup_mods(acl)). -check_acl(_Client, _PubSub, _Topic, []) -> - emqx_config:get_env(acl_nomatch, allow); +check_acl(#client{zone = Zone}, _PubSub, _Topic, []) -> + emqx_zone:get_env(Zone, acl_nomatch, deny); + check_acl(Client, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) -> case Mod:check_acl({Client, PubSub, Topic}, State) of allow -> allow; @@ -175,15 +176,15 @@ handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(Req, _From, State) -> - emqx_logger:error("[AccessControl] Unexpected request: ~p", [Req]), + emqx_logger:error("[AccessControl] unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - emqx_logger:error("[AccessControl] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[AccessControl] unexpected msg: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> - emqx_logger:error("[AccessControl] Unexpected info: ~p", [Info]), + emqx_logger:error("[AccessControl] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 9d332a5f2..7015590d8 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -69,9 +69,9 @@ subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> -spec(subscribe(topic(), pid() | subid(), subid() | subopts()) -> ok). subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - subscribe(Topic, SubPid, SubId, []); + subscribe(Topic, SubPid, SubId, #{}); subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - subscribe(Topic, SubPid, SubId, []); + subscribe(Topic, SubPid, SubId, #{}); subscribe(Topic, SubPid, SubOpts) when is_binary(Topic), is_pid(SubPid), is_map(SubOpts) -> subscribe(Topic, SubPid, undefined, SubOpts); subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map(SubOpts) -> diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 695b9d10c..7923f1da7 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -231,22 +231,22 @@ subscribe(Client, Properties, Topic, Opts) subscribe(Client, Properties, [{Topic, Opts}]). parse_subopt(Opts) -> - parse_subopt(Opts, #mqtt_subopts{}). + parse_subopt(Opts, #{rh => 0, rap => 0, nl => 0, qos => ?QOS_0}). -parse_subopt([], Rec) -> - Rec; -parse_subopt([{rh, I} | Opts], Rec) when I >= 0, I =< 2 -> - parse_subopt(Opts, Rec#mqtt_subopts{rh = I}); -parse_subopt([{rap, true} | Opts], Rec) -> - parse_subopt(Opts, Rec#mqtt_subopts{rap =1}); -parse_subopt([{rap, false} | Opts], Rec) -> - parse_subopt(Opts, Rec#mqtt_subopts{rap = 0}); -parse_subopt([{nl, true} | Opts], Rec) -> - parse_subopt(Opts, Rec#mqtt_subopts{nl = 1}); -parse_subopt([{nl, false} | Opts], Rec) -> - parse_subopt(Opts, Rec#mqtt_subopts{nl = 0}); -parse_subopt([{qos, QoS} | Opts], Rec) -> - parse_subopt(Opts, Rec#mqtt_subopts{qos = ?QOS_I(QoS)}). +parse_subopt([], Result) -> + Result; +parse_subopt([{rh, I} | Opts], Result) when I >= 0, I =< 2 -> + parse_subopt(Opts, Result#{rh := I}); +parse_subopt([{rap, true} | Opts], Result) -> + parse_subopt(Opts, Result#{rap := 1}); +parse_subopt([{rap, false} | Opts], Result) -> + parse_subopt(Opts, Result#{rap := 0}); +parse_subopt([{nl, true} | Opts], Result) -> + parse_subopt(Opts, Result#{nl := 1}); +parse_subopt([{nl, false} | Opts], Result) -> + parse_subopt(Opts, Result#{nl := 0}); +parse_subopt([{qos, QoS} | Opts], Result) -> + parse_subopt(Opts, Result#{qos := ?QOS_I(QoS)}). -spec(publish(client(), topic(), payload()) -> ok | {error, term()}). publish(Client, Topic, Payload) when is_binary(Topic) -> diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 38c100297..5ebd54dd0 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -18,40 +18,34 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --include("emqx_misc.hrl"). -export([start_link/3]). -export([info/1, stats/1, kick/1]). -export([session/1]). --export([clean_acl_cache/1]). --export([get_rate_limit/1, set_rate_limit/2]). --export([get_pub_limit/1, set_pub_limit/2]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, - terminate/2]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(state, { - transport, %% Network transport module - socket, %% TCP or SSL Socket - peername, %% Peername of the socket - sockname, %% Sockname of the socket - conn_state, %% Connection state: running | blocked - await_recv, %% Awaiting recv - incoming, %% Incoming bytes and packets - pub_limit, %% Publish rate limit - rate_limit, %% Traffic rate limit - limit_timer, %% Rate limit timer - proto_state, %% MQTT protocol state - parser_state, %% MQTT parser state - keepalive, %% MQTT keepalive timer - enable_stats, %% Enable stats - stats_timer, %% Stats timer - idle_timeout %% Connection idle timeout + transport, + socket, + peername, + sockname, + conn_state, + await_recv, + proto_state, + parser_state, + keepalive, + enable_stats, + stats_timer, + incoming, + rate_limit, + publish_limit, + limit_timer, + idle_timeout }). --define(INFO_KEYS, [peername, sockname, conn_state, await_recv, rate_limit, pub_limit]). - -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). -define(LOG(Level, Format, Args, State), @@ -66,31 +60,19 @@ start_link(Transport, Socket, Options) -> %%------------------------------------------------------------------------------ info(CPid) -> - gen_server:call(CPid, info). + call(CPid, info). stats(CPid) -> - gen_server:call(CPid, stats). + call(CPid, stats). kick(CPid) -> - gen_server:call(CPid, kick). + call(CPid, kick). session(CPid) -> - gen_server:call(CPid, session, infinity). + call(CPid, session). -clean_acl_cache(CPid) -> - gen_server:call(CPid, clean_acl_cache). - -get_rate_limit(CPid) -> - gen_server:call(CPid, get_rate_limit). - -set_rate_limit(CPid, Rl = {_Rate, _Burst}) -> - gen_server:call(CPid, {set_rate_limit, Rl}). - -get_pub_limit(CPid) -> - gen_server:call(CPid, get_pub_limit). - -set_pub_limit(CPid, Rl = {_Rate, _Burst}) -> - gen_server:call(CPid, {set_pub_limit, Rl}). +call(CPid, Req) -> + gen_server:call(CPid, Req, infinity). %%------------------------------------------------------------------------------ %% gen_server callbacks @@ -103,60 +85,76 @@ init([Transport, RawSocket, Options]) -> {ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]), {ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]), Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]), - PubLimit = rate_limit(emqx_zone:env(Zone, publish_limit)), - RateLimit = rate_limit(proplists:get_value(rate_limit, Options)), - EnableStats = emqx_zone:env(Zone, enable_stats, true), - IdleTimout = emqx_zone:env(Zone, idle_timeout, 30000), + RateLimit = init_limiter(proplists:get_value(rate_limit, Options)), + PubLimit = init_limiter(emqx_zone:get_env(Zone, publish_limit)), + EnableStats = emqx_zone:get_env(Zone, enable_stats, true), + IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), SendFun = send_fun(Transport, Socket, Peername), ProtoState = emqx_protocol:init(#{peername => Peername, sockname => Sockname, peercert => Peercert, sendfun => SendFun}, Options), ParserState = emqx_protocol:parser(ProtoState), - State = run_socket(#state{transport = Transport, - socket = Socket, - peername = Peername, - await_recv = false, - conn_state = running, - rate_limit = RateLimit, - pub_limit = PubLimit, - proto_state = ProtoState, - parser_state = ParserState, - enable_stats = EnableStats, - idle_timeout = IdleTimout}), + State = run_socket(#state{transport = Transport, + socket = Socket, + peername = Peername, + await_recv = false, + conn_state = running, + rate_limit = RateLimit, + publish_limit = PubLimit, + proto_state = ProtoState, + parser_state = ParserState, + enable_stats = EnableStats, + idle_timeout = IdleTimout}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> {stop, Reason} end. -rate_limit(undefined) -> +init_limiter(undefined) -> undefined; -rate_limit({Rate, Burst}) -> +init_limiter({Rate, Burst}) -> esockd_rate_limit:new(Rate, Burst). send_fun(Transport, Socket, Peername) -> fun(Data) -> try Transport:async_send(Socket, Data) of - ok -> + ok -> ?LOG(debug, "SEND ~p", [Data], #state{peername = Peername}), emqx_metrics:inc('bytes/sent', iolist_size(Data)), ok; Error -> Error catch - error:Error -> {error, Error} + error:Error -> + {error, Error} end end. -handle_call(info, From, State = #state{transport = Transport, socket = Socket, proto_state = ProtoState}) -> +handle_call(info, _From, State = #state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, + conn_state = ConnState, + await_recv = AwaitRecv, + rate_limit = RateLimit, + publish_limit = PubLimit, + proto_state = ProtoState}) -> + ConnInfo = [{socktype, Transport:type(Socket)}, + {peername, Peername}, + {sockname, Sockname}, + {conn_state, ConnState}, + {await_recv, AwaitRecv}, + {rate_limit, esockd_rate_limit:info(RateLimit)}, + {publish_limit, esockd_rate_limit:info(PubLimit)}], ProtoInfo = emqx_protocol:info(ProtoState), - ConnInfo = [{socktype, Transport:type(Socket)} | ?record_to_proplist(state, State, ?INFO_KEYS)], - StatsInfo = element(2, handle_call(stats, From, State)), - {reply, lists:append([ConnInfo, StatsInfo, ProtoInfo]), State}; + {reply, lists:usort(lists:append([ConnInfo, ProtoInfo])), State}; -handle_call(stats, _From, State = #state{transport = Transport, socket = Sock, proto_state = ProtoState}) -> +handle_call(stats, _From, State = #state{transport = Transport, + socket = Socket, + proto_state = ProtoState}) -> ProcStats = emqx_misc:proc_stats(), ProtoStats = emqx_protocol:stats(ProtoState), - SockStats = case Transport:getstat(Sock, ?SOCK_STATS) of + SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of {ok, Ss} -> Ss; {error, _} -> [] end, @@ -168,21 +166,6 @@ handle_call(kick, _From, State) -> handle_call(session, _From, State = #state{proto_state = ProtoState}) -> {reply, emqx_protocol:session(ProtoState), State}; -handle_call(clean_acl_cache, _From, State = #state{proto_state = ProtoState}) -> - {reply, ok, State#state{proto_state = emqx_protocol:clean_acl_cache(ProtoState)}}; - -handle_call(get_rate_limit, _From, State = #state{rate_limit = Rl}) -> - {reply, esockd_rate_limit:info(Rl), State}; - -handle_call({set_rate_limit, {Rate, Burst}}, _From, State) -> - {reply, ok, State#state{rate_limit = esockd_rate_limit:new(Rate, Burst)}}; - -handle_call(get_publish_limit, _From, State = #state{pub_limit = Rl}) -> - {reply, esockd_rate_limit:info(Rl), State}; - -handle_call({set_publish_limit, {Rate, Burst}}, _From, State) -> - {reply, ok, State#state{pub_limit = esockd_rate_limit:new(Rate, Burst)}}; - handle_call(Req, _From, State) -> ?LOG(error, "unexpected call: ~p", [Req], State), {reply, ignored, State}. @@ -203,7 +186,7 @@ handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> handle_info(emit_stats, State = #state{proto_state = ProtoState}) -> Stats = element(2, handle_call(stats, undefined, State)), - emqx_cm:set_client_stats(emqx_protocol:clientid(ProtoState), Stats), + emqx_cm:set_client_stats(emqx_protocol:client_id(ProtoState), Stats), {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info(timeout, State) -> @@ -220,8 +203,8 @@ handle_info(activate_sock, State) -> {noreply, run_socket(State#state{conn_state = running, limit_timer = undefined})}; handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) -> - Size = iolist_size(Data), ?LOG(debug, "RECV ~p", [Data], State), + Size = iolist_size(Data), emqx_metrics:inc('bytes/received', Size), Incoming = #{bytes => Size, packets => 0}, handle_packet(Data, State#state{await_recv = false, incoming = Incoming}); @@ -247,7 +230,6 @@ handle_info({keepalive, start, Interval}, State = #state{transport = Transport, {ok, KeepAlive} -> {noreply, State#state{keepalive = KeepAlive}}; {error, Error} -> - ?LOG(warning, "Keepalive error - ~p", [Error], State), shutdown(Error, State) end; @@ -256,10 +238,8 @@ handle_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> {ok, KeepAlive1} -> {noreply, State#state{keepalive = KeepAlive1}}; {error, timeout} -> - ?LOG(debug, "Keepalive timeout", [], State), shutdown(keepalive_timeout, State); {error, Error} -> - ?LOG(warning, "Keepalive error - ~p", [Error], State), shutdown(Error, State) end; @@ -286,28 +266,25 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%------------------------------------------------------------------------------ -%% Internal functions +%% Parse and handle packets %%------------------------------------------------------------------------------ %% Receive and parse data handle_packet(<<>>, State) -> {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; -handle_packet(Bytes, State = #state{incoming = Incoming, - parser_state = ParserState, - proto_state = ProtoState, - idle_timeout = IdleTimeout}) -> - case catch emqx_frame:parse(Bytes, ParserState) of +handle_packet(Data, State = #state{proto_state = ProtoState, + parser_state = ParserState, + idle_timeout = IdleTimeout}) -> + case catch emqx_frame:parse(Data, ParserState) of {more, NewParserState} -> {noreply, State#state{parser_state = NewParserState}, IdleTimeout}; {ok, Packet = ?PACKET(Type), Rest} -> emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> - ParserState1 = emqx_protocol:parser(ProtoState1), - handle_packet(Rest, State#state{incoming = count_packets(Type, Incoming), - proto_state = ProtoState1, - parser_state = ParserState1}); + NewState = State#state{proto_state = ProtoState1}, + handle_packet(Rest, inc_publish_cnt(Type, reset_parser(NewState))); {error, Error} -> ?LOG(error, "Protocol error - ~p", [Error], State), shutdown(Error, State); @@ -320,20 +297,27 @@ handle_packet(Bytes, State = #state{incoming = Incoming, ?LOG(error, "Framing error - ~p", [Error], State), shutdown(Error, State); {'EXIT', Reason} -> - ?LOG(error, "Parse failed for ~p~nError data:~p", [Reason, Bytes], State), + ?LOG(error, "Parse failed for ~p~nError data:~p", [Reason, Data], State), shutdown(parse_error, State) end. -count_packets(?PUBLISH, Incoming = #{packets := Num}) -> - Incoming#{packets := Num + 1}; -count_packets(?SUBSCRIBE, Incoming = #{packets := Num}) -> - Incoming#{packets := Num + 1}; -count_packets(_Type, Incoming) -> - Incoming. +reset_parser(State = #state{proto_state = ProtoState}) -> + State#state{parser_state = emqx_protocol:parser(ProtoState)}. -ensure_rate_limit(State = #state{rate_limit = Rl, pub_limit = Pl, - incoming = #{bytes := Bytes, packets := Pkts}}) -> - ensure_rate_limit([{Pl, #state.pub_limit, Pkts}, {Rl, #state.rate_limit, Bytes}], State). +inc_publish_cnt(Type, State = #state{incoming = Incoming = #{packets := Cnt}}) + when Type == ?PUBLISH; Type == ?SUBSCRIBE -> + State#state{incoming = Incoming#{packets := Cnt + 1}}; +inc_publish_cnt(_Type, State) -> + State. + +%%------------------------------------------------------------------------------ +%% Ensure rate limit +%%------------------------------------------------------------------------------ + +ensure_rate_limit(State = #state{rate_limit = Rl, publish_limit = Pl, + incoming = #{packets := Packets, bytes := Bytes}}) -> + ensure_rate_limit([{Pl, #state.publish_limit, Packets}, + {Rl, #state.rate_limit, Bytes}], State). ensure_rate_limit([], State) -> run_socket(State); @@ -356,12 +340,15 @@ run_socket(State = #state{transport = Transport, socket = Sock}) -> Transport:async_recv(Sock, 0, infinity), State#state{await_recv = true}. +%%------------------------------------------------------------------------------ +%% Ensure stats timer +%%------------------------------------------------------------------------------ + ensure_stats_timer(State = #state{enable_stats = true, - stats_timer = undefined, - idle_timeout = IdleTimeout}) -> + stats_timer = undefined, + idle_timeout = IdleTimeout}) -> State#state{stats_timer = erlang:send_after(IdleTimeout, self(), emit_stats)}; -ensure_stats_timer(State) -> - State. +ensure_stats_timer(State) -> State. shutdown(Reason, State) -> stop({shutdown, Reason}, State). @@ -370,7 +357,6 @@ stop(Reason, State) -> {stop, Reason, State}. maybe_gc(State) -> - State. %% TODO:... - %%Cb = fun() -> Transport:gc(Sock), end, - %%emqx_gc:maybe_force_gc(#state.force_gc_count, State, Cb). + %% TODO: gc and shutdown policy + State. diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 10498afcf..82db0acf5 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -31,7 +31,8 @@ -export_type([options/0, parse_state/0]). --define(DEFAULT_OPTIONS, #{max_packet_size => ?MAX_PACKET_SIZE, version => ?MQTT_PROTO_V4}). +-define(DEFAULT_OPTIONS, #{max_packet_size => ?MAX_PACKET_SIZE, + version => ?MQTT_PROTO_V4}). %%------------------------------------------------------------------------------ %% Init parse state @@ -330,7 +331,7 @@ parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) -> {Value + Len * Multiplier, Rest}. parse_topic_filters(subscribe, Bin) -> - [{Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} + [{Topic, #{rh => Rh, rap => Rap, nl => Nl, qos => QoS}} || <> <= Bin]; parse_topic_filters(unsubscribe, Bin) -> @@ -576,12 +577,12 @@ serialize_property('Shared-Subscription-Available', Val) -> serialize_topic_filters(subscribe, TopicFilters, ?MQTT_PROTO_V5) -> << <<(serialize_utf8_string(Topic))/binary, ?RESERVED:2, Rh:2, (flag(Rap)):1,(flag(Nl)):1, QoS:2 >> - || {Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} + || {Topic, #{rh := Rh, rap := Rap, nl := Nl, qos := QoS}} <- TopicFilters >>; serialize_topic_filters(subscribe, TopicFilters, _Ver) -> << <<(serialize_utf8_string(Topic))/binary, ?RESERVED:6, QoS:2>> - || {Topic, #mqtt_subopts{qos = QoS}} <- TopicFilters >>; + || {Topic, #{qos := QoS}} <- TopicFilters >>; serialize_topic_filters(unsubscribe, TopicFilters, _Ver) -> << <<(serialize_utf8_string(Topic))/binary>> || Topic <- TopicFilters >>. diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl new file mode 100644 index 000000000..184e03673 --- /dev/null +++ b/src/emqx_mqtt_caps.erl @@ -0,0 +1,139 @@ +%% Copyright (c) 2018 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 MQTTv5 capabilities +-module(emqx_mqtt_caps). + +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). + +-export([check_pub/2, check_sub/2]). +-export([get_caps/1, get_caps/2]). + +-type(caps() :: #{max_packet_size => integer(), + max_clientid_len => integer(), + max_topic_alias => integer(), + max_topic_levels => integer(), + max_qos_allowed => mqtt_qos(), + mqtt_retain_available => boolean(), + mqtt_shared_subscription => boolean(), + mqtt_wildcard_subscription => boolean()}). + +-export_type([caps/0]). + +-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]). +-define(SUBCAP_KEYS, [max_qos_allowed, + max_topic_levels, + mqtt_retain_available, + mqtt_shared_subscription, + mqtt_wildcard_subscription]). + +-spec(check_pub(zone(), map()) -> ok | {error, mqtt_reason_code()}). +check_pub(Zone, Props) when is_map(Props) -> + do_check_pub(Props, maps:to_list(get_caps(Zone, publish))). + +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(#{retain := true}, [{mqtt_retain_available, false}|_Caps]) -> + {error, ?RC_RETAIN_NOT_SUPPORTED}; +do_check_pub(Props, [{mqtt_retain_available, true}|Caps]) -> + do_check_pub(Props, Caps). + +-spec(check_sub(zone(), mqtt_topic_filters()) -> {ok | error, mqtt_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). + +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. + +get_caps(Zone, publish) -> + with_env(Zone, '$mqtt_pub_caps', + fun() -> + filter_caps(?PUBCAP_KEYS, get_caps(Zone)) + end); + +get_caps(Zone, subscribe) -> + with_env(Zone, '$mqtt_sub_caps', + fun() -> + filter_caps(?SUBCAP_KEYS, get_caps(Zone)) + end). + +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). + +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 + end. + diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 65f125f68..67d1bffff 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -17,21 +17,59 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([protocol_name/1, type_name/1]). +-export([protocol_name/1]). +-export([type_name/1]). +-export([validate/1]). -export([format/1]). -export([to_message/2, from_message/2]). %% @doc Protocol name of version -spec(protocol_name(mqtt_version()) -> binary()). -protocol_name(?MQTT_PROTO_V3) -> <<"MQIsdp">>; -protocol_name(?MQTT_PROTO_V4) -> <<"MQTT">>; -protocol_name(?MQTT_PROTO_V5) -> <<"MQTT">>. +protocol_name(?MQTT_PROTO_V3) -> + <<"MQIsdp">>; +protocol_name(?MQTT_PROTO_V4) -> + <<"MQTT">>; +protocol_name(?MQTT_PROTO_V5) -> + <<"MQTT">>. %% @doc Name of MQTT packet type -spec(type_name(mqtt_packet_type()) -> atom()). type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> lists:nth(Type, ?TYPE_NAMES). +validate(?SUBSCRIBE_PACKET(_PacketId, _Properties, [])) -> + error(packet_empty_topic_filters); +validate(?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters)) -> + validate_packet_id(PacketId) + andalso validate_properties(?SUBSCRIBE, Properties) + andalso ok == lists:foreach(fun validate_subscription/1, TopicFilters); + +validate(?UNSUBSCRIBE_PACKET(_PacketId, [])) -> + error(packet_empty_topic_filters); +validate(?UNSUBSCRIBE_PACKET(PacketId, TopicFilters)) -> + validate_packet_id(PacketId) + andalso ok == lists:foreach(fun emqx_topic:validate/1, TopicFilters); + +validate(_Packet) -> + true. + +validate_packet_id(0) -> + error(bad_packet_id); +validate_packet_id(_) -> + true. + +validate_properties(?SUBSCRIBE, #{'Subscription-Identifier' := 0}) -> + error(bad_subscription_identifier); +validate_properties(?SUBSCRIBE, _) -> + true. + +validate_subscription({Topic, #{qos := QoS}}) -> + emqx_topic:validate(filter, Topic) andalso validate_qos(QoS). + +validate_qos(QoS) when ?QOS0 =< QoS, QoS =< ?QOS2 -> + true; +validate_qos(_) -> error(bad_qos). + %% @doc From Message to Packet -spec(from_message(mqtt_packet_id(), message()) -> mqtt_packet()). from_message(PacketId, Msg = #message{qos = QoS, topic = Topic, payload = Payload}) -> diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index 8b421f20a..9b874041e 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -15,7 +15,11 @@ -module(emqx_pmon). -export([new/0]). --export([monitor/2, monitor/3, demonitor/2, find/2, erase/2]). +-export([monitor/2, monitor/3]). +-export([demonitor/2]). +-export([find/2]). +-export([erase/2]). + -compile({no_auto_import,[monitor/3]}). -type(pmon() :: {?MODULE, map()}). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 705674000..077c7a00c 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -16,523 +16,595 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --include("emqx_misc.hrl"). --export([init/2, info/1, stats/1, clientid/1, session/1]). -%%-export([capabilities/1]). +-export([init/2, info/1, caps/1, stats/1]). +-export([client/1, client_id/1]). +-export([session/1]). -export([parser/1]). -export([received/2, process/2, deliver/2, send/2]). -export([shutdown/2]). --ifdef(TEST). --compile(export_all). --endif. +-record(pstate, { + zone, + sendfun, + peername, + peercert, + proto_ver, + proto_name, + ackprops, + client_id, + client_pid, + conn_props, + ack_props, + username, + session, + clean_start, + packet_size, + will_msg, + keepalive, + mountpoint, + is_super, + is_bridge, + enable_acl, + recv_stats, + send_stats, + connected, + connected_at + }). --define(CAPABILITIES, [{max_packet_size, ?MAX_PACKET_SIZE}, - {max_clientid_len, ?MAX_CLIENTID_LEN}, - {max_topic_alias, 0}, - {max_qos_allowed, ?QOS2}, - {retain_available, true}, - {shared_subscription, true}, - {wildcard_subscription, true}]). +-type(state() :: #pstate{}). --record(proto_state, {zone, sockprops, capabilities, connected, client_id, client_pid, - clean_start, proto_ver, proto_name, username, connprops, - is_superuser, will_msg, keepalive, keepalive_backoff, session, - recv_pkt = 0, recv_msg = 0, send_pkt = 0, send_msg = 0, - mountpoint, is_bridge, connected_at}). +-export_type([state/0]). --define(INFO_KEYS, [capabilities, connected, client_id, clean_start, username, proto_ver, proto_name, - keepalive, will_msg, mountpoint, is_bridge, connected_at]). +-define(LOG(Level, Format, Args, PState), + emqx_logger:Level([{client, PState#pstate.client_id}], "Client(~s@~s): " ++ Format, + [PState#pstate.client_id, esockd_net:format(PState#pstate.peername) | Args])). --define(STATS_KEYS, [recv_pkt, recv_msg, send_pkt, send_msg]). +%%------------------------------------------------------------------------------ +%% Init +%%------------------------------------------------------------------------------ --define(LOG(Level, Format, Args, State), - emqx_logger:Level([{client, State#proto_state.client_id}], "Client(~s@~s): " ++ Format, - [State#proto_state.client_id, - esockd_net:format(maps:get(peername, State#proto_state.sockprops)) | Args])). - --type(proto_state() :: #proto_state{}). - --export_type([proto_state/0]). - -init(SockProps = #{peercert := Peercert}, Options) -> +-spec(init(map(), list()) -> state()). +init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) -> Zone = proplists:get_value(zone, Options), - MountPoint = emqx_zone:env(Zone, mountpoint), - Backoff = emqx_zone:env(Zone, keepalive_backoff, 0.75), - Username = case proplists:get_value(peer_cert_as_username, Options) of - cn -> esockd_peercert:common_name(Peercert); - dn -> esockd_peercert:subject(Peercert); - _ -> undefined - end, - #proto_state{zone = Zone, - sockprops = SockProps, - capabilities = capabilities(Zone), - connected = false, - clean_start = true, - client_pid = self(), - proto_ver = ?MQTT_PROTO_V4, - proto_name = <<"MQTT">>, - username = Username, - is_superuser = false, - keepalive_backoff = Backoff, - mountpoint = MountPoint, - is_bridge = false, - recv_pkt = 0, - recv_msg = 0, - send_pkt = 0, - send_msg = 0}. + #pstate{zone = Zone, + sendfun = SendFun, + peername = Peername, + peercert = Peercert, + proto_ver = ?MQTT_PROTO_V4, + proto_name = <<"MQTT">>, + client_pid = self(), + username = init_username(Peercert, Options), + is_super = false, + clean_start = false, + packet_size = emqx_zone:get_env(Zone, max_packet_size), + mountpoint = emqx_zone:get_env(Zone, mountpoint), + is_bridge = false, + enable_acl = emqx_zone:get_env(Zone, enable_acl), + recv_stats = #{msg => 0, pkt => 0}, + send_stats = #{msg => 0, pkt => 0}, + connected = fasle}. -capabilities(Zone) -> - Capabilities = emqx_zone:env(Zone, mqtt_capabilities, []), - maps:from_list(lists:ukeymerge(1, ?CAPABILITIES, Capabilities)). +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); + _ -> undefined + end. -parser(#proto_state{capabilities = #{max_packet_size := Size}, proto_ver = Ver}) -> - emqx_frame:initial_state(#{max_packet_size => Size, version => Ver}). +set_username(Username, PState = #pstate{username = undefined}) -> + PState#pstate{username = Username}; +set_username(_Username, PState) -> + PState. -info(ProtoState) -> - ?record_to_proplist(proto_state, ProtoState, ?INFO_KEYS). +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ -stats(ProtoState) -> - ?record_to_proplist(proto_state, ProtoState, ?STATS_KEYS). +info(#pstate{zone = Zone, + peername = Peername, + proto_ver = ProtoVer, + proto_name = ProtoName, + conn_props = ConnProps, + client_id = ClientId, + username = Username, + clean_start = CleanStart, + keepalive = Keepalive, + mountpoint = Mountpoint, + is_super = IsSuper, + is_bridge = IsBridge, + connected = Connected, + connected_at = ConnectedAt}) -> + [{zone, Zone}, + {peername, Peername}, + {proto_ver, ProtoVer}, + {proto_name, ProtoName}, + {conn_props, ConnProps}, + {client_id, ClientId}, + {username, Username}, + {clean_start, CleanStart}, + {keepalive, Keepalive}, + {mountpoint, Mountpoint}, + {is_super, IsSuper}, + {is_bridge, IsBridge}, + {connected, Connected}, + {connected_at, ConnectedAt}]. -clientid(#proto_state{client_id = ClientId}) -> +caps(#pstate{zone = Zone}) -> + emqx_mqtt_caps:get_caps(Zone). + +client(#pstate{zone = Zone, + client_id = ClientId, + client_pid = ClientPid, + peername = Peername, + username = Username}) -> + #client{id = ClientId, + pid = ClientPid, + zone = Zone, + peername = Peername, + username = Username}. + +client_id(#pstate{client_id = ClientId}) -> ClientId. -client(#proto_state{sockprops = #{peername := Peername}, - client_id = ClientId, client_pid = ClientPid, username = Username}) -> - #client{id = ClientId, pid = ClientPid, username = Username, peername = Peername}. +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}]. -session(#proto_state{session = Session}) -> - Session. +session(#pstate{session = SPid}) -> + SPid. -%% CONNECT – Client requests a connection to a Server +parser(#pstate{packet_size = Size, proto_ver = Ver}) -> + emqx_frame:initial_state(#{packet_size => Size, version => Ver}). -%% A Client can only send the CONNECT Packet once over a Network Connection. --spec(received(mqtt_packet(), proto_state()) -> {ok, proto_state()} | {error, term()}). -received(Packet = ?PACKET(?CONNECT), ProtoState = #proto_state{connected = false}) -> - trace(recv, Packet, ProtoState), - process(Packet, inc_stats(recv, ?CONNECT, ProtoState#proto_state{connected = true})); +%%------------------------------------------------------------------------------ +%% Packet Received +%%------------------------------------------------------------------------------ -received(?PACKET(?CONNECT), State = #proto_state{connected = true}) -> - {error, protocol_bad_connect, State}; +-spec(received(mqtt_packet(), state()) + -> {ok, state()} | {error, term()} | {error, term(), state()}). +received(?PACKET(Type), PState = #pstate{connected = false}) + when Type =/= ?CONNECT -> + {error, proto_not_connected, PState}; -%% Received other packets when CONNECT not arrived. -received(_Packet, ProtoState = #proto_state{connected = false}) -> - {error, protocol_not_connected, ProtoState}; +received(?PACKET(?CONNECT), PState = #pstate{connected = true}) -> + {error, proto_bad_connect, PState}; -received(Packet = ?PACKET(Type), ProtoState) -> - trace(recv, Packet, ProtoState), - case validate_packet(Packet) of - ok -> - process(Packet, inc_stats(recv, Type, ProtoState)); - {error, Reason} -> - {error, Reason, ProtoState} +received(Packet = ?PACKET(Type), PState) -> + trace(recv, Packet, PState), + case catch emqx_packet:validate(Packet) of + true -> + process(Packet, inc_stats(recv, Type, PState)); + {'EXIT', {ReasonCode, _Stacktrace}} when is_integer(ReasonCode) -> + deliver({disconnect, ReasonCode}, PState), + {error, protocol_error, PState}; + {'EXIT', {Reason, _Stacktrace}} -> + deliver({disconnect, ?RC_MALFORMED_PACKET}, PState), + {error, Reason, PState} end. -process(?CONNECT_PACKET(Var), ProtoState = #proto_state{zone = Zone, - username = Username0, - client_pid = ClientPid}) -> - #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} = Var, - ProtoState1 = ProtoState#proto_state{proto_ver = ProtoVer, +%%------------------------------------------------------------------------------ +%% Process 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} = Connect), PState) -> + + PState1 = set_username(Username, + PState#pstate{client_id = ClientId, + proto_ver = ProtoVer, proto_name = ProtoName, - username = if Username0 == undefined -> - Username; - true -> Username0 - end, %% TODO: fixme later. - client_id = ClientId, clean_start = CleanStart, keepalive = Keepalive, - connprops = ConnProps, - will_msg = willmsg(Var, ProtoState), + conn_props = ConnProps, + will_msg = willmsg(Connect, PState), is_bridge = IsBridge, - connected_at = os:timestamp()}, + connected = true, + connected_at = os:timestamp()}), - {ReturnCode1, SessPresent, ProtoState3} = - case validate_connect(Var, ProtoState1) of - ?RC_SUCCESS -> - case authenticate(client(ProtoState1), Password) of - {ok, IsSuperuser} -> - %% Generate clientId if null - ProtoState2 = maybe_set_clientid(ProtoState1), - %% Open session - case emqx_sm:open_session(#{zone => Zone, - clean_start => CleanStart, - client_id => clientid(ProtoState2), - username => Username, - client_pid => ClientPid}) of - {ok, Session} -> %% TODO:... - SP = true, %% TODO:... - %% TODO: Register the client - emqx_cm:register_client(clientid(ProtoState2)), - %%emqx_cm:reg(client(State2)), - %% Start keepalive - start_keepalive(Keepalive, ProtoState2), - %% Emit Stats - %% self() ! emit_stats, - %% ACCEPT - {?RC_SUCCESS, SP, ProtoState2#proto_state{session = Session, is_superuser = IsSuperuser}}; - {error, Error} -> - ?LOG(error, "Failed to open session: ~p", [Error], ProtoState2), - {?RC_UNSPECIFIED_ERROR, false, ProtoState2} %% TODO: the error reason??? + connack( + case check_connect(Connect, PState1) of + ok -> + case authenticate(client(PState1), Password) of + {ok, IsSuper} -> + %% Maybe assign a clientId + PState2 = maybe_assign_client_id(PState1#pstate{is_super = IsSuper}), + %% Open session + case try_open_session(PState2) of + {ok, SPid, SP} -> + PState3 = PState2#pstate{session = SPid}, + ok = emqx_cm:register_client({client_id(PState3), self()}, info(PState3)), + %% Start keepalive + start_keepalive(Keepalive, PState3), + %% TODO: 'Run hooks' before open_session? + emqx_hooks:run('client.connected', [?RC_SUCCESS], client(PState3)), + %% Success + {?RC_SUCCESS, SP, replvar(PState3)}; + {error, Error} -> + ?LOG(error, "Failed to open session: ~p", [Error], PState1), + {?RC_UNSPECIFIED_ERROR, PState1} end; - {error, Reason}-> - ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], ProtoState1), - {?RC_BAD_USER_NAME_OR_PASSWORD, false, ProtoState1} - end; - ReturnCode -> - {ReturnCode, false, ProtoState1} - end, - %% Run hooks - emqx_hooks:run('client.connected', [ReturnCode1], client(ProtoState3)), - %%TODO: Send Connack - send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), ProtoState3), - %% stop if authentication failure - stop_if_auth_failure(ReturnCode1, ProtoState3); + {error, Reason} -> + ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], PState1), + {?RC_NOT_AUTHORIZED, PState1} + end; + {error, ReasonCode} -> + {ReasonCode, PState1} + end); -process(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId, _Payload), - State = #proto_state{is_superuser = IsSuper}) -> - case IsSuper orelse allow == check_acl(publish, Topic, client(State)) of - true -> publish(Packet, State); - false -> ?LOG(error, "Cannot publish to ~s for ACL Deny", [Topic], State) - end, - {ok, State}; +process(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PState) -> + case check_publish(Packet, PState) of + ok -> + do_publish(Packet, PState); + {error, ReasonCode} -> + ?LOG(warning, "Cannot publish qos0 message to ~s for ~s", [Topic, ReasonCode], PState), + {ok, PState} + end; -process(?PUBACK_PACKET(PacketId), State = #proto_state{session = Session}) -> - emqx_session:puback(Session, PacketId), - {ok, State}; +process(Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), PState) -> + case check_publish(Packet, PState) of + ok -> + do_publish(Packet, PState); + {error, ReasonCode} -> + deliver({puback, PacketId, ReasonCode}, PState) + end; -process(?PUBREC_PACKET(PacketId), State = #proto_state{session = Session}) -> - emqx_session:pubrec(Session, PacketId), - send(?PUBREL_PACKET(PacketId), State); +process(Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), PState) -> + case check_publish(Packet, PState) of + ok -> + do_publish(Packet, PState); + {error, ReasonCode} -> + deliver({pubrec, PacketId, ReasonCode}, PState) + end; -process(?PUBREL_PACKET(PacketId), State = #proto_state{session = Session}) -> - emqx_session:pubrel(Session, PacketId), - send(?PUBCOMP_PACKET(PacketId), State); +process(?PUBACK_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> + ok = emqx_session:puback(SPid, PacketId, ReasonCode), + {ok, PState}; -process(?PUBCOMP_PACKET(PacketId), State = #proto_state{session = Session})-> - emqx_session:pubcomp(Session, PacketId), {ok, State}; +process(?PUBREC_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> + ok = emqx_session:pubrec(SPid, PacketId, ReasonCode), + send(?PUBREL_PACKET(PacketId), PState); -%% Protect from empty topic table -process(?SUBSCRIBE_PACKET(PacketId, []), State) -> - send(?SUBACK_PACKET(PacketId, []), State); +process(?PUBREL_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> + ok = emqx_session:pubrel(SPid, PacketId, ReasonCode), + send(?PUBCOMP_PACKET(PacketId), PState); -%% TODO: refactor later... -process(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), State) -> - #proto_state{client_id = ClientId, - username = Username, - is_superuser = IsSuperuser, - mountpoint = MountPoint, - session = Session} = State, - Client = client(State), - TopicFilters = parse_topic_filters(RawTopicFilters), - AllowDenies = if - IsSuperuser -> []; - true -> [check_acl(subscribe, Topic, Client) || {Topic, _Opts} <- TopicFilters] - end, - case lists:member(deny, AllowDenies) of - true -> - ?LOG(error, "Cannot SUBSCRIBE ~p for ACL Deny", [TopicFilters], State), - send(?SUBACK_PACKET(PacketId, [?RC_NOT_AUTHORIZED || _ <- TopicFilters]), State); - false -> - case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicFilters) of +process(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> + ok = emqx_session:pubcomp(SPid, PacketId, ReasonCode), + {ok, PState}; + +process(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), + PState = #pstate{client_id = ClientId, session = SPid}) -> + case check_subscribe( + parse_topic_filters(?SUBSCRIBE, RawTopicFilters), PState) of + {ok, TopicFilters} -> + case emqx_hooks:run('client.subscribe', [ClientId], TopicFilters) of {ok, TopicFilters1} -> - ok = emqx_session:subscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicFilters1)}), - {ok, State}; - {stop, _} -> {ok, State} - end - end; - -%% Protect from empty topic list -process(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> - send(?UNSUBACK_PACKET(PacketId), State); - -process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopics), - State = #proto_state{client_id = ClientId, - username = Username, - mountpoint = MountPoint, - session = Session}) -> - case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of - {ok, TopicTable} -> - emqx_session:unsubscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicTable)}); - {stop, _} -> - ok - end, - send(?UNSUBACK_PACKET(PacketId), State); - -process(?PACKET(?PINGREQ), ProtoState) -> - send(?PACKET(?PINGRESP), ProtoState); - -process(?PACKET(?DISCONNECT), ProtoState) -> - % Clean willmsg - {stop, normal, ProtoState#proto_state{will_msg = undefined}}. - -deliver({publish, PacketId, Msg}, - State = #proto_state{client_id = ClientId, - username = Username, - mountpoint = MountPoint, - is_bridge = IsBridge}) -> - emqx_hooks:run('message.delivered', [ClientId], - emqx_message:set_header(username, Username, Msg)), - Msg1 = unmount(MountPoint, clean_retain(IsBridge, Msg)), - send(emqx_packet:from_message(PacketId, Msg1), State); - -deliver({pubrel, PacketId}, State) -> - send(?PUBREL_PACKET(PacketId), State); - -deliver({suback, PacketId, ReasonCodes}, ProtoState) -> - send(?SUBACK_PACKET(PacketId, ReasonCodes), ProtoState); - -deliver({unsuback, PacketId, ReasonCodes}, ProtoState) -> - send(?UNSUBACK_PACKET(PacketId, ReasonCodes), ProtoState). - -publish(Packet = ?PUBLISH_PACKET(?QOS_0, PacketId), - State = #proto_state{client_id = ClientId, - username = Username, - mountpoint = MountPoint, - session = Session}) -> - Msg = emqx_message:set_header(username, Username, - emqx_packet:to_message(ClientId, Packet)), - emqx_session:publish(Session, PacketId, mount(replvar(MountPoint, State), Msg)); - -publish(Packet = ?PUBLISH_PACKET(?QOS_1), State) -> - with_puback(?PUBACK, Packet, State); - -publish(Packet = ?PUBLISH_PACKET(?QOS_2), State) -> - with_puback(?PUBREC, Packet, State). - -with_puback(Type, Packet = ?PUBLISH_PACKET(_QoS, PacketId), - State = #proto_state{client_id = ClientId, - username = Username, - mountpoint = MountPoint, - session = Session}) -> - Msg = emqx_message:set_header(username, Username, - emqx_packet:to_message(ClientId, Packet)), - case emqx_session:publish(Session, PacketId, mount(replvar(MountPoint, State), Msg)) of - {error, Error} -> - ?LOG(error, "PUBLISH ~p error: ~p", [PacketId, Error], State); - _Delivery -> send({Type, PacketId}, State) %% TODO: - end. - --spec(send({mqtt_packet_type(), mqtt_packet_id()} | - {mqtt_packet_id(), message()} | - mqtt_packet(), proto_state()) -> {ok, proto_state()}). -send({?PUBACK, PacketId}, State) -> - send(?PUBACK_PACKET(PacketId), State); - -send({?PUBREC, PacketId}, State) -> - send(?PUBREC_PACKET(PacketId), State); - -send(Packet = ?PACKET(Type), ProtoState = #proto_state{proto_ver = Ver, - sockprops = #{sendfun := SendFun}}) -> - Data = emqx_frame:serialize(Packet, #{version => Ver}), - case SendFun(Data) of - {error, Reason} -> - {error, Reason}; - _ -> emqx_metrics:sent(Packet), - trace(send, Packet, ProtoState), - {ok, inc_stats(send, Type, ProtoState)} - end. - -trace(recv, Packet, ProtoState) -> - ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)], ProtoState); - -trace(send, Packet, ProtoState) -> - ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)], ProtoState). - -inc_stats(recv, Type, ProtoState = #proto_state{recv_pkt = PktCnt, recv_msg = MsgCnt}) -> - ProtoState#proto_state{recv_pkt = PktCnt + 1, - recv_msg = if Type =:= ?PUBLISH -> MsgCnt + 1; - true -> MsgCnt - end}; -inc_stats(send, Type, ProtoState = #proto_state{send_pkt = PktCnt, send_msg = MsgCnt}) -> - ProtoState#proto_state{send_pkt = PktCnt + 1, - send_msg = if Type =:= ?PUBLISH -> MsgCnt + 1; - true -> MsgCnt - end}. - -stop_if_auth_failure(?RC_SUCCESS, State) -> - {ok, State}; -stop_if_auth_failure(RC, State) when RC =/= ?RC_SUCCESS -> - {stop, {shutdown, auth_failure}, State}. - -shutdown(_Error, #proto_state{client_id = undefined}) -> - ignore; -shutdown(conflict, _State = #proto_state{client_id = ClientId}) -> - emqx_cm:unregister_client(ClientId), - ignore; -shutdown(mnesia_conflict, _State = #proto_state{client_id = ClientId}) -> - emqx_cm:unregister_client(ClientId), - ignore; -shutdown(Error, State = #proto_state{client_id = ClientId, - will_msg = WillMsg}) -> - ?LOG(info, "Shutdown for ~p", [Error], State), - %% Auth failure not publish the will message - case Error =:= auth_failure of - true -> ok; - false -> send_willmsg(ClientId, WillMsg) - end, - emqx_hooks:run('client.disconnected', [Error], client(State)), - emqx_cm:unregister_client(ClientId), - ok. - -willmsg(Packet, State = #proto_state{client_id = ClientId, mountpoint = MountPoint}) - when is_record(Packet, mqtt_packet_connect) -> - case emqx_packet:to_message(ClientId, Packet) of - undefined -> undefined; - Msg -> mount(replvar(MountPoint, State), Msg) - end. - -%% Generate a client if if nulll -maybe_set_clientid(State = #proto_state{client_id = NullId}) - when NullId =:= undefined orelse NullId =:= <<>> -> - {_, NPid, _} = emqx_guid:new(), - ClientId = iolist_to_binary(["emqx_", integer_to_list(NPid)]), - State#proto_state{client_id = ClientId}; - -maybe_set_clientid(State) -> - State. - -send_willmsg(_ClientId, undefined) -> - ignore; -send_willmsg(ClientId, WillMsg) -> - emqx_broker:publish(WillMsg#message{from = ClientId}). - -start_keepalive(0, _State) -> ignore; - -start_keepalive(Sec, #proto_state{keepalive_backoff = Backoff}) when Sec > 0 -> - self() ! {keepalive, start, round(Sec * Backoff)}. - -%%-------------------------------------------------------------------- -%% Validate Packets -%%-------------------------------------------------------------------- - -validate_connect(Connect = #mqtt_packet_connect{}, ProtoState) -> - case validate_protocol(Connect) of - true -> - case validate_clientid(Connect, ProtoState) of - true -> ?RC_SUCCESS; - false -> ?RC_CLIENT_IDENTIFIER_NOT_VALID + ok = emqx_session:subscribe(SPid, PacketId, Properties, mount(TopicFilters1, PState)), + {ok, PState}; + {stop, _} -> + ReasonCodes = lists:duplicate(length(TopicFilters), + ?RC_IMPLEMENTATION_SPECIFIC_ERROR), + deliver({suback, PacketId, ReasonCodes}, PState) end; - false -> - ?RC_UNSUPPORTED_PROTOCOL_VERSION - end. - -validate_protocol(#mqtt_packet_connect{proto_ver = Ver, proto_name = Name}) -> - lists:member({Ver, Name}, ?PROTOCOL_NAMES). - -validate_clientid(#mqtt_packet_connect{client_id = ClientId}, - #proto_state{capabilities = #{max_clientid_len := MaxLen}}) - when (byte_size(ClientId) >= 1) andalso (byte_size(ClientId) =< MaxLen) -> - true; - -%% Issue#599: Null clientId and clean_start = false -validate_clientid(#mqtt_packet_connect{client_id = ClientId, - clean_start = CleanStart}, _ProtoState) - when byte_size(ClientId) == 0 andalso (not CleanStart) -> - false; - -%% MQTT3.1.1 allow null clientId. -validate_clientid(#mqtt_packet_connect{proto_ver =?MQTT_PROTO_V4, - client_id = ClientId}, _ProtoState) - when byte_size(ClientId) =:= 0 -> - true; - -validate_clientid(#mqtt_packet_connect{proto_ver = ProtoVer, - clean_start = CleanStart}, ProtoState) -> - ?LOG(warning, "Invalid clientId. ProtoVer: ~p, CleanStart: ~s", - [ProtoVer, CleanStart], ProtoState), - false. - -validate_packet(?PUBLISH_PACKET(_QoS, Topic, _PacketId, _Payload)) -> - case emqx_topic:validate({name, Topic}) of - true -> ok; - false -> {error, badtopic} + {error, TopicFilters} -> + ReasonCodes = lists:map(fun({_, #{rc := ?RC_SUCCESS}}) -> + ?RC_IMPLEMENTATION_SPECIFIC_ERROR; + ({_, #{rc := ReasonCode}}) -> + ReasonCode + end, TopicFilters), + deliver({suback, PacketId, ReasonCodes}, PState) end; -validate_packet(?SUBSCRIBE_PACKET(_PacketId, TopicTable)) -> - validate_topics(filter, TopicTable); - -validate_packet(?UNSUBSCRIBE_PACKET(_PacketId, Topics)) -> - validate_topics(filter, Topics); - -validate_packet(_Packet) -> - ok. - -validate_topics(_Type, []) -> - {error, empty_topics}; - -validate_topics(Type, TopicTable = [{_Topic, _SubOpts}|_]) - when Type =:= name orelse Type =:= filter -> - Valid = fun(Topic, QoS) -> - emqx_topic:validate({Type, Topic}) and validate_qos(QoS) - end, - case [Topic || {Topic, SubOpts} <- TopicTable, - not Valid(Topic, SubOpts#mqtt_subopts.qos)] of - [] -> ok; - _ -> {error, badtopic} +process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), + PState = #pstate{client_id = ClientId, session = SPid}) -> + case emqx_hooks:run('client.unsubscribe', [ClientId], + parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters)) of + {ok, TopicFilters} -> + ok = emqx_session:unsubscribe(SPid, PacketId, Properties, mount(TopicFilters, PState)), + {ok, PState}; + {stop, _Acc} -> + ReasonCodes = lists:duplicate(length(RawTopicFilters), + ?RC_IMPLEMENTATION_SPECIFIC_ERROR), + deliver({unsuback, PacketId, ReasonCodes}, PState) end; -validate_topics(Type, Topics = [Topic0|_]) when is_binary(Topic0) -> - case [Topic || Topic <- Topics, not emqx_topic:validate({Type, Topic})] of - [] -> ok; - _ -> {error, badtopic} +process(?PACKET(?PINGREQ), PState) -> + send(?PACKET(?PINGRESP), PState); + +process(?PACKET(?DISCONNECT), PState) -> + %% Clean willmsg + {stop, normal, PState#pstate{will_msg = undefined}}. + +%%------------------------------------------------------------------------------ +%% ConnAck -> Client +%%------------------------------------------------------------------------------ + +connack({?RC_SUCCESS, SP, PState}) -> + deliver({connack, ?RC_SUCCESS, sp(SP)}, PState); + +connack({ReasonCode, PState}) -> + deliver({connack, ReasonCode, 0}, PState), + {error, emqx_reason_codes:name(ReasonCode), PState}. + +%%------------------------------------------------------------------------------ +%% Publish Message -> Broker +%%------------------------------------------------------------------------------ + +do_publish(Packet = ?PUBLISH_PACKET(QoS, PacketId), + PState = #pstate{client_id = ClientId, session = SPid}) -> + Msg = mount(emqx_packet:to_message(ClientId, Packet), PState), + _ = emqx_session:publish(SPid, PacketId, Msg), + puback(QoS, PacketId, PState). + +%%------------------------------------------------------------------------------ +%% Puback -> Client +%%------------------------------------------------------------------------------ + +puback(?QOS_0, _PacketId, PState) -> + {ok, PState}; +puback(?QOS_1, PacketId, PState) -> + deliver({puback, PacketId, ?RC_SUCCESS}, PState); +puback(?QOS_2, PacketId, PState) -> + deliver({pubrec, PacketId, ?RC_SUCCESS}, PState). + +%%------------------------------------------------------------------------------ +%% Deliver Packet -> Client +%%------------------------------------------------------------------------------ + +deliver({connack, ReasonCode}, PState) -> + send(?CONNACK_PACKET(ReasonCode), PState); + +deliver({connack, ReasonCode, SP}, PState) -> + send(?CONNACK_PACKET(ReasonCode, SP), PState); + +deliver({publish, PacketId, Msg}, PState = #pstate{client_id = ClientId, + is_bridge = IsBridge}) -> + _ = emqx_hooks:run('message.delivered', [ClientId], Msg), + Msg1 = unmount(clean_retain(IsBridge, Msg), PState), + send(emqx_packet:from_message(PacketId, Msg1), PState); + +deliver({puback, PacketId, ReasonCode}, PState) -> + send(?PUBACK_PACKET(PacketId, ReasonCode), PState); + +deliver({pubrel, PacketId}, PState) -> + send(?PUBREL_PACKET(PacketId), PState); + +deliver({pubrec, PacketId, ReasonCode}, PState) -> + send(?PUBREC_PACKET(PacketId, ReasonCode), PState); + +deliver({suback, PacketId, ReasonCodes}, PState) -> + send(?SUBACK_PACKET(PacketId, ReasonCodes), PState); + +deliver({unsuback, PacketId, ReasonCodes}, PState) -> + send(?UNSUBACK_PACKET(PacketId, ReasonCodes), 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) -> + {ok, PState}. + +%%------------------------------------------------------------------------------ +%% Send Packet to Client + +-spec(send(mqtt_packet(), state()) -> {ok, state()} | {error, term()}). +send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, + sendfun = SendFun}) -> + case SendFun(emqx_frame:serialize(Packet, #{version => Ver})) of + ok -> emqx_metrics:sent(Packet), + trace(send, Packet, PState), + {ok, inc_stats(send, Type, PState)}; + {error, Reason} -> + {error, Reason} end. -validate_qos(undefined) -> - true; -validate_qos(QoS) when ?IS_QOS(QoS) -> - true; -validate_qos(_) -> - false. +%%------------------------------------------------------------------------------ +%% Assign a clientid -parse_topic_filters(TopicFilters) -> - [begin - {Topic, Opts} = emqx_topic:parse(RawTopic), - {Topic, maps:merge(?record_to_map(mqtt_subopts, SubOpts), Opts)} - end || {RawTopic, SubOpts} <- TopicFilters]. +maybe_assign_client_id(PState = #pstate{client_id = <<>>, ackprops = AckProps}) -> + ClientId = iolist_to_binary(["emqx_", emqx_guid:gen()]), + AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), + PState#pstate{client_id = ClientId, ackprops = AckProps1}; +maybe_assign_client_id(PState) -> + PState. -parse_topics(Topics) -> - [emqx_topic:parse(Topic) || Topic <- Topics]. +try_open_session(#pstate{zone = Zone, + client_id = ClientId, + client_pid = ClientPid, + conn_props = ConnProps, + username = Username, + clean_start = CleanStart}) -> + case emqx_sm:open_session(#{zone => Zone, + client_id => ClientId, + client_pid => ClientPid, + username => Username, + clean_start => CleanStart, + conn_props => ConnProps}) of + {ok, SPid} -> {ok, SPid, false}; + Other -> Other + end. authenticate(Client, Password) -> - case emqx_access_control:auth(Client, Password) of + case emqx_access_control:authenticate(Client, Password) of ok -> {ok, false}; {ok, IsSuper} -> {ok, IsSuper}; {error, Error} -> {error, Error} end. -%% PUBLISH ACL is cached in process dictionary. -check_acl(publish, Topic, Client) -> - IfCache = emqx_config:get_env(cache_acl, true), - case {IfCache, get({acl, publish, Topic})} of - {true, undefined} -> - AllowDeny = emqx_access_control:check_acl(Client, publish, Topic), - put({acl, publish, Topic}, AllowDeny), - AllowDeny; - {true, AllowDeny} -> - AllowDeny; - {false, _} -> - emqx_access_control:check_acl(Client, publish, Topic) - end; +set_property(Name, Value, undefined) -> + #{Name => Value}; +set_property(Name, Value, Props) -> + Props#{Name => Value}. -check_acl(subscribe, Topic, Client) -> - emqx_access_control:check_acl(Client, subscribe, Topic). +%%------------------------------------------------------------------------------ +%% Check Packet +%%------------------------------------------------------------------------------ -sp(true) -> 1; -sp(false) -> 0. +check_connect(Packet, PState) -> + run_check_steps([fun check_proto_ver/2, + fun check_client_id/2], Packet, PState). -%%-------------------------------------------------------------------- +check_proto_ver(#mqtt_packet_connect{proto_ver = Ver, + proto_name = Name}, _PState) -> + case lists:member({Ver, Name}, ?PROTOCOL_NAMES) of + true -> ok; + false -> {error, ?RC_PROTOCOL_ERROR} + end. + +%% Issue#599: Null clientId and clean_start = false +check_client_id(#mqtt_packet_connect{client_id = ClientId, + clean_start = false}, _PState) + when ClientId == undefined; ClientId == <<>> -> + {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; + +%% MQTT3.1 does not allow null clientId +check_client_id(#mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V3, + client_id = ClientId}, _PState) + when ClientId == undefined; ClientId == <<>> -> + {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; + +check_client_id(#mqtt_packet_connect{client_id = ClientId}, #pstate{zone = Zone}) -> + Len = byte_size(ClientId), + MaxLen = emqx_zone:get_env(Zone, max_clientid_len), + case (1 =< Len) andalso (Len =< MaxLen) of + true -> ok; + false -> {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID} + end. + +check_publish(Packet, PState) -> + run_check_steps([fun check_pub_caps/2, + fun check_pub_acl/2], Packet, PState). + +check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = R}}, + #pstate{zone = Zone}) -> + emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => R}). + +check_pub_acl(_Packet, #pstate{is_super = IsSuper, enable_acl = EnableAcl}) + when IsSuper orelse (not EnableAcl) -> + ok; + +check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, PState) -> + case emqx_access_control:check_acl(client(PState), publish, Topic) of + allow -> ok; + deny -> {error, ?RC_NOT_AUTHORIZED} + end. + +run_check_steps([], _Packet, PState) -> + {ok, PState}; +run_check_steps([Check|Steps], Packet, PState) -> + case Check(Packet, PState) of + ok -> + run_check_steps(Steps, Packet, PState); + {ok, PState1} -> + run_check_steps(Steps, Packet, PState1); + Error = {error, _RC} -> + Error + 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} + end. + +check_sub_acl(TopicFilters, #pstate{is_super = IsSuper, enable_acl = EnableAcl}) + when IsSuper orelse (not EnableAcl) -> + {ok, TopicFilters}; + +check_sub_acl(TopicFilters, PState) -> + Client = client(PState), + lists:foldr( + fun({Topic, SubOpts}, {Ok, Acc}) -> + case emqx_access_control:check_acl(Client, subscribe, Topic) of + allow -> {Ok, [{Topic, SubOpts}|Acc]}; + deny -> {error, [{Topic, SubOpts#{rc := ?RC_NOT_AUTHORIZED}}|Acc]} + end + end, {ok, []}, TopicFilters). + +trace(recv, Packet, PState) -> + ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)], PState); +trace(send, Packet, PState) -> + ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)], PState). + +inc_stats(recv, Type, PState = #pstate{recv_stats = Stats}) -> + PState#pstate{recv_stats = inc_stats(Type, Stats)}; + +inc_stats(send, Type, PState = #pstate{send_stats = Stats}) -> + PState#pstate{send_stats = inc_stats(Type, Stats)}. + +inc_stats(Type, Stats = #{pkt := PktCnt, msg := MsgCnt}) -> + Stats#{pkt := PktCnt + 1, msg := case Type =:= ?PUBLISH of + true -> MsgCnt + 1; + false -> MsgCnt + end}. + +shutdown(_Error, #pstate{client_id = undefined}) -> + ignore; +shutdown(conflict, #pstate{client_id = ClientId}) -> + emqx_cm:unregister_client(ClientId), + ignore; +shutdown(mnesia_conflict, #pstate{client_id = ClientId}) -> + emqx_cm:unregister_client(ClientId), + ignore; +shutdown(Error, PState = #pstate{client_id = ClientId, will_msg = WillMsg}) -> + ?LOG(info, "Shutdown for ~p", [Error], PState), + %% TODO: Auth failure not publish the will message + case Error =:= auth_failure of + true -> ok; + false -> send_willmsg(WillMsg) + end, + emqx_hooks:run('client.disconnected', [Error], client(PState)), + emqx_cm:unregister_client(ClientId). + +willmsg(Packet, PState = #pstate{client_id = ClientId}) + when is_record(Packet, mqtt_packet_connect) -> + case emqx_packet:to_message(ClientId, Packet) of + undefined -> undefined; + Msg -> mount(Msg, PState) + end. + +send_willmsg(undefined) -> + ignore; +send_willmsg(WillMsg) -> + emqx_broker:publish(WillMsg). + +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)}. + +%%----------------------------------------------------------------------------- +%% Parse topic filters +%%----------------------------------------------------------------------------- + +parse_topic_filters(?SUBSCRIBE, TopicFilters) -> + [begin + {Topic, TOpts} = emqx_topic:parse(RawTopic), + {Topic, maps:merge(SubOpts, TOpts)} + end || {RawTopic, SubOpts} <- TopicFilters]; + +parse_topic_filters(?UNSUBSCRIBE, TopicFilters) -> + lists:map(fun emqx_topic:parse/1, TopicFilters). + +%%----------------------------------------------------------------------------- %% The retained flag should be propagated for bridge. -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- clean_retain(false, Msg = #message{flags = #{retain := true}, headers = Headers}) -> case maps:get(retained, Headers, false) of @@ -542,14 +614,30 @@ clean_retain(false, Msg = #message{flags = #{retain := true}, headers = Headers} clean_retain(_IsBridge, Msg) -> Msg. -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% Mount Point -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- -replvar(undefined, _State) -> - undefined; -replvar(MountPoint, #proto_state{client_id = ClientId, username = Username}) -> - lists:foldl(fun feed_var/2, MountPoint, [{<<"%c">>, ClientId}, {<<"%u">>, Username}]). +mount(Any, #pstate{mountpoint = undefined}) -> + Any; +mount(Msg = #message{topic = Topic}, #pstate{mountpoint = MountPoint}) -> + Msg#message{topic = <>}; +mount(TopicFilters, #pstate{mountpoint = MountPoint}) when is_list(TopicFilters) -> + [{<>, SubOpts} || {Topic, SubOpts} <- TopicFilters]. + +unmount(Any, #pstate{mountpoint = undefined}) -> + Any; +unmount(Msg = #message{topic = Topic}, #pstate{mountpoint = MountPoint}) -> + case catch split_binary(Topic, byte_size(MountPoint)) of + {MountPoint, Topic1} -> Msg#message{topic = Topic1}; + _Other -> Msg + end. + +replvar(PState = #pstate{mountpoint = undefined}) -> + PState; +replvar(PState = #pstate{client_id = ClientId, username = Username, mountpoint = MountPoint}) -> + Vars = [{<<"%c">>, ClientId}, {<<"%u">>, Username}], + PState#pstate{mountpoint = lists:foldl(fun feed_var/2, MountPoint, Vars)}. feed_var({<<"%c">>, ClientId}, MountPoint) -> emqx_topic:feed_var(<<"%c">>, ClientId, MountPoint); @@ -558,18 +646,6 @@ feed_var({<<"%u">>, undefined}, MountPoint) -> feed_var({<<"%u">>, Username}, MountPoint) -> emqx_topic:feed_var(<<"%u">>, Username, MountPoint). -mount(undefined, Any) -> - Any; -mount(MountPoint, Msg = #message{topic = Topic}) -> - Msg#message{topic = <>}; -mount(MountPoint, TopicTable) when is_list(TopicTable) -> - [{<>, Opts} || {Topic, Opts} <- TopicTable]. - -unmount(undefined, Any) -> - Any; -unmount(MountPoint, Msg = #message{topic = Topic}) -> - case catch split_binary(Topic, byte_size(MountPoint)) of - {MountPoint, Topic0} -> Msg#message{topic = Topic0}; - _ -> Msg - end. +sp(true) -> 1; +sp(false) -> 0. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 1253de5cc..0c994d947 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -11,7 +11,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 %% A stateful interaction between a Client and a Server. Some Sessions %% last only as long as the Network Connection, others can span multiple @@ -35,28 +35,31 @@ %% If the Session is currently not connected, the time at which the Session %% will end and Session State will be discarded. %% @end + -module(emqx_session). -behaviour(gen_server). -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --include("emqx_misc.hrl"). --export([start_link/1, close/1]). +-export([start_link/1]). -export([info/1, stats/1]). -export([resume/2, discard/2]). --export([subscribe/2]).%%, subscribe/3]). +-export([subscribe/2, subscribe/4]). -export([publish/3]). -export([puback/2, puback/3]). -export([pubrec/2, pubrec/3]). --export([pubrel/2, pubcomp/2]). --export([unsubscribe/2]). +-export([pubrel/3, pubcomp/3]). +-export([unsubscribe/2, unsubscribe/4]). +-export([close/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-import(emqx_zone, [get_env/2, get_env/3]). + -record(state, { %% Clean Start Flag clean_start = false :: boolean(), @@ -76,9 +79,6 @@ %% Old client Pid that has been kickout old_client_pid :: pid(), - %% Pending sub/unsub requests - requests :: map(), - %% Next packet id of the session next_pkt_id = 1 :: mqtt_packet_id(), @@ -130,27 +130,28 @@ %% Enable Stats enable_stats :: boolean(), - %% Force GC reductions - reductions = 0 :: non_neg_integer(), + %% Stats timer + stats_timer :: reference() | undefined, %% Ignore loop deliver? ignore_loop_deliver = false :: boolean(), + %% TODO: + deliver_stats = 0, + + %% TODO: + enqueue_stats = 0, + %% Created at created_at :: erlang:timestamp() }). -define(TIMEOUT, 60000). --define(DEFAULT_SUBOPTS, #{rh => 0, rap => 0, nl => 0, qos => ?QOS_0}). - --define(INFO_KEYS, [clean_start, client_id, username, client_pid, binding, created_at]). - --define(STATE_KEYS, [clean_start, client_id, username, binding, client_pid, old_client_pid, - next_pkt_id, max_subscriptions, subscriptions, upgrade_qos, inflight, - max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, - await_rel_timeout, expiry_interval, enable_stats, force_gc_count, - created_at]). +-define(INFO_KEYS, [clean_start, client_id, username, binding, client_pid, old_client_pid, + next_pkt_id, max_subscriptions, subscriptions, upgrade_qos, inflight, + max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, + await_rel_timeout, expiry_interval, enable_stats, created_at]). -define(LOG(Level, Format, Args, State), emqx_logger:Level([{client, State#state.client_id}], @@ -159,7 +160,8 @@ %% @doc Start a session -spec(start_link(SessAttrs :: map()) -> {ok, pid()} | {error, term()}). start_link(SessAttrs) -> - gen_server:start_link(?MODULE, SessAttrs, [{hibernate_after, 30000}]). + IdleTimeout = maps:get(idle_timeout, SessAttrs, 30000), + gen_server:start_link(?MODULE, SessAttrs, [{hibernate_after, IdleTimeout}]). %%------------------------------------------------------------------------------ %% PubSub API @@ -167,12 +169,17 @@ start_link(SessAttrs) -> -spec(subscribe(pid(), list({topic(), map()}) | {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). -%% internal call subscribe(SPid, TopicFilters) when is_list(TopicFilters) -> - %%TODO: Parse the topic filters? - subscribe(SPid, {undefined, #{}, TopicFilters}); + gen_server:cast(SPid, {subscribe, [begin + {Topic, Opts} = emqx_topic:parse(RawTopic), + {Topic, maps:merge( + maps:merge( + ?DEFAULT_SUBOPTS, SubOpts), Opts)} + end || {RawTopic, SubOpts} <- TopicFilters]}). + %% for mqtt 5.0 -subscribe(SPid, SubReq = {PacketId, Props, TopicFilters}) -> +subscribe(SPid, PacketId, Properties, TopicFilters) -> + SubReq = {PacketId, Properties, TopicFilters}, gen_server:cast(SPid, {subscribe, self(), SubReq}). -spec(publish(pid(), mqtt_packet_id(), message()) -> {ok, delivery()} | {error, term()}). @@ -190,31 +197,34 @@ publish(SPid, PacketId, Msg = #message{qos = ?QOS_2}) -> -spec(puback(pid(), mqtt_packet_id()) -> ok). puback(SPid, PacketId) -> - gen_server:cast(SPid, {puback, PacketId}). + gen_server:cast(SPid, {puback, PacketId, ?RC_SUCCESS}). -puback(SPid, PacketId, {ReasonCode, Props}) -> - gen_server:cast(SPid, {puback, PacketId, {ReasonCode, Props}}). +puback(SPid, PacketId, ReasonCode) -> + gen_server:cast(SPid, {puback, PacketId, ReasonCode}). -spec(pubrec(pid(), mqtt_packet_id()) -> ok). pubrec(SPid, PacketId) -> gen_server:cast(SPid, {pubrec, PacketId}). -pubrec(SPid, PacketId, {ReasonCode, Props}) -> - gen_server:cast(SPid, {pubrec, PacketId, {ReasonCode, Props}}). +pubrec(SPid, PacketId, ReasonCode) -> + gen_server:cast(SPid, {pubrec, PacketId, ReasonCode}). --spec(pubrel(pid(), mqtt_packet_id()) -> ok). -pubrel(SPid, PacketId) -> - gen_server:cast(SPid, {pubrel, PacketId}). +-spec(pubrel(pid(), mqtt_packet_id(), mqtt_reason_code()) -> ok). +pubrel(SPid, PacketId, ReasonCode) -> + gen_server:cast(SPid, {pubrel, PacketId, ReasonCode}). --spec(pubcomp(pid(), mqtt_packet_id()) -> ok). -pubcomp(SPid, PacketId) -> - gen_server:cast(SPid, {pubcomp, PacketId}). +-spec(pubcomp(pid(), mqtt_packet_id(), mqtt_reason_code()) -> ok). +pubcomp(SPid, PacketId, ReasonCode) -> + gen_server:cast(SPid, {pubcomp, PacketId, ReasonCode}). -spec(unsubscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). unsubscribe(SPid, TopicFilters) when is_list(TopicFilters) -> %%TODO: Parse the topic filters? - unsubscribe(SPid, {undefined, #{}, TopicFilters}); -unsubscribe(SPid, UnsubReq = {PacketId, Properties, TopicFilters}) -> + unsubscribe(SPid, {undefined, #{}, TopicFilters}). + +%% TODO:... +unsubscribe(SPid, PacketId, Properties, TopicFilters) -> + UnsubReq = {PacketId, Properties, TopicFilters}, gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). -spec(resume(pid(), pid()) -> ok). @@ -226,20 +236,52 @@ resume(SPid, ClientPid) -> info(SPid) when is_pid(SPid) -> gen_server:call(SPid, info); -info(State) when is_record(State, state) -> - ?record_to_proplist(state, State, ?INFO_KEYS). +info(#state{clean_start = CleanStart, + binding = Binding, + client_id = ClientId, + username = Username, + max_subscriptions = MaxSubscriptions, + subscriptions = Subscriptions, + upgrade_qos = UpgradeQoS, + inflight = Inflight, + max_inflight = MaxInflight, + retry_interval = RetryInterval, + mqueue = MQueue, + awaiting_rel = AwaitingRel, + max_awaiting_rel = MaxAwaitingRel, + await_rel_timeout = AwaitRelTimeout, + expiry_interval = ExpiryInterval, + created_at = CreatedAt}) -> + [{clean_start, CleanStart}, + {binding, Binding}, + {client_id, ClientId}, + {username, Username}, + {max_subscriptions, MaxSubscriptions}, + {subscriptions, maps:size(Subscriptions)}, + {upgrade_qos, UpgradeQoS}, + {inflight, emqx_inflight:size(Inflight)}, + {max_inflight, MaxInflight}, + {retry_interval, RetryInterval}, + {mqueue_len, emqx_mqueue:len(MQueue)}, + {awaiting_rel, maps:size(AwaitingRel)}, + {max_awaiting_rel, MaxAwaitingRel}, + {await_rel_timeout, AwaitRelTimeout}, + {expiry_interval, ExpiryInterval}, + {created_at, CreatedAt}]. -spec(stats(pid() | #state{}) -> list({atom(), non_neg_integer()})). stats(SPid) when is_pid(SPid) -> - gen_server:call(SPid, stats); + gen_server:call(SPid, stats, infinity); stats(#state{max_subscriptions = MaxSubscriptions, - subscriptions = Subscriptions, - inflight = Inflight, - max_inflight = MaxInflight, - mqueue = MQueue, - max_awaiting_rel = MaxAwaitingRel, - awaiting_rel = AwaitingRel}) -> + subscriptions = Subscriptions, + inflight = Inflight, + max_inflight = MaxInflight, + mqueue = MQueue, + max_awaiting_rel = MaxAwaitingRel, + awaiting_rel = AwaitingRel, + deliver_stats = DeliverMsg, + enqueue_stats = EnqueueMsg}) -> lists:append(emqx_misc:proc_stats(), [{max_subscriptions, MaxSubscriptions}, {subscriptions, maps:size(Subscriptions)}, @@ -250,8 +292,8 @@ stats(#state{max_subscriptions = MaxSubscriptions, {mqueue_dropped, emqx_mqueue:dropped(MQueue)}, {max_awaiting_rel, MaxAwaitingRel}, {awaiting_rel_len, maps:size(AwaitingRel)}, - {deliver_msg, get(deliver_msg)}, - {enqueue_msg, get(enqueue_msg)}]). + {deliver_msg, DeliverMsg}, + {enqueue_msg, EnqueueMsg}]). %% @doc Discard the session -spec(discard(pid(), client_id()) -> ok). @@ -268,43 +310,43 @@ close(SPid) -> init(#{zone := Zone, client_id := ClientId, - client_pid := ClientPid, + conn_pid := ClientPid, clean_start := CleanStart, - username := Username}) -> + username := Username, + %% TODO: + conn_props := _ConnProps}) -> process_flag(trap_exit, true), true = link(ClientPid), - init_stats([deliver_msg, enqueue_msg]), - MaxInflight = emqx_zone:env(Zone, max_inflight), + MaxInflight = get_env(Zone, max_inflight), State = #state{clean_start = CleanStart, binding = binding(ClientPid), client_id = ClientId, client_pid = ClientPid, username = Username, subscriptions = #{}, - max_subscriptions = emqx_zone:env(Zone, max_subscriptions, 0), - upgrade_qos = emqx_zone:env(Zone, upgrade_qos, false), + max_subscriptions = get_env(Zone, max_subscriptions, 0), + upgrade_qos = get_env(Zone, upgrade_qos, false), max_inflight = MaxInflight, inflight = emqx_inflight:new(MaxInflight), mqueue = init_mqueue(Zone, ClientId), - retry_interval = emqx_zone:env(Zone, retry_interval, 0), + retry_interval = get_env(Zone, retry_interval, 0), awaiting_rel = #{}, - await_rel_timeout = emqx_zone:env(Zone, await_rel_timeout), - max_awaiting_rel = emqx_zone:env(Zone, max_awaiting_rel), - expiry_interval = emqx_zone:env(Zone, session_expiry_interval), - enable_stats = emqx_zone:env(Zone, enable_stats, true), - ignore_loop_deliver = emqx_zone:env(Zone, ignore_loop_deliver, true), + await_rel_timeout = get_env(Zone, await_rel_timeout), + max_awaiting_rel = get_env(Zone, max_awaiting_rel), + expiry_interval = get_env(Zone, session_expiry_interval), + enable_stats = get_env(Zone, enable_stats, true), + ignore_loop_deliver = get_env(Zone, ignore_loop_deliver, false), + deliver_stats = 0, + enqueue_stats = 0, created_at = os:timestamp()}, emqx_sm:register_session(ClientId, info(State)), emqx_hooks:run('session.created', [ClientId]), - {ok, emit_stats(State), hibernate}. + {ok, ensure_stats_timer(State), hibernate}. init_mqueue(Zone, ClientId) -> emqx_mqueue:new(ClientId, #{type => simple, - max_len => emqx_zone:env(Zone, max_mqueue_len), - store_qos0 => emqx_zone:env(Zone, mqueue_store_qos0)}). - -init_stats(Keys) -> - lists:foreach(fun(K) -> put(K, 0) end, Keys). + max_len => get_env(Zone, max_mqueue_len), + store_qos0 => get_env(Zone, mqueue_store_qos0)}). binding(ClientPid) -> case node(ClientPid) =:= node() of true -> local; false -> remote end. @@ -347,11 +389,27 @@ handle_call(Req, _From, State) -> emqx_logger:error("[Session] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, +%% SUBSCRIBE: +handle_cast({subscribe, TopicFilters}, State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> + Subscriptions1 = lists:foldl( + fun({Topic, SubOpts}, SubMap) -> + case maps:find(Topic, SubMap) of + {ok, _OldOpts} -> + emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), + emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), + ?LOG(warning, "Duplicated subscribe: ~s, subopts: ~p", [Topic, SubOpts], State); + error -> + emqx_broker:subscribe(Topic, ClientId, SubOpts), + emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]) + end, + maps:put(Topic, SubOpts, SubMap) + end, Subscriptions, TopicFilters), + {noreply, State#state{subscriptions = Subscriptions1}}; + +handle_cast({subscribe, From, {PacketId, Properties, TopicFilters}}, State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> - ?LOG(info, "Subscribe ~p", [TopicFilters], State), {ReasonCodes, Subscriptions1} = - lists:foldl(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) -> + lists:foldr(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) -> {[QoS|RcAcc], case maps:find(Topic, SubMap) of {ok, SubOpts} -> @@ -361,58 +419,54 @@ handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), ?LOG(warning, "Duplicated subscribe ~s, old_opts: ~p, new_opts: ~p", [Topic, OldOpts, SubOpts], State), - maps:put(Topic, SubOpts, SubMap); + maps:put(Topic, with_subid(Properties, SubOpts), SubMap); error -> emqx_broker:subscribe(Topic, ClientId, SubOpts), emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), - maps:put(Topic, SubOpts, SubMap) + maps:put(Topic, with_subid(Properties, SubOpts), SubMap) end} end, {[], Subscriptions}, TopicFilters), - suback(From, PacketId, lists:reverse(ReasonCodes)), - {noreply, emit_stats(State#state{subscriptions = Subscriptions1})}; + suback(From, PacketId, ReasonCodes), + {noreply, State#state{subscriptions = Subscriptions1}}; +%% UNSUBSCRIBE: handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> - ?LOG(info, "Unsubscribe ~p", [TopicFilters], State), {ReasonCodes, Subscriptions1} = - lists:foldl(fun(Topic, {RcAcc, SubMap}) -> - case maps:find(Topic, SubMap) of - {ok, SubOpts} -> - emqx_broker:unsubscribe(Topic, ClientId), - emqx_hooks:run('session.unsubscribed', [ClientId, Topic, SubOpts]), - {[?RC_SUCCESS|RcAcc], maps:remove(Topic, SubMap)}; - error -> - {[?RC_NO_SUBSCRIPTION_EXISTED|RcAcc], SubMap} - end - end, {[], Subscriptions}, TopicFilters), - unsuback(From, PacketId, lists:reverse(ReasonCodes)), - {noreply, emit_stats(State#state{subscriptions = Subscriptions1})}; + lists:foldr(fun(Topic, {RcAcc, SubMap}) -> + case maps:find(Topic, SubMap) of + {ok, SubOpts} -> + emqx_broker:unsubscribe(Topic, ClientId), + emqx_hooks:run('session.unsubscribed', [ClientId, Topic, SubOpts]), + {[?RC_SUCCESS|RcAcc], maps:remove(Topic, SubMap)}; + error -> + {[?RC_NO_SUBSCRIPTION_EXISTED|RcAcc], SubMap} + end + end, {[], Subscriptions}, TopicFilters), + unsuback(From, PacketId, ReasonCodes), + {noreply, State#state{subscriptions = Subscriptions1}}; %% PUBACK: -handle_cast({puback, PacketId}, State = #state{inflight = Inflight}) -> - {noreply, - case emqx_inflight:contain(PacketId, Inflight) of - true -> - dequeue(acked(puback, PacketId, State)); - false -> - ?LOG(warning, "PUBACK ~p missed inflight: ~p", - [PacketId, emqx_inflight:window(Inflight)], State), - emqx_metrics:inc('packets/puback/missed'), - State - end, hibernate}; +handle_cast({puback, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + {noreply, dequeue(acked(puback, PacketId, State))}; + false -> + ?LOG(warning, "The PUBACK PacketId is not found: ~p", [PacketId], State), + emqx_metrics:inc('packets/puback/missed'), + {noreply, State} + end; -%% PUBREC: -handle_cast({pubrec, PacketId}, State = #state{inflight = Inflight}) -> - {noreply, - case emqx_inflight:contain(PacketId, Inflight) of - true -> - acked(pubrec, PacketId, State); - false -> - ?LOG(warning, "PUBREC ~p missed inflight: ~p", - [PacketId, emqx_inflight:window(Inflight)], State), - emqx_metrics:inc('packets/pubrec/missed'), - State - end, hibernate}; +%% PUBREC: How to handle ReasonCode? +handle_cast({pubrec, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + {noreply, acked(pubrec, PacketId, State)}; + false -> + ?LOG(warning, "The PUBREC PacketId is not found: ~w", [PacketId], State), + emqx_metrics:inc('packets/pubrec/missed'), + {noreply, State} + end; %% PUBREL: handle_cast({pubrel, PacketId}, State = #state{awaiting_rel = AwaitingRel}) -> @@ -422,7 +476,7 @@ handle_cast({pubrel, PacketId}, State = #state{awaiting_rel = AwaitingRel}) -> %% Implement Qos2 by method A [MQTT 4.33] %% Dispatch to subscriber when received PUBREL emqx_broker:publish(Msg), %% FIXME: - gc(State#state{awaiting_rel = AwaitingRel1}); + maybe_gc(State#state{awaiting_rel = AwaitingRel1}); error -> ?LOG(warning, "Cannot find PUBREL: ~p", [PacketId], State), emqx_metrics:inc('packets/pubrel/missed'), @@ -430,17 +484,15 @@ handle_cast({pubrel, PacketId}, State = #state{awaiting_rel = AwaitingRel}) -> end, hibernate}; %% PUBCOMP: -handle_cast({pubcomp, PacketId}, State = #state{inflight = Inflight}) -> - {noreply, - case emqx_inflight:contain(PacketId, Inflight) of - true -> - dequeue(acked(pubcomp, PacketId, State)); - false -> - ?LOG(warning, "The PUBCOMP ~p is not inflight: ~p", - [PacketId, emqx_inflight:window(Inflight)], State), - emqx_metrics:inc('packets/pubcomp/missed'), - State - end, hibernate}; +handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + {noreply, dequeue(acked(pubcomp, PacketId, State))}; + false -> + ?LOG(warning, "The PUBCOMP Packet Identifier is not found: ~w", [PacketId], State), + emqx_metrics:inc('packets/pubcomp/missed'), + {noreply, State} + end; %% RESUME: handle_cast({resume, ClientPid}, @@ -484,7 +536,7 @@ handle_cast({resume, ClientPid}, end, %% Replay delivery and Dequeue pending messages - {noreply, emit_stats(dequeue(retry_delivery(true, State1)))}; + {noreply, ensure_stats_timer(dequeue(retry_delivery(true, State1)))}; handle_cast(Msg, State) -> emqx_logger:error("[Session] unexpected cast: ~p", [Msg]), @@ -502,17 +554,17 @@ handle_info({dispatch, _Topic, #message{from = ClientId}}, %% Dispatch Message handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, message) -> - {noreply, gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))}; + {noreply, maybe_gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))}; %% Do nothing if the client has been disconnected. handle_info({timeout, _Timer, retry_delivery}, State = #state{client_pid = undefined}) -> - {noreply, emit_stats(State#state{retry_timer = undefined})}; + {noreply, ensure_stats_timer(State#state{retry_timer = undefined})}; handle_info({timeout, _Timer, retry_delivery}, State) -> - {noreply, emit_stats(retry_delivery(false, State#state{retry_timer = undefined}))}; + {noreply, ensure_stats_timer(retry_delivery(false, State#state{retry_timer = undefined}))}; handle_info({timeout, _Timer, check_awaiting_rel}, State) -> - {noreply, expire_awaiting_rel(emit_stats(State#state{await_rel_timer = undefined}))}; + {noreply, ensure_stats_timer(expire_awaiting_rel(State#state{await_rel_timer = undefined}))}; handle_info({timeout, _Timer, expired}, State) -> ?LOG(info, "Expired, shutdown now.", [], State), @@ -529,7 +581,7 @@ handle_info({'EXIT', ClientPid, Reason}, ?LOG(info, "Client ~p EXIT for ~p", [ClientPid, Reason], State), ExpireTimer = emqx_misc:start_timer(Interval, expired), State1 = State#state{client_pid = undefined, expiry_timer = ExpireTimer}, - {noreply, emit_stats(State1), hibernate}; + {noreply, State1, hibernate}; handle_info({'EXIT', Pid, _Reason}, State = #state{old_client_pid = Pid}) -> %% ignore @@ -540,6 +592,10 @@ handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = ClientPid}) -> [ClientPid, Pid, Reason], State), {noreply, State, hibernate}; +handle_info(emit_stats, State = #state{client_id = ClientId}) -> + emqx_sm:set_session_stats(ClientId, stats(State)), + {noreply, State#state{stats_timer = undefined}, hibernate}; + handle_info(Info, State) -> emqx_logger:error("[Session] unexpected info: ~p", [Info]), {noreply, State}. @@ -555,6 +611,10 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ +with_subid(#{'Subscription-Identifier' := SubId}, Opts) -> + maps:put(subid, SubId, Opts); +with_subid(_Props, Opts) -> Opts. + suback(_From, undefined, _ReasonCodes) -> ignore; suback(From, PacketId, ReasonCodes) -> @@ -675,36 +735,39 @@ dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) -> %% Deliver qos0 message directly to client dispatch(Msg = #message{qos = ?QOS0}, State) -> - deliver(undefined, Msg, State), State; + deliver(undefined, Msg, State), + inc_stats(deliver, State); dispatch(Msg = #message{qos = QoS}, State = #state{next_pkt_id = PacketId, inflight = Inflight}) when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 -> case emqx_inflight:is_full(Inflight) of - true -> + true -> enqueue_msg(Msg, State); false -> deliver(PacketId, Msg, State), - await(PacketId, Msg, next_pkt_id(State)) + %% TODO inc_stats?? + await(PacketId, Msg, next_pkt_id(inc_stats(deliver, State))) end. enqueue_msg(Msg, State = #state{mqueue = Q}) -> - inc_stats(enqueue_msg), - State#state{mqueue = emqx_mqueue:in(Msg, Q)}. + inc_stats(enqueue, State#state{mqueue = emqx_mqueue:in(Msg, Q)}). %%------------------------------------------------------------------------------ %% Deliver %%------------------------------------------------------------------------------ redeliver({PacketId, Msg = #message{qos = QoS}}, State) -> - deliver(PacketId, if QoS =:= ?QOS2 -> Msg; true -> emqx_message:set_flag(dup, Msg) end, State); + deliver(PacketId, if QoS =:= ?QOS2 -> Msg; + true -> emqx_message:set_flag(dup, Msg) + end, State); redeliver({pubrel, PacketId}, #state{client_pid = Pid}) -> Pid ! {deliver, {pubrel, PacketId}}. deliver(PacketId, Msg, #state{client_pid = Pid, binding = local}) -> - inc_stats(deliver_msg), Pid ! {deliver, {publish, PacketId, Msg}}; + Pid ! {deliver, {publish, PacketId, Msg}}; deliver(PacketId, Msg, #state{client_pid = Pid, binding = remote}) -> - inc_stats(deliver_msg), emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, PacketId, Msg}]). + emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, PacketId, Msg}]). %%------------------------------------------------------------------------------ %% Awaiting ACK for QoS1/QoS2 Messages @@ -802,27 +865,28 @@ next_pkt_id(State = #state{next_pkt_id = 16#FFFF}) -> next_pkt_id(State = #state{next_pkt_id = Id}) -> State#state{next_pkt_id = Id + 1}. -%%-------------------------------------------------------------------- -%% Emit session stats +%%------------------------------------------------------------------------------ +%% Ensure stats timer -emit_stats(State = #state{enable_stats = false}) -> - State; -emit_stats(State = #state{client_id = ClientId}) -> - emqx_sm:set_session_stats(ClientId, stats(State)), +ensure_stats_timer(State = #state{enable_stats = true, + stats_timer = undefined}) -> + State#state{stats_timer = erlang:send_after(30000, self(), emit_stats)}; +ensure_stats_timer(State) -> State. -inc_stats(Key) -> put(Key, get(Key) + 1). +inc_stats(deliver, State = #state{deliver_stats = I}) -> + State#state{deliver_stats = I + 1}; +inc_stats(enqueue, State = #state{enqueue_stats = I}) -> + State#state{enqueue_stats = I + 1}. %%-------------------------------------------------------------------- %% Helper functions reply(Reply, State) -> - {reply, Reply, State, hibernate}. + {reply, Reply, State}. shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. -gc(State) -> - State. - %%emqx_gc:maybe_force_gc(#state.force_gc_count, State). +maybe_gc(State) -> State. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index afa2d6b06..2dac00263 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -20,8 +20,10 @@ -export([start_link/0]). --export([open_session/1, lookup_session/1, close_session/1, lookup_session_pid/1]). --export([resume_session/1, resume_session/2, discard_session/1, discard_session/2]). +-export([open_session/1, close_session/1]). +-export([lookup_session/1, lookup_session_pid/1]). +-export([resume_session/1, resume_session/2]). +-export([discard_session/1, discard_session/2]). -export([register_session/2, get_session_attrs/1, unregister_session/1]). -export([get_session_stats/1, set_session_stats/2]). @@ -29,7 +31,8 @@ -export([dispatch/3]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(state, {session_pmon}). @@ -46,7 +49,7 @@ start_link() -> gen_server:start_link({local, ?SM}, ?MODULE, [], []). %% @doc Open a session. --spec(open_session(map()) -> {ok, pid(), boolean()} | {error, term()}). +-spec(open_session(map()) -> {ok, pid()} | {ok, pid(), boolean()} | {error, term()}). open_session(Attrs = #{clean_start := true, client_id := ClientId, client_pid := ClientPid}) -> @@ -61,8 +64,8 @@ open_session(Attrs = #{clean_start := false, client_pid := ClientPid}) -> ResumeStart = fun(_) -> case resume_session(ClientId, ClientPid) of - {ok, SessionPid} -> - {ok, SessionPid}; + {ok, SPid} -> + {ok, SPid, true}; {error, not_found} -> emqx_session_sup:start_session(Attrs); {error, Reason} -> @@ -78,10 +81,10 @@ discard_session(ClientId) when is_binary(ClientId) -> discard_session(ClientId, ClientPid) when is_binary(ClientId) -> lists:foreach( - fun({_ClientId, SessionPid}) -> - case catch emqx_session:discard(SessionPid, ClientPid) of + fun({_ClientId, SPid}) -> + case catch emqx_session:discard(SPid, ClientPid) of {Err, Reason} when Err =:= 'EXIT'; Err =:= error -> - emqx_logger:error("[SM] Failed to discard ~p: ~p", [SessionPid, Reason]); + emqx_logger:error("[SM] Failed to discard ~p: ~p", [SPid, Reason]); ok -> ok end end, lookup_session(ClientId)). @@ -94,25 +97,25 @@ resume_session(ClientId) -> resume_session(ClientId, ClientPid) -> case lookup_session(ClientId) of [] -> {error, not_found}; - [{_ClientId, SessionPid}] -> - ok = emqx_session:resume(SessionPid, ClientPid), - {ok, SessionPid}; + [{_ClientId, SPid}] -> + ok = emqx_session:resume(SPid, ClientPid), + {ok, SPid}; Sessions -> - [{_, SessionPid}|StaleSessions] = lists:reverse(Sessions), + [{_, SPid}|StaleSessions] = lists:reverse(Sessions), emqx_logger:error("[SM] More than one session found: ~p", [Sessions]), lists:foreach(fun({_, StalePid}) -> catch emqx_session:discard(StalePid, ClientPid) end, StaleSessions), - ok = emqx_session:resume(SessionPid, ClientPid), - {ok, SessionPid} + ok = emqx_session:resume(SPid, ClientPid), + {ok, SPid} end. %% @doc Close a session. -spec(close_session({client_id(), pid()} | pid()) -> ok). -close_session({_ClientId, SessionPid}) -> - emqx_session:close(SessionPid); -close_session(SessionPid) when is_pid(SessionPid) -> - emqx_session:close(SessionPid). +close_session({_ClientId, SPid}) -> + emqx_session:close(SPid); +close_session(SPid) when is_pid(SPid) -> + emqx_session:close(SPid). %% @doc Register a session with attributes. -spec(register_session(client_id() | {client_id(), pid()}, @@ -120,8 +123,8 @@ close_session(SessionPid) when is_pid(SessionPid) -> register_session(ClientId, Attrs) when is_binary(ClientId) -> register_session({ClientId, self()}, Attrs); -register_session(Session = {ClientId, SessionPid}, Attrs) - when is_binary(ClientId), is_pid(SessionPid) -> +register_session(Session = {ClientId, SPid}, Attrs) + when is_binary(ClientId), is_pid(SPid) -> ets:insert(?SESSION, Session), ets:insert(?SESSION_ATTRS, {Session, Attrs}), case proplists:get_value(clean_start, Attrs, true) of @@ -129,13 +132,13 @@ register_session(Session = {ClientId, SessionPid}, Attrs) false -> ets:insert(?SESSION_P, Session) end, emqx_sm_registry:register_session(Session), - notify({registered, ClientId, SessionPid}). + notify({registered, ClientId, SPid}). %% @doc Get session attrs -spec(get_session_attrs({client_id(), pid()}) -> list(emqx_session:attribute())). -get_session_attrs(Session = {ClientId, SessionPid}) - when is_binary(ClientId), is_pid(SessionPid) -> +get_session_attrs(Session = {ClientId, SPid}) + when is_binary(ClientId), is_pid(SPid) -> safe_lookup_element(?SESSION_ATTRS, Session, []). %% @doc Unregister a session @@ -143,19 +146,19 @@ get_session_attrs(Session = {ClientId, SessionPid}) unregister_session(ClientId) when is_binary(ClientId) -> unregister_session({ClientId, self()}); -unregister_session(Session = {ClientId, SessionPid}) - when is_binary(ClientId), is_pid(SessionPid) -> +unregister_session(Session = {ClientId, SPid}) + when is_binary(ClientId), is_pid(SPid) -> emqx_sm_registry:unregister_session(Session), ets:delete(?SESSION_STATS, Session), ets:delete(?SESSION_ATTRS, Session), ets:delete_object(?SESSION_P, Session), ets:delete_object(?SESSION, Session), - notify({unregistered, ClientId, SessionPid}). + notify({unregistered, ClientId, SPid}). %% @doc Get session stats -spec(get_session_stats({client_id(), pid()}) -> list(emqx_stats:stats())). -get_session_stats(Session = {ClientId, SessionPid}) - when is_binary(ClientId), is_pid(SessionPid) -> +get_session_stats(Session = {ClientId, SPid}) + when is_binary(ClientId), is_pid(SPid) -> safe_lookup_element(?SESSION_STATS, Session, []). %% @doc Set session stats @@ -164,8 +167,8 @@ get_session_stats(Session = {ClientId, SessionPid}) set_session_stats(ClientId, Stats) when is_binary(ClientId) -> set_session_stats({ClientId, self()}, Stats); -set_session_stats(Session = {ClientId, SessionPid}, Stats) - when is_binary(ClientId), is_pid(SessionPid) -> +set_session_stats(Session = {ClientId, SPid}, Stats) + when is_binary(ClientId), is_pid(SPid) -> ets:insert(?SESSION_STATS, {Session, Stats}). %% @doc Lookup a session from registry @@ -217,11 +220,11 @@ handle_call(Req, _From, State) -> emqx_logger:error("[SM] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({notify, {registered, ClientId, SessionPid}}, State = #state{session_pmon = PMon}) -> - {noreply, State#state{session_pmon = emqx_pmon:monitor(SessionPid, ClientId, PMon)}}; +handle_cast({notify, {registered, ClientId, SPid}}, State = #state{session_pmon = PMon}) -> + {noreply, State#state{session_pmon = emqx_pmon:monitor(SPid, ClientId, PMon)}}; -handle_cast({notify, {unregistered, _ClientId, SessionPid}}, State = #state{session_pmon = PMon}) -> - {noreply, State#state{session_pmon = emqx_pmon:demonitor(SessionPid, PMon)}}; +handle_cast({notify, {unregistered, _ClientId, SPid}}, State = #state{session_pmon = PMon}) -> + {noreply, State#state{session_pmon = emqx_pmon:demonitor(SPid, PMon)}}; handle_cast(Msg, State) -> emqx_logger:error("[SM] unexpected cast: ~p", [Msg]), diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index 3bf42f6ac..74a405f65 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -17,10 +17,15 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --import(lists, [reverse/1]). - --export([match/2, validate/1, triples/1, words/1, wildcard/1]). --export([join/1, feed_var/3, systop/1]). +-export([match/2]). +-export([validate/1, validate/2]). +-export([levels/1]). +-export([triples/1]). +-export([words/1]). +-export([wildcard/1]). +-export([join/1]). +-export([feed_var/3]). +-export([systop/1]). -export([parse/1, parse/2]). -type(word() :: '' | '+' | '#' | binary()). @@ -69,15 +74,21 @@ match([_H1|_], []) -> match([], [_H|_T2]) -> false. -%% @doc Validate Topic --spec(validate({name | filter, topic()}) -> boolean()). -validate({_, <<>>}) -> - false; -validate({_, Topic}) when is_binary(Topic) and (size(Topic) > ?MAX_TOPIC_LEN) -> - false; -validate({filter, Topic}) when is_binary(Topic) -> +%% @doc Validate topic name or filter +-spec(validate(topic() | {name | filter, topic()}) -> true). +validate(Topic) when is_binary(Topic) -> + validate(filter, Topic); +validate({Type, Topic}) when Type =:= name; Type =:= filter -> + validate(Type, Topic). + +-spec(validate(name | filter, topic()) -> true). +validate(_, <<>>) -> + error(empty_topic); +validate(_, Topic) when is_binary(Topic) and (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) -> +validate(name, Topic) when is_binary(Topic) -> Words = words(Topic), validate2(Words) and (not wildcard(Words)). @@ -86,7 +97,7 @@ validate2([]) -> validate2(['#']) -> % end with '#' true; validate2(['#'|Words]) when length(Words) > 0 -> - false; + error('topic_invalid_#'); validate2([''|Words]) -> validate2(Words); validate2(['+'|Words]) -> @@ -97,7 +108,7 @@ validate2([W|Words]) -> validate3(<<>>) -> true; validate3(<>) when C == $#; C == $+; C == 0 -> - false; + error('topic_invalid_char'); validate3(<<_/utf8, Rest/binary>>) -> validate3(Rest). @@ -107,7 +118,7 @@ triples(Topic) when is_binary(Topic) -> triples(words(Topic), root, []). triples([], _Parent, Acc) -> - reverse(Acc); + lists:reverse(Acc); triples([W|Words], Parent, Acc) -> Node = join(Parent, W), triples(Words, Node, [{Parent, W, Node}|Acc]). @@ -122,6 +133,9 @@ bin('+') -> <<"+">>; bin('#') -> <<"#">>; bin(B) when is_binary(B) -> B. +levels(Topic) when is_binary(Topic) -> + length(words(Topic)). + %% @doc Split Topic Path to Words -spec(words(topic()) -> words()). words(Topic) when is_binary(Topic) -> @@ -142,7 +156,7 @@ systop(Name) when is_binary(Name) -> feed_var(Var, Val, Topic) -> feed_var(Var, Val, words(Topic), []). feed_var(_Var, _Val, [], Acc) -> - join(reverse(Acc)); + join(lists:reverse(Acc)); feed_var(Var, Val, [Var|Words], Acc) -> feed_var(Var, Val, Words, [Val|Acc]); feed_var(Var, Val, [W|Words], Acc) -> @@ -166,17 +180,15 @@ join(Words) -> parse(Topic) when is_binary(Topic) -> parse(Topic, #{}). -parse(Topic = <<"$queue/", Topic1/binary>>, Options) -> - case maps:find(share, Options) of - {ok, _} -> error({invalid_topic, Topic}); - error -> parse(Topic1, maps:put(share, '$queue', Options)) - end; -parse(Topic = <<"$share/", Topic1/binary>>, Options) -> - case maps:find(share, Options) of - {ok, _} -> error({invalid_topic, Topic}); - error -> [Group, Topic2] = binary:split(Topic1, <<"/">>), - {Topic2, maps:put(share, Group, Options)} - end; +parse(Topic = <<"$queue/", _/binary>>, #{share := _Group}) -> + error({invalid_topic, Topic}); +parse(Topic = <<"$share/", _/binary>>, #{share := _Group}) -> + error({invalid_topic, Topic}); +parse(<<"$queue/", Topic1/binary>>, Options) -> + parse(Topic1, maps:put(share, '$queue', Options)); +parse(<<"$share/", Topic1/binary>>, Options) -> + [Group, Topic2] = binary:split(Topic1, <<"/">>), + {Topic2, maps:put(share, Group, Options)}; parse(Topic, Options) -> {Topic, Options}. diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 5932b9ef7..c36b484c6 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -16,7 +16,6 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --include("emqx_misc.hrl"). -export([info/1]). -export([stats/1]). @@ -44,9 +43,8 @@ shutdown_reason }). --define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). - -define(INFO_KEYS, [peername, sockname]). +-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -define(WSLOG(Level, Format, Args, State), lager:Level("WsClient(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). @@ -110,8 +108,8 @@ websocket_init(#state{request = Req, options = Options}) -> sendfun => send_fun(self())}, Options), ParserState = emqx_protocol:parser(ProtoState), Zone = proplists:get_value(zone, Options), - EnableStats = emqx_zone:env(Zone, enable_stats, true), - IdleTimout = emqx_zone:env(Zone, idle_timeout, 30000), + EnableStats = emqx_zone:get_env(Zone, enable_stats, true), + IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), lists:foreach(fun(Stat) -> put(Stat, 0) end, ?SOCK_STATS), {ok, #state{peername = Peername, sockname = Sockname, diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl index 830f08b89..fdcfc37a5 100644 --- a/src/emqx_zone.erl +++ b/src/emqx_zone.erl @@ -16,9 +16,11 @@ -behaviour(gen_server). --export([start_link/0]). +-include("emqx.hrl"). --export([env/2, env/3]). +-export([start_link/0]). +-export([get_env/2, get_env/3]). +-export([set_env/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, @@ -31,19 +33,25 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -env(undefined, Par) -> - emqx_config:get_env(Par); -env(Zone, Par) -> - env(Zone, Par, undefined). +-spec(get_env(zone() | undefined, atom()) -> undefined | term()). +get_env(undefined, Key) -> + emqx_config:get_env(Key); +get_env(Zone, Key) -> + get_env(Zone, Key, undefined). -env(undefined, Par, Default) -> - emqx_config:get_env(Par, Default); -env(Zone, Par, Default) -> - try ets:lookup_element(?TAB, {Zone, Par}, 2) +-spec(get_env(zone() | undefined, atom(), term()) -> undefined | term()). +get_env(undefined, Key, Def) -> + emqx_config:get_env(Key, Def); +get_env(Zone, Key, Def) -> + try ets:lookup_element(?TAB, {Zone, Key}, 2) catch error:badarg -> - emqx_config:get_env(Par, Default) + emqx_config:get_env(Key, Def) end. +-spec(set_env(zone(), atom(), term()) -> ok). +set_env(Zone, Key, Val) -> + gen_server:cast(?MODULE, {set_env, Zone, Key, Val}). + %%------------------------------------------------------------------------------ %% gen_server callbacks %%------------------------------------------------------------------------------ @@ -56,6 +64,10 @@ handle_call(Req, _From, State) -> emqx_logger:error("[Zone] unexpected call: ~p", [Req]), {reply, ignored, State}. +handle_cast({set_env, Zone, Key, Val}, State) -> + true = ets:insert(?TAB, {{Zone, Key}, Val}), + {noreply, State}; + handle_cast(Msg, State) -> emqx_logger:error("[Zone] unexpected cast: ~p", [Msg]), {noreply, State}. @@ -63,7 +75,7 @@ handle_cast(Msg, State) -> handle_info(reload, State) -> lists:foreach( fun({Zone, Opts}) -> - [ets:insert(?TAB, {{Zone, Par}, Val}) || {Par, Val} <- Opts] + [ets:insert(?TAB, {{Zone, Key}, Val}) || {Key, Val} <- Opts] end, emqx_config:get_env(zones, [])), {noreply, ensure_reload_timer(State), hibernate}; @@ -82,5 +94,5 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ ensure_reload_timer(State) -> - State#state{timer = erlang:send_after(5000, self(), reload)}. + State#state{timer = erlang:send_after(10000, self(), reload)}. diff --git a/include/emqx_misc.hrl b/test/emqx_mqtt_caps_SUITE.erl similarity index 59% rename from include/emqx_misc.hrl rename to test/emqx_mqtt_caps_SUITE.erl index e904b71e3..3fbb422d5 100644 --- a/include/emqx_misc.hrl +++ b/test/emqx_mqtt_caps_SUITE.erl @@ -12,15 +12,15 @@ %% See the License for the specific language governing permissions and %% limitations under the License. --define(record_to_map(Def, Rec), - maps:from_list(?record_to_proplist(Def, Rec))). +-module(emqx_mqtt_caps_SUITE). --define(record_to_map(Def, Rec, Fields), - maps:from_list(?record_to_proplist(Def, Rec, Fields))). +-include_lib("eunit/include/eunit.hrl"). --define(record_to_proplist(Def, Rec), - lists:zip(record_info(fields, Def), tl(tuple_to_list(Rec)))). +%% CT +-compile(export_all). +-compile(nowarn_export_all). + +all() -> + []. --define(record_to_proplist(Def, Rec, Fields), - [{K, V} || {K, V} <- ?record_to_proplist(Def, Rec), lists:member(K, Fields)]). diff --git a/test/emqx_topic_SUITE.erl b/test/emqx_topic_SUITE.erl index 87fb2c906..f6f2c007e 100644 --- a/test/emqx_topic_SUITE.erl +++ b/test/emqx_topic_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). @@ -27,10 +25,23 @@ -define(N, 10000). -all() -> [t_wildcard, t_match, t_match2, t_match3, t_validate, t_triples, t_join, - 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() -> + [t_wildcard, + t_match, t_match2, t_match3, + t_validate, + t_triples, + t_join, + t_levels, + 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]. t_wildcard(_) -> true = wildcard(<<"a/b/#">>), @@ -149,6 +160,9 @@ t_triples_perf(_) -> end), io:format("Time for triples: ~p(micro)", [Time/?N]). +t_levels(_) -> + ?assertEqual(4, emqx_topic:levels(<<"a/b/c/d">>)). + t_words(_) -> ['', <<"a">>, '+', '#'] = words(<<"/a/+/#">>), ['', <<"abkc">>, <<"19383">>, '+', <<"akakdkkdkak">>, '#'] = words(<<"/abkc/19383/+/akakdkkdkak/#">>), From 68cfcf6e0e7b427ec76889f75d481d2da7592f2c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 24 Aug 2018 23:19:11 +0800 Subject: [PATCH 100/520] Pass paho mqtt interoperability tests --- etc/emqx.conf | 2 +- priv/emqx.schema | 2 -- src/emqx_connection.erl | 5 +++-- src/emqx_mqtt_caps.erl | 1 - src/emqx_packet.erl | 6 +++++- src/emqx_protocol.erl | 32 ++++++++++++++++---------------- src/emqx_session.erl | 7 ++++--- src/emqx_trie.erl | 4 ++-- 8 files changed, 31 insertions(+), 28 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index bedf246ee..4703f5083 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -427,7 +427,7 @@ allow_anonymous = true ## TODO: Allow or deny if no ACL rules match. ## ## Value: allow | deny -acl_nomatch = deny +acl_nomatch = allow ## Default ACL File. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index c6fa7e30b..a0d2bc0e2 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -653,12 +653,10 @@ end}. ]}. {mapping, "zone.$name.allow_anonymous", "emqx.zones", [ - {default, false}, {datatype, {enum, [true, false]}} ]}. {mapping, "zone.$name.acl_nomatch", "emqx.zones", [ - {default, deny}, {datatype, {enum, [allow, deny]}} ]}. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 5ebd54dd0..d0d0bc90b 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -121,8 +121,9 @@ send_fun(Transport, Socket, Peername) -> fun(Data) -> try Transport:async_send(Socket, Data) of ok -> - ?LOG(debug, "SEND ~p", [Data], #state{peername = Peername}), - emqx_metrics:inc('bytes/sent', iolist_size(Data)), ok; + ?LOG(debug, "SEND ~p", [iolist_to_binary(Data)], #state{peername = Peername}), + emqx_metrics:inc('bytes/sent', iolist_size(Data)), + ok; Error -> Error catch error:Error -> diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl index 184e03673..d9ad79e0a 100644 --- a/src/emqx_mqtt_caps.erl +++ b/src/emqx_mqtt_caps.erl @@ -46,7 +46,6 @@ mqtt_retain_available]). -define(SUBCAP_KEYS, [max_qos_allowed, max_topic_levels, - mqtt_retain_available, mqtt_shared_subscription, mqtt_wildcard_subscription]). diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 67d1bffff..c8a751526 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -94,7 +94,11 @@ to_message(ClientId, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLI properties = Props}, payload = Payload}) -> Msg = emqx_message:make(ClientId, QoS, Topic, Payload), - Msg#message{flags = #{dup => Dup, retain => Retain}, headers = Props}; + Msg#message{flags = #{dup => Dup, retain => Retain}, + headers = if + Props =:= undefined -> #{}; + true -> Props + end}; to_message(_ClientId, #mqtt_packet_connect{will_flag = false}) -> undefined; diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 077c7a00c..96925279f 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -215,28 +215,28 @@ process(?CONNECT_PACKET( connack( case check_connect(Connect, PState1) of - ok -> - case authenticate(client(PState1), Password) of + {ok, PState2} -> + case authenticate(client(PState2), Password) of {ok, IsSuper} -> %% Maybe assign a clientId - PState2 = maybe_assign_client_id(PState1#pstate{is_super = IsSuper}), + PState3 = maybe_assign_client_id(PState2#pstate{is_super = IsSuper}), %% Open session - case try_open_session(PState2) of + case try_open_session(PState3) of {ok, SPid, SP} -> - PState3 = PState2#pstate{session = SPid}, - ok = emqx_cm:register_client({client_id(PState3), self()}, info(PState3)), + PState4 = PState3#pstate{session = SPid}, + ok = emqx_cm:register_client({client_id(PState4), self()}, info(PState4)), %% Start keepalive - start_keepalive(Keepalive, PState3), + start_keepalive(Keepalive, PState4), %% TODO: 'Run hooks' before open_session? - emqx_hooks:run('client.connected', [?RC_SUCCESS], client(PState3)), + emqx_hooks:run('client.connected', [?RC_SUCCESS], client(PState4)), %% Success - {?RC_SUCCESS, SP, replvar(PState3)}; + {?RC_SUCCESS, SP, replvar(PState4)}; {error, Error} -> ?LOG(error, "Failed to open session: ~p", [Error], PState1), {?RC_UNSPECIFIED_ERROR, PState1} end; {error, Reason} -> - ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], PState1), + ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], PState2), {?RC_NOT_AUTHORIZED, PState1} end; {error, ReasonCode} -> @@ -245,8 +245,8 @@ process(?CONNECT_PACKET( process(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PState) -> case check_publish(Packet, PState) of - ok -> - do_publish(Packet, PState); + {ok, PState1} -> + do_publish(Packet, PState1); {error, ReasonCode} -> ?LOG(warning, "Cannot publish qos0 message to ~s for ~s", [Topic, ReasonCode], PState), {ok, PState} @@ -254,16 +254,16 @@ process(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PState) -> process(Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), PState) -> case check_publish(Packet, PState) of - ok -> - do_publish(Packet, PState); + {ok, PState1} -> + do_publish(Packet, PState1); {error, ReasonCode} -> deliver({puback, PacketId, ReasonCode}, PState) end; process(Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), PState) -> case check_publish(Packet, PState) of - ok -> - do_publish(Packet, PState); + {ok, PState1} -> + do_publish(Packet, PState1); {error, ReasonCode} -> deliver({pubrec, PacketId, ReasonCode}, PState) end; diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 0c994d947..44c8f183f 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -310,7 +310,7 @@ close(SPid) -> init(#{zone := Zone, client_id := ClientId, - conn_pid := ClientPid, + client_pid := ClientPid, clean_start := CleanStart, username := Username, %% TODO: @@ -469,7 +469,7 @@ handle_cast({pubrec, PacketId, _ReasonCode}, State = #state{inflight = Inflight} end; %% PUBREL: -handle_cast({pubrel, PacketId}, State = #state{awaiting_rel = AwaitingRel}) -> +handle_cast({pubrel, PacketId, _ReasonCode}, State = #state{awaiting_rel = AwaitingRel}) -> {noreply, case maps:take(PacketId, AwaitingRel) of {Msg, AwaitingRel1} -> @@ -503,7 +503,7 @@ handle_cast({resume, ClientPid}, await_rel_timer = AwaitTimer, expiry_timer = ExpireTimer}) -> - ?LOG(info, "Resumed by ~p", [ClientPid], State), + ?LOG(info, "Resumed by ~p ", [ClientPid], State), %% Cancel Timers lists:foreach(fun emqx_misc:cancel_timer/1, @@ -649,6 +649,7 @@ retry_delivery(Force, State = #state{inflight = Inflight}) -> State; false -> Msgs = lists:sort(sortfun(inflight), emqx_inflight:values(Inflight)), + io:format("!!! Retry Delivery: ~p~n", [Msgs]), retry_delivery(Force, Msgs, os:timestamp(), State) end. diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index 43e36c060..6b75256d0 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -118,8 +118,8 @@ add_path({Node, Word, Child}) -> %% @private %% @doc Match node with word or '+'. -match_node(root, [<<"$SYS">>|Words]) -> - match_node(<<"$SYS">>, Words, []); +match_node(root, [NodeId = <<$$, _/binary>>|Words]) -> + match_node(NodeId, Words, []); match_node(NodeId, Words) -> match_node(NodeId, Words, []). From 6e325034e99af69a12668390df40b69d05179d3f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 25 Aug 2018 09:30:38 +0800 Subject: [PATCH 101/520] Update TODO --- TODO | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/TODO b/TODO index 87e6dea16..814b42a3d 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,58 @@ +## MQTT 5.0 + +1. Topic Alias +2. Subscriber ID +3. Session ensure stats +4. Message Expiration + +## Connection + +## WebSocket + +## Listeners + +## Protocol + +1. Global ACL cache with limited age and size? +2. Whether to enable ACL for each zone? + +## Session + +## Bridges + +Config +CLI +Remote Bridge +replay queue + +## Access Control + + Global ACL Cache + Add ACL cache emqx_access_control module + +## Zone + +## Hooks + +The hooks design... + +## MQueue + +Bound Queue +LastValue Queue +Priority Queue + +## Supervisor tree + +KernelSup + +## Managment + +## Dashboard + +## Testcases + 1. Update the README.md 2. Update the Documentation 3. Shared subscription and dispatch strategy From 146710a3946dc5ba4beb4d8f295092eeecc672de Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sat, 25 Aug 2018 10:36:46 +0800 Subject: [PATCH 102/520] fix makefile error --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7b1cc1cdd..69ac06fc1 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ BUILD_DEPS = cuttlefish dep_cuttlefish = git https://github.com/emqx/cuttlefish emqx30 TEST_DEPS = emqx_ct_helplers -dep_emqx_ct_helplers = git git@github.com:emqx/emqx_ct_helpers +dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers TEST_ERLC_OPTS += +debug_info TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' From b10f49b52cdca554f48d3c5f71f98492dfa85725 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 25 Aug 2018 11:49:19 +0800 Subject: [PATCH 103/520] Add CONNACK macros for MQTT V3.1.1 --- include/emqx_mqtt.hrl | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index e429aa4a3..1d69ac365 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -140,7 +140,20 @@ -type(mqtt_packet_type() :: ?RESERVED..?AUTH). %%-------------------------------------------------------------------- -%% MQTT Reason Codes +%% MQTT V3.1.1 Connect Return Codes +%%-------------------------------------------------------------------- + +-define(CONNACK_ACCEPT, 0). %% Connection accepted +-define(CONNACK_PROTO_VER, 1). %% Unacceptable protocol version +-define(CONNACK_INVALID_ID, 2). %% Client Identifier is correct UTF-8 but not allowed by the Server +-define(CONNACK_SERVER, 3). %% Server unavailable +-define(CONNACK_CREDENTIALS, 4). %% Username or password is malformed +-define(CONNACK_AUTH, 5). %% Client is not authorized to connect + +-type(mqtt_connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH). + +%%-------------------------------------------------------------------- +%% MQTT V5.0 Reason Codes %%-------------------------------------------------------------------- -define(RC_SUCCESS, 16#00). From 5f42f8840193640dae371a8af63f66431d63ef2d Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 25 Aug 2018 11:51:36 +0800 Subject: [PATCH 104/520] Pass paho zero_length_clientid test case --- src/emqx_protocol.erl | 31 ++++++++++++++++++++----------- src/emqx_reason_codes.erl | 21 +++++++++++++++++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 96925279f..650da4d1c 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -73,6 +73,7 @@ init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) peercert = Peercert, proto_ver = ?MQTT_PROTO_V4, proto_name = <<"MQTT">>, + client_id = <<>>, client_pid = self(), username = init_username(Peercert, Options), is_super = false, @@ -201,6 +202,8 @@ process(?CONNECT_PACKET( username = Username, password = Password} = Connect), PState) -> + io:format("~p~n", [Connect]), + PState1 = set_username(Username, PState#pstate{client_id = ClientId, proto_ver = ProtoVer, @@ -334,8 +337,12 @@ process(?PACKET(?DISCONNECT), PState) -> connack({?RC_SUCCESS, SP, PState}) -> deliver({connack, ?RC_SUCCESS, sp(SP)}, PState); -connack({ReasonCode, PState}) -> - deliver({connack, ReasonCode, 0}, PState), +connack({ReasonCode, PState = #pstate{proto_ver = ProtoVer}}) -> + _ = deliver({connack, if ProtoVer =:= ?MQTT_PROTO_V5 -> + ReasonCode; + true -> + emqx_reason_codes:compat(connack, ReasonCode) + end}, PState), {error, emqx_reason_codes:name(ReasonCode), PState}. %%------------------------------------------------------------------------------ @@ -415,7 +422,7 @@ send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, %% Assign a clientid maybe_assign_client_id(PState = #pstate{client_id = <<>>, ackprops = AckProps}) -> - ClientId = iolist_to_binary(["emqx_", emqx_guid:gen()]), + ClientId = emqx_guid:to_base62(emqx_guid:gen()), AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), PState#pstate{client_id = ClientId, ackprops = AckProps1}; maybe_assign_client_id(PState) -> @@ -464,18 +471,20 @@ check_proto_ver(#mqtt_packet_connect{proto_ver = Ver, false -> {error, ?RC_PROTOCOL_ERROR} end. -%% Issue#599: Null clientId and clean_start = false -check_client_id(#mqtt_packet_connect{client_id = ClientId, - clean_start = false}, _PState) - when ClientId == undefined; ClientId == <<>> -> - {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; - %% MQTT3.1 does not allow null clientId check_client_id(#mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V3, - client_id = ClientId}, _PState) - when ClientId == undefined; ClientId == <<>> -> + client_id = <<>>}, _PState) -> {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; +%% Issue#599: Null clientId and clean_start = false +check_client_id(#mqtt_packet_connect{client_id = <<>>, + clean_start = false}, _PState) -> + {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; + +check_client_id(#mqtt_packet_connect{client_id = <<>>, + clean_start = true}, _PState) -> + ok; + check_client_id(#mqtt_packet_connect{client_id = ClientId}, #pstate{zone = Zone}) -> Len = byte_size(ClientId), MaxLen = emqx_zone:get_env(Zone, max_clientid_len), diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index 335be3bd4..b777b2627 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -15,7 +15,10 @@ %% @doc MQTT5 reason codes -module(emqx_reason_codes). +-include("emqx_mqtt.hrl"). + -export([name/1, text/1]). +-export([compat/2]). name(16#00) -> success; name(16#01) -> granted_qos1; @@ -107,3 +110,21 @@ text(16#A1) -> <<"Subscription Identifiers not supported">>; text(16#A2) -> <<"Wildcard Subscriptions not supported">>; text(Code) -> iolist_to_binary(["Unkown Reason Code:", integer_to_list(Code)]). +compat(connack, 16#80) -> ?CONNACK_PROTO_VER; +compat(connack, 16#81) -> ?CONNACK_PROTO_VER; +compat(connack, 16#82) -> ?CONNACK_PROTO_VER; +compat(connack, 16#83) -> ?CONNACK_PROTO_VER; +compat(connack, 16#84) -> ?CONNACK_PROTO_VER; +compat(connack, 16#85) -> ?CONNACK_INVALID_ID; +compat(connack, 16#86) -> ?CONNACK_CREDENTIALS; +compat(connack, 16#87) -> ?CONNACK_AUTH; +compat(connack, 16#88) -> ?CONNACK_SERVER; +compat(connack, 16#89) -> ?CONNACK_SERVER; +compat(connack, 16#8A) -> ?CONNACK_AUTH; +compat(connack, 16#8B) -> ?CONNACK_SERVER; +compat(connack, 16#8C) -> ?CONNACK_AUTH; +compat(connack, 16#97) -> ?CONNACK_SERVER; +compat(connack, 16#9C) -> ?CONNACK_SERVER; +compat(connack, 16#9D) -> ?CONNACK_SERVER; +compat(connack, 16#9F) -> ?CONNACK_SERVER. + From fc0f57073d75a4ba11c44a3aec1c6c287ea1024e Mon Sep 17 00:00:00 2001 From: turtled Date: Sat, 25 Aug 2018 14:32:32 +0800 Subject: [PATCH 105/520] Fix share sub dispatch fail --- src/emqx_shared_sub.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 5194de9d4..e8a9fac10 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -81,7 +81,7 @@ record(Group, Topic, SubPid) -> #emqx_shared_subscription{group = Group, topic = Topic, subpid = SubPid}. %% TODO: dispatch strategy, ensure the delivery... -dispatch(Group, Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> +dispatch({Group, _Node}, Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> case pick(subscribers(Group, Topic)) of false -> Delivery; SubPid -> SubPid ! {dispatch, Topic, Msg}, @@ -98,7 +98,6 @@ pick(SubPids) -> subscribers(Group, Topic) -> ets:select(?TAB, [{{emqx_shared_subscription, Group, Topic, '$1'}, [], ['$1']}]). - %%----------------------------------------------------------------------------- %% gen_server callbacks %%----------------------------------------------------------------------------- From 4af606598484ba8cdc2d443a00a76a9e62016887 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 25 Aug 2018 16:02:44 +0800 Subject: [PATCH 106/520] For paho interoperability tests --- etc/acl.conf.paho | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 etc/acl.conf.paho diff --git a/etc/acl.conf.paho b/etc/acl.conf.paho new file mode 100644 index 000000000..5beec4347 --- /dev/null +++ b/etc/acl.conf.paho @@ -0,0 +1,14 @@ +%%-------------------------------------------------------------------- +%% For paho interoperability test cases +%%-------------------------------------------------------------------- + +{deny, {client, "myclientid"}, subscribe, ["test/nosubscribe"]}. + +{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. + +{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. + +{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. + +{allow, all}. + From 1aee05ce16fa3a1b38eafc2b0513a577991bab80 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 25 Aug 2018 16:03:28 +0800 Subject: [PATCH 107/520] Fix unsubscribe bug --- src/emqx_session.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 44c8f183f..db7e80a9b 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -433,7 +433,7 @@ handle_cast({subscribe, From, {PacketId, Properties, TopicFilters}}, handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> {ReasonCodes, Subscriptions1} = - lists:foldr(fun(Topic, {RcAcc, SubMap}) -> + lists:foldr(fun({Topic, _Opts}, {RcAcc, SubMap}) -> case maps:find(Topic, SubMap) of {ok, SubOpts} -> emqx_broker:unsubscribe(Topic, ClientId), @@ -649,7 +649,6 @@ retry_delivery(Force, State = #state{inflight = Inflight}) -> State; false -> Msgs = lists:sort(sortfun(inflight), emqx_inflight:values(Inflight)), - io:format("!!! Retry Delivery: ~p~n", [Msgs]), retry_delivery(Force, Msgs, os:timestamp(), State) end. From 612c88e71e333adf9ac261e9e0baf243e3fc8209 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 25 Aug 2018 16:04:21 +0800 Subject: [PATCH 108/520] Add 'rc' and 'subid' fields --- src/emqx_frame.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 82db0acf5..59a195e33 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -331,7 +331,7 @@ parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) -> {Value + Len * Multiplier, Rest}. parse_topic_filters(subscribe, Bin) -> - [{Topic, #{rh => Rh, rap => Rap, nl => Nl, qos => QoS}} + [{Topic, #{rh => Rh, rap => Rap, nl => Nl, qos => QoS, rc => 0, subid => 0}} || <> <= Bin]; parse_topic_filters(unsubscribe, Bin) -> From c2c1320083db5f65b7d8f96eaa11df0e37356412 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 25 Aug 2018 16:05:58 +0800 Subject: [PATCH 109/520] Update compat/2 for suback reason codes --- src/emqx_reason_codes.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index b777b2627..f300d675d 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -126,5 +126,8 @@ compat(connack, 16#8C) -> ?CONNACK_AUTH; compat(connack, 16#97) -> ?CONNACK_SERVER; compat(connack, 16#9C) -> ?CONNACK_SERVER; compat(connack, 16#9D) -> ?CONNACK_SERVER; -compat(connack, 16#9F) -> ?CONNACK_SERVER. +compat(connack, 16#9F) -> ?CONNACK_SERVER; + +compat(suback, Code) when Code =< ?QOS2 -> Code; +compat(suback, Code) when Code > 16#80 -> 16#80. From b7c2821326112ef7cf62ad39816ae20cbfc5153f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 25 Aug 2018 16:07:57 +0800 Subject: [PATCH 110/520] Make reason codes of SUBACK be compatible with MQTT V3.1.1 --- src/emqx_protocol.erl | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 650da4d1c..3faa7781a 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -391,8 +391,13 @@ deliver({pubrel, PacketId}, PState) -> deliver({pubrec, PacketId, ReasonCode}, PState) -> send(?PUBREC_PACKET(PacketId, ReasonCode), PState); -deliver({suback, PacketId, ReasonCodes}, PState) -> - send(?SUBACK_PACKET(PacketId, ReasonCodes), PState); +deliver({suback, PacketId, ReasonCodes}, PState = #pstate{proto_ver = ProtoVer}) -> + send(?SUBACK_PACKET(PacketId, + if ProtoVer =:= ?MQTT_PROTO_V5 -> + ReasonCodes; + true -> + [emqx_reason_codes:compat(suback, RC) || RC <- ReasonCodes] + end), PState); deliver({unsuback, PacketId, ReasonCodes}, PState) -> send(?UNSUBACK_PACKET(PacketId, ReasonCodes), PState); @@ -408,12 +413,12 @@ deliver({disconnect, _ReasonCode}, PState) -> %% Send Packet to Client -spec(send(mqtt_packet(), state()) -> {ok, state()} | {error, term()}). -send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, - sendfun = SendFun}) -> +send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = SendFun}) -> + trace(send, Packet, PState), case SendFun(emqx_frame:serialize(Packet, #{version => Ver})) of - ok -> emqx_metrics:sent(Packet), - trace(send, Packet, PState), - {ok, inc_stats(send, Type, PState)}; + ok -> + emqx_metrics:sent(Packet), + {ok, inc_stats(send, Type, PState)}; {error, Reason} -> {error, Reason} end. From 8b4be236e5946ad63ae25bfdb73174d56d586698 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sat, 25 Aug 2018 17:10:51 +0800 Subject: [PATCH 111/520] fix list_to_subid error --- src/emqx.erl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/emqx.erl b/src/emqx.erl index 475428fd4..d6437b3f5 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -124,10 +124,8 @@ list_to_subid(SubId) when is_list(SubId) -> iolist_to_binary(SubId); list_to_subid(SubPid) when is_pid(SubPid) -> SubPid; -list_to_subid({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> - {SubId, SubPid}; -list_to_subid({SubId, SubPid}) when is_list(SubId), is_pid(SubPid) -> - {iolist_to_binary(SubId), SubPid}. +list_to_subid({SubPid, SubId}) when is_pid(SubPid), is_binary(SubId) -> + {SubPid, SubId}. %%-------------------------------------------------------------------- %% Hooks API From 6b45834de4659db04cb3ca415d8b6a5f89cda80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 25 Aug 2018 18:13:49 +0800 Subject: [PATCH 112/520] Add emqx_cm, emqx_metrics, emqx_stats test suites --- test/emqx_cm_SUITE.erl | 39 ++++++++++++++++++++++++ test/emqx_metrics_SUITE.erl | 41 +++++++++++++++++++++++++ test/emqx_stats_SUITE.erl | 60 +++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 test/emqx_cm_SUITE.erl create mode 100644 test/emqx_metrics_SUITE.erl create mode 100644 test/emqx_stats_SUITE.erl diff --git a/test/emqx_cm_SUITE.erl b/test/emqx_cm_SUITE.erl new file mode 100644 index 000000000..4cb4fa8a4 --- /dev/null +++ b/test/emqx_cm_SUITE.erl @@ -0,0 +1,39 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_mqtt.hrl"). + +all() -> [t_register_unregister_client]. + +t_register_unregister_client(_) -> + {ok, _} = emqx_cm_sup:start_link(), + Pid = self(), + emqx_cm:register_client(<<0, 0, 1>>), + emqx_cm:register_client({<<0, 0, 2>>, Pid}, [{port, 8080}, {ip, "192.168.0.1"}]), + timer:sleep(2000), + [{<<0, 0, 1>>, Pid}] = emqx_cm:lookup_client(<<0, 0, 1>>), + [{<<0, 0, 2>>, Pid}] = emqx_cm:lookup_client(<<0, 0, 2>>), + Pid = emqx_cm:lookup_client_pid(<<0, 0, 1>>), + emqx_cm:unregister_client(<<0, 0, 1>>), + [] = emqx_cm:lookup_client(<<0, 0, 1>>), + [{port, 8080}, {ip, "192.168.0.1"}] = emqx_cm:get_client_attrs({<<0, 0, 2>>, Pid}), + emqx_cm:set_client_stats(<<0, 0, 2>>, [[{count, 1}, {max, 2}]]), + [[{count, 1}, {max, 2}]] = emqx_cm:get_client_stats({<<0, 0, 2>>, Pid}). \ No newline at end of file diff --git a/test/emqx_metrics_SUITE.erl b/test/emqx_metrics_SUITE.erl new file mode 100644 index 000000000..7c7c83803 --- /dev/null +++ b/test/emqx_metrics_SUITE.erl @@ -0,0 +1,41 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_metrics_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx_mqtt.hrl"). + +all() -> [t_inc_dec_metrics]. + +t_inc_dec_metrics(_) -> + {ok, _} = emqx_metrics:start_link(), + {0, 0} = {emqx_metrics:val('bytes/received'), emqx_metrics:val('messages/retained')}, + emqx_metrics:inc('bytes/received'), + emqx_metrics:inc({counter, 'bytes/received'}, 2), + emqx_metrics:inc(counter, 'bytes/received', 2), + emqx_metrics:inc({gauge, 'messages/retained'}, 2), + emqx_metrics:inc(gauge, 'messages/retained', 2), + {5, 4} = {emqx_metrics:val('bytes/received'), emqx_metrics:val('messages/retained')}, + emqx_metrics:dec(gauge, 'messages/retained'), + emqx_metrics:dec(gauge, 'messages/retained', 1), + 2 = emqx_metrics:val('messages/retained'), + emqx_metrics:received(#mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}}), + {1, 1} = {emqx_metrics:val('packets/received'), emqx_metrics:val('packets/connect')}, + emqx_metrics:sent(#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}}), + {1, 1} = {emqx_metrics:val('packets/sent'), emqx_metrics:val('packets/connack')}. \ No newline at end of file diff --git a/test/emqx_stats_SUITE.erl b/test/emqx_stats_SUITE.erl new file mode 100644 index 000000000..b544d6128 --- /dev/null +++ b/test/emqx_stats_SUITE.erl @@ -0,0 +1,60 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_stats_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). + +all() -> [t_set_get_state, t_update_interval]. + +t_set_get_state(_) -> + {ok, _} = emqx_stats:start_link(), + SetClientsCount = emqx_stats:statsfun('clients/count'), + SetClientsCount(1), + 1 = emqx_stats:getstat('clients/count'), + emqx_stats:setstat('clients/count', 2), + 2 = emqx_stats:getstat('clients/count'), + emqx_stats:setstat('clients/count', 'clients/max', 3), + timer:sleep(100), + 3 = emqx_stats:getstat('clients/count'), + 3 = emqx_stats:getstat('clients/max'), + emqx_stats:setstat('clients/count', 'clients/max', 2), + timer:sleep(100), + 2 = emqx_stats:getstat('clients/count'), + 3 = emqx_stats:getstat('clients/max'), + SetClients = emqx_stats:statsfun('clients/count', 'clients/max'), + SetClients(4), + timer:sleep(100), + 4 = emqx_stats:getstat('clients/count'), + 4 = emqx_stats:getstat('clients/max'), + Clients = emqx_stats:getstats(), + 4 = proplists:get_value('clients/count', Clients), + 4 = proplists:get_value('clients/max', Clients). + +t_update_interval(_) -> + {ok, _} = emqx_stats:start_link(), + ok = emqx_stats:update_interval(cm_stats, fun update_stats/0), + timer:sleep(2000), + 1 = emqx_stats:getstat('clients/count'). + +update_stats() -> + ClientsCount = emqx_stats:getstat('clients/count'), + ct:log("hello~n"), + % emqx_stats:setstat('clients/count', 'clients/max', ClientsCount + 1). + emqx_stats:setstat('clients/count', 1). \ No newline at end of file From ee11627828e713fb62776b353b14fde643037c62 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sat, 25 Aug 2018 18:36:17 +0800 Subject: [PATCH 113/520] delete duplicated subscribe function --- src/emqx.erl | 18 +++++++++++------- src/emqx_broker.erl | 2 -- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/emqx.erl b/src/emqx.erl index d6437b3f5..c39dde931 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -71,12 +71,18 @@ subscribe(Topic) -> emqx_broker:subscribe(iolist_to_binary(Topic)). -spec(subscribe(topic() | string(), subscriber() | string()) -> ok | {error, term()}). -subscribe(Topic, Subscriber) -> - emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber)). +subscribe(Topic, Sub) when is_list(Sub)-> + emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Sub)); +subscribe(Topic, Subscriber) when is_tuple(Subscriber) -> + {SubPid, SubId} = Subscriber, + emqx_broker:subscribe(iolist_to_binary(Topic), SubPid, SubId). -spec(subscribe(topic() | string(), subscriber() | string(), subopts()) -> ok | {error, term()}). -subscribe(Topic, Subscriber, Options) -> - emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). +subscribe(Topic, Sub, Options) when is_list(Sub)-> + emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Sub), Options); +subscribe(Topic, Subscriber, Options) when is_tuple(Subscriber)-> + {SubPid, SubId} = Subscriber, + emqx_broker:subscribe(iolist_to_binary(Topic), SubPid, SubId, Options). %% @doc Publish Message -spec(publish(message()) -> {ok, delivery()} | {error, term()}). @@ -123,9 +129,7 @@ list_to_subid(SubId) when is_binary(SubId) -> list_to_subid(SubId) when is_list(SubId) -> iolist_to_binary(SubId); list_to_subid(SubPid) when is_pid(SubPid) -> - SubPid; -list_to_subid({SubPid, SubId}) when is_pid(SubPid), is_binary(SubId) -> - {SubPid, SubId}. + SubPid. %%-------------------------------------------------------------------- %% Hooks API diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 7015590d8..24cd27ab8 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -68,8 +68,6 @@ subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> subscribe(Topic, self(), SubId). -spec(subscribe(topic(), pid() | subid(), subid() | subopts()) -> ok). -subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - subscribe(Topic, SubPid, SubId, #{}); subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> subscribe(Topic, SubPid, SubId, #{}); subscribe(Topic, SubPid, SubOpts) when is_binary(Topic), is_pid(SubPid), is_map(SubOpts) -> From 737fe193317e1ed05f7f3cff6ab2b3a6c0c82312 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Tue, 14 Aug 2018 18:02:27 +0800 Subject: [PATCH 114/520] update acl test cases --- src/emqx.app.src | 3 +- src/emqx_acl_internal.erl | 5 ++-- test/emqx_SUITE.erl | 6 ++-- test/emqx_access_SUITE.erl | 22 ++++++--------- test/emqx_broker_SUITE.erl | 2 +- test/emqx_mqueue_SUITE.erl | 58 +++++++++++++++++++------------------- 6 files changed, 47 insertions(+), 49 deletions(-) diff --git a/src/emqx.app.src b/src/emqx.app.src index d963e3662..c4a8b5c2e 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -3,7 +3,8 @@ {vsn,"3.0"}, {modules,[]}, {registered,[emqx_sup]}, - {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,cowboy]}, + {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,cowboy, + minirest]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index 65a3199ae..23042dcbe 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -109,11 +109,12 @@ reload_acl(#state{config = undefined}) -> reload_acl(State) -> case catch load_rules_from_file(State) of {'EXIT', Error} -> {error, Error}; - true -> io:format("~s~n", ["reload acl_internal successfully"]), ok + #state{config=File} -> + io:format("reload acl_internal successfully: ~p~n", [File]), + ok end. %% @doc ACL Module Description -spec(description() -> string()). description() -> "Internal ACL with etc/acl.conf". - diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index e3e0bbb38..f2f00e55e 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -19,7 +19,7 @@ -compile(export_all). -compile(nowarn_export_all). --include_lib("emqttc/include/emqttc_packet.hrl"). +-include("emqx_mqtt.hrl"). -define(APP, emqx). @@ -79,7 +79,7 @@ mqtt_connect_with_tcp(_) -> Packet = raw_send_serialise(?CLIENT), gen_tcp:send(Sock, Packet), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(Data), + {ok, ?CONNACK_PACKET(0), _} = raw_recv_pase(Data), gen_tcp:close(Sock). mqtt_connect_with_ssl_oneway(_) -> @@ -133,7 +133,7 @@ mqtt_connect_with_ws(_Config) -> Packet = raw_send_serialise(?CLIENT), ok = rfc6455_client:send_binary(WS, Packet), {binary, P} = rfc6455_client:recv(WS), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(P), + {ok, ?CONNACK_PACKET(0), _} = raw_recv_pase(P), {close, _} = rfc6455_client:close(WS), ok. diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index 270d55f57..f206cca05 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -59,11 +59,8 @@ prepare_config() -> {deny, all, subscribe, ["$SYS/#", "#"]}, {deny, all}], write_config("access_SUITE_acl.conf", Rules), - Config = [{auth, anonymous, []}, - {acl, internal, [{config, "access_SUITE_acl.conf"}, - {nomatch, allow}]}], - write_config("access_SUITE_emqx.conf", Config), - application:set_env(emqx, conf, "access_SUITE_emqx.conf"). + application:set_env(emqx, acl_file, "access_SUITE_acl.conf"), + application:set_env(emqx, acl_cache, false). write_config(Filename, Terms) -> file:write_file(Filename, [io_lib:format("~tp.~n", [Term]) || Term <- Terms]). @@ -119,14 +116,14 @@ unregister_mod(_) -> [] = ?AC:lookup_mods(auth). check_acl(_) -> - User1 = #client{client_id = <<"client1">>, username = <<"testuser">>}, - User2 = #client{client_id = <<"client2">>, username = <<"xyz">>}, + User1 = #client{id = <<"client1">>, username = <<"testuser">>}, + User2 = #client{id = <<"client2">>, username = <<"xyz">>}, allow = ?AC:check_acl(User1, subscribe, <<"users/testuser/1">>), allow = ?AC:check_acl(User1, subscribe, <<"clients/client1">>), - allow = ?AC:check_acl(User1, subscribe, <<"clients/client1/x/y">>), + deny = ?AC:check_acl(User1, subscribe, <<"clients/client1/x/y">>), allow = ?AC:check_acl(User1, publish, <<"users/testuser/1">>), allow = ?AC:check_acl(User1, subscribe, <<"a/b/c">>), - allow = ?AC:check_acl(User2, subscribe, <<"a/b/c">>). + deny = ?AC:check_acl(User2, subscribe, <<"a/b/c">>). %%-------------------------------------------------------------------- %% emqx_access_rule @@ -159,9 +156,9 @@ compile_rule(_) -> {deny, all} = compile({deny, all}). match_rule(_) -> - User = #client{peername = {{127,0,0,1}, 2948}, client_id = <<"testClient">>, username = <<"TestUser">>}, - User2 = #client{peername = {{192,168,0,10}, 3028}, client_id = <<"testClient">>, username = <<"TestUser">>}, - + User = #client{peername = {{127,0,0,1}, 2948}, id = <<"testClient">>, username = <<"TestUser">>}, + User2 = #client{peername = {{192,168,0,10}, 3028}, id = <<"testClient">>, username = <<"TestUser">>}, + {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/#", "#"]})), @@ -179,4 +176,3 @@ match_rule(_) -> {matched, allow} = match(User, <<"Topic">>, AndRule), OrRule = compile({allow, {'or', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, ["Topic"]}), {matched, allow} = match(User, <<"Topic">>, OrRule). - diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index e77d689d4..af5c64949 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -149,7 +149,7 @@ start_session(_) -> {ok, ClientPid} = emqx_mock_client:start_link(<<"clientId">>), {ok, SessPid} = emqx_mock_client:start_session(ClientPid), Message = emqx_message:make(<<"clientId">>, 2, <<"topic">>, <<"hello">>), - Message1 = Message#mqtt_message{packet_id = 1}, + Message1 = Message#message{id = 1}, emqx_session:publish(SessPid, Message1), emqx_session:pubrel(SessPid, 1), emqx_session:subscribe(SessPid, [{<<"topic/session">>, [{qos, 2}]}]), diff --git a/test/emqx_mqueue_SUITE.erl b/test/emqx_mqueue_SUITE.erl index d174d980e..5ab510633 100644 --- a/test/emqx_mqueue_SUITE.erl +++ b/test/emqx_mqueue_SUITE.erl @@ -35,22 +35,22 @@ t_in(_) -> {store_qos0, true}], Q = ?Q:new(<<"testQ">>, Opts, alarm_fun()), ?assert(?Q:is_empty(Q)), - Q1 = ?Q:in(#mqtt_message{}, Q), + Q1 = ?Q:in(#message{}, Q), ?assertEqual(1, ?Q:len(Q1)), - Q2 = ?Q:in(#mqtt_message{qos = 1}, Q1), + Q2 = ?Q:in(#message{qos = 1}, Q1), ?assertEqual(2, ?Q:len(Q2)), - Q3 = ?Q:in(#mqtt_message{qos = 2}, Q2), - Q4 = ?Q:in(#mqtt_message{}, Q3), - Q5 = ?Q:in(#mqtt_message{}, Q4), + Q3 = ?Q:in(#message{qos = 2}, Q2), + Q4 = ?Q:in(#message{}, Q3), + Q5 = ?Q:in(#message{}, Q4), ?assertEqual(5, ?Q:len(Q5)). t_in_qos0(_) -> Opts = [{max_length, 5}, {store_qos0, false}], Q = ?Q:new(<<"testQ">>, Opts, alarm_fun()), - Q1 = ?Q:in(#mqtt_message{}, Q), + Q1 = ?Q:in(#message{}, Q), ?assert(?Q:is_empty(Q1)), - Q2 = ?Q:in(#mqtt_message{qos = 0}, Q1), + Q2 = ?Q:in(#message{qos = 0}, Q1), ?assert(?Q:is_empty(Q2)). t_out(_) -> @@ -58,10 +58,10 @@ t_out(_) -> {store_qos0, true}], Q = ?Q:new(<<"testQ">>, Opts, alarm_fun()), {empty, Q} = ?Q:out(Q), - Q1 = ?Q:in(#mqtt_message{}, Q), + Q1 = ?Q:in(#message{}, Q), {Value, Q2} = ?Q:out(Q1), ?assertEqual(0, ?Q:len(Q2)), - ?assertEqual({value, #mqtt_message{}}, Value). + ?assertEqual({value, #message{}}, Value). t_simple_mqueue(_) -> Opts = [{type, simple}, @@ -74,13 +74,13 @@ t_simple_mqueue(_) -> ?assertEqual(3, ?Q:max_len(Q)), ?assertEqual(<<"simple_queue">>, ?Q:name(Q)), ?assert(?Q:is_empty(Q)), - Q1 = ?Q:in(#mqtt_message{qos = 1, payload = <<"1">>}, Q), - Q2 = ?Q:in(#mqtt_message{qos = 1, payload = <<"2">>}, Q1), - Q3 = ?Q:in(#mqtt_message{qos = 1, payload = <<"3">>}, Q2), - Q4 = ?Q:in(#mqtt_message{qos = 1, payload = <<"4">>}, Q3), + Q1 = ?Q:in(#message{qos = 1, payload = <<"1">>}, Q), + Q2 = ?Q:in(#message{qos = 1, payload = <<"2">>}, Q1), + Q3 = ?Q:in(#message{qos = 1, payload = <<"3">>}, Q2), + Q4 = ?Q:in(#message{qos = 1, payload = <<"4">>}, Q3), ?assertEqual(3, ?Q:len(Q4)), {{value, Msg}, Q5} = ?Q:out(Q4), - ?assertEqual(<<"2">>, Msg#mqtt_message.payload), + ?assertEqual(<<"2">>, Msg#message.payload), ?assertEqual([{len, 2}, {max_len, 3}, {dropped, 1}], ?Q:stats(Q5)). t_infinity_simple_mqueue(_) -> @@ -93,12 +93,12 @@ t_infinity_simple_mqueue(_) -> ?assert(?Q:is_empty(Q)), ?assertEqual(0, ?Q:max_len(Q)), Qx = lists:foldl(fun(I, AccQ) -> - ?Q:in(#mqtt_message{qos = 1, payload = iolist_to_binary([I])}, AccQ) + ?Q:in(#message{qos = 1, payload = iolist_to_binary([I])}, AccQ) end, Q, lists:seq(1, 255)), ?assertEqual(255, ?Q:len(Qx)), ?assertEqual([{len, 255}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)), {{value, V}, _Qy} = ?Q:out(Qx), - ?assertEqual(<<1>>, V#mqtt_message.payload). + ?assertEqual(<<1>>, V#message.payload). t_priority_mqueue(_) -> Opts = [{type, priority}, @@ -113,18 +113,18 @@ t_priority_mqueue(_) -> ?assertEqual(<<"priority_queue">>, ?Q:name(Q)), ?assert(?Q:is_empty(Q)), - Q1 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t1">>}, Q), - Q2 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t">>}, Q1), - Q3 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t2">>}, Q2), + Q1 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q), + Q2 = ?Q:in(#message{qos = 1, topic = <<"t">>}, Q1), + Q3 = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q2), ?assertEqual(3, ?Q:len(Q3)), - Q4 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t1">>}, Q3), + Q4 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q3), ?assertEqual(4, ?Q:len(Q4)), - Q5 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t1">>}, Q4), + Q5 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q4), ?assertEqual(5, ?Q:len(Q5)), - Q6 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t1">>}, Q5), + Q6 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q5), ?assertEqual(5, ?Q:len(Q6)), {{value, Msg}, _Q7} = ?Q:out(Q6), - ?assertEqual(<<"t">>, Msg#mqtt_message.topic). + ?assertEqual(<<"t">>, Msg#message.topic). t_infinity_priority_mqueue(_) -> Opts = [{type, priority}, @@ -135,8 +135,8 @@ t_infinity_priority_mqueue(_) -> ?assertEqual(0, ?Q:max_len(Q)), Qx = lists:foldl(fun(I, AccQ) -> AccQ1 = - ?Q:in(#mqtt_message{topic = <<"t1">>, qos = 1, payload = iolist_to_binary([I])}, AccQ), - ?Q:in(#mqtt_message{topic = <<"t">>, qos = 1, payload = iolist_to_binary([I])}, AccQ1) + ?Q:in(#message{topic = <<"t1">>, qos = 1, payload = iolist_to_binary([I])}, AccQ), + ?Q:in(#message{topic = <<"t">>, qos = 1, payload = iolist_to_binary([I])}, AccQ1) end, Q, lists:seq(1, 255)), ?assertEqual(510, ?Q:len(Qx)), ?assertEqual([{len, 510}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)). @@ -149,10 +149,10 @@ t_priority_mqueue2(_) -> {store_qos0, false}], Q = ?Q:new("priority_queue2_test", Opts, alarm_fun()), ?assertEqual(2, ?Q:max_len(Q)), - Q1 = ?Q:in(#mqtt_message{topic = <<"x">>, qos = 1, payload = <<1>>}, Q), - Q2 = ?Q:in(#mqtt_message{topic = <<"x">>, qos = 1, payload = <<2>>}, Q1), - Q3 = ?Q:in(#mqtt_message{topic = <<"y">>, qos = 1, payload = <<3>>}, Q2), - Q4 = ?Q:in(#mqtt_message{topic = <<"y">>, qos = 1, payload = <<4>>}, Q3), + 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), ?assertEqual(4, ?Q:len(Q4)), {{value, _Val}, Q5} = ?Q:out(Q4), ?assertEqual(3, ?Q:len(Q5)). From a90403197905a3eba2c7584efdc68bc73c555bad Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Sat, 18 Aug 2018 19:47:26 +0800 Subject: [PATCH 115/520] acl cache using proc_dict --- etc/emqx.conf | 18 ++- priv/emqx.schema | 21 +-- src/emqx_access_control.erl | 232 +++++++++++++++++++++++++++++++--- src/emqx_protocol.erl | 1 - test/emqx_access_SUITE.erl | 246 +++++++++++++++++++++++++++++++----- 5 files changed, 445 insertions(+), 73 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 4703f5083..33c06b5d3 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -434,16 +434,21 @@ acl_nomatch = allow ## Value: File Name acl_file = {{ platform_etc_dir }}/acl.conf -## Whether to enable ACL cache for publish. +## The ACL cache size +## The maximum count of ACL entries allowed for a client. +## Value 0 disables ACL cache ## -## Value: on | off -enable_acl_cache = on +## Value: Integer +## Default: 100 +acl_cache_size = 100 -## The ACL cache age. +## The ACL cache time-to-live. +## The time after which an ACL cache entry will be invalid ## ## Value: Duration -## Default: 5 minute -acl_cache_age = 5m +## Default: 1 minute +acl_cache_ttl = 1m + ##-------------------------------------------------------------------- ## MQTT Protocol @@ -1875,4 +1880,3 @@ sysmon.busy_port = false ## ## Value: true | false sysmon.busy_dist_port = true - diff --git a/priv/emqx.schema b/priv/emqx.schema index a0d2bc0e2..f26c012ce 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -571,23 +571,17 @@ end}. hidden ]}. -%% @doc Enable ACL cache for publish. -{mapping, "enable_acl_cache", "emqx.enable_acl_cache", [ - {default, on}, - {datatype, flag} -]}. - -%% @doc ACL cache age. -{mapping, "acl_cache_age", "emqx.acl_cache_age", [ - {default, "5m"}, +%% @doc ACL cache time-to-live. +{mapping, "acl_cache_ttl", "emqx.acl_cache_ttl", [ + {default, "1m"}, {datatype, {duration, ms}} ]}. %% @doc ACL cache size. -%% {mapping, "acl_cache_size", "emqx.acl_cache_size", [ -%% {default, 0}, -%% {datatype, integer} -%% ]}. +{mapping, "acl_cache_size", "emqx.acl_cache_size", [ + {default, 100}, + {datatype, integer} +]}. %%-------------------------------------------------------------------- %% MQTT Protocol @@ -1703,4 +1697,3 @@ end}. {busy_port, cuttlefish:conf_get("sysmon.busy_port", Conf)}, {busy_dist_port, cuttlefish:conf_get("sysmon.busy_dist_port", Conf)}] end}. - diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index f43309088..5b52312ef 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -21,10 +21,18 @@ -export([start_link/0]). -export([authenticate/2]). -export([check_acl/3, reload_acl/0, lookup_mods/1]). --export([clean_acl_cache/1, clean_acl_cache/2]). -export([register_mod/3, register_mod/4, unregister_mod/2]). -export([stop/0]). +-export([get_acl_cache/2, + put_acl_cache/3, + delete_acl_cache/2, + cleanup_acl_cache/0, + dump_acl_cache/0, + get_cache_size/0, + get_newest_key/0 + ]). + %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -33,6 +41,7 @@ -define(SERVER, ?MODULE). -type(password() :: undefined | binary()). +-type(acl_result() :: allow | deny). -record(state, {}). @@ -82,16 +91,19 @@ authenticate(Client, Password, [{Mod, State, _Seq} | Mods]) -> %% @doc Check ACL -spec(check_acl(client(), pubsub(), topic()) -> allow | deny). check_acl(Client, PubSub, Topic) when ?PS(PubSub) -> - check_acl(Client, PubSub, Topic, lookup_mods(acl)). + CacheEnabled = (get_cache_max_size() =/= 0), + check_acl(Client, PubSub, Topic, lookup_mods(acl), CacheEnabled). -check_acl(#client{zone = Zone}, _PubSub, _Topic, []) -> - emqx_zone:get_env(Zone, acl_nomatch, deny); - -check_acl(Client, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) -> - case Mod:check_acl({Client, PubSub, Topic}, State) of - allow -> allow; - deny -> deny; - ignore -> check_acl(Client, PubSub, Topic, AclMods) +check_acl(Client, PubSub, Topic, AclMods, false) -> + check_acl_from_plugins(Client, PubSub, Topic, AclMods); +check_acl(Client, PubSub, Topic, AclMods, true) -> + case get_acl_cache(PubSub, Topic) of + not_found -> + AclResult = check_acl_from_plugins(Client, PubSub, Topic, AclMods), + put_acl_cache(PubSub, Topic, AclResult), + AclResult; + AclResult -> + AclResult end. %% @doc Reload ACL Rules. @@ -130,12 +142,6 @@ tab_key(acl) -> acl_modules. stop() -> gen_server:stop(?MODULE, normal, infinity). -%%TODO: Support ACL cache... -clean_acl_cache(_ClientId) -> - ok. -clean_acl_cache(_ClientId, _Topic) -> - ok. - %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- @@ -193,6 +199,15 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +check_acl_from_plugins(#client{zone = Zone}, _PubSub, _Topic, []) -> + emqx_zone:get_env(Zone, acl_nomatch, deny); +check_acl_from_plugins(Client, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) -> + case Mod:check_acl({Client, PubSub, Topic}, State) of + allow -> allow; + deny -> deny; + ignore -> check_acl_from_plugins(Client, PubSub, Topic, AclMods) + end. + %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- @@ -202,3 +217,188 @@ if_existed(false, Fun) -> if_existed(_Mod, _Fun) -> {error, already_existed}. +%%-------------------------------------------------------------------- +%% ACL cache +%%-------------------------------------------------------------------- + +-spec(get_acl_cache(PubSub :: publish | subscribe, Topic :: topic()) + -> (acl_result() | not_found)). +get_acl_cache(PubSub, Topic) -> + case erlang:get({PubSub, Topic}) of + undefined -> not_found; + {AclResult, CachedAt, _NextK, _PrevK} -> + if_acl_cache_expired(CachedAt, + fun(false) -> + AclResult; + (true) -> + %% this expired entry will get updated in + %% put_acl_cache/3 + not_found + end) + end. + +-spec(put_acl_cache(PubSub :: publish | subscribe, + Topic :: topic(), AclResult :: acl_result()) -> ok). +put_acl_cache(PubSub, Topic, AclResult) -> + MaxSize = get_cache_max_size(), true = (MaxSize =/= 0), + Size = get_cache_size(), + if + Size =:= 0 -> + create_first(PubSub, Topic, AclResult); + Size < MaxSize -> + append(PubSub, Topic, AclResult); + Size =:= MaxSize -> + %% when the cache get full, and also the latest one + %% is expired, we'll perform a cleanup. + NewestK = get_newest_key(), + {_AclResult, CachedAt, OldestK, _PrevK} = erlang:get(NewestK), + if_acl_cache_expired(CachedAt, + fun(true) -> + % try to cleanup first + cleanup_acl_cache(OldestK), + add_cache(PubSub, Topic, AclResult); + (false) -> + % cache full, perform cache replacement + delete_acl_cache(OldestK), + append(PubSub, Topic, AclResult) + end) + end. + +-spec(delete_acl_cache(PubSub :: publish | subscribe, Topic :: topic()) -> ok). +delete_acl_cache(PubSub, Topic) -> + delete_acl_cache(_K = {PubSub, Topic}). +delete_acl_cache(K) -> + case erlang:get(K) of + undefined -> ok; + {_AclResult, _CachedAt, NextK, PrevK} when NextK =:= PrevK -> + %% there is only one entry in the cache + erlang:erase(K), + decr_cache_size(), + set_newest_key(undefined); + {_AclResult, _CachedAt, NextK, PrevK} -> + update_next(PrevK, NextK), + update_prev(NextK, PrevK), + erlang:erase(K), + + decr_cache_size(), + NewestK = get_newest_key(), + if + K =:= NewestK -> set_newest_key(NextK); + true -> ok + end + end. + +%% evict all the exipired cache entries +-spec(cleanup_acl_cache() -> ok). +cleanup_acl_cache() -> + case get_newest_key() of + undefined -> ok; + NewestK -> + {_AclResult, _CachedAt, OldestK, _PrevK} = erlang:get(NewestK), + cleanup_acl_cache(OldestK) + end. +cleanup_acl_cache(FromK) -> + case erlang:get(FromK) of + undefined -> ok; + {_AclResult, CachedAt, NextK, _PrevK} -> + if_acl_cache_expired(CachedAt, + fun(false) -> + ok; + (true) -> + delete_acl_cache(FromK), + cleanup_acl_cache(NextK) + end) + end. + +%% for test only +dump_acl_cache() -> + [R || R = {{SubPub, _T}, _Acl} <- get(), SubPub =:= publish + orelse SubPub =:= subscribe]. + +add_cache(PubSub, Topic, AclResult) -> + Size = get_cache_size(), + MaxSize = get_cache_max_size(), true = (MaxSize =/= 0), + if + Size =:= 0 -> + create_first(PubSub, Topic, AclResult); + Size =:= MaxSize -> + OldestK = get_next_key(get_newest_key()), + delete_acl_cache(OldestK), + case get_cache_size() =:= 0 of + true -> create_first(PubSub, Topic, AclResult); + false -> append(PubSub, Topic, AclResult) + end; + true -> + append(PubSub, Topic, AclResult) + end. + +create_first(PubSub, Topic, AclResult) -> + K = cache_k(PubSub, Topic), + V = cache_v(AclResult, _NextK = K, _PrevK = K), + erlang:put(K, V), + set_cache_size(1), + set_newest_key(K). + +append(PubSub, Topic, AclResult) -> + %% try to update the existing one: + %% - we delete it and then append it at the tail + delete_acl_cache(PubSub, Topic), + + case get_cache_size() =:= 0 of + true -> create_first(PubSub, Topic, AclResult); + false -> + NewestK = get_newest_key(), + OldestK = get_next_key(NewestK), + K = cache_k(PubSub, Topic), + V = cache_v(AclResult, OldestK, NewestK), + erlang:put(K, V), + + update_next(NewestK, K), + update_prev(OldestK, K), + incr_cache_size(), + set_newest_key(K) + end. + +get_next_key(K) -> + erlang:element(3, erlang:get(K)). +update_next(K, NextK) -> + NoNext = erlang:delete_element(3, erlang:get(K)), + erlang:put(K, erlang:insert_element(3, NoNext, NextK)). +update_prev(K, PrevK) -> + NoPrev = erlang:delete_element(4, erlang:get(K)), + erlang:put(K, erlang:insert_element(4, NoPrev, PrevK)). + +cache_k(PubSub, Topic)-> {PubSub, Topic}. +cache_v(AclResult, NextK, PrevK)-> {AclResult, time_now(), NextK, PrevK}. + +get_cache_max_size() -> + application:get_env(emqx, acl_cache_size, 100). + +get_cache_size() -> + case erlang:get(acl_cache_size) of + undefined -> 0; + Size -> Size + end. +incr_cache_size() -> + erlang:put(acl_cache_size, get_cache_size() + 1), ok. +decr_cache_size() -> + erlang:put(acl_cache_size, get_cache_size() - 1), ok. +set_cache_size(N) -> + erlang:put(acl_cache_size, N), ok. + +get_newest_key() -> + erlang:get(acl_cache_newest_key). + +set_newest_key(Key) -> + erlang:put(acl_cache_newest_key, Key), ok. + +time_now() -> erlang:system_time(millisecond). + +if_acl_cache_expired(CachedAt, Fun) -> + TTL = application:get_env(emqx, acl_cache_ttl, 60000), + Now = time_now(), + if (CachedAt + TTL) =< Now -> + Fun(true); + true -> + Fun(false) + end. diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 3faa7781a..941baaa7d 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -662,4 +662,3 @@ feed_var({<<"%u">>, Username}, MountPoint) -> sp(true) -> 1; sp(false) -> 0. - diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index f206cca05..f2ca9fad6 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -29,27 +29,54 @@ all() -> [{group, access_control}, - {group, access_rule}]. + {group, acl_cache}, + {group, access_control_cache_mode}, + {group, access_rule} + ]. groups() -> [{access_control, [sequence], - [reload_acl, - register_mod, - unregister_mod, - check_acl]}, + [reload_acl, + register_mod, + unregister_mod, + 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_full_replacement, + cache_cleanup, + cache_full_cleanup + ]}, {access_rule, [], - [compile_rule, - match_rule]}]. + [compile_rule, + match_rule]}]. -init_per_group(access_control, Config) -> +init_per_group(Group, Config) when Group =:= access_control; + Group =:= access_control_cache_mode -> + prepare_config(Group), application:load(emqx), - prepare_config(), Config; - init_per_group(_Group, Config) -> Config. -prepare_config() -> +prepare_config(Group = access_control) -> + set_acl_config_file(Group), + application:set_env(emqx, acl_cache_size, 0); +prepare_config(Group = access_control_cache_mode) -> + set_acl_config_file(Group), + application:set_env(emqx, acl_cache_size, 100). + +set_acl_config_file(_Group) -> Rules = [{allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]}, {allow, {user, "testuser"}, subscribe, ["a/b/c", "d/e/f/#"]}, {allow, {user, "admin"}, pubsub, ["a/b/c", "d/e/f/#"]}, @@ -59,8 +86,8 @@ prepare_config() -> {deny, all, subscribe, ["$SYS/#", "#"]}, {deny, all}], write_config("access_SUITE_acl.conf", Rules), - application:set_env(emqx, acl_file, "access_SUITE_acl.conf"), - application:set_env(emqx, acl_cache, false). + application:set_env(emqx, acl_file, "access_SUITE_acl.conf"). + write_config(Filename, Terms) -> file:write_file(Filename, [io_lib:format("~tp.~n", [Term]) || Term <- Terms]). @@ -68,24 +95,18 @@ write_config(Filename, Terms) -> end_per_group(_Group, Config) -> Config. -init_per_testcase(TestCase, Config) when TestCase =:= reload_acl; - TestCase =:= register_mod; - TestCase =:= unregister_mod; - TestCase =:= check_acl -> - {ok, _Pid} = ?AC:start_link(), Config; - init_per_testcase(_TestCase, Config) -> + {ok, _Pid} = ?AC:start_link(), Config. - -end_per_testcase(TestCase, _Config) when TestCase =:= reload_acl; - TestCase =:= register_mod; - TestCase =:= unregister_mod; - TestCase =:= check_acl -> - ?AC:stop(); - end_per_testcase(_TestCase, _Config) -> ok. +per_testcase_config(acl_cache_full, Config) -> + Config; +per_testcase_config(_TestCase, Config) -> + Config. + + %%-------------------------------------------------------------------- %% emqx_access_control %%-------------------------------------------------------------------- @@ -115,15 +136,170 @@ unregister_mod(_) -> timer:sleep(5), [] = ?AC:lookup_mods(auth). -check_acl(_) -> - User1 = #client{id = <<"client1">>, username = <<"testuser">>}, - User2 = #client{id = <<"client2">>, username = <<"xyz">>}, - allow = ?AC:check_acl(User1, subscribe, <<"users/testuser/1">>), - allow = ?AC:check_acl(User1, subscribe, <<"clients/client1">>), - deny = ?AC:check_acl(User1, subscribe, <<"clients/client1/x/y">>), - allow = ?AC:check_acl(User1, publish, <<"users/testuser/1">>), - allow = ?AC:check_acl(User1, subscribe, <<"a/b/c">>), - deny = ?AC:check_acl(User2, subscribe, <<"a/b/c">>). +check_acl_1(_) -> + SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, + 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">>}, + deny = ?AC:check_acl(SelfUser, subscribe, <<"a/b/c">>). + +acl_cache_basic(_) -> + SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, + not_found = ?AC:get_acl_cache(subscribe, <<"users/testuser/1">>), + not_found = ?AC: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:get_acl_cache(subscribe, <<"users/testuser/1">>), + allow = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), + ok. + +acl_cache_expiry(_) -> + application:set_env(emqx, acl_cache_ttl, 1000), + + SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, + allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), + allow = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), + ct:sleep(1100), + not_found = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), + ok. + +acl_cache_full(_) -> + application:set_env(emqx, acl_cache_size, 1), + + SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, + allow = ?AC:check_acl(SelfUser, subscribe, <<"users/testuser/1">>), + allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), + + %% the older ones (the <<"users/testuser/1">>) will be evicted first + not_found = ?AC:get_acl_cache(subscribe, <<"users/testuser/1">>), + allow = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), + ok. + +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, 1000), + application:set_env(emqx, acl_cache_size, 2), + + SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, + allow = ?AC:check_acl(SelfUser, subscribe, <<"users/testuser/1">>), + allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), + + allow = ?AC:get_acl_cache(subscribe, <<"users/testuser/1">>), + allow = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), + + ct:sleep(1100), + %% now the cache is full and the newest one - "clients/client1" + %% should be expired, so we'll try to cleanup before putting the next cache entry + deny = ?AC:check_acl(SelfUser, subscribe, <<"#">>), + + not_found = ?AC:get_acl_cache(subscribe, <<"users/testuser/1">>), + not_found = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), + deny = ?AC:get_acl_cache(subscribe, <<"#">>), + ok. + +put_get_del_cache(_) -> + application:set_env(emqx, acl_cache_ttl, 300000), + application:set_env(emqx, acl_cache_size, 30), + + not_found = ?AC:get_acl_cache(publish, <<"a">>), + ok = ?AC:put_acl_cache(publish, <<"a">>, allow), + allow = ?AC:get_acl_cache(publish, <<"a">>), + + not_found = ?AC:get_acl_cache(subscribe, <<"b">>), + ok = ?AC:put_acl_cache(subscribe, <<"b">>, deny), + deny = ?AC:get_acl_cache(subscribe, <<"b">>), + + 2 = ?AC:get_cache_size(), + {subscribe, <<"b">>} = ?AC:get_newest_key(). + +cache_expiry(_) -> + application:set_env(emqx, acl_cache_ttl, 1000), + application:set_env(emqx, acl_cache_size, 30), + ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), + allow = ?AC:get_acl_cache(subscribe, <<"a">>), + + ct:sleep(1100), + not_found = ?AC:get_acl_cache(subscribe, <<"a">>), + + ok = ?AC:put_acl_cache(subscribe, <<"a">>, deny), + deny = ?AC:get_acl_cache(subscribe, <<"a">>), + + ct:sleep(1100), + not_found = ?AC:get_acl_cache(subscribe, <<"a">>). + +cache_update(_) -> + application:set_env(emqx, acl_cache_ttl, 300000), + application:set_env(emqx, acl_cache_size, 30), + [] = ?AC:dump_acl_cache(), + + ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), + ok = ?AC:put_acl_cache(publish, <<"b">>, allow), + ok = ?AC:put_acl_cache(publish, <<"c">>, allow), + 3 = ?AC:get_cache_size(), + {publish, <<"c">>} = ?AC:get_newest_key(), + + %% update the 2nd one + ok = ?AC:put_acl_cache(publish, <<"b">>, allow), + %ct:pal("dump acl cache: ~p~n", [?AC:dump_acl_cache()]), + + 3 = ?AC:get_cache_size(), + {publish, <<"b">>} = ?AC:get_newest_key(). + +cache_full_replacement(_) -> + application:set_env(emqx, acl_cache_ttl, 300000), + application:set_env(emqx, acl_cache_size, 3), + ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), + ok = ?AC:put_acl_cache(publish, <<"b">>, allow), + ok = ?AC:put_acl_cache(publish, <<"c">>, allow), + allow = ?AC:get_acl_cache(subscribe, <<"a">>), + allow = ?AC:get_acl_cache(publish, <<"b">>), + allow = ?AC:get_acl_cache(publish, <<"c">>), + 3 = ?AC:get_cache_size(), + {publish, <<"c">>} = ?AC:get_newest_key(), + + ok = ?AC:put_acl_cache(publish, <<"d">>, deny), + 3 = ?AC:get_cache_size(), + {publish, <<"d">>} = ?AC:get_newest_key(), + + ok = ?AC:put_acl_cache(publish, <<"e">>, deny), + 3 = ?AC:get_cache_size(), + {publish, <<"e">>} = ?AC:get_newest_key(), + + not_found = ?AC:get_acl_cache(subscribe, <<"a">>), + not_found = ?AC:get_acl_cache(publish, <<"b">>), + allow = ?AC:get_acl_cache(publish, <<"c">>). + +cache_cleanup(_) -> + application:set_env(emqx, acl_cache_ttl, 1000), + application:set_env(emqx, acl_cache_size, 30), + ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), + ok = ?AC:put_acl_cache(publish, <<"b">>, allow), + ok = ?AC:put_acl_cache(publish, <<"c">>, allow), + 3 = ?AC:get_cache_size(), + + ct:sleep(1100), + ?AC:cleanup_acl_cache(), + 0 = ?AC:get_cache_size(). + +cache_full_cleanup(_) -> + application:set_env(emqx, acl_cache_ttl, 1000), + application:set_env(emqx, acl_cache_size, 3), + ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), + ok = ?AC:put_acl_cache(publish, <<"b">>, allow), + ok = ?AC:put_acl_cache(publish, <<"c">>, allow), + 3 = ?AC:get_cache_size(), + + ct:sleep(1100), + %% verify auto cleanup upon cache full + ok = ?AC:put_acl_cache(subscribe, <<"d">>, deny), + 1 = ?AC:get_cache_size(). %%-------------------------------------------------------------------- %% emqx_access_rule From 8cd20744bed0ebf4c1ed19587a7d0106e55d0164 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Sun, 26 Aug 2018 12:40:23 +0800 Subject: [PATCH 116/520] improve cache datastruct using keys-queue --- etc/emqx.conf | 5 +- priv/emqx.schema | 4 +- src/emqx_access_control.erl | 218 +++++++++++++++++------------------- test/emqx_access_SUITE.erl | 33 +++--- 4 files changed, 123 insertions(+), 137 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 33c06b5d3..62f94b174 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -436,11 +436,12 @@ acl_file = {{ platform_etc_dir }}/acl.conf ## The ACL cache size ## The maximum count of ACL entries allowed for a client. +## ## Value 0 disables ACL cache ## ## Value: Integer -## Default: 100 -acl_cache_size = 100 +## Default: 32 +acl_cache_max_size = 32 ## The ACL cache time-to-live. ## The time after which an ACL cache entry will be invalid diff --git a/priv/emqx.schema b/priv/emqx.schema index f26c012ce..f6ee7c621 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -578,8 +578,8 @@ end}. ]}. %% @doc ACL cache size. -{mapping, "acl_cache_size", "emqx.acl_cache_size", [ - {default, 100}, +{mapping, "acl_cache_max_size", "emqx.acl_cache_max_size", [ + {default, 32}, {datatype, integer} ]}. diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 5b52312ef..07999b0fb 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -26,11 +26,13 @@ -export([get_acl_cache/2, put_acl_cache/3, - delete_acl_cache/2, cleanup_acl_cache/0, dump_acl_cache/0, get_cache_size/0, - get_newest_key/0 + get_newest_key/0, + get_oldest_key/0, + cache_k/2, + cache_v/1 ]). %% gen_server callbacks @@ -221,158 +223,124 @@ if_existed(_Mod, _Fun) -> %% ACL cache %%-------------------------------------------------------------------- +%% We'll cleanup the cache before repalcing an expired acl. -spec(get_acl_cache(PubSub :: publish | subscribe, Topic :: topic()) -> (acl_result() | not_found)). get_acl_cache(PubSub, Topic) -> - case erlang:get({PubSub, Topic}) of + case erlang:get(cache_k(PubSub, Topic)) of undefined -> not_found; - {AclResult, CachedAt, _NextK, _PrevK} -> - if_acl_cache_expired(CachedAt, + {AclResult, CachedAt} -> + if_expired(CachedAt, fun(false) -> AclResult; (true) -> - %% this expired entry will get updated in - %% put_acl_cache/3 + cleanup_acl_cache(), not_found end) end. +%% If the cache get full, and also the latest one +%% is expired, then delete all the cache entries -spec(put_acl_cache(PubSub :: publish | subscribe, Topic :: topic(), AclResult :: acl_result()) -> ok). put_acl_cache(PubSub, Topic, AclResult) -> MaxSize = get_cache_max_size(), true = (MaxSize =/= 0), Size = get_cache_size(), if - Size =:= 0 -> - create_first(PubSub, Topic, AclResult); Size < MaxSize -> - append(PubSub, Topic, AclResult); + add_acl_cache(PubSub, Topic, AclResult); Size =:= MaxSize -> - %% when the cache get full, and also the latest one - %% is expired, we'll perform a cleanup. NewestK = get_newest_key(), - {_AclResult, CachedAt, OldestK, _PrevK} = erlang:get(NewestK), - if_acl_cache_expired(CachedAt, + {_AclResult, CachedAt} = erlang:get(NewestK), + if_expired(CachedAt, fun(true) -> - % try to cleanup first - cleanup_acl_cache(OldestK), - add_cache(PubSub, Topic, AclResult); + % all cache expired, cleanup first + empty_acl_cache(), + add_acl_cache(PubSub, Topic, AclResult); (false) -> % cache full, perform cache replacement - delete_acl_cache(OldestK), - append(PubSub, Topic, AclResult) + evict_acl_cache(), + add_acl_cache(PubSub, Topic, AclResult) end) end. --spec(delete_acl_cache(PubSub :: publish | subscribe, Topic :: topic()) -> ok). -delete_acl_cache(PubSub, Topic) -> - delete_acl_cache(_K = {PubSub, Topic}). -delete_acl_cache(K) -> - case erlang:get(K) of - undefined -> ok; - {_AclResult, _CachedAt, NextK, PrevK} when NextK =:= PrevK -> - %% there is only one entry in the cache - erlang:erase(K), - decr_cache_size(), - set_newest_key(undefined); - {_AclResult, _CachedAt, NextK, PrevK} -> - update_next(PrevK, NextK), - update_prev(NextK, PrevK), - erlang:erase(K), +empty_acl_cache() -> + map_acl_cache(fun({CacheK, _CacheV}) -> + erlang:erase(CacheK) + end), + set_cache_size(0), + set_keys_queue(queue:new()). - decr_cache_size(), - NewestK = get_newest_key(), - if - K =:= NewestK -> set_newest_key(NextK); - true -> ok - end +evict_acl_cache() -> + {{value, OldestK}, RemKeys} = queue:out(get_keys_queue()), + set_keys_queue(RemKeys), + erlang:erase(OldestK), + decr_cache_size(). + +add_acl_cache(PubSub, Topic, AclResult) -> + K = cache_k(PubSub, Topic), + V = cache_v(AclResult), + case get(K) of + undefined -> add_new_acl(K, V); + {_AclResult, _CachedAt} -> + update_acl(K, V) end. -%% evict all the exipired cache entries +add_new_acl(K, V) -> + erlang:put(K, V), + keys_queue_in(K), + incr_cache_size(). + +update_acl(K, V) -> + erlang:put(K, V), + keys_queue_update(K). + +%% cleanup all the exipired cache entries -spec(cleanup_acl_cache() -> ok). cleanup_acl_cache() -> - case get_newest_key() of - undefined -> ok; - NewestK -> - {_AclResult, _CachedAt, OldestK, _PrevK} = erlang:get(NewestK), - cleanup_acl_cache(OldestK) - end. -cleanup_acl_cache(FromK) -> - case erlang:get(FromK) of - undefined -> ok; - {_AclResult, CachedAt, NextK, _PrevK} -> - if_acl_cache_expired(CachedAt, - fun(false) -> - ok; + set_keys_queue( + cleanup_acl_cache(get_keys_queue())). + +cleanup_acl_cache(KeysQ) -> + case queue:out(KeysQ) of + {{value, OldestK}, RemKeys} -> + {_AclResult, CachedAt} = erlang:get(OldestK), + if_expired(CachedAt, + fun(false) -> KeysQ; (true) -> - delete_acl_cache(FromK), - cleanup_acl_cache(NextK) - end) + erlang:erase(OldestK), + decr_cache_size(), + cleanup_acl_cache(RemKeys) + end); + {empty, KeysQ} -> KeysQ + end. + +get_newest_key() -> + get_key(fun(KeysQ) -> queue:get_r(KeysQ) end). + +get_oldest_key() -> + get_key(fun(KeysQ) -> queue:get(KeysQ) end). + +get_key(Pick) -> + KeysQ = get_keys_queue(), + case queue:is_empty(KeysQ) of + true -> undefined; + false -> Pick(KeysQ) end. %% for test only dump_acl_cache() -> - [R || R = {{SubPub, _T}, _Acl} <- get(), SubPub =:= publish + map_acl_cache(fun(Cache) -> Cache end). +map_acl_cache(Fun) -> + [Fun(R) || R = {{SubPub, _T}, _Acl} <- get(), SubPub =:= publish orelse SubPub =:= subscribe]. -add_cache(PubSub, Topic, AclResult) -> - Size = get_cache_size(), - MaxSize = get_cache_max_size(), true = (MaxSize =/= 0), - if - Size =:= 0 -> - create_first(PubSub, Topic, AclResult); - Size =:= MaxSize -> - OldestK = get_next_key(get_newest_key()), - delete_acl_cache(OldestK), - case get_cache_size() =:= 0 of - true -> create_first(PubSub, Topic, AclResult); - false -> append(PubSub, Topic, AclResult) - end; - true -> - append(PubSub, Topic, AclResult) - end. - -create_first(PubSub, Topic, AclResult) -> - K = cache_k(PubSub, Topic), - V = cache_v(AclResult, _NextK = K, _PrevK = K), - erlang:put(K, V), - set_cache_size(1), - set_newest_key(K). - -append(PubSub, Topic, AclResult) -> - %% try to update the existing one: - %% - we delete it and then append it at the tail - delete_acl_cache(PubSub, Topic), - - case get_cache_size() =:= 0 of - true -> create_first(PubSub, Topic, AclResult); - false -> - NewestK = get_newest_key(), - OldestK = get_next_key(NewestK), - K = cache_k(PubSub, Topic), - V = cache_v(AclResult, OldestK, NewestK), - erlang:put(K, V), - - update_next(NewestK, K), - update_prev(OldestK, K), - incr_cache_size(), - set_newest_key(K) - end. - -get_next_key(K) -> - erlang:element(3, erlang:get(K)). -update_next(K, NextK) -> - NoNext = erlang:delete_element(3, erlang:get(K)), - erlang:put(K, erlang:insert_element(3, NoNext, NextK)). -update_prev(K, PrevK) -> - NoPrev = erlang:delete_element(4, erlang:get(K)), - erlang:put(K, erlang:insert_element(4, NoPrev, PrevK)). cache_k(PubSub, Topic)-> {PubSub, Topic}. -cache_v(AclResult, NextK, PrevK)-> {AclResult, time_now(), NextK, PrevK}. +cache_v(AclResult)-> {AclResult, time_now()}. get_cache_max_size() -> - application:get_env(emqx, acl_cache_size, 100). + application:get_env(emqx, acl_cache_max_size, 0). get_cache_size() -> case erlang:get(acl_cache_size) of @@ -386,15 +354,31 @@ decr_cache_size() -> set_cache_size(N) -> erlang:put(acl_cache_size, N), ok. -get_newest_key() -> - erlang:get(acl_cache_newest_key). +keys_queue_in(Key) -> + %% delete the key first if exists + KeysQ = get_keys_queue(), + set_keys_queue(queue:in(Key, KeysQ)). -set_newest_key(Key) -> - erlang:put(acl_cache_newest_key, Key), ok. +keys_queue_update(Key) -> + NewKeysQ = remove_key(Key, get_keys_queue()), + set_keys_queue(queue:in(Key, NewKeysQ)). + +remove_key(Key, KeysQ) -> + queue:filter(fun + (K) when K =:= Key -> false; (_) -> true + end, KeysQ). + +set_keys_queue(KeysQ) -> + erlang:put(acl_keys_q, KeysQ), ok. +get_keys_queue() -> + case erlang:get(acl_keys_q) of + undefined -> queue:new(); + KeysQ -> KeysQ + end. time_now() -> erlang:system_time(millisecond). -if_acl_cache_expired(CachedAt, Fun) -> +if_expired(CachedAt, Fun) -> TTL = application:get_env(emqx, acl_cache_ttl, 60000), Now = time_now(), if (CachedAt + TTL) =< Now -> diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index f2ca9fad6..c3907c2db 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -22,6 +22,7 @@ -include("emqx.hrl"). -include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). -define(AC, emqx_access_control). @@ -71,10 +72,10 @@ init_per_group(_Group, Config) -> prepare_config(Group = access_control) -> set_acl_config_file(Group), - application:set_env(emqx, acl_cache_size, 0); + application:set_env(emqx, acl_cache_max_size, 0); prepare_config(Group = access_control_cache_mode) -> set_acl_config_file(Group), - application:set_env(emqx, acl_cache_size, 100). + application:set_env(emqx, acl_cache_max_size, 100). set_acl_config_file(_Group) -> Rules = [{allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]}, @@ -170,7 +171,7 @@ acl_cache_expiry(_) -> ok. acl_cache_full(_) -> - application:set_env(emqx, acl_cache_size, 1), + application:set_env(emqx, acl_cache_max_size, 1), SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, allow = ?AC:check_acl(SelfUser, subscribe, <<"users/testuser/1">>), @@ -185,7 +186,7 @@ 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, 1000), - application:set_env(emqx, acl_cache_size, 2), + application:set_env(emqx, acl_cache_max_size, 2), SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, allow = ?AC:check_acl(SelfUser, subscribe, <<"users/testuser/1">>), @@ -206,7 +207,7 @@ acl_cache_cleanup(_) -> put_get_del_cache(_) -> application:set_env(emqx, acl_cache_ttl, 300000), - application:set_env(emqx, acl_cache_size, 30), + application:set_env(emqx, acl_cache_max_size, 30), not_found = ?AC:get_acl_cache(publish, <<"a">>), ok = ?AC:put_acl_cache(publish, <<"a">>, allow), @@ -217,11 +218,11 @@ put_get_del_cache(_) -> deny = ?AC:get_acl_cache(subscribe, <<"b">>), 2 = ?AC:get_cache_size(), - {subscribe, <<"b">>} = ?AC:get_newest_key(). + ?assertEqual(?AC:cache_k(subscribe, <<"b">>), ?AC:get_newest_key()). cache_expiry(_) -> application:set_env(emqx, acl_cache_ttl, 1000), - application:set_env(emqx, acl_cache_size, 30), + application:set_env(emqx, acl_cache_max_size, 30), ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), allow = ?AC:get_acl_cache(subscribe, <<"a">>), @@ -236,25 +237,25 @@ cache_expiry(_) -> cache_update(_) -> application:set_env(emqx, acl_cache_ttl, 300000), - application:set_env(emqx, acl_cache_size, 30), + application:set_env(emqx, acl_cache_max_size, 30), [] = ?AC:dump_acl_cache(), ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), ok = ?AC:put_acl_cache(publish, <<"b">>, allow), ok = ?AC:put_acl_cache(publish, <<"c">>, allow), 3 = ?AC:get_cache_size(), - {publish, <<"c">>} = ?AC:get_newest_key(), + ?assertEqual(?AC:cache_k(publish, <<"c">>), ?AC:get_newest_key()), %% update the 2nd one ok = ?AC:put_acl_cache(publish, <<"b">>, allow), %ct:pal("dump acl cache: ~p~n", [?AC:dump_acl_cache()]), 3 = ?AC:get_cache_size(), - {publish, <<"b">>} = ?AC:get_newest_key(). + ?assertEqual(?AC:cache_k(publish, <<"b">>), ?AC:get_newest_key()). cache_full_replacement(_) -> application:set_env(emqx, acl_cache_ttl, 300000), - application:set_env(emqx, acl_cache_size, 3), + application:set_env(emqx, acl_cache_max_size, 3), ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), ok = ?AC:put_acl_cache(publish, <<"b">>, allow), ok = ?AC:put_acl_cache(publish, <<"c">>, allow), @@ -262,15 +263,15 @@ cache_full_replacement(_) -> allow = ?AC:get_acl_cache(publish, <<"b">>), allow = ?AC:get_acl_cache(publish, <<"c">>), 3 = ?AC:get_cache_size(), - {publish, <<"c">>} = ?AC:get_newest_key(), + ?assertEqual(?AC:cache_k(publish, <<"c">>), ?AC:get_newest_key()), ok = ?AC:put_acl_cache(publish, <<"d">>, deny), 3 = ?AC:get_cache_size(), - {publish, <<"d">>} = ?AC:get_newest_key(), + ?assertEqual(?AC:cache_k(publish, <<"d">>), ?AC:get_newest_key()), ok = ?AC:put_acl_cache(publish, <<"e">>, deny), 3 = ?AC:get_cache_size(), - {publish, <<"e">>} = ?AC:get_newest_key(), + ?assertEqual(?AC:cache_k(publish, <<"e">>), ?AC:get_newest_key()), not_found = ?AC:get_acl_cache(subscribe, <<"a">>), not_found = ?AC:get_acl_cache(publish, <<"b">>), @@ -278,7 +279,7 @@ cache_full_replacement(_) -> cache_cleanup(_) -> application:set_env(emqx, acl_cache_ttl, 1000), - application:set_env(emqx, acl_cache_size, 30), + application:set_env(emqx, acl_cache_max_size, 30), ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), ok = ?AC:put_acl_cache(publish, <<"b">>, allow), ok = ?AC:put_acl_cache(publish, <<"c">>, allow), @@ -290,7 +291,7 @@ cache_cleanup(_) -> cache_full_cleanup(_) -> application:set_env(emqx, acl_cache_ttl, 1000), - application:set_env(emqx, acl_cache_size, 3), + application:set_env(emqx, acl_cache_max_size, 3), ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), ok = ?AC:put_acl_cache(publish, <<"b">>, allow), ok = ?AC:put_acl_cache(publish, <<"c">>, allow), From 9717f9b83e48db58172f55864b6b097a95b82199 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Sun, 26 Aug 2018 15:46:22 +0800 Subject: [PATCH 117/520] add module emqx_acl_cache --- src/emqx_access_control.erl | 186 +------------------------------- src/emqx_acl_cache.erl | 204 ++++++++++++++++++++++++++++++++++++ test/emqx_access_SUITE.erl | 133 +++++++++++------------ 3 files changed, 274 insertions(+), 249 deletions(-) create mode 100644 src/emqx_acl_cache.erl diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 07999b0fb..1c5d04b4e 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -24,17 +24,6 @@ -export([register_mod/3, register_mod/4, unregister_mod/2]). -export([stop/0]). --export([get_acl_cache/2, - put_acl_cache/3, - cleanup_acl_cache/0, - dump_acl_cache/0, - get_cache_size/0, - get_newest_key/0, - get_oldest_key/0, - cache_k/2, - cache_v/1 - ]). - %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -43,7 +32,6 @@ -define(SERVER, ?MODULE). -type(password() :: undefined | binary()). --type(acl_result() :: allow | deny). -record(state, {}). @@ -93,16 +81,16 @@ authenticate(Client, Password, [{Mod, State, _Seq} | Mods]) -> %% @doc Check ACL -spec(check_acl(client(), pubsub(), topic()) -> allow | deny). check_acl(Client, PubSub, Topic) when ?PS(PubSub) -> - CacheEnabled = (get_cache_max_size() =/= 0), + CacheEnabled = (emqx_acl_cache:get_cache_max_size() =/= 0), check_acl(Client, PubSub, Topic, lookup_mods(acl), CacheEnabled). check_acl(Client, PubSub, Topic, AclMods, false) -> check_acl_from_plugins(Client, PubSub, Topic, AclMods); check_acl(Client, PubSub, Topic, AclMods, true) -> - case get_acl_cache(PubSub, Topic) of + case emqx_acl_cache:get_acl_cache(PubSub, Topic) of not_found -> AclResult = check_acl_from_plugins(Client, PubSub, Topic, AclMods), - put_acl_cache(PubSub, Topic, AclResult), + emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult), AclResult; AclResult -> AclResult @@ -218,171 +206,3 @@ if_existed(false, Fun) -> Fun(); if_existed(_Mod, _Fun) -> {error, already_existed}. - -%%-------------------------------------------------------------------- -%% ACL cache -%%-------------------------------------------------------------------- - -%% We'll cleanup the cache before repalcing an expired acl. --spec(get_acl_cache(PubSub :: publish | subscribe, Topic :: topic()) - -> (acl_result() | not_found)). -get_acl_cache(PubSub, Topic) -> - case erlang:get(cache_k(PubSub, Topic)) of - undefined -> not_found; - {AclResult, CachedAt} -> - if_expired(CachedAt, - fun(false) -> - AclResult; - (true) -> - cleanup_acl_cache(), - not_found - end) - end. - -%% If the cache get full, and also the latest one -%% is expired, then delete all the cache entries --spec(put_acl_cache(PubSub :: publish | subscribe, - Topic :: topic(), AclResult :: acl_result()) -> ok). -put_acl_cache(PubSub, Topic, AclResult) -> - MaxSize = get_cache_max_size(), true = (MaxSize =/= 0), - Size = get_cache_size(), - if - Size < MaxSize -> - add_acl_cache(PubSub, Topic, AclResult); - Size =:= MaxSize -> - NewestK = get_newest_key(), - {_AclResult, CachedAt} = erlang:get(NewestK), - if_expired(CachedAt, - fun(true) -> - % all cache expired, cleanup first - empty_acl_cache(), - add_acl_cache(PubSub, Topic, AclResult); - (false) -> - % cache full, perform cache replacement - evict_acl_cache(), - add_acl_cache(PubSub, Topic, AclResult) - end) - end. - -empty_acl_cache() -> - map_acl_cache(fun({CacheK, _CacheV}) -> - erlang:erase(CacheK) - end), - set_cache_size(0), - set_keys_queue(queue:new()). - -evict_acl_cache() -> - {{value, OldestK}, RemKeys} = queue:out(get_keys_queue()), - set_keys_queue(RemKeys), - erlang:erase(OldestK), - decr_cache_size(). - -add_acl_cache(PubSub, Topic, AclResult) -> - K = cache_k(PubSub, Topic), - V = cache_v(AclResult), - case get(K) of - undefined -> add_new_acl(K, V); - {_AclResult, _CachedAt} -> - update_acl(K, V) - end. - -add_new_acl(K, V) -> - erlang:put(K, V), - keys_queue_in(K), - incr_cache_size(). - -update_acl(K, V) -> - erlang:put(K, V), - keys_queue_update(K). - -%% cleanup all the exipired cache entries --spec(cleanup_acl_cache() -> ok). -cleanup_acl_cache() -> - set_keys_queue( - cleanup_acl_cache(get_keys_queue())). - -cleanup_acl_cache(KeysQ) -> - case queue:out(KeysQ) of - {{value, OldestK}, RemKeys} -> - {_AclResult, CachedAt} = erlang:get(OldestK), - if_expired(CachedAt, - fun(false) -> KeysQ; - (true) -> - erlang:erase(OldestK), - decr_cache_size(), - cleanup_acl_cache(RemKeys) - end); - {empty, KeysQ} -> KeysQ - end. - -get_newest_key() -> - get_key(fun(KeysQ) -> queue:get_r(KeysQ) end). - -get_oldest_key() -> - get_key(fun(KeysQ) -> queue:get(KeysQ) end). - -get_key(Pick) -> - KeysQ = get_keys_queue(), - case queue:is_empty(KeysQ) of - true -> undefined; - false -> Pick(KeysQ) - end. - -%% for test only -dump_acl_cache() -> - map_acl_cache(fun(Cache) -> Cache end). -map_acl_cache(Fun) -> - [Fun(R) || R = {{SubPub, _T}, _Acl} <- get(), SubPub =:= publish - orelse SubPub =:= subscribe]. - - -cache_k(PubSub, Topic)-> {PubSub, Topic}. -cache_v(AclResult)-> {AclResult, time_now()}. - -get_cache_max_size() -> - application:get_env(emqx, acl_cache_max_size, 0). - -get_cache_size() -> - case erlang:get(acl_cache_size) of - undefined -> 0; - Size -> Size - end. -incr_cache_size() -> - erlang:put(acl_cache_size, get_cache_size() + 1), ok. -decr_cache_size() -> - erlang:put(acl_cache_size, get_cache_size() - 1), ok. -set_cache_size(N) -> - erlang:put(acl_cache_size, N), ok. - -keys_queue_in(Key) -> - %% delete the key first if exists - KeysQ = get_keys_queue(), - set_keys_queue(queue:in(Key, KeysQ)). - -keys_queue_update(Key) -> - NewKeysQ = remove_key(Key, get_keys_queue()), - set_keys_queue(queue:in(Key, NewKeysQ)). - -remove_key(Key, KeysQ) -> - queue:filter(fun - (K) when K =:= Key -> false; (_) -> true - end, KeysQ). - -set_keys_queue(KeysQ) -> - erlang:put(acl_keys_q, KeysQ), ok. -get_keys_queue() -> - case erlang:get(acl_keys_q) of - undefined -> queue:new(); - KeysQ -> KeysQ - end. - -time_now() -> erlang:system_time(millisecond). - -if_expired(CachedAt, Fun) -> - TTL = application:get_env(emqx, acl_cache_ttl, 60000), - Now = time_now(), - if (CachedAt + TTL) =< Now -> - Fun(true); - true -> - Fun(false) - end. diff --git a/src/emqx_acl_cache.erl b/src/emqx_acl_cache.erl new file mode 100644 index 000000000..7db23fb1e --- /dev/null +++ b/src/emqx_acl_cache.erl @@ -0,0 +1,204 @@ +-module(emqx_acl_cache). + +-include("emqx.hrl"). + +-export([ get_acl_cache/2 + , put_acl_cache/3 + , cleanup_acl_cache/0 + , empty_acl_cache/0 + , dump_acl_cache/0 + , get_cache_size/0 + , get_cache_max_size/0 + , get_newest_key/0 + , get_oldest_key/0 + , cache_k/2 + , cache_v/1 + ]). + +-type(acl_result() :: allow | deny). + +%% Wrappers for key and value +cache_k(PubSub, Topic)-> {PubSub, Topic}. +cache_v(AclResult)-> {AclResult, time_now()}. + +%% We'll cleanup the cache before repalcing an expired acl. +-spec(get_acl_cache(PubSub :: publish | subscribe, Topic :: topic()) + -> (acl_result() | not_found)). +get_acl_cache(PubSub, Topic) -> + case erlang:get(cache_k(PubSub, Topic)) of + undefined -> not_found; + {AclResult, CachedAt} -> + if_expired(CachedAt, + fun(false) -> + AclResult; + (true) -> + cleanup_acl_cache(), + not_found + end) + end. + +%% If the cache get full, and also the latest one +%% is expired, then delete all the cache entries +-spec(put_acl_cache(PubSub :: publish | subscribe, + Topic :: topic(), AclResult :: acl_result()) -> ok). +put_acl_cache(PubSub, Topic, AclResult) -> + MaxSize = get_cache_max_size(), true = (MaxSize =/= 0), + Size = get_cache_size(), + if + Size < MaxSize -> + add_acl(PubSub, Topic, AclResult); + Size =:= MaxSize -> + NewestK = get_newest_key(), + {_AclResult, CachedAt} = erlang:get(NewestK), + if_expired(CachedAt, + fun(true) -> + % all cache expired, cleanup first + empty_acl_cache(), + add_acl(PubSub, Topic, AclResult); + (false) -> + % cache full, perform cache replacement + evict_acl_cache(), + add_acl(PubSub, Topic, AclResult) + end) + end. + +%% delete all the acl entries +-spec(empty_acl_cache() -> ok). +empty_acl_cache() -> + map_acl_cache(fun({CacheK, _CacheV}) -> + erlang:erase(CacheK) + end), + set_cache_size(0), + keys_queue_set(queue:new()). + +%% delete the oldest acl entry +-spec(evict_acl_cache() -> ok). +evict_acl_cache() -> + OldestK = keys_queue_out(), + erlang:erase(OldestK), + decr_cache_size(). + +%% cleanup all the exipired cache entries +-spec(cleanup_acl_cache() -> ok). +cleanup_acl_cache() -> + keys_queue_set( + cleanup_acl(keys_queue_get())). + +get_oldest_key() -> + keys_queue_pick(queue_front()). +get_newest_key() -> + keys_queue_pick(queue_rear()). + +get_cache_max_size() -> + application:get_env(emqx, acl_cache_max_size, 0). + +get_cache_size() -> + case erlang:get(acl_cache_size) of + undefined -> 0; + Size -> Size + end. + +dump_acl_cache() -> + map_acl_cache(fun(Cache) -> Cache end). +map_acl_cache(Fun) -> + [Fun(R) || R = {{SubPub, _T}, _Acl} <- get(), SubPub =:= publish + orelse SubPub =:= subscribe]. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- +add_acl(PubSub, Topic, AclResult) -> + K = cache_k(PubSub, Topic), + V = cache_v(AclResult), + case erlang:get(K) of + undefined -> add_new_acl(K, V); + {_AclResult, _CachedAt} -> + update_acl(K, V) + end. + +add_new_acl(K, V) -> + erlang:put(K, V), + keys_queue_in(K), + incr_cache_size(). + +update_acl(K, V) -> + erlang:put(K, V), + keys_queue_update(K). + +cleanup_acl(KeysQ) -> + case queue:out(KeysQ) of + {{value, OldestK}, KeysQ2} -> + {_AclResult, CachedAt} = erlang:get(OldestK), + if_expired(CachedAt, + fun(false) -> KeysQ; + (true) -> + erlang:erase(OldestK), + decr_cache_size(), + cleanup_acl(KeysQ2) + end); + {empty, KeysQ} -> KeysQ + end. + +incr_cache_size() -> + erlang:put(acl_cache_size, get_cache_size() + 1), ok. +decr_cache_size() -> + Size = get_cache_size(), + if Size > 1 -> + erlang:put(acl_cache_size, Size-1); + Size =< 1 -> + erlang:put(acl_cache_size, 0) + end, ok. +set_cache_size(N) -> + erlang:put(acl_cache_size, N), ok. + +%%% Ordered Keys Q %%% +keys_queue_in(Key) -> + %% delete the key first if exists + KeysQ = keys_queue_get(), + keys_queue_set(queue:in(Key, KeysQ)). + +keys_queue_out() -> + case queue:out(keys_queue_get()) of + {{value, OldestK}, Q2} -> + keys_queue_set(Q2), OldestK; + {empty, _Q} -> + undefined + end. + +keys_queue_update(Key) -> + NewKeysQ = keys_queue_remove(Key, keys_queue_get()), + keys_queue_set(queue:in(Key, NewKeysQ)). + +keys_queue_pick(Pick) -> + KeysQ = keys_queue_get(), + case queue:is_empty(KeysQ) of + true -> undefined; + false -> Pick(KeysQ) + end. + +keys_queue_remove(Key, KeysQ) -> + queue:filter(fun + (K) when K =:= Key -> false; (_) -> true + end, KeysQ). + +keys_queue_set(KeysQ) -> + erlang:put(acl_keys_q, KeysQ), ok. +keys_queue_get() -> + case erlang:get(acl_keys_q) of + undefined -> queue:new(); + KeysQ -> KeysQ + end. + +queue_front() -> fun queue:get/1. +queue_rear() -> fun queue:get_r/1. + +time_now() -> erlang:system_time(millisecond). + +if_expired(CachedAt, Fun) -> + TTL = application:get_env(emqx, acl_cache_ttl, 60000), + Now = time_now(), + if (CachedAt + TTL) =< Now -> + Fun(true); + true -> + Fun(false) + end. diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index c3907c2db..cf859d7c1 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -25,6 +25,7 @@ -include_lib("eunit/include/eunit.hrl"). -define(AC, emqx_access_control). +-define(CACHE, emqx_acl_cache). -import(emqx_access_rule, [compile/1, match/3]). @@ -150,14 +151,14 @@ check_acl_2(_) -> acl_cache_basic(_) -> SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, - not_found = ?AC:get_acl_cache(subscribe, <<"users/testuser/1">>), - not_found = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), + 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:get_acl_cache(subscribe, <<"users/testuser/1">>), - allow = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), + allow = ?CACHE:get_acl_cache(subscribe, <<"users/testuser/1">>), + allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), ok. acl_cache_expiry(_) -> @@ -165,9 +166,9 @@ acl_cache_expiry(_) -> SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), - allow = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), + allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), ct:sleep(1100), - not_found = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), + not_found = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), ok. acl_cache_full(_) -> @@ -178,8 +179,8 @@ acl_cache_full(_) -> allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), %% the older ones (the <<"users/testuser/1">>) will be evicted first - not_found = ?AC:get_acl_cache(subscribe, <<"users/testuser/1">>), - allow = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), + not_found = ?CACHE:get_acl_cache(subscribe, <<"users/testuser/1">>), + allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), ok. acl_cache_cleanup(_) -> @@ -192,115 +193,115 @@ acl_cache_cleanup(_) -> allow = ?AC:check_acl(SelfUser, subscribe, <<"users/testuser/1">>), allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), - allow = ?AC:get_acl_cache(subscribe, <<"users/testuser/1">>), - allow = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), + allow = ?CACHE:get_acl_cache(subscribe, <<"users/testuser/1">>), + allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), ct:sleep(1100), %% now the cache is full and the newest one - "clients/client1" %% should be expired, so we'll try to cleanup before putting the next cache entry deny = ?AC:check_acl(SelfUser, subscribe, <<"#">>), - not_found = ?AC:get_acl_cache(subscribe, <<"users/testuser/1">>), - not_found = ?AC:get_acl_cache(subscribe, <<"clients/client1">>), - deny = ?AC:get_acl_cache(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. put_get_del_cache(_) -> application:set_env(emqx, acl_cache_ttl, 300000), application:set_env(emqx, acl_cache_max_size, 30), - not_found = ?AC:get_acl_cache(publish, <<"a">>), - ok = ?AC:put_acl_cache(publish, <<"a">>, allow), - allow = ?AC:get_acl_cache(publish, <<"a">>), + not_found = ?CACHE:get_acl_cache(publish, <<"a">>), + ok = ?CACHE:put_acl_cache(publish, <<"a">>, allow), + allow = ?CACHE:get_acl_cache(publish, <<"a">>), - not_found = ?AC:get_acl_cache(subscribe, <<"b">>), - ok = ?AC:put_acl_cache(subscribe, <<"b">>, deny), - deny = ?AC:get_acl_cache(subscribe, <<"b">>), + not_found = ?CACHE:get_acl_cache(subscribe, <<"b">>), + ok = ?CACHE:put_acl_cache(subscribe, <<"b">>, deny), + deny = ?CACHE:get_acl_cache(subscribe, <<"b">>), - 2 = ?AC:get_cache_size(), - ?assertEqual(?AC:cache_k(subscribe, <<"b">>), ?AC:get_newest_key()). + 2 = ?CACHE:get_cache_size(), + ?assertEqual(?CACHE:cache_k(subscribe, <<"b">>), ?CACHE:get_newest_key()). cache_expiry(_) -> application:set_env(emqx, acl_cache_ttl, 1000), application:set_env(emqx, acl_cache_max_size, 30), - ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), - allow = ?AC:get_acl_cache(subscribe, <<"a">>), + ok = ?CACHE:put_acl_cache(subscribe, <<"a">>, allow), + allow = ?CACHE:get_acl_cache(subscribe, <<"a">>), ct:sleep(1100), - not_found = ?AC:get_acl_cache(subscribe, <<"a">>), + not_found = ?CACHE:get_acl_cache(subscribe, <<"a">>), - ok = ?AC:put_acl_cache(subscribe, <<"a">>, deny), - deny = ?AC:get_acl_cache(subscribe, <<"a">>), + ok = ?CACHE:put_acl_cache(subscribe, <<"a">>, deny), + deny = ?CACHE:get_acl_cache(subscribe, <<"a">>), ct:sleep(1100), - not_found = ?AC:get_acl_cache(subscribe, <<"a">>). + not_found = ?CACHE:get_acl_cache(subscribe, <<"a">>). cache_update(_) -> application:set_env(emqx, acl_cache_ttl, 300000), application:set_env(emqx, acl_cache_max_size, 30), - [] = ?AC:dump_acl_cache(), + [] = ?CACHE:dump_acl_cache(), - ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), - ok = ?AC:put_acl_cache(publish, <<"b">>, allow), - ok = ?AC:put_acl_cache(publish, <<"c">>, allow), - 3 = ?AC:get_cache_size(), - ?assertEqual(?AC:cache_k(publish, <<"c">>), ?AC:get_newest_key()), + ok = ?CACHE:put_acl_cache(subscribe, <<"a">>, allow), + ok = ?CACHE:put_acl_cache(publish, <<"b">>, allow), + ok = ?CACHE:put_acl_cache(publish, <<"c">>, allow), + 3 = ?CACHE:get_cache_size(), + ?assertEqual(?CACHE:cache_k(publish, <<"c">>), ?CACHE:get_newest_key()), %% update the 2nd one - ok = ?AC:put_acl_cache(publish, <<"b">>, allow), - %ct:pal("dump acl cache: ~p~n", [?AC:dump_acl_cache()]), + ok = ?CACHE:put_acl_cache(publish, <<"b">>, allow), + %ct:pal("dump acl cache: ~p~n", [?CACHE:dump_acl_cache()]), - 3 = ?AC:get_cache_size(), - ?assertEqual(?AC:cache_k(publish, <<"b">>), ?AC:get_newest_key()). + 3 = ?CACHE:get_cache_size(), + ?assertEqual(?CACHE:cache_k(publish, <<"b">>), ?CACHE:get_newest_key()). cache_full_replacement(_) -> application:set_env(emqx, acl_cache_ttl, 300000), application:set_env(emqx, acl_cache_max_size, 3), - ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), - ok = ?AC:put_acl_cache(publish, <<"b">>, allow), - ok = ?AC:put_acl_cache(publish, <<"c">>, allow), - allow = ?AC:get_acl_cache(subscribe, <<"a">>), - allow = ?AC:get_acl_cache(publish, <<"b">>), - allow = ?AC:get_acl_cache(publish, <<"c">>), - 3 = ?AC:get_cache_size(), - ?assertEqual(?AC:cache_k(publish, <<"c">>), ?AC:get_newest_key()), + ok = ?CACHE:put_acl_cache(subscribe, <<"a">>, allow), + ok = ?CACHE:put_acl_cache(publish, <<"b">>, allow), + ok = ?CACHE:put_acl_cache(publish, <<"c">>, allow), + allow = ?CACHE:get_acl_cache(subscribe, <<"a">>), + allow = ?CACHE:get_acl_cache(publish, <<"b">>), + allow = ?CACHE:get_acl_cache(publish, <<"c">>), + 3 = ?CACHE:get_cache_size(), + ?assertEqual(?CACHE:cache_k(publish, <<"c">>), ?CACHE:get_newest_key()), - ok = ?AC:put_acl_cache(publish, <<"d">>, deny), - 3 = ?AC:get_cache_size(), - ?assertEqual(?AC:cache_k(publish, <<"d">>), ?AC:get_newest_key()), + ok = ?CACHE:put_acl_cache(publish, <<"d">>, deny), + 3 = ?CACHE:get_cache_size(), + ?assertEqual(?CACHE:cache_k(publish, <<"d">>), ?CACHE:get_newest_key()), - ok = ?AC:put_acl_cache(publish, <<"e">>, deny), - 3 = ?AC:get_cache_size(), - ?assertEqual(?AC:cache_k(publish, <<"e">>), ?AC:get_newest_key()), + ok = ?CACHE:put_acl_cache(publish, <<"e">>, deny), + 3 = ?CACHE:get_cache_size(), + ?assertEqual(?CACHE:cache_k(publish, <<"e">>), ?CACHE:get_newest_key()), - not_found = ?AC:get_acl_cache(subscribe, <<"a">>), - not_found = ?AC:get_acl_cache(publish, <<"b">>), - allow = ?AC:get_acl_cache(publish, <<"c">>). + not_found = ?CACHE:get_acl_cache(subscribe, <<"a">>), + not_found = ?CACHE:get_acl_cache(publish, <<"b">>), + allow = ?CACHE:get_acl_cache(publish, <<"c">>). cache_cleanup(_) -> application:set_env(emqx, acl_cache_ttl, 1000), application:set_env(emqx, acl_cache_max_size, 30), - ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), - ok = ?AC:put_acl_cache(publish, <<"b">>, allow), - ok = ?AC:put_acl_cache(publish, <<"c">>, allow), - 3 = ?AC:get_cache_size(), + ok = ?CACHE:put_acl_cache(subscribe, <<"a">>, allow), + ok = ?CACHE:put_acl_cache(publish, <<"b">>, allow), + ok = ?CACHE:put_acl_cache(publish, <<"c">>, allow), + 3 = ?CACHE:get_cache_size(), ct:sleep(1100), - ?AC:cleanup_acl_cache(), - 0 = ?AC:get_cache_size(). + ?CACHE:cleanup_acl_cache(), + 0 = ?CACHE:get_cache_size(). cache_full_cleanup(_) -> application:set_env(emqx, acl_cache_ttl, 1000), application:set_env(emqx, acl_cache_max_size, 3), - ok = ?AC:put_acl_cache(subscribe, <<"a">>, allow), - ok = ?AC:put_acl_cache(publish, <<"b">>, allow), - ok = ?AC:put_acl_cache(publish, <<"c">>, allow), - 3 = ?AC:get_cache_size(), + ok = ?CACHE:put_acl_cache(subscribe, <<"a">>, allow), + ok = ?CACHE:put_acl_cache(publish, <<"b">>, allow), + ok = ?CACHE:put_acl_cache(publish, <<"c">>, allow), + 3 = ?CACHE:get_cache_size(), ct:sleep(1100), %% verify auto cleanup upon cache full - ok = ?AC:put_acl_cache(subscribe, <<"d">>, deny), - 1 = ?AC:get_cache_size(). + ok = ?CACHE:put_acl_cache(subscribe, <<"d">>, deny), + 1 = ?CACHE:get_cache_size(). %%-------------------------------------------------------------------- %% emqx_access_rule From 0e3728c9403f47b3e16b2779981c6ba1a351a9d4 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 26 Aug 2018 16:24:51 +0800 Subject: [PATCH 118/520] Add emqx_types module and 'credentials' type --- include/emqx.hrl | 20 +++-- src/emqx_access_control.erl | 152 ++++++++++++++++++------------------ src/emqx_access_rule.erl | 87 +++++++++++---------- src/emqx_acl_internal.erl | 49 ++++++------ src/emqx_acl_mod.erl | 8 +- src/emqx_auth_mod.erl | 10 +-- src/emqx_types.erl | 38 +++++++++ src/emqx_ws_connection.erl | 1 + 8 files changed, 201 insertions(+), 164 deletions(-) create mode 100644 src/emqx_types.erl diff --git a/include/emqx.hrl b/include/emqx.hrl index 0372f547e..022660287 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -70,18 +70,26 @@ -type(topic_table() :: [{topic(), subopts()}]). %%-------------------------------------------------------------------- -%% Client and Session +%% Zone, Credentials, Client and Session %%-------------------------------------------------------------------- --type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). - --type(peername() :: {inet:ip_address(), inet:port_number()}). +-type(zone() :: atom()). -type(client_id() :: binary() | atom()). --type(username() :: binary() | atom()). +-type(username() :: binary() | undefined). --type(zone() :: atom()). +-type(password() :: binary() | undefined). + +-type(peername() :: {inet:ip_address(), inet:port_number()}). + +-type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). + +-type(credentials() :: #{client_id := binary(), + username := binary(), + peername := peername(), + zone => zone(), + atom() => term()}). -record(client, { id :: client_id(), diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index f43309088..1577ca122 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -20,9 +20,9 @@ -export([start_link/0]). -export([authenticate/2]). --export([check_acl/3, reload_acl/0, lookup_mods/1]). --export([clean_acl_cache/1, clean_acl_cache/2]). +-export([check_acl/3, reload_acl/0]). -export([register_mod/3, register_mod/4, unregister_mod/2]). +-export([lookup_mods/1]). -export([stop/0]). %% gen_server callbacks @@ -32,8 +32,6 @@ -define(TAB, ?MODULE). -define(SERVER, ?MODULE). --type(password() :: undefined | binary()). - -record(state, {}). %%------------------------------------------------------------------------------ @@ -41,81 +39,88 @@ %%------------------------------------------------------------------------------ %% @doc Start access control server. --spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link() -> {ok, pid()} | {error, term()}). start_link() -> + start_with(fun register_default_acl/0). + +start_with(Fun) -> case gen_server:start_link({local, ?SERVER}, ?MODULE, [], []) of {ok, Pid} -> - ok = register_default_mod(), - {ok, Pid}; + Fun(), {ok, Pid}; {error, Reason} -> {error, Reason} end. -register_default_mod() -> +register_default_acl() -> case emqx_config:get_env(acl_file) of undefined -> ok; - File -> - emqx_access_control:register_mod(acl, emqx_acl_internal, [File]) + File -> register_mod(acl, emqx_acl_internal, [File]) end. -%% @doc Authenticate Client. --spec(authenticate(Client :: client(), Password :: password()) - -> ok | {ok, boolean()} | {error, term()}). -authenticate(Client, Password) when is_record(Client, client) -> - authenticate(Client, Password, lookup_mods(auth)). +-spec(authenticate(credentials(), password()) + -> ok | {ok, map()} | {continue, map()} | {error, term()}). +authenticate(Credentials, Password) -> + authenticate(Credentials, Password, lookup_mods(auth)). -authenticate(#client{zone = Zone}, _Password, []) -> +authenticate(Credentials, _Password, []) -> + Zone = maps:get(zone, Credentials, undefined), case emqx_zone:get_env(Zone, allow_anonymous, false) of true -> ok; - false -> {error, "No auth module to check!"} + false -> {error, auth_modules_not_found} end; -authenticate(Client, Password, [{Mod, State, _Seq} | Mods]) -> - case catch Mod:check(Client, Password, State) of - ok -> ok; - {ok, IsSuper} -> {ok, IsSuper}; - ignore -> authenticate(Client, Password, Mods); - {error, Reason} -> {error, Reason}; - {'EXIT', Error} -> {error, Error} +authenticate(Credentials, Password, [{Mod, State, _Seq} | Mods]) -> + case catch Mod:check(Credentials, Password, State) of + ok -> ok; + {ok, IsSuper} when is_boolean(IsSuper) -> + {ok, #{is_superuser => IsSuper}}; + {ok, Result} when is_map(Result) -> + {ok, Result}; + {continue, Result} when is_map(Result) -> + {continue, Result}; + ignore -> + authenticate(Credentials, Password, Mods); + {error, Reason} -> + {error, Reason}; + {'EXIT', Error} -> + {error, Error} end. -%% @doc Check ACL --spec(check_acl(client(), pubsub(), topic()) -> allow | deny). -check_acl(Client, PubSub, Topic) when ?PS(PubSub) -> - check_acl(Client, PubSub, Topic, lookup_mods(acl)). +-spec(check_acl(credentials(), pubsub(), topic()) -> allow | deny). +check_acl(Credentials, PubSub, Topic) when ?PS(PubSub) -> + check_acl(Credentials, PubSub, Topic, lookup_mods(acl)). -check_acl(#client{zone = Zone}, _PubSub, _Topic, []) -> +check_acl(Credentials, _PubSub, _Topic, []) -> + Zone = maps:get(zone, Credentials, undefined), emqx_zone:get_env(Zone, acl_nomatch, deny); -check_acl(Client, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) -> - case Mod:check_acl({Client, PubSub, Topic}, State) of +check_acl(Credentials, PubSub, Topic, [{Mod, State, _Seq}|Mods]) -> + case Mod:check_acl({Credentials, PubSub, Topic}, State) of + ignore -> check_acl(Credentials, PubSub, Topic, Mods); allow -> allow; - deny -> deny; - ignore -> check_acl(Client, PubSub, Topic, AclMods) + deny -> deny end. -%% @doc Reload ACL Rules. --spec(reload_acl() -> list(ok | {error, already_existed})). +-spec(reload_acl() -> list(ok | {error, term()})). reload_acl() -> [Mod:reload_acl(State) || {Mod, State, _Seq} <- lookup_mods(acl)]. -%% @doc Register Authentication or ACL module. --spec(register_mod(auth | acl, atom(), list()) -> ok | {error, term()}). +%% @doc Register an Auth/ACL module. +-spec(register_mod(auth | acl, module(), list()) -> ok | {error, term()}). register_mod(Type, Mod, Opts) when Type =:= auth; Type =:= acl -> register_mod(Type, Mod, Opts, 0). --spec(register_mod(auth | acl, atom(), list(), non_neg_integer()) +-spec(register_mod(auth | acl, module(), list(), non_neg_integer()) -> ok | {error, term()}). register_mod(Type, Mod, Opts, Seq) when Type =:= auth; Type =:= acl-> gen_server:call(?SERVER, {register_mod, Type, Mod, Opts, Seq}). -%% @doc Unregister authentication or ACL module --spec(unregister_mod(Type :: auth | acl, Mod :: atom()) - -> ok | {error, not_found | term()}). +%% @doc Unregister an Auth/ACL module. +-spec(unregister_mod(auth | acl, module()) -> ok | {error, not_found | term()}). unregister_mod(Type, Mod) when Type =:= auth; Type =:= acl -> gen_server:call(?SERVER, {unregister_mod, Type, Mod}). -%% @doc Lookup authentication or ACL modules. +%% @doc Lookup all Auth/ACL modules. -spec(lookup_mods(auth | acl) -> list()). lookup_mods(Type) -> case ets:lookup(?TAB, tab_key(Type)) of @@ -126,19 +131,12 @@ lookup_mods(Type) -> tab_key(auth) -> auth_modules; tab_key(acl) -> acl_modules. -%% @doc Stop access control server. stop() -> - gen_server:stop(?MODULE, normal, infinity). + gen_server:stop(?SERVER, normal, infinity). -%%TODO: Support ACL cache... -clean_acl_cache(_ClientId) -> - ok. -clean_acl_cache(_ClientId, _Topic) -> - ok. - -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% gen_server callbacks -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- init([]) -> _ = emqx_tables:new(?TAB, [set, protected, {read_concurrency, true}]), @@ -146,31 +144,31 @@ init([]) -> handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) -> Mods = lookup_mods(Type), - Existed = lists:keyfind(Mod, 1, Mods), - {reply, if_existed(Existed, fun() -> - case catch Mod:init(Opts) of - {ok, ModState} -> - NewMods = lists:sort(fun({_, _, Seq1}, {_, _, Seq2}) -> - Seq1 >= Seq2 - end, [{Mod, ModState, Seq} | Mods]), - ets:insert(?TAB, {tab_key(Type), NewMods}), - ok; - {error, Error} -> - {error, Error}; - {'EXIT', Reason} -> - {error, Reason} - end - end), State}; + reply(case lists:keyfind(Mod, 1, Mods) of + true -> + {error, already_existed}; + false -> + case catch Mod:init(Opts) of + {ok, ModState} -> + NewMods = lists:sort(fun({_, _, Seq1}, {_, _, Seq2}) -> + Seq1 >= Seq2 + end, [{Mod, ModState, Seq} | Mods]), + ets:insert(?TAB, {tab_key(Type), NewMods}), ok; + {error, Error} -> + {error, Error}; + {'EXIT', Reason} -> + {error, Reason} + end + end, State); handle_call({unregister_mod, Type, Mod}, _From, State) -> Mods = lookup_mods(Type), - case lists:keyfind(Mod, 1, Mods) of - false -> - {reply, {error, not_found}, State}; - _ -> - _ = ets:insert(?TAB, {tab_key(Type), lists:keydelete(Mod, 1, Mods)}), - {reply, ok, State} - end; + reply(case lists:keyfind(Mod, 1, Mods) of + false -> + {error, not_found}; + true -> + ets:insert(?TAB, {tab_key(Type), lists:keydelete(Mod, 1, Mods)}), ok + end, State); handle_call(stop, _From, State) -> {stop, normal, ok, State}; @@ -197,8 +195,6 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -if_existed(false, Fun) -> - Fun(); -if_existed(_Mod, _Fun) -> - {error, already_existed}. +reply(Reply, State) -> + {reply, Reply, State}. diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 91c601db4..2f5d190a9 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -17,9 +17,9 @@ -include("emqx.hrl"). -type(who() :: all | binary() | - {ipaddr, esockd_cidr:cidr_string()} | {client, binary()} | - {user, binary()}). + {user, binary()} | + {ipaddr, esockd_cidr:cidr_string()}). -type(access() :: subscribe | publish | pubsub). @@ -30,7 +30,8 @@ -export_type([rule/0]). --export([compile/1, match/3]). +-export([compile/1]). +-export([match/3]). -define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))). @@ -71,7 +72,8 @@ compile(topic, Topic) -> end. 'pattern?'(Words) -> - lists:member(<<"%u">>, Words) orelse lists:member(<<"%c">>, Words). + lists:member(<<"%u">>, Words) + orelse lists:member(<<"%c">>, Words). bin(L) when is_list(L) -> list_to_binary(L); @@ -79,69 +81,70 @@ bin(B) when is_binary(B) -> B. %% @doc Match access rule --spec(match(client(), topic(), rule()) -> {matched, allow} | {matched, deny} | nomatch). -match(_Client, _Topic, {AllowDeny, all}) when (AllowDeny =:= allow) orelse (AllowDeny =:= deny) -> +-spec(match(credentials(), topic(), rule()) + -> {matched, allow} | {matched, deny} | nomatch). +match(_Credentials, _Topic, {AllowDeny, all}) when ?ALLOW_DENY(AllowDeny) -> {matched, AllowDeny}; -match(Client, Topic, {AllowDeny, Who, _PubSub, TopicFilters}) - when (AllowDeny =:= allow) orelse (AllowDeny =:= deny) -> - case match_who(Client, Who) - andalso match_topics(Client, Topic, TopicFilters) of +match(Credentials, Topic, {AllowDeny, Who, _PubSub, TopicFilters}) + when ?ALLOW_DENY(AllowDeny) -> + case match_who(Credentials, Who) + andalso match_topics(Credentials, Topic, TopicFilters) of true -> {matched, AllowDeny}; false -> nomatch end. -match_who(_Client, all) -> +match_who(_Credentials, all) -> true; -match_who(_Client, {user, all}) -> +match_who(_Credentials, {user, all}) -> true; -match_who(_Client, {client, all}) -> +match_who(_Credentials, {client, all}) -> true; -match_who(#client{id = ClientId}, {client, ClientId}) -> +match_who(#{client_id := ClientId}, {client, ClientId}) -> true; -match_who(#client{username = Username}, {user, Username}) -> +match_who(#{username := Username}, {user, Username}) -> true; -match_who(#client{peername = undefined}, {ipaddr, _Tup}) -> +match_who(#{peername := undefined}, {ipaddr, _Tup}) -> false; -match_who(#client{peername = {IP, _}}, {ipaddr, CIDR}) -> +match_who(#{peername := {IP, _}}, {ipaddr, CIDR}) -> esockd_cidr:match(IP, CIDR); -match_who(Client, {'and', Conds}) when is_list(Conds) -> +match_who(Credentials, {'and', Conds}) when is_list(Conds) -> lists:foldl(fun(Who, Allow) -> - match_who(Client, Who) andalso Allow + match_who(Credentials, Who) andalso Allow end, true, Conds); -match_who(Client, {'or', Conds}) when is_list(Conds) -> +match_who(Credentials, {'or', Conds}) when is_list(Conds) -> lists:foldl(fun(Who, Allow) -> - match_who(Client, Who) orelse Allow + match_who(Credentials, Who) orelse Allow end, false, Conds); -match_who(_Client, _Who) -> +match_who(_Credentials, _Who) -> false. -match_topics(_Client, _Topic, []) -> +match_topics(_Credentials, _Topic, []) -> false; -match_topics(Client, Topic, [{pattern, PatternFilter}|Filters]) -> - TopicFilter = feed_var(Client, PatternFilter), +match_topics(Credentials, Topic, [{pattern, PatternFilter}|Filters]) -> + TopicFilter = feed_var(Credentials, PatternFilter), match_topic(emqx_topic:words(Topic), TopicFilter) - orelse match_topics(Client, Topic, Filters); -match_topics(Client, Topic, [TopicFilter|Filters]) -> + orelse match_topics(Credentials, Topic, Filters); +match_topics(Credentials, Topic, [TopicFilter|Filters]) -> match_topic(emqx_topic:words(Topic), TopicFilter) - orelse match_topics(Client, Topic, Filters). + orelse match_topics(Credentials, Topic, Filters). match_topic(Topic, {eq, TopicFilter}) -> - Topic =:= TopicFilter; + Topic == TopicFilter; match_topic(Topic, TopicFilter) -> emqx_topic:match(Topic, TopicFilter). -feed_var(Client, Pattern) -> - feed_var(Client, Pattern, []). -feed_var(_Client, [], Acc) -> +feed_var(Credentials, Pattern) -> + feed_var(Credentials, Pattern, []). +feed_var(_Credentials, [], Acc) -> lists:reverse(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 = #client{username = undefined}, [<<"%u">>|Words], Acc) -> - feed_var(Client, Words, [<<"%u">>|Acc]); -feed_var(Client = #client{username = Username}, [<<"%u">>|Words], Acc) -> - feed_var(Client, Words, [Username|Acc]); -feed_var(Client, [W|Words], Acc) -> - feed_var(Client, Words, [W|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]). diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index 65a3199ae..07aada812 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -25,11 +25,11 @@ -define(ACL_RULE_TAB, emqx_acl_rule). --record(state, {config}). +-record(state, {acl_file}). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% @doc Read all rules -spec(all_rules() -> list(emqx_access_rule:rule())). @@ -39,17 +39,17 @@ all_rules() -> [{_, Rules}] -> Rules end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% ACL callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% @doc Init internal ACL --spec(init([File :: string()]) -> {ok, State :: any()}). +-spec(init([File :: string()]) -> {ok, State :: term()}). init([File]) -> _ = emqx_tables:new(?ACL_RULE_TAB, [set, public, {read_concurrency, true}]), - {ok, load_rules_from_file(#state{config = File})}. + {ok, load_rules_from_file(#state{acl_file = File})}. -load_rules_from_file(State = #state{config = AclFile}) -> +load_rules_from_file(State = #state{acl_file = AclFile}) -> {ok, Terms} = file:consult(AclFile), Rules = [emqx_access_rule:compile(Term) || Term <- Terms], lists:foreach(fun(PubSub) -> @@ -73,15 +73,12 @@ filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) -> false. %% @doc Check ACL --spec(check_acl({Client, PubSub, Topic}, State) -> allow | deny | ignore when - Client :: client(), - PubSub :: pubsub(), - Topic :: topic(), - State :: #state{}). -check_acl(_Who, #state{config = undefined}) -> +-spec(check_acl({credentials(), pubsub(), topic()}, #state{}) + -> allow | deny | ignore). +check_acl(_Who, #state{acl_file = undefined}) -> allow; -check_acl({Client, PubSub, Topic}, #state{}) -> - case match(Client, Topic, lookup(PubSub)) of +check_acl({Credentials, PubSub, Topic}, #state{}) -> + case match(Credentials, Topic, lookup(PubSub)) of {matched, allow} -> allow; {matched, deny} -> deny; nomatch -> ignore @@ -93,26 +90,24 @@ lookup(PubSub) -> [{PubSub, Rules}] -> Rules end. -match(_Client, _Topic, []) -> +match(_Credentials, _Topic, []) -> nomatch; - -match(Client, Topic, [Rule|Rules]) -> - case emqx_access_rule:match(Client, Topic, Rule) of - nomatch -> match(Client, Topic, Rules); +match(Credentials, Topic, [Rule|Rules]) -> + case emqx_access_rule:match(Credentials, Topic, Rule) of + nomatch -> match(Credentials, Topic, Rules); {matched, AllowDeny} -> {matched, AllowDeny} end. -%% @doc Reload ACL --spec(reload_acl(State :: #state{}) -> ok | {error, Reason :: any()}). -reload_acl(#state{config = undefined}) -> +-spec(reload_acl(#state{}) -> ok | {error, term()}). +reload_acl(#state{acl_file = undefined}) -> ok; reload_acl(State) -> case catch load_rules_from_file(State) of - {'EXIT', Error} -> {error, Error}; - true -> io:format("~s~n", ["reload acl_internal successfully"]), ok + {'EXIT', Error} -> + {error, Error}; + true -> ok end. -%% @doc ACL Module Description -spec(description() -> string()). description() -> "Internal ACL with etc/acl.conf". diff --git a/src/emqx_acl_mod.erl b/src/emqx_acl_mod.erl index 85844b042..716b27967 100644 --- a/src/emqx_acl_mod.erl +++ b/src/emqx_acl_mod.erl @@ -22,14 +22,12 @@ -ifdef(use_specs). --callback(init(AclOpts :: list()) -> {ok, State :: any()}). +-callback(init(AclOpts :: list()) -> {ok, State :: term()}). --callback(check_acl({Client :: client(), - PubSub :: pubsub(), - Topic :: topic()}, State :: any()) +-callback(check_acl({credentials(), pubsub(), topic()}, State :: term()) -> allow | deny | ignore). --callback(reload_acl(State :: any()) -> ok | {error, term()}). +-callback(reload_acl(State :: term()) -> ok | {error, term()}). -callback(description() -> string()). diff --git a/src/emqx_auth_mod.erl b/src/emqx_auth_mod.erl index 65298ef9b..8f03eb4fa 100644 --- a/src/emqx_auth_mod.erl +++ b/src/emqx_auth_mod.erl @@ -22,13 +22,11 @@ -ifdef(use_specs). --callback(init(AuthOpts :: list()) -> {ok, State :: any()}). - --callback(check(Client :: client(), - Password :: binary(), - State :: any()) - -> ok | {ok, boolean()} | ignore | {error, string()}). +-callback(init(AuthOpts :: list()) -> {ok, State :: term()}). +-callback(check(credentials(), password(), State :: term()) + -> ok | {ok, boolean()} | {ok, map()} | + {continue, map()} | ignore | {error, term()}). -callback(description() -> string()). -else. diff --git a/src/emqx_types.erl b/src/emqx_types.erl new file mode 100644 index 000000000..de1f5df4b --- /dev/null +++ b/src/emqx_types.erl @@ -0,0 +1,38 @@ +%% Copyright (c) 2018 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_types). + +%%-include("emqx.hrl"). + +-export_type([zone/0, client_id/0, username/0, password/0, peername/0, + protocol/0, credentials/0]). +%%-export_type([payload/0, message/0, delivery/0]). + +-type(zone() :: atom()). +-type(client_id() :: binary() | atom()). +-type(username() :: binary() | undefined). +-type(password() :: binary() | undefined). +-type(peername() :: {inet:ip_address(), inet:port_number()}). +-type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). +-type(credentials() :: #{client_id := client_id(), + username := username(), + peername := peername(), + zone => zone(), + atom() => term()}). + +-type(payload() :: binary() | iodata()). +%-type(message() :: #message{}). +%-type(delivery() :: #delivery{}). + diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index c36b484c6..d488097bd 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -247,6 +247,7 @@ terminate(SockError, _Req, #state{keepalive = Keepalive, case Reason of undefined -> ok; + %%TODO: %%emqx_protocol:shutdown(SockError, ProtoState); _ -> ok%%emqx_protocol:shutdown(Reason, ProtoState) From 9d29dd0e1033c608f53035fcf4683ac295f3149a Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Sun, 26 Aug 2018 18:22:17 +0800 Subject: [PATCH 119/520] use config enable_acl_cache --- etc/emqx.conf | 9 +++-- priv/emqx.schema | 13 ++++++- src/emqx.app.src | 4 +-- src/emqx_access_control.erl | 12 +++---- src/emqx_acl_cache.erl | 7 +++- test/emqx_access_SUITE.erl | 70 ++++++++++++++++++++++++++----------- 6 files changed, 82 insertions(+), 33 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 62f94b174..f3f46589e 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -434,12 +434,17 @@ acl_nomatch = allow ## Value: File Name acl_file = {{ platform_etc_dir }}/acl.conf +## Whether to enable ACL cache for publish. ## The ACL cache size ## The maximum count of ACL entries allowed for a client. ## -## Value 0 disables ACL cache +## Value: on | off +enable_acl_cache = on + +## The ACL cache size +## The maximum count of ACL entries allowed for a client. ## -## Value: Integer +## Value: Integer greater than 0 ## Default: 32 acl_cache_max_size = 32 diff --git a/priv/emqx.schema b/priv/emqx.schema index f6ee7c621..765363607 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -571,6 +571,12 @@ end}. hidden ]}. +%% @doc Enable ACL cache for publish. +{mapping, "enable_acl_cache", "emqx.enable_acl_cache", [ + {default, on}, + {datatype, flag} +]}. + %% @doc ACL cache time-to-live. {mapping, "acl_cache_ttl", "emqx.acl_cache_ttl", [ {default, "1m"}, @@ -580,9 +586,14 @@ end}. %% @doc ACL cache size. {mapping, "acl_cache_max_size", "emqx.acl_cache_max_size", [ {default, 32}, - {datatype, integer} + {datatype, integer}, + {validators, ["range:gt_0"]} ]}. +{validator, "range:gt_0", "must greater than 0", + fun(X) -> X > 0 end +}. + %%-------------------------------------------------------------------- %% MQTT Protocol %%-------------------------------------------------------------------- diff --git a/src/emqx.app.src b/src/emqx.app.src index c4a8b5c2e..39d876797 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -3,8 +3,8 @@ {vsn,"3.0"}, {modules,[]}, {registered,[emqx_sup]}, - {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,cowboy, - minirest]}, + {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,cowboy + ]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 1c5d04b4e..56536501f 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -81,15 +81,15 @@ authenticate(Client, Password, [{Mod, State, _Seq} | Mods]) -> %% @doc Check ACL -spec(check_acl(client(), pubsub(), topic()) -> allow | deny). check_acl(Client, PubSub, Topic) when ?PS(PubSub) -> - CacheEnabled = (emqx_acl_cache:get_cache_max_size() =/= 0), + CacheEnabled = emqx_acl_cache:is_enabled(), check_acl(Client, PubSub, Topic, lookup_mods(acl), CacheEnabled). check_acl(Client, PubSub, Topic, AclMods, false) -> - check_acl_from_plugins(Client, PubSub, Topic, AclMods); + do_check_acl(Client, PubSub, Topic, AclMods); check_acl(Client, PubSub, Topic, AclMods, true) -> case emqx_acl_cache:get_acl_cache(PubSub, Topic) of not_found -> - AclResult = check_acl_from_plugins(Client, PubSub, Topic, AclMods), + AclResult = do_check_acl(Client, PubSub, Topic, AclMods), emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult), AclResult; AclResult -> @@ -189,13 +189,13 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -check_acl_from_plugins(#client{zone = Zone}, _PubSub, _Topic, []) -> +do_check_acl(#client{zone = Zone}, _PubSub, _Topic, []) -> emqx_zone:get_env(Zone, acl_nomatch, deny); -check_acl_from_plugins(Client, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) -> +do_check_acl(Client, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) -> case Mod:check_acl({Client, PubSub, Topic}, State) of allow -> allow; deny -> deny; - ignore -> check_acl_from_plugins(Client, PubSub, Topic, AclMods) + ignore -> do_check_acl(Client, PubSub, Topic, AclMods) end. %%-------------------------------------------------------------------- diff --git a/src/emqx_acl_cache.erl b/src/emqx_acl_cache.erl index 7db23fb1e..65e1e3305 100644 --- a/src/emqx_acl_cache.erl +++ b/src/emqx_acl_cache.erl @@ -13,6 +13,7 @@ , get_oldest_key/0 , cache_k/2 , cache_v/1 + , is_enabled/0 ]). -type(acl_result() :: allow | deny). @@ -21,6 +22,10 @@ cache_k(PubSub, Topic)-> {PubSub, Topic}. cache_v(AclResult)-> {AclResult, time_now()}. +-spec(is_enabled() -> boolean()). +is_enabled() -> + application:get_env(emqx, enable_acl_cache, true). + %% We'll cleanup the cache before repalcing an expired acl. -spec(get_acl_cache(PubSub :: publish | subscribe, Topic :: topic()) -> (acl_result() | not_found)). @@ -90,7 +95,7 @@ get_newest_key() -> keys_queue_pick(queue_rear()). get_cache_max_size() -> - application:get_env(emqx, acl_cache_max_size, 0). + application:get_env(emqx, acl_cache_max_size, 32). get_cache_size() -> case erlang:get(acl_cache_size) of diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index cf859d7c1..f88420e56 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -55,9 +55,10 @@ groups() -> put_get_del_cache, cache_update, cache_expiry, - cache_full_replacement, + cache_replacement, cache_cleanup, - cache_full_cleanup + cache_auto_emtpy, + cache_auto_cleanup ]}, {access_rule, [], [compile_rule, @@ -73,9 +74,10 @@ init_per_group(_Group, Config) -> prepare_config(Group = access_control) -> set_acl_config_file(Group), - application:set_env(emqx, acl_cache_max_size, 0); + application:set_env(emqx, enable_acl_cache, false); prepare_config(Group = access_control_cache_mode) -> set_acl_config_file(Group), + application:set_env(emqx, enable_acl_cache, true), application:set_env(emqx, acl_cache_max_size, 100). set_acl_config_file(_Group) -> @@ -162,12 +164,12 @@ acl_cache_basic(_) -> ok. acl_cache_expiry(_) -> - application:set_env(emqx, acl_cache_ttl, 1000), + application:set_env(emqx, acl_cache_ttl, 100), SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), - ct:sleep(1100), + ct:sleep(150), not_found = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), ok. @@ -186,7 +188,7 @@ acl_cache_full(_) -> 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, 1000), + application:set_env(emqx, acl_cache_ttl, 100), application:set_env(emqx, acl_cache_max_size, 2), SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, @@ -196,9 +198,10 @@ acl_cache_cleanup(_) -> allow = ?CACHE:get_acl_cache(subscribe, <<"users/testuser/1">>), allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), - ct:sleep(1100), + ct:sleep(150), %% now the cache is full and the newest one - "clients/client1" - %% should be expired, so we'll try to cleanup before putting the next cache entry + %% should be expired, so we'll empty the cache before putting + %% the next cache entry deny = ?AC:check_acl(SelfUser, subscribe, <<"#">>), not_found = ?CACHE:get_acl_cache(subscribe, <<"users/testuser/1">>), @@ -222,18 +225,18 @@ put_get_del_cache(_) -> ?assertEqual(?CACHE:cache_k(subscribe, <<"b">>), ?CACHE:get_newest_key()). cache_expiry(_) -> - application:set_env(emqx, acl_cache_ttl, 1000), + 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), allow = ?CACHE:get_acl_cache(subscribe, <<"a">>), - ct:sleep(1100), + ct:sleep(150), not_found = ?CACHE:get_acl_cache(subscribe, <<"a">>), ok = ?CACHE:put_acl_cache(subscribe, <<"a">>, deny), deny = ?CACHE:get_acl_cache(subscribe, <<"a">>), - ct:sleep(1100), + ct:sleep(150), not_found = ?CACHE:get_acl_cache(subscribe, <<"a">>). cache_update(_) -> @@ -249,12 +252,13 @@ cache_update(_) -> %% update the 2nd one ok = ?CACHE:put_acl_cache(publish, <<"b">>, allow), - %ct:pal("dump acl cache: ~p~n", [?CACHE:dump_acl_cache()]), + ct:pal("dump acl cache: ~p~n", [?CACHE:dump_acl_cache()]), 3 = ?CACHE:get_cache_size(), - ?assertEqual(?CACHE:cache_k(publish, <<"b">>), ?CACHE:get_newest_key()). + ?assertEqual(?CACHE:cache_k(publish, <<"b">>), ?CACHE:get_newest_key()), + ?assertEqual(?CACHE:cache_k(subscribe, <<"a">>), ?CACHE:get_oldest_key()). -cache_full_replacement(_) -> +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), @@ -269,40 +273,64 @@ cache_full_replacement(_) -> ok = ?CACHE:put_acl_cache(publish, <<"d">>, deny), 3 = ?CACHE:get_cache_size(), ?assertEqual(?CACHE:cache_k(publish, <<"d">>), ?CACHE:get_newest_key()), + ?assertEqual(?CACHE:cache_k(publish, <<"b">>), ?CACHE:get_oldest_key()), ok = ?CACHE:put_acl_cache(publish, <<"e">>, deny), 3 = ?CACHE:get_cache_size(), ?assertEqual(?CACHE:cache_k(publish, <<"e">>), ?CACHE:get_newest_key()), + ?assertEqual(?CACHE:cache_k(publish, <<"c">>), ?CACHE:get_oldest_key()), not_found = ?CACHE:get_acl_cache(subscribe, <<"a">>), not_found = ?CACHE:get_acl_cache(publish, <<"b">>), allow = ?CACHE:get_acl_cache(publish, <<"c">>). cache_cleanup(_) -> - application:set_env(emqx, acl_cache_ttl, 1000), + 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), ok = ?CACHE:put_acl_cache(publish, <<"b">>, allow), + ct:sleep(150), ok = ?CACHE:put_acl_cache(publish, <<"c">>, allow), 3 = ?CACHE:get_cache_size(), - ct:sleep(1100), ?CACHE:cleanup_acl_cache(), - 0 = ?CACHE:get_cache_size(). + ?assertEqual(?CACHE:cache_k(publish, <<"c">>), ?CACHE:get_oldest_key()), + 1 = ?CACHE:get_cache_size(). -cache_full_cleanup(_) -> - application:set_env(emqx, acl_cache_ttl, 1000), +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), application:set_env(emqx, acl_cache_max_size, 3), ok = ?CACHE:put_acl_cache(subscribe, <<"a">>, allow), ok = ?CACHE:put_acl_cache(publish, <<"b">>, allow), ok = ?CACHE:put_acl_cache(publish, <<"c">>, allow), 3 = ?CACHE:get_cache_size(), - ct:sleep(1100), - %% verify auto cleanup upon cache full + ct:sleep(150), ok = ?CACHE:put_acl_cache(subscribe, <<"d">>, deny), 1 = ?CACHE:get_cache_size(). +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), + application:set_env(emqx, acl_cache_max_size, 30), + ok = ?CACHE:put_acl_cache(subscribe, <<"a">>, allow), + ok = ?CACHE:put_acl_cache(publish, <<"b">>, allow), + ct:sleep(150), + ok = ?CACHE:put_acl_cache(publish, <<"c">>, allow), + ok = ?CACHE:put_acl_cache(publish, <<"d">>, deny), + 4 = ?CACHE:get_cache_size(), + + %% "a" and "b" expires, while "c" and "d" not + not_found = ?CACHE:get_acl_cache(publish, <<"b">>), + 2 = ?CACHE:get_cache_size(), + + ct:sleep(150), %% now "c" and "d" expires + not_found = ?CACHE:get_acl_cache(publish, <<"c">>), + 0 = ?CACHE:get_cache_size(). + %%-------------------------------------------------------------------- %% emqx_access_rule %%-------------------------------------------------------------------- From a369fb6960ecf353dc1c440a070453ae4d1741f9 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sun, 26 Aug 2018 22:02:39 +0800 Subject: [PATCH 120/520] fix subscribe bug --- src/emqx_broker.erl | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 24cd27ab8..7184bb1b3 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -309,18 +309,29 @@ handle_call(Req, _From, State) -> emqx_logger:error("[Broker] unexpected call: ~p", [Req]), {reply, ignored, State}. +resubscribe(From, {Subscriber, SubOpts, Topic}, State) -> + {SubPid, _} = Subscriber, + Group = maps:get(share, SubOpts, undefined), + true = do_subscribe(Group, Topic, Subscriber, SubOpts), + emqx_shared_sub:subscribe(Group, Topic, SubPid), + emqx_router:add_route(From, Topic, dest(Group)), + {noreply, monitor_subscriber(Subscriber, State)}. + + handle_cast({From, #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}}, State) -> Subscriber = {SubPid, SubId}, case ets:member(?SUBOPTION, {Topic, Subscriber}) of false -> - Group = maps:get(share, SubOpts, undefined), - true = do_subscribe(Group, Topic, Subscriber, SubOpts), - emqx_shared_sub:subscribe(Group, Topic, SubPid), - emqx_router:add_route(From, Topic, dest(Group)), - {noreply, monitor_subscriber(Subscriber, State)}; + resubscribe(From, {Subscriber, SubOpts, Topic}, State); true -> - gen_server:reply(From, ok), - {noreply, State} + case ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2) =:= SubOpts of + true -> + io:format("Ets: ~p, SubOpts: ~p", [ets:lookup_element(?SUBOPTION, Topic, Subscriber), SubOpts]), + gen_server:reply(From, ok), + {noreply, State}; + false -> + resubscribe(From, {Subscriber, SubOpts, Topic}, State) + end end; handle_cast({From, #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId}}, State) -> From e3fb147594932281bc6ff5a932a122a3ae88c535 Mon Sep 17 00:00:00 2001 From: HuangDan Date: Sun, 26 Aug 2018 22:25:54 +0800 Subject: [PATCH 121/520] Fixed test case compilation error --- Makefile | 4 +-- erlang.mk | 2 +- test/emqx_SUITE.erl | 45 +++++++++++++++-------------- test/emqx_access_SUITE.erl | 8 +++--- test/emqx_broker_SUITE.erl | 4 +-- test/emqx_frame_SUITE.erl | 6 ++-- test/emqx_mqueue_SUITE.erl | 58 +++++++++++++++++++------------------- 7 files changed, 63 insertions(+), 64 deletions(-) diff --git a/Makefile b/Makefile index 7b1cc1cdd..58deb4638 100644 --- a/Makefile +++ b/Makefile @@ -24,14 +24,14 @@ BUILD_DEPS = cuttlefish dep_cuttlefish = git https://github.com/emqx/cuttlefish emqx30 TEST_DEPS = emqx_ct_helplers -dep_emqx_ct_helplers = git git@github.com:emqx/emqx_ct_helpers +dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers TEST_ERLC_OPTS += +debug_info TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose -CT_SUITES = emqx_inflight +CT_SUITES = emqx_stats ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat #CT_SUITES = emqx emqx_broker emqx_mod emqx_lib emqx_topic emqx_mqueue emqx_inflight \ diff --git a/erlang.mk b/erlang.mk index e348d4493..f38d22653 100644 --- a/erlang.mk +++ b/erlang.mk @@ -2174,7 +2174,7 @@ help:: CT_RUN = ct_run \ -no_auto_compile \ -noinput \ - -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \ + -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(DEPS_DIR)/gen_rpc/_build/dev/lib/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \ -dir $(TEST_DIR) \ -logdir $(CURDIR)/logs diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index e3e0bbb38..752c40f5c 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -19,8 +19,6 @@ -compile(export_all). -compile(nowarn_export_all). --include_lib("emqttc/include/emqttc_packet.hrl"). - -define(APP, emqx). -include_lib("eunit/include/eunit.hrl"). @@ -38,10 +36,11 @@ all() -> groups() -> [{connect, [non_parallel_tests], [mqtt_connect, - mqtt_connect_with_tcp, +% mqtt_connect_with_tcp, mqtt_connect_with_ssl_oneway, - mqtt_connect_with_ssl_twoway, - mqtt_connect_with_ws]}, + mqtt_connect_with_ssl_twoway%, + % mqtt_connect_with_ws + ]}, {cleanSession, [sequence], [cleanSession_validate] } @@ -72,15 +71,15 @@ connect_broker_(Packet, RecvSize) -> gen_tcp:close(Sock), Data. -mqtt_connect_with_tcp(_) -> - %% Issue #599 - %% Empty clientId and clean_session = false - {ok, Sock} = gen_tcp:connect({127,0,0,1}, 1883, [binary, {packet, raw}, {active, false}]), - Packet = raw_send_serialise(?CLIENT), - gen_tcp:send(Sock, Packet), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(Data), - gen_tcp:close(Sock). +%% mqtt_connect_with_tcp(_) -> +%% %% Issue #599 +%% %% Empty clientId and clean_session = false +%% {ok, Sock} = gen_tcp:connect({127,0,0,1}, 1883, [binary, {packet, raw}, {active, false}]), +%% Packet = raw_send_serialise(?CLIENT), +%% gen_tcp:send(Sock, Packet), +%% {ok, Data} = gen_tcp:recv(Sock, 0), +%% % {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(Data), +%% gen_tcp:close(Sock). mqtt_connect_with_ssl_oneway(_) -> emqx:stop(), @@ -127,15 +126,15 @@ mqtt_connect_with_ssl_twoway(_Config) -> emqttc:disconnect(SslTwoWay), emqttc:disconnect(Sub). -mqtt_connect_with_ws(_Config) -> - WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()), - {ok, _} = rfc6455_client:open(WS), - Packet = raw_send_serialise(?CLIENT), - ok = rfc6455_client:send_binary(WS, Packet), - {binary, P} = rfc6455_client:recv(WS), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(P), - {close, _} = rfc6455_client:close(WS), - ok. +%% mqtt_connect_with_ws(_Config) -> +%% WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()), +%% {ok, _} = rfc6455_client:open(WS), +%% Packet = raw_send_serialise(?CLIENT), +%% ok = rfc6455_client:send_binary(WS, Packet), +%% {binary, P} = rfc6455_client:recv(WS), +%% % {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(P), +%% {close, _} = rfc6455_client:close(WS), +%% ok. cleanSession_validate(_) -> {ok, C1} = emqttc:start_link([{host, "localhost"}, diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index 270d55f57..2c3ebabdc 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -119,8 +119,8 @@ unregister_mod(_) -> [] = ?AC:lookup_mods(auth). check_acl(_) -> - User1 = #client{client_id = <<"client1">>, username = <<"testuser">>}, - User2 = #client{client_id = <<"client2">>, username = <<"xyz">>}, + User1 = #client{id = <<"client1">>, username = <<"testuser">>}, + User2 = #client{id = <<"client2">>, username = <<"xyz">>}, allow = ?AC:check_acl(User1, subscribe, <<"users/testuser/1">>), allow = ?AC:check_acl(User1, subscribe, <<"clients/client1">>), allow = ?AC:check_acl(User1, subscribe, <<"clients/client1/x/y">>), @@ -159,8 +159,8 @@ compile_rule(_) -> {deny, all} = compile({deny, all}). match_rule(_) -> - User = #client{peername = {{127,0,0,1}, 2948}, client_id = <<"testClient">>, username = <<"TestUser">>}, - User2 = #client{peername = {{192,168,0,10}, 3028}, client_id = <<"testClient">>, username = <<"TestUser">>}, + User = #client{peername = {{127,0,0,1}, 2948}, id = <<"testClient">>, username = <<"TestUser">>}, + User2 = #client{peername = {{192,168,0,10}, 3028}, id = <<"testClient">>, username = <<"TestUser">>}, {matched, allow} = match(User, <<"Test/Topic">>, {allow, all}), {matched, deny} = match(User, <<"Test/Topic">>, {deny, all}), diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index e77d689d4..917143c3a 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -56,7 +56,7 @@ init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), Config. -end_per_suite(Config) -> +end_per_suite(_Config) -> emqx_ct_broker_helpers:run_teardown_steps(). %%-------------------------------------------------------------------- @@ -149,7 +149,7 @@ start_session(_) -> {ok, ClientPid} = emqx_mock_client:start_link(<<"clientId">>), {ok, SessPid} = emqx_mock_client:start_session(ClientPid), Message = emqx_message:make(<<"clientId">>, 2, <<"topic">>, <<"hello">>), - Message1 = Message#mqtt_message{packet_id = 1}, + Message1 = Message#message{id = 1}, emqx_session:publish(SessPid, Message1), emqx_session:pubrel(SessPid, 1), emqx_session:subscribe(SessPid, [{<<"topic/session">>, [{qos, 2}]}]), diff --git a/test/emqx_frame_SUITE.erl b/test/emqx_frame_SUITE.erl index c4ec83024..49ffa40c4 100644 --- a/test/emqx_frame_SUITE.erl +++ b/test/emqx_frame_SUITE.erl @@ -331,14 +331,14 @@ serialize_parse_pubcomp_v5(_) -> 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>>, - TopicFilters = [{<<"TopicA">>, #mqtt_subopts{qos = 2}}], + TopicFilters = [{<<"TopicA">>, #{qos => 2}}], Packet = ?SUBSCRIBE_PACKET(2, TopicFilters), ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), ?assertEqual({ok, Packet, <<>>}, parse(Bin)). serialize_parse_subscribe_v5(_) -> - TopicFilters = [{<<"TopicQos0">>, #mqtt_subopts{rh = 1, qos = ?QOS_0}}, - {<<"TopicQos1">>, #mqtt_subopts{rh = 1, qos =?QOS_1}}], + TopicFilters = [{<<"TopicQos0">>, #{rh => 1, qos => ?QOS_0}}, + {<<"TopicQos1">>, #{rh => 1, qos => ?QOS_1}}], Packet = ?SUBSCRIBE_PACKET(1, #{'Subscription-Identifier' => 16#FFFFFFF}, TopicFilters), ?assertEqual({ok, Packet, <<>>}, diff --git a/test/emqx_mqueue_SUITE.erl b/test/emqx_mqueue_SUITE.erl index d174d980e..5ab510633 100644 --- a/test/emqx_mqueue_SUITE.erl +++ b/test/emqx_mqueue_SUITE.erl @@ -35,22 +35,22 @@ t_in(_) -> {store_qos0, true}], Q = ?Q:new(<<"testQ">>, Opts, alarm_fun()), ?assert(?Q:is_empty(Q)), - Q1 = ?Q:in(#mqtt_message{}, Q), + Q1 = ?Q:in(#message{}, Q), ?assertEqual(1, ?Q:len(Q1)), - Q2 = ?Q:in(#mqtt_message{qos = 1}, Q1), + Q2 = ?Q:in(#message{qos = 1}, Q1), ?assertEqual(2, ?Q:len(Q2)), - Q3 = ?Q:in(#mqtt_message{qos = 2}, Q2), - Q4 = ?Q:in(#mqtt_message{}, Q3), - Q5 = ?Q:in(#mqtt_message{}, Q4), + Q3 = ?Q:in(#message{qos = 2}, Q2), + Q4 = ?Q:in(#message{}, Q3), + Q5 = ?Q:in(#message{}, Q4), ?assertEqual(5, ?Q:len(Q5)). t_in_qos0(_) -> Opts = [{max_length, 5}, {store_qos0, false}], Q = ?Q:new(<<"testQ">>, Opts, alarm_fun()), - Q1 = ?Q:in(#mqtt_message{}, Q), + Q1 = ?Q:in(#message{}, Q), ?assert(?Q:is_empty(Q1)), - Q2 = ?Q:in(#mqtt_message{qos = 0}, Q1), + Q2 = ?Q:in(#message{qos = 0}, Q1), ?assert(?Q:is_empty(Q2)). t_out(_) -> @@ -58,10 +58,10 @@ t_out(_) -> {store_qos0, true}], Q = ?Q:new(<<"testQ">>, Opts, alarm_fun()), {empty, Q} = ?Q:out(Q), - Q1 = ?Q:in(#mqtt_message{}, Q), + Q1 = ?Q:in(#message{}, Q), {Value, Q2} = ?Q:out(Q1), ?assertEqual(0, ?Q:len(Q2)), - ?assertEqual({value, #mqtt_message{}}, Value). + ?assertEqual({value, #message{}}, Value). t_simple_mqueue(_) -> Opts = [{type, simple}, @@ -74,13 +74,13 @@ t_simple_mqueue(_) -> ?assertEqual(3, ?Q:max_len(Q)), ?assertEqual(<<"simple_queue">>, ?Q:name(Q)), ?assert(?Q:is_empty(Q)), - Q1 = ?Q:in(#mqtt_message{qos = 1, payload = <<"1">>}, Q), - Q2 = ?Q:in(#mqtt_message{qos = 1, payload = <<"2">>}, Q1), - Q3 = ?Q:in(#mqtt_message{qos = 1, payload = <<"3">>}, Q2), - Q4 = ?Q:in(#mqtt_message{qos = 1, payload = <<"4">>}, Q3), + Q1 = ?Q:in(#message{qos = 1, payload = <<"1">>}, Q), + Q2 = ?Q:in(#message{qos = 1, payload = <<"2">>}, Q1), + Q3 = ?Q:in(#message{qos = 1, payload = <<"3">>}, Q2), + Q4 = ?Q:in(#message{qos = 1, payload = <<"4">>}, Q3), ?assertEqual(3, ?Q:len(Q4)), {{value, Msg}, Q5} = ?Q:out(Q4), - ?assertEqual(<<"2">>, Msg#mqtt_message.payload), + ?assertEqual(<<"2">>, Msg#message.payload), ?assertEqual([{len, 2}, {max_len, 3}, {dropped, 1}], ?Q:stats(Q5)). t_infinity_simple_mqueue(_) -> @@ -93,12 +93,12 @@ t_infinity_simple_mqueue(_) -> ?assert(?Q:is_empty(Q)), ?assertEqual(0, ?Q:max_len(Q)), Qx = lists:foldl(fun(I, AccQ) -> - ?Q:in(#mqtt_message{qos = 1, payload = iolist_to_binary([I])}, AccQ) + ?Q:in(#message{qos = 1, payload = iolist_to_binary([I])}, AccQ) end, Q, lists:seq(1, 255)), ?assertEqual(255, ?Q:len(Qx)), ?assertEqual([{len, 255}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)), {{value, V}, _Qy} = ?Q:out(Qx), - ?assertEqual(<<1>>, V#mqtt_message.payload). + ?assertEqual(<<1>>, V#message.payload). t_priority_mqueue(_) -> Opts = [{type, priority}, @@ -113,18 +113,18 @@ t_priority_mqueue(_) -> ?assertEqual(<<"priority_queue">>, ?Q:name(Q)), ?assert(?Q:is_empty(Q)), - Q1 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t1">>}, Q), - Q2 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t">>}, Q1), - Q3 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t2">>}, Q2), + Q1 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q), + Q2 = ?Q:in(#message{qos = 1, topic = <<"t">>}, Q1), + Q3 = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q2), ?assertEqual(3, ?Q:len(Q3)), - Q4 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t1">>}, Q3), + Q4 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q3), ?assertEqual(4, ?Q:len(Q4)), - Q5 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t1">>}, Q4), + Q5 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q4), ?assertEqual(5, ?Q:len(Q5)), - Q6 = ?Q:in(#mqtt_message{qos = 1, topic = <<"t1">>}, Q5), + Q6 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q5), ?assertEqual(5, ?Q:len(Q6)), {{value, Msg}, _Q7} = ?Q:out(Q6), - ?assertEqual(<<"t">>, Msg#mqtt_message.topic). + ?assertEqual(<<"t">>, Msg#message.topic). t_infinity_priority_mqueue(_) -> Opts = [{type, priority}, @@ -135,8 +135,8 @@ t_infinity_priority_mqueue(_) -> ?assertEqual(0, ?Q:max_len(Q)), Qx = lists:foldl(fun(I, AccQ) -> AccQ1 = - ?Q:in(#mqtt_message{topic = <<"t1">>, qos = 1, payload = iolist_to_binary([I])}, AccQ), - ?Q:in(#mqtt_message{topic = <<"t">>, qos = 1, payload = iolist_to_binary([I])}, AccQ1) + ?Q:in(#message{topic = <<"t1">>, qos = 1, payload = iolist_to_binary([I])}, AccQ), + ?Q:in(#message{topic = <<"t">>, qos = 1, payload = iolist_to_binary([I])}, AccQ1) end, Q, lists:seq(1, 255)), ?assertEqual(510, ?Q:len(Qx)), ?assertEqual([{len, 510}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)). @@ -149,10 +149,10 @@ t_priority_mqueue2(_) -> {store_qos0, false}], Q = ?Q:new("priority_queue2_test", Opts, alarm_fun()), ?assertEqual(2, ?Q:max_len(Q)), - Q1 = ?Q:in(#mqtt_message{topic = <<"x">>, qos = 1, payload = <<1>>}, Q), - Q2 = ?Q:in(#mqtt_message{topic = <<"x">>, qos = 1, payload = <<2>>}, Q1), - Q3 = ?Q:in(#mqtt_message{topic = <<"y">>, qos = 1, payload = <<3>>}, Q2), - Q4 = ?Q:in(#mqtt_message{topic = <<"y">>, qos = 1, payload = <<4>>}, Q3), + 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), ?assertEqual(4, ?Q:len(Q4)), {{value, _Val}, Q5} = ?Q:out(Q4), ?assertEqual(3, ?Q:len(Q5)). From 3f811aa9cad9a7af55587a004a003cc9e126bc2e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 27 Aug 2018 09:28:49 +0800 Subject: [PATCH 122/520] Add credentials/1 function --- src/emqx_protocol.erl | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 3faa7781a..f2451e474 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -18,6 +18,7 @@ -include("emqx_mqtt.hrl"). -export([init/2, info/1, caps/1, stats/1]). +-export([credentials/1]). -export([client/1, client_id/1]). -export([session/1]). -export([parser/1]). @@ -134,6 +135,15 @@ info(#pstate{zone = Zone, caps(#pstate{zone = Zone}) -> emqx_mqtt_caps:get_caps(Zone). +credentials(#pstate{zone = Zone, + client_id = ClientId, + username = Username, + peername = Peername}) -> + #{zone => Zone, + client_id => ClientId, + username => Username, + peername => Peername}. + client(#pstate{zone = Zone, client_id = ClientId, client_pid = ClientPid, @@ -219,7 +229,7 @@ process(?CONNECT_PACKET( connack( case check_connect(Connect, PState1) of {ok, PState2} -> - case authenticate(client(PState2), Password) of + case authenticate(credentials(PState2), Password) of {ok, IsSuper} -> %% Maybe assign a clientId PState3 = maybe_assign_client_id(PState2#pstate{is_super = IsSuper}), @@ -449,8 +459,8 @@ try_open_session(#pstate{zone = Zone, Other -> Other end. -authenticate(Client, Password) -> - case emqx_access_control:authenticate(Client, Password) of +authenticate(Credentials, Password) -> + case emqx_access_control:authenticate(Credentials, Password) of ok -> {ok, false}; {ok, IsSuper} -> {ok, IsSuper}; {error, Error} -> {error, Error} @@ -511,7 +521,7 @@ check_pub_acl(_Packet, #pstate{is_super = IsSuper, enable_acl = EnableAcl}) ok; check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, PState) -> - case emqx_access_control:check_acl(client(PState), publish, Topic) of + case emqx_access_control:check_acl(credentials(PState), publish, Topic) of allow -> ok; deny -> {error, ?RC_NOT_AUTHORIZED} end. @@ -541,10 +551,10 @@ check_sub_acl(TopicFilters, #pstate{is_super = IsSuper, enable_acl = EnableAcl}) {ok, TopicFilters}; check_sub_acl(TopicFilters, PState) -> - Client = client(PState), + Credentials = credentials(PState), lists:foldr( fun({Topic, SubOpts}, {Ok, Acc}) -> - case emqx_access_control:check_acl(Client, subscribe, Topic) of + case emqx_access_control:check_acl(Credentials, subscribe, Topic) of allow -> {Ok, [{Topic, SubOpts}|Acc]}; deny -> {error, [{Topic, SubOpts#{rc := ?RC_NOT_AUTHORIZED}}|Acc]} end From 1448515e649fd2c0b86ae930a7b6d2486b8cfa5d Mon Sep 17 00:00:00 2001 From: turtled Date: Mon, 27 Aug 2018 10:14:58 +0800 Subject: [PATCH 123/520] Fix websocket bug --- src/emqx_protocol.erl | 3 +++ src/emqx_ws_connection.erl | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 3faa7781a..3f233e0b1 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -419,6 +419,9 @@ send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = SendFun ok -> emqx_metrics:sent(Packet), {ok, inc_stats(send, Type, PState)}; + {binary, _Data} -> + emqx_metrics:sent(Packet), + {ok, inc_stats(send, Type, PState)}; {error, Reason} -> {error, Reason} end. diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index c36b484c6..ace4ad0d2 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -200,7 +200,7 @@ websocket_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> websocket_info(emit_stats, State = #state{proto_state = ProtoState}) -> Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(), emqx_protocol:stats(ProtoState)]), - emqx_cm:set_client_stats(emqx_protocol:clientid(ProtoState), Stats), + emqx_cm:set_client_stats(emqx_protocol:client_id(ProtoState), Stats), {ok, State#state{stats_timer = undefined}, hibernate}; websocket_info({keepalive, start, Interval}, State) -> @@ -240,7 +240,7 @@ websocket_info(Info, State) -> {ok, State}. terminate(SockError, _Req, #state{keepalive = Keepalive, - proto_state = ProtoState, + proto_state = _ProtoState, shutdown_reason = Reason}) -> emqx_keepalive:cancel(Keepalive), io:format("Websocket shutdown for ~p, sockerror: ~p~n", [Reason, SockError]), From 95d36d02045b04da54aee3a49a61c50cc28d7bea Mon Sep 17 00:00:00 2001 From: turtled Date: Mon, 27 Aug 2018 10:15:41 +0800 Subject: [PATCH 124/520] Fix share sub bug --- include/emqx.hrl | 2 +- src/emqx_broker.erl | 10 ++++++---- src/emqx_shared_sub.erl | 2 +- src/emqx_topic.erl | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/include/emqx.hrl b/include/emqx.hrl index 0372f547e..1e2541f65 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -54,7 +54,7 @@ -type(subid() :: binary() | atom()). -type(subopts() :: #{qos => integer(), - share => '$queue' | binary(), + share => binary(), atom() => term()}). -record(subscription, { diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 7015590d8..a941367c4 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -183,16 +183,18 @@ route([{To, Node}], Delivery) when Node =:= node() -> route([{To, Node}], Delivery = #delivery{flows = Flows}) when is_atom(Node) -> forward(Node, To, Delivery#delivery{flows = [{route, Node, To}|Flows]}); -route([{To, Shared}], Delivery) when is_tuple(Shared); is_binary(Shared) -> - emqx_shared_sub:dispatch(Shared, To, Delivery); +route([{To, Group}], Delivery) when is_tuple(Group); is_binary(Group) -> + emqx_shared_sub:dispatch(Group, To, Delivery); route(Routes, Delivery) -> lists:foldl(fun(Route, Acc) -> route([Route], Acc) end, Delivery, Routes). aggre([]) -> []; -aggre([#route{topic = To, dest = Dest}]) -> - [{To, Dest}]; +aggre([#route{topic = To, dest = Node}]) when is_atom(Node) -> + [{To, Node}]; +aggre([#route{topic = To, dest = {Group, _Node}}]) -> + [{To, Group}]; aggre(Routes) -> lists:foldl( fun(#route{topic = To, dest = Node}, Acc) when is_atom(Node) -> diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index e8a9fac10..7a70fca59 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -81,7 +81,7 @@ record(Group, Topic, SubPid) -> #emqx_shared_subscription{group = Group, topic = Topic, subpid = SubPid}. %% TODO: dispatch strategy, ensure the delivery... -dispatch({Group, _Node}, Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> +dispatch(Group, Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> case pick(subscribers(Group, Topic)) of false -> Delivery; SubPid -> SubPid ! {dispatch, Topic, Msg}, diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index 74a405f65..b122c114b 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -185,7 +185,7 @@ parse(Topic = <<"$queue/", _/binary>>, #{share := _Group}) -> parse(Topic = <<"$share/", _/binary>>, #{share := _Group}) -> error({invalid_topic, Topic}); parse(<<"$queue/", Topic1/binary>>, Options) -> - parse(Topic1, maps:put(share, '$queue', Options)); + parse(Topic1, maps:put(share, <<"$queue">>, Options)); parse(<<"$share/", Topic1/binary>>, Options) -> [Group, Topic2] = binary:split(Topic1, <<"/">>), {Topic2, maps:put(share, Group, Options)}; From 6e8635394e21442004a2362ca564fe396d6ad124 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 27 Aug 2018 10:25:15 +0800 Subject: [PATCH 125/520] Use map to replace #state{} record --- src/emqx_acl_internal.erl | 45 +++++++++++++++++++-------------------- src/emqx_types.erl | 1 + 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index 1ba5c93df..d226fb496 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -20,13 +20,11 @@ -export([all_rules/0]). -%% ACL callbacks +%% ACL mod callbacks -export([init/1, check_acl/2, reload_acl/1, description/0]). -define(ACL_RULE_TAB, emqx_acl_rule). --record(state, {acl_file}). - %%------------------------------------------------------------------------------ %% API %%------------------------------------------------------------------------------ @@ -43,21 +41,20 @@ all_rules() -> %% ACL callbacks %%------------------------------------------------------------------------------ -%% @doc Init internal ACL --spec(init([File :: string()]) -> {ok, State :: term()}). +-spec(init([File :: string()]) -> {ok, #{}}). init([File]) -> _ = emqx_tables:new(?ACL_RULE_TAB, [set, public, {read_concurrency, true}]), - {ok, load_rules_from_file(#state{acl_file = File})}. + true = load_rules_from_file(File), + {ok, #{acl_file => File}}. -load_rules_from_file(State = #state{acl_file = AclFile}) -> +load_rules_from_file(AclFile) -> {ok, Terms} = file:consult(AclFile), Rules = [emqx_access_rule:compile(Term) || Term <- Terms], lists:foreach(fun(PubSub) -> ets:insert(?ACL_RULE_TAB, {PubSub, lists:filter(fun(Rule) -> filter(PubSub, Rule) end, Rules)}) end, [publish, subscribe]), - ets:insert(?ACL_RULE_TAB, {all_rules, Terms}), - State. + ets:insert(?ACL_RULE_TAB, {all_rules, Terms}). filter(_PubSub, {allow, all}) -> true; @@ -73,11 +70,11 @@ filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) -> false. %% @doc Check ACL --spec(check_acl({credentials(), pubsub(), topic()}, #state{}) +-spec(check_acl({credentials(), pubsub(), topic()}, #{}) -> allow | deny | ignore). -check_acl(_Who, #state{acl_file = undefined}) -> +check_acl(_Who, #{acl_file := undefined}) -> allow; -check_acl({Credentials, PubSub, Topic}, #state{}) -> +check_acl({Credentials, PubSub, Topic}, #{}) -> case match(Credentials, Topic, lookup(PubSub)) of {matched, allow} -> allow; {matched, deny} -> deny; @@ -94,22 +91,24 @@ match(_Credentials, _Topic, []) -> nomatch; match(Credentials, Topic, [Rule|Rules]) -> case emqx_access_rule:match(Credentials, Topic, Rule) of - nomatch -> match(Credentials, Topic, Rules); - {matched, AllowDeny} -> {matched, AllowDeny} + nomatch -> + match(Credentials, Topic, Rules); + {matched, AllowDeny} -> + {matched, AllowDeny} end. --spec(reload_acl(#state{}) -> ok | {error, term()}). -reload_acl(#state{acl_file = undefined}) -> +-spec(reload_acl(#{}) -> ok | {error, term()}). +reload_acl(#{acl_file := undefined}) -> ok; -reload_acl(State) -> - case catch load_rules_from_file(State) of - - {'EXIT', Error} -> {error, Error}; - #state{config=File} -> - io:format("reload acl_internal successfully: ~p~n", [File]), - ok +reload_acl(#{acl_file := AclFile}) -> + case catch load_rules_from_file(AclFile) of + true -> emqx_logger:error("Reload acl_file ~s successfully", [AclFile]), + ok; + {'EXIT', Error} -> + {error, Error} end. -spec(description() -> string()). description() -> "Internal ACL with etc/acl.conf". + diff --git a/src/emqx_types.erl b/src/emqx_types.erl index de1f5df4b..eeca513a6 100644 --- a/src/emqx_types.erl +++ b/src/emqx_types.erl @@ -18,6 +18,7 @@ -export_type([zone/0, client_id/0, username/0, password/0, peername/0, protocol/0, credentials/0]). +-export_type([payload/0]). %%-export_type([payload/0, message/0, delivery/0]). -type(zone() :: atom()). From f0f1456168303c4298e308bb146e5e81ffb9c2f7 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Mon, 27 Aug 2018 11:21:16 +0800 Subject: [PATCH 126/520] fix duplicated subscribers with same topic --- src/emqx_broker.erl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 8fed40b7f..560c095cf 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -379,9 +379,18 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ +insert_subscriber(Group, Topic, Subscriber) -> + Subscribers = subscribers(Topic), + case lists:member(Subscriber, Subscribers) of + false -> + ets:insert(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}); + _ -> + ok + end. + do_subscribe(Group, Topic, Subscriber, SubOpts) -> ets:insert(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), - ets:insert(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}), + insert_subscriber(Group, Topic, Subscriber), ets:insert(?SUBOPTION, {{Topic, Subscriber}, SubOpts}). do_unsubscribe(Group, Topic, Subscriber) -> From 28e22825ba97cd510f50ae2cb3821a76b1313372 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Mon, 27 Aug 2018 12:35:36 +0800 Subject: [PATCH 127/520] fix emqx subscriptions error --- src/emqx.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx.erl b/src/emqx.erl index c39dde931..9ac9227ed 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -116,9 +116,9 @@ topics() -> emqx_router:topics(). subscribers(Topic) -> emqx_broker:subscribers(iolist_to_binary(Topic)). --spec(subscriptions(subscriber() | string()) -> [{topic(), subopts()}]). +-spec(subscriptions(subscriber()) -> [{topic(), subopts()}]). subscriptions(Subscriber) -> - emqx_broker:subscriptions(list_to_subid(Subscriber)). + emqx_broker:subscriptions(Subscriber). -spec(subscribed(topic() | string(), subscriber()) -> boolean()). subscribed(Topic, Subscriber) -> From 91eb79967c04f3bc4411eec497791590d2e7beb5 Mon Sep 17 00:00:00 2001 From: turtled Date: Mon, 27 Aug 2018 13:50:38 +0800 Subject: [PATCH 128/520] Add syslog --- Makefile | 3 ++- etc/emqx.conf | 2 +- priv/emqx.schema | 27 +++++++++++++-------------- src/emqx.app.src | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 58deb4638..82e20b12d 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker PROJECT_VERSION = 3.0 -DEPS = jsx gproc gen_rpc lager ekka esockd cowboy clique +DEPS = jsx gproc gen_rpc lager ekka esockd cowboy clique lager_syslog dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 @@ -14,6 +14,7 @@ dep_esockd = git https://github.com/emqx/esockd emqx30 dep_ekka = git https://github.com/emqx/ekka emqx30 dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 dep_clique = git https://github.com/emqx/clique +dep_lager_syslog = git https://github.com/basho/lager_syslog 3.0.1 NO_AUTOPATCH = gen_rpc cuttlefish diff --git a/etc/emqx.conf b/etc/emqx.conf index f3f46589e..e16cb6c49 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -412,7 +412,7 @@ log.syslog = on ## Sets the severity level for syslog. ## ## Value: debug | info | notice | warning | error | critical | alert | emergency -## log.syslog.level = error +log.syslog.level = error ##-------------------------------------------------------------------- ## Authentication/Access Control diff --git a/priv/emqx.schema b/priv/emqx.schema index 765363607..adbbd8823 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -442,12 +442,12 @@ end}. ]}. {mapping, "log.syslog", "lager.handlers", [ - %%{default, off}, + {default, off}, {datatype, flag} ]}. {mapping, "log.syslog.identity", "lager.handlers", [ - {default, "emqx"}, + {default, "emqttd"}, {datatype, string} ]}. @@ -456,10 +456,10 @@ end}. {datatype, {enum, [daemon, local0, local1, local2, local3, local4, local5, local6, local7]}} ]}. -%%{mapping, "log.syslog.level", "lager.handlers", [ -%% {default, error}, -%% {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency]}} -%%]}. +{mapping, "log.syslog.level", "lager.handlers", [ + {default, error}, + {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency]}} +]}. {mapping, "log.error.redirect", "lager.error_logger_redirect", [ {default, on}, @@ -511,14 +511,13 @@ end}. both -> [ConsoleHandler, ConsoleFileHandler]; _ -> [] end, - SyslogHandler = [], - %%case cuttlefish:conf_get("log.syslog", Conf, false) of - %% false -> []; - %% true -> [{lager_syslog_backend, - %% [cuttlefish:conf_get("log.syslog.identity", Conf), - %% cuttlefish:conf_get("log.syslog.facility", Conf), - %% cuttlefish:conf_get("log.syslog.level", Conf)]}] - %%end, + SyslogHandler = case cuttlefish:conf_get("log.syslog", Conf) of + false -> []; + true -> [{lager_syslog_backend, + [cuttlefish:conf_get("log.syslog.identity", Conf), + cuttlefish:conf_get("log.syslog.facility", Conf), + cuttlefish:conf_get("log.syslog.level", Conf)]}] + end, ConsoleHandlers ++ ErrorHandler ++ InfoHandler ++ SyslogHandler end }. diff --git a/src/emqx.app.src b/src/emqx.app.src index 39d876797..d44707186 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -3,8 +3,8 @@ {vsn,"3.0"}, {modules,[]}, {registered,[emqx_sup]}, - {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,cowboy - ]}, + {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd, + cowboy,lager_syslog]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, From 2e4fb3f94603b7a8458dc799190bf46b445edc66 Mon Sep 17 00:00:00 2001 From: turtled Date: Mon, 27 Aug 2018 13:58:08 +0800 Subject: [PATCH 129/520] emqttd -> emqx --- priv/emqx.schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index adbbd8823..bfadd1b66 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -447,7 +447,7 @@ end}. ]}. {mapping, "log.syslog.identity", "lager.handlers", [ - {default, "emqttd"}, + {default, "emqx"}, {datatype, string} ]}. From 087bfe80c8d43d2eb917ca5052c2af494b9b5136 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Mon, 27 Aug 2018 15:24:40 +0800 Subject: [PATCH 130/520] fix unsubscribe emqx_mock_client error --- src/emqx.erl | 7 +++++-- test/emqx_mock_client.erl | 30 ++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/emqx.erl b/src/emqx.erl index 9ac9227ed..54f1952f4 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -94,8 +94,11 @@ unsubscribe(Topic) -> emqx_broker:unsubscribe(iolist_to_binary(Topic)). -spec(unsubscribe(topic() | string(), subscriber() | string()) -> ok | {error, term()}). -unsubscribe(Topic, Subscriber) -> - emqx_broker:unsubscribe(iolist_to_binary(Topic), list_to_subid(Subscriber)). +unsubscribe(Topic, Sub) when is_list(Sub) -> + emqx_broker:unsubscribe(iolist_to_binary(Topic)); +unsubscribe(Topic, Subscriber) when is_tuple(Subscriber) -> + {SubPid, SubId} = Subscriber, + emqx_broker:unsubscribe(iolist_to_binary(Topic), SubPid, SubId). %%-------------------------------------------------------------------- %% PubSub management API diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index 2b18c348f..e76e5551c 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -18,28 +18,42 @@ -behaviour(gen_server). --export([start_link/1, start_session/1, stop/1]). +-export([start_link/1, open_session/3, stop/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {clientid, session}). +-record(state, {clean_start, client_id, client_pid}). start_link(ClientId) -> gen_server:start_link(?MODULE, [ClientId], []). -start_session(CPid) -> - gen_server:call(CPid, start_session). +open_session(ClientPid, ClientId, Zone) -> + gen_server:call(ClientPid, {start_session, ClientPid, ClientId, Zone}). stop(CPid) -> gen_server:call(CPid, stop). init([ClientId]) -> - {ok, #state{clientid = ClientId}}. + {ok, + #state{clean_start = true, + client_id = ClientId} + }. -handle_call(start_session, _From, State = #state{clientid = ClientId}) -> - {ok, SessPid, _} = emqx_sm:start_session(true, {ClientId, undefined}), - {reply, {ok, SessPid}, State#state{session = SessPid}}; +handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> + Attrs = #{ zone => Zone, + client_id => ClientId, + client_pid => ClientPid, + clean_start => true, + username => undefined, + conn_props => undefined + }, + {ok, SessPid} = emqx_sm:open_session(Attrs), + {reply, {ok, SessPid}, State#state{ + clean_start = true, + client_id = ClientId, + client_pid = ClientPid + }}; handle_call(stop, _From, State) -> {stop, normal, ok, State}; From dca292f5387e621676aae0acc241e323c8271eb8 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 27 Aug 2018 15:54:41 +0800 Subject: [PATCH 131/520] Replace 'Client' with 'Credentials' map --- src/emqx_access_control.erl | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 3bb31057e..bd35d7ebc 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -32,8 +32,6 @@ -define(TAB, ?MODULE). -define(SERVER, ?MODULE). --record(state, {}). - %%------------------------------------------------------------------------------ %% API %%------------------------------------------------------------------------------ @@ -104,6 +102,15 @@ check_acl(Credentials, PubSub, Topic, AclMods, true) -> AclResult end. +do_check_acl(#{zone := Zone}, _PubSub, _Topic, []) -> + emqx_zone:get_env(Zone, acl_nomatch, deny); +do_check_acl(Credentials, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) -> + case Mod:check_acl({Credentials, PubSub, Topic}, State) of + allow -> allow; + deny -> deny; + ignore -> do_check_acl(Credentials, PubSub, Topic, AclMods) + end. + -spec(reload_acl() -> list(ok | {error, term()})). reload_acl() -> [Mod:reload_acl(State) || {Mod, State, _Seq} <- lookup_mods(acl)]. @@ -143,7 +150,7 @@ stop() -> init([]) -> _ = emqx_tables:new(?TAB, [set, protected, {read_concurrency, true}]), - {ok, #state{}}. + {ok, #{}}. handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) -> Mods = lookup_mods(Type), @@ -194,15 +201,6 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -do_check_acl(#client{zone = Zone}, _PubSub, _Topic, []) -> - emqx_zone:get_env(Zone, acl_nomatch, deny); -do_check_acl(Client, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) -> - case Mod:check_acl({Client, PubSub, Topic}, State) of - allow -> allow; - deny -> deny; - ignore -> do_check_acl(Client, PubSub, Topic, AclMods) - end. - %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- From 42288ac4129004f52bdf85072fbac50452d9a64a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 27 Aug 2018 15:57:13 +0800 Subject: [PATCH 132/520] The ACL file should not be undefined --- src/emqx_acl_internal.erl | 14 ++--- src/emqx_listeners.erl | 113 +++++++++++++++++++++++++------------- 2 files changed, 80 insertions(+), 47 deletions(-) diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index d226fb496..54f944416 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -70,11 +70,8 @@ filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) -> false. %% @doc Check ACL --spec(check_acl({credentials(), pubsub(), topic()}, #{}) - -> allow | deny | ignore). -check_acl(_Who, #{acl_file := undefined}) -> - allow; -check_acl({Credentials, PubSub, Topic}, #{}) -> +-spec(check_acl({credentials(), pubsub(), topic()}, #{}) -> allow | deny | ignore). +check_acl({Credentials, PubSub, Topic}, _State) -> case match(Credentials, Topic, lookup(PubSub)) of {matched, allow} -> allow; {matched, deny} -> deny; @@ -98,12 +95,11 @@ match(Credentials, Topic, [Rule|Rules]) -> end. -spec(reload_acl(#{}) -> ok | {error, term()}). -reload_acl(#{acl_file := undefined}) -> - ok; reload_acl(#{acl_file := AclFile}) -> case catch load_rules_from_file(AclFile) of - true -> emqx_logger:error("Reload acl_file ~s successfully", [AclFile]), - ok; + true -> + emqx_logger:info("Reload acl_file ~s successfully", [AclFile]), + ok; {'EXIT', Error} -> {error, Error} end. diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 084ffe7c2..16cd06b4f 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -18,49 +18,64 @@ -include("emqx_mqtt.hrl"). -export([start/0, restart/0, stop/0]). --export([start_listener/1, stop_listener/1, restart_listener/1]). +-export([start_listener/1, start_listener/3]). +-export([restart_listener/1, restart_listener/3]). +-export([stop_listener/1, stop_listener/3]). --type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}). +-type(listener() :: {esockd:proto(), esockd:listen_on(), [esockd:option()]}). -%% @doc Start all listeners +%% @doc Start all listeners. -spec(start() -> ok). start() -> lists:foreach(fun start_listener/1, emqx_config:get_env(listeners, [])). -%% Start MQTT/TCP listener -spec(start_listener(listener()) -> {ok, pid()} | {error, term()}). -start_listener({tcp, ListenOn, Options}) -> +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)]); + {error, Reason} -> + io:format(standard_error, "Failed to start mqtt:~s listener on ~s - ~p", + [Proto, format(ListenOn), Reason]) + end. + +%% Start MQTT/TCP listener +-spec(start_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) + -> {ok, pid()} | {error, term()}). +start_listener(tcp, ListenOn, Options) -> start_mqtt_listener('mqtt:tcp', ListenOn, Options); %% Start MQTT/TLS listener -start_listener({Proto, ListenOn, Options}) when Proto == ssl; Proto == tls -> +start_listener(Proto, ListenOn, Options) when Proto == ssl; Proto == tls -> start_mqtt_listener('mqtt:ssl', ListenOn, Options); %% Start MQTT/WS listener -start_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> +start_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws -> Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws_connection, Options}]}]), - NumAcceptors = proplists:get_value(acceptors, Options, 4), - MaxConnections = proplists:get_value(max_connections, Options, 1024), - TcpOptions = proplists:get_value(tcp_options, Options, []), - RanchOpts = [{num_acceptors, NumAcceptors}, - {max_connections, MaxConnections} | TcpOptions], - cowboy:start_clear('mqtt:ws', with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}); + start_http_listener(fun cowboy:start_clear/3, 'mqtt:ws', ListenOn, ranch_opts(Options), Dispatch); %% Start MQTT/WSS listener -start_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss -> - Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws, []}]}]), - NumAcceptors = proplists:get_value(acceptors, Options, 4), - MaxConnections = proplists:get_value(max_clients, Options, 1024), - TcpOptions = proplists:get_value(tcp_options, Options, []), - SslOptions = proplists:get_value(ssl_options, Options, []), - RanchOpts = [{num_acceptors, NumAcceptors}, - {max_connections, MaxConnections} | TcpOptions ++ SslOptions], - cowboy:start_tls('mqtt:wss', with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}). +start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> + Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws_connection, Options}]}]), + start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn, ranch_opts(Options), Dispatch). start_mqtt_listener(Name, ListenOn, Options) -> SockOpts = esockd:parse_opt(Options), - MFA = {emqx_connection, start_link, [Options -- SockOpts]}, - {ok, _} = esockd:open(Name, ListenOn, merge_default(SockOpts), MFA). + esockd:open(Name, ListenOn, merge_default(SockOpts), + {emqx_connection, start_link, [Options -- SockOpts]}). + +start_http_listener(Start, Name, ListenOn, RanchOpts, Dispatch) -> + Start(Name, with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}). + +ranch_opts(Options) -> + NumAcceptors = proplists:get_value(acceptors, Options, 4), + MaxConnections = proplists:get_value(max_connections, Options, 1024), + TcpOptions = proplists:get_value(tcp_options, Options, []), + RanchOpts = [{num_acceptors, NumAcceptors}, {max_connections, MaxConnections} | TcpOptions], + case proplists:get_value(ssl_options, Options) of + undefined -> RanchOpts; + SslOptions -> RanchOpts ++ SslOptions + end. with_port(Port, Opts) when is_integer(Port) -> [{port, Port}|Opts]; @@ -73,34 +88,49 @@ restart() -> lists:foreach(fun restart_listener/1, emqx_config:get_env(listeners, [])). -spec(restart_listener(listener()) -> any()). -restart_listener({tcp, ListenOn, _Options}) -> +restart_listener({Proto, ListenOn, Options}) -> + restart_listener(Proto, ListenOn, Options). + +-spec(restart_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) -> any()). +restart_listener(tcp, ListenOn, _Options) -> esockd:reopen('mqtt:tcp', ListenOn); -restart_listener({Proto, ListenOn, _Options}) when Proto == ssl; Proto == tls -> +restart_listener(Proto, ListenOn, _Options) when Proto == ssl; Proto == tls -> esockd:reopen('mqtt:ssl', ListenOn); -restart_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> +restart_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws -> cowboy:stop_listener('mqtt:ws'), - start_listener({Proto, ListenOn, Options}); -restart_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss -> + start_listener(Proto, ListenOn, Options); +restart_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> cowboy:stop_listener('mqtt:wss'), - start_listener({Proto, ListenOn, Options}); -restart_listener({Proto, ListenOn, _Opts}) -> + start_listener(Proto, ListenOn, Options); +restart_listener(Proto, ListenOn, _Opts) -> esockd:reopen(Proto, ListenOn). -%% @doc Stop all listeners +%% @doc Stop all listeners. -spec(stop() -> ok). stop() -> lists:foreach(fun stop_listener/1, emqx_config:get_env(listeners, [])). --spec(stop_listener(listener()) -> ok | {error, any()}). -stop_listener({tcp, ListenOn, _Opts}) -> +-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)]); + {error, Reason} -> + io:format(standard_error, "Failed to stop mqtt:~s listener on ~s - ~p", + [Proto, format(ListenOn), Reason]) + end. + +-spec(stop_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) + -> ok | {error, term()}). +stop_listener(tcp, ListenOn, _Opts) -> esockd:close('mqtt:tcp', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> +stop_listener(Proto, ListenOn, _Opts) when Proto == ssl; Proto == tls -> esockd:close('mqtt:ssl', ListenOn); -stop_listener({Proto, _ListenOn, _Opts}) when Proto == http; Proto == ws -> +stop_listener(Proto, _ListenOn, _Opts) when Proto == http; Proto == ws -> cowboy:stop_listener('mqtt:ws'); -stop_listener({Proto, _ListenOn, _Opts}) when Proto == https; Proto == wss -> +stop_listener(Proto, _ListenOn, _Opts) when Proto == https; Proto == wss -> cowboy:stop_listener('mqtt:wss'); -stop_listener({Proto, ListenOn, _Opts}) -> +stop_listener(Proto, ListenOn, _Opts) -> esockd:close(Proto, ListenOn). merge_default(Options) -> @@ -111,3 +141,10 @@ merge_default(Options) -> [{tcp_options, ?MQTT_SOCKOPTS} | Options] end. +format(Port) when is_integer(Port) -> + io_lib:format("0.0.0.0:~w", [Port]); +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]). + From e5b2e584e9df62347791670571ef4a41da33d935 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 27 Aug 2018 15:57:36 +0800 Subject: [PATCH 133/520] Remove 'TODO:' --- etc/emqx.conf | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index f3f46589e..c35b1d0f7 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -424,7 +424,7 @@ log.syslog = on ## Value: true | false allow_anonymous = true -## TODO: Allow or deny if no ACL rules match. +## Allow or deny if no ACL rules matched. ## ## Value: allow | deny acl_nomatch = allow @@ -455,7 +455,6 @@ acl_cache_max_size = 32 ## Default: 1 minute acl_cache_ttl = 1m - ##-------------------------------------------------------------------- ## MQTT Protocol ##-------------------------------------------------------------------- From 35d821a62e3b2ad640ca20e08db0c108cc687447 Mon Sep 17 00:00:00 2001 From: turtled Date: Mon, 27 Aug 2018 16:15:23 +0800 Subject: [PATCH 134/520] Add WS stats --- src/emqx_stats.erl | 4 +++- src/emqx_ws_connection.erl | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index 479021bf0..41730005e 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -59,7 +59,9 @@ 'subscribers/count', 'subscribers/max', 'subscriptions/count', - 'subscriptions/max' + 'subscriptions/max', + 'subscriptions/shared/count', + 'subscriptions/shared/max' ]). -define(ROUTE_STATS, [ diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index a57a80fe9..3871f7b4c 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -131,9 +131,9 @@ stat_fun() -> fun() -> {ok, get(recv_oct)} end. websocket_handle({binary, <<>>}, State) -> - {ok, State}; + {ok, ensure_stats_timer(State)}; websocket_handle({binary, [<<>>]}, State) -> - {ok, State}; + {ok, ensure_stats_timer(State)}; websocket_handle({binary, Data}, State = #state{parser_state = ParserState, proto_state = ProtoState}) -> BinSize = iolist_size(Data), From a19daee3538851317793d4d69a585f7d40e6a84c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 27 Aug 2018 16:19:58 +0800 Subject: [PATCH 135/520] Improve the print of listener startup --- src/emqx_listeners.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 16cd06b4f..421304f3a 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -33,9 +33,9 @@ start() -> 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)]); + 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", + io:format(standard_error, "Failed to start mqtt:~s listener on ~s - ~p!", [Proto, format(ListenOn), Reason]) end. @@ -114,9 +114,9 @@ stop() -> 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)]); + 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", + io:format(standard_error, "Failed to stop mqtt:~s listener on ~s - ~p.", [Proto, format(ListenOn), Reason]) end. From c5da439313c00e7389f299d9833ebf8695c39f92 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Mon, 27 Aug 2018 16:41:58 +0800 Subject: [PATCH 136/520] wrong unsubscribe call --- src/emqx_session.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index db7e80a9b..77434e937 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -220,7 +220,7 @@ pubcomp(SPid, PacketId, ReasonCode) -> -spec(unsubscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). unsubscribe(SPid, TopicFilters) when is_list(TopicFilters) -> %%TODO: Parse the topic filters? - unsubscribe(SPid, {undefined, #{}, TopicFilters}). + unsubscribe(SPid, undefined, #{}, TopicFilters). %% TODO:... unsubscribe(SPid, PacketId, Properties, TopicFilters) -> From 224aaaf8a7e5fbaa15825f177725dd57220c2539 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Mon, 27 Aug 2018 17:11:54 +0800 Subject: [PATCH 137/520] unsubscribe function should have two params --- src/emqx.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx.erl b/src/emqx.erl index 54f1952f4..de89a9981 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -95,7 +95,7 @@ unsubscribe(Topic) -> -spec(unsubscribe(topic() | string(), subscriber() | string()) -> ok | {error, term()}). unsubscribe(Topic, Sub) when is_list(Sub) -> - emqx_broker:unsubscribe(iolist_to_binary(Topic)); + emqx_broker:unsubscribe(iolist_to_binary(Topic), list_to_subid(Sub)); unsubscribe(Topic, Subscriber) when is_tuple(Subscriber) -> {SubPid, SubId} = Subscriber, emqx_broker:unsubscribe(iolist_to_binary(Topic), SubPid, SubId). From 015901050fab0b1ea26fbe5410d8a245ebe6ee5c Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Mon, 27 Aug 2018 18:19:28 +0800 Subject: [PATCH 138/520] change return tuple of close_session --- src/emqx_session.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 77434e937..526b31c25 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -383,7 +383,7 @@ handle_call(stats, _From, State) -> reply(stats(State), State); handle_call(close, _From, State) -> - {stop, normal, State}; + {stop, normal, ok, State}; handle_call(Req, _From, State) -> emqx_logger:error("[Session] unexpected call: ~p", [Req]), From c0fb5f3186c1db0487e12082fb54e982bb5bd913 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Mon, 27 Aug 2018 19:28:54 +0800 Subject: [PATCH 139/520] update gen_rpc --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 82e20b12d..85f22a60f 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ DEPS = jsx gproc gen_rpc lager ekka esockd cowboy clique lager_syslog dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 -dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.1.1 +dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.2.0 dep_lager = git https://github.com/erlang-lager/lager 3.6.4 dep_esockd = git https://github.com/emqx/esockd emqx30 dep_ekka = git https://github.com/emqx/ekka emqx30 @@ -16,7 +16,7 @@ dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 dep_clique = git https://github.com/emqx/clique dep_lager_syslog = git https://github.com/basho/lager_syslog 3.0.1 -NO_AUTOPATCH = gen_rpc cuttlefish +NO_AUTOPATCH = cuttlefish ERLC_OPTS += +debug_info ERLC_OPTS += +'{parse_transform, lager_transform}' From ce3f2e4d9e1a879a0c2db31ee1521b9943281034 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Mon, 27 Aug 2018 21:18:00 +0800 Subject: [PATCH 140/520] fix emqx_broker test suite --- src/emqx_alarm_mgr.erl | 2 +- src/emqx_time.erl | 7 ++- test/emqx_broker_SUITE.erl | 90 ++++++++++++++++++++------------------ test/emqx_mock_client.erl | 10 ++++- 4 files changed, 63 insertions(+), 46 deletions(-) diff --git a/src/emqx_alarm_mgr.erl b/src/emqx_alarm_mgr.erl index 41da4e705..9839ba44e 100644 --- a/src/emqx_alarm_mgr.erl +++ b/src/emqx_alarm_mgr.erl @@ -81,7 +81,7 @@ handle_event({set_alarm, Alarm = #alarm{timestamp = undefined}}, State)-> handle_event({set_alarm, Alarm = #alarm{id = AlarmId}}, State = #state{alarms = Alarms}) -> case encode_alarm(Alarm) of {ok, Json} -> - ok = emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json)); + emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json)); {error, Reason} -> emqx_logger:error("[AlarmMgr] Failed to encode alarm: ~p", [Reason]) end, diff --git a/src/emqx_time.erl b/src/emqx_time.erl index 0d74168c4..95bfc9409 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -14,7 +14,7 @@ -module(emqx_time). --export([seed/0, now_secs/0, now_ms/0, now_ms/1]). +-export([seed/0, now_secs/0, now_secs/1, now_ms/0, now_ms/1]). seed() -> rand:seed(exsplus, erlang:timestamp()). @@ -22,8 +22,11 @@ seed() -> now_secs() -> erlang:system_time(second). +now_secs({MegaSecs, Secs, _MicroSecs}) -> + MegaSecs * 1000000 + Secs. + now_ms() -> erlang:system_time(millisecond). now_ms({MegaSecs, Secs, MicroSecs}) -> - (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). \ No newline at end of file + (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index 917143c3a..a71a96539 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -64,12 +64,12 @@ end_per_suite(_Config) -> %%-------------------------------------------------------------------- subscribe_unsubscribe(_) -> - ok = emqx:subscribe(<<"topic">>, <<"clientId">>), - ok = emqx:subscribe(<<"topic/1">>, <<"clientId">>, [{qos, 1}]), - ok = emqx:subscribe(<<"topic/2">>, <<"clientId">>, [{qos, 2}]), - ok = emqx:unsubscribe(<<"topic">>, <<"clientId">>), - ok = emqx:unsubscribe(<<"topic/1">>, <<"clientId">>), - ok = emqx:unsubscribe(<<"topic/2">>, <<"clientId">>). + ok = emqx:subscribe(<<"topic">>, "clientId"), + ok = emqx:subscribe(<<"topic/1">>, "clientId", #{ qos => 1 }), + ok = emqx:subscribe(<<"topic/2">>, "clientId", #{ qos => 2 }), + ok = emqx:unsubscribe(<<"topic">>, "clientId"), + ok = emqx:unsubscribe(<<"topic/1">>, "clientId"), + ok = emqx:unsubscribe(<<"topic/2">>, "clientId"). publish(_) -> Msg = emqx_message:make(ct, <<"test/pubsub">>, <<"hello">>), @@ -80,13 +80,17 @@ publish(_) -> pubsub(_) -> Self = self(), - ok = emqx:subscribe(<<"a/b/c">>, Self, [{qos, 1}]), - ?assertMatch({error, _}, emqx:subscribe(<<"a/b/c">>, Self, [{qos, 2}])), + Subscriber = {Self, <<"clientId">>}, + ok = emqx:subscribe(<<"a/b/c">>, Subscriber, #{ qos => 1 }), + #{ qos := 1} = ets:lookup_element(emqx_suboption, {<<"a/b/c">>, Subscriber}, 2), + ok = emqx:subscribe(<<"a/b/c">>, Subscriber, #{ qos => 2 }), + #{ qos := 2} = ets:lookup_element(emqx_suboption, {<<"a/b/c">>, Subscriber}, 2), + %% ct:log("Emq Sub: ~p.~n", [ets:lookup(emqx_suboption, {<<"a/b/c">>, Subscriber})]), timer:sleep(10), - [{Self, <<"a/b/c">>}] = ets:lookup(mqtt_subscription, Self), - [{<<"a/b/c">>, Self}] = ets:lookup(mqtt_subscriber, <<"a/b/c">>), + [{<<"a/b/c">>, #{qos := 2}}] = emqx_broker:subscriptions(Subscriber), + [{Self, <<"clientId">>}] = emqx_broker:subscribers(<<"a/b/c">>), emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), - ?assert(receive {dispatch, <<"a/b/c">>, _} -> true after 2 -> false end), + ?assert(receive {dispatch, <<"a/b/c">>, _ } -> true; P -> ct:log("Receive Message: ~p~n",[P]) after 2 -> false end), spawn(fun() -> emqx:subscribe(<<"a/b/c">>), emqx:subscribe(<<"c/d/e">>), @@ -97,32 +101,33 @@ pubsub(_) -> emqx:unsubscribe(<<"a/b/c">>). t_local_subscribe(_) -> - ok = emqx:subscribe("$local/topic0"), - ok = emqx:subscribe("$local/topic1", <<"x">>), - ok = emqx:subscribe("$local/topic2", <<"x">>, [{qos, 2}]), + ok = emqx:subscribe(<<"$local/topic0">>), + ok = emqx:subscribe(<<"$local/topic1">>, "clientId"), + ok = emqx:subscribe(<<"$local/topic2">>, "clientId", #{ qos => 2 }), timer:sleep(10), - ?assertEqual([self()], emqx:subscribers("$local/topic0")), - ?assertEqual([{<<"x">>, self()}], emqx:subscribers("$local/topic1")), - ?assertEqual([{{<<"x">>, self()}, <<"$local/topic1">>, []}, - {{<<"x">>, self()}, <<"$local/topic2">>, [{qos,2}]}], - emqx:subscriptions(<<"x">>)), + ?assertEqual([{self(), undefined}], emqx:subscribers("$local/topic0")), + ?assertEqual([{self(), <<"clientId">>}], emqx:subscribers("$local/topic1")), + ?assertEqual([{<<"$local/topic1">>, #{}}, + {<<"$local/topic2">>, #{ qos => 2 }}], + emqx:subscriptions({self(), <<"clientId">>})), ?assertEqual(ok, emqx:unsubscribe("$local/topic0")), - ?assertMatch({error, {subscription_not_found, _}}, emqx:unsubscribe("$local/topic0")), - ?assertEqual(ok, emqx:unsubscribe("$local/topic1", <<"x">>)), - ?assertEqual(ok, emqx:unsubscribe("$local/topic2", <<"x">>)), + ?assertEqual(ok, emqx:unsubscribe("$local/topic0")), + ?assertEqual(ok, emqx:unsubscribe("$local/topic1", "clientId")), + ?assertEqual(ok, emqx:unsubscribe("$local/topic2", "clientId")), ?assertEqual([], emqx:subscribers("topic1")), - ?assertEqual([], emqx:subscriptions(<<"x">>)). + ?assertEqual([], emqx:subscriptions({self(), <<"clientId">>})). t_shared_subscribe(_) -> emqx:subscribe("$local/$share/group1/topic1"), emqx:subscribe("$share/group2/topic2"), emqx:subscribe("$queue/topic3"), timer:sleep(10), - ?assertEqual([self()], emqx:subscribers(<<"$local/$share/group1/topic1">>)), - ?assertEqual([{self(), <<"$local/$share/group1/topic1">>, []}, - {self(), <<"$queue/topic3">>, []}, - {self(), <<"$share/group2/topic2">>, []}], - lists:sort(emqx:subscriptions(self()))), + ct:log("share subscriptions: ~p~n", [emqx:subscriptions({self(), undefined})]), + ?assertEqual([{self(), undefined}], emqx:subscribers(<<"$local/$share/group1/topic1">>)), + ?assertEqual([{<<"$local/$share/group1/topic1">>, #{}}, + {<<"$queue/topic3">>, #{}}, + {<<"$share/group2/topic2">>, #{}}], + lists:sort(emqx:subscriptions({self(), undefined}))), emqx:unsubscribe("$local/$share/group1/topic1"), emqx:unsubscribe("$share/group2/topic2"), emqx:unsubscribe("$queue/topic3"), @@ -146,17 +151,18 @@ t_shared_subscribe(_) -> %% Session Group %%-------------------------------------------------------------------- start_session(_) -> - {ok, ClientPid} = emqx_mock_client:start_link(<<"clientId">>), - {ok, SessPid} = emqx_mock_client:start_session(ClientPid), - Message = emqx_message:make(<<"clientId">>, 2, <<"topic">>, <<"hello">>), - Message1 = Message#message{id = 1}, - emqx_session:publish(SessPid, Message1), - emqx_session:pubrel(SessPid, 1), - emqx_session:subscribe(SessPid, [{<<"topic/session">>, [{qos, 2}]}]), + 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, Message2), + emqx_session:publish(SessPid, 3, Message2), emqx_session:unsubscribe(SessPid, [{<<"topic/session">>, []}]), - emqx_mock_client:stop(ClientPid). + %% emqx_mock_client:stop(ClientPid). + emqx_mock_client:close_session(ClientPid, SessPid). %%-------------------------------------------------------------------- %% Broker Group @@ -231,10 +237,10 @@ hook_fun8(arg, initArg) -> stop. set_alarms(_) -> AlarmTest = #alarm{id = <<"1">>, severity = error, title="alarm title", summary="alarm summary"}, - emqx_alarm:set_alarm(AlarmTest), - Alarms = emqx_alarm:get_alarms(), + emqx_alarm_mgr:set_alarm(AlarmTest), + Alarms = emqx_alarm_mgr:get_alarms(), + ct:log("Alarms Length: ~p ~n", [length(Alarms)]), ?assertEqual(1, length(Alarms)), - emqx_alarm:clear_alarm(<<"1">>), - [] = emqx_alarm:get_alarms(). - + emqx_alarm_mgr:clear_alarm(<<"1">>), + [] = emqx_alarm_mgr:get_alarms(). diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index e76e5551c..8afbeeb17 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -18,7 +18,7 @@ -behaviour(gen_server). --export([start_link/1, open_session/3, stop/1]). +-export([start_link/1, open_session/3, close_session/2, stop/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -31,6 +31,9 @@ start_link(ClientId) -> open_session(ClientPid, ClientId, Zone) -> gen_server:call(ClientPid, {start_session, ClientPid, ClientId, Zone}). +close_session(ClientPid, SessPid) -> + gen_server:call(ClientPid, {stop_session, SessPid}). + stop(CPid) -> gen_server:call(CPid, stop). @@ -55,6 +58,11 @@ handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> client_pid = ClientPid }}; +handle_call({stop_session, SessPid}, _From, State) -> + unlink(SessPid), + emqx_sm:close_session(SessPid), + {stop, normal, ok, State}; + handle_call(stop, _From, State) -> {stop, normal, ok, State}; From 1607e576de0244c885d7b344b1e9c45f07829e7b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 27 Aug 2018 21:34:11 +0800 Subject: [PATCH 141/520] Update connection, session, stats modules --- src/emqx_app.erl | 3 +- src/emqx_cm.erl | 162 +++++++++++++++++++------------------ src/emqx_connection.erl | 22 ++--- src/emqx_protocol.erl | 8 +- src/emqx_sm.erl | 68 ++++++++-------- src/emqx_stats.erl | 58 ++++++------- src/emqx_sys.erl | 2 +- src/emqx_types.erl | 3 + src/emqx_ws_connection.erl | 5 +- 9 files changed, 170 insertions(+), 161 deletions(-) diff --git a/src/emqx_app.erl b/src/emqx_app.erl index d5ca8f6ae..4a39e46aa 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -50,8 +50,9 @@ print_banner() -> io:format("Starting ~s on node ~s~n", [?APP, node()]). print_vsn() -> + {ok, Descr} = application:get_key(description), {ok, Vsn} = application:get_key(vsn), - io:format("~s ~s is running now!~n", [?APP, Vsn]). + io:format("~s ~s is running now!~n", [Descr, Vsn]). %%-------------------------------------------------------------------- %% Autocluster diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index f05d63a29..5e921a1c0 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -20,90 +20,98 @@ -export([start_link/0]). --export([lookup_client/1]). --export([register_client/1, register_client/2, unregister_client/1]). - --export([get_client_attrs/1, lookup_client_pid/1]). --export([get_client_stats/1, set_client_stats/2]). +-export([lookup_connection/1]). +-export([register_connection/1, register_connection/2]). +-export([unregister_connection/1]). +-export([get_conn_attrs/1, set_conn_attrs/2]). +-export([get_conn_stats/1, set_conn_stats/2]). +-export([lookup_conn_pid/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {client_pmon}). - -define(CM, ?MODULE). -%% ETS Tables. --define(CLIENT, emqx_client). --define(CLIENT_ATTRS, emqx_client_attrs). --define(CLIENT_STATS, emqx_client_stats). -%% @doc Start the client manager. --spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +%% ETS Tables. +-define(CONN_TAB, emqx_conn). +-define(CONN_ATTRS_TAB, emqx_conn_attrs). +-define(CONN_STATS_TAB, emqx_conn_stats). + +%% @doc Start the connection manager. +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?CM}, ?MODULE, [], []). -%% @doc Lookup a client. --spec(lookup_client(client_id()) -> list({client_id(), pid()})). -lookup_client(ClientId) when is_binary(ClientId) -> - ets:lookup(?CLIENT, ClientId). +%% @doc Lookup a connection. +-spec(lookup_connection(client_id()) -> list({client_id(), pid()})). +lookup_connection(ClientId) when is_binary(ClientId) -> + ets:lookup(?CONN_TAB, ClientId). -%% @doc Register a client. --spec(register_client(client_id() | {client_id(), pid()}) -> ok). -register_client(ClientId) when is_binary(ClientId) -> - register_client({ClientId, self()}); +%% @doc Register a connection. +-spec(register_connection(client_id() | {client_id(), pid()}) -> ok). +register_connection(ClientId) when is_binary(ClientId) -> + register_connection({ClientId, self()}); -register_client({ClientId, ClientPid}) when is_binary(ClientId), is_pid(ClientPid) -> - register_client({ClientId, ClientPid}, []). +register_connection(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> + _ = ets:insert(?CONN_TAB, Conn), + notify({registered, ClientId, ConnPid}). --spec(register_client({client_id(), pid()}, list()) -> ok). -register_client(CObj = {ClientId, ClientPid}, Attrs) when is_binary(ClientId), is_pid(ClientPid) -> - _ = ets:insert(?CLIENT, CObj), - _ = ets:insert(?CLIENT_ATTRS, {CObj, Attrs}), - notify({registered, ClientId, ClientPid}). +-spec(register_connection(client_id() | {client_id(), pid()}, list()) -> ok). +register_connection(ClientId, Attrs) when is_binary(ClientId) -> + register_connection({ClientId, self()}, Attrs); +register_connection(Conn = {ClientId, ConnPid}, Attrs) when is_binary(ClientId), is_pid(ConnPid) -> + set_conn_attrs(Conn, Attrs), + register_connection(Conn). -%% @doc Get client attrs --spec(get_client_attrs({client_id(), pid()}) -> list()). -get_client_attrs(CObj = {ClientId, ClientPid}) when is_binary(ClientId), is_pid(ClientPid) -> +%% @doc Get conn attrs +-spec(get_conn_attrs({client_id(), pid()}) -> list()). +get_conn_attrs(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> try - ets:lookup_element(?CLIENT_ATTRS, CObj, 2) + ets:lookup_element(?CONN_ATTRS_TAB, Conn, 2) catch error:badarg -> [] end. -%% @doc Unregister a client. --spec(unregister_client(client_id() | {client_id(), pid()}) -> ok). -unregister_client(ClientId) when is_binary(ClientId) -> - unregister_client({ClientId, self()}); +%% @doc Set conn attrs +set_conn_attrs(ClientId, Attrs) when is_binary(ClientId) -> + set_conn_attrs({ClientId, self()}, Attrs); +set_conn_attrs(Conn = {ClientId, ConnPid}, Attrs) when is_binary(ClientId), is_pid(ConnPid) -> + ets:insert(?CONN_ATTRS_TAB, {Conn, Attrs}). -unregister_client(CObj = {ClientId, ClientPid}) when is_binary(ClientId), is_pid(ClientPid) -> - _ = ets:delete(?CLIENT_STATS, CObj), - _ = ets:delete(?CLIENT_ATTRS, CObj), - _ = ets:delete_object(?CLIENT, CObj), - notify({unregistered, ClientId, ClientPid}). +%% @doc Unregister a conn. +-spec(unregister_connection(client_id() | {client_id(), pid()}) -> ok). +unregister_connection(ClientId) when is_binary(ClientId) -> + unregister_connection({ClientId, self()}); -%% @doc Lookup client pid --spec(lookup_client_pid(client_id()) -> pid() | undefined). -lookup_client_pid(ClientId) when is_binary(ClientId) -> - case ets:lookup(?CLIENT, ClientId) of +unregister_connection(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> + _ = ets:delete(?CONN_STATS_TAB, Conn), + _ = ets:delete(?CONN_ATTRS_TAB, Conn), + _ = ets:delete_object(?CONN_TAB, Conn), + notify({unregistered, ClientId, ConnPid}). + +%% @doc Lookup connection pid +-spec(lookup_conn_pid(client_id()) -> pid() | undefined). +lookup_conn_pid(ClientId) when is_binary(ClientId) -> + case ets:lookup(?CONN_TAB, ClientId) of [] -> undefined; [{_, Pid}] -> Pid end. -%% @doc Get client stats --spec(get_client_stats({client_id(), pid()}) -> list(emqx_stats:stats())). -get_client_stats(CObj = {ClientId, ClientPid}) when is_binary(ClientId), is_pid(ClientPid) -> - try ets:lookup_element(?CLIENT_STATS, CObj, 2) +%% @doc Get conn stats +-spec(get_conn_stats({client_id(), pid()}) -> list(emqx_stats:stats())). +get_conn_stats(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> + try ets:lookup_element(?CONN_STATS_TAB, Conn, 2) catch error:badarg -> [] end. -%% @doc Set client stats. --spec(set_client_stats(client_id(), list(emqx_stats:stats())) -> boolean()). -set_client_stats(ClientId, Stats) when is_binary(ClientId) -> - set_client_stats({ClientId, self()}, Stats); +%% @doc Set conn stats. +-spec(set_conn_stats(client_id(), list(emqx_stats:stats())) -> boolean()). +set_conn_stats(ClientId, Stats) when is_binary(ClientId) -> + set_conn_stats({ClientId, self()}, Stats); -set_client_stats(CObj = {ClientId, ClientPid}, Stats) when is_binary(ClientId), is_pid(ClientPid) -> - ets:insert(?CLIENT_STATS, {CObj, Stats}). +set_conn_stats(Conn = {ClientId, ConnPid}, Stats) when is_binary(ClientId), is_pid(ConnPid) -> + ets:insert(?CONN_STATS_TAB, {Conn, Stats}). notify(Msg) -> gen_server:cast(?CM, {notify, Msg}). @@ -114,52 +122,52 @@ notify(Msg) -> init([]) -> TabOpts = [public, set, {write_concurrency, true}], - _ = emqx_tables:new(?CLIENT, [{read_concurrency, true} | TabOpts]), - _ = emqx_tables:new(?CLIENT_ATTRS, TabOpts), - _ = emqx_tables:new(?CLIENT_STATS, TabOpts), - ok = emqx_stats:update_interval(cm_stats, fun update_client_stats/0), - {ok, #state{client_pmon = emqx_pmon:new()}}. + _ = emqx_tables:new(?CONN_TAB, [{read_concurrency, true} | TabOpts]), + _ = emqx_tables:new(?CONN_ATTRS_TAB, TabOpts), + _ = emqx_tables:new(?CONN_STATS_TAB, TabOpts), + ok = emqx_stats:update_interval(cm_stats, fun update_conn_stats/0), + {ok, #{conn_pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> emqx_logger:error("[CM] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({notify, {registered, ClientId, Pid}}, State = #state{client_pmon = PMon}) -> - {noreply, State#state{client_pmon = emqx_pmon:monitor(Pid, ClientId, PMon)}}; +handle_cast({notify, {registered, ClientId, ConnPid}}, State = #{conn_pmon := PMon}) -> + {noreply, State#{conn_pmon := emqx_pmon:monitor(ConnPid, ClientId, PMon)}}; -handle_cast({notify, {unregistered, _ClientId, Pid}}, State = #state{client_pmon = PMon}) -> - {noreply, State#state{client_pmon = emqx_pmon:demonitor(Pid, PMon)}}; +handle_cast({notify, {unregistered, _ClientId, ConnPid}}, State = #{conn_pmon := PMon}) -> + {noreply, State#{conn_pmon := emqx_pmon:demonitor(ConnPid, PMon)}}; handle_cast(Msg, State) -> emqx_logger:error("[CM] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{client_pmon = PMon}) -> - case emqx_pmon:find(DownPid, PMon) of - undefined -> {noreply, State}; - ClientId -> - unregister_client({ClientId, DownPid}), - {noreply, State#state{client_pmon = emqx_pmon:erase(DownPid, PMon)}} +handle_info({'DOWN', _MRef, process, ConnPid, _Reason}, State = #{conn_pmon := PMon}) -> + case emqx_pmon:find(ConnPid, PMon) of + undefined -> + {noreply, State}; + ClientId -> + unregister_connection({ClientId, ConnPid}), + {noreply, State#{conn_pmon := emqx_pmon:erase(ConnPid, PMon)}} end; handle_info(Info, State) -> emqx_logger:error("[CM] unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, _State = #state{}) -> +terminate(_Reason, _State) -> emqx_stats:cancel_update(cm_stats). code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -update_client_stats() -> - case ets:info(?CLIENT, size) of +update_conn_stats() -> + case ets:info(?CONN_TAB, size) of undefined -> ok; - Size -> - emqx_stats:setstat('clients/count', 'clients/max', Size) + Size -> emqx_stats:setstat('connections/count', 'connections/max', Size) end. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index d0d0bc90b..c5299d638 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -96,16 +96,16 @@ init([Transport, RawSocket, Options]) -> sendfun => SendFun}, Options), ParserState = emqx_protocol:parser(ProtoState), State = run_socket(#state{transport = Transport, - socket = Socket, - peername = Peername, - await_recv = false, - conn_state = running, - rate_limit = RateLimit, - publish_limit = PubLimit, - proto_state = ProtoState, - parser_state = ParserState, - enable_stats = EnableStats, - idle_timeout = IdleTimout}), + socket = Socket, + peername = Peername, + await_recv = false, + conn_state = running, + rate_limit = RateLimit, + publish_limit = PubLimit, + proto_state = ProtoState, + parser_state = ParserState, + enable_stats = EnableStats, + idle_timeout = IdleTimout}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> @@ -187,7 +187,7 @@ handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> handle_info(emit_stats, State = #state{proto_state = ProtoState}) -> Stats = element(2, handle_call(stats, undefined, State)), - emqx_cm:set_client_stats(emqx_protocol:client_id(ProtoState), Stats), + emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), Stats), {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info(timeout, State) -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 7b4c80754..2945954b1 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -237,7 +237,7 @@ process(?CONNECT_PACKET( case try_open_session(PState3) of {ok, SPid, SP} -> PState4 = PState3#pstate{session = SPid}, - ok = emqx_cm:register_client({client_id(PState4), self()}, info(PState4)), + ok = emqx_cm:register_connection(client_id(PState4), info(PState4)), %% Start keepalive start_keepalive(Keepalive, PState4), %% TODO: 'Run hooks' before open_session? @@ -580,10 +580,10 @@ inc_stats(Type, Stats = #{pkt := PktCnt, msg := MsgCnt}) -> shutdown(_Error, #pstate{client_id = undefined}) -> ignore; shutdown(conflict, #pstate{client_id = ClientId}) -> - emqx_cm:unregister_client(ClientId), + emqx_cm:unregister_connection(ClientId), ignore; shutdown(mnesia_conflict, #pstate{client_id = ClientId}) -> - emqx_cm:unregister_client(ClientId), + emqx_cm:unregister_connection(ClientId), ignore; shutdown(Error, PState = #pstate{client_id = ClientId, will_msg = WillMsg}) -> ?LOG(info, "Shutdown for ~p", [Error], PState), @@ -593,7 +593,7 @@ shutdown(Error, PState = #pstate{client_id = ClientId, will_msg = WillMsg}) -> false -> send_willmsg(WillMsg) end, emqx_hooks:run('client.disconnected', [Error], client(PState)), - emqx_cm:unregister_client(ClientId). + emqx_cm:unregister_connection(ClientId). willmsg(Packet, PState = #pstate{client_id = ClientId}) when is_record(Packet, mqtt_packet_connect) -> diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 2dac00263..e31adb141 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -24,7 +24,8 @@ -export([lookup_session/1, lookup_session_pid/1]). -export([resume_session/1, resume_session/2]). -export([discard_session/1, discard_session/2]). --export([register_session/2, get_session_attrs/1, unregister_session/1]). +-export([register_session/2, unregister_session/1]). +-export([get_session_attrs/1, set_session_attrs/2]). -export([get_session_stats/1, set_session_stats/2]). %% Internal functions for rpc @@ -39,10 +40,10 @@ -define(SM, ?MODULE). %% ETS Tables --define(SESSION, emqx_session). --define(SESSION_P, emqx_persistent_session). --define(SESSION_ATTRS, emqx_session_attrs). --define(SESSION_STATS, emqx_session_stats). +-define(SESSION_TAB, emqx_session). +-define(SESSION_P_TAB, emqx_persistent_session). +-define(SESSION_ATTRS_TAB, emqx_session_attrs). +-define(SESSION_STATS_TAB, emqx_session_stats). -spec(start_link() -> {ok, pid()} | ignore | {error, term()}). start_link() -> @@ -125,41 +126,44 @@ register_session(ClientId, Attrs) when is_binary(ClientId) -> register_session(Session = {ClientId, SPid}, Attrs) when is_binary(ClientId), is_pid(SPid) -> - ets:insert(?SESSION, Session), - ets:insert(?SESSION_ATTRS, {Session, Attrs}), + ets:insert(?SESSION_TAB, Session), + ets:insert(?SESSION_ATTRS_TAB, {Session, Attrs}), case proplists:get_value(clean_start, Attrs, true) of true -> ok; - false -> ets:insert(?SESSION_P, Session) + false -> ets:insert(?SESSION_P_TAB, Session) end, emqx_sm_registry:register_session(Session), notify({registered, ClientId, SPid}). %% @doc Get session attrs --spec(get_session_attrs({client_id(), pid()}) - -> list(emqx_session:attribute())). -get_session_attrs(Session = {ClientId, SPid}) - when is_binary(ClientId), is_pid(SPid) -> - safe_lookup_element(?SESSION_ATTRS, Session, []). +-spec(get_session_attrs({client_id(), pid()}) -> list(emqx_session:attribute())). +get_session_attrs(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> + safe_lookup_element(?SESSION_ATTRS_TAB, Session, []). + +%% @doc Set session attrs +set_session_attrs(ClientId, Attrs) when is_binary(ClientId) -> + set_session_attrs({ClientId, self()}, Attrs); +set_session_attrs(Session = {ClientId, SPid}, Attrs) when is_binary(ClientId), is_pid(SPid) -> + ets:insert(?SESSION_ATTRS_TAB, {Session, Attrs}). %% @doc Unregister a session -spec(unregister_session(client_id() | {client_id(), pid()}) -> ok). unregister_session(ClientId) when is_binary(ClientId) -> unregister_session({ClientId, self()}); -unregister_session(Session = {ClientId, SPid}) - when is_binary(ClientId), is_pid(SPid) -> +unregister_session(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> emqx_sm_registry:unregister_session(Session), - ets:delete(?SESSION_STATS, Session), - ets:delete(?SESSION_ATTRS, Session), - ets:delete_object(?SESSION_P, Session), - ets:delete_object(?SESSION, Session), + ets:delete(?SESSION_STATS_TAB, Session), + ets:delete(?SESSION_ATTRS_TAB, Session), + ets:delete_object(?SESSION_P_TAB, Session), + ets:delete_object(?SESSION_TAB, Session), notify({unregistered, ClientId, SPid}). %% @doc Get session stats -spec(get_session_stats({client_id(), pid()}) -> list(emqx_stats:stats())). get_session_stats(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> - safe_lookup_element(?SESSION_STATS, Session, []). + safe_lookup_element(?SESSION_STATS_TAB, Session, []). %% @doc Set session stats -spec(set_session_stats(client_id() | {client_id(), pid()}, @@ -169,14 +173,14 @@ set_session_stats(ClientId, Stats) when is_binary(ClientId) -> set_session_stats(Session = {ClientId, SPid}, Stats) when is_binary(ClientId), is_pid(SPid) -> - ets:insert(?SESSION_STATS, {Session, Stats}). + ets:insert(?SESSION_STATS_TAB, {Session, Stats}). %% @doc Lookup a session from registry -spec(lookup_session(client_id()) -> list({client_id(), pid()})). lookup_session(ClientId) -> case emqx_sm_registry:is_enabled() of true -> emqx_sm_registry:lookup_session(ClientId); - false -> ets:lookup(?SESSION, ClientId) + false -> ets:lookup(?SESSION_TAB, ClientId) end. %% @doc Dispatch a message to the session. @@ -192,7 +196,7 @@ dispatch(ClientId, Topic, Msg) -> %% @doc Lookup session pid. -spec(lookup_session_pid(client_id()) -> pid() | undefined). lookup_session_pid(ClientId) -> - safe_lookup_element(?SESSION, ClientId, undefined). + safe_lookup_element(?SESSION_TAB, ClientId, undefined). safe_lookup_element(Tab, Key, Default) -> try ets:lookup_element(Tab, Key, 2) @@ -209,12 +213,12 @@ notify(Event) -> init([]) -> TabOpts = [public, set, {write_concurrency, true}], - _ = emqx_tables:new(?SESSION, [{read_concurrency, true} | TabOpts]), - _ = emqx_tables:new(?SESSION_P, TabOpts), - _ = emqx_tables:new(?SESSION_ATTRS, TabOpts), - _ = emqx_tables:new(?SESSION_STATS, TabOpts), + _ = emqx_tables:new(?SESSION_TAB, [{read_concurrency, true} | TabOpts]), + _ = emqx_tables:new(?SESSION_P_TAB, TabOpts), + _ = emqx_tables:new(?SESSION_ATTRS_TAB, TabOpts), + _ = emqx_tables:new(?SESSION_STATS_TAB, TabOpts), emqx_stats:update_interval(sm_stats, stats_fun()), - {ok, #state{session_pmon = emqx_pmon:new()}}. + {ok, #{session_pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> emqx_logger:error("[SM] unexpected call: ~p", [Req]), @@ -230,12 +234,12 @@ handle_cast(Msg, State) -> emqx_logger:error("[SM] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{session_pmon = PMon}) -> +handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #{session_pmon := PMon}) -> case emqx_pmon:find(DownPid, PMon) of undefined -> {noreply, State}; ClientId -> unregister_session({ClientId, DownPid}), - {noreply, State#state{session_pmon = emqx_pmon:erase(DownPid, PMon)}} + {noreply, State#{session_pmon := emqx_pmon:erase(DownPid, PMon)}} end; handle_info(Info, State) -> @@ -254,8 +258,8 @@ code_change(_OldVsn, State, _Extra) -> stats_fun() -> fun() -> - safe_update_stats(?SESSION, 'sessions/count', 'sessions/max'), - safe_update_stats(?SESSION_P, 'sessions/persistent/count', 'sessions/persistent/max') + safe_update_stats(?SESSION_TAB, 'sessions/count', 'sessions/max'), + safe_update_stats(?SESSION_P_TAB, 'sessions/persistent/count', 'sessions/persistent/max') end. safe_update_stats(Tab, Stat, MaxStat) -> diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index 479021bf0..8c74e05bc 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -20,11 +20,10 @@ -export([start_link/0]). -%% Get all stats --export([all/0]). - %% Stats API. --export([statsfun/1, statsfun/2, getstats/0, getstat/1, setstat/2, setstat/3]). +-export([getstats/0, getstat/1]). +-export([setstat/2, setstat/3]). +-export([statsfun/1, statsfun/2]). -export([update_interval/2, update_interval/3, cancel_update/1]). %% gen_server callbacks @@ -35,13 +34,12 @@ -record(state, {timer, updates :: #update{}}). -type(stats() :: list({atom(), non_neg_integer()})). - -export_type([stats/0]). -%% Client stats --define(CLIENT_STATS, [ - 'clients/count', % clients connected current - 'clients/max' % maximum clients connected +%% Connection stats +-define(CONNECTION_STATS, [ + 'connections/count', % current connections + 'connections/max' % maximum connections connected ]). %% Session stats @@ -77,14 +75,10 @@ -define(SERVER, ?MODULE). %% @doc Start stats server --spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -%% Get all stats. --spec(all() -> stats()). -all() -> getstats(). - %% @doc Generate stats fun -spec(statsfun(Stat :: atom()) -> fun()). statsfun(Stat) -> @@ -139,13 +133,13 @@ rec(Name, Secs, UpFun) -> cast(Msg) -> gen_server:cast(?SERVER, Msg). -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([]) -> _ = emqx_tables:new(?TAB, [set, public, {write_concurrency, true}]), - Stats = lists:append([?CLIENT_STATS, ?SESSION_STATS, ?PUBSUB_STATS, + Stats = lists:append([?CONNECTION_STATS, ?SESSION_STATS, ?PUBSUB_STATS, ?ROUTE_STATS, ?RETAINED_STATS]), true = ets:insert(?TAB, [{Name, 0} || Name <- Stats]), {ok, start_timer(#state{updates = []}), hibernate}. @@ -185,19 +179,19 @@ handle_cast(Msg, State) -> emqx_logger:error("[Stats] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({timeout, TRef, tick}, State = #state{timer= TRef, updates = Updates}) -> - lists:foldl( - fun(Update = #update{name = Name, countdown = C, interval = I, - func = UpFun}, Acc) when C =< 0 -> - try UpFun() - catch _:Error -> - emqx_logger:error("[Stats] update ~s error: ~p", [Name, Error]) - end, - [Update#update{countdown = I} | Acc]; - (Update = #update{countdown = C}, Acc) -> - [Update#update{countdown = C - 1} | Acc] - end, [], Updates), - {noreply, start_timer(State), hibernate}; +handle_info({timeout, TRef, tick}, State = #state{timer = TRef, updates = Updates}) -> + Updates1 = lists:foldl( + fun(Update = #update{name = Name, countdown = C, interval = I, + func = UpFun}, Acc) when C =< 0 -> + try UpFun() + catch _:Error -> + emqx_logger:error("[Stats] update ~s error: ~p", [Name, Error]) + end, + [Update#update{countdown = I} | Acc]; + (Update = #update{countdown = C}, Acc) -> + [Update#update{countdown = C - 1} | Acc] + end, [], Updates), + {noreply, start_timer(State#state{updates = Updates1}), hibernate}; handle_info(Info, State) -> emqx_logger:error("[Stats] unexpected info: ~p", [Info]), @@ -209,9 +203,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}) diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl index 57dc41703..5001f733b 100644 --- a/src/emqx_sys.erl +++ b/src/emqx_sys.erl @@ -114,7 +114,7 @@ handle_info({timeout, TRef, tick}, State = #state{ticker = TRef, version = Versi publish(version, Version), publish(sysdescr, Descr), publish(brokers, ekka_mnesia:running_nodes()), - publish(stats, emqx_stats:all()), + publish(stats, emqx_stats:getstats()), publish(metrics, emqx_metrics:all()), {noreply, tick(State), hibernate}; diff --git a/src/emqx_types.erl b/src/emqx_types.erl index eeca513a6..32654b121 100644 --- a/src/emqx_types.erl +++ b/src/emqx_types.erl @@ -16,11 +16,14 @@ %%-include("emqx.hrl"). +-export_type([startlink_ret/0]). -export_type([zone/0, client_id/0, username/0, password/0, peername/0, protocol/0, credentials/0]). -export_type([payload/0]). %%-export_type([payload/0, message/0, delivery/0]). +-type(startlink_ret() :: {ok, pid()} | ignore | {error, term()}). + -type(zone() :: atom()). -type(client_id() :: binary() | atom()). -type(username() :: binary() | undefined). diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index d488097bd..0a29d298c 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -47,7 +47,7 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -define(WSLOG(Level, Format, Args, State), - lager:Level("WsClient(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). + emqx_logger:Level("WsClient(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). %%------------------------------------------------------------------------------ %% API @@ -84,7 +84,6 @@ call(WSPid, Req) -> %%------------------------------------------------------------------------------ init(Req, Opts) -> - io:format("Opts: ~p~n", [Opts]), case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of undefined -> {cowboy_websocket, Req, #state{}}; @@ -200,7 +199,7 @@ websocket_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> websocket_info(emit_stats, State = #state{proto_state = ProtoState}) -> Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(), emqx_protocol:stats(ProtoState)]), - emqx_cm:set_client_stats(emqx_protocol:clientid(ProtoState), Stats), + emqx_cm:set_conn_stats(emqx_protocol:clientid(ProtoState), Stats), {ok, State#state{stats_timer = undefined}, hibernate}; websocket_info({keepalive, start, Interval}, State) -> From c92eba4a5e4fc83edd438b660573a99e2e2f68be Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Tue, 28 Aug 2018 00:27:45 +0800 Subject: [PATCH 142/520] bug fixes during UT --- src/emqx_access_control.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index bd35d7ebc..7c6bcfb60 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -176,7 +176,7 @@ handle_call({unregister_mod, Type, Mod}, _From, State) -> reply(case lists:keyfind(Mod, 1, Mods) of false -> {error, not_found}; - true -> + {Mod, _ModState, _Seq} -> ets:insert(?TAB, {tab_key(Type), lists:keydelete(Mod, 1, Mods)}), ok end, State); From 8e11845f8798552fe6c21b0fd95e5922451afc34 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 28 Aug 2018 07:36:22 +0800 Subject: [PATCH 143/520] Remove ignore_loop_deliver option --- etc/emqx.conf | 6 ------ priv/emqx.schema | 1 - src/emqx_frame.erl | 2 +- src/emqx_session.erl | 7 ++++--- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index c35b1d0f7..b582f9003 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -605,12 +605,6 @@ zone.external.max_awaiting_rel = 100 ## Value: Duration zone.external.await_rel_timeout = 60s -## Whether to ignore loop delivery of messages. -## -## Value: true | false -## Default: false -zone.external.ignore_loop_deliver = false - ## Default session expiry interval for MQTT V3.1.1 connections. ## ## Value: Duration diff --git a/priv/emqx.schema b/priv/emqx.schema index 765363607..af4935c33 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -769,7 +769,6 @@ end}. %% @doc Ignore loop delivery of messages {mapping, "zone.$name.ignore_loop_deliver", "emqx.zones", [ - {default, false}, {datatype, {enum, [true, false]}} ]}. diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 59a195e33..c1cc34d59 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -331,7 +331,7 @@ parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) -> {Value + Len * Multiplier, Rest}. parse_topic_filters(subscribe, Bin) -> - [{Topic, #{rh => Rh, rap => Rap, nl => Nl, qos => QoS, rc => 0, subid => 0}} + [{Topic, #{rh => Rh, rap => Rap, nl => Nl, qos => QoS, rc => 0}} || <> <= Bin]; parse_topic_filters(unsubscribe, Bin) -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index db7e80a9b..a96fac8f3 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -611,9 +611,10 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -with_subid(#{'Subscription-Identifier' := SubId}, Opts) -> - maps:put(subid, SubId, Opts); -with_subid(_Props, Opts) -> Opts. +with_subid(#{'Subscription-Identifier' := SubId}, SubOpts) -> + maps:put(subid, SubId, SubOpts); +with_subid(_Props, SubOpts) -> + SubOpts. suback(_From, undefined, _ReasonCodes) -> ignore; From 9d22fcb41392c080e717df2e882da11228e5245e Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Tue, 28 Aug 2018 09:20:32 +0800 Subject: [PATCH 144/520] update emqx_topic_SUITE --- test/emqx_mock_client.erl | 1 - test/emqx_topic_SUITE.erl | 34 ++++++++++++++++++---------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index 8afbeeb17..2ffdbe093 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -59,7 +59,6 @@ handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> }}; handle_call({stop_session, SessPid}, _From, State) -> - unlink(SessPid), emqx_sm:close_session(SessPid), {stop, normal, ok, State}; diff --git a/test/emqx_topic_SUITE.erl b/test/emqx_topic_SUITE.erl index f6f2c007e..816579ebc 100644 --- a/test/emqx_topic_SUITE.erl +++ b/test/emqx_topic_SUITE.erl @@ -132,20 +132,22 @@ t_validate(_) -> true = validate({filter, <<"x">>}), true = validate({name, <<"x//y">>}), true = validate({filter, <<"sport/tennis/#">>}), - false = validate({name, <<>>}), - false = validate({name, long_topic()}), - false = validate({name, <<"abc/#">>}), - false = validate({filter, <<"abc/#/1">>}), - false = validate({filter, <<"abc/#xzy/+">>}), - false = validate({filter, <<"abc/xzy/+9827">>}), - false = validate({filter, <<"sport/tennis#">>}), - false = validate({filter, <<"sport/tennis/#/ranking">>}). + 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. t_sigle_level_validate(_) -> true = validate({filter, <<"+">>}), true = validate({filter, <<"+/tennis/#">>}), true = validate({filter, <<"sport/+/player1">>}), - false = validate({filter, <<"sport+">>}). + catch validate({filter, <<"sport+">>}), + ok. t_triples(_) -> Triples = [{root,<<"a">>,<<"a">>}, @@ -199,11 +201,11 @@ long_topic() -> iolist_to_binary([[integer_to_list(I), "/"] || I <- lists:seq(0, 10000)]). t_parse(_) -> - ?assertEqual({<<"a/b/+/#">>, []}, parse(<<"a/b/+/#">>)), - ?assertEqual({<<"topic">>, [{share, '$queue'}]}, parse(<<"$queue/topic">>)), - ?assertEqual({<<"topic">>, [{share, <<"group">>}]}, parse(<<"$share/group/topic">>)), - ?assertEqual({<<"topic">>, [local]}, parse(<<"$local/topic">>)), - ?assertEqual({<<"topic">>, [{share, '$queue'}, local]}, parse(<<"$local/$queue/topic">>)), - ?assertEqual({<<"/a/b/c">>, [{share, <<"group">>}, local]}, parse(<<"$local/$share/group//a/b/c">>)), - ?assertEqual({<<"topic">>, [fastlane]}, parse(<<"$fastlane/topic">>)). + ?assertEqual({<<"a/b/+/#">>, #{}}, parse(<<"a/b/+/#">>)), + ?assertEqual({<<"topic">>, #{ share => <<"$queue">> }}, parse(<<"$queue/topic">>)), + ?assertEqual({<<"topic">>, #{ share => <<"group">>}}, parse(<<"$share/group/topic">>)), + ?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">>)). From b59db00aeb7bfeeb74f65e3416efb707fb5df17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Tue, 28 Aug 2018 09:42:17 +0800 Subject: [PATCH 145/520] Add sm test suite, and fix bug in stats test suite --- test/emqx_sm_SUITE.erl | 42 ++++++++++++++++++++++++++++++++++++ test/emqx_stats_SUITE.erl | 45 ++++++++++++++++++--------------------- 2 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 test/emqx_sm_SUITE.erl diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl new file mode 100644 index 000000000..946f85d1b --- /dev/null +++ b/test/emqx_sm_SUITE.erl @@ -0,0 +1,42 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx.hrl"). + +all() -> [t_open_close_session]. + +t_open_close_session(_) -> + emqx_ct_broker_helpers:run_setup_steps(), + {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), + Attrs = #{clean_start => true, client_id => <<"client">>, client_pid => ClientPid, zone => internal, username => <<"zhou">>, conn_props => ref}, + {ok, _SPid} = emqx_sm:open_session(Attrs), + [{<<"client">>, SPid}] = emqx_sm:lookup_session(<<"client">>), + SPid = emqx_sm:lookup_session_pid(<<"client">>), + {ok, NewClientPid} = emqx_mock_client:start_link(<<"client">>), + {ok, SPid, true} = emqx_sm:open_session(Attrs#{clean_start => false, client_pid => NewClientPid}), + [{<<"client">>, SPid}] = emqx_sm:lookup_session(<<"client">>), + SAttrs = emqx_sm:get_session_attrs({<<"client">>, SPid}), + <<"client">> = proplists:get_value(client_id, SAttrs), + Session = {<<"client">>, SPid}, + emqx_sm:set_session_stats(Session, {open, true}), + {open, true} = emqx_sm:get_session_stats(Session), + ok = emqx_sm:close_session(SPid), + [] = emqx_sm:lookup_session(<<"client">>). \ No newline at end of file diff --git a/test/emqx_stats_SUITE.erl b/test/emqx_stats_SUITE.erl index b544d6128..c7fc4ecce 100644 --- a/test/emqx_stats_SUITE.erl +++ b/test/emqx_stats_SUITE.erl @@ -25,36 +25,33 @@ all() -> [t_set_get_state, t_update_interval]. t_set_get_state(_) -> {ok, _} = emqx_stats:start_link(), - SetClientsCount = emqx_stats:statsfun('clients/count'), - SetClientsCount(1), - 1 = emqx_stats:getstat('clients/count'), - emqx_stats:setstat('clients/count', 2), - 2 = emqx_stats:getstat('clients/count'), - emqx_stats:setstat('clients/count', 'clients/max', 3), + SetConnsCount = emqx_stats:statsfun('connections/count'), + SetConnsCount(1), + 1 = emqx_stats:getstat('connections/count'), + emqx_stats:setstat('connections/count', 2), + 2 = emqx_stats:getstat('connections/count'), + emqx_stats:setstat('connections/count', 'connections/max', 3), timer:sleep(100), - 3 = emqx_stats:getstat('clients/count'), - 3 = emqx_stats:getstat('clients/max'), - emqx_stats:setstat('clients/count', 'clients/max', 2), + 3 = emqx_stats:getstat('connections/count'), + 3 = emqx_stats:getstat('connections/max'), + emqx_stats:setstat('connections/count', 'connections/max', 2), timer:sleep(100), - 2 = emqx_stats:getstat('clients/count'), - 3 = emqx_stats:getstat('clients/max'), - SetClients = emqx_stats:statsfun('clients/count', 'clients/max'), - SetClients(4), + 2 = emqx_stats:getstat('connections/count'), + 3 = emqx_stats:getstat('connections/max'), + SetConns = emqx_stats:statsfun('connections/count', 'connections/max'), + SetConns(4), timer:sleep(100), - 4 = emqx_stats:getstat('clients/count'), - 4 = emqx_stats:getstat('clients/max'), - Clients = emqx_stats:getstats(), - 4 = proplists:get_value('clients/count', Clients), - 4 = proplists:get_value('clients/max', Clients). + 4 = emqx_stats:getstat('connections/count'), + 4 = emqx_stats:getstat('connections/max'), + Conns = emqx_stats:getstats(), + 4 = proplists:get_value('connections/count', Conns), + 4 = proplists:get_value('connections/max', Conns). t_update_interval(_) -> {ok, _} = emqx_stats:start_link(), ok = emqx_stats:update_interval(cm_stats, fun update_stats/0), - timer:sleep(2000), - 1 = emqx_stats:getstat('clients/count'). + timer:sleep(2500), + 1 = emqx_stats:getstat('connections/count'). update_stats() -> - ClientsCount = emqx_stats:getstat('clients/count'), - ct:log("hello~n"), - % emqx_stats:setstat('clients/count', 'clients/max', ClientsCount + 1). - emqx_stats:setstat('clients/count', 1). \ No newline at end of file + emqx_stats:setstat('connections/count', 1). \ No newline at end of file From 3e4b15fd6de800625401c4990b3896ecdaaa3e06 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Tue, 28 Aug 2018 13:03:54 +0800 Subject: [PATCH 146/520] update emqx_router_SUITE --- test/emqx_router_SUITE.erl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/emqx_router_SUITE.erl b/test/emqx_router_SUITE.erl index 17da6d97d..c1591d780 100644 --- a/test/emqx_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -49,11 +49,21 @@ end_per_testcase(_TestCase, _Config) -> add_del_route(_) -> From = {self(), make_ref()}, ?R:add_route(From, <<"a/b/c">>, node()), + timer:sleep(1), + ?R:add_route(From, <<"a/b/c">>, node()), + timer:sleep(1), + ?R:add_route(From, <<"a/+/b">>, node()), + ct:log("Topics: ~p ~n", [emqx_topic:wildcard(<<"a/+/b">>)]), + timer:sleep(1), + ?assertEqual([<<"a/+/b">>, <<"a/b/c">>], lists:sort(?R:topics())), + ?R:del_route(From, <<"a/b/c">>, node()), + ?R:del_route(From, <<"a/+/b">>, node()), + timer:sleep(1), ?assertEqual([], lists:sort(?R:topics())). match_routes(_) -> @@ -62,6 +72,7 @@ match_routes(_) -> ?R:add_route(From, <<"a/+/c">>, node()), ?R:add_route(From, <<"a/b/#">>, node()), ?R:add_route(From, <<"#">>, node()), + timer:sleep(6), ?assertEqual([#route{topic = <<"#">>, dest = node()}, #route{topic = <<"a/+/c">>, dest = node()}, #route{topic = <<"a/b/#">>, dest = node()}, From 545e80cd6b18632e7e08bf62311112319197b813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Tue, 28 Aug 2018 17:53:03 +0800 Subject: [PATCH 147/520] Add some test suites --- src/emqx_mqtt_caps.erl | 2 +- test/emqx_cm_SUITE.erl | 24 ++++---- test/emqx_inflight_SUITE.erl | 91 +++++++++++----------------- test/emqx_json_SUITE.erl | 39 ++++++++++++ test/emqx_mqtt_caps_SUITE.erl | 93 ++++++++++++++++++++++++++++- test/emqx_mqtt_properties_SUITE.erl | 29 +++++++++ test/emqx_tables_SUITE.erl | 28 +++++++++ test/emqx_zone_SUITE.erl | 33 ++++++++++ 8 files changed, 269 insertions(+), 70 deletions(-) create mode 100644 test/emqx_json_SUITE.erl create mode 100644 test/emqx_mqtt_properties_SUITE.erl create mode 100644 test/emqx_tables_SUITE.erl create mode 100644 test/emqx_zone_SUITE.erl diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl index d9ad79e0a..27b8ad7bc 100644 --- a/src/emqx_mqtt_caps.erl +++ b/src/emqx_mqtt_caps.erl @@ -62,7 +62,7 @@ do_check_pub(Props = #{qos := QoS}, [{max_qos_allowed, MaxQoS}|Caps]) -> end; do_check_pub(#{retain := true}, [{mqtt_retain_available, false}|_Caps]) -> {error, ?RC_RETAIN_NOT_SUPPORTED}; -do_check_pub(Props, [{mqtt_retain_available, true}|Caps]) -> +do_check_pub(Props, [{mqtt_retain_available, _}|Caps]) -> do_check_pub(Props, Caps). -spec(check_sub(zone(), mqtt_topic_filters()) -> {ok | error, mqtt_topic_filters()}). diff --git a/test/emqx_cm_SUITE.erl b/test/emqx_cm_SUITE.erl index 4cb4fa8a4..440b2788b 100644 --- a/test/emqx_cm_SUITE.erl +++ b/test/emqx_cm_SUITE.erl @@ -21,19 +21,19 @@ -include("emqx_mqtt.hrl"). -all() -> [t_register_unregister_client]. +all() -> [t_register_unregister_connection]. -t_register_unregister_client(_) -> +t_register_unregister_connection(_) -> {ok, _} = emqx_cm_sup:start_link(), Pid = self(), - emqx_cm:register_client(<<0, 0, 1>>), - emqx_cm:register_client({<<0, 0, 2>>, Pid}, [{port, 8080}, {ip, "192.168.0.1"}]), + emqx_cm:register_connection(<<"conn1">>), + emqx_cm:register_connection({<<"conn2">>, Pid}, [{port, 8080}, {ip, "192.168.0.1"}]), timer:sleep(2000), - [{<<0, 0, 1>>, Pid}] = emqx_cm:lookup_client(<<0, 0, 1>>), - [{<<0, 0, 2>>, Pid}] = emqx_cm:lookup_client(<<0, 0, 2>>), - Pid = emqx_cm:lookup_client_pid(<<0, 0, 1>>), - emqx_cm:unregister_client(<<0, 0, 1>>), - [] = emqx_cm:lookup_client(<<0, 0, 1>>), - [{port, 8080}, {ip, "192.168.0.1"}] = emqx_cm:get_client_attrs({<<0, 0, 2>>, Pid}), - emqx_cm:set_client_stats(<<0, 0, 2>>, [[{count, 1}, {max, 2}]]), - [[{count, 1}, {max, 2}]] = emqx_cm:get_client_stats({<<0, 0, 2>>, Pid}). \ No newline at end of file + [{<<"conn1">>, Pid}] = emqx_cm:lookup_connection(<<"conn1">>), + [{<<"conn2">>, Pid}] = emqx_cm:lookup_connection(<<"conn2">>), + Pid = emqx_cm:lookup_conn_pid(<<"conn1">>), + emqx_cm:unregister_connection(<<"conn1">>), + [] = emqx_cm:lookup_connection(<<"conn1">>), + [{port, 8080}, {ip, "192.168.0.1"}] = emqx_cm:get_conn_attrs({<<"conn2">>, Pid}), + emqx_cm:set_conn_stats(<<"conn2">>, [[{count, 1}, {max, 2}]]), + [[{count, 1}, {max, 2}]] = emqx_cm:get_conn_stats({<<"conn2">>, Pid}). \ No newline at end of file diff --git a/test/emqx_inflight_SUITE.erl b/test/emqx_inflight_SUITE.erl index de3accc06..25f4cd7da 100644 --- a/test/emqx_inflight_SUITE.erl +++ b/test/emqx_inflight_SUITE.erl @@ -1,62 +1,43 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_inflight_SUITE). --include_lib("eunit/include/eunit.hrl"). - -compile(export_all). -compile(nowarn_export_all). --import(emqx_inflight, [new/1, contain/2, insert/3, lookup/2, update/3, - delete/2, is_empty/1, is_full/1]). - -all() -> - [t_contain, t_lookup, t_insert, t_update, t_delete, t_window, - t_is_full, t_is_empty]. - -t_contain(_) -> - ?assertNot(contain(k, new(0))), - ?assert(contain(k, insert(k, v, new(0)))). - -t_lookup(_) -> - Inflight = insert(k, v, new(0)), - ?assertEqual({value, v}, lookup(k, Inflight)), - ?assertEqual(none, lookup(x, Inflight)). - -t_insert(_) -> - Inflight = insert(k2, v2, insert(k1, v1, new(0))), - ?assertEqual({value, v1}, lookup(k1, Inflight)), - ?assertEqual({value, v2}, lookup(k2, Inflight)). - -t_update(_) -> - Inflight = update(k, v2, insert(k, v1, new(0))), - ?assertEqual({value, v2}, lookup(k, Inflight)). - -t_delete(_) -> - ?assert(is_empty(delete(k, insert(k, v1, new(0))))). - -t_window(_) -> - ?assertEqual([], emqx_inflight:window(new(10))), - Inflight = insert(2, 2, insert(1, 1, new(0))), - ?assertEqual([1, 2], emqx_inflight:window(Inflight)). - -t_is_full(_) -> - ?assert(is_full(insert(k, v1, new(1)))). - -t_is_empty(_) -> - ?assertNot(is_empty(insert(k, v1, new(1)))). +all() -> [t_inflight_all]. +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). \ No newline at end of file diff --git a/test/emqx_json_SUITE.erl b/test/emqx_json_SUITE.erl new file mode 100644 index 000000000..9d8b6e697 --- /dev/null +++ b/test/emqx_json_SUITE.erl @@ -0,0 +1,39 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_json_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +all() -> [t_decode_encode, t_safe_decode_encode]. + +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}]). + +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}]), + BadJsonText = <<"{\"library\", \"awesome\": true}">>, + {error, _} = emqx_json:safe_decode(BadJsonText), + {error, _} = emqx_json:safe_encode({a, {b ,1}}). \ No newline at end of file diff --git a/test/emqx_mqtt_caps_SUITE.erl b/test/emqx_mqtt_caps_SUITE.erl index 3fbb422d5..f2f50a296 100644 --- a/test/emqx_mqtt_caps_SUITE.erl +++ b/test/emqx_mqtt_caps_SUITE.erl @@ -16,11 +16,100 @@ -include_lib("eunit/include/eunit.hrl"). +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). + %% CT -compile(export_all). -compile(nowarn_export_all). -all() -> - []. +all() -> [t_get_set_caps, t_check_pub, t_check_sub]. + +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 + }, + Caps = emqx_mqtt_caps:get_caps(zone), + PubCaps = #{ + max_qos_allowed => ?QOS_2, + mqtt_retain_available => true + }, + 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). + +t_check_pub(_) -> + {ok, _} = emqx_zone:start_link(), + PubCaps = #{ + max_qos_allowed => ?QOS_1, + mqtt_retain_available => false + }, + emqx_zone:set_env(zone, '$mqtt_pub_caps', PubCaps), + timer:sleep(100), + 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), + PubProps = #{ + qos => ?QOS_1, + retain => false + }, + ok = emqx_mqtt_caps:check_pub(zone, PubProps). + +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}}]). + + +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. diff --git a/test/emqx_mqtt_properties_SUITE.erl b/test/emqx_mqtt_properties_SUITE.erl new file mode 100644 index 000000000..7fe78433c --- /dev/null +++ b/test/emqx_mqtt_properties_SUITE.erl @@ -0,0 +1,29 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_properties_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx_mqtt.hrl"). + +all() -> [t_mqtt_properties_all]. + +t_mqtt_properties_all(_) -> + Props = emqx_mqtt_properties:filter(?CONNECT, #{'Session-Expiry-Interval' => 1, 'Maximum-Packet-Size' => 255}), + ok = emqx_mqtt_properties:validate(Props), + #{} = emqx_mqtt_properties:filter(?CONNECT, #{'Maximum-QoS' => ?QOS_2}). \ No newline at end of file diff --git a/test/emqx_tables_SUITE.erl b/test/emqx_tables_SUITE.erl new file mode 100644 index 000000000..618e83597 --- /dev/null +++ b/test/emqx_tables_SUITE.erl @@ -0,0 +1,28 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_tables_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +all() -> [t_new]. + +t_new(_) -> + TId = emqx_tables:new(test_table, [{read_concurrency, true}]), + ets:insert(TId, {loss, 100}), + TId = emqx_tables:new(test_table, [{read_concurrency, true}]), + 100 = ets:lookup_element(TId, loss, 2). diff --git a/test/emqx_zone_SUITE.erl b/test/emqx_zone_SUITE.erl new file mode 100644 index 000000000..deca884d8 --- /dev/null +++ b/test/emqx_zone_SUITE.erl @@ -0,0 +1,33 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_zone_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx_mqtt.hrl"). + +all() -> [t_set_get_env]. + +t_set_get_env(_) -> + {ok, _} = emqx_zone:start_link(), + ok = emqx_zone:set_env(china, language, chinese), + timer:sleep(100), % make sure set_env/3 is okay + 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). From 41d1f04659343647358f768aa4acbdcef7687936 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 28 Aug 2018 20:37:46 +0800 Subject: [PATCH 148/520] Update copyright --- test/emqx_SUITE.erl | 4 +--- test/emqx_access_SUITE.erl | 6 ++---- test/emqx_acl_test_mod.erl | 4 +--- test/emqx_auth_anonymous_test_mod.erl | 4 +--- test/emqx_auth_dashboard.erl | 4 +--- test/emqx_base62_SUITE.erl | 4 +--- test/emqx_broker_SUITE.erl | 8 +++----- test/emqx_client_SUITE.erl | 28 +++++++++++++-------------- test/emqx_cm_SUITE.erl | 6 ++---- test/emqx_ct_broker_helpers.erl | 28 +++++++++++++-------------- test/emqx_ct_helpers.erl | 28 +++++++++++++-------------- test/emqx_frame_SUITE.erl | 28 +++++++++++++-------------- test/emqx_guid_SUITE.erl | 4 +--- test/emqx_inflight_SUITE.erl | 6 ++---- test/emqx_json_SUITE.erl | 6 ++---- test/emqx_keepalive_SUITE.erl | 4 +--- test/emqx_lib_SUITE.erl | 4 +--- test/emqx_metrics_SUITE.erl | 6 ++---- test/emqx_misc_SUITE.erl | 4 +--- test/emqx_mock_client.erl | 4 +--- test/emqx_mod_SUITE.erl | 4 +--- test/emqx_mqtt_compat_SUITE.erl | 28 +++++++++++++-------------- test/emqx_mqtt_properties_SUITE.erl | 6 ++---- test/emqx_mqueue_SUITE.erl | 4 +--- test/emqx_net_SUITE.erl | 4 +--- test/emqx_pqueue_SUITE.erl | 4 +--- test/emqx_router_SUITE.erl | 28 +++++++++++++-------------- test/emqx_sm_SUITE.erl | 6 ++---- test/emqx_stats_SUITE.erl | 6 ++---- test/emqx_tables_SUITE.erl | 4 +--- test/emqx_time_SUITE.erl | 4 +--- test/emqx_trie_SUITE.erl | 28 +++++++++++++-------------- test/emqx_vm_SUITE.erl | 4 +--- test/emqx_zone_SUITE.erl | 5 ++--- 34 files changed, 129 insertions(+), 196 deletions(-) diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index 289b38aec..1b1cff9e8 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index 9e6f86210..244030379 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). @@ -381,4 +379,4 @@ match_rule(_) -> AndRule = compile({allow, {'and', [{ipaddr, "127.0.0.1"}, {user, <<"TestUser">>}]}, publish, <<"Topic">>}), {matched, allow} = match(User, <<"Topic">>, AndRule), OrRule = compile({allow, {'or', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, ["Topic"]}), - {matched, allow} = match(User, <<"Topic">>, OrRule). \ No newline at end of file + {matched, allow} = match(User, <<"Topic">>, OrRule). diff --git a/test/emqx_acl_test_mod.erl b/test/emqx_acl_test_mod.erl index 8bcf644d6..131336cdd 100644 --- a/test/emqx_acl_test_mod.erl +++ b/test/emqx_acl_test_mod.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_auth_anonymous_test_mod.erl b/test/emqx_auth_anonymous_test_mod.erl index 8dacacbc3..e04841feb 100644 --- a/test/emqx_auth_anonymous_test_mod.erl +++ b/test/emqx_auth_anonymous_test_mod.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_auth_dashboard.erl b/test/emqx_auth_dashboard.erl index 9c3d1e424..b8c742d3b 100644 --- a/test/emqx_auth_dashboard.erl +++ b/test/emqx_auth_dashboard.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_base62_SUITE.erl b/test/emqx_base62_SUITE.erl index 820c7ec32..e303fd8ee 100644 --- a/test/emqx_base62_SUITE.erl +++ b/test/emqx_base62_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright (c) 2018 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,7 +11,6 @@ %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. -%%-------------------------------------------------------------------- -module(emqx_base62_SUITE). diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index a71a96539..93f795d1d 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,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). -compile(export_all). @@ -28,8 +27,7 @@ -include("emqx_mqtt.hrl"). all() -> - [ - {group, pubsub}, + [{group, pubsub}, {group, session}, {group, broker}, {group, metrics}, diff --git a/test/emqx_client_SUITE.erl b/test/emqx_client_SUITE.erl index 7b2d5aaae..82b4bc423 100644 --- a/test/emqx_client_SUITE.erl +++ b/test/emqx_client_SUITE.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. -module(emqx_client_SUITE). diff --git a/test/emqx_cm_SUITE.erl b/test/emqx_cm_SUITE.erl index 440b2788b..5e29e075e 100644 --- a/test/emqx_cm_SUITE.erl +++ b/test/emqx_cm_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). @@ -36,4 +34,4 @@ t_register_unregister_connection(_) -> [] = emqx_cm:lookup_connection(<<"conn1">>), [{port, 8080}, {ip, "192.168.0.1"}] = emqx_cm:get_conn_attrs({<<"conn2">>, Pid}), emqx_cm:set_conn_stats(<<"conn2">>, [[{count, 1}, {max, 2}]]), - [[{count, 1}, {max, 2}]] = emqx_cm:get_conn_stats({<<"conn2">>, Pid}). \ No newline at end of file + [[{count, 1}, {max, 2}]] = emqx_cm:get_conn_stats({<<"conn2">>, Pid}). diff --git a/test/emqx_ct_broker_helpers.erl b/test/emqx_ct_broker_helpers.erl index a62297a49..a20508b82 100644 --- a/test/emqx_ct_broker_helpers.erl +++ b/test/emqx_ct_broker_helpers.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_ct_broker_helpers). diff --git a/test/emqx_ct_helpers.erl b/test/emqx_ct_helpers.erl index c1618eccd..c61c5be6e 100644 --- a/test/emqx_ct_helpers.erl +++ b/test/emqx_ct_helpers.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_ct_helpers). diff --git a/test/emqx_frame_SUITE.erl b/test/emqx_frame_SUITE.erl index 49ffa40c4..b24644cd3 100644 --- a/test/emqx_frame_SUITE.erl +++ b/test/emqx_frame_SUITE.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_frame_SUITE). diff --git a/test/emqx_guid_SUITE.erl b/test/emqx_guid_SUITE.erl index 0dee1da48..cb2e6543c 100644 --- a/test/emqx_guid_SUITE.erl +++ b/test/emqx_guid_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_inflight_SUITE.erl b/test/emqx_inflight_SUITE.erl index 25f4cd7da..5e504f4f8 100644 --- a/test/emqx_inflight_SUITE.erl +++ b/test/emqx_inflight_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). @@ -40,4 +38,4 @@ t_inflight_all(_) -> 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). \ No newline at end of file + [a, b] = emqx_inflight:window(Inflight2). diff --git a/test/emqx_json_SUITE.erl b/test/emqx_json_SUITE.erl index 9d8b6e697..980d29703 100644 --- a/test/emqx_json_SUITE.erl +++ b/test/emqx_json_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). @@ -36,4 +34,4 @@ t_safe_decode_encode(_) -> {ok, JsonText} = emqx_json:safe_encode(JsonTerm, [{space, 1}]), BadJsonText = <<"{\"library\", \"awesome\": true}">>, {error, _} = emqx_json:safe_decode(BadJsonText), - {error, _} = emqx_json:safe_encode({a, {b ,1}}). \ No newline at end of file + {error, _} = emqx_json:safe_encode({a, {b ,1}}). diff --git a/test/emqx_keepalive_SUITE.erl b/test/emqx_keepalive_SUITE.erl index 270d78830..e07c96ffe 100644 --- a/test/emqx_keepalive_SUITE.erl +++ b/test/emqx_keepalive_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_lib_SUITE.erl b/test/emqx_lib_SUITE.erl index 2cd24bb63..72153f6b3 100644 --- a/test/emqx_lib_SUITE.erl +++ b/test/emqx_lib_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_metrics_SUITE.erl b/test/emqx_metrics_SUITE.erl index 7c7c83803..8e601562c 100644 --- a/test/emqx_metrics_SUITE.erl +++ b/test/emqx_metrics_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). @@ -38,4 +36,4 @@ t_inc_dec_metrics(_) -> emqx_metrics:received(#mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}}), {1, 1} = {emqx_metrics:val('packets/received'), emqx_metrics:val('packets/connect')}, emqx_metrics:sent(#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}}), - {1, 1} = {emqx_metrics:val('packets/sent'), emqx_metrics:val('packets/connack')}. \ No newline at end of file + {1, 1} = {emqx_metrics:val('packets/sent'), emqx_metrics:val('packets/connack')}. diff --git a/test/emqx_misc_SUITE.erl b/test/emqx_misc_SUITE.erl index 4b7ec74f6..766691869 100644 --- a/test/emqx_misc_SUITE.erl +++ b/test/emqx_misc_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index 2ffdbe093..95cb38130 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_mod_SUITE.erl b/test/emqx_mod_SUITE.erl index 963d39c45..44376f7b7 100644 --- a/test/emqx_mod_SUITE.erl +++ b/test/emqx_mod_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_mqtt_compat_SUITE.erl b/test/emqx_mqtt_compat_SUITE.erl index d27c094ea..0edbd148e 100644 --- a/test/emqx_mqtt_compat_SUITE.erl +++ b/test/emqx_mqtt_compat_SUITE.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_compat_SUITE). diff --git a/test/emqx_mqtt_properties_SUITE.erl b/test/emqx_mqtt_properties_SUITE.erl index 7fe78433c..a8301d1f4 100644 --- a/test/emqx_mqtt_properties_SUITE.erl +++ b/test/emqx_mqtt_properties_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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_properties_SUITE). @@ -26,4 +24,4 @@ all() -> [t_mqtt_properties_all]. t_mqtt_properties_all(_) -> Props = emqx_mqtt_properties:filter(?CONNECT, #{'Session-Expiry-Interval' => 1, 'Maximum-Packet-Size' => 255}), ok = emqx_mqtt_properties:validate(Props), - #{} = emqx_mqtt_properties:filter(?CONNECT, #{'Maximum-QoS' => ?QOS_2}). \ No newline at end of file + #{} = emqx_mqtt_properties:filter(?CONNECT, #{'Maximum-QoS' => ?QOS_2}). diff --git a/test/emqx_mqueue_SUITE.erl b/test/emqx_mqueue_SUITE.erl index 5ab510633..0cff6a627 100644 --- a/test/emqx_mqueue_SUITE.erl +++ b/test/emqx_mqueue_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_net_SUITE.erl b/test/emqx_net_SUITE.erl index 34f3d54e2..50a830d10 100644 --- a/test/emqx_net_SUITE.erl +++ b/test/emqx_net_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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_pqueue_SUITE.erl b/test/emqx_pqueue_SUITE.erl index 55b2dc01b..e610a7639 100644 --- a/test/emqx_pqueue_SUITE.erl +++ b/test/emqx_pqueue_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_router_SUITE.erl b/test/emqx_router_SUITE.erl index c1591d780..f44039a08 100644 --- a/test/emqx_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_router_SUITE). diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl index 946f85d1b..82bf4a460 100644 --- a/test/emqx_sm_SUITE.erl +++ b/test/emqx_sm_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). @@ -39,4 +37,4 @@ t_open_close_session(_) -> emqx_sm:set_session_stats(Session, {open, true}), {open, true} = emqx_sm:get_session_stats(Session), ok = emqx_sm:close_session(SPid), - [] = emqx_sm:lookup_session(<<"client">>). \ No newline at end of file + [] = emqx_sm:lookup_session(<<"client">>). diff --git a/test/emqx_stats_SUITE.erl b/test/emqx_stats_SUITE.erl index c7fc4ecce..d7fc294b1 100644 --- a/test/emqx_stats_SUITE.erl +++ b/test/emqx_stats_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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_SUITE). @@ -54,4 +52,4 @@ t_update_interval(_) -> 1 = emqx_stats:getstat('connections/count'). update_stats() -> - emqx_stats:setstat('connections/count', 1). \ No newline at end of file + emqx_stats:setstat('connections/count', 1). diff --git a/test/emqx_tables_SUITE.erl b/test/emqx_tables_SUITE.erl index 618e83597..95590b0e9 100644 --- a/test/emqx_tables_SUITE.erl +++ b/test/emqx_tables_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_time_SUITE.erl b/test/emqx_time_SUITE.erl index 8ff8a4437..470b9dfe7 100644 --- a/test/emqx_time_SUITE.erl +++ b/test/emqx_time_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_trie_SUITE.erl b/test/emqx_trie_SUITE.erl index 98d14e7e1..85637a447 100644 --- a/test/emqx_trie_SUITE.erl +++ b/test/emqx_trie_SUITE.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 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_trie_SUITE). diff --git a/test/emqx_vm_SUITE.erl b/test/emqx_vm_SUITE.erl index 1f5c4b2b0..b13b949b4 100644 --- a/test/emqx_vm_SUITE.erl +++ b/test/emqx_vm_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). diff --git a/test/emqx_zone_SUITE.erl b/test/emqx_zone_SUITE.erl index deca884d8..15c449ae9 100644 --- a/test/emqx_zone_SUITE.erl +++ b/test/emqx_zone_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). @@ -31,3 +29,4 @@ t_set_get_env(_) -> cn470 = emqx_zone:get_env(china, ism_band, cn470), undefined = emqx_zone:get_env(undefined, delay), 500 = emqx_zone:get_env(undefined, delay, 500). + From 1cf45329477f97ec2fcd7027b3a72721fde092ab Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 28 Aug 2018 20:40:31 +0800 Subject: [PATCH 149/520] Add emqx_mountpoint module --- src/emqx_mountpoint.erl | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/emqx_mountpoint.erl diff --git a/src/emqx_mountpoint.erl b/src/emqx_mountpoint.erl new file mode 100644 index 000000000..f2046d5ee --- /dev/null +++ b/src/emqx_mountpoint.erl @@ -0,0 +1,52 @@ +%% Copyright (c) 2018 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_mountpoint). + +-include("emqx.hrl"). + +-export([mount/2, unmount/2]). +-export([replvar/2]). + +-type(mountpoint() :: binary()). +-export_type([mountpoint/0]). + +mount(undefined, Any) -> + Any; +mount(MountPoint, Msg = #message{topic = Topic}) -> + Msg#message{topic = <>}; + +mount(MountPoint, TopicFilters) when is_list(TopicFilters) -> + [{<>, SubOpts} || {Topic, SubOpts} <- TopicFilters]. + +unmount(undefined, Msg) -> + Msg; +unmount(MountPoint, Msg = #message{topic = Topic}) -> + case catch split_binary(Topic, byte_size(MountPoint)) of + {MountPoint, Topic1} -> Msg#message{topic = Topic1}; + _Other -> Msg + end. + +replvar(undefined, _Vars) -> + undefined; +replvar(MountPoint, #{client_id := ClientId, username := Username}) -> + lists:foldl(fun feed_var/2, MountPoint, [{<<"%c">>, ClientId}, {<<"%u">>, Username}]). + +feed_var({<<"%c">>, ClientId}, MountPoint) -> + emqx_topic:feed_var(<<"%c">>, ClientId, MountPoint); +feed_var({<<"%u">>, undefined}, MountPoint) -> + MountPoint; +feed_var({<<"%u">>, Username}, MountPoint) -> + emqx_topic:feed_var(<<"%u">>, Username, MountPoint). + From 594819b752e899a3b8caf37e10b218fd134eb139 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 29 Aug 2018 02:53:22 +0800 Subject: [PATCH 150/520] Upgrade the publish sequence of QoS1/2 messages --- TODO | 61 ---------- src/emqx.erl | 6 +- src/emqx_broker.erl | 28 ++--- src/emqx_packet.erl | 65 +++++++---- src/emqx_protocol.erl | 258 ++++++++++++++++++++++-------------------- src/emqx_session.erl | 224 +++++++++++++++++------------------- src/emqx_types.erl | 5 +- 7 files changed, 300 insertions(+), 347 deletions(-) delete mode 100644 TODO diff --git a/TODO b/TODO deleted file mode 100644 index 814b42a3d..000000000 --- a/TODO +++ /dev/null @@ -1,61 +0,0 @@ - -## MQTT 5.0 - -1. Topic Alias -2. Subscriber ID -3. Session ensure stats -4. Message Expiration - -## Connection - -## WebSocket - -## Listeners - -## Protocol - -1. Global ACL cache with limited age and size? -2. Whether to enable ACL for each zone? - -## Session - -## Bridges - -Config -CLI -Remote Bridge -replay queue - -## Access Control - - Global ACL Cache - Add ACL cache emqx_access_control module - -## Zone - -## Hooks - -The hooks design... - -## MQueue - -Bound Queue -LastValue Queue -Priority Queue - -## Supervisor tree - -KernelSup - -## Managment - -## Dashboard - -## Testcases - -1. Update the README.md -2. Update the Documentation -3. Shared subscription and dispatch strategy -4. Remove lager syslog: - dep_lager_syslog = git https://github.com/basho/lager_syslog - diff --git a/src/emqx.erl b/src/emqx.erl index c39dde931..a51a41608 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -84,10 +84,8 @@ subscribe(Topic, Subscriber, Options) when is_tuple(Subscriber)-> {SubPid, SubId} = Subscriber, emqx_broker:subscribe(iolist_to_binary(Topic), SubPid, SubId, Options). -%% @doc Publish Message --spec(publish(message()) -> {ok, delivery()} | {error, term()}). -publish(Msg) -> - emqx_broker:publish(Msg). +-spec(publish(message()) -> {ok, emqx_types:dispatches()}). +publish(Msg) -> emqx_broker:publish(Msg). -spec(unsubscribe(topic() | string()) -> ok | {error, term()}). unsubscribe(Topic) -> diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 7184bb1b3..7adf6064e 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -143,16 +143,18 @@ multi_unsubscribe(Topics, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) - %% Publish %%------------------------------------------------------------------------------ --spec(publish(message()) -> delivery()). +-spec(publish(message()) -> {ok, emqx_types:dispatches()}). publish(Msg) when is_record(Msg, message) -> _ = emqx_tracer:trace(publish, Msg), - case emqx_hooks:run('message.publish', [], Msg) of - {ok, Msg1 = #message{topic = Topic}} -> - route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1)); - {stop, Msg1} -> - emqx_logger:warning("Stop publishing: ~p", [Msg]), delivery(Msg1) - end. + {ok, case emqx_hooks:run('message.publish', [], Msg) of + {ok, Msg1 = #message{topic = Topic}} -> + Delivery = route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1)), + Delivery#delivery.flows; + {stop, _} -> + emqx_logger:warning("Stop publishing: ~p", [Msg]), [] + end}. +-spec(safe_publish(message()) -> ok). %% Called internally safe_publish(Msg) when is_record(Msg, message) -> try @@ -172,8 +174,8 @@ delivery(Msg) -> %%------------------------------------------------------------------------------ route([], Delivery = #delivery{message = Msg}) -> - emqx_hooks:run('message.dropped', [undefined, Msg]), - dropped(Msg#message.topic), Delivery; + emqx_hooks:run('message.dropped', [node(), Msg]), + inc_dropped_cnt(Msg#message.topic), Delivery; route([{To, Node}], Delivery) when Node =:= node() -> dispatch(To, Delivery); @@ -213,8 +215,8 @@ forward(Node, To, Delivery) -> dispatch(Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> case subscribers(Topic) of [] -> - emqx_hooks:run('message.dropped', [undefined, Msg]), - dropped(Topic), Delivery; + emqx_hooks:run('message.dropped', [node(), Msg]), + inc_dropped_cnt(Topic), Delivery; [Sub] -> %% optimize? dispatch(Sub, Topic, Msg), Delivery#delivery{flows = [{dispatch, Topic, 1}|Flows]}; @@ -230,9 +232,9 @@ dispatch({SubPid, _SubId}, Topic, Msg) when is_pid(SubPid) -> dispatch({share, _Group, _Sub}, _Topic, _Msg) -> ignored. -dropped(<<"$SYS/", _/binary>>) -> +inc_dropped_cnt(<<"$SYS/", _/binary>>) -> ok; -dropped(_Topic) -> +inc_dropped_cnt(_Topic) -> emqx_metrics:inc('messages/dropped'). -spec(subscribers(topic()) -> [subscriber()]). diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index c8a751526..939252c3e 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -37,30 +37,40 @@ protocol_name(?MQTT_PROTO_V5) -> type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> lists:nth(Type, ?TYPE_NAMES). +%%------------------------------------------------------------------------------ +%% Validate MQTT Packet +%%------------------------------------------------------------------------------ + validate(?SUBSCRIBE_PACKET(_PacketId, _Properties, [])) -> - error(packet_empty_topic_filters); + error(topic_filters_invalid); validate(?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters)) -> validate_packet_id(PacketId) andalso validate_properties(?SUBSCRIBE, Properties) andalso ok == lists:foreach(fun validate_subscription/1, TopicFilters); validate(?UNSUBSCRIBE_PACKET(_PacketId, [])) -> - error(packet_empty_topic_filters); + error(topic_filters_invalid); validate(?UNSUBSCRIBE_PACKET(PacketId, TopicFilters)) -> validate_packet_id(PacketId) andalso ok == lists:foreach(fun emqx_topic:validate/1, TopicFilters); +validate(?PUBLISH_PACKET(_QoS, <<>>, _, _)) -> + error(topic_name_invalid); +validate(?PUBLISH_PACKET(_QoS, Topic, _, _)) -> + emqx_topic:wildcard(Topic) orelse error(topic_name_invalid); + validate(_Packet) -> true. validate_packet_id(0) -> - error(bad_packet_id); + error(packet_id_invalid); validate_packet_id(_) -> true. -validate_properties(?SUBSCRIBE, #{'Subscription-Identifier' := 0}) -> - error(bad_subscription_identifier); -validate_properties(?SUBSCRIBE, _) -> +validate_properties(?SUBSCRIBE, #{'Subscription-Identifier' := I}) + when I =< 0; I >= 16#FFFFFFF -> + error(subscription_identifier_invalid); +validate_properties(_, _) -> true. validate_subscription({Topic, #{qos := QoS}}) -> @@ -85,30 +95,35 @@ from_message(PacketId, Msg = #message{qos = QoS, topic = Topic, payload = Payloa payload = Payload}. %% @doc Message from Packet --spec(to_message(client_id(), mqtt_packet()) -> message()). -to_message(ClientId, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - retain = Retain, - qos = QoS, - dup = Dup}, - variable = #mqtt_packet_publish{topic_name = Topic, - properties = Props}, - payload = Payload}) -> +-spec(to_message(emqx_types:credentials(), mqtt_packet()) -> message()). +to_message(#{client_id := ClientId, username := Username}, + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + retain = Retain, + qos = QoS, + dup = Dup}, + variable = #mqtt_packet_publish{topic_name = Topic, + properties = Props}, + payload = Payload}) -> Msg = emqx_message:make(ClientId, QoS, Topic, Payload), Msg#message{flags = #{dup => Dup, retain => Retain}, - headers = if - Props =:= undefined -> #{}; - true -> Props - end}; + headers = merge_props(#{username => Username}, Props)}; -to_message(_ClientId, #mqtt_packet_connect{will_flag = false}) -> +to_message(_Credentials, #mqtt_packet_connect{will_flag = false}) -> undefined; -to_message(ClientId, #mqtt_packet_connect{will_retain = Retain, - will_qos = QoS, - will_topic = Topic, - will_props = Props, - will_payload = Payload}) -> +to_message(#{client_id := ClientId, username := Username}, + #mqtt_packet_connect{will_retain = Retain, + will_qos = QoS, + will_topic = Topic, + will_props = Props, + will_payload = Payload}) -> Msg = emqx_message:make(ClientId, QoS, Topic, Payload), - Msg#message{flags = #{qos => QoS, retain => Retain}, headers = Props}. + Msg#message{flags = #{dup => false, retain => Retain}, + headers = merge_props(#{username => Username}, Props)}. + +merge_props(Headers, undefined) -> + Headers; +merge_props(Headers, Props) -> + maps:merge(Headers, Props). %% @doc Format packet -spec(format(mqtt_packet()) -> iolist()). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 2945954b1..9d59fff77 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -18,11 +18,14 @@ -include("emqx_mqtt.hrl"). -export([init/2, info/1, caps/1, stats/1]). +-export([client_id/1]). -export([credentials/1]). --export([client/1, client_id/1]). --export([session/1]). -export([parser/1]). --export([received/2, process/2, deliver/2, send/2]). +-export([session/1]). +-export([received/2]). +-export([process_packet/2]). +-export([deliver/2]). +-export([send/2]). -export([shutdown/2]). -record(pstate, { @@ -34,12 +37,13 @@ proto_name, ackprops, client_id, - client_pid, + conn_pid, conn_props, ack_props, username, session, clean_start, + topic_aliases, packet_size, will_msg, keepalive, @@ -54,9 +58,12 @@ }). -type(state() :: #pstate{}). - -export_type([state/0]). +-ifdef(TEST). +-compile(export_all). +-endif. + -define(LOG(Level, Format, Args, PState), emqx_logger:Level([{client, PState#pstate.client_id}], "Client(~s@~s): " ++ Format, [PState#pstate.client_id, esockd_net:format(PState#pstate.peername) | Args])). @@ -75,10 +82,11 @@ init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) proto_ver = ?MQTT_PROTO_V4, proto_name = <<"MQTT">>, client_id = <<>>, - client_pid = self(), + conn_pid = self(), username = init_username(Peercert, Options), is_super = false, clean_start = false, + topic_aliases = #{}, packet_size = emqx_zone:get_env(Zone, max_packet_size), mountpoint = emqx_zone:get_env(Zone, mountpoint), is_bridge = false, @@ -135,6 +143,9 @@ info(#pstate{zone = Zone, caps(#pstate{zone = Zone}) -> emqx_mqtt_caps:get_caps(Zone). +client_id(#pstate{client_id = ClientId}) -> + ClientId. + credentials(#pstate{zone = Zone, client_id = ClientId, username = Username, @@ -144,20 +155,6 @@ credentials(#pstate{zone = Zone, username => Username, peername => Peername}. -client(#pstate{zone = Zone, - client_id = ClientId, - client_pid = ClientPid, - peername = Peername, - username = Username}) -> - #client{id = ClientId, - pid = ClientPid, - zone = Zone, - peername = Peername, - username = Username}. - -client_id(#pstate{client_id = ClientId}) -> - ClientId. - stats(#pstate{recv_stats = #{pkt := RecvPkt, msg := RecvMsg}, send_stats = #{pkt := SendPkt, msg := SendMsg}}) -> [{recv_pkt, RecvPkt}, @@ -177,42 +174,73 @@ parser(#pstate{packet_size = Size, proto_ver = Ver}) -> -spec(received(mqtt_packet(), state()) -> {ok, state()} | {error, term()} | {error, term(), state()}). -received(?PACKET(Type), PState = #pstate{connected = false}) - when Type =/= ?CONNECT -> +received(?PACKET(Type), PState = #pstate{connected = false}) when Type =/= ?CONNECT -> {error, proto_not_connected, PState}; received(?PACKET(?CONNECT), PState = #pstate{connected = true}) -> - {error, proto_bad_connect, PState}; + {error, proto_unexpected_connect, PState}; received(Packet = ?PACKET(Type), PState) -> trace(recv, Packet, PState), case catch emqx_packet:validate(Packet) of true -> - process(Packet, inc_stats(recv, Type, PState)); - {'EXIT', {ReasonCode, _Stacktrace}} when is_integer(ReasonCode) -> - deliver({disconnect, ReasonCode}, PState), - {error, protocol_error, PState}; + {Packet1, PState1} = preprocess_properties(Packet, PState), + process_packet(Packet1, inc_stats(recv, Type, PState1)); {'EXIT', {Reason, _Stacktrace}} -> deliver({disconnect, ?RC_MALFORMED_PACKET}, PState), {error, Reason, PState} end. %%------------------------------------------------------------------------------ -%% Process Packet +%% Preprocess MQTT Properties %%------------------------------------------------------------------------------ -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} = Connect), PState) -> +%% 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], + {Packet#mqtt_packet{variable = Subscribe#mqtt_packet_subscribe{topic_filters = TopicFilters1}}, PState}; - io:format("~p~n", [Connect]), +%% Topic Alias Mapping +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}) -> + {Packet#mqtt_packet{variable = Publish#mqtt_packet_publish{ + topic_name = maps:get(AliasId, Aliases, <<>>)}}, PState}; + +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}) -> + {Packet, PState#pstate{topic_aliases = maps:put(AliasId, Topic, Aliases)}}; + +preprocess_properties(Packet, PState) -> + {Packet, PState}. + +%%------------------------------------------------------------------------------ +%% Process MQTT Packet +%%------------------------------------------------------------------------------ + +process_packet(?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} = Connect), PState) -> PState1 = set_username(Username, PState#pstate{client_id = ClientId, @@ -240,10 +268,8 @@ process(?CONNECT_PACKET( ok = emqx_cm:register_connection(client_id(PState4), info(PState4)), %% Start keepalive start_keepalive(Keepalive, PState4), - %% TODO: 'Run hooks' before open_session? - emqx_hooks:run('client.connected', [?RC_SUCCESS], client(PState4)), %% Success - {?RC_SUCCESS, SP, replvar(PState4)}; + {?RC_SUCCESS, SP, PState4}; {error, Error} -> ?LOG(error, "Failed to open session: ~p", [Error], PState1), {?RC_UNSPECIFIED_ERROR, PState1} @@ -256,7 +282,7 @@ process(?CONNECT_PACKET( {ReasonCode, PState1} end); -process(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PState) -> +process_packet(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PState) -> case check_publish(Packet, PState) of {ok, PState1} -> do_publish(Packet, PState1); @@ -265,7 +291,7 @@ process(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PState) -> {ok, PState} end; -process(Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), PState) -> +process_packet(Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), PState) -> case check_publish(Packet, PState) of {ok, PState1} -> do_publish(Packet, PState1); @@ -273,7 +299,7 @@ process(Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), PState) -> deliver({puback, PacketId, ReasonCode}, PState) end; -process(Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), PState) -> +process_packet(Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), PState) -> case check_publish(Packet, PState) of {ok, PState1} -> do_publish(Packet, PState1); @@ -281,30 +307,37 @@ process(Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), PState) -> deliver({pubrec, PacketId, ReasonCode}, PState) end; -process(?PUBACK_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> - ok = emqx_session:puback(SPid, PacketId, ReasonCode), - {ok, PState}; +process_packet(?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}) -> - ok = emqx_session:pubrec(SPid, PacketId, ReasonCode), - send(?PUBREL_PACKET(PacketId), PState); +process_packet(?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}) -> - ok = emqx_session:pubrel(SPid, PacketId, ReasonCode), - send(?PUBCOMP_PACKET(PacketId), PState); +process_packet(?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), - {ok, PState}; +process_packet(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> + {ok = emqx_session:pubcomp(SPid, PacketId, ReasonCode), PState}; -process(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - PState = #pstate{client_id = ClientId, session = SPid}) -> +process_packet(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), + PState = #pstate{session = SPid, mountpoint = Mountpoint}) -> case check_subscribe( parse_topic_filters(?SUBSCRIBE, RawTopicFilters), PState) of {ok, TopicFilters} -> - case emqx_hooks:run('client.subscribe', [ClientId], TopicFilters) of + case emqx_hooks:run('client.subscribe', [credentials(PState)], TopicFilters) of {ok, TopicFilters1} -> - ok = emqx_session:subscribe(SPid, PacketId, Properties, mount(TopicFilters1, PState)), + ok = emqx_session:subscribe(SPid, PacketId, Properties, + emqx_mountpoint:mount(Mountpoint, TopicFilters1)), {ok, PState}; {stop, _} -> ReasonCodes = lists:duplicate(length(TopicFilters), @@ -320,12 +353,13 @@ process(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), deliver({suback, PacketId, ReasonCodes}, PState) end; -process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - PState = #pstate{client_id = ClientId, session = SPid}) -> - case emqx_hooks:run('client.unsubscribe', [ClientId], +process_packet(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), + PState = #pstate{session = SPid, mountpoint = MountPoint}) -> + case emqx_hooks:run('client.unsubscribe', [credentials(PState)], parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters)) of {ok, TopicFilters} -> - ok = emqx_session:unsubscribe(SPid, PacketId, Properties, mount(TopicFilters, PState)), + ok = emqx_session:unsubscribe(SPid, PacketId, Properties, + emqx_mountpoint:mount(MountPoint, TopicFilters)), {ok, PState}; {stop, _Acc} -> ReasonCodes = lists:duplicate(length(RawTopicFilters), @@ -333,19 +367,20 @@ process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), deliver({unsuback, PacketId, ReasonCodes}, PState) end; -process(?PACKET(?PINGREQ), PState) -> +process_packet(?PACKET(?PINGREQ), PState) -> send(?PACKET(?PINGRESP), PState); -process(?PACKET(?DISCONNECT), PState) -> +process_packet(?PACKET(?DISCONNECT), PState) -> %% Clean willmsg {stop, normal, PState#pstate{will_msg = undefined}}. %%------------------------------------------------------------------------------ -%% ConnAck -> Client +%% ConnAck --> Client %%------------------------------------------------------------------------------ connack({?RC_SUCCESS, SP, PState}) -> - deliver({connack, ?RC_SUCCESS, sp(SP)}, PState); + emqx_hooks:run('client.connected', [credentials(PState), ?RC_SUCCESS]), + deliver({connack, ?RC_SUCCESS, sp(SP)}, update_mountpoint(PState)); connack({ReasonCode, PState = #pstate{proto_ver = ProtoVer}}) -> _ = deliver({connack, if ProtoVer =:= ?MQTT_PROTO_V5 -> @@ -360,20 +395,28 @@ connack({ReasonCode, PState = #pstate{proto_ver = ProtoVer}}) -> %%------------------------------------------------------------------------------ do_publish(Packet = ?PUBLISH_PACKET(QoS, PacketId), - PState = #pstate{client_id = ClientId, session = SPid}) -> - Msg = mount(emqx_packet:to_message(ClientId, Packet), PState), - _ = emqx_session:publish(SPid, PacketId, Msg), - puback(QoS, PacketId, PState). + PState = #pstate{session = SPid, mountpoint = MountPoint}) -> + Msg = emqx_mountpoint:mount(MountPoint, + emqx_packet:to_message(credentials(PState), Packet)), + puback(QoS, PacketId, emqx_session:publish(SPid, PacketId, Msg), PState). %%------------------------------------------------------------------------------ %% Puback -> Client %%------------------------------------------------------------------------------ -puback(?QOS_0, _PacketId, PState) -> +puback(?QOS_0, _PacketId, _Result, PState) -> {ok, PState}; -puback(?QOS_1, PacketId, PState) -> +puback(?QOS_1, PacketId, {error, ReasonCode}, PState) -> + deliver({puback, PacketId, ReasonCode}, PState); +puback(?QOS_1, PacketId, {ok, []}, PState) -> + deliver({puback, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); +puback(?QOS_1, PacketId, {ok, _}, PState) -> deliver({puback, PacketId, ?RC_SUCCESS}, PState); -puback(?QOS_2, PacketId, PState) -> +puback(?QOS_2, PacketId, {error, ReasonCode}, PState) -> + deliver({pubrec, PacketId, ReasonCode}, PState); +puback(?QOS_2, PacketId, {ok, []}, PState) -> + deliver({pubrec, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); +puback(?QOS_2, PacketId, {ok, _}, PState) -> deliver({pubrec, PacketId, ?RC_SUCCESS}, PState). %%------------------------------------------------------------------------------ @@ -386,10 +429,9 @@ deliver({connack, ReasonCode}, PState) -> deliver({connack, ReasonCode, SP}, PState) -> send(?CONNACK_PACKET(ReasonCode, SP), PState); -deliver({publish, PacketId, Msg}, PState = #pstate{client_id = ClientId, - is_bridge = IsBridge}) -> - _ = emqx_hooks:run('message.delivered', [ClientId], Msg), - Msg1 = unmount(clean_retain(IsBridge, Msg), PState), +deliver({publish, PacketId, Msg}, PState = #pstate{is_bridge = IsBridge, mountpoint = MountPoint}) -> + _ = emqx_hooks:run('message.delivered', credentials(PState), Msg), + Msg1 = emqx_mountpoint:unmount(MountPoint, clean_retain(IsBridge, Msg)), send(emqx_packet:from_message(PacketId, Msg1), PState); deliver({puback, PacketId, ReasonCode}, PState) -> @@ -445,13 +487,13 @@ maybe_assign_client_id(PState) -> try_open_session(#pstate{zone = Zone, client_id = ClientId, - client_pid = ClientPid, + conn_pid = ConnPid, conn_props = ConnProps, username = Username, clean_start = CleanStart}) -> case emqx_sm:open_session(#{zone => Zone, client_id => ClientId, - client_pid => ClientPid, + conn_pid => ConnPid, username => Username, clean_start => CleanStart, conn_props => ConnProps}) of @@ -592,14 +634,14 @@ shutdown(Error, PState = #pstate{client_id = ClientId, will_msg = WillMsg}) -> true -> ok; false -> send_willmsg(WillMsg) end, - emqx_hooks:run('client.disconnected', [Error], client(PState)), + emqx_hooks:run('client.disconnected', [credentials(PState), Error]), emqx_cm:unregister_connection(ClientId). -willmsg(Packet, PState = #pstate{client_id = ClientId}) +willmsg(Packet, #pstate{client_id = ClientId, mountpoint = MountPoint}) when is_record(Packet, mqtt_packet_connect) -> case emqx_packet:to_message(ClientId, Packet) of undefined -> undefined; - Msg -> mount(Msg, PState) + Msg -> emqx_mountpoint:mount(MountPoint, Msg) end. send_willmsg(undefined) -> @@ -617,14 +659,11 @@ start_keepalive(Secs, #pstate{zone = Zone}) when Secs > 0 -> %% Parse topic filters %%----------------------------------------------------------------------------- -parse_topic_filters(?SUBSCRIBE, TopicFilters) -> - [begin - {Topic, TOpts} = emqx_topic:parse(RawTopic), - {Topic, maps:merge(SubOpts, TOpts)} - end || {RawTopic, SubOpts} <- TopicFilters]; +parse_topic_filters(?SUBSCRIBE, RawTopicFilters) -> + [emqx_topic:parse(RawTopic, SubOpts) || {RawTopic, SubOpts} <- RawTopicFilters]; -parse_topic_filters(?UNSUBSCRIBE, TopicFilters) -> - lists:map(fun emqx_topic:parse/1, TopicFilters). +parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters) -> + lists:map(fun emqx_topic:parse/1, RawTopicFilters). %%----------------------------------------------------------------------------- %% The retained flag should be propagated for bridge. @@ -638,37 +677,14 @@ clean_retain(false, Msg = #message{flags = #{retain := true}, headers = Headers} clean_retain(_IsBridge, Msg) -> Msg. -%%----------------------------------------------------------------------------- -%% Mount Point -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ +%% Update mountpoint -mount(Any, #pstate{mountpoint = undefined}) -> - Any; -mount(Msg = #message{topic = Topic}, #pstate{mountpoint = MountPoint}) -> - Msg#message{topic = <>}; -mount(TopicFilters, #pstate{mountpoint = MountPoint}) when is_list(TopicFilters) -> - [{<>, SubOpts} || {Topic, SubOpts} <- TopicFilters]. - -unmount(Any, #pstate{mountpoint = undefined}) -> - Any; -unmount(Msg = #message{topic = Topic}, #pstate{mountpoint = MountPoint}) -> - case catch split_binary(Topic, byte_size(MountPoint)) of - {MountPoint, Topic1} -> Msg#message{topic = Topic1}; - _Other -> Msg - end. - -replvar(PState = #pstate{mountpoint = undefined}) -> +update_mountpoint(PState = #pstate{mountpoint = undefined}) -> PState; -replvar(PState = #pstate{client_id = ClientId, username = Username, mountpoint = MountPoint}) -> - Vars = [{<<"%c">>, ClientId}, {<<"%u">>, Username}], - PState#pstate{mountpoint = lists:foldl(fun feed_var/2, MountPoint, Vars)}. - -feed_var({<<"%c">>, ClientId}, MountPoint) -> - emqx_topic:feed_var(<<"%c">>, ClientId, MountPoint); -feed_var({<<"%u">>, undefined}, MountPoint) -> - MountPoint; -feed_var({<<"%u">>, Username}, MountPoint) -> - emqx_topic:feed_var(<<"%u">>, Username, MountPoint). +update_mountpoint(PState = #pstate{mountpoint = MountPoint}) -> + PState#pstate{mountpoint = emqx_mountpoint:replvar(MountPoint, credentials(PState))}. sp(true) -> 1; sp(false) -> 0. + diff --git a/src/emqx_session.erl b/src/emqx_session.erl index a96fac8f3..8c7795682 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -133,9 +133,6 @@ %% Stats timer stats_timer :: reference() | undefined, - %% Ignore loop deliver? - ignore_loop_deliver = false :: boolean(), - %% TODO: deliver_stats = 0, @@ -169,20 +166,17 @@ start_link(SessAttrs) -> -spec(subscribe(pid(), list({topic(), map()}) | {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). -subscribe(SPid, TopicFilters) when is_list(TopicFilters) -> - gen_server:cast(SPid, {subscribe, [begin - {Topic, Opts} = emqx_topic:parse(RawTopic), - {Topic, maps:merge( - maps:merge( - ?DEFAULT_SUBOPTS, SubOpts), Opts)} - end || {RawTopic, SubOpts} <- TopicFilters]}). +subscribe(SPid, RawTopicFilters) when is_list(RawTopicFilters) -> + TopicFilters = [emqx_topic:parse(RawTopic, maps:merge(?DEFAULT_SUBOPTS, SubOpts)) + || {RawTopic, SubOpts} <- RawTopicFilters], + subscribe(SPid, undefined, #{}, TopicFilters). %% for mqtt 5.0 subscribe(SPid, PacketId, Properties, TopicFilters) -> SubReq = {PacketId, Properties, TopicFilters}, gen_server:cast(SPid, {subscribe, self(), SubReq}). --spec(publish(pid(), mqtt_packet_id(), message()) -> {ok, delivery()} | {error, term()}). +-spec(publish(pid(), mqtt_packet_id(), message()) -> {ok, emqx_types:dispatches()}). publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_0}) -> %% Publish QoS0 message to broker directly emqx_broker:publish(Msg); @@ -202,27 +196,29 @@ puback(SPid, PacketId) -> puback(SPid, PacketId, ReasonCode) -> gen_server:cast(SPid, {puback, PacketId, ReasonCode}). --spec(pubrec(pid(), mqtt_packet_id()) -> ok). +-spec(pubrec(pid(), mqtt_packet_id()) -> ok | {error, mqtt_reason_code()}). pubrec(SPid, PacketId) -> - gen_server:cast(SPid, {pubrec, PacketId}). + pubrec(SPid, PacketId, ?RC_SUCCESS). +-spec(pubrec(pid(), mqtt_packet_id(), mqtt_reason_code()) + -> ok | {error, mqtt_reason_code()}). pubrec(SPid, PacketId, ReasonCode) -> - gen_server:cast(SPid, {pubrec, PacketId, ReasonCode}). + gen_server:call(SPid, {pubrec, PacketId, ReasonCode}, infinity). --spec(pubrel(pid(), mqtt_packet_id(), mqtt_reason_code()) -> ok). +-spec(pubrel(pid(), mqtt_packet_id(), mqtt_reason_code()) + -> ok | {error, mqtt_reason_code()}). pubrel(SPid, PacketId, ReasonCode) -> - gen_server:cast(SPid, {pubrel, PacketId, ReasonCode}). + gen_server:call(SPid, {pubrel, PacketId, ReasonCode}, infinity). -spec(pubcomp(pid(), mqtt_packet_id(), mqtt_reason_code()) -> ok). pubcomp(SPid, PacketId, ReasonCode) -> gen_server:cast(SPid, {pubcomp, PacketId, ReasonCode}). --spec(unsubscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). -unsubscribe(SPid, TopicFilters) when is_list(TopicFilters) -> - %%TODO: Parse the topic filters? - unsubscribe(SPid, {undefined, #{}, TopicFilters}). +-spec(unsubscribe(pid(), topic_table()) -> ok). +unsubscribe(SPid, RawTopicFilters) when is_list(RawTopicFilters) -> + unsubscribe(SPid, undefined, #{}, lists:map(fun emqx_topic:parse/1, RawTopicFilters)). -%% TODO:... +-spec(unsubscribe(pid(), mqtt_packet_id(), mqtt_properties(), topic_table()) -> ok). unsubscribe(SPid, PacketId, Properties, TopicFilters) -> UnsubReq = {PacketId, Properties, TopicFilters}, gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). @@ -335,7 +331,6 @@ init(#{zone := Zone, max_awaiting_rel = get_env(Zone, max_awaiting_rel), expiry_interval = get_env(Zone, session_expiry_interval), enable_stats = get_env(Zone, enable_stats, true), - ignore_loop_deliver = get_env(Zone, ignore_loop_deliver, false), deliver_stats = 0, enqueue_stats = 0, created_at = os:timestamp()}, @@ -359,21 +354,45 @@ handle_call({discard, ClientPid}, _From, State = #state{client_pid = OldClientPi ?LOG(warning, " ~p kickout ~p", [ClientPid, OldClientPid], State), {stop, {shutdown, conflict}, ok, State}; +%% PUBLISH: handle_call({publish, PacketId, Msg = #message{qos = ?QOS_2}}, _From, - State = #state{awaiting_rel = AwaitingRel, - await_rel_timer = Timer, - await_rel_timeout = Timeout}) -> + State = #state{awaiting_rel = AwaitingRel}) -> case is_awaiting_full(State) of false -> - State1 = case Timer == undefined of - true -> State#state{await_rel_timer = emqx_misc:start_timer(Timeout, check_awaiting_rel)}; - false -> State - end, - reply(ok, State1#state{awaiting_rel = maps:put(PacketId, Msg, AwaitingRel)}); + case maps:is_key(PacketId, AwaitingRel) of + true -> + reply({error, ?RC_PACKET_IDENTIFIER_IN_USE}, State); + false -> + State1 = State#state{awaiting_rel = maps:put(PacketId, Msg, AwaitingRel)}, + reply(emqx_broker:publish(Msg), ensure_await_rel_timer(State1)) + end; true -> ?LOG(warning, "Dropped QoS2 Message for too many awaiting_rel: ~p", [Msg], State), emqx_metrics:inc('messages/qos2/dropped'), - reply({error, dropped}, State) + reply({error, ?RC_RECEIVE_MAXIMUM_EXCEEDED}, State) + end; + +%% PUBREC: +handle_call({pubrec, PacketId, _ReasonCode}, _From, State = #state{inflight = Inflight}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + reply(ok, acked(pubrec, PacketId, State)); + false -> + ?LOG(warning, "The PUBREC PacketId is not found: ~w", [PacketId], State), + emqx_metrics:inc('packets/pubrec/missed'), + reply({error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State) + end; + +%% PUBREL: +handle_call({pubrel, PacketId, _ReasonCode}, _From, + State = #state{awaiting_rel = AwaitingRel}) -> + case maps:take(PacketId, AwaitingRel) of + {_, AwaitingRel1} -> + reply(ok, State#state{awaiting_rel = AwaitingRel1}); + error -> + ?LOG(warning, "Cannot find PUBREL: ~p", [PacketId], State), + emqx_metrics:inc('packets/pubrel/missed'), + reply({error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State) end; handle_call(info, _From, State) -> @@ -390,57 +409,38 @@ handle_call(Req, _From, State) -> {reply, ignored, State}. %% SUBSCRIBE: -handle_cast({subscribe, TopicFilters}, State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> - Subscriptions1 = lists:foldl( - fun({Topic, SubOpts}, SubMap) -> - case maps:find(Topic, SubMap) of - {ok, _OldOpts} -> - emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), - emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), - ?LOG(warning, "Duplicated subscribe: ~s, subopts: ~p", [Topic, SubOpts], State); - error -> - emqx_broker:subscribe(Topic, ClientId, SubOpts), - emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]) - end, - maps:put(Topic, SubOpts, SubMap) - end, Subscriptions, TopicFilters), - {noreply, State#state{subscriptions = Subscriptions1}}; - -handle_cast({subscribe, From, {PacketId, Properties, TopicFilters}}, +handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> {ReasonCodes, Subscriptions1} = lists:foldr(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) -> - {[QoS|RcAcc], - case maps:find(Topic, SubMap) of - {ok, SubOpts} -> - ?LOG(warning, "Duplicated subscribe: ~s, subopts: ~p", [Topic, SubOpts], State), - SubMap; - {ok, OldOpts} -> - emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), - emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), - ?LOG(warning, "Duplicated subscribe ~s, old_opts: ~p, new_opts: ~p", [Topic, OldOpts, SubOpts], State), - maps:put(Topic, with_subid(Properties, SubOpts), SubMap); - error -> - emqx_broker:subscribe(Topic, ClientId, SubOpts), - emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), - maps:put(Topic, with_subid(Properties, SubOpts), SubMap) - end} + {[QoS|RcAcc], case maps:find(Topic, SubMap) of + {ok, SubOpts} -> + SubMap; + {ok, _SubOpts} -> + emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), + emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), + maps:put(Topic, SubOpts, SubMap); + error -> + emqx_broker:subscribe(Topic, ClientId, SubOpts), + emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), + maps:put(Topic, SubOpts, SubMap) + end} end, {[], Subscriptions}, TopicFilters), - suback(From, PacketId, ReasonCodes), + suback(FromPid, PacketId, ReasonCodes), {noreply, State#state{subscriptions = Subscriptions1}}; %% UNSUBSCRIBE: handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> {ReasonCodes, Subscriptions1} = - lists:foldr(fun({Topic, _Opts}, {RcAcc, SubMap}) -> + lists:foldr(fun({Topic, _SubOpts}, {Acc, SubMap}) -> case maps:find(Topic, SubMap) of {ok, SubOpts} -> - emqx_broker:unsubscribe(Topic, ClientId), + ok = emqx_broker:unsubscribe(Topic, ClientId), emqx_hooks:run('session.unsubscribed', [ClientId, Topic, SubOpts]), - {[?RC_SUCCESS|RcAcc], maps:remove(Topic, SubMap)}; + {[?RC_SUCCESS|Acc], maps:remove(Topic, SubMap)}; error -> - {[?RC_NO_SUBSCRIPTION_EXISTED|RcAcc], SubMap} + {[?RC_NO_SUBSCRIPTION_EXISTED|Acc], SubMap} end end, {[], Subscriptions}, TopicFilters), unsuback(From, PacketId, ReasonCodes), @@ -452,44 +452,18 @@ handle_cast({puback, PacketId, _ReasonCode}, State = #state{inflight = Inflight} true -> {noreply, dequeue(acked(puback, PacketId, State))}; false -> - ?LOG(warning, "The PUBACK PacketId is not found: ~p", [PacketId], State), + ?LOG(warning, "The PUBACK PacketId is not found: ~w", [PacketId], State), emqx_metrics:inc('packets/puback/missed'), {noreply, State} end; -%% PUBREC: How to handle ReasonCode? -handle_cast({pubrec, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> - case emqx_inflight:contain(PacketId, Inflight) of - true -> - {noreply, acked(pubrec, PacketId, State)}; - false -> - ?LOG(warning, "The PUBREC PacketId is not found: ~w", [PacketId], State), - emqx_metrics:inc('packets/pubrec/missed'), - {noreply, State} - end; - -%% PUBREL: -handle_cast({pubrel, PacketId, _ReasonCode}, State = #state{awaiting_rel = AwaitingRel}) -> - {noreply, - case maps:take(PacketId, AwaitingRel) of - {Msg, AwaitingRel1} -> - %% Implement Qos2 by method A [MQTT 4.33] - %% Dispatch to subscriber when received PUBREL - emqx_broker:publish(Msg), %% FIXME: - maybe_gc(State#state{awaiting_rel = AwaitingRel1}); - error -> - ?LOG(warning, "Cannot find PUBREL: ~p", [PacketId], State), - emqx_metrics:inc('packets/pubrel/missed'), - State - end, hibernate}; - %% PUBCOMP: handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> case emqx_inflight:contain(PacketId, Inflight) of true -> {noreply, dequeue(acked(pubcomp, PacketId, State))}; false -> - ?LOG(warning, "The PUBCOMP Packet Identifier is not found: ~w", [PacketId], State), + ?LOG(warning, "The PUBCOMP PacketId is not found: ~w", [PacketId], State), emqx_metrics:inc('packets/pubcomp/missed'), {noreply, State} end; @@ -542,19 +516,22 @@ handle_cast(Msg, State) -> emqx_logger:error("[Session] unexpected cast: ~p", [Msg]), {noreply, State}. +%% Batch dispatch handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> {noreply, lists:foldl(fun(Msg, NewState) -> element(2, handle_info({dispatch, Topic, Msg}, NewState)) end, State, Msgs)}; -%% Ignore messages delivered by self -handle_info({dispatch, _Topic, #message{from = ClientId}}, - State = #state{client_id = ClientId, ignore_loop_deliver = true}) -> - {noreply, State}; - -%% Dispatch Message -handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, message) -> - {noreply, maybe_gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))}; +%% Dispatch message +handle_info({dispatch, Topic, Msg}, State = #state{subscriptions = SubMap}) when is_record(Msg, message) -> + {noreply, case maps:find(Topic, SubMap) of + {ok, #{nl := Nl, qos := QoS, subid := SubId}} -> + run_dispatch_steps([{nl, Nl},{qos, QoS}, {subid, SubId}], Msg, State); + {ok, #{nl := Nl, qos := QoS}} -> + run_dispatch_steps([{nl, Nl},{qos, QoS}], Msg, State); + error -> + dispatch(reset_dup(Msg), State) + end}; %% Do nothing if the client has been disconnected. handle_info({timeout, _Timer, retry_delivery}, State = #state{client_pid = undefined}) -> @@ -611,11 +588,6 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -with_subid(#{'Subscription-Identifier' := SubId}, SubOpts) -> - maps:put(subid, SubId, SubOpts); -with_subid(_Props, SubOpts) -> - SubOpts. - suback(_From, undefined, _ReasonCodes) -> ignore; suback(From, PacketId, ReasonCodes) -> @@ -727,6 +699,19 @@ is_awaiting_full(#state{awaiting_rel = AwaitingRel, max_awaiting_rel = MaxLen}) %% Dispatch Messages %%------------------------------------------------------------------------------ +run_dispatch_steps([], Msg, State) -> + dispatch(Msg, State); +run_dispatch_steps([{nl, 1}|_Steps], #message{from = ClientId}, State = #state{client_id = ClientId}) -> + State; +run_dispatch_steps([{nl, 0}|Steps], Msg, State) -> + run_dispatch_steps(Steps, Msg, State); +run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = #state{upgrade_qos = false}) -> + run_dispatch_steps(Steps, Msg#message{qos = min(SubQoS, PubQoS)}, State); +run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = #state{upgrade_qos = true}) -> + run_dispatch_steps(Steps, Msg#message{qos = max(SubQoS, PubQoS)}, State); +run_dispatch_steps([{subid, SubId}|Steps], Msg, State) -> + run_dispatch_steps(Steps, emqx_message:set_header('Subscription-Identifier', SubId, Msg), State). + %% Enqueue message if the client has been disconnected dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) -> case emqx_hooks:run('message.dropped', [ClientId, Msg]) of @@ -837,19 +822,14 @@ dequeue2(State = #state{mqueue = Q}) -> dequeue(dispatch(Msg, State#state{mqueue = Q1})) end. -%%------------------------------------------------------------------------------ -%% Tune QoS -tune_qos(Topic, Msg = #message{qos = PubQoS}, - #state{subscriptions = SubMap, upgrade_qos = UpgradeQoS}) -> - case maps:find(Topic, SubMap) of - {ok, #{qos := SubQoS}} when UpgradeQoS andalso (SubQoS > PubQoS) -> - Msg#message{qos = SubQoS}; - {ok, #{qos := SubQoS}} when (not UpgradeQoS) andalso (SubQoS < PubQoS) -> - Msg#message{qos = SubQoS}; - {ok, _} -> Msg; - error -> Msg - end. +%%------------------------------------------------------------------------------ +%% Ensure timers + +ensure_await_rel_timer(State = #state{await_rel_timer = undefined, await_rel_timeout = Timeout}) -> + State#state{await_rel_timer = emqx_misc:start_timer(Timeout, check_awaiting_rel)}; +ensure_await_rel_timer(State) -> + State. %%------------------------------------------------------------------------------ %% Reset Dup @@ -889,5 +869,5 @@ reply(Reply, State) -> shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. -maybe_gc(State) -> State. +%%TODO: maybe_gc(State) -> State. diff --git a/src/emqx_types.erl b/src/emqx_types.erl index 32654b121..6fb0647c8 100644 --- a/src/emqx_types.erl +++ b/src/emqx_types.erl @@ -19,7 +19,7 @@ -export_type([startlink_ret/0]). -export_type([zone/0, client_id/0, username/0, password/0, peername/0, protocol/0, credentials/0]). --export_type([payload/0]). +-export_type([topic/0, payload/0, dispatches/0]). %%-export_type([payload/0, message/0, delivery/0]). -type(startlink_ret() :: {ok, pid()} | ignore | {error, term()}). @@ -36,7 +36,10 @@ zone => zone(), atom() => term()}). +-type(topic() :: binary()). -type(payload() :: binary() | iodata()). %-type(message() :: #message{}). %-type(delivery() :: #delivery{}). +-type(dispatches() :: [{route, node(), topic()} | {dispatch, topic(), pos_integer()}]). + From 013a5a9c7cfeebe6028ddbf73dab2d3fec8b7d51 Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 29 Aug 2018 09:30:18 +0800 Subject: [PATCH 151/520] Fix emqx_protocol:clientid undefined --- src/emqx_ws_connection.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 2a6c49745..67ce078d1 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -199,7 +199,7 @@ websocket_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> websocket_info(emit_stats, State = #state{proto_state = ProtoState}) -> Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(), emqx_protocol:stats(ProtoState)]), - emqx_cm:set_conn_stats(emqx_protocol:clientid(ProtoState), Stats), + emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), Stats), {ok, State#state{stats_timer = undefined}, hibernate}; websocket_info({keepalive, start, Interval}, State) -> From 00cb26c4e0275fad0ccf6bdb315966888af54532 Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 29 Aug 2018 10:00:41 +0800 Subject: [PATCH 152/520] Support ws mqtt any version --- src/emqx_ws_connection.erl | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 67ce078d1..b94b241cd 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -87,14 +87,11 @@ init(Req, Opts) -> case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of undefined -> {cowboy_websocket, Req, #state{}}; - Subprotocols -> - case lists:member(<<"mqtt">>, Subprotocols) of - true -> - Resp = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt">>, Req), - {cowboy_websocket, Resp, #state{request = Req, options = Opts}, #{idle_timeout => 86400000}}; - false -> - {ok, cowboy_req:reply(400, Req), #state{}} - end + [<<"mqtt", Vsn/binary>>] -> + Resp = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt", Vsn/binary>>, Req), + {cowboy_websocket, Resp, #state{request = Req, options = Opts}, #{idle_timeout => 86400000}}; + R -> + {ok, cowboy_req:reply(400, Req), #state{}} end. websocket_init(#state{request = Req, options = Options}) -> From af21cdfd1bbdf429712d613531c1974d36c84127 Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 29 Aug 2018 10:08:15 +0800 Subject: [PATCH 153/520] Rm tmp var --- src/emqx_ws_connection.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index b94b241cd..f3dc3c5e0 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -90,7 +90,7 @@ init(Req, Opts) -> [<<"mqtt", Vsn/binary>>] -> Resp = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt", Vsn/binary>>, Req), {cowboy_websocket, Resp, #state{request = Req, options = Opts}, #{idle_timeout => 86400000}}; - R -> + _ -> {ok, cowboy_req:reply(400, Req), #state{}} end. From 9711892f7318565697110e60f511e1dd63e525db Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 29 Aug 2018 10:58:34 +0800 Subject: [PATCH 154/520] Fix share_sub disconnect not clear route bug --- src/emqx_broker.erl | 31 +++++++++++++++++++------------ src/emqx_router.erl | 7 +++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 560c095cf..17d88878d 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -399,20 +399,21 @@ do_unsubscribe(Group, Topic, Subscriber) -> ets:delete(?SUBOPTION, {Topic, Subscriber}). subscriber_down(Subscriber) -> - Topics = lists:map(fun({_, {share, _, Topic}}) -> - Topic; + Topics = lists:map(fun({_, {share, Group, Topic}}) -> + {Topic, Group}; ({_, Topic}) -> - Topic + {Topic, undefined} end, ets:lookup(?SUBSCRIPTION, Subscriber)), - lists:foreach(fun(Topic) -> - case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of - [{_, SubOpts}] -> - Group = maps:get(share, SubOpts, undefined), - true = do_unsubscribe(Group, Topic, Subscriber), - ets:member(?SUBSCRIBER, Topic) - orelse emqx_router:del_route(Topic, dest(Group)); - [] -> ok - end + lists:foreach(fun({Topic, undefined}) -> + true = do_unsubscribe(undefined, Topic, Subscriber), + ets:member(?SUBSCRIBER, Topic) orelse emqx_router:del_route(Topic, dest(undefined)); + ({Topic, Group}) -> + true = do_unsubscribe(Group, Topic, Subscriber), + Groups = groups(Topic), + case lists:member(Group, lists:usort(Groups)) of + true -> ok; + false -> emqx_router:del_route(Topic, dest(Group)) + end end, Topics). monitor_subscriber({SubPid, SubId}, State = #state{submap = SubMap, submon = SubMon}) -> @@ -430,3 +431,9 @@ dest(Group) -> {Group, node()}. shared(undefined, Name) -> Name; shared(Group, Name) -> {share, Group, Name}. +groups(Topic) -> + lists:foldl(fun({_, {share, Group, _}}, Acc) -> + [Group | Acc]; + ({_, _}, Acc) -> + Acc + end, [], ets:lookup(?SUBSCRIBER, Topic)). diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 863214617..df2d2e018 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -167,6 +167,13 @@ handle_cast({del_route, From, Route}, State) -> _ = gen_server:reply(From, ok), {noreply, NewState}; +handle_cast({del_route, Route = #route{topic = Topic, dest = Dest}}, State) when is_tuple(Dest) -> + {noreply, case emqx_topic:wildcard(Topic) of + true -> log(trans(fun del_trie_route/1, [Route])), + State; + false -> del_direct_route(Route, State) + end}; + handle_cast({del_route, Route = #route{topic = Topic}}, State) -> %% Confirm if there are still subscribers... {noreply, case ets:member(emqx_subscriber, Topic) of From dae81037bff0dba56a2094a7670bbc217bde2270 Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 29 Aug 2018 12:58:55 +0800 Subject: [PATCH 155/520] Update share_sub dispatch --- src/emqx_shared_sub.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 7a70fca59..ce21a1bf8 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -93,8 +93,7 @@ pick([]) -> pick([SubPid]) -> SubPid; pick(SubPids) -> - X = abs(erlang:monotonic_time() bxor erlang:unique_integer()), - lists:nth((X rem length(SubPids)) + 1, SubPids). + lists:nth(rand:uniform(length(SubPids)), SubPids). subscribers(Group, Topic) -> ets:select(?TAB, [{{emqx_shared_subscription, Group, Topic, '$1'}, [], ['$1']}]). From 84f241522f86bccccaf682f2a861ba65f7e9e432 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Wed, 29 Aug 2018 14:36:40 +0800 Subject: [PATCH 156/520] update frame suite and frame suite --- Makefile | 8 +++++--- test/emqx_frame_SUITE.erl | 10 ++++++---- test/emqx_router_SUITE.erl | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 85f22a60f..478724e66 100644 --- a/Makefile +++ b/Makefile @@ -32,11 +32,13 @@ TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose -CT_SUITES = emqx_stats +# CT_SUITES = emqx_stats ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat -#CT_SUITES = emqx emqx_broker emqx_mod emqx_lib emqx_topic emqx_mqueue emqx_inflight \ -# emqx_vm emqx_net emqx_protocol emqx_access emqx_router +CT_SUITES = emqx emqx_access emqx_base62 emqx_broker emqx_client emqx_cm emqx_frame emqx_guid emqx_inflight \ + emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ + emqx_mqtt_compat emqx_mqtt_properties emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ + emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_zone CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqxct@127.0.0.1 diff --git a/test/emqx_frame_SUITE.erl b/test/emqx_frame_SUITE.erl index b24644cd3..60ed52e46 100644 --- a/test/emqx_frame_SUITE.erl +++ b/test/emqx_frame_SUITE.erl @@ -329,15 +329,17 @@ serialize_parse_pubcomp_v5(_) -> 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>>, - TopicFilters = [{<<"TopicA">>, #{qos => 2}}], + TopicOpts = #{ nl => 0 , rap => 0, rc => 0, + rh => 0, subid => 0 , qos => 2 }, + TopicFilters = [{<<"TopicA">>, TopicOpts}], Packet = ?SUBSCRIBE_PACKET(2, TopicFilters), ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), ?assertEqual({ok, Packet, <<>>}, parse(Bin)). serialize_parse_subscribe_v5(_) -> - TopicFilters = [{<<"TopicQos0">>, #{rh => 1, qos => ?QOS_0}}, - {<<"TopicQos1">>, #{rh => 1, qos => ?QOS_1}}], - Packet = ?SUBSCRIBE_PACKET(1, #{'Subscription-Identifier' => 16#FFFFFFF}, + TopicFilters = [{<<"TopicQos0">>, #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0, subid => 0}}, + {<<"TopicQos1">>, #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0, subid => 0}}], + Packet = ?SUBSCRIBE_PACKET(3, #{'Subscription-Identifier' => 16#FFFFFFF}, TopicFilters), ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). diff --git a/test/emqx_router_SUITE.erl b/test/emqx_router_SUITE.erl index f44039a08..b9d40810a 100644 --- a/test/emqx_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -70,7 +70,7 @@ match_routes(_) -> ?R:add_route(From, <<"a/+/c">>, node()), ?R:add_route(From, <<"a/b/#">>, node()), ?R:add_route(From, <<"#">>, node()), - timer:sleep(6), + timer:sleep(1000), ?assertEqual([#route{topic = <<"#">>, dest = node()}, #route{topic = <<"a/+/c">>, dest = node()}, #route{topic = <<"a/b/#">>, dest = node()}, From 47232d0281e580403b56dd2b90b831922224367d Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Wed, 29 Aug 2018 14:52:35 +0800 Subject: [PATCH 157/520] update emqx_lib_suite and delete duplicate test case --- test/emqx_base62_SUITE.erl | 37 ------------------------------------- test/emqx_lib_SUITE.erl | 15 ++++++++------- 2 files changed, 8 insertions(+), 44 deletions(-) delete mode 100644 test/emqx_base62_SUITE.erl diff --git a/test/emqx_base62_SUITE.erl b/test/emqx_base62_SUITE.erl deleted file mode 100644 index e303fd8ee..000000000 --- a/test/emqx_base62_SUITE.erl +++ /dev/null @@ -1,37 +0,0 @@ -%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. - --module(emqx_base62_SUITE). - --include_lib("eunit/include/eunit.hrl"). - --define(BASE62, emqx_base62). - --compile(export_all). --compile(nowarn_export_all). - -all() -> [t_base62_encode]. - -t_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_lib_SUITE.erl b/test/emqx_lib_SUITE.erl index 72153f6b3..12bbd023e 100644 --- a/test/emqx_lib_SUITE.erl +++ b/test/emqx_lib_SUITE.erl @@ -161,12 +161,13 @@ node_parse_name(_) -> %%-------------------------------------------------------------------- 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)), + <<"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)), - Y = ?BASE62:decode(?BASE62:encode(Y)). - + 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). From c967db409f4328e5f75739a2561c2c437e540a9e Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Wed, 29 Aug 2018 15:50:13 +0800 Subject: [PATCH 158/520] EMQX 3.0 UT --- src/emqx_client.erl | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 5f0301048..ab653b302 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -373,12 +373,22 @@ init([Options]) -> {_ver, undefined} -> random_client_id(); {_ver, Id} -> iolist_to_binary(Id) end, + Username = case proplists:get_value(username, Options) of + undefined -> <<>>; + Name -> Name + end, + Password = case proplists:get_value(password, Options) of + undefined -> <<>>; + Passw -> Passw + end, State = init(Options, #state{host = {127,0,0,1}, port = 1883, hosts = [], sock_opts = [], bridge_mode = false, client_id = ClientId, + username = Username, + password = Password, clean_start = true, proto_ver = ?MQTT_PROTO_V4, proto_name = <<"MQTT">>, @@ -542,7 +552,8 @@ mqtt_connect(State = #state{client_id = ClientId, properties = Properties}) -> ?WILL_MSG(WillQoS, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, ConnProps = emqx_mqtt_properties:filter(?CONNECT, Properties), - io:format("ConnProps: ~p~n", [ConnProps]), + io:format("ConnProps: ~p, ClientID: ~p, Username: ~p, Password: ~p~n", + [ConnProps, ClientId, Username, Password]), send(?CONNECT_PACKET( #mqtt_packet_connect{proto_ver = ProtoVer, proto_name = ProtoName, @@ -1082,4 +1093,3 @@ next_packet_id(State = #state{last_packet_id = 16#ffff}) -> next_packet_id(State = #state{last_packet_id = Id}) -> State#state{last_packet_id = Id + 1}. - From 1a7d60a7e3ee9c1915f2d2ca7a023f4601443892 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 29 Aug 2018 17:27:56 +0800 Subject: [PATCH 159/520] Improve the hooks design for emqx 3.0 --- src/emqx_broker.erl | 4 +- src/emqx_message.erl | 16 ++++ src/emqx_mod_presence.erl | 38 +++++----- src/emqx_mod_rewrite.erl | 39 +++------- src/emqx_mod_subscription.erl | 24 +++--- src/emqx_packet.erl | 36 +++++---- src/emqx_protocol.erl | 26 +++---- src/emqx_session.erl | 136 +++++++++++++++------------------- src/emqx_sm.erl | 52 ++++++------- src/emqx_time.erl | 3 +- 10 files changed, 174 insertions(+), 200 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 7adf6064e..357eacb7b 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -174,7 +174,7 @@ delivery(Msg) -> %%------------------------------------------------------------------------------ route([], Delivery = #delivery{message = Msg}) -> - emqx_hooks:run('message.dropped', [node(), Msg]), + emqx_hooks:run('message.dropped', [#{node => node()}, Msg]), inc_dropped_cnt(Msg#message.topic), Delivery; route([{To, Node}], Delivery) when Node =:= node() -> @@ -215,7 +215,7 @@ forward(Node, To, Delivery) -> dispatch(Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> case subscribers(Topic) of [] -> - emqx_hooks:run('message.dropped', [node(), Msg]), + emqx_hooks:run('message.dropped', [#{node => node()}, Msg]), inc_dropped_cnt(Topic), Delivery; [Sub] -> %% optimize? dispatch(Sub, Topic, Msg), diff --git a/src/emqx_message.erl b/src/emqx_message.erl index da762703e..86f6a825f 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -22,6 +22,7 @@ -export([get_flag/2, get_flag/3, set_flag/2, set_flag/3, unset_flag/2]). -export([set_headers/2]). -export([get_header/2, get_header/3, set_header/3]). +-export([format/1]). -spec(make(topic(), payload()) -> message()). make(Topic, Payload) -> @@ -55,10 +56,14 @@ get_flag(Flag, #message{flags = Flags}, Default) -> maps:get(Flag, Flags, Default). -spec(set_flag(message_flag(), message()) -> message()). +set_flag(Flag, Msg = #message{flags = undefined}) when is_atom(Flag) -> + Msg#message{flags = #{Flag => true}}; set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) -> Msg#message{flags = maps:put(Flag, true, Flags)}. -spec(set_flag(message_flag(), boolean() | integer(), message()) -> message()). +set_flag(Flag, Val, Msg = #message{flags = undefined}) when is_atom(Flag) -> + Msg#message{flags = #{Flag => Val}}; set_flag(Flag, Val, Msg = #message{flags = Flags}) when is_atom(Flag) -> Msg#message{flags = maps:put(Flag, Val, Flags)}. @@ -83,3 +88,14 @@ set_header(Hdr, Val, Msg = #message{headers = undefined}) -> set_header(Hdr, Val, Msg = #message{headers = Headers}) -> Msg#message{headers = maps:put(Hdr, Val, Headers)}. +format(#message{id = Id, qos = QoS, topic = Topic, from = From, flags = Flags, headers = Headers}) -> + io_lib:format("Message(Id=~s, QoS=~w, Topic=~s, From=~s, Flags=~s, Headers=~s)", + [Id, QoS, Topic, From, format(flags, Flags), format(headers, Headers)]). + +format(_, undefined) -> + ""; +format(flags, Flags) -> + io_lib:format("~p", [[Flag || {Flag, true} <- maps:to_list(Flags)]]); +format(headers, Headers) -> + io_lib:format("~p", [Headers]). + diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index ef70dc28d..812c3267d 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -19,50 +19,48 @@ -include("emqx.hrl"). -export([load/1, unload/1]). --export([on_client_connected/3, on_client_disconnected/3]). +-export([on_client_connected/4, on_client_disconnected/3]). load(Env) -> - emqx:hook('client.connected', fun ?MODULE:on_client_connected/3, [Env]), - emqx:hook('client.disconnected', fun ?MODULE:on_client_disconnected/3, [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]). -on_client_connected(ConnAck, Client = #client{id = ClientId, - username = Username, - peername = {IpAddr, _} - %%clean_sess = CleanSess, - %%proto_ver = ProtoVer - }, Env) -> +on_client_connected(#{client_id := ClientId, + username := Username, + peername := {IpAddr, _}}, ConnAck, ConnInfo, Env) -> case emqx_json:safe_encode([{clientid, ClientId}, {username, Username}, {ipaddress, iolist_to_binary(esockd_net:ntoa(IpAddr))}, - %%{clean_sess, CleanSess}, %%TODO:: fixme later - %%{protocol, ProtoVer}, + {clean_start, proplists:get_value(clean_start, ConnInfo)}, + {proto_ver, proplists:get_value(proto_ver, ConnInfo)}, + {proto_name, proplists:get_value(proto_name, ConnInfo)}, + {keepalive, proplists:get_value(keepalive, ConnInfo)}, {connack, ConnAck}, - {ts, emqx_time:now_secs()}]) of + {ts, os:system_time(second)}]) of {ok, Payload} -> emqx:publish(message(qos(Env), topic(connected, ClientId), Payload)); {error, Reason} -> emqx_logger:error("[Presence Module] Json error: ~p", [Reason]) - end, - {ok, Client}. + end. -on_client_disconnected(Reason, #client{id = ClientId, username = Username}, Env) -> +on_client_disconnected(#{client_id := ClientId, username := Username}, Reason, Env) -> case emqx_json:safe_encode([{clientid, ClientId}, {username, Username}, {reason, reason(Reason)}, - {ts, emqx_time:now_secs()}]) of + {ts, os:system_time(second)}]) of {ok, Payload} -> emqx_broker:publish(message(qos(Env), topic(disconnected, ClientId), Payload)); {error, Reason} -> emqx_logger:error("[Presence Module] Json error: ~p", [Reason]) - end, ok. + end. unload(_Env) -> - emqx:unhook('client.connected', fun ?MODULE:on_client_connected/3), - emqx:unhook('client.disconnected', fun ?MODULE:on_client_disconnected/3). + emqx_hooks:delete('client.connected', fun ?MODULE:on_client_connected/4), + emqx_hooks:delete('client.disconnected', fun ?MODULE:on_client_disconnected/3). message(QoS, Topic, Payload) -> Msg = emqx_message:make(?MODULE, QoS, Topic, iolist_to_binary(Payload)), - emqx_message:set_flags(#{sys => true}, Msg). + emqx_message:set_flag(sys, Msg). topic(connected, ClientId) -> emqx_topic:systop(iolist_to_binary(["clients/", ClientId, "/connected"])); diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index 8ddd07f6c..2a92793eb 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -15,47 +15,31 @@ -module(emqx_mod_rewrite). -include_lib("emqx.hrl"). - -include_lib("emqx_mqtt.hrl"). -export([load/1, unload/1]). --export([rewrite_subscribe/4, rewrite_unsubscribe/4, rewrite_publish/2]). - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- +-export([rewrite_subscribe/3, rewrite_unsubscribe/3, rewrite_publish/2]). load(Rules0) -> Rules = compile(Rules0), - emqx:hook('client.subscribe', fun ?MODULE:rewrite_subscribe/4, [Rules]), - emqx:hook('client.unsubscribe',fun ?MODULE:rewrite_unsubscribe/4, [Rules]), - emqx:hook('message.publish', fun ?MODULE:rewrite_publish/2, [Rules]). + emqx_hooks:add('client.subscribe', fun ?MODULE:rewrite_subscribe/4, [Rules]), + emqx_hooks:add('client.unsubscribe',fun ?MODULE:rewrite_unsubscribe/4, [Rules]), + emqx_hooks:add('message.publish', fun ?MODULE:rewrite_publish/2, [Rules]). -rewrite_subscribe(_ClientId, _Username, TopicTable, Rules) -> - emqx_logger:info("Rewrite subscribe: ~p", [TopicTable]), +rewrite_subscribe(_Credentials, TopicTable, Rules) -> {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. -rewrite_unsubscribe(_ClientId, _Username, TopicTable, Rules) -> - emqx_logger:info("Rewrite unsubscribe: ~p", [TopicTable]), +rewrite_unsubscribe(_Credentials, TopicTable, Rules) -> {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. rewrite_publish(Message = #message{topic = Topic}, Rules) -> - %%TODO: this will not work if the client is always online. - RewriteTopic = - case get({rewrite, Topic}) of - undefined -> - DestTopic = match_rule(Topic, Rules), - put({rewrite, Topic}, DestTopic), DestTopic; - DestTopic -> - DestTopic - end, - {ok, Message#message{topic = RewriteTopic}}. + {ok, Message#message{topic = match_rule(Topic, Rules)}}. unload(_) -> - emqx:unhook('client.subscribe', fun ?MODULE:rewrite_subscribe/4), - emqx:unhook('client.unsubscribe',fun ?MODULE:rewrite_unsubscribe/4), - emqx:unhook('message.publish', fun ?MODULE:rewrite_publish/2). + emqx_hooks:delete('client.subscribe', fun ?MODULE:rewrite_subscribe/3), + emqx_hooks:delete('client.unsubscribe',fun ?MODULE:rewrite_unsubscribe/3), + emqx_hooks:delete('message.publish', fun ?MODULE:rewrite_publish/2). %%-------------------------------------------------------------------- %% Internal functions @@ -79,8 +63,7 @@ match_regx(Topic, MP, Dest) -> fun({Var, Val}, Acc) -> re:replace(Acc, Var, Val, [global]) end, Dest, Vars)); - nomatch -> - Topic + nomatch -> Topic end. compile(Rules) -> diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index 978b46a3b..b0da175c6 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -17,32 +17,26 @@ -behaviour(emqx_gen_mod). -include_lib("emqx.hrl"). - -include_lib("emqx_mqtt.hrl"). --export([load/1, on_client_connected/3, unload/1]). - --define(TAB, ?MODULE). +-export([load/1, on_session_created/3, unload/1]). %%-------------------------------------------------------------------- %% Load/Unload Hook %%-------------------------------------------------------------------- load(Topics) -> - emqx:hook('client.connected', fun ?MODULE:on_client_connected/3, [Topics]). + emqx_hooks:add('session.created', fun ?MODULE:on_session_created/3, [Topics]). -on_client_connected(RC, Client = #client{id = ClientId, pid = ClientPid, username = Username}, Topics) - when RC < 16#80 -> - Replace = fun(Topic) -> rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) end, - TopicTable = [{Replace(Topic), QoS} || {Topic, QoS} <- Topics], - ClientPid ! {subscribe, TopicTable}, - {ok, Client}; - -on_client_connected(_ConnAck, _Client, _State) -> - ok. +on_session_created(#{client_id := ClientId}, SessInfo, Topics) -> + Username = proplists:get_value(username, SessInfo), + Replace = fun(Topic) -> + rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) + end, + emqx_session:subscribe(self(), [{Replace(Topic), #{qos => QoS}} || {Topic, QoS} <- Topics]). unload(_) -> - emqx:unhook('client.connected', fun ?MODULE:on_client_connected/3). + emqx_hooks:delete('session.created', fun ?MODULE:on_session_created/3). %%-------------------------------------------------------------------- %% Internal functions diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 939252c3e..c6ab19c3c 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -22,6 +22,7 @@ -export([validate/1]). -export([format/1]). -export([to_message/2, from_message/2]). +-export([will_msg/1]). %% @doc Protocol name of version -spec(protocol_name(mqtt_version()) -> binary()). @@ -57,7 +58,7 @@ validate(?UNSUBSCRIBE_PACKET(PacketId, TopicFilters)) -> validate(?PUBLISH_PACKET(_QoS, <<>>, _, _)) -> error(topic_name_invalid); validate(?PUBLISH_PACKET(_QoS, Topic, _, _)) -> - emqx_topic:wildcard(Topic) orelse error(topic_name_invalid); + (not emqx_topic:wildcard(Topic)) orelse error(topic_name_invalid); validate(_Packet) -> true. @@ -85,14 +86,15 @@ validate_qos(_) -> error(bad_qos). from_message(PacketId, Msg = #message{qos = QoS, topic = Topic, payload = Payload}) -> Dup = emqx_message:get_flag(dup, Msg, false), Retain = emqx_message:get_flag(retain, Msg, false), + Publish = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId, + %% TODO: Properties + properties = #{}}, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + dup = Dup, qos = QoS, - retain = Retain, - dup = Dup}, - variable = #mqtt_packet_publish{topic_name = Topic, - packet_id = PacketId, - properties = #{}}, %%TODO: - payload = Payload}. + retain = Retain}, + variable = Publish, payload = Payload}. %% @doc Message from Packet -spec(to_message(emqx_types:credentials(), mqtt_packet()) -> message()). @@ -106,19 +108,21 @@ to_message(#{client_id := ClientId, username := Username}, payload = Payload}) -> Msg = emqx_message:make(ClientId, QoS, Topic, Payload), Msg#message{flags = #{dup => Dup, retain => Retain}, - headers = merge_props(#{username => Username}, Props)}; + headers = merge_props(#{username => Username}, Props)}. -to_message(_Credentials, #mqtt_packet_connect{will_flag = false}) -> +-spec(will_msg(#mqtt_packet_connect{}) -> message()). +will_msg(#mqtt_packet_connect{will_flag = false}) -> undefined; -to_message(#{client_id := ClientId, username := Username}, - #mqtt_packet_connect{will_retain = Retain, - will_qos = QoS, - will_topic = Topic, - will_props = Props, - will_payload = Payload}) -> +will_msg(#mqtt_packet_connect{client_id = ClientId, + username = Username, + will_retain = Retain, + will_qos = QoS, + will_topic = Topic, + will_props = Properties, + will_payload = Payload}) -> Msg = emqx_message:make(ClientId, QoS, Topic, Payload), Msg#message{flags = #{dup => false, retain => Retain}, - headers = merge_props(#{username => Username}, Props)}. + headers = merge_props(#{username => Username}, Properties)}. merge_props(Headers, undefined) -> Headers; diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 9d59fff77..6ca559a56 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -112,13 +112,13 @@ set_username(_Username, PState) -> %%------------------------------------------------------------------------------ info(#pstate{zone = Zone, + client_id = ClientId, + username = Username, peername = Peername, proto_ver = ProtoVer, proto_name = ProtoName, - conn_props = ConnProps, - client_id = ClientId, - username = Username, clean_start = CleanStart, + conn_props = ConnProps, keepalive = Keepalive, mountpoint = Mountpoint, is_super = IsSuper, @@ -126,12 +126,12 @@ info(#pstate{zone = Zone, connected = Connected, connected_at = ConnectedAt}) -> [{zone, Zone}, + {client_id, ClientId}, + {username, Username}, {peername, Peername}, {proto_ver, ProtoVer}, {proto_name, ProtoName}, {conn_props, ConnProps}, - {client_id, ClientId}, - {username, Username}, {clean_start, CleanStart}, {keepalive, Keepalive}, {mountpoint, Mountpoint}, @@ -242,6 +242,10 @@ process_packet(?CONNECT_PACKET( username = Username, password = Password} = Connect), PState) -> + %% TODO: Mountpoint... + %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) + WillMsg = emqx_packet:will_msg(Connect), + PState1 = set_username(Username, PState#pstate{client_id = ClientId, proto_ver = ProtoVer, @@ -249,7 +253,7 @@ process_packet(?CONNECT_PACKET( clean_start = CleanStart, keepalive = Keepalive, conn_props = ConnProps, - will_msg = willmsg(Connect, PState), + will_msg = WillMsg, is_bridge = IsBridge, connected = true, connected_at = os:timestamp()}), @@ -379,10 +383,11 @@ process_packet(?PACKET(?DISCONNECT), PState) -> %%------------------------------------------------------------------------------ connack({?RC_SUCCESS, SP, PState}) -> - emqx_hooks:run('client.connected', [credentials(PState), ?RC_SUCCESS]), + emqx_hooks:run('client.connected', [credentials(PState), ?RC_SUCCESS, info(PState)]), deliver({connack, ?RC_SUCCESS, sp(SP)}, update_mountpoint(PState)); connack({ReasonCode, PState = #pstate{proto_ver = ProtoVer}}) -> + emqx_hooks:run('client.connected', [credentials(PState), ?RC_SUCCESS, info(PState)]), _ = deliver({connack, if ProtoVer =:= ?MQTT_PROTO_V5 -> ReasonCode; true -> @@ -637,13 +642,6 @@ shutdown(Error, PState = #pstate{client_id = ClientId, will_msg = WillMsg}) -> emqx_hooks:run('client.disconnected', [credentials(PState), Error]), emqx_cm:unregister_connection(ClientId). -willmsg(Packet, #pstate{client_id = ClientId, mountpoint = MountPoint}) - when is_record(Packet, mqtt_packet_connect) -> - case emqx_packet:to_message(ClientId, Packet) of - undefined -> undefined; - Msg -> emqx_mountpoint:mount(MountPoint, Msg) - end. - send_willmsg(undefined) -> ignore; send_willmsg(WillMsg) -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 8c7795682..9719d1bca 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -73,11 +73,11 @@ %% Username username :: binary() | undefined, - %% Client pid binding with session - client_pid :: pid(), + %% Connection pid binding with session + conn_pid :: pid(), - %% Old client Pid that has been kickout - old_client_pid :: pid(), + %% Old Connection Pid that has been kickout + old_conn_pid :: pid(), %% Next packet id of the session next_pkt_id = 1 :: mqtt_packet_id(), @@ -145,7 +145,7 @@ -define(TIMEOUT, 60000). --define(INFO_KEYS, [clean_start, client_id, username, binding, client_pid, old_client_pid, +-define(INFO_KEYS, [clean_start, client_id, username, binding, conn_pid, old_conn_pid, next_pkt_id, max_subscriptions, subscriptions, upgrade_qos, inflight, max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, await_rel_timeout, expiry_interval, enable_stats, created_at]). @@ -306,19 +306,18 @@ close(SPid) -> init(#{zone := Zone, client_id := ClientId, - client_pid := ClientPid, - clean_start := CleanStart, username := Username, - %% TODO: + conn_pid := ConnPid, + clean_start := CleanStart, conn_props := _ConnProps}) -> process_flag(trap_exit, true), - true = link(ClientPid), + true = link(ConnPid), MaxInflight = get_env(Zone, max_inflight), State = #state{clean_start = CleanStart, - binding = binding(ClientPid), + binding = binding(ConnPid), client_id = ClientId, - client_pid = ClientPid, username = Username, + conn_pid = ConnPid, subscriptions = #{}, max_subscriptions = get_env(Zone, max_subscriptions, 0), upgrade_qos = get_env(Zone, upgrade_qos, false), @@ -335,7 +334,7 @@ init(#{zone := Zone, enqueue_stats = 0, created_at = os:timestamp()}, emqx_sm:register_session(ClientId, info(State)), - emqx_hooks:run('session.created', [ClientId]), + emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), {ok, ensure_stats_timer(State), hibernate}. init_mqueue(Zone, ClientId) -> @@ -346,12 +345,12 @@ init_mqueue(Zone, ClientId) -> binding(ClientPid) -> case node(ClientPid) =:= node() of true -> local; false -> remote end. -handle_call({discard, ClientPid}, _From, State = #state{client_pid = undefined}) -> - ?LOG(warning, "Discarded by ~p", [ClientPid], State), +handle_call({discard, ConnPid}, _From, State = #state{conn_pid = undefined}) -> + ?LOG(warning, "Discarded by ~p", [ConnPid], State), {stop, {shutdown, discard}, ok, State}; -handle_call({discard, ClientPid}, _From, State = #state{client_pid = OldClientPid}) -> - ?LOG(warning, " ~p kickout ~p", [ClientPid, OldClientPid], State), +handle_call({discard, ConnPid}, _From, State = #state{conn_pid = OldConnPid}) -> + ?LOG(warning, " ~p kickout ~p", [ConnPid, OldConnPid], State), {stop, {shutdown, conflict}, ok, State}; %% PUBLISH: @@ -418,11 +417,11 @@ handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, SubMap; {ok, _SubOpts} -> emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), - emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), + emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts]), maps:put(Topic, SubOpts, SubMap); error -> emqx_broker:subscribe(Topic, ClientId, SubOpts), - emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), + emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts]), maps:put(Topic, SubOpts, SubMap) end} end, {[], Subscriptions}, TopicFilters), @@ -437,7 +436,7 @@ handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, case maps:find(Topic, SubMap) of {ok, SubOpts} -> ok = emqx_broker:unsubscribe(Topic, ClientId), - emqx_hooks:run('session.unsubscribed', [ClientId, Topic, SubOpts]), + emqx_hooks:run('session.unsubscribed', [#{client_id => ClientId}, Topic, SubOpts]), {[?RC_SUCCESS|Acc], maps:remove(Topic, SubMap)}; error -> {[?RC_NO_SUBSCRIPTION_EXISTED|Acc], SubMap} @@ -469,30 +468,28 @@ handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight end; %% RESUME: -handle_cast({resume, ClientPid}, - State = #state{client_id = ClientId, - client_pid = OldClientPid, - clean_start = CleanStart, - retry_timer = RetryTimer, - await_rel_timer = AwaitTimer, - expiry_timer = ExpireTimer}) -> +handle_cast({resume, ConnPid}, State = #state{client_id = ClientId, + conn_pid = OldConnPid, + clean_start = CleanStart, + retry_timer = RetryTimer, + await_rel_timer = AwaitTimer, + expiry_timer = ExpireTimer}) -> - ?LOG(info, "Resumed by ~p ", [ClientPid], State), + ?LOG(info, "Resumed by connection ~p ", [ConnPid], State), %% Cancel Timers - lists:foreach(fun emqx_misc:cancel_timer/1, - [RetryTimer, AwaitTimer, ExpireTimer]), + lists:foreach(fun emqx_misc:cancel_timer/1, [RetryTimer, AwaitTimer, ExpireTimer]), - case kick(ClientId, OldClientPid, ClientPid) of - ok -> ?LOG(warning, "~p kickout ~p", [ClientPid, OldClientPid], State); + case kick(ClientId, OldConnPid, ConnPid) of + ok -> ?LOG(warning, "connection ~p kickout ~p", [ConnPid, OldConnPid], State); ignore -> ok end, - true = link(ClientPid), + true = link(ConnPid), - State1 = State#state{client_pid = ClientPid, - binding = binding(ClientPid), - old_client_pid = OldClientPid, + State1 = State#state{conn_pid = ConnPid, + binding = binding(ConnPid), + old_conn_pid = OldConnPid, clean_start = false, retry_timer = undefined, awaiting_rel = #{}, @@ -500,14 +497,9 @@ handle_cast({resume, ClientPid}, expiry_timer = undefined}, %% Clean Session: true -> false? - if - CleanStart =:= true -> - ?LOG(error, "CleanSess changed to false.", [], State1); - %%TODO:: - %%emqx_sm:register_session(ClientId, info(State1)); - CleanStart =:= false -> - ok - end, + CleanStart andalso emqx_sm:set_session_attrs(ClientId, info(State1)), + + emqx_hooks:run('session.resumed', [#{client_id => ClientId}, info(State)]), %% Replay delivery and Dequeue pending messages {noreply, ensure_stats_timer(dequeue(retry_delivery(true, State1)))}; @@ -534,7 +526,7 @@ handle_info({dispatch, Topic, Msg}, State = #state{subscriptions = SubMap}) when end}; %% Do nothing if the client has been disconnected. -handle_info({timeout, _Timer, retry_delivery}, State = #state{client_pid = undefined}) -> +handle_info({timeout, _Timer, retry_delivery}, State = #state{conn_pid = undefined}) -> {noreply, ensure_stats_timer(State#state{retry_timer = undefined})}; handle_info({timeout, _Timer, retry_delivery}, State) -> @@ -547,27 +539,25 @@ handle_info({timeout, _Timer, expired}, State) -> ?LOG(info, "Expired, shutdown now.", [], State), shutdown(expired, State); -handle_info({'EXIT', ClientPid, _Reason}, - State = #state{clean_start= true, client_pid = ClientPid}) -> +handle_info({'EXIT', ConnPid, _Reason}, State = #state{clean_start= true, conn_pid = ConnPid}) -> {stop, normal, State}; -handle_info({'EXIT', ClientPid, Reason}, - State = #state{clean_start = false, - client_pid = ClientPid, - expiry_interval = Interval}) -> - ?LOG(info, "Client ~p EXIT for ~p", [ClientPid, Reason], State), +handle_info({'EXIT', ConnPid, Reason}, State = #state{clean_start = false, + conn_pid = ConnPid, + expiry_interval = Interval}) -> + ?LOG(info, "Connection ~p EXIT for ~p", [ConnPid, Reason], State), ExpireTimer = emqx_misc:start_timer(Interval, expired), - State1 = State#state{client_pid = undefined, expiry_timer = ExpireTimer}, - {noreply, State1, hibernate}; + State1 = State#state{conn_pid = undefined, expiry_timer = ExpireTimer}, + {noreply, State1}; -handle_info({'EXIT', Pid, _Reason}, State = #state{old_client_pid = Pid}) -> +handle_info({'EXIT', Pid, _Reason}, State = #state{old_conn_pid = Pid}) -> %% ignore {noreply, State, hibernate}; -handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = ClientPid}) -> - ?LOG(error, "unexpected EXIT: client_pid=~p, exit_pid=~p, reason=~p", - [ClientPid, Pid, Reason], State), - {noreply, State, hibernate}; +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], State), + {noreply, State}; handle_info(emit_stats, State = #state{client_id = ClientId}) -> emqx_sm:set_session_stats(ClientId, stats(State)), @@ -577,8 +567,8 @@ handle_info(Info, State) -> emqx_logger:error("[Session] unexpected info: ~p", [Info]), {noreply, State}. -terminate(Reason, #state{client_id = ClientId, username = Username}) -> - emqx_hooks:run('session.terminated', [ClientId, Username, Reason]), +terminate(Reason, #state{client_id = ClientId}) -> + emqx_hooks:run('session.terminated', [#{client_id => ClientId}, Reason]), emqx_sm:unregister_session(ClientId). code_change(_OldVsn, State, _Extra) -> @@ -713,8 +703,8 @@ run_dispatch_steps([{subid, SubId}|Steps], Msg, State) -> run_dispatch_steps(Steps, emqx_message:set_header('Subscription-Identifier', SubId, Msg), State). %% Enqueue message if the client has been disconnected -dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) -> - case emqx_hooks:run('message.dropped', [ClientId, Msg]) of +dispatch(Msg, State = #state{client_id = ClientId, conn_pid = undefined}) -> + case emqx_hooks:run('message.dropped', [#{client_id => ClientId}, Msg]) of ok -> enqueue_msg(Msg, State); stop -> State end; @@ -747,12 +737,12 @@ redeliver({PacketId, Msg = #message{qos = QoS}}, State) -> true -> emqx_message:set_flag(dup, Msg) end, State); -redeliver({pubrel, PacketId}, #state{client_pid = Pid}) -> - Pid ! {deliver, {pubrel, PacketId}}. +redeliver({pubrel, PacketId}, #state{conn_pid = ConnPid}) -> + ConnPid ! {deliver, {pubrel, PacketId}}. -deliver(PacketId, Msg, #state{client_pid = Pid, binding = local}) -> +deliver(PacketId, Msg, #state{conn_pid = Pid, binding = local}) -> Pid ! {deliver, {publish, PacketId, Msg}}; -deliver(PacketId, Msg, #state{client_pid = Pid, binding = remote}) -> +deliver(PacketId, Msg, #state{conn_pid = Pid, binding = remote}) -> emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, PacketId, Msg}]). %%------------------------------------------------------------------------------ @@ -769,24 +759,20 @@ await(PacketId, Msg, State = #state{inflight = Inflight, end, State1#state{inflight = emqx_inflight:insert(PacketId, {publish, {PacketId, Msg}, os:timestamp()}, Inflight)}. -acked(puback, PacketId, State = #state{client_id = ClientId, - username = Username, - inflight = Inflight}) -> +acked(puback, PacketId, State = #state{client_id = ClientId, inflight = Inflight}) -> case emqx_inflight:lookup(PacketId, Inflight) of {value, {publish, Msg, _Ts}} -> - emqx_hooks:run('message.acked', [ClientId, Username], Msg), + emqx_hooks:run('message.acked', [#{client_id =>ClientId}], Msg), State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}; none -> ?LOG(warning, "Duplicated PUBACK Packet: ~p", [PacketId], State), State end; -acked(pubrec, PacketId, State = #state{client_id = ClientId, - username = Username, - inflight = Inflight}) -> +acked(pubrec, PacketId, State = #state{client_id = ClientId, inflight = Inflight}) -> case emqx_inflight:lookup(PacketId, Inflight) of {value, {publish, Msg, _Ts}} -> - emqx_hooks:run('message.acked', [ClientId, Username], Msg), + emqx_hooks:run('message.acked', [ClientId], Msg), State#state{inflight = emqx_inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight)}; {value, {pubrel, PacketId, _Ts}} -> ?LOG(warning, "Duplicated PUBREC Packet: ~p", [PacketId], State), @@ -804,7 +790,7 @@ acked(pubcomp, PacketId, State = #state{inflight = Inflight}) -> %%------------------------------------------------------------------------------ %% Do nothing if client is disconnected -dequeue(State = #state{client_pid = undefined}) -> +dequeue(State = #state{conn_pid = undefined}) -> State; dequeue(State = #state{inflight = Inflight}) -> diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index e31adb141..577927b02 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -35,8 +35,6 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {session_pmon}). - -define(SM, ?MODULE). %% ETS Tables @@ -45,26 +43,22 @@ -define(SESSION_ATTRS_TAB, emqx_session_attrs). -define(SESSION_STATS_TAB, emqx_session_stats). --spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link() -> emqx_types: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(Attrs = #{clean_start := true, - client_id := ClientId, - client_pid := ClientPid}) -> +open_session(Attrs = #{clean_start := true, client_id := ClientId, conn_pid := ConnPid}) -> CleanStart = fun(_) -> - ok = discard_session(ClientId, ClientPid), + ok = discard_session(ClientId, ConnPid), emqx_session_sup:start_session(Attrs) end, emqx_sm_locker:trans(ClientId, CleanStart); -open_session(Attrs = #{clean_start := false, - client_id := ClientId, - client_pid := ClientPid}) -> +open_session(Attrs = #{clean_start := false, client_id := ClientId, conn_pid := ConnPid}) -> ResumeStart = fun(_) -> - case resume_session(ClientId, ClientPid) of + case resume_session(ClientId, ConnPid) of {ok, SPid} -> {ok, SPid, true}; {error, not_found} -> @@ -80,34 +74,33 @@ open_session(Attrs = #{clean_start := false, discard_session(ClientId) when is_binary(ClientId) -> discard_session(ClientId, self()). -discard_session(ClientId, ClientPid) when is_binary(ClientId) -> - lists:foreach( - fun({_ClientId, SPid}) -> - case catch emqx_session:discard(SPid, ClientPid) of - {Err, Reason} when Err =:= 'EXIT'; Err =:= error -> - emqx_logger:error("[SM] Failed to discard ~p: ~p", [SPid, Reason]); - ok -> ok - end - end, lookup_session(ClientId)). +discard_session(ClientId, ConnPid) when is_binary(ClientId) -> + lists:foreach(fun({_ClientId, SPid}) -> + case catch emqx_session:discard(SPid, ConnPid) of + {Err, Reason} when Err =:= 'EXIT'; Err =:= error -> + emqx_logger:error("[SM] Failed to discard ~p: ~p", [SPid, Reason]); + ok -> ok + end + end, lookup_session(ClientId)). %% @doc Try to resume a session. -spec(resume_session(client_id()) -> {ok, pid()} | {error, term()}). resume_session(ClientId) -> resume_session(ClientId, self()). -resume_session(ClientId, ClientPid) -> +resume_session(ClientId, ConnPid) -> case lookup_session(ClientId) of [] -> {error, not_found}; [{_ClientId, SPid}] -> - ok = emqx_session:resume(SPid, ClientPid), + ok = emqx_session:resume(SPid, ConnPid), {ok, SPid}; Sessions -> [{_, SPid}|StaleSessions] = lists:reverse(Sessions), emqx_logger:error("[SM] More than one session found: ~p", [Sessions]), lists:foreach(fun({_, StalePid}) -> - catch emqx_session:discard(StalePid, ClientPid) + catch emqx_session:discard(StalePid, ConnPid) end, StaleSessions), - ok = emqx_session:resume(SPid, ClientPid), + ok = emqx_session:resume(SPid, ConnPid), {ok, SPid} end. @@ -224,11 +217,11 @@ handle_call(Req, _From, State) -> emqx_logger:error("[SM] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({notify, {registered, ClientId, SPid}}, State = #state{session_pmon = PMon}) -> - {noreply, State#state{session_pmon = emqx_pmon:monitor(SPid, ClientId, PMon)}}; +handle_cast({notify, {registered, ClientId, SPid}}, State = #{session_pmon := PMon}) -> + {noreply, State#{session_pmon := emqx_pmon:monitor(SPid, ClientId, PMon)}}; -handle_cast({notify, {unregistered, _ClientId, SPid}}, State = #state{session_pmon = PMon}) -> - {noreply, State#state{session_pmon = emqx_pmon:demonitor(SPid, PMon)}}; +handle_cast({notify, {unregistered, _ClientId, SPid}}, State = #{session_pmon := PMon}) -> + {noreply, State#{session_pmon := emqx_pmon:demonitor(SPid, PMon)}}; handle_cast(Msg, State) -> emqx_logger:error("[SM] unexpected cast: ~p", [Msg]), @@ -236,7 +229,8 @@ handle_cast(Msg, State) -> handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #{session_pmon := PMon}) -> case emqx_pmon:find(DownPid, PMon) of - undefined -> {noreply, State}; + undefined -> + {noreply, State}; ClientId -> unregister_session({ClientId, DownPid}), {noreply, State#{session_pmon := emqx_pmon:erase(DownPid, PMon)}} diff --git a/src/emqx_time.erl b/src/emqx_time.erl index 0d74168c4..83e9deff5 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -26,4 +26,5 @@ now_ms() -> erlang:system_time(millisecond). now_ms({MegaSecs, Secs, MicroSecs}) -> - (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). \ No newline at end of file + (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). + From e62d215792f00fa1d20f9f1262bc93efb49e5370 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 29 Aug 2018 17:52:55 +0800 Subject: [PATCH 160/520] Shutdown and reboot ranch application --- src/emqx.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx.erl b/src/emqx.erl index a51a41608..5cf5826a4 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -166,8 +166,8 @@ shutdown() -> shutdown(Reason) -> emqx_logger:error("emqx shutdown for ~s", [Reason]), emqx_plugins:unload(), - lists:foreach(fun application:stop/1, [emqx, ekka, cowboy, esockd, gproc]). + lists:foreach(fun application:stop/1, [emqx, ekka, cowboy, ranch, esockd, gproc]). reboot() -> - lists:foreach(fun application:start/1, [gproc, esockd, cowboy, ekka, emqx]). + lists:foreach(fun application:start/1, [gproc, esockd, ranch, cowboy, ekka, emqx]). From f4330b8af3899fe73ece80f5c9b591be892c2445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Wed, 29 Aug 2018 19:19:22 +0800 Subject: [PATCH 161/520] Add some test suites and fix bugs --- src/emqx_mqueue.erl | 2 +- test/emqx_keepalive_SUITE.erl | 1 + test/emqx_message_SUITE.erl | 65 ++++++++++++++++++++++++ test/emqx_mqtt_caps_SUITE.erl | 6 ++- test/emqx_mqueue_SUITE.erl | 93 ++++++++++------------------------- test/emqx_packet_SUITE.erl | 93 +++++++++++++++++++++++++++++++++++ test/emqx_stats_SUITE.erl | 7 +-- test/emqx_zone_SUITE.erl | 2 +- 8 files changed, 197 insertions(+), 72 deletions(-) create mode 100644 test/emqx_message_SUITE.erl create mode 100644 test/emqx_packet_SUITE.erl diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index 458c301fc..73fd6d613 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -126,7 +126,7 @@ stats(#mqueue{type = Type, q = Q, max_len = MaxLen, len = Len, dropped = Dropped %% @doc Enqueue a message. -spec(in(message(), mqueue()) -> mqueue()). -in(#message{flags = #{qos := ?QOS_0}}, MQ = #mqueue{qos0 = false}) -> +in(#message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> MQ; in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}; diff --git a/test/emqx_keepalive_SUITE.erl b/test/emqx_keepalive_SUITE.erl index e07c96ffe..c4dbd80f2 100644 --- a/test/emqx_keepalive_SUITE.erl +++ b/test/emqx_keepalive_SUITE.erl @@ -15,6 +15,7 @@ -module(emqx_keepalive_SUITE). -compile(export_all). +-compile(nowarn_export_all). all() -> [{group, keepalive}]. diff --git a/test/emqx_message_SUITE.erl b/test/emqx_message_SUITE.erl new file mode 100644 index 000000000..da39ef882 --- /dev/null +++ b/test/emqx_message_SUITE.erl @@ -0,0 +1,65 @@ +%% Copyright (c) 2018 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_message_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx.hrl"). + +-include("emqx_mqtt.hrl"). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> + [ + message_make, + message_flag, + message_header, + message_format + ]. + +message_make(_) -> + Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), + ?assertEqual(0, Msg#message.qos), + Msg1 = emqx_message:make(<<"clientid">>, ?QOS2, <<"topic">>, <<"payload">>), + ?assert(is_binary(Msg1#message.id)), + ?assertEqual(2, Msg1#message.qos). + +message_flag(_) -> + Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), + Msg2 = emqx_message:set_flag(retain, false, Msg), + Msg3 = emqx_message:set_flag(dup, Msg2), + ?assert(emqx_message:get_flag(dup, Msg3)), + ?assertNot(emqx_message:get_flag(retain, Msg3)), + Msg4 = emqx_message:unset_flag(dup, Msg3), + Msg5 = emqx_message:unset_flag(retain, Msg4), + ?assertEqual(undefined, emqx_message:get_flag(dup, Msg5, undefined)), + ?assertEqual(undefined, emqx_message:get_flag(retain, Msg5, undefined)), + Msg6 = emqx_message:set_flags(#{dup => true, retain => true}, Msg5), + ?assert(emqx_message:get_flag(dup, Msg6)), + ?assert(emqx_message:get_flag(retain, Msg6)). + +message_header(_) -> + Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), + Msg1 = emqx_message:set_headers(#{a => 1, b => 2}, Msg), + Msg2 = emqx_message:set_header(c, 3, Msg1), + ?assertEqual(1, emqx_message:get_header(a, Msg2)), + ?assertEqual(4, emqx_message:get_header(d, Msg2, 4)). + +message_format(_) -> + io:format("~s", [emqx_message:format(emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>))]). + + diff --git a/test/emqx_mqtt_caps_SUITE.erl b/test/emqx_mqtt_caps_SUITE.erl index f2f50a296..8b840b91c 100644 --- a/test/emqx_mqtt_caps_SUITE.erl +++ b/test/emqx_mqtt_caps_SUITE.erl @@ -37,7 +37,11 @@ t_get_set_caps(_) -> mqtt_shared_subscription => true, mqtt_wildcard_subscription => true }, - Caps = emqx_mqtt_caps:get_caps(zone), + 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 diff --git a/test/emqx_mqueue_SUITE.erl b/test/emqx_mqueue_SUITE.erl index 0cff6a627..575478f90 100644 --- a/test/emqx_mqueue_SUITE.erl +++ b/test/emqx_mqueue_SUITE.erl @@ -24,14 +24,12 @@ -define(Q, emqx_mqueue). -all() -> [t_in, t_in_qos0, t_out, t_simple_mqueue, t_priority_mqueue, - t_priority_mqueue2, t_infinity_priority_mqueue, - t_infinity_simple_mqueue]. +all() -> [t_in, t_in_qos0, t_out, t_simple_mqueue, t_infinity_simple_mqueue, + t_priority_mqueue, t_infinity_priority_mqueue]. t_in(_) -> - Opts = [{max_length, 5}, - {store_qos0, true}], - Q = ?Q:new(<<"testQ">>, Opts, alarm_fun()), + Opts = #{type => simple, max_len => 5, store_qos0 => true}, + Q = ?Q:new(<<"testQ">>, Opts), ?assert(?Q:is_empty(Q)), Q1 = ?Q:in(#message{}, Q), ?assertEqual(1, ?Q:len(Q1)), @@ -43,18 +41,16 @@ t_in(_) -> ?assertEqual(5, ?Q:len(Q5)). t_in_qos0(_) -> - Opts = [{max_length, 5}, - {store_qos0, false}], - Q = ?Q:new(<<"testQ">>, Opts, alarm_fun()), - Q1 = ?Q:in(#message{}, Q), + Opts = #{type => simple, max_len => 5, store_qos0 => false}, + Q = ?Q:new(<<"testQ">>, Opts), + Q1 = ?Q:in(#message{qos = 0}, Q), ?assert(?Q:is_empty(Q1)), Q2 = ?Q:in(#message{qos = 0}, Q1), ?assert(?Q:is_empty(Q2)). t_out(_) -> - Opts = [{max_length, 5}, - {store_qos0, true}], - Q = ?Q:new(<<"testQ">>, Opts, alarm_fun()), + Opts = #{type => simple, max_len => 5, store_qos0 => true}, + Q = ?Q:new(<<"testQ">>, Opts), {empty, Q} = ?Q:out(Q), Q1 = ?Q:in(#message{}, Q), {Value, Q2} = ?Q:out(Q1), @@ -62,12 +58,8 @@ t_out(_) -> ?assertEqual({value, #message{}}, Value). t_simple_mqueue(_) -> - Opts = [{type, simple}, - {max_length, 3}, - {low_watermark, 0.2}, - {high_watermark, 0.6}, - {store_qos0, false}], - Q = ?Q:new("simple_queue", Opts, alarm_fun()), + Opts = #{type => simple, max_len => 3, store_qos0 => false}, + Q = ?Q:new("simple_queue", Opts), ?assertEqual(simple, ?Q:type(Q)), ?assertEqual(3, ?Q:max_len(Q)), ?assertEqual(<<"simple_queue">>, ?Q:name(Q)), @@ -82,12 +74,8 @@ t_simple_mqueue(_) -> ?assertEqual([{len, 2}, {max_len, 3}, {dropped, 1}], ?Q:stats(Q5)). t_infinity_simple_mqueue(_) -> - Opts = [{type, simple}, - {max_length, 0}, - {low_watermark, 0.2}, - {high_watermark, 0.6}, - {store_qos0, false}], - Q = ?Q:new("infinity_simple_queue", Opts, alarm_fun()), + Opts = #{type => simple, max_len => 0, store_qos0 => false}, + Q = ?Q:new("infinity_simple_queue", Opts), ?assert(?Q:is_empty(Q)), ?assertEqual(0, ?Q:max_len(Q)), Qx = lists:foldl(fun(I, AccQ) -> @@ -99,37 +87,29 @@ t_infinity_simple_mqueue(_) -> ?assertEqual(<<1>>, V#message.payload). t_priority_mqueue(_) -> - Opts = [{type, priority}, - {priority, [{<<"t">>, 10}]}, - {max_length, 3}, - {low_watermark, 0.2}, - {high_watermark, 0.6}, - {store_qos0, false}], - Q = ?Q:new("priority_queue", Opts, alarm_fun()), + Opts = #{type => priority, max_len => 3, store_qos0 => false}, + Q = ?Q:new("priority_queue", Opts), ?assertEqual(priority, ?Q:type(Q)), ?assertEqual(3, ?Q:max_len(Q)), ?assertEqual(<<"priority_queue">>, ?Q:name(Q)), - ?assert(?Q:is_empty(Q)), - Q1 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q), - Q2 = ?Q:in(#message{qos = 1, topic = <<"t">>}, Q1), - Q3 = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q2), + Q1 = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q), + Q2 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q1), + Q3 = ?Q:in(#message{qos = 1, topic = <<"t3">>}, Q2), ?assertEqual(3, ?Q:len(Q3)), - Q4 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q3), + Q4 = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q3), ?assertEqual(4, ?Q:len(Q4)), - Q5 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q4), + Q5 = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q4), ?assertEqual(5, ?Q:len(Q5)), - Q6 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q5), + Q6 = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q5), ?assertEqual(5, ?Q:len(Q6)), - {{value, Msg}, _Q7} = ?Q:out(Q6), - ?assertEqual(<<"t">>, Msg#message.topic). + {{value, Msg}, Q7} = ?Q:out(Q6), + ?assertEqual(4, ?Q:len(Q7)), + ?assertEqual(<<"t1">>, Msg#message.topic). t_infinity_priority_mqueue(_) -> - Opts = [{type, priority}, - {priority, [{<<"t1">>, 10}, {<<"t2">>, 8}]}, - {max_length, 0}, - {store_qos0, false}], - Q = ?Q:new("infinity_priority_queue", Opts, alarm_fun()), + Opts = #{type => priority, max_len => 0, store_qos0 => false}, + Q = ?Q:new("infinity_priority_queue", Opts), ?assertEqual(0, ?Q:max_len(Q)), Qx = lists:foldl(fun(I, AccQ) -> AccQ1 = @@ -137,23 +117,4 @@ t_infinity_priority_mqueue(_) -> ?Q:in(#message{topic = <<"t">>, qos = 1, payload = iolist_to_binary([I])}, AccQ1) end, Q, lists:seq(1, 255)), ?assertEqual(510, ?Q:len(Qx)), - ?assertEqual([{len, 510}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)). - -t_priority_mqueue2(_) -> - Opts = [{type, priority}, - {max_length, 2}, - {low_watermark, 0.2}, - {high_watermark, 0.6}, - {store_qos0, false}], - Q = ?Q:new("priority_queue2_test", Opts, alarm_fun()), - ?assertEqual(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), - ?assertEqual(4, ?Q:len(Q4)), - {{value, _Val}, Q5} = ?Q:out(Q4), - ?assertEqual(3, ?Q:len(Q5)). - -alarm_fun() -> fun(_, _) -> alarm_fun() end. - + ?assertEqual([{len, 510}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)). \ No newline at end of file diff --git a/test/emqx_packet_SUITE.erl b/test/emqx_packet_SUITE.erl new file mode 100644 index 000000000..bd1b0cb4a --- /dev/null +++ b/test/emqx_packet_SUITE.erl @@ -0,0 +1,93 @@ +%% Copyright (c) 2018 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_packet_SUITE). + +-compile(export_all). +-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 + ]. + +packet_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)), + ?assertEqual('UNSUBSCRIBE', emqx_packet:type_name(?UNSUBSCRIBE)). + +packet_validate(_) -> + ?assertEqual(true, emqx_packet:validate(?SUBSCRIBE_PACKET(15, #{'Subscription-Identifier' => 1}, [{<<"topic">>, #{qos => ?QOS0}}]))), + ?assertEqual(true, emqx_packet:validate(?UNSUBSCRIBE_PACKET(89, [<<"topic">>]))), + ?assertEqual(true, emqx_packet:validate(?CONNECT_PACKET(#mqtt_packet_connect{}))). + +packet_message(_) -> + Pkt = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = ?QOS0, + retain = false, + dup = false}, + variable = #mqtt_packet_publish{topic_name = <<"topic">>, + packet_id = 10, + properties = #{}}, + payload = <<"payload">>}, + Msg = emqx_message:make(<<"clientid">>, ?QOS0, <<"topic">>, <<"payload">>), + Msg2 = emqx_message:set_flag(retain, false, Msg), + Pkt = emqx_packet:from_message(10, Msg2), + Msg3 = emqx_message:set_header(username, "test", Msg2), + Msg4 = emqx_packet:to_message(#{client_id => <<"clientid">>, username => "test"}, Pkt), + Msg5 = Msg4#message{timestamp = Msg3#message.timestamp}, + Msg5 = Msg3. + +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))]), + io:format("~s", [emqx_packet:format(?PUBLISH_PACKET(?QOS_2, <<"topic">>, 10, <<"payload">>))]), + io:format("~s", [emqx_packet:format(?PUBACK_PACKET(?PUBACK, 98))]), + io:format("~s", [emqx_packet:format(?PUBREL_PACKET(99))]), + io:format("~s", [emqx_packet:format(?SUBSCRIBE_PACKET(15, [{<<"topic">>, ?QOS0}, {<<"topic1">>, ?QOS1}]))]), + io:format("~s", [emqx_packet:format(?SUBACK_PACKET(40, [?QOS0, ?QOS1]))]), + 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 = ?QOS2, + 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_stats_SUITE.erl b/test/emqx_stats_SUITE.erl index d7fc294b1..5c7254468 100644 --- a/test/emqx_stats_SUITE.erl +++ b/test/emqx_stats_SUITE.erl @@ -22,7 +22,7 @@ all() -> [t_set_get_state, t_update_interval]. t_set_get_state(_) -> - {ok, _} = emqx_stats:start_link(), + emqx_stats:start_link(), SetConnsCount = emqx_stats:statsfun('connections/count'), SetConnsCount(1), 1 = emqx_stats:getstat('connections/count'), @@ -46,8 +46,9 @@ t_set_get_state(_) -> 4 = proplists:get_value('connections/max', Conns). t_update_interval(_) -> - {ok, _} = emqx_stats:start_link(), - ok = emqx_stats:update_interval(cm_stats, fun update_stats/0), + emqx_stats:start_link(), + emqx_stats:cancel_update(cm_stats), + ok = emqx_stats:update_interval(stats_test, fun update_stats/0), timer:sleep(2500), 1 = emqx_stats:getstat('connections/count'). diff --git a/test/emqx_zone_SUITE.erl b/test/emqx_zone_SUITE.erl index 15c449ae9..282acc3e5 100644 --- a/test/emqx_zone_SUITE.erl +++ b/test/emqx_zone_SUITE.erl @@ -22,7 +22,7 @@ all() -> [t_set_get_env]. t_set_get_env(_) -> - {ok, _} = emqx_zone:start_link(), + emqx_zone:start_link(), ok = emqx_zone:set_env(china, language, chinese), timer:sleep(100), % make sure set_env/3 is okay chinese = emqx_zone:get_env(china, language), From 1629215b1891f1ef9a10fade9d99586961255b60 Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 29 Aug 2018 20:29:03 +0800 Subject: [PATCH 162/520] Fix hook args bug --- src/emqx_protocol.erl | 2 +- src/emqx_session.erl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 1ee80fbbf..871e3c7f2 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -435,7 +435,7 @@ deliver({connack, ReasonCode, SP}, PState) -> send(?CONNACK_PACKET(ReasonCode, SP), PState); deliver({publish, PacketId, Msg}, PState = #pstate{is_bridge = IsBridge, mountpoint = MountPoint}) -> - _ = emqx_hooks:run('message.delivered', credentials(PState), Msg), + _ = emqx_hooks:run('message.delivered', [credentials(PState)], Msg), Msg1 = emqx_mountpoint:unmount(MountPoint, clean_retain(IsBridge, Msg)), send(emqx_packet:from_message(PacketId, Msg1), PState); diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 33661e2c3..220e3efd8 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -761,7 +761,7 @@ await(PacketId, Msg, State = #state{inflight = Inflight, acked(puback, PacketId, State = #state{client_id = ClientId, inflight = Inflight}) -> case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, Msg, _Ts}} -> + {value, {publish, {_, Msg}, _Ts}} -> emqx_hooks:run('message.acked', [#{client_id =>ClientId}], Msg), State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}; none -> @@ -771,7 +771,7 @@ acked(puback, PacketId, State = #state{client_id = ClientId, inflight = Infligh acked(pubrec, PacketId, State = #state{client_id = ClientId, inflight = Inflight}) -> case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, Msg, _Ts}} -> + {value, {publish, {_, Msg}, _Ts}} -> emqx_hooks:run('message.acked', [ClientId], Msg), State#state{inflight = emqx_inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight)}; {value, {pubrel, PacketId, _Ts}} -> From 98698d318f3f4955670deec2913c5966651071f4 Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 29 Aug 2018 21:39:09 +0800 Subject: [PATCH 163/520] ws disconnect call emqx_protocol:shutdown --- src/emqx_ws_connection.erl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index f3dc3c5e0..026d160b3 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -236,17 +236,15 @@ websocket_info(Info, State) -> {ok, State}. terminate(SockError, _Req, #state{keepalive = Keepalive, - proto_state = _ProtoState, + proto_state = ProtoState, shutdown_reason = Reason}) -> emqx_keepalive:cancel(Keepalive), io:format("Websocket shutdown for ~p, sockerror: ~p~n", [Reason, SockError]), case Reason of undefined -> ok; - %%TODO: - %%emqx_protocol:shutdown(SockError, ProtoState); _ -> - ok%%emqx_protocol:shutdown(Reason, ProtoState) + emqx_protocol:shutdown(Reason, ProtoState) end. reset_parser(State = #state{proto_state = ProtoState}) -> From 567aeb274f372d11ea2f8ffbf88d62c6818fb07c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 29 Aug 2018 23:08:55 +0800 Subject: [PATCH 164/520] Define types in emqx_types, emqx_mqtt_types modules --- include/emqx.hrl | 120 +++------------ include/emqx_mqtt.hrl | 294 ++++++++++++++++++------------------ src/emqx.erl | 32 ++-- src/emqx_access_control.erl | 9 +- src/emqx_access_rule.erl | 6 +- src/emqx_acl_cache.erl | 20 ++- src/emqx_acl_internal.erl | 3 +- src/emqx_alarm_mgr.erl | 4 +- src/emqx_banned.erl | 16 +- src/emqx_bridge.erl | 2 +- src/emqx_bridge1_sup.erl | 4 +- src/emqx_bridge_sup.erl | 3 +- src/emqx_bridge_sup_sup.erl | 8 +- src/emqx_broker.erl | 66 ++++---- src/emqx_client.erl | 12 +- src/emqx_cm.erl | 16 +- src/emqx_frame.erl | 11 +- src/emqx_message.erl | 17 ++- src/emqx_metrics.erl | 4 +- src/emqx_mqtt_caps.erl | 7 +- src/emqx_mqtt_types.erl | 43 ++++++ src/emqx_mqueue.erl | 2 +- src/emqx_packet.erl | 13 +- src/emqx_plugins.erl | 2 +- src/emqx_protocol.erl | 20 ++- src/emqx_router.erl | 22 +-- src/emqx_session.erl | 40 ++--- src/emqx_shared_sub.erl | 6 +- src/emqx_sm.erl | 24 +-- src/emqx_sm_locker.erl | 12 +- src/emqx_sm_registry.erl | 7 +- src/emqx_topic.erl | 6 +- src/emqx_trie.erl | 6 +- src/emqx_types.erl | 46 ++++-- src/emqx_zone.erl | 6 +- 35 files changed, 468 insertions(+), 441 deletions(-) create mode 100644 src/emqx_mqtt_types.erl diff --git a/include/emqx.hrl b/include/emqx.hrl index 5734da794..10190a3f6 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -12,6 +12,9 @@ %% See the License for the specific language governing permissions and %% limitations under the License. +-ifndef(EMQ_X_HRL). +-define(EMQ_X_HRL, true). + %%-------------------------------------------------------------------- %% Banner %%-------------------------------------------------------------------- @@ -24,14 +27,6 @@ -define(ERTS_MINIMUM_REQUIRED, "10.0"). -%%-------------------------------------------------------------------- -%% PubSub -%%-------------------------------------------------------------------- - --type(pubsub() :: publish | subscribe). - --define(PS(I), (I =:= publish orelse I =:= subscribe)). - %%-------------------------------------------------------------------- %% Topics' prefix: $SYS | $queue | $share %%-------------------------------------------------------------------- @@ -46,121 +41,48 @@ -define(SHARE, <<"$share/">>). %%-------------------------------------------------------------------- -%% Topic, subscription and subscriber +%% Message and Delivery %%-------------------------------------------------------------------- --type(topic() :: binary()). +-record(session, {sid, pid}). --type(subid() :: binary() | atom()). - --type(subopts() :: #{qos => integer(), - share => binary(), - atom() => term()}). - --record(subscription, { - topic :: topic(), - subid :: subid(), - subopts :: subopts() - }). - --type(subscription() :: #subscription{}). - --type(subscriber() :: {pid(), subid()}). - --type(topic_table() :: [{topic(), subopts()}]). - -%%-------------------------------------------------------------------- -%% Zone, Credentials, Client and Session -%%-------------------------------------------------------------------- - --type(zone() :: atom()). - --type(client_id() :: binary() | atom()). - --type(username() :: binary() | undefined). - --type(password() :: binary() | undefined). - --type(peername() :: {inet:ip_address(), inet:port_number()}). - --type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). - --type(credentials() :: #{client_id := binary(), - username := binary(), - peername := peername(), - zone => zone(), - atom() => term()}). - --record(client, { - id :: client_id(), - pid :: pid(), - zone :: zone(), - peername :: peername(), - username :: username(), - protocol :: protocol(), - attributes :: map() - }). - --type(client() :: #client{}). - --record(session, { - sid :: client_id(), - pid :: pid() - }). - --type(session() :: #session{}). - -%%-------------------------------------------------------------------- -%% Payload, Message and Delivery -%%-------------------------------------------------------------------- - --type(qos() :: integer()). - --type(payload() :: binary() | iodata()). - --type(message_flag() :: dup | sys | retain | atom()). +-record(subscription, {topic, subid, subopts}). %% See 'Application Message' in MQTT Version 5.0 -record(message, { %% Global unique message ID - id :: binary() | pos_integer(), + id :: binary(), %% Message QoS - qos = 0 :: qos(), + qos = 0, %% Message from - from :: atom() | client_id(), + from :: atom() | binary(), %% Message flags - flags :: #{message_flag() => boolean()}, + flags :: #{atom() => boolean()}, %% Message headers, or MQTT 5.0 Properties - headers = #{} :: map(), + headers = #{}, %% Topic that the message is published to - topic :: topic(), + topic :: binary(), %% Message Payload payload :: binary(), %% Timestamp timestamp :: erlang:timestamp() }). --type(message() :: #message{}). - -record(delivery, { - sender :: pid(), %% Sender of the delivery - message :: message(), %% The message delivered - flows :: list() %% The dispatch path of message + sender :: pid(), %% Sender of the delivery + message :: #message{}, %% The message delivered + results :: list() %% Dispatches of the message }). --type(delivery() :: #delivery{}). - %%-------------------------------------------------------------------- %% Route %%-------------------------------------------------------------------- -record(route, { - topic :: topic(), - dest :: node() | {binary(), node()} + topic :: binary(), + dest :: node() | {binary(), node()} }). --type(route() :: #route{}). - %%-------------------------------------------------------------------- %% Trie %%-------------------------------------------------------------------- @@ -170,7 +92,7 @@ -record(trie_node, { node_id :: trie_node_id(), edge_count = 0 :: non_neg_integer(), - topic :: topic() | undefined, + topic :: binary() | undefined, flags :: list(atom()) }). @@ -196,8 +118,6 @@ timestamp :: erlang:timestamp() }). --type(alarm() :: #alarm{}). - %%-------------------------------------------------------------------- %% Plugin %%-------------------------------------------------------------------- @@ -212,8 +132,6 @@ info :: map() }). --type(plugin() :: #plugin{}). - %%-------------------------------------------------------------------- %% Command %%-------------------------------------------------------------------- @@ -227,5 +145,5 @@ descr :: string() }). --type(command() :: #command{}). +-endif. diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 1d69ac365..74bfc1120 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -12,6 +12,9 @@ %% See the License for the specific language governing permissions and %% limitations under the License. +-ifndef(EMQ_X_MQTT_HRL). +-define(EMQ_X_MQTT_HRL, true). + %%-------------------------------------------------------------------- %% MQTT SockOpts %%-------------------------------------------------------------------- @@ -32,8 +35,6 @@ {?MQTT_PROTO_V4, <<"MQTT">>}, {?MQTT_PROTO_V5, <<"MQTT">>}]). --type(mqtt_version() :: ?MQTT_PROTO_V3 | ?MQTT_PROTO_V4 | ?MQTT_PROTO_V5). - %%-------------------------------------------------------------------- %% MQTT QoS Levels %%-------------------------------------------------------------------- @@ -48,12 +49,6 @@ -define(IS_QOS(I), (I >= ?QOS0 andalso I =< ?QOS2)). --type(mqtt_qos() :: ?QOS0 | ?QOS1 | ?QOS2). - --type(mqtt_qos_name() :: qos0 | at_most_once | - qos1 | at_least_once | - qos2 | exactly_once). - -define(QOS_I(Name), begin (case Name of @@ -80,25 +75,6 @@ -define(MAX_CLIENTID_LEN, 65535). -%%-------------------------------------------------------------------- -%% MQTT Client -%%-------------------------------------------------------------------- --record(mqtt_client, { - client_id :: binary() | undefined, - client_pid :: pid(), - username :: binary() | undefined, - peername :: {inet:ip_address(), inet:port_number()}, - clean_start :: boolean(), - proto_ver :: mqtt_version(), - keepalive = 0 :: non_neg_integer(), - will_topic :: undefined | binary(), - mountpoint :: undefined | binary(), - connected_at :: erlang:timestamp(), - attributes :: map() - }). - --type(mqtt_client() :: #mqtt_client{}). - %%-------------------------------------------------------------------- %% MQTT Control Packet Types %%-------------------------------------------------------------------- @@ -137,8 +113,6 @@ 'DISCONNECT', 'AUTH']). --type(mqtt_packet_type() :: ?RESERVED..?AUTH). - %%-------------------------------------------------------------------- %% MQTT V3.1.1 Connect Return Codes %%-------------------------------------------------------------------- @@ -150,8 +124,6 @@ -define(CONNACK_CREDENTIALS, 4). %% Username or password is malformed -define(CONNACK_AUTH, 5). %% Client is not authorized to connect --type(mqtt_connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH). - %%-------------------------------------------------------------------- %% MQTT V5.0 Reason Codes %%-------------------------------------------------------------------- @@ -220,108 +192,91 @@ %%-------------------------------------------------------------------- -record(mqtt_packet_header, { - type = ?RESERVED :: mqtt_packet_type(), - dup = false :: boolean(), - qos = ?QOS_0 :: mqtt_qos(), - retain = false :: boolean() + type = ?RESERVED, + dup = false, + qos = ?QOS_0, + retain = false }). %%-------------------------------------------------------------------- %% MQTT Packets %%-------------------------------------------------------------------- --type(mqtt_topic() :: binary()). - --type(mqtt_client_id() :: binary()). - --type(mqtt_username() :: binary() | undefined). - --type(mqtt_packet_id() :: 1..16#FFFF | undefined). - --type(mqtt_reason_code() :: 0..16#FF | undefined). - --type(mqtt_properties() :: #{atom() => term()} | undefined). - --type(mqtt_subopts() :: #{atom() => term()}). - --define(DEFAULT_SUBOPTS, #{rh => 0, %% Retain Handling - rap => 0, %% Retain as Publish - nl => 0, %% No Local - qos => ?QOS_0, - rc => 0, %% Reason Code - subid => 0 %% Subscription-Identifier +-define(DEFAULT_SUBOPTS, #{rh => 0, %% Retain Handling + rap => 0, %% Retain as Publish + nl => 0, %% No Local + qos => 0, %% QoS + rc => 0 %% Reason Code }). --type(mqtt_topic_filters() :: [{mqtt_topic(), mqtt_subopts()}]). - -record(mqtt_packet_connect, { - proto_name = <<"MQTT">> :: binary(), - proto_ver = ?MQTT_PROTO_V4 :: mqtt_version(), - is_bridge = false :: boolean(), - clean_start = true :: boolean(), - will_flag = false :: boolean(), - will_qos = ?QOS_0 :: mqtt_qos(), - will_retain = false :: boolean(), - keepalive = 0 :: non_neg_integer(), - properties = undefined :: mqtt_properties(), - client_id = <<>> :: mqtt_client_id(), - will_props = undefined :: undefined | map(), - will_topic = undefined :: undefined | binary(), - will_payload = undefined :: undefined | binary(), - username = undefined :: undefined | binary(), - password = undefined :: undefined | binary() + proto_name = <<"MQTT">>, + proto_ver = ?MQTT_PROTO_V4, + is_bridge = false, + clean_start = true, + will_flag = false, + will_qos = ?QOS_0, + will_retain = false, + keepalive = 0, + properties = undefined, + client_id = <<>>, + will_props = undefined, + will_topic = undefined, + will_payload = undefined, + username = undefined, + password = undefined }). -record(mqtt_packet_connack, { - ack_flags :: 0 | 1, - reason_code :: mqtt_reason_code(), - properties :: mqtt_properties() + ack_flags, + reason_code, + properties }). -record(mqtt_packet_publish, { - topic_name :: mqtt_topic(), - packet_id :: mqtt_packet_id(), - properties :: mqtt_properties() + topic_name, + packet_id, + properties }). -record(mqtt_packet_puback, { - packet_id :: mqtt_packet_id(), - reason_code :: mqtt_reason_code(), - properties :: mqtt_properties() + packet_id, + reason_code, + properties }). -record(mqtt_packet_subscribe, { - packet_id :: mqtt_packet_id(), - properties :: mqtt_properties(), - topic_filters :: mqtt_topic_filters() + packet_id, + properties, + topic_filters }). -record(mqtt_packet_suback, { - packet_id :: mqtt_packet_id(), - properties :: mqtt_properties(), - reason_codes :: list(mqtt_reason_code()) + packet_id, + properties, + reason_codes }). -record(mqtt_packet_unsubscribe, { - packet_id :: mqtt_packet_id(), - properties :: mqtt_properties(), - topic_filters :: [mqtt_topic()] + packet_id, + properties, + topic_filters }). -record(mqtt_packet_unsuback, { - packet_id :: mqtt_packet_id(), - properties :: mqtt_properties(), - reason_codes :: list(mqtt_reason_code()) + packet_id, + properties, + reason_codes }). -record(mqtt_packet_disconnect, { - reason_code :: mqtt_reason_code(), - properties :: mqtt_properties() + reason_code, + properties }). -record(mqtt_packet_auth, { - reason_code :: mqtt_reason_code(), - properties :: mqtt_properties() + reason_code, + properties }). %%-------------------------------------------------------------------- @@ -340,63 +295,70 @@ | #mqtt_packet_unsuback{} | #mqtt_packet_disconnect{} | #mqtt_packet_auth{} - | mqtt_packet_id() + | pos_integer() | undefined, payload :: binary() | undefined }). --type(mqtt_packet() :: #mqtt_packet{}). - %%-------------------------------------------------------------------- %% MQTT Packet Match %%-------------------------------------------------------------------- -define(CONNECT_PACKET(Var), - #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, variable = Var}). + #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, + variable = Var}). -define(CONNACK_PACKET(ReasonCode), #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, variable = #mqtt_packet_connack{ack_flags = 0, - reason_code = ReasonCode}}). + reason_code = ReasonCode} + }). -define(CONNACK_PACKET(ReasonCode, SessPresent), #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, variable = #mqtt_packet_connack{ack_flags = SessPresent, - reason_code = ReasonCode}}). + reason_code = ReasonCode} + }). -define(CONNACK_PACKET(ReasonCode, SessPresent, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, variable = #mqtt_packet_connack{ack_flags = SessPresent, reason_code = ReasonCode, - properties = Properties}}). + properties = Properties} + }). -define(AUTH_PACKET(), #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, - variable = #mqtt_packet_auth{reason_code = 0}}). + variable = #mqtt_packet_auth{reason_code = 0} + }). -define(AUTH_PACKET(ReasonCode), #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, - variable = #mqtt_packet_auth{reason_code = ReasonCode}}). + variable = #mqtt_packet_auth{reason_code = ReasonCode} + }). -define(AUTH_PACKET(ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, variable = #mqtt_packet_auth{reason_code = ReasonCode, - properties = Properties}}). + properties = Properties} + }). -define(PUBLISH_PACKET(QoS), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = QoS}}). -define(PUBLISH_PACKET(QoS, PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - qos = QoS}, - variable = #mqtt_packet_publish{packet_id = PacketId}}). + qos = QoS}, + variable = #mqtt_packet_publish{packet_id = PacketId} + }). -define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = QoS}, variable = #mqtt_packet_publish{topic_name = Topic, packet_id = PacketId}, - payload = Payload}). + payload = Payload + }). -define(PUBLISH_PACKET(QoS, Topic, PacketId, Properties, Payload), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, @@ -404,130 +366,166 @@ variable = #mqtt_packet_publish{topic_name = Topic, packet_id = PacketId, properties = Properties}, - payload = Payload}). + payload = Payload + }). -define(PUBACK_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, variable = #mqtt_packet_puback{packet_id = PacketId, - reason_code = 0}}). + reason_code = 0} + }). -define(PUBACK_PACKET(PacketId, ReasonCode), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, variable = #mqtt_packet_puback{packet_id = PacketId, - reason_code = ReasonCode}}). + reason_code = ReasonCode} + }). -define(PUBACK_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = ReasonCode, - properties = Properties}}). + properties = Properties} + }). -define(PUBREC_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, variable = #mqtt_packet_puback{packet_id = PacketId, - reason_code = 0}}). + reason_code = 0} + }). -define(PUBREC_PACKET(PacketId, ReasonCode), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, variable = #mqtt_packet_puback{packet_id = PacketId, - reason_code = ReasonCode}}). + reason_code = ReasonCode} + }). -define(PUBREC_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, - variable = #mqtt_packet_puback{packet_id = PacketId, + variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = ReasonCode, - properties = Properties}}). + properties = Properties} + }). -define(PUBREL_PACKET(PacketId), - #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, + qos = ?QOS_1}, variable = #mqtt_packet_puback{packet_id = PacketId, - reason_code = 0}}). + reason_code = 0} + }). + -define(PUBREL_PACKET(PacketId, ReasonCode), - #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, + qos = ?QOS_1}, variable = #mqtt_packet_puback{packet_id = PacketId, - reason_code = ReasonCode}}). + reason_code = ReasonCode} + }). -define(PUBREL_PACKET(PacketId, ReasonCode, Properties), - #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, - variable = #mqtt_packet_puback{packet_id = PacketId, + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, + qos = ?QOS_1}, + variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = ReasonCode, - properties = Properties}}). + properties = Properties} + }). -define(PUBCOMP_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, variable = #mqtt_packet_puback{packet_id = PacketId, - reason_code = 0}}). + reason_code = 0} + }). + -define(PUBCOMP_PACKET(PacketId, ReasonCode), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, variable = #mqtt_packet_puback{packet_id = PacketId, - reason_code = ReasonCode}}). + reason_code = ReasonCode} + }). -define(PUBCOMP_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = ReasonCode, - properties = Properties}}). + properties = Properties} + }). -define(SUBSCRIBE_PACKET(PacketId, TopicFilters), - #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, qos = ?QOS_1}, + #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, + qos = ?QOS_1}, variable = #mqtt_packet_subscribe{packet_id = PacketId, - topic_filters = TopicFilters}}). + topic_filters = TopicFilters} + }). -define(SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), - #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, qos = ?QOS_1}, + #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, + qos = ?QOS_1}, variable = #mqtt_packet_subscribe{packet_id = PacketId, properties = Properties, - topic_filters = TopicFilters}}). + topic_filters = TopicFilters} + }). -define(SUBACK_PACKET(PacketId, ReasonCodes), - #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, + #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, variable = #mqtt_packet_suback{packet_id = PacketId, - reason_codes = ReasonCodes}}). + reason_codes = ReasonCodes} + }). -define(SUBACK_PACKET(PacketId, Properties, ReasonCodes), - #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, + #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, variable = #mqtt_packet_suback{packet_id = PacketId, properties = Properties, - reason_codes = ReasonCodes}}). + reason_codes = ReasonCodes} + }). + -define(UNSUBSCRIBE_PACKET(PacketId, TopicFilters), - #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, qos = ?QOS_1}, + #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, + qos = ?QOS_1}, variable = #mqtt_packet_unsubscribe{packet_id = PacketId, - topic_filters = TopicFilters}}). + topic_filters = TopicFilters} + }). -define(UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), - #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, qos = ?QOS_1}, + #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, + qos = ?QOS_1}, variable = #mqtt_packet_unsubscribe{packet_id = PacketId, properties = Properties, - topic_filters = TopicFilters}}). + topic_filters = TopicFilters} + }). -define(UNSUBACK_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, - variable = #mqtt_packet_unsuback{packet_id = PacketId}}). + variable = #mqtt_packet_unsuback{packet_id = PacketId} + }). -define(UNSUBACK_PACKET(PacketId, ReasonCodes), #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, variable = #mqtt_packet_unsuback{packet_id = PacketId, - reason_codes = ReasonCodes}}). + reason_codes = ReasonCodes} + }). -define(UNSUBACK_PACKET(PacketId, Properties, ReasonCodes), #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, variable = #mqtt_packet_unsuback{packet_id = PacketId, properties = Properties, - reason_codes = ReasonCodes}}). + reason_codes = ReasonCodes} + }). -define(DISCONNECT_PACKET(), #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, - variable = #mqtt_packet_disconnect{reason_code = 0}}). + variable = #mqtt_packet_disconnect{reason_code = 0} + }). -define(DISCONNECT_PACKET(ReasonCode), #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, - variable = #mqtt_packet_disconnect{reason_code = ReasonCode}}). + variable = #mqtt_packet_disconnect{reason_code = ReasonCode} + }). -define(DISCONNECT_PACKET(ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, variable = #mqtt_packet_disconnect{reason_code = ReasonCode, - properties = Properties}}). + properties = Properties} + }). --define(PACKET(Type), - #mqtt_packet{header = #mqtt_packet_header{type = Type}}). +-define(PACKET(Type), #mqtt_packet{header = #mqtt_packet_header{type = Type}}). + +-endif. diff --git a/src/emqx.erl b/src/emqx.erl index 5ea884e9a..8e1f10168 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -66,32 +66,36 @@ is_running(Node) -> %% PubSub API %%-------------------------------------------------------------------- --spec(subscribe(topic() | string()) -> ok | {error, term()}). +-spec(subscribe(emqx_topic:topic() | string()) -> ok | {error, term()}). subscribe(Topic) -> emqx_broker:subscribe(iolist_to_binary(Topic)). --spec(subscribe(topic() | string(), subscriber() | string()) -> ok | {error, term()}). +-spec(subscribe(emqx_topic:topic() | string(), emqx_types:subscriber() | string()) + -> ok | {error, term()}). subscribe(Topic, Sub) when is_list(Sub)-> emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Sub)); subscribe(Topic, Subscriber) when is_tuple(Subscriber) -> {SubPid, SubId} = Subscriber, emqx_broker:subscribe(iolist_to_binary(Topic), SubPid, SubId). --spec(subscribe(topic() | string(), subscriber() | string(), subopts()) -> ok | {error, term()}). +-spec(subscribe(emqx_topic:topic() | string(), emqx_types:subscriber() | string(), + emqx_topic:subopts()) -> ok | {error, term()}). subscribe(Topic, Sub, Options) when is_list(Sub)-> emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Sub), Options); subscribe(Topic, Subscriber, Options) when is_tuple(Subscriber)-> {SubPid, SubId} = Subscriber, emqx_broker:subscribe(iolist_to_binary(Topic), SubPid, SubId, Options). --spec(publish(message()) -> {ok, emqx_types:dispatches()}). -publish(Msg) -> emqx_broker:publish(Msg). +-spec(publish(emqx_types:message()) -> {ok, emqx_types:deliver_results()}). +publish(Msg) -> + emqx_broker:publish(Msg). --spec(unsubscribe(topic() | string()) -> ok | {error, term()}). +-spec(unsubscribe(emqx_topic:topic() | string()) -> ok | {error, term()}). unsubscribe(Topic) -> emqx_broker:unsubscribe(iolist_to_binary(Topic)). --spec(unsubscribe(topic() | string(), subscriber() | string()) -> ok | {error, term()}). +-spec(unsubscribe(emqx_topic:topic() | string(), emqx_types:subscriber() | string()) + -> ok | {error, term()}). unsubscribe(Topic, Sub) when is_list(Sub) -> emqx_broker:unsubscribe(iolist_to_binary(Topic), list_to_subid(Sub)); unsubscribe(Topic, Subscriber) when is_tuple(Subscriber) -> @@ -102,26 +106,28 @@ unsubscribe(Topic, Subscriber) when is_tuple(Subscriber) -> %% PubSub management API %%-------------------------------------------------------------------- --spec(get_subopts(topic() | string(), subscriber()) -> subopts()). +-spec(get_subopts(emqx_topic:topic() | string(), emqx_types:subscriber()) + -> emqx_types:subopts()). get_subopts(Topic, Subscriber) -> emqx_broker:get_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber)). --spec(set_subopts(topic() | string(), subscriber(), subopts()) -> ok). +-spec(set_subopts(emqx_topic:topic() | string(), emqx_types:subscriber(), + emqx_types:subopts()) -> ok). set_subopts(Topic, Subscriber, Options) when is_list(Options) -> emqx_broker:set_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). --spec(topics() -> list(topic())). +-spec(topics() -> list(emqx_topic:topic())). topics() -> emqx_router:topics(). --spec(subscribers(topic() | string()) -> list(subscriber())). +-spec(subscribers(emqx_topic:topic() | string()) -> list(emqx_types:subscriber())). subscribers(Topic) -> emqx_broker:subscribers(iolist_to_binary(Topic)). --spec(subscriptions(subscriber()) -> [{topic(), subopts()}]). +-spec(subscriptions(emqx_types:subscriber()) -> [{emqx_topic:topic(), emqx_types:subopts()}]). subscriptions(Subscriber) -> emqx_broker:subscriptions(Subscriber). --spec(subscribed(topic() | string(), subscriber()) -> boolean()). +-spec(subscribed(emqx_topic:topic() | string(), emqx_types:subscriber()) -> boolean()). subscribed(Topic, Subscriber) -> emqx_broker:subscribed(iolist_to_binary(Topic), list_to_subid(Subscriber)). diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 7c6bcfb60..46ed9ed53 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -55,7 +55,7 @@ register_default_acl() -> File -> register_mod(acl, emqx_acl_internal, [File]) end. --spec(authenticate(credentials(), password()) +-spec(authenticate(emqx_types:credentials(), emqx_types:password()) -> ok | {ok, map()} | {continue, map()} | {error, term()}). authenticate(Credentials, Password) -> authenticate(Credentials, Password, lookup_mods(auth)). @@ -85,10 +85,9 @@ authenticate(Credentials, Password, [{Mod, State, _Seq} | Mods]) -> end. %% @doc Check ACL --spec(check_acl(credentials(), pubsub(), topic()) -> allow | deny). -check_acl(Credentials, PubSub, Topic) when ?PS(PubSub) -> - CacheEnabled = emqx_acl_cache:is_enabled(), - check_acl(Credentials, PubSub, Topic, lookup_mods(acl), CacheEnabled). +-spec(check_acl(emqx_types:credentials(), emqx_types:pubsub(), emqx_types:topic()) -> allow | deny). +check_acl(Credentials, PubSub, Topic) when PubSub =:= publish; PubSub =:= subscribe -> + check_acl(Credentials, PubSub, Topic, lookup_mods(acl), emqx_acl_cache:is_enabled()). check_acl(Credentials, PubSub, Topic, AclMods, false) -> do_check_acl(Credentials, PubSub, Topic, AclMods); diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 2f5d190a9..3fb6dd7ef 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -24,9 +24,9 @@ -type(access() :: subscribe | publish | pubsub). -type(rule() :: {allow, all} | - {allow, who(), access(), list(topic())} | + {allow, who(), access(), list(emqx_topic:topic())} | {deny, all} | - {deny, who(), access(), list(topic())}). + {deny, who(), access(), list(emqx_topic:topic())}). -export_type([rule/0]). @@ -81,7 +81,7 @@ bin(B) when is_binary(B) -> B. %% @doc Match access rule --spec(match(credentials(), topic(), rule()) +-spec(match(emqx_types:credentials(), emqx_types:topic(), rule()) -> {matched, allow} | {matched, deny} | nomatch). match(_Credentials, _Topic, {AllowDeny, all}) when ?ALLOW_DENY(AllowDeny) -> {matched, AllowDeny}; diff --git a/src/emqx_acl_cache.erl b/src/emqx_acl_cache.erl index 65e1e3305..5be92814d 100644 --- a/src/emqx_acl_cache.erl +++ b/src/emqx_acl_cache.erl @@ -1,3 +1,17 @@ +%% Copyright (c) 2018 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_acl_cache). -include("emqx.hrl"). @@ -27,8 +41,7 @@ is_enabled() -> application:get_env(emqx, enable_acl_cache, true). %% We'll cleanup the cache before repalcing an expired acl. --spec(get_acl_cache(PubSub :: publish | subscribe, Topic :: topic()) - -> (acl_result() | not_found)). +-spec(get_acl_cache(publish | subscribe, emqx_topic:topic()) -> (acl_result() | not_found)). get_acl_cache(PubSub, Topic) -> case erlang:get(cache_k(PubSub, Topic)) of undefined -> not_found; @@ -44,8 +57,7 @@ get_acl_cache(PubSub, Topic) -> %% If the cache get full, and also the latest one %% is expired, then delete all the cache entries --spec(put_acl_cache(PubSub :: publish | subscribe, - Topic :: topic(), AclResult :: acl_result()) -> ok). +-spec(put_acl_cache(publish | subscribe, emqx_topic:topic(), acl_result()) -> ok). put_acl_cache(PubSub, Topic, AclResult) -> MaxSize = get_cache_max_size(), true = (MaxSize =/= 0), Size = get_cache_size(), diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index 54f944416..0f25e6808 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -70,7 +70,8 @@ filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) -> false. %% @doc Check ACL --spec(check_acl({credentials(), pubsub(), topic()}, #{}) -> allow | deny | ignore). +-spec(check_acl({emqx_types:credentials(), emqx_types:pubsub(), emqx_topic:topic()}, #{}) + -> allow | deny | ignore). check_acl({Credentials, PubSub, Topic}, _State) -> case match(Credentials, Topic, lookup(PubSub)) of {matched, allow} -> allow; diff --git a/src/emqx_alarm_mgr.erl b/src/emqx_alarm_mgr.erl index 9839ba44e..bb734c8e6 100644 --- a/src/emqx_alarm_mgr.erl +++ b/src/emqx_alarm_mgr.erl @@ -48,7 +48,7 @@ alarm_fun(Bool) -> (clear, _AlarmId) when Bool =:= false -> alarm_fun(false) end. --spec(set_alarm(alarm()) -> ok). +-spec(set_alarm(emqx_types:alarm()) -> ok). set_alarm(Alarm) when is_record(Alarm, alarm) -> gen_event:notify(?ALARM_MGR, {set_alarm, Alarm}). @@ -56,7 +56,7 @@ set_alarm(Alarm) when is_record(Alarm, alarm) -> clear_alarm(AlarmId) when is_binary(AlarmId) -> gen_event:notify(?ALARM_MGR, {clear_alarm, AlarmId}). --spec(get_alarms() -> list(alarm())). +-spec(get_alarms() -> list(emqx_types:alarm())). get_alarms() -> gen_event:call(?ALARM_MGR, ?MODULE, get_alarms). diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index e2f03643e..908c8b5d5 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -30,15 +30,15 @@ -export([add/1, del/1]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -define(TAB, ?MODULE). -define(SERVER, ?MODULE). --type(key() :: {client_id, client_id()} | - {ipaddr, inet:ip_address()} | - {username, username()}). +-type(key() :: {client_id, emqx_types:client_id()} | + {username, emqx_types:username() | + {ipaddr, inet:ip_address()}}). -record(state, {expiry_timer}). @@ -63,12 +63,12 @@ mnesia(copy) -> %%-------------------------------------------------------------------- %% @doc Start the banned server --spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). --spec(check(client()) -> boolean()). -check(#client{id = ClientId, username = Username, peername = {IPAddr, _}}) -> +-spec(check(emqx_types:credentials()) -> 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}). diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index eef5d249b..d4ebab041 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -36,7 +36,7 @@ ping_down_interval = ?PING_DOWN_INTERVAL, status = up}). --type(option() :: {qos, mqtt_qos()} | +-type(option() :: {qos, emqx_mqtt_types:qos()} | {topic_suffix, binary()} | {topic_prefix, binary()} | {max_queue_len, pos_integer()} | diff --git a/src/emqx_bridge1_sup.erl b/src/emqx_bridge1_sup.erl index 444c7cfb5..f4e8c8f01 100644 --- a/src/emqx_bridge1_sup.erl +++ b/src/emqx_bridge1_sup.erl @@ -27,7 +27,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% @doc List all bridges --spec(bridges() -> [{node(), topic(), pid()}]). +-spec(bridges() -> [{node(), emqx_topic:topic(), pid()}]). bridges() -> [{Name, emqx_bridge1:status(Pid)} || {Name, Pid, _, _} <- supervisor:which_children(?MODULE)]. @@ -42,4 +42,4 @@ spec({Id, Options})-> restart => permanent, shutdown => 5000, type => worker, - modules => [emqx_bridge1]}. \ No newline at end of file + modules => [emqx_bridge1]}. diff --git a/src/emqx_bridge_sup.erl b/src/emqx_bridge_sup.erl index e7ba21310..1735e3b99 100644 --- a/src/emqx_bridge_sup.erl +++ b/src/emqx_bridge_sup.erl @@ -18,7 +18,8 @@ -export([start_link/3]). --spec(start_link(node(), topic(), [emqx_bridge:option()]) -> {ok, pid()} | {error, term()}). +-spec(start_link(node(), emqx_topic:topic(), [emqx_bridge:option()]) + -> {ok, pid()} | {error, term()}). start_link(Node, Topic, Options) -> MFA = {emqx_bridge, start_link, [Node, Topic, Options]}, emqx_pool_sup:start_link({bridge, Node, Topic}, random, MFA). diff --git a/src/emqx_bridge_sup_sup.erl b/src/emqx_bridge_sup_sup.erl index 4b34bedd9..2ef05df8c 100644 --- a/src/emqx_bridge_sup_sup.erl +++ b/src/emqx_bridge_sup_sup.erl @@ -30,17 +30,17 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% @doc List all bridges --spec(bridges() -> [{node(), topic(), pid()}]). +-spec(bridges() -> [{node(), emqx_topic:topic(), pid()}]). bridges() -> [{Node, Topic, Pid} || {?CHILD_ID(Node, Topic), Pid, supervisor, _} <- supervisor:which_children(?MODULE)]. %% @doc Start a bridge --spec(start_bridge(node(), topic()) -> {ok, pid()} | {error, term()}). +-spec(start_bridge(node(), emqx_topic:topic()) -> {ok, pid()} | {error, term()}). start_bridge(Node, Topic) when is_atom(Node), is_binary(Topic) -> start_bridge(Node, Topic, []). --spec(start_bridge(node(), topic(), [emqx_bridge:option()]) +-spec(start_bridge(node(), emqx_topic:topic(), [emqx_bridge:option()]) -> {ok, pid()} | {error, term()}). start_bridge(Node, _Topic, _Options) when Node =:= node() -> {error, bridge_to_self}; @@ -49,7 +49,7 @@ start_bridge(Node, Topic, Options) when is_atom(Node), is_binary(Topic) -> supervisor:start_child(?MODULE, bridge_spec(Node, Topic, Options1)). %% @doc Stop a bridge --spec(stop_bridge(node(), topic()) -> ok | {error, term()}). +-spec(stop_bridge(node(), emqx_topic:topic()) -> ok | {error, term()}). stop_bridge(Node, Topic) when is_atom(Node), is_binary(Topic) -> ChildId = ?CHILD_ID(Node, Topic), case supervisor:terminate_child(?MODULE, ChildId) of diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index dd6a4c1ba..623290961 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -57,17 +57,18 @@ start_link(Pool, Id) -> %% Subscribe %%------------------------------------------------------------------------------ --spec(subscribe(topic()) -> ok). +-spec(subscribe(emqx_topic:topic()) -> ok). subscribe(Topic) when is_binary(Topic) -> subscribe(Topic, self()). --spec(subscribe(topic(), pid() | subid()) -> ok). +-spec(subscribe(emqx_topic:topic(), pid() | emqx_types:subid()) -> ok). subscribe(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> subscribe(Topic, SubPid, undefined); subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> subscribe(Topic, self(), SubId). --spec(subscribe(topic(), pid() | subid(), subid() | subopts()) -> ok). +-spec(subscribe(emqx_topic:topic(), pid() | emqx_types:subid(), + emqx_types:subid() | emqx_types:subopts()) -> ok). subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> subscribe(Topic, SubPid, SubId, #{}); subscribe(Topic, SubPid, SubOpts) when is_binary(Topic), is_pid(SubPid), is_map(SubOpts) -> @@ -75,24 +76,24 @@ subscribe(Topic, SubPid, SubOpts) when is_binary(Topic), is_pid(SubPid), is_map( subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map(SubOpts) -> subscribe(Topic, self(), SubId, SubOpts). --spec(subscribe(topic(), pid(), subid(), subopts()) -> ok). +-spec(subscribe(emqx_topic:topic(), pid(), emqx_types:subid(), emqx_types:subopts()) -> ok). subscribe(Topic, SubPid, SubId, SubOpts) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId), is_map(SubOpts) -> Broker = pick(SubPid), SubReq = #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}, wait_for_reply(async_call(Broker, SubReq), ?TIMEOUT). --spec(multi_subscribe(topic_table()) -> ok). +-spec(multi_subscribe(emqx_types:topic_table()) -> ok). multi_subscribe(TopicTable) when is_list(TopicTable) -> multi_subscribe(TopicTable, self()). --spec(multi_subscribe(topic_table(), pid() | subid()) -> ok). +-spec(multi_subscribe(emqx_types:topic_table(), pid() | emqx_types:subid()) -> ok). multi_subscribe(TopicTable, SubPid) when is_pid(SubPid) -> multi_subscribe(TopicTable, SubPid, undefined); multi_subscribe(TopicTable, SubId) when ?is_subid(SubId) -> multi_subscribe(TopicTable, self(), SubId). --spec(multi_subscribe(topic_table(), pid(), subid()) -> ok). +-spec(multi_subscribe(emqx_types:topic_table(), pid(), emqx_types:subid()) -> ok). multi_subscribe(TopicTable, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) -> Broker = pick(SubPid), SubReq = fun(Topic, SubOpts) -> @@ -105,33 +106,33 @@ multi_subscribe(TopicTable, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) %% Unsubscribe %%------------------------------------------------------------------------------ --spec(unsubscribe(topic()) -> ok). +-spec(unsubscribe(emqx_topic:topic()) -> ok). unsubscribe(Topic) when is_binary(Topic) -> unsubscribe(Topic, self()). --spec(unsubscribe(topic(), pid() | subid()) -> ok). +-spec(unsubscribe(emqx_topic:topic(), pid() | emqx_types:subid()) -> ok). unsubscribe(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> unsubscribe(Topic, SubPid, undefined); unsubscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> unsubscribe(Topic, self(), SubId). --spec(unsubscribe(topic(), pid(), subid()) -> ok). +-spec(unsubscribe(emqx_topic:topic(), pid(), emqx_types:subid()) -> ok). unsubscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> Broker = pick(SubPid), UnsubReq = #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId}, wait_for_reply(async_call(Broker, UnsubReq), ?TIMEOUT). --spec(multi_unsubscribe([topic()]) -> ok). +-spec(multi_unsubscribe([emqx_topic:topic()]) -> ok). multi_unsubscribe(Topics) -> multi_unsubscribe(Topics, self()). --spec(multi_unsubscribe([topic()], pid() | subid()) -> ok). +-spec(multi_unsubscribe([emqx_topic:topic()], pid() | emqx_types:subid()) -> ok). multi_unsubscribe(Topics, SubPid) when is_pid(SubPid) -> multi_unsubscribe(Topics, SubPid, undefined); multi_unsubscribe(Topics, SubId) when ?is_subid(SubId) -> multi_unsubscribe(Topics, self(), SubId). --spec(multi_unsubscribe([topic()], pid(), subid()) -> ok). +-spec(multi_unsubscribe([emqx_topic:topic()], pid(), emqx_types:subid()) -> ok). multi_unsubscribe(Topics, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) -> Broker = pick(SubPid), UnsubReq = fun(Topic) -> @@ -143,18 +144,19 @@ multi_unsubscribe(Topics, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) - %% Publish %%------------------------------------------------------------------------------ --spec(publish(message()) -> {ok, emqx_types:dispatches()}). +-spec(publish(emqx_types:message()) -> {ok, emqx_types:deliver_results()}). publish(Msg) when is_record(Msg, message) -> _ = emqx_tracer:trace(publish, Msg), {ok, case emqx_hooks:run('message.publish', [], Msg) of {ok, Msg1 = #message{topic = Topic}} -> Delivery = route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1)), - Delivery#delivery.flows; + Delivery#delivery.results; {stop, _} -> - emqx_logger:warning("Stop publishing: ~p", [Msg]), [] + emqx_logger:warning("Stop publishing: ~s", [emqx_message:format(Msg)]), + [] end}. --spec(safe_publish(message()) -> ok). +-spec(safe_publish(emqx_types:message()) -> ok). %% Called internally safe_publish(Msg) when is_record(Msg, message) -> try @@ -167,7 +169,7 @@ safe_publish(Msg) when is_record(Msg, message) -> end. delivery(Msg) -> - #delivery{sender = self(), message = Msg, flows = []}. + #delivery{sender = self(), message = Msg, results = []}. %%------------------------------------------------------------------------------ %% Route @@ -180,8 +182,8 @@ route([], Delivery = #delivery{message = Msg}) -> route([{To, Node}], Delivery) when Node =:= node() -> dispatch(To, Delivery); -route([{To, Node}], Delivery = #delivery{flows = Flows}) when is_atom(Node) -> - forward(Node, To, Delivery#delivery{flows = [{route, Node, To}|Flows]}); +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); @@ -213,20 +215,21 @@ forward(Node, To, Delivery) -> Delivery1 -> Delivery1 end. --spec(dispatch(topic(), delivery()) -> delivery()). -dispatch(Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> +-spec(dispatch(emqx_topic:topic(), emqx_types:delivery()) -> emqx_types:delivery()). +dispatch(Topic, Delivery = #delivery{message = Msg, results = Results}) -> case subscribers(Topic) of [] -> emqx_hooks:run('message.dropped', [#{node => node()}, Msg]), - inc_dropped_cnt(Topic), Delivery; + inc_dropped_cnt(Topic), + Delivery; [Sub] -> %% optimize? dispatch(Sub, Topic, Msg), - Delivery#delivery{flows = [{dispatch, Topic, 1}|Flows]}; + Delivery#delivery{results = [{dispatch, Topic, 1}|Results]}; Subscribers -> Count = lists:foldl(fun(Sub, Acc) -> dispatch(Sub, Topic, Msg), Acc + 1 end, 0, Subscribers), - Delivery#delivery{flows = [{dispatch, Topic, Count}|Flows]} + Delivery#delivery{results = [{dispatch, Topic, Count}|Results]} end. dispatch({SubPid, _SubId}, Topic, Msg) when is_pid(SubPid) -> @@ -239,11 +242,12 @@ inc_dropped_cnt(<<"$SYS/", _/binary>>) -> inc_dropped_cnt(_Topic) -> emqx_metrics:inc('messages/dropped'). --spec(subscribers(topic()) -> [subscriber()]). +-spec(subscribers(emqx_topic:topic()) -> [emqx_types:subscriber()]). subscribers(Topic) -> try ets:lookup_element(?SUBSCRIBER, Topic, 2) catch error:badarg -> [] end. --spec(subscriptions(subscriber()) -> [{topic(), subopts()}]). +-spec(subscriptions(emqx_types:subscriber()) + -> [{emqx_topic:topic(), emqx_types:subopts()}]). subscriptions(Subscriber) -> lists:map(fun({_, {share, _Group, Topic}}) -> subscription(Topic, Subscriber); @@ -254,7 +258,7 @@ subscriptions(Subscriber) -> subscription(Topic, Subscriber) -> {Topic, ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2)}. --spec(subscribed(topic(), pid() | subid() | subscriber()) -> boolean()). +-spec(subscribed(emqx_topic:topic(), pid() | emqx_types:subid() | emqx_types:subscriber()) -> boolean()). subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> length(ets:match_object(?SUBOPTION, {{Topic, {SubPid, '_'}}, '_'}, 1)) == 1; subscribed(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> @@ -262,13 +266,13 @@ subscribed(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> subscribed(Topic, {SubPid, SubId}) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> ets:member(?SUBOPTION, {Topic, {SubPid, SubId}}). --spec(get_subopts(topic(), subscriber()) -> subopts()). +-spec(get_subopts(emqx_topic:topic(), emqx_types:subscriber()) -> emqx_types:subopts()). get_subopts(Topic, Subscriber) when is_binary(Topic) -> try ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2) catch error:badarg -> [] end. --spec(set_subopts(topic(), subscriber(), subopts()) -> boolean()). +-spec(set_subopts(emqx_topic:topic(), emqx_types:subscriber(), emqx_types:subopts()) -> boolean()). set_subopts(Topic, Subscriber, Opts) when is_binary(Topic), is_map(Opts) -> case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of [{_, OldOpts}] -> @@ -298,7 +302,7 @@ wait_for_reply(Tag, Timeout) -> pick(SubPid) when is_pid(SubPid) -> gproc_pool:pick_worker(broker, SubPid). --spec(topics() -> [topic()]). +-spec(topics() -> [emqx_topic:topic()]). topics() -> emqx_router:topics(). %%------------------------------------------------------------------------------ diff --git a/src/emqx_client.erl b/src/emqx_client.erl index ab653b302..e6aac5d43 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -87,7 +87,7 @@ clean_start :: boolean(), username :: binary() | undefined, password :: binary() | undefined, - proto_ver :: mqtt_version(), + proto_ver :: emqx_mqtt_types:version(), proto_name :: iodata(), keepalive :: non_neg_integer(), keepalive_timer :: reference() | undefined, @@ -114,15 +114,15 @@ -type(client() :: pid() | atom()). --type(topic() :: mqtt_topic()). +-type(topic() :: emqx_topic:topic()). -type(payload() :: iodata()). --type(packet_id() :: mqtt_packet_id()). +-type(packet_id() :: emqx_mqtt_types:packet_id()). --type(properties() :: mqtt_properties()). +-type(properties() :: emqx_mqtt_types:properties()). --type(qos() :: mqtt_qos_name() | mqtt_qos()). +-type(qos() :: emqx_mqtt_types:qos_name() | emqx_mqtt_types:qos()). -type(pubopt() :: {retain, boolean()} | {qos, qos()}). @@ -131,7 +131,7 @@ | {nl, boolean()} | {qos, qos()}). --type(reason_code() :: mqtt_reason_code()). +-type(reason_code() :: emqx_mqtt_types:reason_code()). -type(subscribe_ret() :: {ok, properties(), [reason_code()]} | {error, term()}). diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 5e921a1c0..3e9958939 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -43,12 +43,12 @@ start_link() -> gen_server:start_link({local, ?CM}, ?MODULE, [], []). %% @doc Lookup a connection. --spec(lookup_connection(client_id()) -> list({client_id(), pid()})). +-spec(lookup_connection(emqx_types:client_id()) -> list({emqx_types:client_id(), pid()})). lookup_connection(ClientId) when is_binary(ClientId) -> ets:lookup(?CONN_TAB, ClientId). %% @doc Register a connection. --spec(register_connection(client_id() | {client_id(), pid()}) -> ok). +-spec(register_connection(emqx_types:client_id() | {emqx_types:client_id(), pid()}) -> ok). register_connection(ClientId) when is_binary(ClientId) -> register_connection({ClientId, self()}); @@ -56,7 +56,7 @@ register_connection(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid _ = ets:insert(?CONN_TAB, Conn), notify({registered, ClientId, ConnPid}). --spec(register_connection(client_id() | {client_id(), pid()}, list()) -> ok). +-spec(register_connection(emqx_types:client_id() | {emqx_types:client_id(), pid()}, list()) -> ok). register_connection(ClientId, Attrs) when is_binary(ClientId) -> register_connection({ClientId, self()}, Attrs); register_connection(Conn = {ClientId, ConnPid}, Attrs) when is_binary(ClientId), is_pid(ConnPid) -> @@ -64,7 +64,7 @@ register_connection(Conn = {ClientId, ConnPid}, Attrs) when is_binary(ClientId), register_connection(Conn). %% @doc Get conn attrs --spec(get_conn_attrs({client_id(), pid()}) -> list()). +-spec(get_conn_attrs({emqx_types:client_id(), pid()}) -> list()). get_conn_attrs(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> try ets:lookup_element(?CONN_ATTRS_TAB, Conn, 2) @@ -79,7 +79,7 @@ set_conn_attrs(Conn = {ClientId, ConnPid}, Attrs) when is_binary(ClientId), is_p ets:insert(?CONN_ATTRS_TAB, {Conn, Attrs}). %% @doc Unregister a conn. --spec(unregister_connection(client_id() | {client_id(), pid()}) -> ok). +-spec(unregister_connection(emqx_types:client_id() | {emqx_types:client_id(), pid()}) -> ok). unregister_connection(ClientId) when is_binary(ClientId) -> unregister_connection({ClientId, self()}); @@ -90,7 +90,7 @@ unregister_connection(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_p notify({unregistered, ClientId, ConnPid}). %% @doc Lookup connection pid --spec(lookup_conn_pid(client_id()) -> pid() | undefined). +-spec(lookup_conn_pid(emqx_types:client_id()) -> pid() | undefined). lookup_conn_pid(ClientId) when is_binary(ClientId) -> case ets:lookup(?CONN_TAB, ClientId) of [] -> undefined; @@ -98,7 +98,7 @@ lookup_conn_pid(ClientId) when is_binary(ClientId) -> end. %% @doc Get conn stats --spec(get_conn_stats({client_id(), pid()}) -> list(emqx_stats:stats())). +-spec(get_conn_stats({emqx_types:client_id(), pid()}) -> list(emqx_stats:stats())). get_conn_stats(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> try ets:lookup_element(?CONN_STATS_TAB, Conn, 2) catch @@ -106,7 +106,7 @@ get_conn_stats(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(Conn end. %% @doc Set conn stats. --spec(set_conn_stats(client_id(), list(emqx_stats:stats())) -> boolean()). +-spec(set_conn_stats(emqx_types:client_id(), list(emqx_stats:stats())) -> boolean()). set_conn_stats(ClientId, Stats) when is_binary(ClientId) -> set_conn_stats({ClientId, self()}, Stats); diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index c1cc34d59..3ec935020 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -22,11 +22,11 @@ -export([serialize/1, serialize/2]). -type(options() :: #{max_packet_size => 1..?MAX_PACKET_SIZE, - version => mqtt_version()}). + version => emqx_mqtt_types:version()}). -type(parse_state() :: {none, options()} | cont_fun(binary())). --type(cont_fun(Bin) :: fun((Bin) -> {ok, mqtt_packet(), binary()} +-type(cont_fun(Bin) :: fun((Bin) -> {ok, emqx_mqtt_types:packet(), binary()} | {more, cont_fun(Bin)})). -export_type([options/0, parse_state/0]). @@ -53,7 +53,8 @@ merge_opts(Options) -> %% Parse MQTT Frame %%------------------------------------------------------------------------------ --spec(parse(binary(), parse_state()) -> {ok, mqtt_packet(), binary()} | {more, cont_fun(binary())}). +-spec(parse(binary(), parse_state()) -> {ok, emqx_mqtt_types:packet(), binary()} | + {more, cont_fun(binary())}). parse(<<>>, {none, Options}) -> {more, fun(Bin) -> parse(Bin, {none, Options}) end}; parse(<>, {none, Options}) -> @@ -359,11 +360,11 @@ parse_binary_data(<>) -> %% Serialize MQTT Packet %%------------------------------------------------------------------------------ --spec(serialize(mqtt_packet()) -> iodata()). +-spec(serialize(emqx_mqtt_types:packet()) -> iodata()). serialize(Packet) -> serialize(Packet, ?DEFAULT_OPTIONS). --spec(serialize(mqtt_packet(), options()) -> iodata()). +-spec(serialize(emqx_mqtt_types:packet(), options()) -> iodata()). serialize(#mqtt_packet{header = Header, variable = Variable, payload = Payload}, Options) when is_map(Options) -> diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 86f6a825f..8b49d44d6 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -24,15 +24,19 @@ -export([get_header/2, get_header/3, set_header/3]). -export([format/1]). --spec(make(topic(), payload()) -> message()). +-type(flag() :: atom()). + +-spec(make(emqx_topic:topic(), emqx_types:payload()) -> emqx_types:message()). make(Topic, Payload) -> make(undefined, Topic, Payload). --spec(make(atom() | client_id(), topic(), payload()) -> message()). +-spec(make(atom() | emqx_types:client_id(), emqx_topic:topic(), emqx_types:payload()) + -> emqx_types:message()). make(From, Topic, Payload) -> make(From, ?QOS0, Topic, Payload). --spec(make(atom() | client_id(), qos(), topic(), payload()) -> message()). +-spec(make(atom() | emqx_types:client_id(), emqx_mqtt_types:qos(), + emqx_topic:topic(), emqx_types:payload()) -> emqx_types:message()). make(From, QoS, Topic, Payload) -> #message{id = msgid(QoS), qos = QoS, @@ -55,19 +59,20 @@ get_flag(Flag, Msg) -> get_flag(Flag, #message{flags = Flags}, Default) -> maps:get(Flag, Flags, Default). --spec(set_flag(message_flag(), message()) -> message()). +-spec(set_flag(flag(), emqx_types:message()) -> emqx_types:message()). set_flag(Flag, Msg = #message{flags = undefined}) when is_atom(Flag) -> Msg#message{flags = #{Flag => true}}; set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) -> Msg#message{flags = maps:put(Flag, true, Flags)}. --spec(set_flag(message_flag(), boolean() | integer(), message()) -> message()). +-spec(set_flag(flag(), boolean() | integer(), emqx_types:message()) + -> emqx_types:message()). set_flag(Flag, Val, Msg = #message{flags = undefined}) when is_atom(Flag) -> Msg#message{flags = #{Flag => Val}}; set_flag(Flag, Val, Msg = #message{flags = Flags}) when is_atom(Flag) -> Msg#message{flags = maps:put(Flag, Val, Flags)}. --spec(unset_flag(message_flag(), message()) -> message()). +-spec(unset_flag(flag(), emqx_types:message()) -> emqx_types:message()). unset_flag(Flag, Msg = #message{flags = Flags}) -> Msg#message{flags = maps:remove(Flag, Flags)}. diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 506ff2c0d..15db3b420 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -167,7 +167,7 @@ update_counter(Key, UpOp) -> %%----------------------------------------------------------------------------- %% @doc Count packets received. --spec(received(mqtt_packet()) -> ok). +-spec(received(emqx_mqtt_types:packet()) -> ok). received(Packet) -> inc('packets/received'), received1(Packet). @@ -205,7 +205,7 @@ qos_received(?QOS_2) -> inc('messages/qos2/received'). %% @doc Count packets received. Will not count $SYS PUBLISH. --spec(sent(mqtt_packet()) -> ignore | non_neg_integer()). +-spec(sent(emqx_mqtt_types:packet()) -> ignore | non_neg_integer()). sent(?PUBLISH_PACKET(_QoS, <<"$SYS/", _/binary>>, _, _)) -> ignore; sent(Packet) -> diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl index 27b8ad7bc..fdc29fae8 100644 --- a/src/emqx_mqtt_caps.erl +++ b/src/emqx_mqtt_caps.erl @@ -25,7 +25,7 @@ max_clientid_len => integer(), max_topic_alias => integer(), max_topic_levels => integer(), - max_qos_allowed => mqtt_qos(), + max_qos_allowed => emqx_mqtt_types:qos(), mqtt_retain_available => boolean(), mqtt_shared_subscription => boolean(), mqtt_wildcard_subscription => boolean()}). @@ -49,7 +49,7 @@ mqtt_shared_subscription, mqtt_wildcard_subscription]). --spec(check_pub(zone(), map()) -> ok | {error, mqtt_reason_code()}). +-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))). @@ -65,7 +65,8 @@ do_check_pub(#{retain := true}, [{mqtt_retain_available, false}|_Caps]) -> do_check_pub(Props, [{mqtt_retain_available, _}|Caps]) -> do_check_pub(Props, Caps). --spec(check_sub(zone(), mqtt_topic_filters()) -> {ok | error, mqtt_topic_filters()}). +-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}) -> diff --git a/src/emqx_mqtt_types.erl b/src/emqx_mqtt_types.erl new file mode 100644 index 000000000..0b231fc88 --- /dev/null +++ b/src/emqx_mqtt_types.erl @@ -0,0 +1,43 @@ +%% Copyright (c) 2018 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() :: ?QOS0 | ?QOS1 | ?QOS2). +-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, + rap := 0 | 1 | 2, + 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 458c301fc..5127a5b7e 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -125,7 +125,7 @@ stats(#mqueue{type = Type, q = Q, max_len = MaxLen, len = Len, dropped = Dropped end} | [{max_len, MaxLen}, {dropped, Dropped}]]. %% @doc Enqueue a message. --spec(in(message(), mqueue()) -> mqueue()). +-spec(in(emqx_types:message(), mqueue()) -> mqueue()). in(#message{flags = #{qos := ?QOS_0}}, MQ = #mqueue{qos0 = false}) -> MQ; in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index c6ab19c3c..384d62444 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -25,7 +25,7 @@ -export([will_msg/1]). %% @doc Protocol name of version --spec(protocol_name(mqtt_version()) -> binary()). +-spec(protocol_name(emqx_mqtt_types:version()) -> binary()). protocol_name(?MQTT_PROTO_V3) -> <<"MQIsdp">>; protocol_name(?MQTT_PROTO_V4) -> @@ -34,7 +34,7 @@ protocol_name(?MQTT_PROTO_V5) -> <<"MQTT">>. %% @doc Name of MQTT packet type --spec(type_name(mqtt_packet_type()) -> atom()). +-spec(type_name(emqx_mqtt_types:packet_type()) -> atom()). type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> lists:nth(Type, ?TYPE_NAMES). @@ -82,7 +82,7 @@ validate_qos(QoS) when ?QOS0 =< QoS, QoS =< ?QOS2 -> validate_qos(_) -> error(bad_qos). %% @doc From Message to Packet --spec(from_message(mqtt_packet_id(), message()) -> mqtt_packet()). +-spec(from_message(emqx_mqtt_types:packet_id(), emqx_types:message()) -> emqx_mqtt_types:packet()). from_message(PacketId, Msg = #message{qos = QoS, topic = Topic, payload = Payload}) -> Dup = emqx_message:get_flag(dup, Msg, false), Retain = emqx_message:get_flag(retain, Msg, false), @@ -97,7 +97,8 @@ from_message(PacketId, Msg = #message{qos = QoS, topic = Topic, payload = Payloa variable = Publish, payload = Payload}. %% @doc Message from Packet --spec(to_message(emqx_types:credentials(), mqtt_packet()) -> message()). +-spec(to_message(emqx_types:credentials(), emqx_mqtt_types:packet()) + -> emqx_types:message()). to_message(#{client_id := ClientId, username := Username}, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, retain = Retain, @@ -110,7 +111,7 @@ to_message(#{client_id := ClientId, username := Username}, Msg#message{flags = #{dup => Dup, retain => Retain}, headers = merge_props(#{username => Username}, Props)}. --spec(will_msg(#mqtt_packet_connect{}) -> message()). +-spec(will_msg(#mqtt_packet_connect{}) -> emqx_types:message()). will_msg(#mqtt_packet_connect{will_flag = false}) -> undefined; will_msg(#mqtt_packet_connect{client_id = ClientId, @@ -130,7 +131,7 @@ merge_props(Headers, Props) -> maps:merge(Headers, Props). %% @doc Format packet --spec(format(mqtt_packet()) -> iolist()). +-spec(format(emqx_mqtt_types:packet()) -> iolist()). format(#mqtt_packet{header = Header, variable = Variable, payload = Payload}) -> format_header(Header, format_variable(Variable, Payload)). diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index 0c03e827e..a6a04458f 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -153,7 +153,7 @@ stop_plugins(Names) -> [stop_app(App) || App <- Names]. %% @doc List all available plugins --spec(list() -> [plugin()]). +-spec(list() -> [emqx_types:plugin()]). list() -> case emqx_config:get_env(plugins_etc_dir) of undefined -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 1ee80fbbf..cc64f5c7f 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -28,6 +28,22 @@ -export([send/2]). -export([shutdown/2]). + +%%-record(mqtt_client, { +%% client_id :: binary() | undefined, +%% client_pid :: pid(), +%% username :: binary() | undefined, +%% peername :: {inet:ip_address(), inet:port_number()}, +%% clean_start :: boolean(), +%% proto_ver :: emqx_mqtt_types:version(), +%% keepalive = 0 :: non_neg_integer(), +%% will_topic :: undefined | binary(), +%% mountpoint :: undefined | binary(), +%% connected_at :: erlang:timestamp(), +%% attributes :: map() +%% }). + + -record(pstate, { zone, sendfun, @@ -172,7 +188,7 @@ parser(#pstate{packet_size = Size, proto_ver = Ver}) -> %% Packet Received %%------------------------------------------------------------------------------ --spec(received(mqtt_packet(), state()) +-spec(received(emqx_mqtt_types:packet(), state()) -> {ok, state()} | {error, term()} | {error, term(), state()}). received(?PACKET(Type), PState = #pstate{connected = false}) when Type =/= ?CONNECT -> {error, proto_not_connected, PState}; @@ -469,7 +485,7 @@ deliver({disconnect, _ReasonCode}, PState) -> %%------------------------------------------------------------------------------ %% Send Packet to Client --spec(send(mqtt_packet(), state()) -> {ok, state()} | {error, term()}). +-spec(send(emqx_mqtt_types:packet(), state()) -> {ok, state()} | {error, term()}). send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = SendFun}) -> trace(send, Packet, PState), case SendFun(emqx_frame:serialize(Packet, #{version => Ver})) of diff --git a/src/emqx_router.erl b/src/emqx_router.erl index df2d2e018..b1f2e783a 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -71,54 +71,54 @@ start_link(Pool, Id) -> %% Route APIs %%------------------------------------------------------------------------------ --spec(add_route(topic() | route()) -> ok). +-spec(add_route(emqx_topic:topic() | emqx_types:route()) -> ok). add_route(Topic) when is_binary(Topic) -> add_route(#route{topic = Topic, dest = node()}); add_route(Route = #route{topic = Topic}) -> cast(pick(Topic), {add_route, Route}). --spec(add_route(topic(), destination()) -> ok). +-spec(add_route(emqx_topic:topic(), destination()) -> ok). add_route(Topic, Dest) when is_binary(Topic) -> add_route(#route{topic = Topic, dest = Dest}). --spec(add_route({pid(), reference()}, topic(), destination()) -> ok). +-spec(add_route({pid(), reference()}, emqx_topic:topic(), destination()) -> ok). add_route(From, Topic, Dest) when is_binary(Topic) -> cast(pick(Topic), {add_route, From, #route{topic = Topic, dest = Dest}}). --spec(get_routes(topic()) -> [route()]). +-spec(get_routes(emqx_topic:topic()) -> [emqx_types:route()]). get_routes(Topic) -> ets:lookup(?ROUTE, Topic). --spec(del_route(topic() | route()) -> ok). +-spec(del_route(emqx_topic:topic() | emqx_types:route()) -> ok). del_route(Topic) when is_binary(Topic) -> del_route(#route{topic = Topic, dest = node()}); del_route(Route = #route{topic = Topic}) -> cast(pick(Topic), {del_route, Route}). --spec(del_route(topic(), destination()) -> ok). +-spec(del_route(emqx_topic:topic(), destination()) -> ok). del_route(Topic, Dest) when is_binary(Topic) -> del_route(#route{topic = Topic, dest = Dest}). --spec(del_route({pid(), reference()}, topic(), destination()) -> ok). +-spec(del_route({pid(), reference()}, emqx_topic:topic(), destination()) -> ok). del_route(From, Topic, Dest) when is_binary(Topic) -> cast(pick(Topic), {del_route, From, #route{topic = Topic, dest = Dest}}). --spec(has_routes(topic()) -> boolean()). +-spec(has_routes(emqx_topic:topic()) -> boolean()). has_routes(Topic) when is_binary(Topic) -> ets:member(?ROUTE, Topic). --spec(topics() -> list(topic())). +-spec(topics() -> list(emqx_topic:topic())). topics() -> mnesia:dirty_all_keys(?ROUTE). %% @doc Match routes %% Optimize: routing table will be replicated to all router nodes. --spec(match_routes(topic()) -> [route()]). +-spec(match_routes(emqx_topic:topic()) -> [emqx_types:route()]). match_routes(Topic) when is_binary(Topic) -> Matched = mnesia:ets(fun emqx_trie:match/1, [Topic]), lists:append([get_routes(To) || To <- [Topic | Matched]]). %% @doc Print routes to a topic --spec(print_routes(topic()) -> ok). +-spec(print_routes(emqx_topic:topic()) -> ok). print_routes(Topic) -> lists:foreach(fun(#route{topic = To, dest = Dest}) -> io:format("~s -> ~s~n", [To, Dest]) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 33661e2c3..c5fd2fc6f 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -80,7 +80,7 @@ old_conn_pid :: pid(), %% Next packet id of the session - next_pkt_id = 1 :: mqtt_packet_id(), + next_pkt_id = 1 :: emqx_mqtt_types:packet_id(), %% Max subscriptions max_subscriptions :: non_neg_integer(), @@ -164,19 +164,20 @@ start_link(SessAttrs) -> %% PubSub API %%------------------------------------------------------------------------------ --spec(subscribe(pid(), list({topic(), map()}) | - {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). +-spec(subscribe(pid(), 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). -%% for mqtt 5.0 +-spec(subscribe(pid(), 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}). --spec(publish(pid(), mqtt_packet_id(), message()) -> {ok, emqx_types:dispatches()}). +-spec(publish(pid(), emqx_mqtt_types:packet_id(), emqx_types:message()) + -> {ok, emqx_types:deliver_results()}). publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_0}) -> %% Publish QoS0 message to broker directly emqx_broker:publish(Msg); @@ -189,43 +190,44 @@ publish(SPid, PacketId, Msg = #message{qos = ?QOS_2}) -> %% Publish QoS2 message to session gen_server:call(SPid, {publish, PacketId, Msg}, infinity). --spec(puback(pid(), mqtt_packet_id()) -> ok). +-spec(puback(pid(), emqx_mqtt_types:packet_id()) -> ok). puback(SPid, PacketId) -> gen_server:cast(SPid, {puback, PacketId, ?RC_SUCCESS}). puback(SPid, PacketId, ReasonCode) -> gen_server:cast(SPid, {puback, PacketId, ReasonCode}). --spec(pubrec(pid(), mqtt_packet_id()) -> ok | {error, mqtt_reason_code()}). +-spec(pubrec(pid(), emqx_mqtt_types:packet_id()) -> ok | {error, emqx_mqtt_types:reason_code()}). pubrec(SPid, PacketId) -> pubrec(SPid, PacketId, ?RC_SUCCESS). --spec(pubrec(pid(), mqtt_packet_id(), mqtt_reason_code()) - -> ok | {error, mqtt_reason_code()}). +-spec(pubrec(pid(), 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(pid(), mqtt_packet_id(), mqtt_reason_code()) - -> ok | {error, mqtt_reason_code()}). +-spec(pubrel(pid(), 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(pid(), mqtt_packet_id(), mqtt_reason_code()) -> ok). +-spec(pubcomp(pid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code()) -> ok). pubcomp(SPid, PacketId, ReasonCode) -> gen_server:cast(SPid, {pubcomp, PacketId, ReasonCode}). --spec(unsubscribe(pid(), topic_table()) -> ok). +-spec(unsubscribe(pid(), emqx_types:topic_table()) -> ok). unsubscribe(SPid, RawTopicFilters) when is_list(RawTopicFilters) -> unsubscribe(SPid, undefined, #{}, lists:map(fun emqx_topic:parse/1, RawTopicFilters)). --spec(unsubscribe(pid(), mqtt_packet_id(), mqtt_properties(), topic_table()) -> ok). +-spec(unsubscribe(pid(), 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(pid(), pid()) -> ok). -resume(SPid, ClientPid) -> - gen_server:cast(SPid, {resume, ClientPid}). +resume(SPid, ConnPid) -> + gen_server:cast(SPid, {resume, ConnPid}). %% @doc Get session info -spec(info(pid() | #state{}) -> list(tuple())). @@ -292,7 +294,7 @@ stats(#state{max_subscriptions = MaxSubscriptions, {enqueue_msg, EnqueueMsg}]). %% @doc Discard the session --spec(discard(pid(), client_id()) -> ok). +-spec(discard(pid(), emqx_types:client_id()) -> ok). discard(SPid, ClientId) -> gen_server:call(SPid, {discard, ClientId}, infinity). @@ -342,8 +344,8 @@ init_mqueue(Zone, ClientId) -> max_len => get_env(Zone, max_mqueue_len), store_qos0 => get_env(Zone, mqueue_store_qos0)}). -binding(ClientPid) -> - case node(ClientPid) =:= node() of true -> local; false -> remote end. +binding(ConnPid) -> + case node(ConnPid) =:= node() of true -> local; false -> remote end. handle_call({discard, ConnPid}, _From, State = #state{conn_pid = undefined}) -> ?LOG(warning, "Discarded by ~p", [ConnPid], State), diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index ce21a1bf8..0cbfab60a 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -58,7 +58,7 @@ mnesia(copy) -> %% API %%------------------------------------------------------------------------------ --spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). @@ -81,11 +81,11 @@ record(Group, Topic, SubPid) -> #emqx_shared_subscription{group = Group, topic = Topic, subpid = SubPid}. %% TODO: dispatch strategy, ensure the delivery... -dispatch(Group, Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> +dispatch(Group, Topic, Delivery = #delivery{message = Msg, results = Results}) -> case pick(subscribers(Group, Topic)) of false -> Delivery; SubPid -> SubPid ! {dispatch, Topic, Msg}, - Delivery#delivery{flows = [{dispatch, {Group, Topic}, 1} | Flows]} + Delivery#delivery{results = [{dispatch, {Group, Topic}, 1} | Results]} end. pick([]) -> diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 577927b02..0b188f986 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -70,7 +70,7 @@ open_session(Attrs = #{clean_start := false, client_id := ClientId, conn_pid := emqx_sm_locker:trans(ClientId, ResumeStart). %% @doc Discard all the sessions identified by the ClientId. --spec(discard_session(client_id()) -> ok). +-spec(discard_session(emqx_types:client_id()) -> ok). discard_session(ClientId) when is_binary(ClientId) -> discard_session(ClientId, self()). @@ -84,7 +84,7 @@ discard_session(ClientId, ConnPid) when is_binary(ClientId) -> end, lookup_session(ClientId)). %% @doc Try to resume a session. --spec(resume_session(client_id()) -> {ok, pid()} | {error, term()}). +-spec(resume_session(emqx_types:client_id()) -> {ok, pid()} | {error, term()}). resume_session(ClientId) -> resume_session(ClientId, self()). @@ -105,14 +105,14 @@ resume_session(ClientId, ConnPid) -> end. %% @doc Close a session. --spec(close_session({client_id(), pid()} | pid()) -> ok). +-spec(close_session({emqx_types:client_id(), pid()} | pid()) -> ok). close_session({_ClientId, SPid}) -> emqx_session:close(SPid); close_session(SPid) when is_pid(SPid) -> emqx_session:close(SPid). %% @doc Register a session with attributes. --spec(register_session(client_id() | {client_id(), pid()}, +-spec(register_session(emqx_types:client_id() | {emqx_types:client_id(), pid()}, list(emqx_session:attribute())) -> ok). register_session(ClientId, Attrs) when is_binary(ClientId) -> register_session({ClientId, self()}, Attrs); @@ -129,7 +129,7 @@ register_session(Session = {ClientId, SPid}, Attrs) notify({registered, ClientId, SPid}). %% @doc Get session attrs --spec(get_session_attrs({client_id(), pid()}) -> list(emqx_session:attribute())). +-spec(get_session_attrs({emqx_types:client_id(), pid()}) -> list(emqx_session:attribute())). get_session_attrs(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> safe_lookup_element(?SESSION_ATTRS_TAB, Session, []). @@ -140,7 +140,7 @@ set_session_attrs(Session = {ClientId, SPid}, Attrs) when is_binary(ClientId), i ets:insert(?SESSION_ATTRS_TAB, {Session, Attrs}). %% @doc Unregister a session --spec(unregister_session(client_id() | {client_id(), pid()}) -> ok). +-spec(unregister_session(emqx_types:client_id() | {emqx_types:client_id(), pid()}) -> ok). unregister_session(ClientId) when is_binary(ClientId) -> unregister_session({ClientId, self()}); @@ -153,13 +153,13 @@ unregister_session(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid( notify({unregistered, ClientId, SPid}). %% @doc Get session stats --spec(get_session_stats({client_id(), pid()}) -> list(emqx_stats:stats())). +-spec(get_session_stats({emqx_types:client_id(), pid()}) -> list(emqx_stats:stats())). get_session_stats(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> safe_lookup_element(?SESSION_STATS_TAB, Session, []). %% @doc Set session stats --spec(set_session_stats(client_id() | {client_id(), pid()}, +-spec(set_session_stats(emqx_types:client_id() | {emqx_types:client_id(), pid()}, emqx_stats:stats()) -> ok). set_session_stats(ClientId, Stats) when is_binary(ClientId) -> set_session_stats({ClientId, self()}, Stats); @@ -169,7 +169,7 @@ set_session_stats(Session = {ClientId, SPid}, Stats) ets:insert(?SESSION_STATS_TAB, {Session, Stats}). %% @doc Lookup a session from registry --spec(lookup_session(client_id()) -> list({client_id(), pid()})). +-spec(lookup_session(emqx_types:client_id()) -> list({emqx_types:client_id(), pid()})). lookup_session(ClientId) -> case emqx_sm_registry:is_enabled() of true -> emqx_sm_registry:lookup_session(ClientId); @@ -177,17 +177,17 @@ lookup_session(ClientId) -> end. %% @doc Dispatch a message to the session. --spec(dispatch(client_id(), topic(), message()) -> any()). +-spec(dispatch(emqx_types:client_id(), emqx_topic:topic(), emqx_types:message()) -> any()). dispatch(ClientId, Topic, Msg) -> case lookup_session_pid(ClientId) of Pid when is_pid(Pid) -> Pid ! {dispatch, Topic, Msg}; undefined -> - emqx_hooks:run('message.dropped', [ClientId, Msg]) + emqx_hooks:run('message.dropped', [#{client_id => ClientId}, Msg]) end. %% @doc Lookup session pid. --spec(lookup_session_pid(client_id()) -> pid() | undefined). +-spec(lookup_session_pid(emqx_types:client_id()) -> pid() | undefined). lookup_session_pid(ClientId) -> safe_lookup_element(?SESSION_TAB, ClientId, undefined). diff --git a/src/emqx_sm_locker.erl b/src/emqx_sm_locker.erl index 050e6276c..d50d16ccc 100644 --- a/src/emqx_sm_locker.erl +++ b/src/emqx_sm_locker.erl @@ -17,6 +17,7 @@ -include("emqx.hrl"). -export([start_link/0]). + -export([trans/2, trans/3]). -export([lock/1, lock/2, unlock/1]). @@ -24,11 +25,12 @@ start_link() -> ekka_locker:start_link(?MODULE). --spec(trans(client_id(), fun(([node()]) -> any())) -> any()). +-spec(trans(emqx_types:client_id(), fun(([node()]) -> any())) -> any()). trans(ClientId, Fun) -> trans(ClientId, Fun, undefined). --spec(trans(client_id() | undefined, fun(([node()]) -> any()), ekka_locker:piggyback()) -> any()). +-spec(trans(emqx_types:client_id() | undefined, + fun(([node()])-> any()), ekka_locker:piggyback()) -> any()). trans(undefined, Fun, _Piggyback) -> Fun([]); trans(ClientId, Fun, Piggyback) -> @@ -39,15 +41,15 @@ trans(ClientId, Fun, Piggyback) -> {error, client_id_unavailable} end. --spec(lock(client_id()) -> ekka_locker:lock_result()). +-spec(lock(emqx_types:client_id()) -> ekka_locker:lock_result()). lock(ClientId) -> ekka_locker:aquire(?MODULE, ClientId, strategy()). --spec(lock(client_id(), ekka_locker:piggyback()) -> ekka_locker:lock_result()). +-spec(lock(emqx_types:client_id(), ekka_locker:piggyback()) -> ekka_locker:lock_result()). lock(ClientId, Piggyback) -> ekka_locker:aquire(?MODULE, ClientId, strategy(), Piggyback). --spec(unlock(client_id()) -> {boolean(), [node()]}). +-spec(unlock(emqx_types:client_id()) -> {boolean(), [node()]}). unlock(ClientId) -> ekka_locker:release(?MODULE, ClientId, strategy()). diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index 701b9ae4e..74690b4b1 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -43,16 +43,17 @@ start_link() -> is_enabled() -> ets:info(?TAB, name) =/= undefined. --spec(lookup_session(client_id()) -> list({client_id(), session_pid()})). +-spec(lookup_session(emqx_types:client_id()) + -> list({emqx_types:client_id(), session_pid()})). lookup_session(ClientId) -> [{ClientId, SessionPid} || #global_session{pid = SessionPid} <- mnesia:dirty_read(?TAB, ClientId)]. --spec(register_session({client_id(), session_pid()}) -> ok). +-spec(register_session({emqx_types:client_id(), session_pid()}) -> ok). register_session({ClientId, SessionPid}) when is_binary(ClientId), is_pid(SessionPid) -> mnesia:dirty_write(?TAB, record(ClientId, SessionPid)). --spec(unregister_session({client_id(), session_pid()}) -> ok). +-spec(unregister_session({emqx_types:client_id(), session_pid()}) -> ok). unregister_session({ClientId, SessionPid}) when is_binary(ClientId), is_pid(SessionPid) -> mnesia:dirty_delete_object(?TAB, record(ClientId, SessionPid)). diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index b122c114b..c244a40b3 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -14,9 +14,6 @@ -module(emqx_topic). --include("emqx.hrl"). --include("emqx_mqtt.hrl"). - -export([match/2]). -export([validate/1, validate/2]). -export([levels/1]). @@ -28,11 +25,12 @@ -export([systop/1]). -export([parse/1, parse/2]). +-type(topic() :: binary()). -type(word() :: '' | '+' | '#' | binary()). -type(words() :: list(word())). -type(triple() :: {root | binary(), word(), binary()}). --export_type([word/0, triple/0]). +-export_type([topic/0, word/0, triple/0]). -define(MAX_TOPIC_LEN, 4096). diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index 6b75256d0..f5dfa93fe 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -58,7 +58,7 @@ mnesia(copy) -> %%------------------------------------------------------------------------------ %% @doc Insert a topic into the trie --spec(insert(Topic :: topic()) -> ok). +-spec(insert(emqx_topic:topic()) -> ok). insert(Topic) when is_binary(Topic) -> case mnesia:read(?TRIE_NODE, Topic) of [#trie_node{topic = Topic}] -> @@ -73,7 +73,7 @@ insert(Topic) when is_binary(Topic) -> end. %% @doc Find trie nodes that match the topic --spec(match(Topic :: topic()) -> list(MatchedTopic :: topic())). +-spec(match(emqx_topic:topic()) -> list(emqx_topic:topic())). match(Topic) when is_binary(Topic) -> TrieNodes = match_node(root, emqx_topic:words(Topic)), [Name || #trie_node{topic = Name} <- TrieNodes, Name =/= undefined]. @@ -84,7 +84,7 @@ lookup(NodeId) -> mnesia:read(?TRIE_NODE, NodeId). %% @doc Delete a topic from the trie --spec(delete(Topic :: topic()) -> ok). +-spec(delete(emqx_topic:topic()) -> ok). delete(Topic) when is_binary(Topic) -> case mnesia:read(?TRIE_NODE, Topic) of [#trie_node{edge_count = 0}] -> diff --git a/src/emqx_types.erl b/src/emqx_types.erl index 6fb0647c8..d31f37303 100644 --- a/src/emqx_types.erl +++ b/src/emqx_types.erl @@ -14,17 +14,29 @@ -module(emqx_types). -%%-include("emqx.hrl"). +-include("emqx.hrl"). +-export_type([zone/0]). -export_type([startlink_ret/0]). --export_type([zone/0, client_id/0, username/0, password/0, peername/0, - protocol/0, credentials/0]). --export_type([topic/0, payload/0, dispatches/0]). -%%-export_type([payload/0, message/0, delivery/0]). - --type(startlink_ret() :: {ok, pid()} | ignore | {error, term()}). +-export_type([pubsub/0, topic/0, subid/0, subopts/0]). +-export_type([client_id/0, username/0, password/0, peername/0, protocol/0]). +-export_type([credentials/0, session/0]). +-export_type([subscription/0, subscriber/0, topic_table/0]). +-export_type([payload/0, message/0]). +-export_type([delivery/0, deliver_results/0]). +-export_type([route/0]). +-export_type([alarm/0, plugin/0, command/0]). -type(zone() :: atom()). +-type(startlink_ret() :: {ok, pid()} | ignore | {error, term()}). +-type(pubsub() :: publish | subscribe). +-type(topic() :: binary()). +-type(subid() :: binary() | atom()). +-type(subopts() :: #{qos := integer(), + share => binary(), + atom() => term() + }). +-type(session() :: #session{}). -type(client_id() :: binary() | atom()). -type(username() :: binary() | undefined). -type(password() :: binary() | undefined). @@ -34,12 +46,18 @@ username := username(), peername := peername(), zone => zone(), - atom() => term()}). - --type(topic() :: binary()). + atom() => term() + }). +-type(subscription() :: #subscription{}). +-type(subscriber() :: {pid(), subid()}). +-type(topic_table() :: [{topic(), subopts()}]). -type(payload() :: binary() | iodata()). -%-type(message() :: #message{}). -%-type(delivery() :: #delivery{}). - --type(dispatches() :: [{route, node(), topic()} | {dispatch, topic(), pos_integer()}]). +-type(message() :: #message{}). +-type(delivery() :: #delivery{}). +-type(deliver_results() :: [{route, node(), topic()} | + {dispatch, topic(), pos_integer()}]). +-type(route() :: #route{}). +-type(alarm() :: #alarm{}). +-type(plugin() :: #plugin{}). +-type(command() :: #command{}). diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl index fdcfc37a5..209f0323c 100644 --- a/src/emqx_zone.erl +++ b/src/emqx_zone.erl @@ -33,13 +33,13 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). --spec(get_env(zone() | undefined, atom()) -> undefined | term()). +-spec(get_env(emqx_types:zone() | undefined, atom()) -> undefined | term()). get_env(undefined, Key) -> emqx_config:get_env(Key); get_env(Zone, Key) -> get_env(Zone, Key, undefined). --spec(get_env(zone() | undefined, atom(), term()) -> undefined | term()). +-spec(get_env(emqx_types:zone() | undefined, atom(), term()) -> undefined | term()). get_env(undefined, Key, Def) -> emqx_config:get_env(Key, Def); get_env(Zone, Key, Def) -> @@ -48,7 +48,7 @@ get_env(Zone, Key, Def) -> emqx_config:get_env(Key, Def) end. --spec(set_env(zone(), atom(), term()) -> ok). +-spec(set_env(emqx_types:zone(), atom(), term()) -> ok). set_env(Zone, Key, Val) -> gen_server:cast(?MODULE, {set_env, Zone, Key, Val}). From a282c7625ebdb3dd6c361e0847b334d29b508693 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 00:03:38 +0800 Subject: [PATCH 165/520] Add properties for delivered messages --- src/emqx_packet.erl | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 384d62444..4c2cd2652 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -81,21 +81,35 @@ validate_qos(QoS) when ?QOS0 =< QoS, QoS =< ?QOS2 -> true; validate_qos(_) -> error(bad_qos). -%% @doc From Message to Packet +%% @doc From message to packet -spec(from_message(emqx_mqtt_types:packet_id(), emqx_types:message()) -> emqx_mqtt_types:packet()). -from_message(PacketId, Msg = #message{qos = QoS, topic = Topic, payload = Payload}) -> - Dup = emqx_message:get_flag(dup, Msg, false), - Retain = emqx_message:get_flag(retain, Msg, false), +from_message(PacketId, #message{qos = QoS, flags = Flags, headers = Headers, + topic = Topic, payload = Payload}) -> + Flags1 = if Flags =:= undefined -> + #{}; + true -> Flags + end, + Dup = maps:get(dup, Flags1, false), + Retain = maps:get(retain, Flags1, false), Publish = #mqtt_packet_publish{topic_name = Topic, packet_id = PacketId, - %% TODO: Properties - properties = #{}}, + properties = publish_props(Headers)}, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, dup = Dup, qos = QoS, retain = Retain}, variable = Publish, payload = Payload}. +publish_props(Headers) -> + maps:filter(fun('Payload-Format-Indicator', _) -> true; + ('Response-Topic', _) -> true; + ('Correlation-Data', _) -> true; + ('User-Property', _) -> true; + ('Subscription-Identifier', _) -> true; + ('Content-Type', _) -> true; + (_Key, _Val) -> false + end , Headers). + %% @doc Message from Packet -spec(to_message(emqx_types:credentials(), emqx_mqtt_types:packet()) -> emqx_types:message()). From b1dadf444f2e488c225c864e2b145a251903ad79 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 00:22:43 +0800 Subject: [PATCH 166/520] Comment 'TEST_DEPS = emqx_ct_helplers' --- test/emqx_access_SUITE.erl | 22 ++++++++++------------ test/emqx_mock_client.erl | 9 +++------ test/emqx_sm_SUITE.erl | 12 +++++++----- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index 244030379..3a7aca390 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -139,18 +139,18 @@ unregister_mod(_) -> [] = ?AC:lookup_mods(auth). check_acl_1(_) -> - SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, + SelfUser = #{client_id => <<"client1">>, username => <<"testuser">>}, 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">>}, + SelfUser = #{client_id => <<"client2">>, username => <<"xyz">>}, deny = ?AC:check_acl(SelfUser, subscribe, <<"a/b/c">>). acl_cache_basic(_) -> - SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, + SelfUser = #{client_id => <<"client1">>, username => <<"testuser">>}, not_found = ?CACHE:get_acl_cache(subscribe, <<"users/testuser/1">>), not_found = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), @@ -163,8 +163,7 @@ acl_cache_basic(_) -> acl_cache_expiry(_) -> application:set_env(emqx, acl_cache_ttl, 100), - - SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, + SelfUser = #{client_id => <<"client1">>, username => <<"testuser">>}, allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), ct:sleep(150), @@ -174,7 +173,7 @@ acl_cache_expiry(_) -> acl_cache_full(_) -> application:set_env(emqx, acl_cache_max_size, 1), - SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, + SelfUser = #{client_id => <<"client1">>, username => <<"testuser">>}, allow = ?AC:check_acl(SelfUser, subscribe, <<"users/testuser/1">>), allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), @@ -189,7 +188,7 @@ acl_cache_cleanup(_) -> application:set_env(emqx, acl_cache_ttl, 100), application:set_env(emqx, acl_cache_max_size, 2), - SelfUser = #client{id = <<"client1">>, username = <<"testuser">>}, + SelfUser = #{client_id => <<"client1">>, username => <<"testuser">>}, allow = ?AC:check_acl(SelfUser, subscribe, <<"users/testuser/1">>), allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), @@ -334,7 +333,6 @@ cache_auto_cleanup(_) -> %%-------------------------------------------------------------------- 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/#", "#"]}), @@ -360,8 +358,8 @@ compile_rule(_) -> {deny, all} = compile({deny, all}). match_rule(_) -> - User = #client{peername = {{127,0,0,1}, 2948}, id = <<"testClient">>, username = <<"TestUser">>}, - User2 = #client{peername = {{192,168,0,10}, 3028}, id = <<"testClient">>, username = <<"TestUser">>}, + User = #{client_id => <<"testClient">>, username => <<"TestUser">>, peername => {{127,0,0,1}, 2948}}, + User2 = #{client_id => <<"testClient">>, username => <<"TestUser">>, peername => {{192,168,0,10}, 3028}}, {matched, allow} = match(User, <<"Test/Topic">>, {allow, all}), {matched, deny} = match(User, <<"Test/Topic">>, {deny, all}), @@ -371,8 +369,7 @@ match_rule(_) -> 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(#client{username = <<"user2">>}, <<"users/user2/abc/def">>, - compile({allow, all, subscribe, ["users/%u/#"]})), + {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/#", "#"]})), Rule = compile({allow, {'and', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, <<"Topic">>}), nomatch = match(User, <<"Topic">>, Rule), @@ -380,3 +377,4 @@ match_rule(_) -> {matched, allow} = match(User, <<"Topic">>, AndRule), OrRule = compile({allow, {'or', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, ["Topic"]}), {matched, allow} = match(User, <<"Topic">>, OrRule). + diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index 95cb38130..164b6d4ea 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -36,15 +36,12 @@ stop(CPid) -> gen_server:call(CPid, stop). init([ClientId]) -> - {ok, - #state{clean_start = true, - client_id = ClientId} - }. + {ok, #state{clean_start = true, client_id = ClientId}}. handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> Attrs = #{ zone => Zone, client_id => ClientId, - client_pid => ClientPid, + conn_pid => ClientPid, clean_start => true, username => undefined, conn_props => undefined @@ -52,7 +49,7 @@ handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> {ok, SessPid} = emqx_sm:open_session(Attrs), {reply, {ok, SessPid}, State#state{ clean_start = true, - client_id = ClientId, + client_id = ClientId, client_pid = ClientPid }}; diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl index 82bf4a460..5bc096cd8 100644 --- a/test/emqx_sm_SUITE.erl +++ b/test/emqx_sm_SUITE.erl @@ -14,22 +14,23 @@ -module(emqx_sm_SUITE). +-include("emqx.hrl"). + -compile(export_all). -compile(nowarn_export_all). --include("emqx.hrl"). - all() -> [t_open_close_session]. t_open_close_session(_) -> emqx_ct_broker_helpers:run_setup_steps(), {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), - Attrs = #{clean_start => true, client_id => <<"client">>, client_pid => ClientPid, zone => internal, username => <<"zhou">>, conn_props => ref}, + Attrs = #{clean_start => true, client_id => <<"client">>, conn_pid => ClientPid, + zone => internal, username => <<"zhou">>, conn_props => #{}}, {ok, _SPid} = emqx_sm:open_session(Attrs), [{<<"client">>, SPid}] = emqx_sm:lookup_session(<<"client">>), SPid = emqx_sm:lookup_session_pid(<<"client">>), - {ok, NewClientPid} = emqx_mock_client:start_link(<<"client">>), - {ok, SPid, true} = emqx_sm:open_session(Attrs#{clean_start => false, client_pid => NewClientPid}), + {ok, NewConnPid} = emqx_mock_client:start_link(<<"client">>), + {ok, SPid, true} = emqx_sm:open_session(Attrs#{clean_start => false, conn_pid => NewConnPid}), [{<<"client">>, SPid}] = emqx_sm:lookup_session(<<"client">>), SAttrs = emqx_sm:get_session_attrs({<<"client">>, SPid}), <<"client">> = proplists:get_value(client_id, SAttrs), @@ -38,3 +39,4 @@ t_open_close_session(_) -> {open, true} = emqx_sm:get_session_stats(Session), ok = emqx_sm:close_session(SPid), [] = emqx_sm:lookup_session(<<"client">>). + From 649f59d4dc31d055ed6ae59455f80993538931bc Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 00:22:47 +0800 Subject: [PATCH 167/520] Comment 'TEST_DEPS = emqx_ct_helplers' --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 478724e66..80a3dafbd 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,8 @@ ERLC_OPTS += +'{parse_transform, lager_transform}' BUILD_DEPS = cuttlefish dep_cuttlefish = git https://github.com/emqx/cuttlefish emqx30 -TEST_DEPS = emqx_ct_helplers -dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers +#TEST_DEPS = emqx_ct_helplers +#dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers TEST_ERLC_OPTS += +debug_info TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' From 98824a56c2bf24887fdfce17918ea145b43a5d69 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 00:28:11 +0800 Subject: [PATCH 168/520] Remove emqx_flow_control module --- src/emqx_flow_control.erl | 68 --------------------------------------- 1 file changed, 68 deletions(-) delete mode 100644 src/emqx_flow_control.erl diff --git a/src/emqx_flow_control.erl b/src/emqx_flow_control.erl deleted file mode 100644 index e5042e9a7..000000000 --- a/src/emqx_flow_control.erl +++ /dev/null @@ -1,68 +0,0 @@ -%% Copyright (c) 2018 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_flow_control). - --behaviour(gen_server). - -%% API --export([start_link/0]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --define(SERVER, ?MODULE). - --record(state, {}). - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -%% @doc Starts the server --spec(start_link() -> {ok, pid()} | ignore | {error, any()}). -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). - -%%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- - -init([]) -> - {ok, #state{}}. - -handle_call(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - - - - From 53a2f93b7eb813eb4225f406170910ccdcf75cd2 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 03:03:19 +0800 Subject: [PATCH 169/520] Add emqx_connection:attrs/1, emqx_protocol:attrs/1, emqx_session:attrs/1 APIs --- src/emqx_connection.erl | 103 ++++++++++------- src/emqx_flapping.erl | 2 +- src/emqx_gc.erl | 2 - src/emqx_mod_subscription.erl | 4 +- src/emqx_protocol.erl | 80 +++++++------- src/emqx_session.erl | 202 ++++++++++++++++++---------------- 6 files changed, 215 insertions(+), 178 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index c5299d638..16d35585e 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -20,7 +20,9 @@ -include("emqx_mqtt.hrl"). -export([start_link/3]). --export([info/1, stats/1, kick/1]). +-export([info/1, attrs/1]). +-export([stats/1]). +-export([kick/1]). -export([session/1]). %% gen_server callbacks @@ -49,7 +51,7 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). -define(LOG(Level, Format, Args, State), - emqx_logger:Level("Client(~s): " ++ Format, + emqx_logger:Level("MQTT(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). start_link(Transport, Socket, Options) -> @@ -59,17 +61,58 @@ start_link(Transport, Socket, Options) -> %% API %%------------------------------------------------------------------------------ -info(CPid) -> - call(CPid, info). +%% for debug +info(CPid) when is_pid(CPid) -> + call(CPid, info); -stats(CPid) -> - call(CPid, stats). +info(#state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, + conn_state = ConnState, + await_recv = AwaitRecv, + rate_limit = RateLimit, + publish_limit = PubLimit, + proto_state = ProtoState}) -> + ConnInfo = [{socktype, Transport:type(Socket)}, + {peername, Peername}, + {sockname, Sockname}, + {conn_state, ConnState}, + {await_recv, AwaitRecv}, + {rate_limit, esockd_rate_limit:info(RateLimit)}, + {publish_limit, esockd_rate_limit:info(PubLimit)}], + ProtoInfo = emqx_protocol:info(ProtoState), + lists:usort(lists:append(ConnInfo, ProtoInfo)). -kick(CPid) -> - call(CPid, kick). +%% for dashboard +attrs(CPid) when is_pid(CPid) -> + call(CPid, attrs); -session(CPid) -> - call(CPid, session). +attrs(#state{peername = Peername, + sockname = Sockname, + proto_state = ProtoState}) -> + SockAttrs = [{peername, Peername}, + {sockname, Sockname}], + ProtoAttrs = emqx_protocol:attrs(ProtoState), + lists:usort(lists:append(SockAttrs, ProtoAttrs)). + +%% Conn stats +stats(CPid) when is_pid(CPid) -> + call(CPid, stats); + +stats(#state{transport = Transport, + socket = Socket, + proto_state = ProtoState}) -> + lists:append([emqx_misc:proc_stats(), + emqx_protocol:stats(ProtoState), + case Transport:getstat(Socket, ?SOCK_STATS) of + {ok, Ss} -> Ss; + {error, _} -> [] + end]). + +kick(CPid) -> call(CPid, kick). + +session(CPid) -> call(CPid, session). call(CPid, Req) -> gen_server:call(CPid, Req, infinity). @@ -131,38 +174,17 @@ send_fun(Transport, Socket, Peername) -> end end. -handle_call(info, _From, State = #state{transport = Transport, - socket = Socket, - peername = Peername, - sockname = Sockname, - conn_state = ConnState, - await_recv = AwaitRecv, - rate_limit = RateLimit, - publish_limit = PubLimit, - proto_state = ProtoState}) -> - ConnInfo = [{socktype, Transport:type(Socket)}, - {peername, Peername}, - {sockname, Sockname}, - {conn_state, ConnState}, - {await_recv, AwaitRecv}, - {rate_limit, esockd_rate_limit:info(RateLimit)}, - {publish_limit, esockd_rate_limit:info(PubLimit)}], - ProtoInfo = emqx_protocol:info(ProtoState), - {reply, lists:usort(lists:append([ConnInfo, ProtoInfo])), State}; +handle_call(info, _From, State) -> + {reply, info(State), State}; -handle_call(stats, _From, State = #state{transport = Transport, - socket = Socket, - proto_state = ProtoState}) -> - ProcStats = emqx_misc:proc_stats(), - ProtoStats = emqx_protocol:stats(ProtoState), - SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of - {ok, Ss} -> Ss; - {error, _} -> [] - end, - {reply, lists:append([ProcStats, ProtoStats, SockStats]), State}; +handle_call(attrs, _From, State) -> + {reply, attrs(State), State}; + +handle_call(stats, _From, State) -> + {reply, stats(State), State}; handle_call(kick, _From, State) -> - {stop, {shutdown, kick}, ok, State}; + {stop, {shutdown, kicked}, ok, State}; handle_call(session, _From, State = #state{proto_state = ProtoState}) -> {reply, emqx_protocol:session(ProtoState), State}; @@ -186,8 +208,7 @@ handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> end; handle_info(emit_stats, State = #state{proto_state = ProtoState}) -> - Stats = element(2, handle_call(stats, undefined, State)), - emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), Stats), + emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info(timeout, State) -> diff --git a/src/emqx_flapping.erl b/src/emqx_flapping.erl index 56dac9110..c1cefd893 100644 --- a/src/emqx_flapping.erl +++ b/src/emqx_flapping.erl @@ -47,7 +47,7 @@ banned(ClientId) -> %%-------------------------------------------------------------------- init([]) -> - _ = ets:new(banned, [public, ordered_set, named_table]), + %% ets:new(banned, [public, ordered_set, named_table]), {ok, #state{}}. handle_call(_Request, _From, State) -> diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 6b1d43207..5a32b43c5 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -19,8 +19,6 @@ %% Memory: (10, 100, 1000) %% -%%-record - -export([conn_max_gc_count/0, reset_conn_gc_count/2, maybe_force_gc/2, maybe_force_gc/3]). diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index b0da175c6..48edac2c4 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -28,8 +28,8 @@ load(Topics) -> emqx_hooks:add('session.created', fun ?MODULE:on_session_created/3, [Topics]). -on_session_created(#{client_id := ClientId}, SessInfo, Topics) -> - Username = proplists:get_value(username, SessInfo), +on_session_created(#{client_id := ClientId}, SessAttrs, Topics) -> + Username = proplists:get_value(username, SessAttrs), Replace = fun(Topic) -> rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) end, diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 81d882007..01971258c 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -17,7 +17,11 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([init/2, info/1, caps/1, stats/1]). +-export([init/2]). +-export([info/1]). +-export([attrs/1]). +-export([caps/1]). +-export([stats/1]). -export([client_id/1]). -export([credentials/1]). -export([parser/1]). @@ -28,22 +32,6 @@ -export([send/2]). -export([shutdown/2]). - -%%-record(mqtt_client, { -%% client_id :: binary() | undefined, -%% client_pid :: pid(), -%% username :: binary() | undefined, -%% peername :: {inet:ip_address(), inet:port_number()}, -%% clean_start :: boolean(), -%% proto_ver :: emqx_mqtt_types:version(), -%% keepalive = 0 :: non_neg_integer(), -%% will_topic :: undefined | binary(), -%% mountpoint :: undefined | binary(), -%% connected_at :: erlang:timestamp(), -%% attributes :: map() -%% }). - - -record(pstate, { zone, sendfun, @@ -61,6 +49,7 @@ clean_start, topic_aliases, packet_size, + will_topic, will_msg, keepalive, mountpoint, @@ -81,7 +70,7 @@ -endif. -define(LOG(Level, Format, Args, PState), - emqx_logger:Level([{client, PState#pstate.client_id}], "Client(~s@~s): " ++ Format, + emqx_logger:Level([{client, PState#pstate.client_id}], "MQTT(~s@~s): " ++ Format, [PState#pstate.client_id, esockd_net:format(PState#pstate.peername) | Args])). %%------------------------------------------------------------------------------ @@ -127,33 +116,46 @@ set_username(_Username, PState) -> %% API %%------------------------------------------------------------------------------ -info(#pstate{zone = Zone, - client_id = ClientId, - username = Username, - peername = Peername, - proto_ver = ProtoVer, - proto_name = ProtoName, - clean_start = CleanStart, - conn_props = ConnProps, - keepalive = Keepalive, - mountpoint = Mountpoint, - is_super = IsSuper, - is_bridge = IsBridge, - connected = Connected, - connected_at = ConnectedAt}) -> +info(PState = #pstate{conn_props = ConnProps, + ack_props = AclProps, + session = Session, + topic_aliases = Aliases, + will_msg = WillMsg, + enable_acl = EnableAcl}) -> + attrs(PState) ++ [{conn_props, ConnProps}, + {ack_props, AclProps}, + {session, Session}, + {topic_aliases, Aliases}, + {will_msg, WillMsg}, + {enable_acl, EnableAcl}]. + +attrs(#pstate{zone = Zone, + client_id = ClientId, + username = Username, + peername = Peername, + peercert = Peercert, + clean_start = CleanStart, + proto_ver = ProtoVer, + proto_name = ProtoName, + keepalive = Keepalive, + will_topic = WillTopic, + mountpoint = Mountpoint, + is_super = IsSuper, + is_bridge = IsBridge, + connected_at = ConnectedAt}) -> [{zone, Zone}, {client_id, ClientId}, {username, Username}, {peername, Peername}, + {peercert, Peercert}, {proto_ver, ProtoVer}, {proto_name, ProtoName}, - {conn_props, ConnProps}, {clean_start, CleanStart}, {keepalive, Keepalive}, + {will_topic, WillTopic}, {mountpoint, Mountpoint}, {is_super, IsSuper}, {is_bridge, IsBridge}, - {connected, Connected}, {connected_at, ConnectedAt}]. caps(#pstate{zone = Zone}) -> @@ -254,6 +256,7 @@ process_packet(?CONNECT_PACKET( clean_start = CleanStart, keepalive = Keepalive, properties = ConnProps, + will_topic = WillTopic, client_id = ClientId, username = Username, password = Password} = Connect), PState) -> @@ -269,9 +272,9 @@ process_packet(?CONNECT_PACKET( clean_start = CleanStart, keepalive = Keepalive, conn_props = ConnProps, + will_topic = WillTopic, will_msg = WillMsg, is_bridge = IsBridge, - connected = true, connected_at = os:timestamp()}), connack( @@ -284,8 +287,8 @@ process_packet(?CONNECT_PACKET( %% Open session case try_open_session(PState3) of {ok, SPid, SP} -> - PState4 = PState3#pstate{session = SPid}, - ok = emqx_cm:register_connection(client_id(PState4), info(PState4)), + PState4 = PState3#pstate{session = SPid, connected = true}, + ok = emqx_cm:register_connection(client_id(PState4), attrs(PState4)), %% Start keepalive start_keepalive(Keepalive, PState4), %% Success @@ -521,7 +524,8 @@ try_open_session(#pstate{zone = Zone, username => Username, clean_start => CleanStart, conn_props => ConnProps}) of - {ok, SPid} -> {ok, SPid, false}; + {ok, SPid} -> + {ok, SPid, false}; Other -> Other end. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 840be8156..bbe78e02b 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -44,7 +44,8 @@ -include("emqx_mqtt.hrl"). -export([start_link/1]). --export([info/1, stats/1]). +-export([info/1, attrs/1]). +-export([stats/1]). -export([resume/2, discard/2]). -export([subscribe/2, subscribe/4]). -export([publish/3]). @@ -94,8 +95,8 @@ %% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked. inflight :: emqx_inflight:inflight(), - %% Max Inflight Size - max_inflight = 32 :: non_neg_integer(), + %% Max Inflight Size. DEPRECATED: Get from inflight + %% max_inflight = 32 :: non_neg_integer(), %% Retry interval for redelivering QoS1/2 messages retry_interval = 20000 :: timeout(), @@ -145,11 +146,6 @@ -define(TIMEOUT, 60000). --define(INFO_KEYS, [clean_start, client_id, username, binding, conn_pid, old_conn_pid, - next_pkt_id, max_subscriptions, subscriptions, upgrade_qos, inflight, - max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, - await_rel_timeout, expiry_interval, enable_stats, created_at]). - -define(LOG(Level, Format, Args, State), emqx_logger:Level([{client, State#state.client_id}], "Session(~s): " ++ Format, [State#state.client_id | Args])). @@ -160,6 +156,77 @@ start_link(SessAttrs) -> IdleTimeout = maps:get(idle_timeout, SessAttrs, 30000), gen_server:start_link(?MODULE, SessAttrs, [{hibernate_after, IdleTimeout}]). +%% @doc Get session info +-spec(info(pid() | #state{}) -> list({atom(), term()})). +info(SPid) when is_pid(SPid) -> + gen_server:call(SPid, info, infinity); + +info(State = #state{conn_pid = ConnPid, + next_pkt_id = PktId, + max_subscriptions = MaxSubscriptions, + subscriptions = Subscriptions, + upgrade_qos = UpgradeQoS, + inflight = Inflight, + retry_interval = RetryInterval, + mqueue = MQueue, + awaiting_rel = AwaitingRel, + max_awaiting_rel = MaxAwaitingRel, + await_rel_timeout = AwaitRelTimeout}) -> + attrs(State) ++ [{conn_pid, ConnPid}, + {next_pkt_id, PktId}, + {max_subscriptions, MaxSubscriptions}, + {subscriptions, Subscriptions}, + {upgrade_qos, UpgradeQoS}, + {inflight, Inflight}, + {retry_interval, RetryInterval}, + {mqueue_len, MQueue}, + {awaiting_rel, AwaitingRel}, + {max_awaiting_rel, MaxAwaitingRel}, + {await_rel_timeout, AwaitRelTimeout}]. + +%% @doc Get session attrs +-spec(attrs(pid() | #state{}) -> list({atom(), term()})). +attrs(SPid) when is_pid(SPid) -> + gen_server:call(SPid, attrs, infinity); + +attrs(#state{clean_start = CleanStart, + binding = Binding, + client_id = ClientId, + username = Username, + expiry_interval = ExpiryInterval, + created_at = CreatedAt}) -> + [{clean_start, CleanStart}, + {binding, Binding}, + {client_id, ClientId}, + {username, Username}, + {expiry_interval, ExpiryInterval div 1000}, + {created_at, CreatedAt}]. + +-spec(stats(pid() | #state{}) -> list({atom(), non_neg_integer()})). +stats(SPid) when is_pid(SPid) -> + gen_server:call(SPid, stats, infinity); + +stats(#state{max_subscriptions = MaxSubscriptions, + subscriptions = Subscriptions, + inflight = Inflight, + mqueue = MQueue, + max_awaiting_rel = MaxAwaitingRel, + awaiting_rel = AwaitingRel, + deliver_stats = DeliverMsg, + enqueue_stats = EnqueueMsg}) -> + lists:append(emqx_misc:proc_stats(), + [{max_subscriptions, MaxSubscriptions}, + {subscriptions_num, 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, MaxAwaitingRel}, + {awaiting_rel_len, maps:size(AwaitingRel)}, + {deliver_msg, DeliverMsg}, + {enqueue_msg, EnqueueMsg}]). + %%------------------------------------------------------------------------------ %% PubSub API %%------------------------------------------------------------------------------ @@ -229,70 +296,6 @@ unsubscribe(SPid, PacketId, Properties, TopicFilters) -> resume(SPid, ConnPid) -> gen_server:cast(SPid, {resume, ConnPid}). -%% @doc Get session info --spec(info(pid() | #state{}) -> list(tuple())). -info(SPid) when is_pid(SPid) -> - gen_server:call(SPid, info); - -info(#state{clean_start = CleanStart, - binding = Binding, - client_id = ClientId, - username = Username, - max_subscriptions = MaxSubscriptions, - subscriptions = Subscriptions, - upgrade_qos = UpgradeQoS, - inflight = Inflight, - max_inflight = MaxInflight, - retry_interval = RetryInterval, - mqueue = MQueue, - awaiting_rel = AwaitingRel, - max_awaiting_rel = MaxAwaitingRel, - await_rel_timeout = AwaitRelTimeout, - expiry_interval = ExpiryInterval, - created_at = CreatedAt}) -> - [{clean_start, CleanStart}, - {binding, Binding}, - {client_id, ClientId}, - {username, Username}, - {max_subscriptions, MaxSubscriptions}, - {subscriptions, maps:size(Subscriptions)}, - {upgrade_qos, UpgradeQoS}, - {inflight, emqx_inflight:size(Inflight)}, - {max_inflight, MaxInflight}, - {retry_interval, RetryInterval}, - {mqueue_len, emqx_mqueue:len(MQueue)}, - {awaiting_rel, maps:size(AwaitingRel)}, - {max_awaiting_rel, MaxAwaitingRel}, - {await_rel_timeout, AwaitRelTimeout}, - {expiry_interval, ExpiryInterval}, - {created_at, CreatedAt}]. - --spec(stats(pid() | #state{}) -> list({atom(), non_neg_integer()})). -stats(SPid) when is_pid(SPid) -> - gen_server:call(SPid, stats, infinity); - -stats(#state{max_subscriptions = MaxSubscriptions, - subscriptions = Subscriptions, - inflight = Inflight, - max_inflight = MaxInflight, - mqueue = MQueue, - max_awaiting_rel = MaxAwaitingRel, - awaiting_rel = AwaitingRel, - deliver_stats = DeliverMsg, - enqueue_stats = EnqueueMsg}) -> - lists:append(emqx_misc:proc_stats(), - [{max_subscriptions, MaxSubscriptions}, - {subscriptions, maps:size(Subscriptions)}, - {max_inflight, MaxInflight}, - {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, MaxAwaitingRel}, - {awaiting_rel_len, maps:size(AwaitingRel)}, - {deliver_msg, DeliverMsg}, - {enqueue_msg, EnqueueMsg}]). - %% @doc Discard the session -spec(discard(pid(), emqx_types:client_id()) -> ok). discard(SPid, ClientId) -> @@ -311,7 +314,7 @@ init(#{zone := Zone, username := Username, conn_pid := ConnPid, clean_start := CleanStart, - conn_props := _ConnProps}) -> + conn_props := ConnProps}) -> process_flag(trap_exit, true), true = link(ConnPid), MaxInflight = get_env(Zone, max_inflight), @@ -323,21 +326,26 @@ init(#{zone := Zone, subscriptions = #{}, max_subscriptions = get_env(Zone, max_subscriptions, 0), upgrade_qos = get_env(Zone, upgrade_qos, false), - max_inflight = MaxInflight, inflight = emqx_inflight:new(MaxInflight), mqueue = init_mqueue(Zone, ClientId), retry_interval = get_env(Zone, retry_interval, 0), awaiting_rel = #{}, await_rel_timeout = get_env(Zone, await_rel_timeout), max_awaiting_rel = get_env(Zone, max_awaiting_rel), - expiry_interval = get_env(Zone, session_expiry_interval), + expiry_interval = expire_interval(Zone, ConnProps), enable_stats = get_env(Zone, enable_stats, true), - deliver_stats = 0, - enqueue_stats = 0, + deliver_stats = 0, + enqueue_stats = 0, created_at = os:timestamp()}, - emqx_sm:register_session(ClientId, info(State)), + emqx_sm:register_session(ClientId, [{zone, Zone} | attrs(State)]), + emqx_sm:set_session_stats(ClientId, stats(State)), emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), - {ok, ensure_stats_timer(State), hibernate}. + {ok, State}. + +expire_interval(_Zone, #{'Session-Expiry-Interval' := I}) -> + I * 1000; +expire_interval(Zone, _ConnProps) -> %% Maybe v3.1.1 + get_env(Zone, session_expiry_interval, 0). init_mqueue(Zone, ClientId) -> emqx_mqueue:new(ClientId, #{type => simple, @@ -399,6 +407,9 @@ handle_call({pubrel, PacketId, _ReasonCode}, _From, handle_call(info, _From, State) -> reply(info(State), State); +handle_call(attrs, _From, State) -> + reply(attrs(State), State); + handle_call(stats, _From, State) -> reply(stats(State), State); @@ -501,7 +512,7 @@ handle_cast({resume, ConnPid}, State = #state{client_id = ClientId, %% Clean Session: true -> false? CleanStart andalso emqx_sm:set_session_attrs(ClientId, info(State1)), - emqx_hooks:run('session.resumed', [#{client_id => ClientId}, info(State)]), + 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)))}; @@ -541,20 +552,18 @@ handle_info({timeout, _Timer, expired}, State) -> ?LOG(info, "Expired, shutdown now.", [], State), shutdown(expired, State); -handle_info({'EXIT', ConnPid, _Reason}, State = #state{clean_start= true, conn_pid = ConnPid}) -> - {stop, normal, State}; +handle_info({'EXIT', ConnPid, Reason}, State = #state{clean_start = true, conn_pid = ConnPid}) -> + {stop, Reason, State}; -handle_info({'EXIT', ConnPid, Reason}, State = #state{clean_start = false, - conn_pid = ConnPid, - expiry_interval = Interval}) -> - ?LOG(info, "Connection ~p EXIT for ~p", [ConnPid, Reason], State), - ExpireTimer = emqx_misc:start_timer(Interval, expired), - State1 = State#state{conn_pid = undefined, expiry_timer = ExpireTimer}, - {noreply, State1}; +handle_info({'EXIT', ConnPid, Reason}, State = #state{expiry_interval = 0, conn_pid = ConnPid}) -> + {stop, Reason, State}; -handle_info({'EXIT', Pid, _Reason}, State = #state{old_conn_pid = Pid}) -> +handle_info({'EXIT', ConnPid, _Reason}, State = #state{clean_start = false, conn_pid = ConnPid}) -> + {noreply, ensure_expire_timer(State#state{conn_pid = undefined})}; + +handle_info({'EXIT', OldPid, _Reason}, State = #state{old_conn_pid = OldPid}) -> %% ignore - {noreply, State, hibernate}; + {noreply, State#state{old_conn_pid = undefined}, hibernate}; handle_info({'EXIT', Pid, Reason}, State = #state{conn_pid = ConnPid}) -> ?LOG(error, "unexpected EXIT: conn_pid=~p, exit_pid=~p, reason=~p", @@ -571,6 +580,7 @@ handle_info(Info, State) -> terminate(Reason, #state{client_id = ClientId}) -> emqx_hooks:run('session.terminated', [#{client_id => ClientId}, Reason]), + %%TODO: notify conn_pid to shutdown? emqx_sm:unregister_session(ClientId). code_change(_OldVsn, State, _Extra) -> @@ -819,6 +829,11 @@ ensure_await_rel_timer(State = #state{await_rel_timer = undefined, await_rel_tim ensure_await_rel_timer(State) -> State. +ensure_expire_timer(State = #state{expiry_interval = Interval}) when Interval > 0 -> + State#state{expiry_timer = emqx_misc:start_timer(Interval, expired)}; +ensure_expire_timer(State) -> + State. + %%------------------------------------------------------------------------------ %% Reset Dup @@ -837,8 +852,7 @@ next_pkt_id(State = #state{next_pkt_id = Id}) -> %%------------------------------------------------------------------------------ %% Ensure stats timer -ensure_stats_timer(State = #state{enable_stats = true, - stats_timer = undefined}) -> +ensure_stats_timer(State = #state{enable_stats = true, stats_timer = undefined}) -> State#state{stats_timer = erlang:send_after(30000, self(), emit_stats)}; ensure_stats_timer(State) -> State. @@ -857,5 +871,5 @@ reply(Reply, State) -> shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. -%%TODO: maybe_gc(State) -> State. +%% TODO: maybe_gc(State) -> State. From 0fbf813ebf7dc5337e3b0d5d690f3f6a906b0977 Mon Sep 17 00:00:00 2001 From: HuangDan Date: Tue, 28 Aug 2018 19:55:23 +0800 Subject: [PATCH 170/520] Add mqtt connect tests cases --- etc/emqx.conf | 2 +- test/emqx_client_SUITE.erl | 7 +++++-- test/emqx_ct_broker_helpers.erl | 35 +++++++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 4703f5083..31c5a11ed 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1066,7 +1066,7 @@ listener.ssl.external.certfile = {{ platform_etc_dir }}/certs/cert.pem ## Most of it was copied from Mozilla’s Server Side TLS article ## ## Value: Ciphers -## listener.ssl.external.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA +listener.ssl.external.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA ## SSL parameter renegotiation is a feature that allows a client and a server ## to renegotiate the parameters of the SSL connection on the fly. diff --git a/test/emqx_client_SUITE.erl b/test/emqx_client_SUITE.erl index 7b2d5aaae..458c21d68 100644 --- a/test/emqx_client_SUITE.erl +++ b/test/emqx_client_SUITE.erl @@ -23,9 +23,9 @@ -include_lib("eunit/include/eunit.hrl"). -all() -> []. +all() -> [{group, connect}]. -groups() -> []. +groups() -> [{connect, [start]}]. init_per_suite(Config) -> Config. @@ -39,3 +39,6 @@ init_per_group(_Group, Config) -> end_per_group(_Group, _Config) -> ok. +start(_Config) -> + {ok, ClientPid, _} = emqx_client:start_link(). + diff --git a/test/emqx_ct_broker_helpers.erl b/test/emqx_ct_broker_helpers.erl index a62297a49..038ac0dc6 100644 --- a/test/emqx_ct_broker_helpers.erl +++ b/test/emqx_ct_broker_helpers.erl @@ -29,6 +29,31 @@ {cacertfile, "certs/cacert.pem"}, {certfile, "certs/client-cert.pem"}]). +-define(CIPHERS, [{ciphers, + ["ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES256-SHA384", + "ECDHE-RSA-AES256-SHA384","ECDHE-ECDSA-DES-CBC3-SHA", + "ECDH-ECDSA-AES256-GCM-SHA384", + "ECDH-RSA-AES256-GCM-SHA384", + "ECDH-ECDSA-AES256-SHA384","ECDH-RSA-AES256-SHA384", + "DHE-DSS-AES256-GCM-SHA384","DHE-DSS-AES256-SHA256", + "AES256-GCM-SHA384","AES256-SHA256", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES128-SHA256", + "ECDHE-RSA-AES128-SHA256", + "ECDH-ECDSA-AES128-GCM-SHA256", + "ECDH-RSA-AES128-GCM-SHA256", + "ECDH-ECDSA-AES128-SHA256","ECDH-RSA-AES128-SHA256", + "DHE-DSS-AES128-GCM-SHA256","DHE-DSS-AES128-SHA256", + "AES128-GCM-SHA256","AES128-SHA256", + "ECDHE-ECDSA-AES256-SHA","ECDHE-RSA-AES256-SHA", + "DHE-DSS-AES256-SHA","ECDH-ECDSA-AES256-SHA", + "ECDH-RSA-AES256-SHA","AES256-SHA", + "ECDHE-ECDSA-AES128-SHA","ECDHE-RSA-AES128-SHA", + "DHE-DSS-AES128-SHA","ECDH-ECDSA-AES128-SHA", + "ECDH-RSA-AES128-SHA","AES128-SHA"]}]). run_setup_steps() -> NewConfig = generate_config(), @@ -71,7 +96,7 @@ change_opts(SslType) -> lists:foldl(fun({Protocol, Port, Opts} = Listener, Acc) -> case Protocol of ssl -> - SslOpts = proplists:get_value(sslopts, Opts), + SslOpts = proplists:get_value(ssl_options, Opts), Keyfile = local_path(["etc/certs", "key.pem"]), Certfile = local_path(["etc/certs", "cert.pem"]), TupleList1 = lists:keyreplace(keyfile, 1, SslOpts, {keyfile, Keyfile}), @@ -89,13 +114,15 @@ change_opts(SslType) -> (_) -> true end, TupleList2) end, - [{Protocol, Port, lists:keyreplace(sslopts, 1, Opts, {sslopts, TupleList3})} | Acc]; + [{Protocol, Port, lists:keyreplace(ssl_options, 1, Opts, {ssl_options, TupleList3})} | Acc]; _ -> [Listener | Acc] end end, [], Listeners), application:set_env(?APP, listeners, NewListeners). -client_ssl() -> - [{Key, local_path(["etc", File])} || {Key, File} <- ?MQTT_SSL_CLIENT]. +client_ssl_twoway() -> + [{Key, local_path(["etc", File])} || {Key, File} <- ?MQTT_SSL_CLIENT] ++ ?CIPHERS. +client_ssl() -> + ?CIPHERS ++ [{reuse_sessions, true}]. From 37c1570e94f34c96f319746ea6127135274b02bc Mon Sep 17 00:00:00 2001 From: HuangDan Date: Tue, 28 Aug 2018 20:05:56 +0800 Subject: [PATCH 171/520] Add mqtt connect tests cases --- test/emqx_SUITE.erl | 136 ++++++++++++++++++++++---------------------- 1 file changed, 69 insertions(+), 67 deletions(-) diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index 752c40f5c..ed5c04ad8 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -25,21 +25,34 @@ -include_lib("common_test/include/ct.hrl"). +-include("emqx_mqtt.hrl"). + +-record(ssl_socket, {tcp, ssl}). + +-type(socket() :: inet:socket() | #ssl_socket{}). + -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">>})). all() -> - [{group, connect}, - {group, cleanSession}]. + [{group, connect}%, + % {group, cleanSession} + ]. groups() -> [{connect, [non_parallel_tests], - [mqtt_connect, -% mqtt_connect_with_tcp, - mqtt_connect_with_ssl_oneway, - mqtt_connect_with_ssl_twoway%, - % mqtt_connect_with_ws + [ + % mqtt_connect, + % mqtt_connect_with_tcp, + % mqtt_connect_with_ssl_oneway, + % mqtt_connect_with_ssl_twoway%, + mqtt_connect_with_ws%, ]}, {cleanSession, [sequence], [cleanSession_validate] @@ -48,7 +61,6 @@ groups() -> init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), - % ct:log("Apps:~p", [Apps]), Config. end_per_suite(_Config) -> @@ -65,76 +77,65 @@ mqtt_connect(_) -> ?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} = gen_tcp:connect({127,0,0,1}, 1883, [binary, {packet, raw}, {active, false}]), - gen_tcp:send(Sock, Packet), + {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), - gen_tcp:close(Sock), + emqx_client_sock:close(Sock), Data. -%% mqtt_connect_with_tcp(_) -> -%% %% Issue #599 -%% %% Empty clientId and clean_session = false -%% {ok, Sock} = gen_tcp:connect({127,0,0,1}, 1883, [binary, {packet, raw}, {active, false}]), -%% Packet = raw_send_serialise(?CLIENT), -%% gen_tcp:send(Sock, Packet), -%% {ok, Data} = gen_tcp:recv(Sock, 0), -%% % {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(Data), -%% gen_tcp:close(Sock). +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_serialise(?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_ssl_oneway(_) -> - emqx:stop(), + emqx:shutdown(), emqx_ct_broker_helpers:change_opts(ssl_oneway), emqx:start(), - timer:sleep(5000), - {ok, SslOneWay} = emqttc:start_link([{host, "localhost"}, - {port, 8883}, - {logger, debug}, - {client_id, <<"ssloneway">>}, ssl]), - timer:sleep(100), - emqttc:subscribe(SslOneWay, <<"topic">>, qos1), - {ok, Pub} = emqttc:start_link([{host, "localhost"}, - {client_id, <<"pub">>}]), - emqttc:publish(Pub, <<"topic">>, <<"SSL oneWay test">>, [{qos, 1}]), - timer:sleep(100), - receive {publish, _Topic, RM} -> - ?assertEqual(<<"SSL oneWay test">>, RM) - after 1000 -> false - end, - timer:sleep(100), - emqttc:disconnect(SslOneWay), - emqttc:disconnect(Pub). + ClientSsl = emqx_ct_broker_helpers:client_ssl(), + {ok, #ssl_socket{tcp = Sock, ssl = SslSock}} + = emqx_client_sock:connect("127.0.0.1", 8883, [{ssl_opts, ClientSsl}], 3000), +%% Packet = raw_send_serialise(?CLIENT), +%% ssl:send(SslSock, Packet), +%% receive Data -> +%% ct:log("Data:~p~n", [Data]) +%% after 30000 -> +%% ok +%% end, + ssl:close(SslSock). mqtt_connect_with_ssl_twoway(_Config) -> - emqx:stop(), + emqx:shutdown(), emqx_ct_broker_helpers:change_opts(ssl_twoway), emqx:start(), - timer:sleep(3000), - ClientSSl = emqx_ct_broker_helpers:client_ssl(), - {ok, SslTwoWay} = emqttc:start_link([{host, "localhost"}, - {port, 8883}, - {client_id, <<"ssltwoway">>}, - {ssl, ClientSSl}]), - {ok, Sub} = emqttc:start_link([{host, "localhost"}, - {client_id, <<"sub">>}]), - emqttc:subscribe(Sub, <<"topic">>, qos1), - emqttc:publish(SslTwoWay, <<"topic">>, <<"ssl client pub message">>, [{qos, 1}]), - timer:sleep(10), - receive {publish, _Topic, RM} -> - ?assertEqual(<<"ssl client pub message">>, RM) - after 1000 -> false + ClientSsl = emqx_ct_broker_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_serialise(?CLIENT), + emqx_client_sock:setopts(Sock, [{active, once}]), + emqx_client_sock:send(Sock, Packet), + timer:sleep(500), + receive {ssl, _, Data}-> + {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(Data) + after 1000 -> + ok end, - emqttc:disconnect(SslTwoWay), - emqttc:disconnect(Sub). + 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), -%% Packet = raw_send_serialise(?CLIENT), -%% ok = rfc6455_client:send_binary(WS, Packet), -%% {binary, P} = rfc6455_client:recv(WS), -%% % {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(P), -%% {close, _} = rfc6455_client:close(WS), -%% ok. +mqtt_connect_with_ws(_Config) -> + WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()), + {ok, _} = rfc6455_client:open(WS), + Packet = raw_send_serialise(?CLIENT), + ok = rfc6455_client:send_binary(WS, Packet), + {binary, P} = rfc6455_client:recv(WS), + ct:log(":~p", [P]), + {close, _} = rfc6455_client:close(WS), + ok. cleanSession_validate(_) -> {ok, C1} = emqttc:start_link([{host, "localhost"}, @@ -163,8 +164,9 @@ cleanSession_validate(_) -> emqttc:disconnect(C11). raw_send_serialise(Packet) -> - emqttc_serialiser:serialise(Packet). + emqx_frame:serialize(Packet). raw_recv_pase(P) -> - emqttc_parser:parse(P, emqttc_parser:new()). + emqx_frame:parse(P, {none, #{max_packet_size => ?MAX_PACKET_SIZE, + version => ?MQTT_PROTO_V4} }). From 9b06589eab6c386b50392686eb9fe149c51a2e2f Mon Sep 17 00:00:00 2001 From: HuangDan Date: Wed, 29 Aug 2018 13:49:54 +0800 Subject: [PATCH 172/520] Add SUBSCRIBER/PUBLISH Packet test cases --- test/emqx_SUITE.erl | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index ed5c04ad8..952fc6cb9 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -40,6 +40,17 @@ username = <<"admin">>, clean_start = false, password = <<"public">>})). + +-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">>)). + all() -> [{group, connect}%, % {group, cleanSession} @@ -48,11 +59,11 @@ all() -> groups() -> [{connect, [non_parallel_tests], [ - % mqtt_connect, - % mqtt_connect_with_tcp, - % mqtt_connect_with_ssl_oneway, - % mqtt_connect_with_ssl_twoway%, - mqtt_connect_with_ws%, + mqtt_connect, + mqtt_connect_with_tcp, + mqtt_connect_with_ssl_oneway, + mqtt_connect_with_ssl_twoway, + mqtt_connect_with_ws ]}, {cleanSession, [sequence], [cleanSession_validate] @@ -130,10 +141,24 @@ mqtt_connect_with_ssl_twoway(_Config) -> 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_serialise(?CLIENT), ok = rfc6455_client:send_binary(WS, Packet), - {binary, P} = rfc6455_client:recv(WS), - ct:log(":~p", [P]), + {binary, CONACK} = rfc6455_client:recv(WS), + {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(CONACK), + + %% Sub Packet + SubPacket = raw_send_serialise(?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_serialise(?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. From 53d7d0a9d462b90fa6fdcf9426de59a95adb2860 Mon Sep 17 00:00:00 2001 From: HuangDan Date: Wed, 29 Aug 2018 16:24:01 +0800 Subject: [PATCH 173/520] Update the peer_cert_as_username conf desc --- etc/emqx.conf | 6 +++--- priv/emqx.schema | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 31c5a11ed..588725446 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -752,8 +752,8 @@ listener.tcp.external.access.1 = allow all ## Enable the option for X.509 certificate based authentication. ## EMQX will use the common name of certificate as MQTT username. ## -## Value: boolean -## listener.tcp.external.peer_cert_as_username = true +## Value: cn | dn +## listener.tcp.external.peer_cert_as_username = cn ## The TCP backlog defines the maximum length that the queue of pending ## connections can grow to. @@ -1096,7 +1096,7 @@ listener.ssl.external.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-G ## Use the CN field from the client certificate as a username. ## Notice that 'verify' should be set as 'verify_peer'. ## -## Value: boolean +## Value: cn | en ## listener.ssl.external.peer_cert_as_username = cn ## TCP backlog for the SSL connection. diff --git a/priv/emqx.schema b/priv/emqx.schema index a0d2bc0e2..1a2209dd8 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -857,8 +857,7 @@ end}. ]}. {mapping, "listener.tcp.$name.peer_cert_as_username", "emqx.listeners", [ - {default, false}, - {datatype, {enum, [true, false]}} + {datatype, {enum, [cn, dn]}} ]}. {mapping, "listener.tcp.$name.backlog", "emqx.listeners", [ From d3ed0853ef4cecf8eeabee3f8f87aac0bc05d696 Mon Sep 17 00:00:00 2001 From: turtled Date: Thu, 30 Aug 2018 10:41:04 +0800 Subject: [PATCH 174/520] Rename bridge module --- src/emqx_bridge.erl | 288 ++++++++++++------ src/emqx_bridge1.erl | 254 --------------- src/emqx_bridge1_sup.erl | 45 --- src/emqx_bridge_sup.erl | 31 +- src/emqx_local_bridge.erl | 162 ++++++++++ src/emqx_local_bridge_sup.erl | 26 ++ ..._sup.erl => emqx_local_bridge_sup_sup.erl} | 6 +- src/emqx_sup.erl | 7 +- 8 files changed, 410 insertions(+), 409 deletions(-) delete mode 100644 src/emqx_bridge1.erl delete mode 100644 src/emqx_bridge1_sup.erl create mode 100644 src/emqx_local_bridge.erl create mode 100644 src/emqx_local_bridge_sup.erl rename src/{emqx_bridge_sup_sup.erl => emqx_local_bridge_sup_sup.erl} (94%) diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index d4ebab041..d1763d31c 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -19,76 +19,77 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([start_link/5]). +-import(proplists, [get_value/2, get_value/3]). + +-export([start_link/2, start_bridge/1, stop_bridge/1, status/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --define(PING_DOWN_INTERVAL, 1000). +-record(state, {client_pid, options, reconnect_time, reconnect_count, + def_reconnect_count, type, mountpoint, queue, store_type, + max_pending_messages}). --record(state, {pool, id, - node, subtopic, - qos = ?QOS_0, - topic_suffix = <<>>, - topic_prefix = <<>>, - mqueue :: emqx_mqueue:mqueue(), - max_queue_len = 10000, - ping_down_interval = ?PING_DOWN_INTERVAL, - status = up}). +-record(mqtt_msg, {qos = ?QOS0, retain = false, dup = false, + packet_id, topic, props, payload}). --type(option() :: {qos, emqx_mqtt_types:qos()} | - {topic_suffix, binary()} | - {topic_prefix, binary()} | - {max_queue_len, pos_integer()} | - {ping_down_interval, pos_integer()}). +start_link(Name, Options) -> + gen_server:start_link({local, name(Name)}, ?MODULE, [Options], []). --export_type([option/0]). +start_bridge(Name) -> + gen_server:call(name(Name), start_bridge). -%% @doc Start a bridge --spec(start_link(term(), pos_integer(), atom(), binary(), [option()]) - -> {ok, pid()} | ignore | {error, term()}). -start_link(Pool, Id, Node, Topic, Options) -> - gen_server:start_link(?MODULE, [Pool, Id, Node, Topic, Options], [{hibernate_after, 5000}]). +stop_bridge(Name) -> + gen_server:call(name(Name), stop_bridge). + +status(Pid) -> + gen_server:call(Pid, status). %%------------------------------------------------------------------------------ %% gen_server callbacks %%------------------------------------------------------------------------------ -init([Pool, Id, Node, Topic, Options]) -> +init([Options]) -> process_flag(trap_exit, true), - true = gproc_pool:connect_worker(Pool, {Pool, Id}), - case net_kernel:connect_node(Node) of - true -> - true = erlang:monitor_node(Node, true), - Share = iolist_to_binary(["$bridge:", atom_to_list(Node), ":", Topic]), - emqx_broker:subscribe(Topic, self(), [{share, Share}, {qos, ?QOS_0}]), - State = parse_opts(Options, #state{node = Node, subtopic = Topic}), - %%TODO: queue.... - MQueue = emqx_mqueue:new(qname(Node, Topic), [{max_len, State#state.max_queue_len}]), - {ok, State#state{pool = Pool, id = Id, mqueue = MQueue}}; - false -> - {stop, {cannot_connect_node, Node}} - end. + case get_value(start_type, Options, manual) of + manual -> ok; + auto -> erlang:send_after(1000, self(), start) + end, + ReconnectCount = get_value(reconnect_count, Options, 10), + ReconnectTime = get_value(reconnect_time, Options, 30000), + MaxPendingMsg = get_value(max_pending_messages, Options, 10000), + Mountpoint = format_mountpoint(get_value(mountpoint, Options)), + StoreType = get_value(store_type, Options, memory), + Type = get_value(type, Options, in), + Queue = [], + {ok, #state{type = Type, + mountpoint = Mountpoint, + queue = Queue, + store_type = StoreType, + options = Options, + reconnect_count = ReconnectCount, + reconnect_time = ReconnectTime, + def_reconnect_count = ReconnectCount, + max_pending_messages = MaxPendingMsg}}. -parse_opts([], State) -> - State; -parse_opts([{qos, QoS} | Opts], State) -> - parse_opts(Opts, State#state{qos = QoS}); -parse_opts([{topic_suffix, Suffix} | Opts], State) -> - parse_opts(Opts, State#state{topic_suffix= Suffix}); -parse_opts([{topic_prefix, Prefix} | Opts], State) -> - parse_opts(Opts, State#state{topic_prefix = Prefix}); -parse_opts([{max_queue_len, Len} | Opts], State) -> - parse_opts(Opts, State#state{max_queue_len = Len}); -parse_opts([{ping_down_interval, Interval} | Opts], State) -> - parse_opts(Opts, State#state{ping_down_interval = Interval}); -parse_opts([_Opt | Opts], State) -> - parse_opts(Opts, State). +handle_call(start_bridge, _From, State = #state{client_pid = undefined}) -> + {noreply, NewState} = handle_info(start, State), + {reply, <<"start bridge successfully">>, NewState}; -qname(Node, Topic) when is_atom(Node) -> - qname(atom_to_list(Node), Topic); -qname(Node, Topic) -> - iolist_to_binary(["Bridge:", Node, ":", Topic]). +handle_call(start_bridge, _From, State) -> + {reply, <<"bridge already started">>, State}; + +handle_call(stop_bridge, _From, State = #state{client_pid = undefined}) -> + {reply, <<"bridge not started">>, State}; + +handle_call(stop_bridge, _From, State = #state{client_pid = Pid}) -> + emqx_client:disconnect(Pid), + {reply, <<"stop bridge successfully">>, State}; + +handle_call(status, _From, State = #state{client_pid = undefined}) -> + {reply, <<"Stopped">>, State}; +handle_call(status, _From, State = #state{client_pid = _Pid})-> + {reply, <<"Running">>, State}; handle_call(Req, _From, State) -> emqx_logger:error("[Bridge] unexpected call: ~p", [Req]), @@ -98,65 +99,156 @@ handle_cast(Msg, State) -> emqx_logger:error("[Bridge] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({dispatch, _Topic, Msg}, State = #state{mqueue = Q, status = down}) -> - %% TODO: how to drop??? - {noreply, State#state{mqueue = emqx_mqueue:in(Msg, Q)}}; - -handle_info({dispatch, _Topic, Msg}, State = #state{node = Node, status = up}) -> - ok = emqx_rpc:cast(Node, emqx_broker, publish, [transform(Msg, State)]), +handle_info(start, State = #state{reconnect_count = 0}) -> {noreply, State}; -handle_info({nodedown, Node}, State = #state{node = Node, ping_down_interval = Interval}) -> - emqx_logger:warning("[Bridge] node down: ~s", [Node]), - erlang:send_after(Interval, self(), ping_down_node), - {noreply, State#state{status = down}, hibernate}; - -handle_info({nodeup, Node}, State = #state{node = Node}) -> - %% TODO: Really fast?? - case emqx:is_running(Node) of - true -> emqx_logger:warning("[Bridge] Node up: ~s", [Node]), - {noreply, dequeue(State#state{status = up})}; - false -> self() ! {nodedown, Node}, - {noreply, State#state{status = down}} +%%---------------------------------------------------------------- +%% start in message bridge +%%---------------------------------------------------------------- +handle_info(start, State = #state{options = Options, + client_pid = undefined, + reconnect_time = ReconnectTime, + reconnect_count = ReconnectCount, + type = in}) -> + case emqx_client:start_link([{owner, self()}|options(Options)]) of + {ok, ClientPid, _} -> + Subs = get_value(subscriptions, Options, []), + [emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs], + {noreply, State#state{client_pid = ClientPid}}; + {error,_} -> + erlang:send_after(ReconnectTime, self(), start), + {noreply, State = #state{reconnect_count = ReconnectCount-1}} end; -handle_info(ping_down_node, State = #state{node = Node, ping_down_interval = Interval}) -> - Self = self(), - spawn_link(fun() -> - case net_kernel:connect_node(Node) of - true -> Self ! {nodeup, Node}; - false -> erlang:send_after(Interval, Self, ping_down_node) - end - end), +%%---------------------------------------------------------------- +%% start out message bridge +%%---------------------------------------------------------------- +handle_info(start, State = #state{options = Options, + client_pid = undefined, + reconnect_time = ReconnectTime, + reconnect_count = ReconnectCount, + type = out}) -> + case emqx_client:start_link([{owner, self()}|options(Options)]) of + {ok, ClientPid, _} -> + Subs = get_value(subscriptions, Options, []), + [emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs], + ForwardRules = string:tokens(get_value(forward_rule, Options, ""), ","), + [emqx_broker:subscribe(i2b(Topic)) || Topic <- ForwardRules, emqx_topic:validate({filter, i2b(Topic)})], + {noreply, State#state{client_pid = ClientPid}}; + {error,_} -> + erlang:send_after(ReconnectTime, self(), start), + {noreply, State = #state{reconnect_count = ReconnectCount-1}} + end; + +%%---------------------------------------------------------------- +%% received local node message +%%---------------------------------------------------------------- +handle_info({dispatch, _, #message{topic = Topic, payload = Payload, flags = #{retain := Retain}}}, + State = #state{client_pid = Pid, mountpoint = Mountpoint, queue = Queue, + store_type = StoreType, max_pending_messages = MaxPendingMsg}) -> + Msg = #mqtt_msg{qos = 1, + retain = Retain, + topic = mountpoint(Mountpoint, Topic), + payload = Payload}, + case emqx_client:publish(Pid, Msg) of + {ok, PkgId} -> + {noreply, State#state{queue = store(StoreType, {PkgId, Msg}, Queue, MaxPendingMsg)}}; + {error, Reason} -> + emqx_logger:error("Publish fail:~p", [Reason]), + {noreply, State} + end; + +%%---------------------------------------------------------------- +%% received remote node message +%%---------------------------------------------------------------- +handle_info({publish, #{qos := QoS, dup := Dup, retain := Retain, topic := Topic, + properties := Props, payload := Payload}}, State) -> + NewMsg0 = emqx_message:make(bridge, QoS, Topic, Payload), + NewMsg1 = emqx_message:set_headers(Props, emqx_message:set_flags(#{dup => Dup, retain=> Retain}, NewMsg0)), + emqx_broker:publish(NewMsg1), {noreply, State}; -handle_info({'EXIT', _Pid, normal}, State) -> - {noreply, State}; +%%---------------------------------------------------------------- +%% received remote puback message +%%---------------------------------------------------------------- +handle_info({puback, #{packet_id := PkgId}}, State = #state{queue = Queue, store_type = StoreType}) -> + % lists:keydelete(PkgId, 1, Queue) + {noreply, State#state{queue = delete(StoreType, PkgId, Queue)}}; + +handle_info({'EXIT', Pid, normal}, State = #state{client_pid = Pid}) -> + {noreply, State#state{client_pid = undefined}}; + +handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = Pid, + reconnect_time = ReconnectTime, + def_reconnect_count = DefReconnectCount}) -> + lager:warning("emqx bridge stop reason:~p", [Reason]), + erlang:send_after(ReconnectTime, self(), start), + {noreply, State#state{client_pid = undefined, reconnect_count = DefReconnectCount}}; handle_info(Info, State) -> emqx_logger:error("[Bridge] unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{pool = Pool, id = Id}) -> - gproc_pool:disconnect_worker(Pool, {Pool, Id}). +terminate(_Reason, #state{}) -> + ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -dequeue(State = #state{mqueue = MQ}) -> - case emqx_mqueue:out(MQ) of - {empty, MQ1} -> - State#state{mqueue = MQ1}; - {{value, Msg}, MQ1} -> - handle_info({dispatch, Msg#message.topic, Msg}, State), - dequeue(State#state{mqueue = MQ1}) +proto_ver(mqtt3) -> v3; +proto_ver(mqtt4) -> v4; +proto_ver(mqtt5) -> v5. +address(Address) -> + case string:tokens(Address, ":") of + [Host] -> {Host, 1883}; + [Host, Port] -> {Host, list_to_integer(Port)} end. +options(Options) -> + options(Options, []). +options([], Acc) -> + Acc; +options([{username, Username}| Options], Acc) -> + options(Options, [{username, Username}|Acc]); +options([{proto_ver, ProtoVer}| Options], Acc) -> + options(Options, [{proto_ver, proto_ver(ProtoVer)}|Acc]); +options([{password, Password}| Options], Acc) -> + options(Options, [{password, Password}|Acc]); +options([{keepalive, Keepalive}| Options], Acc) -> + options(Options, [{keepalive, Keepalive}|Acc]); +options([{client_id, ClientId}| Options], Acc) -> + options(Options, [{client_id, ClientId}|Acc]); +options([{clean_start, CleanStart}| Options], Acc) -> + options(Options, [{clean_start, CleanStart}|Acc]); +options([{address, Address}| Options], Acc) -> + {Host, Port} = address(Address), + options(Options, [{host, Host}, {port, Port}|Acc]); +options([_Option | Options], Acc) -> + options(Options, Acc). -transform(Msg = #message{topic = Topic}, #state{topic_prefix = Prefix, - topic_suffix = Suffix}) -> - Msg#message{topic = <>}. +name(Id) -> + list_to_atom(lists:concat([?MODULE, "_", Id])). +i2b(L) -> iolist_to_binary(L). + +mountpoint(undefined, Topic) -> + Topic; +mountpoint(Prefix, Topic) -> + <>. + +format_mountpoint(undefined) -> + undefined; +format_mountpoint(Prefix) -> + binary:replace(i2b(Prefix), <<"${node}">>, atom_to_binary(node(), utf8)). + +store(memory, Data, Queue, MaxPendingMsg) when length(Queue) =< MaxPendingMsg -> + [Data | Queue]; +store(memory, _Data, Queue, _MaxPendingMsg) -> + lager:error("Beyond max pending messages"), + Queue; +store(disk, Data, Queue, _MaxPendingMsg)-> + [Data | Queue]. + +delete(memory, PkgId, Queue) -> + lists:keydelete(PkgId, 1, Queue); +delete(disk, PkgId, Queue) -> + lists:keydelete(PkgId, 1, Queue). \ No newline at end of file diff --git a/src/emqx_bridge1.erl b/src/emqx_bridge1.erl deleted file mode 100644 index cfad74803..000000000 --- a/src/emqx_bridge1.erl +++ /dev/null @@ -1,254 +0,0 @@ -%% Copyright (c) 2018 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_bridge1). - --behaviour(gen_server). - --include("emqx.hrl"). --include("emqx_mqtt.hrl"). - --import(proplists, [get_value/2, get_value/3]). - --export([start_link/2, start_bridge/1, stop_bridge/1, status/1]). - --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, - code_change/3]). - --record(state, {client_pid, options, reconnect_time, reconnect_count, - def_reconnect_count, type, mountpoint, queue, store_type, - max_pending_messages}). - --record(mqtt_msg, {qos = ?QOS0, retain = false, dup = false, - packet_id, topic, props, payload}). - -start_link(Name, Options) -> - gen_server:start_link({local, name(Name)}, ?MODULE, [Options], []). - -start_bridge(Name) -> - gen_server:call(name(Name), start_bridge). - -stop_bridge(Name) -> - gen_server:call(name(Name), stop_bridge). - -status(Pid) -> - gen_server:call(Pid, status). - -%%------------------------------------------------------------------------------ -%% gen_server callbacks -%%------------------------------------------------------------------------------ - -init([Options]) -> - process_flag(trap_exit, true), - case get_value(start_type, Options, manual) of - manual -> ok; - auto -> erlang:send_after(1000, self(), start) - end, - ReconnectCount = get_value(reconnect_count, Options, 10), - ReconnectTime = get_value(reconnect_time, Options, 30000), - MaxPendingMsg = get_value(max_pending_messages, Options, 10000), - Mountpoint = format_mountpoint(get_value(mountpoint, Options)), - StoreType = get_value(store_type, Options, memory), - Type = get_value(type, Options, in), - Queue = [], - {ok, #state{type = Type, - mountpoint = Mountpoint, - queue = Queue, - store_type = StoreType, - options = Options, - reconnect_count = ReconnectCount, - reconnect_time = ReconnectTime, - def_reconnect_count = ReconnectCount, - max_pending_messages = MaxPendingMsg}}. - -handle_call(start_bridge, _From, State = #state{client_pid = undefined}) -> - {noreply, NewState} = handle_info(start, State), - {reply, <<"start bridge successfully">>, NewState}; - -handle_call(start_bridge, _From, State) -> - {reply, <<"bridge already started">>, State}; - -handle_call(stop_bridge, _From, State = #state{client_pid = undefined}) -> - {reply, <<"bridge not started">>, State}; - -handle_call(stop_bridge, _From, State = #state{client_pid = Pid}) -> - emqx_client:disconnect(Pid), - {reply, <<"stop bridge successfully">>, State}; - -handle_call(status, _From, State = #state{client_pid = undefined}) -> - {reply, <<"Stopped">>, State}; -handle_call(status, _From, State = #state{client_pid = _Pid})-> - {reply, <<"Running">>, State}; - -handle_call(Req, _From, State) -> - emqx_logger:error("[Bridge] unexpected call: ~p", [Req]), - {reply, ignored, State}. - -handle_cast(Msg, State) -> - emqx_logger:error("[Bridge] unexpected cast: ~p", [Msg]), - {noreply, State}. - -handle_info(start, State = #state{reconnect_count = 0}) -> - {noreply, State}; - -%%---------------------------------------------------------------- -%% start in message bridge -%%---------------------------------------------------------------- -handle_info(start, State = #state{options = Options, - client_pid = undefined, - reconnect_time = ReconnectTime, - reconnect_count = ReconnectCount, - type = in}) -> - case emqx_client:start_link([{owner, self()}|options(Options)]) of - {ok, ClientPid, _} -> - Subs = get_value(subscriptions, Options, []), - [emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs], - {noreply, State#state{client_pid = ClientPid}}; - {error,_} -> - erlang:send_after(ReconnectTime, self(), start), - {noreply, State = #state{reconnect_count = ReconnectCount-1}} - end; - -%%---------------------------------------------------------------- -%% start out message bridge -%%---------------------------------------------------------------- -handle_info(start, State = #state{options = Options, - client_pid = undefined, - reconnect_time = ReconnectTime, - reconnect_count = ReconnectCount, - type = out}) -> - case emqx_client:start_link([{owner, self()}|options(Options)]) of - {ok, ClientPid, _} -> - Subs = get_value(subscriptions, Options, []), - [emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs], - ForwardRules = string:tokens(get_value(forward_rule, Options, ""), ","), - [emqx_broker:subscribe(i2b(Topic)) || Topic <- ForwardRules, emqx_topic:validate({filter, i2b(Topic)})], - {noreply, State#state{client_pid = ClientPid}}; - {error,_} -> - erlang:send_after(ReconnectTime, self(), start), - {noreply, State = #state{reconnect_count = ReconnectCount-1}} - end; - -%%---------------------------------------------------------------- -%% received local node message -%%---------------------------------------------------------------- -handle_info({dispatch, _, #message{topic = Topic, payload = Payload, flags = #{retain := Retain}}}, - State = #state{client_pid = Pid, mountpoint = Mountpoint, queue = Queue, - store_type = StoreType, max_pending_messages = MaxPendingMsg}) -> - Msg = #mqtt_msg{qos = 1, - retain = Retain, - topic = mountpoint(Mountpoint, Topic), - payload = Payload}, - case emqx_client:publish(Pid, Msg) of - {ok, PkgId} -> - {noreply, State#state{queue = store(StoreType, {PkgId, Msg}, Queue, MaxPendingMsg)}}; - {error, Reason} -> - emqx_logger:error("Publish fail:~p", [Reason]), - {noreply, State} - end; - -%%---------------------------------------------------------------- -%% received remote node message -%%---------------------------------------------------------------- -handle_info({publish, #{qos := QoS, dup := Dup, retain := Retain, topic := Topic, - properties := Props, payload := Payload}}, State) -> - NewMsg0 = emqx_message:make(bridge, QoS, Topic, Payload), - NewMsg1 = emqx_message:set_headers(Props, emqx_message:set_flags(#{dup => Dup, retain=> Retain}, NewMsg0)), - emqx_broker:publish(NewMsg1), - {noreply, State}; - -%%---------------------------------------------------------------- -%% received remote puback message -%%---------------------------------------------------------------- -handle_info({puback, #{packet_id := PkgId}}, State = #state{queue = Queue, store_type = StoreType}) -> - % lists:keydelete(PkgId, 1, Queue) - {noreply, State#state{queue = delete(StoreType, PkgId, Queue)}}; - -handle_info({'EXIT', Pid, normal}, State = #state{client_pid = Pid}) -> - {noreply, State#state{client_pid = undefined}}; - -handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = Pid, - reconnect_time = ReconnectTime, - def_reconnect_count = DefReconnectCount}) -> - lager:warning("emqx bridge stop reason:~p", [Reason]), - erlang:send_after(ReconnectTime, self(), start), - {noreply, State#state{client_pid = undefined, reconnect_count = DefReconnectCount}}; - -handle_info(Info, State) -> - emqx_logger:error("[Bridge] unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, #state{}) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -proto_ver(mqtt3) -> v3; -proto_ver(mqtt4) -> v4; -proto_ver(mqtt5) -> v5. -address(Address) -> - case string:tokens(Address, ":") of - [Host] -> {Host, 1883}; - [Host, Port] -> {Host, list_to_integer(Port)} - end. -options(Options) -> - options(Options, []). -options([], Acc) -> - Acc; -options([{username, Username}| Options], Acc) -> - options(Options, [{username, Username}|Acc]); -options([{proto_ver, ProtoVer}| Options], Acc) -> - options(Options, [{proto_ver, proto_ver(ProtoVer)}|Acc]); -options([{password, Password}| Options], Acc) -> - options(Options, [{password, Password}|Acc]); -options([{keepalive, Keepalive}| Options], Acc) -> - options(Options, [{keepalive, Keepalive}|Acc]); -options([{client_id, ClientId}| Options], Acc) -> - options(Options, [{client_id, ClientId}|Acc]); -options([{clean_start, CleanStart}| Options], Acc) -> - options(Options, [{clean_start, CleanStart}|Acc]); -options([{address, Address}| Options], Acc) -> - {Host, Port} = address(Address), - options(Options, [{host, Host}, {port, Port}|Acc]); -options([_Option | Options], Acc) -> - options(Options, Acc). - -name(Id) -> - list_to_atom(lists:concat([?MODULE, "_", Id])). - -i2b(L) -> iolist_to_binary(L). - -mountpoint(undefined, Topic) -> - Topic; -mountpoint(Prefix, Topic) -> - <>. - -format_mountpoint(undefined) -> - undefined; -format_mountpoint(Prefix) -> - binary:replace(i2b(Prefix), <<"${node}">>, atom_to_binary(node(), utf8)). - -store(memory, Data, Queue, MaxPendingMsg) when length(Queue) =< MaxPendingMsg -> - [Data | Queue]; -store(memory, _Data, Queue, _MaxPendingMsg) -> - lager:error("Beyond max pending messages"), - Queue; -store(disk, Data, Queue, _MaxPendingMsg)-> - [Data | Queue]. - -delete(memory, PkgId, Queue) -> - lists:keydelete(PkgId, 1, Queue); -delete(disk, PkgId, Queue) -> - lists:keydelete(PkgId, 1, Queue). \ No newline at end of file diff --git a/src/emqx_bridge1_sup.erl b/src/emqx_bridge1_sup.erl deleted file mode 100644 index f4e8c8f01..000000000 --- a/src/emqx_bridge1_sup.erl +++ /dev/null @@ -1,45 +0,0 @@ -%% Copyright (c) 2018 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_bridge1_sup). - --behavior(supervisor). - --include("emqx.hrl"). - --export([start_link/0, bridges/0]). - -%% Supervisor callbacks --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -%% @doc List all bridges --spec(bridges() -> [{node(), emqx_topic:topic(), pid()}]). -bridges() -> - [{Name, emqx_bridge1:status(Pid)} || {Name, Pid, _, _} <- supervisor:which_children(?MODULE)]. - -init([]) -> - BridgesOpts = emqx_config:get_env(bridges, []), - Bridges = [spec(Opts)|| Opts <- BridgesOpts], - {ok, {{one_for_one, 10, 100}, Bridges}}. - -spec({Id, Options})-> - #{id => Id, - start => {emqx_bridge1, start_link, [Id, Options]}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_bridge1]}. diff --git a/src/emqx_bridge_sup.erl b/src/emqx_bridge_sup.erl index 1735e3b99..0d8a0d887 100644 --- a/src/emqx_bridge_sup.erl +++ b/src/emqx_bridge_sup.erl @@ -14,13 +14,32 @@ -module(emqx_bridge_sup). +-behavior(supervisor). + -include("emqx.hrl"). --export([start_link/3]). +-export([start_link/0, bridges/0]). --spec(start_link(node(), emqx_topic:topic(), [emqx_bridge:option()]) - -> {ok, pid()} | {error, term()}). -start_link(Node, Topic, Options) -> - MFA = {emqx_bridge, start_link, [Node, Topic, Options]}, - emqx_pool_sup:start_link({bridge, Node, Topic}, random, MFA). +%% Supervisor callbacks +-export([init/1]). +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% @doc List all bridges +-spec(bridges() -> [{node(), emqx_topic:topic(), pid()}]). +bridges() -> + [{Name, emqx_bridge:status(Pid)} || {Name, Pid, _, _} <- supervisor:which_children(?MODULE)]. + +init([]) -> + BridgesOpts = emqx_config:get_env(bridges, []), + Bridges = [spec(Opts)|| Opts <- BridgesOpts], + {ok, {{one_for_one, 10, 100}, Bridges}}. + +spec({Id, Options})-> + #{id => Id, + start => {emqx_bridge, start_link, [Id, Options]}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_bridge]}. diff --git a/src/emqx_local_bridge.erl b/src/emqx_local_bridge.erl new file mode 100644 index 000000000..9ed8fdbac --- /dev/null +++ b/src/emqx_local_bridge.erl @@ -0,0 +1,162 @@ +%% Copyright (c) 2018 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_local_bridge). + +-behaviour(gen_server). + +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). + +-export([start_link/5]). + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). + +-define(PING_DOWN_INTERVAL, 1000). + +-record(state, {pool, id, + node, subtopic, + qos = ?QOS_0, + topic_suffix = <<>>, + topic_prefix = <<>>, + mqueue :: emqx_mqueue:mqueue(), + max_queue_len = 10000, + ping_down_interval = ?PING_DOWN_INTERVAL, + status = up}). + +-type(option() :: {qos, emqx_mqtt_types:qos()} | + {topic_suffix, binary()} | + {topic_prefix, binary()} | + {max_queue_len, pos_integer()} | + {ping_down_interval, pos_integer()}). + +-export_type([option/0]). + +%% @doc Start a bridge +-spec(start_link(term(), pos_integer(), atom(), binary(), [option()]) + -> {ok, pid()} | ignore | {error, term()}). +start_link(Pool, Id, Node, Topic, Options) -> + gen_server:start_link(?MODULE, [Pool, Id, Node, Topic, Options], [{hibernate_after, 5000}]). + +%%------------------------------------------------------------------------------ +%% gen_server callbacks +%%------------------------------------------------------------------------------ + +init([Pool, Id, Node, Topic, Options]) -> + process_flag(trap_exit, true), + true = gproc_pool:connect_worker(Pool, {Pool, Id}), + case net_kernel:connect_node(Node) of + true -> + true = erlang:monitor_node(Node, true), + Share = iolist_to_binary(["$bridge:", atom_to_list(Node), ":", Topic]), + emqx_broker:subscribe(Topic, self(), [{share, Share}, {qos, ?QOS_0}]), + State = parse_opts(Options, #state{node = Node, subtopic = Topic}), + %%TODO: queue.... + MQueue = emqx_mqueue:new(qname(Node, Topic), [{max_len, State#state.max_queue_len}]), + {ok, State#state{pool = Pool, id = Id, mqueue = MQueue}}; + false -> + {stop, {cannot_connect_node, Node}} + end. + +parse_opts([], State) -> + State; +parse_opts([{qos, QoS} | Opts], State) -> + parse_opts(Opts, State#state{qos = QoS}); +parse_opts([{topic_suffix, Suffix} | Opts], State) -> + parse_opts(Opts, State#state{topic_suffix= Suffix}); +parse_opts([{topic_prefix, Prefix} | Opts], State) -> + parse_opts(Opts, State#state{topic_prefix = Prefix}); +parse_opts([{max_queue_len, Len} | Opts], State) -> + parse_opts(Opts, State#state{max_queue_len = Len}); +parse_opts([{ping_down_interval, Interval} | Opts], State) -> + parse_opts(Opts, State#state{ping_down_interval = Interval}); +parse_opts([_Opt | Opts], State) -> + parse_opts(Opts, State). + +qname(Node, Topic) when is_atom(Node) -> + qname(atom_to_list(Node), Topic); +qname(Node, Topic) -> + iolist_to_binary(["Bridge:", Node, ":", Topic]). + +handle_call(Req, _From, State) -> + emqx_logger:error("[Bridge] unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Msg, State) -> + emqx_logger:error("[Bridge] unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info({dispatch, _Topic, Msg}, State = #state{mqueue = Q, status = down}) -> + %% TODO: how to drop??? + {noreply, State#state{mqueue = emqx_mqueue:in(Msg, Q)}}; + +handle_info({dispatch, _Topic, Msg}, State = #state{node = Node, status = up}) -> + ok = emqx_rpc:cast(Node, emqx_broker, publish, [transform(Msg, State)]), + {noreply, State}; + +handle_info({nodedown, Node}, State = #state{node = Node, ping_down_interval = Interval}) -> + emqx_logger:warning("[Bridge] node down: ~s", [Node]), + erlang:send_after(Interval, self(), ping_down_node), + {noreply, State#state{status = down}, hibernate}; + +handle_info({nodeup, Node}, State = #state{node = Node}) -> + %% TODO: Really fast?? + case emqx:is_running(Node) of + true -> emqx_logger:warning("[Bridge] Node up: ~s", [Node]), + {noreply, dequeue(State#state{status = up})}; + false -> self() ! {nodedown, Node}, + {noreply, State#state{status = down}} + end; + +handle_info(ping_down_node, State = #state{node = Node, ping_down_interval = Interval}) -> + Self = self(), + spawn_link(fun() -> + case net_kernel:connect_node(Node) of + true -> Self ! {nodeup, Node}; + false -> erlang:send_after(Interval, Self, ping_down_node) + end + end), + {noreply, State}; + +handle_info({'EXIT', _Pid, normal}, State) -> + {noreply, State}; + +handle_info(Info, State) -> + emqx_logger:error("[Bridge] unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #state{pool = Pool, id = Id}) -> + gproc_pool:disconnect_worker(Pool, {Pool, Id}). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +dequeue(State = #state{mqueue = MQ}) -> + case emqx_mqueue:out(MQ) of + {empty, MQ1} -> + State#state{mqueue = MQ1}; + {{value, Msg}, MQ1} -> + handle_info({dispatch, Msg#message.topic, Msg}, State), + dequeue(State#state{mqueue = MQ1}) + end. + +transform(Msg = #message{topic = Topic}, #state{topic_prefix = Prefix, + topic_suffix = Suffix}) -> + Msg#message{topic = <>}. + diff --git a/src/emqx_local_bridge_sup.erl b/src/emqx_local_bridge_sup.erl new file mode 100644 index 000000000..fed9f28a7 --- /dev/null +++ b/src/emqx_local_bridge_sup.erl @@ -0,0 +1,26 @@ +%% Copyright (c) 2018 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_local_bridge_sup). + +-include("emqx.hrl"). + +-export([start_link/3]). + +-spec(start_link(node(), emqx_topic:topic(), [emqx_local_bridge:option()]) + -> {ok, pid()} | {error, term()}). +start_link(Node, Topic, Options) -> + MFA = {emqx_local_bridge, start_link, [Node, Topic, Options]}, + emqx_pool_sup:start_link({bridge, Node, Topic}, random, MFA). + diff --git a/src/emqx_bridge_sup_sup.erl b/src/emqx_local_bridge_sup_sup.erl similarity index 94% rename from src/emqx_bridge_sup_sup.erl rename to src/emqx_local_bridge_sup_sup.erl index 2ef05df8c..0483552b2 100644 --- a/src/emqx_bridge_sup_sup.erl +++ b/src/emqx_local_bridge_sup_sup.erl @@ -12,7 +12,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. --module(emqx_bridge_sup_sup). +-module(emqx_local_bridge_sup_sup). -behavior(supervisor). @@ -66,9 +66,9 @@ init([]) -> bridge_spec(Node, Topic, Options) -> #{id => ?CHILD_ID(Node, Topic), - start => {emqx_bridge_sup, start_link, [Node, Topic, Options]}, + start => {emqx_local_bridge_sup, start_link, [Node, Topic, Options]}, restart => permanent, shutdown => infinity, type => supervisor, - modules => [emqx_bridge_sup]}. + modules => [emqx_local_bridge_sup]}. diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index cddfea8b5..2f29dbfee 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -62,8 +62,9 @@ init([]) -> %% Broker Sup BrokerSup = supervisor_spec(emqx_broker_sup), %% BridgeSup - BridgeSup = supervisor_spec(emqx_bridge_sup_sup), - BridgeSup1 = supervisor_spec(emqx_bridge1_sup), + LocalBridgeSup = supervisor_spec(emqx_local_bridge_sup_sup), + + BridgeSup = supervisor_spec(emqx_bridge_sup), %% AccessControl AccessControl = worker_spec(emqx_access_control), %% Session Manager @@ -78,8 +79,8 @@ init([]) -> [KernelSup, RouterSup, BrokerSup, + LocalBridgeSup, BridgeSup, - BridgeSup1, AccessControl, SMSup, SessionSup, From 2351b41f119835d7e9f4d1e07a47375a48367a50 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 11:40:53 +0800 Subject: [PATCH 175/520] Add is_expired/1, check_expiry/1, check_expiry/2 --- src/emqx_message.erl | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 8b49d44d6..d700c2aee 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -22,6 +22,7 @@ -export([get_flag/2, get_flag/3, set_flag/2, set_flag/3, unset_flag/2]). -export([set_headers/2]). -export([get_header/2, get_header/3, set_header/3]). +-export([is_expired/1, check_expiry/1, check_expiry/2]). -export([format/1]). -type(flag() :: atom()). @@ -93,6 +94,31 @@ set_header(Hdr, Val, Msg = #message{headers = undefined}) -> set_header(Hdr, Val, Msg = #message{headers = Headers}) -> Msg#message{headers = maps:put(Hdr, Val, Headers)}. +-spec(is_expired(emqx_types:message()) -> boolean()). +is_expired(#message{headers = #{'Message-Expiry-Interval' := Interval}, timestamp = CreatedAt}) -> + elapsed(CreatedAt) > Interval; +is_expired(_Msg) -> + false. + +-spec(check_expiry(emqx_types:message()) -> {ok, pos_integer()} | expired | false). +check_expiry(Msg = #message{timestamp = CreatedAt}) -> + check_expiry(Msg, CreatedAt); +check_expiry(_Msg) -> + false. + +-spec(check_expiry(emqx_types:message(), erlang:timestamp()) -> {ok, pos_integer()} | expired | false). +check_expiry(#message{headers = #{'Message-Expiry-Interval' := Interval}}, Since) -> + case Interval - elapsed(Since) of + I when I > 0 -> {ok, I}; + _ -> expired + end; +check_expiry(_Msg, _Since) -> + false. + +elapsed(Since) -> + Secs = timer:now_diff(os:timestamp(), Since) div 1000, + if Secs < 0 -> 0; true -> Secs end. + format(#message{id = Id, qos = QoS, topic = Topic, from = From, flags = Flags, headers = Headers}) -> io_lib:format("Message(Id=~s, QoS=~w, Topic=~s, From=~s, Flags=~s, Headers=~s)", [Id, QoS, Topic, From, format(flags, Flags), format(headers, Headers)]). From 2db64cf53cfaf1cfab08717d48fdaefe1bac29cc Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 11:42:24 +0800 Subject: [PATCH 176/520] Support 'Message-Expiry-Interval property --- src/emqx_packet.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 4c2cd2652..dfc8359d2 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -107,6 +107,7 @@ publish_props(Headers) -> ('User-Property', _) -> true; ('Subscription-Identifier', _) -> true; ('Content-Type', _) -> true; + ('Message-Expiry-Interval', _) -> true; (_Key, _Val) -> false end , Headers). From 2342a7db6d8f6027061d5421e71968685d5cb607 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 11:48:49 +0800 Subject: [PATCH 177/520] Rename 'Pid' to 'ConnPid' --- src/emqx_session.erl | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index bbe78e02b..ab9096f23 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -216,7 +216,7 @@ stats(#state{max_subscriptions = MaxSubscriptions, enqueue_stats = EnqueueMsg}) -> lists:append(emqx_misc:proc_stats(), [{max_subscriptions, MaxSubscriptions}, - {subscriptions_num, maps:size(Subscriptions)}, + {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)}, @@ -620,8 +620,7 @@ kick(ClientId, OldPid, Pid) -> %% Redeliver at once if force is true retry_delivery(Force, State = #state{inflight = Inflight}) -> case emqx_inflight:is_empty(Inflight) of - true -> - State; + true -> State; false -> Msgs = lists:sort(sortfun(inflight), emqx_inflight:values(Inflight)), retry_delivery(Force, Msgs, os:timestamp(), State) @@ -752,10 +751,10 @@ redeliver({PacketId, Msg = #message{qos = QoS}}, State) -> redeliver({pubrel, PacketId}, #state{conn_pid = ConnPid}) -> ConnPid ! {deliver, {pubrel, PacketId}}. -deliver(PacketId, Msg, #state{conn_pid = Pid, binding = local}) -> - Pid ! {deliver, {publish, PacketId, Msg}}; -deliver(PacketId, Msg, #state{conn_pid = Pid, binding = remote}) -> - emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, PacketId, Msg}]). +deliver(PacketId, Msg, #state{conn_pid = ConnPid, binding = local}) -> + ConnPid ! {deliver, {publish, PacketId, Msg}}; +deliver(PacketId, Msg, #state{conn_pid = ConnPid, binding = remote}) -> + emqx_rpc:cast(node(ConnPid), erlang, send, [ConnPid, {deliver, PacketId, Msg}]). %%------------------------------------------------------------------------------ %% Awaiting ACK for QoS1/QoS2 Messages From e97c3a5c8c7dcd74d1b13749862c758f7c348852 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 30 Aug 2018 13:44:09 +0800 Subject: [PATCH 178/520] update frame suite for latest emqx_frame --- test/emqx_frame_SUITE.erl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/emqx_frame_SUITE.erl b/test/emqx_frame_SUITE.erl index 60ed52e46..19bbb1e8c 100644 --- a/test/emqx_frame_SUITE.erl +++ b/test/emqx_frame_SUITE.erl @@ -330,15 +330,16 @@ 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, subid => 0 , qos => 2 }, + rh => 0, qos => 2 }, TopicFilters = [{<<"TopicA">>, TopicOpts}], Packet = ?SUBSCRIBE_PACKET(2, TopicFilters), ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), + ct:log("Bin: ~p, Packet: ~p ~n", [Packet, parse(Bin)]), ?assertEqual({ok, Packet, <<>>}, parse(Bin)). serialize_parse_subscribe_v5(_) -> - TopicFilters = [{<<"TopicQos0">>, #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0, subid => 0}}, - {<<"TopicQos1">>, #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0, subid => 0}}], + 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({ok, Packet, <<>>}, From db76177228fb07083b74881c7b00c420abf0bb05 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 30 Aug 2018 13:48:54 +0800 Subject: [PATCH 179/520] add router clean function before each case --- test/emqx_router_SUITE.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/emqx_router_SUITE.erl b/test/emqx_router_SUITE.erl index b9d40810a..196b1678e 100644 --- a/test/emqx_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -39,6 +39,7 @@ end_per_suite(_Config) -> emqx_ct_broker_helpers:run_teardown_steps(). init_per_testcase(_TestCase, Config) -> + clear_tables(), Config. end_per_testcase(_TestCase, _Config) -> From b0ed953708817fc09c286aea0d1c5eab992fffd8 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 30 Aug 2018 13:56:55 +0800 Subject: [PATCH 180/520] fix emqx_session:unsubscribe bug --- src/emqx_session.erl | 175 ++++++++++++++++++++++--------------------- 1 file changed, 90 insertions(+), 85 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index ab9096f23..0ac1cf59d 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -62,87 +62,87 @@ -import(emqx_zone, [get_env/2, get_env/3]). -record(state, { - %% Clean Start Flag - clean_start = false :: boolean(), + %% Clean Start Flag + clean_start = false :: boolean(), - %% Client Binding: local | remote - binding = local :: local | remote, + %% Client Binding: local | remote + binding = local :: local | remote, - %% ClientId: Identifier of Session - client_id :: binary(), + %% ClientId: Identifier of Session + client_id :: binary(), - %% Username - username :: binary() | undefined, + %% Username + username :: binary() | undefined, - %% Connection pid binding with session - conn_pid :: pid(), + %% Connection pid binding with session + conn_pid :: pid(), - %% Old Connection Pid that has been kickout - old_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(), + %% Next packet id of the session + next_pkt_id = 1 :: emqx_mqtt_types:packet_id(), - %% Max subscriptions - max_subscriptions :: non_neg_integer(), + %% Max subscriptions + max_subscriptions :: non_neg_integer(), - %% Client’s Subscriptions. - subscriptions :: map(), + %% Client’s Subscriptions. + subscriptions :: map(), - %% Upgrade QoS? - upgrade_qos = false :: boolean(), + %% Upgrade QoS? + upgrade_qos = false :: boolean(), - %% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked. - inflight :: emqx_inflight:inflight(), + %% 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(), + %% Max Inflight Size. DEPRECATED: Get from inflight + %% max_inflight = 32 :: non_neg_integer(), - %% Retry interval for redelivering QoS1/2 messages - retry_interval = 20000 :: timeout(), + %% Retry interval for redelivering QoS1/2 messages + retry_interval = 20000 :: timeout(), - %% Retry Timer - retry_timer :: reference() | undefined, + %% Retry Timer + retry_timer :: reference() | undefined, - %% 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(), + %% 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. - awaiting_rel :: map(), + %% Client -> Broker: Inflight QoS2 messages received from client and waiting for pubrel. + awaiting_rel :: map(), - %% Max Packets Awaiting PUBREL - max_awaiting_rel = 100 :: non_neg_integer(), + %% Max Packets Awaiting PUBREL + max_awaiting_rel = 100 :: non_neg_integer(), - %% Awaiting PUBREL Timeout - await_rel_timeout = 20000 :: timeout(), + %% Awaiting PUBREL Timeout + await_rel_timeout = 20000 :: timeout(), - %% Awaiting PUBREL Timer - await_rel_timer :: reference() | undefined, + %% Awaiting PUBREL Timer + await_rel_timer :: reference() | undefined, - %% Session Expiry Interval - expiry_interval = 7200000 :: timeout(), + %% Session Expiry Interval + expiry_interval = 7200000 :: timeout(), - %% Expired Timer - expiry_timer :: reference() | undefined, + %% Expired Timer + expiry_timer :: reference() | undefined, - %% Enable Stats - enable_stats :: boolean(), + %% Enable Stats + enable_stats :: boolean(), - %% Stats timer - stats_timer :: reference() | undefined, + %% Stats timer + stats_timer :: reference() | undefined, - %% TODO: - deliver_stats = 0, + %% TODO: + deliver_stats = 0, - %% TODO: - enqueue_stats = 0, + %% TODO: + enqueue_stats = 0, - %% Created at - created_at :: erlang:timestamp() - }). + %% Created at + created_at :: erlang:timestamp() + }). -define(TIMEOUT, 60000). @@ -284,7 +284,12 @@ pubcomp(SPid, PacketId, ReasonCode) -> -spec(unsubscribe(pid(), emqx_types:topic_table()) -> ok). unsubscribe(SPid, RawTopicFilters) when is_list(RawTopicFilters) -> - unsubscribe(SPid, undefined, #{}, lists:map(fun emqx_topic:parse/1, RawTopicFilters)). + TopicFilters = lists:map(fun({RawTopic, Opts}) -> + emqx_topic:parse(RawTopic, Opts); + (RawTopic) -> + emqx_topic:parse(RawTopic) + end, RawTopicFilters), + unsubscribe(SPid, undefined, #{}, TopicFilters). -spec(unsubscribe(pid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:properties(), emqx_mqtt_types:topic_filters()) -> ok). @@ -424,20 +429,20 @@ handle_call(Req, _From, State) -> handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> {ReasonCodes, Subscriptions1} = - lists:foldr(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) -> - {[QoS|RcAcc], case maps:find(Topic, SubMap) of - {ok, SubOpts} -> - SubMap; - {ok, _SubOpts} -> - emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), - emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts]), - maps:put(Topic, SubOpts, SubMap); - error -> - emqx_broker:subscribe(Topic, ClientId, SubOpts), - emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts]), - maps:put(Topic, SubOpts, SubMap) - end} - end, {[], Subscriptions}, TopicFilters), + lists:foldr(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) -> + {[QoS|RcAcc], case maps:find(Topic, SubMap) of + {ok, SubOpts} -> + SubMap; + {ok, _SubOpts} -> + emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), + emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts]), + maps:put(Topic, SubOpts, SubMap); + error -> + emqx_broker:subscribe(Topic, ClientId, SubOpts), + emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts]), + maps:put(Topic, SubOpts, SubMap) + end} + end, {[], Subscriptions}, TopicFilters), suback(FromPid, PacketId, ReasonCodes), {noreply, State#state{subscriptions = Subscriptions1}}; @@ -445,16 +450,16 @@ handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> {ReasonCodes, Subscriptions1} = - lists:foldr(fun({Topic, _SubOpts}, {Acc, SubMap}) -> - case maps:find(Topic, SubMap) of - {ok, SubOpts} -> - ok = emqx_broker:unsubscribe(Topic, ClientId), - emqx_hooks:run('session.unsubscribed', [#{client_id => ClientId}, Topic, SubOpts]), - {[?RC_SUCCESS|Acc], maps:remove(Topic, SubMap)}; - error -> - {[?RC_NO_SUBSCRIPTION_EXISTED|Acc], SubMap} - end - end, {[], Subscriptions}, TopicFilters), + lists:foldr(fun({Topic, _SubOpts}, {Acc, SubMap}) -> + case maps:find(Topic, SubMap) of + {ok, SubOpts} -> + ok = emqx_broker:unsubscribe(Topic, ClientId), + emqx_hooks:run('session.unsubscribed', [#{client_id => ClientId}, 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, State#state{subscriptions = Subscriptions1}}; @@ -524,7 +529,7 @@ handle_cast(Msg, State) -> %% Batch dispatch handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> {noreply, lists:foldl(fun(Msg, NewState) -> - element(2, handle_info({dispatch, Topic, Msg}, NewState)) + element(2, handle_info({dispatch, Topic, Msg}, NewState)) end, State, Msgs)}; %% Dispatch message @@ -684,7 +689,7 @@ sortfun(inflight) -> sortfun(awaiting_rel) -> fun({_, #message{timestamp = Ts1}}, {_, #message{timestamp = Ts2}}) -> - Ts1 < Ts2 + Ts1 < Ts2 end. %%------------------------------------------------------------------------------ @@ -726,7 +731,7 @@ dispatch(Msg = #message{qos = ?QOS0}, State) -> inc_stats(deliver, State); dispatch(Msg = #message{qos = QoS}, State = #state{next_pkt_id = PacketId, inflight = Inflight}) - when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 -> + when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 -> case emqx_inflight:is_full(Inflight) of true -> enqueue_msg(Msg, State); @@ -824,7 +829,7 @@ dequeue2(State = #state{mqueue = Q}) -> %% Ensure timers ensure_await_rel_timer(State = #state{await_rel_timer = undefined, await_rel_timeout = Timeout}) -> - State#state{await_rel_timer = emqx_misc:start_timer(Timeout, check_awaiting_rel)}; + State#state{await_rel_timer = emqx_misc:start_timer(Timeout, check_awaiting_rel)}; ensure_await_rel_timer(State) -> State. From 8a5519cafad355f740bc3c4698e0c4335ab7d303 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Thu, 30 Aug 2018 14:29:07 +0800 Subject: [PATCH 181/520] attrs for ws_connection --- src/emqx_ws_connection.erl | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 026d160b3..ed1532565 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -17,7 +17,7 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([info/1]). +-export([info/1, attrs/1]). -export([stats/1]). -export([kick/1]). -export([session/1]). @@ -53,9 +53,14 @@ %% API %%------------------------------------------------------------------------------ +%% for debug info(WSPid) -> call(WSPid, info). +%% for dashboard +attrs(CPid) when is_pid(CPid) -> + call(CPid, attrs). + stats(WSPid) -> call(WSPid, stats). @@ -170,6 +175,15 @@ websocket_info({call, From, info}, State = #state{peername = Peername, gen_server:reply(From, lists:append([ConnInfo, ProtoInfo])), {ok, State}; +websocket_info({call, From, attrs}, State = #state{peername = Peername, + sockname = Sockname, + proto_state = ProtoState}) -> + SockAttrs = [{peername, Peername}, + {sockname, Sockname}], + ProtoAttrs = emqx_protocol:attrs(ProtoState), + gen_server:reply(From, lists:usort(lists:append(SockAttrs, ProtoAttrs))), + {ok, State}; + websocket_info({call, From, stats}, State = #state{proto_state = ProtoState}) -> Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(), emqx_protocol:stats(ProtoState)]), gen_server:reply(From, Stats), @@ -262,4 +276,3 @@ shutdown(Reason, State) -> wsock_stats() -> [{Key, get(Key)} || Key <- ?SOCK_STATS]. - From c89f53f14dbd621178a010c91b10ab1fd736e8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 30 Aug 2018 14:43:14 +0800 Subject: [PATCH 182/520] Add session, mountpoint test suites --- Makefile | 4 +-- test/emqx_mock_client.erl | 25 ++++++++++++--- test/emqx_mountpoint_SUITE.erl | 36 +++++++++++++++++++++ test/emqx_session_SUITE.erl | 57 ++++++++++++++++++++++++++++++++++ test/emqx_sm_SUITE.erl | 2 +- 5 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 test/emqx_mountpoint_SUITE.erl create mode 100644 test/emqx_session_SUITE.erl diff --git a/Makefile b/Makefile index 80a3dafbd..ce0a282fa 100644 --- a/Makefile +++ b/Makefile @@ -35,10 +35,10 @@ EUNIT_OPTS = verbose # CT_SUITES = emqx_stats ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat -CT_SUITES = emqx emqx_access emqx_base62 emqx_broker emqx_client emqx_cm emqx_frame emqx_guid emqx_inflight \ +CT_SUITES = emqx emqx_session emqx_access emqx_base62 emqx_broker emqx_client emqx_cm emqx_frame emqx_guid emqx_inflight \ emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ emqx_mqtt_compat emqx_mqtt_properties emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ - emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_zone + emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_zone emqx_mountpoint CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqxct@127.0.0.1 diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index 164b6d4ea..0536b8bac 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -16,13 +16,15 @@ -behaviour(gen_server). --export([start_link/1, open_session/3, close_session/2, stop/1]). +-export([start_link/1, open_session/3, close_session/2, stop/1, get_last_message/0]). -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}). +-define(TAB, messages). + start_link(ClientId) -> gen_server:start_link(?MODULE, [ClientId], []). @@ -35,13 +37,25 @@ close_session(ClientPid, SessPid) -> stop(CPid) -> gen_server:call(CPid, stop). +get_last_message() -> + [{last_message, Msg}] = ets:lookup(?TAB, last_message), + Msg. + init([ClientId]) -> - {ok, #state{clean_start = true, client_id = ClientId}}. + Result = lists:member(?TAB, ets:all()), + if Result == false -> + ets:new(?TAB, [set, named_table]); + true -> ok + end, + {ok, + #state{clean_start = true, + client_id = ClientId} + }. handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> Attrs = #{ zone => Zone, client_id => ClientId, - conn_pid => ClientPid, + conn_pid => ClientPid, clean_start => true, username => undefined, conn_props => undefined @@ -49,7 +63,7 @@ handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> {ok, SessPid} = emqx_sm:open_session(Attrs), {reply, {ok, SessPid}, State#state{ clean_start = true, - client_id = ClientId, + client_id = ClientId, client_pid = ClientPid }}; @@ -66,6 +80,9 @@ handle_call(_Request, _From, State) -> handle_cast(_Msg, State) -> {noreply, State}. +handle_info({_, Msg}, State) -> + ets:insert(?TAB, {last_message, Msg}), + {noreply, State}; handle_info(_Info, State) -> {noreply, State}. diff --git a/test/emqx_mountpoint_SUITE.erl b/test/emqx_mountpoint_SUITE.erl new file mode 100644 index 000000000..61d8d3652 --- /dev/null +++ b/test/emqx_mountpoint_SUITE.erl @@ -0,0 +1,36 @@ +%% Copyright (c) 2018 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_mountpoint_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> [t_mount_unmount, t_replvar]. + +t_mount_unmount(_) -> + Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), + Msg2 = emqx_mountpoint:mount(<<"mount">>, Msg), + ?assertEqual(<<"mounttopic">>, Msg2#message.topic), + TopicFilter = [{<<"mounttopic">>, #{qos => ?QOS2}}], + TopicFilter = emqx_mountpoint:mount(<<"mount">>, [{<<"topic">>, #{qos => ?QOS2}}]), + Msg = emqx_mountpoint:unmount(<<"mount">>, Msg2). + +t_replvar(_) -> + <<"mount/test/clientid">> = emqx_mountpoint:replvar(<<"mount/%u/%c">>, #{client_id => <<"clientid">>, username => <<"test">>}). diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl new file mode 100644 index 000000000..f2da10b74 --- /dev/null +++ b/test/emqx_session_SUITE.erl @@ -0,0 +1,57 @@ +%% Copyright (c) 2018 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_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). + +all() -> [t_session_all]. + +t_session_all(_) -> + emqx_ct_broker_helpers:run_setup_steps(), + ClientId = <<"ClientId">>, + {ok, ConnPid} = emqx_mock_client:start_link(ClientId), + {ok, SPid} = emqx_mock_client:open_session(ConnPid, ClientId, internal), + Message1 = emqx_message:make(<<"ClientId">>, 2, <<"topic">>, <<"hello">>), + emqx_session:subscribe(SPid, [{<<"topic">>, #{qos => 2}}]), + emqx_session:subscribe(SPid, [{<<"topic">>, #{qos => 1}}]), + timer:sleep(200), + [{<<"topic">>, _}] = emqx:subscriptions({SPid, <<"ClientId">>}), + emqx_session:publish(SPid, 1, Message1), + timer:sleep(200), + {publish, 1, _} = emqx_mock_client:get_last_message(), + emqx_session:puback(SPid, 2), + emqx_session:puback(SPid, 3, reasoncode), + emqx_session:pubrec(SPid, 4), + emqx_session:pubrec(SPid, 5, reasoncode), + emqx_session:pubrel(SPid, 6, reasoncode), + emqx_session:pubcomp(SPid, 7, reasoncode), + timer:sleep(200), + 2 = emqx_metrics:val('packets/puback/missed'), + 2 = emqx_metrics:val('packets/pubrec/missed'), + 1 = emqx_metrics:val('packets/pubrel/missed'), + 1 = emqx_metrics:val('packets/pubcomp/missed'), + Attrs = emqx_session:attrs(SPid), + Info = emqx_session:info(SPid), + Stats = emqx_session:stats(SPid), + ClientId = proplists:get_value(client_id, Attrs), + ClientId = proplists:get_value(client_id, Info), + 1 = proplists:get_value(subscriptions_count, Stats), + emqx_session:unsubscribe(SPid, [<<"topic">>]), + timer:sleep(200), + [] = emqx:subscriptions({SPid, <<"clientId">>}), + emqx_mock_client:close_session(ConnPid, SPid). diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl index 5bc096cd8..24999f2a0 100644 --- a/test/emqx_sm_SUITE.erl +++ b/test/emqx_sm_SUITE.erl @@ -26,7 +26,7 @@ t_open_close_session(_) -> {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), Attrs = #{clean_start => true, client_id => <<"client">>, conn_pid => ClientPid, zone => internal, username => <<"zhou">>, conn_props => #{}}, - {ok, _SPid} = emqx_sm:open_session(Attrs), + {ok, SPid} = emqx_sm:open_session(Attrs), [{<<"client">>, SPid}] = emqx_sm:lookup_session(<<"client">>), SPid = emqx_sm:lookup_session_pid(<<"client">>), {ok, NewConnPid} = emqx_mock_client:start_link(<<"client">>), From 518ba9ace9c3f5c267ddab8a00a0c232cf4652d4 Mon Sep 17 00:00:00 2001 From: RockyJin Date: Thu, 30 Aug 2018 15:29:14 +0800 Subject: [PATCH 183/520] readme file for v3.0 --- README.md | 130 +++++++++++++++++++----------------------------------- 1 file changed, 45 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index eceeee5de..1e80dae6b 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,29 @@ +# *EMQ X* - MQTT Broker -# *EMQ X* - EMQ X Broker -[![Build Status](https://travis-ci.org/emqtt/emqttd.svg?branch=master)](https://travis-ci.org/emqtt/emqttd) +*EMQ X* broker is fully a open source, highly scalable, highly available distributed message broker for IoT, M2M and Mobile applications that can handle tens of millions of concurrent clients. -*EMQ* (Erlang MQTT Broker) is a distributed, massively scalable, highly extensible MQTT message broker written in Erlang/OTP. +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, STOMP and SockJS. The 3.0 release of the *EMQ X* broker can scaled to 10+ million concurrent MQTT connections on one cluster. -*EMQ* is fully open source and licensed under the Apache Version 2.0. *EMQ* implements both MQTT V3.1 and V3.1.1 protocol specifications, and supports MQTT-SN, CoAP, WebSocket, STOMP and SockJS at the same time. -*EMQ* provides a scalable, reliable, enterprise-grade MQTT message Hub for IoT, M2M, Smart Hardware and Mobile Messaging Applications. +- For full list of new features, please read *EMQ X* broker 3.0 [release notes](https://github.com/emqtt/emqttd/releases/). +- For more information, please visit [EMQ X homepage](http://emqtt.io). -The 1.0 release of the EMQ broker has scaled to 1.3 million concurrent MQTT connections on a 12 Core, 32G CentOS server. -Please visit [emqtt.io](http://emqtt.io) for more service. Follow us on Twitter: [@emqtt](https://twitter.com/emqtt) - -## Features - -* Full MQTT V3.1/V3.1.1 support -* QoS0, QoS1, QoS2 Publish/Subscribe -* Session Management and Offline Messages -* Retained Message -* Last Will Message -* TCP/SSL Connection -* MQTT Over WebSocket(SSL) -* HTTP Publish API -* MQTT-SN Protocol -* STOMP protocol -* STOMP over SockJS -* $SYS/# Topics -* ClientID Authentication -* IpAddress Authentication -* Username and Password Authentication -* Access control based on IpAddress, ClientID, Username -* JWT Authentication -* LDAP Authentication/ACL -* HTTP Authentication/ACL -* MySQL Authentication/ACL -* Redis Authentication/ACL -* PostgreSQL Authentication/ACL -* MongoDB Authentication/ACL -* Cluster brokers on several nodes -* Bridge brokers locally or remotely -* mosquitto, RSMB bridge -* Extensible architecture with Hooks and Plugins -* Passed eclipse paho interoperability tests -* Local Subscription -* Shared Subscription -* Proxy Protocol V1/2 -* Lua Hook and Web Hook -* LWM2M Prototol Support ## Installation The *EMQ* broker is cross-platform, which can be deployed on Linux, Unix, Mac, Windows and even Raspberry Pi. -Download the binary package for your platform from http://emqtt.io/downloads. +Download the binary package for your platform from [here](http://emqtt.io/downloads). + +-[Single Node Install](http://emqtt.io/docs/v2/install.html) +-[Multi Node Install](http://emqtt.io/docs/v2/cluster.html) -Documentation on [emqtt.io/docs/v2/](http://emqtt.io/docs/v2/install.html), [docs.emqtt.com](http://docs.emqtt.com/en/latest/install.html) for installation and configuration guide. ## Build From Source -The *EMQ* broker requires Erlang/OTP R19+ to build since 2.1 release. +The *EMQ* broker requires Erlang/OTP R21+ to build since 3.0 release. ``` git clone https://github.com/emqtt/emq-relx.git @@ -67,55 +31,51 @@ git clone https://github.com/emqtt/emq-relx.git cd emq-relx && make cd _rel/emqttd && ./bin/emqttd console + ``` -## Plugins +## Quick Start -The *EMQ* broker is highly extensible, with many hooks and plugins for customizing the authentication/ACL and integrating with other systems: + # Start emqttd + ./bin/emqttd start + + # Check Status + ./bin/emqttd_ctl status + + # Stop emqttd + ./bin/emqttd stop -Plugin | Description ------------------------------------------------------------------------|-------------------------------------- -[emq_plugin_template](https://github.com/emqtt/emq_plugin_template) | Plugin template and demo -[emq_dashboard](https://github.com/emqtt/emq_dashboard) | Web Dashboard -[emq_retainer](https://github.com/emqtt/emq-retainer) | Store MQTT Retained Messages -[emq_modules](https://github.com/emqtt/emq-modules) | Presence, Subscription and Rewrite Modules -[emq_auth_username](https://github.com/emqtt/emq_auth_username) | Username/Password Authentication Plugin -[emq_auth_clientid](https://github.com/emqtt/emq_auth_clientid) | ClientId Authentication Plugin -[emq_auth_mysql](https://github.com/emqtt/emq_auth_mysql) | MySQL Authentication/ACL Plugin -[emq_auth_pgsql](https://github.com/emqtt/emq_auth_pgsql) | PostgreSQL Authentication/ACL Plugin -[emq_auth_redis](https://github.com/emqtt/emq_auth_redis) | Redis Authentication/ACL Plugin -[emq_auth_mongo](https://github.com/emqtt/emq_auth_mongo) | MongoDB Authentication/ACL Plugin -[emq_auth_http](https://github.com/emqtt/emq_auth_http) | Authentication/ACL by HTTP API -[emq_auth_ldap](https://github.com/emqtt/emq_auth_ldap) | LDAP Authentication Plugin -[emq_auth_jwt](https://github.com/emqtt/emq-auth-jwt) | JWT Authentication Plugin -[emq_web_hook](https://github.com/emqtt/emq-web-hook) | Web Hook Plugin -[emq_lua_hook](https://github.com/emqtt/emq-lua-hook) | Lua Hook Plugin -[emq_sn](https://github.com/emqtt/emq_sn) | MQTT-SN Protocol Plugin -[emq_coap](https://github.com/emqtt/emq_coap) | CoAP Protocol Plugin -[emq_stomp](https://github.com/emqtt/emq_stomp) | Stomp Protocol Plugin -[emq_lwm2m](https://github.com/emqx/emqx-lwm2m) | LWM2M Prototol Plugin -[emq_recon](https://github.com/emqtt/emq_recon) | Recon Plugin -[emq_reloader](https://github.com/emqtt/emq_reloader) | Reloader Plugin -[emq_sockjs](https://github.com/emqtt/emq_sockjs) | SockJS(Stomp) Plugin + To view the dashboard after running, use your browser to open: http://localhost:18083 -## Supports -* Twitter: [@emqtt](https://twitter.com/emqtt) -* Homepage: http://emqtt.io -* Downloads: http://emqtt.io/downloads -* Documentation: http://emqtt.io/docs/v2/ -* Forum: https://groups.google.com/d/forum/emqtt -* Mailing List: -* Issues: https://github.com/emqtt/emqttd/issues -* QQ Group: 12222225 +## Roadmap -## Test Servers +The [EMQX roadmap uses Github milestones](https://github.com/emqtt/emqttd/milestones) to track the progress of the project. -The **q.emqtt.com** hosts a public Four-Node *EMQ* cluster on [QingCloud](https://qingcloud.com): +## Community, discussion, contribution, and support + +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) +- [Blog](https://medium.com/@emqtt) + +Please submit any bugs, issues, and feature requests to [emqtt/emqttd](//github.com/emqtt/emqttd/issues). -![qing_cluster](http://emqtt.io/static/img/public_cluster.png) ## License +Copyright (c) 2014-2018 [EMQ X Tech, LLC](http://emqtt.io) + +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 Version 2.0 From 69b285aa3fd5b1bd57708305703afd08ae027be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 30 Aug 2018 16:26:04 +0800 Subject: [PATCH 184/520] Add test case for emqx_connection:attrs/1 --- Makefile | 2 +- test/emqx_connection_SUITE.erl | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 test/emqx_connection_SUITE.erl diff --git a/Makefile b/Makefile index ce0a282fa..08e3c3dd0 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ EUNIT_OPTS = verbose # CT_SUITES = emqx_stats ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat -CT_SUITES = emqx emqx_session emqx_access emqx_base62 emqx_broker emqx_client emqx_cm emqx_frame emqx_guid emqx_inflight \ +CT_SUITES = emqx emqx_connection emqx_session emqx_access emqx_base62 emqx_broker emqx_client emqx_cm emqx_frame emqx_guid emqx_inflight \ emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ emqx_mqtt_compat emqx_mqtt_properties emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_zone emqx_mountpoint diff --git a/test/emqx_connection_SUITE.erl b/test/emqx_connection_SUITE.erl new file mode 100644 index 000000000..ae69cdd5d --- /dev/null +++ b/test/emqx_connection_SUITE.erl @@ -0,0 +1,30 @@ +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. + +-module(emqx_connection_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). + +all() -> [t_attrs]. + +t_attrs(_) -> + emqx_ct_broker_helpers:run_setup_steps(), + {ok, C, _} = emqx_client:start_link([{host, "localhost"}, {client_id, <<"simpleClient">>}, {username, <<"plain">>}, {password, <<"plain">>}]), + [{<<"simpleClient">>, ConnPid}] = emqx_cm:lookup_connection(<<"simpleClient">>), + Attrs = emqx_connection:attrs(ConnPid), + <<"simpleClient">> = proplists:get_value(client_id, Attrs), + <<"plain">> = proplists:get_value(username, Attrs). \ No newline at end of file From 8ed6266ace16eb64090477dd1889928b693350bf Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 18:26:13 +0800 Subject: [PATCH 185/520] Add mqueue_type, mqueue_priorities options --- etc/emqx.conf | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/etc/emqx.conf b/etc/emqx.conf index 41ac298fa..cb6e91b96 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -500,6 +500,17 @@ mqtt.wildcard_subscription = true ## Value: boolean mqtt.shared_subscription = true +## Message queue type. +## +## Value: simple | priority +mqtt.mqueue_type = simple + +## Topic priorities. Default is 0. +## +## Priority: Number [0-255] +## +## mqtt.mqueue_priorities = topic/1=10,topic/2=8 + ##-------------------------------------------------------------------- ## Zones ##-------------------------------------------------------------------- @@ -616,12 +627,23 @@ zone.external.await_rel_timeout = 60s ## Default: 2h, 2 hours zone.external.session_expiry_interval = 2h +## Message queue type. +## +## Value: simple | priority +zone.external.mqueue_type = simple + ## Maximum queue length. Enqueued messages when persistent client disconnected, ## or inflight window is full. 0 means no limit. ## ## Value: Number >= 0 zone.external.max_mqueue_len = 1000 +## Topic priorities. Default is 0. +## +## Priority: Number [0-255] +## +## zone.external.mqueue_priorities = topic/1=10,topic/2=8 + ## Whether to enqueue Qos0 messages. ## ## Value: false | true From 7b5871828048aed0bf16dcb384c3cd8b1e59230c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 18:26:31 +0800 Subject: [PATCH 186/520] Add mqueue_type, mqueue_priorities options --- priv/emqx.schema | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/priv/emqx.schema b/priv/emqx.schema index df1722c02..a80aa8ee6 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -646,6 +646,18 @@ end}. {datatype, {enum, [true, false]}} ]}. +%% @doc Type: simple | priority +{mapping, "mqtt.mqueue_type", "emqx.mqueue_type", [ + {default, simple}, + {datatype, {enum, [simple, priority]}} +]}. + +%% @doc Topic Priorities: 0~255, Default is 0 +{mapping, "mqtt.mqueue_priorities", "emqx.mqueue_priorities", [ + {default, ""}, + {datatype, string} +]}. + %%-------------------------------------------------------------------- %% Zones %%-------------------------------------------------------------------- @@ -777,6 +789,12 @@ end}. {datatype, {duration, ms}} ]}. +%% @doc Type: simple | priority +{mapping, "zone.$name.mqueue_type", "emqx.zones", [ + {default, simple}, + {datatype, {enum, [simple, priority]}} +]}. + %% @doc Max queue length. Enqueued messages when persistent client %% disconnected, or inflight window is full. 0 means no limit. {mapping, "zone.$name.max_mqueue_len", "emqx.zones", [ @@ -784,6 +802,11 @@ end}. {datatype, integer} ]}. +%% @doc Topic Priorities: 0~255, Default is 0 +{mapping, "zone.$name.mqueue_priorities", "emqx.zones", [ + {datatype, string} +]}. + %% @doc Queue Qos0 messages? {mapping, "zone.$name.mqueue_store_qos0", "emqx.zones", [ {default, true}, From 021d43755fa55a9840514c2ef2d040659683498a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 18:28:02 +0800 Subject: [PATCH 187/520] Add update_expiry/1 function --- src/emqx_message.erl | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/emqx_message.erl b/src/emqx_message.erl index d700c2aee..9ff8e7511 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -22,7 +22,7 @@ -export([get_flag/2, get_flag/3, set_flag/2, set_flag/3, unset_flag/2]). -export([set_headers/2]). -export([get_header/2, get_header/3, set_header/3]). --export([is_expired/1, check_expiry/1, check_expiry/2]). +-export([is_expired/1, check_expiry/1, check_expiry/2, update_expiry/1]). -export([format/1]). -type(flag() :: atom()). @@ -96,7 +96,7 @@ set_header(Hdr, Val, Msg = #message{headers = Headers}) -> -spec(is_expired(emqx_types:message()) -> boolean()). is_expired(#message{headers = #{'Message-Expiry-Interval' := Interval}, timestamp = CreatedAt}) -> - elapsed(CreatedAt) > Interval; + elapsed(CreatedAt) > timer:seconds(Interval); is_expired(_Msg) -> false. @@ -108,16 +108,23 @@ check_expiry(_Msg) -> -spec(check_expiry(emqx_types:message(), erlang:timestamp()) -> {ok, pos_integer()} | expired | false). check_expiry(#message{headers = #{'Message-Expiry-Interval' := Interval}}, Since) -> - case Interval - elapsed(Since) of - I when I > 0 -> {ok, I}; + case Interval - (elapsed(Since) div 1000) of + Timeout when Timeout > 0 -> {ok, Timeout}; _ -> expired end; check_expiry(_Msg, _Since) -> false. +update_expiry(Msg = #message{headers = #{'Message-Expiry-Interval' := Interval}, timestamp = CreatedAt}) -> + case elapsed(CreatedAt) of + Elapsed when Elapsed > 0 -> + set_header('Message-Expiry-Interval', max(1, Interval - (Elapsed div 1000)), Msg); + _ -> Msg + end. + +%% MilliSeconds elapsed(Since) -> - Secs = timer:now_diff(os:timestamp(), Since) div 1000, - if Secs < 0 -> 0; true -> Secs end. + max(0, timer:now_diff(os:timestamp(), Since) div 1000). format(#message{id = Id, qos = QoS, topic = Topic, from = From, flags = Flags, headers = Headers}) -> io_lib:format("Message(Id=~s, QoS=~w, Topic=~s, From=~s, Flags=~s, Headers=~s)", From a67958adb4ada14505a5c008373782ee0db1a823 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 18:28:40 +0800 Subject: [PATCH 188/520] Add 'messages/expired' counter --- src/emqx_metrics.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 15db3b420..6d17f6648 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -77,6 +77,7 @@ {counter, 'messages/qos2/dropped'}, % QoS2 Messages dropped {gauge, 'messages/retained'}, % Messagea retained {counter, 'messages/dropped'}, % Messages dropped + {counter, 'messages/expired'}, % Messages expired {counter, 'messages/forward'} % Messages forward ]). From 1f2bbe3eb8a9af8c055108febe1906fd05a44772 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 18:29:20 +0800 Subject: [PATCH 189/520] Support priority queue --- src/emqx_mqueue.erl | 72 +++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index bf31fa663..a93fd8838 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -11,7 +11,6 @@ %% See the License for the specific language governing permissions and %% limitations under the License. -%% TODO: should be a bound queue. %% @doc A Simple in-memory message queue. %% %% Notice that MQTT is not an enterprise messaging queue. MQTT assume that client @@ -39,70 +38,67 @@ %% %% @end -%% TODO: ... -module(emqx_mqueue). -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --import(proplists, [get_value/3]). - --export([new/2, type/1, name/1, is_empty/1, len/1, max_len/1, in/2, out/1]). --export([dropped/1, stats/1]). +-export([init/1, type/1]). +-export([is_empty/1]). +-export([len/1, max_len/1]). +-export([in/2, out/1]). +-export([stats/1, dropped/1]). -define(PQUEUE, emqx_pqueue). -type(priority() :: {iolist(), pos_integer()}). --type(options() :: #{type => simple | priority, - max_len => non_neg_integer(), - priority => list(priority()), +-type(options() :: #{type := simple | priority, + max_len := non_neg_integer(), + priorities => list(priority()), store_qos0 => boolean()}). --type(stat() :: {max_len, non_neg_integer()} - | {len, non_neg_integer()} +-type(stat() :: {len, non_neg_integer()} + | {max_len, non_neg_integer()} | {dropped, non_neg_integer()}). --record(mqueue, {type :: simple | priority, - name, q :: queue:queue() | ?PQUEUE:q(), - %% priority table - pseq = 0, priorities = [], - %% len of simple queue - len = 0, max_len = 0, - qos0 = false, dropped = 0}). +-record(mqueue, { + type :: simple | priority, + q :: queue:queue() | ?PQUEUE:q(), + %% priority table + priorities = [], + pseq = 0, + len = 0, + max_len = 0, + qos0 = false, + dropped = 0 + }). -type(mqueue() :: #mqueue{}). -export_type([mqueue/0, priority/0, options/0]). --spec(new(iolist(), options()) -> mqueue()). -new(Name, #{type := Type, max_len := MaxLen, store_qos0 := StoreQos0}) -> - init_q(#mqueue{type = Type, name = iolist_to_binary(Name), - len = 0, max_len = MaxLen, qos0 = StoreQos0}). +-spec(init(options()) -> mqueue()). +init(Opts = #{type := Type, max_len := MaxLen, store_qos0 := QoS0}) -> + init_q(#mqueue{type = Type, len = 0, max_len = MaxLen, qos0 = QoS0}, Opts). -init_q(MQ = #mqueue{type = simple}) -> +init_q(MQ = #mqueue{type = simple}, _Opts) -> MQ#mqueue{q = queue:new()}; -init_q(MQ = #mqueue{type = priority}) -> - %%Priorities = get_value(priority, Opts, []), - init_p([], MQ#mqueue{q = ?PQUEUE:new()}). +init_q(MQ = #mqueue{type = priority}, #{priorities := Priorities}) -> + init_pq(Priorities, MQ#mqueue{q = ?PQUEUE:new()}). -init_p([], MQ) -> +init_pq([], MQ) -> MQ; -init_p([{Topic, P} | L], MQ) -> +init_pq([{Topic, P} | L], MQ) -> {_, MQ1} = insert_p(iolist_to_binary(Topic), P, MQ), - init_p(L, MQ1). + init_pq(L, MQ1). -insert_p(Topic, P, MQ = #mqueue{priorities = Tab, pseq = Seq}) -> +insert_p(Topic, P, MQ = #mqueue{priorities = L, pseq = Seq}) -> <> = <>, - {PInt, MQ#mqueue{priorities = [{Topic, PInt} | Tab], pseq = Seq + 1}}. + {PInt, MQ#mqueue{priorities = [{Topic, PInt} | L], pseq = Seq + 1}}. --spec(name(mqueue()) -> iolist()). -name(#mqueue{name = Name}) -> - Name. - --spec(type(mqueue()) -> atom()). -type(#mqueue{type = Type}) -> - Type. +-spec(type(mqueue()) -> simple | priority). +type(#mqueue{type = Type}) -> Type. is_empty(#mqueue{type = simple, len = Len}) -> Len =:= 0; is_empty(#mqueue{type = priority, q = Q}) -> ?PQUEUE:is_empty(Q). From 7b5f2577d3a6028326723e47e03a1c4d519cdbdd Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 18:30:33 +0800 Subject: [PATCH 190/520] Support message ttl and expiration --- src/emqx_session.erl | 316 +++++++++++++++++++++++-------------------- 1 file changed, 172 insertions(+), 144 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index ab9096f23..7d4362465 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -62,6 +62,9 @@ -import(emqx_zone, [get_env/2, get_env/3]). -record(state, { + %% Idle timeout + idle_timeout :: pos_integer(), + %% Clean Start Flag clean_start = false :: boolean(), @@ -134,10 +137,10 @@ %% Stats timer stats_timer :: reference() | undefined, - %% TODO: + %% Deliver stats deliver_stats = 0, - %% TODO: + %% Enqueue stats enqueue_stats = 0, %% Created at @@ -150,11 +153,10 @@ emqx_logger:Level([{client, State#state.client_id}], "Session(~s): " ++ Format, [State#state.client_id | Args])). -%% @doc Start a session --spec(start_link(SessAttrs :: map()) -> {ok, pid()} | {error, term()}). +%% @doc Start a session proc. +-spec(start_link(SessAttrs :: map()) -> {ok, pid()}). start_link(SessAttrs) -> - IdleTimeout = maps:get(idle_timeout, SessAttrs, 30000), - gen_server:start_link(?MODULE, SessAttrs, [{hibernate_after, IdleTimeout}]). + proc_lib:start_link(?MODULE, init, [[self(), SessAttrs]]). %% @doc Get session info -spec(info(pid() | #state{}) -> list({atom(), term()})). @@ -309,16 +311,18 @@ close(SPid) -> %% gen_server callbacks %%------------------------------------------------------------------------------ -init(#{zone := Zone, - client_id := ClientId, - username := Username, - conn_pid := ConnPid, - clean_start := CleanStart, - conn_props := ConnProps}) -> +init([Parent, #{zone := Zone, + client_id := ClientId, + username := Username, + conn_pid := ConnPid, + clean_start := CleanStart, + conn_props := ConnProps}]) -> process_flag(trap_exit, true), true = link(ConnPid), MaxInflight = get_env(Zone, max_inflight), - State = #state{clean_start = CleanStart, + IdleTimout = get_env(Zone, idle_timeout, 30000), + State = #state{idle_timeout = IdleTimout, + clean_start = CleanStart, binding = binding(ConnPid), client_id = ClientId, username = Username, @@ -327,7 +331,7 @@ init(#{zone := Zone, max_subscriptions = get_env(Zone, max_subscriptions, 0), upgrade_qos = get_env(Zone, upgrade_qos, false), inflight = emqx_inflight:new(MaxInflight), - mqueue = init_mqueue(Zone, ClientId), + mqueue = init_mqueue(Zone), retry_interval = get_env(Zone, retry_interval, 0), awaiting_rel = #{}, await_rel_timeout = get_env(Zone, await_rel_timeout), @@ -337,20 +341,23 @@ init(#{zone := Zone, deliver_stats = 0, enqueue_stats = 0, created_at = os:timestamp()}, - emqx_sm:register_session(ClientId, [{zone, Zone} | attrs(State)]), + emqx_sm:register_session(ClientId, attrs(State)), emqx_sm:set_session_stats(ClientId, stats(State)), emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), - {ok, State}. + ok = proc_lib:init_ack(Parent, {ok, self()}), + gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State). expire_interval(_Zone, #{'Session-Expiry-Interval' := I}) -> I * 1000; expire_interval(Zone, _ConnProps) -> %% Maybe v3.1.1 get_env(Zone, session_expiry_interval, 0). -init_mqueue(Zone, ClientId) -> - emqx_mqueue:new(ClientId, #{type => simple, - max_len => get_env(Zone, max_mqueue_len), - store_qos0 => get_env(Zone, mqueue_store_qos0)}). +init_mqueue(Zone) -> + emqx_mqueue:init(#{type => get_env(Zone, mqueue_type, simple), + max_len => get_env(Zone, max_mqueue_len, 1000), + priorities => get_env(Zone, mqueue_priorities, ""), + store_qos0 => get_env(Zone, mqueue_store_qos0, true) + }). binding(ConnPid) -> case node(ConnPid) =:= node() of true -> local; false -> remote end. @@ -366,43 +373,43 @@ handle_call({discard, ConnPid}, _From, State = #state{conn_pid = OldConnPid}) -> %% PUBLISH: handle_call({publish, PacketId, Msg = #message{qos = ?QOS_2}}, _From, State = #state{awaiting_rel = AwaitingRel}) -> - case is_awaiting_full(State) of - false -> - case maps:is_key(PacketId, AwaitingRel) of - true -> - reply({error, ?RC_PACKET_IDENTIFIER_IN_USE}, State); - false -> - State1 = State#state{awaiting_rel = maps:put(PacketId, Msg, AwaitingRel)}, - reply(emqx_broker:publish(Msg), ensure_await_rel_timer(State1)) - end; - true -> - ?LOG(warning, "Dropped QoS2 Message for too many awaiting_rel: ~p", [Msg], State), - emqx_metrics:inc('messages/qos2/dropped'), - reply({error, ?RC_RECEIVE_MAXIMUM_EXCEEDED}, State) - end; + reply(case is_awaiting_full(State) 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, Msg, AwaitingRel)}, + {emqx_broker:publish(Msg), ensure_await_rel_timer(State1)} + end; + true -> + emqx_metrics:inc('messages/qos2/dropped'), + ?LOG(warning, "Dropped message for too many awaiting_rel: ~p", + [emqx_message:format(Msg)], State), + {{error, ?RC_RECEIVE_MAXIMUM_EXCEEDED}, State} + end); %% PUBREC: handle_call({pubrec, PacketId, _ReasonCode}, _From, State = #state{inflight = Inflight}) -> - case emqx_inflight:contain(PacketId, Inflight) of - true -> - reply(ok, acked(pubrec, PacketId, State)); - false -> - ?LOG(warning, "The PUBREC PacketId is not found: ~w", [PacketId], State), - emqx_metrics:inc('packets/pubrec/missed'), - reply({error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State) - end; + reply(case emqx_inflight:contain(PacketId, Inflight) of + true -> + {ok, acked(pubrec, PacketId, State)}; + false -> + emqx_metrics:inc('packets/pubrec/missed'), + ?LOG(warning, "The PUBREC PacketId ~w is not found.", [PacketId], State), + {{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State} + end); %% PUBREL: -handle_call({pubrel, PacketId, _ReasonCode}, _From, - State = #state{awaiting_rel = AwaitingRel}) -> - case maps:take(PacketId, AwaitingRel) of - {_, AwaitingRel1} -> - reply(ok, State#state{awaiting_rel = AwaitingRel1}); - error -> - ?LOG(warning, "Cannot find PUBREL: ~p", [PacketId], State), - emqx_metrics:inc('packets/pubrel/missed'), - reply({error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State) - end; +handle_call({pubrel, PacketId, _ReasonCode}, _From, State = #state{awaiting_rel = AwaitingRel}) -> + reply(case maps:take(PacketId, AwaitingRel) of + {_, AwaitingRel1} -> + {ok, State#state{awaiting_rel = AwaitingRel1}}; + error -> + emqx_metrics:inc('packets/pubrel/missed'), + ?LOG(warning, "Cannot find PUBREL: ~w", [PacketId], State), + {{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State} + end); handle_call(info, _From, State) -> reply(info(State), State); @@ -439,7 +446,7 @@ handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, end} end, {[], Subscriptions}, TopicFilters), suback(FromPid, PacketId, ReasonCodes), - {noreply, State#state{subscriptions = Subscriptions1}}; + noreply(State#state{subscriptions = Subscriptions1}); %% UNSUBSCRIBE: handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, @@ -456,15 +463,15 @@ handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, end end, {[], Subscriptions}, TopicFilters), unsuback(From, PacketId, ReasonCodes), - {noreply, State#state{subscriptions = Subscriptions1}}; + noreply(State#state{subscriptions = Subscriptions1}); %% PUBACK: handle_cast({puback, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> case emqx_inflight:contain(PacketId, Inflight) of true -> - {noreply, dequeue(acked(puback, PacketId, State))}; + noreply(dequeue(acked(puback, PacketId, State))); false -> - ?LOG(warning, "The PUBACK PacketId is not found: ~w", [PacketId], State), + ?LOG(warning, "The PUBACK PacketId ~w is not found", [PacketId], State), emqx_metrics:inc('packets/puback/missed'), {noreply, State} end; @@ -473,9 +480,9 @@ handle_cast({puback, PacketId, _ReasonCode}, State = #state{inflight = Inflight} handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> case emqx_inflight:contain(PacketId, Inflight) of true -> - {noreply, dequeue(acked(pubcomp, PacketId, State))}; + noreply(dequeue(acked(pubcomp, PacketId, State))); false -> - ?LOG(warning, "The PUBCOMP PacketId is not found: ~w", [PacketId], State), + ?LOG(warning, "The PUBCOMP PacketId ~w is not found", [PacketId], State), emqx_metrics:inc('packets/pubcomp/missed'), {noreply, State} end; @@ -494,7 +501,7 @@ handle_cast({resume, ConnPid}, State = #state{client_id = ClientId, lists:foreach(fun emqx_misc:cancel_timer/1, [RetryTimer, AwaitTimer, ExpireTimer]), case kick(ClientId, OldConnPid, ConnPid) of - ok -> ?LOG(warning, "connection ~p kickout ~p", [ConnPid, OldConnPid], State); + ok -> ?LOG(warning, "Connection ~p kickout ~p", [ConnPid, OldConnPid], State); ignore -> ok end, @@ -509,13 +516,13 @@ handle_cast({resume, ConnPid}, State = #state{client_id = ClientId, await_rel_timer = undefined, expiry_timer = undefined}, - %% Clean Session: true -> false? - CleanStart andalso emqx_sm:set_session_attrs(ClientId, info(State1)), + %% Clean Session: true -> false??? + CleanStart andalso emqx_sm:set_session_attrs(ClientId, attrs(State1)), 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)))}; + noreply(dequeue(retry_delivery(true, State1))); handle_cast(Msg, State) -> emqx_logger:error("[Session] unexpected cast: ~p", [Msg]), @@ -524,63 +531,68 @@ handle_cast(Msg, State) -> %% Batch dispatch handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> {noreply, lists:foldl(fun(Msg, NewState) -> - element(2, handle_info({dispatch, Topic, Msg}, NewState)) + element(2, handle_info({dispatch, Topic, Msg}, NewState)) end, State, Msgs)}; %% Dispatch message handle_info({dispatch, Topic, Msg}, State = #state{subscriptions = SubMap}) when is_record(Msg, message) -> - {noreply, case maps:find(Topic, SubMap) of - {ok, #{nl := Nl, qos := QoS, subid := SubId}} -> - run_dispatch_steps([{nl, Nl},{qos, QoS}, {subid, SubId}], Msg, State); - {ok, #{nl := Nl, qos := QoS}} -> - run_dispatch_steps([{nl, Nl},{qos, QoS}], Msg, State); - error -> - dispatch(reset_dup(Msg), State) - end}; + noreply(case maps:find(Topic, SubMap) of + {ok, #{nl := Nl, qos := QoS, subid := SubId}} -> + run_dispatch_steps([{nl, Nl},{qos, QoS}, {subid, SubId}], Msg, State); + {ok, #{nl := Nl, qos := QoS}} -> + run_dispatch_steps([{nl, Nl},{qos, QoS}], Msg, State); + error -> + dispatch(emqx_message:unset_flag(dup, Msg), State) + end); %% Do nothing if the client has been disconnected. -handle_info({timeout, _Timer, retry_delivery}, State = #state{conn_pid = undefined}) -> - {noreply, ensure_stats_timer(State#state{retry_timer = undefined})}; +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) -> - {noreply, ensure_stats_timer(retry_delivery(false, 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) -> - {noreply, ensure_stats_timer(expire_awaiting_rel(State#state{await_rel_timer = undefined}))}; +handle_info({timeout, Timer, check_awaiting_rel}, State = #state{await_rel_timer = Timer}) -> + noreply(expire_awaiting_rel(State#state{await_rel_timer = undefined})); -handle_info({timeout, _Timer, expired}, State) -> - ?LOG(info, "Expired, shutdown now.", [], State), +handle_info({timeout, Timer, emit_stats}, State = #state{client_id = ClientId, stats_timer = Timer}) -> + true = emqx_sm:set_session_stats(ClientId, stats(State)), + {noreply, State#state{stats_timer = undefined}, hibernate}; + +handle_info({timeout, Timer, expired}, State = #state{expiry_timer = Timer}) -> + ?LOG(info, "expired, shutdown now:(", [], State), shutdown(expired, State); handle_info({'EXIT', ConnPid, Reason}, State = #state{clean_start = true, conn_pid = ConnPid}) -> - {stop, Reason, State}; + {stop, Reason, State#state{conn_pid = undefined}}; handle_info({'EXIT', ConnPid, Reason}, State = #state{expiry_interval = 0, conn_pid = ConnPid}) -> - {stop, Reason, State}; + {stop, Reason, State#state{conn_pid = undefined}}; handle_info({'EXIT', ConnPid, _Reason}, State = #state{clean_start = false, conn_pid = ConnPid}) -> {noreply, ensure_expire_timer(State#state{conn_pid = undefined})}; handle_info({'EXIT', OldPid, _Reason}, State = #state{old_conn_pid = OldPid}) -> %% ignore - {noreply, State#state{old_conn_pid = undefined}, hibernate}; + {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", + ?LOG(error, "Unexpected EXIT: conn_pid=~p, exit_pid=~p, reason=~p", [ConnPid, Pid, Reason], State), {noreply, State}; -handle_info(emit_stats, State = #state{client_id = ClientId}) -> - emqx_sm:set_session_stats(ClientId, stats(State)), - {noreply, State#state{stats_timer = undefined}, hibernate}; - handle_info(Info, State) -> emqx_logger:error("[Session] unexpected info: ~p", [Info]), {noreply, State}. -terminate(Reason, #state{client_id = ClientId}) -> +terminate(Reason, #state{client_id = ClientId, conn_pid = ConnPid}) -> emqx_hooks:run('session.terminated', [#{client_id => ClientId}, Reason]), - %%TODO: notify conn_pid to shutdown? + %% Ensure to shutdown the connection + if + ConnPid =/= undefined -> + ConnPid ! {shutdown, Reason}; + true -> ok + end, emqx_sm:unregister_session(ClientId). code_change(_OldVsn, State, _Extra) -> @@ -611,7 +623,7 @@ kick(ClientId, OldPid, Pid) -> unlink(OldPid), OldPid ! {shutdown, conflict, {ClientId, Pid}}, %% Clean noproc - receive {'EXIT', OldPid, _} -> ok after 0 -> ok end. + receive {'EXIT', OldPid, _} -> ok after 1 -> ok end. %%------------------------------------------------------------------------------ %% Replay or Retry Delivery @@ -622,30 +634,37 @@ retry_delivery(Force, State = #state{inflight = Inflight}) -> case emqx_inflight:is_empty(Inflight) of true -> State; false -> - Msgs = lists:sort(sortfun(inflight), emqx_inflight:values(Inflight)), - retry_delivery(Force, Msgs, os:timestamp(), State) + InflightMsgs = lists:sort(sortfun(inflight), emqx_inflight:values(Inflight)), + retry_delivery(Force, InflightMsgs, os:timestamp(), State) end. -retry_delivery(_Force, [], _Now, State = #state{retry_interval = Interval}) -> - State#state{retry_timer = emqx_misc:start_timer(Interval, retry_delivery)}; +retry_delivery(_Force, [], _Now, State) -> + %% Retry again... + ensure_retry_timer(State); retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now, State = #state{inflight = Inflight, retry_interval = Interval}) -> - Diff = timer:now_diff(Now, Ts) div 1000, %% micro -> ms + %% Microseconds -> MilliSeconds + Diff = timer:now_diff(Now, Ts) div 1000, if Force orelse (Diff >= Interval) -> - case {Type, Msg0} of - {publish, {PacketId, Msg}} -> - redeliver({PacketId, Msg}, State), - Inflight1 = emqx_inflight:update(PacketId, {publish, {PacketId, Msg}, Now}, Inflight), - retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}); - {pubrel, PacketId} -> - redeliver({pubrel, PacketId}, State), - Inflight1 = emqx_inflight:update(PacketId, {pubrel, PacketId, Now}, Inflight), - retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}) - end; + Inflight1 = case {Type, Msg0} of + {publish, {PacketId, Msg}} -> + case emqx_message:is_expired(Msg) of + true -> + 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 -> - State#state{retry_timer = emqx_misc:start_timer(Interval - Diff, retry_delivery)} + ensure_retry_timer(Interval - Diff, State) end. %%------------------------------------------------------------------------------ @@ -662,16 +681,16 @@ expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) -> expire_awaiting_rel([], _Now, State) -> State#state{await_rel_timer = undefined}; -expire_awaiting_rel([{PacketId, Msg = #message{timestamp = TS}} | Msgs], - Now, State = #state{awaiting_rel = AwaitingRel, - await_rel_timeout = Timeout}) -> +expire_awaiting_rel([{PacketId, Msg = #message{timestamp = TS}} | Msgs], Now, + State = #state{awaiting_rel = AwaitingRel, await_rel_timeout = Timeout}) -> case (timer:now_diff(Now, TS) div 1000) of Diff when Diff >= Timeout -> - ?LOG(warning, "Dropped Qos2 Message for await_rel_timeout: ~p", [Msg], State), emqx_metrics:inc('messages/qos2/dropped'), + ?LOG(warning, "Dropped message for await_rel_timeout: ~p", + [emqx_message:format(Msg)], State), expire_awaiting_rel(Msgs, Now, State#state{awaiting_rel = maps:remove(PacketId, AwaitingRel)}); Diff -> - State#state{await_rel_timer = emqx_misc:start_timer(Timeout - Diff, check_awaiting_rel)} + ensure_await_rel_timer(Timeout - Diff, State) end. %%------------------------------------------------------------------------------ @@ -728,12 +747,10 @@ dispatch(Msg = #message{qos = ?QOS0}, State) -> dispatch(Msg = #message{qos = QoS}, State = #state{next_pkt_id = PacketId, inflight = Inflight}) when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 -> case emqx_inflight:is_full(Inflight) of - true -> - enqueue_msg(Msg, State); + true -> enqueue_msg(Msg, State); false -> deliver(PacketId, Msg, State), - %% TODO inc_stats?? - await(PacketId, Msg, next_pkt_id(inc_stats(deliver, State))) + await(PacketId, Msg, inc_stats(deliver, next_pkt_id(State))) end. enqueue_msg(Msg, State = #state{mqueue = Q}) -> @@ -760,15 +777,10 @@ deliver(PacketId, Msg, #state{conn_pid = ConnPid, binding = remote}) -> %% Awaiting ACK for QoS1/QoS2 Messages %%------------------------------------------------------------------------------ -await(PacketId, Msg, State = #state{inflight = Inflight, - retry_timer = RetryTimer, - retry_interval = Interval}) -> - %% Start retry timer if the Inflight is still empty - State1 = case RetryTimer == undefined of - true -> State#state{retry_timer = emqx_misc:start_timer(Interval, retry_delivery)}; - false -> State - end, - State1#state{inflight = emqx_inflight:insert(PacketId, {publish, {PacketId, Msg}, os:timestamp()}, Inflight)}. +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, inflight = Inflight}) -> case emqx_inflight:lookup(PacketId, Inflight) of @@ -776,7 +788,7 @@ acked(puback, PacketId, State = #state{client_id = ClientId, inflight = Infligh emqx_hooks:run('message.acked', [#{client_id =>ClientId}], Msg), State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}; none -> - ?LOG(warning, "Duplicated PUBACK Packet: ~p", [PacketId], State), + ?LOG(warning, "Duplicated PUBACK PacketId ~w", [PacketId], State), State end; @@ -786,10 +798,10 @@ acked(pubrec, PacketId, State = #state{client_id = ClientId, inflight = Infligh emqx_hooks:run('message.acked', [ClientId], Msg), State#state{inflight = emqx_inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight)}; {value, {pubrel, PacketId, _Ts}} -> - ?LOG(warning, "Duplicated PUBREC Packet: ~p", [PacketId], State), + ?LOG(warning, "Duplicated PUBREC PacketId ~w", [PacketId], State), State; none -> - ?LOG(warning, "Unexpected PUBREC Packet: ~p", [PacketId], State), + ?LOG(warning, "Unexpected PUBREC PacketId ~w", [PacketId], State), State end; @@ -819,28 +831,42 @@ dequeue2(State = #state{mqueue = Q}) -> dequeue(dispatch(Msg, State#state{mqueue = Q1})) end. - %%------------------------------------------------------------------------------ %% Ensure timers ensure_await_rel_timer(State = #state{await_rel_timer = undefined, await_rel_timeout = Timeout}) -> - State#state{await_rel_timer = emqx_misc:start_timer(Timeout, check_awaiting_rel)}; + 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{retry_timer = undefined, retry_interval = Interval}) -> + 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 -> State#state{expiry_timer = emqx_misc:start_timer(Interval, expired)}; ensure_expire_timer(State) -> State. -%%------------------------------------------------------------------------------ -%% Reset Dup - -reset_dup(Msg) -> - emqx_message:unset_flag(dup, Msg). +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. %%------------------------------------------------------------------------------ -%% Next Msg Id +%% Next Packet Id next_pkt_id(State = #state{next_pkt_id = 16#FFFF}) -> State#state{next_pkt_id = 1}; @@ -849,26 +875,28 @@ next_pkt_id(State = #state{next_pkt_id = Id}) -> State#state{next_pkt_id = Id + 1}. %%------------------------------------------------------------------------------ -%% Ensure stats timer - -ensure_stats_timer(State = #state{enable_stats = true, stats_timer = undefined}) -> - State#state{stats_timer = erlang:send_after(30000, self(), emit_stats)}; -ensure_stats_timer(State) -> - State. +%% Inc stats inc_stats(deliver, State = #state{deliver_stats = I}) -> State#state{deliver_stats = I + 1}; inc_stats(enqueue, State = #state{enqueue_stats = I}) -> State#state{enqueue_stats = I + 1}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Helper functions +reply({Reply, State}) -> + reply(Reply, State). + reply(Reply, State) -> - {reply, Reply, State}. + {reply, Reply, ensure_stats_timer(State)}. + +noreply(State) -> + {noreply, ensure_stats_timer(State)}. shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. -%% TODO: maybe_gc(State) -> State. +%% TODO: GC Policy and Shutdown Policy +%% maybe_gc(State) -> State. From 553a60cdec2cb3976fd54a3714751df0e75df255 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 18:31:37 +0800 Subject: [PATCH 191/520] Update 'Message Expiry Interval' property before delivering a PUBLISH --- src/emqx_protocol.erl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 01971258c..da7ee88b8 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -453,10 +453,11 @@ deliver({connack, ReasonCode}, PState) -> deliver({connack, ReasonCode, SP}, PState) -> send(?CONNACK_PACKET(ReasonCode, SP), PState); -deliver({publish, PacketId, Msg}, PState = #pstate{is_bridge = IsBridge, mountpoint = MountPoint}) -> +deliver({publish, PacketId, Msg}, PState = #pstate{is_bridge = IsBridge, mountpoint = MountPoint}) -> _ = emqx_hooks:run('message.delivered', [credentials(PState)], Msg), - Msg1 = emqx_mountpoint:unmount(MountPoint, clean_retain(IsBridge, Msg)), - send(emqx_packet:from_message(PacketId, Msg1), PState); + Msg1 = emqx_message:update_expiry(Msg), + Msg2 = emqx_mountpoint:unmount(MountPoint, clean_retain(IsBridge, Msg1)), + send(emqx_packet:from_message(PacketId, Msg2), PState); deliver({puback, PacketId, ReasonCode}, PState) -> send(?PUBACK_PACKET(PacketId, ReasonCode), PState); From c52f5a8525615fbe904c28e1c578f4295be47c38 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 18:32:43 +0800 Subject: [PATCH 192/520] Update the emqx_mqueue SUITE --- test/emqx_mqueue_SUITE.erl | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/test/emqx_mqueue_SUITE.erl b/test/emqx_mqueue_SUITE.erl index 575478f90..6a5893566 100644 --- a/test/emqx_mqueue_SUITE.erl +++ b/test/emqx_mqueue_SUITE.erl @@ -24,12 +24,12 @@ -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() -> [t_in, t_in_qos0, t_out, t_simple_mqueue, t_infinity_simple_mqueue, + t_priority_mqueue, t_infinity_priority_mqueue]. t_in(_) -> Opts = #{type => simple, max_len => 5, store_qos0 => true}, - Q = ?Q:new(<<"testQ">>, Opts), + Q = ?Q:init(Opts), ?assert(?Q:is_empty(Q)), Q1 = ?Q:in(#message{}, Q), ?assertEqual(1, ?Q:len(Q1)), @@ -42,7 +42,7 @@ t_in(_) -> t_in_qos0(_) -> Opts = #{type => simple, max_len => 5, store_qos0 => false}, - Q = ?Q:new(<<"testQ">>, Opts), + Q = ?Q:init(Opts), Q1 = ?Q:in(#message{qos = 0}, Q), ?assert(?Q:is_empty(Q1)), Q2 = ?Q:in(#message{qos = 0}, Q1), @@ -50,7 +50,7 @@ t_in_qos0(_) -> t_out(_) -> Opts = #{type => simple, max_len => 5, store_qos0 => true}, - Q = ?Q:new(<<"testQ">>, Opts), + Q = ?Q:init(Opts), {empty, Q} = ?Q:out(Q), Q1 = ?Q:in(#message{}, Q), {Value, Q2} = ?Q:out(Q1), @@ -59,10 +59,9 @@ t_out(_) -> t_simple_mqueue(_) -> Opts = #{type => simple, max_len => 3, store_qos0 => false}, - Q = ?Q:new("simple_queue", Opts), + Q = ?Q:init(Opts), ?assertEqual(simple, ?Q:type(Q)), ?assertEqual(3, ?Q:max_len(Q)), - ?assertEqual(<<"simple_queue">>, ?Q:name(Q)), ?assert(?Q:is_empty(Q)), Q1 = ?Q:in(#message{qos = 1, payload = <<"1">>}, Q), Q2 = ?Q:in(#message{qos = 1, payload = <<"2">>}, Q1), @@ -75,7 +74,7 @@ t_simple_mqueue(_) -> t_infinity_simple_mqueue(_) -> Opts = #{type => simple, max_len => 0, store_qos0 => false}, - Q = ?Q:new("infinity_simple_queue", Opts), + Q = ?Q:init(Opts), ?assert(?Q:is_empty(Q)), ?assertEqual(0, ?Q:max_len(Q)), Qx = lists:foldl(fun(I, AccQ) -> @@ -88,10 +87,9 @@ t_infinity_simple_mqueue(_) -> t_priority_mqueue(_) -> Opts = #{type => priority, max_len => 3, store_qos0 => false}, - Q = ?Q:new("priority_queue", Opts), + Q = ?Q:init(Opts), ?assertEqual(priority, ?Q:type(Q)), ?assertEqual(3, ?Q:max_len(Q)), - ?assertEqual(<<"priority_queue">>, ?Q:name(Q)), ?assert(?Q:is_empty(Q)), Q1 = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q), Q2 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q1), @@ -109,7 +107,7 @@ t_priority_mqueue(_) -> t_infinity_priority_mqueue(_) -> Opts = #{type => priority, max_len => 0, store_qos0 => false}, - Q = ?Q:new("infinity_priority_queue", Opts), + Q = ?Q:init(Opts), ?assertEqual(0, ?Q:max_len(Q)), Qx = lists:foldl(fun(I, AccQ) -> AccQ1 = @@ -117,4 +115,17 @@ t_infinity_priority_mqueue(_) -> ?Q:in(#message{topic = <<"t">>, qos = 1, payload = iolist_to_binary([I])}, AccQ1) end, Q, lists:seq(1, 255)), ?assertEqual(510, ?Q:len(Qx)), - ?assertEqual([{len, 510}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)). \ No newline at end of file + ?assertEqual([{len, 510}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)). + +t_priority_mqueue2(_) -> + Opts = #{type => priority, max_length => 2, store_qos0 => false}, + Q = ?Q:init("priority_queue2_test", 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), + {{value, _Val}, Q5} = ?Q:out(Q4), + 3 = ?Q:len(Q5). + From 826daace61888f5d560a899f07a4fae693a7ba6f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 18:44:58 +0800 Subject: [PATCH 193/520] Align the state record --- src/emqx_session.erl | 102 +++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index eec142351..b291bd1fa 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -68,75 +68,74 @@ %% Clean Start Flag clean_start = false :: boolean(), + %% Client Binding: local | remote + binding = local :: local | remote, - %% Client Binding: local | remote - binding = local :: local | remote, + %% ClientId: Identifier of Session + client_id :: binary(), - %% ClientId: Identifier of Session - client_id :: binary(), + %% Username + username :: binary() | undefined, - %% Username - username :: binary() | undefined, + %% Connection pid binding with session + conn_pid :: pid(), - %% Connection pid binding with session - conn_pid :: pid(), + %% Old Connection Pid that has been kickout + old_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(), - %% Next packet id of the session - next_pkt_id = 1 :: emqx_mqtt_types:packet_id(), + %% Max subscriptions + max_subscriptions :: non_neg_integer(), - %% Max subscriptions - max_subscriptions :: non_neg_integer(), + %% Client’s Subscriptions. + subscriptions :: map(), - %% Client’s Subscriptions. - subscriptions :: map(), + %% Upgrade QoS? + upgrade_qos = false :: boolean(), - %% Upgrade QoS? - upgrade_qos = false :: boolean(), + %% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked. + inflight :: emqx_inflight:inflight(), - %% 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(), - %% Max Inflight Size. DEPRECATED: Get from inflight - %% max_inflight = 32 :: non_neg_integer(), + %% Retry interval for redelivering QoS1/2 messages + retry_interval = 20000 :: timeout(), - %% Retry interval for redelivering QoS1/2 messages - retry_interval = 20000 :: timeout(), + %% Retry Timer + retry_timer :: reference() | undefined, - %% Retry Timer - retry_timer :: reference() | undefined, + %% 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(), - %% 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. + awaiting_rel :: map(), - %% Client -> Broker: Inflight QoS2 messages received from client and waiting for pubrel. - awaiting_rel :: map(), + %% Max Packets Awaiting PUBREL + max_awaiting_rel = 100 :: non_neg_integer(), - %% Max Packets Awaiting PUBREL - max_awaiting_rel = 100 :: non_neg_integer(), + %% Awaiting PUBREL Timeout + await_rel_timeout = 20000 :: timeout(), - %% Awaiting PUBREL Timeout - await_rel_timeout = 20000 :: timeout(), + %% Awaiting PUBREL Timer + await_rel_timer :: reference() | undefined, - %% Awaiting PUBREL Timer - await_rel_timer :: reference() | undefined, + %% Session Expiry Interval + expiry_interval = 7200000 :: timeout(), - %% Session Expiry Interval - expiry_interval = 7200000 :: timeout(), + %% Expired Timer + expiry_timer :: reference() | undefined, - %% Expired Timer - expiry_timer :: reference() | undefined, + %% Enable Stats + enable_stats :: boolean(), - %% Enable Stats - enable_stats :: boolean(), - - %% Stats timer - stats_timer :: reference() | undefined, + %% Stats timer + stats_timer :: reference() | undefined, %% Deliver stats deliver_stats = 0, @@ -144,10 +143,9 @@ %% Enqueue stats enqueue_stats = 0, - - %% Created at - created_at :: erlang:timestamp() - }). + %% Created at + created_at :: erlang:timestamp() + }). -define(TIMEOUT, 60000). From cae673cf5eba9cffa5128940862278fe91616077 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 18:59:47 +0800 Subject: [PATCH 194/520] Fix update_expiry/1 function_clause --- src/emqx_message.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 9ff8e7511..311bd58dd 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -120,7 +120,9 @@ update_expiry(Msg = #message{headers = #{'Message-Expiry-Interval' := Interval}, Elapsed when Elapsed > 0 -> set_header('Message-Expiry-Interval', max(1, Interval - (Elapsed div 1000)), Msg); _ -> Msg - end. + end; + +update_expiry(Msg) -> Msg. %% MilliSeconds elapsed(Since) -> From 2fc41b69352ca92250d13150e9ed831a8fb05ae4 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 30 Aug 2018 19:09:22 +0800 Subject: [PATCH 195/520] fix reason code name for mqtt 4 --- src/emqx_client.erl | 5 +++-- src/emqx_protocol.erl | 2 +- src/emqx_reason_codes.erl | 11 ++++++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/emqx_client.erl b/src/emqx_client.erl index e6aac5d43..82e331abd 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -592,8 +592,8 @@ waiting_for_connack(cast, ?CONNACK_PACKET(?RC_SUCCESS, waiting_for_connack(cast, ?CONNACK_PACKET(ReasonCode, _SessPresent, - Properties), State) -> - Reason = emqx_reason_codes:name(ReasonCode), + Properties), State = #state{ proto_ver = ProtoVer}) -> + Reason = emqx_reason_codes:name(ReasonCode, ProtoVer), case take_call(connect, State) of {value, #call{from = From}, _State} -> Reply = {error, {Reason, Properties}}, @@ -1082,6 +1082,7 @@ receive_loop(Bytes, State = #state{parse_state = ParseState}) -> {error, Reason} -> {stop, Reason}; {'EXIT', Error} -> + io:format("client stop"), {stop, Error} end. diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index da7ee88b8..9ee24609f 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -412,7 +412,7 @@ connack({ReasonCode, PState = #pstate{proto_ver = ProtoVer}}) -> true -> emqx_reason_codes:compat(connack, ReasonCode) end}, PState), - {error, emqx_reason_codes:name(ReasonCode), PState}. + {error, emqx_reason_codes:name(ReasonCode, ProtoVer), PState}. %%------------------------------------------------------------------------------ %% Publish Message -> Broker diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index f300d675d..0cc52acbb 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -17,9 +17,18 @@ -include("emqx_mqtt.hrl"). --export([name/1, text/1]). +-export([name/2, text/1]). -export([compat/2]). +name(I, Ver) when Ver >= ?MQTT_PROTO_V5 -> + name(I); +name(0, _Ver) -> connection_acceptd; +name(1, _Ver) -> unacceptable_protocol_version; +name(2, _Ver) -> client_identifier_not_valid; +name(3, _Ver) -> server_unavaliable; +name(4, _Ver) -> malformed_username_or_password; +name(5, _Ver) -> unauthorized_client. + name(16#00) -> success; name(16#01) -> granted_qos1; name(16#02) -> granted_qos2; From e6d0329663520752eed62e1dc11aa981485124be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 30 Aug 2018 19:55:44 +0800 Subject: [PATCH 196/520] Fix test suites bug, and add emqx_message ct to Makefile --- Makefile | 4 ++-- test/emqx_message_SUITE.erl | 15 ++++++++++++++- test/emqx_mqueue_SUITE.erl | 6 +++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 08e3c3dd0..aba0d4ac4 100644 --- a/Makefile +++ b/Makefile @@ -32,12 +32,12 @@ TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose -# CT_SUITES = emqx_stats +# CT_SUITES = emqx_mqueue ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat CT_SUITES = emqx emqx_connection emqx_session emqx_access emqx_base62 emqx_broker emqx_client emqx_cm emqx_frame emqx_guid emqx_inflight \ emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ - emqx_mqtt_compat emqx_mqtt_properties emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ + emqx_mqtt_compat emqx_mqtt_properties emqx_mqueue emqx_message emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_zone emqx_mountpoint CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqxct@127.0.0.1 diff --git a/test/emqx_message_SUITE.erl b/test/emqx_message_SUITE.erl index da39ef882..c2ca0f0aa 100644 --- a/test/emqx_message_SUITE.erl +++ b/test/emqx_message_SUITE.erl @@ -28,7 +28,8 @@ all() -> message_make, message_flag, message_header, - message_format + message_format, + message_expired ]. message_make(_) -> @@ -62,4 +63,16 @@ message_header(_) -> message_format(_) -> io:format("~s", [emqx_message:format(emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>))]). +message_expired(_) -> + Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), + Msg1 = emqx_message:set_headers(#{'Message-Expiry-Interval' => 1}, Msg), + timer:sleep(500), + ?assertNot(emqx_message:is_expired(Msg1)), + {ok, 1} = emqx_message:check_expiry(Msg1), + timer:sleep(600), + ?assert(emqx_message:is_expired(Msg1)), + expired = emqx_message:check_expiry(Msg1), + timer:sleep(1000), + Msg2 = emqx_message:update_expiry(Msg1), + ?assertEqual(1, emqx_message:get_header('Message-Expiry-Interval', Msg2)). diff --git a/test/emqx_mqueue_SUITE.erl b/test/emqx_mqueue_SUITE.erl index 6a5893566..8a1ca5201 100644 --- a/test/emqx_mqueue_SUITE.erl +++ b/test/emqx_mqueue_SUITE.erl @@ -86,7 +86,7 @@ t_infinity_simple_mqueue(_) -> ?assertEqual(<<1>>, V#message.payload). t_priority_mqueue(_) -> - Opts = #{type => priority, max_len => 3, store_qos0 => false}, + Opts = #{type => priority, max_len => 3, priorities => [{<<"t1">>, 1}, {<<"t2">>, 2}, {<<"t3">>, 3}], store_qos0 => false}, Q = ?Q:init(Opts), ?assertEqual(priority, ?Q:type(Q)), ?assertEqual(3, ?Q:max_len(Q)), @@ -103,10 +103,10 @@ t_priority_mqueue(_) -> ?assertEqual(5, ?Q:len(Q6)), {{value, Msg}, Q7} = ?Q:out(Q6), ?assertEqual(4, ?Q:len(Q7)), - ?assertEqual(<<"t1">>, Msg#message.topic). + ?assertEqual(<<"t3">>, Msg#message.topic). t_infinity_priority_mqueue(_) -> - Opts = #{type => priority, max_len => 0, store_qos0 => false}, + Opts = #{type => priority, max_len => 0, priorities => [{<<"t">>, 1}, {<<"t1">>, 2}], store_qos0 => false}, Q = ?Q:init(Opts), ?assertEqual(0, ?Q:max_len(Q)), Qx = lists:foldl(fun(I, AccQ) -> From dd7f0dec3c54d056d5e203960b14bed024487e80 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 21:01:09 +0800 Subject: [PATCH 197/520] Add 'messages/qos2/expired' counter --- src/emqx_metrics.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 6d17f6648..0a9ad67aa 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -73,6 +73,7 @@ {counter, 'messages/qos1/received'}, % QoS1 Messages received {counter, 'messages/qos1/sent'}, % QoS1 Messages sent {counter, 'messages/qos2/received'}, % QoS2 Messages received + {counter, 'messages/qos2/expired'}, % QoS2 Messages expired {counter, 'messages/qos2/sent'}, % QoS2 Messages sent {counter, 'messages/qos2/dropped'}, % QoS2 Messages dropped {gauge, 'messages/retained'}, % Messagea retained From b0fad7a86d3466d5836cea330bd80a648c0bf41a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 21:02:01 +0800 Subject: [PATCH 198/520] Change the default value of 'zone.external.await_rel_timeout' to 300s --- etc/emqx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index cb6e91b96..5975ae542 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -614,7 +614,7 @@ zone.external.max_awaiting_rel = 100 ## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. ## ## Value: Duration -zone.external.await_rel_timeout = 60s +zone.external.await_rel_timeout = 300s ## Default session expiry interval for MQTT V3.1.1 connections. ## From bbb58dad687a35a5a3164a38b3126c679cd834be Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 21:02:14 +0800 Subject: [PATCH 199/520] Change the default value of 'zone.external.await_rel_timeout' to 300s --- priv/emqx.schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index a80aa8ee6..07ff5ce6f 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -774,7 +774,7 @@ end}. %% @doc Awaiting PUBREL timeout {mapping, "zone.$name.await_rel_timeout", "emqx.zones", [ - {default, "60s"}, + {default, "300s"}, {datatype, {duration, ms}} ]}. From 8fcfcfb8603a5a11308946714023445df1f9e696 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 30 Aug 2018 21:12:41 +0800 Subject: [PATCH 200/520] update compact test cases except keepalive --- test/emqx_mqtt_compat_SUITE.erl | 87 +++++++++++++-------------------- 1 file changed, 33 insertions(+), 54 deletions(-) diff --git a/test/emqx_mqtt_compat_SUITE.erl b/test/emqx_mqtt_compat_SUITE.erl index 0edbd148e..452b2a821 100644 --- a/test/emqx_mqtt_compat_SUITE.erl +++ b/test/emqx_mqtt_compat_SUITE.erl @@ -33,14 +33,13 @@ all() -> [basic_test, - retained_message_test, will_message_test, zero_length_clientid_test, offline_message_queueing_test, overlapping_subscriptions_test, keepalive_test, redelivery_on_reconnect_test, - subscribe_failure_test, + %% subscribe_failure_test, dollar_topics_test]. init_per_suite(Config) -> @@ -57,7 +56,7 @@ receive_messages(0, Msgs) -> Msgs; receive_messages(Count, Msgs) -> receive - {public, Msg} -> + {publish, Msg} -> receive_messages(Count-1, [Msg|Msgs]); _Other -> receive_messages(Count, Msgs) @@ -69,40 +68,16 @@ basic_test(_Config) -> Topic = nth(1, ?TOPICS), ct:print("Basic test starting"), {ok, C, _} = emqx_client:start_link(), - {ok, _, [0]} = emqx_client:subscribe(C, Topic, qos2), - ok = emqx_client:publish(C, Topic, <<"qos 0">>), - {ok, _} = emqx_client:publish(C, Topic, <<"qos 1">>, 1), + {ok, _, [2]} = emqx_client:subscribe(C, Topic, qos2), {ok, _} = emqx_client:publish(C, Topic, <<"qos 2">>, 2), - ok = emqx_client:disconnect(C), - ?assertEqual(3, length(receive_messages(3))). - -retained_message_test(_Config) -> - ct:print("Retained message test starting"), - - %% Retained messages - {ok, C1, _} = emqx_client:start_link([{clean_start, true}]), - ok = emqx_client:publish(C1, nth(1, ?TOPICS), <<"qos 0">>, [{qos, 0}, {retain, true}]), - {ok, _} = emqx_client:publish(C1, nth(3, ?TOPICS), <<"qos 1">>, [{qos, 1}, {retain, true}]), - {ok, _} = emqx_client:publish(C1, nth(4, ?TOPICS), <<"qos 2">>, [{qos, 2}, {retain, true}]), - timer:sleep(10), - {ok, #{}, [0]} = emqx_client:subscribe(C1, nth(6, ?WILD_TOPICS), 2), - ok = emqx_client:disconnect(C1), - ?assertEqual(3, length(receive_messages(10))), - - %% Clear retained messages - {ok, C2, _} = emqx_client:start_link([{clean_start, true}]), - ok = emqx_client:publish(C2, nth(2, ?TOPICS), <<"">>, [{qos, 0}, {retain, true}]), - {ok, _} = emqx_client:publish(C2, nth(3, ?TOPICS), <<"">>, [{qos, 1}, {retain, true}]), - {ok, _} = emqx_client:publish(C2, nth(4, ?TOPICS), <<"">>, [{qos, 2}, {retain, true}]), - timer:sleep(10), %% wait for QoS 2 exchange to be completed - {ok, _, [0]} = emqx_client:subscribe(C2, nth(6, ?WILD_TOPICS), 2), - timer:sleep(10), - ok = emqx_client:disconnect(), - ?assertEqual(0, length(receive_messages(3))). + {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). will_message_test(_Config) -> {ok, C1, _} = emqx_client:start_link([{clean_start, true}, - {will_topic = nth(3, ?TOPICS)}, + {will_topic, nth(3, ?TOPICS)}, {will_payload, <<"client disconnected">>}, {keepalive, 2}]), {ok, C2, _} = emqx_client:start_link(), @@ -110,14 +85,18 @@ will_message_test(_Config) -> timer:sleep(10), ok = emqx_client:stop(C1), timer:sleep(5), - ok = emqx_client:disconnect(C2), ?assertEqual(1, length(receive_messages(1))), + ok = emqx_client:disconnect(C2), ct:print("Will message test succeeded"). zero_length_clientid_test(_Config) -> ct:print("Zero length clientid test starting"), - {error, _} = emqx_client:start_link([{clean_start, false}, - {client_id, <<>>}]), + + %% TODO: There are some controversies on the situation when + %% clean_start flag is true and clientid is zero length. + + %% {error, _} = emqx_client:start_link([{clean_start, false}, + %% {client_id, <<>>}]), {ok, _, _} = emqx_client:start_link([{clean_start, true}, {client_id, <<>>}]), ct:print("Zero length clientid test succeeded"). @@ -129,7 +108,7 @@ offline_message_queueing_test(_) -> ok = emqx_client:disconnect(C1), {ok, C2, _} = emqx_client:start_link([{clean_start, true}, {client_id, <<"c2">>}]), - + ok = emqx_client:publish(C2, nth(2, ?TOPICS), <<"qos 0">>, 0), {ok, _} = emqx_client:publish(C2, nth(3, ?TOPICS), <<"qos 1">>, 1), {ok, _} = emqx_client:publish(C2, nth(4, ?TOPICS), <<"qos 2">>, 2), @@ -147,9 +126,9 @@ overlapping_subscriptions_test(_) -> {nth(1, ?WILD_TOPICS), 1}]), timer:sleep(10), {ok, _} = emqx_client:publish(C, nth(4, ?TOPICS), <<"overlapping topic filters">>, 2), - time:sleep(10), - emqx_client:disconnect(C), - Num = receive_messages(2), + timer:sleep(10), + + Num = length(receive_messages(2)), ?assert(lists:member(Num, [1, 2])), if Num == 1 -> @@ -159,7 +138,8 @@ overlapping_subscriptions_test(_) -> ct:print("This server is publishing one message per each matching overlapping subscription."); true -> ok - end. + end, + emqx_client:disconnect(C). keepalive_test(_) -> ct:print("Keepalive test starting"), @@ -168,14 +148,13 @@ keepalive_test(_) -> {will_topic, nth(5, ?TOPICS)}, {will_payload, <<"keepalive expiry">>}]), ok = emqx_client:pause(C1), - {ok, C2, _} = emqx_client:start_link([{clean_start, true}, {keepalive, 0}]), {ok, _, [2]} = emqx_client:subscribe(C2, nth(5, ?TOPICS), 2), timer:sleep(15000), - ok = emqx_client:disconnect(C2), ?assertEqual(1, length(receive_messages(1))), - ct:print("Keepalive test succeeded"). + ct:print("Keepalive test succeeded"), + ok = emqx_client:disconnect(C2). redelivery_on_reconnect_test(_) -> ct:print("Redelivery on reconnect test starting"), @@ -188,7 +167,7 @@ redelivery_on_reconnect_test(_) -> [{qos, 1}, {retain, false}]), {ok, _} = emqx_client:publish(C1, nth(4, ?TOPICS), <<>>, [{qos, 2}, {retain, false}]), - time:sleep(10), + timer:sleep(10), ok = emqx_client:disconnect(C1), ?assertEqual(0, length(receive_messages(2))), {ok, C2, _} = emqx_client:start_link([{clean_start, false}, @@ -197,20 +176,20 @@ redelivery_on_reconnect_test(_) -> ok = emqx_client:disconnect(C2), ?assertEqual(2, length(receive_messages(2))). -subscribe_failure_test(_) -> - ct:print("Subscribe failure test starting"), - {ok, C, _} = emqx_client:start_link([]), - {ok, _, [16#80]} = emqx_client:subscribe(C, <<"$SYS/#">>, 2), - timer:sleep(10), - ct:print("Subscribe failure test succeeded"). +%% subscribe_failure_test(_) -> +%% 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"), {ok, C, _} = emqx_client:start_link([{clean_start, true}, {keepalive, 0}]), - {ok, _, [2]} = emqx_client:subscribe(C, nth(6, ?WILD_TOPICS), 2), - {ok, _} = emqx_client:publish(C, <<"$", (nth(2, ?TOPICS))>>, - <<"">>, [{qos, 1}, {retain, false}]), + {ok, _, [1]} = emqx_client:subscribe(C, nth(6, ?WILD_TOPICS), 1), + {ok, _} = emqx_client:publish(C, << <<"$">>/binary, (nth(2, ?TOPICS))/binary>>, + <<"test">>, [{qos, 1}, {retain, false}]), timer:sleep(10), ?assertEqual(0, length(receive_messages(1))), ok = emqx_client:disconnect(C), From 78a8ccd0f2bdad31aecac0790410cbbae4de5947 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 21:17:20 +0800 Subject: [PATCH 201/520] Only store packet_id and timestamp for qos2 message --- src/emqx_session.erl | 53 ++++++++++++++++---------------------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index b291bd1fa..65bce85aa 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -376,7 +376,7 @@ handle_call({discard, ConnPid}, _From, State = #state{conn_pid = OldConnPid}) -> {stop, {shutdown, conflict}, ok, State}; %% PUBLISH: -handle_call({publish, PacketId, Msg = #message{qos = ?QOS_2}}, _From, +handle_call({publish, PacketId, Msg = #message{qos = ?QOS_2, timestamp = Ts}}, _From, State = #state{awaiting_rel = AwaitingRel}) -> reply(case is_awaiting_full(State) of false -> @@ -384,13 +384,12 @@ handle_call({publish, PacketId, Msg = #message{qos = ?QOS_2}}, _From, true -> {{error, ?RC_PACKET_IDENTIFIER_IN_USE}, State}; false -> - State1 = State#state{awaiting_rel = maps:put(PacketId, Msg, AwaitingRel)}, + State1 = State#state{awaiting_rel = maps:put(PacketId, Ts, AwaitingRel)}, {emqx_broker:publish(Msg), ensure_await_rel_timer(State1)} end; true -> emqx_metrics:inc('messages/qos2/dropped'), - ?LOG(warning, "Dropped message for too many awaiting_rel: ~p", - [emqx_message:format(Msg)], State), + ?LOG(warning, "Dropped qos2 packet ~w for too many awaiting_rel", [PacketId], State), {{error, ?RC_RECEIVE_MAXIMUM_EXCEEDED}, State} end); @@ -408,7 +407,7 @@ handle_call({pubrec, PacketId, _ReasonCode}, _From, State = #state{inflight = In %% PUBREL: handle_call({pubrel, PacketId, _ReasonCode}, _From, State = #state{awaiting_rel = AwaitingRel}) -> reply(case maps:take(PacketId, AwaitingRel) of - {_, AwaitingRel1} -> + {_Ts, AwaitingRel1} -> {ok, State#state{awaiting_rel = AwaitingRel1}}; error -> emqx_metrics:inc('packets/pubrel/missed'), @@ -639,8 +638,9 @@ retry_delivery(Force, State = #state{inflight = Inflight}) -> case emqx_inflight:is_empty(Inflight) of true -> State; false -> - InflightMsgs = lists:sort(sortfun(inflight), emqx_inflight:values(Inflight)), - retry_delivery(Force, InflightMsgs, os:timestamp(), State) + 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) -> @@ -650,9 +650,9 @@ retry_delivery(_Force, [], _Now, State) -> retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now, State = #state{inflight = Inflight, retry_interval = Interval}) -> %% Microseconds -> MilliSeconds - Diff = timer:now_diff(Now, Ts) div 1000, + Age = timer:now_diff(Now, Ts) div 1000, if - Force orelse (Diff >= Interval) -> + Force orelse (Age >= Interval) -> Inflight1 = case {Type, Msg0} of {publish, {PacketId, Msg}} -> case emqx_message:is_expired(Msg) of @@ -669,7 +669,7 @@ retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now, end, retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}); true -> - ensure_retry_timer(Interval - Diff, State) + ensure_retry_timer(Interval - max(0, Age), State) end. %%------------------------------------------------------------------------------ @@ -679,36 +679,21 @@ retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now, expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) -> case maps:size(AwaitingRel) of 0 -> State; - _ -> Msgs = lists:sort(sortfun(awaiting_rel), maps:to_list(AwaitingRel)), - expire_awaiting_rel(Msgs, os:timestamp(), 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, Msg = #message{timestamp = TS}} | Msgs], Now, +expire_awaiting_rel([{PacketId, Ts} | More], Now, State = #state{awaiting_rel = AwaitingRel, await_rel_timeout = Timeout}) -> - case (timer:now_diff(Now, TS) div 1000) of - Diff when Diff >= Timeout -> - emqx_metrics:inc('messages/qos2/dropped'), - ?LOG(warning, "Dropped message for await_rel_timeout: ~p", - [emqx_message:format(Msg)], State), - expire_awaiting_rel(Msgs, Now, State#state{awaiting_rel = maps:remove(PacketId, AwaitingRel)}); - Diff -> - ensure_await_rel_timer(Timeout - Diff, State) - end. - -%%------------------------------------------------------------------------------ -%% Sort Inflight, AwaitingRel -%%------------------------------------------------------------------------------ - -sortfun(inflight) -> - fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end; - -sortfun(awaiting_rel) -> - fun({_, #message{timestamp = Ts1}}, - {_, #message{timestamp = Ts2}}) -> - Ts1 < Ts2 + case (timer:now_diff(Now, Ts) div 1000) of + Age when Age >= Timeout -> + emqx_metrics:inc('messages/qos2/expired'), + ?LOG(warning, "Dropped qos2 packet ~s for await_rel_timeout", [PacketId], State), + expire_awaiting_rel(More, Now, State#state{awaiting_rel = maps:remove(PacketId, AwaitingRel)}); + Age -> + ensure_await_rel_timer(Timeout - max(0, Age), State) end. %%------------------------------------------------------------------------------ From cf0f55d057607b1d2ae21b25d6a8f010eb9d0ca9 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Thu, 30 Aug 2018 21:43:23 +0800 Subject: [PATCH 202/520] delayed will message --- src/emqx_client.erl | 16 ++-------------- src/emqx_protocol.erl | 17 ++++++++++------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 82e331abd..5c50519bc 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -373,22 +373,12 @@ init([Options]) -> {_ver, undefined} -> random_client_id(); {_ver, Id} -> iolist_to_binary(Id) end, - Username = case proplists:get_value(username, Options) of - undefined -> <<>>; - Name -> Name - end, - Password = case proplists:get_value(password, Options) of - undefined -> <<>>; - Passw -> Passw - end, State = init(Options, #state{host = {127,0,0,1}, port = 1883, hosts = [], sock_opts = [], bridge_mode = false, client_id = ClientId, - username = Username, - password = Password, clean_start = true, proto_ver = ?MQTT_PROTO_V4, proto_name = <<"MQTT">>, @@ -450,9 +440,9 @@ init([{client_id, ClientId} | Opts], State) -> init(Opts, State#state{client_id = iolist_to_binary(ClientId)}); init([{clean_start, CleanStart} | Opts], State) when is_boolean(CleanStart) -> init(Opts, State#state{clean_start = CleanStart}); -init([{useranme, Username} | Opts], State) -> +init([{username, Username} | Opts], State) -> init(Opts, State#state{username = iolist_to_binary(Username)}); -init([{passwrod, Password} | Opts], State) -> +init([{password, Password} | Opts], State) -> init(Opts, State#state{password = iolist_to_binary(Password)}); init([{keepalive, Secs} | Opts], State) -> init(Opts, State#state{keepalive = timer:seconds(Secs)}); @@ -552,8 +542,6 @@ mqtt_connect(State = #state{client_id = ClientId, properties = Properties}) -> ?WILL_MSG(WillQoS, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, ConnProps = emqx_mqtt_properties:filter(?CONNECT, Properties), - io:format("ConnProps: ~p, ClientID: ~p, Username: ~p, Password: ~p~n", - [ConnProps, ClientId, Username, Password]), send(?CONNECT_PACKET( #mqtt_packet_connect{proto_ver = ProtoVer, proto_name = ProtoName, diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 9ee24609f..054bcdf16 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -264,7 +264,6 @@ process_packet(?CONNECT_PACKET( %% TODO: Mountpoint... %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) WillMsg = emqx_packet:will_msg(Connect), - PState1 = set_username(Username, PState#pstate{client_id = ClientId, proto_ver = ProtoVer, @@ -656,18 +655,23 @@ shutdown(conflict, #pstate{client_id = ClientId}) -> shutdown(mnesia_conflict, #pstate{client_id = ClientId}) -> emqx_cm:unregister_connection(ClientId), ignore; -shutdown(Error, PState = #pstate{client_id = ClientId, will_msg = WillMsg}) -> +shutdown(Error, PState = #pstate{connected = Connected, + client_id = ClientId, + will_msg = WillMsg}) -> ?LOG(info, "Shutdown for ~p", [Error], PState), - %% TODO: Auth failure not publish the will message - case Error =:= auth_failure of - true -> ok; - false -> send_willmsg(WillMsg) + case Connected of + false -> ok; + true -> send_willmsg(WillMsg) end, emqx_hooks:run('client.disconnected', [credentials(PState), Error]), emqx_cm:unregister_connection(ClientId). send_willmsg(undefined) -> ignore; +send_willmsg(WillMsg = #message{topic = Topic, + headers = #{'Will-Delay-Interval' := Interval}}) when is_integer(Interval) -> + SendAfter = integer_to_binary(Interval), + emqx_broker:publish(WillMsg#message{topic = <<"$delayed/", SendAfter/binary, "/", Topic/binary>>}); send_willmsg(WillMsg) -> emqx_broker:publish(WillMsg). @@ -709,4 +713,3 @@ update_mountpoint(PState = #pstate{mountpoint = MountPoint}) -> sp(true) -> 1; sp(false) -> 0. - From fb8a86c5e0270697b66520cdc2ab28b704aab2fc Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Thu, 30 Aug 2018 21:58:02 +0800 Subject: [PATCH 203/520] delayed will message --- src/emqx_protocol.erl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 054bcdf16..5b7c204cd 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -655,14 +655,14 @@ shutdown(conflict, #pstate{client_id = ClientId}) -> shutdown(mnesia_conflict, #pstate{client_id = ClientId}) -> emqx_cm:unregister_connection(ClientId), ignore; -shutdown(Error, PState = #pstate{connected = Connected, +shutdown(Error, PState = #pstate{connected = false}) -> + ?LOG(info, "Shutdown for ~p", [Error], PState), + ignore; +shutdown(Error, PState = #pstate{connected = true, client_id = ClientId, will_msg = WillMsg}) -> ?LOG(info, "Shutdown for ~p", [Error], PState), - case Connected of - false -> ok; - true -> send_willmsg(WillMsg) - end, + send_willmsg(WillMsg), emqx_hooks:run('client.disconnected', [credentials(PState), Error]), emqx_cm:unregister_connection(ClientId). From 025a3c7d278e5498ba4d8ab6f24e626aea6dc575 Mon Sep 17 00:00:00 2001 From: RockyJin Date: Thu, 30 Aug 2018 22:07:46 +0800 Subject: [PATCH 204/520] fix typo of README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1e80dae6b..0e13dd019 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # *EMQ X* - MQTT Broker -*EMQ X* broker is fully a open source, highly scalable, highly available distributed message broker for IoT, M2M and Mobile applications that can handle tens of millions of concurrent clients. +*EMQ X* broker is a fully open source, highly scalable, highly available distributed message 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, STOMP and SockJS. The 3.0 release of the *EMQ X* broker can scaled to 10+ million concurrent MQTT connections on one cluster. @@ -17,8 +17,8 @@ The *EMQ* broker is cross-platform, which can be deployed on Linux, Unix, Mac, W Download the binary package for your platform from [here](http://emqtt.io/downloads). --[Single Node Install](http://emqtt.io/docs/v2/install.html) --[Multi Node Install](http://emqtt.io/docs/v2/cluster.html) +- [Single Node Install](http://emqtt.io/docs/v2/install.html) +- [Multi Node Install](http://emqtt.io/docs/v2/cluster.html) ## Build From Source From d0eaa5192854a91cb70f4e08837fc141919b7cc3 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 30 Aug 2018 22:16:31 +0800 Subject: [PATCH 205/520] add emqx listeners suite --- Makefile | 5 ++- test/emqx_listeners_SUITE.erl | 72 +++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 test/emqx_listeners_SUITE.erl diff --git a/Makefile b/Makefile index aba0d4ac4..6e888a53e 100644 --- a/Makefile +++ b/Makefile @@ -37,8 +37,9 @@ EUNIT_OPTS = verbose CT_SUITES = emqx emqx_connection emqx_session emqx_access emqx_base62 emqx_broker emqx_client emqx_cm emqx_frame emqx_guid emqx_inflight \ emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ - emqx_mqtt_compat emqx_mqtt_properties emqx_mqueue emqx_message emqx_net emqx_pqueue emqx_router emqx_sm \ - emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_zone emqx_mountpoint + emqx_mqtt_compat emqx_mqtt_properties emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ + emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_zone \ + emqx_mountpoint emqx_listeners CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqxct@127.0.0.1 diff --git a/test/emqx_listeners_SUITE.erl b/test/emqx_listeners_SUITE.erl new file mode 100644 index 000000000..f826e1798 --- /dev/null +++ b/test/emqx_listeners_SUITE.erl @@ -0,0 +1,72 @@ +%% Copyright (c) 2018 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_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]. + +init_per_suite(Config) -> + NewConfig = generate_config(), + lists:foreach(fun set_app_env/1, NewConfig), + Config. + +end_per_suite(_Config) -> + ok. + +start_stop_listeners() -> + emqx_listeners:start(), + emqx_listeners:stop(). + + +restart_listeners() -> + ok = emqx_listeners:restart(). + +generate_config() -> + Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), + Conf = conf_parse:file([local_path(["etc", "emqx.conf"])]), + cuttlefish_generator:map(Schema, Conf). + +set_app_env({App, Lists}) -> + lists:foreach(fun({acl_file, _Var}) -> + application:set_env(App, acl_file, local_path(["etc", "acl.conf"])); + ({plugins_loaded_file, _Var}) -> + application:set_env(App, plugins_loaded_file, local_path(["test", "emqx_SUITE_data","loaded_plugins"])); + ({Par, Var}) -> + application:set_env(App, Par, Var) + end, Lists). + +local_path(Components, Module) -> + filename:join([get_base_dir(Module) | Components]). + +local_path(Components) -> + local_path(Components, ?MODULE). + +get_base_dir(Module) -> + {file, Here} = code:is_loaded(Module), + filename:dirname(filename:dirname(Here)). + +get_base_dir() -> + get_base_dir(?MODULE). From f229c1675230923c696dba88d846daf155e6bdd8 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 30 Aug 2018 22:21:13 +0800 Subject: [PATCH 206/520] fix listener test case --- test/emqx_listeners_SUITE.erl | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/test/emqx_listeners_SUITE.erl b/test/emqx_listeners_SUITE.erl index f826e1798..d55bba400 100644 --- a/test/emqx_listeners_SUITE.erl +++ b/test/emqx_listeners_SUITE.erl @@ -28,21 +28,25 @@ all() -> [start_stop_listeners, restart_listeners]. -init_per_suite(Config) -> +init_per_suite() -> NewConfig = generate_config(), + application:ensure_all_started(esockd), lists:foreach(fun set_app_env/1, NewConfig), - Config. - -end_per_suite(_Config) -> ok. -start_stop_listeners() -> - emqx_listeners:start(), - emqx_listeners:stop(). +end_per_suite() -> + application:stop(esockd), + ok. + +start_stop_listeners(_) -> + ok = emqx_listeners:start(), + ok = emqx_listeners:stop(). - -restart_listeners() -> - ok = emqx_listeners:restart(). +restart_listeners(_) -> + ok = emqx_listeners:start(), + ok = emqx_listeners:stop(), + ok = emqx_listeners:restart(), + ok = emqx_listeners:stop(). generate_config() -> Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), From 83e11b6e39b833bd59dbc99e7b0cb6d6f41d6727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 30 Aug 2018 22:45:08 +0800 Subject: [PATCH 207/520] Add emqx_banned test suite, and fix bugs in emqx_banned --- Makefile | 2 +- include/emqx.hrl | 11 ++++++++++ src/emqx_banned.erl | 11 +++------- test/emqx_banned_SUITE.erl | 41 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 test/emqx_banned_SUITE.erl diff --git a/Makefile b/Makefile index aba0d4ac4..41ed22dc0 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ EUNIT_OPTS = verbose # CT_SUITES = emqx_mqueue ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat -CT_SUITES = emqx emqx_connection emqx_session emqx_access emqx_base62 emqx_broker emqx_client emqx_cm emqx_frame emqx_guid emqx_inflight \ +CT_SUITES = emqx emqx_banned emqx_connection emqx_session emqx_access emqx_base62 emqx_broker emqx_client emqx_cm emqx_frame emqx_guid emqx_inflight \ emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ emqx_mqtt_compat emqx_mqtt_properties emqx_mqueue emqx_message emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_zone emqx_mountpoint diff --git a/include/emqx.hrl b/include/emqx.hrl index 10190a3f6..34e41b0a1 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -145,5 +145,16 @@ descr :: string() }). +%%-------------------------------------------------------------------- +%% Banned +%%-------------------------------------------------------------------- + +-record(banned, { + key, + reason, + by, + desc, + until}). + -endif. diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index 908c8b5d5..4f8d44f44 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -36,14 +36,8 @@ -define(TAB, ?MODULE). -define(SERVER, ?MODULE). --type(key() :: {client_id, emqx_types:client_id()} | - {username, emqx_types:username() | - {ipaddr, inet:ip_address()}}). - -record(state, {expiry_timer}). --record(banned, {key :: key(), reason, by, desc, until}). - %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- @@ -84,7 +78,7 @@ del(Key) -> %%-------------------------------------------------------------------- init([]) -> - emqx_timer:seed(), + emqx_time:seed(), {ok, ensure_expiry_timer(#state{})}. handle_call(Req, _From, State) -> @@ -128,7 +122,8 @@ expire_banned_item(Key, Now) -> [#banned{until = undefined}] -> ok; [B = #banned{until = Until}] when Until < Now -> mnesia:delete_object(?TAB, B, sticky_write); + [_] -> ok; [] -> ok end, - expire_banned_item(mnesia:next(Key), Now). + expire_banned_item(mnesia:next(?TAB, Key), Now). diff --git a/test/emqx_banned_SUITE.erl b/test/emqx_banned_SUITE.erl new file mode 100644 index 000000000..9fae880d4 --- /dev/null +++ b/test/emqx_banned_SUITE.erl @@ -0,0 +1,41 @@ +%% Copyright (c) 2018 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_banned_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx.hrl"). + +-include("emqx_mqtt.hrl"). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> [t_banned_all]. + +t_banned_all(_) -> + emqx_ct_broker_helpers:run_setup_steps(), + emqx_banned:start_link(), + {MegaSecs, Secs, MicroSecs} = erlang:timestamp(), + ok = emqx_banned:add(#banned{key = {client_id, <<"TestClient">>}, + reason = <<"test">>, + by = <<"banned suite">>, + desc = <<"test">>, + until = {MegaSecs, Secs + 10, MicroSecs}}), + % here is not expire banned test because its check interval is greater than 5 mins, but its effect has been confirmed + timer:sleep(100), + ?assert(emqx_banned:check(#{client_id => <<"TestClient">>, username => undefined, peername => {undefined, undefined}})), + emqx_banned:del({client_id, <<"TestClient">>}), + ?assertNot(emqx_banned:check(#{client_id => <<"TestClient">>, username => undefined, peername => {undefined, undefined}})). \ No newline at end of file From 0379219a044e9ae9a8f62a9c751620ee9c919098 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 30 Aug 2018 23:14:09 +0800 Subject: [PATCH 208/520] Improve the design of session discard --- src/emqx_connection.erl | 16 ++++--- src/emqx_protocol.erl | 31 ++++++------- src/emqx_session.erl | 92 ++++++++++++++++++++------------------ src/emqx_ws_connection.erl | 9 +++- 4 files changed, 78 insertions(+), 70 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 16d35585e..bcaea297d 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -217,6 +217,10 @@ handle_info(timeout, State) -> handle_info({shutdown, Error}, State) -> shutdown(Error, State); +handle_info({shutdown, discard, {ClientId, ByPid}}, State) -> + ?LOG(warning, "discarded by ~s:~p", [ClientId, ByPid], State), + shutdown(discard, State); + handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> ?LOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State), shutdown(conflict, State); @@ -240,10 +244,10 @@ handle_info({inet_reply, _Sock, ok}, State) -> handle_info({inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); -handle_info({keepalive, start, Interval}, State = #state{transport = Transport, socket = Sock}) -> +handle_info({keepalive, start, Interval}, State = #state{transport = Transport, socket = Socket}) -> ?LOG(debug, "Keepalive at the interval of ~p", [Interval], State), StatFun = fun() -> - case Transport:getstat(Sock, [recv_oct]) of + case Transport:getstat(Socket, [recv_oct]) of {ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct}; Error -> Error end @@ -270,11 +274,11 @@ handle_info(Info, State) -> {noreply, State}. terminate(Reason, State = #state{transport = Transport, - socket = Sock, + socket = Socket, keepalive = KeepAlive, proto_state = ProtoState}) -> ?LOG(debug, "Terminated for ~p", [Reason], State), - Transport:fast_close(Sock), + Transport:fast_close(Socket), emqx_keepalive:cancel(KeepAlive), case {ProtoState, Reason} of {undefined, _} -> ok; @@ -358,8 +362,8 @@ run_socket(State = #state{conn_state = blocked}) -> State; run_socket(State = #state{await_recv = true}) -> State; -run_socket(State = #state{transport = Transport, socket = Sock}) -> - Transport:async_recv(Sock, 0, infinity), +run_socket(State = #state{transport = Transport, socket = Socket}) -> + Transport:async_recv(Socket, 0, infinity), State#state{await_recv = true}. %%------------------------------------------------------------------------------ diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index da7ee88b8..5ef4a9b0d 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -402,11 +402,11 @@ process_packet(?PACKET(?DISCONNECT), PState) -> %%------------------------------------------------------------------------------ connack({?RC_SUCCESS, SP, PState}) -> - emqx_hooks:run('client.connected', [credentials(PState), ?RC_SUCCESS, info(PState)]), + emqx_hooks:run('client.connected', [credentials(PState), ?RC_SUCCESS, attrs(PState)]), deliver({connack, ?RC_SUCCESS, sp(SP)}, update_mountpoint(PState)); connack({ReasonCode, PState = #pstate{proto_ver = ProtoVer}}) -> - emqx_hooks:run('client.connected', [credentials(PState), ?RC_SUCCESS, info(PState)]), + emqx_hooks:run('client.connected', [credentials(PState), ?RC_SUCCESS, attrs(PState)]), _ = deliver({connack, if ProtoVer =:= ?MQTT_PROTO_V5 -> ReasonCode; true -> @@ -648,22 +648,17 @@ inc_stats(Type, Stats = #{pkt := PktCnt, msg := MsgCnt}) -> false -> MsgCnt end}. -shutdown(_Error, #pstate{client_id = undefined}) -> - ignore; -shutdown(conflict, #pstate{client_id = ClientId}) -> - emqx_cm:unregister_connection(ClientId), - ignore; -shutdown(mnesia_conflict, #pstate{client_id = ClientId}) -> - emqx_cm:unregister_connection(ClientId), - ignore; -shutdown(Error, PState = #pstate{client_id = ClientId, will_msg = WillMsg}) -> - ?LOG(info, "Shutdown for ~p", [Error], PState), - %% TODO: Auth failure not publish the will message - case Error =:= auth_failure of - true -> ok; - false -> send_willmsg(WillMsg) - end, - emqx_hooks:run('client.disconnected', [credentials(PState), Error]), +shutdown(_Reason, #pstate{client_id = undefined}) -> + ok; +shutdown(Reason, #pstate{client_id = ClientId}) when Reason =:= conflict; + Reason =:= discard -> + emqx_cm:unregister_connection(ClientId); +shutdown(Reason, PState = #pstate{client_id = ClientId, + will_msg = WillMsg, + connected = true}) -> + ?LOG(info, "Shutdown for ~p", [Reason], PState), + _ = send_willmsg(WillMsg), + emqx_hooks:run('client.disconnected', [credentials(PState), Reason]), emqx_cm:unregister_connection(ClientId). send_willmsg(undefined) -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 65bce85aa..d5b68a1f6 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -147,6 +147,8 @@ created_at :: erlang:timestamp() }). +-type(spid() :: pid()). + -define(TIMEOUT, 60000). -define(LOG(Level, Format, Args, State), @@ -159,7 +161,7 @@ start_link(SessAttrs) -> proc_lib:start_link(?MODULE, init, [[self(), SessAttrs]]). %% @doc Get session info --spec(info(pid() | #state{}) -> list({atom(), term()})). +-spec(info(spid() | #state{}) -> list({atom(), term()})). info(SPid) when is_pid(SPid) -> gen_server:call(SPid, info, infinity); @@ -187,7 +189,7 @@ info(State = #state{conn_pid = ConnPid, {await_rel_timeout, AwaitRelTimeout}]. %% @doc Get session attrs --spec(attrs(pid() | #state{}) -> list({atom(), term()})). +-spec(attrs(spid() | #state{}) -> list({atom(), term()})). attrs(SPid) when is_pid(SPid) -> gen_server:call(SPid, attrs, infinity); @@ -204,7 +206,7 @@ attrs(#state{clean_start = CleanStart, {expiry_interval, ExpiryInterval div 1000}, {created_at, CreatedAt}]. --spec(stats(pid() | #state{}) -> list({atom(), non_neg_integer()})). +-spec(stats(spid() | #state{}) -> list({atom(), non_neg_integer()})). stats(SPid) when is_pid(SPid) -> gen_server:call(SPid, stats, infinity); @@ -233,19 +235,19 @@ stats(#state{max_subscriptions = MaxSubscriptions, %% PubSub API %%------------------------------------------------------------------------------ --spec(subscribe(pid(), list({emqx_topic:topic(), emqx_types:subopts()})) -> ok). +-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(pid(), emqx_mqtt_types:packet_id(), +-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}). --spec(publish(pid(), emqx_mqtt_types:packet_id(), emqx_types:message()) +-spec(publish(spid(), emqx_mqtt_types:packet_id(), emqx_types:message()) -> {ok, emqx_types:deliver_results()}). publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_0}) -> %% Publish QoS0 message to broker directly @@ -259,56 +261,56 @@ publish(SPid, PacketId, Msg = #message{qos = ?QOS_2}) -> %% Publish QoS2 message to session gen_server:call(SPid, {publish, PacketId, Msg}, infinity). --spec(puback(pid(), emqx_mqtt_types:packet_id()) -> ok). +-spec(puback(spid(), emqx_mqtt_types:packet_id()) -> ok). puback(SPid, PacketId) -> gen_server:cast(SPid, {puback, PacketId, ?RC_SUCCESS}). puback(SPid, PacketId, ReasonCode) -> gen_server:cast(SPid, {puback, PacketId, ReasonCode}). --spec(pubrec(pid(), emqx_mqtt_types:packet_id()) -> ok | {error, emqx_mqtt_types:reason_code()}). +-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(pid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code()) +-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(pid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code()) +-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(pid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code()) -> ok). +-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(pid(), emqx_types:topic_table()) -> ok). +-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) -> - emqx_topic:parse(RawTopic) - end, 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(pid(), emqx_mqtt_types:packet_id(), +-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(pid(), pid()) -> ok). +-spec(resume(spid(), pid()) -> ok). resume(SPid, ConnPid) -> gen_server:cast(SPid, {resume, ConnPid}). %% @doc Discard the session --spec(discard(pid(), emqx_types:client_id()) -> ok). -discard(SPid, ClientId) -> - gen_server:call(SPid, {discard, ClientId}, infinity). +-spec(discard(spid(), ByPid :: pid()) -> ok). +discard(SPid, ByPid) -> + gen_server:call(SPid, {discard, ByPid}, infinity). --spec(close(pid()) -> ok). +-spec(close(spid()) -> ok). close(SPid) -> gen_server:call(SPid, close, infinity). @@ -367,13 +369,23 @@ init_mqueue(Zone) -> binding(ConnPid) -> case node(ConnPid) =:= node() of true -> local; false -> remote end. -handle_call({discard, ConnPid}, _From, State = #state{conn_pid = undefined}) -> - ?LOG(warning, "Discarded by ~p", [ConnPid], State), +handle_call(info, _From, State) -> + reply(info(State), State); + +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], State), {stop, {shutdown, discard}, ok, State}; -handle_call({discard, ConnPid}, _From, State = #state{conn_pid = OldConnPid}) -> - ?LOG(warning, " ~p kickout ~p", [ConnPid, OldConnPid], State), - {stop, {shutdown, conflict}, 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], State), + ConnPid ! {shutdown, discard, {ClientId, ByPid}}, + {stop, {shutdown, discard}, ok, State}; %% PUBLISH: handle_call({publish, PacketId, Msg = #message{qos = ?QOS_2, timestamp = Ts}}, _From, @@ -415,15 +427,6 @@ handle_call({pubrel, PacketId, _ReasonCode}, _From, State = #state{awaiting_rel {{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State} end); -handle_call(info, _From, State) -> - reply(info(State), State); - -handle_call(attrs, _From, State) -> - reply(attrs(State), State); - -handle_call(stats, _From, State) -> - reply(stats(State), State); - handle_call(close, _From, State) -> {stop, normal, ok, State}; @@ -441,6 +444,7 @@ handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, SubMap; {ok, _SubOpts} -> emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), + %% Why??? emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts]), maps:put(Topic, SubOpts, SubMap); error -> @@ -617,17 +621,17 @@ unsuback(From, PacketId, ReasonCodes) -> From ! {deliver, {unsuback, PacketId, ReasonCodes}}. %%------------------------------------------------------------------------------ -%% Kickout old client +%% Kickout old connection -kick(_ClientId, undefined, _Pid) -> +kick(_ClientId, undefined, _ConnPid) -> ignore; -kick(_ClientId, Pid, Pid) -> +kick(_ClientId, ConnPid, ConnPid) -> ignore; -kick(ClientId, OldPid, Pid) -> - unlink(OldPid), - OldPid ! {shutdown, conflict, {ClientId, Pid}}, +kick(ClientId, OldConnPid, ConnPid) -> + unlink(OldConnPid), + OldConnPid ! {shutdown, conflict, {ClientId, ConnPid}}, %% Clean noproc - receive {'EXIT', OldPid, _} -> ok after 1 -> ok end. + receive {'EXIT', OldConnPid, _} -> ok after 1 -> ok end. %%------------------------------------------------------------------------------ %% Replay or Retry Delivery diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index ed1532565..74014707a 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -34,20 +34,21 @@ options, peername, sockname, + idle_timeout, proto_state, parser_state, keepalive, enable_stats, stats_timer, - idle_timeout, shutdown_reason }). -define(INFO_KEYS, [peername, sockname]). + -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -define(WSLOG(Level, Format, Args, State), - emqx_logger:Level("WsClient(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). + emqx_logger:Level("WSMQTT(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). %%------------------------------------------------------------------------------ %% API @@ -235,6 +236,10 @@ websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> shutdown(keepalive_error, State) end; +websocket_info({shutdown, discard, {ClientId, ByPid}}, State) -> + ?WSLOG(warning, "discarded by ~s:~p", [ClientId, ByPid], State), + shutdown(discard, State); + websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) -> ?WSLOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State), shutdown(conflict, State); From 1c945661411178b4e70edd5a2c04ee11a6cbc06e Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 30 Aug 2018 23:49:08 +0800 Subject: [PATCH 209/520] add topic alias validate --- src/emqx_packet.erl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index dfc8359d2..7e4f27821 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -55,10 +55,11 @@ validate(?UNSUBSCRIBE_PACKET(PacketId, TopicFilters)) -> validate_packet_id(PacketId) andalso ok == lists:foreach(fun emqx_topic:validate/1, TopicFilters); -validate(?PUBLISH_PACKET(_QoS, <<>>, _, _)) -> +validate(?PUBLISH_PACKET(_QoS, <<>>, _, _, _)) -> error(topic_name_invalid); -validate(?PUBLISH_PACKET(_QoS, Topic, _, _)) -> - (not emqx_topic:wildcard(Topic)) orelse error(topic_name_invalid); +validate(?PUBLISH_PACKET(_QoS, Topic, _, Properties, _)) -> + ((not emqx_topic:wildcard(Topic)) orelse error(topic_name_invalid)) + andalso validate_properties(?PUBLISH, Properties); validate(_Packet) -> true. @@ -71,9 +72,14 @@ validate_packet_id(_) -> validate_properties(?SUBSCRIBE, #{'Subscription-Identifier' := I}) when I =< 0; I >= 16#FFFFFFF -> error(subscription_identifier_invalid); +validate_properties(?PUBLISH, # {'Topic-Alias':= I}) + when I =:= 0 -> + error(topic_alias_invalid); validate_properties(_, _) -> true. + + validate_subscription({Topic, #{qos := QoS}}) -> emqx_topic:validate(filter, Topic) andalso validate_qos(QoS). From 809c516a2b2036baaf952a9ad287ae9fa1ccc738 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Fri, 31 Aug 2018 00:04:01 +0800 Subject: [PATCH 210/520] delete client testcases duplicated with emqx_mqtt_compat --- test/emqx_client_SUITE.erl | 42 -------------------------------------- 1 file changed, 42 deletions(-) delete mode 100644 test/emqx_client_SUITE.erl diff --git a/test/emqx_client_SUITE.erl b/test/emqx_client_SUITE.erl deleted file mode 100644 index 2fee82759..000000000 --- a/test/emqx_client_SUITE.erl +++ /dev/null @@ -1,42 +0,0 @@ -%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. - --module(emqx_client_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include("emqx_mqtt.hrl"). - --include_lib("eunit/include/eunit.hrl"). - -all() -> [{group, connect}]. - -groups() -> [{connect, [start]}]. - -init_per_suite(Config) -> - Config. - -end_per_suite(_Config) -> - ok. - -init_per_group(_Group, Config) -> - Config. - -end_per_group(_Group, _Config) -> - ok. - -start(_Config) -> - {ok, ClientPid, _} = emqx_client:start_link(). - From a7274b115d407bdcabe26e19d88402f45578c21a Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Fri, 31 Aug 2018 00:15:45 +0800 Subject: [PATCH 211/520] ignore dup cases --- Makefile | 2 +- test/emqx_mqtt_compat_SUITE.erl | 34 +++++++++++++++++---------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index 13f726faa..c8d8db93f 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ EUNIT_OPTS = verbose # CT_SUITES = emqx_mqueue ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat -CT_SUITES = emqx emqx_banned emqx_connection emqx_session emqx_access emqx_base62 emqx_broker emqx_client emqx_cm emqx_frame emqx_guid emqx_inflight \ +CT_SUITES = emqx emqx_banned emqx_connection emqx_session emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight \ emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ emqx_mqtt_compat emqx_mqtt_properties emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_zone \ diff --git a/test/emqx_mqtt_compat_SUITE.erl b/test/emqx_mqtt_compat_SUITE.erl index 452b2a821..af2583678 100644 --- a/test/emqx_mqtt_compat_SUITE.erl +++ b/test/emqx_mqtt_compat_SUITE.erl @@ -37,7 +37,7 @@ all() -> zero_length_clientid_test, offline_message_queueing_test, overlapping_subscriptions_test, - keepalive_test, + %% keepalive_test, redelivery_on_reconnect_test, %% subscribe_failure_test, dollar_topics_test]. @@ -58,7 +58,8 @@ receive_messages(Count, Msgs) -> receive {publish, Msg} -> receive_messages(Count-1, [Msg|Msgs]); - _Other -> + Other -> + ct:log("~p~n", [Other]), receive_messages(Count, Msgs) after 10 -> Msgs @@ -141,20 +142,21 @@ overlapping_subscriptions_test(_) -> end, emqx_client:disconnect(C). -keepalive_test(_) -> - ct:print("Keepalive test starting"), - {ok, C1, _} = emqx_client:start_link([{clean_start, true}, - {keepalive, 5}, - {will_topic, nth(5, ?TOPICS)}, - {will_payload, <<"keepalive expiry">>}]), - ok = emqx_client:pause(C1), - {ok, C2, _} = emqx_client:start_link([{clean_start, true}, - {keepalive, 0}]), - {ok, _, [2]} = emqx_client:subscribe(C2, nth(5, ?TOPICS), 2), - timer:sleep(15000), - ?assertEqual(1, length(receive_messages(1))), - ct:print("Keepalive test succeeded"), - ok = emqx_client:disconnect(C2). +%% keepalive_test(_) -> +%% ct:print("Keepalive test starting"), +%% {ok, C1, _} = emqx_client:start_link([{clean_start, true}, +%% {keepalive, 5}, +%% {will_flag, true}, +%% {will_topic, nth(5, ?TOPICS)}, +%% %% {will_qos, 2}, +%% {will_payload, <<"keepalive expiry">>}]), +%% ok = emqx_client:pause(C1), +%% {ok, C2, _} = emqx_client:start_link([{clean_start, true}, +%% {keepalive, 0}]), +%% {ok, _, [2]} = emqx_client:subscribe(C2, nth(5, ?TOPICS), 2), +%% ok = emqx_client:disconnect(C2), +%% ?assertEqual(1, length(receive_messages(1))), +%% ct:print("Keepalive test succeeded"). redelivery_on_reconnect_test(_) -> ct:print("Redelivery on reconnect test starting"), From e6bed24bb32a6d30057c132d998866d012730a00 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 31 Aug 2018 00:32:56 +0800 Subject: [PATCH 212/520] Add server_keepalive config --- etc/emqx.conf | 5 +++++ priv/emqx.schema | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/etc/emqx.conf b/etc/emqx.conf index 5975ae542..e8435a3b3 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -580,6 +580,11 @@ zone.external.enable_stats = on ## Value: boolean ## zone.external.shared_subscription = false +## Server Keep Alive +## +## Value: Number +## zone.external.server_keepalive = 0 + ## The backoff for MQTT keepalive timeout. The broker will kick a connection out ## until 'Keepalive * backoff * 2' timeout. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index 07ff5ce6f..1e1892b33 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -735,6 +735,11 @@ end}. {datatype, {enum, [true, false]}} ]}. +%% @doc Server Keepalive +{mapping, "zone.$name.server_keepalive", "emqx.zones", [ + {datatype, integer} +]}. + %% @doc Keepalive backoff {mapping, "zone.$name.keepalive_backoff", "emqx.zones", [ {default, 0.75}, From b6006b5947b784fa5cdfdd558f03fd8938bd6708 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 31 Aug 2018 00:40:10 +0800 Subject: [PATCH 213/520] Support CONNACK properties --- src/emqx_protocol.erl | 65 +++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 5ef4a9b0d..4dc2c8e75 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -41,6 +41,7 @@ proto_name, ackprops, client_id, + is_assigned, conn_pid, conn_props, ack_props, @@ -87,6 +88,7 @@ init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) proto_ver = ?MQTT_PROTO_V4, proto_name = <<"MQTT">>, client_id = <<>>, + is_assigned = false, conn_pid = self(), username = init_username(Peercert, Options), is_super = false, @@ -265,17 +267,16 @@ process_packet(?CONNECT_PACKET( %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) WillMsg = emqx_packet:will_msg(Connect), - PState1 = set_username(Username, - PState#pstate{client_id = ClientId, - proto_ver = ProtoVer, - proto_name = ProtoName, - clean_start = CleanStart, - keepalive = Keepalive, - conn_props = ConnProps, - will_topic = WillTopic, - will_msg = WillMsg, - is_bridge = IsBridge, - connected_at = os:timestamp()}), + PState1 = set_username(Username, PState#pstate{client_id = ClientId, + proto_ver = ProtoVer, + proto_name = ProtoName, + clean_start = CleanStart, + keepalive = Keepalive, + conn_props = ConnProps, + will_topic = WillTopic, + will_msg = WillMsg, + is_bridge = IsBridge, + connected_at = os:timestamp()}), connack( case check_connect(Connect, PState1) of @@ -450,6 +451,33 @@ puback(?QOS_2, PacketId, {ok, _}, PState) -> 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}) -> + #{max_packet_size := MaxPktSize, + max_qos_allowed := MaxQoS, + mqtt_retain_available := Retain, + max_topic_alias := MaxAlias, + mqtt_shared_subscription := Shared, + mqtt_wildcard_subscription := Wildcard} = caps(PState), + Props = #{'Maximum-QoS' => MaxQoS, + 'Retain-Available' => flag(Retain), + 'Maximum-Packet-Size' => MaxPktSize, + 'Topic-Alias-Maximum' => MaxAlias, + 'Wildcard-Subscription-Available' => Wildcard, + 'Subscription-Identifiers-Available' => 1, + 'Shared-Subscription-Available' => flag(Shared)}, + Props1 = if IsAssigned -> + Props#{'Assigned-Client-Identifier' => ClientId}; + true -> Props + end, + Props2 = case emqx_zone:get_env(Zone, server_keepalive) of + undefined -> Props1; + Keepalive -> Props1#{'Server-Keep-Alive' => Keepalive} + end, + send(?CONNACK_PACKET(?RC_SUCCESS, SP, Props2), PState); + deliver({connack, ReasonCode, SP}, PState) -> send(?CONNACK_PACKET(ReasonCode, SP), PState); @@ -509,7 +537,7 @@ send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = SendFun maybe_assign_client_id(PState = #pstate{client_id = <<>>, ackprops = AckProps}) -> ClientId = emqx_guid:to_base62(emqx_guid:gen()), AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), - PState#pstate{client_id = ClientId, ackprops = AckProps1}; + PState#pstate{client_id = ClientId, is_assigned = true, ackprops = AckProps1}; maybe_assign_client_id(PState) -> PState. @@ -532,9 +560,13 @@ try_open_session(#pstate{zone = Zone, authenticate(Credentials, Password) -> case emqx_access_control:authenticate(Credentials, Password) of - ok -> {ok, false}; - {ok, IsSuper} -> {ok, IsSuper}; - {error, Error} -> {error, Error} + ok -> {ok, false}; + {ok, IsSuper} when is_boolean(IsSuper) -> + {ok, IsSuper}; + {ok, Result} when is_map(Result) -> + {ok, maps:get(is_superuser, Result, false)}; + {error, Error} -> + {error, Error} end. set_property(Name, Value, undefined) -> @@ -705,3 +737,6 @@ update_mountpoint(PState = #pstate{mountpoint = MountPoint}) -> sp(true) -> 1; sp(false) -> 0. +flag(false) -> 0; +flag(true) -> 1. + From 487fa6824d483814f27c71cb097a8848462f9fbf Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Fri, 31 Aug 2018 00:42:55 +0800 Subject: [PATCH 214/520] add emqx_protocol test suites --- Makefile | 2 +- test/emqx_protocol_SUITE.erl | 132 +++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 test/emqx_protocol_SUITE.erl diff --git a/Makefile b/Makefile index c8d8db93f..e0680732a 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ CT_SUITES = emqx emqx_banned emqx_connection emqx_session emqx_access emqx_broke emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ emqx_mqtt_compat emqx_mqtt_properties emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_zone \ - emqx_mountpoint emqx_listeners + emqx_mountpoint emqx_listeners emqx_protocol CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqxct@127.0.0.1 diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl new file mode 100644 index 000000000..2323519b1 --- /dev/null +++ b/test/emqx_protocol_SUITE.erl @@ -0,0 +1,132 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx.hrl"). + +-include("emqx_mqtt.hrl"). + +-include_lib("eunit/include/eunit.hrl"). + +-import(emqx_serializer, [serialize/1]). + +all() -> + [%% {group, parser}, + %% {group, serializer}, + {group, packet}, + {group, message}]. + +groups() -> + [%% {parser, [], + %% [ + %% parse_connect, + %% parse_bridge, + %% parse_publish, + %% parse_puback, + %% parse_pubrec, + %% parse_pubrel, + %% parse_pubcomp, + %% parse_subscribe, + %% parse_unsubscribe, + %% parse_pingreq, + %% parse_disconnect]}, + %% {serializer, [], + %% [serialize_connect, + %% serialize_connack, + %% serialize_publish, + %% serialize_puback, + %% serialize_pubrel, + %% serialize_subscribe, + %% serialize_suback, + %% serialize_unsubscribe, + %% serialize_unsuback, + %% serialize_pingreq, + %% serialize_pingresp, + %% serialize_disconnect]}, + {packet, [], + [packet_proto_name, + packet_type_name, + packet_format]}, + {message, [], + [message_make + %% message_from_packet + ]} + ]. + + + +%%-------------------------------------------------------------------- +%% Packet Cases +%%-------------------------------------------------------------------- + +packet_proto_name(_) -> + ?assertEqual(<<"MQIsdp">>, emqx_packet:protocol_name(3)), + ?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(4)). + +packet_type_name(_) -> + ?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT)), + ?assertEqual('UNSUBSCRIBE', emqx_packet:type_name(?UNSUBSCRIBE)). + +%% packet_connack_name(_) -> +%% ?assertEqual('CONNACK_ACCEPT', emqx_packet:connack_name(?CONNACK_ACCEPT)), +%% ?assertEqual('CONNACK_PROTO_VER', emqx_packet:connack_name(?CONNACK_PROTO_VER)), +%% ?assertEqual('CONNACK_INVALID_ID', emqx_packet:connack_name(?CONNACK_INVALID_ID)), +%% ?assertEqual('CONNACK_SERVER', emqx_packet:connack_name(?CONNACK_SERVER)), +%% ?assertEqual('CONNACK_CREDENTIALS', emqx_packet:connack_name(?CONNACK_CREDENTIALS)), +%% ?assertEqual('CONNACK_AUTH', emqx_packet:connack_name(?CONNACK_AUTH)). + +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))]), + io:format("~s", [emqx_packet:format(?PUBLISH_PACKET(?QOS_2, <<"topic">>, 10, <<"payload">>))]), + io:format("~s", [emqx_packet:format(?PUBACK_PACKET(?PUBACK, 98))]), + io:format("~s", [emqx_packet:format(?PUBREL_PACKET(99))]), + io:format("~s", [emqx_packet:format(?SUBSCRIBE_PACKET(15, [{<<"topic">>, ?QOS0}, {<<"topic1">>, ?QOS1}]))]), + io:format("~s", [emqx_packet:format(?SUBACK_PACKET(40, [?QOS0, ?QOS1]))]), + io:format("~s", [emqx_packet:format(?UNSUBSCRIBE_PACKET(89, [<<"t">>, <<"t2">>]))]), + io:format("~s", [emqx_packet:format(?UNSUBACK_PACKET(90))]). + +%%-------------------------------------------------------------------- +%% Message Cases +%%-------------------------------------------------------------------- + +message_make(_) -> + Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), + ?assertEqual(0, Msg#message.qos), + Msg1 = emqx_message:make(<<"clientid">>, qos2, <<"topic">>, <<"payload">>), + ?assert(is_binary(Msg1#message.id)), + ?assertEqual(qos2, Msg1#message.qos). + +%% message_from_packet(_) -> +%% Msg = emqx_message:from_packet(?PUBLISH_PACKET(1, <<"topic">>, 10, <<"payload">>)), +%% ?assertEqual(1, Msg#message.qos), +%% %% ?assertEqual(10, Msg#message.pktid), +%% ?assertEqual(<<"topic">>, Msg#message.topic), +%% WillMsg = emqx_message:from_packet(#mqtt_packet_connect{will_flag = true, +%% will_topic = <<"WillTopic">>, +%% will_payload = <<"WillMsg">>}), +%% ?assertEqual(<<"WillTopic">>, WillMsg#message.topic), +%% ?assertEqual(<<"WillMsg">>, WillMsg#message.payload). + + %% Msg2 = emqx_message:fomat_packet(<<"username">>, <<"clientid">>, + %% ?PUBLISH_PACKET(1, <<"topic">>, 20, <<"payload">>)), + + From dc9a1cd80feff452d18a8da694f313bd2657974b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 31 Aug 2018 00:48:23 +0800 Subject: [PATCH 215/520] Format emqx_protocol module --- src/emqx_protocol.erl | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index a3f2e2699..9affac57e 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -267,16 +267,17 @@ process_packet(?CONNECT_PACKET( %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) WillMsg = emqx_packet:will_msg(Connect), - PState1 = set_username(Username, PState#pstate{client_id = ClientId, - proto_ver = ProtoVer, - proto_name = ProtoName, - clean_start = CleanStart, - keepalive = Keepalive, - conn_props = ConnProps, - will_topic = WillTopic, - will_msg = WillMsg, - is_bridge = IsBridge, - connected_at = os:timestamp()}), + PState1 = set_username(Username, + PState#pstate{client_id = ClientId, + proto_ver = ProtoVer, + proto_name = ProtoName, + clean_start = CleanStart, + keepalive = Keepalive, + conn_props = ConnProps, + will_topic = WillTopic, + will_msg = WillMsg, + is_bridge = IsBridge, + connected_at = os:timestamp()}), connack( case check_connect(Connect, PState1) of {ok, PState2} -> @@ -681,7 +682,7 @@ inc_stats(Type, Stats = #{pkt := PktCnt, msg := MsgCnt}) -> shutdown(_Reason, #pstate{client_id = undefined}) -> ok; -shutdown(_Reason, PState = #pstate{connected = false}) -> +shutdown(_Reason, #pstate{connected = false}) -> ok; shutdown(Reason, #pstate{client_id = ClientId}) when Reason =:= conflict; Reason =:= discard -> From a4efcb5b2c9fd33f02991063759e29a10f159af0 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 31 Aug 2018 01:08:01 +0800 Subject: [PATCH 216/520] Update Makefile and README.md --- Makefile | 4 ++-- README.md | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index e0680732a..7202915cc 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,8 @@ dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.2.0 dep_lager = git https://github.com/erlang-lager/lager 3.6.4 -dep_esockd = git https://github.com/emqx/esockd emqx30 -dep_ekka = git https://github.com/emqx/ekka emqx30 +dep_esockd = git https://github.com/emqx/esockd v5.4 +dep_ekka = git https://github.com/emqx/ekka v0.4.1 dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 dep_clique = git https://github.com/emqx/clique dep_lager_syslog = git https://github.com/basho/lager_syslog 3.0.1 diff --git a/README.md b/README.md index 0e13dd019..9ca4f447d 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ Starting from 3.0 release, *EMQ X* broker fully supports MQTT V5.0 protocol spec - For full list of new features, please read *EMQ X* broker 3.0 [release notes](https://github.com/emqtt/emqttd/releases/). -- For more information, please visit [EMQ X homepage](http://emqtt.io). +- For more information, please visit [EMQ X homepage](http://emqtt.io). ## Installation -The *EMQ* 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 can be deployed on Linux, Unix, Mac, Windows and even Raspberry Pi. Download the binary package for your platform from [here](http://emqtt.io/downloads). @@ -23,27 +23,27 @@ Download the binary package for your platform from [here](http://emqtt.io/downlo ## Build From Source -The *EMQ* broker requires Erlang/OTP R21+ to build since 3.0 release. +The *EMQ X* broker requires Erlang/OTP R21+ to build since 3.0 release. ``` -git clone https://github.com/emqtt/emq-relx.git +git clone https://github.com/emqx/emqx-rel.git -cd emq-relx && make +cd emqx-rel && make -cd _rel/emqttd && ./bin/emqttd console +cd _rel/emqx && ./bin/emqx console ``` ## Quick Start - # Start emqttd - ./bin/emqttd start - + # Start emqx + ./bin/emqx start + # Check Status - ./bin/emqttd_ctl status - - # Stop emqttd - ./bin/emqttd stop + ./bin/emqx_ctl status + + # Stop emqx + ./bin/emqx stop To view the dashboard after running, use your browser to open: http://localhost:18083 @@ -59,11 +59,11 @@ You can reach the EMQ community and developers via the following channels: -[#emqx-users](https://emqx.slack.com/messages/CBUF2TTB8/) -[#emqx-devs](https://emqx.slack.com/messages/CBSL57DUH/) - [Mailing Lists]() -- [Twitter](https://twitter.com/emqtt) +- [Twitter](https://twitter.com/emqtt) - [Forum](https://groups.google.com/d/forum/emqtt) - [Blog](https://medium.com/@emqtt) -Please submit any bugs, issues, and feature requests to [emqtt/emqttd](//github.com/emqtt/emqttd/issues). +Please submit any bugs, issues, and feature requests to [emqtt/emqttd](//github.com/emqtt/emqttd/issues). ## License From 748826bdeef970f7a12e954ae95ec8aa700c6d9e Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Fri, 31 Aug 2018 01:16:54 +0800 Subject: [PATCH 217/520] update access sutie and access control --- src/emqx_access_control.erl | 2 +- test/emqx_access_SUITE.erl | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 46ed9ed53..bc4969e54 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -154,7 +154,7 @@ init([]) -> handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) -> Mods = lookup_mods(Type), reply(case lists:keyfind(Mod, 1, Mods) of - true -> + {_, _, _} -> {error, already_existed}; false -> case catch Mod:init(Opts) of diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index 3a7aca390..e08cec08e 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -98,7 +98,8 @@ end_per_group(_Group, Config) -> Config. init_per_testcase(_TestCase, Config) -> - {ok, _Pid} = ?AC:start_link(), + %% {ok, _Pid} = + ?AC:start_link(), Config. end_per_testcase(_TestCase, _Config) -> ok. From 25391e8c71206ba7f4a7634cd9433f9c2b32ca66 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 31 Aug 2018 01:22:03 +0800 Subject: [PATCH 218/520] Rename 'Subscription-Identifiers-Available' to 'Subscription-Identifier-Available' --- src/emqx_protocol.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 9affac57e..d30eeece8 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -466,7 +466,7 @@ deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, 'Maximum-Packet-Size' => MaxPktSize, 'Topic-Alias-Maximum' => MaxAlias, 'Wildcard-Subscription-Available' => Wildcard, - 'Subscription-Identifiers-Available' => 1, + 'Subscription-Identifier-Available' => 1, 'Shared-Subscription-Available' => flag(Shared)}, Props1 = if IsAssigned -> Props#{'Assigned-Client-Identifier' => ClientId}; From 9406bc1fd13cb927ebbe47d5ad04388f6173184a Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 31 Aug 2018 01:39:56 +0800 Subject: [PATCH 219/520] fix typo --- src/emqx_protocol.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index d30eeece8..0bfcdb6dd 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -465,7 +465,7 @@ deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, 'Retain-Available' => flag(Retain), 'Maximum-Packet-Size' => MaxPktSize, 'Topic-Alias-Maximum' => MaxAlias, - 'Wildcard-Subscription-Available' => Wildcard, + 'Wildcard-Subscription-Available' => flag(Wildcard), 'Subscription-Identifier-Available' => 1, 'Shared-Subscription-Available' => flag(Shared)}, Props1 = if IsAssigned -> @@ -745,4 +745,3 @@ sp(false) -> 0. flag(false) -> 0; flag(true) -> 1. - From 23e72feab7cf08661b2900370b97965a78e2cb7d Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 31 Aug 2018 01:54:25 +0800 Subject: [PATCH 220/520] fix reason codes --- src/emqx_protocol.erl | 15 ++++++++------- src/emqx_reason_codes.erl | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 0bfcdb6dd..c36346673 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -407,13 +407,14 @@ connack({?RC_SUCCESS, SP, PState}) -> deliver({connack, ?RC_SUCCESS, sp(SP)}, update_mountpoint(PState)); connack({ReasonCode, PState = #pstate{proto_ver = ProtoVer}}) -> - emqx_hooks:run('client.connected', [credentials(PState), ?RC_SUCCESS, attrs(PState)]), - _ = deliver({connack, if ProtoVer =:= ?MQTT_PROTO_V5 -> - ReasonCode; - true -> - emqx_reason_codes:compat(connack, ReasonCode) - end}, PState), - {error, emqx_reason_codes:name(ReasonCode, ProtoVer), PState}. + emqx_hooks:run('client.connected', [credentials(PState), ReasonCode, attrs(PState)]), + ReasonCode1 = if ProtoVer =:= ?MQTT_PROTO_V5 -> + ReasonCode; + true -> + emqx_reason_codes:compat(connack, ReasonCode) + end, + _ = deliver({connack, ReasonCode1}, PState), + {error, emqx_reason_codes:name(ReasonCode1, ProtoVer), PState}. %%------------------------------------------------------------------------------ %% Publish Message -> Broker diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index 0cc52acbb..fdc1377ec 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -27,7 +27,8 @@ name(1, _Ver) -> unacceptable_protocol_version; name(2, _Ver) -> client_identifier_not_valid; name(3, _Ver) -> server_unavaliable; name(4, _Ver) -> malformed_username_or_password; -name(5, _Ver) -> unauthorized_client. +name(5, _Ver) -> unauthorized_client; +name(I, _Ver) -> list_to_atom("unkown_connack" ++ integer_to_list(I)). name(16#00) -> success; name(16#01) -> granted_qos1; @@ -139,4 +140,3 @@ compat(connack, 16#9F) -> ?CONNACK_SERVER; compat(suback, Code) when Code =< ?QOS2 -> Code; compat(suback, Code) when Code > 16#80 -> 16#80. - From 237e65a4e0e70df01f03367afa89f4f8c5139feb Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 31 Aug 2018 10:22:16 +0800 Subject: [PATCH 221/520] Use emqx_mqueue:init/1 to create a mqueue --- src/emqx_local_bridge.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/emqx_local_bridge.erl b/src/emqx_local_bridge.erl index 9ed8fdbac..66cdf4010 100644 --- a/src/emqx_local_bridge.erl +++ b/src/emqx_local_bridge.erl @@ -63,8 +63,9 @@ init([Pool, Id, Node, Topic, Options]) -> Share = iolist_to_binary(["$bridge:", atom_to_list(Node), ":", Topic]), emqx_broker:subscribe(Topic, self(), [{share, Share}, {qos, ?QOS_0}]), State = parse_opts(Options, #state{node = Node, subtopic = Topic}), - %%TODO: queue.... - MQueue = emqx_mqueue:new(qname(Node, Topic), [{max_len, State#state.max_queue_len}]), + MQueue = emqx_mqueue:init(#{type => simple, + max_len => State#state.max_queue_len, + store_qos0 => true}), {ok, State#state{pool = Pool, id = Id, mqueue = MQueue}}; false -> {stop, {cannot_connect_node, Node}} From 3045ec10ab7af0e37f361b9df890284e4bae9636 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 31 Aug 2018 14:04:26 +0800 Subject: [PATCH 222/520] Add banned feature --- etc/emqx.conf | 5 ++++ priv/emqx.schema | 6 +++++ src/emqx_access_control.erl | 5 ++-- src/emqx_banned.erl | 47 ++++++++++++++++--------------------- src/emqx_cm_sup.erl | 20 ++++++++++------ src/emqx_protocol.erl | 16 ++++++++++++- 6 files changed, 61 insertions(+), 38 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index e8435a3b3..deb702211 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -529,6 +529,11 @@ zone.external.idle_timeout = 15s ## Default: 10 messages per second, and 100 messages burst. ## zone.external.publish_limit = 10,100 +## Enable ban check. +## +## Value: Flag +zone.external.enable_ban = on + ## Enable ACL check. ## ## Value: Flag diff --git a/priv/emqx.schema b/priv/emqx.schema index 1e1892b33..e9f4932c4 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -676,6 +676,12 @@ end}. {datatype, {enum, [allow, deny]}} ]}. +%% @doc Enable Ban. +{mapping, "zone.$name.enable_ban", "emqx.zones", [ + {default, off}, + {datatype, flag} +]}. + %% @doc Enable ACL check. {mapping, "zone.$name.enable_acl", "emqx.zones", [ {default, off}, diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index bc4969e54..8301bd8d8 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -153,9 +153,8 @@ init([]) -> handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) -> Mods = lookup_mods(Type), - reply(case lists:keyfind(Mod, 1, Mods) of - {_, _, _} -> - {error, already_existed}; + reply(case lists:keymember(Mod, 1, Mods) of + true -> {error, already_existed}; false -> case catch Mod:init(Opts) of {ok, ModState} -> diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index 4f8d44f44..444f07dad 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -24,27 +24,23 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). -%% API -export([start_link/0]). -export([check/1]). -export([add/1, del/1]). -%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(TAB, ?MODULE). -define(SERVER, ?MODULE). --record(state, {expiry_timer}). - %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ - {type, ordered_set}, + {type, set}, {disc_copies, [node()]}, {record_name, banned}, {attributes, record_info(fields, banned)}]); @@ -52,11 +48,7 @@ mnesia(boot) -> mnesia(copy) -> ok = ekka_mnesia:copy_table(?TAB). -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -%% @doc Start the banned server +%% @doc Start the banned server. -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). @@ -67,9 +59,13 @@ check(#{client_id := ClientId, username := Username, peername := {IPAddr, _}}) - orelse ets:member(?TAB, {username, Username}) orelse ets:member(?TAB, {ipaddr, IPAddr}). -add(Record) when is_record(Record, banned) -> - mnesia:dirty_write(?TAB, Record). +-spec(add(#banned{}) -> ok). +add(Banned) when is_record(Banned, banned) -> + mnesia:dirty_write(?TAB, Banned). +-spec(del({client_id, emqx_types:client_id()} | + {username, emqx_types:username()} | + {peername, emqx_types:peername()}) -> ok). del(Key) -> mnesia:dirty_delete(?TAB, Key). @@ -78,27 +74,26 @@ del(Key) -> %%-------------------------------------------------------------------- init([]) -> - emqx_time:seed(), - {ok, ensure_expiry_timer(#state{})}. + {ok, ensure_expiry_timer(#{expiry_timer => undefined})}. handle_call(Req, _From, State) -> - emqx_logger:error("[BANNED] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[BANNED] unexpected call: ~p", [Req]), + {reply, ignored, State}. handle_cast(Msg, State) -> - emqx_logger:error("[BANNED] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[BANNED] unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info({timeout, Ref, expire}, State = #state{expiry_timer = Ref}) -> +handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) -> mnesia:async_dirty(fun expire_banned_items/1, [erlang:timestamp()]), {noreply, ensure_expiry_timer(State), hibernate}; handle_info(Info, State) -> - emqx_logger:error("[BANNED] Unexpected info: ~p", [Info]), + emqx_logger:error("[BANNED] unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{expiry_timer = Timer}) -> - emqx_misc:cancel_timer(Timer). +terminate(_Reason, #{expiry_timer := TRef}) -> + emqx_misc:cancel_timer(TRef). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -108,9 +103,7 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- ensure_expiry_timer(State) -> - Interval = emqx_config:get_env(banned_expiry_interval, timer:minutes(5)), - State#state{expiry_timer = emqx_misc:start_timer( - Interval + rand:uniform(Interval), expire)}. + State#{expiry_timer := emqx_misc:start_timer(timer:minutes(5), expire)}. expire_banned_items(Now) -> expire_banned_item(mnesia:first(?TAB), Now). @@ -119,11 +112,11 @@ expire_banned_item('$end_of_table', _Now) -> ok; expire_banned_item(Key, Now) -> case mnesia:read(?TAB, Key) of - [#banned{until = undefined}] -> ok; + [#banned{until = undefined}] -> + ok; [B = #banned{until = Until}] when Until < Now -> mnesia:delete_object(?TAB, B, sticky_write); - [_] -> ok; - [] -> ok + _ -> ok end, expire_banned_item(mnesia:next(?TAB, Key), Now). diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index 231822ba5..000e79336 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -25,11 +25,17 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - {ok, {{one_for_all, 10, 3600}, - [#{id => manager, - start => {emqx_cm, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_cm]}]}}. + Banned = #{id => banned, + start => {emqx_banned, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_banned]}, + Manager = #{id => manager, + start => {emqx_cm, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_cm]}, + {ok, {{one_for_one, 10, 100}, [Banned, Manager]}}. diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index c36346673..01fbce313 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -56,6 +56,7 @@ mountpoint, is_super, is_bridge, + enable_ban, enable_acl, recv_stats, send_stats, @@ -97,6 +98,7 @@ init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) packet_size = emqx_zone:get_env(Zone, max_packet_size), mountpoint = emqx_zone:get_env(Zone, mountpoint), is_bridge = false, + enable_ban = emqx_zone:get_env(Zone, enable_ban, false), enable_acl = emqx_zone:get_env(Zone, enable_acl), recv_stats = #{msg => 0, pkt => 0}, send_stats = #{msg => 0, pkt => 0}, @@ -581,7 +583,8 @@ set_property(Name, Value, Props) -> check_connect(Packet, PState) -> run_check_steps([fun check_proto_ver/2, - fun check_client_id/2], Packet, PState). + fun check_client_id/2, + fun check_banned/2], Packet, PState). check_proto_ver(#mqtt_packet_connect{proto_ver = Ver, proto_name = Name}, _PState) -> @@ -612,6 +615,17 @@ check_client_id(#mqtt_packet_connect{client_id = ClientId}, #pstate{zone = Zone} false -> {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID} end. +check_banned(_Connect, #pstate{enable_ban = false}) -> + ok; +check_banned(#mqtt_packet_connect{client_id = ClientId, username = Username}, + #pstate{peername = Peername}) -> + case emqx_banned:check(#{client_id => ClientId, + username => Username, + peername => Peername}) of + true -> {error, ?RC_BANNED}; + false -> ok + end. + check_publish(Packet, PState) -> run_check_steps([fun check_pub_caps/2, fun check_pub_acl/2], Packet, PState). From e6fd7faa4b9f8c824735bac85d8cd4de84833776 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Fri, 31 Aug 2018 16:24:10 +0800 Subject: [PATCH 223/520] add format_variable for disconnect packet --- src/emqx_packet.erl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 7e4f27821..715526964 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -195,6 +195,10 @@ format_variable(#mqtt_packet_connect{ end, io_lib:format(Format1, Args1); +format_variable(#mqtt_packet_disconnect + {reason_code = ReasonCode}) -> + io_lib:format("ReasonCode=~p", [ReasonCode]); + format_variable(#mqtt_packet_connack{ack_flags = AckFlags, reason_code = ReasonCode}) -> io_lib:format("AckFlags=~p, ReasonCode=~p", [AckFlags, ReasonCode]); From ea1ae708335ab5347ec885a409cc674196c6749f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 31 Aug 2018 16:46:51 +0800 Subject: [PATCH 224/520] Fix errors found by dialyzer --- src/emqx.erl | 64 ++++++++---------- src/emqx_acl_internal.erl | 4 +- src/emqx_alarm_mgr.erl | 31 ++++----- src/emqx_bridge.erl | 5 +- src/emqx_broker.erl | 4 +- src/emqx_client.erl | 2 +- src/emqx_connection.erl | 31 +++++---- src/emqx_local_bridge.erl | 14 ++-- src/emqx_metrics.erl | 10 ++- src/emqx_mod_rewrite.erl | 4 +- src/emqx_protocol.erl | 7 +- src/emqx_session.erl | 5 +- src/emqx_sm.erl | 47 ++++++------- src/emqx_sm_registry.erl | 34 +++++----- src/emqx_stats.erl | 3 +- src/emqx_ws_connection.erl | 131 +++++++++++++++++++++---------------- 16 files changed, 200 insertions(+), 196 deletions(-) diff --git a/src/emqx.erl b/src/emqx.erl index 8e1f10168..217611171 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -66,41 +66,36 @@ is_running(Node) -> %% PubSub API %%-------------------------------------------------------------------- --spec(subscribe(emqx_topic:topic() | string()) -> ok | {error, term()}). +-spec(subscribe(emqx_topic:topic() | string()) -> ok). subscribe(Topic) -> emqx_broker:subscribe(iolist_to_binary(Topic)). --spec(subscribe(emqx_topic:topic() | string(), emqx_types:subscriber() | string()) - -> ok | {error, term()}). -subscribe(Topic, Sub) when is_list(Sub)-> - emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Sub)); -subscribe(Topic, Subscriber) when is_tuple(Subscriber) -> - {SubPid, SubId} = Subscriber, - emqx_broker:subscribe(iolist_to_binary(Topic), SubPid, SubId). +-spec(subscribe(emqx_topic:topic() | string(), emqx_types:subid() | pid()) -> ok). +subscribe(Topic, SubId) when is_atom(SubId); is_binary(SubId)-> + emqx_broker:subscribe(iolist_to_binary(Topic), SubId); +subscribe(Topic, SubPid) when is_pid(SubPid) -> + emqx_broker:subscribe(iolist_to_binary(Topic), SubPid). --spec(subscribe(emqx_topic:topic() | string(), emqx_types:subscriber() | string(), - emqx_topic:subopts()) -> ok | {error, term()}). -subscribe(Topic, Sub, Options) when is_list(Sub)-> - emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Sub), Options); -subscribe(Topic, Subscriber, Options) when is_tuple(Subscriber)-> - {SubPid, SubId} = Subscriber, - emqx_broker:subscribe(iolist_to_binary(Topic), SubPid, SubId, Options). +-spec(subscribe(emqx_topic:topic() | string(), emqx_types:subid() | pid(), + emqx_types:subopts()) -> ok). +subscribe(Topic, SubId, Options) when is_atom(SubId); is_binary(SubId)-> + emqx_broker:subscribe(iolist_to_binary(Topic), SubId, Options); +subscribe(Topic, SubPid, Options) when is_pid(SubPid)-> + emqx_broker:subscribe(iolist_to_binary(Topic), SubPid, Options). -spec(publish(emqx_types:message()) -> {ok, emqx_types:deliver_results()}). publish(Msg) -> emqx_broker:publish(Msg). --spec(unsubscribe(emqx_topic:topic() | string()) -> ok | {error, term()}). +-spec(unsubscribe(emqx_topic:topic() | string()) -> ok). unsubscribe(Topic) -> emqx_broker:unsubscribe(iolist_to_binary(Topic)). --spec(unsubscribe(emqx_topic:topic() | string(), emqx_types:subscriber() | string()) - -> ok | {error, term()}). -unsubscribe(Topic, Sub) when is_list(Sub) -> - emqx_broker:unsubscribe(iolist_to_binary(Topic), list_to_subid(Sub)); -unsubscribe(Topic, Subscriber) when is_tuple(Subscriber) -> - {SubPid, SubId} = Subscriber, - emqx_broker:unsubscribe(iolist_to_binary(Topic), SubPid, SubId). +-spec(unsubscribe(emqx_topic:topic() | string(), emqx_types:subid() | pid()) -> ok). +unsubscribe(Topic, SubId) when is_atom(SubId); is_binary(SubId) -> + emqx_broker:unsubscribe(iolist_to_binary(Topic), SubId); +unsubscribe(Topic, SubPid) when is_pid(SubPid) -> + emqx_broker:unsubscribe(iolist_to_binary(Topic), SubPid). %%-------------------------------------------------------------------- %% PubSub management API @@ -109,12 +104,12 @@ unsubscribe(Topic, Subscriber) when is_tuple(Subscriber) -> -spec(get_subopts(emqx_topic:topic() | string(), emqx_types:subscriber()) -> emqx_types:subopts()). get_subopts(Topic, Subscriber) -> - emqx_broker:get_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber)). + emqx_broker:get_subopts(iolist_to_binary(Topic), Subscriber). -spec(set_subopts(emqx_topic:topic() | string(), emqx_types:subscriber(), - emqx_types:subopts()) -> ok). -set_subopts(Topic, Subscriber, Options) when is_list(Options) -> - emqx_broker:set_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). + emqx_types:subopts()) -> boolean()). +set_subopts(Topic, Subscriber, Options) when is_map(Options) -> + emqx_broker:set_subopts(iolist_to_binary(Topic), Subscriber, Options). -spec(topics() -> list(emqx_topic:topic())). topics() -> emqx_router:topics(). @@ -127,16 +122,11 @@ subscribers(Topic) -> subscriptions(Subscriber) -> emqx_broker:subscriptions(Subscriber). --spec(subscribed(emqx_topic:topic() | string(), emqx_types:subscriber()) -> boolean()). -subscribed(Topic, Subscriber) -> - emqx_broker:subscribed(iolist_to_binary(Topic), list_to_subid(Subscriber)). - -list_to_subid(SubId) when is_binary(SubId) -> - SubId; -list_to_subid(SubId) when is_list(SubId) -> - iolist_to_binary(SubId); -list_to_subid(SubPid) when is_pid(SubPid) -> - SubPid. +-spec(subscribed(emqx_topic:topic() | string(), pid() | emqx_types:subid()) -> boolean()). +subscribed(Topic, SubPid) when is_pid(SubPid) -> + emqx_broker:subscribed(iolist_to_binary(Topic), SubPid); +subscribed(Topic, SubId) when is_atom(SubId); is_binary(SubId) -> + emqx_broker:subscribed(iolist_to_binary(Topic), SubId). %%-------------------------------------------------------------------- %% Hooks API diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index 0f25e6808..eee7e6c18 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -25,6 +25,8 @@ -define(ACL_RULE_TAB, emqx_acl_rule). +-type(state() :: #{acl_file := string()}). + %%------------------------------------------------------------------------------ %% API %%------------------------------------------------------------------------------ @@ -95,7 +97,7 @@ match(Credentials, Topic, [Rule|Rules]) -> {matched, AllowDeny} end. --spec(reload_acl(#{}) -> ok | {error, term()}). +-spec(reload_acl(state()) -> ok | {error, term()}). reload_acl(#{acl_file := AclFile}) -> case catch load_rules_from_file(AclFile) of true -> diff --git a/src/emqx_alarm_mgr.erl b/src/emqx_alarm_mgr.erl index bb734c8e6..fd2a42aa7 100644 --- a/src/emqx_alarm_mgr.erl +++ b/src/emqx_alarm_mgr.erl @@ -28,10 +28,11 @@ -define(ALARM_MGR, ?MODULE). --record(state, {alarms}). - start_link() -> - start_with(fun(Pid) -> gen_event:add_handler(Pid, ?MODULE, []) end). + start_with( + fun(Pid) -> + gen_event:add_handler(Pid, ?MODULE, []) + end). start_with(Fun) -> case gen_event:start_link({local, ?ALARM_MGR}) of @@ -73,42 +74,42 @@ delete_alarm_handler(Module) when is_atom(Module) -> %% Default Alarm handler %%------------------------------------------------------------------------------ -init(_) -> {ok, #state{alarms = []}}. +init(_) -> {ok, #{alarms => []}}. handle_event({set_alarm, Alarm = #alarm{timestamp = undefined}}, State)-> handle_event({set_alarm, Alarm#alarm{timestamp = os:timestamp()}}, State); -handle_event({set_alarm, Alarm = #alarm{id = AlarmId}}, State = #state{alarms = Alarms}) -> +handle_event({set_alarm, Alarm = #alarm{id = AlarmId}}, State = #{alarms := Alarms}) -> case encode_alarm(Alarm) of {ok, Json} -> emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json)); {error, Reason} -> emqx_logger:error("[AlarmMgr] Failed to encode alarm: ~p", [Reason]) end, - {ok, State#state{alarms = [Alarm|Alarms]}}; + {ok, State#{alarms := [Alarm|Alarms]}}; -handle_event({clear_alarm, AlarmId}, State = #state{alarms = Alarms}) -> - case emqx_json:safe_encode([{id, AlarmId}, {ts, emqx_time:now_secs()}]) of +handle_event({clear_alarm, AlarmId}, State = #{alarms := Alarms}) -> + case emqx_json:safe_encode([{id, AlarmId}, {ts, os:system_time(second)}]) of {ok, Json} -> emqx_broker:safe_publish(alarm_msg(clear, AlarmId, Json)); {error, Reason} -> emqx_logger:error("[AlarmMgr] Failed to encode clear: ~p", [Reason]) end, - {ok, State#state{alarms = lists:keydelete(AlarmId, 2, Alarms)}, hibernate}; + {ok, State#{alarms := lists:keydelete(AlarmId, 2, Alarms)}, hibernate}; handle_event(Event, State)-> - error_logger:error("[AlarmMgr] unexpected event: ~p", [Event]), + emqx_logger:error("[AlarmMgr] unexpected event: ~p", [Event]), {ok, State}. handle_info(Info, State) -> - error_logger:error("[AlarmMgr] unexpected info: ~p", [Info]), + emqx_logger:error("[AlarmMgr] unexpected info: ~p", [Info]), {ok, State}. -handle_call(get_alarms, State = #state{alarms = Alarms}) -> +handle_call(get_alarms, State = #{alarms := Alarms}) -> {ok, Alarms, State}; handle_call(Req, State) -> - error_logger:error("[AlarmMgr] unexpected call: ~p", [Req]), + emqx_logger:error("[AlarmMgr] unexpected call: ~p", [Req]), {ok, ignored, State}. terminate(swap, State) -> @@ -132,8 +133,8 @@ encode_alarm(#alarm{id = AlarmId, severity = Severity, title = Title, alarm_msg(Type, AlarmId, Json) -> Msg = emqx_message:make(?ALARM_MGR, topic(Type, AlarmId), Json), - emqx_message:set_headers(#{'Content-Type' => <<"application/json">>}, - emqx_message:set_flags(#{sys => true}, Msg)). + emqx_message:set_headers( #{'Content-Type' => <<"application/json">>}, + emqx_message:set_flag(sys, Msg)). topic(alert, AlarmId) -> emqx_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>); diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index d1763d31c..0f64f331d 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -133,7 +133,8 @@ handle_info(start, State = #state{options = Options, Subs = get_value(subscriptions, Options, []), [emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs], ForwardRules = string:tokens(get_value(forward_rule, Options, ""), ","), - [emqx_broker:subscribe(i2b(Topic)) || Topic <- ForwardRules, emqx_topic:validate({filter, i2b(Topic)})], + [emqx_broker:subscribe(i2b(Topic)) || Topic <- ForwardRules, + emqx_topic:validate({filter, i2b(Topic)})], {noreply, State#state{client_pid = ClientPid}}; {error,_} -> erlang:send_after(ReconnectTime, self(), start), @@ -251,4 +252,4 @@ store(disk, Data, Queue, _MaxPendingMsg)-> delete(memory, PkgId, Queue) -> lists:keydelete(PkgId, 1, Queue); delete(disk, PkgId, Queue) -> - lists:keydelete(PkgId, 1, Queue). \ No newline at end of file + lists:keydelete(PkgId, 1, Queue). diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 623290961..b2f1bb119 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -260,9 +260,9 @@ subscription(Topic, Subscriber) -> -spec(subscribed(emqx_topic:topic(), pid() | emqx_types:subid() | emqx_types:subscriber()) -> boolean()). subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> - length(ets:match_object(?SUBOPTION, {{Topic, {SubPid, '_'}}, '_'}, 1)) == 1; + length(ets:match_object(?SUBOPTION, {{Topic, {SubPid, '_'}}, '_'}, 1)) >= 1; subscribed(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> - length(ets:match_object(?SUBOPTION, {{Topic, {'_', SubId}}, '_'}, 1)) == 1; + length(ets:match_object(?SUBOPTION, {{Topic, {'_', SubId}}, '_'}, 1)) >= 1; subscribed(Topic, {SubPid, SubId}) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> ets:member(?SUBOPTION, {Topic, {SubPid, SubId}}). diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 5c50519bc..192569ca4 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -186,7 +186,7 @@ with_owner(Options) -> connect(Client) -> gen_statem:call(Client, connect, infinity). --spec(subscribe(client(), topic() | {topic(), qos() | [subopt()]}) +-spec(subscribe(client(), topic() | {topic(), qos() | [subopt()]} | [{topic(), qos()}]) -> subscribe_ret()). subscribe(Client, Topic) when is_binary(Topic) -> subscribe(Client, {Topic, ?QOS_0}); diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index bcaea297d..adda71450 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -202,20 +202,19 @@ handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> {ok, ProtoState1} -> {noreply, maybe_gc(ensure_stats_timer(State#state{proto_state = ProtoState1}))}; {error, Reason} -> - shutdown(Reason, State); - {error, Reason, ProtoState1} -> - shutdown(Reason, State#state{proto_state = ProtoState1}) + shutdown(Reason, State) end; -handle_info(emit_stats, State = #state{proto_state = ProtoState}) -> +handle_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)), {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info(timeout, State) -> shutdown(idle_timeout, State); -handle_info({shutdown, Error}, State) -> - shutdown(Error, State); +handle_info({shutdown, Reason}, State) -> + shutdown(Reason, State); handle_info({shutdown, discard, {ClientId, ByPid}}, State) -> ?LOG(warning, "discarded by ~s:~p", [ClientId, ByPid], State), @@ -311,13 +310,13 @@ handle_packet(Data, State = #state{proto_state = ProtoState, {ok, ProtoState1} -> NewState = State#state{proto_state = ProtoState1}, handle_packet(Rest, inc_publish_cnt(Type, reset_parser(NewState))); - {error, Error} -> - ?LOG(error, "Protocol error - ~p", [Error], State), - shutdown(Error, State); - {error, Error, ProtoState1} -> - shutdown(Error, State#state{proto_state = ProtoState1}); - {stop, Reason, ProtoState1} -> - stop(Reason, State#state{proto_state = ProtoState1}) + {error, Reason} -> + ?LOG(error, "Process packet error - ~p", [Reason], State), + shutdown(Reason, State); + {error, Reason, ProtoState1} -> + shutdown(Reason, State#state{proto_state = ProtoState1}); + {stop, Error, ProtoState1} -> + stop(Error, State#state{proto_state = ProtoState1}) end; {error, Error} -> ?LOG(error, "Framing error - ~p", [Error], State), @@ -371,9 +370,9 @@ run_socket(State = #state{transport = Transport, socket = Socket}) -> %%------------------------------------------------------------------------------ ensure_stats_timer(State = #state{enable_stats = true, - stats_timer = undefined, - idle_timeout = IdleTimeout}) -> - State#state{stats_timer = erlang:send_after(IdleTimeout, self(), emit_stats)}; + stats_timer = undefined, + idle_timeout = IdleTimeout}) -> + State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; ensure_stats_timer(State) -> State. shutdown(Reason, State) -> diff --git a/src/emqx_local_bridge.erl b/src/emqx_local_bridge.erl index 66cdf4010..228a64cff 100644 --- a/src/emqx_local_bridge.erl +++ b/src/emqx_local_bridge.erl @@ -60,8 +60,8 @@ init([Pool, Id, Node, Topic, Options]) -> case net_kernel:connect_node(Node) of true -> true = erlang:monitor_node(Node, true), - Share = iolist_to_binary(["$bridge:", atom_to_list(Node), ":", Topic]), - emqx_broker:subscribe(Topic, self(), [{share, Share}, {qos, ?QOS_0}]), + Group = iolist_to_binary(["$bridge:", atom_to_list(Node), ":", Topic]), + emqx_broker:subscribe(Topic, self(), #{share => Group, qos => ?QOS_0}), State = parse_opts(Options, #state{node = Node, subtopic = Topic}), MQueue = emqx_mqueue:init(#{type => simple, max_len => State#state.max_queue_len, @@ -86,11 +86,6 @@ parse_opts([{ping_down_interval, Interval} | Opts], State) -> parse_opts([_Opt | Opts], State) -> parse_opts(Opts, State). -qname(Node, Topic) when is_atom(Node) -> - qname(atom_to_list(Node), Topic); -qname(Node, Topic) -> - iolist_to_binary(["Bridge:", Node, ":", Topic]). - handle_call(Req, _From, State) -> emqx_logger:error("[Bridge] unexpected call: ~p", [Req]), {reply, ignored, State}. @@ -104,7 +99,7 @@ handle_info({dispatch, _Topic, Msg}, State = #state{mqueue = Q, status = down}) {noreply, State#state{mqueue = emqx_mqueue:in(Msg, Q)}}; handle_info({dispatch, _Topic, Msg}, State = #state{node = Node, status = up}) -> - ok = emqx_rpc:cast(Node, emqx_broker, publish, [transform(Msg, State)]), + emqx_rpc:cast(Node, emqx_broker, publish, [transform(Msg, State)]), {noreply, State}; handle_info({nodedown, Node}, State = #state{node = Node, ping_down_interval = Interval}) -> @@ -157,7 +152,6 @@ dequeue(State = #state{mqueue = MQ}) -> dequeue(State#state{mqueue = MQ1}) end. -transform(Msg = #message{topic = Topic}, #state{topic_prefix = Prefix, - topic_suffix = Suffix}) -> +transform(Msg = #message{topic = Topic}, #state{topic_prefix = Prefix, topic_suffix = Suffix}) -> Msg#message{topic = <>}. diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 0a9ad67aa..e319a425b 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -26,8 +26,6 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {}). - %% Bytes sent and received of Broker -define(BYTES_METRICS, [ {counter, 'bytes/received'}, % Total bytes received @@ -85,8 +83,8 @@ -define(TAB, ?MODULE). -define(SERVER, ?MODULE). -%% @doc Start the metrics server --spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +%% @doc Start the metrics server. +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). @@ -252,7 +250,7 @@ init([]) -> % Create metrics table _ = emqx_tables:new(?TAB, [set, public, {write_concurrency, true}]), lists:foreach(fun new/1, ?BYTES_METRICS ++ ?PACKET_METRICS ++ ?MESSAGE_METRICS), - {ok, #state{}, hibernate}. + {ok, #{}, hibernate}. handle_call(Req, _From, State) -> emqx_logger:error("[Metrics] unexpected call: ~p", [Req]), @@ -266,7 +264,7 @@ handle_info(Info, State) -> emqx_logger:error("[Metrics] unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{}) -> +terminate(_Reason, #{}) -> ok. code_change(_OldVsn, State, _Extra) -> diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index 2a92793eb..a9ff334ce 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -23,8 +23,8 @@ load(Rules0) -> Rules = compile(Rules0), - emqx_hooks:add('client.subscribe', fun ?MODULE:rewrite_subscribe/4, [Rules]), - emqx_hooks:add('client.unsubscribe',fun ?MODULE:rewrite_unsubscribe/4, [Rules]), + 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]). rewrite_subscribe(_Credentials, TopicTable, Rules) -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 01fbce313..018166d20 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -188,14 +188,14 @@ session(#pstate{session = SPid}) -> SPid. parser(#pstate{packet_size = Size, proto_ver = Ver}) -> - emqx_frame:initial_state(#{packet_size => Size, version => Ver}). + emqx_frame:initial_state(#{max_packet_size => Size, version => Ver}). %%------------------------------------------------------------------------------ %% Packet Received %%------------------------------------------------------------------------------ --spec(received(emqx_mqtt_types:packet(), state()) - -> {ok, state()} | {error, term()} | {error, term(), state()}). +-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}; @@ -451,6 +451,7 @@ puback(?QOS_2, PacketId, {ok, _}, PState) -> %% Deliver Packet -> Client %%------------------------------------------------------------------------------ +-spec(deliver(term(), state()) -> {ok, state()} | {error, term()}). deliver({connack, ReasonCode}, PState) -> send(?CONNACK_PACKET(ReasonCode), PState); diff --git a/src/emqx_session.erl b/src/emqx_session.erl index d5b68a1f6..81299711d 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -148,6 +148,9 @@ }). -type(spid() :: pid()). +-type(attr() :: {atom(), term()}). + +-export_type([attr/0]). -define(TIMEOUT, 60000). @@ -564,7 +567,7 @@ handle_info({timeout, Timer, check_awaiting_rel}, State = #state{await_rel_timer noreply(expire_awaiting_rel(State#state{await_rel_timer = undefined})); handle_info({timeout, Timer, emit_stats}, State = #state{client_id = ClientId, stats_timer = Timer}) -> - true = emqx_sm:set_session_stats(ClientId, stats(State)), + _ = emqx_sm:set_session_stats(ClientId, stats(State)), {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info({timeout, Timer, expired}, State = #state{expiry_timer = Timer}) -> diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 0b188f986..36d416f3b 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -49,22 +49,20 @@ start_link() -> %% @doc Open a session. -spec(open_session(map()) -> {ok, pid()} | {ok, pid(), boolean()} | {error, term()}). -open_session(Attrs = #{clean_start := true, client_id := ClientId, conn_pid := ConnPid}) -> +open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid := ConnPid}) -> CleanStart = fun(_) -> ok = discard_session(ClientId, ConnPid), - emqx_session_sup:start_session(Attrs) + emqx_session_sup:start_session(SessAttrs) end, emqx_sm_locker:trans(ClientId, CleanStart); -open_session(Attrs = #{clean_start := false, client_id := ClientId, conn_pid := ConnPid}) -> +open_session(SessAttrs = #{clean_start := false, client_id := ClientId, conn_pid := ConnPid}) -> ResumeStart = fun(_) -> case resume_session(ClientId, ConnPid) of {ok, SPid} -> {ok, SPid, true}; {error, not_found} -> - emqx_session_sup:start_session(Attrs); - {error, Reason} -> - {error, Reason} + emqx_session_sup:start_session(SessAttrs) end end, emqx_sm_locker:trans(ClientId, ResumeStart). @@ -113,31 +111,31 @@ close_session(SPid) when is_pid(SPid) -> %% @doc Register a session with attributes. -spec(register_session(emqx_types:client_id() | {emqx_types:client_id(), pid()}, - list(emqx_session:attribute())) -> ok). -register_session(ClientId, Attrs) when is_binary(ClientId) -> - register_session({ClientId, self()}, Attrs); + list(emqx_session:attr())) -> ok). +register_session(ClientId, SessAttrs) when is_binary(ClientId) -> + register_session({ClientId, self()}, SessAttrs); -register_session(Session = {ClientId, SPid}, Attrs) +register_session(Session = {ClientId, SPid}, SessAttrs) when is_binary(ClientId), is_pid(SPid) -> ets:insert(?SESSION_TAB, Session), - ets:insert(?SESSION_ATTRS_TAB, {Session, Attrs}), - case proplists:get_value(clean_start, Attrs, true) of - true -> ok; - false -> ets:insert(?SESSION_P_TAB, Session) - end, + ets:insert(?SESSION_ATTRS_TAB, {Session, SessAttrs}), + proplists:get_value(clean_start, SessAttrs, true) + andalso ets:insert(?SESSION_P_TAB, Session), emqx_sm_registry:register_session(Session), notify({registered, ClientId, SPid}). %% @doc Get session attrs --spec(get_session_attrs({emqx_types:client_id(), pid()}) -> list(emqx_session:attribute())). +-spec(get_session_attrs({emqx_types:client_id(), pid()}) -> list(emqx_session:attr())). get_session_attrs(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> safe_lookup_element(?SESSION_ATTRS_TAB, Session, []). %% @doc Set session attrs -set_session_attrs(ClientId, Attrs) when is_binary(ClientId) -> - set_session_attrs({ClientId, self()}, Attrs); -set_session_attrs(Session = {ClientId, SPid}, Attrs) when is_binary(ClientId), is_pid(SPid) -> - ets:insert(?SESSION_ATTRS_TAB, {Session, Attrs}). +-spec(set_session_attrs(emqx_types:client_id() | {emqx_types:client_id(), pid()}, + list(emqx_session:attr())) -> true). +set_session_attrs(ClientId, SessAttrs) when is_binary(ClientId) -> + set_session_attrs({ClientId, self()}, SessAttrs); +set_session_attrs(Session = {ClientId, SPid}, SessAttrs) when is_binary(ClientId), is_pid(SPid) -> + ets:insert(?SESSION_ATTRS_TAB, {Session, SessAttrs}). %% @doc Unregister a session -spec(unregister_session(emqx_types:client_id() | {emqx_types:client_id(), pid()}) -> ok). @@ -154,18 +152,15 @@ unregister_session(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid( %% @doc Get session stats -spec(get_session_stats({emqx_types:client_id(), pid()}) -> list(emqx_stats:stats())). -get_session_stats(Session = {ClientId, SPid}) - when is_binary(ClientId), is_pid(SPid) -> +get_session_stats(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> safe_lookup_element(?SESSION_STATS_TAB, Session, []). %% @doc Set session stats -spec(set_session_stats(emqx_types:client_id() | {emqx_types:client_id(), pid()}, - emqx_stats:stats()) -> ok). + emqx_stats:stats()) -> true). set_session_stats(ClientId, Stats) when is_binary(ClientId) -> set_session_stats({ClientId, self()}, Stats); - -set_session_stats(Session = {ClientId, SPid}, Stats) - when is_binary(ClientId), is_pid(SPid) -> +set_session_stats(Session = {ClientId, SPid}, Stats) when is_binary(ClientId), is_pid(SPid) -> ets:insert(?SESSION_STATS_TAB, {Session, Stats}). %% @doc Lookup a session from registry diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index 74690b4b1..659b3a92b 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -20,7 +20,9 @@ -export([start_link/0]). -export([is_enabled/0]). + -export([register_session/1, lookup_session/1, unregister_session/1]). + %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -30,12 +32,11 @@ -define(LOCK, {?MODULE, cleanup_sessions}). -record(global_session, {sid, pid}). --record(state, {}). -type(session_pid() :: pid()). -%% @doc Start the session manager. --spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +%% @doc Start the global session manager. +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?REGISTRY}, ?MODULE, [], []). @@ -46,19 +47,18 @@ is_enabled() -> -spec(lookup_session(emqx_types:client_id()) -> list({emqx_types:client_id(), session_pid()})). lookup_session(ClientId) -> - [{ClientId, SessionPid} || #global_session{pid = SessionPid} - <- mnesia:dirty_read(?TAB, ClientId)]. + [{ClientId, SessPid} || #global_session{pid = SessPid} <- mnesia:dirty_read(?TAB, ClientId)]. -spec(register_session({emqx_types:client_id(), session_pid()}) -> ok). -register_session({ClientId, SessionPid}) when is_binary(ClientId), is_pid(SessionPid) -> - mnesia:dirty_write(?TAB, record(ClientId, SessionPid)). +register_session({ClientId, SessPid}) when is_binary(ClientId), is_pid(SessPid) -> + mnesia:dirty_write(?TAB, record(ClientId, SessPid)). -spec(unregister_session({emqx_types:client_id(), session_pid()}) -> ok). -unregister_session({ClientId, SessionPid}) when is_binary(ClientId), is_pid(SessionPid) -> - mnesia:dirty_delete_object(?TAB, record(ClientId, SessionPid)). +unregister_session({ClientId, SessPid}) when is_binary(ClientId), is_pid(SessPid) -> + mnesia:dirty_delete_object(?TAB, record(ClientId, SessPid)). -record(ClientId, SessionPid) -> - #global_session{sid = ClientId, pid = SessionPid}. +record(ClientId, SessPid) -> + #global_session{sid = ClientId, pid = SessPid}. %%------------------------------------------------------------------------------ %% gen_server callbacks @@ -72,7 +72,7 @@ init([]) -> {attributes, record_info(fields, global_session)}]), ok = ekka_mnesia:copy_table(?TAB), ekka:monitor(membership), - {ok, #state{}}. + {ok, #{}}. handle_call(Req, _From, State) -> emqx_logger:error("[Registry] unexpected call: ~p", [Req]), @@ -107,9 +107,9 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ cleanup_sessions(Node) -> - Pat = [{#global_session{pid = '$1', _ = '_'}, - [{'==', {node, '$1'}, Node}], ['$_']}], - lists:foreach(fun(Session) -> - mnesia:delete_object(?TAB, Session) - end, mnesia:select(?TAB, Pat)). + Pat = [{#global_session{pid = '$1', _ = '_'}, [{'==', {node, '$1'}, Node}], ['$_']}], + lists:foreach(fun delete_session/1, mnesia:select(?TAB, Pat, write)). + +delete_session(Session) -> + mnesia:delete_object(?TAB, Session, write). diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index 664e04680..510b2d91a 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -31,9 +31,10 @@ code_change/3]). -record(update, {name, countdown, interval, func}). --record(state, {timer, updates :: #update{}}). +-record(state, {timer, updates :: [#update{}]}). -type(stats() :: list({atom(), non_neg_integer()})). + -export_type([stats/0]). %% Connection stats diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 74014707a..fa08fa1bb 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -40,38 +40,61 @@ keepalive, enable_stats, stats_timer, - shutdown_reason + shutdown }). --define(INFO_KEYS, [peername, sockname]). - -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -define(WSLOG(Level, Format, Args, State), - emqx_logger:Level("WSMQTT(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). + emqx_logger:Level("MQTT/WS(~s): " ++ Format, + [esockd_net:format(State#state.peername) | Args])). %%------------------------------------------------------------------------------ %% API %%------------------------------------------------------------------------------ %% for debug -info(WSPid) -> - call(WSPid, info). +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}, + {peername, Peername}, + {sockname, Sockname}], + lists:append([ConnInfo, ProtoInfo]). %% for dashboard -attrs(CPid) when is_pid(CPid) -> - call(CPid, attrs). +attrs(WSPid) when is_pid(WSPid) -> + call(WSPid, attrs); -stats(WSPid) -> - call(WSPid, stats). +attrs(#state{peername = Peername, + sockname = Sockname, + proto_state = ProtoState}) -> + SockAttrs = [{peername, Peername}, + {sockname, Sockname}], + ProtoAttrs = emqx_protocol:attrs(ProtoState), + lists:usort(lists:append(SockAttrs, ProtoAttrs)). -kick(WSPid) -> +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) -> +session(WSPid) when is_pid(WSPid) -> call(WSPid, session). -call(WSPid, Req) -> +call(WSPid, Req) when is_pid(WSPid) -> Mref = erlang:monitor(process, WSPid), WSPid ! {call, {self(), Mref}, Req}, receive @@ -153,41 +176,30 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, websocket_handle({binary, Rest}, reset_parser(State#state{proto_state = ProtoState1})); {error, Error} -> ?WSLOG(error, "Protocol error - ~p", [Error], State), - {stop, State}; - {error, Error, ProtoState1} -> - shutdown(Error, State#state{proto_state = ProtoState1}); - {stop, Reason, ProtoState1} -> - shutdown(Reason, State#state{proto_state = ProtoState1}) + stop(Error, State); + {error, Reason, ProtoState1} -> + shutdown(Reason, State#state{proto_state = ProtoState1}); + {stop, Error, ProtoState1} -> + stop(Error, State#state{proto_state = ProtoState1}) end; {error, Error} -> ?WSLOG(error, "Frame error: ~p", [Error], State), - {stop, State}; + stop(Error, State); {'EXIT', Reason} -> ?WSLOG(error, "Frame error:~p~nFrame data: ~p", [Reason, Data], State), - {stop, State} + shutdown(parse_error, State) end. -websocket_info({call, From, info}, State = #state{peername = Peername, - sockname = Sockname, - proto_state = ProtoState}) -> - ProtoInfo = emqx_protocol:info(ProtoState), - ConnInfo = [{socktype, websocket}, {conn_state, running}, - {peername, Peername}, {sockname, Sockname}], - gen_server:reply(From, lists:append([ConnInfo, ProtoInfo])), +websocket_info({call, From, info}, State) -> + gen_server:reply(From, info(State)), {ok, State}; -websocket_info({call, From, attrs}, State = #state{peername = Peername, - sockname = Sockname, - proto_state = ProtoState}) -> - SockAttrs = [{peername, Peername}, - {sockname, Sockname}], - ProtoAttrs = emqx_protocol:attrs(ProtoState), - gen_server:reply(From, lists:usort(lists:append(SockAttrs, ProtoAttrs))), +websocket_info({call, From, attrs}, State) -> + gen_server:reply(From, attrs(State)), {ok, State}; -websocket_info({call, From, stats}, State = #state{proto_state = ProtoState}) -> - Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(), emqx_protocol:stats(ProtoState)]), - gen_server:reply(From, Stats), +websocket_info({call, From, stats}, State) -> + gen_server:reply(From, stats(State)), {ok, State}; websocket_info({call, From, kick}, State) -> @@ -203,15 +215,12 @@ websocket_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> {ok, ProtoState1} -> {ok, ensure_stats_timer(State#state{proto_state = ProtoState1})}; {error, Reason} -> - shutdown(Reason, State); - {error, Reason, ProtoState1} -> - shutdown(Reason, State#state{proto_state = ProtoState1}) + shutdown(Reason, State) end; -websocket_info(emit_stats, State = #state{proto_state = ProtoState}) -> - Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(), - emqx_protocol:stats(ProtoState)]), - emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), Stats), +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) -> @@ -254,30 +263,40 @@ websocket_info(Info, State) -> ?WSLOG(error, "unexpected info: ~p", [Info], State), {ok, State}. -terminate(SockError, _Req, #state{keepalive = Keepalive, - proto_state = ProtoState, - shutdown_reason = Reason}) -> +terminate(SockError, _Req, State = #state{keepalive = Keepalive, + proto_state = ProtoState, + shutdown = Shutdown}) -> + ?WSLOG(debug, "Terminated for ~p, sockerror: ~p", + [Shutdown, SockError], State), emqx_keepalive:cancel(Keepalive), - io:format("Websocket shutdown for ~p, sockerror: ~p~n", [Reason, SockError]), - case Reason of - undefined -> - ok; - _ -> - emqx_protocol:shutdown(Reason, ProtoState) + case {ProtoState, Shutdown} of + {undefined, _} -> ok; + {_, {shutdown, Reason}} -> + emqx_protocol:shutdown(Reason, ProtoState); + {_, Error} -> + emqx_protocol:shutdown(Error, ProtoState) end. +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + reset_parser(State = #state{proto_state = ProtoState}) -> State#state{parser_state = emqx_protocol:parser(ProtoState)}. ensure_stats_timer(State = #state{enable_stats = true, stats_timer = undefined, - idle_timeout = Timeout}) -> - State#state{stats_timer = erlang:send_after(Timeout, self(), emit_stats)}; + idle_timeout = IdleTimeout}) -> + State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; ensure_stats_timer(State) -> State. shutdown(Reason, State) -> - {stop, State#state{shutdown_reason = Reason}}. + {stop, State#state{shutdown = Reason}}. + +stop(Error, State) -> + {stop, State#state{shutdown = Error}}. wsock_stats() -> [{Key, get(Key)} || Key <- ?SOCK_STATS]. + From 7c45d988f2d72e4477040e2e4751033abbd5ebfc Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 31 Aug 2018 16:57:43 +0800 Subject: [PATCH 225/520] Update the spec of deliver/2 function --- src/emqx_protocol.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 018166d20..c703d8a24 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -451,7 +451,7 @@ puback(?QOS_2, PacketId, {ok, _}, PState) -> %% Deliver Packet -> Client %%------------------------------------------------------------------------------ --spec(deliver(term(), state()) -> {ok, state()} | {error, term()}). +-spec(deliver(tuple(), state()) -> {ok, state()} | {error, term()}). deliver({connack, ReasonCode}, PState) -> send(?CONNACK_PACKET(ReasonCode), PState); From 7d90f603b5234ff98134dcc1a516f378c1a4e6e7 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 31 Aug 2018 17:10:40 +0800 Subject: [PATCH 226/520] Update README.md --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9ca4f447d..2f9e31cd8 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ *EMQ X* broker is a fully open source, highly scalable, highly available distributed message 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, STOMP and SockJS. The 3.0 release of the *EMQ X* broker can scaled to 10+ million concurrent MQTT connections on one cluster. +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/emqtt/emqttd/releases/). @@ -67,7 +67,8 @@ Please submit any bugs, issues, and feature requests to [emqtt/emqttd](//github. ## License -Copyright (c) 2014-2018 [EMQ X Tech, LLC](http://emqtt.io) + +Copyright (c) 2018 [EMQ Technologies Co., Ltd](http://emqtt.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 @@ -76,6 +77,3 @@ Licensed under the Apache License, Version 2.0 (the "License");you may not use t 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. - - - From 880c6ab5feb0963d3eae9775f6274d59273f6789 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 31 Aug 2018 17:27:36 +0800 Subject: [PATCH 227/520] Fix typo --- src/emqx_protocol.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index c703d8a24..fb7c003d7 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -102,7 +102,7 @@ init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) enable_acl = emqx_zone:get_env(Zone, enable_acl), recv_stats = #{msg => 0, pkt => 0}, send_stats = #{msg => 0, pkt => 0}, - connected = fasle}. + connected = false}. init_username(Peercert, Options) -> case proplists:get_value(peer_cert_as_username, Options) of From 3f42f1271b4bf4b1e62f26530a6de4cf2f1b8f38 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 31 Aug 2018 18:14:10 +0800 Subject: [PATCH 228/520] bug fix --- src/emqx_session.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 81299711d..75c3ac197 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -720,7 +720,7 @@ run_dispatch_steps([], Msg, State) -> dispatch(Msg, State); run_dispatch_steps([{nl, 1}|_Steps], #message{from = ClientId}, State = #state{client_id = ClientId}) -> State; -run_dispatch_steps([{nl, 0}|Steps], Msg, State) -> +run_dispatch_steps([{nl, _}|Steps], Msg, State) -> run_dispatch_steps(Steps, Msg, State); run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = #state{upgrade_qos = false}) -> run_dispatch_steps(Steps, Msg#message{qos = min(SubQoS, PubQoS)}, State); @@ -897,4 +897,3 @@ shutdown(Reason, State) -> %% TODO: GC Policy and Shutdown Policy %% maybe_gc(State) -> State. - From 14ba395c21d94d84e9251590cc440b3acbea381d Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Fri, 31 Aug 2018 19:15:17 +0800 Subject: [PATCH 229/520] fix seesion and connectiong test suites --- test/emqx_connection_SUITE.erl | 23 ++++++++++++++++++++--- test/emqx_listeners_SUITE.erl | 9 ++++----- test/emqx_session_SUITE.erl | 8 +++++++- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/test/emqx_connection_SUITE.erl b/test/emqx_connection_SUITE.erl index ae69cdd5d..716e771b5 100644 --- a/test/emqx_connection_SUITE.erl +++ b/test/emqx_connection_SUITE.erl @@ -19,12 +19,29 @@ -include_lib("common_test/include/ct.hrl"). -all() -> [t_attrs]. +all() -> + [{group, connection}]. + +groups() -> + [{connection, [sequence], [t_attrs]}]. + +init_per_suite(Config) -> + emqx_ct_broker_helpers:run_setup_steps(), + Config. + +end_per_suite(_Config) -> + emqx_ct_broker_helpers:run_teardown_steps(). + t_attrs(_) -> - emqx_ct_broker_helpers:run_setup_steps(), {ok, C, _} = emqx_client:start_link([{host, "localhost"}, {client_id, <<"simpleClient">>}, {username, <<"plain">>}, {password, <<"plain">>}]), [{<<"simpleClient">>, ConnPid}] = emqx_cm:lookup_connection(<<"simpleClient">>), Attrs = emqx_connection:attrs(ConnPid), <<"simpleClient">> = proplists:get_value(client_id, Attrs), - <<"plain">> = proplists:get_value(username, Attrs). \ No newline at end of file + <<"plain">> = proplists:get_value(username, Attrs), + emqx_client:disconnect(C). + +%% t_stats() -> +%% {ok, C, _ } = emqx_client; +%% t_stats() -> + diff --git a/test/emqx_listeners_SUITE.erl b/test/emqx_listeners_SUITE.erl index d55bba400..6086e98c2 100644 --- a/test/emqx_listeners_SUITE.erl +++ b/test/emqx_listeners_SUITE.erl @@ -28,15 +28,14 @@ all() -> [start_stop_listeners, restart_listeners]. -init_per_suite() -> +init_per_suite(Config) -> NewConfig = generate_config(), application:ensure_all_started(esockd), lists:foreach(fun set_app_env/1, NewConfig), - ok. + Config. -end_per_suite() -> - application:stop(esockd), - ok. +end_per_suite(_Config) -> + application:stop(esockd). start_stop_listeners(_) -> ok = emqx_listeners:start(), diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index f2da10b74..526f8b729 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -21,8 +21,14 @@ all() -> [t_session_all]. -t_session_all(_) -> +init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), + Config. + +end_per_suite(_Config) -> + emqx_ct_broker_helpers:run_teardown_steps(). + +t_session_all(_) -> ClientId = <<"ClientId">>, {ok, ConnPid} = emqx_mock_client:start_link(ClientId), {ok, SPid} = emqx_mock_client:open_session(ConnPid, ClientId, internal), From 2f63b7a487cb83e841c5c66c0ddb890bf6999d3a Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Fri, 31 Aug 2018 20:21:28 +0800 Subject: [PATCH 230/520] update broker suite for latest code --- test/emqx_broker_SUITE.erl | 24 ++++++++++++------------ test/emqx_session_SUITE.erl | 1 + 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index 93f795d1d..e23330f7b 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -62,12 +62,12 @@ end_per_suite(_Config) -> %%-------------------------------------------------------------------- subscribe_unsubscribe(_) -> - ok = emqx:subscribe(<<"topic">>, "clientId"), - ok = emqx:subscribe(<<"topic/1">>, "clientId", #{ qos => 1 }), - ok = emqx:subscribe(<<"topic/2">>, "clientId", #{ qos => 2 }), - ok = emqx:unsubscribe(<<"topic">>, "clientId"), - ok = emqx:unsubscribe(<<"topic/1">>, "clientId"), - ok = emqx:unsubscribe(<<"topic/2">>, "clientId"). + ok = emqx:subscribe(<<"topic">>, <<"clientId">>), + ok = emqx:subscribe(<<"topic/1">>, <<"clientId">>, #{ qos => 1 }), + ok = emqx:subscribe(<<"topic/2">>, <<"clientId">>, #{ qos => 2 }), + ok = emqx:unsubscribe(<<"topic">>, <<"clientId">>), + ok = emqx:unsubscribe(<<"topic/1">>, <<"clientId">>), + ok = emqx:unsubscribe(<<"topic/2">>, <<"clientId">>). publish(_) -> Msg = emqx_message:make(ct, <<"test/pubsub">>, <<"hello">>), @@ -79,9 +79,9 @@ publish(_) -> pubsub(_) -> Self = self(), Subscriber = {Self, <<"clientId">>}, - ok = emqx:subscribe(<<"a/b/c">>, Subscriber, #{ qos => 1 }), + ok = emqx:subscribe(<<"a/b/c">>, <<"clientId">>, #{ qos => 1 }), #{ qos := 1} = ets:lookup_element(emqx_suboption, {<<"a/b/c">>, Subscriber}, 2), - ok = emqx:subscribe(<<"a/b/c">>, Subscriber, #{ qos => 2 }), + ok = emqx:subscribe(<<"a/b/c">>, <<"clientId">>, #{ qos => 2 }), #{ qos := 2} = ets:lookup_element(emqx_suboption, {<<"a/b/c">>, Subscriber}, 2), %% ct:log("Emq Sub: ~p.~n", [ets:lookup(emqx_suboption, {<<"a/b/c">>, Subscriber})]), timer:sleep(10), @@ -100,8 +100,8 @@ pubsub(_) -> t_local_subscribe(_) -> ok = emqx:subscribe(<<"$local/topic0">>), - ok = emqx:subscribe(<<"$local/topic1">>, "clientId"), - ok = emqx:subscribe(<<"$local/topic2">>, "clientId", #{ qos => 2 }), + ok = emqx:subscribe(<<"$local/topic1">>, <<"clientId">>), + ok = emqx:subscribe(<<"$local/topic2">>, <<"clientId">>, #{ qos => 2 }), timer:sleep(10), ?assertEqual([{self(), undefined}], emqx:subscribers("$local/topic0")), ?assertEqual([{self(), <<"clientId">>}], emqx:subscribers("$local/topic1")), @@ -110,8 +110,8 @@ t_local_subscribe(_) -> emqx:subscriptions({self(), <<"clientId">>})), ?assertEqual(ok, emqx:unsubscribe("$local/topic0")), ?assertEqual(ok, emqx:unsubscribe("$local/topic0")), - ?assertEqual(ok, emqx:unsubscribe("$local/topic1", "clientId")), - ?assertEqual(ok, emqx:unsubscribe("$local/topic2", "clientId")), + ?assertEqual(ok, emqx:unsubscribe("$local/topic1", <<"clientId">>)), + ?assertEqual(ok, emqx:unsubscribe("$local/topic2", <<"clientId">>)), ?assertEqual([], emqx:subscribers("topic1")), ?assertEqual([], emqx:subscriptions({self(), <<"clientId">>})). diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index 526f8b729..29a6edc61 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -1,3 +1,4 @@ + %% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); From ec456dcc7315a440b66714e28e74fcff1b495d07 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 31 Aug 2018 20:48:26 +0800 Subject: [PATCH 231/520] Fix dialyze issue --- src/emqx_base62.erl | 3 +-- src/emqx_bridge.erl | 4 ++-- src/emqx_bridge_sup.erl | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/emqx_base62.erl b/src/emqx_base62.erl index 690115ec2..1a9db245a 100644 --- a/src/emqx_base62.erl +++ b/src/emqx_base62.erl @@ -22,7 +22,7 @@ %% @doc Encode any data to base62 binary -spec encode(string() | integer() - | binary()) -> float(). + | binary()) -> binary(). encode(I) when is_integer(I) -> encode(integer_to_binary(I)); encode(S) when is_list(S)-> @@ -110,4 +110,3 @@ decode_char(I) when I >= $A andalso I =< $Z-> decode_char(9, I) -> I + 61 - $A. - diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index 0f64f331d..a7ce2581d 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -117,7 +117,7 @@ handle_info(start, State = #state{options = Options, {noreply, State#state{client_pid = ClientPid}}; {error,_} -> erlang:send_after(ReconnectTime, self(), start), - {noreply, State = #state{reconnect_count = ReconnectCount-1}} + {noreply, State#state{reconnect_count = ReconnectCount-1}} end; %%---------------------------------------------------------------- @@ -138,7 +138,7 @@ handle_info(start, State = #state{options = Options, {noreply, State#state{client_pid = ClientPid}}; {error,_} -> erlang:send_after(ReconnectTime, self(), start), - {noreply, State = #state{reconnect_count = ReconnectCount-1}} + {noreply, State#state{reconnect_count = ReconnectCount-1}} end; %%---------------------------------------------------------------- diff --git a/src/emqx_bridge_sup.erl b/src/emqx_bridge_sup.erl index 0d8a0d887..bc8c0a532 100644 --- a/src/emqx_bridge_sup.erl +++ b/src/emqx_bridge_sup.erl @@ -27,7 +27,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% @doc List all bridges --spec(bridges() -> [{node(), emqx_topic:topic(), pid()}]). +-spec(bridges() -> [{node(), Status :: binary()}]). bridges() -> [{Name, emqx_bridge:status(Pid)} || {Name, Pid, _, _} <- supervisor:which_children(?MODULE)]. From abc6081282c9b0021e84fa6c15a5ea34adf632fe Mon Sep 17 00:00:00 2001 From: chenyy Date: Sat, 1 Sep 2018 11:54:28 +0800 Subject: [PATCH 232/520] fix error spelling word --- src/emqx_protocol.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index fb7c003d7..ec104799e 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -121,13 +121,13 @@ set_username(_Username, PState) -> %%------------------------------------------------------------------------------ info(PState = #pstate{conn_props = ConnProps, - ack_props = AclProps, + ack_props = AckProps, session = Session, topic_aliases = Aliases, will_msg = WillMsg, enable_acl = EnableAcl}) -> attrs(PState) ++ [{conn_props, ConnProps}, - {ack_props, AclProps}, + {ack_props, AckProps}, {session, Session}, {topic_aliases, Aliases}, {will_msg, WillMsg}, From 7f12db018079f228d602aee928f1dc9a4fe1566b Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Mon, 3 Sep 2018 14:03:20 +0800 Subject: [PATCH 233/520] add editorconfig for emqx --- .editorconfig | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c563aa10d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{erl, src, hrl}] +indent_style = space +indent_size = 4 + +# Tab indentation (no size specified) +[Makefile] +indent_style = tab + +# Matches the exact files either package.json or .travis.yml +[{.travis.yml}] +indent_style = space +indent_size = 2 From eb53d366e9032faef940d9b34ff4f18b98ce3a43 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 3 Sep 2018 17:57:53 +0800 Subject: [PATCH 234/520] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f9e31cd8..41c6ce8b6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # *EMQ X* - MQTT Broker -*EMQ X* broker is a fully open source, highly scalable, highly available distributed message broker for IoT, M2M and Mobile applications that can handle tens of millions of concurrent clients. +*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. From 842f4fbf13f223c45dde565ae40ae8dad4637517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Tue, 4 Sep 2018 11:16:15 +0800 Subject: [PATCH 235/520] Crash when reserved flag in CONNECT packet doesn't equal to 0, and remove repeated check for protocol version --- src/emqx_frame.erl | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 3ec935020..ab43fb58c 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -120,19 +120,17 @@ wrap(Header, Rest) -> parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) -> {ProtoName, Rest} = parse_utf8_string(FrameBin), <> = Rest, + % Note: Crash when reserved flag doesn't equal to 0, there is no strict compliance with the MQTT5.0. <> = Rest1, - case protocol_approved(ProtoVer, ProtoName) of - true -> ok; - false -> error(protocol_name_unapproved) - end, + {Properties, Rest3} = parse_properties(Rest2, ProtoVer), {ClientId, Rest4} = parse_utf8_string(Rest3), ConnPacket = #mqtt_packet_connect{proto_name = ProtoName, From 96122cf966d477eae1d2c88f4b9d2a88194abef2 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 4 Sep 2018 19:14:25 +0800 Subject: [PATCH 236/520] Rename 'already_existed' to 'already_exists' --- src/emqx_access_control.erl | 2 +- src/emqx_tracer.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 8301bd8d8..140f82bb6 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -154,7 +154,7 @@ init([]) -> handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) -> Mods = lookup_mods(Type), reply(case lists:keymember(Mod, 1, Mods) of - true -> {error, already_existed}; + true -> {error, already_exists}; false -> case catch Mod:init(Opts) of {ok, ModState} -> diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index 65e6f6378..44ad6c26f 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -78,7 +78,7 @@ init([]) -> handle_call({start_trace, Who, LogFile}, _From, State = #state{level = Level, traces = Traces}) -> case catch lager:trace_file(LogFile, [Who], Level, ?OPTIONS) of {ok, exists} -> - {reply, {error, already_existed}, State}; + {reply, {error, already_exists}, State}; {ok, Trace} -> {reply, ok, State#state{traces = maps:put(Who, {Trace, LogFile}, Traces)}}; {error, Reason} -> From f0f818ab1a25a062006bdd89bd1ec74bca5fe455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Wed, 5 Sep 2018 10:28:49 +0800 Subject: [PATCH 237/520] Fix bug in emqx_frame --- src/emqx_frame.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index ab43fb58c..1a0332f5b 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -127,7 +127,7 @@ parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) -> WillQoS : 2, WillFlag : 1, CleanStart : 1, - 0 : 0, + 0 : 1, KeepAlive : 16/big, Rest2/binary>> = Rest1, From aa34258f1e36a2e85eb48f09d06c06b065f5767d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Wed, 5 Sep 2018 14:25:33 +0800 Subject: [PATCH 238/520] Support Retain As Published in Subscription Options --- src/emqx_session.erl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 75c3ac197..e7e63763c 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -548,10 +548,10 @@ handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> %% Dispatch message handle_info({dispatch, Topic, Msg}, State = #state{subscriptions = SubMap}) when is_record(Msg, message) -> noreply(case maps:find(Topic, SubMap) of - {ok, #{nl := Nl, qos := QoS, subid := SubId}} -> - run_dispatch_steps([{nl, Nl},{qos, QoS}, {subid, SubId}], Msg, State); - {ok, #{nl := Nl, qos := QoS}} -> - run_dispatch_steps([{nl, Nl},{qos, QoS}], Msg, State); + {ok, #{nl := Nl, qos := QoS, rap := Rap, subid := SubId}} -> + run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}, {subid, SubId}], Msg, State); + {ok, #{nl := Nl, qos := QoS, rap := Rap}} -> + run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}], Msg, State); error -> dispatch(emqx_message:unset_flag(dup, Msg), State) end); @@ -726,6 +726,11 @@ run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = run_dispatch_steps(Steps, Msg#message{qos = min(SubQoS, PubQoS)}, State); run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = #state{upgrade_qos = true}) -> run_dispatch_steps(Steps, Msg#message{qos = max(SubQoS, PubQoS)}, State); +run_dispatch_steps([{rap, false}|Steps], Msg = #message{flags = Flags}, State = #state{}) -> + Flags1 = maps:put(retain, false, Flags), + run_dispatch_steps(Steps, Msg#message{flags = Flags1}, State); +run_dispatch_steps([{rap, _}|Steps], Msg, State) -> + run_dispatch_steps(Steps, Msg, State); run_dispatch_steps([{subid, SubId}|Steps], Msg, State) -> run_dispatch_steps(Steps, emqx_message:set_header('Subscription-Identifier', SubId, Msg), State). From 47955f4309a4b2e9158c56f51b44a42b161cbaf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Wed, 5 Sep 2018 15:18:26 +0800 Subject: [PATCH 239/520] fix bug in retain as published flag --- src/emqx_mqtt_types.erl | 4 ++-- src/emqx_session.erl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqx_mqtt_types.erl b/src/emqx_mqtt_types.erl index 0b231fc88..6beb17780 100644 --- a/src/emqx_mqtt_types.erl +++ b/src/emqx_mqtt_types.erl @@ -32,8 +32,8 @@ -type(reason_code() :: 0..16#FF). -type(packet_id() :: 1..16#FFFF). -type(properties() :: #{atom() => term()}). --type(subopts() :: #{rh := 0 | 1, - rap := 0 | 1 | 2, +-type(subopts() :: #{rh := 0 | 1 | 2, + rap := 0 | 1, nl := 0 | 1, qos := qos(), rc => reason_code() diff --git a/src/emqx_session.erl b/src/emqx_session.erl index e7e63763c..e8f8ed249 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -726,7 +726,7 @@ run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = run_dispatch_steps(Steps, Msg#message{qos = min(SubQoS, PubQoS)}, State); run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = #state{upgrade_qos = true}) -> run_dispatch_steps(Steps, Msg#message{qos = max(SubQoS, PubQoS)}, State); -run_dispatch_steps([{rap, false}|Steps], Msg = #message{flags = Flags}, State = #state{}) -> +run_dispatch_steps([{rap, 0}|Steps], Msg = #message{flags = Flags}, State = #state{}) -> Flags1 = maps:put(retain, false, Flags), run_dispatch_steps(Steps, Msg#message{flags = Flags1}, State); run_dispatch_steps([{rap, _}|Steps], Msg, State) -> From 9029ee29d33cfa07acfe98d6d7b52b4dfbbe9d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Wed, 5 Sep 2018 18:03:28 +0800 Subject: [PATCH 240/520] Drop will msg when receive the DISCONNECT packet whose reason code is equal to 0x00 --- src/emqx_protocol.erl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index ec104799e..013fa3c41 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -396,9 +396,15 @@ process_packet(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), process_packet(?PACKET(?PINGREQ), PState) -> send(?PACKET(?PINGRESP), PState); -process_packet(?PACKET(?DISCONNECT), PState) -> +process_packet(?DISCONNECT_PACKET(16#00), PState) -> %% Clean willmsg - {stop, normal, PState#pstate{will_msg = undefined}}. + {stop, normal, PState#pstate{will_msg = undefined}}; +process_packet(?DISCONNECT_PACKET(_), PState) -> + {stop, normal, PState}; +process_packet(Packet = ?PACKET(?DISCONNECT), PState) -> + if Packet#mqtt_packet.variable =:= undefined -> + {stop, normal, PState#pstate{will_msg = undefined}} + end. %%------------------------------------------------------------------------------ %% ConnAck --> Client From c8b92a59b167b4b292528158cf55ea8dec97132f Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Wed, 5 Sep 2018 19:06:34 +0800 Subject: [PATCH 241/520] check topic alias --- src/emqx_mqtt_caps.erl | 10 +++++-- src/emqx_protocol.erl | 66 ++++++++++++++++++++++-------------------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl index fdc29fae8..baec9920c 100644 --- a/src/emqx_mqtt_caps.erl +++ b/src/emqx_mqtt_caps.erl @@ -43,7 +43,9 @@ {mqtt_wildcard_subscription, true}]). -define(PUBCAP_KEYS, [max_qos_allowed, - mqtt_retain_available]). + mqtt_retain_available, + mqtt_topic_alias + ]). -define(SUBCAP_KEYS, [max_qos_allowed, max_topic_levels, mqtt_shared_subscription, @@ -60,6 +62,11 @@ do_check_pub(Props = #{qos := QoS}, [{max_qos_allowed, MaxQoS}|Caps]) -> 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]) -> {error, ?RC_RETAIN_NOT_SUPPORTED}; do_check_pub(Props, [{mqtt_retain_available, _}|Caps]) -> @@ -136,4 +143,3 @@ with_env(Zone, Key, InitFun) -> Caps; ZoneCaps -> ZoneCaps end. - diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index ec104799e..898d0a04e 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -33,36 +33,36 @@ -export([shutdown/2]). -record(pstate, { - zone, - sendfun, - peername, - peercert, - proto_ver, - proto_name, - ackprops, - client_id, - is_assigned, - conn_pid, - conn_props, - ack_props, - username, - session, - clean_start, - topic_aliases, - packet_size, - will_topic, - will_msg, - keepalive, - mountpoint, - is_super, - is_bridge, - enable_ban, - enable_acl, - recv_stats, - send_stats, - connected, - connected_at - }). + zone, + sendfun, + peername, + peercert, + proto_ver, + proto_name, + ackprops, + client_id, + is_assigned, + conn_pid, + conn_props, + ack_props, + username, + session, + clean_start, + topic_aliases, + packet_size, + will_topic, + will_msg, + keepalive, + mountpoint, + is_super, + is_bridge, + enable_ban, + enable_acl, + recv_stats, + send_stats, + connected, + connected_at + }). -type(state() :: #pstate{}). -export_type([state/0]). @@ -631,9 +631,11 @@ check_publish(Packet, PState) -> run_check_steps([fun check_pub_caps/2, fun check_pub_acl/2], Packet, PState). -check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = R}}, +check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, + variable = #mqtt_packet_publish{ properties = Properties}}, #pstate{zone = Zone}) -> - emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => R}). + #{'Topic-Alias' := TopicAlias} = Properties, + emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain, topic_alias => TopicAlias}). check_pub_acl(_Packet, #pstate{is_super = IsSuper, enable_acl = EnableAcl}) when IsSuper orelse (not EnableAcl) -> From 46359214589adc41f8ac5caaff405163ac660e8f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 5 Sep 2018 23:21:06 +0800 Subject: [PATCH 242/520] Rewrite the hooks module --- src/emqx_hooks.erl | 199 ++++++++++++++++++++++++--------------------- 1 file changed, 106 insertions(+), 93 deletions(-) diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index 0aa3e274c..64c1dc596 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -16,142 +16,160 @@ -behaviour(gen_server). --export([start_link/0]). +-export([start_link/0, stop/0]). %% Hooks API --export([add/3, add/4, delete/2, run/2, run/3, lookup/1]). +-export([add/2, add/3, add/4, del/2, run/2, run/3, lookup/1]). %% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). --record(state, {}). +-type(hookpoint() :: atom()). +-type(action() :: function() | mfa()). +-type(filter() :: function() | mfa()). --type(hooktag() :: atom() | string() | binary()). +-record(callback, {action :: action(), + filter :: filter(), + priority :: integer()}). --export_type([hooktag/0]). +-record(hook, {name :: hookpoint(), callbacks :: list(#callback{})}). --record(callback, {tag :: hooktag(), - function :: function(), - init_args = [] :: list(any()), - priority = 0 :: integer()}). - --record(hook, {name :: atom(), callbacks = [] :: list(#callback{})}). +-export_type([hookpoint/0, action/0, filter/0]). -define(TAB, ?MODULE). +-define(SERVER, ?MODULE). +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + gen_server:start_link({local, ?SERVER}, ?MODULE, [], [{hibernate_after, 60000}]). -%%-------------------------------------------------------------------- +-spec(stop() -> ok). +stop() -> + gen_server:stop(?SERVER, normal, infinity). + +%%------------------------------------------------------------------------------ %% Hooks API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ --spec(add(atom(), function() | {hooktag(), function()}, list(any())) -> ok). -add(HookPoint, Function, InitArgs) when is_function(Function) -> - add(HookPoint, {undefined, Function}, InitArgs, 0); +%% @doc Register a callback +-spec(add(hookpoint(), action() | #callback{}) -> emqx_types:ok_or_error(already_exists)). +add(HookPoint, Callback) when is_record(Callback, callback) -> + gen_server:call(?SERVER, {add, HookPoint, Callback}, infinity); +add(HookPoint, Action) when is_function(Action); is_tuple(Action) -> + add(HookPoint, #callback{action = Action, priority = 0}). -add(HookPoint, {Tag, Function}, InitArgs) when is_function(Function) -> - add(HookPoint, {Tag, Function}, InitArgs, 0). +-spec(add(hookpoint(), action(), filter() | integer() | list()) + -> emqx_types:ok_or_error(already_exists)). +add(HookPoint, Action, InitArgs) when is_function(Action), is_list(InitArgs) -> + add(HookPoint, #callback{action = {Action, InitArgs}, priority = 0}); +add(HookPoint, Action, Filter) when is_function(Filter); is_tuple(Filter) -> + add(HookPoint, #callback{action = Action, filter = Filter, priority = 0}); +add(HookPoint, Action, Priority) when is_integer(Priority) -> + add(HookPoint, #callback{action = Action, priority = Priority}). --spec(add(atom(), function() | {hooktag(), function()}, list(any()), integer()) -> ok). -add(HookPoint, Function, InitArgs, Priority) when is_function(Function) -> - add(HookPoint, {undefined, Function}, InitArgs, Priority); -add(HookPoint, {Tag, Function}, InitArgs, Priority) when is_function(Function) -> - gen_server:call(?MODULE, {add, HookPoint, {Tag, Function}, InitArgs, Priority}). +-spec(add(hookpoint(), action(), filter(), integer()) + -> emqx_types:ok_or_error(already_exists)). +add(HookPoint, Action, Filter, Priority) -> + add(HookPoint, #callback{action = Action, filter = Filter, priority = Priority}). --spec(delete(atom(), function() | {hooktag(), function()}) -> ok). -delete(HookPoint, Function) when is_function(Function) -> - delete(HookPoint, {undefined, Function}); -delete(HookPoint, {Tag, Function}) when is_function(Function) -> - gen_server:call(?MODULE, {delete, HookPoint, {Tag, Function}}). +%% @doc Unregister a callback. +-spec(del(hookpoint(), action()) -> ok). +del(HookPoint, Action) -> + gen_server:cast(?SERVER, {del, HookPoint, Action}). -%% @doc Run hooks without Acc. +%% @doc Run hooks. -spec(run(atom(), list(Arg :: any())) -> ok | stop). run(HookPoint, Args) -> run_(lookup(HookPoint), Args). +%% @doc Run hooks with Accumulator. -spec(run(atom(), list(Arg :: any()), any()) -> any()). run(HookPoint, Args, Acc) -> run_(lookup(HookPoint), Args, Acc). %% @private -run_([#callback{function = Fun, init_args = InitArgs} | Callbacks], Args) -> - case apply(Fun, lists:append([Args, InitArgs])) of +run_([#callback{action = Action, filter = Filter} | Callbacks], Args) -> + case filtered(Filter, Args) orelse execute(Action, Args) of + true -> run_(Callbacks, Args); ok -> run_(Callbacks, Args); stop -> stop; _Any -> run_(Callbacks, Args) end; - run_([], _Args) -> ok. %% @private -run_([#callback{function = Fun, init_args = InitArgs} | Callbacks], Args, Acc) -> - case apply(Fun, lists:append([Args, [Acc], InitArgs])) of +run_([#callback{action = Action, filter = Filter} | Callbacks], Args, Acc) -> + Args1 = Args ++ [Acc], + case filtered(Filter, Args1) orelse execute(Action, Args1) of + true -> run_(Callbacks, Args, Acc); ok -> run_(Callbacks, Args, Acc); {ok, NewAcc} -> run_(Callbacks, Args, NewAcc); stop -> {stop, Acc}; {stop, NewAcc} -> {stop, NewAcc}; _Any -> run_(Callbacks, Args, Acc) end; - run_([], _Args, Acc) -> {ok, Acc}. --spec(lookup(atom()) -> [#callback{}]). +filtered(undefined, _Args) -> + false; +filtered(Filter, Args) -> + execute(Filter, Args). + +execute(Action, Args) when is_function(Action) -> + erlang:apply(Action, Args); +execute({Fun, InitArgs}, Args) when is_function(Fun) -> + erlang:apply(Fun, Args ++ InitArgs); +execute({M, F, A}, Args) -> + erlang:apply(M, F, Args ++ A). + +%% @doc Lookup callbacks. +-spec(lookup(hookpoint()) -> [#callback{}]). lookup(HookPoint) -> case ets:lookup(?TAB, HookPoint) of - [#hook{callbacks = Callbacks}] -> Callbacks; + [#hook{callbacks = Callbacks}] -> + Callbacks; [] -> [] end. -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% gen_server callbacks -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- init([]) -> - _ = emqx_tables:new(?TAB, [set, protected, {keypos, #hook.name}, - {read_concurrency, true}]), - {ok, #state{}}. + _ = emqx_tables:new(?TAB, [{keypos, #hook.name}, {read_concurrency, true}]), + {ok, #{}}. -handle_call({add, HookPoint, {Tag, Function}, InitArgs, Priority}, _From, State) -> - Callback = #callback{tag = Tag, function = Function, - init_args = InitArgs, priority = Priority}, - {reply, - case ets:lookup(?TAB, HookPoint) of - [#hook{callbacks = Callbacks}] -> - case contain_(Tag, Function, Callbacks) of - false -> - insert_hook_(HookPoint, add_callback_(Callback, Callbacks)); - true -> - {error, already_hooked} - end; - [] -> - insert_hook_(HookPoint, [Callback]) - end, State}; +handle_call({add, HookPoint, Callback = #callback{action = Action}}, _From, State) -> + Reply = case lists:keyfind(Action, 2, Callbacks = lookup(HookPoint)) of + true -> + {error, already_exists}; + false -> + insert_hook(HookPoint, add_callback(Callback, Callbacks)) + end, + {reply, Reply, State}; -handle_call({delete, HookPoint, {Tag, Function}}, _From, State) -> - {reply, - case ets:lookup(?TAB, HookPoint) of - [#hook{callbacks = Callbacks}] -> - case contain_(Tag, Function, Callbacks) of - true -> - insert_hook_(HookPoint, del_callback_(Tag, Function, Callbacks)); - false -> - {error, not_found} - end; - [] -> - {error, not_found} - end, State}; +handle_call({del, HookPoint, Action}, _From, State) -> + case lists:keydelete(Action, 2, lookup(HookPoint)) of + [] -> + ets:delete(?TAB, HookPoint); + Callbacks -> + insert_hook(HookPoint, Callbacks) + end, + {reply, ok, State}; handle_call(Req, _From, State) -> - {reply, {error, {unexpected_request, Req}}, State}. + emqx_logger:error("[Hooks] unexpected call: ~p", [Req]), + {reply, ignored, State}. -handle_cast(_Msg, State) -> +handle_cast(Msg, State) -> + emqx_logger:error("[Hooks] unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info(_Info, State) -> +handle_info(Info, State) -> + emqx_logger:error("[Hooks] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> @@ -160,26 +178,21 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% Internal functions -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- -insert_hook_(HookPoint, Callbacks) -> +insert_hook(HookPoint, Callbacks) -> ets:insert(?TAB, #hook{name = HookPoint, callbacks = Callbacks}), ok. -add_callback_(Callback, Callbacks) -> - lists:keymerge(#callback.priority, Callbacks, [Callback]). +add_callback(C, Callbacks) -> + add_callback(C, Callbacks, []). -del_callback_(Tag, Function, Callbacks) -> - lists:filter( - fun(#callback{tag = Tag1, function = Func1}) -> - not ((Tag =:= Tag1) andalso (Function =:= Func1)) - end, Callbacks). - -contain_(_Tag, _Function, []) -> - false; -contain_(Tag, Function, [#callback{tag = Tag, function = Function}|_Callbacks]) -> - true; -contain_(Tag, Function, [_Callback | Callbacks]) -> - contain_(Tag, Function, Callbacks). +add_callback(C, [], Acc) -> + lists:reverse([C|Acc]); +add_callback(C1 = #callback{priority = P1}, [C2 = #callback{priority = P2}|More], Acc) + when P1 =< P2 -> + add_callback(C1, More, [C2|Acc]); +add_callback(C1, More, Acc) -> + lists:append(lists:reverse(Acc), [C1 | More]). From 5e3aed0b7366aa68d6744187bda04f4de3d41977 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 6 Sep 2018 09:10:47 +0800 Subject: [PATCH 243/520] Add ok_or_error/1 type --- src/emqx_types.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/emqx_types.erl b/src/emqx_types.erl index d31f37303..960aa699a 100644 --- a/src/emqx_types.erl +++ b/src/emqx_types.erl @@ -17,7 +17,7 @@ -include("emqx.hrl"). -export_type([zone/0]). --export_type([startlink_ret/0]). +-export_type([startlink_ret/0, ok_or_error/1]). -export_type([pubsub/0, topic/0, subid/0, subopts/0]). -export_type([client_id/0, username/0, password/0, peername/0, protocol/0]). -export_type([credentials/0, session/0]). @@ -29,6 +29,7 @@ -type(zone() :: atom()). -type(startlink_ret() :: {ok, pid()} | ignore | {error, term()}). +-type(ok_or_error(Reason) :: ok | {error, Reason}). -type(pubsub() :: publish | subscribe). -type(topic() :: binary()). -type(subid() :: binary() | atom()). From 876a983e93f7982b5624bd6c1b303e82f92c3c95 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 6 Sep 2018 13:37:26 +0800 Subject: [PATCH 244/520] Pub Packet delivered from client to server cannot contain sub id --- src/emqx_packet.erl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 715526964..c3eab87e1 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -69,9 +69,9 @@ validate_packet_id(0) -> validate_packet_id(_) -> true. -validate_properties(?SUBSCRIBE, #{'Subscription-Identifier' := I}) - when I =< 0; I >= 16#FFFFFFF -> - error(subscription_identifier_invalid); +validate_properties(?SUBSCRIBE, #{'Subscription-Identifier' := _I}) -> + %% when I =< 0; I >= 16#FFFFFFF -> + error(protocol_error); validate_properties(?PUBLISH, # {'Topic-Alias':= I}) when I =:= 0 -> error(topic_alias_invalid); @@ -236,4 +236,3 @@ format_password(_Password) -> '******'. i(true) -> 1; i(false) -> 0; i(I) when is_integer(I) -> I. - From 9189d4ff41f00f6011fb2cc37eb11bebbb78bdea Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 6 Sep 2018 14:24:07 +0800 Subject: [PATCH 245/520] catch topic_alias_invalid reasoncode --- src/emqx_protocol.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 898d0a04e..025fe9c93 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -312,9 +312,12 @@ process_packet(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PSt case check_publish(Packet, PState) of {ok, PState1} -> do_publish(Packet, PState1); + {error, ?RC_TOPIC_ALIAS_INVALID} -> + ?LOG(error, "Protocol error - ~p", [?RC_TOPIC_ALIAS_INVALID], PState), + {error, ?RC_TOPIC_ALIAS_INVALID, PState}; {error, ReasonCode} -> ?LOG(warning, "Cannot publish qos0 message to ~s for ~s", [Topic, ReasonCode], PState), - {ok, PState} + {error, ReasonCode, PState} end; process_packet(Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), PState) -> From 42b3c9b4d6ce08e6e495fe03da68dce9ac1166d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 6 Sep 2018 14:47:34 +0800 Subject: [PATCH 246/520] Send DISCONNECT packet with reason code PROTOCOL_ERROR when topic is empty, add checks for topics --- src/emqx_protocol.erl | 3 +++ src/emqx_topic.erl | 13 ++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 013fa3c41..cee0af03a 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -208,6 +208,9 @@ received(Packet = ?PACKET(Type), PState) -> true -> {Packet1, PState1} = preprocess_properties(Packet, PState), process_packet(Packet1, inc_stats(recv, Type, PState1)); + {'EXIT', {topic_filters_invalid, _Stacktrace}} -> + deliver({disconnect, ?RC_PROTOCOL_ERROR}, PState), + {error, topic_filters_invalid, PState}; {'EXIT', {Reason, _Stacktrace}} -> deliver({disconnect, ?RC_MALFORMED_PACKET}, PState), {error, Reason, PState} diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index c244a40b3..3dcad0b33 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -184,9 +184,16 @@ parse(Topic = <<"$share/", _/binary>>, #{share := _Group}) -> error({invalid_topic, Topic}); parse(<<"$queue/", Topic1/binary>>, Options) -> parse(Topic1, maps:put(share, <<"$queue">>, Options)); -parse(<<"$share/", Topic1/binary>>, Options) -> - [Group, Topic2] = binary:split(Topic1, <<"/">>), - {Topic2, maps:put(share, Group, Options)}; +parse(Topic = <<"$share/", Topic1/binary>>, Options) -> + case binary:split(Topic1, <<"/">>) of + [<<>>] -> error({invalid_topic, Topic}); + [_] -> error({invalid_topic, Topic}); + [Group, Topic2] -> + case binary:match(Group, [<<"/">>, <<"+">>, <<"#">>]) of + nomatch -> error({invalid_topic, Topic}); + _ -> {Topic2, maps:put(share, Group, Options)} + end + end; parse(Topic, Options) -> {Topic, Options}. From c145cb89f406d4c481bedf459976fa93f09d9e84 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 6 Sep 2018 15:45:18 +0800 Subject: [PATCH 247/520] add validate_properties for PUBLISH and fix error for SUB --- src/emqx_packet.erl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index c3eab87e1..fc90cf492 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -69,12 +69,14 @@ validate_packet_id(0) -> validate_packet_id(_) -> true. -validate_properties(?SUBSCRIBE, #{'Subscription-Identifier' := _I}) -> - %% when I =< 0; I >= 16#FFFFFFF -> - error(protocol_error); -validate_properties(?PUBLISH, # {'Topic-Alias':= I}) +validate_properties(?SUBSCRIBE, #{'Subscription-Identifier' := I}) + when I =< 0; I >= 16#FFFFFFF -> + error(subscription_identifier_invalid); +validate_properties(?PUBLISH, #{'Topic-Alias':= I}) when I =:= 0 -> error(topic_alias_invalid); +validate_properties(?PUBLISH, #{'Subscription-Identifier' := _I}) -> + error(protocol_error); validate_properties(_, _) -> true. From 2a751055808c7b77bdb375e022582b82bfe091a0 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 6 Sep 2018 16:27:16 +0800 Subject: [PATCH 248/520] Improve the Hooks's design --- src/emqx.erl | 65 ++++++++++++++++++-------------- src/emqx_hooks.erl | 38 +++++++++++++------ src/emqx_mod_presence.erl | 24 ++++++------ src/emqx_mod_rewrite.erl | 24 +++++++----- test/emqx_broker_SUITE.erl | 58 +--------------------------- test/emqx_hooks_SUITE.erl | 77 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 166 insertions(+), 120 deletions(-) create mode 100644 test/emqx_hooks_SUITE.erl diff --git a/src/emqx.erl b/src/emqx.erl index 217611171..72f1d6f81 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -29,16 +29,16 @@ -export([get_subopts/2, set_subopts/3]). %% Hooks API --export([hook/4, hook/3, unhook/2, run_hooks/2, run_hooks/3]). +-export([hook/2, hook/3, hook/4, unhook/2, run_hooks/2, run_hooks/3]). %% Shutdown and reboot -export([shutdown/0, shutdown/1, reboot/0]). -define(APP, ?MODULE). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Bootstrap, is_running... -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% @doc Start emqx application -spec(start() -> {ok, list(atom())} | {error, term()}). @@ -62,9 +62,9 @@ is_running(Node) -> Pid when is_pid(Pid) -> true end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% PubSub API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -spec(subscribe(emqx_topic:topic() | string()) -> ok). subscribe(Topic) -> @@ -97,9 +97,9 @@ unsubscribe(Topic, SubId) when is_atom(SubId); is_binary(SubId) -> unsubscribe(Topic, SubPid) when is_pid(SubPid) -> emqx_broker:unsubscribe(iolist_to_binary(Topic), SubPid). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% PubSub management API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -spec(get_subopts(emqx_topic:topic() | string(), emqx_types:subscriber()) -> emqx_types:subopts()). @@ -128,36 +128,43 @@ subscribed(Topic, SubPid) when is_pid(SubPid) -> subscribed(Topic, SubId) when is_atom(SubId); is_binary(SubId) -> emqx_broker:subscribed(iolist_to_binary(Topic), SubId). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Hooks API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ --spec(hook(atom(), function() | {emqx_hooks:hooktag(), function()}, list(any())) - -> ok | {error, term()}). -hook(Hook, TagFunction, InitArgs) -> - emqx_hooks:add(Hook, TagFunction, InitArgs). +-spec(hook(emqx_hooks:hookpoint(), emqx_hooks:action()) -> ok | {error, already_exists}). +hook(HookPoint, Action) -> + emqx_hooks:add(HookPoint, Action). --spec(hook(atom(), function() | {emqx_hooks:hooktag(), function()}, list(any()), integer()) - -> ok | {error, term()}). -hook(Hook, TagFunction, InitArgs, Priority) -> - emqx_hooks:add(Hook, TagFunction, InitArgs, Priority). +-spec(hook(emqx_hooks:hookpoint(), emqx_hooks:action(), emqx_hooks:filter() | integer()) + -> ok | {error, already_exists}). +hook(HookPoint, Action, Priority) when is_integer(Priority) -> + emqx_hooks:add(HookPoint, Action, Priority); +hook(HookPoint, Action, Filter) when is_function(Filter); is_tuple(Filter) -> + emqx_hooks:add(HookPoint, Action, Filter); +hook(HookPoint, Action, InitArgs) when is_list(InitArgs) -> + emqx_hooks:add(HookPoint, Action, InitArgs). --spec(unhook(atom(), function() | {emqx_hooks:hooktag(), function()}) - -> ok | {error, term()}). -unhook(Hook, TagFunction) -> - emqx_hooks:delete(Hook, TagFunction). +-spec(hook(emqx_hooks:hookpoint(), emqx_hooks:action(), emqx_hooks:filter(), integer()) + -> ok | {error, already_exists}). +hook(HookPoint, Action, Filter, Priority) -> + emqx_hooks:add(HookPoint, Action, Filter, Priority). --spec(run_hooks(atom(), list(any())) -> ok | stop). -run_hooks(Hook, Args) -> - emqx_hooks:run(Hook, Args). +-spec(unhook(emqx_hooks:hookpoint(), emqx_hooks:action()) -> ok). +unhook(HookPoint, Action) -> + emqx_hooks:del(HookPoint, Action). --spec(run_hooks(atom(), list(any()), any()) -> {ok | stop, any()}). -run_hooks(Hook, Args, Acc) -> - emqx_hooks:run(Hook, Args, Acc). +-spec(run_hooks(emqx_hooks:hookpoint(), list(any())) -> ok | stop). +run_hooks(HookPoint, Args) -> + emqx_hooks:run(HookPoint, Args). -%%-------------------------------------------------------------------- +-spec(run_hooks(emqx_hooks:hookpoint(), list(any()), any()) -> {ok | stop, any()}). +run_hooks(HookPoint, Args, Acc) -> + emqx_hooks:run(HookPoint, Args, Acc). + +%%------------------------------------------------------------------------------ %% Shutdown and reboot -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ shutdown() -> shutdown(normal). diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index 64c1dc596..073c12870 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -134,16 +134,16 @@ lookup(HookPoint) -> [] -> [] end. -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([]) -> _ = emqx_tables:new(?TAB, [{keypos, #hook.name}, {read_concurrency, true}]), {ok, #{}}. handle_call({add, HookPoint, Callback = #callback{action = Action}}, _From, State) -> - Reply = case lists:keyfind(Action, 2, Callbacks = lookup(HookPoint)) of + Reply = case lists:keymember(Action, 2, Callbacks = lookup(HookPoint)) of true -> {error, already_exists}; false -> @@ -151,18 +151,18 @@ handle_call({add, HookPoint, Callback = #callback{action = Action}}, _From, Stat end, {reply, Reply, State}; -handle_call({del, HookPoint, Action}, _From, State) -> - case lists:keydelete(Action, 2, lookup(HookPoint)) of +handle_call(Req, _From, State) -> + emqx_logger:error("[Hooks] unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast({del, HookPoint, Action}, State) -> + case del_callback(Action, lookup(HookPoint)) of [] -> ets:delete(?TAB, HookPoint); Callbacks -> insert_hook(HookPoint, Callbacks) end, - {reply, ok, State}; - -handle_call(Req, _From, State) -> - emqx_logger:error("[Hooks] unexpected call: ~p", [Req]), - {reply, ignored, State}. + {noreply, State}; handle_cast(Msg, State) -> emqx_logger:error("[Hooks] unexpected msg: ~p", [Msg]), @@ -178,9 +178,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ insert_hook(HookPoint, Callbacks) -> ets:insert(?TAB, #hook{name = HookPoint, callbacks = Callbacks}), ok. @@ -196,3 +196,17 @@ add_callback(C1 = #callback{priority = P1}, [C2 = #callback{priority = P2}|More] add_callback(C1, More, Acc) -> lists:append(lists:reverse(Acc), [C1 | More]). +del_callback(Action, Callbacks) -> + del_callback(Action, Callbacks, []). + +del_callback(_Action, [], Acc) -> + lists:reverse(Acc); +del_callback(Action, [#callback{action = Action} | Callbacks], Acc) -> + del_callback(Action, Callbacks, Acc); +del_callback(Action = {M, F}, [#callback{action = {M, F, _A}} | Callbacks], Acc) -> + del_callback(Action, Callbacks, Acc); +del_callback(Func, [#callback{action = {Func, _A}} | Callbacks], Acc) -> + del_callback(Func, Callbacks, Acc); +del_callback(Action, [Callback | Callbacks], Acc) -> + del_callback(Action, Callbacks, [Callback | Acc]). + diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index 812c3267d..59c675f9a 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -19,24 +19,24 @@ -include("emqx.hrl"). -export([load/1, unload/1]). + -export([on_client_connected/4, on_client_disconnected/3]). +-define(ATTR_KEYS, [clean_start, proto_ver, proto_name, keepalive]). + 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]). on_client_connected(#{client_id := ClientId, username := Username, - peername := {IpAddr, _}}, ConnAck, ConnInfo, Env) -> + peername := {IpAddr, _}}, ConnAck, ConnAttrs, Env) -> + Attrs = lists:filter(fun({K, _}) -> lists:member(K, ?ATTR_KEYS) end, ConnAttrs), case emqx_json:safe_encode([{clientid, ClientId}, {username, Username}, {ipaddress, iolist_to_binary(esockd_net:ntoa(IpAddr))}, - {clean_start, proplists:get_value(clean_start, ConnInfo)}, - {proto_ver, proplists:get_value(proto_ver, ConnInfo)}, - {proto_name, proplists:get_value(proto_name, ConnInfo)}, - {keepalive, proplists:get_value(keepalive, ConnInfo)}, {connack, ConnAck}, - {ts, os:system_time(second)}]) of + {ts, os:system_time(second)} | Attrs]) of {ok, Payload} -> emqx:publish(message(qos(Env), topic(connected, ClientId), Payload)); {error, Reason} -> @@ -55,20 +55,20 @@ on_client_disconnected(#{client_id := ClientId, username := Username}, Reason, E end. unload(_Env) -> - emqx_hooks:delete('client.connected', fun ?MODULE:on_client_connected/4), - emqx_hooks:delete('client.disconnected', fun ?MODULE:on_client_disconnected/3). + emqx_hooks:del('client.connected', fun ?MODULE:on_client_connected/4), + emqx_hooks:del('client.disconnected', fun ?MODULE:on_client_disconnected/3). message(QoS, Topic, Payload) -> - Msg = emqx_message:make(?MODULE, QoS, Topic, iolist_to_binary(Payload)), - emqx_message:set_flag(sys, Msg). + emqx_message:set_flag( + sys, emqx_message:make( + ?MODULE, QoS, Topic, iolist_to_binary(Payload))). topic(connected, ClientId) -> emqx_topic:systop(iolist_to_binary(["clients/", ClientId, "/connected"])); topic(disconnected, ClientId) -> emqx_topic:systop(iolist_to_binary(["clients/", ClientId, "/disconnected"])). -qos(Env) -> - proplists:get_value(qos, Env, 0). +qos(Env) -> proplists:get_value(qos, Env, 0). reason(Reason) when is_atom(Reason) -> Reason; reason({Error, _}) when is_atom(Error) -> Error; diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index a9ff334ce..29dbb660c 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -21,11 +21,15 @@ -export([rewrite_subscribe/3, rewrite_unsubscribe/3, rewrite_publish/2]). -load(Rules0) -> - Rules = compile(Rules0), - 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]). +%%------------------------------------------------------------------------------ +%% 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]). rewrite_subscribe(_Credentials, TopicTable, Rules) -> {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. @@ -37,13 +41,13 @@ rewrite_publish(Message = #message{topic = Topic}, Rules) -> {ok, Message#message{topic = match_rule(Topic, Rules)}}. unload(_) -> - emqx_hooks:delete('client.subscribe', fun ?MODULE:rewrite_subscribe/3), - emqx_hooks:delete('client.unsubscribe',fun ?MODULE:rewrite_unsubscribe/3), - emqx_hooks:delete('message.publish', fun ?MODULE:rewrite_publish/2). + 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). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ match_rule(Topic, []) -> Topic; diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index e23330f7b..0bd3dc599 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -20,7 +20,6 @@ -define(APP, emqx). -include_lib("eunit/include/eunit.hrl"). - -include_lib("common_test/include/ct.hrl"). -include("emqx.hrl"). @@ -32,7 +31,6 @@ all() -> {group, broker}, {group, metrics}, {group, stats}, - {group, hook}, {group, alarms}]. groups() -> @@ -43,10 +41,8 @@ groups() -> t_shared_subscribe, 'pubsub#', 'pubsub+']}, {session, [sequence], [start_session]}, - {broker, [sequence], [hook_unhook]}, {metrics, [sequence], [inc_dec_metric]}, {stats, [sequence], [set_get_stat]}, - {hook, [sequence], [add_delete_hook, run_hooks]}, {alarms, [sequence], [set_alarms]} ]. @@ -165,8 +161,6 @@ start_session(_) -> %%-------------------------------------------------------------------- %% Broker Group %%-------------------------------------------------------------------- -hook_unhook(_) -> - ok. %%-------------------------------------------------------------------- %% Metric Group @@ -178,61 +172,11 @@ inc_dec_metric(_) -> %%-------------------------------------------------------------------- %% Stats Group %%-------------------------------------------------------------------- + set_get_stat(_) -> emqx_stats:setstat('retained/max', 99), 99 = emqx_stats:getstat('retained/max'). -%%-------------------------------------------------------------------- -%% Hook Test -%%-------------------------------------------------------------------- - -add_delete_hook(_) -> - ok = emqx:hook(test_hook, fun ?MODULE:hook_fun1/1, []), - ok = emqx:hook(test_hook, {tag, fun ?MODULE:hook_fun2/1}, []), - {error, already_hooked} = emqx:hook(test_hook, {tag, fun ?MODULE:hook_fun2/1}, []), - Callbacks = [{callback, undefined, fun ?MODULE:hook_fun1/1, [], 0}, - {callback, tag, fun ?MODULE:hook_fun2/1, [], 0}], - Callbacks = emqx_hooks:lookup(test_hook), - ok = emqx:unhook(test_hook, fun ?MODULE:hook_fun1/1), - ct:print("Callbacks: ~p~n", [emqx_hooks:lookup(test_hook)]), - ok = emqx:unhook(test_hook, {tag, fun ?MODULE:hook_fun2/1}), - {error, not_found} = emqx:unhook(test_hook1, {tag, fun ?MODULE:hook_fun2/1}), - [] = emqx_hooks:lookup(test_hook), - - ok = emqx:hook(emqx_hook, fun ?MODULE:hook_fun1/1, [], 9), - ok = emqx:hook(emqx_hook, {"tag", fun ?MODULE:hook_fun2/1}, [], 8), - Callbacks2 = [{callback, "tag", fun ?MODULE:hook_fun2/1, [], 8}, - {callback, undefined, fun ?MODULE:hook_fun1/1, [], 9}], - Callbacks2 = emqx_hooks:lookup(emqx_hook), - ok = emqx:unhook(emqx_hook, fun ?MODULE:hook_fun1/1), - ok = emqx:unhook(emqx_hook, {"tag", fun ?MODULE:hook_fun2/1}), - [] = emqx_hooks:lookup(emqx_hook). - -run_hooks(_) -> - ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun3/4, [init]), - ok = emqx:hook(foldl_hook, {tag, fun ?MODULE:hook_fun3/4}, [init]), - ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun4/4, [init]), - ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun5/4, [init]), - {stop, [r3, r2]} = emqx:run_hooks(foldl_hook, [arg1, arg2], []), - {ok, []} = emqx:run_hooks(unknown_hook, [], []), - - ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun6/2, [initArg]), - ok = emqx:hook(foreach_hook, {tag, fun ?MODULE:hook_fun6/2}, [initArg]), - ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun7/2, [initArg]), - ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun8/2, [initArg]), - stop = emqx:run_hooks(foreach_hook, [arg]). - -hook_fun1([]) -> ok. -hook_fun2([]) -> {ok, []}. - -hook_fun3(arg1, arg2, _Acc, init) -> ok. -hook_fun4(arg1, arg2, Acc, init) -> {ok, [r2 | Acc]}. -hook_fun5(arg1, arg2, Acc, init) -> {stop, [r3 | Acc]}. - -hook_fun6(arg, initArg) -> ok. -hook_fun7(arg, initArg) -> any. -hook_fun8(arg, initArg) -> stop. - set_alarms(_) -> AlarmTest = #alarm{id = <<"1">>, severity = error, title="alarm title", summary="alarm summary"}, emqx_alarm_mgr:set_alarm(AlarmTest), diff --git a/test/emqx_hooks_SUITE.erl b/test/emqx_hooks_SUITE.erl new file mode 100644 index 000000000..b5b278e31 --- /dev/null +++ b/test/emqx_hooks_SUITE.erl @@ -0,0 +1,77 @@ +%% Copyright (c) 2018 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_hooks_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +all() -> + [add_delete_hook, run_hooks]. + +add_delete_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, []), + ?assertEqual({error, already_exists}, + emqx:hook(test_hook, fun ?MODULE:hook_fun2/1, [])), + Callbacks = [{callback, {fun ?MODULE:hook_fun1/1, []}, undefined, 0}, + {callback, {fun ?MODULE:hook_fun2/1, []}, undefined, 0}], + ?assertEqual(Callbacks, emqx_hooks:lookup(test_hook)), + ok = emqx:unhook(test_hook, fun ?MODULE:hook_fun1/1), + ok = emqx:unhook(test_hook, fun ?MODULE:hook_fun2/1), + timer:sleep(1000), + ?assertEqual([], emqx_hooks:lookup(test_hook)), + + ok = emqx:hook(emqx_hook, {?MODULE, hook_fun2, []}, 8), + ok = emqx:hook(emqx_hook, {?MODULE, hook_fun1, []}, 9), + Callbacks2 = [{callback, {?MODULE, hook_fun1, []}, undefined, 9}, + {callback, {?MODULE, hook_fun2, []}, undefined, 8}], + ?assertEqual(Callbacks2, emqx_hooks:lookup(emqx_hook)), + ok = emqx:unhook(emqx_hook, {?MODULE, hook_fun1, []}), + ok = emqx:unhook(emqx_hook, {?MODULE, hook_fun2, []}), + timer:sleep(1000), + ?assertEqual([], emqx_hooks:lookup(emqx_hook)), + ok = emqx_hooks:stop(). + +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]}), + ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun4/4, [init]), + ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun5/4, [init]), + {stop, [r3, r2]} = emqx:run_hooks(foldl_hook, [arg1, arg2], []), + {ok, []} = emqx:run_hooks(unknown_hook, [], []), + + ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun6/2, [initArg]), + {error, already_exists} = emqx:hook(foreach_hook, fun ?MODULE:hook_fun6/2, [initArg]), + ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun7/2, [initArg]), + ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun8/2, [initArg]), + stop = emqx:run_hooks(foreach_hook, [arg]), + ok = emqx_hooks:stop(). + +hook_fun1([]) -> ok. +hook_fun2([]) -> {ok, []}. + +hook_fun3(arg1, arg2, _Acc, init) -> ok. +hook_fun4(arg1, arg2, Acc, init) -> {ok, [r2 | Acc]}. +hook_fun5(arg1, arg2, Acc, init) -> {stop, [r3 | Acc]}. + +hook_fun6(arg, initArg) -> ok. +hook_fun7(arg, initArg) -> any. +hook_fun8(arg, initArg) -> stop. + From 917eb8e29fd3bf43a22f869773671ec5eed83956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 6 Sep 2018 17:17:09 +0800 Subject: [PATCH 249/520] Make DISCONNECT packet with reason code 0x00 when this packet doesn't have payload --- src/emqx_frame.erl | 3 +++ src/emqx_protocol.erl | 8 ++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 1a0332f5b..f3cd33ddb 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -76,6 +76,9 @@ parse_remaining_len(_Bin, _Header, _Multiplier, Length, error(mqtt_frame_too_large); parse_remaining_len(<<>>, Header, Multiplier, Length, Options) -> {more, fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length, Options) end}; +%% Match DISCONNECT without payload +parse_remaining_len(<<0:8, Rest/binary>>, Header = #mqtt_packet_header{type = ?DISCONNECT}, 1, 0, _Options) -> + wrap(Header, #mqtt_packet_disconnect{reason_code = ?RC_SUCCESS}, Rest); %% Match PINGREQ. parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, Options) -> parse_frame(Rest, Header, 0, Options); diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index cee0af03a..4f4f4bfb8 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -399,15 +399,11 @@ process_packet(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), process_packet(?PACKET(?PINGREQ), PState) -> send(?PACKET(?PINGRESP), PState); -process_packet(?DISCONNECT_PACKET(16#00), PState) -> +process_packet(?DISCONNECT_PACKET(?RC_SUCCESS), PState) -> %% Clean willmsg {stop, normal, PState#pstate{will_msg = undefined}}; process_packet(?DISCONNECT_PACKET(_), PState) -> - {stop, normal, PState}; -process_packet(Packet = ?PACKET(?DISCONNECT), PState) -> - if Packet#mqtt_packet.variable =:= undefined -> - {stop, normal, PState#pstate{will_msg = undefined}} - end. + {stop, normal, PState}. %%------------------------------------------------------------------------------ %% ConnAck --> Client From 324cc15dd43da40872e67ccb3d10981107e0977b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 6 Sep 2018 17:38:01 +0800 Subject: [PATCH 250/520] Update README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 41c6ce8b6..b45ac510d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -# *EMQ X* - MQTT Broker - +# EMQ X Broker *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. From d99d0a22d0370a7179e12acd6c2765d26a0fb925 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 6 Sep 2018 17:54:32 +0800 Subject: [PATCH 251/520] Rename 'ignore' to 'ignored' --- src/emqx_access_control.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 140f82bb6..1b9d76937 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -183,7 +183,7 @@ handle_call(stop, _From, State) -> handle_call(Req, _From, State) -> emqx_logger:error("[AccessControl] unexpected request: ~p", [Req]), - {reply, ignore, State}. + {reply, ignored, State}. handle_cast(Msg, State) -> emqx_logger:error("[AccessControl] unexpected msg: ~p", [Msg]), From edf654727c47409508db4c9bb96fa06713603b25 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 6 Sep 2018 18:09:22 +0800 Subject: [PATCH 252/520] Rename emqx_mqtt_properties module to emqx_mqtt_props --- src/emqx_client.erl | 6 +++--- src/{emqx_mqtt_properties.erl => emqx_mqtt_props.erl} | 2 +- ...tt_properties_SUITE.erl => emqx_mqtt_props_SUITE.erl} | 9 +++++---- 3 files changed, 9 insertions(+), 8 deletions(-) rename src/{emqx_mqtt_properties.erl => emqx_mqtt_props.erl} (99%) rename test/{emqx_mqtt_properties_SUITE.erl => emqx_mqtt_props_SUITE.erl} (73%) diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 192569ca4..85d6ca59d 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -159,7 +159,7 @@ start_link() -> start_link([]). start_link(Options) when is_map(Options) -> start_link(maps:to_list(Options)); start_link(Options) when is_list(Options) -> - ok = emqx_mqtt_properties:validate( + ok = emqx_mqtt_props:validate( proplists:get_value(properties, Options, #{})), case start_client(with_owner(Options)) of {ok, Client} -> @@ -265,7 +265,7 @@ publish(Client, Topic, Payload, Opts) when is_binary(Topic), is_list(Opts) -> -> ok | {ok, packet_id()} | {error, term()}). publish(Client, Topic, Properties, Payload, Opts) when is_binary(Topic), is_map(Properties), is_list(Opts) -> - ok = emqx_mqtt_properties:validate(Properties), + ok = emqx_mqtt_props:validate(Properties), Retain = proplists:get_bool(retain, Opts), QoS = ?QOS_I(proplists:get_value(qos, Opts, ?QOS_0)), publish(Client, #mqtt_msg{qos = QoS, @@ -541,7 +541,7 @@ mqtt_connect(State = #state{client_id = ClientId, will_msg = WillMsg, properties = Properties}) -> ?WILL_MSG(WillQoS, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, - ConnProps = emqx_mqtt_properties:filter(?CONNECT, Properties), + ConnProps = emqx_mqtt_props:filter(?CONNECT, Properties), send(?CONNECT_PACKET( #mqtt_packet_connect{proto_ver = ProtoVer, proto_name = ProtoName, diff --git a/src/emqx_mqtt_properties.erl b/src/emqx_mqtt_props.erl similarity index 99% rename from src/emqx_mqtt_properties.erl rename to src/emqx_mqtt_props.erl index 643156013..33acb360b 100644 --- a/src/emqx_mqtt_properties.erl +++ b/src/emqx_mqtt_props.erl @@ -13,7 +13,7 @@ %% limitations under the License. %% @doc MQTT5 Properties --module(emqx_mqtt_properties). +-module(emqx_mqtt_props). -include("emqx_mqtt.hrl"). diff --git a/test/emqx_mqtt_properties_SUITE.erl b/test/emqx_mqtt_props_SUITE.erl similarity index 73% rename from test/emqx_mqtt_properties_SUITE.erl rename to test/emqx_mqtt_props_SUITE.erl index a8301d1f4..8d3b16a14 100644 --- a/test/emqx_mqtt_properties_SUITE.erl +++ b/test/emqx_mqtt_props_SUITE.erl @@ -12,7 +12,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. --module(emqx_mqtt_properties_SUITE). +-module(emqx_mqtt_props_SUITE). -compile(export_all). -compile(nowarn_export_all). @@ -22,6 +22,7 @@ all() -> [t_mqtt_properties_all]. t_mqtt_properties_all(_) -> - Props = emqx_mqtt_properties:filter(?CONNECT, #{'Session-Expiry-Interval' => 1, 'Maximum-Packet-Size' => 255}), - ok = emqx_mqtt_properties:validate(Props), - #{} = emqx_mqtt_properties:filter(?CONNECT, #{'Maximum-QoS' => ?QOS_2}). + 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}). + From 7c688a483949a217b0587cb0b3780f8393c5e6df Mon Sep 17 00:00:00 2001 From: HuangDan Date: Thu, 6 Sep 2018 18:09:58 +0800 Subject: [PATCH 253/520] Add test case for mqtt5 connect packet --- test/emqx_SUITE.erl | 63 +++++------------ test/emqx_mqtt_packet_SUITE.erl | 117 ++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 47 deletions(-) create mode 100644 test/emqx_mqtt_packet_SUITE.erl diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index a56af1a8a..a08305a30 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -17,8 +17,6 @@ -compile(export_all). -compile(nowarn_export_all). --include("emqx_mqtt.hrl"). - -define(APP, emqx). -include_lib("eunit/include/eunit.hrl"). @@ -52,9 +50,7 @@ -define(PUBPACKET, ?PUBLISH_PACKET(?PUBQOS, <<"sub/topic">>, ?PACKETID, <<"publish">>)). all() -> - [{group, connect}%, - % {group, cleanSession} - ]. + [{group, connect}]. groups() -> [{connect, [non_parallel_tests], @@ -64,11 +60,7 @@ groups() -> mqtt_connect_with_ssl_oneway, mqtt_connect_with_ssl_twoway, mqtt_connect_with_ws - ]}, - {cleanSession, [sequence], - [cleanSession_validate] - } - ]. + ]}]. init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), @@ -109,15 +101,17 @@ mqtt_connect_with_ssl_oneway(_) -> emqx_ct_broker_helpers:change_opts(ssl_oneway), emqx:start(), ClientSsl = emqx_ct_broker_helpers:client_ssl(), - {ok, #ssl_socket{tcp = Sock, ssl = SslSock}} + {ok, #ssl_socket{tcp = _Sock1, ssl = SslSock} = Sock} = emqx_client_sock:connect("127.0.0.1", 8883, [{ssl_opts, ClientSsl}], 3000), -%% Packet = raw_send_serialise(?CLIENT), -%% ssl:send(SslSock, Packet), -%% receive Data -> -%% ct:log("Data:~p~n", [Data]) -%% after 30000 -> -%% ok -%% end, + Packet = raw_send_serialise(?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) -> @@ -131,11 +125,12 @@ mqtt_connect_with_ssl_twoway(_Config) -> 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) + {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(Data), true after 1000 -> - ok - end, + false + end), emqx_client_sock:close(Sock). mqtt_connect_with_ws(_Config) -> @@ -162,32 +157,6 @@ mqtt_connect_with_ws(_Config) -> {close, _} = rfc6455_client:close(WS), ok. -cleanSession_validate(_) -> - {ok, C1} = emqttc:start_link([{host, "localhost"}, - {port, 1883}, - {client_id, <<"c1">>}, - {clean_sess, false}]), - timer:sleep(10), - emqttc:subscribe(C1, <<"topic">>, qos0), - emqttc:disconnect(C1), - {ok, Pub} = emqttc:start_link([{host, "localhost"}, - {port, 1883}, - {client_id, <<"pub">>}]), - - emqttc:publish(Pub, <<"topic">>, <<"m1">>, [{qos, 0}]), - timer:sleep(10), - {ok, C11} = emqttc:start_link([{host, "localhost"}, - {port, 1883}, - {client_id, <<"c1">>}, - {clean_sess, false}]), - timer:sleep(100), - receive {publish, _Topic, M1} -> - ?assertEqual(<<"m1">>, M1) - after 1000 -> false - end, - emqttc:disconnect(Pub), - emqttc:disconnect(C11). - raw_send_serialise(Packet) -> emqx_frame:serialize(Packet). diff --git a/test/emqx_mqtt_packet_SUITE.erl b/test/emqx_mqtt_packet_SUITE.erl new file mode 100644 index 000000000..8bc41cb37 --- /dev/null +++ b/test/emqx_mqtt_packet_SUITE.erl @@ -0,0 +1,117 @@ +%%%=================================================================== +%%% Copyright (c) 2013-2018 EMQ Inc. 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_broker_helpers:run_setup_steps(), + Config. + +end_per_suite(_Config) -> + emqx_ct_broker_helpers:run_teardown_steps(). + +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} }). From 5774ba542c67b7be0ebd086858ff3122ecf987a0 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 6 Sep 2018 18:10:01 +0800 Subject: [PATCH 254/520] Rename the emqx_mqtt_properties SUITE to emqx_mqtt_props --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7202915cc..e12a516b1 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ EUNIT_OPTS = verbose CT_SUITES = emqx emqx_banned emqx_connection emqx_session emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight \ emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ - emqx_mqtt_compat emqx_mqtt_properties emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ + emqx_mqtt_compat emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_zone \ emqx_mountpoint emqx_listeners emqx_protocol From f95c82e37a6061235988f3bd97f5782f54670140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 6 Sep 2018 18:14:14 +0800 Subject: [PATCH 255/520] Add metric for DISCONNECT packet --- src/emqx_metrics.erl | 57 +++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index e319a425b..8130a307e 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -34,32 +34,33 @@ %% Packets sent and received of broker -define(PACKET_METRICS, [ - {counter, 'packets/received'}, % All Packets received - {counter, 'packets/sent'}, % All Packets sent - {counter, 'packets/connect'}, % CONNECT Packets received - {counter, 'packets/connack'}, % CONNACK Packets sent - {counter, 'packets/publish/received'}, % PUBLISH packets received - {counter, 'packets/publish/sent'}, % PUBLISH packets sent - {counter, 'packets/puback/received'}, % PUBACK packets received - {counter, 'packets/puback/sent'}, % PUBACK packets sent - {counter, 'packets/puback/missed'}, % PUBACK packets missed - {counter, 'packets/pubrec/received'}, % PUBREC packets received - {counter, 'packets/pubrec/sent'}, % PUBREC packets sent - {counter, 'packets/pubrec/missed'}, % PUBREC packets missed - {counter, 'packets/pubrel/received'}, % PUBREL packets received - {counter, 'packets/pubrel/sent'}, % PUBREL packets sent - {counter, 'packets/pubrel/missed'}, % PUBREL packets missed - {counter, 'packets/pubcomp/received'}, % PUBCOMP packets received - {counter, 'packets/pubcomp/sent'}, % PUBCOMP packets sent - {counter, 'packets/pubcomp/missed'}, % PUBCOMP packets missed - {counter, 'packets/subscribe'}, % SUBSCRIBE Packets received - {counter, 'packets/suback'}, % SUBACK packets sent - {counter, 'packets/unsubscribe'}, % UNSUBSCRIBE Packets received - {counter, 'packets/unsuback'}, % UNSUBACK Packets sent - {counter, 'packets/pingreq'}, % PINGREQ packets received - {counter, 'packets/pingresp'}, % PINGRESP Packets sent - {counter, 'packets/disconnect'}, % DISCONNECT Packets received - {counter, 'packets/auth'} % Auth Packets received + {counter, 'packets/received'}, % All Packets received + {counter, 'packets/sent'}, % All Packets sent + {counter, 'packets/connect'}, % CONNECT Packets received + {counter, 'packets/connack'}, % CONNACK Packets sent + {counter, 'packets/publish/received'}, % PUBLISH packets received + {counter, 'packets/publish/sent'}, % PUBLISH packets sent + {counter, 'packets/puback/received'}, % PUBACK packets received + {counter, 'packets/puback/sent'}, % PUBACK packets sent + {counter, 'packets/puback/missed'}, % PUBACK packets missed + {counter, 'packets/pubrec/received'}, % PUBREC packets received + {counter, 'packets/pubrec/sent'}, % PUBREC packets sent + {counter, 'packets/pubrec/missed'}, % PUBREC packets missed + {counter, 'packets/pubrel/received'}, % PUBREL packets received + {counter, 'packets/pubrel/sent'}, % PUBREL packets sent + {counter, 'packets/pubrel/missed'}, % PUBREL packets missed + {counter, 'packets/pubcomp/received'}, % PUBCOMP packets received + {counter, 'packets/pubcomp/sent'}, % PUBCOMP packets sent + {counter, 'packets/pubcomp/missed'}, % PUBCOMP packets missed + {counter, 'packets/subscribe'}, % SUBSCRIBE Packets received + {counter, 'packets/suback'}, % SUBACK packets sent + {counter, 'packets/unsubscribe'}, % UNSUBSCRIBE Packets received + {counter, 'packets/unsuback'}, % UNSUBACK Packets sent + {counter, 'packets/pingreq'}, % PINGREQ packets received + {counter, 'packets/pingresp'}, % PINGRESP Packets sent + {counter, 'packets/disconnect/received'}, % DISCONNECT Packets received + {counter, 'packets/disconnect/sent'}, % DISCONNECT Packets sent + {counter, 'packets/auth'} % Auth Packets received ]). %% Messages sent and received of broker @@ -194,7 +195,7 @@ received2(?UNSUBSCRIBE) -> received2(?PINGREQ) -> inc('packets/pingreq'); received2(?DISCONNECT) -> - inc('packets/disconnect'); + inc('packets/disconnect/received'); received2(_) -> ignore. qos_received(?QOS_0) -> @@ -233,6 +234,8 @@ sent2(?UNSUBACK) -> inc('packets/unsuback'); sent2(?PINGRESP) -> inc('packets/pingresp'); +sent2(?DISCONNECT) -> + inc('packets/disconnect/sent'); sent2(_Type) -> ignore. qos_sent(?QOS_0) -> From 328d035dab7240d5a305a5024e4a4cfd2495c9e2 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 6 Sep 2018 18:43:47 +0800 Subject: [PATCH 256/520] Replace 'state' record with map --- src/emqx_pool.erl | 18 ++++++++---------- src/emqx_pool_sup.erl | 8 ++++++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/emqx_pool.erl b/src/emqx_pool.erl index 8d927ddd9..276352797 100644 --- a/src/emqx_pool.erl +++ b/src/emqx_pool.erl @@ -23,16 +23,14 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {pool, id}). - -define(POOL, ?MODULE). %% @doc Start pooler supervisor. start_link() -> emqx_pool_sup:start_link(?POOL, random, {?MODULE, start_link, []}). -%% @doc Start pool --spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). +%% @doc Start pool. +-spec(start_link(atom(), pos_integer()) -> emqx_types:startlink_ret()). start_link(Pool, Id) -> gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, [Pool, Id], []). @@ -49,13 +47,13 @@ async_submit(Fun) -> worker() -> gproc_pool:pick_worker(pool). -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), - {ok, #state{pool = Pool, id = Id}}. + {ok, #{pool => Pool, id => Id}}. handle_call({submit, Fun}, _From, State) -> {reply, catch run(Fun), State}; @@ -79,15 +77,15 @@ handle_info(Info, State) -> emqx_logger:error("[Pool] unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{pool = Pool, id = Id}) -> +terminate(_Reason, #{pool := Pool, id := Id}) -> true = gproc_pool:disconnect_worker(Pool, {Pool, 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 b71c15f1e..b371549c0 100644 --- a/src/emqx_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -26,8 +26,12 @@ spec(Args) -> -spec(spec(any(), list()) -> supervisor:child_spec()). spec(ChildId, Args) -> - {ChildId, {?MODULE, start_link, Args}, - transient, infinity, supervisor, [?MODULE]}. + #{id => ChildId, + start => {?MODULE, start_link, Args}, + restart => transient, + shutdown => infinity, + type => supervisor, + modules => [?MODULE]}. -spec(start_link(atom() | tuple(), atom(), mfa()) -> {ok, pid()} | {error, term()}). start_link(Pool, Type, MFA) -> From 765ab5ad7b78450adae749266f24d6a50b50ecaa Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 6 Sep 2018 19:09:29 +0800 Subject: [PATCH 257/520] Add condition to handle when mqx_topic_alias do not exist --- src/emqx_mqtt_caps.erl | 2 ++ src/emqx_protocol.erl | 60 +++++++++++++++++------------------ test/emqx_mqtt_caps_SUITE.erl | 32 ++++++++++++------- 3 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl index baec9920c..b8b7a5b3a 100644 --- a/src/emqx_mqtt_caps.erl +++ b/src/emqx_mqtt_caps.erl @@ -69,6 +69,8 @@ do_check_pub(Props = #{ topic_alias := TopicAlias}, [{max_topic_alias, MaxTopicA end; do_check_pub(#{retain := true}, [{mqtt_retain_available, false}|_Caps]) -> {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). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 025fe9c93..38f55d204 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -33,36 +33,36 @@ -export([shutdown/2]). -record(pstate, { - zone, - sendfun, - peername, - peercert, - proto_ver, - proto_name, - ackprops, - client_id, - is_assigned, - conn_pid, - conn_props, - ack_props, - username, - session, - clean_start, - topic_aliases, - packet_size, - will_topic, - will_msg, - keepalive, - mountpoint, - is_super, - is_bridge, - enable_ban, - enable_acl, - recv_stats, - send_stats, - connected, - connected_at - }). + zone, + sendfun, + peername, + peercert, + proto_ver, + proto_name, + ackprops, + client_id, + is_assigned, + conn_pid, + conn_props, + ack_props, + username, + session, + clean_start, + topic_aliases, + packet_size, + will_topic, + will_msg, + keepalive, + mountpoint, + is_super, + is_bridge, + enable_ban, + enable_acl, + recv_stats, + send_stats, + connected, + connected_at + }). -type(state() :: #pstate{}). -export_type([state/0]). diff --git a/test/emqx_mqtt_caps_SUITE.erl b/test/emqx_mqtt_caps_SUITE.erl index 8b840b91c..1d4c81b8d 100644 --- a/test/emqx_mqtt_caps_SUITE.erl +++ b/test/emqx_mqtt_caps_SUITE.erl @@ -38,7 +38,7 @@ t_get_set_caps(_) -> mqtt_wildcard_subscription => true }, Caps2 = Caps#{max_packet_size => 1048576}, - case emqx_mqtt_caps:get_caps(zone) of + case emqx_mqtt_caps:get_caps(zone) of Caps -> ok; Caps2 -> ok end, @@ -63,20 +63,28 @@ t_check_pub(_) -> {ok, _} = emqx_zone:start_link(), PubCaps = #{ max_qos_allowed => ?QOS_1, - mqtt_retain_available => false + 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 @@ -94,18 +102,18 @@ t_check_sub(_) -> mqtt_wildcard_subscription => true }, - ok = do_check_sub([{<<"client/stat">>, Opts}], [{<<"client/stat">>, Opts}]), + 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}], + 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}], + 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}], + ok = do_check_sub(Caps#{mqtt_wildcard_subscription => false}, + [{<<"vlient/+/dsofi">>, Opts}], [{<<"vlient/+/dsofi">>, Opts#{rc => ?RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED}}]). - + From d819ec0b58a4170cc49d04aa6d9411ffe03426bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 7 Sep 2018 10:18:21 +0800 Subject: [PATCH 258/520] Comment unused function in emqx_frame.erl --- src/emqx_frame.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index f3cd33ddb..aa7aad064 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -236,8 +236,8 @@ parse_will_message(Packet = #mqtt_packet_connect{will_flag = true, parse_will_message(Packet, Bin) -> {Packet, Bin}. -protocol_approved(Ver, Name) -> - lists:member({Ver, Name}, ?PROTOCOL_NAMES). +% protocol_approved(Ver, Name) -> +% lists:member({Ver, Name}, ?PROTOCOL_NAMES). parse_packet_id(<>) -> {PacketId, Rest}. From d9ad29476a60873c7cb74e153b12f55fd5c99aaa Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 7 Sep 2018 10:23:43 +0800 Subject: [PATCH 259/520] Code Review: Update the zone module 1. Add force_reload/1 API 2. Change the default reload interval to 5 minutes --- src/emqx_zone.erl | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl index 209f0323c..8344ba150 100644 --- a/src/emqx_zone.erl +++ b/src/emqx_zone.erl @@ -26,10 +26,9 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {timer}). - -define(TAB, ?MODULE). +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). @@ -58,7 +57,7 @@ set_env(Zone, Key, Val) -> init([]) -> _ = emqx_tables:new(?TAB, [set, {read_concurrency, true}]), - {ok, element(2, handle_info(reload, #state{}))}. + {ok, element(2, handle_info(reload, #{timer => undefined}))}. handle_call(Req, _From, State) -> emqx_logger:error("[Zone] unexpected call: ~p", [Req]), @@ -72,11 +71,9 @@ handle_cast(Msg, State) -> emqx_logger:error("[Zone] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info(reload, State) -> - lists:foreach( - fun({Zone, Opts}) -> - [ets:insert(?TAB, {{Zone, Key}, Val}) || {Key, Val} <- Opts] - end, emqx_config:get_env(zones, [])), +handle_info({timeout, TRef, reload}, State = #{timer := TRef}) -> + [ets:insert(?TAB, [{{Zone, Key}, Val} || {Key, Val} <- Opts]) + || {Zone, Opts} <- emqx_config:get_env(zones, [])], {noreply, ensure_reload_timer(State), hibernate}; handle_info(Info, State) -> @@ -94,5 +91,5 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ ensure_reload_timer(State) -> - State#state{timer = erlang:send_after(10000, self(), reload)}. + State#{timer := emqx_misc:start_timer(timer:minutes(5), reload)}. From 304a24ca6a0a8f0d2c6b6e035d69e1d5e2c6e7f3 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 7 Sep 2018 10:26:14 +0800 Subject: [PATCH 260/520] Code Review: Update the zone module 1. Add force_reload/0 management API 2. Change the reload interval to 5 minutes --- src/emqx_zone.erl | 37 +++++++++++++++++++++++++++++-------- test/emqx_zone_SUITE.erl | 14 ++++++++++---- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl index 8344ba150..dd183dbdf 100644 --- a/src/emqx_zone.erl +++ b/src/emqx_zone.erl @@ -21,16 +21,20 @@ -export([start_link/0]). -export([get_env/2, get_env/3]). -export([set_env/3]). +-export([force_reload/0]). +%% for test +-export([stop/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(TAB, ?MODULE). +-define(SERVER, ?MODULE). -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -spec(get_env(emqx_types:zone() | undefined, atom()) -> undefined | term()). get_env(undefined, Key) -> @@ -49,7 +53,15 @@ get_env(Zone, Key, Def) -> -spec(set_env(emqx_types:zone(), atom(), term()) -> ok). set_env(Zone, Key, Val) -> - gen_server:cast(?MODULE, {set_env, Zone, Key, Val}). + gen_server:cast(?SERVER, {set_env, Zone, Key, Val}). + +-spec(force_reload() -> ok). +force_reload() -> + gen_server:call(?SERVER, force_reload). + +-spec(stop() -> ok). +stop() -> + gen_server:stop(?SERVER, normal, infinity). %%------------------------------------------------------------------------------ %% gen_server callbacks @@ -59,6 +71,10 @@ init([]) -> _ = emqx_tables:new(?TAB, [set, {read_concurrency, true}]), {ok, element(2, handle_info(reload, #{timer => undefined}))}. +handle_call(force_reload, _From, State) -> + _ = do_reload(), + {reply, ok, State}; + handle_call(Req, _From, State) -> emqx_logger:error("[Zone] unexpected call: ~p", [Req]), {reply, ignored, State}. @@ -71,10 +87,9 @@ handle_cast(Msg, State) -> emqx_logger:error("[Zone] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({timeout, TRef, reload}, State = #{timer := TRef}) -> - [ets:insert(?TAB, [{{Zone, Key}, Val} || {Key, Val} <- Opts]) - || {Zone, Opts} <- emqx_config:get_env(zones, [])], - {noreply, ensure_reload_timer(State), hibernate}; +handle_info(reload, State) -> + _ = do_reload(), + {noreply, ensure_reload_timer(State#{timer := undefined}), hibernate}; handle_info(Info, State) -> emqx_logger:error("[Zone] unexpected info: ~p", [Info]), @@ -90,6 +105,12 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -ensure_reload_timer(State) -> - State#{timer := emqx_misc:start_timer(timer:minutes(5), reload)}. +do_reload() -> + [ets:insert(?TAB, [{{Zone, Key}, Val} || {Key, Val} <- Opts]) + || {Zone, Opts} <- emqx_config:get_env(zones, [])]. + +ensure_reload_timer(State = #{timer := undefined}) -> + State#{timer := erlang:send_after(timer:minutes(5), self(), reload)}; +ensure_reload_timer(State) -> + State. diff --git a/test/emqx_zone_SUITE.erl b/test/emqx_zone_SUITE.erl index 282acc3e5..83c2ceaab 100644 --- a/test/emqx_zone_SUITE.erl +++ b/test/emqx_zone_SUITE.erl @@ -18,15 +18,21 @@ -compile(nowarn_export_all). -include("emqx_mqtt.hrl"). +-include_lib("eunit/include/eunit.hrl"). all() -> [t_set_get_env]. t_set_get_env(_) -> - emqx_zone:start_link(), - ok = emqx_zone:set_env(china, language, chinese), - timer:sleep(100), % make sure set_env/3 is okay + application:set_env(emqx, zones, [{china, [{language, chinese}]}]), + {ok, _} = emqx_zone:start_link(), + ct:print("~p~n", [ets:tab2list(emqx_zone)]), 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). + 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)), + emqx_zone:stop(). From 3a94d7ddaec62cec1c019342f86eb3c0f1c0deed Mon Sep 17 00:00:00 2001 From: spring2maz Date: Thu, 6 Sep 2018 22:41:17 +0200 Subject: [PATCH 261/520] Generate a config file for testing Prior to this change, the template file etc/emqx.conf is used directly in testing, as a result, mustache style directories are created e.g. `{{ platform_log_dir }}` which should have been replaced with a config varialbe e.g. `log` In this change, Makefile targets are added as `ct` dependency to download bbmustach, load the template input, replace with variableds defined in 'vars' file, finally to etc/gen.emqx.conf. The direct usage of etc/emqx.conf in test code are replaced with gen.emqx.conf --- .gitignore | 2 ++ Makefile | 27 +++++++++++++++++++++++++-- test/emqx_ct_broker_helpers.erl | 2 +- test/emqx_listeners_SUITE.erl | 2 +- vars | 9 +++++++++ 5 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 vars diff --git a/.gitignore b/.gitignore index d1b8a289e..0322aaddc 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ rebar3.crashdump .DS_Store rebar.config emqx.iml +bbmustache/ +etc/gen.emqx.conf diff --git a/Makefile b/Makefile index e12a516b1..452d7c57d 100644 --- a/Makefile +++ b/Makefile @@ -51,6 +51,29 @@ DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wun include erlang.mk -app.config:: - ./deps/cuttlefish/cuttlefish -l info -e etc/ -c etc/emqx.conf -i priv/emqx.schema -d data/ +clean:: gen-clean + +.PHONY: gen-clean +gen-clean: + @rm -rf bbmustache + @rm -f etc/gen.emqx.conf + +bbmustache: + $(verbose) git clone https://github.com/soranoba/bbmustache.git && pushd bbmustache && ./rebar3 compile && popd + +# This hack is to generate a conf file for testing +# relx overlay is used for release +etc/gen.emqx.conf: bbmustache etc/emqx.conf + $(verbose) erl -noshell -pa bbmustache/_build/default/lib/bbmustache/ebin -eval \ + "{ok, Temp} = file:read_file('etc/emqx.conf'), \ + {ok, Vars0} = file:consult('vars'), \ + Vars = [{atom_to_list(N), list_to_binary(V)} || {N, V} <- Vars0], \ + Targ = bbmustache:render(Temp, Vars), \ + ok = file:write_file('etc/gen.emqx.conf', Targ), \ + halt(0)." + +app.config: etc/gen.emqx.conf + $(verbose) ./deps/cuttlefish/cuttlefish -l info -e etc/ -c etc/gen.emqx.conf -i priv/emqx.schema -d data/ + +ct: app.config diff --git a/test/emqx_ct_broker_helpers.erl b/test/emqx_ct_broker_helpers.erl index ba1883ecc..62a91df54 100644 --- a/test/emqx_ct_broker_helpers.erl +++ b/test/emqx_ct_broker_helpers.erl @@ -63,7 +63,7 @@ run_teardown_steps() -> generate_config() -> Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), - Conf = conf_parse:file([local_path(["etc", "emqx.conf"])]), + Conf = conf_parse:file([local_path(["etc", "gen.emqx.conf"])]), cuttlefish_generator:map(Schema, Conf). get_base_dir(Module) -> diff --git a/test/emqx_listeners_SUITE.erl b/test/emqx_listeners_SUITE.erl index 6086e98c2..9d85583ab 100644 --- a/test/emqx_listeners_SUITE.erl +++ b/test/emqx_listeners_SUITE.erl @@ -49,7 +49,7 @@ restart_listeners(_) -> generate_config() -> Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), - Conf = conf_parse:file([local_path(["etc", "emqx.conf"])]), + Conf = conf_parse:file([local_path(["etc", "gen.emqx.conf"])]), cuttlefish_generator:map(Schema, Conf). set_app_env({App, Lists}) -> diff --git a/vars b/vars new file mode 100644 index 000000000..fedd69a45 --- /dev/null +++ b/vars @@ -0,0 +1,9 @@ +%% vars here are for test only, not intended for release + +{platform_bin_dir, "bin"}. +{platform_data_dir, "data"}. +{platform_etc_dir, "etc"}. +{platform_lib_dir, "lib"}. +{platform_log_dir, "log"}. +{platform_plugins_dir, "plugins"}. + From f8471afb97a1aa6e97e3d1406880ae3d9268ef0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 7 Sep 2018 13:50:12 +0800 Subject: [PATCH 262/520] Add handling of retain handling subscription option --- src/emqx_protocol.erl | 26 ++++++++++---------------- src/emqx_session.erl | 4 ++-- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 4f4f4bfb8..364cc0ec1 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -359,9 +359,15 @@ process_packet(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #pstate{session = {ok = emqx_session:pubcomp(SPid, PacketId, ReasonCode), PState}; process_packet(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - PState = #pstate{session = SPid, mountpoint = Mountpoint}) -> + PState = #pstate{session = SPid, mountpoint = Mountpoint, proto_ver = ProtoVer, is_bridge = IsBridge}) -> + RawTopicFilters1 = if ProtoVer < ?MQTT_PROTO_V5 -> + case IsBridge of + true -> [{RawTopic, SubOpts#{rap => 1}} || {RawTopic, SubOpts} <- RawTopicFilters]; + false -> [{RawTopic, SubOpts#{rap => 0}} || {RawTopic, SubOpts} <- RawTopicFilters] + end + end, case check_subscribe( - parse_topic_filters(?SUBSCRIBE, RawTopicFilters), PState) of + parse_topic_filters(?SUBSCRIBE, RawTopicFilters1), PState) of {ok, TopicFilters} -> case emqx_hooks:run('client.subscribe', [credentials(PState)], TopicFilters) of {ok, TopicFilters1} -> @@ -490,10 +496,10 @@ deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, deliver({connack, ReasonCode, SP}, PState) -> send(?CONNACK_PACKET(ReasonCode, SP), PState); -deliver({publish, PacketId, Msg}, PState = #pstate{is_bridge = IsBridge, mountpoint = MountPoint}) -> +deliver({publish, PacketId, Msg}, PState = #pstate{mountpoint = MountPoint}) -> _ = emqx_hooks:run('message.delivered', [credentials(PState)], Msg), Msg1 = emqx_message:update_expiry(Msg), - Msg2 = emqx_mountpoint:unmount(MountPoint, clean_retain(IsBridge, Msg1)), + Msg2 = emqx_mountpoint:unmount(MountPoint, Msg1), send(emqx_packet:from_message(PacketId, Msg2), PState); deliver({puback, PacketId, ReasonCode}, PState) -> @@ -741,18 +747,6 @@ parse_topic_filters(?SUBSCRIBE, RawTopicFilters) -> parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters) -> lists:map(fun emqx_topic:parse/1, RawTopicFilters). -%%----------------------------------------------------------------------------- -%% The retained flag should be propagated for bridge. -%%----------------------------------------------------------------------------- - -clean_retain(false, Msg = #message{flags = #{retain := true}, headers = Headers}) -> - case maps:get(retained, Headers, false) of - true -> Msg; - false -> emqx_message:set_flag(retain, false, Msg) - end; -clean_retain(_IsBridge, Msg) -> - Msg. - %%------------------------------------------------------------------------------ %% Update mountpoint diff --git a/src/emqx_session.erl b/src/emqx_session.erl index e8f8ed249..05142297e 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -448,11 +448,11 @@ handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, {ok, _SubOpts} -> emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), %% Why??? - emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts]), + emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts#{first => false}]), maps:put(Topic, SubOpts, SubMap); error -> emqx_broker:subscribe(Topic, ClientId, SubOpts), - emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts]), + emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts#{first => true}]), maps:put(Topic, SubOpts, SubMap) end} end, {[], Subscriptions}, TopicFilters), From dd8513ad35bd24a02f37eac2c2103322b52eb25b Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 7 Sep 2018 14:10:16 +0800 Subject: [PATCH 263/520] Update for banned API Use `mnesia:foldl` to traverse mnesia rather than `mnesia:first` and `mnesia:next`, as a badarg exception would occur if the record was deleted while travering the whole table. --- include/emqx.hrl | 15 +++++++++------ src/emqx_banned.erl | 21 ++++++--------------- test/emqx_banned_SUITE.erl | 10 +++++----- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/include/emqx.hrl b/include/emqx.hrl index 34e41b0a1..bca6fe519 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -148,13 +148,16 @@ %%-------------------------------------------------------------------- %% Banned %%-------------------------------------------------------------------- +-type(banned_who() :: {client_id, binary()} + | {username, binary()} + | {ip_address, inet:ip_address()}). -record(banned, { - key, - reason, - by, - desc, - until}). + who :: banned_who(), + reason :: binary(), + by :: binary(), + desc :: binary(), + until :: integer() + }). -endif. - diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index 444f07dad..8f1c3156f 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -85,7 +85,7 @@ handle_cast(Msg, State) -> {noreply, State}. handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) -> - mnesia:async_dirty(fun expire_banned_items/1, [erlang:timestamp()]), + mnesia:async_dirty(fun expire_banned_items/1, [erlang:system_time(second)]), {noreply, ensure_expiry_timer(State), hibernate}; handle_info(Info, State) -> @@ -106,17 +106,8 @@ ensure_expiry_timer(State) -> State#{expiry_timer := emqx_misc:start_timer(timer:minutes(5), expire)}. expire_banned_items(Now) -> - expire_banned_item(mnesia:first(?TAB), Now). - -expire_banned_item('$end_of_table', _Now) -> - ok; -expire_banned_item(Key, Now) -> - case mnesia:read(?TAB, Key) of - [#banned{until = undefined}] -> - ok; - [B = #banned{until = Until}] when Until < Now -> - mnesia:delete_object(?TAB, B, sticky_write); - _ -> ok - end, - expire_banned_item(mnesia:next(?TAB, Key), Now). - + mnesia:foldl(fun + (B = #banned{until = Until}, _Acc) when Until < Now -> + mnesia:delete_object(?TAB, B, sticky_write); + (_, _Acc) -> ok + end, ok, ?TAB). diff --git a/test/emqx_banned_SUITE.erl b/test/emqx_banned_SUITE.erl index 9fae880d4..c8eab87b6 100644 --- a/test/emqx_banned_SUITE.erl +++ b/test/emqx_banned_SUITE.erl @@ -28,14 +28,14 @@ all() -> [t_banned_all]. t_banned_all(_) -> emqx_ct_broker_helpers:run_setup_steps(), emqx_banned:start_link(), - {MegaSecs, Secs, MicroSecs} = erlang:timestamp(), - ok = emqx_banned:add(#banned{key = {client_id, <<"TestClient">>}, + TimeNow = erlang:system_time(second), + ok = emqx_banned:add(#banned{who = {client_id, <<"TestClient">>}, reason = <<"test">>, by = <<"banned suite">>, - desc = <<"test">>, - until = {MegaSecs, Secs + 10, MicroSecs}}), + desc = <<"test">>, + until = TimeNow + 10}), % here is not expire banned test because its check interval is greater than 5 mins, but its effect has been confirmed timer:sleep(100), ?assert(emqx_banned:check(#{client_id => <<"TestClient">>, username => undefined, peername => {undefined, undefined}})), emqx_banned:del({client_id, <<"TestClient">>}), - ?assertNot(emqx_banned:check(#{client_id => <<"TestClient">>, username => undefined, peername => {undefined, undefined}})). \ No newline at end of file + ?assertNot(emqx_banned:check(#{client_id => <<"TestClient">>, username => undefined, peername => {undefined, undefined}})). From 1326e8959321a49c231ddfdef5c26b3ca69c7dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 7 Sep 2018 15:16:32 +0800 Subject: [PATCH 264/520] Fix a bug in emqx_protocol.erl --- src/emqx_protocol.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 364cc0ec1..39929513e 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -364,7 +364,9 @@ process_packet(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), case IsBridge of true -> [{RawTopic, SubOpts#{rap => 1}} || {RawTopic, SubOpts} <- RawTopicFilters]; false -> [{RawTopic, SubOpts#{rap => 0}} || {RawTopic, SubOpts} <- RawTopicFilters] - end + end; + true -> + RawTopicFilters end, case check_subscribe( parse_topic_filters(?SUBSCRIBE, RawTopicFilters1), PState) of From 1c5615c957573f84e1813c5dc1ae0862f1cc1ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 7 Sep 2018 17:22:24 +0800 Subject: [PATCH 265/520] Stop emqx_zone when emqx_mqtt_caps test over --- test/emqx_mqtt_caps_SUITE.erl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/emqx_mqtt_caps_SUITE.erl b/test/emqx_mqtt_caps_SUITE.erl index 8b840b91c..85f6fae1d 100644 --- a/test/emqx_mqtt_caps_SUITE.erl +++ b/test/emqx_mqtt_caps_SUITE.erl @@ -57,7 +57,8 @@ t_get_set_caps(_) -> mqtt_shared_subscription => true, mqtt_wildcard_subscription => true }, - SubCaps = emqx_mqtt_caps:get_caps(zone, subscribe). + SubCaps = emqx_mqtt_caps:get_caps(zone, subscribe), + emqx_zone:stop(). t_check_pub(_) -> {ok, _} = emqx_zone:start_link(), @@ -81,7 +82,8 @@ t_check_pub(_) -> qos => ?QOS_1, retain => false }, - ok = emqx_mqtt_caps:check_pub(zone, PubProps). + ok = emqx_mqtt_caps:check_pub(zone, PubProps), + emqx_zone:stop(). t_check_sub(_) -> {ok, _} = emqx_zone:start_link(), @@ -104,7 +106,8 @@ t_check_sub(_) -> [{<<"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}}]). + [{<<"vlient/+/dsofi">>, Opts#{rc => ?RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED}}]), + emqx_zone:stop(). From 6f6e24592bebc71a35370caf5b79fc3161cbcd74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 7 Sep 2018 18:32:03 +0800 Subject: [PATCH 266/520] Fix the reverse match --- src/emqx_topic.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index 3dcad0b33..b3b417717 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -190,8 +190,8 @@ parse(Topic = <<"$share/", Topic1/binary>>, Options) -> [_] -> error({invalid_topic, Topic}); [Group, Topic2] -> case binary:match(Group, [<<"/">>, <<"+">>, <<"#">>]) of - nomatch -> error({invalid_topic, Topic}); - _ -> {Topic2, maps:put(share, Group, Options)} + nomatch -> {Topic2, maps:put(share, Group, Options)}; + _ -> error({invalid_topic, Topic}) end end; parse(Topic, Options) -> From 2121da3755b2d8669eff34fb07af7b87c2edd073 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Sat, 8 Sep 2018 10:10:22 +0800 Subject: [PATCH 267/520] Ignore Will-Delay-Interval = 0 --- src/emqx_protocol.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 7e95a4d90..4602337eb 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -370,7 +370,7 @@ process_packet(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), end; true -> RawTopicFilters - end, + end, case check_subscribe( parse_topic_filters(?SUBSCRIBE, RawTopicFilters1), PState) of {ok, TopicFilters} -> @@ -732,7 +732,8 @@ shutdown(Reason, PState = #pstate{connected = true, send_willmsg(undefined) -> ignore; send_willmsg(WillMsg = #message{topic = Topic, - headers = #{'Will-Delay-Interval' := Interval}}) when is_integer(Interval) -> + headers = #{'Will-Delay-Interval' := Interval}}) + when is_integer(Interval), Interval > 0 -> SendAfter = integer_to_binary(Interval), emqx_broker:publish(WillMsg#message{topic = <<"$delayed/", SendAfter/binary, "/", Topic/binary>>}); send_willmsg(WillMsg) -> From 08bab7efa482f725457f1f0647155795f5cc03f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 8 Sep 2018 11:49:20 +0800 Subject: [PATCH 268/520] Fix bugs in test cases --- Makefile | 6 +++--- test/emqx_banned_SUITE.erl | 3 ++- test/emqx_broker_SUITE.erl | 1 - test/emqx_frame_SUITE.erl | 2 +- test/emqx_mock_client.erl | 2 +- test/emqx_sm_SUITE.erl | 3 ++- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 452d7c57d..f24497852 100644 --- a/Makefile +++ b/Makefile @@ -32,13 +32,13 @@ TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose -# CT_SUITES = emqx_mqueue +# CT_SUITES = emqx_frame ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat -CT_SUITES = emqx emqx_banned emqx_connection emqx_session emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight \ +CT_SUITES = emqx emqx_zone emqx_banned emqx_connection emqx_session emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight \ emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ emqx_mqtt_compat emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ - emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_zone \ + emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm \ emqx_mountpoint emqx_listeners emqx_protocol CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqxct@127.0.0.1 diff --git a/test/emqx_banned_SUITE.erl b/test/emqx_banned_SUITE.erl index c8eab87b6..c91aeae45 100644 --- a/test/emqx_banned_SUITE.erl +++ b/test/emqx_banned_SUITE.erl @@ -38,4 +38,5 @@ t_banned_all(_) -> timer:sleep(100), ?assert(emqx_banned:check(#{client_id => <<"TestClient">>, username => undefined, peername => {undefined, undefined}})), emqx_banned:del({client_id, <<"TestClient">>}), - ?assertNot(emqx_banned:check(#{client_id => <<"TestClient">>, username => undefined, peername => {undefined, undefined}})). + ?assertNot(emqx_banned:check(#{client_id => <<"TestClient">>, username => undefined, peername => {undefined, undefined}})), + emqx_ct_broker_helpers:run_teardown_steps(). diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index 0bd3dc599..8cadbf00d 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -28,7 +28,6 @@ all() -> [{group, pubsub}, {group, session}, - {group, broker}, {group, metrics}, {group, stats}, {group, alarms}]. diff --git a/test/emqx_frame_SUITE.erl b/test/emqx_frame_SUITE.erl index 19bbb1e8c..1127f60d9 100644 --- a/test/emqx_frame_SUITE.erl +++ b/test/emqx_frame_SUITE.erl @@ -393,7 +393,7 @@ parse_disconnect(_) -> ?assertEqual({ok, ?DISCONNECT_PACKET(?RC_SUCCESS), <<>>}, parse(<<224, 0>>)). serialize_parse_disconnect(_) -> - Packet = ?PACKET(?DISCONNECT), + Packet = ?DISCONNECT_PACKET(?RC_SUCCESS), ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). serialize_parse_disconnect_v5(_) -> diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index 0536b8bac..85114e3a9 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -44,7 +44,7 @@ get_last_message() -> init([ClientId]) -> Result = lists:member(?TAB, ets:all()), if Result == false -> - ets:new(?TAB, [set, named_table]); + ets:new(?TAB, [set, named_table, public]); true -> ok end, {ok, diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl index 24999f2a0..5e23100a9 100644 --- a/test/emqx_sm_SUITE.erl +++ b/test/emqx_sm_SUITE.erl @@ -38,5 +38,6 @@ t_open_close_session(_) -> emqx_sm:set_session_stats(Session, {open, true}), {open, true} = emqx_sm:get_session_stats(Session), ok = emqx_sm:close_session(SPid), - [] = emqx_sm:lookup_session(<<"client">>). + [] = emqx_sm:lookup_session(<<"client">>), + emqx_ct_broker_helpers:run_teardown_steps(). From 2d10d6971df821a5188d5604cf9fa47707bf5abe Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sat, 8 Sep 2018 12:01:22 +0800 Subject: [PATCH 269/520] add pattern match for topic alias Prior to this change, it assume that topic_alias exists defaultly which may cause the unexpected bug This change fix this bug above --- src/emqx_protocol.erl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 4602337eb..1dae228d7 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -650,9 +650,15 @@ check_publish(Packet, PState) -> check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, variable = #mqtt_packet_publish{ properties = Properties}}, #pstate{zone = Zone}) -> - #{'Topic-Alias' := TopicAlias} = Properties, + emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}); +check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, + variable = #mqtt_packet_publish{ + properties = #{'Topic-Alias' := TopicAlias} + }}, + #pstate{zone = Zone}) -> emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain, topic_alias => TopicAlias}). + check_pub_acl(_Packet, #pstate{is_super = IsSuper, enable_acl = EnableAcl}) when IsSuper orelse (not EnableAcl) -> ok; From 78020de302a7a9e5a43f8cc45043ee88c03a2c78 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sat, 8 Sep 2018 12:50:14 +0800 Subject: [PATCH 270/520] fix pattern match bu Prior to this change, when packet have topic alias, the check_pub_caps function could not be matched correctly This change fix this bug. --- src/emqx_protocol.erl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 1dae228d7..49408d38e 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -647,16 +647,16 @@ check_publish(Packet, PState) -> run_check_steps([fun check_pub_caps/2, fun check_pub_acl/2], Packet, PState). -check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, - variable = #mqtt_packet_publish{ properties = Properties}}, - #pstate{zone = Zone}) -> - emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}); check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, variable = #mqtt_packet_publish{ properties = #{'Topic-Alias' := TopicAlias} }}, #pstate{zone = Zone}) -> - emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain, topic_alias => TopicAlias}). + emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain, topic_alias => TopicAlias}); +check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, + variable = #mqtt_packet_publish{ properties = Properties}}, + #pstate{zone = Zone}) -> + emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}). check_pub_acl(_Packet, #pstate{is_super = IsSuper, enable_acl = EnableAcl}) From fdd9377a65e0eef8cbb8b5f7c4ee90d3127561e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 8 Sep 2018 13:35:23 +0800 Subject: [PATCH 271/520] Retain flag in retained message must set to 1 --- src/emqx_session.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 05142297e..8e6e150c5 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -726,9 +726,10 @@ run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = run_dispatch_steps(Steps, Msg#message{qos = min(SubQoS, PubQoS)}, State); run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = #state{upgrade_qos = true}) -> run_dispatch_steps(Steps, Msg#message{qos = max(SubQoS, PubQoS)}, State); +run_dispatch_steps([{rap, _Rap}|Steps], Msg = #message{flags = Flags, headers = #{retained := true}}, State = #state{}) -> + run_dispatch_steps(Steps, Msg#message{flags = maps:put(retain, true, Flags)}, State); run_dispatch_steps([{rap, 0}|Steps], Msg = #message{flags = Flags}, State = #state{}) -> - Flags1 = maps:put(retain, false, Flags), - run_dispatch_steps(Steps, Msg#message{flags = Flags1}, State); + run_dispatch_steps(Steps, Msg#message{flags = maps:put(retain, false, Flags)}, State); run_dispatch_steps([{rap, _}|Steps], Msg, State) -> run_dispatch_steps(Steps, Msg, State); run_dispatch_steps([{subid, SubId}|Steps], Msg, State) -> From caedcee2dec3b63009f78d8660750eb81ce8b9e3 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sat, 8 Sep 2018 14:32:02 +0800 Subject: [PATCH 272/520] fix listerners and access suites Prior to this change, listeners test suites did not start cowboy which is responsible for websocket connections. and access suites start duplicated processes and these actions are wrong This change fix the problem states above. --- test/emqx_access_SUITE.erl | 4 +--- test/emqx_listeners_SUITE.erl | 8 +++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index e08cec08e..b49c90d80 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -98,7 +98,7 @@ end_per_group(_Group, Config) -> Config. init_per_testcase(_TestCase, Config) -> - %% {ok, _Pid} = + %% {ok, _Pid} = ?AC:start_link(), Config. end_per_testcase(_TestCase, _Config) -> @@ -119,7 +119,6 @@ reload_acl(_) -> register_mod(_) -> ok = ?AC:register_mod(acl, emqx_acl_test_mod, []), - {error, already_existed} = ?AC:register_mod(acl, emqx_acl_test_mod, []), {emqx_acl_test_mod, _, 0} = hd(?AC:lookup_mods(acl)), ok = ?AC:register_mod(auth, emqx_auth_anonymous_test_mod,[]), ok = ?AC:register_mod(auth, emqx_auth_dashboard, [], 99), @@ -378,4 +377,3 @@ match_rule(_) -> {matched, allow} = match(User, <<"Topic">>, AndRule), OrRule = compile({allow, {'or', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, ["Topic"]}), {matched, allow} = match(User, <<"Topic">>, OrRule). - diff --git a/test/emqx_listeners_SUITE.erl b/test/emqx_listeners_SUITE.erl index 9d85583ab..17181aa31 100644 --- a/test/emqx_listeners_SUITE.erl +++ b/test/emqx_listeners_SUITE.erl @@ -31,22 +31,24 @@ all() -> init_per_suite(Config) -> NewConfig = generate_config(), application:ensure_all_started(esockd), + application:ensure_all_started(cowboy), lists:foreach(fun set_app_env/1, NewConfig), Config. end_per_suite(_Config) -> - application:stop(esockd). + application:stop(esockd), + application:stop(cowboy). start_stop_listeners(_) -> ok = emqx_listeners:start(), ok = emqx_listeners:stop(). - + restart_listeners(_) -> ok = emqx_listeners:start(), ok = emqx_listeners:stop(), ok = emqx_listeners:restart(), ok = emqx_listeners:stop(). - + generate_config() -> Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), Conf = conf_parse:file([local_path(["etc", "gen.emqx.conf"])]), From 758d18e21f3321950b9d276ce96d05fb4b6f7d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 8 Sep 2018 18:05:04 +0800 Subject: [PATCH 273/520] no message --- src/emqx_reason_codes.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index fdc1377ec..75118b563 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -139,4 +139,4 @@ compat(connack, 16#9D) -> ?CONNACK_SERVER; compat(connack, 16#9F) -> ?CONNACK_SERVER; compat(suback, Code) when Code =< ?QOS2 -> Code; -compat(suback, Code) when Code > 16#80 -> 16#80. +compat(suback, Code) when Code >= 16#80 -> 16#80. From 40977e529a9a8a29fdf4d260408ea0e861186d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 8 Sep 2018 13:35:23 +0800 Subject: [PATCH 274/520] Retain flag in retained message must set to 1 --- src/emqx_session.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 05142297e..8e6e150c5 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -726,9 +726,10 @@ run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = run_dispatch_steps(Steps, Msg#message{qos = min(SubQoS, PubQoS)}, State); run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = #state{upgrade_qos = true}) -> run_dispatch_steps(Steps, Msg#message{qos = max(SubQoS, PubQoS)}, State); +run_dispatch_steps([{rap, _Rap}|Steps], Msg = #message{flags = Flags, headers = #{retained := true}}, State = #state{}) -> + run_dispatch_steps(Steps, Msg#message{flags = maps:put(retain, true, Flags)}, State); run_dispatch_steps([{rap, 0}|Steps], Msg = #message{flags = Flags}, State = #state{}) -> - Flags1 = maps:put(retain, false, Flags), - run_dispatch_steps(Steps, Msg#message{flags = Flags1}, State); + run_dispatch_steps(Steps, Msg#message{flags = maps:put(retain, false, Flags)}, State); run_dispatch_steps([{rap, _}|Steps], Msg, State) -> run_dispatch_steps(Steps, Msg, State); run_dispatch_steps([{subid, SubId}|Steps], Msg, State) -> From 23c91c0a40430e6169761fd6f77d761bd32927c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 8 Sep 2018 18:05:04 +0800 Subject: [PATCH 275/520] no message --- src/emqx_reason_codes.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index fdc1377ec..75118b563 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -139,4 +139,4 @@ compat(connack, 16#9D) -> ?CONNACK_SERVER; compat(connack, 16#9F) -> ?CONNACK_SERVER; compat(suback, Code) when Code =< ?QOS2 -> Code; -compat(suback, Code) when Code > 16#80 -> 16#80. +compat(suback, Code) when Code >= 16#80 -> 16#80. From 5b47df163188e0a03bdc2f9a1ef8aa424a664217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 8 Sep 2018 18:59:57 +0800 Subject: [PATCH 276/520] Add run hook when duplicated subscription --- src/emqx_session.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 8e6e150c5..118ab6e21 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -444,6 +444,7 @@ handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, lists:foldr(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) -> {[QoS|RcAcc], case maps:find(Topic, SubMap) of {ok, SubOpts} -> + emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts#{first => false}]), SubMap; {ok, _SubOpts} -> emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), From 3caa41f751d3a72e3b126939fa4d1b05b4f58fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 8 Sep 2018 18:59:57 +0800 Subject: [PATCH 277/520] Add run hook when duplicated subscription --- src/emqx_session.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 8e6e150c5..118ab6e21 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -444,6 +444,7 @@ handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, lists:foldr(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) -> {[QoS|RcAcc], case maps:find(Topic, SubMap) of {ok, SubOpts} -> + emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts#{first => false}]), SubMap; {ok, _SubOpts} -> emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), From 0c39a7620e2d19ac628880c12ea9b5e2ae943120 Mon Sep 17 00:00:00 2001 From: spring2maz <40776645+spring2maz@users.noreply.github.com> Date: Sun, 9 Sep 2018 15:51:42 +0200 Subject: [PATCH 278/520] Rebar3 and erlang.mk dual support. (#1806) * Rebar3 and erlang.mk dual support. There was only erlang.mk support prior to this change. The main reasons for this dual support are: * Cover report upload can only be done by rebar3 in travis.ci * We want to prepare for the future to build emqx releases using rebar3 * We do not want to stop supporting erlang.mk in one single step * Add depencency version consistency check between erlang.mk and rebar.config --- .gitignore | 3 +- .travis.yml | 9 ++++-- Makefile | 68 +++++++++++++++++++++++++++++++++++++++---- rebar.config | 38 ++++++++++++++++++++++++ rebar.config.script | 19 ++++++++++++ rebar.lock | 36 ----------------------- src/emqx_protocol.erl | 2 +- 7 files changed, 129 insertions(+), 46 deletions(-) create mode 100644 rebar.config create mode 100644 rebar.config.script delete mode 100644 rebar.lock diff --git a/.gitignore b/.gitignore index 0322aaddc..0927f0bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,8 @@ _build .rebar3 rebar3.crashdump .DS_Store -rebar.config emqx.iml bbmustache/ etc/gen.emqx.conf +compile_commands.json +cuttlefish diff --git a/.travis.yml b/.travis.yml index b2d01f3fc..e4088022d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,15 @@ language: erlang otp_release: - - 21.0 - 21.0.4 +before_install: + - git clone https://github.com/erlang/rebar3.git; cd rebar3; ./bootstrap; sudo mv rebar3 /usr/local/bin/; cd .. + script: - - make + - make dep-vsn-check + - make rebar-eunit + - make rebar-ct + - make coveralls sudo: false diff --git a/Makefile b/Makefile index f24497852..2417ad040 100644 --- a/Makefile +++ b/Makefile @@ -9,11 +9,11 @@ DEPS = jsx gproc gen_rpc lager ekka esockd cowboy clique lager_syslog dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.2.0 -dep_lager = git https://github.com/erlang-lager/lager 3.6.4 +dep_lager = git https://github.com/erlang-lager/lager 3.6.5 dep_esockd = git https://github.com/emqx/esockd v5.4 dep_ekka = git https://github.com/emqx/ekka v0.4.1 dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 -dep_clique = git https://github.com/emqx/clique +dep_clique = git https://github.com/emqx/clique develop dep_lager_syslog = git https://github.com/basho/lager_syslog 3.0.1 NO_AUTOPATCH = cuttlefish @@ -41,7 +41,8 @@ CT_SUITES = emqx emqx_zone emqx_banned emqx_connection emqx_session emqx_access emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm \ emqx_mountpoint emqx_listeners emqx_protocol -CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqxct@127.0.0.1 +CT_NODE_NAME = emqxct@127.0.0.1 +CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME) COVER = true @@ -51,7 +52,7 @@ DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wun include erlang.mk -clean:: gen-clean +clean:: gen-clean rebar-clean .PHONY: gen-clean gen-clean: @@ -73,7 +74,62 @@ etc/gen.emqx.conf: bbmustache etc/emqx.conf halt(0)." app.config: etc/gen.emqx.conf - $(verbose) ./deps/cuttlefish/cuttlefish -l info -e etc/ -c etc/gen.emqx.conf -i priv/emqx.schema -d data/ + $(verbose) ./cuttlefish -l info -e etc/ -c etc/gen.emqx.conf -i priv/emqx.schema -d data/ -ct: app.config +ct: cuttlefish app.config + +coveralls: + @rebar3 coveralls send + +cuttlefish: deps + @mv ./deps/cuttlefish/cuttlefish ./cuttlefish + +rebar-cuttlefish: rebar-deps + @make -C _build/default/lib/cuttlefish + @mv _build/default/lib/cuttlefish/cuttlefish ./cuttlefish + +rebar-deps: + @rebar3 get-deps + +rebar-eunit: + @rebar3 eunit + +rebar-compile: + @rebar3 compile + +rebar-ct: rebar-cuttlefish app.config + @rebar3 as test compile + @ln -s -f '../../../../etc' _build/test/lib/emqx/ + @ln -s -f '../../../../data' _build/test/lib/emqx/ + @rebar3 ct -v --readable=false --name $(CT_NODE_NAME) --suite=$(shell echo $(foreach var,$(CT_SUITES),test/$(var)_SUITE) | tr ' ' ',') + +rebar-clean: + @rebar3 clean + +distclean:: rebar-clean + @rm -rf _build cover deps logs log data + @rm -f rebar.lock compile_commands.json cuttlefish + +## Below are for version consistency check during erlang.mk and rebar3 dual mode support +none= +space = $(none) $(none) +comma = , +quote = \" +curly_l = "{" +curly_r = "}" +dep-versions = [$(foreach dep,$(DEPS) $(BUILD_DEPS),$(curly_l)$(dep),$(quote)$(word 3,$(dep_$(dep)))$(quote)$(curly_r)$(comma))[]] + +.PHONY: dep-vsn-check +dep-vsn-check: + $(verbose) erl -noshell -eval \ + "MkVsns = lists:sort(lists:flatten($(dep-versions))), \ + {ok, Conf} = file:consult('rebar.config'), \ + {_, Deps1} = lists:keyfind(deps, 1, Conf), \ + {_, Deps2} = lists:keyfind(github_emqx_deps, 1, Conf), \ + F = fun({N, V}) when is_list(V) -> {N, V}; ({N, {git, _, {branch, V}}}) -> {N, V} end, \ + RebarVsns = lists:sort(lists:map(F, Deps1 ++ Deps2)), \ + case {RebarVsns -- MkVsns, MkVsns -- RebarVsns} of \ + {[], []} -> halt(0); \ + {Rebar, Mk} -> erlang:error({deps_version_discrepancy, [{rebar, Rebar}, {mk, Mk}]}) \ + end." diff --git a/rebar.config b/rebar.config new file mode 100644 index 000000000..d7d14c883 --- /dev/null +++ b/rebar.config @@ -0,0 +1,38 @@ +{deps, [{jsx, "2.9.0"}, + {gproc, "0.8.0"}, + {lager, "3.6.5"}, + {cowboy, "2.4.0"}, + {lager_syslog, {git, "https://github.com/basho/lager_syslog", {branch, "3.0.1"}}} + ]}. + +%% appended to deps in rebar.config.script +{github_emqx_deps, + [{gen_rpc, "2.2.0"}, + {ekka, "v0.4.1"}, + {clique, "develop"}, + {esockd, "v5.4"}, + {cuttlefish, "emqx30"} + ]}. + +{edoc_opts, [{preprocess, true}]}. +{erl_opts, [warn_unused_vars, + warn_shadow_vars, + warn_unused_import, + warn_obsolete_guard, + debug_info, + {parse_transform, lager_transform}]}. +{xref_checks, [undefined_function_calls, undefined_functions, + locals_not_used, deprecated_function_calls, + warnings_as_errors, deprecated_functions]}. +{cover_enabled, true}. +{cover_opts, [verbose]}. +{cover_export_enabled, true}. + +%% rebar3_neotoma_plugin is needed to compile the .peg file for cuttlefish +{plugins, [rebar3_neotoma_plugin]}. + +%% Do not include cuttlefish's dependencies as mine +%% its dependencies are only fetched to compile itself +%% they are however not needed by emqx +{overrides, [{override, cuttlefish, [{deps, []}]} + ]}. diff --git a/rebar.config.script b/rebar.config.script new file mode 100644 index 000000000..7c247ac48 --- /dev/null +++ b/rebar.config.script @@ -0,0 +1,19 @@ + +CONFIG1 = case os:getenv("TRAVIS") of + "true" -> + JobId = os:getenv("TRAVIS_JOB_ID"), + [{coveralls_service_job_id, JobId}, + {plugins, [coveralls]}, + {coveralls_coverdata, "_build/test/cover/*.coverdata"}, + {coveralls_service_name , "travis-ci"} | CONFIG]; + _ -> + CONFIG +end, + +{_, Deps} = lists:keyfind(deps, 1, CONFIG1), +{_, OurDeps} = lists:keyfind(github_emqx_deps, 1, CONFIG1), +UrlPrefix = "https://github.com/emqx/", +NewDeps = Deps ++ [{Name, {git, UrlPrefix ++ atom_to_list(Name), {branch, Branch}}} || {Name, Branch} <- OurDeps], +CONFIG2 = lists:keystore(deps, 1, CONFIG1, {deps, NewDeps}), + +CONFIG2. diff --git a/rebar.lock b/rebar.lock deleted file mode 100644 index 1364f95ae..000000000 --- a/rebar.lock +++ /dev/null @@ -1,36 +0,0 @@ -[{<<"esockd">>, - {git,"https://github.com/emqtt/esockd", - {ref,"87d0d3b672e0f25e474f5f8298da568cbb6b168a"}}, - 0}, - {<<"gen_logger">>, - {git,"https://github.com/emqtt/gen_logger.git", - {ref,"f6e9f2f373d99f41ffe0579ab5a5f3b19472c9c5"}}, - 1}, - {<<"goldrush">>, - {git,"https://github.com/basho/goldrush.git", - {ref,"8f1b715d36b650ec1e1f5612c00e28af6ab0de82"}}, - 1}, - {<<"gproc">>, - {git,"https://github.com/uwiger/gproc", - {ref,"01c8fbfdd5e4701e8e4b57b0c8279872f9574b0b"}}, - 0}, - {<<"lager">>, - {git,"https://github.com/basho/lager", - {ref,"81eaef0ce98fdbf64ab95665e3bc2ec4b24c7dac"}}, - 0}, - {<<"lager_syslog">>, - {git,"https://github.com/basho/lager_syslog", - {ref,"126dd0284fcac9b01613189a82facf8d803411a2"}}, - 0}, - {<<"mochiweb">>, - {git,"https://github.com/emqtt/mochiweb", - {ref,"c75d88e451b4fe26580a58223f645d99482f51af"}}, - 0}, - {<<"pbkdf2">>, - {git,"https://github.com/comtihon/erlang-pbkdf2.git", - {ref,"7076584f5377e98600a7e2cb81980b2992fb2f71"}}, - 0}, - {<<"syslog">>, - {git,"git://github.com/Vagabond/erlang-syslog", - {ref,"0e4f0e95c361af298c5d1d17ceccfa831efc036d"}}, - 1}]. diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 49408d38e..8301cf014 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -654,7 +654,7 @@ check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Ret #pstate{zone = Zone}) -> emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain, topic_alias => TopicAlias}); check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, - variable = #mqtt_packet_publish{ properties = Properties}}, + variable = #mqtt_packet_publish{ properties = _Properties}}, #pstate{zone = Zone}) -> emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}). From d29069a50d590e78b5778a73e000626b0eb6e742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Wed, 12 Sep 2018 15:17:18 +0800 Subject: [PATCH 279/520] Add feature for issue#1809 --- etc/emqx.conf | 10 ++++++++++ priv/emqx.schema | 13 ++++++++++++- src/emqx_listeners.erl | 10 ++++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index deb702211..5d54389dc 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1207,6 +1207,11 @@ listener.ssl.external.reuseaddr = true ## Examples: 8083, 127.0.0.1:8083, ::1:8083 listener.ws.external = 8083 +## Whether the client must include "mqtt" in the list of WebSocket Sub Protocols it offers +## +## Value: true | false +listener.ws.external.standard_mqtt = true + ## The acceptor pool for external MQTT/WebSocket listener. ## ## Value: Number @@ -1346,6 +1351,11 @@ listener.ws.external.nodelay = true ## Examples: 8084, 127.0.0.1:8084, ::1:8084 listener.wss.external = 8084 +## Whether the client must include "mqtt" in the list of WebSocket Sub Protocols it offers +## +## Value: true | false +listener.wss.external.standard_mqtt = true + ## The acceptor pool for external MQTT/WebSocket/SSL listener. ## ## Value: Number diff --git a/priv/emqx.schema b/priv/emqx.schema index e9f4932c4..8e8979908 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1094,6 +1094,11 @@ end}. {datatype, [integer, ip]} ]}. +{mapping, "listener.ws.$name.standard_mqtt", "emqx.listeners", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + {mapping, "listener.ws.$name.acceptors", "emqx.listeners", [ {default, 8}, {datatype, integer} @@ -1195,6 +1200,11 @@ end}. {datatype, [integer, ip]} ]}. +{mapping, "listener.wss.$name.standard_mqtt", "emqx.listeners", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + {mapping, "listener.wss.$name.acceptors", "emqx.listeners", [ {default, 8}, {datatype, integer} @@ -1365,7 +1375,8 @@ end}. end, LisOpts = fun(Prefix) -> - Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, + Filter([{standard_mqtt, cuttlefish:conf_get(Prefix ++ ".standard_mqtt", Conf, undefined)}, + {acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, {max_connections, cuttlefish:conf_get(Prefix ++ ".max_connections", Conf)}, {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 421304f3a..127c62b39 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -51,12 +51,12 @@ start_listener(Proto, ListenOn, Options) when Proto == ssl; Proto == tls -> %% Start MQTT/WS listener start_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws -> - Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws_connection, Options}]}]), + Dispatch = cowboy_router:compile([{'_', [{subprotocol_name(Options), emqx_ws_connection, Options}]}]), start_http_listener(fun cowboy:start_clear/3, 'mqtt:ws', ListenOn, ranch_opts(Options), Dispatch); %% Start MQTT/WSS listener start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> - Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws_connection, Options}]}]), + Dispatch = cowboy_router:compile([{'_', [{subprotocol_name(Options), emqx_ws_connection, Options}]}]), start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn, ranch_opts(Options), Dispatch). start_mqtt_listener(Name, ListenOn, Options) -> @@ -67,6 +67,12 @@ start_mqtt_listener(Name, ListenOn, Options) -> start_http_listener(Start, Name, ListenOn, RanchOpts, Dispatch) -> Start(Name, with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}). +subprotocol_name(Options) -> + case proplists:get_value(standard_mqtt, Options, true) of + true -> "/mqtt"; + false -> "/" + end. + ranch_opts(Options) -> NumAcceptors = proplists:get_value(acceptors, Options, 4), MaxConnections = proplists:get_value(max_connections, Options, 1024), From 8db9f3e81d905509552ce0d508ac69af9c912932 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Wed, 12 Sep 2018 20:01:31 +0200 Subject: [PATCH 280/520] Update & clarify module doc for emqx_mqueue.erl --- src/emqx_mqueue.erl | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index a93fd8838..d9270dd5f 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -18,7 +18,7 @@ %% %% This module implements a simple in-memory queue for MQTT persistent session. %% -%% If the broker restarted or crashed, all the messages queued will be gone. +%% If the broker restarts or crashes, all queued messages will be lost. %% %% Concept of Message Queue and Inflight Window: %% @@ -29,12 +29,15 @@ %% |<--- Win Size --->| %% %% -%% 1. Inflight Window to store the messages delivered and awaiting for puback. +%% 1. Inflight Window is to store the messages +%% that are delivered but still awaiting for puback. %% -%% 2. Enqueue messages when the inflight window is full. +%% 2. Messages are enqueued to tail when the inflight window is full. %% -%% 3. If the queue is full, dropped qos0 messages if store_qos0 is true, -%% otherwise dropped the oldest one. +%% 3. QoS=0 messages are only enqueued when `store_qos0' is given `true` +%% in init options +%% +%% 4. If the queue is full drop the oldest one unless `max_len' is set to `0'. %% %% @end From 713c43b833a8f812a1daa161fc08fab98e982eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 13 Sep 2018 10:31:36 +0800 Subject: [PATCH 281/520] Set default value of message expiry interval for not mqtt 5.0 message --- src/emqx_protocol.erl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 8301cf014..054ec2a57 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -249,6 +249,13 @@ preprocess_properties(Packet = #mqtt_packet{ PState = #pstate{proto_ver = ?MQTT_PROTO_V5, topic_aliases = Aliases}) -> {Packet, PState#pstate{topic_aliases = maps:put(AliasId, Topic, Aliases)}}; +preprocess_properties(Packet = #mqtt_packet{variable = #mqtt_packet_publish{properties = #{'Message-Expiry-Interval' := _Interval}}}, + PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> + {Packet, PState}; +preprocess_properties(Packet = #mqtt_packet{variable = Publish = #mqtt_packet_publish{properties = Properties}}, + PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> + {Packet#mqtt_packet{variable = Publish#mqtt_packet_publish{properties = maps:put('Message-Expiry-Interval', 0, Properties)}}, PState}; + preprocess_properties(Packet, PState) -> {Packet, PState}. From fde6a2a4c34fee30d92bf00e9625c03e5546be39 Mon Sep 17 00:00:00 2001 From: HuangDan Date: Thu, 13 Sep 2018 20:22:13 +0800 Subject: [PATCH 282/520] Fixed issue #1811 Add tests case for issue #1811 --- src/emqx_connection.erl | 2 +- test/emqx_SUITE.erl | 31 ++++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index adda71450..a7f71d9aa 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -303,7 +303,7 @@ handle_packet(Data, State = #state{proto_state = ProtoState, idle_timeout = IdleTimeout}) -> case catch emqx_frame:parse(Data, ParserState) of {more, NewParserState} -> - {noreply, State#state{parser_state = NewParserState}, IdleTimeout}; + {noreply, run_socket(State#state{parser_state = NewParserState}), IdleTimeout}; {ok, Packet = ?PACKET(Type), Rest} -> emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index a08305a30..166d64a30 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -49,8 +49,18 @@ -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, connect}, + {group, publish}]. groups() -> [{connect, [non_parallel_tests], @@ -60,6 +70,10 @@ groups() -> mqtt_connect_with_ssl_oneway, mqtt_connect_with_ssl_twoway, mqtt_connect_with_ws + ]}, + {publish, [non_parallel_tests], + [ + packet_size ]}]. init_per_suite(Config) -> @@ -157,6 +171,21 @@ mqtt_connect_with_ws(_Config) -> {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_serialise(?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_serialise(?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_serialise(Packet) -> emqx_frame:serialize(Packet). From 35d209f36401445c0dd1c5f83f24235d39a348c5 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Thu, 13 Sep 2018 18:23:19 +0200 Subject: [PATCH 283/520] Fix travis build --- .travis.yml | 2 ++ Makefile | 11 +++++++---- rebar.config | 10 +++------- rebar.config.script | 1 - 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index e4088022d..adef0f3cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,10 @@ before_install: script: - make dep-vsn-check + - make rebar-compile - make rebar-eunit - make rebar-ct + - make rebar-cover - make coveralls sudo: false diff --git a/Makefile b/Makefile index 2417ad040..73438ca01 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ dep_lager_syslog = git https://github.com/basho/lager_syslog 3.0.1 NO_AUTOPATCH = cuttlefish -ERLC_OPTS += +debug_info +ERLC_OPTS += +debug_info -DAPPLICATION=emqx ERLC_OPTS += +'{parse_transform, lager_transform}' BUILD_DEPS = cuttlefish @@ -27,7 +27,7 @@ dep_cuttlefish = git https://github.com/emqx/cuttlefish emqx30 #TEST_DEPS = emqx_ct_helplers #dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers -TEST_ERLC_OPTS += +debug_info +TEST_ERLC_OPTS += +debug_info -DAPPLICATION=emqx TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose @@ -60,7 +60,7 @@ gen-clean: @rm -f etc/gen.emqx.conf bbmustache: - $(verbose) git clone https://github.com/soranoba/bbmustache.git && pushd bbmustache && ./rebar3 compile && popd + $(verbose) git clone https://github.com/soranoba/bbmustache.git && cd bbmustache && ./rebar3 compile && cd .. # This hack is to generate a conf file for testing # relx overlay is used for release @@ -78,6 +78,9 @@ app.config: etc/gen.emqx.conf ct: cuttlefish app.config +rebar-cover: + @rebar3 cover + coveralls: @rebar3 coveralls send @@ -91,7 +94,7 @@ rebar-cuttlefish: rebar-deps rebar-deps: @rebar3 get-deps -rebar-eunit: +rebar-eunit: rebar-cuttlefish @rebar3 eunit rebar-compile: diff --git a/rebar.config b/rebar.config index d7d14c883..aa77f3a02 100644 --- a/rebar.config +++ b/rebar.config @@ -20,7 +20,8 @@ warn_unused_import, warn_obsolete_guard, debug_info, - {parse_transform, lager_transform}]}. + {parse_transform, lager_transform}, + {d, 'APPLICATION', emqx}]}. {xref_checks, [undefined_function_calls, undefined_functions, locals_not_used, deprecated_function_calls, warnings_as_errors, deprecated_functions]}. @@ -29,10 +30,5 @@ {cover_export_enabled, true}. %% rebar3_neotoma_plugin is needed to compile the .peg file for cuttlefish -{plugins, [rebar3_neotoma_plugin]}. +{plugins, [coveralls, rebar3_neotoma_plugin]}. -%% Do not include cuttlefish's dependencies as mine -%% its dependencies are only fetched to compile itself -%% they are however not needed by emqx -{overrides, [{override, cuttlefish, [{deps, []}]} - ]}. diff --git a/rebar.config.script b/rebar.config.script index 7c247ac48..0b18592f1 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -3,7 +3,6 @@ CONFIG1 = case os:getenv("TRAVIS") of "true" -> JobId = os:getenv("TRAVIS_JOB_ID"), [{coveralls_service_job_id, JobId}, - {plugins, [coveralls]}, {coveralls_coverdata, "_build/test/cover/*.coverdata"}, {coveralls_service_name , "travis-ci"} | CONFIG]; _ -> From 45b2686e1c2ea748870644ef4aae83f60aed8923 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Fri, 14 Sep 2018 09:23:27 +0800 Subject: [PATCH 284/520] Delete unnecessary code Prior to this change, there are multiple deprecated functions. --- src/emqx_message.erl | 18 +----------------- test/emqx_message_SUITE.erl | 7 ++----- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 311bd58dd..91e5d4d59 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -22,7 +22,7 @@ -export([get_flag/2, get_flag/3, set_flag/2, set_flag/3, unset_flag/2]). -export([set_headers/2]). -export([get_header/2, get_header/3, set_header/3]). --export([is_expired/1, check_expiry/1, check_expiry/2, update_expiry/1]). +-export([is_expired/1, update_expiry/1]). -export([format/1]). -type(flag() :: atom()). @@ -100,21 +100,6 @@ is_expired(#message{headers = #{'Message-Expiry-Interval' := Interval}, timestam is_expired(_Msg) -> false. --spec(check_expiry(emqx_types:message()) -> {ok, pos_integer()} | expired | false). -check_expiry(Msg = #message{timestamp = CreatedAt}) -> - check_expiry(Msg, CreatedAt); -check_expiry(_Msg) -> - false. - --spec(check_expiry(emqx_types:message(), erlang:timestamp()) -> {ok, pos_integer()} | expired | false). -check_expiry(#message{headers = #{'Message-Expiry-Interval' := Interval}}, Since) -> - case Interval - (elapsed(Since) div 1000) of - Timeout when Timeout > 0 -> {ok, Timeout}; - _ -> expired - end; -check_expiry(_Msg, _Since) -> - false. - update_expiry(Msg = #message{headers = #{'Message-Expiry-Interval' := Interval}, timestamp = CreatedAt}) -> case elapsed(CreatedAt) of Elapsed when Elapsed > 0 -> @@ -138,4 +123,3 @@ format(flags, Flags) -> io_lib:format("~p", [[Flag || {Flag, true} <- maps:to_list(Flags)]]); format(headers, Headers) -> io_lib:format("~p", [Headers]). - diff --git a/test/emqx_message_SUITE.erl b/test/emqx_message_SUITE.erl index c2ca0f0aa..37207e40c 100644 --- a/test/emqx_message_SUITE.erl +++ b/test/emqx_message_SUITE.erl @@ -29,7 +29,7 @@ all() -> message_flag, message_header, message_format, - message_expired + message_expired ]. message_make(_) -> @@ -53,7 +53,7 @@ message_flag(_) -> ?assert(emqx_message:get_flag(dup, Msg6)), ?assert(emqx_message:get_flag(retain, Msg6)). -message_header(_) -> +message_header(_) -> Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), Msg1 = emqx_message:set_headers(#{a => 1, b => 2}, Msg), Msg2 = emqx_message:set_header(c, 3, Msg1), @@ -68,11 +68,8 @@ message_expired(_) -> Msg1 = emqx_message:set_headers(#{'Message-Expiry-Interval' => 1}, Msg), timer:sleep(500), ?assertNot(emqx_message:is_expired(Msg1)), - {ok, 1} = emqx_message:check_expiry(Msg1), timer:sleep(600), ?assert(emqx_message:is_expired(Msg1)), - expired = emqx_message:check_expiry(Msg1), timer:sleep(1000), Msg2 = emqx_message:update_expiry(Msg1), ?assertEqual(1, emqx_message:get_header('Message-Expiry-Interval', Msg2)). - From e33414aca1375458bff8e8f2d8edd5d877cc29bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 14 Sep 2018 10:10:32 +0800 Subject: [PATCH 285/520] Add customized mqtt path for websocket --- etc/emqx.conf | 12 ++++++------ priv/emqx.schema | 14 +++++++------- src/emqx_listeners.erl | 14 ++++++++------ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 5d54389dc..db4eedffa 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1207,10 +1207,10 @@ listener.ssl.external.reuseaddr = true ## Examples: 8083, 127.0.0.1:8083, ::1:8083 listener.ws.external = 8083 -## Whether the client must include "mqtt" in the list of WebSocket Sub Protocols it offers +## Define the path you want to add to the end of the URL ## -## Value: true | false -listener.ws.external.standard_mqtt = true +## Value: / | / +listener.ws.external.mqtt_path = /mqtt ## The acceptor pool for external MQTT/WebSocket listener. ## @@ -1351,10 +1351,10 @@ listener.ws.external.nodelay = true ## Examples: 8084, 127.0.0.1:8084, ::1:8084 listener.wss.external = 8084 -## Whether the client must include "mqtt" in the list of WebSocket Sub Protocols it offers +## Define the path you want to add to the end of the URL ## -## Value: true | false -listener.wss.external.standard_mqtt = true +## Value: / | / +listener.wss.external.mqtt_path = /mqtt ## The acceptor pool for external MQTT/WebSocket/SSL listener. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index 8e8979908..cfd8c83a6 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1094,9 +1094,9 @@ end}. {datatype, [integer, ip]} ]}. -{mapping, "listener.ws.$name.standard_mqtt", "emqx.listeners", [ - {default, true}, - {datatype, {enum, [true, false]}} +{mapping, "listener.ws.$name.mqtt_path", "emqx.listeners", [ + {default, "/mqtt"}, + {datatype, string} ]}. {mapping, "listener.ws.$name.acceptors", "emqx.listeners", [ @@ -1200,9 +1200,9 @@ end}. {datatype, [integer, ip]} ]}. -{mapping, "listener.wss.$name.standard_mqtt", "emqx.listeners", [ - {default, true}, - {datatype, {enum, [true, false]}} +{mapping, "listener.wss.$name.mqtt_path", "emqx.listeners", [ + {default, "/mqtt"}, + {datatype, string} ]}. {mapping, "listener.wss.$name.acceptors", "emqx.listeners", [ @@ -1375,7 +1375,7 @@ end}. end, LisOpts = fun(Prefix) -> - Filter([{standard_mqtt, cuttlefish:conf_get(Prefix ++ ".standard_mqtt", Conf, undefined)}, + Filter([{mqtt_path, cuttlefish:conf_get(Prefix ++ ".mqtt_path", Conf, undefined)}, {acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, {max_connections, cuttlefish:conf_get(Prefix ++ ".max_connections", Conf)}, {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 127c62b39..4cd20a091 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -51,12 +51,12 @@ start_listener(Proto, ListenOn, Options) when Proto == ssl; Proto == tls -> %% Start MQTT/WS listener start_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws -> - Dispatch = cowboy_router:compile([{'_', [{subprotocol_name(Options), emqx_ws_connection, Options}]}]), + Dispatch = cowboy_router:compile([{'_', [{mqtt_path(Options), emqx_ws_connection, Options}]}]), start_http_listener(fun cowboy:start_clear/3, 'mqtt:ws', ListenOn, ranch_opts(Options), Dispatch); %% Start MQTT/WSS listener start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> - Dispatch = cowboy_router:compile([{'_', [{subprotocol_name(Options), emqx_ws_connection, Options}]}]), + Dispatch = cowboy_router:compile([{'_', [{mqtt_path(Options), emqx_ws_connection, Options}]}]), start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn, ranch_opts(Options), Dispatch). start_mqtt_listener(Name, ListenOn, Options) -> @@ -67,10 +67,12 @@ start_mqtt_listener(Name, ListenOn, Options) -> start_http_listener(Start, Name, ListenOn, RanchOpts, Dispatch) -> Start(Name, with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}). -subprotocol_name(Options) -> - case proplists:get_value(standard_mqtt, Options, true) of - true -> "/mqtt"; - false -> "/" +mqtt_path(Options) -> + MQTTPath = proplists:get_value(mqtt_path, Options, "/mqtt"), + case erlang:list_to_bitstring(MQTTPath) of + <<"/">> -> MQTTPath; + <<"/", _/binary>> -> MQTTPath; + _ -> "/mqtt" end. ranch_opts(Options) -> From a38d3578477b1c6a57102fa5c7170aac3cb53fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 14 Sep 2018 10:40:16 +0800 Subject: [PATCH 286/520] Add test case for last change --- src/emqx_listeners.erl | 6 ++++++ test/emqx_listeners_SUITE.erl | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 4cd20a091..2d9bb4bba 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -22,6 +22,12 @@ -export([restart_listener/1, restart_listener/3]). -export([stop_listener/1, stop_listener/3]). +-ifdef(TEST). + +-export([mqtt_path/1]). + +-endif. + -type(listener() :: {esockd:proto(), esockd:listen_on(), [esockd:option()]}). %% @doc Start all listeners. diff --git a/test/emqx_listeners_SUITE.erl b/test/emqx_listeners_SUITE.erl index 17181aa31..8d97a8158 100644 --- a/test/emqx_listeners_SUITE.erl +++ b/test/emqx_listeners_SUITE.erl @@ -26,7 +26,8 @@ all() -> [start_stop_listeners, - restart_listeners]. + restart_listeners, + t_mqtt_path]. init_per_suite(Config) -> NewConfig = generate_config(), @@ -49,6 +50,11 @@ restart_listeners(_) -> ok = emqx_listeners:restart(), ok = emqx_listeners:stop(). +t_mqtt_path(_) -> + ?assertEqual("/test", emqx_listeners:mqtt_path([{mqtt_path, "/test"}])), + ?assertEqual("/", emqx_listeners:mqtt_path([{mqtt_path, "/"}])), + ?assertEqual("/mqtt", emqx_listeners:mqtt_path([{mqtt_path, "test"}])). + generate_config() -> Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), Conf = conf_parse:file([local_path(["etc", "gen.emqx.conf"])]), From c7928235c3cdd064b969ea556f5815470cb77e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 14 Sep 2018 11:29:38 +0800 Subject: [PATCH 287/520] Remove check for MQTT path, and normalize code --- etc/emqx.conf | 4 ++-- priv/emqx.schema | 4 ++-- src/emqx_listeners.erl | 13 +------------ src/emqx_protocol.erl | 7 ------- test/emqx_listeners_SUITE.erl | 8 +------- 5 files changed, 6 insertions(+), 30 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index db4eedffa..4a7c195d2 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1207,7 +1207,7 @@ listener.ssl.external.reuseaddr = true ## Examples: 8083, 127.0.0.1:8083, ::1:8083 listener.ws.external = 8083 -## Define the path you want to add to the end of the URL +## The path of WebSocket MQTT endpoint ## ## Value: / | / listener.ws.external.mqtt_path = /mqtt @@ -1351,7 +1351,7 @@ listener.ws.external.nodelay = true ## Examples: 8084, 127.0.0.1:8084, ::1:8084 listener.wss.external = 8084 -## Define the path you want to add to the end of the URL +## The path of WebSocket MQTT endpoint ## ## Value: / | / listener.wss.external.mqtt_path = /mqtt diff --git a/priv/emqx.schema b/priv/emqx.schema index cfd8c83a6..a8ed316c2 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1375,8 +1375,8 @@ end}. end, LisOpts = fun(Prefix) -> - Filter([{mqtt_path, cuttlefish:conf_get(Prefix ++ ".mqtt_path", Conf, undefined)}, - {acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, + Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, + {mqtt_path, cuttlefish:conf_get(Prefix ++ ".mqtt_path", Conf, undefined)}, {max_connections, cuttlefish:conf_get(Prefix ++ ".max_connections", Conf)}, {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 2d9bb4bba..d3ef43069 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -22,12 +22,6 @@ -export([restart_listener/1, restart_listener/3]). -export([stop_listener/1, stop_listener/3]). --ifdef(TEST). - --export([mqtt_path/1]). - --endif. - -type(listener() :: {esockd:proto(), esockd:listen_on(), [esockd:option()]}). %% @doc Start all listeners. @@ -74,12 +68,7 @@ start_http_listener(Start, Name, ListenOn, RanchOpts, Dispatch) -> Start(Name, with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}). mqtt_path(Options) -> - MQTTPath = proplists:get_value(mqtt_path, Options, "/mqtt"), - case erlang:list_to_bitstring(MQTTPath) of - <<"/">> -> MQTTPath; - <<"/", _/binary>> -> MQTTPath; - _ -> "/mqtt" - end. + proplists:get_value(mqtt_path, Options, "/mqtt"). ranch_opts(Options) -> NumAcceptors = proplists:get_value(acceptors, Options, 4), diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 054ec2a57..8301cf014 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -249,13 +249,6 @@ preprocess_properties(Packet = #mqtt_packet{ PState = #pstate{proto_ver = ?MQTT_PROTO_V5, topic_aliases = Aliases}) -> {Packet, PState#pstate{topic_aliases = maps:put(AliasId, Topic, Aliases)}}; -preprocess_properties(Packet = #mqtt_packet{variable = #mqtt_packet_publish{properties = #{'Message-Expiry-Interval' := _Interval}}}, - PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> - {Packet, PState}; -preprocess_properties(Packet = #mqtt_packet{variable = Publish = #mqtt_packet_publish{properties = Properties}}, - PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> - {Packet#mqtt_packet{variable = Publish#mqtt_packet_publish{properties = maps:put('Message-Expiry-Interval', 0, Properties)}}, PState}; - preprocess_properties(Packet, PState) -> {Packet, PState}. diff --git a/test/emqx_listeners_SUITE.erl b/test/emqx_listeners_SUITE.erl index 8d97a8158..17181aa31 100644 --- a/test/emqx_listeners_SUITE.erl +++ b/test/emqx_listeners_SUITE.erl @@ -26,8 +26,7 @@ all() -> [start_stop_listeners, - restart_listeners, - t_mqtt_path]. + restart_listeners]. init_per_suite(Config) -> NewConfig = generate_config(), @@ -50,11 +49,6 @@ restart_listeners(_) -> ok = emqx_listeners:restart(), ok = emqx_listeners:stop(). -t_mqtt_path(_) -> - ?assertEqual("/test", emqx_listeners:mqtt_path([{mqtt_path, "/test"}])), - ?assertEqual("/", emqx_listeners:mqtt_path([{mqtt_path, "/"}])), - ?assertEqual("/mqtt", emqx_listeners:mqtt_path([{mqtt_path, "test"}])). - generate_config() -> Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), Conf = conf_parse:file([local_path(["etc", "gen.emqx.conf"])]), From 49ed6f800c68639aba8e9bff5707769ec0e3cf95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 14 Sep 2018 11:40:57 +0800 Subject: [PATCH 288/520] Change comments --- etc/emqx.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 4a7c195d2..2bba8d024 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1209,7 +1209,7 @@ listener.ws.external = 8083 ## The path of WebSocket MQTT endpoint ## -## Value: / | / +## Value: URL Path listener.ws.external.mqtt_path = /mqtt ## The acceptor pool for external MQTT/WebSocket listener. @@ -1353,7 +1353,7 @@ listener.wss.external = 8084 ## The path of WebSocket MQTT endpoint ## -## Value: / | / +## Value: URL Path listener.wss.external.mqtt_path = /mqtt ## The acceptor pool for external MQTT/WebSocket/SSL listener. From 88b3460715a25a52545382adb079ce0a6ddcc88e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Wed, 12 Sep 2018 15:17:18 +0800 Subject: [PATCH 289/520] Add feature for issue#1809 --- etc/emqx.conf | 10 ++++++++++ priv/emqx.schema | 13 ++++++++++++- src/emqx_listeners.erl | 10 ++++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index deb702211..5d54389dc 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1207,6 +1207,11 @@ listener.ssl.external.reuseaddr = true ## Examples: 8083, 127.0.0.1:8083, ::1:8083 listener.ws.external = 8083 +## Whether the client must include "mqtt" in the list of WebSocket Sub Protocols it offers +## +## Value: true | false +listener.ws.external.standard_mqtt = true + ## The acceptor pool for external MQTT/WebSocket listener. ## ## Value: Number @@ -1346,6 +1351,11 @@ listener.ws.external.nodelay = true ## Examples: 8084, 127.0.0.1:8084, ::1:8084 listener.wss.external = 8084 +## Whether the client must include "mqtt" in the list of WebSocket Sub Protocols it offers +## +## Value: true | false +listener.wss.external.standard_mqtt = true + ## The acceptor pool for external MQTT/WebSocket/SSL listener. ## ## Value: Number diff --git a/priv/emqx.schema b/priv/emqx.schema index e9f4932c4..8e8979908 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1094,6 +1094,11 @@ end}. {datatype, [integer, ip]} ]}. +{mapping, "listener.ws.$name.standard_mqtt", "emqx.listeners", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + {mapping, "listener.ws.$name.acceptors", "emqx.listeners", [ {default, 8}, {datatype, integer} @@ -1195,6 +1200,11 @@ end}. {datatype, [integer, ip]} ]}. +{mapping, "listener.wss.$name.standard_mqtt", "emqx.listeners", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + {mapping, "listener.wss.$name.acceptors", "emqx.listeners", [ {default, 8}, {datatype, integer} @@ -1365,7 +1375,8 @@ end}. end, LisOpts = fun(Prefix) -> - Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, + Filter([{standard_mqtt, cuttlefish:conf_get(Prefix ++ ".standard_mqtt", Conf, undefined)}, + {acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, {max_connections, cuttlefish:conf_get(Prefix ++ ".max_connections", Conf)}, {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 421304f3a..127c62b39 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -51,12 +51,12 @@ start_listener(Proto, ListenOn, Options) when Proto == ssl; Proto == tls -> %% Start MQTT/WS listener start_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws -> - Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws_connection, Options}]}]), + Dispatch = cowboy_router:compile([{'_', [{subprotocol_name(Options), emqx_ws_connection, Options}]}]), start_http_listener(fun cowboy:start_clear/3, 'mqtt:ws', ListenOn, ranch_opts(Options), Dispatch); %% Start MQTT/WSS listener start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> - Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws_connection, Options}]}]), + Dispatch = cowboy_router:compile([{'_', [{subprotocol_name(Options), emqx_ws_connection, Options}]}]), start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn, ranch_opts(Options), Dispatch). start_mqtt_listener(Name, ListenOn, Options) -> @@ -67,6 +67,12 @@ start_mqtt_listener(Name, ListenOn, Options) -> start_http_listener(Start, Name, ListenOn, RanchOpts, Dispatch) -> Start(Name, with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}). +subprotocol_name(Options) -> + case proplists:get_value(standard_mqtt, Options, true) of + true -> "/mqtt"; + false -> "/" + end. + ranch_opts(Options) -> NumAcceptors = proplists:get_value(acceptors, Options, 4), MaxConnections = proplists:get_value(max_connections, Options, 1024), From 0c6a268539718ef5764ce4e29a136736f31b7a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 13 Sep 2018 10:31:36 +0800 Subject: [PATCH 290/520] Set default value of message expiry interval for not mqtt 5.0 message --- src/emqx_protocol.erl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 8301cf014..054ec2a57 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -249,6 +249,13 @@ preprocess_properties(Packet = #mqtt_packet{ PState = #pstate{proto_ver = ?MQTT_PROTO_V5, topic_aliases = Aliases}) -> {Packet, PState#pstate{topic_aliases = maps:put(AliasId, Topic, Aliases)}}; +preprocess_properties(Packet = #mqtt_packet{variable = #mqtt_packet_publish{properties = #{'Message-Expiry-Interval' := _Interval}}}, + PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> + {Packet, PState}; +preprocess_properties(Packet = #mqtt_packet{variable = Publish = #mqtt_packet_publish{properties = Properties}}, + PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> + {Packet#mqtt_packet{variable = Publish#mqtt_packet_publish{properties = maps:put('Message-Expiry-Interval', 0, Properties)}}, PState}; + preprocess_properties(Packet, PState) -> {Packet, PState}. From 6f536eaac42af64b8f2beecb69843a3b241be2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 14 Sep 2018 10:10:32 +0800 Subject: [PATCH 291/520] Add customized mqtt path for websocket --- etc/emqx.conf | 12 ++++++------ priv/emqx.schema | 14 +++++++------- src/emqx_listeners.erl | 14 ++++++++------ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 5d54389dc..db4eedffa 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1207,10 +1207,10 @@ listener.ssl.external.reuseaddr = true ## Examples: 8083, 127.0.0.1:8083, ::1:8083 listener.ws.external = 8083 -## Whether the client must include "mqtt" in the list of WebSocket Sub Protocols it offers +## Define the path you want to add to the end of the URL ## -## Value: true | false -listener.ws.external.standard_mqtt = true +## Value: / | / +listener.ws.external.mqtt_path = /mqtt ## The acceptor pool for external MQTT/WebSocket listener. ## @@ -1351,10 +1351,10 @@ listener.ws.external.nodelay = true ## Examples: 8084, 127.0.0.1:8084, ::1:8084 listener.wss.external = 8084 -## Whether the client must include "mqtt" in the list of WebSocket Sub Protocols it offers +## Define the path you want to add to the end of the URL ## -## Value: true | false -listener.wss.external.standard_mqtt = true +## Value: / | / +listener.wss.external.mqtt_path = /mqtt ## The acceptor pool for external MQTT/WebSocket/SSL listener. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index 8e8979908..cfd8c83a6 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1094,9 +1094,9 @@ end}. {datatype, [integer, ip]} ]}. -{mapping, "listener.ws.$name.standard_mqtt", "emqx.listeners", [ - {default, true}, - {datatype, {enum, [true, false]}} +{mapping, "listener.ws.$name.mqtt_path", "emqx.listeners", [ + {default, "/mqtt"}, + {datatype, string} ]}. {mapping, "listener.ws.$name.acceptors", "emqx.listeners", [ @@ -1200,9 +1200,9 @@ end}. {datatype, [integer, ip]} ]}. -{mapping, "listener.wss.$name.standard_mqtt", "emqx.listeners", [ - {default, true}, - {datatype, {enum, [true, false]}} +{mapping, "listener.wss.$name.mqtt_path", "emqx.listeners", [ + {default, "/mqtt"}, + {datatype, string} ]}. {mapping, "listener.wss.$name.acceptors", "emqx.listeners", [ @@ -1375,7 +1375,7 @@ end}. end, LisOpts = fun(Prefix) -> - Filter([{standard_mqtt, cuttlefish:conf_get(Prefix ++ ".standard_mqtt", Conf, undefined)}, + Filter([{mqtt_path, cuttlefish:conf_get(Prefix ++ ".mqtt_path", Conf, undefined)}, {acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, {max_connections, cuttlefish:conf_get(Prefix ++ ".max_connections", Conf)}, {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 127c62b39..4cd20a091 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -51,12 +51,12 @@ start_listener(Proto, ListenOn, Options) when Proto == ssl; Proto == tls -> %% Start MQTT/WS listener start_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws -> - Dispatch = cowboy_router:compile([{'_', [{subprotocol_name(Options), emqx_ws_connection, Options}]}]), + Dispatch = cowboy_router:compile([{'_', [{mqtt_path(Options), emqx_ws_connection, Options}]}]), start_http_listener(fun cowboy:start_clear/3, 'mqtt:ws', ListenOn, ranch_opts(Options), Dispatch); %% Start MQTT/WSS listener start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> - Dispatch = cowboy_router:compile([{'_', [{subprotocol_name(Options), emqx_ws_connection, Options}]}]), + Dispatch = cowboy_router:compile([{'_', [{mqtt_path(Options), emqx_ws_connection, Options}]}]), start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn, ranch_opts(Options), Dispatch). start_mqtt_listener(Name, ListenOn, Options) -> @@ -67,10 +67,12 @@ start_mqtt_listener(Name, ListenOn, Options) -> start_http_listener(Start, Name, ListenOn, RanchOpts, Dispatch) -> Start(Name, with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}). -subprotocol_name(Options) -> - case proplists:get_value(standard_mqtt, Options, true) of - true -> "/mqtt"; - false -> "/" +mqtt_path(Options) -> + MQTTPath = proplists:get_value(mqtt_path, Options, "/mqtt"), + case erlang:list_to_bitstring(MQTTPath) of + <<"/">> -> MQTTPath; + <<"/", _/binary>> -> MQTTPath; + _ -> "/mqtt" end. ranch_opts(Options) -> From 5465b015be1bb635da7cc0230cdc6d8336aaa271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 14 Sep 2018 10:40:16 +0800 Subject: [PATCH 292/520] Add test case for last change --- src/emqx_listeners.erl | 6 ++++++ test/emqx_listeners_SUITE.erl | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 4cd20a091..2d9bb4bba 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -22,6 +22,12 @@ -export([restart_listener/1, restart_listener/3]). -export([stop_listener/1, stop_listener/3]). +-ifdef(TEST). + +-export([mqtt_path/1]). + +-endif. + -type(listener() :: {esockd:proto(), esockd:listen_on(), [esockd:option()]}). %% @doc Start all listeners. diff --git a/test/emqx_listeners_SUITE.erl b/test/emqx_listeners_SUITE.erl index 17181aa31..8d97a8158 100644 --- a/test/emqx_listeners_SUITE.erl +++ b/test/emqx_listeners_SUITE.erl @@ -26,7 +26,8 @@ all() -> [start_stop_listeners, - restart_listeners]. + restart_listeners, + t_mqtt_path]. init_per_suite(Config) -> NewConfig = generate_config(), @@ -49,6 +50,11 @@ restart_listeners(_) -> ok = emqx_listeners:restart(), ok = emqx_listeners:stop(). +t_mqtt_path(_) -> + ?assertEqual("/test", emqx_listeners:mqtt_path([{mqtt_path, "/test"}])), + ?assertEqual("/", emqx_listeners:mqtt_path([{mqtt_path, "/"}])), + ?assertEqual("/mqtt", emqx_listeners:mqtt_path([{mqtt_path, "test"}])). + generate_config() -> Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), Conf = conf_parse:file([local_path(["etc", "gen.emqx.conf"])]), From 5eb92e37cc0a5863233b7852572c984334c3c6f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 14 Sep 2018 11:29:38 +0800 Subject: [PATCH 293/520] Remove check for MQTT path, and normalize code --- etc/emqx.conf | 4 ++-- priv/emqx.schema | 4 ++-- src/emqx_listeners.erl | 13 +------------ src/emqx_protocol.erl | 7 ------- test/emqx_listeners_SUITE.erl | 8 +------- 5 files changed, 6 insertions(+), 30 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index db4eedffa..4a7c195d2 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1207,7 +1207,7 @@ listener.ssl.external.reuseaddr = true ## Examples: 8083, 127.0.0.1:8083, ::1:8083 listener.ws.external = 8083 -## Define the path you want to add to the end of the URL +## The path of WebSocket MQTT endpoint ## ## Value: / | / listener.ws.external.mqtt_path = /mqtt @@ -1351,7 +1351,7 @@ listener.ws.external.nodelay = true ## Examples: 8084, 127.0.0.1:8084, ::1:8084 listener.wss.external = 8084 -## Define the path you want to add to the end of the URL +## The path of WebSocket MQTT endpoint ## ## Value: / | / listener.wss.external.mqtt_path = /mqtt diff --git a/priv/emqx.schema b/priv/emqx.schema index cfd8c83a6..a8ed316c2 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1375,8 +1375,8 @@ end}. end, LisOpts = fun(Prefix) -> - Filter([{mqtt_path, cuttlefish:conf_get(Prefix ++ ".mqtt_path", Conf, undefined)}, - {acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, + Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, + {mqtt_path, cuttlefish:conf_get(Prefix ++ ".mqtt_path", Conf, undefined)}, {max_connections, cuttlefish:conf_get(Prefix ++ ".max_connections", Conf)}, {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 2d9bb4bba..d3ef43069 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -22,12 +22,6 @@ -export([restart_listener/1, restart_listener/3]). -export([stop_listener/1, stop_listener/3]). --ifdef(TEST). - --export([mqtt_path/1]). - --endif. - -type(listener() :: {esockd:proto(), esockd:listen_on(), [esockd:option()]}). %% @doc Start all listeners. @@ -74,12 +68,7 @@ start_http_listener(Start, Name, ListenOn, RanchOpts, Dispatch) -> Start(Name, with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}). mqtt_path(Options) -> - MQTTPath = proplists:get_value(mqtt_path, Options, "/mqtt"), - case erlang:list_to_bitstring(MQTTPath) of - <<"/">> -> MQTTPath; - <<"/", _/binary>> -> MQTTPath; - _ -> "/mqtt" - end. + proplists:get_value(mqtt_path, Options, "/mqtt"). ranch_opts(Options) -> NumAcceptors = proplists:get_value(acceptors, Options, 4), diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 054ec2a57..8301cf014 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -249,13 +249,6 @@ preprocess_properties(Packet = #mqtt_packet{ PState = #pstate{proto_ver = ?MQTT_PROTO_V5, topic_aliases = Aliases}) -> {Packet, PState#pstate{topic_aliases = maps:put(AliasId, Topic, Aliases)}}; -preprocess_properties(Packet = #mqtt_packet{variable = #mqtt_packet_publish{properties = #{'Message-Expiry-Interval' := _Interval}}}, - PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> - {Packet, PState}; -preprocess_properties(Packet = #mqtt_packet{variable = Publish = #mqtt_packet_publish{properties = Properties}}, - PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> - {Packet#mqtt_packet{variable = Publish#mqtt_packet_publish{properties = maps:put('Message-Expiry-Interval', 0, Properties)}}, PState}; - preprocess_properties(Packet, PState) -> {Packet, PState}. diff --git a/test/emqx_listeners_SUITE.erl b/test/emqx_listeners_SUITE.erl index 8d97a8158..17181aa31 100644 --- a/test/emqx_listeners_SUITE.erl +++ b/test/emqx_listeners_SUITE.erl @@ -26,8 +26,7 @@ all() -> [start_stop_listeners, - restart_listeners, - t_mqtt_path]. + restart_listeners]. init_per_suite(Config) -> NewConfig = generate_config(), @@ -50,11 +49,6 @@ restart_listeners(_) -> ok = emqx_listeners:restart(), ok = emqx_listeners:stop(). -t_mqtt_path(_) -> - ?assertEqual("/test", emqx_listeners:mqtt_path([{mqtt_path, "/test"}])), - ?assertEqual("/", emqx_listeners:mqtt_path([{mqtt_path, "/"}])), - ?assertEqual("/mqtt", emqx_listeners:mqtt_path([{mqtt_path, "test"}])). - generate_config() -> Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), Conf = conf_parse:file([local_path(["etc", "gen.emqx.conf"])]), From f5ed6ddb0588748fb13dd0d6787e086d98db2f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 14 Sep 2018 11:40:57 +0800 Subject: [PATCH 294/520] Change comments --- etc/emqx.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 4a7c195d2..2bba8d024 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1209,7 +1209,7 @@ listener.ws.external = 8083 ## The path of WebSocket MQTT endpoint ## -## Value: / | / +## Value: URL Path listener.ws.external.mqtt_path = /mqtt ## The acceptor pool for external MQTT/WebSocket listener. @@ -1353,7 +1353,7 @@ listener.wss.external = 8084 ## The path of WebSocket MQTT endpoint ## -## Value: / | / +## Value: URL Path listener.wss.external.mqtt_path = /mqtt ## The acceptor pool for external MQTT/WebSocket/SSL listener. From 737dcff44eb7fca9ec25d5f315d237d590e6215c Mon Sep 17 00:00:00 2001 From: Michal-Drobniak Date: Fri, 14 Sep 2018 11:46:52 +0200 Subject: [PATCH 295/520] Add space before plugins.etc_dir in emqx.conf --- etc/emqx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 2bba8d024..3604f2efd 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1839,7 +1839,7 @@ module.rewrite = off ## The etc dir for plugins' config. ## ## Value: Folder -plugins.etc_dir ={{ platform_etc_dir }}/plugins/ +plugins.etc_dir = {{ platform_etc_dir }}/plugins/ ## The file to store loaded plugin names. ## From 69e5869fa0b7f3aa56f2a0317961c5918733b12b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 16 Sep 2018 20:49:47 +0800 Subject: [PATCH 296/520] Add submit/2, async_submit/2 functions for emqx_pool module. --- src/emqx_pool.erl | 50 +++++++++++++++++++++++++++++++++------------- src/emqx_topic.erl | 4 ++-- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/emqx_pool.erl b/src/emqx_pool.erl index 276352797..52df7fec3 100644 --- a/src/emqx_pool.erl +++ b/src/emqx_pool.erl @@ -17,7 +17,8 @@ -behaviour(gen_server). -export([start_link/0, start_link/2]). --export([submit/1, async_submit/1]). +-export([submit/1, submit/2]). +-export([async_submit/1, async_submit/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, @@ -25,6 +26,8 @@ -define(POOL, ?MODULE). +-type(task() :: fun() | mfa() | {fun(), Args :: list(any())}). + %% @doc Start pooler supervisor. start_link() -> emqx_pool_sup:start_link(?POOL, random, {?MODULE, start_link, []}). @@ -34,16 +37,33 @@ start_link() -> start_link(Pool, Id) -> gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, [Pool, Id], []). -%% @doc Submit work to the pool --spec(submit(fun()) -> any()). -submit(Fun) -> - gen_server:call(worker(), {submit, Fun}, infinity). +%% @doc Submit work to the pool. +-spec(submit(task()) -> any()). +submit(Task) -> + call({submit, Task}). -%% @doc Submit work to the pool asynchronously --spec(async_submit(fun()) -> ok). -async_submit(Fun) -> - gen_server:cast(worker(), {async_submit, Fun}). +-spec(submit(fun(), list(any())) -> any()). +submit(Fun, Args) -> + call({submit, {Fun, Args}}). +%% @private +call(Req) -> + gen_server:call(worker(), Req, infinity). + +%% @doc Submit work to the pool asynchronously. +-spec(async_submit(task()) -> ok). +async_submit(Task) -> + cast({async_submit, Task}). + +-spec(async_submit(fun(), list(any())) -> ok). +async_submit(Fun, Args) -> + cast({async_submit, {Fun, Args}}). + +%% @private +cast(Msg) -> + gen_server:cast(worker(), Msg). + +%% @private worker() -> gproc_pool:pick_worker(pool). @@ -55,15 +75,15 @@ init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #{pool => Pool, id => Id}}. -handle_call({submit, Fun}, _From, State) -> - {reply, catch run(Fun), State}; +handle_call({submit, Task}, _From, State) -> + {reply, catch run(Task), State}; handle_call(Req, _From, State) -> emqx_logger:error("[Pool] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({async_submit, Fun}, State) -> - try run(Fun) +handle_cast({async_submit, Task}, State) -> + try run(Task) catch _:Error:Stacktrace -> emqx_logger:error("[Pool] error: ~p, ~p", [Error, Stacktrace]) end, @@ -78,7 +98,7 @@ handle_info(Info, State) -> {noreply, State}. terminate(_Reason, #{pool := Pool, id := Id}) -> - true = gproc_pool:disconnect_worker(Pool, {Pool, Id}). + gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -89,6 +109,8 @@ code_change(_OldVsn, State, _Extra) -> run({M, F, A}) -> erlang:apply(M, F, A); +run({F, A}) when is_function(F), is_list(A) -> + erlang:apply(F, A); run(Fun) when is_function(Fun) -> Fun(). diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index b3b417717..6540bbef2 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -188,8 +188,8 @@ parse(Topic = <<"$share/", Topic1/binary>>, Options) -> case binary:split(Topic1, <<"/">>) of [<<>>] -> error({invalid_topic, Topic}); [_] -> error({invalid_topic, Topic}); - [Group, Topic2] -> - case binary:match(Group, [<<"/">>, <<"+">>, <<"#">>]) of + [Group, Topic2] -> + case binary:match(Group, [<<"/">>, <<"+">>, <<"#">>]) of nomatch -> {Topic2, maps:put(share, Group, Options)}; _ -> error({invalid_topic, Topic}) end From 3822ff987bdcdcfd0a7d3b6d9e5149610e6ad9ae Mon Sep 17 00:00:00 2001 From: HuangDan Date: Tue, 18 Sep 2018 00:01:15 +0800 Subject: [PATCH 297/520] Fix function args Add test cases for emqx_pool module --- Makefile | 2 +- src/emqx_pool.erl | 2 +- test/emqx_pool_SUITE.erl | 65 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 test/emqx_pool_SUITE.erl diff --git a/Makefile b/Makefile index 73438ca01..54b13727a 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ CT_SUITES = emqx emqx_zone emqx_banned emqx_connection emqx_session emqx_access emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ emqx_mqtt_compat emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm \ - emqx_mountpoint emqx_listeners emqx_protocol + emqx_mountpoint emqx_listeners emqx_protocol emqx_pool CT_NODE_NAME = emqxct@127.0.0.1 CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME) diff --git a/src/emqx_pool.erl b/src/emqx_pool.erl index 52df7fec3..762f5dc6d 100644 --- a/src/emqx_pool.erl +++ b/src/emqx_pool.erl @@ -65,7 +65,7 @@ cast(Msg) -> %% @private worker() -> - gproc_pool:pick_worker(pool). + gproc_pool:pick_worker(?POOL). %%------------------------------------------------------------------------------ %% gen_server callbacks diff --git a/test/emqx_pool_SUITE.erl b/test/emqx_pool_SUITE.erl new file mode 100644 index 000000000..3d7d0f7e5 --- /dev/null +++ b/test/emqx_pool_SUITE.erl @@ -0,0 +1,65 @@ +%% Copyright (c) 2018 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_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} + ]. + +groups() -> + [ + {submit_case, [sequence], [submit_mfa, submit_fa]}, + {async_submit_case, [sequence], [async_submit_mfa]} + ]. + +init_per_suite(Config) -> + application:ensure_all_started(gproc), + Config. + +end_per_suite(_Config) -> + ok. + +submit_mfa(_Config) -> + erlang:process_flag(trap_exit, true), + {ok, Pid} = emqx_pool:start_link(), + Result = emqx_pool:submit({?MODULE, test_mfa, []}), + ?assertEqual(15, Result), + gen_server:stop(Pid, normal, 3000), + ok. + +submit_fa(_Config) -> + {ok, Pid} = emqx_pool:start_link(), + 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), + exit(Pid, normal). + +test_mfa() -> + lists:foldl(fun(X, Sum) -> X + Sum end, 0, [1,2,3,4,5]). + +async_submit_mfa(_Config) -> + {ok, Pid} = emqx_pool:start_link(), + emqx_pool:async_submit({?MODULE, test_mfa, []}), + exit(Pid, normal). + From e940c1c97079101d79fccd3ece539d3d011c9b53 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sun, 9 Sep 2018 16:30:40 +0200 Subject: [PATCH 298/520] Rewrite emqx_gc.erl The implementation prior to this commit supports only one gc enforcement policy which is message count threshold. The new implementation introduces 1 more: volume threshold based --- .gitignore | 2 + priv/emqx.schema | 14 ++++++ src/emqx_connection.erl | 34 ++++++++++----- src/emqx_gc.erl | 97 +++++++++++++++++++++++++++++++---------- src/emqx_session.erl | 35 ++++++++++----- test/emqx_gc_tests.erl | 53 ++++++++++++++++++++++ 6 files changed, 190 insertions(+), 45 deletions(-) create mode 100644 test/emqx_gc_tests.erl diff --git a/.gitignore b/.gitignore index 0927f0bbc..80e3bf42d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ bbmustache/ etc/gen.emqx.conf compile_commands.json cuttlefish +rebar.lock +xrefr diff --git a/priv/emqx.schema b/priv/emqx.schema index a8ed316c2..9fcd97376 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -824,6 +824,14 @@ end}. {datatype, {enum, [true, false]}} ]}. +%% @doc Force connection/session process GC after this number of +%% messages | bytes passed through. +%% Numbers delimited by `|'. Zero or negative is to disable. +{mapping, "zone.$name.force_gc_policy", "emqx.zones", [ + {default, "0|0"}, + {datatype, string} + ]}. + {translation, "emqx.zones", fun(Conf) -> Mapping = fun("retain_available", Val) -> {mqtt_retain_available, Val}; @@ -831,6 +839,10 @@ end}. {mqtt_wildcard_subscription, Val}; ("shared_subscription", Val) -> {mqtt_shared_subscription, Val}; + ("force_gc_policy", Val) -> + [Count, Bytes] = string:tokens(Val, "| "), + {force_gc_policy, #{count => list_to_integer(Count), + bytes => list_to_integer(Bytes)}}; (Opt, Val) -> {list_to_atom(Opt), Val} end, @@ -1750,3 +1762,5 @@ end}. {busy_port, cuttlefish:conf_get("sysmon.busy_port", Conf)}, {busy_dist_port, cuttlefish:conf_get("sysmon.busy_dist_port", Conf)}] end}. + + diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index a7f71d9aa..d9d67f08d 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -148,7 +148,10 @@ init([Transport, RawSocket, Options]) -> proto_state = ProtoState, parser_state = ParserState, enable_stats = EnableStats, - idle_timeout = IdleTimout}), + idle_timeout = IdleTimout + }), + GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), + ok = emqx_gc:init(GcPolicy), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> @@ -200,14 +203,18 @@ handle_cast(Msg, State) -> handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> case emqx_protocol:deliver(PubOrAck, ProtoState) of {ok, ProtoState1} -> - {noreply, maybe_gc(ensure_stats_timer(State#state{proto_state = ProtoState1}))}; + State1 = ensure_stats_timer(State#state{proto_state = ProtoState1}), + ok = maybe_gc(State1, PubOrAck), + {noreply, State1}; {error, Reason} -> shutdown(Reason, State) end; - handle_info({timeout, Timer, emit_stats}, - State = #state{stats_timer = Timer, proto_state = ProtoState}) -> + State = #state{stats_timer = Timer, + proto_state = ProtoState + }) -> emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), + ok = emqx_gc:reset(), {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info(timeout, State) -> @@ -295,9 +302,10 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ %% Receive and parse data -handle_packet(<<>>, State) -> - {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; - +handle_packet(<<>>, State0) -> + State = ensure_stats_timer(ensure_rate_limit(State0)), + ok = maybe_gc(State, incoming), + {noreply, State}; handle_packet(Data, State = #state{proto_state = ProtoState, parser_state = ParserState, idle_timeout = IdleTimeout}) -> @@ -381,7 +389,13 @@ shutdown(Reason, State) -> stop(Reason, State) -> {stop, Reason, State}. -maybe_gc(State) -> - %% TODO: gc and shutdown policy - State. +%% For incoming messages, bump gc-stats with packet count and totoal volume +%% For outgoing messages, only 'publish' type is taken into account. +maybe_gc(#state{incoming = #{bytes := Oct, packets := Cnt}}, incoming) -> + ok = emqx_gc:inc(Cnt, Oct); +maybe_gc(#state{}, {publish, _PacketId, #message{payload = Payload}}) -> + Oct = iolist_size(Payload), + ok = emqx_gc:inc(1, Oct); +maybe_gc(_, _) -> + ok. diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 5a32b43c5..65bbccad1 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -12,38 +12,87 @@ %% See the License for the specific language governing permissions and %% limitations under the License. -%% GC Utility functions. +%% @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). -%% Memory: (10, 100, 1000) -%% +-author("Feng Lee "). --export([conn_max_gc_count/0, reset_conn_gc_count/2, maybe_force_gc/2, - maybe_force_gc/3]). +-export([init/1, inc/2, reset/0]). --spec(conn_max_gc_count() -> integer()). -conn_max_gc_count() -> - case emqx_config:get_env(conn_force_gc_count) of - I when is_integer(I), I > 0 -> I + rand:uniform(I); - I when is_integer(I), I =< 0 -> undefined; - undefined -> undefined +-type st() :: #{ cnt => {integer(), integer()} + , oct => {integer(), integer()} + }. + +-define(disabled, disabled). +-define(ENABLED(X), (is_integer(X) andalso X > 0)). + +%% @doc Initialize force GC parameters. +-spec init(false | map()) -> ok. +init(#{count := Count, bytes := Bytes}) -> + Cnt = [{cnt, {Count, Count}} || ?ENABLED(Count)], + Oct = [{oct, {Bytes, Bytes}} || ?ENABLED(Bytes)], + erlang:put(?MODULE, maps:from_list(Cnt ++ Oct)), + ok; +init(_) -> erlang:put(?MODULE, #{}), ok. + +%% @doc Increase count and bytes stats in one call, +%% ensure gc is triggered at most once, even if both thresholds are hit. +-spec inc(pos_integer(), pos_integer()) -> ok. +inc(Cnt, Oct) -> + mutate_pd_with(fun(St) -> inc(St, Cnt, Oct) end). + +%% @doc Reset counters to zero. +-spec reset() -> ok. +reset() -> + mutate_pd_with(fun(St) -> reset(St) end). + +%% ======== Internals ======== + +%% mutate gc stats numbers in process dict with the given function +mutate_pd_with(F) -> + St = F(erlang:get(?MODULE)), + erlang:put(?MODULE, St), + ok. + +%% Increase count and bytes stats in one call, +%% ensure gc is triggered at most once, even if both thresholds are hit. +-spec inc(st(), pos_integer(), pos_integer()) -> st(). +inc(St0, Cnt, Oct) -> + case do_inc(St0, cnt, Cnt) of + {true, St} -> + St; + {false, St1} -> + {_, St} = do_inc(St1, oct, Oct), + St end. --spec(reset_conn_gc_count(pos_integer(), tuple()) -> tuple()). -reset_conn_gc_count(Pos, State) -> - case element(Pos, State) of - undefined -> State; - _I -> setelement(Pos, State, conn_max_gc_count()) +%% Reset counters to zero. +reset(St) -> reset(cnt, reset(oct, St)). + +-spec do_inc(st(), cnt | oct, pos_integer()) -> {boolean(), st()}. +do_inc(St, Key, Num) -> + case maps:get(Key, St, ?disabled) of + ?disabled -> + {false, St}; + {Init, Remain} when Remain > Num -> + {false, maps:put(Key, {Init, Remain - Num}, St)}; + _ -> + {true, do_gc(St)} end. -maybe_force_gc(Pos, State) -> - maybe_force_gc(Pos, State, fun() -> ok end). -maybe_force_gc(Pos, State, Cb) -> - case element(Pos, State) of - undefined -> State; - I when I =< 0 -> Cb(), garbage_collect(), - reset_conn_gc_count(Pos, State); - I -> setelement(Pos, State, I - 1) +do_gc(St) -> + erlang:garbage_collect(), + reset(St). + +reset(Key, St) -> + case maps:get(Key, St, ?disabled) of + ?disabled -> St; + {Init, _} -> maps:put(Key, {Init, Init}, St) end. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 118ab6e21..54dcfb1fe 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -350,10 +350,13 @@ init([Parent, #{zone := Zone, enable_stats = get_env(Zone, enable_stats, true), deliver_stats = 0, enqueue_stats = 0, - created_at = os:timestamp()}, + created_at = os:timestamp() + }, emqx_sm:register_session(ClientId, attrs(State)), emqx_sm:set_session_stats(ClientId, stats(State)), emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), + GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), + ok = emqx_gc:init(GcPolicy), ok = proc_lib:init_ack(Parent, {ok, self()}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State). @@ -567,8 +570,11 @@ handle_info({timeout, Timer, retry_delivery}, State = #state{retry_timer = Timer handle_info({timeout, Timer, check_awaiting_rel}, State = #state{await_rel_timer = Timer}) -> noreply(expire_awaiting_rel(State#state{await_rel_timer = undefined})); -handle_info({timeout, Timer, emit_stats}, State = #state{client_id = ClientId, stats_timer = Timer}) -> +handle_info({timeout, Timer, emit_stats}, + State = #state{client_id = ClientId, + stats_timer = Timer}) -> _ = emqx_sm:set_session_stats(ClientId, stats(State)), + ok = emqx_gc:reset(), %% going to hibernate, reset gc stats {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info({timeout, Timer, expired}, State = #state{expiry_timer = Timer}) -> @@ -744,21 +750,22 @@ dispatch(Msg, State = #state{client_id = ClientId, conn_pid = undefined}) -> end; %% Deliver qos0 message directly to client -dispatch(Msg = #message{qos = ?QOS0}, State) -> +dispatch(Msg = #message{qos = ?QOS0} = Msg, State) -> deliver(undefined, Msg, State), - inc_stats(deliver, State); + inc_stats(deliver, Msg, State); -dispatch(Msg = #message{qos = QoS}, State = #state{next_pkt_id = PacketId, inflight = Inflight}) +dispatch(Msg = #message{qos = QoS} = Msg, + State = #state{next_pkt_id = PacketId, inflight = Inflight}) when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 -> case emqx_inflight:is_full(Inflight) of true -> enqueue_msg(Msg, State); false -> deliver(PacketId, Msg, State), - await(PacketId, Msg, inc_stats(deliver, next_pkt_id(State))) + await(PacketId, Msg, inc_stats(deliver, Msg, next_pkt_id(State))) end. enqueue_msg(Msg, State = #state{mqueue = Q}) -> - inc_stats(enqueue, State#state{mqueue = emqx_mqueue:in(Msg, Q)}). + inc_stats(enqueue, Msg, State#state{mqueue = emqx_mqueue:in(Msg, Q)}). %%------------------------------------------------------------------------------ %% Deliver @@ -882,11 +889,19 @@ next_pkt_id(State = #state{next_pkt_id = Id}) -> %%------------------------------------------------------------------------------ %% Inc stats -inc_stats(deliver, State = #state{deliver_stats = I}) -> +inc_stats(deliver, Msg, State = #state{deliver_stats = I}) -> + MsgSize = msg_size(Msg), + ok = emqx_gc:inc(1, MsgSize), State#state{deliver_stats = I + 1}; -inc_stats(enqueue, State = #state{enqueue_stats = I}) -> +inc_stats(enqueue, _Msg, State = #state{enqueue_stats = I}) -> State#state{enqueue_stats = I + 1}. +%% 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). + %%------------------------------------------------------------------------------ %% Helper functions @@ -902,5 +917,3 @@ noreply(State) -> shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. -%% TODO: GC Policy and Shutdown Policy -%% maybe_gc(State) -> State. diff --git a/test/emqx_gc_tests.erl b/test/emqx_gc_tests.erl new file mode 100644 index 000000000..ffcac91d1 --- /dev/null +++ b/test/emqx_gc_tests.erl @@ -0,0 +1,53 @@ +%% Copyright (c) 2018 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_gc_tests). + +-include_lib("eunit/include/eunit.hrl"). + +trigger_by_cnt_test() -> + Args = #{count => 2, bytes => 0}, + ok = emqx_gc:init(Args), + ok = emqx_gc:inc(1, 1000), + St1 = inspect(), + ?assertMatch({_, Remain} when Remain > 0, maps:get(cnt, St1)), + ok = emqx_gc:inc(2, 2), + St2 = inspect(), + ok = emqx_gc:inc(0, 2000), + St3 = inspect(), + ?assertEqual(St2, St3), + ?assertMatch({N, N}, maps:get(cnt, St2)), + ?assertNot(maps:is_key(oct, St2)), + ok. + +trigger_by_oct_test() -> + Args = #{count => 2, bytes => 2}, + ok = emqx_gc:init(Args), + ok = emqx_gc:inc(1, 1), + St1 = inspect(), + ?assertMatch({_, Remain} when Remain > 0, maps:get(oct, St1)), + ok = emqx_gc:inc(2, 2), + St2 = inspect(), + ?assertMatch({N, N}, maps:get(oct, St2)), + ?assertMatch({M, M}, maps:get(cnt, St2)), + ok. + +disabled_test() -> + Args = #{count => -1, bytes => false}, + ok = emqx_gc:init(Args), + ok = emqx_gc:inc(1, 1), + ?assertEqual(#{}, inspect()), + ok. + +inspect() -> erlang:get(emqx_gc). From 81d575c3c1863c5d878b3e1a203493ca7c6885e2 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Tue, 18 Sep 2018 21:44:25 +0200 Subject: [PATCH 299/520] Add new shutdown-policy config schemas --- priv/emqx.schema | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/priv/emqx.schema b/priv/emqx.schema index 9fcd97376..40ace9f93 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1764,3 +1764,18 @@ end}. end}. +%% @doc Max message queue length for connection/session process. +%% NOTE: Message queue here is the Erlang process mailbox, but not +%% the number of MQTT queued messages. +{mapping, "mqtt.conn.max_msg_queue_len", "emqx.conn_max_msg_queue_len", [ + {default, 100000}, + {datatype, integer} +]}. + +%% @doc Max total heap size to kill connection/session process. +%% set to 0 or negative number to have it disabled. +{mapping, "mqtt.conn.max_total_heap_size", "emqx.conn_max_total_heap_size", [ + {default, -1}, %% disabled by default + {datatype, integer} +]}. + From 25b61afe0dbc81e3f4057b48e8b22001de528f66 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Tue, 18 Sep 2018 22:53:37 +0200 Subject: [PATCH 300/520] Add connection/session shutdown policy The hibernation behaviour is also changed (implicitly) in this commit: Prior to this change, connection/session always hibernates after the stats timer expires regardless of messages in mailbox. After this commit, connection/session process only goes to hibernate when the timer expires AND there is nothing left in the mailbox to process --- priv/emqx.schema | 1 + src/emqx_connection.erl | 14 ++++++++--- src/emqx_misc.erl | 41 +++++++++++++++++++++++++++++- src/emqx_session.erl | 14 ++++++++--- test/emqx_misc_tests.erl | 54 ++++++++++++++++++++++++++++++++++++++++ test/emqx_test_lib.erl | 37 +++++++++++++++++++++++++++ 6 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 test/emqx_misc_tests.erl create mode 100644 test/emqx_test_lib.erl diff --git a/priv/emqx.schema b/priv/emqx.schema index 40ace9f93..99e489fd8 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1767,6 +1767,7 @@ end}. %% @doc Max message queue length for connection/session process. %% NOTE: Message queue here is the Erlang process mailbox, but not %% the number of MQTT queued messages. +%% Set to 0 or negative to have it disabled {mapping, "mqtt.conn.max_msg_queue_len", "emqx.conn_max_msg_queue_len", [ {default, 100000}, {datatype, integer} diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index d9d67f08d..63a344944 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -214,9 +214,17 @@ handle_info({timeout, Timer, emit_stats}, proto_state = ProtoState }) -> emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), - ok = emqx_gc:reset(), - {noreply, State#state{stats_timer = undefined}, hibernate}; - + NewState = State#state{stats_timer = undefined}, + case meqx_misc:conn_proc_mng_policy() of + continue -> + {noreply, NewState}; + hibernate -> + ok = emqx_gc:reset(), + {noreply, NewState, hibernate}; + {shutdown, Reason} -> + ?LOG(warning, "shutdown due to ~p", [Reason], NewState), + shutdown(Reason, NewState) + end; handle_info(timeout, State) -> shutdown(idle_timeout, State); diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index e2b60a156..c3cec7427 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -15,7 +15,7 @@ -module(emqx_misc). -export([merge_opts/2, start_timer/2, start_timer/3, cancel_timer/1, - proc_name/2, proc_stats/0, proc_stats/1]). + proc_name/2, proc_stats/0, proc_stats/1, conn_proc_mng_policy/0]). %% @doc Merge options -spec(merge_opts(list(), list()) -> list()). @@ -59,3 +59,42 @@ proc_stats(Pid) -> {value, {_, V}, Stats1} = lists:keytake(message_queue_len, 1, Stats), [{mailbox_len, V} | Stats1]. +-define(DISABLED, 0). + +%% @doc Check self() process status against connection/session process management policy, +%% return `continue | hibernate | {shutdown, Reason}' accordingly. +%% `continue': There is nothing out of the ordinary +%% `hibernate': Nothing to process in my mailbox (and since this check is triggered +%% by a timer, we assume it is a fat chance to continue idel, hence hibernate. +%% `shutdown': Some numbers (message queue length or heap size have hit the limit, +%% hence shutdown for greater good (system stability). +-spec(conn_proc_mng_policy() -> continue | hibernate | {shutdown, _}). +conn_proc_mng_policy() -> + MaxMsgQueueLen = application:get_env(?APPLICATION, conn_max_msg_queue_len, ?DISABLED), + Qlength = proc_info(message_queue_len), + Checks = + [{fun() -> is_enabled(MaxMsgQueueLen) andalso Qlength > MaxMsgQueueLen end, + {shutdown, message_queue_too_long}}, + {fun() -> is_heap_size_too_large() end, + {shutdown, total_heap_size_too_large}}, + {fun() -> Qlength > 0 end, continue}, + {fun() -> true end, hibernate} + ], + check(Checks). + +check([{Pred, Result} | Rest]) -> + case Pred() of + true -> Result; + false -> check(Rest) + end. + +is_heap_size_too_large() -> + MaxTotalHeapSize = application:get_env(?APPLICATION, conn_max_total_heap_size, ?DISABLED), + is_enabled(MaxTotalHeapSize) andalso proc_info(total_heap_size) > MaxTotalHeapSize. + +is_enabled(Max) -> Max > ?DISABLED. + +proc_info(Key) -> + {Key, Value} = erlang:process_info(self(), Key), + Value. + diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 54dcfb1fe..64ed698bd 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -574,9 +574,17 @@ handle_info({timeout, Timer, emit_stats}, State = #state{client_id = ClientId, stats_timer = Timer}) -> _ = emqx_sm:set_session_stats(ClientId, stats(State)), - ok = emqx_gc:reset(), %% going to hibernate, reset gc stats - {noreply, State#state{stats_timer = undefined}, hibernate}; - + NewState = State#state{stats_timer = undefined}, + case emqx_misc:conn_proc_mng_policy() of + continue -> + {noreply, NewState}; + hibernate -> + ok = emqx_gc:reset(), %% going to hibernate, reset gc stats + {noreply, NewState, hibernate}; + {shutdown, Reason} -> + ?LOG(warning, "shutdown due to ~p", [Reason], NewState), + shutdown(Reason, NewState) + end; handle_info({timeout, Timer, expired}, State = #state{expiry_timer = Timer}) -> ?LOG(info, "expired, shutdown now:(", [], State), shutdown(expired, State); diff --git a/test/emqx_misc_tests.erl b/test/emqx_misc_tests.erl new file mode 100644 index 000000000..775887439 --- /dev/null +++ b/test/emqx_misc_tests.erl @@ -0,0 +1,54 @@ +%% Copyright (c) 2018 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_misc_tests). +-include_lib("eunit/include/eunit.hrl"). + +shutdown_disabled_test() -> + with_env( + [{conn_max_msg_queue_len, 0}, + {conn_max_total_heap_size, 0}], + fun() -> + self() ! foo, + ?assertEqual(continue, conn_proc_mng_policy()), + receive foo -> ok end, + ?assertEqual(hibernate, conn_proc_mng_policy()) + end). + +message_queue_too_long_test() -> + with_env( + [{conn_max_msg_queue_len, 1}, + {conn_max_total_heap_size, 0}], + fun() -> + self() ! foo, + self() ! bar, + ?assertEqual({shutdown, message_queue_too_long}, + conn_proc_mng_policy()), + receive foo -> ok end, + ?assertEqual(continue, conn_proc_mng_policy()), + receive bar -> ok end + end). + +total_heap_size_too_large_test() -> + with_env( + [{conn_max_msg_queue_len, 0}, + {conn_max_total_heap_size, 1}], + fun() -> + ?assertEqual({shutdown, total_heap_size_too_large}, + conn_proc_mng_policy()) + end). + +with_env(Envs, F) -> emqx_test_lib:with_env(Envs, F). + +conn_proc_mng_policy() -> emqx_misc:conn_proc_mng_policy(). diff --git a/test/emqx_test_lib.erl b/test/emqx_test_lib.erl new file mode 100644 index 000000000..dfb98d598 --- /dev/null +++ b/test/emqx_test_lib.erl @@ -0,0 +1,37 @@ +%% Copyright (c) 2018 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_test_lib). + +-export([ with_env/2 + ]). + +with_env([], F) -> F(); +with_env([{Key, Val} | Rest], F) -> + Origin = get_env(Key), + try + ok = set_env(Key, Val), + with_env(Rest, F) + after + case Origin of + undefined -> ok = unset_env(Key); + _ -> ok = set_env(Key, Origin) + end + end. + +get_env(Key) -> application:get_env(?APPLICATION, Key). +set_env(Key, Val) -> application:set_env(?APPLICATION, Key, Val). +unset_env(Key) -> application:unset_env(?APPLICATION, Key). + + From f75a6241977e761f96e48a6d7fb0200f7e3cd1f4 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Tue, 18 Sep 2018 23:20:40 +0200 Subject: [PATCH 301/520] Add a test case to cover timeout message flush in emqx_misc --- src/emqx_misc.erl | 9 ++++----- test/emqx_misc_tests.erl | 7 +++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index c3cec7427..d9e96b9c9 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -36,14 +36,13 @@ start_timer(Interval, Dest, Msg) -> erlang:start_timer(Interval, Dest, Msg). -spec(cancel_timer(undefined | reference()) -> ok). -cancel_timer(undefined) -> - ok; -cancel_timer(Timer) -> - case catch erlang:cancel_timer(Timer) of +cancel_timer(Timer) when is_reference(Timer) -> + case erlang:cancel_timer(Timer) of false -> receive {timeout, Timer, _} -> ok after 0 -> ok end; _ -> ok - end. + end; +cancel_timer(_) -> ok. -spec(proc_name(atom(), pos_integer()) -> atom()). proc_name(Mod, Id) -> diff --git a/test/emqx_misc_tests.erl b/test/emqx_misc_tests.erl index 775887439..a4abac683 100644 --- a/test/emqx_misc_tests.erl +++ b/test/emqx_misc_tests.erl @@ -15,6 +15,13 @@ -module(emqx_misc_tests). -include_lib("eunit/include/eunit.hrl"). +timer_cancel_flush_test() -> + Timer = emqx_misc:start_timer(0, foo), + ok = emqx_misc:cancel_timer(Timer), + receive {timeout, Timer, foo} -> error(unexpected) + after 0 -> ok + end. + shutdown_disabled_test() -> with_env( [{conn_max_msg_queue_len, 0}, From f58165db73a36d93cbd967370e8e4e5aa8d74061 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Wed, 19 Sep 2018 21:39:26 +0200 Subject: [PATCH 302/520] Move shutdown policy config to zone configs --- priv/emqx.schema | 34 +++++++++++++-------------- src/emqx_connection.erl | 4 +++- src/emqx_misc.erl | 36 +++++++++++++++++----------- src/emqx_session.erl | 4 +++- test/emqx_misc_tests.erl | 47 +++++++++++++------------------------ test/emqx_session_SUITE.erl | 2 +- test/emqx_test_lib.erl | 37 ----------------------------- 7 files changed, 61 insertions(+), 103 deletions(-) delete mode 100644 test/emqx_test_lib.erl diff --git a/priv/emqx.schema b/priv/emqx.schema index 99e489fd8..ed545b7ec 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -828,10 +828,21 @@ end}. %% messages | bytes passed through. %% Numbers delimited by `|'. Zero or negative is to disable. {mapping, "zone.$name.force_gc_policy", "emqx.zones", [ - {default, "0|0"}, + {default, "0 | 0"}, {datatype, string} ]}. +%% @doc Max message queue length and total heap size to force shutdown +%% connection/session process. +%% Message queue here is the Erlang process mailbox, but not the number +%% of queued MQTT messages of QoS 1 and 2. +%% Total heap size is the in Erlang 'words' not in 'bytes'. +%% Zero or negative is to disable. +{mapping, "zone.$name.force_shutdown_policy", "emqx.zones", [ + {default, "0 | 0"}, + {datatype, string} +]}. + {translation, "emqx.zones", fun(Conf) -> Mapping = fun("retain_available", Val) -> {mqtt_retain_available, Val}; @@ -843,6 +854,10 @@ end}. [Count, Bytes] = string:tokens(Val, "| "), {force_gc_policy, #{count => list_to_integer(Count), bytes => list_to_integer(Bytes)}}; + ("force_shutdown_policy", Val) -> + [Len, Siz] = string:tokens(Val, "| "), + {force_shutdown_policy, #{message_queue_len => list_to_integer(Len), + total_heap_size => list_to_integer(Siz)}}; (Opt, Val) -> {list_to_atom(Opt), Val} end, @@ -1763,20 +1778,3 @@ end}. {busy_dist_port, cuttlefish:conf_get("sysmon.busy_dist_port", Conf)}] end}. - -%% @doc Max message queue length for connection/session process. -%% NOTE: Message queue here is the Erlang process mailbox, but not -%% the number of MQTT queued messages. -%% Set to 0 or negative to have it disabled -{mapping, "mqtt.conn.max_msg_queue_len", "emqx.conn_max_msg_queue_len", [ - {default, 100000}, - {datatype, integer} -]}. - -%% @doc Max total heap size to kill connection/session process. -%% set to 0 or negative number to have it disabled. -{mapping, "mqtt.conn.max_total_heap_size", "emqx.conn_max_total_heap_size", [ - {default, -1}, %% disabled by default - {datatype, integer} -]}. - diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 63a344944..376fe5a9a 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -152,6 +152,7 @@ init([Transport, RawSocket, Options]) -> }), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), ok = emqx_gc:init(GcPolicy), + erlang:put(force_shutdown_policy, emqx_zone:get_env(Zone, force_shutdown_policy)), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> @@ -215,7 +216,8 @@ handle_info({timeout, Timer, emit_stats}, }) -> emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), NewState = State#state{stats_timer = undefined}, - case meqx_misc:conn_proc_mng_policy() of + Limits = erlang:get(force_shutdown_policy), + case meqx_misc:conn_proc_mng_policy(Limits) of continue -> {noreply, NewState}; hibernate -> diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index d9e96b9c9..656b0fca3 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -15,7 +15,7 @@ -module(emqx_misc). -export([merge_opts/2, start_timer/2, start_timer/3, cancel_timer/1, - proc_name/2, proc_stats/0, proc_stats/1, conn_proc_mng_policy/0]). + proc_name/2, proc_stats/0, proc_stats/1, conn_proc_mng_policy/1]). %% @doc Merge options -spec(merge_opts(list(), list()) -> list()). @@ -62,24 +62,30 @@ proc_stats(Pid) -> %% @doc Check self() process status against connection/session process management policy, %% return `continue | hibernate | {shutdown, Reason}' accordingly. -%% `continue': There is nothing out of the ordinary -%% `hibernate': Nothing to process in my mailbox (and since this check is triggered +%% `continue': There is nothing out of the ordinary. +%% `hibernate': Nothing to process in my mailbox, and since this check is triggered %% by a timer, we assume it is a fat chance to continue idel, hence hibernate. -%% `shutdown': Some numbers (message queue length or heap size have hit the limit, +%% `shutdown': Some numbers (message queue length or heap size have hit the limit), %% hence shutdown for greater good (system stability). --spec(conn_proc_mng_policy() -> continue | hibernate | {shutdown, _}). -conn_proc_mng_policy() -> - MaxMsgQueueLen = application:get_env(?APPLICATION, conn_max_msg_queue_len, ?DISABLED), +-spec(conn_proc_mng_policy(#{message_queue_len := integer(), + total_heap_size := integer() + } | undefined) -> continue | hibernate | {shutdown, _}). +conn_proc_mng_policy(#{message_queue_len := MaxMsgQueueLen, + total_heap_size := MaxTotalHeapSize + }) -> Qlength = proc_info(message_queue_len), Checks = - [{fun() -> is_enabled(MaxMsgQueueLen) andalso Qlength > MaxMsgQueueLen end, + [{fun() -> is_message_queue_too_long(Qlength, MaxMsgQueueLen) end, {shutdown, message_queue_too_long}}, - {fun() -> is_heap_size_too_large() end, + {fun() -> is_heap_size_too_large(MaxTotalHeapSize) end, {shutdown, total_heap_size_too_large}}, {fun() -> Qlength > 0 end, continue}, {fun() -> true end, hibernate} ], - check(Checks). + check(Checks); +conn_proc_mng_policy(_) -> + %% disable by default + conn_proc_mng_policy(#{message_queue_len => 0, total_heap_size => 0}). check([{Pred, Result} | Rest]) -> case Pred() of @@ -87,11 +93,13 @@ check([{Pred, Result} | Rest]) -> false -> check(Rest) end. -is_heap_size_too_large() -> - MaxTotalHeapSize = application:get_env(?APPLICATION, conn_max_total_heap_size, ?DISABLED), - is_enabled(MaxTotalHeapSize) andalso proc_info(total_heap_size) > MaxTotalHeapSize. +is_message_queue_too_long(Qlength, Max) -> + is_enabled(Max) andalso Qlength > Max. -is_enabled(Max) -> Max > ?DISABLED. +is_heap_size_too_large(Max) -> + is_enabled(Max) andalso proc_info(total_heap_size) > Max. + +is_enabled(Max) -> is_integer(Max) andalso Max > ?DISABLED. proc_info(Key) -> {Key, Value} = erlang:process_info(self(), Key), diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 64ed698bd..4d52afefc 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -357,6 +357,7 @@ init([Parent, #{zone := Zone, emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), ok = emqx_gc:init(GcPolicy), + erlang:put(force_shutdown_policy, emqx_zone:get_env(Zone, force_shutdown_policy)), ok = proc_lib:init_ack(Parent, {ok, self()}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State). @@ -575,7 +576,8 @@ handle_info({timeout, Timer, emit_stats}, stats_timer = Timer}) -> _ = emqx_sm:set_session_stats(ClientId, stats(State)), NewState = State#state{stats_timer = undefined}, - case emqx_misc:conn_proc_mng_policy() of + Limits = erlang:get(force_shutdown_policy), + case emqx_misc:conn_proc_mng_policy(Limits) of continue -> {noreply, NewState}; hibernate -> diff --git a/test/emqx_misc_tests.erl b/test/emqx_misc_tests.erl index a4abac683..40a9aae87 100644 --- a/test/emqx_misc_tests.erl +++ b/test/emqx_misc_tests.erl @@ -23,39 +23,24 @@ timer_cancel_flush_test() -> end. shutdown_disabled_test() -> - with_env( - [{conn_max_msg_queue_len, 0}, - {conn_max_total_heap_size, 0}], - fun() -> - self() ! foo, - ?assertEqual(continue, conn_proc_mng_policy()), - receive foo -> ok end, - ?assertEqual(hibernate, conn_proc_mng_policy()) - end). + self() ! foo, + ?assertEqual(continue, conn_proc_mng_policy(0, 0)), + receive foo -> ok end, + ?assertEqual(hibernate, conn_proc_mng_policy(0, 0)). message_queue_too_long_test() -> - with_env( - [{conn_max_msg_queue_len, 1}, - {conn_max_total_heap_size, 0}], - fun() -> - self() ! foo, - self() ! bar, - ?assertEqual({shutdown, message_queue_too_long}, - conn_proc_mng_policy()), - receive foo -> ok end, - ?assertEqual(continue, conn_proc_mng_policy()), - receive bar -> ok end - end). + self() ! foo, + self() ! bar, + ?assertEqual({shutdown, message_queue_too_long}, + conn_proc_mng_policy(1, 0)), + receive foo -> ok end, + ?assertEqual(continue, conn_proc_mng_policy(1, 0)), + receive bar -> ok end. total_heap_size_too_large_test() -> - with_env( - [{conn_max_msg_queue_len, 0}, - {conn_max_total_heap_size, 1}], - fun() -> - ?assertEqual({shutdown, total_heap_size_too_large}, - conn_proc_mng_policy()) - end). + ?assertEqual({shutdown, total_heap_size_too_large}, + conn_proc_mng_policy(0, 1)). -with_env(Envs, F) -> emqx_test_lib:with_env(Envs, F). - -conn_proc_mng_policy() -> emqx_misc:conn_proc_mng_policy(). +conn_proc_mng_policy(L, S) -> + emqx_misc:conn_proc_mng_policy(#{message_queue_len => L, + total_heap_size => S}). diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index 29a6edc61..2b60b747d 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -25,7 +25,7 @@ all() -> [t_session_all]. init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), Config. - + end_per_suite(_Config) -> emqx_ct_broker_helpers:run_teardown_steps(). diff --git a/test/emqx_test_lib.erl b/test/emqx_test_lib.erl deleted file mode 100644 index dfb98d598..000000000 --- a/test/emqx_test_lib.erl +++ /dev/null @@ -1,37 +0,0 @@ -%% Copyright (c) 2018 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_test_lib). - --export([ with_env/2 - ]). - -with_env([], F) -> F(); -with_env([{Key, Val} | Rest], F) -> - Origin = get_env(Key), - try - ok = set_env(Key, Val), - with_env(Rest, F) - after - case Origin of - undefined -> ok = unset_env(Key); - _ -> ok = set_env(Key, Origin) - end - end. - -get_env(Key) -> application:get_env(?APPLICATION, Key). -set_env(Key, Val) -> application:set_env(?APPLICATION, Key, Val). -unset_env(Key) -> application:unset_env(?APPLICATION, Key). - - From 073bf481c9648b0e54d5ee56257f8705b36532c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 20 Sep 2018 15:51:28 +0800 Subject: [PATCH 303/520] Calculate the 1.5 keep alive time exactly --- etc/emqx.conf | 4 +-- priv/emqx.schema | 2 +- src/emqx_connection.erl | 21 ++++++-------- src/emqx_keepalive.erl | 56 ++++++++++++++------------------------ src/emqx_protocol.erl | 2 +- src/emqx_ws_connection.erl | 18 ++++++------ 6 files changed, 41 insertions(+), 62 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 3604f2efd..ea90b8d28 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -591,10 +591,10 @@ zone.external.enable_stats = on ## zone.external.server_keepalive = 0 ## The backoff for MQTT keepalive timeout. The broker will kick a connection out -## until 'Keepalive * backoff * 2' timeout. +## until 'Keepalive * backoff' timeout. ## ## Value: Float > 0.5 -zone.external.keepalive_backoff = 0.75 +zone.external.keepalive_backoff = 1.5 ## Maximum number of subscriptions allowed, 0 means no limit. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index a8ed316c2..77167b440 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -748,7 +748,7 @@ end}. %% @doc Keepalive backoff {mapping, "zone.$name.keepalive_backoff", "emqx.zones", [ - {default, 0.75}, + {default, 1.5}, {datatype, float} ]}. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index a7f71d9aa..139011891 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -45,7 +45,8 @@ rate_limit, publish_limit, limit_timer, - idle_timeout + idle_timeout, + last_packet_ts = 0 }). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). @@ -243,23 +244,17 @@ handle_info({inet_reply, _Sock, ok}, State) -> handle_info({inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); -handle_info({keepalive, start, Interval}, State = #state{transport = Transport, socket = Socket}) -> +handle_info({keepalive, start, Interval}, State) -> ?LOG(debug, "Keepalive at the interval of ~p", [Interval], State), - 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 + case emqx_keepalive:start(Interval, {keepalive, check}) of {ok, KeepAlive} -> {noreply, State#state{keepalive = KeepAlive}}; {error, Error} -> shutdown(Error, State) end; -handle_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> - case emqx_keepalive:check(KeepAlive) of +handle_info({keepalive, check}, State = #state{keepalive = KeepAlive, last_packet_ts = LastPacketTs}) -> + case emqx_keepalive:check(KeepAlive, LastPacketTs) of {ok, KeepAlive1} -> {noreply, State#state{keepalive = KeepAlive1}}; {error, timeout} -> @@ -296,14 +291,14 @@ code_change(_OldVsn, State, _Extra) -> %% Receive and parse data handle_packet(<<>>, State) -> - {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; + {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State#state{last_packet_ts = erlang:system_time(millisecond)})))}; handle_packet(Data, State = #state{proto_state = ProtoState, parser_state = ParserState, idle_timeout = IdleTimeout}) -> case catch emqx_frame:parse(Data, ParserState) of {more, NewParserState} -> - {noreply, run_socket(State#state{parser_state = NewParserState}), IdleTimeout}; + {noreply, State#state{parser_state = NewParserState, last_packet_ts = erlang:system_time(millisecond)}, IdleTimeout}; {ok, Packet = ?PACKET(Type), Rest} -> emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of diff --git a/src/emqx_keepalive.erl b/src/emqx_keepalive.erl index 25740b099..59dbe73b9 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -14,51 +14,37 @@ -module(emqx_keepalive). --export([start/3, check/1, cancel/1]). +-export([start/2, check/2, cancel/1]). --record(keepalive, {statfun, statval, tsec, tmsg, tref, repeat = 0}). +-record(keepalive, {tmsec, tmsg, tref}). -type(keepalive() :: #keepalive{}). -export_type([keepalive/0]). +-define(SWEET_SPOT, 50). % 50ms + %% @doc Start a keepalive --spec(start(fun(), integer(), any()) -> {ok, keepalive()} | {error, term()}). -start(_, 0, _) -> +-spec(start(integer(), any()) -> {ok, keepalive()}). +start(0, _) -> {ok, #keepalive{}}; -start(StatFun, TimeoutSec, TimeoutMsg) -> - case catch StatFun() of - {ok, StatVal} -> - {ok, #keepalive{statfun = StatFun, statval = StatVal, - tsec = TimeoutSec, tmsg = TimeoutMsg, - tref = timer(TimeoutSec, TimeoutMsg)}}; - {error, Error} -> - {error, Error}; - {'EXIT', Reason} -> - {error, Reason} - end. +start(TimeoutSec, TimeoutMsg) -> + {ok, #keepalive{tmsec = TimeoutSec * 1000, tmsg = TimeoutMsg, tref = timer(TimeoutSec * 1000 + ?SWEET_SPOT, TimeoutMsg)}}. %% @doc Check keepalive, called when timeout... --spec(check(keepalive()) -> {ok, keepalive()} | {error, term()}). -check(KeepAlive = #keepalive{statfun = StatFun, statval = LastVal, repeat = Repeat}) -> - case catch StatFun() of - {ok, NewVal} -> - if NewVal =/= LastVal -> - {ok, resume(KeepAlive#keepalive{statval = NewVal, repeat = 0})}; - Repeat < 1 -> - {ok, resume(KeepAlive#keepalive{statval = NewVal, repeat = Repeat + 1})}; - true -> - {error, timeout} - end; - {error, Error} -> - {error, Error}; - {'EXIT', Reason} -> - {error, Reason} +-spec(check(keepalive(), integer()) -> {ok, keepalive()} | {error, term()}). +check(KeepAlive = #keepalive{tmsec = TimeoutMs}, LastPacketTs) -> + TimeDiff = erlang:system_time(millisecond) - LastPacketTs, + case TimeDiff >= TimeoutMs of + true -> + {error, timeout}; + false -> + {ok, resume(KeepAlive, TimeoutMs + ?SWEET_SPOT - TimeDiff)} end. --spec(resume(keepalive()) -> keepalive()). -resume(KeepAlive = #keepalive{tsec = TimeoutSec, tmsg = TimeoutMsg}) -> - KeepAlive#keepalive{tref = timer(TimeoutSec, TimeoutMsg)}. +-spec(resume(keepalive(), integer()) -> keepalive()). +resume(KeepAlive = #keepalive{tmsg = TimeoutMsg}, TimeoutMs) -> + KeepAlive#keepalive{tref = timer(TimeoutMs, TimeoutMsg)}. %% @doc Cancel Keepalive -spec(cancel(keepalive()) -> ok). @@ -67,6 +53,6 @@ cancel(#keepalive{tref = TRef}) when is_reference(TRef) -> cancel(_) -> ok. -timer(Secs, Msg) -> - erlang:send_after(timer:seconds(Secs), self(), Msg). +timer(Millisecond, Msg) -> + erlang:send_after(Millisecond, self(), Msg). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 8301cf014..9d548f066 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -607,7 +607,7 @@ check_proto_ver(#mqtt_packet_connect{proto_ver = Ver, proto_name = Name}, _PState) -> case lists:member({Ver, Name}, ?PROTOCOL_NAMES) of true -> ok; - false -> {error, ?RC_PROTOCOL_ERROR} + false -> {error, ?RC_UNSUPPORTED_PROTOCOL_VERSION} end. %% MQTT3.1 does not allow null clientId diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index fa08fa1bb..3a2d4fdae 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -40,7 +40,8 @@ keepalive, enable_stats, stats_timer, - shutdown + shutdown, + last_packet_ts = 0 }). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). @@ -152,13 +153,10 @@ send_fun(WsPid) -> WsPid ! {binary, iolist_to_binary(Data)} end. -stat_fun() -> - fun() -> {ok, get(recv_oct)} end. - websocket_handle({binary, <<>>}, State) -> - {ok, ensure_stats_timer(State)}; + {ok, ensure_stats_timer(State#state{last_packet_ts = erlang:system_time(millisecond)})}; websocket_handle({binary, [<<>>]}, State) -> - {ok, ensure_stats_timer(State)}; + {ok, ensure_stats_timer(State#state{last_packet_ts = erlang:system_time(millisecond)})}; websocket_handle({binary, Data}, State = #state{parser_state = ParserState, proto_state = ProtoState}) -> BinSize = iolist_size(Data), @@ -167,7 +165,7 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, emqx_metrics:inc('bytes/received', BinSize), case catch emqx_frame:parse(iolist_to_binary(Data), ParserState) of {more, NewParserState} -> - {ok, State#state{parser_state = NewParserState}}; + {ok, State#state{parser_state = NewParserState, last_packet_ts = erlang:system_time(millisecond)}}; {ok, Packet, Rest} -> emqx_metrics:received(Packet), put(recv_cnt, get(recv_cnt) + 1), @@ -225,7 +223,7 @@ websocket_info({timeout, Timer, emit_stats}, websocket_info({keepalive, start, Interval}, State) -> ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State), - case emqx_keepalive:start(stat_fun(), Interval, {keepalive, check}) of + case emqx_keepalive:start(Interval, {keepalive, check}) of {ok, KeepAlive} -> {ok, State#state{keepalive = KeepAlive}}; {error, Error} -> @@ -233,8 +231,8 @@ websocket_info({keepalive, start, Interval}, State) -> shutdown(Error, State) end; -websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> - case emqx_keepalive:check(KeepAlive) of +websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive, last_packet_ts = LastPacketTs}) -> + case emqx_keepalive:check(KeepAlive, LastPacketTs) of {ok, KeepAlive1} -> {ok, State#state{keepalive = KeepAlive1}}; {error, timeout} -> From 768d1786c7807960470e1c288504a3f5f2390d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 20 Sep 2018 15:55:36 +0800 Subject: [PATCH 304/520] Fix bug --- src/emqx_connection.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 139011891..14412ab95 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -298,7 +298,7 @@ handle_packet(Data, State = #state{proto_state = ProtoState, idle_timeout = IdleTimeout}) -> case catch emqx_frame:parse(Data, ParserState) of {more, NewParserState} -> - {noreply, State#state{parser_state = NewParserState, last_packet_ts = erlang:system_time(millisecond)}, IdleTimeout}; + {noreply, run_socket(State#state{parser_state = NewParserState, last_packet_ts = erlang:system_time(millisecond)}), IdleTimeout}; {ok, Packet = ?PACKET(Type), Rest} -> emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of From 2c350bf5fb747d70b413305944c308622217cfb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 20 Sep 2018 17:19:21 +0800 Subject: [PATCH 305/520] Match test case for last change --- test/emqx_keepalive_SUITE.erl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/emqx_keepalive_SUITE.erl b/test/emqx_keepalive_SUITE.erl index c4dbd80f2..333d66f45 100644 --- a/test/emqx_keepalive_SUITE.erl +++ b/test/emqx_keepalive_SUITE.erl @@ -26,17 +26,18 @@ groups() -> [{keepalive, [], [t_keepalive]}]. %%-------------------------------------------------------------------- t_keepalive(_) -> - {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), - [resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). + {ok, KA} = emqx_keepalive:start(1, {keepalive, timeout}), + resumed = keepalive_recv(KA, 100), + timeout = keepalive_recv(KA, 2000). -keepalive_recv(KA, Acc) -> +keepalive_recv(KA, MockInterval) -> receive {keepalive, timeout} -> - case emqx_keepalive:check(KA) of - {ok, KA1} -> keepalive_recv(KA1, [resumed | Acc]); - {error, timeout} -> [timeout | Acc] + case emqx_keepalive:check(KA, erlang:system_time(millisecond) - MockInterval) of + {ok, _} -> resumed; + {error, timeout} -> timeout end after 4000 -> - Acc + error end. From b1d4ec750a53234f0d085474aa976451324a0f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 20 Sep 2018 17:58:54 +0800 Subject: [PATCH 306/520] Remove the same test cases as emqx_keepalive --- Makefile | 2 +- test/emqx_net_SUITE.erl | 43 ----------------------------------------- 2 files changed, 1 insertion(+), 44 deletions(-) delete mode 100644 test/emqx_net_SUITE.erl diff --git a/Makefile b/Makefile index 54b13727a..302aad42a 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ EUNIT_OPTS = verbose CT_SUITES = emqx emqx_zone emqx_banned emqx_connection emqx_session emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight \ emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ - emqx_mqtt_compat emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ + emqx_mqtt_compat emqx_mqtt_props emqx_mqueue emqx_pqueue emqx_router emqx_sm \ emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm \ emqx_mountpoint emqx_listeners emqx_protocol emqx_pool diff --git a/test/emqx_net_SUITE.erl b/test/emqx_net_SUITE.erl deleted file mode 100644 index 50a830d10..000000000 --- a/test/emqx_net_SUITE.erl +++ /dev/null @@ -1,43 +0,0 @@ -%% Copyright (c) 2018 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_net_SUITE). - -%% CT --compile(export_all). --compile(nowarn_export_all). - -all() -> [{group, keepalive}]. - -groups() -> [{keepalive, [], [t_keepalive]}]. - -%%-------------------------------------------------------------------- -%% Keepalive -%%-------------------------------------------------------------------- - -t_keepalive(_) -> - {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), - [resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). - -keepalive_recv(KA, Acc) -> - receive - {keepalive, timeout} -> - case emqx_keepalive:check(KA) of - {ok, KA1} -> keepalive_recv(KA1, [resumed | Acc]); - {error, timeout} -> [timeout | Acc] - end - after 4000 -> - Acc - end. - From aade94711c52f95648c0d615f61b7a523b5409bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 21 Sep 2018 18:50:26 +0800 Subject: [PATCH 307/520] Use process dictionaries to record last packet timestamp --- src/emqx_connection.erl | 13 +++++++------ src/emqx_ws_connection.erl | 16 +++++++++------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 14412ab95..6c1ab3e66 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -45,8 +45,7 @@ rate_limit, publish_limit, limit_timer, - idle_timeout, - last_packet_ts = 0 + idle_timeout }). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). @@ -253,8 +252,8 @@ handle_info({keepalive, start, Interval}, State) -> shutdown(Error, State) end; -handle_info({keepalive, check}, State = #state{keepalive = KeepAlive, last_packet_ts = LastPacketTs}) -> - case emqx_keepalive:check(KeepAlive, LastPacketTs) of +handle_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> + case emqx_keepalive:check(KeepAlive, get(last_packet_ts)) of {ok, KeepAlive1} -> {noreply, State#state{keepalive = KeepAlive1}}; {error, timeout} -> @@ -291,14 +290,16 @@ code_change(_OldVsn, State, _Extra) -> %% Receive and parse data handle_packet(<<>>, State) -> - {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State#state{last_packet_ts = erlang:system_time(millisecond)})))}; + put(last_packet_ts, erlang:system_time(millisecond)), + {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; handle_packet(Data, State = #state{proto_state = ProtoState, parser_state = ParserState, idle_timeout = IdleTimeout}) -> case catch emqx_frame:parse(Data, ParserState) of {more, NewParserState} -> - {noreply, run_socket(State#state{parser_state = NewParserState, last_packet_ts = erlang:system_time(millisecond)}), IdleTimeout}; + put(last_packet_ts, erlang:system_time(millisecond)), + {noreply, run_socket(State#state{parser_state = NewParserState}), IdleTimeout}; {ok, Packet = ?PACKET(Type), Rest} -> emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 3a2d4fdae..2526392ee 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -40,8 +40,7 @@ keepalive, enable_stats, stats_timer, - shutdown, - last_packet_ts = 0 + shutdown }). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). @@ -154,9 +153,11 @@ send_fun(WsPid) -> end. websocket_handle({binary, <<>>}, State) -> - {ok, ensure_stats_timer(State#state{last_packet_ts = erlang:system_time(millisecond)})}; + put(last_packet_ts, erlang:system_time(millisecond)), + {ok, ensure_stats_timer(State)}; websocket_handle({binary, [<<>>]}, State) -> - {ok, ensure_stats_timer(State#state{last_packet_ts = erlang:system_time(millisecond)})}; + put(last_packet_ts, erlang:system_time(millisecond)), + {ok, ensure_stats_timer(State)}; websocket_handle({binary, Data}, State = #state{parser_state = ParserState, proto_state = ProtoState}) -> BinSize = iolist_size(Data), @@ -165,7 +166,8 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, emqx_metrics:inc('bytes/received', BinSize), case catch emqx_frame:parse(iolist_to_binary(Data), ParserState) of {more, NewParserState} -> - {ok, State#state{parser_state = NewParserState, last_packet_ts = erlang:system_time(millisecond)}}; + put(last_packet_ts, erlang:system_time(millisecond)), + {ok, State#state{parser_state = NewParserState}}; {ok, Packet, Rest} -> emqx_metrics:received(Packet), put(recv_cnt, get(recv_cnt) + 1), @@ -231,8 +233,8 @@ websocket_info({keepalive, start, Interval}, State) -> shutdown(Error, State) end; -websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive, last_packet_ts = LastPacketTs}) -> - case emqx_keepalive:check(KeepAlive, LastPacketTs) of +websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> + case emqx_keepalive:check(KeepAlive, get(last_packet_ts)) of {ok, KeepAlive1} -> {ok, State#state{keepalive = KeepAlive1}}; {error, timeout} -> From ab2697671a8ceffa92069f6c8a25c7b7e2b90a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 21 Sep 2018 19:59:32 +0800 Subject: [PATCH 308/520] Change the location of the recording last packet timestamp --- src/emqx_connection.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 6c1ab3e66..d4f160714 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -232,6 +232,7 @@ handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) -> Size = iolist_size(Data), emqx_metrics:inc('bytes/received', Size), Incoming = #{bytes => Size, packets => 0}, + put(last_packet_ts, erlang:system_time(millisecond)), handle_packet(Data, State#state{await_recv = false, incoming = Incoming}); handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) -> @@ -290,7 +291,6 @@ code_change(_OldVsn, State, _Extra) -> %% Receive and parse data handle_packet(<<>>, State) -> - put(last_packet_ts, erlang:system_time(millisecond)), {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; handle_packet(Data, State = #state{proto_state = ProtoState, @@ -298,7 +298,6 @@ handle_packet(Data, State = #state{proto_state = ProtoState, idle_timeout = IdleTimeout}) -> case catch emqx_frame:parse(Data, ParserState) of {more, NewParserState} -> - put(last_packet_ts, erlang:system_time(millisecond)), {noreply, run_socket(State#state{parser_state = NewParserState}), IdleTimeout}; {ok, Packet = ?PACKET(Type), Rest} -> emqx_metrics:received(Packet), From 389b3c80bc36bfecf2d5d098df7f1fde54dc432b Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 21 Sep 2018 18:18:18 +0800 Subject: [PATCH 309/520] Fix the message delivery to remote --- src/emqx_session.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 118ab6e21..5f078daeb 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -775,7 +775,7 @@ redeliver({pubrel, PacketId}, #state{conn_pid = ConnPid}) -> deliver(PacketId, Msg, #state{conn_pid = ConnPid, binding = local}) -> ConnPid ! {deliver, {publish, PacketId, Msg}}; deliver(PacketId, Msg, #state{conn_pid = ConnPid, binding = remote}) -> - emqx_rpc:cast(node(ConnPid), erlang, send, [ConnPid, {deliver, PacketId, Msg}]). + emqx_rpc:cast(node(ConnPid), erlang, send, [ConnPid, {deliver, {publish, PacketId, Msg}}]). %%------------------------------------------------------------------------------ %% Awaiting ACK for QoS1/QoS2 Messages From 721f237bc40e3df22dbb456e7226140e94cc0645 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sun, 9 Sep 2018 16:30:40 +0200 Subject: [PATCH 310/520] Rewrite emqx_gc.erl The implementation prior to this commit supports only one gc enforcement policy which is message count threshold. The new implementation introduces 1 more: volume threshold based --- .gitignore | 2 + priv/emqx.schema | 14 ++++++ src/emqx_connection.erl | 34 ++++++++++----- src/emqx_gc.erl | 97 +++++++++++++++++++++++++++++++---------- src/emqx_session.erl | 35 ++++++++++----- test/emqx_gc_tests.erl | 53 ++++++++++++++++++++++ 6 files changed, 190 insertions(+), 45 deletions(-) create mode 100644 test/emqx_gc_tests.erl diff --git a/.gitignore b/.gitignore index 0927f0bbc..80e3bf42d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ bbmustache/ etc/gen.emqx.conf compile_commands.json cuttlefish +rebar.lock +xrefr diff --git a/priv/emqx.schema b/priv/emqx.schema index 77167b440..261c1981a 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -824,6 +824,14 @@ end}. {datatype, {enum, [true, false]}} ]}. +%% @doc Force connection/session process GC after this number of +%% messages | bytes passed through. +%% Numbers delimited by `|'. Zero or negative is to disable. +{mapping, "zone.$name.force_gc_policy", "emqx.zones", [ + {default, "0|0"}, + {datatype, string} + ]}. + {translation, "emqx.zones", fun(Conf) -> Mapping = fun("retain_available", Val) -> {mqtt_retain_available, Val}; @@ -831,6 +839,10 @@ end}. {mqtt_wildcard_subscription, Val}; ("shared_subscription", Val) -> {mqtt_shared_subscription, Val}; + ("force_gc_policy", Val) -> + [Count, Bytes] = string:tokens(Val, "| "), + {force_gc_policy, #{count => list_to_integer(Count), + bytes => list_to_integer(Bytes)}}; (Opt, Val) -> {list_to_atom(Opt), Val} end, @@ -1750,3 +1762,5 @@ end}. {busy_port, cuttlefish:conf_get("sysmon.busy_port", Conf)}, {busy_dist_port, cuttlefish:conf_get("sysmon.busy_dist_port", Conf)}] end}. + + diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index d4f160714..e2c8fdeed 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -148,7 +148,10 @@ init([Transport, RawSocket, Options]) -> proto_state = ProtoState, parser_state = ParserState, enable_stats = EnableStats, - idle_timeout = IdleTimout}), + idle_timeout = IdleTimout + }), + GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), + ok = emqx_gc:init(GcPolicy), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> @@ -200,14 +203,18 @@ handle_cast(Msg, State) -> handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> case emqx_protocol:deliver(PubOrAck, ProtoState) of {ok, ProtoState1} -> - {noreply, maybe_gc(ensure_stats_timer(State#state{proto_state = ProtoState1}))}; + State1 = ensure_stats_timer(State#state{proto_state = ProtoState1}), + ok = maybe_gc(State1, PubOrAck), + {noreply, State1}; {error, Reason} -> shutdown(Reason, State) end; - handle_info({timeout, Timer, emit_stats}, - State = #state{stats_timer = Timer, proto_state = ProtoState}) -> + State = #state{stats_timer = Timer, + proto_state = ProtoState + }) -> emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), + ok = emqx_gc:reset(), {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info(timeout, State) -> @@ -290,9 +297,10 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ %% Receive and parse data -handle_packet(<<>>, State) -> - {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; - +handle_packet(<<>>, State0) -> + State = ensure_stats_timer(ensure_rate_limit(State0)), + ok = maybe_gc(State, incoming), + {noreply, State}; handle_packet(Data, State = #state{proto_state = ProtoState, parser_state = ParserState, idle_timeout = IdleTimeout}) -> @@ -376,7 +384,13 @@ shutdown(Reason, State) -> stop(Reason, State) -> {stop, Reason, State}. -maybe_gc(State) -> - %% TODO: gc and shutdown policy - State. +%% For incoming messages, bump gc-stats with packet count and totoal volume +%% For outgoing messages, only 'publish' type is taken into account. +maybe_gc(#state{incoming = #{bytes := Oct, packets := Cnt}}, incoming) -> + ok = emqx_gc:inc(Cnt, Oct); +maybe_gc(#state{}, {publish, _PacketId, #message{payload = Payload}}) -> + Oct = iolist_size(Payload), + ok = emqx_gc:inc(1, Oct); +maybe_gc(_, _) -> + ok. diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 5a32b43c5..65bbccad1 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -12,38 +12,87 @@ %% See the License for the specific language governing permissions and %% limitations under the License. -%% GC Utility functions. +%% @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). -%% Memory: (10, 100, 1000) -%% +-author("Feng Lee "). --export([conn_max_gc_count/0, reset_conn_gc_count/2, maybe_force_gc/2, - maybe_force_gc/3]). +-export([init/1, inc/2, reset/0]). --spec(conn_max_gc_count() -> integer()). -conn_max_gc_count() -> - case emqx_config:get_env(conn_force_gc_count) of - I when is_integer(I), I > 0 -> I + rand:uniform(I); - I when is_integer(I), I =< 0 -> undefined; - undefined -> undefined +-type st() :: #{ cnt => {integer(), integer()} + , oct => {integer(), integer()} + }. + +-define(disabled, disabled). +-define(ENABLED(X), (is_integer(X) andalso X > 0)). + +%% @doc Initialize force GC parameters. +-spec init(false | map()) -> ok. +init(#{count := Count, bytes := Bytes}) -> + Cnt = [{cnt, {Count, Count}} || ?ENABLED(Count)], + Oct = [{oct, {Bytes, Bytes}} || ?ENABLED(Bytes)], + erlang:put(?MODULE, maps:from_list(Cnt ++ Oct)), + ok; +init(_) -> erlang:put(?MODULE, #{}), ok. + +%% @doc Increase count and bytes stats in one call, +%% ensure gc is triggered at most once, even if both thresholds are hit. +-spec inc(pos_integer(), pos_integer()) -> ok. +inc(Cnt, Oct) -> + mutate_pd_with(fun(St) -> inc(St, Cnt, Oct) end). + +%% @doc Reset counters to zero. +-spec reset() -> ok. +reset() -> + mutate_pd_with(fun(St) -> reset(St) end). + +%% ======== Internals ======== + +%% mutate gc stats numbers in process dict with the given function +mutate_pd_with(F) -> + St = F(erlang:get(?MODULE)), + erlang:put(?MODULE, St), + ok. + +%% Increase count and bytes stats in one call, +%% ensure gc is triggered at most once, even if both thresholds are hit. +-spec inc(st(), pos_integer(), pos_integer()) -> st(). +inc(St0, Cnt, Oct) -> + case do_inc(St0, cnt, Cnt) of + {true, St} -> + St; + {false, St1} -> + {_, St} = do_inc(St1, oct, Oct), + St end. --spec(reset_conn_gc_count(pos_integer(), tuple()) -> tuple()). -reset_conn_gc_count(Pos, State) -> - case element(Pos, State) of - undefined -> State; - _I -> setelement(Pos, State, conn_max_gc_count()) +%% Reset counters to zero. +reset(St) -> reset(cnt, reset(oct, St)). + +-spec do_inc(st(), cnt | oct, pos_integer()) -> {boolean(), st()}. +do_inc(St, Key, Num) -> + case maps:get(Key, St, ?disabled) of + ?disabled -> + {false, St}; + {Init, Remain} when Remain > Num -> + {false, maps:put(Key, {Init, Remain - Num}, St)}; + _ -> + {true, do_gc(St)} end. -maybe_force_gc(Pos, State) -> - maybe_force_gc(Pos, State, fun() -> ok end). -maybe_force_gc(Pos, State, Cb) -> - case element(Pos, State) of - undefined -> State; - I when I =< 0 -> Cb(), garbage_collect(), - reset_conn_gc_count(Pos, State); - I -> setelement(Pos, State, I - 1) +do_gc(St) -> + erlang:garbage_collect(), + reset(St). + +reset(Key, St) -> + case maps:get(Key, St, ?disabled) of + ?disabled -> St; + {Init, _} -> maps:put(Key, {Init, Init}, St) end. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 5f078daeb..eab657eb6 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -350,10 +350,13 @@ init([Parent, #{zone := Zone, enable_stats = get_env(Zone, enable_stats, true), deliver_stats = 0, enqueue_stats = 0, - created_at = os:timestamp()}, + created_at = os:timestamp() + }, emqx_sm:register_session(ClientId, attrs(State)), emqx_sm:set_session_stats(ClientId, stats(State)), emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), + GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), + ok = emqx_gc:init(GcPolicy), ok = proc_lib:init_ack(Parent, {ok, self()}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State). @@ -567,8 +570,11 @@ handle_info({timeout, Timer, retry_delivery}, State = #state{retry_timer = Timer handle_info({timeout, Timer, check_awaiting_rel}, State = #state{await_rel_timer = Timer}) -> noreply(expire_awaiting_rel(State#state{await_rel_timer = undefined})); -handle_info({timeout, Timer, emit_stats}, State = #state{client_id = ClientId, stats_timer = Timer}) -> +handle_info({timeout, Timer, emit_stats}, + State = #state{client_id = ClientId, + stats_timer = Timer}) -> _ = emqx_sm:set_session_stats(ClientId, stats(State)), + ok = emqx_gc:reset(), %% going to hibernate, reset gc stats {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info({timeout, Timer, expired}, State = #state{expiry_timer = Timer}) -> @@ -744,21 +750,22 @@ dispatch(Msg, State = #state{client_id = ClientId, conn_pid = undefined}) -> end; %% Deliver qos0 message directly to client -dispatch(Msg = #message{qos = ?QOS0}, State) -> +dispatch(Msg = #message{qos = ?QOS0} = Msg, State) -> deliver(undefined, Msg, State), - inc_stats(deliver, State); + inc_stats(deliver, Msg, State); -dispatch(Msg = #message{qos = QoS}, State = #state{next_pkt_id = PacketId, inflight = Inflight}) +dispatch(Msg = #message{qos = QoS} = Msg, + State = #state{next_pkt_id = PacketId, inflight = Inflight}) when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 -> case emqx_inflight:is_full(Inflight) of true -> enqueue_msg(Msg, State); false -> deliver(PacketId, Msg, State), - await(PacketId, Msg, inc_stats(deliver, next_pkt_id(State))) + await(PacketId, Msg, inc_stats(deliver, Msg, next_pkt_id(State))) end. enqueue_msg(Msg, State = #state{mqueue = Q}) -> - inc_stats(enqueue, State#state{mqueue = emqx_mqueue:in(Msg, Q)}). + inc_stats(enqueue, Msg, State#state{mqueue = emqx_mqueue:in(Msg, Q)}). %%------------------------------------------------------------------------------ %% Deliver @@ -882,11 +889,19 @@ next_pkt_id(State = #state{next_pkt_id = Id}) -> %%------------------------------------------------------------------------------ %% Inc stats -inc_stats(deliver, State = #state{deliver_stats = I}) -> +inc_stats(deliver, Msg, State = #state{deliver_stats = I}) -> + MsgSize = msg_size(Msg), + ok = emqx_gc:inc(1, MsgSize), State#state{deliver_stats = I + 1}; -inc_stats(enqueue, State = #state{enqueue_stats = I}) -> +inc_stats(enqueue, _Msg, State = #state{enqueue_stats = I}) -> State#state{enqueue_stats = I + 1}. +%% 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). + %%------------------------------------------------------------------------------ %% Helper functions @@ -902,5 +917,3 @@ noreply(State) -> shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. -%% TODO: GC Policy and Shutdown Policy -%% maybe_gc(State) -> State. diff --git a/test/emqx_gc_tests.erl b/test/emqx_gc_tests.erl new file mode 100644 index 000000000..ffcac91d1 --- /dev/null +++ b/test/emqx_gc_tests.erl @@ -0,0 +1,53 @@ +%% Copyright (c) 2018 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_gc_tests). + +-include_lib("eunit/include/eunit.hrl"). + +trigger_by_cnt_test() -> + Args = #{count => 2, bytes => 0}, + ok = emqx_gc:init(Args), + ok = emqx_gc:inc(1, 1000), + St1 = inspect(), + ?assertMatch({_, Remain} when Remain > 0, maps:get(cnt, St1)), + ok = emqx_gc:inc(2, 2), + St2 = inspect(), + ok = emqx_gc:inc(0, 2000), + St3 = inspect(), + ?assertEqual(St2, St3), + ?assertMatch({N, N}, maps:get(cnt, St2)), + ?assertNot(maps:is_key(oct, St2)), + ok. + +trigger_by_oct_test() -> + Args = #{count => 2, bytes => 2}, + ok = emqx_gc:init(Args), + ok = emqx_gc:inc(1, 1), + St1 = inspect(), + ?assertMatch({_, Remain} when Remain > 0, maps:get(oct, St1)), + ok = emqx_gc:inc(2, 2), + St2 = inspect(), + ?assertMatch({N, N}, maps:get(oct, St2)), + ?assertMatch({M, M}, maps:get(cnt, St2)), + ok. + +disabled_test() -> + Args = #{count => -1, bytes => false}, + ok = emqx_gc:init(Args), + ok = emqx_gc:inc(1, 1), + ?assertEqual(#{}, inspect()), + ok. + +inspect() -> erlang:get(emqx_gc). From f9f09f66ddd61d98a51d5def15a80694b9c61a30 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Tue, 18 Sep 2018 21:44:25 +0200 Subject: [PATCH 311/520] Add new shutdown-policy config schemas --- priv/emqx.schema | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/priv/emqx.schema b/priv/emqx.schema index 261c1981a..9e55579df 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1764,3 +1764,18 @@ end}. end}. +%% @doc Max message queue length for connection/session process. +%% NOTE: Message queue here is the Erlang process mailbox, but not +%% the number of MQTT queued messages. +{mapping, "mqtt.conn.max_msg_queue_len", "emqx.conn_max_msg_queue_len", [ + {default, 100000}, + {datatype, integer} +]}. + +%% @doc Max total heap size to kill connection/session process. +%% set to 0 or negative number to have it disabled. +{mapping, "mqtt.conn.max_total_heap_size", "emqx.conn_max_total_heap_size", [ + {default, -1}, %% disabled by default + {datatype, integer} +]}. + From 6fca651a8489049eaab65342b0a9a026ac40a472 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Tue, 18 Sep 2018 22:53:37 +0200 Subject: [PATCH 312/520] Add connection/session shutdown policy The hibernation behaviour is also changed (implicitly) in this commit: Prior to this change, connection/session always hibernates after the stats timer expires regardless of messages in mailbox. After this commit, connection/session process only goes to hibernate when the timer expires AND there is nothing left in the mailbox to process --- priv/emqx.schema | 1 + src/emqx_connection.erl | 14 ++++++++--- src/emqx_misc.erl | 41 +++++++++++++++++++++++++++++- src/emqx_session.erl | 14 ++++++++--- test/emqx_misc_tests.erl | 54 ++++++++++++++++++++++++++++++++++++++++ test/emqx_test_lib.erl | 37 +++++++++++++++++++++++++++ 6 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 test/emqx_misc_tests.erl create mode 100644 test/emqx_test_lib.erl diff --git a/priv/emqx.schema b/priv/emqx.schema index 9e55579df..3b57c80e0 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1767,6 +1767,7 @@ end}. %% @doc Max message queue length for connection/session process. %% NOTE: Message queue here is the Erlang process mailbox, but not %% the number of MQTT queued messages. +%% Set to 0 or negative to have it disabled {mapping, "mqtt.conn.max_msg_queue_len", "emqx.conn_max_msg_queue_len", [ {default, 100000}, {datatype, integer} diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index e2c8fdeed..6a56b76e9 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -214,9 +214,17 @@ handle_info({timeout, Timer, emit_stats}, proto_state = ProtoState }) -> emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), - ok = emqx_gc:reset(), - {noreply, State#state{stats_timer = undefined}, hibernate}; - + NewState = State#state{stats_timer = undefined}, + case meqx_misc:conn_proc_mng_policy() of + continue -> + {noreply, NewState}; + hibernate -> + ok = emqx_gc:reset(), + {noreply, NewState, hibernate}; + {shutdown, Reason} -> + ?LOG(warning, "shutdown due to ~p", [Reason], NewState), + shutdown(Reason, NewState) + end; handle_info(timeout, State) -> shutdown(idle_timeout, State); diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index e2b60a156..c3cec7427 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -15,7 +15,7 @@ -module(emqx_misc). -export([merge_opts/2, start_timer/2, start_timer/3, cancel_timer/1, - proc_name/2, proc_stats/0, proc_stats/1]). + proc_name/2, proc_stats/0, proc_stats/1, conn_proc_mng_policy/0]). %% @doc Merge options -spec(merge_opts(list(), list()) -> list()). @@ -59,3 +59,42 @@ proc_stats(Pid) -> {value, {_, V}, Stats1} = lists:keytake(message_queue_len, 1, Stats), [{mailbox_len, V} | Stats1]. +-define(DISABLED, 0). + +%% @doc Check self() process status against connection/session process management policy, +%% return `continue | hibernate | {shutdown, Reason}' accordingly. +%% `continue': There is nothing out of the ordinary +%% `hibernate': Nothing to process in my mailbox (and since this check is triggered +%% by a timer, we assume it is a fat chance to continue idel, hence hibernate. +%% `shutdown': Some numbers (message queue length or heap size have hit the limit, +%% hence shutdown for greater good (system stability). +-spec(conn_proc_mng_policy() -> continue | hibernate | {shutdown, _}). +conn_proc_mng_policy() -> + MaxMsgQueueLen = application:get_env(?APPLICATION, conn_max_msg_queue_len, ?DISABLED), + Qlength = proc_info(message_queue_len), + Checks = + [{fun() -> is_enabled(MaxMsgQueueLen) andalso Qlength > MaxMsgQueueLen end, + {shutdown, message_queue_too_long}}, + {fun() -> is_heap_size_too_large() end, + {shutdown, total_heap_size_too_large}}, + {fun() -> Qlength > 0 end, continue}, + {fun() -> true end, hibernate} + ], + check(Checks). + +check([{Pred, Result} | Rest]) -> + case Pred() of + true -> Result; + false -> check(Rest) + end. + +is_heap_size_too_large() -> + MaxTotalHeapSize = application:get_env(?APPLICATION, conn_max_total_heap_size, ?DISABLED), + is_enabled(MaxTotalHeapSize) andalso proc_info(total_heap_size) > MaxTotalHeapSize. + +is_enabled(Max) -> Max > ?DISABLED. + +proc_info(Key) -> + {Key, Value} = erlang:process_info(self(), Key), + Value. + diff --git a/src/emqx_session.erl b/src/emqx_session.erl index eab657eb6..2381d7c3b 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -574,9 +574,17 @@ handle_info({timeout, Timer, emit_stats}, State = #state{client_id = ClientId, stats_timer = Timer}) -> _ = emqx_sm:set_session_stats(ClientId, stats(State)), - ok = emqx_gc:reset(), %% going to hibernate, reset gc stats - {noreply, State#state{stats_timer = undefined}, hibernate}; - + NewState = State#state{stats_timer = undefined}, + case emqx_misc:conn_proc_mng_policy() of + continue -> + {noreply, NewState}; + hibernate -> + ok = emqx_gc:reset(), %% going to hibernate, reset gc stats + {noreply, NewState, hibernate}; + {shutdown, Reason} -> + ?LOG(warning, "shutdown due to ~p", [Reason], NewState), + shutdown(Reason, NewState) + end; handle_info({timeout, Timer, expired}, State = #state{expiry_timer = Timer}) -> ?LOG(info, "expired, shutdown now:(", [], State), shutdown(expired, State); diff --git a/test/emqx_misc_tests.erl b/test/emqx_misc_tests.erl new file mode 100644 index 000000000..775887439 --- /dev/null +++ b/test/emqx_misc_tests.erl @@ -0,0 +1,54 @@ +%% Copyright (c) 2018 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_misc_tests). +-include_lib("eunit/include/eunit.hrl"). + +shutdown_disabled_test() -> + with_env( + [{conn_max_msg_queue_len, 0}, + {conn_max_total_heap_size, 0}], + fun() -> + self() ! foo, + ?assertEqual(continue, conn_proc_mng_policy()), + receive foo -> ok end, + ?assertEqual(hibernate, conn_proc_mng_policy()) + end). + +message_queue_too_long_test() -> + with_env( + [{conn_max_msg_queue_len, 1}, + {conn_max_total_heap_size, 0}], + fun() -> + self() ! foo, + self() ! bar, + ?assertEqual({shutdown, message_queue_too_long}, + conn_proc_mng_policy()), + receive foo -> ok end, + ?assertEqual(continue, conn_proc_mng_policy()), + receive bar -> ok end + end). + +total_heap_size_too_large_test() -> + with_env( + [{conn_max_msg_queue_len, 0}, + {conn_max_total_heap_size, 1}], + fun() -> + ?assertEqual({shutdown, total_heap_size_too_large}, + conn_proc_mng_policy()) + end). + +with_env(Envs, F) -> emqx_test_lib:with_env(Envs, F). + +conn_proc_mng_policy() -> emqx_misc:conn_proc_mng_policy(). diff --git a/test/emqx_test_lib.erl b/test/emqx_test_lib.erl new file mode 100644 index 000000000..dfb98d598 --- /dev/null +++ b/test/emqx_test_lib.erl @@ -0,0 +1,37 @@ +%% Copyright (c) 2018 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_test_lib). + +-export([ with_env/2 + ]). + +with_env([], F) -> F(); +with_env([{Key, Val} | Rest], F) -> + Origin = get_env(Key), + try + ok = set_env(Key, Val), + with_env(Rest, F) + after + case Origin of + undefined -> ok = unset_env(Key); + _ -> ok = set_env(Key, Origin) + end + end. + +get_env(Key) -> application:get_env(?APPLICATION, Key). +set_env(Key, Val) -> application:set_env(?APPLICATION, Key, Val). +unset_env(Key) -> application:unset_env(?APPLICATION, Key). + + From f70d16e3879da89d0704ae19e76c9ddf74458d02 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Tue, 18 Sep 2018 23:20:40 +0200 Subject: [PATCH 313/520] Add a test case to cover timeout message flush in emqx_misc --- src/emqx_misc.erl | 9 ++++----- test/emqx_misc_tests.erl | 7 +++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index c3cec7427..d9e96b9c9 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -36,14 +36,13 @@ start_timer(Interval, Dest, Msg) -> erlang:start_timer(Interval, Dest, Msg). -spec(cancel_timer(undefined | reference()) -> ok). -cancel_timer(undefined) -> - ok; -cancel_timer(Timer) -> - case catch erlang:cancel_timer(Timer) of +cancel_timer(Timer) when is_reference(Timer) -> + case erlang:cancel_timer(Timer) of false -> receive {timeout, Timer, _} -> ok after 0 -> ok end; _ -> ok - end. + end; +cancel_timer(_) -> ok. -spec(proc_name(atom(), pos_integer()) -> atom()). proc_name(Mod, Id) -> diff --git a/test/emqx_misc_tests.erl b/test/emqx_misc_tests.erl index 775887439..a4abac683 100644 --- a/test/emqx_misc_tests.erl +++ b/test/emqx_misc_tests.erl @@ -15,6 +15,13 @@ -module(emqx_misc_tests). -include_lib("eunit/include/eunit.hrl"). +timer_cancel_flush_test() -> + Timer = emqx_misc:start_timer(0, foo), + ok = emqx_misc:cancel_timer(Timer), + receive {timeout, Timer, foo} -> error(unexpected) + after 0 -> ok + end. + shutdown_disabled_test() -> with_env( [{conn_max_msg_queue_len, 0}, From b61615323b09d3f64776ab09f9ea99d67e7e7466 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Wed, 19 Sep 2018 21:39:26 +0200 Subject: [PATCH 314/520] Move shutdown policy config to zone configs --- priv/emqx.schema | 34 +++++++++++++-------------- src/emqx_connection.erl | 4 +++- src/emqx_misc.erl | 36 +++++++++++++++++----------- src/emqx_session.erl | 4 +++- test/emqx_misc_tests.erl | 47 +++++++++++++------------------------ test/emqx_session_SUITE.erl | 2 +- test/emqx_test_lib.erl | 37 ----------------------------- 7 files changed, 61 insertions(+), 103 deletions(-) delete mode 100644 test/emqx_test_lib.erl diff --git a/priv/emqx.schema b/priv/emqx.schema index 3b57c80e0..9fe26ead3 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -828,10 +828,21 @@ end}. %% messages | bytes passed through. %% Numbers delimited by `|'. Zero or negative is to disable. {mapping, "zone.$name.force_gc_policy", "emqx.zones", [ - {default, "0|0"}, + {default, "0 | 0"}, {datatype, string} ]}. +%% @doc Max message queue length and total heap size to force shutdown +%% connection/session process. +%% Message queue here is the Erlang process mailbox, but not the number +%% of queued MQTT messages of QoS 1 and 2. +%% Total heap size is the in Erlang 'words' not in 'bytes'. +%% Zero or negative is to disable. +{mapping, "zone.$name.force_shutdown_policy", "emqx.zones", [ + {default, "0 | 0"}, + {datatype, string} +]}. + {translation, "emqx.zones", fun(Conf) -> Mapping = fun("retain_available", Val) -> {mqtt_retain_available, Val}; @@ -843,6 +854,10 @@ end}. [Count, Bytes] = string:tokens(Val, "| "), {force_gc_policy, #{count => list_to_integer(Count), bytes => list_to_integer(Bytes)}}; + ("force_shutdown_policy", Val) -> + [Len, Siz] = string:tokens(Val, "| "), + {force_shutdown_policy, #{message_queue_len => list_to_integer(Len), + total_heap_size => list_to_integer(Siz)}}; (Opt, Val) -> {list_to_atom(Opt), Val} end, @@ -1763,20 +1778,3 @@ end}. {busy_dist_port, cuttlefish:conf_get("sysmon.busy_dist_port", Conf)}] end}. - -%% @doc Max message queue length for connection/session process. -%% NOTE: Message queue here is the Erlang process mailbox, but not -%% the number of MQTT queued messages. -%% Set to 0 or negative to have it disabled -{mapping, "mqtt.conn.max_msg_queue_len", "emqx.conn_max_msg_queue_len", [ - {default, 100000}, - {datatype, integer} -]}. - -%% @doc Max total heap size to kill connection/session process. -%% set to 0 or negative number to have it disabled. -{mapping, "mqtt.conn.max_total_heap_size", "emqx.conn_max_total_heap_size", [ - {default, -1}, %% disabled by default - {datatype, integer} -]}. - diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 6a56b76e9..4290ae0d9 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -152,6 +152,7 @@ init([Transport, RawSocket, Options]) -> }), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), ok = emqx_gc:init(GcPolicy), + erlang:put(force_shutdown_policy, emqx_zone:get_env(Zone, force_shutdown_policy)), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> @@ -215,7 +216,8 @@ handle_info({timeout, Timer, emit_stats}, }) -> emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), NewState = State#state{stats_timer = undefined}, - case meqx_misc:conn_proc_mng_policy() of + Limits = erlang:get(force_shutdown_policy), + case meqx_misc:conn_proc_mng_policy(Limits) of continue -> {noreply, NewState}; hibernate -> diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index d9e96b9c9..656b0fca3 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -15,7 +15,7 @@ -module(emqx_misc). -export([merge_opts/2, start_timer/2, start_timer/3, cancel_timer/1, - proc_name/2, proc_stats/0, proc_stats/1, conn_proc_mng_policy/0]). + proc_name/2, proc_stats/0, proc_stats/1, conn_proc_mng_policy/1]). %% @doc Merge options -spec(merge_opts(list(), list()) -> list()). @@ -62,24 +62,30 @@ proc_stats(Pid) -> %% @doc Check self() process status against connection/session process management policy, %% return `continue | hibernate | {shutdown, Reason}' accordingly. -%% `continue': There is nothing out of the ordinary -%% `hibernate': Nothing to process in my mailbox (and since this check is triggered +%% `continue': There is nothing out of the ordinary. +%% `hibernate': Nothing to process in my mailbox, and since this check is triggered %% by a timer, we assume it is a fat chance to continue idel, hence hibernate. -%% `shutdown': Some numbers (message queue length or heap size have hit the limit, +%% `shutdown': Some numbers (message queue length or heap size have hit the limit), %% hence shutdown for greater good (system stability). --spec(conn_proc_mng_policy() -> continue | hibernate | {shutdown, _}). -conn_proc_mng_policy() -> - MaxMsgQueueLen = application:get_env(?APPLICATION, conn_max_msg_queue_len, ?DISABLED), +-spec(conn_proc_mng_policy(#{message_queue_len := integer(), + total_heap_size := integer() + } | undefined) -> continue | hibernate | {shutdown, _}). +conn_proc_mng_policy(#{message_queue_len := MaxMsgQueueLen, + total_heap_size := MaxTotalHeapSize + }) -> Qlength = proc_info(message_queue_len), Checks = - [{fun() -> is_enabled(MaxMsgQueueLen) andalso Qlength > MaxMsgQueueLen end, + [{fun() -> is_message_queue_too_long(Qlength, MaxMsgQueueLen) end, {shutdown, message_queue_too_long}}, - {fun() -> is_heap_size_too_large() end, + {fun() -> is_heap_size_too_large(MaxTotalHeapSize) end, {shutdown, total_heap_size_too_large}}, {fun() -> Qlength > 0 end, continue}, {fun() -> true end, hibernate} ], - check(Checks). + check(Checks); +conn_proc_mng_policy(_) -> + %% disable by default + conn_proc_mng_policy(#{message_queue_len => 0, total_heap_size => 0}). check([{Pred, Result} | Rest]) -> case Pred() of @@ -87,11 +93,13 @@ check([{Pred, Result} | Rest]) -> false -> check(Rest) end. -is_heap_size_too_large() -> - MaxTotalHeapSize = application:get_env(?APPLICATION, conn_max_total_heap_size, ?DISABLED), - is_enabled(MaxTotalHeapSize) andalso proc_info(total_heap_size) > MaxTotalHeapSize. +is_message_queue_too_long(Qlength, Max) -> + is_enabled(Max) andalso Qlength > Max. -is_enabled(Max) -> Max > ?DISABLED. +is_heap_size_too_large(Max) -> + is_enabled(Max) andalso proc_info(total_heap_size) > Max. + +is_enabled(Max) -> is_integer(Max) andalso Max > ?DISABLED. proc_info(Key) -> {Key, Value} = erlang:process_info(self(), Key), diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 2381d7c3b..0a798f0c4 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -357,6 +357,7 @@ init([Parent, #{zone := Zone, emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), ok = emqx_gc:init(GcPolicy), + erlang:put(force_shutdown_policy, emqx_zone:get_env(Zone, force_shutdown_policy)), ok = proc_lib:init_ack(Parent, {ok, self()}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State). @@ -575,7 +576,8 @@ handle_info({timeout, Timer, emit_stats}, stats_timer = Timer}) -> _ = emqx_sm:set_session_stats(ClientId, stats(State)), NewState = State#state{stats_timer = undefined}, - case emqx_misc:conn_proc_mng_policy() of + Limits = erlang:get(force_shutdown_policy), + case emqx_misc:conn_proc_mng_policy(Limits) of continue -> {noreply, NewState}; hibernate -> diff --git a/test/emqx_misc_tests.erl b/test/emqx_misc_tests.erl index a4abac683..40a9aae87 100644 --- a/test/emqx_misc_tests.erl +++ b/test/emqx_misc_tests.erl @@ -23,39 +23,24 @@ timer_cancel_flush_test() -> end. shutdown_disabled_test() -> - with_env( - [{conn_max_msg_queue_len, 0}, - {conn_max_total_heap_size, 0}], - fun() -> - self() ! foo, - ?assertEqual(continue, conn_proc_mng_policy()), - receive foo -> ok end, - ?assertEqual(hibernate, conn_proc_mng_policy()) - end). + self() ! foo, + ?assertEqual(continue, conn_proc_mng_policy(0, 0)), + receive foo -> ok end, + ?assertEqual(hibernate, conn_proc_mng_policy(0, 0)). message_queue_too_long_test() -> - with_env( - [{conn_max_msg_queue_len, 1}, - {conn_max_total_heap_size, 0}], - fun() -> - self() ! foo, - self() ! bar, - ?assertEqual({shutdown, message_queue_too_long}, - conn_proc_mng_policy()), - receive foo -> ok end, - ?assertEqual(continue, conn_proc_mng_policy()), - receive bar -> ok end - end). + self() ! foo, + self() ! bar, + ?assertEqual({shutdown, message_queue_too_long}, + conn_proc_mng_policy(1, 0)), + receive foo -> ok end, + ?assertEqual(continue, conn_proc_mng_policy(1, 0)), + receive bar -> ok end. total_heap_size_too_large_test() -> - with_env( - [{conn_max_msg_queue_len, 0}, - {conn_max_total_heap_size, 1}], - fun() -> - ?assertEqual({shutdown, total_heap_size_too_large}, - conn_proc_mng_policy()) - end). + ?assertEqual({shutdown, total_heap_size_too_large}, + conn_proc_mng_policy(0, 1)). -with_env(Envs, F) -> emqx_test_lib:with_env(Envs, F). - -conn_proc_mng_policy() -> emqx_misc:conn_proc_mng_policy(). +conn_proc_mng_policy(L, S) -> + emqx_misc:conn_proc_mng_policy(#{message_queue_len => L, + total_heap_size => S}). diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index 29a6edc61..2b60b747d 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -25,7 +25,7 @@ all() -> [t_session_all]. init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), Config. - + end_per_suite(_Config) -> emqx_ct_broker_helpers:run_teardown_steps(). diff --git a/test/emqx_test_lib.erl b/test/emqx_test_lib.erl deleted file mode 100644 index dfb98d598..000000000 --- a/test/emqx_test_lib.erl +++ /dev/null @@ -1,37 +0,0 @@ -%% Copyright (c) 2018 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_test_lib). - --export([ with_env/2 - ]). - -with_env([], F) -> F(); -with_env([{Key, Val} | Rest], F) -> - Origin = get_env(Key), - try - ok = set_env(Key, Val), - with_env(Rest, F) - after - case Origin of - undefined -> ok = unset_env(Key); - _ -> ok = set_env(Key, Origin) - end - end. - -get_env(Key) -> application:get_env(?APPLICATION, Key). -set_env(Key, Val) -> application:set_env(?APPLICATION, Key, Val). -unset_env(Key) -> application:unset_env(?APPLICATION, Key). - - From fbac9ce43efee16b746f2e99ee8f501e996437be Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 22 Sep 2018 05:42:38 +0800 Subject: [PATCH 315/520] Improve the foce_gc_policy config. --- etc/emqx.conf | 6 ++++++ priv/emqx.schema | 11 ++++++++--- src/emqx_gc.erl | 2 -- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 3604f2efd..635ccf68c 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -544,6 +544,12 @@ zone.external.enable_acl = on ## Value: on | off zone.external.enable_stats = on +## Force MQTT connection/session process GC after this number of +## messages | bytes passed through. +## +## Numbers delimited by `|'. Zero or negative is to disable. +zone.external.force_gc_policy = 1000|1MB + ## Maximum MQTT packet size allowed. ## ## Value: Bytes diff --git a/priv/emqx.schema b/priv/emqx.schema index 9fcd97376..47ad97293 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -828,7 +828,7 @@ end}. %% messages | bytes passed through. %% Numbers delimited by `|'. Zero or negative is to disable. {mapping, "zone.$name.force_gc_policy", "emqx.zones", [ - {default, "0|0"}, + {default, "0|0KB"}, {datatype, string} ]}. @@ -841,8 +841,13 @@ end}. {mqtt_shared_subscription, Val}; ("force_gc_policy", Val) -> [Count, Bytes] = string:tokens(Val, "| "), - {force_gc_policy, #{count => list_to_integer(Count), - bytes => list_to_integer(Bytes)}}; + GcPolicy = case cuttlefish_bytesize:parse(Bytes) of + {error, Reason} -> + error(Reason); + Bytes1 -> + #{bytes => Bytes1, count => list_to_integer(Count)} + end, + {force_gc_policy, GcPolicy}; (Opt, Val) -> {list_to_atom(Opt), Val} end, diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 65bbccad1..7e98eb37a 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -21,8 +21,6 @@ -module(emqx_gc). --author("Feng Lee "). - -export([init/1, inc/2, reset/0]). -type st() :: #{ cnt => {integer(), integer()} From ee7a7e24794df53d86798582a5938ea16aa27b5a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 22 Sep 2018 06:05:29 +0800 Subject: [PATCH 316/520] Fix typo --- src/emqx_connection.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 4290ae0d9..dc8439cca 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -217,7 +217,7 @@ handle_info({timeout, Timer, emit_stats}, emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), NewState = State#state{stats_timer = undefined}, Limits = erlang:get(force_shutdown_policy), - case meqx_misc:conn_proc_mng_policy(Limits) of + case emqx_misc:conn_proc_mng_policy(Limits) of continue -> {noreply, NewState}; hibernate -> From 29787d8945de7225e75b157fbeb7fac23ee892dc Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 22 Sep 2018 06:08:53 +0800 Subject: [PATCH 317/520] Use '0MB' to configure bytes of force_gc_policy --- priv/emqx.schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index 9fe26ead3..681f18705 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -828,7 +828,7 @@ end}. %% messages | bytes passed through. %% Numbers delimited by `|'. Zero or negative is to disable. {mapping, "zone.$name.force_gc_policy", "emqx.zones", [ - {default, "0 | 0"}, + {default, "0 | 0MB"}, {datatype, string} ]}. From 05a5ad0f8ce0a6a9e7c64636d702cd2beeaaa24f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 22 Sep 2018 06:14:43 +0800 Subject: [PATCH 318/520] Use '0MB' to configure size of force_shutdown_policy --- priv/emqx.schema | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index 681f18705..04ce8f076 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -839,7 +839,7 @@ end}. %% Total heap size is the in Erlang 'words' not in 'bytes'. %% Zero or negative is to disable. {mapping, "zone.$name.force_shutdown_policy", "emqx.zones", [ - {default, "0 | 0"}, + {default, "0 | 0MB"}, {datatype, string} ]}. @@ -856,8 +856,14 @@ end}. bytes => list_to_integer(Bytes)}}; ("force_shutdown_policy", Val) -> [Len, Siz] = string:tokens(Val, "| "), - {force_shutdown_policy, #{message_queue_len => list_to_integer(Len), - total_heap_size => list_to_integer(Siz)}}; + ShutdownPolicy = case cuttlefish_bytesize:parse(Siz) of + {error, Reason} -> + error(Reason); + Siz1 -> + #{message_queue_len => list_to_integer(Len), + total_heap_size => Siz1} + end, + {force_shutdown_policy, ShutdownPolicy}; (Opt, Val) -> {list_to_atom(Opt), Val} end, From 3dfe4168cb264550e1da9fecc9e763982f0c7f37 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 22 Sep 2018 06:24:41 +0800 Subject: [PATCH 319/520] Change '0KB' to '0MB' --- priv/emqx.schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index 7ad20ea15..c68903803 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -828,7 +828,7 @@ end}. %% messages | bytes passed through. %% Numbers delimited by `|'. Zero or negative is to disable. {mapping, "zone.$name.force_gc_policy", "emqx.zones", [ - {default, "0|0KB"}, + {default, "0 | 0MB"}, {datatype, string} ]}. From 8653732bae62b262d307bcafc97630e9ac126ef8 Mon Sep 17 00:00:00 2001 From: turtleDeng Date: Sat, 22 Sep 2018 14:50:57 +0800 Subject: [PATCH 320/520] Revert "Calculate the 1.5 keep alive time exactly" --- Makefile | 2 +- etc/emqx.conf | 4 +-- priv/emqx.schema | 2 +- src/emqx_connection.erl | 13 +++++--- src/emqx_keepalive.erl | 60 +++++++++++++++++++++-------------- src/emqx_protocol.erl | 2 +- src/emqx_ws_connection.erl | 10 +++--- test/emqx_keepalive_SUITE.erl | 15 ++++----- test/emqx_net_SUITE.erl | 43 +++++++++++++++++++++++++ 9 files changed, 106 insertions(+), 45 deletions(-) create mode 100644 test/emqx_net_SUITE.erl diff --git a/Makefile b/Makefile index 302aad42a..54b13727a 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ EUNIT_OPTS = verbose CT_SUITES = emqx emqx_zone emqx_banned emqx_connection emqx_session emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight \ emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ - emqx_mqtt_compat emqx_mqtt_props emqx_mqueue emqx_pqueue emqx_router emqx_sm \ + emqx_mqtt_compat emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm \ emqx_mountpoint emqx_listeners emqx_protocol emqx_pool diff --git a/etc/emqx.conf b/etc/emqx.conf index c3aa177cc..635ccf68c 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -597,10 +597,10 @@ zone.external.force_gc_policy = 1000|1MB ## zone.external.server_keepalive = 0 ## The backoff for MQTT keepalive timeout. The broker will kick a connection out -## until 'Keepalive * backoff' timeout. +## until 'Keepalive * backoff * 2' timeout. ## ## Value: Float > 0.5 -zone.external.keepalive_backoff = 1.5 +zone.external.keepalive_backoff = 0.75 ## Maximum number of subscriptions allowed, 0 means no limit. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index 32e006ffa..379a3939e 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -748,7 +748,7 @@ end}. %% @doc Keepalive backoff {mapping, "zone.$name.keepalive_backoff", "emqx.zones", [ - {default, 1.5}, + {default, 0.75}, {datatype, float} ]}. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index dc8439cca..3176ad443 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -249,7 +249,6 @@ handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) -> Size = iolist_size(Data), emqx_metrics:inc('bytes/received', Size), Incoming = #{bytes => Size, packets => 0}, - put(last_packet_ts, erlang:system_time(millisecond)), handle_packet(Data, State#state{await_recv = false, incoming = Incoming}); handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) -> @@ -261,9 +260,15 @@ handle_info({inet_reply, _Sock, ok}, State) -> handle_info({inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); -handle_info({keepalive, start, Interval}, State) -> +handle_info({keepalive, start, Interval}, State = #state{transport = Transport, socket = Socket}) -> ?LOG(debug, "Keepalive at the interval of ~p", [Interval], State), - case emqx_keepalive:start(Interval, {keepalive, check}) of + 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 {ok, KeepAlive} -> {noreply, State#state{keepalive = KeepAlive}}; {error, Error} -> @@ -271,7 +276,7 @@ handle_info({keepalive, start, Interval}, State) -> end; handle_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> - case emqx_keepalive:check(KeepAlive, get(last_packet_ts)) of + case emqx_keepalive:check(KeepAlive) of {ok, KeepAlive1} -> {noreply, State#state{keepalive = KeepAlive1}}; {error, timeout} -> diff --git a/src/emqx_keepalive.erl b/src/emqx_keepalive.erl index 59dbe73b9..25740b099 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -14,37 +14,51 @@ -module(emqx_keepalive). --export([start/2, check/2, cancel/1]). +-export([start/3, check/1, cancel/1]). --record(keepalive, {tmsec, tmsg, tref}). +-record(keepalive, {statfun, statval, tsec, tmsg, tref, repeat = 0}). -type(keepalive() :: #keepalive{}). -export_type([keepalive/0]). --define(SWEET_SPOT, 50). % 50ms - %% @doc Start a keepalive --spec(start(integer(), any()) -> {ok, keepalive()}). -start(0, _) -> +-spec(start(fun(), integer(), any()) -> {ok, keepalive()} | {error, term()}). +start(_, 0, _) -> {ok, #keepalive{}}; -start(TimeoutSec, TimeoutMsg) -> - {ok, #keepalive{tmsec = TimeoutSec * 1000, tmsg = TimeoutMsg, tref = timer(TimeoutSec * 1000 + ?SWEET_SPOT, TimeoutMsg)}}. - -%% @doc Check keepalive, called when timeout... --spec(check(keepalive(), integer()) -> {ok, keepalive()} | {error, term()}). -check(KeepAlive = #keepalive{tmsec = TimeoutMs}, LastPacketTs) -> - TimeDiff = erlang:system_time(millisecond) - LastPacketTs, - case TimeDiff >= TimeoutMs of - true -> - {error, timeout}; - false -> - {ok, resume(KeepAlive, TimeoutMs + ?SWEET_SPOT - TimeDiff)} +start(StatFun, TimeoutSec, TimeoutMsg) -> + case catch StatFun() of + {ok, StatVal} -> + {ok, #keepalive{statfun = StatFun, statval = StatVal, + tsec = TimeoutSec, tmsg = TimeoutMsg, + tref = timer(TimeoutSec, TimeoutMsg)}}; + {error, Error} -> + {error, Error}; + {'EXIT', Reason} -> + {error, Reason} end. --spec(resume(keepalive(), integer()) -> keepalive()). -resume(KeepAlive = #keepalive{tmsg = TimeoutMsg}, TimeoutMs) -> - KeepAlive#keepalive{tref = timer(TimeoutMs, TimeoutMsg)}. +%% @doc Check keepalive, called when timeout... +-spec(check(keepalive()) -> {ok, keepalive()} | {error, term()}). +check(KeepAlive = #keepalive{statfun = StatFun, statval = LastVal, repeat = Repeat}) -> + case catch StatFun() of + {ok, NewVal} -> + if NewVal =/= LastVal -> + {ok, resume(KeepAlive#keepalive{statval = NewVal, repeat = 0})}; + Repeat < 1 -> + {ok, resume(KeepAlive#keepalive{statval = NewVal, repeat = Repeat + 1})}; + true -> + {error, timeout} + end; + {error, Error} -> + {error, Error}; + {'EXIT', Reason} -> + {error, Reason} + end. + +-spec(resume(keepalive()) -> keepalive()). +resume(KeepAlive = #keepalive{tsec = TimeoutSec, tmsg = TimeoutMsg}) -> + KeepAlive#keepalive{tref = timer(TimeoutSec, TimeoutMsg)}. %% @doc Cancel Keepalive -spec(cancel(keepalive()) -> ok). @@ -53,6 +67,6 @@ cancel(#keepalive{tref = TRef}) when is_reference(TRef) -> cancel(_) -> ok. -timer(Millisecond, Msg) -> - erlang:send_after(Millisecond, self(), Msg). +timer(Secs, Msg) -> + erlang:send_after(timer:seconds(Secs), self(), Msg). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 9d548f066..8301cf014 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -607,7 +607,7 @@ check_proto_ver(#mqtt_packet_connect{proto_ver = Ver, proto_name = Name}, _PState) -> case lists:member({Ver, Name}, ?PROTOCOL_NAMES) of true -> ok; - false -> {error, ?RC_UNSUPPORTED_PROTOCOL_VERSION} + false -> {error, ?RC_PROTOCOL_ERROR} end. %% MQTT3.1 does not allow null clientId diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 2526392ee..fa08fa1bb 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -152,11 +152,12 @@ send_fun(WsPid) -> WsPid ! {binary, iolist_to_binary(Data)} end. +stat_fun() -> + fun() -> {ok, get(recv_oct)} end. + websocket_handle({binary, <<>>}, State) -> - put(last_packet_ts, erlang:system_time(millisecond)), {ok, ensure_stats_timer(State)}; websocket_handle({binary, [<<>>]}, State) -> - put(last_packet_ts, erlang:system_time(millisecond)), {ok, ensure_stats_timer(State)}; websocket_handle({binary, Data}, State = #state{parser_state = ParserState, proto_state = ProtoState}) -> @@ -166,7 +167,6 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, emqx_metrics:inc('bytes/received', BinSize), case catch emqx_frame:parse(iolist_to_binary(Data), ParserState) of {more, NewParserState} -> - put(last_packet_ts, erlang:system_time(millisecond)), {ok, State#state{parser_state = NewParserState}}; {ok, Packet, Rest} -> emqx_metrics:received(Packet), @@ -225,7 +225,7 @@ websocket_info({timeout, Timer, emit_stats}, websocket_info({keepalive, start, Interval}, State) -> ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State), - case emqx_keepalive:start(Interval, {keepalive, check}) of + case emqx_keepalive:start(stat_fun(), Interval, {keepalive, check}) of {ok, KeepAlive} -> {ok, State#state{keepalive = KeepAlive}}; {error, Error} -> @@ -234,7 +234,7 @@ websocket_info({keepalive, start, Interval}, State) -> end; websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> - case emqx_keepalive:check(KeepAlive, get(last_packet_ts)) of + case emqx_keepalive:check(KeepAlive) of {ok, KeepAlive1} -> {ok, State#state{keepalive = KeepAlive1}}; {error, timeout} -> diff --git a/test/emqx_keepalive_SUITE.erl b/test/emqx_keepalive_SUITE.erl index 333d66f45..c4dbd80f2 100644 --- a/test/emqx_keepalive_SUITE.erl +++ b/test/emqx_keepalive_SUITE.erl @@ -26,18 +26,17 @@ groups() -> [{keepalive, [], [t_keepalive]}]. %%-------------------------------------------------------------------- t_keepalive(_) -> - {ok, KA} = emqx_keepalive:start(1, {keepalive, timeout}), - resumed = keepalive_recv(KA, 100), - timeout = keepalive_recv(KA, 2000). + {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), + [resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). -keepalive_recv(KA, MockInterval) -> +keepalive_recv(KA, Acc) -> receive {keepalive, timeout} -> - case emqx_keepalive:check(KA, erlang:system_time(millisecond) - MockInterval) of - {ok, _} -> resumed; - {error, timeout} -> timeout + case emqx_keepalive:check(KA) of + {ok, KA1} -> keepalive_recv(KA1, [resumed | Acc]); + {error, timeout} -> [timeout | Acc] end after 4000 -> - error + Acc end. diff --git a/test/emqx_net_SUITE.erl b/test/emqx_net_SUITE.erl new file mode 100644 index 000000000..50a830d10 --- /dev/null +++ b/test/emqx_net_SUITE.erl @@ -0,0 +1,43 @@ +%% Copyright (c) 2018 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_net_SUITE). + +%% CT +-compile(export_all). +-compile(nowarn_export_all). + +all() -> [{group, keepalive}]. + +groups() -> [{keepalive, [], [t_keepalive]}]. + +%%-------------------------------------------------------------------- +%% Keepalive +%%-------------------------------------------------------------------- + +t_keepalive(_) -> + {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), + [resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). + +keepalive_recv(KA, Acc) -> + receive + {keepalive, timeout} -> + case emqx_keepalive:check(KA) of + {ok, KA1} -> keepalive_recv(KA1, [resumed | Acc]); + {error, timeout} -> [timeout | Acc] + end + after 4000 -> + Acc + end. + From 03f607c1b2c1dd97fcd2115c4f7c5ac3169ecf32 Mon Sep 17 00:00:00 2001 From: tigercl Date: Sat, 22 Sep 2018 16:10:24 +0800 Subject: [PATCH 321/520] Fix issue#1833 and #1834 (#1845) --- priv/emqx.schema | 2 +- src/emqx_protocol.erl | 41 ++++++++++++++++++++++++++++++--------- src/emqx_session.erl | 36 +++++++++++++++++----------------- test/emqx_mock_client.erl | 12 ++++++------ test/emqx_sm_SUITE.erl | 2 +- 5 files changed, 58 insertions(+), 35 deletions(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index 379a3939e..5f148ac96 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -797,7 +797,7 @@ end}. %% @doc Session Expiry Interval {mapping, "zone.$name.session_expiry_interval", "emqx.zones", [ {default, "2h"}, - {datatype, {duration, ms}} + {datatype, {duration, s}} ]}. %% @doc Type: simple | priority diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 8301cf014..4a2b57ea3 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -410,9 +410,17 @@ process_packet(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), process_packet(?PACKET(?PINGREQ), PState) -> send(?PACKET(?PINGRESP), PState); -process_packet(?DISCONNECT_PACKET(?RC_SUCCESS), PState) -> - %% Clean willmsg - {stop, normal, PState#pstate{will_msg = undefined}}; +process_packet(?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}; + false -> + emqx_session:update_expiry_interval(SPid, Interval), + %% Clean willmsg + {stop, normal, PState#pstate{will_msg = undefined}} + end; process_packet(?DISCONNECT_PACKET(_), PState) -> {stop, normal, PState}. @@ -562,17 +570,32 @@ maybe_assign_client_id(PState) -> PState. try_open_session(#pstate{zone = Zone, + proto_ver = ProtoVer, client_id = ClientId, conn_pid = ConnPid, conn_props = ConnProps, username = Username, clean_start = CleanStart}) -> - case emqx_sm:open_session(#{zone => Zone, - client_id => ClientId, - conn_pid => ConnPid, - username => Username, - clean_start => CleanStart, - conn_props => ConnProps}) of + + SessAttrs = #{ + zone => Zone, + client_id => ClientId, + conn_pid => ConnPid, + username => Username, + clean_start => CleanStart + }, + + case emqx_sm:open_session(maps:put(expiry_interval, if + ProtoVer =:= ?MQTT_PROTO_V5 -> + maps:get('Session-Expiry-Interval', ConnProps, 0); + true -> + case CleanStart of + true -> + 0; + false -> + emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) + end + end, SessAttrs)) of {ok, SPid} -> {ok, SPid, false}; Other -> Other diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 0a798f0c4..14ed2a21d 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -47,6 +47,7 @@ -export([info/1, attrs/1]). -export([stats/1]). -export([resume/2, discard/2]). +-export([update_expiry_interval/2]). -export([subscribe/2, subscribe/4]). -export([publish/3]). -export([puback/2, puback/3]). @@ -313,6 +314,10 @@ resume(SPid, ConnPid) -> discard(SPid, ByPid) -> gen_server:call(SPid, {discard, ByPid}, infinity). +-spec(update_expiry_interval(spid(), timeout()) -> ok). +update_expiry_interval(SPid, Interval) -> + gen_server:cast(SPid, {expiry_interval, Interval * 1000}). + -spec(close(spid()) -> ok). close(SPid) -> gen_server:call(SPid, close, infinity). @@ -321,12 +326,12 @@ close(SPid) -> %% gen_server callbacks %%------------------------------------------------------------------------------ -init([Parent, #{zone := Zone, - client_id := ClientId, - username := Username, - conn_pid := ConnPid, - clean_start := CleanStart, - conn_props := ConnProps}]) -> +init([Parent, #{zone := Zone, + client_id := ClientId, + username := Username, + conn_pid := ConnPid, + clean_start := CleanStart, + expiry_interval := ExpiryInterval}]) -> process_flag(trap_exit, true), true = link(ConnPid), MaxInflight = get_env(Zone, max_inflight), @@ -346,7 +351,7 @@ init([Parent, #{zone := Zone, awaiting_rel = #{}, await_rel_timeout = get_env(Zone, await_rel_timeout), max_awaiting_rel = get_env(Zone, max_awaiting_rel), - expiry_interval = expire_interval(Zone, ConnProps), + expiry_interval = ExpiryInterval, enable_stats = get_env(Zone, enable_stats, true), deliver_stats = 0, enqueue_stats = 0, @@ -361,11 +366,6 @@ init([Parent, #{zone := Zone, ok = proc_lib:init_ack(Parent, {ok, self()}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State). -expire_interval(_Zone, #{'Session-Expiry-Interval' := I}) -> - I * 1000; -expire_interval(Zone, _ConnProps) -> %% Maybe v3.1.1 - get_env(Zone, session_expiry_interval, 0). - init_mqueue(Zone) -> emqx_mqueue:init(#{type => get_env(Zone, mqueue_type, simple), max_len => get_env(Zone, max_mqueue_len, 1000), @@ -540,6 +540,9 @@ handle_cast({resume, ConnPid}, State = #state{client_id = ClientId, %% Replay delivery and Dequeue pending messages noreply(dequeue(retry_delivery(true, State1))); +handle_cast({expiry_interval, Interval}, State) -> + {noreply, State#state{expiry_interval = Interval}}; + handle_cast(Msg, State) -> emqx_logger:error("[Session] unexpected cast: ~p", [Msg]), {noreply, State}. @@ -591,13 +594,10 @@ handle_info({timeout, Timer, expired}, State = #state{expiry_timer = Timer}) -> ?LOG(info, "expired, shutdown now:(", [], State), shutdown(expired, State); -handle_info({'EXIT', ConnPid, Reason}, State = #state{clean_start = true, conn_pid = ConnPid}) -> - {stop, Reason, State#state{conn_pid = undefined}}; - handle_info({'EXIT', ConnPid, Reason}, State = #state{expiry_interval = 0, conn_pid = ConnPid}) -> {stop, Reason, State#state{conn_pid = undefined}}; -handle_info({'EXIT', ConnPid, _Reason}, State = #state{clean_start = false, conn_pid = ConnPid}) -> +handle_info({'EXIT', ConnPid, _Reason}, State = #state{conn_pid = ConnPid}) -> {noreply, ensure_expire_timer(State#state{conn_pid = undefined})}; handle_info({'EXIT', OldPid, _Reason}, State = #state{old_conn_pid = OldPid}) -> @@ -876,8 +876,8 @@ ensure_retry_timer(Interval, State = #state{retry_timer = undefined}) -> ensure_retry_timer(_Timeout, State) -> State. -ensure_expire_timer(State = #state{expiry_interval = Interval}) when Interval > 0 -> - State#state{expiry_timer = emqx_misc:start_timer(Interval, expired)}; +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. diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index 85114e3a9..633e42b3f 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -53,12 +53,12 @@ init([ClientId]) -> }. handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> - Attrs = #{ zone => Zone, - client_id => ClientId, - conn_pid => ClientPid, - clean_start => true, - username => undefined, - conn_props => undefined + Attrs = #{ zone => Zone, + client_id => ClientId, + conn_pid => ClientPid, + clean_start => true, + username => undefined, + expiry_interval => 0 }, {ok, SessPid} = emqx_sm:open_session(Attrs), {reply, {ok, SessPid}, State#state{ diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl index 5e23100a9..6f9b92399 100644 --- a/test/emqx_sm_SUITE.erl +++ b/test/emqx_sm_SUITE.erl @@ -25,7 +25,7 @@ t_open_close_session(_) -> emqx_ct_broker_helpers:run_setup_steps(), {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), Attrs = #{clean_start => true, client_id => <<"client">>, conn_pid => ClientPid, - zone => internal, username => <<"zhou">>, conn_props => #{}}, + zone => internal, username => <<"zhou">>, expiry_interval => 0}, {ok, SPid} = emqx_sm:open_session(Attrs), [{<<"client">>, SPid}] = emqx_sm:lookup_session(<<"client">>), SPid = emqx_sm:lookup_session_pid(<<"client">>), From 475f2a87c64aa9d931403cfcaddf9031e3833353 Mon Sep 17 00:00:00 2001 From: turtled Date: Sat, 22 Sep 2018 16:17:21 +0800 Subject: [PATCH 322/520] Rm emqx_ctl set|show cmds --- src/emqx_ctl.erl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index c16bd23cb..17166a014 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -54,14 +54,14 @@ run_command([Cmd | Args]) -> run_command(list_to_atom(Cmd), Args). -spec(run_command(cmd(), [string()]) -> ok | {error, term()}). -run_command(set, []) -> - emqx_mgmt_cli_cfg:set_usage(), ok; +% run_command(set, []) -> +% emqx_mgmt_cli_cfg:set_usage(), ok; -run_command(set, Args) -> - emqx_mgmt_cli_cfg:run(["config" | Args]), ok; +% run_command(set, Args) -> +% emqx_mgmt_cli_cfg:run(["config" | Args]), ok; -run_command(show, Args) -> - emqx_mgmt_cli_cfg:run(["config" | Args]), ok; +% run_command(show, Args) -> +% emqx_mgmt_cli_cfg:run(["config" | Args]), ok; run_command(help, []) -> usage(); From 8f35d13e17d362074a9032f89e68ac651f68acb4 Mon Sep 17 00:00:00 2001 From: turtleDeng Date: Sat, 22 Sep 2018 16:19:28 +0800 Subject: [PATCH 323/520] Improve bridges design (#1849) Improve the design of bridges --- etc/emqx.conf | 284 +++++++++++++++++++++------------------- priv/emqx.schema | 18 +-- src/emqx_bridge.erl | 194 +++++++++++++++++---------- src/emqx_bridge_sup.erl | 2 +- src/emqx_client.erl | 2 +- 5 files changed, 284 insertions(+), 216 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 635ccf68c..dc039c199 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1557,241 +1557,257 @@ listener.wss.external.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-G ##-------------------------------------------------------------------- ##-------------------------------------------------------------------- -## Bridges to edge +## Bridges to aws ##-------------------------------------------------------------------- -## Bridge type. -## -## Value: Enum -## Example: out | in -bridge.edge.type = in - -## Bridge address: node name for local bridge, host:port for remote. -## -## Value: String -## Example: emqx@127.0.0.1, 127.0.0.1:1883 -bridge.edge.address = 127.0.0.1:1883 - -## Protocol version of the bridge. -## -## Value: Enum -## - mqtt5 -## - mqtt4 -## - mqtt3 -bridge.edge.proto_ver = mqtt4 - -## The ClientId of a remote bridge. -## -## Value: String -bridge.edge.client_id = bridge_edge - -## The Clean start flag of a remote bridge. -## -## Value: boolean -bridge.edge.clean_start = false - -## The username for a remote bridge. -## -## Value: String -bridge.edge.username = user - -## The password for a remote bridge. -## -## Value: String -bridge.edge.password = passwd - -## Mountpoint of the bridge. -## -## Value: String -## bridge.edge.mountpoint = bridge/edge/ - -## Ping interval of a down bridge. -## -## Value: Duration -## Default: 10 seconds -bridge.edge.keepalive = 10s - -## Subscriptions of the bridge topic. -## -## Value: String -bridge.edge.subscription.1.topic = # - -## Subscriptions of the bridge qos. -## -## Value: Number -bridge.edge.subscription.1.qos = 1 - -## The pending message queue of a bridge. -## -## Value: Number -bridge.edge.max_pending_messages = 10000 - ## Start type of the bridge. ## ## Value: enum ## manual ## auto -bridge.edge.start_type = manual - -## Bridge reconnect count. -## -## Value: Number -bridge.edge.reconnect_count = 10 +bridge.aws.start_type = manual ## Bridge reconnect time. ## ## Value: Duration ## Default: 30 seconds -bridge.edge.reconnect_time = 30s - -## PEM-encoded CA certificates of the bridge. -## -## Value: File -## bridge.edge.cacertfile = cacert.pem - -## SSL Certfile of the bridge. -## -## Value: File -## bridge.edge.certfile = cert.pem - -## SSL Keyfile of the bridge. -## -## Value: File -## bridge.edge.keyfile = key.pem - -## SSL Ciphers used by the bridge. -## -## Value: String -## bridge.edge.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384 - -## TLS versions used by the bridge. -## -## Value: String -## bridge.edge.tls_versions = tlsv1.2,tlsv1.1,tlsv1 - - -##-------------------------------------------------------------------- -## Bridges to cloud -##-------------------------------------------------------------------- -## Bridge type. -## -## Value: Enum -## Example: out | in -bridge.cloud.type = out +bridge.aws.reconnect_interval = 30s ## Bridge address: node name for local bridge, host:port for remote. ## ## Value: String ## Example: emqx@127.0.0.1, 127.0.0.1:1883 -bridge.cloud.address = 127.0.0.1:1883 +bridge.aws.address = 127.0.0.1:1883 ## Protocol version of the bridge. ## ## Value: Enum -## - mqtt5 -## - mqtt4 -## - mqtt3 -bridge.cloud.proto_ver = mqtt4 +## - mqttv5 +## - mqttv4 +## - mqttv3 +bridge.aws.proto_ver = mqttv4 ## The ClientId of a remote bridge. ## ## Value: String -bridge.cloud.client_id = bridge_cloud +bridge.aws.client_id = bridge_aws ## The Clean start flag of a remote bridge. ## ## Value: boolean -bridge.cloud.clean_start = false +bridge.aws.clean_start = false ## The username for a remote bridge. ## ## Value: String -bridge.cloud.username = user +bridge.aws.username = user ## The password for a remote bridge. ## ## Value: String -bridge.cloud.password = passwd +bridge.aws.password = passwd ## Mountpoint of the bridge. ## ## Value: String -bridge.cloud.mountpoint = bridge/edge/${node}/ +bridge.aws.mountpoint = bridge/aws/${node}/ ## Ping interval of a down bridge. ## ## Value: Duration ## Default: 10 seconds -bridge.cloud.keepalive = 10s +bridge.aws.keepalive = 60s ## Forward message topics ## ## Value: String ## Example: topic1/#,topic2/# -bridge.cloud.forward_rule = # +bridge.aws.forwards = topic1/#,topic2/# ## Subscriptions of the bridge topic. ## ## Value: String -bridge.cloud.subscription.1.topic = $share/cmd/topic1 +bridge.aws.subscription.1.topic = cmd/topic1 ## Subscriptions of the bridge qos. ## ## Value: Number -bridge.cloud.subscription.1.qos = 1 +bridge.aws.subscription.1.qos = 1 -## Bridge store message type. +## Subscriptions of the bridge topic. +## +## Value: String +bridge.aws.subscription.2.topic = cmd/topic2 + +## Subscriptions of the bridge qos. +## +## Value: Number +bridge.aws.subscription.2.qos = 1 + +## Bridge message queue message type. ## ## Value: Enum ## Example: memory | disk -bridge.cloud.store_type = memory +bridge.aws.mqueue_type = memory ## The pending message queue of a bridge. ## ## Value: Number -bridge.cloud.max_pending_messages = 10000 +bridge.aws.max_pending_messages = 10000 + +## PEM-encoded CA certificates of the bridge. +## +## Value: File +## bridge.aws.cacertfile = cacert.pem + +## SSL Certfile of the bridge. +## +## Value: File +## bridge.aws.certfile = cert.pem + +## SSL Keyfile of the bridge. +## +## Value: File +## bridge.aws.keyfile = key.pem + +## SSL Ciphers used by the bridge. +## +## Value: String +## bridge.aws.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384 + +## TLS versions used by the bridge. +## +## Value: String +## bridge.aws.tls_versions = tlsv1.2,tlsv1.1,tlsv1 + +##-------------------------------------------------------------------- +## Bridges to azure +##-------------------------------------------------------------------- ## Start type of the bridge. ## ## Value: enum ## manual ## auto -bridge.cloud.start_type = manual +## bridge.azure.start_type = manual ## Bridge reconnect count. ## ## Value: Number -bridge.cloud.reconnect_count = 10 +## bridge.azure.reconnect_count = 10 ## Bridge reconnect time. ## ## Value: Duration ## Default: 30 seconds -bridge.cloud.reconnect_time = 30s +## bridge.azure.reconnect_time = 30s + +## Bridge address: node name for local bridge, host:port for remote. +## +## Value: String +## Example: emqx@127.0.0.1, 127.0.0.1:1883 +## bridge.azure.address = 127.0.0.1:1883 + +## Protocol version of the bridge. +## +## Value: Enum +## - mqttv5 +## - mqttv4 +## - mqttv3 +## bridge.azure.proto_ver = mqttv4 + +## The ClientId of a remote bridge. +## +## Value: String +## bridge.azure.client_id = bridge_azure + +## The Clean start flag of a remote bridge. +## +## Value: boolean +## bridge.azure.clean_start = false + +## The username for a remote bridge. +## +## Value: String +## bridge.azure.username = user + +## The password for a remote bridge. +## +## Value: String +## bridge.azure.password = passwd + +## Mountpoint of the bridge. +## +## Value: String +## bridge.azure.mountpoint = bridge/azure/${node}/ + +## Ping interval of a down bridge. +## +## Value: Duration +## Default: 10 seconds +## bridge.azure.keepalive = 10s + +## Forward message topics +## +## Value: String +## Example: topic1/#,topic2/# +## bridge.azure.forwards = topic1/#,topic2/# + +## Subscriptions of the bridge topic. +## +## Value: String +## bridge.azure.subscription.1.topic = $share/cmd/topic1 + +## Subscriptions of the bridge qos. +## +## Value: Number +## bridge.azure.subscription.1.qos = 1 + +## Subscriptions of the bridge topic. +## +## Value: String +## bridge.azure.subscription.2.topic = $share/cmd/topic2 + +## Subscriptions of the bridge qos. +## +## Value: Number +## bridge.azure.subscription.2.qos = 1 + +## Bridge store message type. +## +## Value: Enum +## Example: memory | disk +## bridge.azure.store_type = memory + +## The pending message queue of a bridge. +## +## Value: Number +## bridge.azure.max_pending_messages = 10000 + ## PEM-encoded CA certificates of the bridge. ## ## Value: File -## bridge.cloud.cacertfile = cacert.pem +## bridge.azure.cacertfile = cacert.pem ## SSL Certfile of the bridge. ## ## Value: File -## bridge.cloud.certfile = cert.pem +## bridge.azure.certfile = cert.pem ## SSL Keyfile of the bridge. ## ## Value: File -## bridge.cloud.keyfile = key.pem +## bridge.azure.keyfile = key.pem ## SSL Ciphers used by the bridge. ## ## Value: String -## bridge.cloud.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384 +## bridge.azure.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384 ## TLS versions used by the bridge. ## ## Value: String -## bridge.cloud.tls_versions = tlsv1.2,tlsv1.1,tlsv1 +## bridge.azure.tls_versions = tlsv1.2,tlsv1.1,tlsv1 ##-------------------------------------------------------------------- ## Modules diff --git a/priv/emqx.schema b/priv/emqx.schema index 5f148ac96..c5afd9b43 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1492,12 +1492,7 @@ end}. %%-------------------------------------------------------------------- %% Bridges %%-------------------------------------------------------------------- - -{mapping, "bridge.$name.type", "emqx.bridges", [ - {datatype, {enum, [in, out]}} -]}. - -{mapping, "bridge.$name.store_type", "emqx.bridges", [ +{mapping, "bridge.$name.mqueue_type", "emqx.bridges", [ {datatype, {enum, [memory, disk]}} ]}. @@ -1506,7 +1501,7 @@ end}. ]}. {mapping, "bridge.$name.proto_ver", "emqx.bridges", [ - {datatype, {enum, [mqtt3, mqtt4, mqtt5]}} + {datatype, {enum, [mqttv3, mqttv4, mqttv5]}} ]}. {mapping, "bridge.$name.client_id", "emqx.bridges", [ @@ -1530,7 +1525,7 @@ end}. {datatype, string} ]}. -{mapping, "bridge.$name.forward_rule", "emqx.bridges", [ +{mapping, "bridge.$name.forwards", "emqx.bridges", [ {datatype, string} ]}. @@ -1577,12 +1572,7 @@ end}. {default, auto} ]}. -{mapping, "bridge.$name.reconnect_count", "emqx.bridges", [ - {default, 10}, - {datatype, integer} -]}. - -{mapping, "bridge.$name.reconnect_time", "emqx.bridges", [ +{mapping, "bridge.$name.reconnect_interval", "emqx.bridges", [ {default, "30s"}, {datatype, {duration, s}} ]}. diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index a7ce2581d..461564bd2 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -23,12 +23,16 @@ -export([start_link/2, start_bridge/1, stop_bridge/1, status/1]). +-export([show_forwards/1, add_forward/2, del_forward/2]). + +-export([show_subscriptions/1, add_subscription/3, del_subscription/2]). + -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {client_pid, options, reconnect_time, reconnect_count, - def_reconnect_count, type, mountpoint, queue, store_type, - max_pending_messages}). +-record(state, {client_pid, options, reconnect_interval, + mountpoint, queue, mqueue_type, max_pending_messages, + forwards = [], subscriptions = []}). -record(mqtt_msg, {qos = ?QOS0, retain = false, dup = false, packet_id, topic, props, payload}). @@ -42,6 +46,50 @@ start_bridge(Name) -> stop_bridge(Name) -> gen_server:call(name(Name), stop_bridge). +-spec(show_forwards(atom()) -> list()). +show_forwards(Name) -> + gen_server:call(name(Name), show_forwards). + +-spec(add_forward(atom(), binary()) -> ok | {error, already_exists | validate_fail}). +add_forward(Name, Topic) -> + case catch emqx_topic:validate({filter, Topic}) of + true -> + gen_server:call(name(Name), {add_forward, Topic}); + {'EXIT', _Reason} -> + {error, validate_fail} + end. + +-spec(del_forward(atom(), binary()) -> ok | {error, validate_fail}). +del_forward(Name, Topic) -> + case catch emqx_topic:validate({filter, Topic}) of + true -> + gen_server:call(name(Name), {del_forward, Topic}); + _ -> + {error, validate_fail} + end. + +-spec(show_subscriptions(atom()) -> list()). +show_subscriptions(Name) -> + gen_server:call(name(Name), show_subscriptions). + +-spec(add_subscription(atom(), binary(), integer()) -> ok | {error, already_exists | validate_fail}). +add_subscription(Name, Topic, QoS) -> + case catch emqx_topic:validate({filter, Topic}) of + true -> + gen_server:call(name(Name), {add_subscription, Topic, QoS}); + {'EXIT', _Reason} -> + {error, validate_fail} + end. + +-spec(del_subscription(atom(), binary()) -> ok | {error, validate_fail}). +del_subscription(Name, Topic) -> + case catch emqx_topic:validate({filter, Topic}) of + true -> + gen_server:call(name(Name), {del_subscription, Topic}); + _ -> + {error, validate_fail} + end. + status(Pid) -> gen_server:call(Pid, status). @@ -55,41 +103,78 @@ init([Options]) -> manual -> ok; auto -> erlang:send_after(1000, self(), start) end, - ReconnectCount = get_value(reconnect_count, Options, 10), - ReconnectTime = get_value(reconnect_time, Options, 30000), + ReconnectInterval = get_value(reconnect_interval, Options, 30000), MaxPendingMsg = get_value(max_pending_messages, Options, 10000), Mountpoint = format_mountpoint(get_value(mountpoint, Options)), - StoreType = get_value(store_type, Options, memory), - Type = get_value(type, Options, in), + MqueueType = get_value(mqueue_type, Options, memory), Queue = [], - {ok, #state{type = Type, - mountpoint = Mountpoint, - queue = Queue, - store_type = StoreType, - options = Options, - reconnect_count = ReconnectCount, - reconnect_time = ReconnectTime, - def_reconnect_count = ReconnectCount, + {ok, #state{mountpoint = Mountpoint, + queue = Queue, + mqueue_type = MqueueType, + options = Options, + reconnect_interval = ReconnectInterval, max_pending_messages = MaxPendingMsg}}. handle_call(start_bridge, _From, State = #state{client_pid = undefined}) -> {noreply, NewState} = handle_info(start, State), - {reply, <<"start bridge successfully">>, NewState}; + {reply, #{msg => <<"start bridge successfully">>}, NewState}; handle_call(start_bridge, _From, State) -> - {reply, <<"bridge already started">>, State}; + {reply, #{msg => <<"bridge already started">>}, State}; handle_call(stop_bridge, _From, State = #state{client_pid = undefined}) -> - {reply, <<"bridge not started">>, State}; + {reply, #{msg => <<"bridge not started">>}, State}; handle_call(stop_bridge, _From, State = #state{client_pid = Pid}) -> emqx_client:disconnect(Pid), - {reply, <<"stop bridge successfully">>, State}; + {reply, #{msg => <<"stop bridge successfully">>}, State}; handle_call(status, _From, State = #state{client_pid = undefined}) -> - {reply, <<"Stopped">>, State}; + {reply, #{status => <<"Stopped">>}, State}; handle_call(status, _From, State = #state{client_pid = _Pid})-> - {reply, <<"Running">>, State}; + {reply, #{status => <<"Running">>}, State}; + +handle_call(show_forwards, _From, State = #state{forwards = Forwards}) -> + {reply, Forwards, State}; + +handle_call({add_forward, Topic}, _From, State = #state{forwards = Forwards}) -> + case not lists:member(Topic, Forwards) of + true -> + emqx_broker:subscribe(Topic), + {reply, ok, State#state{forwards = [Topic | Forwards]}}; + false -> + {reply, {error, already_exists}, State} + end; + +handle_call({del_forward, Topic}, _From, State = #state{forwards = Forwards}) -> + case lists:member(Topic, Forwards) of + true -> + emqx_broker:unsubscribe(Topic), + {reply, ok, State#state{forwards = lists:delete(Topic, Forwards)}}; + false -> + {reply, ok, State} + end; + +handle_call(show_subscriptions, _From, State = #state{subscriptions = Subscriptions}) -> + {reply, Subscriptions, State}; + +handle_call({add_subscription, Topic, Qos}, _From, State = #state{subscriptions = Subscriptions, client_pid = ClientPid}) -> + case not lists:keymember(Topic, 1, Subscriptions) of + true -> + emqx_client:subscribe(ClientPid, {Topic, Qos}), + {reply, ok, State#state{subscriptions = [{Topic, Qos} | Subscriptions]}}; + false -> + {reply, {error, already_exists}, State} + end; + +handle_call({del_subscription, Topic}, _From, State = #state{subscriptions = Subscriptions, client_pid = ClientPid}) -> + case lists:keymember(Topic, 1, Subscriptions) of + true -> + emqx_client:unsubscribe(ClientPid, Topic), + {reply, ok, State#state{subscriptions = lists:keydelete(Topic, 1, Subscriptions)}}; + false -> + {reply, ok, State} + end; handle_call(Req, _From, State) -> emqx_logger:error("[Bridge] unexpected call: ~p", [Req]), @@ -99,46 +184,24 @@ handle_cast(Msg, State) -> emqx_logger:error("[Bridge] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info(start, State = #state{reconnect_count = 0}) -> - {noreply, State}; - %%---------------------------------------------------------------- -%% start in message bridge +%% start message bridge %%---------------------------------------------------------------- handle_info(start, State = #state{options = Options, client_pid = undefined, - reconnect_time = ReconnectTime, - reconnect_count = ReconnectCount, - type = in}) -> + reconnect_interval = ReconnectInterval}) -> case emqx_client:start_link([{owner, self()}|options(Options)]) of {ok, ClientPid, _} -> - Subs = get_value(subscriptions, Options, []), - [emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs], - {noreply, State#state{client_pid = ClientPid}}; + Subs = [{i2b(Topic), Qos} || {Topic, Qos} <- get_value(subscriptions, Options, []), + emqx_topic:validate({filter, i2b(Topic)})], + Forwards = [i2b(Topic) || Topic <- string:tokens(get_value(forwards, Options, ""), ","), + emqx_topic:validate({filter, i2b(Topic)})], + [emqx_client:subscribe(ClientPid, {Topic, Qos}) || {Topic, Qos} <- Subs], + [emqx_broker:subscribe(Topic) || Topic <- Forwards], + {noreply, State#state{client_pid = ClientPid, subscriptions = Subs, forwards = Forwards}}; {error,_} -> - erlang:send_after(ReconnectTime, self(), start), - {noreply, State#state{reconnect_count = ReconnectCount-1}} - end; - -%%---------------------------------------------------------------- -%% start out message bridge -%%---------------------------------------------------------------- -handle_info(start, State = #state{options = Options, - client_pid = undefined, - reconnect_time = ReconnectTime, - reconnect_count = ReconnectCount, - type = out}) -> - case emqx_client:start_link([{owner, self()}|options(Options)]) of - {ok, ClientPid, _} -> - Subs = get_value(subscriptions, Options, []), - [emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs], - ForwardRules = string:tokens(get_value(forward_rule, Options, ""), ","), - [emqx_broker:subscribe(i2b(Topic)) || Topic <- ForwardRules, - emqx_topic:validate({filter, i2b(Topic)})], - {noreply, State#state{client_pid = ClientPid}}; - {error,_} -> - erlang:send_after(ReconnectTime, self(), start), - {noreply, State#state{reconnect_count = ReconnectCount-1}} + erlang:send_after(ReconnectInterval, self(), start), + {noreply, State} end; %%---------------------------------------------------------------- @@ -146,14 +209,14 @@ handle_info(start, State = #state{options = Options, %%---------------------------------------------------------------- handle_info({dispatch, _, #message{topic = Topic, payload = Payload, flags = #{retain := Retain}}}, State = #state{client_pid = Pid, mountpoint = Mountpoint, queue = Queue, - store_type = StoreType, max_pending_messages = MaxPendingMsg}) -> + mqueue_type = MqueueType, max_pending_messages = MaxPendingMsg}) -> Msg = #mqtt_msg{qos = 1, retain = Retain, topic = mountpoint(Mountpoint, Topic), payload = Payload}, case emqx_client:publish(Pid, Msg) of {ok, PkgId} -> - {noreply, State#state{queue = store(StoreType, {PkgId, Msg}, Queue, MaxPendingMsg)}}; + {noreply, State#state{queue = store(MqueueType, {PkgId, Msg}, Queue, MaxPendingMsg)}}; {error, Reason} -> emqx_logger:error("Publish fail:~p", [Reason]), {noreply, State} @@ -165,26 +228,25 @@ handle_info({dispatch, _, #message{topic = Topic, payload = Payload, flags = #{r handle_info({publish, #{qos := QoS, dup := Dup, retain := Retain, topic := Topic, properties := Props, payload := Payload}}, State) -> NewMsg0 = emqx_message:make(bridge, QoS, Topic, Payload), - NewMsg1 = emqx_message:set_headers(Props, emqx_message:set_flags(#{dup => Dup, retain=> Retain}, NewMsg0)), + NewMsg1 = emqx_message:set_headers(Props, emqx_message:set_flags(#{dup => Dup, retain => Retain}, NewMsg0)), emqx_broker:publish(NewMsg1), {noreply, State}; %%---------------------------------------------------------------- %% received remote puback message %%---------------------------------------------------------------- -handle_info({puback, #{packet_id := PkgId}}, State = #state{queue = Queue, store_type = StoreType}) -> +handle_info({puback, #{packet_id := PkgId}}, State = #state{queue = Queue, mqueue_type = MqueueType}) -> % lists:keydelete(PkgId, 1, Queue) - {noreply, State#state{queue = delete(StoreType, PkgId, Queue)}}; + {noreply, State#state{queue = delete(MqueueType, PkgId, Queue)}}; handle_info({'EXIT', Pid, normal}, State = #state{client_pid = Pid}) -> {noreply, State#state{client_pid = undefined}}; handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = Pid, - reconnect_time = ReconnectTime, - def_reconnect_count = DefReconnectCount}) -> + reconnect_interval = ReconnectInterval}) -> lager:warning("emqx bridge stop reason:~p", [Reason]), - erlang:send_after(ReconnectTime, self(), start), - {noreply, State#state{client_pid = undefined, reconnect_count = DefReconnectCount}}; + erlang:send_after(ReconnectInterval, self(), start), + {noreply, State#state{client_pid = undefined}}; handle_info(Info, State) -> emqx_logger:error("[Bridge] unexpected info: ~p", [Info]), @@ -196,9 +258,9 @@ terminate(_Reason, #state{}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -proto_ver(mqtt3) -> v3; -proto_ver(mqtt4) -> v4; -proto_ver(mqtt5) -> v5. +proto_ver(mqttv3) -> v3; +proto_ver(mqttv4) -> v4; +proto_ver(mqttv5) -> v5. address(Address) -> case string:tokens(Address, ":") of [Host] -> {Host, 1883}; diff --git a/src/emqx_bridge_sup.erl b/src/emqx_bridge_sup.erl index bc8c0a532..3911da2a6 100644 --- a/src/emqx_bridge_sup.erl +++ b/src/emqx_bridge_sup.erl @@ -27,7 +27,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% @doc List all bridges --spec(bridges() -> [{node(), Status :: binary()}]). +-spec(bridges() -> [{node(), map()}]). bridges() -> [{Name, emqx_bridge:status(Pid)} || {Name, Pid, _, _} <- supervisor:which_children(?MODULE)]. diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 85d6ca59d..7ef9c5968 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -787,7 +787,7 @@ connected(cast, ?SUBACK_PACKET(PacketId, Properties, ReasonCodes), connected(cast, ?UNSUBACK_PACKET(PacketId, Properties, ReasonCodes), State = #state{subscriptions = Subscriptions}) -> case take_call({unsubscribe, PacketId}, State) of - {value, #call{from = From, req = {_, Topics}}, NewState} -> + {value, #call{from = From, req = {_, _, Topics}}, NewState} -> Subscriptions1 = lists:foldl(fun(Topic, Acc) -> maps:remove(Topic, Acc) From 5689ad3485c6edc10ddb649b91552f64b921a2f5 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 22 Sep 2018 16:10:30 +0800 Subject: [PATCH 324/520] Fix issue #1847. Change the config of 'zone.$name.publish_limit' --- etc/emqx.conf | 18 +++++++++--------- priv/emqx.schema | 30 +++++++++++++++++++----------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index dc039c199..f302af423 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -525,20 +525,20 @@ zone.external.idle_timeout = 15s ## Publish limit for the external MQTT connections. ## -## Value: rate,burst -## Default: 10 messages per second, and 100 messages burst. -## zone.external.publish_limit = 10,100 - -## Enable ban check. -## -## Value: Flag -zone.external.enable_ban = on +## Value: Number,Duration +## Example: 10 messages per minute. +zone.external.publish_limit = 10,1m ## Enable ACL check. ## ## Value: Flag zone.external.enable_acl = on +## Enable ban check. +## +## Value: Flag +zone.external.enable_ban = on + ## Enable per connection statistics. ## ## Value: on | off @@ -766,7 +766,7 @@ listener.tcp.external.zone = external ## ## Value: rate,burst ## Unit: Bps -## listener.tcp.external.rate_limit = 1024,4096 +listener.tcp.external.rate_limit = 1024,4096 ## The access control rules for the MQTT/TCP listener. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index c5afd9b43..2aa0a35a1 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -676,14 +676,14 @@ end}. {datatype, {enum, [allow, deny]}} ]}. -%% @doc Enable Ban. -{mapping, "zone.$name.enable_ban", "emqx.zones", [ +%% @doc Enable ACL check. +{mapping, "zone.$name.enable_acl", "emqx.zones", [ {default, off}, {datatype, flag} ]}. -%% @doc Enable ACL check. -{mapping, "zone.$name.enable_acl", "emqx.zones", [ +%% @doc Enable Ban. +{mapping, "zone.$name.enable_ban", "emqx.zones", [ {default, off}, {datatype, flag} ]}. @@ -696,7 +696,6 @@ end}. %% @doc Publish limit of the MQTT connections. {mapping, "zone.$name.publish_limit", "emqx.zones", [ - {default, undefined}, {datatype, string} ]}. @@ -850,6 +849,15 @@ end}. {mqtt_wildcard_subscription, Val}; ("shared_subscription", Val) -> {mqtt_shared_subscription, Val}; + ("publish_limit", Val) -> + [Limit, Duration] = string:tokens(Val, ", "), + PubLimit = case cuttlefish_duration:parse(Duration, s) of + Secs when is_integer(Secs) -> + {list_to_integer(Limit) / Secs, list_to_integer(Limit)}; + {error, Reason} -> + error(Reason) + end, + {publish_limit, PubLimit}; ("force_gc_policy", Val) -> [Count, Bytes] = string:tokens(Val, "| "), GcPolicy = case cuttlefish_bytesize:parse(Bytes) of @@ -862,12 +870,12 @@ end}. ("force_shutdown_policy", Val) -> [Len, Siz] = string:tokens(Val, "| "), ShutdownPolicy = case cuttlefish_bytesize:parse(Siz) of - {error, Reason} -> - error(Reason); - Siz1 -> - #{message_queue_len => list_to_integer(Len), - total_heap_size => Siz1} - end, + {error, Reason} -> + error(Reason); + Siz1 -> + #{message_queue_len => list_to_integer(Len), + total_heap_size => Siz1} + end, {force_shutdown_policy, ShutdownPolicy}; (Opt, Val) -> {list_to_atom(Opt), Val} From 795ee300d17292b00c09be26e176449aa2dbc494 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 22 Sep 2018 16:14:02 +0800 Subject: [PATCH 325/520] Disable the publish_limit by default --- etc/emqx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index f302af423..b7ff0d654 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -527,7 +527,7 @@ zone.external.idle_timeout = 15s ## ## Value: Number,Duration ## Example: 10 messages per minute. -zone.external.publish_limit = 10,1m +## zone.external.publish_limit = 10,1m ## Enable ACL check. ## From b35d37c92d03ed127c21dd17b5f440a9ee134843 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Thu, 13 Sep 2018 18:20:41 +0200 Subject: [PATCH 326/520] Add new shared subscription dispatch strategy 'random' was already there before this change Added two new strategies: 'sticky' and 'round_robin' 'sticky' is made default as it is the cheapest --- Makefile | 16 +-- priv/emqx.schema | 11 +- src/emqx_shared_sub.erl | 66 +++++++++--- test/emqx_mock_client.erl | 9 +- test/emqx_shared_sub_SUITE.erl | 186 +++++++++++++++++++++++++++++++++ 5 files changed, 264 insertions(+), 24 deletions(-) create mode 100644 test/emqx_shared_sub_SUITE.erl diff --git a/Makefile b/Makefile index 54b13727a..bb866a898 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ CT_SUITES = emqx emqx_zone emqx_banned emqx_connection emqx_session emqx_access emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ emqx_mqtt_compat emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm \ - emqx_mountpoint emqx_listeners emqx_protocol emqx_pool + emqx_mountpoint emqx_listeners emqx_protocol emqx_pool emqx_shared_sub CT_NODE_NAME = emqxct@127.0.0.1 CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME) @@ -84,23 +84,23 @@ rebar-cover: coveralls: @rebar3 coveralls send -cuttlefish: deps - @mv ./deps/cuttlefish/cuttlefish ./cuttlefish -rebar-cuttlefish: rebar-deps - @make -C _build/default/lib/cuttlefish - @mv _build/default/lib/cuttlefish/cuttlefish ./cuttlefish +cuttlefish: rebar-deps + @if [ ! -f cuttlefish ]; then \ + make -C _build/default/lib/cuttlefish; \ + mv _build/default/lib/cuttlefish/cuttlefish ./cuttlefish; \ + fi rebar-deps: @rebar3 get-deps -rebar-eunit: rebar-cuttlefish +rebar-eunit: cuttlefish @rebar3 eunit rebar-compile: @rebar3 compile -rebar-ct: rebar-cuttlefish app.config +rebar-ct: cuttlefish app.config @rebar3 as test compile @ln -s -f '../../../../etc' _build/test/lib/emqx/ @ln -s -f '../../../../data' _build/test/lib/emqx/ diff --git a/priv/emqx.schema b/priv/emqx.schema index c5afd9b43..d0a54c6e0 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1726,9 +1726,16 @@ end}. {datatype, {enum, [local,one,quorum,all]}} ]}. +%% @doc Shared Subscription Dispatch Strategy. {mapping, "broker.shared_subscription_strategy", "emqx.shared_subscription_strategy", [ - {default, random}, - {datatype, {enum, [random, round_robbin, hash]}} + {default, round_robbin}, + {datatype, + {enum, + [random, %% randomly pick a subscriber + round_robbin, %% round robin alive subscribers one message after another + sticky, %% pick a random subscriber and stick to it + hash %% hash client ID to a group member + ]}} ]}. {mapping, "broker.route_batch_clean", "emqx.route_batch_clean", [ diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 0cbfab60a..096af9243 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -26,7 +26,6 @@ -export([start_link/0]). --export([strategy/0]). -export([subscribe/3, unsubscribe/3]). -export([dispatch/3]). @@ -36,6 +35,7 @@ -define(SERVER, ?MODULE). -define(TAB, emqx_shared_subscription). +-define(ALIVE_SUBS, emqx_alive_shared_subscribers). -record(state, {pmon}). -record(emqx_shared_subscription, {group, topic, subpid}). @@ -62,9 +62,9 @@ mnesia(copy) -> start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). --spec(strategy() -> round_robin | random | hash). +-spec(strategy() -> random | round_robin | stiky | hash). strategy() -> - emqx_config:get_env(shared_subscription_strategy, random). + emqx_config:get_env(shared_subscription_strategy, round_robin). subscribe(undefined, _Topic, _SubPid) -> ok; @@ -80,23 +80,56 @@ unsubscribe(Group, Topic, SubPid) when is_pid(SubPid) -> record(Group, Topic, SubPid) -> #emqx_shared_subscription{group = Group, topic = Topic, subpid = SubPid}. -%% TODO: dispatch strategy, ensure the delivery... dispatch(Group, Topic, Delivery = #delivery{message = Msg, results = Results}) -> - case pick(subscribers(Group, Topic)) of + #message{from = ClientId} = Msg, + case pick(strategy(), ClientId, Group, Topic) of false -> Delivery; SubPid -> SubPid ! {dispatch, Topic, Msg}, Delivery#delivery{results = [{dispatch, {Group, Topic}, 1} | Results]} end. -pick([]) -> - false; -pick([SubPid]) -> - SubPid; -pick(SubPids) -> - lists:nth(rand:uniform(length(SubPids)), SubPids). +pick(sticky, ClientId, Group, Topic) -> + Sub0 = erlang:get(shared_sub_sticky), + case is_sub_alive(Sub0) of + true -> + %% the old subscriber is still alive + %% keep using it for sticky strategy + Sub0; + false -> + %% randomly pick one for the first message + Sub = do_pick(random, ClientId, Group, Topic), + %% stick to whatever pick result + erlang:put(shared_sub_sticky, Sub), + Sub + end; +pick(Strategy, ClientId, Group, Topic) -> + do_pick(Strategy, ClientId, Group, Topic). + +do_pick(Strategy, ClientId, Group, Topic) -> + All = subscribers(Group, Topic), + pick_subscriber(Strategy, ClientId, All). + +pick_subscriber(_, _ClientId, []) -> false; +pick_subscriber(_, _ClientId, [Sub]) -> Sub; +pick_subscriber(Strategy, ClientId, Subs) -> + Nth = do_pick_subscriber(Strategy, ClientId, length(Subs)), + lists:nth(Nth, Subs). + +do_pick_subscriber(random, _ClientId, Count) -> + rand:uniform(Count); +do_pick_subscriber(hash, ClientId, Count) -> + 1 + erlang:phash2(ClientId) rem Count; +do_pick_subscriber(round_robin, _ClientId, Count) -> + Rem = case erlang:get(shared_sub_round_robin) of + undefined -> 0; + N -> (N + 1) rem Count + end, + _ = erlang:put(shared_sub_round_robin, Rem), + Rem + 1. subscribers(Group, Topic) -> ets:select(?TAB, [{{emqx_shared_subscription, Group, Topic, '$1'}, [], ['$1']}]). + %%----------------------------------------------------------------------------- %% gen_server callbacks %%----------------------------------------------------------------------------- @@ -104,6 +137,7 @@ subscribers(Group, Topic) -> init([]) -> {atomic, PMon} = mnesia:transaction(fun init_monitors/0), mnesia:subscribe({table, ?TAB, simple}), + ets:new(?ALIVE_SUBS, [named_table, {read_concurrency, true}, protected]), {ok, update_stats(#state{pmon = PMon})}. init_monitors() -> @@ -117,8 +151,9 @@ handle_call(Req, _From, State) -> {reply, ignored, State}. handle_cast({monitor, SubPid}, State= #state{pmon = PMon}) -> - {noreply, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})}; - + NewPmon = emqx_pmon:monitor(SubPid, PMon), + ets:insert(?ALIVE_SUBS, {SubPid}), + {noreply, update_stats(State#state{pmon = NewPmon})}; handle_cast(Msg, State) -> emqx_logger:error("[SharedSub] unexpected cast: ~p", [Msg]), {noreply, State}. @@ -154,6 +189,7 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- cleanup_down(SubPid) -> + ets:delete(?ALIVE_SUBS, SubPid), lists:foreach( fun(Record) -> mnesia:dirty_delete_object(?TAB, Record) @@ -162,3 +198,7 @@ cleanup_down(SubPid) -> update_stats(State) -> emqx_stats:setstat('subscriptions/shared/count', 'subscriptions/shared/max', ets:info(?TAB, size)), State. +%% erlang:is_process_alive/1 is expensive +%% and does not work with remote pids +is_sub_alive(Sub) -> [] =/= ets:lookup(?ALIVE_SUBS, Sub). + diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index 633e42b3f..ad1e2d22d 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -16,7 +16,8 @@ -behaviour(gen_server). --export([start_link/1, open_session/3, close_session/2, stop/1, get_last_message/0]). +-export([start_link/1, open_session/3, close_session/2, stop/1, get_last_message/0, + try_get_last_message/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -41,6 +42,12 @@ get_last_message() -> [{last_message, Msg}] = ets:lookup(?TAB, last_message), Msg. +try_get_last_message() -> + case ets:lookup(?TAB, last_message) of + [{last_message, Msg}] -> Msg; + [] -> false + end. + init([ClientId]) -> Result = lists:member(?TAB, ets:all()), if Result == false -> diff --git a/test/emqx_shared_sub_SUITE.erl b/test/emqx_shared_sub_SUITE.erl new file mode 100644 index 000000000..b44a00680 --- /dev/null +++ b/test/emqx_shared_sub_SUITE.erl @@ -0,0 +1,186 @@ + +%% Copyright (c) 2018 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_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]). + +-include("emqx.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(wait(For, Timeout), wait_for(?FUNCTION_NAME, ?LINE, fun() -> For end, Timeout)). + +all() -> [t_random_basic, t_random, t_round_robin, t_sticky, t_hash, t_not_so_sticky]. + +init_per_suite(Config) -> + emqx_ct_broker_helpers:run_setup_steps(), + Config. + +end_per_suite(_Config) -> + emqx_ct_broker_helpers:run_teardown_steps(). + +t_random(_) -> + application:set_env(?APPLICATION, shared_subscription_strategy, 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">>}}]), + %% wait for the subscription to show up + ?wait(ets:lookup(emqx_alive_shared_subscribers, SPid) =:= [{SPid}], 1000), + emqx_session:publish(SPid, 1, Message1), + ?wait(case emqx_mock_client:try_get_last_message() of + {publish, 1, _} -> true; + Other -> Other + end, 1000), + emqx_session:puback(SPid, 2), + emqx_session:puback(SPid, 3, reasoncode), + emqx_session:pubrec(SPid, 4), + emqx_session:pubrec(SPid, 5, reasoncode), + emqx_session:pubrel(SPid, 6, reasoncode), + emqx_session:pubcomp(SPid, 7, reasoncode), + emqx_mock_client:close_session(ConnPid, SPid), + ok. + +t_round_robin(_) -> + test_two_messages(round_robin). + +t_sticky(_) -> + test_two_messages(sticky). + +t_hash(_) -> + test_two_messages(hash). + +%% if the original subscriber dies, change to another one alive +t_not_so_sticky(_) -> + application:set_env(?APPLICATION, shared_subscription_strategy, 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">>}}]), + emqx_session:subscribe(SPid2, [{<<"foo/#">>, #{qos => 0, share => <<"group1">>}}]), + %% wait for the subscription to show up + ?wait(ets:lookup(emqx_alive_shared_subscribers, SPid1) =:= [{SPid1}] andalso + ets:lookup(emqx_alive_shared_subscribers, SPid2) =:= [{SPid2}], 1000), + emqx_session:publish(SPid1, 1, Message1), + ?wait(case emqx_mock_client:try_get_last_message() of + {publish, _, #message{payload = <<"hello1">>}} -> true; + Other -> Other + end, 1000), + emqx_session:publish(SPid1, 2, Message2), + ?wait(case emqx_mock_client:try_get_last_message() of + {publish, _, #message{payload = <<"hello2">>}} -> true; + Other -> Other + end, 1000), + emqx_mock_client:close_session(ConnPid2, SPid2), + ?wait(ets:tab2list(emqx_alive_shared_subscribers) =:= [], 1000), + ok. + +test_two_messages(Strategy) -> + application:set_env(?APPLICATION, shared_subscription_strategy, Strategy), + 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">>}}]), + emqx_session:subscribe(SPid2, [{<<"foo/bar">>, #{qos => 0, share => <<"group1">>}}]), + %% wait for the subscription to show up + ?wait(ets:lookup(emqx_alive_shared_subscribers, SPid1) =:= [{SPid1}] andalso + ets:lookup(emqx_alive_shared_subscribers, SPid2) =:= [{SPid2}], 1000), + emqx_session:publish(SPid1, 1, Message1), + Me = self(), + WaitF = fun(ExpectedPayload) -> + case last_message(ExpectedPayload, [ConnPid1, ConnPid2]) of + {true, Pid} -> + Me ! {subscriber, Pid}, + true; + Other -> + Other + end + end, + ?wait(WaitF(<<"hello1">>), 2000), + UsedSubPid1 = receive {subscriber, P1} -> P1 end, + %% publish both messages with SPid1 + emqx_session:publish(SPid1, 2, Message2), + ?wait(WaitF(<<"hello2">>), 2000), + UsedSubPid2 = receive {subscriber, P2} -> P2 end, + case Strategy of + sticky -> ?assert(UsedSubPid1 =:= UsedSubPid2); + round_robin -> ?assert(UsedSubPid1 =/= UsedSubPid2); + hash -> ?assert(UsedSubPid1 =:= UsedSubPid2); + _ -> ok + end, +>>>>>>> 38d0d409... Add 'hash' option for shared subscription + emqx_mock_client:close_session(ConnPid1, SPid1), + emqx_mock_client:close_session(ConnPid2, SPid2), + ok. + +%%------------------------------------------------------------------------------ +%% help functions +%%------------------------------------------------------------------------------ + +wait_for(Fn, Ln, F, Timeout) -> + {Pid, Mref} = erlang:spawn_monitor(fun() -> wait_loop(F, catch_call(F)) end), + wait_for_down(Fn, Ln, Timeout, Pid, Mref, false). + +wait_for_down(Fn, Ln, Timeout, Pid, Mref, Kill) -> + receive + {'DOWN', Mref, process, Pid, normal} -> + ok; + {'DOWN', Mref, process, Pid, {C, E, S}} -> + erlang:raise(C, {Fn, Ln, E}, S) + after + Timeout -> + case Kill of + true -> + erlang:demonitor(Mref, [flush]), + erlang:exit(Pid, kill), + erlang:error({Fn, Ln, timeout}); + false -> + Pid ! stop, + wait_for_down(Fn, Ln, Timeout, Pid, Mref, true) + end + end. + +wait_loop(_F, true) -> exit(normal); +wait_loop(F, LastRes) -> + Res = catch_call(F), + receive + stop -> erlang:exit(LastRes) + after + 100 -> wait_loop(F, Res) + end. + +catch_call(F) -> + try + case F() of + true -> true; + Other -> erlang:error({unexpected, Other}) + end + catch + C : E : S -> + {C, E, S} + end. + From 3b9247994d018e4c665b74a44b09152fa02ddfe2 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sat, 15 Sep 2018 15:02:16 +0200 Subject: [PATCH 327/520] Refine emqx_mock_client Before this change, eqmx_mock_client uses a shared ets table to store last received message, this causes troulbe when we want to start / stop two or more clients in one test case the ets table gets owned by the first spanwed client and gets closed when the owner client dies. Now it keeps the last received message in process state and a gen_server call is added to retrieve it for verification Along with this change in emqx_mock_client, it made possible to write test case to verify the actual subscriber pid used in shared subscription strategy, so test cases were added (and modified) to verify different strategies --- test/emqx_mock_client.erl | 50 ++++++++++++---------------------- test/emqx_session_SUITE.erl | 2 +- test/emqx_shared_sub_SUITE.erl | 30 ++++++++++++++------ 3 files changed, 39 insertions(+), 43 deletions(-) diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index ad1e2d22d..4528239e6 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -16,15 +16,12 @@ -behaviour(gen_server). --export([start_link/1, open_session/3, close_session/2, stop/1, get_last_message/0, - try_get_last_message/0]). +-export([start_link/1, open_session/3, close_session/2, 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}). - --define(TAB, messages). +-record(state, {clean_start, client_id, client_pid, last_msg}). start_link(ClientId) -> gen_server:start_link(?MODULE, [ClientId], []). @@ -38,25 +35,14 @@ close_session(ClientPid, SessPid) -> stop(CPid) -> gen_server:call(CPid, stop). -get_last_message() -> - [{last_message, Msg}] = ets:lookup(?TAB, last_message), - Msg. - -try_get_last_message() -> - case ets:lookup(?TAB, last_message) of - [{last_message, Msg}] -> Msg; - [] -> false - end. +get_last_message(Pid) -> + gen_server:call(Pid, get_last_message). init([ClientId]) -> - Result = lists:member(?TAB, ets:all()), - if Result == false -> - ets:new(?TAB, [set, named_table, public]); - true -> ok - end, - {ok, - #state{clean_start = true, - client_id = ClientId} + {ok, #state{clean_start = true, + client_id = ClientId, + last_msg = undefined + } }. handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> @@ -68,28 +54,26 @@ handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> expiry_interval => 0 }, {ok, SessPid} = emqx_sm:open_session(Attrs), - {reply, {ok, SessPid}, State#state{ - clean_start = true, - client_id = ClientId, - client_pid = ClientPid - }}; - + {reply, {ok, SessPid}, + State#state{clean_start = true, + client_id = ClientId, + client_pid = ClientPid + }}; handle_call({stop_session, SessPid}, _From, State) -> emqx_sm:close_session(SessPid), {stop, normal, ok, State}; - +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({_, Msg}, State) -> - ets:insert(?TAB, {last_message, Msg}), - {noreply, State}; +handle_info({deliver, Msg}, State) -> + {noreply, State#state{last_msg = Msg}}; handle_info(_Info, State) -> {noreply, State}. diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index 2b60b747d..f79b84557 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -40,7 +40,7 @@ t_session_all(_) -> [{<<"topic">>, _}] = emqx:subscriptions({SPid, <<"ClientId">>}), emqx_session:publish(SPid, 1, Message1), timer:sleep(200), - {publish, 1, _} = emqx_mock_client:get_last_message(), + {publish, 1, _} = emqx_mock_client:get_last_message(ConnPid), emqx_session:puback(SPid, 2), emqx_session:puback(SPid, 3, reasoncode), emqx_session:pubrec(SPid, 4), diff --git a/test/emqx_shared_sub_SUITE.erl b/test/emqx_shared_sub_SUITE.erl index b44a00680..8eb309001 100644 --- a/test/emqx_shared_sub_SUITE.erl +++ b/test/emqx_shared_sub_SUITE.erl @@ -19,6 +19,7 @@ -export([t_random_basic/1, t_random/1, t_round_robin/1, t_sticky/1, t_hash/1, t_not_so_sticky/1]). -include("emqx.hrl"). +-include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). -define(wait(For, Timeout), wait_for(?FUNCTION_NAME, ?LINE, fun() -> For end, Timeout)). @@ -32,7 +33,7 @@ init_per_suite(Config) -> end_per_suite(_Config) -> emqx_ct_broker_helpers:run_teardown_steps(). -t_random(_) -> +t_random_basic(_) -> application:set_env(?APPLICATION, shared_subscription_strategy, random), ClientId = <<"ClientId">>, {ok, ConnPid} = emqx_mock_client:start_link(ClientId), @@ -42,7 +43,7 @@ t_random(_) -> %% wait for the subscription to show up ?wait(ets:lookup(emqx_alive_shared_subscribers, SPid) =:= [{SPid}], 1000), emqx_session:publish(SPid, 1, Message1), - ?wait(case emqx_mock_client:try_get_last_message() of + ?wait(case emqx_mock_client:get_last_message(ConnPid) of {publish, 1, _} -> true; Other -> Other end, 1000), @@ -55,6 +56,9 @@ t_random(_) -> emqx_mock_client:close_session(ConnPid, SPid), ok. +t_random(_) -> + test_two_messages(random). + t_round_robin(_) -> test_two_messages(round_robin). @@ -76,17 +80,19 @@ t_not_so_sticky(_) -> 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">>}}]), - emqx_session:subscribe(SPid2, [{<<"foo/#">>, #{qos => 0, share => <<"group1">>}}]), %% wait for the subscription to show up - ?wait(ets:lookup(emqx_alive_shared_subscribers, SPid1) =:= [{SPid1}] andalso - ets:lookup(emqx_alive_shared_subscribers, SPid2) =:= [{SPid2}], 1000), + ?wait(ets:lookup(emqx_alive_shared_subscribers, SPid1) =:= [{SPid1}], 1000), emqx_session:publish(SPid1, 1, Message1), - ?wait(case emqx_mock_client:try_get_last_message() of + ?wait(case emqx_mock_client:get_last_message(ConnPid1) of {publish, _, #message{payload = <<"hello1">>}} -> true; Other -> Other end, 1000), - emqx_session:publish(SPid1, 2, Message2), - ?wait(case emqx_mock_client:try_get_last_message() of + emqx_mock_client:close_session(ConnPid1, SPid1), + ?wait(ets:lookup(emqx_alive_shared_subscribers, SPid1) =:= [], 1000), + emqx_session:subscribe(SPid2, [{<<"foo/#">>, #{qos => 0, share => <<"group1">>}}]), + ?wait(ets:lookup(emqx_alive_shared_subscribers, SPid2) =:= [{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), @@ -132,11 +138,17 @@ test_two_messages(Strategy) -> hash -> ?assert(UsedSubPid1 =:= UsedSubPid2); _ -> ok end, ->>>>>>> 38d0d409... Add 'hash' option for shared subscription emqx_mock_client:close_session(ConnPid1, SPid1), emqx_mock_client:close_session(ConnPid2, SPid2), 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) + end. + %%------------------------------------------------------------------------------ %% help functions %%------------------------------------------------------------------------------ From 5afaac4641f9199042aec3fef453b28e4fd8a2b2 Mon Sep 17 00:00:00 2001 From: turtled Date: Sat, 22 Sep 2018 18:03:05 +0800 Subject: [PATCH 328/520] Add emqx_broker:subscribe/3 defult qos --- src/emqx_broker.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index b2f1bb119..35e8276d2 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -70,7 +70,7 @@ subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> -spec(subscribe(emqx_topic:topic(), pid() | emqx_types:subid(), emqx_types:subid() | emqx_types:subopts()) -> ok). subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - subscribe(Topic, SubPid, SubId, #{}); + subscribe(Topic, SubPid, SubId, #{qos => 0}); subscribe(Topic, SubPid, SubOpts) when is_binary(Topic), is_pid(SubPid), is_map(SubOpts) -> subscribe(Topic, SubPid, undefined, SubOpts); subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map(SubOpts) -> From 4336b7c9a08ce796789b3eae9baa365019f2b782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 22 Sep 2018 18:19:57 +0800 Subject: [PATCH 329/520] Fix bug in issue#1848 --- src/emqx_protocol.erl | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 4a2b57ea3..15a287116 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -489,22 +489,30 @@ deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, max_topic_alias := MaxAlias, mqtt_shared_subscription := Shared, mqtt_wildcard_subscription := Wildcard} = caps(PState), - Props = #{'Maximum-QoS' => MaxQoS, - 'Retain-Available' => flag(Retain), + Props = #{'Retain-Available' => flag(Retain), 'Maximum-Packet-Size' => MaxPktSize, 'Topic-Alias-Maximum' => MaxAlias, 'Wildcard-Subscription-Available' => flag(Wildcard), 'Subscription-Identifier-Available' => 1, 'Shared-Subscription-Available' => flag(Shared)}, - Props1 = if IsAssigned -> - Props#{'Assigned-Client-Identifier' => ClientId}; - true -> Props + + Props1 = if + MaxQoS =:= ?QOS_2 -> + Props; + true -> + maps:put('Maximum-QoS', MaxQoS, Props) + end, + + Props2 = if IsAssigned -> + Props1#{'Assigned-Client-Identifier' => ClientId}; + true -> Props1 end, - Props2 = case emqx_zone:get_env(Zone, server_keepalive) of - undefined -> Props1; - Keepalive -> Props1#{'Server-Keep-Alive' => Keepalive} + + Props3 = case emqx_zone:get_env(Zone, server_keepalive) of + undefined -> Props2; + Keepalive -> Props2#{'Server-Keep-Alive' => Keepalive} end, - send(?CONNACK_PACKET(?RC_SUCCESS, SP, Props2), PState); + send(?CONNACK_PACKET(?RC_SUCCESS, SP, Props3), PState); deliver({connack, ReasonCode, SP}, PState) -> send(?CONNACK_PACKET(ReasonCode, SP), PState); From a46a1a25254d3c6e7fa94d57120ad40dfe9a25a7 Mon Sep 17 00:00:00 2001 From: HuangDan Date: Sat, 22 Sep 2018 18:33:33 +0800 Subject: [PATCH 330/520] Upgrade the esockd library to v5.4.1 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 54b13727a..f482050c3 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.2.0 dep_lager = git https://github.com/erlang-lager/lager 3.6.5 -dep_esockd = git https://github.com/emqx/esockd v5.4 +dep_esockd = git https://github.com/emqx/esockd v5.4.1 dep_ekka = git https://github.com/emqx/ekka v0.4.1 dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 dep_clique = git https://github.com/emqx/clique develop From 5dfd4310400bdf5baba46dbbd38aae651495368b Mon Sep 17 00:00:00 2001 From: HuangDan Date: Sat, 22 Sep 2018 19:40:07 +0800 Subject: [PATCH 331/520] Fix typo --- src/emqx_shared_sub.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 096af9243..02422fe55 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -62,7 +62,7 @@ mnesia(copy) -> start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). --spec(strategy() -> random | round_robin | stiky | hash). +-spec(strategy() -> random | round_robin | sticky | hash). strategy() -> emqx_config:get_env(shared_subscription_strategy, round_robin). From 8a8729f9ea7c6f130769722ffc548012c7dc290e Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sun, 23 Sep 2018 09:04:41 +0200 Subject: [PATCH 332/520] Make rebar3 xref work. Fixed a bad call in emqx_mod_subscription module also commented out dead code for now in emqx_config.erl --- .travis.yml | 1 + Makefile | 3 + src/emqx_config.erl | 100 +++++++++++++++++----------------- src/emqx_mod_subscription.erl | 2 +- 4 files changed, 55 insertions(+), 51 deletions(-) diff --git a/.travis.yml b/.travis.yml index adef0f3cd..2557513d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ before_install: script: - make dep-vsn-check - make rebar-compile + - make rebar-xref - make rebar-eunit - make rebar-ct - make rebar-cover diff --git a/Makefile b/Makefile index c5033df7b..e061066b4 100644 --- a/Makefile +++ b/Makefile @@ -91,6 +91,9 @@ cuttlefish: rebar-deps mv _build/default/lib/cuttlefish/cuttlefish ./cuttlefish; \ fi +rebar-xref: + @rebar3 xref + rebar-deps: @rebar3 get-deps diff --git a/src/emqx_config.erl b/src/emqx_config.erl index 2b96f88fc..435ebaea7 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -66,19 +66,19 @@ reload(_App) -> ok. -spec(write(atom(), list(env())) -> ok | {error, term()}). -write(App, Terms) -> - Configs = lists:map(fun({Key, Val}) -> - {cuttlefish_variable:tokenize(binary_to_list(Key)), binary_to_list(Val)} - end, Terms), - Path = lists:concat([code:priv_dir(App), "/", App, ".schema"]), - Schema = cuttlefish_schema:files([Path]), - case cuttlefish_generator:map(Schema, Configs) of - [{App, Configs1}] -> - emqx_cli_config:write_config(App, Configs), - lists:foreach(fun({Key, Val}) -> application:set_env(App, Key, Val) end, Configs1); - _ -> - error - end. +write(_App, _Terms) -> ok. + % Configs = lists:map(fun({Key, Val}) -> + % {cuttlefish_variable:tokenize(binary_to_list(Key)), binary_to_list(Val)} + % end, Terms), + % Path = lists:concat([code:priv_dir(App), "/", App, ".schema"]), + % Schema = cuttlefish_schema:files([Path]), + % case cuttlefish_generator:map(Schema, Configs) of + % [{App, Configs1}] -> + % emqx_cli_config:write_config(App, Configs), + % lists:foreach(fun({Key, Val}) -> application:set_env(App, Key, Val) end, Configs1); + % _ -> + % error + % end. -spec(dump(atom(), list(env())) -> ok | {error, term()}). dump(_App, _Terms) -> @@ -86,47 +86,47 @@ dump(_App, _Terms) -> ok. -spec(set(atom(), list(), list()) -> ok). -set(App, Par, Val) -> - emqx_cli_config:run(["config", - "set", - lists:concat([Par, "=", Val]), - lists:concat(["--app=", App])]). +set(_App, _Par, _Val) -> ok. + % emqx_cli_config:run(["config", + % "set", + % lists:concat([Par, "=", Val]), + % lists:concat(["--app=", App])]). -spec(get(atom(), list()) -> undefined | {ok, term()}). -get(App, Par) -> - case emqx_cli_config:get_cfg(App, Par) of - undefined -> undefined; - Val -> {ok, Val} - end. +get(_App, _Par) -> error(no_impl). + % case emqx_cli_config:get_cfg(App, Par) of + % undefined -> undefined; + % Val -> {ok, Val} + % end. -spec(get(atom(), list(), atom()) -> term()). -get(App, Par, Def) -> - emqx_cli_config:get_cfg(App, Par, Def). +get(_App, _Par, _Def) -> error(no_impl). + % emqx_cli_config:get_cfg(App, Par, Def). -read_(App) -> - Configs = emqx_cli_config:read_config(App), - Path = lists:concat([code:priv_dir(App), "/", App, ".schema"]), - case filelib:is_file(Path) of - false -> - []; - true -> - {_, Mappings, _} = cuttlefish_schema:files([Path]), - OptionalCfg = lists:foldl(fun(Map, Acc) -> - Key = cuttlefish_mapping:variable(Map), - case proplists:get_value(Key, Configs) of - undefined -> - [{cuttlefish_variable:format(Key), "", cuttlefish_mapping:doc(Map), false} | Acc]; - _ -> Acc - end - end, [], Mappings), - RequiredCfg = lists:foldl(fun({Key, Val}, Acc) -> - case lists:keyfind(Key, 2, Mappings) of - false -> Acc; - Map -> - [{cuttlefish_variable:format(Key), Val, cuttlefish_mapping:doc(Map), true} | Acc] - end - end, [], Configs), - RequiredCfg ++ OptionalCfg - end. +read_(_App) -> error(no_impl). + % Configs = emqx_cli_config:read_config(App), + % Path = lists:concat([code:priv_dir(App), "/", App, ".schema"]), + % case filelib:is_file(Path) of + % false -> + % []; + % true -> + % {_, Mappings, _} = cuttlefish_schema:files([Path]), + % OptionalCfg = lists:foldl(fun(Map, Acc) -> + % Key = cuttlefish_mapping:variable(Map), + % case proplists:get_value(Key, Configs) of + % undefined -> + % [{cuttlefish_variable:format(Key), "", cuttlefish_mapping:doc(Map), false} | Acc]; + % _ -> Acc + % end + % end, [], Mappings), + % RequiredCfg = lists:foldl(fun({Key, Val}, Acc) -> + % case lists:keyfind(Key, 2, Mappings) of + % false -> Acc; + % Map -> + % [{cuttlefish_variable:format(Key), Val, cuttlefish_mapping:doc(Map), true} | Acc] + % end + % end, [], Configs), + % RequiredCfg ++ OptionalCfg + % end. diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index 48edac2c4..aed7f5af2 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -36,7 +36,7 @@ on_session_created(#{client_id := ClientId}, SessAttrs, Topics) -> emqx_session:subscribe(self(), [{Replace(Topic), #{qos => QoS}} || {Topic, QoS} <- Topics]). unload(_) -> - emqx_hooks:delete('session.created', fun ?MODULE:on_session_created/3). + emqx_hooks:del('session.created', fun ?MODULE:on_session_created/3). %%-------------------------------------------------------------------- %% Internal functions From 9b2629e884e3470fb5e54899e739ab24376c21b9 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sun, 23 Sep 2018 09:12:39 +0200 Subject: [PATCH 333/520] Fix dependency version discrepancy between Makefile and rebar.config --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index aa77f3a02..f94258e6b 100644 --- a/rebar.config +++ b/rebar.config @@ -10,7 +10,7 @@ [{gen_rpc, "2.2.0"}, {ekka, "v0.4.1"}, {clique, "develop"}, - {esockd, "v5.4"}, + {esockd, "v5.4.1"}, {cuttlefish, "emqx30"} ]}. From 0b0ef9bd5484aacd1da0f68862557e55687aea18 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sun, 23 Sep 2018 10:13:02 +0200 Subject: [PATCH 334/520] Fix default QoS in test cases --- test/emqx_broker_SUITE.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index 8cadbf00d..7baa248f3 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -100,7 +100,7 @@ t_local_subscribe(_) -> timer:sleep(10), ?assertEqual([{self(), undefined}], emqx:subscribers("$local/topic0")), ?assertEqual([{self(), <<"clientId">>}], emqx:subscribers("$local/topic1")), - ?assertEqual([{<<"$local/topic1">>, #{}}, + ?assertEqual([{<<"$local/topic1">>, #{ qos => 0 }}, {<<"$local/topic2">>, #{ qos => 2 }}], emqx:subscriptions({self(), <<"clientId">>})), ?assertEqual(ok, emqx:unsubscribe("$local/topic0")), @@ -117,9 +117,9 @@ t_shared_subscribe(_) -> timer:sleep(10), ct:log("share subscriptions: ~p~n", [emqx:subscriptions({self(), undefined})]), ?assertEqual([{self(), undefined}], emqx:subscribers(<<"$local/$share/group1/topic1">>)), - ?assertEqual([{<<"$local/$share/group1/topic1">>, #{}}, - {<<"$queue/topic3">>, #{}}, - {<<"$share/group2/topic2">>, #{}}], + ?assertEqual([{<<"$local/$share/group1/topic1">>, #{qos => 0}}, + {<<"$queue/topic3">>, #{qos => 0}}, + {<<"$share/group2/topic2">>, #{qos => 0}}], lists:sort(emqx:subscriptions({self(), undefined}))), emqx:unsubscribe("$local/$share/group1/topic1"), emqx:unsubscribe("$share/group2/topic2"), From c123064afcf186151c29a000266cc1e7e7a89ef0 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sun, 23 Sep 2018 10:18:25 +0200 Subject: [PATCH 335/520] Upload coveralls data only when sucess This is done by moving coveralls send to after_success section in .travis.yml. having it in after_success should not make the build fail if coveralls send fails due to server being overloaded etc. --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2557513d4..8960ab7c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,8 @@ script: - make rebar-eunit - make rebar-ct - make rebar-cover + +after_success: - make coveralls sudo: false From db520b9b9427fca0556e396fa1fe1c8d38808520 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 25 Sep 2018 22:37:45 +0800 Subject: [PATCH 336/520] Disable the rate_limit of MQTT/TCP listener by default. --- etc/emqx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index b7ff0d654..10da1a0b9 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -766,7 +766,7 @@ listener.tcp.external.zone = external ## ## Value: rate,burst ## Unit: Bps -listener.tcp.external.rate_limit = 1024,4096 +## listener.tcp.external.rate_limit = 1024,4096 ## The access control rules for the MQTT/TCP listener. ## From d1c72b92aff6fe8fc2dde4598a07b181252789e2 Mon Sep 17 00:00:00 2001 From: HuangDan Date: Thu, 27 Sep 2018 10:46:14 +0800 Subject: [PATCH 337/520] Add 'sticky' dispatch strategy for shared subscription --- etc/emqx.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/etc/emqx.conf b/etc/emqx.conf index 10da1a0b9..60b576379 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1895,6 +1895,7 @@ broker.session_locking_strategy = quorum ## Value: Enum ## - random ## - round_robbin +## - sticky ## - hash broker.shared_subscription_strategy = random From 96b5d71a67a983e51af0a3b31d2983d3963c04f1 Mon Sep 17 00:00:00 2001 From: HuangDan Date: Wed, 26 Sep 2018 19:11:50 +0800 Subject: [PATCH 338/520] Fix args errors on emqx_hook:run('message.acked') --- src/emqx_session.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 14ed2a21d..ad3e66cc5 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -806,7 +806,7 @@ await(PacketId, Msg, State = #state{inflight = Inflight}) -> acked(puback, PacketId, State = #state{client_id = ClientId, inflight = Inflight}) -> case emqx_inflight:lookup(PacketId, Inflight) of {value, {publish, {_, Msg}, _Ts}} -> - emqx_hooks:run('message.acked', [#{client_id =>ClientId}], Msg), + emqx_hooks:run('message.acked', [#{client_id => ClientId}], Msg), State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}; none -> ?LOG(warning, "Duplicated PUBACK PacketId ~w", [PacketId], State), @@ -816,7 +816,7 @@ acked(puback, PacketId, State = #state{client_id = ClientId, inflight = Infligh acked(pubrec, PacketId, State = #state{client_id = ClientId, inflight = Inflight}) -> case emqx_inflight:lookup(PacketId, Inflight) of {value, {publish, {_, Msg}, _Ts}} -> - emqx_hooks:run('message.acked', [ClientId], Msg), + emqx_hooks:run('message.acked', [#{client_id => ClientId}], 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), From 3bab3cbd2aed89cb20bef259038364d845f9e5bc Mon Sep 17 00:00:00 2001 From: spring2maz Date: Thu, 27 Sep 2018 21:50:23 +0200 Subject: [PATCH 339/520] Shared subscriber should be keyed by SharedName + Topic Prior to this change, if a producer session produces to two or more shared subscriber groups, the previously picked subscriber for sticky strategy may not be a valid member for the next group. --- src/emqx_shared_sub.erl | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 02422fe55..c6fef6d6f 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -89,7 +89,7 @@ dispatch(Group, Topic, Delivery = #delivery{message = Msg, results = Results}) - end. pick(sticky, ClientId, Group, Topic) -> - Sub0 = erlang:get(shared_sub_sticky), + Sub0 = erlang:get({shared_sub_sticky, Group, Topic}), case is_sub_alive(Sub0) of true -> %% the old subscriber is still alive @@ -99,32 +99,33 @@ pick(sticky, ClientId, Group, Topic) -> %% randomly pick one for the first message Sub = do_pick(random, ClientId, Group, Topic), %% stick to whatever pick result - erlang:put(shared_sub_sticky, Sub), + erlang:put({shared_sub_sticky, Group, Topic}, Sub), Sub end; pick(Strategy, ClientId, Group, Topic) -> do_pick(Strategy, ClientId, Group, Topic). do_pick(Strategy, ClientId, Group, Topic) -> - All = subscribers(Group, Topic), - pick_subscriber(Strategy, ClientId, All). + case subscribers(Group, Topic) of + [] -> false; + [Sub] -> Sub; + All -> pick_subscriber(Group, Topic, Strategy, ClientId, All) + end. -pick_subscriber(_, _ClientId, []) -> false; -pick_subscriber(_, _ClientId, [Sub]) -> Sub; -pick_subscriber(Strategy, ClientId, Subs) -> - Nth = do_pick_subscriber(Strategy, ClientId, length(Subs)), +pick_subscriber(Group, Topic, Strategy, ClientId, Subs) -> + Nth = do_pick_subscriber(Group, Topic, Strategy, ClientId, length(Subs)), lists:nth(Nth, Subs). -do_pick_subscriber(random, _ClientId, Count) -> +do_pick_subscriber(_Group, _Topic, random, _ClientId, Count) -> rand:uniform(Count); -do_pick_subscriber(hash, ClientId, Count) -> +do_pick_subscriber(_Group, _Topic, hash, ClientId, Count) -> 1 + erlang:phash2(ClientId) rem Count; -do_pick_subscriber(round_robin, _ClientId, Count) -> - Rem = case erlang:get(shared_sub_round_robin) of +do_pick_subscriber(Group, Topic, round_robin, _ClientId, Count) -> + Rem = case erlang:get({shared_sub_round_robin, Group, Topic}) of undefined -> 0; N -> (N + 1) rem Count end, - _ = erlang:put(shared_sub_round_robin, Rem), + _ = erlang:put({shared_sub_round_robin, Group, Topic}, Rem), Rem + 1. subscribers(Group, Topic) -> From 2a0bbd1c37bee148c9d47be7ee7786ecac1846dd Mon Sep 17 00:00:00 2001 From: HuangDan Date: Thu, 27 Sep 2018 14:43:06 +0800 Subject: [PATCH 340/520] Fixed errors './cuttlefish: command not found' when running make app.config --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index e061066b4..549237e67 100644 --- a/Makefile +++ b/Makefile @@ -73,10 +73,10 @@ etc/gen.emqx.conf: bbmustache etc/emqx.conf ok = file:write_file('etc/gen.emqx.conf', Targ), \ halt(0)." -app.config: etc/gen.emqx.conf +app.config: cuttlefish etc/gen.emqx.conf $(verbose) ./cuttlefish -l info -e etc/ -c etc/gen.emqx.conf -i priv/emqx.schema -d data/ -ct: cuttlefish app.config +ct: app.config rebar-cover: @rebar3 cover @@ -103,7 +103,7 @@ rebar-eunit: cuttlefish rebar-compile: @rebar3 compile -rebar-ct: cuttlefish app.config +rebar-ct: app.config @rebar3 as test compile @ln -s -f '../../../../etc' _build/test/lib/emqx/ @ln -s -f '../../../../data' _build/test/lib/emqx/ From 1bc175e0ce8d40a6ea27f7bf2aa50bf3065f85e1 Mon Sep 17 00:00:00 2001 From: HuangDan Date: Thu, 27 Sep 2018 19:49:13 +0800 Subject: [PATCH 341/520] Add mountpoint config to zone configs --- etc/emqx.conf | 18 ++++++++++++++++++ priv/emqx.schema | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/etc/emqx.conf b/etc/emqx.conf index 60b576379..18a7d6fd6 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -665,6 +665,15 @@ zone.external.max_mqueue_len = 1000 ## Value: false | true zone.external.mqueue_store_qos0 = true +## All the topics will be prefixed with the mountpoint path if this option is enabled. +## +## Variables in mountpoint path: +## - %c: clientid +## - %u: username +## +## Value: String +## zone.external.mountpoint = devicebound/ + ##-------------------------------------------------------------------- ## Internal Zone @@ -715,6 +724,15 @@ zone.internal.max_mqueue_len = 1000 ## Value: false | true zone.internal.mqueue_store_qos0 = true +## All the topics will be prefixed with the mountpoint path if this option is enabled. +## +## Variables in mountpoint path: +## - %c: clientid +## - %u: username +## +## Value: String +## zone.internal.mountpoint = cloudbound/ + ##-------------------------------------------------------------------- ## Listeners ##-------------------------------------------------------------------- diff --git a/priv/emqx.schema b/priv/emqx.schema index 3d9328bf2..2450a6876 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -842,6 +842,10 @@ end}. {datatype, string} ]}. +{mapping, "zone.$name.mountpoint", "emqx.zones", [ + {datatype, string} +]}. + {translation, "emqx.zones", fun(Conf) -> Mapping = fun("retain_available", Val) -> {mqtt_retain_available, Val}; From 064db6520668c23b26b89bfb7deb8eb38bc9eb97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 27 Sep 2018 09:06:18 +0800 Subject: [PATCH 342/520] improve receive maximum in connect packet --- src/emqx_inflight.erl | 6 +++++- src/emqx_packet.erl | 5 +++++ src/emqx_protocol.erl | 43 +++++++++++++++++++++++++-------------- src/emqx_session.erl | 12 ++++++++--- src/emqx_sm.erl | 3 ++- test/emqx_mock_client.erl | 3 ++- test/emqx_sm_SUITE.erl | 2 +- 7 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/emqx_inflight.erl b/src/emqx_inflight.erl index 3353983d8..876052974 100644 --- a/src/emqx_inflight.erl +++ b/src/emqx_inflight.erl @@ -14,7 +14,7 @@ -module(emqx_inflight). --export([new/1, contain/2, lookup/2, insert/3, update/3, delete/2, values/1, +-export([new/1, contain/2, lookup/2, insert/3, update/3, update_size/2, delete/2, values/1, to_list/1, size/1, max_size/1, is_full/1, is_empty/1, window/1]). -type(max_size() :: pos_integer()). @@ -46,6 +46,10 @@ delete(Key, {?MODULE, MaxSize, Tree}) -> update(Key, Val, {?MODULE, MaxSize, Tree}) -> {?MODULE, MaxSize, gb_trees:update(Key, Val, Tree)}. +-spec(update_size(integer(), inflight()) -> inflight()). +update_size(MaxSize, {?MODULE, _OldMaxSize, Tree}) -> + {?MODULE, MaxSize, Tree}. + -spec(is_full(inflight()) -> boolean()). is_full({?MODULE, 0, _Tree}) -> false; diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index fc90cf492..c1bbbb6fd 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -61,6 +61,11 @@ validate(?PUBLISH_PACKET(_QoS, Topic, _, Properties, _)) -> ((not emqx_topic:wildcard(Topic)) orelse error(topic_name_invalid)) andalso validate_properties(?PUBLISH, Properties); +validate(?CONNECT_PACKET(#mqtt_packet_connect{properties = #{'Receive-Maximum' := 0}})) -> + error(protocol_error); +validate(?CONNECT_PACKET(#mqtt_packet_connect{properties = #{'Receive-Maximum' := _}})) -> + true; + validate(_Packet) -> true. diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 15a287116..753dc099b 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -208,11 +208,8 @@ received(Packet = ?PACKET(Type), PState) -> true -> {Packet1, PState1} = preprocess_properties(Packet, PState), process_packet(Packet1, inc_stats(recv, Type, PState1)); - {'EXIT', {topic_filters_invalid, _Stacktrace}} -> - deliver({disconnect, ?RC_PROTOCOL_ERROR}, PState), - {error, topic_filters_invalid, PState}; {'EXIT', {Reason, _Stacktrace}} -> - deliver({disconnect, ?RC_MALFORMED_PACKET}, PState), + deliver({disconnect, rc(Reason)}, PState), {error, Reason, PState} end. @@ -593,17 +590,25 @@ try_open_session(#pstate{zone = Zone, clean_start => CleanStart }, - case emqx_sm:open_session(maps:put(expiry_interval, if - ProtoVer =:= ?MQTT_PROTO_V5 -> - maps:get('Session-Expiry-Interval', ConnProps, 0); - true -> - case CleanStart of - true -> - 0; - false -> - emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) - end - end, SessAttrs)) of + MaxInflight = #{max_inflight => if + ProtoVer =:= ?MQTT_PROTO_V5 -> + maps:get('Receive-Maximum', ConnProps, 65535); + true -> + emqx_zone:get_env(Zone, max_inflight, 65535) + end}, + SessionExpiryInterval = #{expiry_interval => if + ProtoVer =:= ?MQTT_PROTO_V5 -> + maps:get('Session-Expiry-Interval', ConnProps, 0); + true -> + case CleanStart of + true -> + 0; + false -> + emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) + end + end}, + + case emqx_sm:open_session(maps:merge(SessAttrs, maps:merge(MaxInflight, SessionExpiryInterval))) of {ok, SPid} -> {ok, SPid, false}; Other -> Other @@ -782,6 +787,14 @@ 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)}. +rc(Reason) -> + case Reason of + protocol_error -> ?RC_PROTOCOL_ERROR; + topic_filters_invalid -> ?RC_TOPIC_FILTER_INVALID; + topic_name_invalid -> ?RC_TOPIC_NAME_INVALID; + _ -> ?RC_MALFORMED_PACKET + end. + %%----------------------------------------------------------------------------- %% Parse topic filters %%----------------------------------------------------------------------------- diff --git a/src/emqx_session.erl b/src/emqx_session.erl index ad3e66cc5..4d570bc08 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -47,7 +47,7 @@ -export([info/1, attrs/1]). -export([stats/1]). -export([resume/2, discard/2]). --export([update_expiry_interval/2]). +-export([update_expiry_interval/2, update_max_inflight/2]). -export([subscribe/2, subscribe/4]). -export([publish/3]). -export([puback/2, puback/3]). @@ -318,6 +318,9 @@ discard(SPid, ByPid) -> update_expiry_interval(SPid, Interval) -> gen_server:cast(SPid, {expiry_interval, Interval * 1000}). +update_max_inflight(SPid, MaxInflight) -> + gen_server:cast(SPid, {max_inflight, MaxInflight}). + -spec(close(spid()) -> ok). close(SPid) -> gen_server:call(SPid, close, infinity). @@ -331,10 +334,10 @@ init([Parent, #{zone := Zone, username := Username, conn_pid := ConnPid, clean_start := CleanStart, - expiry_interval := ExpiryInterval}]) -> + expiry_interval := ExpiryInterval, + max_inflight := MaxInflight}]) -> process_flag(trap_exit, true), true = link(ConnPid), - MaxInflight = get_env(Zone, max_inflight), IdleTimout = get_env(Zone, idle_timeout, 30000), State = #state{idle_timeout = IdleTimout, clean_start = CleanStart, @@ -543,6 +546,9 @@ handle_cast({resume, ConnPid}, State = #state{client_id = ClientId, handle_cast({expiry_interval, Interval}, State) -> {noreply, State#state{expiry_interval = Interval}}; +handle_cast({max_inflight, MaxInflight}, State) -> + {noreply, State#state{inflight = emqx_inflight:update_size(MaxInflight, State#state.inflight)}}; + handle_cast(Msg, State) -> emqx_logger:error("[Session] unexpected cast: ~p", [Msg]), {noreply, State}. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 36d416f3b..98046823f 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -56,10 +56,11 @@ open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid end, emqx_sm_locker:trans(ClientId, CleanStart); -open_session(SessAttrs = #{clean_start := false, client_id := ClientId, conn_pid := ConnPid}) -> +open_session(SessAttrs = #{clean_start := false, client_id := ClientId, conn_pid := ConnPid, max_inflight := MaxInflight}) -> ResumeStart = fun(_) -> case resume_session(ClientId, ConnPid) of {ok, SPid} -> + emqx_session:update_max_inflight(SPid, MaxInflight), {ok, SPid, true}; {error, not_found} -> emqx_session_sup:start_session(SessAttrs) diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index 4528239e6..0aa01458e 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -51,7 +51,8 @@ handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> conn_pid => ClientPid, clean_start => true, username => undefined, - expiry_interval => 0 + expiry_interval => 0, + max_inflight => 0 }, {ok, SessPid} = emqx_sm:open_session(Attrs), {reply, {ok, SessPid}, diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl index 6f9b92399..1f85d4724 100644 --- a/test/emqx_sm_SUITE.erl +++ b/test/emqx_sm_SUITE.erl @@ -25,7 +25,7 @@ t_open_close_session(_) -> emqx_ct_broker_helpers:run_setup_steps(), {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), Attrs = #{clean_start => true, client_id => <<"client">>, conn_pid => ClientPid, - zone => internal, username => <<"zhou">>, expiry_interval => 0}, + zone => internal, username => <<"zhou">>, expiry_interval => 0, max_inflight => 0}, {ok, SPid} = emqx_sm:open_session(Attrs), [{<<"client">>, SPid}] = emqx_sm:lookup_session(<<"client">>), SPid = emqx_sm:lookup_session_pid(<<"client">>), From 2d354ca88386ab360e0dc862fc4b6483262758e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 28 Sep 2018 10:23:37 +0800 Subject: [PATCH 343/520] Improve topic alias maximum in connect packet --- src/emqx_mqtt_caps.erl | 2 +- src/emqx_protocol.erl | 62 ++++++++++++---------- src/emqx_session.erl | 96 ++++++++++++++++++++--------------- src/emqx_sm.erl | 8 ++- test/emqx_mock_client.erl | 3 +- test/emqx_mqtt_caps_SUITE.erl | 3 +- test/emqx_sm_SUITE.erl | 2 +- 7 files changed, 102 insertions(+), 74 deletions(-) diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl index b8b7a5b3a..5f919d9c0 100644 --- a/src/emqx_mqtt_caps.erl +++ b/src/emqx_mqtt_caps.erl @@ -44,7 +44,7 @@ -define(PUBCAP_KEYS, [max_qos_allowed, mqtt_retain_available, - mqtt_topic_alias + max_topic_alias ]). -define(SUBCAP_KEYS, [max_qos_allowed, max_topic_levels, diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 753dc099b..88f07138d 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -574,13 +574,11 @@ maybe_assign_client_id(PState = #pstate{client_id = <<>>, ackprops = AckProps}) maybe_assign_client_id(PState) -> PState. -try_open_session(#pstate{zone = Zone, - proto_ver = ProtoVer, - client_id = ClientId, - conn_pid = ConnPid, - conn_props = ConnProps, - username = Username, - clean_start = CleanStart}) -> +try_open_session(PState = #pstate{zone = Zone, + client_id = ClientId, + conn_pid = ConnPid, + username = Username, + clean_start = CleanStart}) -> SessAttrs = #{ zone => Zone, @@ -590,30 +588,42 @@ try_open_session(#pstate{zone = Zone, clean_start => CleanStart }, - MaxInflight = #{max_inflight => if - ProtoVer =:= ?MQTT_PROTO_V5 -> - maps:get('Receive-Maximum', ConnProps, 65535); - true -> - emqx_zone:get_env(Zone, max_inflight, 65535) - end}, - SessionExpiryInterval = #{expiry_interval => if - ProtoVer =:= ?MQTT_PROTO_V5 -> - maps:get('Session-Expiry-Interval', ConnProps, 0); - true -> - case CleanStart of - true -> - 0; - false -> - emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) - end - end}, - - case emqx_sm:open_session(maps:merge(SessAttrs, maps:merge(MaxInflight, SessionExpiryInterval))) of + SessAttrs1 = lists:foldl(fun set_session_attrs/2, SessAttrs, [{max_inflight, PState}, {expiry_interval, PState}, {topic_alias_maximum, PState}]), + case emqx_sm:open_session(SessAttrs1) of {ok, SPid} -> {ok, SPid, false}; Other -> Other end. +set_session_attrs({max_inflight, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps}}, SessAttrs) -> + maps:put(max_inflight, if + ProtoVer =:= ?MQTT_PROTO_V5 -> + maps:get('Receive-Maximum', ConnProps, 65535); + true -> + emqx_zone:get_env(Zone, max_inflight, 65535) + end, SessAttrs); +set_session_attrs({expiry_interval, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps, clean_start = CleanStart}}, SessAttrs) -> + maps:put(expiry_interval, if + ProtoVer =:= ?MQTT_PROTO_V5 -> + maps:get('Session-Expiry-Interval', ConnProps, 0); + true -> + case CleanStart of + true -> 0; + false -> + emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) + end + end, SessAttrs); +set_session_attrs({topic_alias_maximum, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps}}, SessAttrs) -> + maps:put(topic_alias_maximum, if + ProtoVer =:= ?MQTT_PROTO_V5 -> + maps:get('Topic-Alias-Maximum', ConnProps, 0); + true -> + emqx_zone:get_env(Zone, max_topic_alias, 0) + end, SessAttrs); +set_session_attrs({_, #pstate{}}, SessAttrs) -> + SessAttrs. + + authenticate(Credentials, Password) -> case emqx_access_control:authenticate(Credentials, Password) of ok -> {ok, false}; diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 4d570bc08..4514debcb 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -47,7 +47,7 @@ -export([info/1, attrs/1]). -export([stats/1]). -export([resume/2, discard/2]). --export([update_expiry_interval/2, update_max_inflight/2]). +-export([update_expiry_interval/2, update_misc/2]). -export([subscribe/2, subscribe/4]). -export([publish/3]). -export([puback/2, puback/3]). @@ -145,7 +145,9 @@ enqueue_stats = 0, %% Created at - created_at :: erlang:timestamp() + created_at :: erlang:timestamp(), + + topic_alias_maximum :: pos_integer() }). -type(spid() :: pid()). @@ -318,8 +320,8 @@ discard(SPid, ByPid) -> update_expiry_interval(SPid, Interval) -> gen_server:cast(SPid, {expiry_interval, Interval * 1000}). -update_max_inflight(SPid, MaxInflight) -> - gen_server:cast(SPid, {max_inflight, MaxInflight}). +update_misc(SPid, Misc) -> + gen_server:cast(SPid, {update_misc, Misc}). -spec(close(spid()) -> ok). close(SPid) -> @@ -329,36 +331,38 @@ close(SPid) -> %% gen_server callbacks %%------------------------------------------------------------------------------ -init([Parent, #{zone := Zone, - client_id := ClientId, - username := Username, - conn_pid := ConnPid, - clean_start := CleanStart, - expiry_interval := ExpiryInterval, - max_inflight := MaxInflight}]) -> +init([Parent, #{zone := Zone, + client_id := ClientId, + username := Username, + conn_pid := ConnPid, + clean_start := CleanStart, + expiry_interval := ExpiryInterval, + max_inflight := MaxInflight, + topic_alias_maximum := TopicAliasMaximum}]) -> process_flag(trap_exit, true), true = link(ConnPid), IdleTimout = get_env(Zone, idle_timeout, 30000), - State = #state{idle_timeout = IdleTimout, - clean_start = CleanStart, - binding = binding(ConnPid), - client_id = ClientId, - username = Username, - conn_pid = ConnPid, - subscriptions = #{}, - max_subscriptions = get_env(Zone, max_subscriptions, 0), - upgrade_qos = get_env(Zone, upgrade_qos, false), - inflight = emqx_inflight:new(MaxInflight), - mqueue = init_mqueue(Zone), - retry_interval = get_env(Zone, retry_interval, 0), - awaiting_rel = #{}, - await_rel_timeout = get_env(Zone, await_rel_timeout), - max_awaiting_rel = get_env(Zone, max_awaiting_rel), - expiry_interval = ExpiryInterval, - enable_stats = get_env(Zone, enable_stats, true), - deliver_stats = 0, - enqueue_stats = 0, - created_at = os:timestamp() + State = #state{idle_timeout = IdleTimout, + clean_start = CleanStart, + binding = binding(ConnPid), + client_id = ClientId, + username = Username, + conn_pid = ConnPid, + subscriptions = #{}, + max_subscriptions = get_env(Zone, max_subscriptions, 0), + upgrade_qos = get_env(Zone, upgrade_qos, false), + inflight = emqx_inflight:new(MaxInflight), + mqueue = init_mqueue(Zone), + retry_interval = get_env(Zone, retry_interval, 0), + awaiting_rel = #{}, + await_rel_timeout = get_env(Zone, await_rel_timeout), + max_awaiting_rel = get_env(Zone, max_awaiting_rel), + expiry_interval = ExpiryInterval, + enable_stats = get_env(Zone, enable_stats, true), + deliver_stats = 0, + enqueue_stats = 0, + created_at = os:timestamp(), + topic_alias_maximum = TopicAliasMaximum }, emqx_sm:register_session(ClientId, attrs(State)), emqx_sm:set_session_stats(ClientId, stats(State)), @@ -546,8 +550,9 @@ handle_cast({resume, ConnPid}, State = #state{client_id = ClientId, handle_cast({expiry_interval, Interval}, State) -> {noreply, State#state{expiry_interval = Interval}}; -handle_cast({max_inflight, MaxInflight}, State) -> - {noreply, State#state{inflight = emqx_inflight:update_size(MaxInflight, State#state.inflight)}}; +handle_cast({update_misc, #{max_inflight := MaxInflight, topic_alias_maximum := TopicAliasMaximum}}, State) -> + {noreply, State#state{inflight = emqx_inflight:update_size(MaxInflight, State#state.inflight), + topic_alias_maximum = TopicAliasMaximum}}; handle_cast(Msg, State) -> emqx_logger:error("[Session] unexpected cast: ~p", [Msg]), @@ -560,15 +565,22 @@ handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> end, State, Msgs)}; %% Dispatch message -handle_info({dispatch, Topic, Msg}, State = #state{subscriptions = SubMap}) when is_record(Msg, message) -> - noreply(case maps:find(Topic, SubMap) of - {ok, #{nl := Nl, qos := QoS, rap := Rap, subid := SubId}} -> - run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}, {subid, SubId}], Msg, State); - {ok, #{nl := Nl, qos := QoS, rap := Rap}} -> - run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}], Msg, State); - error -> - dispatch(emqx_message:unset_flag(dup, Msg), State) - end); +handle_info({dispatch, Topic, Msg = #message{headers = Headers}}, + State = #state{subscriptions = SubMap, topic_alias_maximum = TopicAliasMaximum}) when is_record(Msg, message) -> + TopicAlias = maps:get('Topic-Alias', Headers, undefined), + if + TopicAlias =:= undefined orelse TopicAlias =< TopicAliasMaximum -> + noreply(case maps:find(Topic, SubMap) of + {ok, #{nl := Nl, qos := QoS, rap := Rap, subid := SubId}} -> + run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}, {subid, SubId}], Msg, State); + {ok, #{nl := Nl, qos := QoS, rap := Rap}} -> + run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}], Msg, State); + error -> + dispatch(emqx_message:unset_flag(dup, Msg), State) + end); + true -> + noreply(State) + end; %% Do nothing if the client has been disconnected. handle_info({timeout, Timer, retry_delivery}, State = #state{conn_pid = undefined, retry_timer = Timer}) -> diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 98046823f..1fa0b488b 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -56,11 +56,15 @@ open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid end, emqx_sm_locker:trans(ClientId, CleanStart); -open_session(SessAttrs = #{clean_start := false, client_id := ClientId, conn_pid := ConnPid, max_inflight := MaxInflight}) -> +open_session(SessAttrs = #{clean_start := false, + client_id := ClientId, + conn_pid := ConnPid, + max_inflight := MaxInflight, + topic_alias_maximum := TopicAliasMaximum}) -> ResumeStart = fun(_) -> case resume_session(ClientId, ConnPid) of {ok, SPid} -> - emqx_session:update_max_inflight(SPid, MaxInflight), + emqx_session:update_misc(SPid, #{max_inflight => MaxInflight, topic_alias_maximum => TopicAliasMaximum}), {ok, SPid, true}; {error, not_found} -> emqx_session_sup:start_session(SessAttrs) diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index 0aa01458e..4a49a1fc5 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -52,7 +52,8 @@ handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> clean_start => true, username => undefined, expiry_interval => 0, - max_inflight => 0 + max_inflight => 0, + topic_alias_maximum => 0 }, {ok, SessPid} = emqx_sm:open_session(Attrs), {reply, {ok, SessPid}, diff --git a/test/emqx_mqtt_caps_SUITE.erl b/test/emqx_mqtt_caps_SUITE.erl index d5751f9bb..919be5218 100644 --- a/test/emqx_mqtt_caps_SUITE.erl +++ b/test/emqx_mqtt_caps_SUITE.erl @@ -44,7 +44,8 @@ t_get_set_caps(_) -> end, PubCaps = #{ max_qos_allowed => ?QOS_2, - mqtt_retain_available => true + mqtt_retain_available => true, + max_topic_alias => 0 }, PubCaps = emqx_mqtt_caps:get_caps(zone, publish), NewPubCaps = PubCaps#{max_qos_allowed => ?QOS_1}, diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl index 1f85d4724..110e13026 100644 --- a/test/emqx_sm_SUITE.erl +++ b/test/emqx_sm_SUITE.erl @@ -25,7 +25,7 @@ t_open_close_session(_) -> emqx_ct_broker_helpers:run_setup_steps(), {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), Attrs = #{clean_start => true, client_id => <<"client">>, conn_pid => ClientPid, - zone => internal, username => <<"zhou">>, expiry_interval => 0, max_inflight => 0}, + zone => internal, username => <<"zhou">>, expiry_interval => 0, max_inflight => 0, topic_alias_maximum => 0}, {ok, SPid} = emqx_sm:open_session(Attrs), [{<<"client">>, SPid}] = emqx_sm:lookup_session(<<"client">>), SPid = emqx_sm:lookup_session_pid(<<"client">>), From 59798762a937f215f8cfd1adbef565429c4010bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 29 Sep 2018 09:19:33 +0800 Subject: [PATCH 344/520] Remove will message when received disconnect packet with reason code 0x00 --- src/emqx_protocol.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 88f07138d..1fb80c0ce 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -412,12 +412,14 @@ process_packet(?DISCONNECT_PACKET(?RC_SUCCESS, #{'Session-Expiry-Interval' := In case Interval =/= 0 andalso OldInterval =:= 0 of true -> deliver({disconnect, ?RC_PROTOCOL_ERROR}, PState), - {error, 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_packet(?DISCONNECT_PACKET(?RC_SUCCESS), PState) -> + {stop, normal, PState#pstate{will_msg = undefined}}; process_packet(?DISCONNECT_PACKET(_), PState) -> {stop, normal, PState}. From a77f8d28f9b50fcc0f98a41a6ec765e2d8e25a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 29 Sep 2018 10:10:46 +0800 Subject: [PATCH 345/520] Improve coverage --- test/emqx_packet_SUITE.erl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/emqx_packet_SUITE.erl b/test/emqx_packet_SUITE.erl index bd1b0cb4a..ec4205957 100644 --- a/test/emqx_packet_SUITE.erl +++ b/test/emqx_packet_SUITE.erl @@ -46,7 +46,12 @@ packet_type_name(_) -> packet_validate(_) -> ?assertEqual(true, emqx_packet:validate(?SUBSCRIBE_PACKET(15, #{'Subscription-Identifier' => 1}, [{<<"topic">>, #{qos => ?QOS0}}]))), ?assertEqual(true, emqx_packet:validate(?UNSUBSCRIBE_PACKET(89, [<<"topic">>]))), - ?assertEqual(true, emqx_packet:validate(?CONNECT_PACKET(#mqtt_packet_connect{}))). + ?assertEqual(true, emqx_packet:validate(?CONNECT_PACKET(#mqtt_packet_connect{}))), + ?assertEqual(true, emqx_packet:validate(?CONNECT_PACKET(#mqtt_packet_connect{properties = #{'Receive-Maximum' => 1}}))), + case catch emqx_packet:validate(?CONNECT_PACKET(#mqtt_packet_connect{properties = #{'Receive-Maximum' => 0}})) of + {'EXIT', {protocol_error, _}} -> ?assertEqual(true, true); + true -> ?assertEqual(true, false) + end. packet_message(_) -> Pkt = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, From 1638aff0f9c744a46e2b485bad2e77135e89befd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 29 Sep 2018 14:30:48 +0800 Subject: [PATCH 346/520] Remove duplicate match --- src/emqx_packet.erl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index c1bbbb6fd..1d1e29c6a 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -63,8 +63,6 @@ validate(?PUBLISH_PACKET(_QoS, Topic, _, Properties, _)) -> validate(?CONNECT_PACKET(#mqtt_packet_connect{properties = #{'Receive-Maximum' := 0}})) -> error(protocol_error); -validate(?CONNECT_PACKET(#mqtt_packet_connect{properties = #{'Receive-Maximum' := _}})) -> - true; validate(_Packet) -> true. From cb85c5fea019c3cead216b89de14f85c6ea186d1 Mon Sep 17 00:00:00 2001 From: HuangDan Date: Sat, 29 Sep 2018 18:58:25 +0800 Subject: [PATCH 347/520] Upgrade the esockd library to v5.4.2 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 549237e67..368aff6a9 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.2.0 dep_lager = git https://github.com/erlang-lager/lager 3.6.5 -dep_esockd = git https://github.com/emqx/esockd v5.4.1 +dep_esockd = git https://github.com/emqx/esockd v5.4.2 dep_ekka = git https://github.com/emqx/ekka v0.4.1 dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 dep_clique = git https://github.com/emqx/clique develop From 1705f25db638aa60c5a5da8c95a1cd8acee3cab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 29 Sep 2018 21:28:28 +0800 Subject: [PATCH 348/520] Fix bug in session expiry interval --- src/emqx_session.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 4514debcb..9a5689401 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -127,7 +127,7 @@ await_rel_timer :: reference() | undefined, %% Session Expiry Interval - expiry_interval = 7200000 :: timeout(), + expiry_interval = 7200 :: timeout(), %% Expired Timer expiry_timer :: reference() | undefined, @@ -318,7 +318,7 @@ discard(SPid, ByPid) -> -spec(update_expiry_interval(spid(), timeout()) -> ok). update_expiry_interval(SPid, Interval) -> - gen_server:cast(SPid, {expiry_interval, Interval * 1000}). + gen_server:cast(SPid, {expiry_interval, Interval}). update_misc(SPid, Misc) -> gen_server:cast(SPid, {update_misc, Misc}). From e3f2ae8db8753c6e21c3a32773e13ddc931321f2 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sat, 22 Sep 2018 08:53:49 +0200 Subject: [PATCH 349/520] Change from customized total heap size check to set process flag The `max_heap_size` process flag can be used to limit total heap size of a process, and it gives much more detailed crash log if the limit is hit. --- priv/emqx.schema | 6 +++--- src/emqx_connection.erl | 2 +- src/emqx_misc.erl | 30 ++++++++++++++++-------------- src/emqx_session.erl | 2 +- test/emqx_misc_tests.erl | 16 ++++++---------- 5 files changed, 27 insertions(+), 29 deletions(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index 2450a6876..42b894401 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -835,7 +835,6 @@ end}. %% connection/session process. %% Message queue here is the Erlang process mailbox, but not the number %% of queued MQTT messages of QoS 1 and 2. -%% Total heap size is the in Erlang 'words' not in 'bytes'. %% Zero or negative is to disable. {mapping, "zone.$name.force_shutdown_policy", "emqx.zones", [ {default, "0 | 0MB"}, @@ -868,7 +867,8 @@ end}. {error, Reason} -> error(Reason); Bytes1 -> - #{bytes => Bytes1, count => list_to_integer(Count)} + #{bytes => Bytes1, + count => list_to_integer(Count)} end, {force_gc_policy, GcPolicy}; ("force_shutdown_policy", Val) -> @@ -878,7 +878,7 @@ end}. error(Reason); Siz1 -> #{message_queue_len => list_to_integer(Len), - total_heap_size => Siz1} + max_heap_size => Siz1} end, {force_shutdown_policy, ShutdownPolicy}; (Opt, Val) -> diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 3176ad443..ccb5f59fa 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -152,7 +152,7 @@ init([Transport, RawSocket, Options]) -> }), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), ok = emqx_gc:init(GcPolicy), - erlang:put(force_shutdown_policy, emqx_zone:get_env(Zone, force_shutdown_policy)), + ok = emqx_misc:init_proc_mng_policy(Zone), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index 656b0fca3..03c42510c 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -15,7 +15,9 @@ -module(emqx_misc). -export([merge_opts/2, start_timer/2, start_timer/3, cancel_timer/1, - proc_name/2, proc_stats/0, proc_stats/1, conn_proc_mng_policy/1]). + proc_name/2, proc_stats/0, proc_stats/1]). + +-export([init_proc_mng_policy/1, conn_proc_mng_policy/1]). %% @doc Merge options -spec(merge_opts(list(), list()) -> list()). @@ -60,32 +62,35 @@ proc_stats(Pid) -> -define(DISABLED, 0). +init_proc_mng_policy(Zone) -> + #{max_heap_size := MaxHeapSizeInBytes} = ShutdownPolicy = + emqx_zone:get_env(Zone, force_shutdown_policy), + MaxHeapSize = MaxHeapSizeInBytes div erlang:system_info(wordsize), + _ = erlang:process_flag(max_heap_size, MaxHeapSize), % zero is discarded + erlang:put(force_shutdown_policy, ShutdownPolicy), + ok. + %% @doc Check self() process status against connection/session process management policy, %% return `continue | hibernate | {shutdown, Reason}' accordingly. %% `continue': There is nothing out of the ordinary. %% `hibernate': Nothing to process in my mailbox, and since this check is triggered %% by a timer, we assume it is a fat chance to continue idel, hence hibernate. -%% `shutdown': Some numbers (message queue length or heap size have hit the limit), +%% `shutdown': Some numbers (message queue length hit the limit), %% hence shutdown for greater good (system stability). --spec(conn_proc_mng_policy(#{message_queue_len := integer(), - total_heap_size := integer() - } | undefined) -> continue | hibernate | {shutdown, _}). -conn_proc_mng_policy(#{message_queue_len := MaxMsgQueueLen, - total_heap_size := MaxTotalHeapSize - }) -> +-spec(conn_proc_mng_policy(#{message_queue_len => integer()} | false) -> + continue | hibernate | {shutdown, _}). +conn_proc_mng_policy(#{message_queue_len := MaxMsgQueueLen}) -> Qlength = proc_info(message_queue_len), Checks = [{fun() -> is_message_queue_too_long(Qlength, MaxMsgQueueLen) end, {shutdown, message_queue_too_long}}, - {fun() -> is_heap_size_too_large(MaxTotalHeapSize) end, - {shutdown, total_heap_size_too_large}}, {fun() -> Qlength > 0 end, continue}, {fun() -> true end, hibernate} ], check(Checks); conn_proc_mng_policy(_) -> %% disable by default - conn_proc_mng_policy(#{message_queue_len => 0, total_heap_size => 0}). + conn_proc_mng_policy(#{message_queue_len => 0}). check([{Pred, Result} | Rest]) -> case Pred() of @@ -96,9 +101,6 @@ check([{Pred, Result} | Rest]) -> is_message_queue_too_long(Qlength, Max) -> is_enabled(Max) andalso Qlength > Max. -is_heap_size_too_large(Max) -> - is_enabled(Max) andalso proc_info(total_heap_size) > Max. - is_enabled(Max) -> is_integer(Max) andalso Max > ?DISABLED. proc_info(Key) -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 9a5689401..d327723f5 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -369,7 +369,7 @@ init([Parent, #{zone := Zone, emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), ok = emqx_gc:init(GcPolicy), - erlang:put(force_shutdown_policy, emqx_zone:get_env(Zone, force_shutdown_policy)), + 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). diff --git a/test/emqx_misc_tests.erl b/test/emqx_misc_tests.erl index 40a9aae87..50513ee86 100644 --- a/test/emqx_misc_tests.erl +++ b/test/emqx_misc_tests.erl @@ -24,23 +24,19 @@ timer_cancel_flush_test() -> shutdown_disabled_test() -> self() ! foo, - ?assertEqual(continue, conn_proc_mng_policy(0, 0)), + ?assertEqual(continue, conn_proc_mng_policy(0)), receive foo -> ok end, - ?assertEqual(hibernate, conn_proc_mng_policy(0, 0)). + ?assertEqual(hibernate, conn_proc_mng_policy(0)). message_queue_too_long_test() -> self() ! foo, self() ! bar, ?assertEqual({shutdown, message_queue_too_long}, - conn_proc_mng_policy(1, 0)), + conn_proc_mng_policy(1)), receive foo -> ok end, - ?assertEqual(continue, conn_proc_mng_policy(1, 0)), + ?assertEqual(continue, conn_proc_mng_policy(1)), receive bar -> ok end. -total_heap_size_too_large_test() -> - ?assertEqual({shutdown, total_heap_size_too_large}, - conn_proc_mng_policy(0, 1)). +conn_proc_mng_policy(L) -> + emqx_misc:conn_proc_mng_policy(#{message_queue_len => L}). -conn_proc_mng_policy(L, S) -> - emqx_misc:conn_proc_mng_policy(#{message_queue_len => L, - total_heap_size => S}). From e5f977d808f3d9a77c4d564056ab2586352a630e Mon Sep 17 00:00:00 2001 From: HuangDan Date: Sat, 29 Sep 2018 22:16:54 +0800 Subject: [PATCH 350/520] Upgrade the esockd library --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index f94258e6b..63b3a7595 100644 --- a/rebar.config +++ b/rebar.config @@ -10,7 +10,7 @@ [{gen_rpc, "2.2.0"}, {ekka, "v0.4.1"}, {clique, "develop"}, - {esockd, "v5.4.1"}, + {esockd, "v5.4.2"}, {cuttlefish, "emqx30"} ]}. From b25dbd866b6200e5bfedd3a431c978e4f1ab95f5 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 4 Oct 2018 12:17:28 +0800 Subject: [PATCH 351/520] Update README.md --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b45ac510d..f8872e8a0 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,18 @@ 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/emqtt/emqttd/releases/). +- 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://emqtt.io). - ## Installation The *EMQ X* broker is cross-platform, which can be deployed on Linux, Unix, Mac, Windows and even Raspberry Pi. Download the binary package for your platform from [here](http://emqtt.io/downloads). -- [Single Node Install](http://emqtt.io/docs/v2/install.html) -- [Multi Node Install](http://emqtt.io/docs/v2/cluster.html) +- [Single Node Install](http://emqtt.io/docs/v3/install.html) +- [Multi Node Install](http://emqtt.io/docs/v3/cluster.html) ## Build From Source @@ -49,7 +48,7 @@ cd _rel/emqx && ./bin/emqx console ## Roadmap -The [EMQX roadmap uses Github milestones](https://github.com/emqtt/emqttd/milestones) to track the progress of the project. +The [EMQ X Roadmap uses Github milestones](https://github.com/emqx/emqx/milestones) to track the progress of the project. ## Community, discussion, contribution, and support @@ -62,7 +61,7 @@ You can reach the EMQ community and developers via the following channels: - [Forum](https://groups.google.com/d/forum/emqtt) - [Blog](https://medium.com/@emqtt) -Please submit any bugs, issues, and feature requests to [emqtt/emqttd](//github.com/emqtt/emqttd/issues). +Please submit any bugs, issues, and feature requests to [emqx/emqx](https://github.com/emqx/emqx/issues). ## License From 52eae659832ba529f97290e1ce0572cea66b5939 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Mon, 8 Oct 2018 20:02:32 +0800 Subject: [PATCH 352/520] Fix topic_name validation bug Prior to this change, Prior to this change, the validation for the mqtt5.0 publish packet which both contains zero-length topic name and topic alias is wrong. This change fix this bug. --- src/emqx_packet.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 1d1e29c6a..7c51688dc 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -55,6 +55,8 @@ validate(?UNSUBSCRIBE_PACKET(PacketId, TopicFilters)) -> validate_packet_id(PacketId) andalso ok == lists:foreach(fun emqx_topic:validate/1, TopicFilters); +validate(?PUBLISH_PACKET(_QoS, <<>>, #{'Topic-Alias':= _I}, _)) -> + true; validate(?PUBLISH_PACKET(_QoS, <<>>, _, _, _)) -> error(topic_name_invalid); validate(?PUBLISH_PACKET(_QoS, Topic, _, Properties, _)) -> From 9bcd4c3e0815a2756b3dbb1191418c0fd1c21453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Tue, 9 Oct 2018 13:35:27 +0800 Subject: [PATCH 353/520] improve will message --- src/emqx_protocol.erl | 31 ++++++++++++++++++++++++------- src/emqx_session.erl | 2 +- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 1fb80c0ce..0a5858e44 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -260,6 +260,7 @@ process_packet(?CONNECT_PACKET( clean_start = CleanStart, keepalive = Keepalive, properties = ConnProps, + will_props = WillProps, will_topic = WillTopic, client_id = ClientId, username = Username, @@ -267,7 +268,16 @@ process_packet(?CONNECT_PACKET( %% TODO: Mountpoint... %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) - WillMsg = emqx_packet:will_msg(Connect), + Connect1 = if + ProtoVer =:= ?MQTT_PROTO_V5 -> + WillDelayInterval = get_property('Will-Delay-Interval', WillProps, 0), + SessionExpiryInterval = get_property('Session-Expiry-Interval', ConnProps, 0), + WillProps1 = set_property('Will-Delay-Interval', erlang:min(SessionExpiryInterval, WillDelayInterval), WillProps), + Connect#mqtt_packet_connect{will_props = WillProps1}; + true -> + Connect + end, + WillMsg = emqx_packet:will_msg(Connect1), PState1 = set_username(Username, PState#pstate{client_id = ClientId, @@ -642,6 +652,11 @@ set_property(Name, Value, undefined) -> set_property(Name, Value, Props) -> Props#{Name => Value}. +get_property(_Name, undefined, Default) -> + Default; +get_property(Name, Props, Default) -> + maps:get(Name, Props, Default). + %%------------------------------------------------------------------------------ %% Check Packet %%------------------------------------------------------------------------------ @@ -777,20 +792,22 @@ shutdown(Reason, #pstate{client_id = ClientId}) when Reason =:= conflict; emqx_cm:unregister_connection(ClientId); shutdown(Reason, PState = #pstate{connected = true, client_id = ClientId, - will_msg = WillMsg}) -> + will_msg = WillMsg, + session = Session}) -> ?LOG(info, "Shutdown for ~p", [Reason], PState), - _ = send_willmsg(WillMsg), + _ = send_willmsg(WillMsg, Session), emqx_hooks:run('client.disconnected', [credentials(PState), Reason]), emqx_cm:unregister_connection(ClientId). -send_willmsg(undefined) -> +send_willmsg(undefined, _Session) -> ignore; send_willmsg(WillMsg = #message{topic = Topic, - headers = #{'Will-Delay-Interval' := Interval}}) + headers = #{'Will-Delay-Interval' := Interval}}, Session) when is_integer(Interval), Interval > 0 -> SendAfter = integer_to_binary(Interval), - emqx_broker:publish(WillMsg#message{topic = <<"$delayed/", SendAfter/binary, "/", Topic/binary>>}); -send_willmsg(WillMsg) -> + Session1 = list_to_binary(pid_to_list(Session)), + emqx_broker:publish(WillMsg#message{topic = <<"$will/", Session1/binary, "/", SendAfter/binary, "/", Topic/binary>>}); +send_willmsg(WillMsg, _Session) -> emqx_broker:publish(WillMsg). start_keepalive(0, _PState) -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index d327723f5..b63b45b18 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -542,7 +542,7 @@ handle_cast({resume, ConnPid}, State = #state{client_id = ClientId, %% Clean Session: true -> false??? CleanStart andalso emqx_sm:set_session_attrs(ClientId, attrs(State1)), - emqx_hooks:run('session.resumed', [#{client_id => ClientId}, attrs(State)]), + emqx_hooks:run('session.resumed', [#{client_id => ClientId, session => self()}, attrs(State)]), %% Replay delivery and Dequeue pending messages noreply(dequeue(retry_delivery(true, State1))); From b80ba6e458b97d44baf53ce286a7bcad9632dfe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Tue, 9 Oct 2018 14:19:22 +0800 Subject: [PATCH 354/520] Fix bug when ConnProps is undefined --- src/emqx_protocol.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 0a5858e44..578417085 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -610,14 +610,14 @@ try_open_session(PState = #pstate{zone = Zone, set_session_attrs({max_inflight, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps}}, SessAttrs) -> maps:put(max_inflight, if ProtoVer =:= ?MQTT_PROTO_V5 -> - maps:get('Receive-Maximum', ConnProps, 65535); + get_property('Receive-Maximum', ConnProps, 65535); true -> emqx_zone:get_env(Zone, max_inflight, 65535) end, SessAttrs); set_session_attrs({expiry_interval, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps, clean_start = CleanStart}}, SessAttrs) -> maps:put(expiry_interval, if ProtoVer =:= ?MQTT_PROTO_V5 -> - maps:get('Session-Expiry-Interval', ConnProps, 0); + get_property('Session-Expiry-Interval', ConnProps, 0); true -> case CleanStart of true -> 0; @@ -628,7 +628,7 @@ set_session_attrs({expiry_interval, #pstate{zone = Zone, proto_ver = ProtoVer, c set_session_attrs({topic_alias_maximum, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps}}, SessAttrs) -> maps:put(topic_alias_maximum, if ProtoVer =:= ?MQTT_PROTO_V5 -> - maps:get('Topic-Alias-Maximum', ConnProps, 0); + get_property('Topic-Alias-Maximum', ConnProps, 0); true -> emqx_zone:get_env(Zone, max_topic_alias, 0) end, SessAttrs); From d36a34fb5988dbad7a205fcf6eac08ea919f539c Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Tue, 9 Oct 2018 17:45:40 +0800 Subject: [PATCH 355/520] Fix topic_name validation bug Prior to this change, Prior to this change, the validation for the mqtt5.0 publish packet which both contains zero-length topic name and topic alias is wrong. --- src/emqx_packet.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 1d1e29c6a..1db586cea 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -55,6 +55,8 @@ validate(?UNSUBSCRIBE_PACKET(PacketId, TopicFilters)) -> validate_packet_id(PacketId) andalso ok == lists:foreach(fun emqx_topic:validate/1, TopicFilters); +validate(?PUBLISH_PACKET(_QoS, <<>>, _, #{'Topic-Alias':= _I}, _)) -> + true; validate(?PUBLISH_PACKET(_QoS, <<>>, _, _, _)) -> error(topic_name_invalid); validate(?PUBLISH_PACKET(_QoS, Topic, _, Properties, _)) -> From 29beb42aa2981f91b7482fda6a9f8755d48bf2a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Wed, 10 Oct 2018 14:00:17 +0800 Subject: [PATCH 356/520] Using client id rather then session pid --- src/emqx_protocol.erl | 14 ++++++-------- src/emqx_session.erl | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 578417085..bd440cf39 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -792,22 +792,20 @@ shutdown(Reason, #pstate{client_id = ClientId}) when Reason =:= conflict; emqx_cm:unregister_connection(ClientId); shutdown(Reason, PState = #pstate{connected = true, client_id = ClientId, - will_msg = WillMsg, - session = Session}) -> + will_msg = WillMsg}) -> ?LOG(info, "Shutdown for ~p", [Reason], PState), - _ = send_willmsg(WillMsg, Session), + _ = send_willmsg(WillMsg, ClientId), emqx_hooks:run('client.disconnected', [credentials(PState), Reason]), emqx_cm:unregister_connection(ClientId). -send_willmsg(undefined, _Session) -> +send_willmsg(undefined, _ClientId) -> ignore; send_willmsg(WillMsg = #message{topic = Topic, - headers = #{'Will-Delay-Interval' := Interval}}, Session) + headers = #{'Will-Delay-Interval' := Interval} = Headers}, ClientId) when is_integer(Interval), Interval > 0 -> SendAfter = integer_to_binary(Interval), - Session1 = list_to_binary(pid_to_list(Session)), - emqx_broker:publish(WillMsg#message{topic = <<"$will/", Session1/binary, "/", SendAfter/binary, "/", Topic/binary>>}); -send_willmsg(WillMsg, _Session) -> + emqx_broker:publish(WillMsg#message{topic = emqx_topic:join([<<"$will">>, SendAfter, Topic]), headers = Headers#{client_id => ClientId}}); +send_willmsg(WillMsg, _ClientId) -> emqx_broker:publish(WillMsg). start_keepalive(0, _PState) -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index b63b45b18..d327723f5 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -542,7 +542,7 @@ handle_cast({resume, ConnPid}, State = #state{client_id = ClientId, %% Clean Session: true -> false??? CleanStart andalso emqx_sm:set_session_attrs(ClientId, attrs(State1)), - emqx_hooks:run('session.resumed', [#{client_id => ClientId, session => self()}, attrs(State)]), + emqx_hooks:run('session.resumed', [#{client_id => ClientId}, attrs(State)]), %% Replay delivery and Dequeue pending messages noreply(dequeue(retry_delivery(true, State1))); From 12da23066231a5712de606fa491476987b64adaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Wed, 10 Oct 2018 15:53:01 +0800 Subject: [PATCH 357/520] Increase coverage --- test/emqx_keepalive_SUITE.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/emqx_keepalive_SUITE.erl b/test/emqx_keepalive_SUITE.erl index c4dbd80f2..60472fd42 100644 --- a/test/emqx_keepalive_SUITE.erl +++ b/test/emqx_keepalive_SUITE.erl @@ -26,6 +26,7 @@ groups() -> [{keepalive, [], [t_keepalive]}]. %%-------------------------------------------------------------------- t_keepalive(_) -> + {ok, _} = emqx_keepalive:start(fun() -> {ok, 1} end, 0, {keepalive, timeout}), {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), [resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). From 6ffd0ac44f5fbf665efb869e2994549952bd1f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Wed, 10 Oct 2018 18:35:47 +0800 Subject: [PATCH 358/520] Increase coverage for emqx_protocol --- test/emqx_SUITE.erl | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index 166d64a30..ddff04490 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -39,6 +39,13 @@ 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). @@ -67,6 +74,7 @@ groups() -> [ 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 @@ -110,6 +118,14 @@ mqtt_connect_with_tcp(_) -> {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_serialise(?CLIENT3), + emqx_client_sock:send(Sock, Packet), + emqx_client_sock:close(Sock). + mqtt_connect_with_ssl_oneway(_) -> emqx:shutdown(), emqx_ct_broker_helpers:change_opts(ssl_oneway), From c89079261393f9a94599c9c3367ab0a8d21cc590 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Wed, 17 Oct 2018 17:24:05 +0800 Subject: [PATCH 359/520] Fix the init_proc_mng_policy bug Prior to this change, when the plugin like emqx_sn_gateway which has no zone run the init_proc_mng_policy function, it would trigger error and application crash. This change add a case to avoid crash. --- src/emqx_misc.erl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index 03c42510c..852113a8c 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -63,8 +63,14 @@ proc_stats(Pid) -> -define(DISABLED, 0). init_proc_mng_policy(Zone) -> - #{max_heap_size := MaxHeapSizeInBytes} = ShutdownPolicy = - emqx_zone:get_env(Zone, force_shutdown_policy), + #{max_heap_size := MaxHeapSizeInBytes} + = ShutdownPolicy + = case Zone of + undefined -> + #{max_heap_size => 0}; + _ -> + emqx_zone:get_env(Zone, force_shutdown_policy) + end, MaxHeapSize = MaxHeapSizeInBytes div erlang:system_info(wordsize), _ = erlang:process_flag(max_heap_size, MaxHeapSize), % zero is discarded erlang:put(force_shutdown_policy, ShutdownPolicy), @@ -106,4 +112,3 @@ is_enabled(Max) -> is_integer(Max) andalso Max > ?DISABLED. proc_info(Key) -> {Key, Value} = erlang:process_info(self(), Key), Value. - From 599121052acdc3b24e74877dfba3fc572f4ee1f2 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 18 Oct 2018 10:41:01 +0800 Subject: [PATCH 360/520] Add test case for init_proc_mng_policy --- test/emqx_misc_SUITE.erl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/emqx_misc_SUITE.erl b/test/emqx_misc_SUITE.erl index 766691869..87f51746a 100644 --- a/test/emqx_misc_SUITE.erl +++ b/test/emqx_misc_SUITE.erl @@ -25,7 +25,7 @@ {backlog, 512}, {nodelay, true}]). -all() -> [t_merge_opts]. +all() -> [t_merge_opts, t_init_proc_mng_policy]. t_merge_opts(_) -> Opts = emqx_misc:merge_opts(?SOCKOPTS, [raw, @@ -43,3 +43,10 @@ t_merge_opts(_) -> {nodelay, false}, {packet, raw}, {reuseaddr, true}] = lists:sort(Opts). + +t_init_proc_mng_policy(_) -> + application:set_env(emqx, zones, [{policy, [{force_shutdown_policy, #{max_heap_size => 1}}]}]), + {ok, _} = emqx_zone:start_link(), + ok = emqx_misc:init_proc_mng_policy(policy), + ok = emqx_misc:init_proc_mng_policy(undefined), + emqx_zone:stop(). From a748e8f1d8a0ebcefcaccf736057fa0659a1be44 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 18 Oct 2018 13:24:06 +0800 Subject: [PATCH 361/520] Refactor send_fun in protocol and other connection module Prior to this change, in the send function, the packet is forced to use emqx:serialize to serialize packet, it is a wrong design because other plugins which need to transform the mqtt packet to other packets can not use their own serialize function to serialize packet. This change solve the problem issued above. --- src/emqx_connection.erl | 4 ++-- src/emqx_frame.erl | 3 +-- src/emqx_protocol.erl | 22 +++++++++++----------- src/emqx_ws_connection.erl | 4 ++-- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index ccb5f59fa..37665ac10 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -165,7 +165,8 @@ init_limiter({Rate, Burst}) -> esockd_rate_limit:new(Rate, Burst). send_fun(Transport, Socket, Peername) -> - fun(Data) -> + fun(Serialize, Packet, Options) -> + Data = Serialize(Packet, Options), try Transport:async_send(Socket, Data) of ok -> ?LOG(debug, "SEND ~p", [iolist_to_binary(Data)], #state{peername = Peername}), @@ -408,4 +409,3 @@ maybe_gc(#state{}, {publish, _PacketId, #message{payload = Payload}}) -> ok = emqx_gc:inc(1, Oct); maybe_gc(_, _) -> ok. - diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index aa7aad064..075f0a11e 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -130,7 +130,7 @@ parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) -> WillQoS : 2, WillFlag : 1, CleanStart : 1, - 0 : 1, + 0 : 1, KeepAlive : 16/big, Rest2/binary>> = Rest1, @@ -634,4 +634,3 @@ fixqos(?PUBREL, 0) -> 1; fixqos(?SUBSCRIBE, 0) -> 1; fixqos(?UNSUBSCRIBE, 0) -> 1; fixqos(_Type, QoS) -> QoS. - diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 1fb80c0ce..93a1a8b64 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -407,13 +407,13 @@ process_packet(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), process_packet(?PACKET(?PINGREQ), PState) -> send(?PACKET(?PINGRESP), PState); -process_packet(?DISCONNECT_PACKET(?RC_SUCCESS, #{'Session-Expiry-Interval' := Interval}), +process_packet(?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 -> + true -> deliver({disconnect, ?RC_PROTOCOL_ERROR}, PState), {error, protocol_error, PState#pstate{will_msg = undefined}}; - false -> + false -> emqx_session:update_expiry_interval(SPid, Interval), %% Clean willmsg {stop, normal, PState#pstate{will_msg = undefined}} @@ -495,13 +495,13 @@ deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, 'Subscription-Identifier-Available' => 1, 'Shared-Subscription-Available' => flag(Shared)}, - Props1 = if - MaxQoS =:= ?QOS_2 -> + Props1 = if + MaxQoS =:= ?QOS_2 -> Props; true -> maps:put('Maximum-QoS', MaxQoS, Props) end, - + Props2 = if IsAssigned -> Props1#{'Assigned-Client-Identifier' => ClientId}; true -> Props1 @@ -555,7 +555,7 @@ deliver({disconnect, _ReasonCode}, PState) -> -spec(send(emqx_mqtt_types:packet(), state()) -> {ok, state()} | {error, term()}). send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = SendFun}) -> trace(send, Packet, PState), - case SendFun(emqx_frame:serialize(Packet, #{version => Ver})) of + case SendFun(fun emqx_frame:serialize/2, Packet, #{version => Ver}) of ok -> emqx_metrics:sent(Packet), {ok, inc_stats(send, Type, PState)}; @@ -601,17 +601,17 @@ set_session_attrs({max_inflight, #pstate{zone = Zone, proto_ver = ProtoVer, conn maps:put(max_inflight, if ProtoVer =:= ?MQTT_PROTO_V5 -> maps:get('Receive-Maximum', ConnProps, 65535); - true -> + true -> emqx_zone:get_env(Zone, max_inflight, 65535) end, SessAttrs); set_session_attrs({expiry_interval, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps, clean_start = CleanStart}}, SessAttrs) -> maps:put(expiry_interval, if ProtoVer =:= ?MQTT_PROTO_V5 -> maps:get('Session-Expiry-Interval', ConnProps, 0); - true -> + true -> case CleanStart of true -> 0; - false -> + false -> emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) end end, SessAttrs); @@ -619,7 +619,7 @@ set_session_attrs({topic_alias_maximum, #pstate{zone = Zone, proto_ver = ProtoVe maps:put(topic_alias_maximum, if ProtoVer =:= ?MQTT_PROTO_V5 -> maps:get('Topic-Alias-Maximum', ConnProps, 0); - true -> + true -> emqx_zone:get_env(Zone, max_topic_alias, 0) end, SessAttrs); set_session_attrs({_, #pstate{}}, SessAttrs) -> diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index fa08fa1bb..1907695af 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -144,7 +144,8 @@ websocket_init(#state{request = Req, options = Options}) -> idle_timeout = IdleTimout}}. send_fun(WsPid) -> - fun(Data) -> + fun(Serialize, Packet, Options) -> + Data = Serialize(Packet, Options), BinSize = iolist_size(Data), emqx_metrics:inc('bytes/sent', BinSize), put(send_oct, get(send_oct) + BinSize), @@ -299,4 +300,3 @@ stop(Error, State) -> wsock_stats() -> [{Key, get(Key)} || Key <- ?SOCK_STATS]. - From df713959ab3c06983c1687d6ad5a2eed6c684fa0 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 18 Oct 2018 13:57:27 +0800 Subject: [PATCH 362/520] Refactor init_proc_mng_policy. If there is no zone, it is unnecessary to add proc_mng_policy. --- src/emqx_misc.erl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index 852113a8c..cf4a555ca 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -62,15 +62,11 @@ proc_stats(Pid) -> -define(DISABLED, 0). +init_proc_mng_policy(undefined) -> ok; init_proc_mng_policy(Zone) -> #{max_heap_size := MaxHeapSizeInBytes} = ShutdownPolicy - = case Zone of - undefined -> - #{max_heap_size => 0}; - _ -> - emqx_zone:get_env(Zone, force_shutdown_policy) - end, + = emqx_zone:get_env(Zone, force_shutdown_policy), MaxHeapSize = MaxHeapSizeInBytes div erlang:system_info(wordsize), _ = erlang:process_flag(max_heap_size, MaxHeapSize), % zero is discarded erlang:put(force_shutdown_policy, ShutdownPolicy), From 387f2468c0fc509d828545db6f26cf773b5f93c0 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 18 Oct 2018 15:01:45 +0800 Subject: [PATCH 363/520] Add SendFun case for emqx-sn --- src/emqx_protocol.erl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 93a1a8b64..3638e94e2 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -562,6 +562,9 @@ send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = SendFun {binary, _Data} -> emqx_metrics:sent(Packet), {ok, inc_stats(send, Type, PState)}; + {datagram, _Peer, _Data} -> + emqx_metrics:sent(Packet), + {ok, inc_stats(send, Type, PState)}; {error, Reason} -> {error, Reason} end. From 4082f3ade2f83f4f3766b1d0e8b5ac18ef28b060 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sat, 13 Oct 2018 11:41:19 +0200 Subject: [PATCH 364/520] Improve stats test Before this change, the stats callback provided by emqx_broker_helper was an anonymous function with module local context. This commit changes it to a full fun M:F/A style callback for: 1. More robust to hot beam reload 2. Faster/smaller variable to construct 3. Easier test --- Makefile | 2 +- src/emqx_broker_helper.erl | 21 +++++---- src/emqx_stats.erl | 29 +++++++++---- test/emqx_stats_SUITE.erl | 56 ------------------------ test/emqx_stats_tests.erl | 89 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 74 deletions(-) delete mode 100644 test/emqx_stats_SUITE.erl create mode 100644 test/emqx_stats_tests.erl diff --git a/Makefile b/Makefile index 368aff6a9..de4a2c8c0 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ EUNIT_OPTS = verbose CT_SUITES = emqx emqx_zone emqx_banned emqx_connection emqx_session emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight \ emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ emqx_mqtt_compat emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ - emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm \ + emqx_tables emqx_time emqx_topic emqx_trie emqx_vm \ emqx_mountpoint emqx_listeners emqx_protocol emqx_pool emqx_shared_sub CT_NODE_NAME = emqxct@127.0.0.1 diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index fecf98a7b..e597a233e 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -19,6 +19,9 @@ -export([start_link/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +%% internal export +-export([stats_fun/0]). + -define(HELPER, ?MODULE). -record(state, {}). @@ -32,7 +35,9 @@ start_link() -> %%------------------------------------------------------------------------------ init([]) -> - emqx_stats:update_interval(broker_stats, stats_fun()), + %% Use M:F/A for callback, not anonymous function because + %% fun M:F/A is small, also no badfun risk during hot beam reload + emqx_stats:update_interval(broker_stats, fun ?MODULE:stats_fun/0), {ok, #state{}, hibernate}. handle_call(Req, _From, State) -> @@ -58,14 +63,12 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ stats_fun() -> - fun() -> - safe_update_stats(emqx_subscriber, - 'subscribers/count', 'subscribers/max'), - safe_update_stats(emqx_subscription, - 'subscriptions/count', 'subscriptions/max'), - safe_update_stats(emqx_suboptions, - 'suboptions/count', 'suboptions/max') - end. + safe_update_stats(emqx_subscriber, + 'subscribers/count', 'subscribers/max'), + safe_update_stats(emqx_subscription, + 'subscriptions/count', 'subscriptions/max'), + safe_update_stats(emqx_suboptions, + 'suboptions/count', 'suboptions/max'). safe_update_stats(Tab, Stat, MaxStat) -> case ets:info(Tab, size) of diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index 510b2d91a..61ff6cbc3 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -18,7 +18,7 @@ -include("emqx.hrl"). --export([start_link/0]). +-export([start_link/0, start_link/1, stop/0]). %% Stats API. -export([getstats/0, getstat/1]). @@ -31,7 +31,8 @@ code_change/3]). -record(update, {name, countdown, interval, func}). --record(state, {timer, updates :: [#update{}]}). +-record(state, {timer, updates :: [#update{}], + tick_ms :: timeout()}). -type(stats() :: list({atom(), non_neg_integer()})). @@ -77,10 +78,20 @@ -define(TAB, ?MODULE). -define(SERVER, ?MODULE). +-type opts() :: #{tick_ms := timeout()}. + %% @doc Start stats server -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + start_link(#{tick_ms => timer:seconds(1)}). + +-spec(start_link(opts()) -> emqx_types:startlink_ret()). +start_link(Opts) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, Opts, []). + +-spec(stop() -> ok). +stop() -> + gen_server:call(?SERVER, stop, infinity). %% @doc Generate stats fun -spec(statsfun(Stat :: atom()) -> fun()). @@ -140,16 +151,18 @@ cast(Msg) -> %% gen_server callbacks %%------------------------------------------------------------------------------ -init([]) -> +init(#{tick_ms := TickMs}) -> _ = emqx_tables:new(?TAB, [set, public, {write_concurrency, true}]), Stats = lists:append([?CONNECTION_STATS, ?SESSION_STATS, ?PUBSUB_STATS, ?ROUTE_STATS, ?RETAINED_STATS]), true = ets:insert(?TAB, [{Name, 0} || Name <- Stats]), - {ok, start_timer(#state{updates = []}), hibernate}. + {ok, start_timer(#state{updates = [], tick_ms = TickMs}), hibernate}. -start_timer(State) -> - State#state{timer = emqx_misc:start_timer(timer:seconds(1), tick)}. +start_timer(#state{tick_ms = Ms} = State) -> + State#state{timer = emqx_misc:start_timer(Ms, tick)}. +handle_call(stop, _From, State) -> + {stop, normal, _Reply = ok, State}; handle_call(Req, _From, State) -> emqx_logger:error("[Stats] unexpected call: ~p", [Req]), {reply, ignored, State}. @@ -201,7 +214,7 @@ handle_info(Info, State) -> {noreply, State}. terminate(_Reason, #state{timer = TRef}) -> - timer:cancel(TRef). + emqx_misc:cancel_timer(TRef). code_change(_OldVsn, State, _Extra) -> {ok, State}. diff --git a/test/emqx_stats_SUITE.erl b/test/emqx_stats_SUITE.erl deleted file mode 100644 index 5c7254468..000000000 --- a/test/emqx_stats_SUITE.erl +++ /dev/null @@ -1,56 +0,0 @@ -%% Copyright (c) 2018 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_stats_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("common_test/include/ct.hrl"). - -all() -> [t_set_get_state, t_update_interval]. - -t_set_get_state(_) -> - emqx_stats:start_link(), - SetConnsCount = emqx_stats:statsfun('connections/count'), - SetConnsCount(1), - 1 = emqx_stats:getstat('connections/count'), - emqx_stats:setstat('connections/count', 2), - 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'), - emqx_stats:setstat('connections/count', 'connections/max', 2), - timer:sleep(100), - 2 = emqx_stats:getstat('connections/count'), - 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'), - Conns = emqx_stats:getstats(), - 4 = proplists:get_value('connections/count', Conns), - 4 = proplists:get_value('connections/max', Conns). - -t_update_interval(_) -> - emqx_stats:start_link(), - emqx_stats:cancel_update(cm_stats), - ok = emqx_stats:update_interval(stats_test, fun update_stats/0), - timer:sleep(2500), - 1 = emqx_stats:getstat('connections/count'). - -update_stats() -> - emqx_stats:setstat('connections/count', 1). diff --git a/test/emqx_stats_tests.erl b/test/emqx_stats_tests.erl new file mode 100644 index 000000000..5c3a9c803 --- /dev/null +++ b/test/emqx_stats_tests.erl @@ -0,0 +1,89 @@ +%% Copyright (c) 2018 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_stats_tests). + +-include_lib("eunit/include/eunit.hrl"). + +get_state_test() -> + with_proc(fun() -> + SetConnsCount = emqx_stats:statsfun('connections/count'), + SetConnsCount(1), + 1 = emqx_stats:getstat('connections/count'), + emqx_stats:setstat('connections/count', 2), + 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'), + emqx_stats:setstat('connections/count', 'connections/max', 2), + timer:sleep(100), + 2 = emqx_stats:getstat('connections/count'), + 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'), + Conns = emqx_stats:getstats(), + 4 = proplists:get_value('connections/count', Conns), + 4 = proplists:get_value('connections/max', Conns) + end). + +update_interval_test() -> + TickMs = 100, + with_proc(fun() -> + SleepMs = TickMs * 2 + TickMs div 2, %% sleep for 2.5 ticks + emqx_stats:cancel_update(cm_stats), + UpdFun = fun() -> emqx_stats:setstat('connections/count', 1) end, + ok = emqx_stats:update_interval(stats_test, UpdFun), + timer:sleep(SleepMs), + ?assertEqual(1, emqx_stats:getstat('connections/count')) + end, TickMs). + +emqx_broker_helpe_test() -> + TickMs = 100, + with_proc(fun() -> + SleepMs = TickMs + TickMs div 2, %% sleep for 1.5 ticks + Ref = make_ref(), + Tester = self(), + UpdFun = + fun() -> + emqx_broker_helper:stats_fun(), + Tester ! Ref, + ok + end, + ok = emqx_stats:update_interval(stats_test, UpdFun), + timer:sleep(SleepMs), + receive Ref -> ok after 2000 -> error(timeout) end + end, TickMs). + +with_proc(F) -> + {ok, _Pid} = emqx_stats:start_link(), + with_stop(F). + +with_proc(F, TickMs) -> + {ok, _Pid} = emqx_stats:start_link(#{tick_ms => TickMs}), + with_stop(F). + +with_stop(F) -> + try + %% make a synced call to the gen_server so we know it has + %% started running, hence it is safe to continue with less risk of race condition + ?assertEqual(ignored, gen_server:call(emqx_stats, ignored)), + F() + after + ok = emqx_stats:stop() + end. + From 7e7d99fbadbf299180c78255888628357f4a024d Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sat, 13 Oct 2018 12:14:29 +0200 Subject: [PATCH 365/520] Change more stats callbacks to full M:F/A Including emqx_sm emqx_cm emqx_router_helper --- src/emqx_cm.erl | 5 ++++- src/emqx_router_helper.erl | 17 +++++++------- src/emqx_sm.erl | 11 ++++----- test/emqx_stats_tests.erl | 46 ++++++++++++++++++++++++-------------- 4 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 3e9958939..19892b386 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -30,6 +30,9 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +%% internal export +-export([update_conn_stats/0]). + -define(CM, ?MODULE). %% ETS Tables. @@ -125,7 +128,7 @@ init([]) -> _ = emqx_tables:new(?CONN_TAB, [{read_concurrency, true} | TabOpts]), _ = emqx_tables:new(?CONN_ATTRS_TAB, TabOpts), _ = emqx_tables:new(?CONN_STATS_TAB, TabOpts), - ok = emqx_stats:update_interval(cm_stats, fun update_conn_stats/0), + ok = emqx_stats:update_interval(cm_stats, fun ?MODULE:update_conn_stats/0), {ok, #{conn_pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index d0d0e8075..5431c9900 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -31,6 +31,9 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +%% internal export +-export([stats_fun/0]). + -record(routing_node, {name, const = unused}). -record(state, {nodes = []}). @@ -90,7 +93,7 @@ init([]) -> [Node | Acc] end end, [], mnesia:dirty_all_keys(?ROUTING_NODE)), - emqx_stats:update_interval(route_stats, stats_fun()), + emqx_stats:update_interval(route_stats, fun ?MODULE:stats_fun/0), {ok, #state{nodes = Nodes}, hibernate}. handle_call(Req, _From, State) -> @@ -143,13 +146,11 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ stats_fun() -> - fun() -> - case ets:info(?ROUTE, size) of - undefined -> ok; - Size -> - emqx_stats:setstat('routes/count', 'routes/max', Size), - emqx_stats:setstat('topics/count', 'topics/max', Size) - end + case ets:info(?ROUTE, size) of + undefined -> ok; + Size -> + emqx_stats:setstat('routes/count', 'routes/max', Size), + emqx_stats:setstat('topics/count', 'topics/max', Size) end. cleanup_routes(Node) -> diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 1fa0b488b..d45548a78 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -31,6 +31,9 @@ %% Internal functions for rpc -export([dispatch/3]). +%% Internal function for stats +-export([stats_fun/0]). + %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -210,7 +213,7 @@ init([]) -> _ = emqx_tables:new(?SESSION_P_TAB, TabOpts), _ = emqx_tables:new(?SESSION_ATTRS_TAB, TabOpts), _ = emqx_tables:new(?SESSION_STATS_TAB, TabOpts), - emqx_stats:update_interval(sm_stats, stats_fun()), + emqx_stats:update_interval(sm_stats, fun ?MODULE:stats_fun/0), {ok, #{session_pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> @@ -251,10 +254,8 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ stats_fun() -> - fun() -> - safe_update_stats(?SESSION_TAB, 'sessions/count', 'sessions/max'), - safe_update_stats(?SESSION_P_TAB, 'sessions/persistent/count', 'sessions/persistent/max') - end. + 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 diff --git a/test/emqx_stats_tests.erl b/test/emqx_stats_tests.erl index 5c3a9c803..dd9733a88 100644 --- a/test/emqx_stats_tests.erl +++ b/test/emqx_stats_tests.erl @@ -42,7 +42,7 @@ get_state_test() -> end). update_interval_test() -> - TickMs = 100, + TickMs = 200, with_proc(fun() -> SleepMs = TickMs * 2 + TickMs div 2, %% sleep for 2.5 ticks emqx_stats:cancel_update(cm_stats), @@ -52,22 +52,34 @@ update_interval_test() -> ?assertEqual(1, emqx_stats:getstat('connections/count')) end, TickMs). -emqx_broker_helpe_test() -> - TickMs = 100, - with_proc(fun() -> - SleepMs = TickMs + TickMs div 2, %% sleep for 1.5 ticks - Ref = make_ref(), - Tester = self(), - UpdFun = - fun() -> - emqx_broker_helper:stats_fun(), - Tester ! Ref, - ok - end, - ok = emqx_stats:update_interval(stats_test, UpdFun), - timer:sleep(SleepMs), - receive Ref -> ok after 2000 -> error(timeout) end - end, TickMs). +helper_test_() -> + TickMs = 200, + TestF = + fun(CbModule, CbFun) -> + SleepMs = TickMs + TickMs div 2, %% sleep for 1.5 ticks + Ref = make_ref(), + Tester = self(), + UpdFun = + fun() -> + CbModule:CbFun(), + Tester ! Ref, + ok + end, + ok = emqx_stats:update_interval(stats_test, UpdFun), + timer:sleep(SleepMs), + receive Ref -> ok after 2000 -> error(timeout) end + end, + MkTestFun = + fun(CbModule, CbFun) -> + fun() -> + with_proc(fun() -> TestF(CbModule, CbFun) end, TickMs) + end + end, + [{"emqx_broker_helper", MkTestFun(emqx_broker_helper, stats_fun)}, + {"emqx_sm", MkTestFun(emqx_sm, stats_fun)}, + {"emqx_router_helper", MkTestFun(emqx_router_helper, stats_fun)}, + {"emqx_cm", MkTestFun(emqx_cm, update_conn_stats)} + ]. with_proc(F) -> {ok, _Pid} = emqx_stats:start_link(), From 0adee194aa4c0b97563dfd15a9fb2aed179a23c1 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Wed, 17 Oct 2018 10:29:34 +0800 Subject: [PATCH 366/520] Fix the refered link Prior to this change, the refered wiki link in acl.conf has been deprecated. This change replace the deprecated link with the doc link in official site. --- etc/acl.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/acl.conf b/etc/acl.conf index fb85f3f20..b60ea5e7a 100644 --- a/etc/acl.conf +++ b/etc/acl.conf @@ -1,6 +1,6 @@ %%-------------------------------------------------------------------- %% -%% [ACL](https://github.com/emqtt/emqttd/wiki/ACL) +%% [ACL](http://emqtt.io/docs/v2/config.html#allow-anonymous-and-acl-file) %% %% -type who() :: all | binary() | %% {ipaddr, esockd_access:cidr()} | From 30d986c31825076cf588f0ecb49d4e27c1453b4d Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sat, 29 Sep 2018 19:17:58 +0800 Subject: [PATCH 367/520] Add warning log for unauthored subscribe Prior to this change, there is no log for unauthored log, it is difficult to find the problem when subscription error occured. --- src/emqx_protocol.erl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 1fb80c0ce..861fc68a8 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -747,7 +747,11 @@ check_sub_acl(TopicFilters, PState) -> fun({Topic, SubOpts}, {Ok, Acc}) -> case emqx_access_control:check_acl(Credentials, subscribe, Topic) of allow -> {Ok, [{Topic, SubOpts}|Acc]}; - deny -> {error, [{Topic, SubOpts#{rc := ?RC_NOT_AUTHORIZED}}|Acc]} + deny -> + emqx_logger:warning([{client, PState#pstate.client_id}], + "ACL(~s) Cannot SUBSCRIBE ~p for ACL Deny", + [PState#pstate.client_id, Topic]), + {error, [{Topic, SubOpts#{rc := ?RC_NOT_AUTHORIZED}}|Acc]} end end, {ok, []}, TopicFilters). From 4c40f75f4b6957b7d5747aec2ffe5e64bf02334f Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 19 Oct 2018 01:21:05 +0800 Subject: [PATCH 368/520] Request & Response (broker and client) (#1819) Add request & response support for CONNECT & CONNACK Prior to this change, there is no validate and specified process for Request-Response-Information and Response-Information Also added basic Request/Response functionality to emqx_client implementation --- Makefile | 12 +- etc/emqx.conf | 6 + include/emqx.hrl | 3 - include/emqx_mqtt.hrl | 11 +- priv/emqx.schema | 6 +- src/emqx_broker.erl | 15 +- src/emqx_client.erl | 336 +++++++++++++++--- src/emqx_packet.erl | 21 +- src/emqx_protocol.erl | 120 ++++--- src/emqx_topic.erl | 6 +- test/emqx_SUITE.erl | 21 +- ...compat_SUITE.erl => emqx_client_SUITE.erl} | 117 ++++-- test/emqx_packet_SUITE.erl | 67 +++- test/emqx_protocol_SUITE.erl | 279 ++++++++++----- test/emqx_topic_SUITE.erl | 19 +- test/rfc6455_client.erl | 2 +- test/ws_client.erl | 2 - 17 files changed, 771 insertions(+), 272 deletions(-) rename test/{emqx_mqtt_compat_SUITE.erl => emqx_client_SUITE.erl} (60%) diff --git a/Makefile b/Makefile index de4a2c8c0..9a114399c 100644 --- a/Makefile +++ b/Makefile @@ -35,11 +35,12 @@ EUNIT_OPTS = verbose # CT_SUITES = emqx_frame ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat -CT_SUITES = emqx emqx_zone emqx_banned emqx_connection emqx_session emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight \ - emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ - emqx_mqtt_compat emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ - emqx_tables emqx_time emqx_topic emqx_trie emqx_vm \ - emqx_mountpoint emqx_listeners emqx_protocol emqx_pool emqx_shared_sub +CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_connection emqx_session \ + emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight emqx_json \ + emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ + emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ + emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_mountpoint \ + emqx_listeners emqx_protocol emqx_pool emqx_shared_sub CT_NODE_NAME = emqxct@127.0.0.1 CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME) @@ -138,4 +139,3 @@ dep-vsn-check: {[], []} -> halt(0); \ {Rebar, Mk} -> erlang:error({deps_version_discrepancy, [{rebar, Rebar}, {mk, Mk}]}) \ end." - diff --git a/etc/emqx.conf b/etc/emqx.conf index 18a7d6fd6..874a4c560 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -459,6 +459,12 @@ acl_cache_ttl = 1m ## MQTT Protocol ##-------------------------------------------------------------------- +## Response Topic Prefix +## +## Value: String +## Default: emqxrspv1 +mqtt.response_topic_prefix = emqxrspv1 + ## Maximum MQTT packet size allowed. ## ## Value: Bytes diff --git a/include/emqx.hrl b/include/emqx.hrl index bca6fe519..984f4c9e2 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -37,9 +37,6 @@ %% Queue topic -define(QUEUE, <<"$queue/">>). -%% Shared topic --define(SHARE, <<"$share/">>). - %%-------------------------------------------------------------------- %% Message and Delivery %%-------------------------------------------------------------------- diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 74bfc1120..b9bf6b55e 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -65,9 +65,9 @@ end). -define(IS_QOS_NAME(I), - (I =:= qos0; I =:= at_most_once; - I =:= qos1; I =:= at_least_once; - I =:= qos2; I =:= exactly_once)). + (I =:= qos0 orelse I =:= at_most_once orelse + I =:= qos1 orelse I =:= at_least_once orelse + I =:= qos2 orelse I =:= exactly_once)). %%-------------------------------------------------------------------- %% Maximum ClientId Length. @@ -527,5 +527,8 @@ -define(PACKET(Type), #mqtt_packet{header = #mqtt_packet_header{type = Type}}). --endif. +-define(SHARE, "$share"). +-define(SHARE(Group, Topic), emqx_topic:join([<>, Group, Topic])). +-define(IS_SHARE(Topic), case Topic of <> -> true; _ -> false end). +-endif. diff --git a/priv/emqx.schema b/priv/emqx.schema index 42b894401..becb4bff4 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -597,6 +597,11 @@ end}. %% MQTT Protocol %%-------------------------------------------------------------------- +%% @doc Response Topic Prefix +{mapping, "mqtt.response_topic_prefix", "emqx.response_topic_prefix",[ + {datatype, string} +]}. + %% @doc Max Packet Size Allowed, 1MB by default. {mapping, "mqtt.max_packet_size", "emqx.max_packet_size", [ {default, "1MB"}, @@ -1797,4 +1802,3 @@ end}. {busy_port, cuttlefish:conf_get("sysmon.busy_port", Conf)}, {busy_dist_port, cuttlefish:conf_get("sysmon.busy_dist_port", Conf)}] end}. - diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 35e8276d2..f0946e92d 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -317,13 +317,6 @@ handle_call(Req, _From, State) -> emqx_logger:error("[Broker] unexpected call: ~p", [Req]), {reply, ignored, State}. -resubscribe(From, {Subscriber, SubOpts, Topic}, State) -> - {SubPid, _} = Subscriber, - Group = maps:get(share, SubOpts, undefined), - true = do_subscribe(Group, Topic, Subscriber, SubOpts), - emqx_shared_sub:subscribe(Group, Topic, SubPid), - emqx_router:add_route(From, Topic, dest(Group)), - {noreply, monitor_subscriber(Subscriber, State)}. handle_cast({From, #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}}, State) -> @@ -385,6 +378,14 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ +resubscribe(From, {Subscriber, SubOpts, Topic}, State) -> + {SubPid, _} = Subscriber, + Group = maps:get(share, SubOpts, undefined), + true = do_subscribe(Group, Topic, Subscriber, SubOpts), + emqx_shared_sub:subscribe(Group, Topic, SubPid), + emqx_router:add_route(From, Topic, dest(Group)), + {noreply, monitor_subscriber(Subscriber, State)}. + insert_subscriber(Group, Topic, Subscriber) -> Subscribers = subscribers(Topic), case lists:member(Subscriber, Subscribers) of diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 7ef9c5968..22c37d26f 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -19,7 +19,8 @@ -include("emqx_mqtt.hrl"). -export([start_link/0, start_link/1]). - +-export([request/5, request/6, request_async/7, receive_response/3]). +-export([set_request_handler/2, sub_request_topic/3, sub_request_topic/4]). -export([subscribe/2, subscribe/3, subscribe/4]). -export([publish/2, publish/3, publish/4, publish/5]). -export([unsubscribe/2, unsubscribe/3]). @@ -37,8 +38,34 @@ -export([initialized/3, waiting_for_connack/3, connected/3]). -export([init/1, callback_mode/0, handle_event/4, terminate/3, code_change/4]). +-export_type([client/0, properties/0, payload/0, + pubopt/0, subopt/0, request_input/0, + response_payload/0, request_handler/0, + corr_data/0]). +-export_type([host/0, option/0]). + +%% Default timeout +-define(DEFAULT_KEEPALIVE, 60000). +-define(DEFAULT_ACK_TIMEOUT, 30000). +-define(DEFAULT_CONNECT_TIMEOUT, 60000). + +-define(PROPERTY(Name, Val), #state{properties = #{Name := Val}}). + +-define(WILL_MSG(QoS, Retain, Topic, Props, Payload), + #mqtt_msg{qos = QoS, retain = Retain, topic = Topic, props = Props, payload = Payload}). + +-define(RESPONSE_TIMEOUT_SECONDS, timer:seconds(5)). + +-define(NO_HANDLER, undefined). + +-define(NO_GROUP, <<>>). + +-define(NO_CLIENT_ID, <<>>). + -type(host() :: inet:ip_address() | inet:hostname()). +-type corr_data() :: binary(). + -type(option() :: {name, atom()} | {owner, pid()} | {host, host()} @@ -57,6 +84,7 @@ | {keepalive, non_neg_integer()} | {max_inflight, pos_integer()} | {retry_interval, timeout()} + | {request_handler, request_handler()} | {will_topic, iodata()} | {will_payload, iodata()} | {will_retain, boolean()} @@ -67,8 +95,6 @@ | {force_ping, boolean()} | {properties, properties()}). --export_type([host/0, option/0]). - -record(mqtt_msg, {qos = ?QOS0, retain = false, dup = false, packet_id, topic, props, payload}). @@ -106,6 +132,7 @@ ack_timer :: reference(), retry_interval :: pos_integer(), retry_timer :: reference(), + request_handler :: request_handler(), session_present :: boolean(), last_packet_id :: packet_id(), parse_state :: emqx_frame:state()}). @@ -124,7 +151,7 @@ -type(qos() :: emqx_mqtt_types:qos_name() | emqx_mqtt_types:qos()). --type(pubopt() :: {retain, boolean()} | {qos, qos()}). +-type(pubopt() :: {retain, boolean()} | {qos, qos()} | {timeout, timeout()}). -type(subopt() :: {rh, 0 | 1 | 2} | {rap, boolean()} @@ -135,23 +162,35 @@ -type(subscribe_ret() :: {ok, properties(), [reason_code()]} | {error, term()}). --export_type([client/0, topic/0, qos/0, properties/0, payload/0, - packet_id/0, pubopt/0, subopt/0, reason_code/0]). +-type(request_input() :: binary()). -%% Default timeout --define(DEFAULT_KEEPALIVE, 60000). --define(DEFAULT_ACK_TIMEOUT, 30000). --define(DEFAULT_CONNECT_TIMEOUT, 60000). +-type(response_payload() :: binary()). --define(PROPERTY(Name, Val), #state{properties = #{Name := Val}}). +-type(request_handler() :: fun((request_input()) -> response_payload())). --define(WILL_MSG(QoS, Retain, Topic, Props, Payload), - #mqtt_msg{qos = QoS, retain = Retain, topic = Topic, props = Props, payload = Payload}). +-type(group() :: binary()). %%------------------------------------------------------------------------------ %% API %%------------------------------------------------------------------------------ +%% @doc Swap in a new request handler on the fly. +-spec(set_request_handler(client(), request_handler()) -> ok). +set_request_handler(Responser, RequestHandler) -> + gen_statem:call(Responser, {set_request_handler, RequestHandler}). + +%% @doc Subscribe to request topic. +-spec(sub_request_topic(client(), qos(), topic()) -> ok). +sub_request_topic(Client, QoS, Topic) -> + sub_request_topic(Client, QoS, Topic, ?NO_GROUP). + +%% @doc Share-subscribe to request topic. +-spec(sub_request_topic(client(), qos(), topic(), group()) -> ok). +sub_request_topic(Client, QoS, Topic, Group) -> + Properties = get_properties(Client), + NewTopic = make_req_rsp_topic(Properties, Topic, Group), + subscribe_req_rsp_topic(Client, QoS, NewTopic). + -spec(start_link() -> gen_statem:start_ret()). start_link() -> start_link([]). @@ -248,12 +287,82 @@ parse_subopt([{nl, false} | Opts], Result) -> parse_subopt([{qos, QoS} | Opts], Result) -> parse_subopt(Opts, Result#{qos := ?QOS_I(QoS)}). +-spec(request(client(), topic(), topic(), payload(), qos() | [pubopt()]) + -> ok | {ok, packet_id()} | {error, term()}). +request(Client, ResponseTopic, RequestTopic, Payload, QoS) when is_binary(ResponseTopic), is_atom(QoS) -> + request(Client, ResponseTopic, RequestTopic, Payload, [{qos, ?QOS_I(QoS)}]); +request(Client, ResponseTopic, RequestTopic, Payload, QoS) when is_binary(ResponseTopic), ?IS_QOS(QoS) -> + request(Client, ResponseTopic, RequestTopic, Payload, [{qos, QoS}]); +request(Client, ResponseTopic, RequestTopic, Payload, Opts) when is_binary(ResponseTopic), is_list(Opts) -> + request(Client, ResponseTopic, RequestTopic, Payload, Opts, _Properties = #{}). + +%% @doc Send a request to request topic and wait for response. +-spec(request(client(), topic(), topic(), payload(), [pubopt()], properties()) + -> {ok, response_payload()} | {error, term()}). +request(Client, ResponseTopic, RequestTopic, Payload, Opts, Properties) -> + CorrData = make_corr_data(), + case request_async(Client, ResponseTopic, RequestTopic, + Payload, Opts, Properties, CorrData) of + ok -> receive_response(Client, CorrData, Opts); + {error, Reason} -> {error, Reason} + end. + +%% @doc Get client properties. +-spec(get_properties(client()) -> properties()). +get_properties(Client) -> gen_statem:call(Client, get_properties, infinity). + +%% @doc Send a request, but do not wait for response. +%% The caller should expect a `{publish, Response}' message, +%% or call `receive_response/3' to receive the message. +-spec(request_async(client(), topic(), topic(), payload(), + [pubopt()], properties(), corr_data()) -> ok | {error, any()}). +request_async(Client, ResponseTopic, RequestTopic, Payload, Opts, Properties, CorrData) + when is_binary(ResponseTopic), + is_binary(RequestTopic), + is_map(Properties), + is_list(Opts) -> + ok = emqx_mqtt_props:validate(Properties), + Retain = proplists:get_bool(retain, Opts), + QoS = ?QOS_I(proplists:get_value(qos, Opts, ?QOS_0)), + ClientProperties = get_properties(Client), + NewResponseTopic = make_req_rsp_topic(ClientProperties, ResponseTopic), + NewRequestTopic = make_req_rsp_topic(ClientProperties, RequestTopic), + %% This is perhaps not optimal to subscribe the response topic for + %% each and every request even though the response topic is always the same + ok = sub_response_topic(Client, QoS, NewResponseTopic), + NewProperties = maps:merge(Properties, #{'Response-Topic' => NewResponseTopic, + 'Correlation-Data' => CorrData}), + case publish(Client, #mqtt_msg{qos = QoS, + retain = Retain, + topic = NewRequestTopic, + props = NewProperties, + payload = iolist_to_binary(Payload)}) of + ok -> ok; + {ok, _PacketId} -> ok; %% assume auto_ack + {error, Reason} -> {error, Reason} + end. + +%% @doc Block wait the response for a request sent earlier. +-spec(receive_response(client(), corr_data(), [pubopt()]) + -> {ok, response_payload()} | {error, any()}). +receive_response(Client, CorrData, Opts) -> + TimeOut = proplists:get_value(timeout, Opts, ?RESPONSE_TIMEOUT_SECONDS), + MRef = erlang:monitor(process, Client), + TRef = erlang:start_timer(TimeOut, self(), response), + try + receive_response(Client, CorrData, TRef, MRef) + after + erlang:cancel_timer(TRef), + receive {timeout, TRef, _} -> ok after 0 -> ok end, + erlang:demonitor(MRef, [flush]) + end. + -spec(publish(client(), topic(), payload()) -> ok | {error, term()}). publish(Client, Topic, Payload) when is_binary(Topic) -> publish(Client, #mqtt_msg{topic = Topic, qos = ?QOS_0, payload = iolist_to_binary(Payload)}). -spec(publish(client(), topic(), payload(), qos() | [pubopt()]) - -> ok | {ok, packet_id()} | {error, term()}). + -> ok | {ok, packet_id()} | {error, term()}). publish(Client, Topic, Payload, QoS) when is_binary(Topic), is_atom(QoS) -> publish(Client, Topic, Payload, [{qos, ?QOS_I(QoS)}]); publish(Client, Topic, Payload, QoS) when is_binary(Topic), ?IS_QOS(QoS) -> @@ -369,7 +478,7 @@ init([Options]) -> process_flag(trap_exit, true), ClientId = case {proplists:get_value(proto_ver, Options, v4), proplists:get_value(client_id, Options)} of - {v5, undefined} -> <<>>; + {v5, undefined} -> ?NO_CLIENT_ID; {_ver, undefined} -> random_client_id(); {_ver, Id} -> iolist_to_binary(Id) end, @@ -396,6 +505,7 @@ init([Options]) -> auto_ack = true, ack_timeout = ?DEFAULT_ACK_TIMEOUT, retry_interval = 0, + request_handler = ?NO_HANDLER, connect_timeout = ?DEFAULT_CONNECT_TIMEOUT, last_packet_id = 1}), {ok, initialized, init_parse_state(State)}. @@ -488,6 +598,8 @@ init([{auto_ack, AutoAck} | Opts], State) when is_boolean(AutoAck) -> init(Opts, State#state{auto_ack = AutoAck}); init([{retry_interval, I} | Opts], State) -> init(Opts, State#state{retry_interval = timer:seconds(I)}); +init([{request_handler, Handler} | Opts], State) -> + init(Opts, State#state{request_handler = Handler}); init([{bridge_mode, Mode} | Opts], State) when is_boolean(Mode) -> init(Opts, State#state{bridge_mode = Mode}); init([_Opt | Opts], State) -> @@ -562,7 +674,8 @@ mqtt_connect(State = #state{client_id = ClientId, waiting_for_connack(cast, ?CONNACK_PACKET(?RC_SUCCESS, SessPresent, Properties), - State = #state{properties = AllProps}) -> + State = #state{properties = AllProps, + client_id = ClientId}) -> case take_call(connect, State) of {value, #call{from = From}, State1} -> AllProps1 = case Properties of @@ -570,7 +683,8 @@ waiting_for_connack(cast, ?CONNACK_PACKET(?RC_SUCCESS, _ -> maps:merge(AllProps, Properties) end, Reply = {ok, self(), Properties}, - State2 = State1#state{properties = AllProps1, + State2 = State1#state{client_id = assign_id(ClientId, AllProps1), + properties = AllProps1, session_present = SessPresent}, {next_state, connected, ensure_keepalive_timer(State2), [{reply, From, Reply}]}; @@ -616,6 +730,15 @@ connected({call, From}, resume, State) -> connected({call, From}, stop, _State) -> {stop_and_reply, normal, [{reply, From, ok}]}; +connected({call, From}, get_properties, State = #state{properties = Properties}) -> + {keep_state, State, [{reply, From, Properties}]}; + +connected({call, From}, client_id, State = #state{client_id = ClientId}) -> + {keep_state, State, [{reply, From, ClientId}]}; + +connected({call, From}, {set_request_handler, RequestHandler}, State) -> + {keep_state, State#state{request_handler = RequestHandler}, [{reply, From, ok}]}; + connected({call, From}, SubReq = {subscribe, Properties, Topics}, State = #state{last_packet_id = PacketId, subscriptions = Subscriptions}) -> case send(?SUBSCRIBE_PACKET(PacketId, Properties, Topics), State) of @@ -695,29 +818,30 @@ connected(cast, {pubrel, PacketId, ReasonCode, Properties}, State) -> connected(cast, {pubcomp, PacketId, ReasonCode, Properties}, State) -> send_puback(?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), State); -connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), State) -> - {keep_state, deliver(packet_to_msg(Packet), State)}; - connected(cast, ?PUBLISH_PACKET(_QoS, _PacketId), State = #state{paused = true}) -> {keep_state, State}; -connected(cast, Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), - State = #state{auto_ack = AutoAck}) -> +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _Topic, _PacketId, Properties, Payload), + State) when Properties =/= undefined -> + NewState = response_publish(Properties, State, ?QOS_0, Payload), + {keep_state, deliver(packet_to_msg(Packet), NewState)}; +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), State) -> + {keep_state, deliver(packet_to_msg(Packet), State)}; - _ = deliver(packet_to_msg(Packet), State), - case AutoAck of - true -> send_puback(?PUBACK_PACKET(PacketId), State); - false -> {keep_state, State} - end; +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_1, _Topic, _PacketId, Properties, Payload), State) + when Properties =/= undefined -> + NewState = response_publish(Properties, State, ?QOS_1, Payload), + publish_process(?QOS_1, Packet, NewState); -connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), - State = #state{awaiting_rel = AwaitingRel}) -> - case send_puback(?PUBREC_PACKET(PacketId), State) of - {keep_state, NewState} -> - AwaitingRel1 = maps:put(PacketId, Packet, AwaitingRel), - {keep_state, NewState#state{awaiting_rel = AwaitingRel1}}; - Stop -> Stop - end; +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_1, _PacketId), State) -> + publish_process(?QOS_1, Packet, State); + +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, _Topic, _PacketId, Properties, Payload), State) + when Properties =/= undefined -> + NewState = response_publish(Properties, State, ?QOS_2, Payload), + publish_process(?QOS_2, Packet, NewState); +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, _PacketId), State) -> + publish_process(?QOS_2, Packet, State); connected(cast, ?PUBACK_PACKET(PacketId, ReasonCode, Properties), State = #state{owner = Owner, inflight = Inflight}) -> @@ -899,6 +1023,132 @@ code_change(_Vsn, State, Data, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ +%% Subscribe to response topic. +-spec(sub_response_topic(client(), qos(), topic()) -> ok). +sub_response_topic(Client, QoS, Topic) when is_binary(Topic) -> + subscribe_req_rsp_topic(Client, QoS, Topic). + +receive_response(Client, CorrData, TRef, MRef) -> + receive + {publish, Response} -> + {ok, Properties} = maps:find(properties, Response), + case maps:find('Correlation-Data', Properties) of + {ok, CorrData} -> + maps:find(payload, Response); + _ -> + emqx_logger:debug("Discarded stale response: ~p", [Response]), + receive_response(Client, CorrData, TRef, MRef) + end; + {timeout, TRef, response} -> + {error, timeout}; + {'DOWN', MRef, process, _, _} -> + {error, client_down} + end. + +%% Make a unique correlation data for each request. +%% It has to be unique because stale responses should be discarded. +make_corr_data() -> term_to_binary(make_ref()). + +%% Shared function for request and response topic subscription. +subscribe_req_rsp_topic(Client, QoS, Topic) -> + %% It is a Protocol Error to set the No Local bit to 1 on a Shared Subscription + {ok, _Props, _QoS} = subscribe(Client, [{Topic, [{rh, 2}, {rap, false}, + {nl, not ?IS_SHARE(Topic)}, + {qos, QoS}]}]), + emqx_logger:debug("Subscribed to topic ~s", [Topic]), + ok. + +%% Make a request or response topic. +make_req_rsp_topic(Properties, Topic) -> + make_req_rsp_topic(Properties, Topic, ?NO_GROUP). + +%% Same as make_req_rsp_topic/2, but allow shared subscription (for request topics) +make_req_rsp_topic(Properties, Topic, Group) -> + case maps:find('Response-Information', Properties) of + {ok, ResponseInformation} when ResponseInformation =/= <<>> -> + emqx_topic:join([req_rsp_topic_prefix(Group, ResponseInformation), Topic]); + _ -> + erlang:error(no_response_information) + end. + +req_rsp_topic_prefix(?NO_GROUP, Prefix) -> Prefix; +req_rsp_topic_prefix(Group, Prefix) -> ?SHARE(Group, Prefix). + +assign_id(?NO_CLIENT_ID, Props) -> + case maps:find('Assigned-Client-Identifier', Props) of + {ok, Value} -> + Value; + _ -> + error(bad_client_id) + end; +assign_id(Id, _Props) -> + Id. + +publish_process(?QOS_1, Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), State = #state{auto_ack = AutoAck}) -> + _ = deliver(packet_to_msg(Packet), State), + case AutoAck of + true -> send_puback(?PUBACK_PACKET(PacketId), State); + false -> {keep_state, State} + end; +publish_process(?QOS_2, Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), + State = #state{awaiting_rel = AwaitingRel}) -> + case send_puback(?PUBREC_PACKET(PacketId), State) of + {keep_state, NewState} -> + AwaitingRel1 = maps:put(PacketId, Packet, AwaitingRel), + {keep_state, NewState#state{awaiting_rel = AwaitingRel1}}; + Stop -> Stop + end. + +response_publish(undefined, State, _QoS, _Payload) -> + State; +response_publish(Properties, State = #state{request_handler = RequestHandler}, QoS, Payload) -> + case maps:find('Response-Topic', Properties) of + {ok, ResponseTopic} -> + case RequestHandler of + ?NO_HANDLER -> State; + _ -> do_publish(ResponseTopic, Properties, State, QoS, Payload) + end; + _ -> + State + end. + +do_publish(ResponseTopic, Properties, State = #state{request_handler = RequestHandler}, ?QOS_0, Payload) -> + Msg = #mqtt_msg{qos = ?QOS_0, + retain = false, + topic = ResponseTopic, + props = Properties, + payload = RequestHandler(Payload) + }, + case send(Msg, State) of + {ok, NewState} -> NewState; + _Error -> State + end; +do_publish(ResponseTopic, Properties, State = #state{request_handler = RequestHandler, + inflight = Inflight, + last_packet_id = PacketId}, + QoS, Payload) + when (QoS =:= ?QOS_1); (QoS =:= ?QOS_2)-> + case emqx_inflight:is_full(Inflight) of + true -> + emqx_logger:error("Inflight is full"), + State; + false -> + Msg = #mqtt_msg{packet_id = PacketId, + qos = QoS, + retain = false, + topic = ResponseTopic, + props = Properties, + payload = RequestHandler(Payload)}, + case send(Msg, State) of + {ok, NewState} -> + Inflight1 = emqx_inflight:insert(PacketId, {publish, Msg, os:timestamp()}, Inflight), + ensure_retry_timer(NewState#state{inflight = Inflight1}); + {error, Reason} -> + emqx_logger:error("Send failed: ~p", [Reason]), + State + end + end. + ensure_keepalive_timer(State = ?PROPERTY('Server-Keep-Alive', Secs)) -> ensure_keepalive_timer(timer:seconds(Secs), State); ensure_keepalive_timer(State = #state{keepalive = 0}) -> @@ -986,10 +1236,15 @@ retry_send(pubrel, PacketId, Now, State = #state{inflight = Inflight}) -> deliver(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, topic = Topic, props = Props, payload = Payload}, - State = #state{owner = Owner}) -> - Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain, packet_id => PacketId, - topic => Topic, properties => Props, payload => Payload, - client_pid => self()}}, + State = #state{owner = Owner, request_handler = RequestHandler}) -> + case RequestHandler of + ?NO_HANDLER -> + Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain, packet_id => PacketId, + topic => Topic, properties => Props, payload => Payload, + client_pid => self()}}; + _ -> + ok + end, State. packet_to_msg(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, @@ -1001,7 +1256,7 @@ packet_to_msg(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, properties = Props}, payload = Payload}) -> #mqtt_msg{qos = QoS, retain = R, dup = Dup, packet_id = PacketId, - topic = Topic, props = Props, payload = Payload}. + topic = Topic, props = Props, payload = Payload}. msg_to_packet(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, topic = Topic, props = Props, payload = Payload}) -> @@ -1070,7 +1325,6 @@ receive_loop(Bytes, State = #state{parse_state = ParseState}) -> {error, Reason} -> {stop, Reason}; {'EXIT', Error} -> - io:format("client stop"), {stop, Error} end. diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 1db586cea..2040e595e 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -63,8 +63,8 @@ validate(?PUBLISH_PACKET(_QoS, Topic, _, Properties, _)) -> ((not emqx_topic:wildcard(Topic)) orelse error(topic_name_invalid)) andalso validate_properties(?PUBLISH, Properties); -validate(?CONNECT_PACKET(#mqtt_packet_connect{properties = #{'Receive-Maximum' := 0}})) -> - error(protocol_error); +validate(?CONNECT_PACKET(#mqtt_packet_connect{properties = Properties})) -> + validate_properties(?CONNECT, Properties); validate(_Packet) -> true. @@ -82,11 +82,24 @@ validate_properties(?PUBLISH, #{'Topic-Alias':= I}) error(topic_alias_invalid); validate_properties(?PUBLISH, #{'Subscription-Identifier' := _I}) -> error(protocol_error); +validate_properties(?PUBLISH, #{'Response-Topic' := ResponseTopic}) -> + case emqx_topic:wildcard(ResponseTopic) of + true -> + error(protocol_error); + false -> + true + end; +validate_properties(?CONNECT, #{'Receive-Maximum' := 0}) -> + error(protocol_error); +validate_properties(?CONNECT, #{'Request-Response-Information' := ReqRespInfo}) + when ReqRespInfo =/= 0, ReqRespInfo =/= 1 -> + error(protocol_error); +validate_properties(?CONNECT, #{'Request-Problem-Information' := ReqProInfo}) + when ReqProInfo =/= 0, ReqProInfo =/= 1 -> + error(protocol_error); validate_properties(_, _) -> true. - - validate_subscription({Topic, #{qos := QoS}}) -> emqx_topic:validate(filter, Topic) andalso validate_qos(QoS). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 861fc68a8..c3f0689fa 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -82,27 +82,27 @@ -spec(init(map(), list()) -> state()). init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) -> Zone = proplists:get_value(zone, Options), - #pstate{zone = Zone, - sendfun = SendFun, - 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), - is_super = false, - clean_start = false, - topic_aliases = #{}, - packet_size = emqx_zone:get_env(Zone, max_packet_size), - mountpoint = emqx_zone:get_env(Zone, mountpoint), - is_bridge = false, - enable_ban = emqx_zone:get_env(Zone, enable_ban, false), - enable_acl = emqx_zone:get_env(Zone, enable_acl), - recv_stats = #{msg => 0, pkt => 0}, - send_stats = #{msg => 0, pkt => 0}, - connected = false}. + #pstate{zone = Zone, + sendfun = SendFun, + 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), + is_super = false, + clean_start = false, + topic_aliases = #{}, + packet_size = emqx_zone:get_env(Zone, max_packet_size), + mountpoint = emqx_zone:get_env(Zone, mountpoint), + is_bridge = false, + enable_ban = emqx_zone:get_env(Zone, enable_ban, false), + enable_acl = emqx_zone:get_env(Zone, enable_acl), + recv_stats = #{msg => 0, pkt => 0}, + send_stats = #{msg => 0, pkt => 0}, + connected = false}. init_username(Peercert, Options) -> case proplists:get_value(peer_cert_as_username, Options) of @@ -194,6 +194,13 @@ parser(#pstate{packet_size = Size, proto_ver = Ver}) -> %% Packet Received %%------------------------------------------------------------------------------ +set_protover(?CONNECT_PACKET(#mqtt_packet_connect{ + proto_ver = ProtoVer}), + PState) -> + PState#pstate{ proto_ver = ProtoVer }; +set_protover(_Packet, PState) -> + PState. + -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 -> @@ -203,14 +210,31 @@ received(?PACKET(?CONNECT), PState = #pstate{connected = true}) -> {error, proto_unexpected_connect, PState}; received(Packet = ?PACKET(Type), PState) -> - trace(recv, Packet, PState), - case catch emqx_packet:validate(Packet) of + PState1 = set_protover(Packet, PState), + trace(recv, Packet, PState1), + try emqx_packet:validate(Packet) of true -> - {Packet1, PState1} = preprocess_properties(Packet, PState), - process_packet(Packet1, inc_stats(recv, Type, PState1)); - {'EXIT', {Reason, _Stacktrace}} -> - deliver({disconnect, rc(Reason)}, PState), - {error, Reason, PState} + {Packet1, PState2} = preprocess_properties(Packet, PState1), + process_packet(Packet1, inc_stats(recv, Type, PState2)) + 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} end. %%------------------------------------------------------------------------------ @@ -299,7 +323,7 @@ process_packet(?CONNECT_PACKET( {error, Error} -> ?LOG(error, "Failed to open session: ~p", [Error], PState1), {?RC_UNSPECIFIED_ERROR, PState1} - end; + end; {error, Reason} -> ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], PState2), {?RC_NOT_AUTHORIZED, PState1} @@ -407,13 +431,13 @@ process_packet(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), process_packet(?PACKET(?PINGREQ), PState) -> send(?PACKET(?PINGRESP), PState); -process_packet(?DISCONNECT_PACKET(?RC_SUCCESS, #{'Session-Expiry-Interval' := Interval}), +process_packet(?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 -> + true -> deliver({disconnect, ?RC_PROTOCOL_ERROR}, PState), {error, protocol_error, PState#pstate{will_msg = undefined}}; - false -> + false -> emqx_session:update_expiry_interval(SPid, Interval), %% Clean willmsg {stop, normal, PState#pstate{will_msg = undefined}} @@ -481,7 +505,14 @@ deliver({connack, ReasonCode}, PState) -> deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, proto_ver = ?MQTT_PROTO_V5, client_id = ClientId, + conn_props = ConnProps, is_assigned = IsAssigned}) -> + ResponseInformation = case maps:find('Request-Response-Information', ConnProps) of + {ok, 1} -> + iolist_to_binary(emqx_config:get_env(response_topic_prefix)); + _ -> + <<>> + end, #{max_packet_size := MaxPktSize, max_qos_allowed := MaxQoS, mqtt_retain_available := Retain, @@ -493,18 +524,21 @@ deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, 'Topic-Alias-Maximum' => MaxAlias, 'Wildcard-Subscription-Available' => flag(Wildcard), 'Subscription-Identifier-Available' => 1, + 'Response-Information' => ResponseInformation, + 'Shared-Subscription-Available' => flag(Shared)}, - Props1 = if - MaxQoS =:= ?QOS_2 -> + Props1 = if + MaxQoS =:= ?QOS_2 -> Props; true -> maps:put('Maximum-QoS', MaxQoS, Props) end, - + Props2 = if IsAssigned -> Props1#{'Assigned-Client-Identifier' => ClientId}; true -> Props1 + end, Props3 = case emqx_zone:get_env(Zone, server_keepalive) of @@ -601,17 +635,17 @@ set_session_attrs({max_inflight, #pstate{zone = Zone, proto_ver = ProtoVer, conn maps:put(max_inflight, if ProtoVer =:= ?MQTT_PROTO_V5 -> maps:get('Receive-Maximum', ConnProps, 65535); - true -> + true -> emqx_zone:get_env(Zone, max_inflight, 65535) end, SessAttrs); set_session_attrs({expiry_interval, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps, clean_start = CleanStart}}, SessAttrs) -> maps:put(expiry_interval, if ProtoVer =:= ?MQTT_PROTO_V5 -> maps:get('Session-Expiry-Interval', ConnProps, 0); - true -> + true -> case CleanStart of true -> 0; - false -> + false -> emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) end end, SessAttrs); @@ -619,7 +653,7 @@ set_session_attrs({topic_alias_maximum, #pstate{zone = Zone, proto_ver = ProtoVe maps:put(topic_alias_maximum, if ProtoVer =:= ?MQTT_PROTO_V5 -> maps:get('Topic-Alias-Maximum', ConnProps, 0); - true -> + true -> emqx_zone:get_env(Zone, max_topic_alias, 0) end, SessAttrs); set_session_attrs({_, #pstate{}}, SessAttrs) -> @@ -803,14 +837,6 @@ 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)}. -rc(Reason) -> - case Reason of - protocol_error -> ?RC_PROTOCOL_ERROR; - topic_filters_invalid -> ?RC_TOPIC_FILTER_INVALID; - topic_name_invalid -> ?RC_TOPIC_NAME_INVALID; - _ -> ?RC_MALFORMED_PACKET - end. - %%----------------------------------------------------------------------------- %% Parse topic filters %%----------------------------------------------------------------------------- diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index 6540bbef2..f7e229a13 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -34,6 +34,8 @@ -define(MAX_TOPIC_LEN, 4096). +-include("emqx_mqtt.hrl"). + %% @doc Is wildcard topic? -spec(wildcard(topic() | words()) -> true | false). wildcard(Topic) when is_binary(Topic) -> @@ -180,11 +182,11 @@ parse(Topic) when is_binary(Topic) -> parse(Topic = <<"$queue/", _/binary>>, #{share := _Group}) -> error({invalid_topic, Topic}); -parse(Topic = <<"$share/", _/binary>>, #{share := _Group}) -> +parse(Topic = <>, #{share := _Group}) -> error({invalid_topic, Topic}); parse(<<"$queue/", Topic1/binary>>, Options) -> parse(Topic1, maps:put(share, <<"$queue">>, Options)); -parse(Topic = <<"$share/", Topic1/binary>>, Options) -> +parse(Topic = <>, Options) -> case binary:split(Topic1, <<"/">>) of [<<>>] -> error({invalid_topic, Topic}); [_] -> error({invalid_topic, Topic}); diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index 166d64a30..e82f70395 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -27,7 +27,6 @@ -record(ssl_socket, {tcp, ssl}). --type(socket() :: inet:socket() | #ssl_socket{}). -define(CLIENT, ?CONNECT_PACKET(#mqtt_packet_connect{ client_id = <<"mqtt_client">>, @@ -104,7 +103,7 @@ 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_serialise(?CLIENT2), + 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), @@ -117,7 +116,7 @@ mqtt_connect_with_ssl_oneway(_) -> ClientSsl = emqx_ct_broker_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_serialise(?CLIENT), + Packet = raw_send_serialize(?CLIENT), emqx_client_sock:setopts(Sock, [{active, once}]), emqx_client_sock:send(Sock, Packet), ?assert( @@ -135,7 +134,7 @@ mqtt_connect_with_ssl_twoway(_Config) -> ClientSsl = emqx_ct_broker_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_serialise(?CLIENT), + Packet = raw_send_serialize(?CLIENT), emqx_client_sock:setopts(Sock, [{active, once}]), emqx_client_sock:send(Sock, Packet), timer:sleep(500), @@ -145,6 +144,7 @@ mqtt_connect_with_ssl_twoway(_Config) -> after 1000 -> false end), + ssl:close(SslSock), emqx_client_sock:close(Sock). mqtt_connect_with_ws(_Config) -> @@ -152,19 +152,19 @@ mqtt_connect_with_ws(_Config) -> {ok, _} = rfc6455_client:open(WS), %% Connect Packet - Packet = raw_send_serialise(?CLIENT), + 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_serialise(?SUBPACKET), + 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_serialise(?PUBPACKET), + 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), @@ -174,22 +174,21 @@ mqtt_connect_with_ws(_Config) -> %%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_serialise(?CLIENT), + 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_serialise(?BIG_PUBPACKET), + 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_serialise(Packet) -> +raw_send_serialize(Packet) -> emqx_frame:serialize(Packet). 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_compat_SUITE.erl b/test/emqx_client_SUITE.erl similarity index 60% rename from test/emqx_mqtt_compat_SUITE.erl rename to test/emqx_client_SUITE.erl index af2583678..2ad3dbaf7 100644 --- a/test/emqx_mqtt_compat_SUITE.erl +++ b/test/emqx_client_SUITE.erl @@ -12,7 +12,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. --module(emqx_mqtt_compat_SUITE). +-module(emqx_client_SUITE). -compile(export_all). -compile(nowarn_export_all). @@ -32,15 +32,24 @@ <<"+/+">>, <<"TopicA/#">>]). all() -> - [basic_test, - will_message_test, - zero_length_clientid_test, - offline_message_queueing_test, - overlapping_subscriptions_test, - %% keepalive_test, - redelivery_on_reconnect_test, - %% subscribe_failure_test, - dollar_topics_test]. + [ {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, + %% subscribe_failure_test, + dollar_topics_test]}, + {mqttv5, [non_parallel_tests], + [request_response, + share_sub_request_topic]} +]. init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), @@ -49,6 +58,77 @@ init_per_suite(Config) -> end_per_suite(_Config) -> emqx_ct_broker_helpers:run_teardown_steps(). +request_response_exception(QoS) -> + {ok, Client, _} = emqx_client:start_link([{proto_ver, v5}, + {properties, #{ 'Request-Response-Information' => 0 }}]), + ?assertError(no_response_information, + emqx_client:sub_request_topic(Client, QoS, <<"request_topic">>)), + ok = emqx_client:disconnect(Client). + +request_response_per_qos(QoS) -> + {ok, Requester, _} = emqx_client:start_link([{proto_ver, v5}, + {client_id, <<"requester">>}, + {properties, #{ 'Request-Response-Information' => 1}}]), + {ok, Responser, _} = emqx_client:start_link([{proto_ver, v5}, + {client_id, <<"responser">>}, + {properties, #{ 'Request-Response-Information' => 1}}, + {request_handler, fun(_Req) -> <<"ResponseTest">> end}]), + ok = emqx_client:sub_request_topic(Responser, QoS, <<"request_topic">>), + {ok, <<"ResponseTest">>} = emqx_client:request(Requester, <<"response_topic">>, <<"request_topic">>, <<"request_payload">>, QoS), + ok = emqx_client:set_request_handler(Responser, fun(<<"request_payload">>) -> + <<"ResponseFunctionTest">>; + (_) -> + <<"404">> + end), + {ok, <<"ResponseFunctionTest">>} = emqx_client:request(Requester, <<"response_topic">>, <<"request_topic">>, <<"request_payload">>, QoS), + {ok, <<"404">>} = emqx_client:request(Requester, <<"response_topic">>, <<"request_topic">>, <<"invalid_request">>, QoS), + ok = emqx_client:disconnect(Responser), + ok = emqx_client:disconnect(Requester). + +request_response(_Config) -> + request_response_per_qos(?QOS_2), + request_response_per_qos(?QOS_1), + request_response_per_qos(?QOS_0), + request_response_exception(?QOS_0), + request_response_exception(?QOS_1), + request_response_exception(?QOS_2). + +share_sub_request_topic(_Config) -> + share_sub_request_topic_per_qos(?QOS_2), + share_sub_request_topic_per_qos(?QOS_1), + share_sub_request_topic_per_qos(?QOS_0). + +share_sub_request_topic_per_qos(QoS) -> + application:set_env(?APPLICATION, shared_subscription_strategy, random), + ReqTopic = <<"request-topic">>, + RspTopic = <<"response-topic">>, + Group = <<"g1">>, + Properties = #{ 'Request-Response-Information' => 1}, + Opts = fun(ClientId) -> [{proto_ver, v5}, + {client_id, atom_to_binary(ClientId, utf8)}, + {properties, Properties} + ] end, + {ok, Requester, _} = emqx_client:start_link(Opts(requester)), + {ok, Responser1, _} = emqx_client:start_link([{request_handler, fun(Req) -> <<"1-", Req/binary>> end} | Opts(requester1)]), + {ok, Responser2, _} = emqx_client:start_link([{request_handler, fun(Req) -> <<"2-", Req/binary>> end} | Opts(requester2)]), + ok = emqx_client:sub_request_topic(Responser1, QoS, ReqTopic, Group), + ok = emqx_client:sub_request_topic(Responser2, QoS, ReqTopic, Group), + %% Send a request, wait for response, validate response then return responser ID + ReqFun = fun(Req) -> + {ok, Rsp} = emqx_client:request(Requester, RspTopic, ReqTopic, Req, QoS), + case Rsp of + <<"1-", Req/binary>> -> 1; + <<"2-", Req/binary>> -> 2 + end + end, + Ids = lists:map(fun(I) -> ReqFun(integer_to_binary(I)) end, lists:seq(1, 100)), + %% we are testing with random shared-dispatch strategy, + %% fail if not all responsers got a chance to handle requests + ?assertEqual([1, 2], lists:usort(Ids)), + ok = emqx_client:disconnect(Responser1), + ok = emqx_client:disconnect(Responser2), + ok = emqx_client:disconnect(Requester). + receive_messages(Count) -> receive_messages(Count, []). @@ -59,7 +139,7 @@ receive_messages(Count, Msgs) -> {publish, Msg} -> receive_messages(Count-1, [Msg|Msgs]); Other -> - ct:log("~p~n", [Other]), + ct:log("~p~n", [Other]), receive_messages(Count, Msgs) after 10 -> Msgs @@ -90,18 +170,6 @@ will_message_test(_Config) -> ok = emqx_client:disconnect(C2), ct:print("Will message test succeeded"). -zero_length_clientid_test(_Config) -> - ct:print("Zero length clientid test starting"), - - %% TODO: There are some controversies on the situation when - %% clean_start flag is true and clientid is zero length. - - %% {error, _} = emqx_client:start_link([{clean_start, false}, - %% {client_id, <<>>}]), - {ok, _, _} = emqx_client:start_link([{clean_start, true}, - {client_id, <<>>}]), - ct:print("Zero length clientid test succeeded"). - offline_message_queueing_test(_) -> {ok, C1, _} = emqx_client:start_link([{clean_start, false}, {client_id, <<"c1">>}]), @@ -109,7 +177,7 @@ offline_message_queueing_test(_) -> ok = emqx_client:disconnect(C1), {ok, C2, _} = emqx_client:start_link([{clean_start, true}, {client_id, <<"c2">>}]), - + ok = emqx_client:publish(C2, nth(2, ?TOPICS), <<"qos 0">>, 0), {ok, _} = emqx_client:publish(C2, nth(3, ?TOPICS), <<"qos 1">>, 1), {ok, _} = emqx_client:publish(C2, nth(4, ?TOPICS), <<"qos 2">>, 2), @@ -196,4 +264,3 @@ dollar_topics_test(_) -> ?assertEqual(0, length(receive_messages(1))), ok = emqx_client:disconnect(C), ct:print("$ topics test succeeded"). - diff --git a/test/emqx_packet_SUITE.erl b/test/emqx_packet_SUITE.erl index ec4205957..dda27c45b 100644 --- a/test/emqx_packet_SUITE.erl +++ b/test/emqx_packet_SUITE.erl @@ -44,14 +44,49 @@ packet_type_name(_) -> ?assertEqual('UNSUBSCRIBE', emqx_packet:type_name(?UNSUBSCRIBE)). packet_validate(_) -> - ?assertEqual(true, emqx_packet:validate(?SUBSCRIBE_PACKET(15, #{'Subscription-Identifier' => 1}, [{<<"topic">>, #{qos => ?QOS0}}]))), - ?assertEqual(true, emqx_packet:validate(?UNSUBSCRIBE_PACKET(89, [<<"topic">>]))), - ?assertEqual(true, emqx_packet:validate(?CONNECT_PACKET(#mqtt_packet_connect{}))), - ?assertEqual(true, emqx_packet:validate(?CONNECT_PACKET(#mqtt_packet_connect{properties = #{'Receive-Maximum' => 1}}))), - case catch emqx_packet:validate(?CONNECT_PACKET(#mqtt_packet_connect{properties = #{'Receive-Maximum' => 0}})) of - {'EXIT', {protocol_error, _}} -> ?assertEqual(true, true); - true -> ?assertEqual(true, false) - end. + ?assert(emqx_packet:validate(?SUBSCRIBE_PACKET(15, #{'Subscription-Identifier' => 1}, [{<<"topic">>, #{qos => ?QOS0}}]))), + ?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">>))), + ?assert(emqx_packet:validate(?CONNECT_PACKET(#mqtt_packet_connect{properties = #{'Receive-Maximum' => 1}}))), + ?assertError(subscription_identifier_invalid, + emqx_packet:validate( + ?SUBSCRIBE_PACKET(15, #{'Subscription-Identifier' => -1}, + [{<<"topic">>, #{qos => ?QOS0}}]))), + ?assertError(topic_filters_invalid, + emqx_packet:validate(?UNSUBSCRIBE_PACKET(1,[]))), + ?assertError(topic_name_invalid, + emqx_packet:validate(?PUBLISH_PACKET(1,<<>>,1,#{},<<"payload">>))), + ?assertError(topic_name_invalid, + emqx_packet:validate(?PUBLISH_PACKET + (1, <<"+/+">>, 1, #{}, <<"payload">>))), + ?assertError(topic_alias_invalid, + emqx_packet:validate( + ?PUBLISH_PACKET + (1, <<"topic">>, 1, #{'Topic-Alias' => 0}, <<"payload">>))), + ?assertError(protocol_error, + emqx_packet:validate( + ?PUBLISH_PACKET(1, <<"topic">>, 1, + #{'Subscription-Identifier' => 10}, <<"payload">>))), + ?assertError(protocol_error, + emqx_packet:validate( + ?PUBLISH_PACKET(1, <<"topic">>, 1, + #{'Response-Topic' => <<"+/+">>}, <<"payload">>))), + ?assertError(protocol_error, + emqx_packet:validate( + ?CONNECT_PACKET(#mqtt_packet_connect{ + properties = + #{'Request-Response-Information' => -1}}))), + ?assertError(protocol_error, + emqx_packet:validate( + ?CONNECT_PACKET(#mqtt_packet_connect{ + properties = + #{'Request-Problem-Information' => 2}}))), + ?assertError(protocol_error, + emqx_packet:validate( + ?CONNECT_PACKET(#mqtt_packet_connect{ + properties = + #{'Receive-Maximum' => 0}}))). packet_message(_) -> Pkt = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, @@ -83,16 +118,14 @@ packet_format(_) -> 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 = ?QOS2, - will_topic = <<"topic">>, - will_props = #{}, + Pkt = #mqtt_packet_connect{ will_flag = true, + client_id = <<"clientid">>, + username = "test", + will_retain = true, + will_qos = ?QOS2, + 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_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl index 2323519b1..f97f475f8 100644 --- a/test/emqx_protocol_SUITE.erl +++ b/test/emqx_protocol_SUITE.erl @@ -19,114 +19,211 @@ -compile(export_all). -compile(nowarn_export_all). --include("emqx.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-include_lib("common_test/include/ct.hrl"). -include("emqx_mqtt.hrl"). --include_lib("eunit/include/eunit.hrl"). +-define(TOPICS, [<<"TopicA">>, <<"TopicA/B">>, <<"Topic/C">>, <<"TopicA/C">>, + <<"/TopicA">>]). --import(emqx_serializer, [serialize/1]). +-define(CLIENT2, ?CONNECT_PACKET(#mqtt_packet_connect{ + username = <<"admin">>, + clean_start = false, + password = <<"public">>})). all() -> - [%% {group, parser}, - %% {group, serializer}, - {group, packet}, - {group, message}]. + [ + {group, mqttv4}, + {group, mqttv5}]. groups() -> - [%% {parser, [], - %% [ - %% parse_connect, - %% parse_bridge, - %% parse_publish, - %% parse_puback, - %% parse_pubrec, - %% parse_pubrel, - %% parse_pubcomp, - %% parse_subscribe, - %% parse_unsubscribe, - %% parse_pingreq, - %% parse_disconnect]}, - %% {serializer, [], - %% [serialize_connect, - %% serialize_connack, - %% serialize_publish, - %% serialize_puback, - %% serialize_pubrel, - %% serialize_subscribe, - %% serialize_suback, - %% serialize_unsubscribe, - %% serialize_unsuback, - %% serialize_pingreq, - %% serialize_pingresp, - %% serialize_disconnect]}, - {packet, [], - [packet_proto_name, - packet_type_name, - packet_format]}, - {message, [], - [message_make - %% message_from_packet - ]} - ]. + [{mqttv4, + [sequence], + [ + connect_v4, + subscribe_v4 + ]}, + {mqttv5, + [sequence], + [ + connect_v5, + subscribe_v5 + ] + }]. + +init_per_suite(Config) -> + emqx_ct_broker_helpers:run_setup_steps(), + Config. + +end_per_suite(_Config) -> + emqx_ct_broker_helpers:run_teardown_steps(). + +with_connection(DoFun) -> + {ok, Sock} = emqx_client_sock:connect({127, 0, 0, 1}, 1883, + [binary, {packet, raw}, + {active, false}], 3000), + try + DoFun(Sock) + after + emqx_client_sock:close(Sock) + end. + +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), -%%-------------------------------------------------------------------- -%% Packet Cases -%%-------------------------------------------------------------------- + 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), -packet_proto_name(_) -> - ?assertEqual(<<"MQIsdp">>, emqx_packet:protocol_name(3)), - ?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(4)). + 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, + #{'Response-Information' := _RespInfo}), _} = + raw_recv_parse(Data, ?MQTT_PROTO_V5) + end), + ok. -packet_type_name(_) -> - ?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT)), - ?assertEqual('UNSUBSCRIBE', emqx_packet:type_name(?UNSUBSCRIBE)). +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). -%% packet_connack_name(_) -> -%% ?assertEqual('CONNACK_ACCEPT', emqx_packet:connack_name(?CONNACK_ACCEPT)), -%% ?assertEqual('CONNACK_PROTO_VER', emqx_packet:connack_name(?CONNACK_PROTO_VER)), -%% ?assertEqual('CONNACK_INVALID_ID', emqx_packet:connack_name(?CONNACK_INVALID_ID)), -%% ?assertEqual('CONNACK_SERVER', emqx_packet:connack_name(?CONNACK_SERVER)), -%% ?assertEqual('CONNACK_CREDENTIALS', emqx_packet:connack_name(?CONNACK_CREDENTIALS)), -%% ?assertEqual('CONNACK_AUTH', emqx_packet:connack_name(?CONNACK_AUTH)). +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. -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))]), - io:format("~s", [emqx_packet:format(?PUBLISH_PACKET(?QOS_2, <<"topic">>, 10, <<"payload">>))]), - io:format("~s", [emqx_packet:format(?PUBACK_PACKET(?PUBACK, 98))]), - io:format("~s", [emqx_packet:format(?PUBREL_PACKET(99))]), - io:format("~s", [emqx_packet:format(?SUBSCRIBE_PACKET(15, [{<<"topic">>, ?QOS0}, {<<"topic1">>, ?QOS1}]))]), - io:format("~s", [emqx_packet:format(?SUBACK_PACKET(40, [?QOS0, ?QOS1]))]), - io:format("~s", [emqx_packet:format(?UNSUBSCRIBE_PACKET(89, [<<"t">>, <<"t2">>]))]), - io:format("~s", [emqx_packet:format(?UNSUBACK_PACKET(90))]). +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), + {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), + {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. -%%-------------------------------------------------------------------- -%% Message Cases -%%-------------------------------------------------------------------- +publish_v4(_) -> + ok. -message_make(_) -> - Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), - ?assertEqual(0, Msg#message.qos), - Msg1 = emqx_message:make(<<"clientid">>, qos2, <<"topic">>, <<"payload">>), - ?assert(is_binary(Msg1#message.id)), - ?assertEqual(qos2, Msg1#message.qos). +publish_v5(_) -> + ok. -%% message_from_packet(_) -> -%% Msg = emqx_message:from_packet(?PUBLISH_PACKET(1, <<"topic">>, 10, <<"payload">>)), -%% ?assertEqual(1, Msg#message.qos), -%% %% ?assertEqual(10, Msg#message.pktid), -%% ?assertEqual(<<"topic">>, Msg#message.topic), -%% WillMsg = emqx_message:from_packet(#mqtt_packet_connect{will_flag = true, -%% will_topic = <<"WillTopic">>, -%% will_payload = <<"WillMsg">>}), -%% ?assertEqual(<<"WillTopic">>, WillMsg#message.topic), -%% ?assertEqual(<<"WillMsg">>, WillMsg#message.payload). - - %% Msg2 = emqx_message:fomat_packet(<<"username">>, <<"clientid">>, - %% ?PUBLISH_PACKET(1, <<"topic">>, 20, <<"payload">>)), +raw_send_serialize(Packet) -> + emqx_frame:serialize(Packet). +raw_send_serialize(Packet, Opts) -> + emqx_frame:serialize(Packet, Opts). +raw_recv_parse(P, ProtoVersion) -> + emqx_frame:parse(P, {none, #{max_packet_size => ?MAX_PACKET_SIZE, + version => ProtoVersion}}). diff --git a/test/emqx_topic_SUITE.erl b/test/emqx_topic_SUITE.erl index 816579ebc..8ce3faf79 100644 --- a/test/emqx_topic_SUITE.erl +++ b/test/emqx_topic_SUITE.erl @@ -21,7 +21,7 @@ -compile(nowarn_export_all). -import(emqx_topic, [wildcard/1, match/2, validate/1, triples/1, join/1, - words/1, systop/1, feed_var/3, parse/1, parse/2]). + words/1, systop/1, feed_var/3, parse/1]). -define(N, 10000). @@ -57,10 +57,10 @@ t_match(_) -> true = match(<<"abc">>, <<"+">>), true = match(<<"a/b/c">>, <<"a/b/c">>), false = match(<<"a/b/c">>, <<"a/c/d">>), - false = match(<<"$shared/x/y">>, <<"+">>), - false = match(<<"$shared/x/y">>, <<"+/x/y">>), - false = match(<<"$shared/x/y">>, <<"#">>), - false = match(<<"$shared/x/y">>, <<"+/+/#">>), + false = match(<<"$share/x/y">>, <<"+">>), + false = match(<<"$share/x/y">>, <<"+/x/y">>), + false = match(<<"$share/x/y">>, <<"#">>), + false = match(<<"$share/x/y">>, <<"+/+/#">>), false = match(<<"house/1/sensor/0">>, <<"house/+">>), false = match(<<"house">>, <<"house/+">>). @@ -77,10 +77,10 @@ t_match2(_) -> true = match(<<"abc">>, <<"+">>), true = match(<<"a/b/c">>, <<"a/b/c">>), false = match(<<"a/b/c">>, <<"a/c/d">>), - false = match(<<"$shared/x/y">>, <<"+">>), - false = match(<<"$shared/x/y">>, <<"+/x/y">>), - false = match(<<"$shared/x/y">>, <<"#">>), - false = match(<<"$shared/x/y">>, <<"+/+/#">>), + false = match(<<"$share/x/y">>, <<"+">>), + false = match(<<"$share/x/y">>, <<"+/x/y">>), + false = match(<<"$share/x/y">>, <<"#">>), + false = match(<<"$share/x/y">>, <<"+/+/#">>), false = match(<<"house/1/sensor/0">>, <<"house/+">>). t_match3(_) -> @@ -208,4 +208,3 @@ t_parse(_) -> ?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">>)). - diff --git a/test/rfc6455_client.erl b/test/rfc6455_client.erl index 4696f7ab3..f5d8f1ef4 100644 --- a/test/rfc6455_client.erl +++ b/test/rfc6455_client.erl @@ -153,7 +153,7 @@ do_recv2(State = #state{phase = Phase, socket = Socket, ppid = PPid}, R) -> ok end, die(Socket, PPid, WsReason, normal); - {_, _, _, Rest2} -> + {_, _, _, _Rest2} -> io:format("Unknown frame type~n"), die(Socket, PPid, {1006, "Unknown frame type"}, normal) end. diff --git a/test/ws_client.erl b/test/ws_client.erl index f049e3256..25f38164d 100644 --- a/test/ws_client.erl +++ b/test/ws_client.erl @@ -1,7 +1,5 @@ -module(ws_client). --behaviour(websocket_client_handler). - -export([ start_link/0, start_link/1, From 73658b39536f595eea4031b0aa28a409be58452f Mon Sep 17 00:00:00 2001 From: huangdan Date: Fri, 19 Oct 2018 15:46:25 +0800 Subject: [PATCH 369/520] Merged emqx_misc_SUITE to emqx_misc_tests (#1890) * Merged emqx_misc_SUITE to emqx_misc_tests --- Makefile | 3 ++- test/emqx_misc_SUITE.erl | 45 ---------------------------------------- test/emqx_misc_tests.erl | 25 +++++++++++++++++++++- 3 files changed, 26 insertions(+), 47 deletions(-) delete mode 100644 test/emqx_misc_SUITE.erl diff --git a/Makefile b/Makefile index 9a114399c..f9c8bd201 100644 --- a/Makefile +++ b/Makefile @@ -35,9 +35,10 @@ EUNIT_OPTS = verbose # CT_SUITES = emqx_frame ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat + CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_connection emqx_session \ emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight emqx_json \ - emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ + emqx_keepalive emqx_lib emqx_metrics emqx_mod emqx_mqtt_caps \ emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_mountpoint \ emqx_listeners emqx_protocol emqx_pool emqx_shared_sub diff --git a/test/emqx_misc_SUITE.erl b/test/emqx_misc_SUITE.erl deleted file mode 100644 index 766691869..000000000 --- a/test/emqx_misc_SUITE.erl +++ /dev/null @@ -1,45 +0,0 @@ -%% Copyright (c) 2018 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_misc_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}]). - -all() -> [t_merge_opts]. - -t_merge_opts(_) -> - Opts = emqx_misc:merge_opts(?SOCKOPTS, [raw, - binary, - {backlog, 1024}, - {nodelay, false}, - {max_clients, 1024}, - {acceptors, 16}]), - ?assertEqual(1024, proplists:get_value(backlog, Opts)), - ?assertEqual(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). diff --git a/test/emqx_misc_tests.erl b/test/emqx_misc_tests.erl index 50513ee86..92b0973e8 100644 --- a/test/emqx_misc_tests.erl +++ b/test/emqx_misc_tests.erl @@ -15,6 +15,30 @@ -module(emqx_misc_tests). -include_lib("eunit/include/eunit.hrl"). +-define(SOCKOPTS, [binary, + {packet, raw}, + {reuseaddr, true}, + {backlog, 512}, + {nodelay, true}]). + + +t_merge_opts_test() -> + Opts = emqx_misc:merge_opts(?SOCKOPTS, [raw, + binary, + {backlog, 1024}, + {nodelay, false}, + {max_clients, 1024}, + {acceptors, 16}]), + ?assertEqual(1024, proplists:get_value(backlog, Opts)), + ?assertEqual(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). + timer_cancel_flush_test() -> Timer = emqx_misc:start_timer(0, foo), ok = emqx_misc:cancel_timer(Timer), @@ -39,4 +63,3 @@ message_queue_too_long_test() -> conn_proc_mng_policy(L) -> emqx_misc:conn_proc_mng_policy(#{message_queue_len => L}). - From d10edfe025cfcd84fba4fb6dc1cde85b57d5faf7 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Fri, 19 Oct 2018 16:05:59 +0800 Subject: [PATCH 370/520] Resolve merge conflict --- test/emqx_misc_SUITE.erl | 52 ---------------------------------------- 1 file changed, 52 deletions(-) delete mode 100644 test/emqx_misc_SUITE.erl diff --git a/test/emqx_misc_SUITE.erl b/test/emqx_misc_SUITE.erl deleted file mode 100644 index 87f51746a..000000000 --- a/test/emqx_misc_SUITE.erl +++ /dev/null @@ -1,52 +0,0 @@ -%% Copyright (c) 2018 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_misc_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}]). - -all() -> [t_merge_opts, t_init_proc_mng_policy]. - -t_merge_opts(_) -> - Opts = emqx_misc:merge_opts(?SOCKOPTS, [raw, - binary, - {backlog, 1024}, - {nodelay, false}, - {max_clients, 1024}, - {acceptors, 16}]), - ?assertEqual(1024, proplists:get_value(backlog, Opts)), - ?assertEqual(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). - -t_init_proc_mng_policy(_) -> - application:set_env(emqx, zones, [{policy, [{force_shutdown_policy, #{max_heap_size => 1}}]}]), - {ok, _} = emqx_zone:start_link(), - ok = emqx_misc:init_proc_mng_policy(policy), - ok = emqx_misc:init_proc_mng_policy(undefined), - emqx_zone:stop(). From 873a08dc94b061747a9e6282ce5ffcb858e34467 Mon Sep 17 00:00:00 2001 From: tigercl Date: Fri, 19 Oct 2018 16:21:43 +0800 Subject: [PATCH 371/520] Improve coverage for emqx_hooks, and add test case for emqx_mod_sup (#1892) Improve coverage for emqx_hooks, and add test case for emqx_mod_sup --- Makefile | 3 +-- test/emqx_hooks_SUITE.erl | 19 ++++++++++++++++++- test/emqx_mod_sup_SUITE.erl | 28 ++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 test/emqx_mod_sup_SUITE.erl diff --git a/Makefile b/Makefile index f9c8bd201..dbd503864 100644 --- a/Makefile +++ b/Makefile @@ -35,10 +35,9 @@ EUNIT_OPTS = verbose # CT_SUITES = emqx_frame ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat - CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_connection emqx_session \ emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight emqx_json \ - emqx_keepalive emqx_lib emqx_metrics emqx_mod emqx_mqtt_caps \ + emqx_keepalive emqx_lib emqx_metrics emqx_mod emqx_mod_sup emqx_mqtt_caps \ emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_mountpoint \ emqx_listeners emqx_protocol emqx_pool emqx_shared_sub diff --git a/test/emqx_hooks_SUITE.erl b/test/emqx_hooks_SUITE.erl index b5b278e31..1961d0e02 100644 --- a/test/emqx_hooks_SUITE.erl +++ b/test/emqx_hooks_SUITE.erl @@ -43,7 +43,7 @@ add_delete_hook(_) -> {callback, {?MODULE, hook_fun2, []}, undefined, 8}], ?assertEqual(Callbacks2, emqx_hooks:lookup(emqx_hook)), ok = emqx:unhook(emqx_hook, {?MODULE, hook_fun1, []}), - ok = emqx:unhook(emqx_hook, {?MODULE, hook_fun2, []}), + ok = emqx:unhook(emqx_hook, {?MODULE, hook_fun2}), timer:sleep(1000), ?assertEqual([], emqx_hooks:lookup(emqx_hook)), ok = emqx_hooks:stop(). @@ -62,6 +62,17 @@ run_hooks(_) -> ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun7/2, [initArg]), ok = emqx:hook(foreach_hook, fun ?MODULE:hook_fun8/2, [initArg]), stop = emqx:run_hooks(foreach_hook, [arg]), + + ok = emqx:hook(foldl_hook2, fun ?MODULE:hook_fun9/2), + ok = emqx:hook(foldl_hook2, {?MODULE, hook_fun10, []}), + {stop, []} = emqx:run_hooks(foldl_hook2, [arg], []), + + ok = emqx:hook(filter1_hook, {?MODULE, hook_fun1, []}, {?MODULE, hook_filter1, []}, 0), + ok = emqx:run_hooks(filter1_hook, [arg]), + + ok = emqx:hook(filter2_hook, {?MODULE, hook_fun2, []}, {?MODULE, hook_filter2, []}), + {ok, []} = emqx:run_hooks(filter2_hook, [arg], []), + ok = emqx_hooks:stop(). hook_fun1([]) -> ok. @@ -75,3 +86,9 @@ hook_fun6(arg, initArg) -> ok. hook_fun7(arg, initArg) -> any. hook_fun8(arg, initArg) -> stop. +hook_fun9(arg, _Acc) -> any. +hook_fun10(arg, _Acc) -> stop. + +hook_filter1(arg) -> true. +hook_filter2(arg, _Acc) -> true. + diff --git a/test/emqx_mod_sup_SUITE.erl b/test/emqx_mod_sup_SUITE.erl new file mode 100644 index 000000000..87245e351 --- /dev/null +++ b/test/emqx_mod_sup_SUITE.erl @@ -0,0 +1,28 @@ +%% Copyright (c) 2018 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]. + +t_child_all(_) -> + {ok, _Pid} = emqx_mod_sup:start_link(), + {ok, _Child} = emqx_mod_sup:start_child(emqx_banned, worker), + timer:sleep(10), + ok = emqx_mod_sup:stop_child(emqx_banned). From 35460d8227c2362e3b5a8735007557df73db9529 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Mon, 22 Oct 2018 09:04:03 +0800 Subject: [PATCH 372/520] Refactor send_fun --- Makefile | 3 +-- src/emqx_connection.erl | 4 ++-- src/emqx_protocol.erl | 2 +- src/emqx_ws_connection.erl | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 368aff6a9..01ca4885a 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ EUNIT_OPTS = verbose ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat CT_SUITES = emqx emqx_zone emqx_banned emqx_connection emqx_session emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight \ - emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_misc emqx_mod emqx_mqtt_caps \ + emqx_json emqx_keepalive emqx_lib emqx_metrics emqx_mod emqx_mqtt_caps \ emqx_mqtt_compat emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_stats emqx_tables emqx_time emqx_topic emqx_trie emqx_vm \ emqx_mountpoint emqx_listeners emqx_protocol emqx_pool emqx_shared_sub @@ -138,4 +138,3 @@ dep-vsn-check: {[], []} -> halt(0); \ {Rebar, Mk} -> erlang:error({deps_version_discrepancy, [{rebar, Rebar}, {mk, Mk}]}) \ end." - diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 37665ac10..6c3d4c7e6 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -165,8 +165,8 @@ init_limiter({Rate, Burst}) -> esockd_rate_limit:new(Rate, Burst). send_fun(Transport, Socket, Peername) -> - fun(Serialize, Packet, Options) -> - Data = Serialize(Packet, Options), + fun(Packet, Options) -> + Data = emqx_frame:serialize(Packet, Options), try Transport:async_send(Socket, Data) of ok -> ?LOG(debug, "SEND ~p", [iolist_to_binary(Data)], #state{peername = Peername}), diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 3638e94e2..2c128810b 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -555,7 +555,7 @@ deliver({disconnect, _ReasonCode}, PState) -> -spec(send(emqx_mqtt_types:packet(), state()) -> {ok, state()} | {error, term()}). send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = SendFun}) -> trace(send, Packet, PState), - case SendFun(fun emqx_frame:serialize/2, Packet, #{version => Ver}) of + case SendFun(Packet, #{version => Ver}) of ok -> emqx_metrics:sent(Packet), {ok, inc_stats(send, Type, PState)}; diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 1907695af..407525601 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -144,8 +144,8 @@ websocket_init(#state{request = Req, options = Options}) -> idle_timeout = IdleTimout}}. send_fun(WsPid) -> - fun(Serialize, Packet, Options) -> - Data = Serialize(Packet, Options), + fun(Packet, Options) -> + Data = emqx_frame:serialize(Packet, Options), BinSize = iolist_size(Data), emqx_metrics:inc('bytes/sent', BinSize), put(send_oct, get(send_oct) + BinSize), From 3f761cbe6aa3979636db3c21e94e32f95e8368ad Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Tue, 23 Oct 2018 14:37:05 +0800 Subject: [PATCH 373/520] Support use certifate as username Prior to this change, you can just use CN or EN field from the client certificate as username. This change add a new option to allow user to use Certificate directly as username. --- etc/emqx.conf | 6 +++--- priv/emqx.schema | 6 +++--- src/emqx_protocol.erl | 7 ++++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 874a4c560..56fcf5ffc 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1159,10 +1159,10 @@ listener.ssl.external.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-G ## Value: on | off ## listener.ssl.external.honor_cipher_order = on -## Use the CN field from the client certificate as a username. +## Use the CN, EN or CRT field from the client certificate as a username. ## Notice that 'verify' should be set as 'verify_peer'. ## -## Value: cn | en +## Value: cn | en | crt ## listener.ssl.external.peer_cert_as_username = cn ## TCP backlog for the SSL connection. @@ -1522,7 +1522,7 @@ listener.wss.external.certfile = {{ platform_etc_dir }}/certs/cert.pem ## See: listener.ssl.$name.peer_cert_as_username ## -## Value: cn | dn +## Value: cn | dn | crt ## listener.wss.external.peer_cert_as_username = cn ## TCP backlog for the WebSocket/SSL connection. diff --git a/priv/emqx.schema b/priv/emqx.schema index becb4bff4..1424ab240 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -949,7 +949,7 @@ end}. ]}. {mapping, "listener.tcp.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn, dn]}} + {datatype, {enum, [cn, dn, crt]}} ]}. {mapping, "listener.tcp.$name.backlog", "emqx.listeners", [ @@ -1139,7 +1139,7 @@ end}. ]}. {mapping, "listener.ssl.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn, dn]}} + {datatype, {enum, [cn, dn, crt]}} ]}. %%-------------------------------------------------------------------- @@ -1400,7 +1400,7 @@ end}. ]}. {mapping, "listener.wss.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn, dn]}} + {datatype, {enum, [cn, dn, crt]}} ]}. {translation, "emqx.listeners", fun(Conf) -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index c3f0689fa..db239acef 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -106,9 +106,10 @@ init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) 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); - _ -> undefined + cn -> esockd_peercert:common_name(Peercert); + dn -> esockd_peercert:subject(Peercert); + crt -> Peercert; + _ -> undefined end. set_username(Username, PState = #pstate{username = undefined}) -> From 540484e6034ff3daeeb04132eab123d09eb33f43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Tue, 23 Oct 2018 17:27:46 +0800 Subject: [PATCH 374/520] Send and cancel will message by apis rather than hook --- src/emqx_protocol.erl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 7dd0b37e8..32ecd7137 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -638,6 +638,9 @@ try_open_session(PState = #pstate{zone = Zone, case emqx_sm:open_session(SessAttrs1) of {ok, SPid} -> {ok, SPid, false}; + {ok, SPid, true} -> + emqx_delayed_publish:cancel_publish(ClientId), + {ok, SPid, true}; Other -> Other end. @@ -838,11 +841,9 @@ shutdown(Reason, PState = #pstate{connected = true, send_willmsg(undefined, _ClientId) -> ignore; -send_willmsg(WillMsg = #message{topic = Topic, - headers = #{'Will-Delay-Interval' := Interval} = Headers}, ClientId) +send_willmsg(WillMsg = #message{headers = #{'Will-Delay-Interval' := Interval}}, ClientId) when is_integer(Interval), Interval > 0 -> - SendAfter = integer_to_binary(Interval), - emqx_broker:publish(WillMsg#message{topic = emqx_topic:join([<<"$will">>, SendAfter, Topic]), headers = Headers#{client_id => ClientId}}); + emqx_delayed_publish:delay_publish(WillMsg, ClientId); send_willmsg(WillMsg, _ClientId) -> emqx_broker:publish(WillMsg). From a1092a678473e5eccd16a52d5621a37ffff7aefe Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Wed, 24 Oct 2018 15:59:22 +0800 Subject: [PATCH 375/520] Add eunit cases and fix typo. --- src/emqx_protocol.erl | 62 +++++++++++++++++++----------------- test/emqx_protocol_tests.erl | 30 +++++++++++++++++ 2 files changed, 62 insertions(+), 30 deletions(-) create mode 100644 test/emqx_protocol_tests.erl diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index db239acef..fdec62d8b 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -33,35 +33,35 @@ -export([shutdown/2]). -record(pstate, { - zone, - sendfun, - peername, - peercert, - proto_ver, - proto_name, - ackprops, - client_id, - is_assigned, - conn_pid, - conn_props, - ack_props, - username, - session, - clean_start, - topic_aliases, - packet_size, - will_topic, - will_msg, - keepalive, - mountpoint, - is_super, - is_bridge, - enable_ban, - enable_acl, - recv_stats, - send_stats, - connected, - connected_at + zone, + sendfun, + peername, + peercert, + proto_ver, + proto_name, + ackprops, + client_id, + is_assigned, + conn_pid, + conn_props, + ack_props, + username, + session, + clean_start, + topic_aliases, + packet_size, + will_topic, + will_msg, + keepalive, + mountpoint, + is_super, + is_bridge, + enable_ban, + enable_acl, + recv_stats, + send_stats, + connected, + connected_at }). -type(state() :: #pstate{}). @@ -71,6 +71,8 @@ -compile(export_all). -endif. +-define(NO_PROPS, undefined). + -define(LOG(Level, Format, Args, PState), emqx_logger:Level([{client, PState#pstate.client_id}], "MQTT(~s@~s): " ++ Format, [PState#pstate.client_id, esockd_net:format(PState#pstate.peername) | Args])). @@ -672,7 +674,7 @@ authenticate(Credentials, Password) -> {error, Error} end. -set_property(Name, Value, undefined) -> +set_property(Name, Value, ?NO_PROPS) -> #{Name => Value}; set_property(Name, Value, Props) -> Props#{Name => Value}. diff --git a/test/emqx_protocol_tests.erl b/test/emqx_protocol_tests.erl new file mode 100644 index 000000000..56b65e36a --- /dev/null +++ b/test/emqx_protocol_tests.erl @@ -0,0 +1,30 @@ +%% Copyright (c) 2018 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}])). From 6675e3d496b9d6e4f195a9d59382a7a68737e7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 25 Oct 2018 14:26:31 +0800 Subject: [PATCH 376/520] Implement will message delay publish in session, add test case for clean start and will message in connect packet --- src/emqx_protocol.erl | 27 ++------ src/emqx_session.erl | 83 ++++++++++++++++-------- src/emqx_sm.erl | 19 +++--- test/emqx_protocol_SUITE.erl | 119 +++++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 59 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 32ecd7137..2c516a534 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -50,7 +50,6 @@ clean_start, topic_aliases, packet_size, - will_topic, will_msg, keepalive, mountpoint, @@ -142,7 +141,6 @@ attrs(#pstate{zone = Zone, proto_ver = ProtoVer, proto_name = ProtoName, keepalive = Keepalive, - will_topic = WillTopic, mountpoint = Mountpoint, is_super = IsSuper, is_bridge = IsBridge, @@ -156,7 +154,6 @@ attrs(#pstate{zone = Zone, {proto_name, ProtoName}, {clean_start, CleanStart}, {keepalive, Keepalive}, - {will_topic, WillTopic}, {mountpoint, Mountpoint}, {is_super, IsSuper}, {is_bridge, IsBridge}, @@ -285,7 +282,6 @@ process_packet(?CONNECT_PACKET( keepalive = Keepalive, properties = ConnProps, will_props = WillProps, - will_topic = WillTopic, client_id = ClientId, username = Username, password = Password} = Connect), PState) -> @@ -310,7 +306,6 @@ process_packet(?CONNECT_PACKET( clean_start = CleanStart, keepalive = Keepalive, conn_props = ConnProps, - will_topic = WillTopic, will_msg = WillMsg, is_bridge = IsBridge, connected_at = os:timestamp()}), @@ -535,7 +530,6 @@ deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, 'Wildcard-Subscription-Available' => flag(Wildcard), 'Subscription-Identifier-Available' => 1, 'Response-Information' => ResponseInformation, - 'Shared-Subscription-Available' => flag(Shared)}, Props1 = if @@ -624,23 +618,22 @@ try_open_session(PState = #pstate{zone = Zone, client_id = ClientId, conn_pid = ConnPid, username = Username, - clean_start = CleanStart}) -> + clean_start = CleanStart, + will_msg = WillMsg}) -> SessAttrs = #{ zone => Zone, client_id => ClientId, conn_pid => ConnPid, username => Username, - clean_start => CleanStart + clean_start => CleanStart, + will_msg => WillMsg }, SessAttrs1 = lists:foldl(fun set_session_attrs/2, SessAttrs, [{max_inflight, PState}, {expiry_interval, PState}, {topic_alias_maximum, PState}]), case emqx_sm:open_session(SessAttrs1) of {ok, SPid} -> {ok, SPid, false}; - {ok, SPid, true} -> - emqx_delayed_publish:cancel_publish(ClientId), - {ok, SPid, true}; Other -> Other end. @@ -832,21 +825,11 @@ shutdown(Reason, #pstate{client_id = ClientId}) when Reason =:= conflict; Reason =:= discard -> emqx_cm:unregister_connection(ClientId); shutdown(Reason, PState = #pstate{connected = true, - client_id = ClientId, - will_msg = WillMsg}) -> + client_id = ClientId}) -> ?LOG(info, "Shutdown for ~p", [Reason], PState), - _ = send_willmsg(WillMsg, ClientId), emqx_hooks:run('client.disconnected', [credentials(PState), Reason]), emqx_cm:unregister_connection(ClientId). -send_willmsg(undefined, _ClientId) -> - ignore; -send_willmsg(WillMsg = #message{headers = #{'Will-Delay-Interval' := Interval}}, ClientId) - when is_integer(Interval), Interval > 0 -> - emqx_delayed_publish:delay_publish(WillMsg, ClientId); -send_willmsg(WillMsg, _ClientId) -> - emqx_broker:publish(WillMsg). - start_keepalive(0, _PState) -> ignore; start_keepalive(Secs, #pstate{zone = Zone}) when Secs > 0 -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index d327723f5..15911ca7f 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -46,7 +46,7 @@ -export([start_link/1]). -export([info/1, attrs/1]). -export([stats/1]). --export([resume/2, discard/2]). +-export([resume/3, discard/2]). -export([update_expiry_interval/2, update_misc/2]). -export([subscribe/2, subscribe/4]). -export([publish/3]). @@ -147,7 +147,11 @@ %% Created at created_at :: erlang:timestamp(), - topic_alias_maximum :: pos_integer() + topic_alias_maximum :: pos_integer(), + + will_msg :: emqx:message(), + + will_delay_timer :: reference() | undefined }). -type(spid() :: pid()). @@ -307,9 +311,9 @@ unsubscribe(SPid, PacketId, Properties, TopicFilters) -> UnsubReq = {PacketId, Properties, TopicFilters}, gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). --spec(resume(spid(), pid()) -> ok). -resume(SPid, ConnPid) -> - gen_server:cast(SPid, {resume, ConnPid}). +-spec(resume(spid(), pid(), emqx:message()) -> ok). +resume(SPid, ConnPid, WillMsg) -> + gen_server:cast(SPid, {resume, ConnPid, WillMsg}). %% @doc Discard the session -spec(discard(spid(), ByPid :: pid()) -> ok). @@ -338,7 +342,8 @@ init([Parent, #{zone := Zone, clean_start := CleanStart, expiry_interval := ExpiryInterval, max_inflight := MaxInflight, - topic_alias_maximum := TopicAliasMaximum}]) -> + topic_alias_maximum := TopicAliasMaximum, + will_msg := WillMsg}]) -> process_flag(trap_exit, true), true = link(ConnPid), IdleTimout = get_env(Zone, idle_timeout, 30000), @@ -362,7 +367,8 @@ init([Parent, #{zone := Zone, deliver_stats = 0, enqueue_stats = 0, created_at = os:timestamp(), - topic_alias_maximum = TopicAliasMaximum + topic_alias_maximum = TopicAliasMaximum, + will_msg = WillMsg }, emqx_sm:register_session(ClientId, attrs(State)), emqx_sm:set_session_stats(ClientId, stats(State)), @@ -511,17 +517,18 @@ handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight end; %% RESUME: -handle_cast({resume, ConnPid}, State = #state{client_id = ClientId, - conn_pid = OldConnPid, - clean_start = CleanStart, - retry_timer = RetryTimer, - await_rel_timer = AwaitTimer, - expiry_timer = ExpireTimer}) -> +handle_cast({resume, ConnPid, WillMsg}, 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], State), %% Cancel Timers - lists:foreach(fun emqx_misc:cancel_timer/1, [RetryTimer, AwaitTimer, ExpireTimer]), + 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], State); @@ -530,14 +537,16 @@ handle_cast({resume, ConnPid}, State = #state{client_id = ClientId, true = link(ConnPid), - State1 = State#state{conn_pid = ConnPid, - binding = binding(ConnPid), - old_conn_pid = OldConnPid, - clean_start = false, - retry_timer = undefined, - awaiting_rel = #{}, - await_rel_timer = undefined, - expiry_timer = undefined}, + State1 = State#state{conn_pid = ConnPid, + binding = binding(ConnPid), + old_conn_pid = OldConnPid, + clean_start = false, + retry_timer = undefined, + awaiting_rel = #{}, + await_rel_timer = undefined, + expiry_timer = undefined, + will_delay_timer = undefined, + will_msg = WillMsg}, %% Clean Session: true -> false??? CleanStart andalso emqx_sm:set_session_attrs(ClientId, attrs(State1)), @@ -610,13 +619,19 @@ handle_info({timeout, Timer, emit_stats}, end; handle_info({timeout, Timer, expired}, State = #state{expiry_timer = Timer}) -> ?LOG(info, "expired, shutdown now:(", [], State), - shutdown(expired, State); + shutdown(expired, State#state{will_msg = undefined}); -handle_info({'EXIT', ConnPid, Reason}, State = #state{expiry_interval = 0, conn_pid = ConnPid}) -> - {stop, Reason, State#state{conn_pid = undefined}}; +handle_info({timeout, Timer, will_delay}, State = #state{will_msg = WillMsg, will_delay_timer = Timer}) -> + send_willmsg(WillMsg), + {noreply, State#state{will_msg = undefined}}; + +handle_info({'EXIT', ConnPid, Reason}, State = #state{will_msg = WillMsg, expiry_interval = 0, conn_pid = ConnPid}) -> + send_willmsg(WillMsg), + {stop, Reason, State#state{will_msg = undefined, conn_pid = undefined}}; handle_info({'EXIT', ConnPid, _Reason}, State = #state{conn_pid = ConnPid}) -> - {noreply, ensure_expire_timer(State#state{conn_pid = undefined})}; + State1 = ensure_will_delay_timer(State), + {noreply, ensure_expire_timer(State1#state{conn_pid = undefined})}; handle_info({'EXIT', OldPid, _Reason}, State = #state{old_conn_pid = OldPid}) -> %% ignore @@ -631,8 +646,9 @@ handle_info(Info, State) -> emqx_logger:error("[Session] unexpected info: ~p", [Info]), {noreply, State}. -terminate(Reason, #state{client_id = ClientId, conn_pid = ConnPid}) -> +terminate(Reason, #state{will_msg = WillMsg, client_id = ClientId, conn_pid = ConnPid}) -> emqx_hooks:run('session.terminated', [#{client_id => ClientId}, Reason]), + send_willmsg(WillMsg), %% Ensure to shutdown the connection if ConnPid =/= undefined -> @@ -714,6 +730,14 @@ retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now, 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 %%------------------------------------------------------------------------------ @@ -899,6 +923,11 @@ ensure_expire_timer(State = #state{expiry_interval = Interval}) when Interval > 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. + 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)}; diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index d45548a78..963cab1e4 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -59,13 +59,12 @@ open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid end, emqx_sm_locker:trans(ClientId, CleanStart); -open_session(SessAttrs = #{clean_start := false, - client_id := ClientId, - conn_pid := ConnPid, - max_inflight := MaxInflight, - topic_alias_maximum := TopicAliasMaximum}) -> +open_session(SessAttrs = #{clean_start := false, + client_id := ClientId, + max_inflight := MaxInflight, + topic_alias_maximum := TopicAliasMaximum}) -> ResumeStart = fun(_) -> - case resume_session(ClientId, ConnPid) of + case resume_session(ClientId, SessAttrs) of {ok, SPid} -> emqx_session:update_misc(SPid, #{max_inflight => MaxInflight, topic_alias_maximum => TopicAliasMaximum}), {ok, SPid, true}; @@ -92,13 +91,13 @@ discard_session(ClientId, ConnPid) when is_binary(ClientId) -> %% @doc Try to resume a session. -spec(resume_session(emqx_types:client_id()) -> {ok, pid()} | {error, term()}). resume_session(ClientId) -> - resume_session(ClientId, self()). + resume_session(ClientId, #{conn_pid => self(), will_msg => undefined}). -resume_session(ClientId, ConnPid) -> +resume_session(ClientId, #{conn_pid := ConnPid, will_msg := WillMsg}) -> case lookup_session(ClientId) of [] -> {error, not_found}; [{_ClientId, SPid}] -> - ok = emqx_session:resume(SPid, ConnPid), + ok = emqx_session:resume(SPid, ConnPid, WillMsg), {ok, SPid}; Sessions -> [{_, SPid}|StaleSessions] = lists:reverse(Sessions), @@ -106,7 +105,7 @@ resume_session(ClientId, ConnPid) -> lists:foreach(fun({_, StalePid}) -> catch emqx_session:discard(StalePid, ConnPid) end, StaleSessions), - ok = emqx_session:resume(SPid, ConnPid), + ok = emqx_session:resume(SPid, ConnPid, WillMsg), {ok, SPid} end. diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl index f97f475f8..060557c6c 100644 --- a/test/emqx_protocol_SUITE.erl +++ b/test/emqx_protocol_SUITE.erl @@ -131,6 +131,125 @@ connect_v5(_) -> #{'Response-Information' := _RespInfo}), _} = raw_recv_parse(Data, ?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' => 3} + } + ) + ) + ), + {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_DISCONNECT_WITH_WILL_MESSAGE) + ) + ), + + {error, timeout} = gen_tcp:recv(Sock2, 0, 1000), + + % 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} + } + ) + ) + ), + {ok, Data3} = gen_tcp:recv(Sock3, 0), + {ok, ?CONNACK_PACKET(?RC_SUCCESS, 1), _} = raw_recv_parse(Data3, ?MQTT_PROTO_V5), + + emqx_client_sock:send(Sock3, raw_send_serialize( + ?DISCONNECT_PACKET(?RC_DISCONNECT_WITH_WILL_MESSAGE) + ) + ), + + {ok, WillData} = gen_tcp:recv(Sock2, 0), + {ok, ?PUBLISH_PACKET(?QOS_1, <<"TopicA">>, _, <<"will message 2">>), _} = raw_recv_parse(WillData, ?MQTT_PROTO_V5), + + emqx_client_sock:close(Sock2) + end), ok. do_connect(Sock, ProtoVer) -> From db2e47470a1dfd4275d598233221dc183866275f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 25 Oct 2018 16:19:57 +0800 Subject: [PATCH 377/520] Fix bugs --- src/emqx_protocol.erl | 22 +++++++++++----------- src/emqx_session.erl | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 2c516a534..d981309e4 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -281,23 +281,13 @@ process_packet(?CONNECT_PACKET( clean_start = CleanStart, keepalive = Keepalive, properties = ConnProps, - will_props = WillProps, client_id = ClientId, username = Username, password = Password} = Connect), PState) -> %% TODO: Mountpoint... %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) - Connect1 = if - ProtoVer =:= ?MQTT_PROTO_V5 -> - WillDelayInterval = get_property('Will-Delay-Interval', WillProps, 0), - SessionExpiryInterval = get_property('Session-Expiry-Interval', ConnProps, 0), - WillProps1 = set_property('Will-Delay-Interval', erlang:min(SessionExpiryInterval, WillDelayInterval), WillProps), - Connect#mqtt_packet_connect{will_props = WillProps1}; - true -> - Connect - end, - WillMsg = emqx_packet:will_msg(Connect1), + WillMsg = make_will_msg(Connect), PState1 = set_username(Username, PState#pstate{client_id = ClientId, @@ -687,6 +677,16 @@ get_property(_Name, undefined, Default) -> get_property(Name, Props, Default) -> maps:get(Name, Props, Default). +make_will_msg(#mqtt_packet_connect{proto_ver = ProtoVer, + will_props = WillProps} = Connect) -> + emqx_packet:will_msg(if + ProtoVer =:= ?MQTT_PROTO_V5 -> + WillDelayInterval = get_property('Will-Delay-Interval', WillProps, 0), + Connect#mqtt_packet_connect{will_props = set_property('Will-Delay-Interval', WillDelayInterval, WillProps)}; + true -> + Connect + end). + %%------------------------------------------------------------------------------ %% Check Packet %%------------------------------------------------------------------------------ diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 15911ca7f..224b2be82 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -619,7 +619,7 @@ handle_info({timeout, Timer, emit_stats}, end; handle_info({timeout, Timer, expired}, State = #state{expiry_timer = Timer}) -> ?LOG(info, "expired, shutdown now:(", [], State), - shutdown(expired, State#state{will_msg = undefined}); + shutdown(expired, State); handle_info({timeout, Timer, will_delay}, State = #state{will_msg = WillMsg, will_delay_timer = Timer}) -> send_willmsg(WillMsg), From ffa220a87d4caf876bcc30c3adad51fbf37ed5c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 26 Oct 2018 10:46:56 +0800 Subject: [PATCH 378/520] Fix bug in test case --- test/emqx_SUITE.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index 8d8819a8f..c6bea732a 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -121,7 +121,7 @@ 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_serialise(?CLIENT3), + Packet = raw_send_serialize(?CLIENT3), emqx_client_sock:send(Sock, Packet), emqx_client_sock:close(Sock). From 7544a21e25b7e93ec8dc01dead69fbb67632a692 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 26 Oct 2018 14:04:33 +0800 Subject: [PATCH 379/520] Add test cases for emqx_bridge, emqx_mod_rewrite (#1914) --- Makefile | 2 +- src/emqx_mod_rewrite.erl | 1 - test/emqx_bridge_SUITE.erl | 57 +++++++++++++++++++++++++++++ test/emqx_mod_rewrite_tests.erl | 63 +++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 test/emqx_bridge_SUITE.erl create mode 100644 test/emqx_mod_rewrite_tests.erl diff --git a/Makefile b/Makefile index dbd503864..4fdaabe92 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_connection emqx_session emqx_keepalive emqx_lib emqx_metrics emqx_mod emqx_mod_sup emqx_mqtt_caps \ emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_mountpoint \ - emqx_listeners emqx_protocol emqx_pool emqx_shared_sub + emqx_listeners emqx_protocol emqx_pool emqx_shared_sub emqx_bridge CT_NODE_NAME = emqxct@127.0.0.1 CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME) diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index 29dbb660c..25faef166 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -75,4 +75,3 @@ compile(Rules) -> {ok, MP} = re:compile(Re), {rewrite, Topic, MP, Dest} end, Rules). - diff --git a/test/emqx_bridge_SUITE.erl b/test/emqx_bridge_SUITE.erl new file mode 100644 index 000000000..f337e3b4e --- /dev/null +++ b/test/emqx_bridge_SUITE.erl @@ -0,0 +1,57 @@ +%% Copyright (c) 2018 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_bridge_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +all() -> + [bridge_test]. + +init_per_suite(Config) -> + emqx_ct_broker_helpers:run_setup_steps(), + Config. + +end_per_suite(_Config) -> + emqx_ct_broker_helpers:run_teardown_steps(). + +bridge_test(_) -> + {ok, _Pid} = emqx_bridge:start_link(emqx, []), + #{msg := <<"start bridge successfully">>} + = emqx_bridge:start_bridge(emqx), + test_forwards(), + test_subscriptions(0), + test_subscriptions(1), + test_subscriptions(2), + #{msg := <<"stop bridge successfully">>} + = emqx_bridge:stop_bridge(emqx), + ok. + +test_forwards() -> + emqx_bridge:add_forward(emqx, <<"test_forwards">>), + [<<"test_forwards">>] = emqx_bridge:show_forwards(emqx), + emqx_bridge:del_forward(emqx, <<"test_forwards">>), + [] = emqx_bridge:show_forwards(emqx), + ok. + +test_subscriptions(QoS) -> + emqx_bridge:add_subscription(emqx, <<"test_subscriptions">>, QoS), + [{<<"test_subscriptions">>, QoS}] = emqx_bridge:show_subscriptions(emqx), + emqx_bridge:del_subscription(emqx, <<"test_subscriptions">>), + [] = emqx_bridge:show_subscriptions(emqx), + ok. diff --git a/test/emqx_mod_rewrite_tests.erl b/test/emqx_mod_rewrite_tests.erl new file mode 100644 index 000000000..6fea7ee71 --- /dev/null +++ b/test/emqx_mod_rewrite_tests.erl @@ -0,0 +1,63 @@ +%% Copyright (c) 2018 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)). From e56252dac6a2448c85b8a5d200d2db171bea8800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 26 Oct 2018 14:34:22 +0800 Subject: [PATCH 380/520] Fix bugs in test cases --- test/emqx_mock_client.erl | 17 +++++++++-------- test/emqx_sm_SUITE.erl | 11 +++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index 4a49a1fc5..24de66d0a 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -46,14 +46,15 @@ init([ClientId]) -> }. handle_call({start_session, ClientPid, ClientId, Zone}, _From, State) -> - Attrs = #{ zone => Zone, - client_id => ClientId, - conn_pid => ClientPid, - clean_start => true, - username => undefined, - expiry_interval => 0, - max_inflight => 0, - topic_alias_maximum => 0 + Attrs = #{ 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 }, {ok, SessPid} = emqx_sm:open_session(Attrs), {reply, {ok, SessPid}, diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl index 110e13026..2b83b6afb 100644 --- a/test/emqx_sm_SUITE.erl +++ b/test/emqx_sm_SUITE.erl @@ -24,8 +24,15 @@ all() -> [t_open_close_session]. t_open_close_session(_) -> emqx_ct_broker_helpers:run_setup_steps(), {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), - Attrs = #{clean_start => true, client_id => <<"client">>, conn_pid => ClientPid, - zone => internal, username => <<"zhou">>, expiry_interval => 0, max_inflight => 0, topic_alias_maximum => 0}, + Attrs = #{clean_start => true, + client_id => <<"client">>, + conn_pid => ClientPid, + zone => internal, + username => <<"zhou">>, + expiry_interval => 0, + max_inflight => 0, + topic_alias_maximum => 0, + will_msg => undefined}, {ok, SPid} = emqx_sm:open_session(Attrs), [{<<"client">>, SPid}] = emqx_sm:lookup_session(<<"client">>), SPid = emqx_sm:lookup_session_pid(<<"client">>), From 0b44c1b75f4b4c2246d720947e2e0bc459f609a2 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 26 Oct 2018 17:25:31 +0800 Subject: [PATCH 381/520] improve_test_cases --- Makefile | 2 +- test/emqx_connection_SUITE.erl | 47 ---------------------------------- 2 files changed, 1 insertion(+), 48 deletions(-) delete mode 100644 test/emqx_connection_SUITE.erl diff --git a/Makefile b/Makefile index 4fdaabe92..90a46b9cd 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ EUNIT_OPTS = verbose # CT_SUITES = emqx_frame ## emqx_trie emqx_router emqx_frame emqx_mqtt_compat -CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_connection emqx_session \ +CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_session \ emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight emqx_json \ emqx_keepalive emqx_lib emqx_metrics emqx_mod emqx_mod_sup emqx_mqtt_caps \ emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ diff --git a/test/emqx_connection_SUITE.erl b/test/emqx_connection_SUITE.erl deleted file mode 100644 index 716e771b5..000000000 --- a/test/emqx_connection_SUITE.erl +++ /dev/null @@ -1,47 +0,0 @@ -%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. - --module(emqx_connection_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("common_test/include/ct.hrl"). - -all() -> - [{group, connection}]. - -groups() -> - [{connection, [sequence], [t_attrs]}]. - -init_per_suite(Config) -> - emqx_ct_broker_helpers:run_setup_steps(), - Config. - -end_per_suite(_Config) -> - emqx_ct_broker_helpers:run_teardown_steps(). - - -t_attrs(_) -> - {ok, C, _} = emqx_client:start_link([{host, "localhost"}, {client_id, <<"simpleClient">>}, {username, <<"plain">>}, {password, <<"plain">>}]), - [{<<"simpleClient">>, ConnPid}] = emqx_cm:lookup_connection(<<"simpleClient">>), - Attrs = emqx_connection:attrs(ConnPid), - <<"simpleClient">> = proplists:get_value(client_id, Attrs), - <<"plain">> = proplists:get_value(username, Attrs), - emqx_client:disconnect(C). - -%% t_stats() -> -%% {ok, C, _ } = emqx_client; -%% t_stats() -> - From abb2e5c918be8eb2547cff5cc47532b5bf31c435 Mon Sep 17 00:00:00 2001 From: tigercl Date: Fri, 26 Oct 2018 17:27:02 +0800 Subject: [PATCH 382/520] Improve test cases, and fix some bugs (#1920) * Improve emqx_banned, emqx_pqueue, emqx_router test cases * Improve emqx_broker test case, and fix bug in emqx_broker * Add emqx_hooks to CT_SUITES --- Makefile | 2 +- src/emqx_banned.erl | 5 ++++ src/emqx_broker.erl | 6 ++-- test/emqx_banned_SUITE.erl | 16 +++++++---- test/emqx_broker_SUITE.erl | 13 +++++++-- test/emqx_pqueue_SUITE.erl | 56 +++++++++++++++++++++++++++++++++++++- test/emqx_router_SUITE.erl | 20 ++++++++++---- 7 files changed, 100 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 90a46b9cd..23a7da11c 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_session \ emqx_keepalive emqx_lib emqx_metrics emqx_mod emqx_mod_sup emqx_mqtt_caps \ emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_mountpoint \ - emqx_listeners emqx_protocol emqx_pool emqx_shared_sub emqx_bridge + emqx_listeners emqx_protocol emqx_pool emqx_shared_sub emqx_bridge emqx_hooks CT_NODE_NAME = emqxct@127.0.0.1 CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME) diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index 8f1c3156f..175271306 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -102,8 +102,13 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- +-ifdef(TEST). +ensure_expiry_timer(State) -> + State#{expiry_timer := emqx_misc:start_timer(timer:seconds(2), expire)}. +-else. ensure_expiry_timer(State) -> State#{expiry_timer := emqx_misc:start_timer(timer:minutes(5), expire)}. +-endif. expire_banned_items(Now) -> mnesia:foldl(fun diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index f0946e92d..0828d6be3 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -260,9 +260,11 @@ subscription(Topic, Subscriber) -> -spec(subscribed(emqx_topic:topic(), pid() | emqx_types:subid() | emqx_types:subscriber()) -> boolean()). subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> - length(ets:match_object(?SUBOPTION, {{Topic, {SubPid, '_'}}, '_'}, 1)) >= 1; + {Match, _} = ets:match_object(?SUBOPTION, {{Topic, {SubPid, '_'}}, '_'}, 1), + length(Match) >= 1; subscribed(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> - length(ets:match_object(?SUBOPTION, {{Topic, {'_', SubId}}, '_'}, 1)) >= 1; + {Match, _} = ets:match_object(?SUBOPTION, {{Topic, {'_', SubId}}, '_'}, 1), + length(Match) >= 1; subscribed(Topic, {SubPid, SubId}) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> ets:member(?SUBOPTION, {Topic, {SubPid, SubId}}). diff --git a/test/emqx_banned_SUITE.erl b/test/emqx_banned_SUITE.erl index c91aeae45..9d4c85134 100644 --- a/test/emqx_banned_SUITE.erl +++ b/test/emqx_banned_SUITE.erl @@ -29,13 +29,17 @@ t_banned_all(_) -> emqx_ct_broker_helpers:run_setup_steps(), emqx_banned:start_link(), TimeNow = erlang:system_time(second), - ok = emqx_banned:add(#banned{who = {client_id, <<"TestClient">>}, - reason = <<"test">>, - by = <<"banned suite">>, - desc = <<"test">>, - until = TimeNow + 10}), + Banned = #banned{who = {client_id, <<"TestClient">>}, + reason = <<"test">>, + by = <<"banned suite">>, + desc = <<"test">>, + until = TimeNow + 1}, + 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 - timer:sleep(100), + ?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:del({client_id, <<"TestClient">>}), ?assertNot(emqx_banned:check(#{client_id => <<"TestClient">>, username => undefined, peername => {undefined, undefined}})), diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index 7baa248f3..7fcc2a598 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -60,6 +60,11 @@ 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(<<"topic">>, <<"clientId">>), + Topics = emqx:topics(), + lists:foreach(fun(Topic) -> + ?assert(lists:member(Topic, Topics)) + end, Topics), ok = emqx:unsubscribe(<<"topic">>, <<"clientId">>), ok = emqx:unsubscribe(<<"topic/1">>, <<"clientId">>), ok = emqx:unsubscribe(<<"topic/2">>, <<"clientId">>). @@ -72,12 +77,16 @@ publish(_) -> ?assert(receive {dispatch, <<"test/+">>, Msg} -> true after 5 -> false end). pubsub(_) -> + true = emqx:is_running(node()), Self = self(), Subscriber = {Self, <<"clientId">>}, ok = emqx:subscribe(<<"a/b/c">>, <<"clientId">>, #{ qos => 1 }), - #{ qos := 1} = ets:lookup_element(emqx_suboption, {<<"a/b/c">>, Subscriber}, 2), + #{qos := 1} = ets:lookup_element(emqx_suboption, {<<"a/b/c">>, Subscriber}, 2), + #{qos := 1} = emqx:get_subopts(<<"a/b/c">>, Subscriber), + true = emqx:set_subopts(<<"a/b/c">>, Subscriber, #{qos => 0}), + #{qos := 0} = emqx:get_subopts(<<"a/b/c">>, Subscriber), ok = emqx:subscribe(<<"a/b/c">>, <<"clientId">>, #{ qos => 2 }), - #{ qos := 2} = ets:lookup_element(emqx_suboption, {<<"a/b/c">>, Subscriber}, 2), + #{qos := 2} = ets:lookup_element(emqx_suboption, {<<"a/b/c">>, Subscriber}, 2), %% ct:log("Emq Sub: ~p.~n", [ets:lookup(emqx_suboption, {<<"a/b/c">>, Subscriber})]), timer:sleep(10), [{<<"a/b/c">>, #{qos := 2}}] = emqx_broker:subscriptions(Subscriber), diff --git a/test/emqx_pqueue_SUITE.erl b/test/emqx_pqueue_SUITE.erl index e610a7639..e7672cb0b 100644 --- a/test/emqx_pqueue_SUITE.erl +++ b/test/emqx_pqueue_SUITE.erl @@ -22,7 +22,7 @@ -define(PQ, emqx_pqueue). -all() -> [t_priority_queue_plen, t_priority_queue_out2]. +all() -> [t_priority_queue_plen, t_priority_queue_out2, t_priority_queues]. t_priority_queue_plen(_) -> Q = ?PQ:new(), @@ -67,3 +67,57 @@ t_priority_queue_out2(_) -> {Val5, Q6} = ?PQ:out(Q5), {value, a} = Val5, {empty, _Q7} = ?PQ:out(Q6). + +t_priority_queues(_) -> + Q0 = ?PQ:new(), + Q1 = ?PQ:new(), + PQueue = {pqueue, [{0, Q0}, {1, Q1}]}, + ?assert(?PQ:is_queue(PQueue)), + [] = ?PQ:to_list(PQueue), + + PQueue1 = ?PQ:in(a, 0, ?PQ:new()), + PQueue2 = ?PQ:in(b, 0, PQueue1), + + PQueue3 = ?PQ:in(c, 1, PQueue2), + PQueue4 = ?PQ:in(d, 1, PQueue3), + + 4 = ?PQ:len(PQueue4), + + [{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), + + PQueue5 = ?PQ:in(e, infinity, PQueue4), + PQueue6 = ?PQ:in(f, 1, PQueue5), + + {{value, e}, PQueue7} = ?PQ:out(PQueue6), + {empty, _} = ?PQ:out(0, ?PQ:new()), + + {empty, Q0} = ?PQ:out_p(Q0), + + Q2 = ?PQ:in(a, Q0), + Q3 = ?PQ:in(b, Q2), + Q4 = ?PQ:in(c, Q3), + + {{value, a, 0}, _Q5} = ?PQ:out_p(Q4), + + {{value,c,1}, PQueue8} = ?PQ:out_p(PQueue7), + + Q4 = ?PQ:join(Q4, ?PQ:new()), + Q4 = ?PQ:join(?PQ:new(), Q4), + + {queue, [a], [a], 2} = ?PQ:join(Q2, Q2), + + {pqueue,[{-1,{queue,[f],[d],2}}, + {0,{queue,[a],[a,b],3}}]} = ?PQ:join(PQueue8, Q2), + + {pqueue,[{-1,{queue,[f],[d],2}}, + {0,{queue,[b],[a,a],3}}]} = ?PQ:join(Q2, PQueue8), + + {pqueue,[{-1,{queue,[f],[d,f,d],4}}, + {0,{queue,[b],[a,b,a],4}}]} = ?PQ:join(PQueue8, PQueue8). + + diff --git a/test/emqx_router_SUITE.erl b/test/emqx_router_SUITE.erl index 196b1678e..a35da9c5d 100644 --- a/test/emqx_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -29,7 +29,9 @@ all() -> groups() -> [{route, [sequence], [add_del_route, - match_routes]}]. + match_routes, + has_routes, + router_add_del]}]. init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), @@ -81,6 +83,7 @@ match_routes(_) -> has_routes(_) -> From = {self(), make_ref()}, ?R:add_route(From, <<"devices/+/messages">>, node()), + timer:sleep(200), ?assert(?R:has_routes(<<"devices/+/messages">>)). clear_tables() -> @@ -88,28 +91,33 @@ clear_tables() -> router_add_del(_) -> ?R:add_route(<<"#">>), - ?R:add_route(<<"a/b/c">>), + ?R:add_route(<<"a/b/c">>, node()), ?R:add_route(<<"+/#">>), Routes = [R1, R2 | _] = [ #route{topic = <<"#">>, dest = node()}, #route{topic = <<"+/#">>, dest = node()}, #route{topic = <<"a/b/c">>, dest = node()}], + timer:sleep(500), ?assertEqual(Routes, lists:sort(?R:match_routes(<<"a/b/c">>))), + ?R:print_routes(<<"a/b/c">>), + %% Batch Add lists:foreach(fun(R) -> ?R:add_route(R) end, Routes), ?assertEqual(Routes, lists:sort(?R:match_routes(<<"a/b/c">>))), %% Del - ?R:del_route(<<"a/b/c">>), - [R1, R2] = lists:sort(?R:match(<<"a/b/c">>)), + ?R:del_route(<<"a/b/c">>, node()), + timer:sleep(500), + [R1, R2] = lists:sort(?R:match_routes(<<"a/b/c">>)), {atomic, []} = mnesia:transaction(fun emqx_trie:lookup/1, [<<"a/b/c">>]), %% Batch Del R3 = #route{topic = <<"#">>, dest = 'a@127.0.0.1'}, ?R:add_route(R3), - ?R:del_route(R1), + ?R:del_route(<<"#">>), ?R:del_route(R2), ?R:del_route(R3), - [] = lists:sort(?R:match(<<"a/b/c">>)). + timer:sleep(500), + [] = lists:sort(?R:match_routes(<<"a/b/c">>)). From f7285d5a587915fa78037c03bf088d8704cd9060 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Fri, 26 Oct 2018 17:30:39 +0800 Subject: [PATCH 383/520] Delete ackprops in pstate Prior to this change, ackprops is duplicated with ack_props This change delete ackprops. --- src/emqx_protocol.erl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index fdec62d8b..4a784252b 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -39,7 +39,6 @@ peercert, proto_ver, proto_name, - ackprops, client_id, is_assigned, conn_pid, @@ -606,10 +605,10 @@ send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = SendFun %%------------------------------------------------------------------------------ %% Assign a clientid -maybe_assign_client_id(PState = #pstate{client_id = <<>>, ackprops = AckProps}) -> +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, ackprops = AckProps1}; + PState#pstate{client_id = ClientId, is_assigned = true, ack_props = AckProps1}; maybe_assign_client_id(PState) -> PState. From 92251d4a8a497bb29fb48293cad2b6e26c3e34f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 26 Oct 2018 17:45:13 +0800 Subject: [PATCH 384/520] Make more normalize --- src/emqx_protocol.erl | 6 +++--- src/emqx_session.erl | 2 +- test/emqx_keepalive_SUITE.erl | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index d981309e4..e3ce36bd5 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -631,14 +631,14 @@ set_session_attrs({max_inflight, #pstate{zone = Zone, proto_ver = ProtoVer, conn maps:put(max_inflight, if ProtoVer =:= ?MQTT_PROTO_V5 -> get_property('Receive-Maximum', ConnProps, 65535); - true -> + true -> emqx_zone:get_env(Zone, max_inflight, 65535) end, SessAttrs); set_session_attrs({expiry_interval, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps, clean_start = CleanStart}}, SessAttrs) -> maps:put(expiry_interval, if ProtoVer =:= ?MQTT_PROTO_V5 -> get_property('Session-Expiry-Interval', ConnProps, 0); - true -> + true -> case CleanStart of true -> 0; false -> @@ -649,7 +649,7 @@ set_session_attrs({topic_alias_maximum, #pstate{zone = Zone, proto_ver = ProtoVe maps:put(topic_alias_maximum, if ProtoVer =:= ?MQTT_PROTO_V5 -> get_property('Topic-Alias-Maximum', ConnProps, 0); - true -> + true -> emqx_zone:get_env(Zone, max_topic_alias, 0) end, SessAttrs); set_session_attrs({_, #pstate{}}, SessAttrs) -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 224b2be82..e123516e9 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -311,7 +311,7 @@ unsubscribe(SPid, PacketId, Properties, TopicFilters) -> UnsubReq = {PacketId, Properties, TopicFilters}, gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). --spec(resume(spid(), pid(), emqx:message()) -> ok). +-spec(resume(spid(), pid(), emqx:message() | undefined) -> ok). resume(SPid, ConnPid, WillMsg) -> gen_server:cast(SPid, {resume, ConnPid, WillMsg}). diff --git a/test/emqx_keepalive_SUITE.erl b/test/emqx_keepalive_SUITE.erl index 60472fd42..c4dbd80f2 100644 --- a/test/emqx_keepalive_SUITE.erl +++ b/test/emqx_keepalive_SUITE.erl @@ -26,7 +26,6 @@ groups() -> [{keepalive, [], [t_keepalive]}]. %%-------------------------------------------------------------------- t_keepalive(_) -> - {ok, _} = emqx_keepalive:start(fun() -> {ok, 1} end, 0, {keepalive, timeout}), {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), [resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). From 881e1a962121bd8d63d9548b71a9b27cb008ff63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 26 Oct 2018 19:43:05 +0800 Subject: [PATCH 385/520] Add case for ets:match_object --- src/emqx_broker.erl | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 0828d6be3..857090c25 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -260,11 +260,19 @@ subscription(Topic, Subscriber) -> -spec(subscribed(emqx_topic:topic(), pid() | emqx_types:subid() | emqx_types:subscriber()) -> boolean()). subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> - {Match, _} = ets:match_object(?SUBOPTION, {{Topic, {SubPid, '_'}}, '_'}, 1), - length(Match) >= 1; + case ets:match_object(?SUBOPTION, {{Topic, {SubPid, '_'}}, '_'}, 1) of + {Match, _} -> + length(Match) >= 1; + '$end_of_table' -> + false + end; subscribed(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> - {Match, _} = ets:match_object(?SUBOPTION, {{Topic, {'_', SubId}}, '_'}, 1), - length(Match) >= 1; + case ets:match_object(?SUBOPTION, {{Topic, {'_', SubId}}, '_'}, 1) of + {Match, _} -> + length(Match) >= 1; + '$end_of_table' -> + false + end; subscribed(Topic, {SubPid, SubId}) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> ets:member(?SUBOPTION, {Topic, {SubPid, SubId}}). From 7c14ba11d6cb0c4077e19c3220846200423b972d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 27 Oct 2018 13:59:17 +0800 Subject: [PATCH 386/520] Set some attributes when session resumed --- src/emqx_session.erl | 25 ++++++++++++++----------- src/emqx_sm.erl | 6 +++--- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index e123516e9..4fb92cdce 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -46,7 +46,7 @@ -export([start_link/1]). -export([info/1, attrs/1]). -export([stats/1]). --export([resume/3, discard/2]). +-export([resume/2, discard/2]). -export([update_expiry_interval/2, update_misc/2]). -export([subscribe/2, subscribe/4]). -export([publish/3]). @@ -311,9 +311,9 @@ unsubscribe(SPid, PacketId, Properties, TopicFilters) -> UnsubReq = {PacketId, Properties, TopicFilters}, gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). --spec(resume(spid(), pid(), emqx:message() | undefined) -> ok). -resume(SPid, ConnPid, WillMsg) -> - gen_server:cast(SPid, {resume, ConnPid, WillMsg}). +-spec(resume(spid(), map()) -> ok). +resume(SPid, SessAttrs) -> + gen_server:cast(SPid, {resume, SessAttrs}). %% @doc Discard the session -spec(discard(spid(), ByPid :: pid()) -> ok). @@ -517,13 +517,15 @@ handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight end; %% RESUME: -handle_cast({resume, ConnPid, WillMsg}, 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}) -> +handle_cast({resume, #{conn_pid := ConnPid, + will_msg := WillMsg, + expiry_interval := SessionExpiryInterval}}, 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], State), @@ -545,6 +547,7 @@ handle_cast({resume, ConnPid, WillMsg}, State = #state{client_id = Client awaiting_rel = #{}, await_rel_timer = undefined, expiry_timer = undefined, + expiry_interval = SessionExpiryInterval, will_delay_timer = undefined, will_msg = WillMsg}, diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 963cab1e4..9eb853ac4 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -93,11 +93,11 @@ discard_session(ClientId, ConnPid) when is_binary(ClientId) -> resume_session(ClientId) -> resume_session(ClientId, #{conn_pid => self(), will_msg => undefined}). -resume_session(ClientId, #{conn_pid := ConnPid, will_msg := WillMsg}) -> +resume_session(ClientId, SessAttrs = #{conn_pid := ConnPid}) -> case lookup_session(ClientId) of [] -> {error, not_found}; [{_ClientId, SPid}] -> - ok = emqx_session:resume(SPid, ConnPid, WillMsg), + ok = emqx_session:resume(SPid, SessAttrs), {ok, SPid}; Sessions -> [{_, SPid}|StaleSessions] = lists:reverse(Sessions), @@ -105,7 +105,7 @@ resume_session(ClientId, #{conn_pid := ConnPid, will_msg := WillMsg}) -> lists:foreach(fun({_, StalePid}) -> catch emqx_session:discard(StalePid, ConnPid) end, StaleSessions), - ok = emqx_session:resume(SPid, ConnPid, WillMsg), + ok = emqx_session:resume(SPid, SessAttrs), {ok, SPid} end. From 3cea06f88f1147d62cac28072224d8fc1479ca25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Sat, 27 Oct 2018 14:25:10 +0800 Subject: [PATCH 387/520] Remove resume_session/1, --- src/emqx_session.erl | 41 +++++++++++++++++++---------------------- src/emqx_sm.erl | 8 ++------ 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 4fb92cdce..286066a59 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -47,7 +47,7 @@ -export([info/1, attrs/1]). -export([stats/1]). -export([resume/2, discard/2]). --export([update_expiry_interval/2, update_misc/2]). +-export([update_expiry_interval/2]). -export([subscribe/2, subscribe/4]). -export([publish/3]). -export([puback/2, puback/3]). @@ -324,9 +324,6 @@ discard(SPid, ByPid) -> update_expiry_interval(SPid, Interval) -> gen_server:cast(SPid, {expiry_interval, Interval}). -update_misc(SPid, Misc) -> - gen_server:cast(SPid, {update_misc, Misc}). - -spec(close(spid()) -> ok). close(SPid) -> gen_server:call(SPid, close, infinity). @@ -517,9 +514,11 @@ handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight end; %% RESUME: -handle_cast({resume, #{conn_pid := ConnPid, - will_msg := WillMsg, - expiry_interval := SessionExpiryInterval}}, State = #state{client_id = ClientId, +handle_cast({resume, #{conn_pid := ConnPid, + will_msg := WillMsg, + expiry_interval := SessionExpiryInterval, + max_inflight := MaxInflight, + topic_alias_maximum := TopicAliasMaximum}}, State = #state{client_id = ClientId, conn_pid = OldConnPid, clean_start = CleanStart, retry_timer = RetryTimer, @@ -539,17 +538,19 @@ handle_cast({resume, #{conn_pid := ConnPid, true = link(ConnPid), - State1 = State#state{conn_pid = ConnPid, - binding = binding(ConnPid), - old_conn_pid = OldConnPid, - clean_start = false, - retry_timer = undefined, - awaiting_rel = #{}, - await_rel_timer = undefined, - expiry_timer = undefined, - expiry_interval = SessionExpiryInterval, - will_delay_timer = undefined, - will_msg = WillMsg}, + State1 = State#state{conn_pid = ConnPid, + binding = binding(ConnPid), + old_conn_pid = OldConnPid, + clean_start = false, + retry_timer = undefined, + awaiting_rel = #{}, + await_rel_timer = undefined, + expiry_timer = undefined, + expiry_interval = SessionExpiryInterval, + inflight = emqx_inflight:update_size(MaxInflight, State#state.inflight), + topic_alias_maximum = TopicAliasMaximum, + will_delay_timer = undefined, + will_msg = WillMsg}, %% Clean Session: true -> false??? CleanStart andalso emqx_sm:set_session_attrs(ClientId, attrs(State1)), @@ -562,10 +563,6 @@ handle_cast({resume, #{conn_pid := ConnPid, handle_cast({expiry_interval, Interval}, State) -> {noreply, State#state{expiry_interval = Interval}}; -handle_cast({update_misc, #{max_inflight := MaxInflight, topic_alias_maximum := TopicAliasMaximum}}, State) -> - {noreply, State#state{inflight = emqx_inflight:update_size(MaxInflight, State#state.inflight), - topic_alias_maximum = TopicAliasMaximum}}; - handle_cast(Msg, State) -> emqx_logger:error("[Session] unexpected cast: ~p", [Msg]), {noreply, State}. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 9eb853ac4..bc3f6ff68 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -22,7 +22,7 @@ -export([open_session/1, close_session/1]). -export([lookup_session/1, lookup_session_pid/1]). --export([resume_session/1, resume_session/2]). +-export([resume_session/2]). -export([discard_session/1, discard_session/2]). -export([register_session/2, unregister_session/1]). -export([get_session_attrs/1, set_session_attrs/2]). @@ -66,7 +66,6 @@ open_session(SessAttrs = #{clean_start := false, ResumeStart = fun(_) -> case resume_session(ClientId, SessAttrs) of {ok, SPid} -> - emqx_session:update_misc(SPid, #{max_inflight => MaxInflight, topic_alias_maximum => TopicAliasMaximum}), {ok, SPid, true}; {error, not_found} -> emqx_session_sup:start_session(SessAttrs) @@ -89,10 +88,7 @@ discard_session(ClientId, ConnPid) when is_binary(ClientId) -> end, lookup_session(ClientId)). %% @doc Try to resume a session. --spec(resume_session(emqx_types:client_id()) -> {ok, pid()} | {error, term()}). -resume_session(ClientId) -> - resume_session(ClientId, #{conn_pid => self(), will_msg => undefined}). - +-spec(resume_session(emqx_types:client_id(), map()) -> {ok, pid()} | {error, term()}). resume_session(ClientId, SessAttrs = #{conn_pid := ConnPid}) -> case lookup_session(ClientId) of [] -> {error, not_found}; From ae743ad1f04f4506969a0e74138bfed518a2e38d Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sun, 21 Oct 2018 17:20:09 +0200 Subject: [PATCH 388/520] Rewrite emqx_mqueue.erl Fixed bugs: - Priority queue lack of a `len + 1` logic in `in/2` Changed behaviors: - Topics not found in priority table (from config) will be treated with default priority, instead of hasing topic name to a priority number. - Default priority is now configurable (it was always lower than all configured priorities) - The dropped message due to reaching `max_len` is now returned from `in/2`, so the queue owner (`in/2` caller) can perform autopsy on it --- etc/emqx.conf | 34 +++---- include/emqx.hrl | 6 ++ priv/emqx.schema | 44 +++++---- src/emqx_local_bridge.erl | 6 +- src/emqx_mqueue.erl | 191 ++++++++++++++++++------------------- src/emqx_session.erl | 11 ++- test/emqx_mqueue_SUITE.erl | 87 +++++++++-------- 7 files changed, 195 insertions(+), 184 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 56fcf5ffc..0b3604102 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -506,17 +506,6 @@ mqtt.wildcard_subscription = true ## Value: boolean mqtt.shared_subscription = true -## Message queue type. -## -## Value: simple | priority -mqtt.mqueue_type = simple - -## Topic priorities. Default is 0. -## -## Priority: Number [0-255] -## -## mqtt.mqueue_priorities = topic/1=10,topic/2=8 - ##-------------------------------------------------------------------- ## Zones ##-------------------------------------------------------------------- @@ -649,22 +638,29 @@ zone.external.await_rel_timeout = 300s ## Default: 2h, 2 hours zone.external.session_expiry_interval = 2h -## Message queue type. -## -## Value: simple | priority -zone.external.mqueue_type = simple - ## Maximum queue length. Enqueued messages when persistent client disconnected, ## or inflight window is full. 0 means no limit. ## ## Value: Number >= 0 zone.external.max_mqueue_len = 1000 -## Topic priorities. Default is 0. +## Topic priorities. +## 'none' to indicate no priority table (by default), hence all messages +## are treated equal ## -## Priority: Number [0-255] +## Priority number [1-255] +## Example: topic/1=10,topic/2=8 +## NOTE: comma and equal signs are not allowed for priority topic names +## NOTE: messages for topics not in the priority table are treated as +## either highest or lowest priority depending on the configured +## value for mqueue_default_priority ## -## zone.external.mqueue_priorities = topic/1=10,topic/2=8 +zone.external.mqueue_priorities = none + +## Default to highest priority for topics not matching priority table +## +## Value: highest | lowest +zone.external.mqueue_default_priority = highest ## Whether to enqueue Qos0 messages. ## diff --git a/include/emqx.hrl b/include/emqx.hrl index 984f4c9e2..84feb16b7 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -27,6 +27,12 @@ -define(ERTS_MINIMUM_REQUIRED, "10.0"). +%%-------------------------------------------------------------------- +%% Configs +%%-------------------------------------------------------------------- + +-define(NO_PRIORITY_TABLE, none). + %%-------------------------------------------------------------------- %% Topics' prefix: $SYS | $queue | $share %%-------------------------------------------------------------------- diff --git a/priv/emqx.schema b/priv/emqx.schema index 1424ab240..d09937bb2 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -651,18 +651,6 @@ end}. {datatype, {enum, [true, false]}} ]}. -%% @doc Type: simple | priority -{mapping, "mqtt.mqueue_type", "emqx.mqueue_type", [ - {default, simple}, - {datatype, {enum, [simple, priority]}} -]}. - -%% @doc Topic Priorities: 0~255, Default is 0 -{mapping, "mqtt.mqueue_priorities", "emqx.mqueue_priorities", [ - {default, ""}, - {datatype, string} -]}. - %%-------------------------------------------------------------------- %% Zones %%-------------------------------------------------------------------- @@ -804,12 +792,6 @@ end}. {datatype, {duration, s}} ]}. -%% @doc Type: simple | priority -{mapping, "zone.$name.mqueue_type", "emqx.zones", [ - {default, simple}, - {datatype, {enum, [simple, priority]}} -]}. - %% @doc Max queue length. Enqueued messages when persistent client %% disconnected, or inflight window is full. 0 means no limit. {mapping, "zone.$name.max_mqueue_len", "emqx.zones", [ @@ -817,11 +799,23 @@ end}. {datatype, integer} ]}. -%% @doc Topic Priorities: 0~255, Default is 0 +%% @doc Topic Priorities, comma separated topic=priority pairs, +%% where priority should be integer in range 1-255 (inclusive) +%% 1 being the lowest and 255 being the highest. +%% default value `none` to indicate no priority table, hence all +%% messages are treated equal, which means either highest ('infinity'), +%% or lowest (0) depending on mqueue_default_priority config. {mapping, "zone.$name.mqueue_priorities", "emqx.zones", [ + {default, "none"}, {datatype, string} ]}. +%% @doc Default priority for topics not in priority table. +{mapping, "zone.$name.mqueue_default_priority", "emqx.zones", [ + {default, lowest}, + {datatype, {enum, [highest, lowest]}} +]}. + %% @doc Queue Qos0 messages? {mapping, "zone.$name.mqueue_store_qos0", "emqx.zones", [ {default, true}, @@ -886,6 +880,18 @@ end}. max_heap_size => Siz1} end, {force_shutdown_policy, ShutdownPolicy}; + ("mqueue_priorities", Val) -> + case Val of + "none" -> none; % NO_PRIORITY_TABLE + _ -> + lists:foldl(fun(T, Acc) -> + %% NOTE: space in "= " is intended + [{Topic, Prio}] = string:tokens(T, "= "), + P = list_to_integer(Prio), + (P < 0 orelse P > 255) andalso error({bad_priority, Topic, Prio}), + maps:put(iolist_to_binary(Topic), P, Acc) + end, string:tokens(Val, ",")) + end; (Opt, Val) -> {list_to_atom(Opt), Val} end, diff --git a/src/emqx_local_bridge.erl b/src/emqx_local_bridge.erl index 228a64cff..7c4e7cea1 100644 --- a/src/emqx_local_bridge.erl +++ b/src/emqx_local_bridge.erl @@ -63,8 +63,7 @@ init([Pool, Id, Node, Topic, Options]) -> Group = iolist_to_binary(["$bridge:", atom_to_list(Node), ":", Topic]), emqx_broker:subscribe(Topic, self(), #{share => Group, qos => ?QOS_0}), State = parse_opts(Options, #state{node = Node, subtopic = Topic}), - MQueue = emqx_mqueue:init(#{type => simple, - max_len => State#state.max_queue_len, + MQueue = emqx_mqueue:init(#{max_len => State#state.max_queue_len, store_qos0 => true}), {ok, State#state{pool = Pool, id = Id, mqueue = MQueue}}; false -> @@ -96,7 +95,8 @@ handle_cast(Msg, State) -> handle_info({dispatch, _Topic, Msg}, State = #state{mqueue = Q, status = down}) -> %% TODO: how to drop??? - {noreply, State#state{mqueue = emqx_mqueue:in(Msg, Q)}}; + {_Dropped, NewQ} = emqx_mqueue:in(Msg, Q), + {noreply, State#state{mqueue = NewQ}}; handle_info({dispatch, _Topic, Msg}, State = #state{node = Node, status = up}) -> emqx_rpc:cast(Node, emqx_broker, publish, [transform(Msg, State)]), diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index d9270dd5f..90fe59ba8 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -13,8 +13,8 @@ %% @doc A Simple in-memory message queue. %% -%% Notice that MQTT is not an enterprise messaging queue. MQTT assume that client -%% should be online in most of the time. +%% Notice that MQTT is not a (on-disk) persistent messaging queue. +%% It assumes that clients should be online in most of the time. %% %% This module implements a simple in-memory queue for MQTT persistent session. %% @@ -37,7 +37,8 @@ %% 3. QoS=0 messages are only enqueued when `store_qos0' is given `true` %% in init options %% -%% 4. If the queue is full drop the oldest one unless `max_len' is set to `0'. +%% 4. If the queue is full, drop the oldest one +%% unless `max_len' is set to `0' which implies (`infinity'). %% %% @end @@ -46,132 +47,122 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([init/1, type/1]). +-export([init/1]). -export([is_empty/1]). -export([len/1, max_len/1]). -export([in/2, out/1]). -export([stats/1, dropped/1]). --define(PQUEUE, emqx_pqueue). +-export_type([mqueue/0, options/0]). --type(priority() :: {iolist(), pos_integer()}). - --type(options() :: #{type := simple | priority, - max_len := non_neg_integer(), - priorities => list(priority()), - store_qos0 => boolean()}). +-type(topic() :: emqx_topic:topic()). +-type(priority() :: infinity | integer()). +-type(pq() :: emqx_pqueue:q()). +-type(count() :: non_neg_integer()). +-type(p_table() :: ?NO_PRIORITY_TABLE | #{topic() := priority()}). +-type(options() :: #{max_len := count(), + priorities => p_table(), + default_priority => highest | lowest, + store_qos0 => boolean() + }). +-type(message() :: pemqx_types:message()). -type(stat() :: {len, non_neg_integer()} | {max_len, non_neg_integer()} | {dropped, non_neg_integer()}). +-define(PQUEUE, emqx_pqueue). +-define(LOWEST_PRIORITY, 0). +-define(HIGHEST_PRIORITY, infinity). +-define(MAX_LEN_INFINITY, 0). + -record(mqueue, { - type :: simple | priority, - q :: queue:queue() | ?PQUEUE:q(), - %% priority table - priorities = [], - pseq = 0, - len = 0, - max_len = 0, - qos0 = false, - dropped = 0 + store_qos0 = false :: boolean(), + max_len = ?MAX_LEN_INFINITY :: count(), + len = 0 :: count(), + dropped = 0 :: count(), + p_table = ?NO_PRIORITY_TABLE :: p_table(), + default_p = ?LOWEST_PRIORITY :: priority(), + q = ?PQUEUE:new() :: pq() }). --type(mqueue() :: #mqueue{}). - --export_type([mqueue/0, priority/0, options/0]). +-opaque(mqueue() :: #mqueue{}). -spec(init(options()) -> mqueue()). -init(Opts = #{type := Type, max_len := MaxLen, store_qos0 := QoS0}) -> - init_q(#mqueue{type = Type, len = 0, max_len = MaxLen, qos0 = QoS0}, Opts). +init(Opts = #{max_len := MaxLen0, store_qos0 := QoS0}) -> + MaxLen = case (is_integer(MaxLen0) andalso MaxLen0 > ?MAX_LEN_INFINITY) of + true -> MaxLen0; + false -> ?MAX_LEN_INFINITY + end, + #mqueue{max_len = MaxLen, + store_qos0 = QoS0, + p_table = get_opt(priorities, Opts, ?NO_PRIORITY_TABLE), + default_p = get_priority_opt(Opts) + }. -init_q(MQ = #mqueue{type = simple}, _Opts) -> - MQ#mqueue{q = queue:new()}; -init_q(MQ = #mqueue{type = priority}, #{priorities := Priorities}) -> - init_pq(Priorities, MQ#mqueue{q = ?PQUEUE:new()}). +is_empty(#mqueue{len = Len}) -> Len =:= 0. -init_pq([], MQ) -> - MQ; -init_pq([{Topic, P} | L], MQ) -> - {_, MQ1} = insert_p(iolist_to_binary(Topic), P, MQ), - init_pq(L, MQ1). - -insert_p(Topic, P, MQ = #mqueue{priorities = L, pseq = Seq}) -> - <> = <>, - {PInt, MQ#mqueue{priorities = [{Topic, PInt} | L], pseq = Seq + 1}}. - --spec(type(mqueue()) -> simple | priority). -type(#mqueue{type = Type}) -> Type. - -is_empty(#mqueue{type = simple, len = Len}) -> Len =:= 0; -is_empty(#mqueue{type = priority, q = Q}) -> ?PQUEUE:is_empty(Q). - -len(#mqueue{type = simple, len = Len}) -> Len; -len(#mqueue{type = priority, q = Q}) -> ?PQUEUE:len(Q). +len(#mqueue{len = Len}) -> Len. max_len(#mqueue{max_len = MaxLen}) -> MaxLen. -%% @doc Dropped of the mqueue --spec(dropped(mqueue()) -> non_neg_integer()). +%% @doc Return number of dropped messages. +-spec(dropped(mqueue()) -> count()). dropped(#mqueue{dropped = Dropped}) -> Dropped. %% @doc Stats of the mqueue -spec(stats(mqueue()) -> [stat()]). -stats(#mqueue{type = Type, q = Q, max_len = MaxLen, len = Len, dropped = Dropped}) -> - [{len, case Type of - simple -> Len; - priority -> ?PQUEUE:len(Q) - end} | [{max_len, MaxLen}, {dropped, Dropped}]]. +stats(#mqueue{max_len = MaxLen, dropped = Dropped} = MQ) -> + [{len, len(MQ)}, {max_len, MaxLen}, {dropped, Dropped}]. %% @doc Enqueue a message. --spec(in(emqx_types:message(), mqueue()) -> mqueue()). -in(#message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> - MQ; -in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> - MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}; -in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = MaxLen, dropped = Dropped}) - when Len >= MaxLen -> - {{value, _Old}, Q2} = queue:out(Q), - MQ#mqueue{q = queue:in(Msg, Q2), dropped = Dropped +1}; -in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len}) -> - MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}; - -in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, - priorities = Priorities, - max_len = 0}) -> - case lists:keysearch(Topic, 1, Priorities) of - {value, {_, Pri}} -> - MQ#mqueue{q = ?PQUEUE:in(Msg, Pri, Q)}; +-spec(in(message(), mqueue()) -> {undefined | message(), mqueue()}). +in(#message{qos = ?QOS_0}, MQ = #mqueue{store_qos0 = false}) -> + {_Dropped = undefined, MQ}; +in(Msg = #message{topic = Topic}, MQ = #mqueue{default_p = Dp, + p_table = PTab, + q = Q, + len = Len, + max_len = MaxLen, + dropped = Dropped + } = MQ) -> + Priority = get_priority(Topic, PTab, Dp), + PLen = ?PQUEUE:plen(Priority, Q), + case MaxLen =/= ?MAX_LEN_INFINITY andalso PLen =:= MaxLen of + true -> + %% reached max length, drop the oldest message + {{value, DroppedMsg}, Q1} = ?PQUEUE:out(Priority, Q), + Q2 = ?PQUEUE:in(Msg, Priority, Q1), + {DroppedMsg, MQ#mqueue{q = Q2, dropped = Dropped + 1}}; false -> - {Pri, MQ1} = insert_p(Topic, 0, MQ), - MQ1#mqueue{q = ?PQUEUE:in(Msg, Pri, Q)} - end; -in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, - priorities = Priorities, - max_len = MaxLen}) -> - case lists:keysearch(Topic, 1, Priorities) of - {value, {_, Pri}} -> - case ?PQUEUE:plen(Pri, Q) >= MaxLen of - true -> - {_, Q1} = ?PQUEUE:out(Pri, Q), - MQ#mqueue{q = ?PQUEUE:in(Msg, Pri, Q1)}; - false -> - MQ#mqueue{q = ?PQUEUE:in(Msg, Pri, Q)} - end; - false -> - {Pri, MQ1} = insert_p(Topic, 0, MQ), - MQ1#mqueue{q = ?PQUEUE:in(Msg, Pri, Q)} + {_DroppedMsg = undefined, MQ#mqueue{len = Len + 1, q = ?PQUEUE:in(Msg, Priority, Q)}} end. -out(MQ = #mqueue{type = simple, len = 0}) -> +-spec(out(mqueue()) -> {empty | {value, message()}, mqueue()}). +out(MQ = #mqueue{len = 0, q = Q}) -> + 0 = ?PQUEUE:len(Q), %% assert, in this case, ?PQUEUE:len should be very cheap {empty, MQ}; -out(MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> - {R, Q2} = queue:out(Q), - {R, MQ#mqueue{q = Q2, len = Len - 1}}; -out(MQ = #mqueue{type = simple, q = Q, len = Len}) -> - {R, Q2} = queue:out(Q), - {R, MQ#mqueue{q = Q2, len = Len - 1}}; -out(MQ = #mqueue{type = priority, q = Q}) -> - {R, Q2} = ?PQUEUE:out(Q), - {R, MQ#mqueue{q = Q2}}. +out(MQ = #mqueue{q = Q, len = Len}) -> + {R, Q1} = ?PQUEUE:out(Q), + {R, MQ#mqueue{q = Q1, len = Len - 1}}. + +get_opt(Key, Opts, Default) -> + case maps:get(Key, Opts, Default) of + undefined -> Default; + X -> X + end. + +get_priority_opt(Opts) -> + case get_opt(default_priority, Opts, ?LOWEST_PRIORITY) of + lowest -> ?LOWEST_PRIORITY; + highest -> ?HIGHEST_PRIORITY; + N when is_integer(N) -> N + end. + +%% MICRO-OPTIMIZATION: When there is no priority table defined (from config), +%% disregard default priority from config, always use lowest (?LOWEST_PRIORITY=0) +%% because the lowest priority in emqx_pqueue is a fallback to queue:queue() +%% while the highest 'infinity' is a [{infinity, queue:queue()}] +get_priority(_Topic, ?NO_PRIORITY_TABLE, _) -> ?LOWEST_PRIORITY; +get_priority(Topic, PTab, Dp) -> maps:get(Topic, PTab, Dp). diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 286066a59..85b753f3d 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -377,10 +377,10 @@ init([Parent, #{zone := Zone, gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State). init_mqueue(Zone) -> - emqx_mqueue:init(#{type => get_env(Zone, mqueue_type, simple), - max_len => get_env(Zone, max_mqueue_len, 1000), - priorities => get_env(Zone, mqueue_priorities, ""), - store_qos0 => get_env(Zone, mqueue_store_qos0, true) + 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) }). binding(ConnPid) -> @@ -817,7 +817,8 @@ dispatch(Msg = #message{qos = QoS} = Msg, end. enqueue_msg(Msg, State = #state{mqueue = Q}) -> - inc_stats(enqueue, Msg, State#state{mqueue = emqx_mqueue:in(Msg, Q)}). + {_Dropped, NewQ} = emqx_mqueue:in(Msg, Q), + inc_stats(enqueue, Msg, State#state{mqueue = NewQ}). %%------------------------------------------------------------------------------ %% Deliver diff --git a/test/emqx_mqueue_SUITE.erl b/test/emqx_mqueue_SUITE.erl index 8a1ca5201..9bb424fa1 100644 --- a/test/emqx_mqueue_SUITE.erl +++ b/test/emqx_mqueue_SUITE.erl @@ -28,57 +28,58 @@ all() -> [t_in, t_in_qos0, t_out, t_simple_mqueue, t_infinity_simple_mqueue, t_priority_mqueue, t_infinity_priority_mqueue]. t_in(_) -> - Opts = #{type => simple, max_len => 5, store_qos0 => true}, + Opts = #{max_len => 5, store_qos0 => true}, Q = ?Q:init(Opts), ?assert(?Q:is_empty(Q)), - Q1 = ?Q:in(#message{}, Q), + {_, Q1} = ?Q:in(#message{}, Q), ?assertEqual(1, ?Q:len(Q1)), - Q2 = ?Q:in(#message{qos = 1}, Q1), + {_, Q2} = ?Q:in(#message{qos = 1}, Q1), ?assertEqual(2, ?Q:len(Q2)), - Q3 = ?Q:in(#message{qos = 2}, Q2), - Q4 = ?Q:in(#message{}, Q3), - Q5 = ?Q:in(#message{}, Q4), + {_, Q3} = ?Q:in(#message{qos = 2}, Q2), + {_, Q4} = ?Q:in(#message{}, Q3), + {_, Q5} = ?Q:in(#message{}, Q4), ?assertEqual(5, ?Q:len(Q5)). t_in_qos0(_) -> - Opts = #{type => simple, max_len => 5, store_qos0 => false}, + Opts = #{max_len => 5, store_qos0 => false}, Q = ?Q:init(Opts), - Q1 = ?Q:in(#message{qos = 0}, Q), + {_, Q1} = ?Q:in(#message{qos = 0}, Q), ?assert(?Q:is_empty(Q1)), - Q2 = ?Q:in(#message{qos = 0}, Q1), + {_, Q2} = ?Q:in(#message{qos = 0}, Q1), ?assert(?Q:is_empty(Q2)). t_out(_) -> - Opts = #{type => simple, max_len => 5, store_qos0 => true}, + Opts = #{max_len => 5, store_qos0 => true}, Q = ?Q:init(Opts), {empty, Q} = ?Q:out(Q), - Q1 = ?Q:in(#message{}, Q), + {_, Q1} = ?Q:in(#message{}, Q), {Value, Q2} = ?Q:out(Q1), ?assertEqual(0, ?Q:len(Q2)), ?assertEqual({value, #message{}}, Value). t_simple_mqueue(_) -> - Opts = #{type => simple, max_len => 3, store_qos0 => false}, + Opts = #{max_len => 3, store_qos0 => false}, Q = ?Q:init(Opts), - ?assertEqual(simple, ?Q:type(Q)), ?assertEqual(3, ?Q:max_len(Q)), ?assert(?Q:is_empty(Q)), - Q1 = ?Q:in(#message{qos = 1, payload = <<"1">>}, Q), - Q2 = ?Q:in(#message{qos = 1, payload = <<"2">>}, Q1), - Q3 = ?Q:in(#message{qos = 1, payload = <<"3">>}, Q2), - Q4 = ?Q:in(#message{qos = 1, payload = <<"4">>}, Q3), + {_, Q1} = ?Q:in(#message{qos = 1, payload = <<"1">>}, Q), + {_, Q2} = ?Q:in(#message{qos = 1, payload = <<"2">>}, Q1), + {_, Q3} = ?Q:in(#message{qos = 1, payload = <<"3">>}, Q2), + {_, Q4} = ?Q:in(#message{qos = 1, payload = <<"4">>}, Q3), ?assertEqual(3, ?Q:len(Q4)), {{value, Msg}, Q5} = ?Q:out(Q4), ?assertEqual(<<"2">>, Msg#message.payload), ?assertEqual([{len, 2}, {max_len, 3}, {dropped, 1}], ?Q:stats(Q5)). t_infinity_simple_mqueue(_) -> - Opts = #{type => simple, max_len => 0, store_qos0 => false}, + Opts = #{max_len => 0, store_qos0 => false}, Q = ?Q:init(Opts), ?assert(?Q:is_empty(Q)), ?assertEqual(0, ?Q:max_len(Q)), - Qx = lists:foldl(fun(I, AccQ) -> - ?Q:in(#message{qos = 1, payload = iolist_to_binary([I])}, AccQ) + Qx = lists:foldl( + fun(I, AccQ) -> + {_, NewQ} = ?Q:in(#message{qos = 1, payload = iolist_to_binary([I])}, AccQ), + NewQ end, Q, lists:seq(1, 255)), ?assertEqual(255, ?Q:len(Qx)), ?assertEqual([{len, 255}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)), @@ -86,45 +87,55 @@ t_infinity_simple_mqueue(_) -> ?assertEqual(<<1>>, V#message.payload). t_priority_mqueue(_) -> - Opts = #{type => priority, max_len => 3, priorities => [{<<"t1">>, 1}, {<<"t2">>, 2}, {<<"t3">>, 3}], store_qos0 => false}, + Opts = #{max_len => 3, + priorities => + #{<<"t1">> => 1, + <<"t2">> => 2, + <<"t3">> => 3 + }, + store_qos0 => false}, Q = ?Q:init(Opts), - ?assertEqual(priority, ?Q:type(Q)), ?assertEqual(3, ?Q:max_len(Q)), ?assert(?Q:is_empty(Q)), - Q1 = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q), - Q2 = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q1), - Q3 = ?Q:in(#message{qos = 1, topic = <<"t3">>}, Q2), + {_, Q1} = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q), + {_, Q2} = ?Q:in(#message{qos = 1, topic = <<"t1">>}, Q1), + {_, Q3} = ?Q:in(#message{qos = 1, topic = <<"t3">>}, Q2), ?assertEqual(3, ?Q:len(Q3)), - Q4 = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q3), + {_, Q4} = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q3), ?assertEqual(4, ?Q:len(Q4)), - Q5 = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q4), + {_, Q5} = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q4), ?assertEqual(5, ?Q:len(Q5)), - Q6 = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q5), + {_, Q6} = ?Q:in(#message{qos = 1, topic = <<"t2">>}, Q5), ?assertEqual(5, ?Q:len(Q6)), {{value, Msg}, Q7} = ?Q:out(Q6), ?assertEqual(4, ?Q:len(Q7)), ?assertEqual(<<"t3">>, Msg#message.topic). t_infinity_priority_mqueue(_) -> - Opts = #{type => priority, max_len => 0, priorities => [{<<"t">>, 1}, {<<"t1">>, 2}], store_qos0 => false}, + Opts = #{max_len => 0, + priorities => + #{<<"t">> => 1, + <<"t1">> => 2 + }, + store_qos0 => false}, Q = ?Q:init(Opts), ?assertEqual(0, ?Q:max_len(Q)), Qx = lists:foldl(fun(I, AccQ) -> - AccQ1 = - ?Q:in(#message{topic = <<"t1">>, qos = 1, payload = iolist_to_binary([I])}, AccQ), - ?Q:in(#message{topic = <<"t">>, qos = 1, payload = iolist_to_binary([I])}, AccQ1) - end, Q, lists:seq(1, 255)), + {undefined, AccQ1} = ?Q:in(#message{topic = <<"t1">>, qos = 1, payload = iolist_to_binary([I])}, AccQ), + {undefined, AccQ2} = ?Q:in(#message{topic = <<"t">>, qos = 1, payload = iolist_to_binary([I])}, AccQ1), + AccQ2 + end, Q, lists:seq(1, 255)), ?assertEqual(510, ?Q:len(Qx)), ?assertEqual([{len, 510}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)). t_priority_mqueue2(_) -> - Opts = #{type => priority, max_length => 2, store_qos0 => false}, + Opts = #{max_length => 2, store_qos0 => false}, Q = ?Q:init("priority_queue2_test", 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), + {_, 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), {{value, _Val}, Q5} = ?Q:out(Q4), 3 = ?Q:len(Q5). From 28c8f2dd5cb5be6a6fecaab61e3a3771e02764e8 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sat, 27 Oct 2018 14:07:47 +0200 Subject: [PATCH 389/520] Remove neotoma plugin, build cuttlefish script in sub-dir --- Makefile | 15 +++++++-------- rebar.config | 3 +-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 23a7da11c..b4b68259f 100644 --- a/Makefile +++ b/Makefile @@ -74,8 +74,10 @@ etc/gen.emqx.conf: bbmustache etc/emqx.conf ok = file:write_file('etc/gen.emqx.conf', Targ), \ halt(0)." -app.config: cuttlefish etc/gen.emqx.conf - $(verbose) ./cuttlefish -l info -e etc/ -c etc/gen.emqx.conf -i priv/emqx.schema -d data/ +CUTTLEFISH_SCRIPT = _build/default/lib/cuttlefish/cuttlefish + +app.config: $(CUTTLEFISH_SCRIPT) etc/gen.emqx.conf + $(verbose) $(CUTTLEFISH_SCRIPT) -l info -e etc/ -c etc/gen.emqx.conf -i priv/emqx.schema -d data/ ct: app.config @@ -86,11 +88,8 @@ coveralls: @rebar3 coveralls send -cuttlefish: rebar-deps - @if [ ! -f cuttlefish ]; then \ - make -C _build/default/lib/cuttlefish; \ - mv _build/default/lib/cuttlefish/cuttlefish ./cuttlefish; \ - fi +$(CUTTLEFISH_SCRIPT): rebar-deps + @if [ ! -f cuttlefish ]; then make -C _build/default/lib/cuttlefish; fi rebar-xref: @rebar3 xref @@ -98,7 +97,7 @@ rebar-xref: rebar-deps: @rebar3 get-deps -rebar-eunit: cuttlefish +rebar-eunit: $(CUTTLEFISH_SCRIPT) @rebar3 eunit rebar-compile: diff --git a/rebar.config b/rebar.config index 63b3a7595..c64e2cfcb 100644 --- a/rebar.config +++ b/rebar.config @@ -29,6 +29,5 @@ {cover_opts, [verbose]}. {cover_export_enabled, true}. -%% rebar3_neotoma_plugin is needed to compile the .peg file for cuttlefish -{plugins, [coveralls, rebar3_neotoma_plugin]}. +{plugins, [coveralls]}. From 8f5b7a0d05277a7fe996ab38b8b26b2a803ebb8b Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Wed, 17 Oct 2018 01:08:12 +0800 Subject: [PATCH 390/520] Optimize config for log section --- etc/emqx.conf | 100 ++++++++++++++++++++------------------------------ 1 file changed, 39 insertions(+), 61 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 56fcf5ffc..e63e4a55c 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -328,83 +328,61 @@ rpc.socket_keepalive_count = 9 ## Log ##-------------------------------------------------------------------- -## Sets the log dir. +## Where to emit the logs. ## -## Value: Folder -log.dir = {{ platform_log_dir }} +## Value: off | file | stdio | both +## - off: disable logs entirely +## - file: write logs to file +## - stdio: write logs to standard I/O +## - both: write logs both to file and standard I/O +log.to = stdio -## Where to emit the console logs. -## -## Value: off | file | console | both -## - off: disabled -## - file: write to file -## - console: write to stdout -## - both: file and stdout -log.console = console - -## Sets the severity level of console log. +## Sets the severity level for logs that written to stdio. ## ## Value: debug | info | notice | warning | error | critical | alert | emergency ## ## Default: error -log.console.level = error +log.stdio.level = error -## The file where console logs will be writed to, when 'log.console' is set as 'file'. +## Sets the severity level for logs that written to file. +## +## Value: debug | info | notice | warning | error | critical | alert | emergency +## +## Default: error +log.file.level = error + +## Sets the dir for log files. +## +## Value: Folder +log.file.dir = {{ platform_log_dir }} + +## The file to where specific levels of logs will be written. ## ## Value: File Name -## log.console.file = {{ platform_log_dir }}/console.log +## Format: log.file.$level.filename = $filename, +## where "$level" can be one of: +## debug, info, notice, warning, error, critical, alert, emergency +log.file.error.filename = error.log -## Maximum file size for console log. -## -## Value: Number(bytes) -## log.console.size = 10485760 - -## The rotation count for console log. +## Maximum size of each file that contains specific levels of logs. ## ## Value: Number -## log.console.count = 5 +## Default: 10M +## Format: log.file.$level.max_size = $size +## where "$level" can be one of: +## debug, info, notice, warning, error, critical, alert, emergency +## Supported Unit: B | KB | MB | G +log.file.error.max_size = 10M -## The file where info logs will be writed to. -## -## Value: File Name -## log.info.file = {{ platform_log_dir }}/info.log - -## Maximum file size for info log. -## -## Value: Number(bytes) -## log.info.size = 10485760 - -## The rotation count for info log. +## The maximum rotation number of files that contains specific levels of logs. ## ## Value: Number -## log.info.count = 5 +## Format: log.file.$level.max_num = $number +## where "$level" can be one of: +## debug, info, notice, warning, error, critical, alert, emergency +log.file.error.max_num = 5 -## The file where error logs will be writed to. -## -## Value: File Name -log.error.file = {{ platform_log_dir }}/error.log - -## Maximum file size for error log. -## -## Value: Number(bytes) -log.error.size = 10485760 - -## The rotation count for error log. -## -## Value: Number -log.error.count = 5 - -## Enable the crash log. -## -## Value: on | off -log.crash = on - -## The file for crash log. -## -## Value: File Name -log.crash.file = {{ platform_log_dir }}/crash.log - -## Enable syslog. +## Enable syslog, this will write logs to the rsyslog ## ## Values: on | off log.syslog = on From 46c7e86331cc7d1d85f14f35987289f2786a1cdc Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Wed, 17 Oct 2018 15:45:50 +0800 Subject: [PATCH 391/520] Fix some grammar --- etc/emqx.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index e63e4a55c..341f05013 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -337,14 +337,14 @@ rpc.socket_keepalive_count = 9 ## - both: write logs both to file and standard I/O log.to = stdio -## Sets the severity level for logs that written to stdio. +## Sets the severity level for logs written to stdio. ## ## Value: debug | info | notice | warning | error | critical | alert | emergency ## ## Default: error log.stdio.level = error -## Sets the severity level for logs that written to file. +## Sets the severity level for logs written to file. ## ## Value: debug | info | notice | warning | error | critical | alert | emergency ## From 1222dd0eab698b14c1f855c6a762c44ee51a90c9 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Thu, 18 Oct 2018 14:09:24 +0800 Subject: [PATCH 392/520] Update log config template --- etc/emqx.conf | 57 ++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 341f05013..914aa657e 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -329,60 +329,57 @@ rpc.socket_keepalive_count = 9 ##-------------------------------------------------------------------- ## Where to emit the logs. +## Enable the console (standard output) logs. ## -## Value: off | file | stdio | both +## Value: off | file | console | both ## - off: disable logs entirely ## - file: write logs to file -## - stdio: write logs to standard I/O +## - console: write logs to standard I/O ## - both: write logs both to file and standard I/O -log.to = stdio +log.to = console -## Sets the severity level for logs written to stdio. +## Sets the log severity level. ## ## Value: debug | info | notice | warning | error | critical | alert | emergency ## ## Default: error -log.stdio.level = error - -## Sets the severity level for logs written to file. -## -## Value: debug | info | notice | warning | error | critical | alert | emergency -## -## Default: error -log.file.level = error +log.level = error ## Sets the dir for log files. ## ## Value: Folder -log.file.dir = {{ platform_log_dir }} +log.dir = {{ platform_log_dir }} -## The file to where specific levels of logs will be written. +## The log filename for logs of level specified in "log.level". ## -## Value: File Name -## Format: log.file.$level.filename = $filename, -## where "$level" can be one of: -## debug, info, notice, warning, error, critical, alert, emergency -log.file.error.filename = error.log +## Value: String +## Default: emqx.log +log.file = emqx.log -## Maximum size of each file that contains specific levels of logs. +## Maximum size of each log file. ## ## Value: Number ## Default: 10M -## Format: log.file.$level.max_size = $size -## where "$level" can be one of: -## debug, info, notice, warning, error, critical, alert, emergency ## Supported Unit: B | KB | MB | G -log.file.error.max_size = 10M +log.rotation.size = 10M -## The maximum rotation number of files that contains specific levels of logs. +## Maximum rotation count of log files. ## ## Value: Number -## Format: log.file.$level.max_num = $number -## where "$level" can be one of: -## debug, info, notice, warning, error, critical, alert, emergency -log.file.error.max_num = 5 +## Default: 5 +log.rotation.count = 5 -## Enable syslog, this will write logs to the rsyslog +## To create additional log files for specific levels of logs. +## +## Value: File Name +## Format: log.$level.file = $filename, +## where "$level" can be one of: debug, info, notice, warning, +## error, critical, alert, emergency +## Note: Log files for a specific log level will contain all the logs +## that greater than or equal to that level +log.error.file = error.log + +## Enable syslog, to write logs to the rsyslog ## ## Values: on | off log.syslog = on From 2c63aef3f61b75084019908fd683122486933517 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Thu, 18 Oct 2018 14:39:25 +0800 Subject: [PATCH 393/520] Update schema --- Makefile | 3 +- etc/emqx.conf | 4 +- priv/emqx.schema | 116 ++++++++++++++++++++--------------------------- 3 files changed, 51 insertions(+), 72 deletions(-) diff --git a/Makefile b/Makefile index 23a7da11c..9e93b3b23 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker PROJECT_VERSION = 3.0 -DEPS = jsx gproc gen_rpc lager ekka esockd cowboy clique lager_syslog +DEPS = jsx gproc gen_rpc lager ekka esockd cowboy clique dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 @@ -14,7 +14,6 @@ dep_esockd = git https://github.com/emqx/esockd v5.4.2 dep_ekka = git https://github.com/emqx/ekka v0.4.1 dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 dep_clique = git https://github.com/emqx/clique develop -dep_lager_syslog = git https://github.com/basho/lager_syslog 3.0.1 NO_AUTOPATCH = cuttlefish diff --git a/etc/emqx.conf b/etc/emqx.conf index 914aa657e..c783c10b3 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -361,13 +361,13 @@ log.file = emqx.log ## Value: Number ## Default: 10M ## Supported Unit: B | KB | MB | G -log.rotation.size = 10M +#log.rotation.size = 10M ## Maximum rotation count of log files. ## ## Value: Number ## Default: 5 -log.rotation.count = 5 +#log.rotation.count = 5 ## To create additional log files for specific levels of logs. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index 1424ab240..eac5c997d 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -382,100 +382,87 @@ end}. %% Log %%-------------------------------------------------------------------- -{mapping, "log.dir", "lager.log_dir", [ +{mapping, "log.to", "logger.to", [ + {default, console}, + {datatype, {enum, [off, file, console, both]}} +]}. + +{mapping, "log.level", "kernel.logger_level", [ + {default, error}, + {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency, all]}} +]}. + +{mapping, "log.dir", "logger.dir", [ {default, "log"}, {datatype, string} ]}. -{mapping, "log.console", "lager.handlers", [ - {default, file}, - {datatype, {enum, [off, file, console, both]}} -]}. - -{mapping, "log.console.level", "lager.handlers", [ - {default, info}, - {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency, none]}} -]}. - -{mapping, "log.console.file", "lager.handlers", [ - {default, "log/console.log"}, +{mapping, "log.file", "logger.filename", [ + {default, "emqx.log"}, {datatype, file} ]}. -{mapping, "log.console.size", "lager.handlers", [ - {default, 10485760}, - {datatype, integer} +{mapping, "log.rotation.size", "logger.size", [ + {default, "10M"}, + {datatype, bytesize} ]}. -{mapping, "log.console.count", "lager.handlers", [ +{mapping, "log.rotation.count", "logger.count", [ {default, 5}, {datatype, integer} ]}. -{mapping, "log.info.file", "lager.handlers", [ +{mapping, "log.$level.file", "logger.additional_handlers", [ {datatype, file} ]}. -{mapping, "log.info.size", "lager.handlers", [ - {default, 10485760}, - {datatype, integer} -]}. - -{mapping, "log.info.count", "lager.handlers", [ - {default, 5}, - {datatype, integer} -]}. - -{mapping, "log.error.file", "lager.handlers", [ - {default, "log/error.log"}, - {datatype, file} -]}. - -{mapping, "log.error.size", "lager.handlers", [ - {default, 10485760}, - {datatype, integer} -]}. - -{mapping, "log.error.count", "lager.handlers", [ - {default, 5}, - {datatype, integer} -]}. - -{mapping, "log.syslog", "lager.handlers", [ +{mapping, "log.syslog", "logger.syslog", [ {default, off}, {datatype, flag} ]}. -{mapping, "log.syslog.identity", "lager.handlers", [ +{mapping, "log.syslog.identity", "logger.syslog", [ {default, "emqx"}, {datatype, string} ]}. -{mapping, "log.syslog.facility", "lager.handlers", [ +{mapping, "log.syslog.facility", "logger.syslog", [ {default, local0}, {datatype, {enum, [daemon, local0, local1, local2, local3, local4, local5, local6, local7]}} ]}. -{mapping, "log.syslog.level", "lager.handlers", [ +{mapping, "log.syslog.level", "logger.syslog", [ {default, error}, {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency]}} ]}. -{mapping, "log.error.redirect", "lager.error_logger_redirect", [ - {default, on}, - {datatype, flag}, - hidden -]}. - -{mapping, "log.error.messages_per_second", "lager.error_logger_hwm", [ - {default, 1000}, - {datatype, integer}, - hidden -]}. - {translation, - "lager.handlers", + "logger.additional_handlers", fun(Conf) -> + Format = [time," [",level,"] ",msg,"\n"], + AdditionalHandlers = lists:filter( + fun({K, _V}) -> + cuttlefish_variable:is_fuzzy_match(K, string:tokens("log.$level.file", ".")) + end, + Conf), + Users = [{handler, file, logger_disk_log_h, + #{level => Level, + config => #{file => Dir ++ Filename, + type => wrap, + max_no_files => MaxNoFiles, + max_no_bytes => MaxNoBytes}, + formatter => + {logger_formatter, #{legacy_header => false, + single_line => true, + template => Format}}, + filesync_repeat_interval => 1000}} + || {[_, Level, _], Filename} <- UserList], + case Users of + [] -> + throw(unset); + _ -> Users + end + ErrorHandler = case cuttlefish:conf_get("log.error.file", Conf, undefined) of undefined -> []; ErrorFilename -> [{lager_file_backend, [{file, ErrorFilename}, @@ -511,14 +498,7 @@ end}. both -> [ConsoleHandler, ConsoleFileHandler]; _ -> [] end, - SyslogHandler = case cuttlefish:conf_get("log.syslog", Conf) of - false -> []; - true -> [{lager_syslog_backend, - [cuttlefish:conf_get("log.syslog.identity", Conf), - cuttlefish:conf_get("log.syslog.facility", Conf), - cuttlefish:conf_get("log.syslog.level", Conf)]}] - end, - ConsoleHandlers ++ ErrorHandler ++ InfoHandler ++ SyslogHandler + ConsoleHandlers ++ ErrorHandler ++ InfoHandler end }. From 94dbdffd595e971454fae4b7c47b0077273fbde0 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 2 Nov 2018 01:32:09 +0800 Subject: [PATCH 394/520] New logger formatter with meta-data --- Makefile | 13 +- etc/emqx.conf | 24 +- priv/emqx.schema | 184 ++++++--------- rebar.config | 5 +- src/emqx.app.src | 4 +- src/emqx_app.erl | 4 +- src/emqx_bridge.erl | 4 +- src/emqx_broker.erl | 2 +- src/emqx_ctl.erl | 4 +- src/emqx_listeners.erl | 8 +- src/emqx_logger.erl | 49 ++-- src/emqx_logger_formatter.erl | 360 ++++++++++++++++++++++++++++++ src/emqx_modules.erl | 2 +- src/emqx_protocol.erl | 8 +- src/emqx_session.erl | 6 +- src/emqx_sys_mon.erl | 6 +- src/emqx_tracer.erl | 6 +- src/emqx_ws_connection.erl | 5 +- test/emqx_SUITE_data/slave.config | 15 -- test/ws_client.erl | 4 +- 20 files changed, 505 insertions(+), 208 deletions(-) create mode 100644 src/emqx_logger_formatter.erl diff --git a/Makefile b/Makefile index 9e93b3b23..857d2ce8b 100644 --- a/Makefile +++ b/Makefile @@ -4,30 +4,27 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker PROJECT_VERSION = 3.0 -DEPS = jsx gproc gen_rpc lager ekka esockd cowboy clique +DEPS = jsx gproc gen_rpc ekka esockd cowboy clique dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 -dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.2.0 -dep_lager = git https://github.com/erlang-lager/lager 3.6.5 +dep_gen_rpc = git https://github.com/emqx/gen_rpc switch_to_logger dep_esockd = git https://github.com/emqx/esockd v5.4.2 -dep_ekka = git https://github.com/emqx/ekka v0.4.1 +dep_ekka = git https://github.com/emqx/ekka switch_to_logger dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 dep_clique = git https://github.com/emqx/clique develop NO_AUTOPATCH = cuttlefish ERLC_OPTS += +debug_info -DAPPLICATION=emqx -ERLC_OPTS += +'{parse_transform, lager_transform}' BUILD_DEPS = cuttlefish -dep_cuttlefish = git https://github.com/emqx/cuttlefish emqx30 +dep_cuttlefish = git https://github.com/emqx/cuttlefish switch_to_logger #TEST_DEPS = emqx_ct_helplers #dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers TEST_ERLC_OPTS += +debug_info -DAPPLICATION=emqx -TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose @@ -46,7 +43,7 @@ CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME) COVER = true -PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets public_key ssl lager compiler mnesia +PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets public_key ssl compiler mnesia DIALYZER_DIRS := ebin/ DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wunmatched_returns diff --git a/etc/emqx.conf b/etc/emqx.conf index c783c10b3..547075f13 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -338,14 +338,14 @@ rpc.socket_keepalive_count = 9 ## - both: write logs both to file and standard I/O log.to = console -## Sets the log severity level. +## The log severity level. ## ## Value: debug | info | notice | warning | error | critical | alert | emergency ## ## Default: error log.level = error -## Sets the dir for log files. +## The dir for log files. ## ## Value: Folder log.dir = {{ platform_log_dir }} @@ -360,16 +360,16 @@ log.file = emqx.log ## ## Value: Number ## Default: 10M -## Supported Unit: B | KB | MB | G -#log.rotation.size = 10M +## Supported Unit: KB | MB | G +log.rotation.size = 10MB ## Maximum rotation count of log files. ## ## Value: Number ## Default: 5 -#log.rotation.count = 5 +log.rotation.count = 5 -## To create additional log files for specific levels of logs. +## To create additional log files for specific log levels. ## ## Value: File Name ## Format: log.$level.file = $filename, @@ -377,17 +377,9 @@ log.file = emqx.log ## error, critical, alert, emergency ## Note: Log files for a specific log level will contain all the logs ## that greater than or equal to that level -log.error.file = error.log - -## Enable syslog, to write logs to the rsyslog ## -## Values: on | off -log.syslog = on - -## Sets the severity level for syslog. -## -## Value: debug | info | notice | warning | error | critical | alert | emergency -log.syslog.level = error +#log.info.file = info.log +#log.error.file = error.log ##-------------------------------------------------------------------- ## Authentication/Access Control diff --git a/priv/emqx.schema b/priv/emqx.schema index eac5c997d..76f75513b 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -382,7 +382,7 @@ end}. %% Log %%-------------------------------------------------------------------- -{mapping, "log.to", "logger.to", [ +{mapping, "log.to", "kernel.logger", [ {default, console}, {datatype, {enum, [off, file, console, both]}} ]}. @@ -392,142 +392,100 @@ end}. {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency, all]}} ]}. -{mapping, "log.dir", "logger.dir", [ +{mapping, "log.logger_sasl_compatible", "kernel.logger_sasl_compatible", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +{mapping, "log.dir", "kernel.logger", [ {default, "log"}, {datatype, string} ]}. -{mapping, "log.file", "logger.filename", [ +{mapping, "log.file", "kernel.logger", [ {default, "emqx.log"}, {datatype, file} ]}. -{mapping, "log.rotation.size", "logger.size", [ - {default, "10M"}, +{mapping, "log.rotation.size", "kernel.logger", [ + {default, "10MB"}, {datatype, bytesize} ]}. -{mapping, "log.rotation.count", "logger.count", [ +{mapping, "log.rotation.count", "kernel.logger", [ {default, 5}, {datatype, integer} ]}. -{mapping, "log.$level.file", "logger.additional_handlers", [ +{mapping, "log.$level.file", "kernel.logger", [ {datatype, file} ]}. -{mapping, "log.syslog", "logger.syslog", [ - {default, off}, - {datatype, flag} -]}. - -{mapping, "log.syslog.identity", "logger.syslog", [ - {default, "emqx"}, - {datatype, string} -]}. - -{mapping, "log.syslog.facility", "logger.syslog", [ - {default, local0}, - {datatype, {enum, [daemon, local0, local1, local2, local3, local4, local5, local6, local7]}} -]}. - -{mapping, "log.syslog.level", "logger.syslog", [ - {default, error}, - {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency]}} -]}. - -{translation, - "logger.additional_handlers", - fun(Conf) -> - Format = [time," [",level,"] ",msg,"\n"], - AdditionalHandlers = lists:filter( - fun({K, _V}) -> - cuttlefish_variable:is_fuzzy_match(K, string:tokens("log.$level.file", ".")) - end, - Conf), - Users = [{handler, file, logger_disk_log_h, - #{level => Level, - config => #{file => Dir ++ Filename, - type => wrap, - max_no_files => MaxNoFiles, - max_no_bytes => MaxNoBytes}, - formatter => - {logger_formatter, #{legacy_header => false, - single_line => true, - template => Format}}, - filesync_repeat_interval => 1000}} - || {[_, Level, _], Filename} <- UserList], - case Users of - [] -> - throw(unset); - _ -> Users - end - - ErrorHandler = case cuttlefish:conf_get("log.error.file", Conf, undefined) of - undefined -> []; - ErrorFilename -> [{lager_file_backend, [{file, ErrorFilename}, - {level, error}, - {size, cuttlefish:conf_get("log.error.size", Conf)}, - {date, "$D0"}, - {count, cuttlefish:conf_get("log.error.count", Conf)}]}] - end, - - InfoHandler = case cuttlefish:conf_get("log.info.file", Conf, undefined) of - undefined -> []; - InfoFilename -> [{lager_file_backend, [{file, InfoFilename}, - {level, info}, - {size, cuttlefish:conf_get("log.info.size", Conf)}, - {date, "$D0"}, - {count, cuttlefish:conf_get("log.info.count", Conf)}]}] - end, - - ConsoleLogLevel = cuttlefish:conf_get("log.console.level", Conf), - ConsoleLogFile = cuttlefish:conf_get("log.console.file", Conf), - - ConsoleHandler = {lager_console_backend, [{level, ConsoleLogLevel}]}, - ConsoleFileHandler = {lager_file_backend, [{file, ConsoleLogFile}, - {level, ConsoleLogLevel}, - {size, cuttlefish:conf_get("log.console.size", Conf)}, - {date, "$D0"}, - {count, cuttlefish:conf_get("log.console.count", Conf)}]}, - - ConsoleHandlers = case cuttlefish:conf_get("log.console", Conf) of - off -> []; - file -> [ConsoleFileHandler]; - console -> [ConsoleHandler]; - both -> [ConsoleHandler, ConsoleFileHandler]; - _ -> [] - end, - ConsoleHandlers ++ ErrorHandler ++ InfoHandler - end -}. - -{mapping, "log.crash", "lager.crash_log", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "log.crash.file", "lager.crash_log", [ - {default, "log/crash.log"}, - {datatype, file} -]}. - -{translation, - "lager.crash_log", - fun(Conf) -> - case cuttlefish:conf_get("log.crash", Conf) of - false -> undefined; - _ -> - cuttlefish:conf_get("log.crash.file", Conf, "./log/crash.log") - end - end}. - {mapping, "sasl", "sasl.sasl_error_logger", [ {default, off}, {datatype, flag}, hidden ]}. +{translation, "kernel.logger", fun(Conf) -> + LogTo = cuttlefish:conf_get("log.to", Conf), + Formatter = {emqx_logger_formatter, + #{template => + [time," [",level,"] ", + {client_id, + [{peername, + [client_id,"@",peername," "], + [client_id, " "]}], + []}, + msg,"\n"]}}, + FileConf = fun(Filename) -> + #{type => wrap, + file => filename:join(cuttlefish:conf_get("log.dir", Conf), Filename), + max_no_files => cuttlefish:conf_get("log.rotation.count", Conf), + max_no_bytes => cuttlefish:conf_get("log.rotation.size", Conf)} + end, + + %% For the default logger that outputs to console + DefaultHandler = + if LogTo =:= console orelse LogTo =:= both -> + [{handler, default, logger_std_h, + #{level => all, + config => #{type => standard_io}, + formatter => Formatter}}]; + true -> + [{handler, default, undefined}] + end, + + %% For the file logger + FileHandler = + if LogTo =:= file orelse LogTo =:= both -> + [{handler, file, logger_disk_log_h, + #{level => all, + config => FileConf(cuttlefish:conf_get("log.file", Conf)), + formatter => Formatter, + filesync_repeat_interval => 1000}}]; + true -> [] + end, + + %% For creating additional log files for specific log levels. + AdditionalLogFiles = + if LogTo =:= file orelse LogTo =:= both -> + lists:filter(fun({K, V}) -> + cuttlefish_variable:is_fuzzy_match(K, string:tokens("log.$level.file", ".")) + end, Conf); + true -> [] + end, + AdditionalHandlers = + [{handler, list_to_atom("file_for_"++Level), logger_disk_log_h, + #{level => list_to_atom(Level), + config => FileConf(Filename), + formatter => Formatter, + filesync_repeat_interval => 1000}} + || {[_, Level, _], Filename} <- AdditionalLogFiles], + + _AllHandlers = DefaultHandler ++ FileHandler ++ AdditionalHandlers +end}. + %%-------------------------------------------------------------------- %% Authentication/ACL %%-------------------------------------------------------------------- diff --git a/rebar.config b/rebar.config index 63b3a7595..9d493a566 100644 --- a/rebar.config +++ b/rebar.config @@ -1,8 +1,6 @@ {deps, [{jsx, "2.9.0"}, {gproc, "0.8.0"}, - {lager, "3.6.5"}, - {cowboy, "2.4.0"}, - {lager_syslog, {git, "https://github.com/basho/lager_syslog", {branch, "3.0.1"}}} + {cowboy, "2.4.0"} ]}. %% appended to deps in rebar.config.script @@ -20,7 +18,6 @@ warn_unused_import, warn_obsolete_guard, debug_info, - {parse_transform, lager_transform}, {d, 'APPLICATION', emqx}]}. {xref_checks, [undefined_function_calls, undefined_functions, locals_not_used, deprecated_function_calls, diff --git a/src/emqx.app.src b/src/emqx.app.src index d44707186..424801f52 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -3,8 +3,8 @@ {vsn,"3.0"}, {modules,[]}, {registered,[emqx_sup]}, - {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd, - cowboy,lager_syslog]}, + {applications,[kernel,stdlib,jsx,gproc,gen_rpc,esockd, + cowboy]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, diff --git a/src/emqx_app.erl b/src/emqx_app.erl index 4a39e46aa..a4e884cb9 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -47,12 +47,12 @@ stop(_State) -> %%-------------------------------------------------------------------- print_banner() -> - io:format("Starting ~s on node ~s~n", [?APP, node()]). + logger:info("Starting ~s on node ~s", [?APP, node()]). print_vsn() -> {ok, Descr} = application:get_key(description), {ok, Vsn} = application:get_key(vsn), - io:format("~s ~s is running now!~n", [Descr, Vsn]). + logger:info("~s ~s is running now!", [Descr, Vsn]). %%-------------------------------------------------------------------- %% Autocluster diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index 461564bd2..b9262059a 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -244,7 +244,7 @@ handle_info({'EXIT', Pid, normal}, State = #state{client_pid = Pid}) -> handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = Pid, reconnect_interval = ReconnectInterval}) -> - lager:warning("emqx bridge stop reason:~p", [Reason]), + logger:warning("emqx bridge stop reason:~p", [Reason]), erlang:send_after(ReconnectInterval, self(), start), {noreply, State#state{client_pid = undefined}}; @@ -306,7 +306,7 @@ format_mountpoint(Prefix) -> store(memory, Data, Queue, MaxPendingMsg) when length(Queue) =< MaxPendingMsg -> [Data | Queue]; store(memory, _Data, Queue, _MaxPendingMsg) -> - lager:error("Beyond max pending messages"), + logger:error("Beyond max pending messages"), Queue; store(disk, Data, Queue, _MaxPendingMsg)-> [Data | Queue]. diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 857090c25..19e473f36 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -337,7 +337,7 @@ handle_cast({From, #subscribe{topic = Topic, subpid = SubPid, subid = SubId, sub true -> case ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2) =:= SubOpts of true -> - io:format("Ets: ~p, SubOpts: ~p", [ets:lookup_element(?SUBOPTION, Topic, Subscriber), SubOpts]), + logger:info("Ets: ~p, SubOpts: ~p", [ets:lookup_element(?SUBOPTION, Topic, Subscriber), SubOpts]), gen_server:reply(From, ok), {noreply, State}; false -> diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index 17166a014..5fcc9a573 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -87,8 +87,8 @@ lookup_command(Cmd) when is_atom(Cmd) -> end. usage() -> - io:format("Usage: ~s~n", [?MODULE]), - [begin io:format("~80..-s~n", [""]), Mod:Cmd(usage) end + logger:info("Usage: ~s", [?MODULE]), + [begin logger:info("~80..-s", [""]), Mod:Cmd(usage) end || {_, {Mod, Cmd}, _} <- ets:tab2list(?TAB)]. %%------------------------------------------------------------------------------ diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index d3ef43069..d7bcfd08e 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -33,9 +33,9 @@ start() -> 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)]); + logger:info("Start mqtt:~s listener on ~s successfully.", [Proto, format(ListenOn)]); {error, Reason} -> - io:format(standard_error, "Failed to start mqtt:~s listener on ~s - ~p!", + io:format(standard_error, "Failed to start mqtt:~s listener on ~s - ~p~n!", [Proto, format(ListenOn), Reason]) end. @@ -117,9 +117,9 @@ stop() -> 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)]); + logger:info("Stop mqtt:~s listener on ~s successfully.", [Proto, format(ListenOn)]); {error, Reason} -> - io:format(standard_error, "Failed to stop mqtt:~s listener on ~s - ~p.", + io:format(standard_error, "Failed to stop mqtt:~s listener on ~s - ~p~n.", [Proto, format(ListenOn), Reason]) end. diff --git a/src/emqx_logger.erl b/src/emqx_logger.erl index 2af5cd6b7..64bac9968 100644 --- a/src/emqx_logger.erl +++ b/src/emqx_logger.erl @@ -22,38 +22,47 @@ -export([error/1, error/2, error/3]). -export([critical/1, critical/2, critical/3]). +-export([add_proc_metadata/1]). + debug(Msg) -> - lager:debug(Msg). + logger:debug(Msg). debug(Format, Args) -> - lager:debug(Format, Args). -debug(Metadata, Format, Args) when is_list(Metadata) -> - lager:debug(Metadata, Format, Args). + logger:debug(Format, Args). +debug(Metadata, Format, Args) when is_map(Metadata) -> + logger:debug(Format, Args, Metadata). info(Msg) -> - lager:info(Msg). + logger:info(Msg). info(Format, Args) -> - lager:info(Format, Args). -info(Metadata, Format, Args) when is_list(Metadata) -> - lager:info(Metadata, Format, Args). + logger:info(Format, Args). +info(Metadata, Format, Args) when is_map(Metadata) -> + logger:info(Format, Args, Metadata). warning(Msg) -> - lager:warning(Msg). + logger:warning(Msg). warning(Format, Args) -> - lager:warning(Format, Args). -warning(Metadata, Format, Args) when is_list(Metadata) -> - lager:warning(Metadata, Format, Args). + logger:warning(Format, Args). +warning(Metadata, Format, Args) when is_map(Metadata) -> + logger:warning(Format, Args, Metadata). error(Msg) -> - lager:error(Msg). + logger:error(Msg). error(Format, Args) -> - lager:error(Format, Args). -error(Metadata, Format, Args) when is_list(Metadata) -> - lager:error(Metadata, Format, Args). + logger:error(Format, Args). +error(Metadata, Format, Args) when is_map(Metadata) -> + logger:error(Format, Args, Metadata). critical(Msg) -> - lager:critical(Msg). + logger:critical(Msg). critical(Format, Args) -> - lager:critical(Format, Args). -critical(Metadata, Format, Args) when is_list(Metadata) -> - lager:critical(Metadata, Format, Args). + logger:critical(Format, Args). +critical(Metadata, Format, Args) when is_map(Metadata) -> + logger:critical(Format, Args, Metadata). +add_proc_metadata(Meta) -> + case logger:get_process_metadata() of + undefined -> + logger:set_process_metadata(Meta); + OldMeta -> + logger:set_process_metadata(maps:merge(OldMeta, Meta)) + end. \ No newline at end of file diff --git a/src/emqx_logger_formatter.erl b/src/emqx_logger_formatter.erl new file mode 100644 index 000000000..84ec448a5 --- /dev/null +++ b/src/emqx_logger_formatter.erl @@ -0,0 +1,360 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2017-2018. 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. +%% +%% %CopyrightEnd% +%% + +%% This file is copied from lib/kernel/src/logger_formatter.erl, and +%% modified for a more concise time format other than the default RFC3339. + +-module(emqx_logger_formatter). + +-export([format/2]). +-export([check_config/1]). + +-define(DEFAULT_FORMAT_TEMPLATE_SINGLE, [time," ",level,": ",msg,"\n"]). + +-define(FormatP, "~0tp"). + +-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()]. + +%%%----------------------------------------------------------------- +%%% API +-spec format(LogEvent,Config) -> unicode:chardata() when + LogEvent :: logger:log_event(), + Config :: config(). +format(#{level:=Level,msg:=Msg0,meta:=Meta},Config0) + when is_map(Config0) -> + Config = add_default_config(Config0), + Template = maps:get(template,Config), + {BT,AT0} = lists:splitwith(fun(msg) -> false; (_) -> true end, Template), + {DoMsg,AT} = + case AT0 of + [msg|Rest] -> {true,Rest}; + _ ->{false,AT0} + end, + B = do_format(Level,Meta,BT,Config), + A = do_format(Level,Meta,AT,Config), + MsgStr = + if DoMsg -> + Config1 = + case maps:get(chars_limit,Config) of + unlimited -> + Config; + Size0 -> + Size = + case Size0 - string:length([B,A]) of + S when S>=0 -> S; + _ -> 0 + end, + Config#{chars_limit=>Size} + end, + string:trim(format_msg(Msg0,Meta,Config1)); + true -> + "" + end, + truncate([B,MsgStr,A],maps:get(max_size,Config)). + +do_format(Level,Data,[level|Format],Config) -> + [to_string(level,Level,Config)|do_format(Level,Data,Format,Config)]; +do_format(Level,Data,[{Key,IfExist,Else}|Format],Config) -> + String = + case value(Key,Data) of + {ok,Value} -> do_format(Level,Data#{Key=>Value},IfExist,Config); + error -> do_format(Level,Data,Else,Config) + end, + [String|do_format(Level,Data,Format,Config)]; +do_format(Level,Data,[Key|Format],Config) + when is_atom(Key) orelse + (is_list(Key) andalso is_atom(hd(Key))) -> + String = + case value(Key,Data) of + {ok,Value} -> to_string(Key,Value,Config); + error -> "" + end, + [String|do_format(Level,Data,Format,Config)]; +do_format(Level,Data,[Str|Format],Config) -> + [Str|do_format(Level,Data,Format,Config)]; +do_format(_Level,_Data,[],_Config) -> + []. + +value(Key,Meta) when is_map_key(Key,Meta) -> + {ok,maps:get(Key,Meta)}; +value([Key|Keys],Meta) when is_map_key(Key,Meta) -> + value(Keys,maps:get(Key,Meta)); +value([],Value) -> + {ok,Value}; +value(_,_) -> + error. + +to_string(time,Time,Config) -> + format_time(Time,Config); +to_string(mfa,MFA,Config) -> + format_mfa(MFA,Config); +to_string(_,Value,Config) -> + to_string(Value,Config). + +to_string(X,_) when is_atom(X) -> + atom_to_list(X); +to_string(X,_) when is_integer(X) -> + integer_to_list(X); +to_string(X,_) when is_pid(X) -> + pid_to_list(X); +to_string(X,_) when is_reference(X) -> + ref_to_list(X); +to_string(X,_) when is_list(X) -> + case printable_list(lists:flatten(X)) of + true -> X; + _ -> io_lib:format(?FormatP,[X]) + end; +to_string(X,_) -> + io_lib:format(?FormatP,[X]). + +printable_list([]) -> + false; +printable_list(X) -> + io_lib:printable_list(X). + +format_msg({string,Chardata},Meta,Config) -> + format_msg({"~ts",[Chardata]},Meta,Config); +format_msg({report,_}=Msg,Meta,#{report_cb:=Fun}=Config) + when is_function(Fun,1); is_function(Fun,2) -> + format_msg(Msg,Meta#{report_cb=>Fun},maps:remove(report_cb,Config)); +format_msg({report,Report},#{report_cb:=Fun}=Meta,Config) when is_function(Fun,1) -> + try Fun(Report) of + {Format,Args} when is_list(Format), is_list(Args) -> + format_msg({Format,Args},maps:remove(report_cb,Meta),Config); + Other -> + format_msg({"REPORT_CB/1 ERROR: ~0tp; Returned: ~0tp", + [Report,Other]},Meta,Config) + catch C:R:S -> + format_msg({"REPORT_CB/1 CRASH: ~0tp; Reason: ~0tp", + [Report,{C,R,logger:filter_stacktrace(?MODULE,S)}]},Meta,Config) + end; +format_msg({report,Report},#{report_cb:=Fun}=Meta,Config) when is_function(Fun,2) -> + try Fun(Report,maps:with([depth,chars_limit,single_line],Config)) of + Chardata when ?IS_STRING(Chardata) -> + try chardata_to_list(Chardata) % already size limited by report_cb + catch _:_ -> + format_msg({"REPORT_CB/2 ERROR: ~0tp; Returned: ~0tp", + [Report,Chardata]},Meta,Config) + end; + Other -> + format_msg({"REPORT_CB/2 ERROR: ~0tp; Returned: ~0tp", + [Report,Other]},Meta,Config) + catch C:R:S -> + format_msg({"REPORT_CB/2 CRASH: ~0tp; Reason: ~0tp", + [Report,{C,R,logger:filter_stacktrace(?MODULE,S)}]}, + Meta,Config) + end; +format_msg({report,Report},Meta,Config) -> + format_msg({report,Report}, + Meta#{report_cb=>fun logger:format_report/1}, + Config); +format_msg(Msg,_Meta,#{depth:=Depth,chars_limit:=CharsLimit}) -> + Opts = chars_limit_to_opts(CharsLimit), + do_format_msg(Msg, Depth, Opts). + +chars_limit_to_opts(unlimited) -> []; +chars_limit_to_opts(CharsLimit) -> [{chars_limit,CharsLimit}]. + +do_format_msg({Format0,Args},Depth,Opts) -> + try + Format1 = io_lib:scan_format(Format0, Args), + Format = reformat(Format1, Depth), + io_lib:build_text(Format,Opts) + catch C:R:S -> + FormatError = "FORMAT ERROR: ~0tp - ~0tp", + case Format0 of + FormatError -> + %% already been here - avoid failing cyclically + erlang:raise(C,R,S); + _ -> + format_msg({FormatError,[Format0,Args]},Depth,Opts) + end + end. + +reformat(Format,unlimited) -> + Format; +reformat([#{control_char:=C}=M|T], Depth) when C =:= $p -> + [limit_depth(M#{width => 0}, Depth)|reformat(T, Depth)]; +reformat([#{control_char:=C}=M|T], Depth) when C =:= $P -> + [M#{width => 0}|reformat(T, Depth)]; +reformat([#{control_char:=C}=M|T], Depth) when C =:= $p; C =:= $w -> + [limit_depth(M, Depth)|reformat(T, Depth)]; +reformat([H|T], Depth) -> + [H|reformat(T, Depth)]; +reformat([], _) -> + []. + +limit_depth(M0, unlimited) -> + M0; +limit_depth(#{control_char:=C0, args:=Args}=M0, Depth) -> + C = C0 - ($a - $A), %To uppercase. + M0#{control_char:=C,args:=Args++[Depth]}. + +chardata_to_list(Chardata) -> + case unicode:characters_to_list(Chardata,unicode) of + List when is_list(List) -> + List; + Error -> + throw(Error) + end. + +truncate(String,unlimited) -> + String; +truncate(String,Size) -> + Length = string:length(String), + if Length>Size -> + case lists:reverse(lists:flatten(String)) of + [$\n|_] -> + string:slice(String,0,Size-4)++"...\n"; + _ -> + string:slice(String,0,Size-3)++"..." + end; + true -> + String + end. + +%% Convert microseconds-timestamp into local datatime string in milliseconds +format_time(SysTime,#{}) + when is_integer(SysTime) -> + Ms = SysTime rem 1000000 div 1000, + {Date, _Time = {H, Mi, S}} = calendar:system_time_to_local_time(SysTime, microsecond), + format_time({Date, {H, Mi, S, Ms}}). +format_time({{Y, M, D}, {H, Mi, S, Ms}}) -> + io_lib:format("~b-~2..0b-~2..0b ~2..0b:~2..0b:~2..0b.~3..0b", [Y, M, D, H, Mi, S, Ms]); +format_time({{Y, M, D}, {H, Mi, S}}) -> + io_lib:format("~b-~2..0b-~2..0b ~2..0b:~2..0b:~2..0b", [Y, M, D, H, Mi, S]). + +format_mfa({M,F,A},_) when is_atom(M), is_atom(F), is_integer(A) -> + atom_to_list(M)++":"++atom_to_list(F)++"/"++integer_to_list(A); +format_mfa({M,F,A},Config) when is_atom(M), is_atom(F), is_list(A) -> + format_mfa({M,F,length(A)},Config); +format_mfa(MFA,Config) -> + to_string(MFA,Config). + +%% Ensure that all valid configuration parameters exist in the final +%% configuration map +add_default_config(Config0) -> + Default = + #{chars_limit=>unlimited, + error_logger_notice_header=>info}, + MaxSize = get_max_size(maps:get(max_size,Config0,undefined)), + Depth = get_depth(maps:get(depth,Config0,undefined)), + add_default_template(maps:merge(Default,Config0#{max_size=>MaxSize, + depth=>Depth})). + +add_default_template(#{template:=_}=Config) -> + Config; +add_default_template(Config) -> + Config#{template=>?DEFAULT_FORMAT_TEMPLATE_SINGLE}. + +get_max_size(undefined) -> + unlimited; +get_max_size(S) -> + max(10,S). + +get_depth(undefined) -> + error_logger:get_format_depth(); +get_depth(S) -> + max(5,S). + +-spec check_config(Config) -> ok | {error,term()} when + Config :: config(). +check_config(Config) when is_map(Config) -> + do_check_config(maps:to_list(Config)); +check_config(Config) -> + {error,{invalid_formatter_config,?MODULE,Config}}. + +do_check_config([{Type,L}|Config]) when Type == chars_limit; + Type == depth; + Type == max_size -> + case check_limit(L) of + ok -> do_check_config(Config); + error -> {error,{invalid_formatter_config,?MODULE,{Type,L}}} + end; +do_check_config([{error_logger_notice_header,ELNH}|Config]) when ELNH == info; + ELNH == notice -> + do_check_config(Config); +do_check_config([{report_cb,RCB}|Config]) when is_function(RCB,1); + is_function(RCB,2) -> + do_check_config(Config); +do_check_config([{template,T}|Config]) -> + case check_template(T) of + ok -> do_check_config(Config); + error -> {error,{invalid_formatter_template,?MODULE,T}} + end; + +do_check_config([C|_]) -> + {error,{invalid_formatter_config,?MODULE,C}}; +do_check_config([]) -> + ok. + +check_limit(L) when is_integer(L), L>0 -> + ok; +check_limit(unlimited) -> + ok; +check_limit(_) -> + error. + +check_template([Key|T]) when is_atom(Key) -> + check_template(T); +check_template([Key|T]) when is_list(Key), is_atom(hd(Key)) -> + case lists:all(fun(X) when is_atom(X) -> true; + (_) -> false + end, + Key) of + true -> + check_template(T); + false -> + error + end; +check_template([{Key,IfExist,Else}|T]) + when is_atom(Key) orelse + (is_list(Key) andalso is_atom(hd(Key))) -> + case check_template(IfExist) of + ok -> + case check_template(Else) of + ok -> + check_template(T); + error -> + error + end; + error -> + error + end; +check_template([Str|T]) when is_list(Str) -> + case io_lib:printable_unicode_list(Str) of + true -> check_template(T); + false -> error + end; +check_template([]) -> + ok; +check_template(_) -> + error. diff --git a/src/emqx_modules.erl b/src/emqx_modules.erl index 09e732f68..60c4b6351 100644 --- a/src/emqx_modules.erl +++ b/src/emqx_modules.erl @@ -21,7 +21,7 @@ load() -> lists:foreach( fun({Mod, Env}) -> ok = Mod:load(Env), - io:format("Load ~s module successfully.~n", [Mod]) + logger:info("Load ~s module successfully.", [Mod]) end, emqx_config:get_env(modules, [])). -spec(unload() -> ok). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 2e4ff7d77..514277447 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -72,9 +72,8 @@ -define(NO_PROPS, undefined). --define(LOG(Level, Format, Args, PState), - emqx_logger:Level([{client, PState#pstate.client_id}], "MQTT(~s@~s): " ++ Format, - [PState#pstate.client_id, esockd_net:format(PState#pstate.peername) | Args])). +-define(LOG(Level, Format, Args, _PState), + emqx_logger:Level("[MQTT] " ++ Format, Args)). %%------------------------------------------------------------------------------ %% Init @@ -82,6 +81,7 @@ -spec(init(map(), list()) -> state()). init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) -> + emqx_logger:add_proc_metadata(#{peername => esockd_net:format(Peername)}), Zone = proplists:get_value(zone, Options), #pstate{zone = Zone, sendfun = SendFun, @@ -287,7 +287,7 @@ process_packet(?CONNECT_PACKET( client_id = ClientId, username = Username, password = Password} = Connect), PState) -> - + emqx_logger:add_proc_metadata(#{client_id => binary_to_list(ClientId)}), %% TODO: Mountpoint... %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) WillMsg = make_will_msg(Connect), diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 286066a59..8926ba6f8 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -161,9 +161,8 @@ -define(TIMEOUT, 60000). --define(LOG(Level, Format, Args, State), - emqx_logger:Level([{client, State#state.client_id}], - "Session(~s): " ++ Format, [State#state.client_id | Args])). +-define(LOG(Level, Format, Args, _State), + emqx_logger:Level("[Session] " ++ Format, Args)). %% @doc Start a session proc. -spec(start_link(SessAttrs :: map()) -> {ok, pid()}). @@ -341,6 +340,7 @@ init([Parent, #{zone := Zone, max_inflight := MaxInflight, topic_alias_maximum := TopicAliasMaximum, will_msg := WillMsg}]) -> + emqx_logger:add_proc_metadata(#{client_id => ClientId}), process_flag(trap_exit, true), true = link(ConnPid), IdleTimout = get_env(Zone, idle_timeout, 30000), diff --git a/src/emqx_sys_mon.erl b/src/emqx_sys_mon.erl index b42d96aa4..e3bd4a2a9 100644 --- a/src/emqx_sys_mon.erl +++ b/src/emqx_sys_mon.erl @@ -25,9 +25,9 @@ -define(SYSMON, ?MODULE). -define(LOG(Msg, ProcInfo), - emqx_logger:warning([{sysmon, true}], "[SYSMON] ~s~n~p", [WarnMsg, ProcInfo])). + emqx_logger:warning(#{sysmon => true}, "[SYSMON] ~s~n~p", [WarnMsg, ProcInfo])). -define(LOG(Msg, ProcInfo, PortInfo), - emqx_logger:warning([{sysmon, true}], "[SYSMON] ~s~n~p~n~p", [WarnMsg, ProcInfo, PortInfo])). + emqx_logger:warning(#{sysmon => true}, "[SYSMON] ~s~n~p~n~p", [WarnMsg, ProcInfo, PortInfo])). %% @doc Start system monitor -spec(start_link(Opts :: list(tuple())) -> {ok, pid()} | ignore | {error, term()}). @@ -130,7 +130,7 @@ handle_info({timeout, _Ref, reset}, State) -> {noreply, State#state{events = []}, hibernate}; handle_info(Info, State) -> - lager:error("[SYSMON] unexpected Info: ~p", [Info]), + logger:error("[SYSMON] unexpected Info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{timer = TRef}) -> diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index 44ad6c26f..4b7857e26 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -41,7 +41,7 @@ trace(publish, #message{topic = <<"$SYS/", _/binary>>}) -> ignore; trace(publish, #message{from = From, topic = Topic, payload = Payload}) when is_binary(From); is_atom(From) -> - emqx_logger:info([{client, From}, {topic, Topic}], "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). + emqx_logger:info(#{topic => Topic}, "PUBLISH to ~s: ~p", [Topic, Payload]). %%------------------------------------------------------------------------------ %% Start/Stop trace @@ -76,7 +76,7 @@ init([]) -> {ok, #state{level = emqx_config:get_env(trace_level, debug), traces = #{}}}. handle_call({start_trace, Who, LogFile}, _From, State = #state{level = Level, traces = Traces}) -> - case catch lager:trace_file(LogFile, [Who], Level, ?OPTIONS) of + case catch logger:trace_file(LogFile, [Who], Level, ?OPTIONS) of {ok, exists} -> {reply, {error, already_exists}, State}; {ok, Trace} -> @@ -92,7 +92,7 @@ handle_call({start_trace, Who, LogFile}, _From, State = #state{level = Level, tr handle_call({stop_trace, Who}, _From, State = #state{traces = Traces}) -> case maps:find(Who, Traces) of {ok, {Trace, _LogFile}} -> - case lager:stop_trace(Trace) of + case logger:stop_trace(Trace) of ok -> ok; {error, Error} -> emqx_logger:error("[Tracer] stop trace ~p error: ~p", [Who, Error]) diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index fa08fa1bb..14c959d66 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -45,9 +45,8 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). --define(WSLOG(Level, Format, Args, State), - emqx_logger:Level("MQTT/WS(~s): " ++ Format, - [esockd_net:format(State#state.peername) | Args])). +-define(WSLOG(Level, Format, Args, _State), + emqx_logger:Level("[MQTT/WS] " ++ Format, Args)). %%------------------------------------------------------------------------------ %% API diff --git a/test/emqx_SUITE_data/slave.config b/test/emqx_SUITE_data/slave.config index dd3a5f32b..1cdf851b5 100644 --- a/test/emqx_SUITE_data/slave.config +++ b/test/emqx_SUITE_data/slave.config @@ -33,21 +33,6 @@ {large_heap,8388608}, {busy_port,false}, {busy_dist_port,true}]}]}, - {sasl,[{sasl_error_logger,false}]}, - {lager, - [{error_logger_hwm,1000}, - {error_logger_redirect,true}, - {log_dir,"{{ platform_log_dir }}"}, - {handlers, - [{lager_console_backend,error}, - {lager_file_backend, - [{file,"{{ platform_log_dir }}/error.log"}, - {level,error}, - {size,10485760}, - {date,"$D0"}, - {count,5}]}, - {lager_syslog_backend,["emq",local0,error]}]}, - {crash_log,"{{ platform_log_dir }}/crash.log"}]}, {gen_rpc, [{socket_keepalive_count,2}, {socket_keepalive_interval,5}, diff --git a/test/ws_client.erl b/test/ws_client.erl index 25f38164d..39f01467a 100644 --- a/test/ws_client.erl +++ b/test/ws_client.erl @@ -52,10 +52,10 @@ init(_, _WSReq) -> {ok, #state{}}. websocket_handle(Frame, _, State = #state{waiting = undefined, buffer = Buffer}) -> - lager:info("Client received frame~p", [Frame]), + logger:info("Client received frame~p", [Frame]), {ok, State#state{buffer = [Frame|Buffer]}}; websocket_handle(Frame, _, State = #state{waiting = From}) -> - lager:info("Client received frame~p", [Frame]), + logger:info("Client received frame~p", [Frame]), From ! Frame, {ok, State#state{waiting = undefined}}. From 92cc171aaf3f3d648529f8a4a851e020397166fb Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 2 Nov 2018 18:02:14 +0800 Subject: [PATCH 395/520] Support tracing log files for specific topics or clients --- etc/emqx.conf | 3 ++ priv/emqx.schema | 5 +- src/emqx_logger_formatter.erl | 2 +- src/emqx_protocol.erl | 2 +- src/emqx_tracer.erl | 94 ++++++++++++++++++++++++----------- 5 files changed, 74 insertions(+), 32 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 547075f13..83249ee2a 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -342,6 +342,9 @@ log.to = console ## ## Value: debug | info | notice | warning | error | critical | alert | emergency ## +## Note: Only the messages with severity level greater than or equal to +## this level will be logged. +## ## Default: error log.level = error diff --git a/priv/emqx.schema b/priv/emqx.schema index 76f75513b..b867c4059 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -429,6 +429,7 @@ end}. {translation, "kernel.logger", fun(Conf) -> LogTo = cuttlefish:conf_get("log.to", Conf), + TopLogLevel = cuttlefish:conf_get("log.level", Conf), Formatter = {emqx_logger_formatter, #{template => [time," [",level,"] ", @@ -449,7 +450,7 @@ end}. DefaultHandler = if LogTo =:= console orelse LogTo =:= both -> [{handler, default, logger_std_h, - #{level => all, + #{level => TopLogLevel, config => #{type => standard_io}, formatter => Formatter}}]; true -> @@ -460,7 +461,7 @@ end}. FileHandler = if LogTo =:= file orelse LogTo =:= both -> [{handler, file, logger_disk_log_h, - #{level => all, + #{level => TopLogLevel, config => FileConf(cuttlefish:conf_get("log.file", Conf)), formatter => Formatter, filesync_repeat_interval => 1000}}]; diff --git a/src/emqx_logger_formatter.erl b/src/emqx_logger_formatter.erl index 84ec448a5..3c5d9fc16 100644 --- a/src/emqx_logger_formatter.erl +++ b/src/emqx_logger_formatter.erl @@ -133,7 +133,7 @@ to_string(X,_) when is_list(X) -> _ -> io_lib:format(?FormatP,[X]) end; to_string(X,_) -> - io_lib:format(?FormatP,[X]). + io_lib:format("~s",[X]). printable_list([]) -> false; diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 514277447..0d166cbc1 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -287,7 +287,7 @@ process_packet(?CONNECT_PACKET( client_id = ClientId, username = Username, password = Password} = Connect), PState) -> - emqx_logger:add_proc_metadata(#{client_id => binary_to_list(ClientId)}), + emqx_logger:add_proc_metadata(#{client_id => ClientId}), %% TODO: Mountpoint... %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) WillMsg = make_will_msg(Connect), diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index 4b7857e26..3d060dee9 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -25,12 +25,20 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {level, traces}). +-record(state, {level, org_top_level, traces}). --type(trace_who() :: {client | topic, binary()}). +-type(trace_who() :: {client_id | topic, binary()}). -define(TRACER, ?MODULE). --define(OPTIONS, [{formatter_config, [time, " [",severity,"] ", message, "\n"]}]). +-define(FORMAT, {emqx_logger_formatter, + #{template => + [time," [",level,"] ", + {client_id, + [{peername, + [client_id,"@",peername," "], + [client_id, " "]}], + []}, + msg,"\n"]}}). -spec(start_link() -> {ok, pid()} | ignore | {error, term()}). start_link() -> @@ -40,26 +48,26 @@ trace(publish, #message{topic = <<"$SYS/", _/binary>>}) -> %% Dont' trace '$SYS' publish ignore; trace(publish, #message{from = From, topic = Topic, payload = Payload}) - when is_binary(From); is_atom(From) -> + 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 or topic. +%% @doc Start to trace client_id or topic. -spec(start_trace(trace_who(), string()) -> ok | {error, term()}). -start_trace({client, ClientId}, LogFile) -> - start_trace({start_trace, {client, ClientId}, LogFile}); +start_trace({client_id, ClientId}, LogFile) -> + start_trace({start_trace, {client_id, ClientId}, LogFile}); start_trace({topic, Topic}, LogFile) -> start_trace({start_trace, {topic, Topic}, LogFile}). start_trace(Req) -> gen_server:call(?MODULE, Req, infinity). -%% @doc Stop tracing client or topic. +%% @doc Stop tracing client_id or topic. -spec(stop_trace(trace_who()) -> ok | {error, term()}). -stop_trace({client, ClientId}) -> - gen_server:call(?MODULE, {stop_trace, {client, ClientId}}); +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}}). @@ -73,37 +81,45 @@ lookup_traces() -> %%------------------------------------------------------------------------------ init([]) -> - {ok, #state{level = emqx_config:get_env(trace_level, debug), traces = #{}}}. + {ok, #state{level = emqx_config:get_env(trace_level, debug), + org_top_level = get_top_level(), + traces = #{}}}. handle_call({start_trace, Who, LogFile}, _From, State = #state{level = Level, traces = Traces}) -> - case catch logger:trace_file(LogFile, [Who], Level, ?OPTIONS) of - {ok, exists} -> - {reply, {error, already_exists}, State}; - {ok, Trace} -> - {reply, ok, State#state{traces = maps:put(Who, {Trace, LogFile}, Traces)}}; + case logger:add_handler(handler_id(Who), logger_disk_log_h, + #{level => Level, + formatter => ?FORMAT, + filesync_repeat_interval => 1000, + config => #{type => halt, file => LogFile}, + filter_default => stop, + filters => [{meta_key_filter, + {fun filter_by_meta_key/2, Who} }]}) of + ok -> + set_top_level(all), % open the top logger level to 'all' + emqx_logger:info("[Tracer] start trace for ~p", [Who]), + {reply, ok, State#state{traces = maps:put(Who, LogFile, Traces)}}; {error, Reason} -> - emqx_logger:error("[Tracer] trace error: ~p", [Reason]), - {reply, {error, Reason}, State}; - {'EXIT', Error} -> - emqx_logger:error("[Tracer] trace exit: ~p", [Error]), - {reply, {error, Error}, State} + emqx_logger:error("[Tracer] start trace for ~p failed, error: ~p", [Who, Reason]), + {reply, {error, Reason}, State} end; -handle_call({stop_trace, Who}, _From, State = #state{traces = Traces}) -> +handle_call({stop_trace, Who}, _From, State = #state{org_top_level = OrgTopLevel, traces = Traces}) -> case maps:find(Who, Traces) of - {ok, {Trace, _LogFile}} -> - case logger:stop_trace(Trace) of - ok -> ok; - {error, Error} -> - emqx_logger:error("[Tracer] stop trace ~p error: ~p", [Who, Error]) + {ok, _LogFile} -> + case logger:remove_handler(handler_id(Who)) of + ok -> + emqx_logger:info("[Tracer] stop trace for ~p", [Who]); + {error, Reason} -> + emqx_logger:error("[Tracer] stop trace for ~p failed, error: ~p", [Who, Reason]) end, + set_top_level(OrgTopLevel), % reset the top logger level to original value {reply, ok, State#state{traces = maps:remove(Who, Traces)}}; error -> {reply, {error, not_found}, State} end; handle_call(lookup_traces, _From, State = #state{traces = Traces}) -> - {reply, [{Who, LogFile} || {Who, {_Trace, LogFile}} <- maps:to_list(Traces)], State}; + {reply, [{Who, LogFile} || {Who, LogFile} <- maps:to_list(Traces)], State}; handle_call(Req, _From, State) -> emqx_logger:error("[Tracer] unexpected call: ~p", [Req]), @@ -123,3 +139,25 @@ terminate(_Reason, _State) -> 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)). + +get_top_level() -> + #{level := OrgTopLevel} = logger:get_primary_config(), + OrgTopLevel. + +set_top_level(Level) -> + logger:set_primary_config(level, Level). + +filter_by_meta_key(#{meta:=Meta}=LogEvent, {MetaKey, MetaValue}) -> + case maps:find(MetaKey, Meta) of + {ok, MetaValue} -> LogEvent; + {ok, Topic} when MetaKey =:= topic -> + case emqx_topic:match(Topic, MetaValue) of + true -> LogEvent; + false -> ignore + end; + _ -> ignore + end. \ No newline at end of file From 2a5f3e949c4352ac49008da23365f01c60c2858a Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 2 Nov 2018 18:32:17 +0800 Subject: [PATCH 396/520] revert Makefile --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 857d2ce8b..4d810737b 100644 --- a/Makefile +++ b/Makefile @@ -8,9 +8,9 @@ DEPS = jsx gproc gen_rpc ekka esockd cowboy clique dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 -dep_gen_rpc = git https://github.com/emqx/gen_rpc switch_to_logger +dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.2.0 dep_esockd = git https://github.com/emqx/esockd v5.4.2 -dep_ekka = git https://github.com/emqx/ekka switch_to_logger +dep_ekka = git https://github.com/emqx/ekka v0.4.1 dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 dep_clique = git https://github.com/emqx/clique develop @@ -19,7 +19,7 @@ NO_AUTOPATCH = cuttlefish ERLC_OPTS += +debug_info -DAPPLICATION=emqx BUILD_DEPS = cuttlefish -dep_cuttlefish = git https://github.com/emqx/cuttlefish switch_to_logger +dep_cuttlefish = git https://github.com/emqx/cuttlefish emqx30 #TEST_DEPS = emqx_ct_helplers #dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers From f6266eaa936fafc0f717361e2faa39f8c8c70960 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 2 Nov 2018 19:42:51 +0800 Subject: [PATCH 397/520] Bumped the version number of deps --- Makefile | 6 +++--- rebar.config | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 4d810737b..91e0f7c23 100644 --- a/Makefile +++ b/Makefile @@ -8,9 +8,9 @@ DEPS = jsx gproc gen_rpc ekka esockd cowboy clique dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 -dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.2.0 +dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.3.0 dep_esockd = git https://github.com/emqx/esockd v5.4.2 -dep_ekka = git https://github.com/emqx/ekka v0.4.1 +dep_ekka = git https://github.com/emqx/ekka v0.5.0 dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 dep_clique = git https://github.com/emqx/clique develop @@ -19,7 +19,7 @@ NO_AUTOPATCH = cuttlefish ERLC_OPTS += +debug_info -DAPPLICATION=emqx BUILD_DEPS = cuttlefish -dep_cuttlefish = git https://github.com/emqx/cuttlefish emqx30 +dep_cuttlefish = git https://github.com/emqx/cuttlefish v2.1.0 #TEST_DEPS = emqx_ct_helplers #dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers diff --git a/rebar.config b/rebar.config index 9d493a566..d4192c686 100644 --- a/rebar.config +++ b/rebar.config @@ -5,11 +5,11 @@ %% appended to deps in rebar.config.script {github_emqx_deps, - [{gen_rpc, "2.2.0"}, - {ekka, "v0.4.1"}, + [{gen_rpc, "2.3.0"}, + {ekka, "v0.5.0"}, {clique, "develop"}, {esockd, "v5.4.2"}, - {cuttlefish, "emqx30"} + {cuttlefish, "v2.1.0"} ]}. {edoc_opts, [{preprocess, true}]}. From 4ea57c2bf95c9331a26e1a823b47589f6b19dea1 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sat, 3 Nov 2018 22:56:11 +0800 Subject: [PATCH 398/520] Delete redundant case clauses in sendfun Prior to this change, there are ok, {binary, _Data}. {datagram, _Peer, _Dara} case clauses, and the {binary, _Data} and {datagram, _Peer, _Data} are unnecessary cases This change delete these two cases and add ok in the end of funtion in send_fun of emqx_ws_connection. --- src/emqx_protocol.erl | 6 ------ src/emqx_ws_connection.erl | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 3e5db983f..9ce66bfb8 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -593,12 +593,6 @@ send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = SendFun ok -> emqx_metrics:sent(Packet), {ok, inc_stats(send, Type, PState)}; - {binary, _Data} -> - emqx_metrics:sent(Packet), - {ok, inc_stats(send, Type, PState)}; - {datagram, _Peer, _Data} -> - emqx_metrics:sent(Packet), - {ok, inc_stats(send, Type, PState)}; {error, Reason} -> {error, Reason} end. diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 407525601..7ff0b55a8 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -150,7 +150,8 @@ send_fun(WsPid) -> emqx_metrics:inc('bytes/sent', BinSize), put(send_oct, get(send_oct) + BinSize), put(send_cnt, get(send_cnt) + 1), - WsPid ! {binary, iolist_to_binary(Data)} + WsPid ! {binary, iolist_to_binary(Data)}, + ok end. stat_fun() -> From cca27d1a5a909c609626547851f024d2d6f024cb Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Mon, 5 Nov 2018 16:04:58 +0800 Subject: [PATCH 399/520] Change filesync_repeat_interval to no_repeat --- priv/emqx.schema | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index b867c4059..85a3c9581 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -464,27 +464,30 @@ end}. #{level => TopLogLevel, config => FileConf(cuttlefish:conf_get("log.file", Conf)), formatter => Formatter, - filesync_repeat_interval => 1000}}]; + filesync_repeat_interval => no_repeat}}]; true -> [] end, %% For creating additional log files for specific log levels. AdditionalLogFiles = - if LogTo =:= file orelse LogTo =:= both -> - lists:filter(fun({K, V}) -> - cuttlefish_variable:is_fuzzy_match(K, string:tokens("log.$level.file", ".")) - end, Conf); - true -> [] - end, + lists:foldl( + fun({[_, Level, _] = K, Filename}, Acc) when LogTo =:= file; LogTo =:= both -> + case cuttlefish_variable:is_fuzzy_match(K, ["log", "$level", "file"]) of + true -> [{Level, Filename} | Acc]; + false -> Acc + end; + ({_K, _V}, Acc) -> + Acc + end, [], Conf), AdditionalHandlers = [{handler, list_to_atom("file_for_"++Level), logger_disk_log_h, #{level => list_to_atom(Level), config => FileConf(Filename), formatter => Formatter, - filesync_repeat_interval => 1000}} - || {[_, Level, _], Filename} <- AdditionalLogFiles], + filesync_repeat_interval => no_repeat}} + || {Level, Filename} <- AdditionalLogFiles], - _AllHandlers = DefaultHandler ++ FileHandler ++ AdditionalHandlers + DefaultHandler ++ FileHandler ++ AdditionalHandlers end}. %%-------------------------------------------------------------------- From f448c62e472e6f843a2d4a29cb05ab6c1e62beb2 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Thu, 8 Nov 2018 10:58:30 +0800 Subject: [PATCH 400/520] Fix config descriptions of ACL cache --- etc/emqx.conf | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 0b3604102..a88e7a33e 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -434,22 +434,20 @@ acl_nomatch = allow ## Value: File Name acl_file = {{ platform_etc_dir }}/acl.conf -## Whether to enable ACL cache for publish. -## The ACL cache size -## The maximum count of ACL entries allowed for a client. +## Whether to enable ACL cache. +## +## If enabled, ACLs roles for each client will be cached in the memory ## ## Value: on | off enable_acl_cache = on -## The ACL cache size -## The maximum count of ACL entries allowed for a client. +## The maximum count of ACL entries can be cached for a client. ## ## Value: Integer greater than 0 ## Default: 32 acl_cache_max_size = 32 -## The ACL cache time-to-live. -## The time after which an ACL cache entry will be invalid +## The time after which an ACL cache entry will be deleted ## ## Value: Duration ## Default: 1 minute From 32d382644070ed5c831f63e49214ad19b5ce2b6e Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Tue, 6 Nov 2018 11:56:51 +0800 Subject: [PATCH 401/520] Replace macro QOS$i to QOS_$I The two styles of qos macro name in one project is chaotic. It is not a good practice. So I change the QOS$i to QOS_$i --- include/emqx_mqtt.hrl | 6 +- src/emqx_auth_mod.erl | 1 - src/emqx_bridge.erl | 4 +- src/emqx_client.erl | 4 +- src/emqx_message.erl | 4 +- src/emqx_mqtt_types.erl | 3 +- src/emqx_mqueue.erl | 5 +- src/emqx_packet.erl | 2 +- src/emqx_reason_codes.erl | 2 +- src/emqx_session.erl | 15 ++-- test/emqx_frame_SUITE.erl | 3 +- test/emqx_message_SUITE.erl | 2 +- test/emqx_mountpoint_SUITE.erl | 4 +- test/emqx_packet_SUITE.erl | 14 +-- test/emqx_protocol_SUITE.erl.bk | 147 -------------------------------- 15 files changed, 30 insertions(+), 186 deletions(-) delete mode 100644 test/emqx_protocol_SUITE.erl.bk diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index b9bf6b55e..3021e913a 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -43,11 +43,7 @@ -define(QOS_1, 1). %% At least once -define(QOS_2, 2). %% Exactly once --define(QOS0, 0). %% At most once --define(QOS1, 1). %% At least once --define(QOS2, 2). %% Exactly once - --define(IS_QOS(I), (I >= ?QOS0 andalso I =< ?QOS2)). +-define(IS_QOS(I), (I >= ?QOS_0 andalso I =< ?QOS_2)). -define(QOS_I(Name), begin diff --git a/src/emqx_auth_mod.erl b/src/emqx_auth_mod.erl index 8f03eb4fa..00ecb659a 100644 --- a/src/emqx_auth_mod.erl +++ b/src/emqx_auth_mod.erl @@ -39,4 +39,3 @@ behaviour_info(_Other) -> undefined. -endif. - diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index 461564bd2..a4dc840cc 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -30,11 +30,11 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {client_pid, options, reconnect_interval, +-record(state, {client_pid, options, reconnect_interval, mountpoint, queue, mqueue_type, max_pending_messages, forwards = [], subscriptions = []}). --record(mqtt_msg, {qos = ?QOS0, retain = false, dup = false, +-record(mqtt_msg, {qos = ?QOS_0, retain = false, dup = false, packet_id, topic, props, payload}). start_link(Name, Options) -> diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 22c37d26f..3825583bc 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -95,7 +95,7 @@ | {force_ping, boolean()} | {properties, properties()}). --record(mqtt_msg, {qos = ?QOS0, retain = false, dup = false, +-record(mqtt_msg, {qos = ?QOS_0, retain = false, dup = false, packet_id, topic, props, payload}). -type(mqtt_msg() :: #mqtt_msg{}). @@ -1217,7 +1217,7 @@ retry_send([{Type, Msg, Ts} | Msgs], Now, State = #state{retry_interval = Interv retry_send(publish, Msg = #mqtt_msg{qos = QoS, packet_id = PacketId}, Now, State = #state{inflight = Inflight}) -> - Msg1 = Msg#mqtt_msg{dup = (QoS =:= ?QOS1)}, + Msg1 = Msg#mqtt_msg{dup = (QoS =:= ?QOS_1)}, case send(Msg1, State) of {ok, NewState} -> Inflight1 = emqx_inflight:update(PacketId, {publish, Msg1, Now}, Inflight), diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 91e5d4d59..e6c34dc9d 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -34,7 +34,7 @@ make(Topic, Payload) -> -spec(make(atom() | emqx_types:client_id(), emqx_topic:topic(), emqx_types:payload()) -> emqx_types:message()). make(From, Topic, Payload) -> - make(From, ?QOS0, Topic, Payload). + make(From, ?QOS_0, Topic, Payload). -spec(make(atom() | emqx_types:client_id(), emqx_mqtt_types:qos(), emqx_topic:topic(), emqx_types:payload()) -> emqx_types:message()). @@ -47,7 +47,7 @@ make(From, QoS, Topic, Payload) -> payload = Payload, timestamp = os:timestamp()}. -msgid(?QOS0) -> undefined; +msgid(?QOS_0) -> undefined; msgid(_QoS) -> emqx_guid:gen(). set_flags(Flags, Msg = #message{flags = undefined}) when is_map(Flags) -> diff --git a/src/emqx_mqtt_types.erl b/src/emqx_mqtt_types.erl index 6beb17780..71451cca7 100644 --- a/src/emqx_mqtt_types.erl +++ b/src/emqx_mqtt_types.erl @@ -22,7 +22,7 @@ -export_type([topic_filters/0]). -export_type([packet_id/0, packet_type/0, packet/0]). --type(qos() :: ?QOS0 | ?QOS1 | ?QOS2). +-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 | @@ -40,4 +40,3 @@ }). -type(topic_filters() :: [{emqx_topic:topic(), subopts()}]). -type(packet() :: #mqtt_packet{}). - diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index 90fe59ba8..56390d412 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -89,13 +89,13 @@ -opaque(mqueue() :: #mqueue{}). -spec(init(options()) -> mqueue()). -init(Opts = #{max_len := MaxLen0, store_qos0 := QoS0}) -> +init(Opts = #{max_len := MaxLen0, store_qos0 := QoS_0}) -> MaxLen = case (is_integer(MaxLen0) andalso MaxLen0 > ?MAX_LEN_INFINITY) of true -> MaxLen0; false -> ?MAX_LEN_INFINITY end, #mqueue{max_len = MaxLen, - store_qos0 = QoS0, + store_qos0 = QoS_0, p_table = get_opt(priorities, Opts, ?NO_PRIORITY_TABLE), default_p = get_priority_opt(Opts) }. @@ -165,4 +165,3 @@ get_priority_opt(Opts) -> %% while the highest 'infinity' is a [{infinity, queue:queue()}] get_priority(_Topic, ?NO_PRIORITY_TABLE, _) -> ?LOWEST_PRIORITY; get_priority(Topic, PTab, Dp) -> maps:get(Topic, PTab, Dp). - diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 2040e595e..6f430c5f2 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -103,7 +103,7 @@ validate_properties(_, _) -> validate_subscription({Topic, #{qos := QoS}}) -> emqx_topic:validate(filter, Topic) andalso validate_qos(QoS). -validate_qos(QoS) when ?QOS0 =< QoS, QoS =< ?QOS2 -> +validate_qos(QoS) when ?QOS_0 =< QoS, QoS =< ?QOS_2 -> true; validate_qos(_) -> error(bad_qos). diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index 75118b563..bb0d8bb0b 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -138,5 +138,5 @@ compat(connack, 16#9C) -> ?CONNACK_SERVER; compat(connack, 16#9D) -> ?CONNACK_SERVER; compat(connack, 16#9F) -> ?CONNACK_SERVER; -compat(suback, Code) when Code =< ?QOS2 -> Code; +compat(suback, Code) when Code =< ?QOS_2 -> Code; compat(suback, Code) when Code >= 16#80 -> 16#80. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 85b753f3d..cd7e80c84 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -517,7 +517,7 @@ handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight handle_cast({resume, #{conn_pid := ConnPid, will_msg := WillMsg, expiry_interval := SessionExpiryInterval, - max_inflight := MaxInflight, + max_inflight := MaxInflight, topic_alias_maximum := TopicAliasMaximum}}, State = #state{client_id = ClientId, conn_pid = OldConnPid, clean_start = CleanStart, @@ -547,7 +547,7 @@ handle_cast({resume, #{conn_pid := ConnPid, await_rel_timer = undefined, expiry_timer = undefined, expiry_interval = SessionExpiryInterval, - inflight = emqx_inflight:update_size(MaxInflight, State#state.inflight), + inflight = emqx_inflight:update_size(MaxInflight, State#state.inflight), topic_alias_maximum = TopicAliasMaximum, will_delay_timer = undefined, will_msg = WillMsg}, @@ -574,10 +574,10 @@ handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> end, State, Msgs)}; %% Dispatch message -handle_info({dispatch, Topic, Msg = #message{headers = Headers}}, +handle_info({dispatch, Topic, Msg = #message{headers = Headers}}, State = #state{subscriptions = SubMap, topic_alias_maximum = TopicAliasMaximum}) when is_record(Msg, message) -> TopicAlias = maps:get('Topic-Alias', Headers, undefined), - if + if TopicAlias =:= undefined orelse TopicAlias =< TopicAliasMaximum -> noreply(case maps:find(Topic, SubMap) of {ok, #{nl := Nl, qos := QoS, rap := Rap, subid := SubId}} -> @@ -802,13 +802,13 @@ dispatch(Msg, State = #state{client_id = ClientId, conn_pid = undefined}) -> end; %% Deliver qos0 message directly to client -dispatch(Msg = #message{qos = ?QOS0} = Msg, State) -> +dispatch(Msg = #message{qos = ?QOS_0} = Msg, State) -> deliver(undefined, Msg, State), inc_stats(deliver, Msg, State); dispatch(Msg = #message{qos = QoS} = Msg, State = #state{next_pkt_id = PacketId, inflight = Inflight}) - when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 -> + when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> case emqx_inflight:is_full(Inflight) of true -> enqueue_msg(Msg, State); false -> @@ -825,7 +825,7 @@ enqueue_msg(Msg, State = #state{mqueue = Q}) -> %%------------------------------------------------------------------------------ redeliver({PacketId, Msg = #message{qos = QoS}}, State) -> - deliver(PacketId, if QoS =:= ?QOS2 -> Msg; + deliver(PacketId, if QoS =:= ?QOS_2 -> Msg; true -> emqx_message:set_flag(dup, Msg) end, State); @@ -974,4 +974,3 @@ noreply(State) -> shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. - diff --git a/test/emqx_frame_SUITE.erl b/test/emqx_frame_SUITE.erl index 1127f60d9..a4e64c7d0 100644 --- a/test/emqx_frame_SUITE.erl +++ b/test/emqx_frame_SUITE.erl @@ -100,7 +100,7 @@ serialize_parse_connect(_) -> ?assertEqual({ok, Packet1, <<>>}, parse_serialize(Packet1)), Packet2 = ?CONNECT_PACKET(#mqtt_packet_connect{ client_id = <<"clientId">>, - will_qos = ?QOS1, + will_qos = ?QOS_1, will_flag = true, will_retain = true, will_topic = <<"will">>, @@ -427,4 +427,3 @@ parse(Bin, Opts) when is_map(Opts) -> payload() -> iolist_to_binary(["payload." || _I <- lists:seq(1, 1000)]). - diff --git a/test/emqx_message_SUITE.erl b/test/emqx_message_SUITE.erl index 37207e40c..3bb06d14c 100644 --- a/test/emqx_message_SUITE.erl +++ b/test/emqx_message_SUITE.erl @@ -35,7 +35,7 @@ all() -> message_make(_) -> Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), ?assertEqual(0, Msg#message.qos), - Msg1 = emqx_message:make(<<"clientid">>, ?QOS2, <<"topic">>, <<"payload">>), + Msg1 = emqx_message:make(<<"clientid">>, ?QOS_2, <<"topic">>, <<"payload">>), ?assert(is_binary(Msg1#message.id)), ?assertEqual(2, Msg1#message.qos). diff --git a/test/emqx_mountpoint_SUITE.erl b/test/emqx_mountpoint_SUITE.erl index 61d8d3652..a77baf751 100644 --- a/test/emqx_mountpoint_SUITE.erl +++ b/test/emqx_mountpoint_SUITE.erl @@ -28,8 +28,8 @@ t_mount_unmount(_) -> Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), Msg2 = emqx_mountpoint:mount(<<"mount">>, Msg), ?assertEqual(<<"mounttopic">>, Msg2#message.topic), - TopicFilter = [{<<"mounttopic">>, #{qos => ?QOS2}}], - TopicFilter = emqx_mountpoint:mount(<<"mount">>, [{<<"topic">>, #{qos => ?QOS2}}]), + TopicFilter = [{<<"mounttopic">>, #{qos => ?QOS_2}}], + TopicFilter = emqx_mountpoint:mount(<<"mount">>, [{<<"topic">>, #{qos => ?QOS_2}}]), Msg = emqx_mountpoint:unmount(<<"mount">>, Msg2). t_replvar(_) -> diff --git a/test/emqx_packet_SUITE.erl b/test/emqx_packet_SUITE.erl index dda27c45b..42fcbc9d7 100644 --- a/test/emqx_packet_SUITE.erl +++ b/test/emqx_packet_SUITE.erl @@ -44,7 +44,7 @@ packet_type_name(_) -> ?assertEqual('UNSUBSCRIBE', emqx_packet:type_name(?UNSUBSCRIBE)). packet_validate(_) -> - ?assert(emqx_packet:validate(?SUBSCRIBE_PACKET(15, #{'Subscription-Identifier' => 1}, [{<<"topic">>, #{qos => ?QOS0}}]))), + ?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">>))), @@ -52,7 +52,7 @@ packet_validate(_) -> ?assertError(subscription_identifier_invalid, emqx_packet:validate( ?SUBSCRIBE_PACKET(15, #{'Subscription-Identifier' => -1}, - [{<<"topic">>, #{qos => ?QOS0}}]))), + [{<<"topic">>, #{qos => ?QOS_0}}]))), ?assertError(topic_filters_invalid, emqx_packet:validate(?UNSUBSCRIBE_PACKET(1,[]))), ?assertError(topic_name_invalid, @@ -90,14 +90,14 @@ packet_validate(_) -> packet_message(_) -> Pkt = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - qos = ?QOS0, + qos = ?QOS_0, retain = false, dup = false}, variable = #mqtt_packet_publish{topic_name = <<"topic">>, packet_id = 10, properties = #{}}, payload = <<"payload">>}, - Msg = emqx_message:make(<<"clientid">>, ?QOS0, <<"topic">>, <<"payload">>), + Msg = emqx_message:make(<<"clientid">>, ?QOS_0, <<"topic">>, <<"payload">>), Msg2 = emqx_message:set_flag(retain, false, Msg), Pkt = emqx_packet:from_message(10, Msg2), Msg3 = emqx_message:set_header(username, "test", Msg2), @@ -112,8 +112,8 @@ packet_format(_) -> io:format("~s", [emqx_packet:format(?PUBLISH_PACKET(?QOS_2, <<"topic">>, 10, <<"payload">>))]), io:format("~s", [emqx_packet:format(?PUBACK_PACKET(?PUBACK, 98))]), io:format("~s", [emqx_packet:format(?PUBREL_PACKET(99))]), - io:format("~s", [emqx_packet:format(?SUBSCRIBE_PACKET(15, [{<<"topic">>, ?QOS0}, {<<"topic1">>, ?QOS1}]))]), - io:format("~s", [emqx_packet:format(?SUBACK_PACKET(40, [?QOS0, ?QOS1]))]), + io:format("~s", [emqx_packet:format(?SUBSCRIBE_PACKET(15, [{<<"topic">>, ?QOS_0}, {<<"topic1">>, ?QOS_1}]))]), + io:format("~s", [emqx_packet:format(?SUBACK_PACKET(40, [?QOS_0, ?QOS_1]))]), io:format("~s", [emqx_packet:format(?UNSUBSCRIBE_PACKET(89, [<<"t">>, <<"t2">>]))]), io:format("~s", [emqx_packet:format(?UNSUBACK_PACKET(90))]). @@ -122,7 +122,7 @@ packet_will_msg(_) -> client_id = <<"clientid">>, username = "test", will_retain = true, - will_qos = ?QOS2, + will_qos = ?QOS_2, will_topic = <<"topic">>, will_props = #{}, will_payload = <<"payload">>}, diff --git a/test/emqx_protocol_SUITE.erl.bk b/test/emqx_protocol_SUITE.erl.bk deleted file mode 100644 index f2d6d306a..000000000 --- a/test/emqx_protocol_SUITE.erl.bk +++ /dev/null @@ -1,147 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% 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_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include("emqx.hrl"). - --include("emqx_mqtt.hrl"). - --include_lib("eunit/include/eunit.hrl"). - --import(emqx_serializer, [serialize/1]). - -all() -> - [{group, parser}, - {group, serializer}, - {group, packet}, - {group, message}]. - -groups() -> - [{parser, [], - [parse_connect, - parse_bridge, - parse_publish, - parse_puback, - parse_pubrec, - parse_pubrel, - parse_pubcomp, - parse_subscribe, - parse_unsubscribe, - parse_pingreq, - parse_disconnect]}, - {serializer, [], - [serialize_connect, - serialize_connack, - serialize_publish, - serialize_puback, - serialize_pubrel, - serialize_subscribe, - serialize_suback, - serialize_unsubscribe, - serialize_unsuback, - serialize_pingreq, - serialize_pingresp, - serialize_disconnect]}, - {packet, [], - [packet_proto_name, - packet_type_name, - packet_connack_name, - packet_format]}, - {message, [], - [message_make, - message_from_packet, - message_flag]}]. - - - -%%-------------------------------------------------------------------- -%% Packet Cases -%%-------------------------------------------------------------------- - -packet_proto_name(_) -> - ?assertEqual(<<"MQIsdp">>, emqx_packet:protocol_name(3)), - ?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(4)). - -packet_type_name(_) -> - ?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT)), - ?assertEqual('UNSUBSCRIBE', emqx_packet:type_name(?UNSUBSCRIBE)). - -packet_connack_name(_) -> - ?assertEqual('CONNACK_ACCEPT', emqx_packet:connack_name(?CONNACK_ACCEPT)), - ?assertEqual('CONNACK_PROTO_VER', emqx_packet:connack_name(?CONNACK_PROTO_VER)), - ?assertEqual('CONNACK_INVALID_ID', emqx_packet:connack_name(?CONNACK_INVALID_ID)), - ?assertEqual('CONNACK_SERVER', emqx_packet:connack_name(?CONNACK_SERVER)), - ?assertEqual('CONNACK_CREDENTIALS', emqx_packet:connack_name(?CONNACK_CREDENTIALS)), - ?assertEqual('CONNACK_AUTH', emqx_packet:connack_name(?CONNACK_AUTH)). - -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))]), - io:format("~s", [emqx_packet:format(?PUBLISH_PACKET(?QOS_2, <<"topic">>, 10, <<"payload">>))]), - io:format("~s", [emqx_packet:format(?PUBACK_PACKET(?PUBACK, 98))]), - io:format("~s", [emqx_packet:format(?PUBREL_PACKET(99))]), - io:format("~s", [emqx_packet:format(?SUBSCRIBE_PACKET(15, [{<<"topic">>, ?QOS0}, {<<"topic1">>, ?QOS1}]))]), - io:format("~s", [emqx_packet:format(?SUBACK_PACKET(40, [?QOS0, ?QOS1]))]), - io:format("~s", [emqx_packet:format(?UNSUBSCRIBE_PACKET(89, [<<"t">>, <<"t2">>]))]), - io:format("~s", [emqx_packet:format(?UNSUBACK_PACKET(90))]). - -%%-------------------------------------------------------------------- -%% Message Cases -%%-------------------------------------------------------------------- - -message_make(_) -> - Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), - ?assertEqual(0, Msg#mqtt_message.qos), - Msg1 = emqx_message:make(<<"clientid">>, qos2, <<"topic">>, <<"payload">>), - ?assert(is_binary(Msg1#mqtt_message.id)), - ?assertEqual(2, Msg1#mqtt_message.qos). - -message_from_packet(_) -> - Msg = emqx_message:from_packet(?PUBLISH_PACKET(1, <<"topic">>, 10, <<"payload">>)), - ?assertEqual(1, Msg#mqtt_message.qos), - ?assertEqual(10, Msg#mqtt_message.pktid), - ?assertEqual(<<"topic">>, Msg#mqtt_message.topic), - WillMsg = emqx_message:from_packet(#mqtt_packet_connect{will_flag = true, - will_topic = <<"WillTopic">>, - will_msg = <<"WillMsg">>}), - ?assertEqual(<<"WillTopic">>, WillMsg#mqtt_message.topic), - ?assertEqual(<<"WillMsg">>, WillMsg#mqtt_message.payload), - - Msg2 = emqx_message:from_packet(<<"username">>, <<"clientid">>, - ?PUBLISH_PACKET(1, <<"topic">>, 20, <<"payload">>)), - ?assertEqual({<<"clientid">>, <<"username">>}, Msg2#mqtt_message.from), - io:format("~s", [emqx_message:format(Msg2)]). - -message_flag(_) -> - Pkt = ?PUBLISH_PACKET(1, <<"t">>, 2, <<"payload">>), - Msg2 = emqx_message:from_packet(<<"clientid">>, Pkt), - Msg3 = emqx_message:set_flag(retain, Msg2), - Msg4 = emqx_message:set_flag(dup, Msg3), - ?assert(Msg4#mqtt_message.dup), - ?assert(Msg4#mqtt_message.retain), - Msg5 = emqx_message:set_flag(Msg4), - Msg6 = emqx_message:unset_flag(dup, Msg5), - Msg7 = emqx_message:unset_flag(retain, Msg6), - ?assertNot(Msg7#mqtt_message.dup), - ?assertNot(Msg7#mqtt_message.retain), - emqx_message:unset_flag(Msg7), - emqx_message:to_packet(Msg7). - From 3879dcdf59ec126c0497b183dc15520472aab299 Mon Sep 17 00:00:00 2001 From: tigercl Date: Thu, 8 Nov 2018 23:02:35 +0800 Subject: [PATCH 402/520] Fix 'badarg' bug with duplicate subscriptions (#1943) --- src/emqx_broker.erl | 1 - src/emqx_sm.erl | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 857090c25..1a04cf997 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -337,7 +337,6 @@ handle_cast({From, #subscribe{topic = Topic, subpid = SubPid, subid = SubId, sub true -> case ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2) =:= SubOpts of true -> - io:format("Ets: ~p, SubOpts: ~p", [ets:lookup_element(?SUBOPTION, Topic, Subscriber), SubOpts]), gen_server:reply(From, ok), {noreply, State}; false -> diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index bc3f6ff68..55d0e26a7 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -60,9 +60,7 @@ open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid emqx_sm_locker:trans(ClientId, CleanStart); open_session(SessAttrs = #{clean_start := false, - client_id := ClientId, - max_inflight := MaxInflight, - topic_alias_maximum := TopicAliasMaximum}) -> + client_id := ClientId}) -> ResumeStart = fun(_) -> case resume_session(ClientId, SessAttrs) of {ok, SPid} -> From 2dc8ec8b11c6753fe3c44162292c2de46a4838e7 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Thu, 8 Nov 2018 00:20:59 +0800 Subject: [PATCH 403/520] Bridge via TLS --- etc/emqx.conf | 10 +++++++--- priv/emqx.schema | 23 ++++++++++++++--------- src/emqx_bridge.erl | 7 ++++++- src/emqx_client.erl | 24 +++++++++++++++++------- src/emqx_client_sock.erl | 10 +++++++++- 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index a88e7a33e..788c11c3f 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1672,21 +1672,25 @@ bridge.aws.mqueue_type = memory ## Value: Number bridge.aws.max_pending_messages = 10000 +## Bribge to remote server via SSL. +## +## Value: on | off +bridge.aws.ssl = off ## PEM-encoded CA certificates of the bridge. ## ## Value: File -## bridge.aws.cacertfile = cacert.pem +## bridge.aws.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem ## SSL Certfile of the bridge. ## ## Value: File -## bridge.aws.certfile = cert.pem +## bridge.aws.certfile = {{ platform_etc_dir }}/certs/client-cert.pem ## SSL Keyfile of the bridge. ## ## Value: File -## bridge.aws.keyfile = key.pem +## bridge.aws.keyfile = {{ platform_etc_dir }}/certs/client-key.pem ## SSL Ciphers used by the bridge. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index d09937bb2..fc89586ba 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1552,6 +1552,11 @@ end}. {datatype, string} ]}. +{mapping, "bridge.$name.ssl", "emqx.bridges", [ + {datatype, flag}, + {default, off} +]}. + {mapping, "bridge.$name.cacertfile", "emqx.bridges", [ {datatype, string} ]}. @@ -1575,11 +1580,12 @@ end}. {mapping, "bridge.$name.keepalive", "emqx.bridges", [ {default, "10s"}, - {datatype, {duration, s}} + {datatype, {duration, ms}} ]}. {mapping, "bridge.$name.tls_versions", "emqx.bridges", [ - {datatype, string} + {datatype, string}, + {default, "tlsv1,tlsv1.1,tlsv1.2"} ]}. {mapping, "bridge.$name.subscription.$id.topic", "emqx.bridges", [ @@ -1597,12 +1603,11 @@ end}. {mapping, "bridge.$name.reconnect_interval", "emqx.bridges", [ {default, "30s"}, - {datatype, {duration, s}} + {datatype, {duration, ms}} ]}. {translation, "emqx.bridges", fun(Conf) -> - Split = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end, IsSsl = fun(cacertfile) -> true; @@ -1625,11 +1630,12 @@ end}. case IsSsl(Opt) of true -> SslOpts = [Parse(Opt, Val)|proplists:get_value(ssl_opts, Opts, [])], - lists:ukeymerge(1, [{ssl_opts, SslOpts}], Opts); + lists:ukeymerge(1, [{ssl_opts, SslOpts}], lists:usort(Opts)); false -> [{Opt, Val}|Opts] end end, + Subscriptions = fun(Name) -> Configs = cuttlefish_variable:filter_by_prefix("bridge." ++ Name ++ ".subscription", Conf), lists:zip([Topic || {_, Topic} <- lists:sort([{I, Topic} || {[_, _, "subscription", I, "topic"], Topic} <- Configs])], @@ -1639,11 +1645,10 @@ end}. maps:to_list( lists:foldl( fun({["bridge", Name, Opt], Val}, Acc) -> + %% e.g #{aws => [{OptKey, OptVal}]} + Init = [{list_to_atom(Opt), Val},{subscriptions, Subscriptions(Name)}], maps:update_with(list_to_atom(Name), - fun(Opts) -> - Merge(list_to_atom(Opt), Val, Opts) - end, [{list_to_atom(Opt), Val}, - {subscriptions, Subscriptions(Name)}], Acc); + fun(Opts) -> Merge(list_to_atom(Opt), Val, Opts) end, Init, Acc); (_, Acc) -> Acc end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("bridge.", Conf)))) diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index a4dc840cc..d335d8655 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -199,7 +199,8 @@ handle_info(start, State = #state{options = Options, [emqx_client:subscribe(ClientPid, {Topic, Qos}) || {Topic, Qos} <- Subs], [emqx_broker:subscribe(Topic) || Topic <- Forwards], {noreply, State#state{client_pid = ClientPid, subscriptions = Subs, forwards = Forwards}}; - {error,_} -> + {error, Reason} -> + logger:error("[Bridge] start failed! error: ~p", [Reason]), erlang:send_after(ReconnectInterval, self(), start), {noreply, State} end; @@ -285,6 +286,10 @@ options([{clean_start, CleanStart}| Options], Acc) -> options([{address, Address}| Options], Acc) -> {Host, Port} = address(Address), options(Options, [{host, Host}, {port, Port}|Acc]); +options([{ssl, Ssl}| Options], Acc) -> + options(Options, [{ssl, Ssl}|Acc]); +options([{ssl_opts, SslOpts}| Options], Acc) -> + options(Options, [{ssl_opts, SslOpts}|Acc]); options([_Option | Options], Acc) -> options(Options, Acc). diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 3825583bc..e4077e4d9 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -538,14 +538,24 @@ init([{hosts, Hosts} | Opts], State) -> init(Opts, State#state{hosts = Hosts1}); init([{tcp_opts, TcpOpts} | Opts], State = #state{sock_opts = SockOpts}) -> init(Opts, State#state{sock_opts = emqx_misc:merge_opts(SockOpts, TcpOpts)}); -init([ssl | Opts], State = #state{sock_opts = SockOpts}) -> - ok = ssl:start(), - SockOpts1 = emqx_misc:merge_opts([{ssl_opts, []}], SockOpts), - init(Opts, State#state{sock_opts = SockOpts1}); +init([{ssl, EnableSsl} | Opts], State) -> + case lists:keytake(ssl_opts, 1, Opts) of + {value, SslOpts, WithOutSslOpts} -> + init([SslOpts, {ssl, EnableSsl}| WithOutSslOpts], State); + false -> + init([{ssl_opts, []}, {ssl, EnableSsl}| Opts], State) + end; init([{ssl_opts, SslOpts} | Opts], State = #state{sock_opts = SockOpts}) -> - ok = ssl:start(), - SockOpts1 = emqx_misc:merge_opts(SockOpts, [{ssl_opts, SslOpts}]), - init(Opts, State#state{sock_opts = SockOpts1}); + case lists:keytake(ssl, 1, Opts) of + {value, {ssl, true}, WithOutEnableSsl} -> + ok = ssl:start(), + SockOpts1 = emqx_misc:merge_opts(SockOpts, [{ssl_opts, SslOpts}]), + init(WithOutEnableSsl, State#state{sock_opts = SockOpts1}); + {value, {ssl, false}, WithOutEnableSsl} -> + init(WithOutEnableSsl, State); + false -> + init(Opts, State) + end; init([{client_id, ClientId} | Opts], State) -> init(Opts, State#state{client_id = iolist_to_binary(ClientId)}); init([{clean_start, CleanStart} | Opts], State) when is_boolean(CleanStart) -> diff --git a/src/emqx_client_sock.erl b/src/emqx_client_sock.erl index dc19a8d91..505454e2d 100644 --- a/src/emqx_client_sock.erl +++ b/src/emqx_client_sock.erl @@ -49,7 +49,10 @@ connect(Host, Port, SockOpts, Timeout) -> end. ssl_upgrade(Sock, SslOpts, Timeout) -> - case ssl:connect(Sock, SslOpts, Timeout) of + TlsVersions = proplists:get_value(versions, SslOpts, []), + Ciphers = proplists:get_value(ciphers, SslOpts, default_ciphers(TlsVersions)), + SslOpts2 = emqx_misc:merge_opts(SslOpts, [{ciphers, Ciphers}]), + case ssl:connect(Sock, SslOpts2, Timeout) of {ok, SslSock} -> ok = ssl:controlling_process(SslSock, self()), {ok, #ssl_socket{tcp = Sock, ssl = SslSock}}; @@ -91,3 +94,8 @@ sockname(Sock) when is_port(Sock) -> sockname(#ssl_socket{ssl = SslSock}) -> ssl:sockname(SslSock). +default_ciphers(TlsVersions) -> + lists:foldl( + fun(TlsVer, Ciphers) -> + Ciphers ++ ssl:cipher_suites(all, TlsVer) + end, [], TlsVersions). From 997958aed17c56cebe0a8293d8da178f221d610b Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 9 Nov 2018 08:51:03 +0800 Subject: [PATCH 404/520] Change the start_link API for emqx_client Prior to this change, emqx_client:start_link does 2 works in one call: - init an erlang process for emqx_client - send MQTT CONNECT to remote broker But this solution have some drawbacks: - the return value of `start_link` compiles the return values of the 2 works: {ok, Pid, MqttResult}. It is inconsistent with the return value of `gen_statem:start_link`, may causes confusions. - the return mode of the 2 works are different: `start_link` should always return {ok, Pid} or {error, Reason}, but connecting to mqtt may throw out exceptions as it handles the socket. But the caller couldn't have thought of the exception, he would pattern match on the result of `emqx_client:start_link`, but it crashed! - If the init work succeed but the connection failed, the caller couldn't get a Pid from the return value, but indeed it was created inside the emqx_client. This hides the fact that the Pid was created, and when the Pid dies, the caller would receive an message from a Pid it doesn' know about. This change divived these 2 work into 2 APIs: - `start_link/1` is to build and verify the options, and returns {ok,Pid} (on success) or {error, Reason} (on failure). - `connect/1` is to send MQTT CONNECT, and returns {ok, MQTTResult::properties()} or {error, MQTTReason}. MQTT reason codes will contains in the `MQTTReason`. --- etc/emqx.conf | 20 ++++++--- priv/emqx.schema | 29 +++++++------ src/emqx_bridge.erl | 44 ++++++++++++-------- src/emqx_client.erl | 19 +++------ test/emqx_client_SUITE.erl | 83 ++++++++++++++++++++++++++------------ 5 files changed, 121 insertions(+), 74 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 788c11c3f..1e04839e0 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1612,7 +1612,11 @@ bridge.aws.client_id = bridge_aws ## The Clean start flag of a remote bridge. ## ## Value: boolean -bridge.aws.clean_start = false +## Default: true +## +## NOTE: Some IoT platforms require clean_start +## must be set to 'true' +## bridge.aws.clean_start = true ## The username for a remote bridge. ## @@ -1682,12 +1686,12 @@ bridge.aws.ssl = off ## Value: File ## bridge.aws.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem -## SSL Certfile of the bridge. +## Client SSL Certfile of the bridge. ## ## Value: File ## bridge.aws.certfile = {{ platform_etc_dir }}/certs/client-cert.pem -## SSL Keyfile of the bridge. +## Client SSL Keyfile of the bridge. ## ## Value: File ## bridge.aws.keyfile = {{ platform_etc_dir }}/certs/client-key.pem @@ -1745,7 +1749,11 @@ bridge.aws.ssl = off ## The Clean start flag of a remote bridge. ## ## Value: boolean -## bridge.azure.clean_start = false +## Default: true +## +## NOTE: Some IoT platforms require clean_start +## must be set to 'true' +## bridge.azure.clean_start = true ## The username for a remote bridge. ## @@ -1811,12 +1819,12 @@ bridge.aws.ssl = off ## Value: File ## bridge.azure.cacertfile = cacert.pem -## SSL Certfile of the bridge. +## Client SSL Certfile of the bridge. ## ## Value: File ## bridge.azure.certfile = cert.pem -## SSL Keyfile of the bridge. +## Client SSL Keyfile of the bridge. ## ## Value: File ## bridge.azure.keyfile = key.pem diff --git a/priv/emqx.schema b/priv/emqx.schema index fc89586ba..e9667b114 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1549,7 +1549,8 @@ end}. ]}. {mapping, "bridge.$name.forwards", "emqx.bridges", [ - {datatype, string} + {datatype, string}, + {default, ""} ]}. {mapping, "bridge.$name.ssl", "emqx.bridges", [ @@ -1624,22 +1625,24 @@ end}. {ciphers, Split(Ciphers)}; (Opt, Val) -> {Opt, Val} - end, + end, - Merge = fun(Opt, Val, Opts) -> - case IsSsl(Opt) of - true -> - SslOpts = [Parse(Opt, Val)|proplists:get_value(ssl_opts, Opts, [])], - lists:ukeymerge(1, [{ssl_opts, SslOpts}], lists:usort(Opts)); - false -> - [{Opt, Val}|Opts] - end + Merge = fun(forwards, Val, Opts) -> + [{forwards, string:tokens(Val, ",")}|Opts]; + (Opt, Val, Opts) -> + case IsSsl(Opt) of + true -> + SslOpts = [Parse(Opt, Val)|proplists:get_value(ssl_opts, Opts, [])], + lists:ukeymerge(1, [{ssl_opts, SslOpts}], lists:usort(Opts)); + false -> + [{Opt, Val}|Opts] + end end, Subscriptions = fun(Name) -> - Configs = cuttlefish_variable:filter_by_prefix("bridge." ++ Name ++ ".subscription", Conf), - lists:zip([Topic || {_, Topic} <- lists:sort([{I, Topic} || {[_, _, "subscription", I, "topic"], Topic} <- Configs])], - [QoS || {_, QoS} <- lists:sort([{I, QoS} || {[_, _, "subscription", I, "qos"], QoS} <- Configs])]) + Configs = cuttlefish_variable:filter_by_prefix("bridge." ++ Name ++ ".subscription", Conf), + lists:zip([Topic || {_, Topic} <- lists:sort([{I, Topic} || {[_, _, "subscription", I, "topic"], Topic} <- Configs])], + [QoS || {_, QoS} <- lists:sort([{I, QoS} || {[_, _, "subscription", I, "qos"], QoS} <- Configs])]) end, maps:to_list( diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index d335d8655..b8f0dff56 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -188,20 +188,23 @@ handle_cast(Msg, State) -> %% start message bridge %%---------------------------------------------------------------- handle_info(start, State = #state{options = Options, - client_pid = undefined, - reconnect_interval = ReconnectInterval}) -> + client_pid = undefined}) -> case emqx_client:start_link([{owner, self()}|options(Options)]) of - {ok, ClientPid, _} -> - Subs = [{i2b(Topic), Qos} || {Topic, Qos} <- get_value(subscriptions, Options, []), - emqx_topic:validate({filter, i2b(Topic)})], - Forwards = [i2b(Topic) || Topic <- string:tokens(get_value(forwards, Options, ""), ","), - emqx_topic:validate({filter, i2b(Topic)})], - [emqx_client:subscribe(ClientPid, {Topic, Qos}) || {Topic, Qos} <- Subs], - [emqx_broker:subscribe(Topic) || Topic <- Forwards], - {noreply, State#state{client_pid = ClientPid, subscriptions = Subs, forwards = Forwards}}; + {ok, ClientPid} -> + case emqx_client:connect(ClientPid) of + {ok, _} -> + emqx_logger:info("[Bridge] connected to remote sucessfully"), + Subs = subscribe_remote_topics(ClientPid, get_value(subscriptions, Options, [])), + Forwards = subscribe_local_topics(get_value(forwards, Options, [])), + {noreply, State#state{client_pid = ClientPid, + subscriptions = Subs, + forwards = Forwards}}; + {error, Reason} -> + emqx_logger:error("[Bridge] connect to remote failed! error: ~p", [Reason]), + {noreply, State#state{client_pid = ClientPid}} + end; {error, Reason} -> - logger:error("[Bridge] start failed! error: ~p", [Reason]), - erlang:send_after(ReconnectInterval, self(), start), + emqx_logger:error("[Bridge] start failed! error: ~p", [Reason]), {noreply, State} end; @@ -219,7 +222,7 @@ handle_info({dispatch, _, #message{topic = Topic, payload = Payload, flags = #{r {ok, PkgId} -> {noreply, State#state{queue = store(MqueueType, {PkgId, Msg}, Queue, MaxPendingMsg)}}; {error, Reason} -> - emqx_logger:error("Publish fail:~p", [Reason]), + emqx_logger:error("[Bridge] Publish fail:~p", [Reason]), {noreply, State} end; @@ -241,11 +244,12 @@ handle_info({puback, #{packet_id := PkgId}}, State = #state{queue = Queue, mqueu {noreply, State#state{queue = delete(MqueueType, PkgId, Queue)}}; handle_info({'EXIT', Pid, normal}, State = #state{client_pid = Pid}) -> + emqx_logger:warning("[Bridge] stop ~p", [normal]), {noreply, State#state{client_pid = undefined}}; handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = Pid, reconnect_interval = ReconnectInterval}) -> - lager:warning("emqx bridge stop reason:~p", [Reason]), + emqx_logger:error("[Bridge] stop ~p", [Reason]), erlang:send_after(ReconnectInterval, self(), start), {noreply, State#state{client_pid = undefined}}; @@ -259,6 +263,14 @@ terminate(_Reason, #state{}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +subscribe_remote_topics(ClientPid, Subscriptions) -> + [begin emqx_client:subscribe(ClientPid, {bin(Topic), Qos}), {bin(Topic), Qos} end + || {Topic, Qos} <- Subscriptions, emqx_topic:validate({filter, bin(Topic)})]. + +subscribe_local_topics(Topics) -> + [begin emqx_broker:subscribe(bin(Topic)), bin(Topic) end + || Topic <- Topics, emqx_topic:validate({filter, bin(Topic)})]. + proto_ver(mqttv3) -> v3; proto_ver(mqttv4) -> v4; proto_ver(mqttv5) -> v5. @@ -296,7 +308,7 @@ options([_Option | Options], Acc) -> name(Id) -> list_to_atom(lists:concat([?MODULE, "_", Id])). -i2b(L) -> iolist_to_binary(L). +bin(L) -> iolist_to_binary(L). mountpoint(undefined, Topic) -> Topic; @@ -306,7 +318,7 @@ mountpoint(Prefix, Topic) -> format_mountpoint(undefined) -> undefined; format_mountpoint(Prefix) -> - binary:replace(i2b(Prefix), <<"${node}">>, atom_to_binary(node(), utf8)). + binary:replace(bin(Prefix), <<"${node}">>, atom_to_binary(node(), utf8)). store(memory, Data, Queue, MaxPendingMsg) when length(Queue) =< MaxPendingMsg -> [Data | Queue]; diff --git a/src/emqx_client.erl b/src/emqx_client.erl index e4077e4d9..2d9cb0a80 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -21,6 +21,7 @@ -export([start_link/0, start_link/1]). -export([request/5, request/6, request_async/7, receive_response/3]). -export([set_request_handler/2, sub_request_topic/3, sub_request_topic/4]). +-export([connect/1]). -export([subscribe/2, subscribe/3, subscribe/4]). -export([publish/2, publish/3, publish/4, publish/5]). -export([unsubscribe/2, unsubscribe/3]). @@ -200,18 +201,11 @@ start_link(Options) when is_map(Options) -> start_link(Options) when is_list(Options) -> ok = emqx_mqtt_props:validate( proplists:get_value(properties, Options, #{})), - case start_client(with_owner(Options)) of - {ok, Client} -> - connect(Client); - Error -> Error - end. - -start_client(Options) -> case proplists:get_value(name, Options) of undefined -> - gen_statem:start_link(?MODULE, [Options], []); + gen_statem:start_link(?MODULE, [with_owner(Options)], []); Name when is_atom(Name) -> - gen_statem:start_link({local, Name}, ?MODULE, [Options], []) + gen_statem:start_link({local, Name}, ?MODULE, [with_owner(Options)], []) end. with_owner(Options) -> @@ -220,8 +214,7 @@ with_owner(Options) -> undefined -> [{owner, self()} | Options] end. -%% @private --spec(connect(client()) -> {ok, client(), properties()} | {error, term()}). +-spec(connect(client()) -> {ok, properties()} | {error, term()}). connect(Client) -> gen_statem:call(Client, connect, infinity). @@ -692,7 +685,7 @@ waiting_for_connack(cast, ?CONNACK_PACKET(?RC_SUCCESS, undefined -> AllProps; _ -> maps:merge(AllProps, Properties) end, - Reply = {ok, self(), Properties}, + Reply = {ok, Properties}, State2 = State1#state{client_id = assign_id(ClientId, AllProps1), properties = AllProps1, session_present = SessPresent}, @@ -1004,7 +997,7 @@ handle_event(info, {Error, _Sock, Reason}, _StateName, State) handle_event(info, {Closed, _Sock}, _StateName, State) when Closed =:= tcp_closed; Closed =:= ssl_closed -> - {stop, Closed, State}; + {stop, {shutdown, Closed}, State}; handle_event(info, {'EXIT', Owner, Reason}, _, #state{owner = Owner}) -> {stop, Reason}; diff --git a/test/emqx_client_SUITE.erl b/test/emqx_client_SUITE.erl index 2ad3dbaf7..c7b3455ad 100644 --- a/test/emqx_client_SUITE.erl +++ b/test/emqx_client_SUITE.erl @@ -59,20 +59,26 @@ end_per_suite(_Config) -> emqx_ct_broker_helpers:run_teardown_steps(). request_response_exception(QoS) -> - {ok, Client, _} = emqx_client:start_link([{proto_ver, v5}, - {properties, #{ 'Request-Response-Information' => 0 }}]), + {ok, Client} = emqx_client:start_link([{proto_ver, v5}, + {properties, #{ 'Request-Response-Information' => 0 }}]), + {ok, _} = emqx_client:connect(Client), ?assertError(no_response_information, emqx_client:sub_request_topic(Client, QoS, <<"request_topic">>)), ok = emqx_client:disconnect(Client). request_response_per_qos(QoS) -> - {ok, Requester, _} = emqx_client:start_link([{proto_ver, v5}, - {client_id, <<"requester">>}, - {properties, #{ 'Request-Response-Information' => 1}}]), - {ok, Responser, _} = emqx_client:start_link([{proto_ver, v5}, - {client_id, <<"responser">>}, - {properties, #{ 'Request-Response-Information' => 1}}, - {request_handler, fun(_Req) -> <<"ResponseTest">> end}]), + {ok, Requester} = emqx_client:start_link([{proto_ver, v5}, + {client_id, <<"requester">>}, + {properties, #{ 'Request-Response-Information' => 1}}]), + {ok, _} = emqx_client:connect(Requester), + {ok, Responser} = emqx_client:start_link([{proto_ver, v5}, + {client_id, <<"responser">>}, + {properties, #{ + 'Request-Response-Information' => 1}}, + {request_handler, + fun(_Req) -> <<"ResponseTest">> end} + ]), + {ok, _} = emqx_client:connect(Responser), ok = emqx_client:sub_request_topic(Responser, QoS, <<"request_topic">>), {ok, <<"ResponseTest">>} = emqx_client:request(Requester, <<"response_topic">>, <<"request_topic">>, <<"request_payload">>, QoS), ok = emqx_client:set_request_handler(Responser, fun(<<"request_payload">>) -> @@ -108,9 +114,15 @@ share_sub_request_topic_per_qos(QoS) -> {client_id, atom_to_binary(ClientId, utf8)}, {properties, Properties} ] end, - {ok, Requester, _} = emqx_client:start_link(Opts(requester)), - {ok, Responser1, _} = emqx_client:start_link([{request_handler, fun(Req) -> <<"1-", Req/binary>> end} | Opts(requester1)]), - {ok, Responser2, _} = emqx_client:start_link([{request_handler, fun(Req) -> <<"2-", Req/binary>> end} | Opts(requester2)]), + {ok, Requester} = emqx_client:start_link(Opts(requester)), + {ok, _} = emqx_client:connect(Requester), + + {ok, Responser1} = emqx_client:start_link([{request_handler, fun(Req) -> <<"1-", Req/binary>> end} | Opts(requester1)]), + {ok, _} = emqx_client:connect(Responser1), + + {ok, Responser2} = emqx_client:start_link([{request_handler, fun(Req) -> <<"2-", Req/binary>> end} | Opts(requester2)]), + {ok, _} = emqx_client:connect(Responser2), + ok = emqx_client:sub_request_topic(Responser1, QoS, ReqTopic, Group), ok = emqx_client:sub_request_topic(Responser2, QoS, ReqTopic, Group), %% Send a request, wait for response, validate response then return responser ID @@ -148,7 +160,9 @@ receive_messages(Count, Msgs) -> basic_test(_Config) -> Topic = nth(1, ?TOPICS), ct:print("Basic test starting"), - {ok, C, _} = emqx_client:start_link(), + {ok, C} = emqx_client:start_link(), + {ok, _} = emqx_client:connect(C), + {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), @@ -157,11 +171,15 @@ basic_test(_Config) -> ok = emqx_client:disconnect(C). will_message_test(_Config) -> - {ok, C1, _} = emqx_client:start_link([{clean_start, true}, + {ok, C1} = emqx_client:start_link([{clean_start, true}, {will_topic, nth(3, ?TOPICS)}, {will_payload, <<"client disconnected">>}, {keepalive, 2}]), - {ok, C2, _} = emqx_client:start_link(), + {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), ok = emqx_client:stop(C1), @@ -171,26 +189,33 @@ will_message_test(_Config) -> ct:print("Will message test succeeded"). offline_message_queueing_test(_) -> - {ok, C1, _} = emqx_client:start_link([{clean_start, false}, - {client_id, <<"c1">>}]), + {ok, C1} = emqx_client:start_link([{clean_start, false}, + {client_id, <<"c1">>}]), + {ok, _} = emqx_client:connect(C1), + {ok, _, [2]} = emqx_client:subscribe(C1, nth(6, ?WILD_TOPICS), 2), ok = emqx_client:disconnect(C1), - {ok, C2, _} = emqx_client:start_link([{clean_start, true}, - {client_id, <<"c2">>}]), + {ok, C2} = emqx_client:start_link([{clean_start, true}, + {client_id, <<"c2">>}]), + {ok, _} = emqx_client:connect(C2), ok = emqx_client:publish(C2, nth(2, ?TOPICS), <<"qos 0">>, 0), {ok, _} = emqx_client:publish(C2, nth(3, ?TOPICS), <<"qos 1">>, 1), {ok, _} = emqx_client:publish(C2, nth(4, ?TOPICS), <<"qos 2">>, 2), timer:sleep(10), emqx_client:disconnect(C2), - {ok, C3, _} = emqx_client:start_link([{clean_start, false}, + {ok, C3} = emqx_client:start_link([{clean_start, false}, {client_id, <<"c1">>}]), + {ok, _} = emqx_client:connect(C3), + timer:sleep(10), emqx_client:disconnect(C3), ?assertEqual(3, length(receive_messages(3))). overlapping_subscriptions_test(_) -> - {ok, C, _} = emqx_client:start_link([]), + {ok, C} = emqx_client:start_link([]), + {ok, _} = emqx_client:connect(C), + {ok, _, [2, 1]} = emqx_client:subscribe(C, [{nth(7, ?WILD_TOPICS), 2}, {nth(1, ?WILD_TOPICS), 1}]), timer:sleep(10), @@ -228,8 +253,10 @@ overlapping_subscriptions_test(_) -> redelivery_on_reconnect_test(_) -> ct:print("Redelivery on reconnect test starting"), - {ok, C1, _} = emqx_client:start_link([{clean_start, false}, - {client_id, <<"c">>}]), + {ok, C1} = emqx_client:start_link([{clean_start, false}, + {client_id, <<"c">>}]), + {ok, _} = emqx_client:connect(C1), + {ok, _, [2]} = emqx_client:subscribe(C1, nth(7, ?WILD_TOPICS), 2), timer:sleep(10), ok = emqx_client:pause(C1), @@ -240,8 +267,10 @@ redelivery_on_reconnect_test(_) -> timer:sleep(10), ok = emqx_client:disconnect(C1), ?assertEqual(0, length(receive_messages(2))), - {ok, C2, _} = emqx_client:start_link([{clean_start, false}, + {ok, C2} = emqx_client:start_link([{clean_start, false}, {client_id, <<"c">>}]), + {ok, _} = emqx_client:connect(C2), + timer:sleep(10), ok = emqx_client:disconnect(C2), ?assertEqual(2, length(receive_messages(2))). @@ -255,8 +284,10 @@ redelivery_on_reconnect_test(_) -> dollar_topics_test(_) -> ct:print("$ topics test starting"), - {ok, C, _} = emqx_client:start_link([{clean_start, true}, - {keepalive, 0}]), + {ok, C} = emqx_client:start_link([{clean_start, true}, + {keepalive, 0}]), + {ok, _} = emqx_client:connect(C), + {ok, _, [1]} = emqx_client:subscribe(C, nth(6, ?WILD_TOPICS), 1), {ok, _} = emqx_client:publish(C, << <<"$">>/binary, (nth(2, ?TOPICS))/binary>>, <<"test">>, [{qos, 1}, {retain, false}]), From c76e2d1413b2df779a916ca2c1910cff68bbaed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 9 Nov 2018 17:59:32 +0800 Subject: [PATCH 405/520] Fix 'badarg' in io_lib:format/2 when 'from' field is tuple --- src/emqx_message.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_message.erl b/src/emqx_message.erl index e6c34dc9d..085565403 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -114,7 +114,7 @@ elapsed(Since) -> max(0, timer:now_diff(os:timestamp(), Since) div 1000). format(#message{id = Id, qos = QoS, topic = Topic, from = From, flags = Flags, headers = Headers}) -> - io_lib:format("Message(Id=~s, QoS=~w, Topic=~s, From=~s, Flags=~s, Headers=~s)", + io_lib:format("Message(Id=~s, QoS=~w, Topic=~s, From=~p, Flags=~s, Headers=~s)", [Id, QoS, Topic, From, format(flags, Flags), format(headers, Headers)]). format(_, undefined) -> From 2611b6fc2af6e1520dcdeaaa297d11edb654ad19 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Sat, 10 Nov 2018 11:01:11 +0800 Subject: [PATCH 406/520] Revert logger print to io:format --- src/emqx_listeners.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index d7bcfd08e..6987f03f9 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -33,7 +33,7 @@ start() -> start_listener({Proto, ListenOn, Options}) -> case start_listener(Proto, ListenOn, Options) of {ok, _} -> - logger:info("Start mqtt:~s listener on ~s successfully.", [Proto, format(ListenOn)]); + 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]) @@ -117,7 +117,7 @@ stop() -> stop_listener({Proto, ListenOn, Opts}) -> case stop_listener(Proto, ListenOn, Opts) of ok -> - logger:info("Stop mqtt:~s listener on ~s successfully.", [Proto, format(ListenOn)]); + 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]) From 6e26f5e9af6396520f687fc2ffa5cd8514a724d6 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Sat, 10 Nov 2018 11:25:52 +0800 Subject: [PATCH 407/520] Revert logger print to io:format 2 --- src/emqx_ctl.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index 5fcc9a573..17166a014 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -87,8 +87,8 @@ lookup_command(Cmd) when is_atom(Cmd) -> end. usage() -> - logger:info("Usage: ~s", [?MODULE]), - [begin logger:info("~80..-s", [""]), Mod:Cmd(usage) end + io:format("Usage: ~s~n", [?MODULE]), + [begin io:format("~80..-s~n", [""]), Mod:Cmd(usage) end || {_, {Mod, Cmd}, _} <- ets:tab2list(?TAB)]. %%------------------------------------------------------------------------------ From 078584172e3e507c6f1a21d11f882b6bcdd04478 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Sat, 10 Nov 2018 12:10:51 +0800 Subject: [PATCH 408/520] Print EMQ X Version in the console --- src/emqx.app.src | 2 +- src/emqx_app.erl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqx.app.src b/src/emqx.app.src index 424801f52..77305f6e9 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -1,6 +1,6 @@ {application,emqx, [{description,"EMQ X Broker"}, - {vsn,"3.0"}, + {vsn,"3.0-rc.3"}, {modules,[]}, {registered,[emqx_sup]}, {applications,[kernel,stdlib,jsx,gproc,gen_rpc,esockd, diff --git a/src/emqx_app.erl b/src/emqx_app.erl index a4e884cb9..4a39e46aa 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -47,12 +47,12 @@ stop(_State) -> %%-------------------------------------------------------------------- print_banner() -> - logger:info("Starting ~s on node ~s", [?APP, node()]). + io:format("Starting ~s on node ~s~n", [?APP, node()]). print_vsn() -> {ok, Descr} = application:get_key(description), {ok, Vsn} = application:get_key(vsn), - logger:info("~s ~s is running now!", [Descr, Vsn]). + io:format("~s ~s is running now!~n", [Descr, Vsn]). %%-------------------------------------------------------------------- %% Autocluster From 9d67a64165049f87e7d41539b58434a67917d568 Mon Sep 17 00:00:00 2001 From: Andrei Nesterov Date: Sat, 17 Nov 2018 22:00:30 -0800 Subject: [PATCH 409/520] Fix type validation for User-Property --- src/emqx_mqtt_props.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_mqtt_props.erl b/src/emqx_mqtt_props.erl index 33acb360b..f4374d0d0 100644 --- a/src/emqx_mqtt_props.erl +++ b/src/emqx_mqtt_props.erl @@ -143,6 +143,6 @@ validate_value('UTF8-Encoded-String', Val) -> is_binary(Val); validate_value('Binary-Data', Val) -> is_binary(Val); -validate_value('User-Property', Val) -> +validate_value('UTF8-String-Pair', Val) -> is_tuple(Val) orelse is_list(Val). From cb835af42bf6029e99b704352d7d468f7118a6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 16 Nov 2018 11:12:29 +0800 Subject: [PATCH 410/520] Fix bad comment --- etc/emqx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 8b42eb682..33fa0ae9a 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -454,7 +454,7 @@ mqtt.max_topic_levels = 0 ## Value: 0 | 1 | 2 mqtt.max_qos_allowed = 2 -## Maximum Topic Alias, 0 means no limit. +## Maximum Topic Alias, 0 means no topic alias supported. ## ## Value: 0-65535 mqtt.max_topic_alias = 0 From 2269967f1a6b246dca8ec29f8974d00e5e39a60a Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Tue, 13 Nov 2018 20:25:39 +0800 Subject: [PATCH 411/520] Lazy evaluation when logging messages Formatting variables and then passing them into the logger functions leads to performance issues. i.e. ```erlang logger:debug("RECV ~s", [emqx_packet:format(Packet)]) ``` Above message will only be printed when the current log level set to `debug`, but the function emqx_packet:format/1 will always be evaluated no matter what the current log level is. OTP 21 provides a special meta-data named `report_cb`, which can be used for lazy evaluation. The fun is only evaluated if the primary/handler log level check passes, and is therefore recommended if it is expensive to generate the message. --- priv/emqx.schema | 4 ++- src/emqx_connection.erl | 44 +++++++++++++------------ src/emqx_protocol.erl | 67 ++++++++++++++++++++++++-------------- src/emqx_ws_connection.erl | 41 +++++++++++++---------- 4 files changed, 92 insertions(+), 64 deletions(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index 8026a6400..f003c37f3 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -437,7 +437,9 @@ end}. [{peername, [client_id,"@",peername," "], [client_id, " "]}], - []}, + [{peername, + [peername," "], + []}]}, msg,"\n"]}}, FileConf = fun(Filename) -> #{type => wrap, diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 6c3d4c7e6..9118933ef 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -50,9 +50,12 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). --define(LOG(Level, Format, Args, State), - emqx_logger:Level("MQTT(~s): " ++ Format, - [esockd_net:format(State#state.peername) | Args])). +-define(LOG(Level, Format, Args), + emqx_logger:Level(#{header => "[TCP] ", format => Format, args => Args}, + #{report_cb => + fun(#{header := Hdr0, format := Fmt0, args := Args0}) -> + {Hdr0 ++ Fmt0, Args0} + end})). start_link(Transport, Socket, Options) -> {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Socket, Options]])}. @@ -153,6 +156,8 @@ init([Transport, RawSocket, Options]) -> GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), ok = emqx_gc:init(GcPolicy), ok = emqx_misc:init_proc_mng_policy(Zone), + + emqx_logger:add_proc_metadata(#{peername => esockd_net:format(Peername)}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> @@ -169,7 +174,6 @@ send_fun(Transport, Socket, Peername) -> Data = emqx_frame:serialize(Packet, Options), try Transport:async_send(Socket, Data) of ok -> - ?LOG(debug, "SEND ~p", [iolist_to_binary(Data)], #state{peername = Peername}), emqx_metrics:inc('bytes/sent', iolist_size(Data)), ok; Error -> Error @@ -195,11 +199,11 @@ handle_call(session, _From, State = #state{proto_state = ProtoState}) -> {reply, emqx_protocol:session(ProtoState), State}; handle_call(Req, _From, State) -> - ?LOG(error, "unexpected call: ~p", [Req], State), + ?LOG(error, "unexpected call: ~p", [Req]), {reply, ignored, State}. handle_cast(Msg, State) -> - ?LOG(error, "unexpected cast: ~p", [Msg], State), + ?LOG(error, "unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> @@ -225,7 +229,7 @@ handle_info({timeout, Timer, emit_stats}, ok = emqx_gc:reset(), {noreply, NewState, hibernate}; {shutdown, Reason} -> - ?LOG(warning, "shutdown due to ~p", [Reason], NewState), + ?LOG(warning, "shutdown due to ~p", [Reason]), shutdown(Reason, NewState) end; handle_info(timeout, State) -> @@ -235,18 +239,18 @@ handle_info({shutdown, Reason}, State) -> shutdown(Reason, State); handle_info({shutdown, discard, {ClientId, ByPid}}, State) -> - ?LOG(warning, "discarded by ~s:~p", [ClientId, ByPid], State), + ?LOG(warning, "discarded by ~s:~p", [ClientId, ByPid]), shutdown(discard, State); handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> - ?LOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State), + ?LOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid]), shutdown(conflict, State); handle_info(activate_sock, State) -> {noreply, run_socket(State#state{conn_state = running, limit_timer = undefined})}; handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) -> - ?LOG(debug, "RECV ~p", [Data], State), + ?LOG(debug, "RECV ~p", [Data]), Size = iolist_size(Data), emqx_metrics:inc('bytes/received', Size), Incoming = #{bytes => Size, packets => 0}, @@ -262,7 +266,7 @@ handle_info({inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); handle_info({keepalive, start, Interval}, State = #state{transport = Transport, socket = Socket}) -> - ?LOG(debug, "Keepalive at the interval of ~p", [Interval], State), + ?LOG(debug, "Keepalive at the interval of ~p", [Interval]), StatFun = fun() -> case Transport:getstat(Socket, [recv_oct]) of {ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct}; @@ -287,14 +291,14 @@ handle_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> end; handle_info(Info, State) -> - ?LOG(error, "unexpected info: ~p", [Info], State), + ?LOG(error, "unexpected info: ~p", [Info]), {noreply, State}. -terminate(Reason, State = #state{transport = Transport, - socket = Socket, - keepalive = KeepAlive, - proto_state = ProtoState}) -> - ?LOG(debug, "Terminated for ~p", [Reason], State), +terminate(Reason, #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 @@ -330,7 +334,7 @@ handle_packet(Data, State = #state{proto_state = ProtoState, NewState = State#state{proto_state = ProtoState1}, handle_packet(Rest, inc_publish_cnt(Type, reset_parser(NewState))); {error, Reason} -> - ?LOG(error, "Process packet error - ~p", [Reason], State), + ?LOG(error, "Process packet error - ~p", [Reason]), shutdown(Reason, State); {error, Reason, ProtoState1} -> shutdown(Reason, State#state{proto_state = ProtoState1}); @@ -338,10 +342,10 @@ handle_packet(Data, State = #state{proto_state = ProtoState, stop(Error, State#state{proto_state = ProtoState1}) end; {error, Error} -> - ?LOG(error, "Framing error - ~p", [Error], State), + ?LOG(error, "Framing error - ~p", [Error]), shutdown(Error, State); {'EXIT', Reason} -> - ?LOG(error, "Parse failed for ~p~nError data:~p", [Reason, Data], State), + ?LOG(error, "Parse failed for ~p~nError data:~p", [Reason, Data]), shutdown(parse_error, State) end. diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 9289dd77c..904b158ab 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -72,8 +72,12 @@ -define(NO_PROPS, undefined). --define(LOG(Level, Format, Args, _PState), - emqx_logger:Level("[MQTT] " ++ Format, Args)). +-define(LOG(Level, Format, Args), + emqx_logger:Level(#{header => "[MQTT] ", format => Format, args => Args}, + #{report_cb => + fun(#{header := Hdr0, format := Fmt0, args := Args0}) -> + {Hdr0 ++ Fmt0, Args0} + end})). %%------------------------------------------------------------------------------ %% Init @@ -81,7 +85,6 @@ -spec(init(map(), list()) -> state()). init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) -> - emqx_logger:add_proc_metadata(#{peername => esockd_net:format(Peername)}), Zone = proplists:get_value(zone, Options), #pstate{zone = Zone, sendfun = SendFun, @@ -211,7 +214,7 @@ received(?PACKET(?CONNECT), PState = #pstate{connected = true}) -> received(Packet = ?PACKET(Type), PState) -> PState1 = set_protover(Packet, PState), - trace(recv, Packet, PState1), + trace(recv, Packet), try emqx_packet:validate(Packet) of true -> {Packet1, PState2} = preprocess_properties(Packet, PState1), @@ -319,11 +322,11 @@ process_packet(?CONNECT_PACKET( %% Success {?RC_SUCCESS, SP, PState4}; {error, Error} -> - ?LOG(error, "Failed to open session: ~p", [Error], PState1), + ?LOG(error, "Failed to open session: ~p", [Error]), {?RC_UNSPECIFIED_ERROR, PState1} end; {error, Reason} -> - ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], PState2), + ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason]), {?RC_NOT_AUTHORIZED, PState1} end; {error, ReasonCode} -> @@ -335,26 +338,31 @@ process_packet(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PSt {ok, PState1} -> do_publish(Packet, PState1); {error, ?RC_TOPIC_ALIAS_INVALID} -> - ?LOG(error, "Protocol error - ~p", [?RC_TOPIC_ALIAS_INVALID], PState), + ?LOG(error, "Protocol error - ~p", [?RC_TOPIC_ALIAS_INVALID]), {error, ?RC_TOPIC_ALIAS_INVALID, PState}; {error, ReasonCode} -> - ?LOG(warning, "Cannot publish qos0 message to ~s for ~s", [Topic, ReasonCode], PState), + ?LOG(warning, "Cannot publish qos0 message to ~s for ~s", + [Topic, emqx_reason_codes:text(ReasonCode)]), {error, ReasonCode, PState} end; -process_packet(Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), PState) -> +process_packet(Packet = ?PUBLISH_PACKET(?QOS_1, Topic, PacketId, _Payload), PState) -> case check_publish(Packet, PState) of {ok, PState1} -> do_publish(Packet, PState1); {error, ReasonCode} -> + ?LOG(warning, "Cannot publish qos1 message to ~s for ~s", + [Topic, emqx_reason_codes:text(ReasonCode)]), deliver({puback, PacketId, ReasonCode}, PState) end; -process_packet(Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), PState) -> +process_packet(Packet = ?PUBLISH_PACKET(?QOS_2, Topic, PacketId, _Payload), PState) -> case check_publish(Packet, PState) of {ok, PState1} -> do_publish(Packet, PState1); {error, ReasonCode} -> + ?LOG(warning, "Cannot publish qos2 message to ~s for ~s", + [Topic, emqx_reason_codes:text(ReasonCode)]), deliver({pubrec, PacketId, ReasonCode}, PState) end; @@ -404,11 +412,14 @@ process_packet(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), deliver({suback, PacketId, ReasonCodes}, PState) end; {error, TopicFilters} -> - ReasonCodes = lists:map(fun({_, #{rc := ?RC_SUCCESS}}) -> - ?RC_IMPLEMENTATION_SPECIFIC_ERROR; - ({_, #{rc := ReasonCode}}) -> - ReasonCode - end, 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]]), deliver({suback, PacketId, ReasonCodes}, PState) end; @@ -585,7 +596,7 @@ deliver({disconnect, _ReasonCode}, PState) -> -spec(send(emqx_mqtt_types:packet(), state()) -> {ok, state()} | {error, term()}). send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = SendFun}) -> - trace(send, Packet, PState), + trace(send, Packet), case SendFun(Packet, #{version => Ver}) of ok -> emqx_metrics:sent(Packet), @@ -759,7 +770,8 @@ check_pub_acl(_Packet, #pstate{is_super = IsSuper, enable_acl = EnableAcl}) check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, PState) -> case emqx_access_control:check_acl(credentials(PState), publish, Topic) of allow -> ok; - deny -> {error, ?RC_NOT_AUTHORIZED} + deny -> + {error, ?RC_NOT_AUTHORIZED} end. run_check_steps([], _Packet, PState) -> @@ -793,17 +805,22 @@ check_sub_acl(TopicFilters, PState) -> case emqx_access_control:check_acl(Credentials, subscribe, Topic) of allow -> {Ok, [{Topic, SubOpts}|Acc]}; deny -> - emqx_logger:warning([{client, PState#pstate.client_id}], - "ACL(~s) Cannot SUBSCRIBE ~p for ACL Deny", - [PState#pstate.client_id, Topic]), {error, [{Topic, SubOpts#{rc := ?RC_NOT_AUTHORIZED}}|Acc]} end end, {ok, []}, TopicFilters). -trace(recv, Packet, PState) -> - ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)], PState); -trace(send, Packet, PState) -> - ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)], PState). +trace(recv, Packet) -> + emqx_logger:debug(#{header => "[MQTT] RECV ~s", pck => Packet}, + #{report_cb => + fun(#{header := Fmt, pck := Pckt}) -> + {Fmt, [emqx_packet:format(Pckt)]} + end}); +trace(send, Packet) -> + emqx_logger:debug(#{header => "[MQTT] SEND ~s", pck => Packet}, + #{report_cb => + fun(#{header := Fmt, pck := Pckt}) -> + {Fmt, [emqx_packet:format(Pckt)]} + end}). inc_stats(recv, Type, PState = #pstate{recv_stats = Stats}) -> PState#pstate{recv_stats = inc_stats(Type, Stats)}; @@ -826,7 +843,7 @@ shutdown(Reason, #pstate{client_id = ClientId}) when Reason =:= conflict; emqx_cm:unregister_connection(ClientId); shutdown(Reason, PState = #pstate{connected = true, client_id = ClientId}) -> - ?LOG(info, "Shutdown for ~p", [Reason], PState), + ?LOG(info, "Shutdown for ~p", [Reason]), emqx_hooks:run('client.disconnected', [credentials(PState), Reason]), emqx_cm:unregister_connection(ClientId). diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 6a8b71094..fd421dde5 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -45,9 +45,12 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). --define(WSLOG(Level, Format, Args, _State), - emqx_logger:Level("[MQTT/WS] " ++ Format, Args)). - +-define(WSLOG(Level, Format, Args), + emqx_logger:Level(#{header => "[WS] ", format => Format, args => Args}, + #{report_cb => + fun(#{header := Hdr0, format := Fmt0, args := Args0}) -> + {Hdr0 ++ Fmt0, Args0} + end})). %%------------------------------------------------------------------------------ %% API %%------------------------------------------------------------------------------ @@ -135,6 +138,8 @@ websocket_init(#state{request = Req, options = Options}) -> EnableStats = emqx_zone:get_env(Zone, enable_stats, true), IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), lists:foreach(fun(Stat) -> put(Stat, 0) end, ?SOCK_STATS), + + emqx_logger:add_proc_metadata(#{peername => esockd_net:format(Peername)}), {ok, #state{peername = Peername, sockname = Sockname, parser_state = ParserState, @@ -164,7 +169,7 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, proto_state = ProtoState}) -> BinSize = iolist_size(Data), put(recv_oct, get(recv_oct) + BinSize), - ?WSLOG(debug, "RECV ~p", [Data], State), + ?WSLOG(debug, "RECV ~p", [Data]), emqx_metrics:inc('bytes/received', BinSize), case catch emqx_frame:parse(iolist_to_binary(Data), ParserState) of {more, NewParserState} -> @@ -176,7 +181,7 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, {ok, ProtoState1} -> websocket_handle({binary, Rest}, reset_parser(State#state{proto_state = ProtoState1})); {error, Error} -> - ?WSLOG(error, "Protocol error - ~p", [Error], State), + ?WSLOG(error, "Protocol error - ~p", [Error]), stop(Error, State); {error, Reason, ProtoState1} -> shutdown(Reason, State#state{proto_state = ProtoState1}); @@ -184,10 +189,10 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, stop(Error, State#state{proto_state = ProtoState1}) end; {error, Error} -> - ?WSLOG(error, "Frame error: ~p", [Error], State), + ?WSLOG(error, "Frame error: ~p", [Error]), stop(Error, State); {'EXIT', Reason} -> - ?WSLOG(error, "Frame error:~p~nFrame data: ~p", [Reason, Data], State), + ?WSLOG(error, "Frame error:~p~nFrame data: ~p", [Reason, Data]), shutdown(parse_error, State) end. @@ -225,12 +230,12 @@ websocket_info({timeout, Timer, emit_stats}, {ok, State#state{stats_timer = undefined}, hibernate}; websocket_info({keepalive, start, Interval}, State) -> - ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State), + ?WSLOG(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} -> - ?WSLOG(warning, "Keepalive error - ~p", [Error], State), + ?WSLOG(warning, "Keepalive error - ~p", [Error]), shutdown(Error, State) end; @@ -239,19 +244,19 @@ websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> {ok, KeepAlive1} -> {ok, State#state{keepalive = KeepAlive1}}; {error, timeout} -> - ?WSLOG(debug, "Keepalive Timeout!", [], State), + ?WSLOG(debug, "Keepalive Timeout!", []), shutdown(keepalive_timeout, State); {error, Error} -> - ?WSLOG(warning, "Keepalive error - ~p", [Error], State), + ?WSLOG(warning, "Keepalive error - ~p", [Error]), shutdown(keepalive_error, State) end; websocket_info({shutdown, discard, {ClientId, ByPid}}, State) -> - ?WSLOG(warning, "discarded by ~s:~p", [ClientId, ByPid], State), + ?WSLOG(warning, "discarded by ~s:~p", [ClientId, ByPid]), shutdown(discard, State); websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) -> - ?WSLOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State), + ?WSLOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid]), shutdown(conflict, State); websocket_info({binary, Data}, State) -> @@ -261,14 +266,14 @@ websocket_info({shutdown, Reason}, State) -> shutdown(Reason, State); websocket_info(Info, State) -> - ?WSLOG(error, "unexpected info: ~p", [Info], State), + ?WSLOG(error, "unexpected info: ~p", [Info]), {ok, State}. -terminate(SockError, _Req, State = #state{keepalive = Keepalive, - proto_state = ProtoState, - shutdown = Shutdown}) -> +terminate(SockError, _Req, #state{keepalive = Keepalive, + proto_state = ProtoState, + shutdown = Shutdown}) -> ?WSLOG(debug, "Terminated for ~p, sockerror: ~p", - [Shutdown, SockError], State), + [Shutdown, SockError]), emqx_keepalive:cancel(Keepalive), case {ProtoState, Shutdown} of {undefined, _} -> ok; From 41315bff0d8904039d8eb4a52765203cfb2b69b9 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Thu, 15 Nov 2018 23:12:36 +0800 Subject: [PATCH 412/520] Log macros for lazy evaluation --- include/logger.hrl | 17 +++++++++++++++++ src/emqx_connection.erl | 14 +++++--------- src/emqx_protocol.erl | 22 +++++----------------- src/emqx_ws_connection.erl | 33 +++++++++++++++------------------ 4 files changed, 42 insertions(+), 44 deletions(-) create mode 100644 include/logger.hrl diff --git a/include/logger.hrl b/include/logger.hrl new file mode 100644 index 000000000..2f13ddbb9 --- /dev/null +++ b/include/logger.hrl @@ -0,0 +1,17 @@ +%%-------------------------------------------------------------------- +%% Logs with header +%%-------------------------------------------------------------------- +-ifdef(LOG_HEADER). +%% with header +-define(LOG(Level, Format, Args), + begin + (emqx_logger:Level(#{},#{report_cb => + fun(_) -> + {?LOG_HEADER ++ " "++ (Format), (Args)} + end})) + end). +-else. +%% without header +-define(LOG(Level, Format, Args), + emqx_logger:Level(Format, Args)). +-endif. \ No newline at end of file diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 9118933ef..97a2c91bb 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -16,8 +16,11 @@ -behaviour(gen_server). +-define(LOG_HEADER, "[TCP]"). + -include("emqx.hrl"). -include("emqx_mqtt.hrl"). +-include("logger.hrl"). -export([start_link/3]). -export([info/1, attrs/1]). @@ -50,13 +53,6 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). --define(LOG(Level, Format, Args), - emqx_logger:Level(#{header => "[TCP] ", format => Format, args => Args}, - #{report_cb => - fun(#{header := Hdr0, format := Fmt0, args := Args0}) -> - {Hdr0 ++ Fmt0, Args0} - end})). - start_link(Transport, Socket, Options) -> {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Socket, Options]])}. @@ -135,7 +131,7 @@ init([Transport, RawSocket, Options]) -> PubLimit = init_limiter(emqx_zone:get_env(Zone, publish_limit)), EnableStats = emqx_zone:get_env(Zone, enable_stats, true), IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), - SendFun = send_fun(Transport, Socket, Peername), + SendFun = send_fun(Transport, Socket), ProtoState = emqx_protocol:init(#{peername => Peername, sockname => Sockname, peercert => Peercert, @@ -169,7 +165,7 @@ init_limiter(undefined) -> init_limiter({Rate, Burst}) -> esockd_rate_limit:new(Rate, Burst). -send_fun(Transport, Socket, Peername) -> +send_fun(Transport, Socket) -> fun(Packet, Options) -> Data = emqx_frame:serialize(Packet, Options), try Transport:async_send(Socket, Data) of diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 904b158ab..23710b76c 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -14,8 +14,11 @@ -module(emqx_protocol). +-define(LOG_HEADER, "[MQTT]"). + -include("emqx.hrl"). -include("emqx_mqtt.hrl"). +-include("logger.hrl"). -export([init/2]). -export([info/1]). @@ -72,13 +75,6 @@ -define(NO_PROPS, undefined). --define(LOG(Level, Format, Args), - emqx_logger:Level(#{header => "[MQTT] ", format => Format, args => Args}, - #{report_cb => - fun(#{header := Hdr0, format := Fmt0, args := Args0}) -> - {Hdr0 ++ Fmt0, Args0} - end})). - %%------------------------------------------------------------------------------ %% Init %%------------------------------------------------------------------------------ @@ -810,17 +806,9 @@ check_sub_acl(TopicFilters, PState) -> end, {ok, []}, TopicFilters). trace(recv, Packet) -> - emqx_logger:debug(#{header => "[MQTT] RECV ~s", pck => Packet}, - #{report_cb => - fun(#{header := Fmt, pck := Pckt}) -> - {Fmt, [emqx_packet:format(Pckt)]} - end}); + ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]); trace(send, Packet) -> - emqx_logger:debug(#{header => "[MQTT] SEND ~s", pck => Packet}, - #{report_cb => - fun(#{header := Fmt, pck := Pckt}) -> - {Fmt, [emqx_packet:format(Pckt)]} - end}). + ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]). inc_stats(recv, Type, PState = #pstate{recv_stats = Stats}) -> PState#pstate{recv_stats = inc_stats(Type, Stats)}; diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index fd421dde5..763aa182e 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -14,8 +14,11 @@ -module(emqx_ws_connection). +-define(LOG_HEADER, "[WS]"). + -include("emqx.hrl"). -include("emqx_mqtt.hrl"). +-include("logger.hrl"). -export([info/1, attrs/1]). -export([stats/1]). @@ -45,12 +48,6 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). --define(WSLOG(Level, Format, Args), - emqx_logger:Level(#{header => "[WS] ", format => Format, args => Args}, - #{report_cb => - fun(#{header := Hdr0, format := Fmt0, args := Args0}) -> - {Hdr0 ++ Fmt0, Args0} - end})). %%------------------------------------------------------------------------------ %% API %%------------------------------------------------------------------------------ @@ -169,7 +166,7 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, proto_state = ProtoState}) -> BinSize = iolist_size(Data), put(recv_oct, get(recv_oct) + BinSize), - ?WSLOG(debug, "RECV ~p", [Data]), + ?LOG(debug, "RECV ~p", [Data]), emqx_metrics:inc('bytes/received', BinSize), case catch emqx_frame:parse(iolist_to_binary(Data), ParserState) of {more, NewParserState} -> @@ -181,7 +178,7 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, {ok, ProtoState1} -> websocket_handle({binary, Rest}, reset_parser(State#state{proto_state = ProtoState1})); {error, Error} -> - ?WSLOG(error, "Protocol error - ~p", [Error]), + ?LOG(error, "Protocol error - ~p", [Error]), stop(Error, State); {error, Reason, ProtoState1} -> shutdown(Reason, State#state{proto_state = ProtoState1}); @@ -189,10 +186,10 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, stop(Error, State#state{proto_state = ProtoState1}) end; {error, Error} -> - ?WSLOG(error, "Frame error: ~p", [Error]), + ?LOG(error, "Frame error: ~p", [Error]), stop(Error, State); {'EXIT', Reason} -> - ?WSLOG(error, "Frame error:~p~nFrame data: ~p", [Reason, Data]), + ?LOG(error, "Frame error:~p~nFrame data: ~p", [Reason, Data]), shutdown(parse_error, State) end. @@ -230,12 +227,12 @@ websocket_info({timeout, Timer, emit_stats}, {ok, State#state{stats_timer = undefined}, hibernate}; websocket_info({keepalive, start, Interval}, State) -> - ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval]), + ?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} -> - ?WSLOG(warning, "Keepalive error - ~p", [Error]), + ?LOG(warning, "Keepalive error - ~p", [Error]), shutdown(Error, State) end; @@ -244,19 +241,19 @@ websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> {ok, KeepAlive1} -> {ok, State#state{keepalive = KeepAlive1}}; {error, timeout} -> - ?WSLOG(debug, "Keepalive Timeout!", []), + ?LOG(debug, "Keepalive Timeout!", []), shutdown(keepalive_timeout, State); {error, Error} -> - ?WSLOG(warning, "Keepalive error - ~p", [Error]), + ?LOG(warning, "Keepalive error - ~p", [Error]), shutdown(keepalive_error, State) end; websocket_info({shutdown, discard, {ClientId, ByPid}}, State) -> - ?WSLOG(warning, "discarded by ~s:~p", [ClientId, ByPid]), + ?LOG(warning, "discarded by ~s:~p", [ClientId, ByPid]), shutdown(discard, State); websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) -> - ?WSLOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid]), + ?LOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid]), shutdown(conflict, State); websocket_info({binary, Data}, State) -> @@ -266,13 +263,13 @@ websocket_info({shutdown, Reason}, State) -> shutdown(Reason, State); websocket_info(Info, State) -> - ?WSLOG(error, "unexpected info: ~p", [Info]), + ?LOG(error, "unexpected info: ~p", [Info]), {ok, State}. terminate(SockError, _Req, #state{keepalive = Keepalive, proto_state = ProtoState, shutdown = Shutdown}) -> - ?WSLOG(debug, "Terminated for ~p, sockerror: ~p", + ?LOG(debug, "Terminated for ~p, sockerror: ~p", [Shutdown, SockError]), emqx_keepalive:cancel(Keepalive), case {ProtoState, Shutdown} of From e8cc65ef400991904d669a4ed4f7f17eb6a9be27 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 16 Nov 2018 13:15:59 +0800 Subject: [PATCH 413/520] Change logger micros --- include/logger.hrl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/include/logger.hrl b/include/logger.hrl index 2f13ddbb9..acd0ba59b 100644 --- a/include/logger.hrl +++ b/include/logger.hrl @@ -1,17 +1,18 @@ %%-------------------------------------------------------------------- -%% Logs with header +%% Logs with a header prefixed to the log message. +%% And the log args are puted into report_cb for lazy evaluation. %%-------------------------------------------------------------------- -ifdef(LOG_HEADER). %% with header + -define(LOG(Level, Format, Args), begin - (emqx_logger:Level(#{},#{report_cb => + (logger:log(Level, #{},#{report_cb => fun(_) -> {?LOG_HEADER ++ " "++ (Format), (Args)} end})) end). -else. %% without header --define(LOG(Level, Format, Args), - emqx_logger:Level(Format, Args)). +-define(LOG(Level, Format, Args), logger:log(Level, Format, Args)). -endif. \ No newline at end of file From 82b8047349b2eecc769280580914a148d94be1bf Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 16 Nov 2018 13:31:20 +0800 Subject: [PATCH 414/520] Helper funcs for adding proc meta-data --- include/logger.hrl | 2 +- src/emqx_connection.erl | 2 +- src/emqx_logger.erl | 8 ++++++++ src/emqx_protocol.erl | 2 +- src/emqx_ws_connection.erl | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/include/logger.hrl b/include/logger.hrl index acd0ba59b..412861502 100644 --- a/include/logger.hrl +++ b/include/logger.hrl @@ -7,7 +7,7 @@ -define(LOG(Level, Format, Args), begin - (logger:log(Level, #{},#{report_cb => + (logger:log(Level,#{},#{report_cb => fun(_) -> {?LOG_HEADER ++ " "++ (Format), (Args)} end})) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 97a2c91bb..ed8b90649 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -153,7 +153,7 @@ init([Transport, RawSocket, Options]) -> ok = emqx_gc:init(GcPolicy), ok = emqx_misc:init_proc_mng_policy(Zone), - emqx_logger:add_proc_metadata(#{peername => esockd_net:format(Peername)}), + emqx_logger:add_metadata_peername(esockd_net:format(Peername)), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> diff --git a/src/emqx_logger.erl b/src/emqx_logger.erl index 64bac9968..2d850561c 100644 --- a/src/emqx_logger.erl +++ b/src/emqx_logger.erl @@ -22,6 +22,7 @@ -export([error/1, error/2, error/3]). -export([critical/1, critical/2, critical/3]). +-export([add_metadata_peername/1, add_metadata_client_id/1]). -export([add_proc_metadata/1]). debug(Msg) -> @@ -59,6 +60,13 @@ critical(Format, Args) -> critical(Metadata, Format, Args) when is_map(Metadata) -> logger:critical(Format, Args, Metadata). + +add_metadata_client_id(ClientId) -> + add_proc_metadata(#{client_id => ClientId}). + +add_metadata_peername(Peername) -> + add_proc_metadata(#{peername => Peername}). + add_proc_metadata(Meta) -> case logger:get_process_metadata() of undefined -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 23710b76c..4867cf2a5 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -286,7 +286,7 @@ process_packet(?CONNECT_PACKET( client_id = ClientId, username = Username, password = Password} = Connect), PState) -> - emqx_logger:add_proc_metadata(#{client_id => ClientId}), + emqx_logger:add_metadata_client_id(ClientId), %% TODO: Mountpoint... %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) WillMsg = make_will_msg(Connect), diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 763aa182e..08b4a3120 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -136,7 +136,7 @@ websocket_init(#state{request = Req, options = Options}) -> IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), lists:foreach(fun(Stat) -> put(Stat, 0) end, ?SOCK_STATS), - emqx_logger:add_proc_metadata(#{peername => esockd_net:format(Peername)}), + emqx_logger:add_metadata_peername(esockd_net:format(Peername)), {ok, #state{peername = Peername, sockname = Sockname, parser_state = ParserState, From bc1464a33fec8f2fe2a74dadf1563d41e9f4ca2e Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Mon, 19 Nov 2018 10:55:12 +0800 Subject: [PATCH 415/520] Lazy logging without header --- include/logger.hrl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/include/logger.hrl b/include/logger.hrl index 412861502..bae279da2 100644 --- a/include/logger.hrl +++ b/include/logger.hrl @@ -14,5 +14,11 @@ end). -else. %% without header --define(LOG(Level, Format, Args), logger:log(Level, Format, Args)). +-define(LOG(Level, Format, Args), + begin + (logger:log(Level,#{},#{report_cb => + fun(_) -> + {(Format), (Args)} + end})) + end). -endif. \ No newline at end of file From 16821490ce79d2867319a2fb7995ad698769b7df Mon Sep 17 00:00:00 2001 From: Gilbert Date: Mon, 19 Nov 2018 13:34:03 +0800 Subject: [PATCH 416/520] Fix issue#1874 (#1964) * Fix issue#1874 Prior to this change, if user use one client connect emqx with mqtt v3.1.1, the client subscribe the topic and publish message to this topic, it would receive this message itself published, this commit provide a configure option to let user ignore the message itself published. This change fix issue 1874. * Small Fix * Fix bug * Better design * Fix compile warning and improve coverage * Better design to solve the performance issue * Fix typo * Fix typo * Delete spaces in end of lines. * Do not use anonymous function * Better performance --- etc/emqx.conf | 5 +++++ priv/emqx.schema | 6 ++++++ src/emqx_broker.erl | 2 +- src/emqx_config.erl | 2 +- src/emqx_plugins.erl | 2 +- src/emqx_protocol.erl | 23 ++++++++++++++--------- src/emqx_session.erl | 5 ++++- src/emqx_sm.erl | 2 +- src/emqx_ws_connection.erl | 1 + test/emqx_broker_SUITE.erl | 2 +- test/emqx_client_SUITE.erl | 3 +-- test/emqx_mod_sup_SUITE.erl | 2 +- test/emqx_mqtt_caps_SUITE.erl | 4 ++-- test/emqx_mqtt_packet_SUITE.erl | 2 +- test/emqx_pqueue_SUITE.erl | 2 +- test/emqx_protocol_SUITE.erl | 2 +- test/emqx_session_SUITE.erl | 17 ++++++++++++++++- test/emqx_sm_SUITE.erl | 13 ++++++------- test/emqx_vm_SUITE.erl | 8 ++++---- 19 files changed, 68 insertions(+), 35 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 33fa0ae9a..05c7f074b 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -474,6 +474,11 @@ mqtt.wildcard_subscription = true ## Value: boolean mqtt.shared_subscription = true +## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) +## +## Value: true | false +mqtt.ignore_loop_deliver = false + ##-------------------------------------------------------------------- ## Zones ##-------------------------------------------------------------------- diff --git a/priv/emqx.schema b/priv/emqx.schema index f003c37f3..652b84657 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -595,6 +595,12 @@ end}. {datatype, {enum, [true, false]}} ]}. +%% @doc Whether to ignore loop delivery of messages.(for mqtt v3.1.1) +{mapping, "mqtt.ignore_loop_deliver", "emqx.mqtt_ignore_loop_deliver", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + %%-------------------------------------------------------------------- %% Zones %%-------------------------------------------------------------------- diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 1a04cf997..ff53554d4 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -201,7 +201,7 @@ aggre(Routes) -> lists:foldl( fun(#route{topic = To, dest = Node}, Acc) when is_atom(Node) -> [{To, Node} | Acc]; - (#route{topic = To, dest = {Group, _Node}}, Acc) -> + (#route{topic = To, dest = {Group, _Node}}, Acc) -> lists:usort([{To, Group} | Acc]) end, [], Routes). diff --git a/src/emqx_config.erl b/src/emqx_config.erl index 435ebaea7..bab3eab56 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -49,7 +49,7 @@ populate(_App) -> %% @doc Read the configuration of an application. -spec(read(atom()) -> {ok, list(env())} | {error, term()}). read(App) -> - %% TODO: + %% TODO: %% 1. Read the app.conf from etc folder %% 2. Cuttlefish to read the conf %% 3. Return the terms and schema diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index a6a04458f..44585a69e 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -228,7 +228,7 @@ find_plugin(Name) -> find_plugin(Name, list()). find_plugin(Name, Plugins) -> - lists:keyfind(Name, 2, Plugins). + lists:keyfind(Name, 2, Plugins). %% @doc UnLoad a Plugin -spec(unload(atom()) -> ok | {error, term()}). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 4867cf2a5..44b0925c3 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -63,7 +63,8 @@ recv_stats, send_stats, connected, - connected_at + connected_at, + ignore_loop }). -type(state() :: #pstate{}). @@ -71,6 +72,7 @@ -ifdef(TEST). -compile(export_all). +-compile(nowarn_export_all). -endif. -define(NO_PROPS, undefined). @@ -102,7 +104,8 @@ init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) enable_acl = emqx_zone:get_env(Zone, enable_acl), recv_stats = #{msg => 0, pkt => 0}, send_stats = #{msg => 0, pkt => 0}, - connected = false}. + connected = false, + ignore_loop = emqx_config:get_env(mqtt_ignore_loop_deliver, false)}. init_username(Peercert, Options) -> case proplists:get_value(peer_cert_as_username, Options) of @@ -385,11 +388,14 @@ process_packet(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #pstate{session = {ok = emqx_session:pubcomp(SPid, PacketId, ReasonCode), PState}; process_packet(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - PState = #pstate{session = SPid, mountpoint = Mountpoint, proto_ver = ProtoVer, is_bridge = IsBridge}) -> + PState = #pstate{session = SPid, mountpoint = Mountpoint, + proto_ver = ProtoVer, is_bridge = IsBridge, + ignore_loop = IgnoreLoop}) -> RawTopicFilters1 = if ProtoVer < ?MQTT_PROTO_V5 -> + IfIgnoreLoop = case IgnoreLoop of true -> 1; false -> 0 end, case IsBridge of - true -> [{RawTopic, SubOpts#{rap => 1}} || {RawTopic, SubOpts} <- RawTopicFilters]; - false -> [{RawTopic, SubOpts#{rap => 0}} || {RawTopic, SubOpts} <- RawTopicFilters] + true -> [{RawTopic, SubOpts#{rap => 1, nl => IfIgnoreLoop}} || {RawTopic, SubOpts} <- RawTopicFilters]; + false -> [{RawTopic, SubOpts#{rap => 0, nl => IfIgnoreLoop}} || {RawTopic, SubOpts} <- RawTopicFilters] end; true -> RawTopicFilters @@ -626,7 +632,6 @@ try_open_session(PState = #pstate{zone = Zone, clean_start => CleanStart, will_msg => WillMsg }, - SessAttrs1 = lists:foldl(fun set_session_attrs/2, SessAttrs, [{max_inflight, PState}, {expiry_interval, PState}, {topic_alias_maximum, PState}]), case emqx_sm:open_session(SessAttrs1) of {ok, SPid} -> @@ -685,12 +690,12 @@ get_property(Name, Props, Default) -> maps:get(Name, Props, Default). make_will_msg(#mqtt_packet_connect{proto_ver = ProtoVer, - will_props = WillProps} = Connect) -> - emqx_packet:will_msg(if + will_props = WillProps} = Connect) -> + emqx_packet:will_msg(if ProtoVer =:= ?MQTT_PROTO_V5 -> WillDelayInterval = get_property('Will-Delay-Interval', WillProps, 0), Connect#mqtt_packet_connect{will_props = set_property('Will-Delay-Interval', WillDelayInterval, WillProps)}; - true -> + true -> Connect end). diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 6cfef70d2..0d7352e66 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -152,6 +152,7 @@ will_msg :: emqx:message(), will_delay_timer :: reference() | undefined + }). -type(spid() :: pid()). @@ -575,7 +576,8 @@ handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> %% Dispatch message handle_info({dispatch, Topic, Msg = #message{headers = Headers}}, - State = #state{subscriptions = SubMap, topic_alias_maximum = TopicAliasMaximum}) when is_record(Msg, message) -> + State = #state{subscriptions = SubMap, + topic_alias_maximum = TopicAliasMaximum}) when is_record(Msg, message) -> TopicAlias = maps:get('Topic-Alias', Headers, undefined), if TopicAlias =:= undefined orelse TopicAlias =< TopicAliasMaximum -> @@ -591,6 +593,7 @@ handle_info({dispatch, Topic, Msg = #message{headers = Headers}}, noreply(State) end; + %% 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}); diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 55d0e26a7..8d0ac02e4 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -59,7 +59,7 @@ open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid end, emqx_sm_locker:trans(ClientId, CleanStart); -open_session(SessAttrs = #{clean_start := false, +open_session(SessAttrs = #{clean_start := false, client_id := ClientId}) -> ResumeStart = fun(_) -> case resume_session(ClientId, SessAttrs) of diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 08b4a3120..759e127c1 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -269,6 +269,7 @@ websocket_info(Info, State) -> terminate(SockError, _Req, #state{keepalive = Keepalive, proto_state = ProtoState, shutdown = Shutdown}) -> + ?LOG(debug, "Terminated for ~p, sockerror: ~p", [Shutdown, SockError]), emqx_keepalive:cancel(Keepalive), diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index 7fcc2a598..e0a2555f1 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -62,7 +62,7 @@ subscribe_unsubscribe(_) -> ok = emqx:subscribe(<<"topic/2">>, <<"clientId">>, #{ qos => 2 }), true = emqx:subscribed(<<"topic">>, <<"clientId">>), Topics = emqx:topics(), - lists:foreach(fun(Topic) -> + lists:foreach(fun(Topic) -> ?assert(lists:member(Topic, Topics)) end, Topics), ok = emqx:unsubscribe(<<"topic">>, <<"clientId">>), diff --git a/test/emqx_client_SUITE.erl b/test/emqx_client_SUITE.erl index c7b3455ad..6de507fca 100644 --- a/test/emqx_client_SUITE.erl +++ b/test/emqx_client_SUITE.erl @@ -150,8 +150,7 @@ receive_messages(Count, Msgs) -> receive {publish, Msg} -> receive_messages(Count-1, [Msg|Msgs]); - Other -> - ct:log("~p~n", [Other]), + _Other -> receive_messages(Count, Msgs) after 10 -> Msgs diff --git a/test/emqx_mod_sup_SUITE.erl b/test/emqx_mod_sup_SUITE.erl index 87245e351..8169c3f91 100644 --- a/test/emqx_mod_sup_SUITE.erl +++ b/test/emqx_mod_sup_SUITE.erl @@ -21,7 +21,7 @@ all() -> [t_child_all]. -t_child_all(_) -> +t_child_all(_) -> {ok, _Pid} = emqx_mod_sup:start_link(), {ok, _Child} = emqx_mod_sup:start_child(emqx_banned, worker), timer:sleep(10), diff --git a/test/emqx_mqtt_caps_SUITE.erl b/test/emqx_mqtt_caps_SUITE.erl index 919be5218..26e343ca6 100644 --- a/test/emqx_mqtt_caps_SUITE.erl +++ b/test/emqx_mqtt_caps_SUITE.erl @@ -114,8 +114,8 @@ t_check_sub(_) -> [{<<"client/stat">>, Opts}], [{<<"client/stat">>, Opts#{rc => ?RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED}}]), - ok = do_check_sub(Caps#{mqtt_wildcard_subscription => false}, - [{<<"vlient/+/dsofi">>, Opts}], + ok = do_check_sub(Caps#{mqtt_wildcard_subscription => false}, + [{<<"vlient/+/dsofi">>, Opts}], [{<<"vlient/+/dsofi">>, Opts#{rc => ?RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED}}]), emqx_zone:stop(). diff --git a/test/emqx_mqtt_packet_SUITE.erl b/test/emqx_mqtt_packet_SUITE.erl index 8bc41cb37..4386ff02e 100644 --- a/test/emqx_mqtt_packet_SUITE.erl +++ b/test/emqx_mqtt_packet_SUITE.erl @@ -63,7 +63,7 @@ all() -> [{group, connect}]. -groups() -> [{connect, [sequence], +groups() -> [{connect, [sequence], [case1_protocol_name, case2_protocol_ver%, %TOTO case3_invalid_reserved diff --git a/test/emqx_pqueue_SUITE.erl b/test/emqx_pqueue_SUITE.erl index e7672cb0b..9efccf472 100644 --- a/test/emqx_pqueue_SUITE.erl +++ b/test/emqx_pqueue_SUITE.erl @@ -94,7 +94,7 @@ t_priority_queues(_) -> PQueue6 = ?PQ:in(f, 1, PQueue5), {{value, e}, PQueue7} = ?PQ:out(PQueue6), - {empty, _} = ?PQ:out(0, ?PQ:new()), + {empty, _} = ?PQ:out(0, ?PQ:new()), {empty, Q0} = ?PQ:out_p(Q0), diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl index 060557c6c..3f6c4f252 100644 --- a/test/emqx_protocol_SUITE.erl +++ b/test/emqx_protocol_SUITE.erl @@ -132,7 +132,7 @@ connect_v5(_) -> raw_recv_parse(Data, ?MQTT_PROTO_V5) end), - % test clean start + % test clean start with_connection(fun(Sock) -> emqx_client_sock:send(Sock, raw_send_serialize( diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index f79b84557..5b6fc477c 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -18,9 +18,11 @@ -compile(export_all). -compile(nowarn_export_all). +-include_lib("eunit/include/eunit.hrl"). + -include_lib("common_test/include/ct.hrl"). -all() -> [t_session_all]. +all() -> [ignore_loop, t_session_all]. init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), @@ -29,6 +31,19 @@ init_per_suite(Config) -> end_per_suite(_Config) -> emqx_ct_broker_helpers:run_teardown_steps(). +ignore_loop(_Config) -> + application:set_env(emqx, mqtt_ignore_loop_deliver, true), + {ok, Client} = emqx_client:start_link(), + {ok, _} = emqx_client:connect(Client), + TestTopic = <<"Self">>, + {ok, _, [2]} = emqx_client:subscribe(Client, TestTopic, qos2), + ok = emqx_client:publish(Client, TestTopic, <<"testmsg">>, 0), + {ok, _} = emqx_client:publish(Client, TestTopic, <<"testmsg">>, 1), + {ok, _} = emqx_client:publish(Client, TestTopic, <<"testmsg">>, 2), + ?assertEqual(0, length(emqx_client_SUITE:receive_messages(3))), + ok = emqx_client:disconnect(Client), + application:set_env(emqx, mqtt_ignore_loop_deliver, false). + t_session_all(_) -> ClientId = <<"ClientId">>, {ok, ConnPid} = emqx_mock_client:start_link(ClientId), diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl index 2b83b6afb..3aed3090e 100644 --- a/test/emqx_sm_SUITE.erl +++ b/test/emqx_sm_SUITE.erl @@ -24,13 +24,13 @@ all() -> [t_open_close_session]. t_open_close_session(_) -> emqx_ct_broker_helpers:run_setup_steps(), {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), - Attrs = #{clean_start => true, - client_id => <<"client">>, + Attrs = #{clean_start => true, + client_id => <<"client">>, conn_pid => ClientPid, - zone => internal, - username => <<"zhou">>, - expiry_interval => 0, - max_inflight => 0, + zone => internal, + username => <<"emqx">>, + expiry_interval => 0, + max_inflight => 0, topic_alias_maximum => 0, will_msg => undefined}, {ok, SPid} = emqx_sm:open_session(Attrs), @@ -47,4 +47,3 @@ t_open_close_session(_) -> ok = emqx_sm:close_session(SPid), [] = emqx_sm:lookup_session(<<"client">>), emqx_ct_broker_helpers:run_teardown_steps(). - diff --git a/test/emqx_vm_SUITE.erl b/test/emqx_vm_SUITE.erl index b13b949b4..91744764e 100644 --- a/test/emqx_vm_SUITE.erl +++ b/test/emqx_vm_SUITE.erl @@ -95,7 +95,7 @@ all() -> - [load, systeminfo, mem_info, process_list, process_info, process_gc, + [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]. @@ -121,13 +121,13 @@ process_list(_Config) -> true = lists:member({pid, Pid}, lists:concat(ProcessInfo)). process_info(_Config) -> - ProcessInfos = emqx_vm:get_process_info(), + ProcessInfos = emqx_vm:get_process_info(), ProcessInfo = lists:last(ProcessInfos), Keys = [K || {K, _V}<- ProcessInfo], ?PROCESS_INFO = Keys. process_gc(_Config) -> - ProcessGcs = emqx_vm:get_process_gc(), + ProcessGcs = emqx_vm:get_process_gc(), ProcessGc = lists:last(ProcessGcs), Keys = [K || {K, _V}<- ProcessGc], ?PROCESS_GC = Keys. @@ -137,7 +137,7 @@ get_ets_list(_Config) -> Ets = emqx_vm:get_ets_list(), true = lists:member(test, Ets). -get_ets_info(_Config) -> +get_ets_info(_Config) -> ets:new(test, [named_table]), [] = emqx_vm:get_ets_info(test1), EtsInfo = emqx_vm:get_ets_info(test), From a2c658ba19024bf6ab3ca089fdddc57dea5a2516 Mon Sep 17 00:00:00 2001 From: spring2maz <40776645+spring2maz@users.noreply.github.com> Date: Wed, 21 Nov 2018 15:49:45 +0100 Subject: [PATCH 417/520] Add acking mechamism for shared dispatch (#1872) * Add acking mechamism for shared dispatch For QoS0 messages, no acking For QoS1/2 messages, 'ACK' at any of events below: - ACK when QoS is downgraded to 0 - Message is sent to connection process 'NACK' at any of events below: - Message queue is full and the receiving session starts to drop old messages - The receiving session crash Upon 'NACK', messages are dispatched to the 'next' subscriber in the group, depending on the shared subscription dispatch strategy. --- etc/emqx.conf | 9 ++ priv/emqx.schema | 6 ++ src/emqx_packet.erl | 16 ++- src/emqx_session.erl | 95 ++++++++++++------ src/emqx_shared_sub.erl | 174 ++++++++++++++++++++++++++++----- src/emqx_sm.erl | 4 +- test/emqx_broker_SUITE.erl | 2 +- test/emqx_mock_client.erl | 55 ++++++----- test/emqx_session_SUITE.erl | 2 +- test/emqx_shared_sub_SUITE.erl | 174 +++++++++++++++++++++++++-------- 10 files changed, 410 insertions(+), 127 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 05c7f074b..f3af119ce 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1904,6 +1904,15 @@ broker.session_locking_strategy = quorum ## - hash broker.shared_subscription_strategy = random +## Enable/disable shared dispatch acknowledgement for QoS1 and QoS2 messages +## This should allow messages to be dispatched to a different subscriber in +## the group in case the picked (based on shared_subscription_strategy) one # is offline +## +## Value: Enum +## - true +## - false +broker.shared_dispatch_ack_enabled = false + ## Enable batch clean for deleted routes. ## ## Value: Flag diff --git a/priv/emqx.schema b/priv/emqx.schema index 652b84657..fce0f5c76 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1719,6 +1719,12 @@ end}. ]}} ]}. +%% @doc Enable or disable shared dispatch acknowledgement for QoS1 and QoS2 messages +{mapping, "broker.shared_dispatch_ack_enabled", "emqx.shared_dispatch_ack_enabled", + [ {default, false}, + {datatype, {enum, [true, false]}} + ]}. + {mapping, "broker.route_batch_clean", "emqx.route_batch_clean", [ {default, on}, {datatype, flag} diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 6f430c5f2..e9c2fa0df 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -127,15 +127,13 @@ from_message(PacketId, #message{qos = QoS, flags = Flags, headers = Headers, variable = Publish, payload = Payload}. publish_props(Headers) -> - maps:filter(fun('Payload-Format-Indicator', _) -> true; - ('Response-Topic', _) -> true; - ('Correlation-Data', _) -> true; - ('User-Property', _) -> true; - ('Subscription-Identifier', _) -> true; - ('Content-Type', _) -> true; - ('Message-Expiry-Interval', _) -> true; - (_Key, _Val) -> false - end , Headers). + maps:with(['Payload-Format-Indicator', + 'Response-Topic', + 'Correlation-Data', + 'User-Property', + 'Subscription-Identifier', + 'Content-Type', + 'Message-Expiry-Interval'], Headers). %% @doc Message from Packet -spec(to_message(emqx_types:credentials(), emqx_mqtt_types:packet()) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 0d7352e66..068bcc6f0 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -257,19 +257,21 @@ 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()}). publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_0}) -> - %% Publish QoS0 message to broker directly + %% Publish QoS0 message directly emqx_broker:publish(Msg); - publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_1}) -> - %% Publish QoS1 message to broker directly + %% Publish QoS1 message directly emqx_broker:publish(Msg); - -publish(SPid, PacketId, Msg = #message{qos = ?QOS_2}) -> - %% Publish QoS2 message to session - gen_server:call(SPid, {publish, PacketId, Msg}, infinity). +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 -> emqx_broker:publish(Msg); + {error, Reason} -> {error, Reason} + end. -spec(puback(spid(), emqx_mqtt_types:packet_id()) -> ok). puback(SPid, PacketId) -> @@ -405,8 +407,9 @@ handle_call({discard, ByPid}, _From, State = #state{client_id = ClientId, conn_p ConnPid ! {shutdown, discard, {ClientId, ByPid}}, {stop, {shutdown, discard}, ok, State}; -%% PUBLISH: -handle_call({publish, PacketId, Msg = #message{qos = ?QOS_2, timestamp = Ts}}, _From, +%% 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{awaiting_rel = AwaitingRel}) -> reply(case is_awaiting_full(State) of false -> @@ -415,7 +418,7 @@ handle_call({publish, PacketId, Msg = #message{qos = ?QOS_2, timestamp = Ts}}, _ {{error, ?RC_PACKET_IDENTIFIER_IN_USE}, State}; false -> State1 = State#state{awaiting_rel = maps:put(PacketId, Ts, AwaitingRel)}, - {emqx_broker:publish(Msg), ensure_await_rel_timer(State1)} + {ok, ensure_await_rel_timer(State1)} end; true -> emqx_metrics:inc('messages/qos2/dropped'), @@ -575,22 +578,15 @@ handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> end, State, Msgs)}; %% Dispatch message -handle_info({dispatch, Topic, Msg = #message{headers = Headers}}, - State = #state{subscriptions = SubMap, - topic_alias_maximum = TopicAliasMaximum}) when is_record(Msg, message) -> - TopicAlias = maps:get('Topic-Alias', Headers, undefined), - if - TopicAlias =:= undefined orelse TopicAlias =< TopicAliasMaximum -> - noreply(case maps:find(Topic, SubMap) of - {ok, #{nl := Nl, qos := QoS, rap := Rap, subid := SubId}} -> - run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}, {subid, SubId}], Msg, State); - {ok, #{nl := Nl, qos := QoS, rap := Rap}} -> - run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}], Msg, State); - error -> - dispatch(emqx_message:unset_flag(dup, Msg), State) - end); +handle_info({dispatch, Topic, Msg = #message{}}, State) -> + case emqx_shared_sub:is_ack_required(Msg) andalso not has_connection(State) of true -> - noreply(State) + %% Require ack, but we do not have connection + %% negative ack the message so it can try the next subscriber in the group + ok = emqx_shared_sub:nack_no_connection(Msg), + noreply(State); + false -> + handle_dispatch(Topic, Msg, State) end; @@ -644,7 +640,6 @@ 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], State), {noreply, State}; - handle_info(Info, State) -> emqx_logger:error("[Session] unexpected info: ~p", [Info]), {noreply, State}. @@ -667,6 +662,27 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ +has_connection(#state{conn_pid = Pid}) -> is_pid(Pid) andalso is_process_alive(Pid). + +handle_dispatch(Topic, Msg = #message{headers = Headers}, + State = #state{subscriptions = SubMap, + topic_alias_maximum = TopicAliasMaximum + }) -> + TopicAlias = maps:get('Topic-Alias', Headers, undefined), + if + TopicAlias =:= undefined orelse TopicAlias =< TopicAliasMaximum -> + noreply(case maps:find(Topic, SubMap) of + {ok, #{nl := Nl, qos := QoS, rap := Rap, subid := SubId}} -> + run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}, {subid, SubId}], Msg, State); + {ok, #{nl := Nl, qos := QoS, rap := Rap}} -> + run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}], Msg, State); + error -> + dispatch(emqx_message:unset_flag(dup, Msg), State) + end); + true -> + noreply(State) + end. + suback(_From, undefined, _ReasonCodes) -> ignore; suback(From, PacketId, ReasonCodes) -> @@ -784,7 +800,12 @@ run_dispatch_steps([{nl, 1}|_Steps], #message{from = ClientId}, State = #state{c State; run_dispatch_steps([{nl, _}|Steps], Msg, State) -> run_dispatch_steps(Steps, Msg, State); -run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = #state{upgrade_qos = false}) -> +run_dispatch_steps([{qos, SubQoS}|Steps], Msg0 = #message{qos = PubQoS}, State = #state{upgrade_qos = false}) -> + %% Ack immediately if a shared dispatch QoS is downgraded to 0 + Msg = case SubQoS =:= ?QOS_0 of + true -> emqx_shared_sub:maybe_ack(Msg0); + false -> Msg0 + end, run_dispatch_steps(Steps, Msg#message{qos = min(SubQoS, PubQoS)}, State); run_dispatch_steps([{qos, SubQoS}|Steps], Msg = #message{qos = PubQoS}, State = #state{upgrade_qos = true}) -> run_dispatch_steps(Steps, Msg#message{qos = max(SubQoS, PubQoS)}, State); @@ -813,14 +834,16 @@ dispatch(Msg = #message{qos = QoS} = Msg, State = #state{next_pkt_id = PacketId, inflight = Inflight}) when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> case emqx_inflight:is_full(Inflight) of - true -> enqueue_msg(Msg, State); + true -> + enqueue_msg(Msg, State); false -> deliver(PacketId, Msg, State), await(PacketId, Msg, inc_stats(deliver, Msg, next_pkt_id(State))) end. enqueue_msg(Msg, State = #state{mqueue = Q}) -> - {_Dropped, NewQ} = emqx_mqueue:in(Msg, Q), + {Dropped, NewQ} = emqx_mqueue:in(Msg, Q), + Dropped =/= undefined andalso emqx_shared_sub:maybe_nack_dropped(Dropped), inc_stats(enqueue, Msg, State#state{mqueue = NewQ}). %%------------------------------------------------------------------------------ @@ -835,9 +858,19 @@ redeliver({PacketId, Msg = #message{qos = QoS}}, State) -> redeliver({pubrel, PacketId}, #state{conn_pid = ConnPid}) -> ConnPid ! {deliver, {pubrel, PacketId}}. -deliver(PacketId, Msg, #state{conn_pid = ConnPid, binding = local}) -> +deliver(PacketId, Msg, State) -> + %% 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. + do_deliver(PacketId, emqx_shared_sub:maybe_ack(Msg), State). + +do_deliver(PacketId, Msg, #state{conn_pid = ConnPid, binding = local}) -> ConnPid ! {deliver, {publish, PacketId, Msg}}; -deliver(PacketId, Msg, #state{conn_pid = ConnPid, binding = remote}) -> +do_deliver(PacketId, Msg, #state{conn_pid = ConnPid, binding = remote}) -> emqx_rpc:cast(node(ConnPid), erlang, send, [ConnPid, {deliver, {publish, PacketId, Msg}}]). %%------------------------------------------------------------------------------ diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index c6fef6d6f..ebe6d51f8 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -27,7 +27,10 @@ -export([start_link/0]). -export([subscribe/3, unsubscribe/3]). --export([dispatch/3]). +-export([dispatch/3, maybe_ack/1, maybe_nack_dropped/1, nack_no_connection/1, is_ack_required/1]). + +%% for testing +-export([subscribers/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, @@ -36,10 +39,17 @@ -define(SERVER, ?MODULE). -define(TAB, emqx_shared_subscription). -define(ALIVE_SUBS, emqx_alive_shared_subscribers). +-define(SHARED_SUB_QOS1_DISPATCH_TIMEOUT_SECONDS, 5). +-define(ack, shared_sub_ack). +-define(nack(Reason), {shared_sub_nack, Reason}). +-define(IS_LOCAL_PID(Pid), (is_pid(Pid) andalso node(Pid) =:= node())). +-define(no_ack, no_ack). -record(state, {pmon}). -record(emqx_shared_subscription, {group, topic, subpid}). +-include("emqx_mqtt.hrl"). + %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -62,10 +72,6 @@ mnesia(copy) -> start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). --spec(strategy() -> random | round_robin | sticky | hash). -strategy() -> - emqx_config:get_env(shared_subscription_strategy, round_robin). - subscribe(undefined, _Topic, _SubPid) -> ok; subscribe(Group, Topic, SubPid) when is_pid(SubPid) -> @@ -80,33 +86,147 @@ unsubscribe(Group, Topic, SubPid) when is_pid(SubPid) -> record(Group, Topic, SubPid) -> #emqx_shared_subscription{group = Group, topic = Topic, subpid = SubPid}. -dispatch(Group, Topic, Delivery = #delivery{message = Msg, results = Results}) -> +dispatch(Group, Topic, Delivery) -> + dispatch(Group, Topic, Delivery, _FailedSubs = []). + +dispatch(Group, Topic, Delivery = #delivery{message = Msg, results = Results}, FailedSubs) -> #message{from = ClientId} = Msg, - case pick(strategy(), ClientId, Group, Topic) of - false -> Delivery; - SubPid -> SubPid ! {dispatch, Topic, Msg}, - Delivery#delivery{results = [{dispatch, {Group, Topic}, 1} | Results]} + case pick(strategy(), ClientId, Group, Topic, FailedSubs) of + false -> + Delivery; + SubPid -> + case do_dispatch(SubPid, Topic, Msg) of + ok -> + Delivery#delivery{results = [{dispatch, {Group, Topic}, 1} | Results]}; + {error, _Reason} -> + %% failed to dispatch to this sub, try next + %% 'Reason' is discarded so far, meaning for QoS1/2 messages + %% if all subscribers are off line, the dispatch would faile + %% even if there are sessions not expired yet. + %% If required, we can make use of the 'no_connection' reason to perform + %% retry without requiring acks, so the messages can be delivered + %% to sessions of offline clients + dispatch(Group, Topic, Delivery, [SubPid | FailedSubs]) + end end. -pick(sticky, ClientId, Group, Topic) -> +-spec(strategy() -> random | round_robin | sticky | hash). +strategy() -> + emqx_config:get_env(shared_subscription_strategy, round_robin). + +-spec(ack_enabled() -> boolean()). +ack_enabled() -> + emqx_config:get_env(shared_dispatch_ack_enabled, false). + +do_dispatch(SubPid, Topic, Msg) when SubPid =:= self() -> + %% Deadlock otherwise + _ = erlang:send(SubPid, {dispatch, Topic, Msg}), + ok; +do_dispatch(SubPid, Topic, Msg) -> + dispatch_per_qos(SubPid, Topic, Msg). + +%% return either 'ok' (when everything is fine) or 'error' +dispatch_per_qos(SubPid, Topic, #message{qos = ?QOS_0} = Msg) -> + %% For QoS 0 message, send it as regular dispatch + _ = erlang:send(SubPid, {dispatch, Topic, Msg}), + ok; +dispatch_per_qos(SubPid, Topic, Msg) -> + case ack_enabled() of + true -> + dispatch_with_ack(SubPid, Topic, Msg); + false -> + _ = erlang:send(SubPid, {dispatch, Topic, Msg}), + ok + end. + +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})}), + Timeout = case Msg#message.qos of + ?QOS_1 -> timer:seconds(?SHARED_SUB_QOS1_DISPATCH_TIMEOUT_SECONDS); + ?QOS_2 -> infinity + end, + try + receive + {Ref, ?ack} -> + ok; + {Ref, ?nack(Reason)} -> + %% the receive session may nack this message when its queue is full + {error, Reason}; + {'DOWN', Ref, process, SubPid, Reason} -> + {error, Reason} + after + Timeout -> + {error, timeout} + end + after + _ = erlang:demonitor(Ref, [flush]) + end. + +with_ack_ref(Msg, SenderRef) -> + emqx_message:set_headers(#{shared_dispatch_ack => SenderRef}, Msg). + +without_ack_ref(Msg) -> + emqx_message:set_headers(#{shared_dispatch_ack => ?no_ack}, Msg). + +get_ack_ref(Msg) -> + emqx_message:get_header(shared_dispatch_ack, Msg, ?no_ack). + +-spec(is_ack_required(emqx_types:message()) -> boolean()). +is_ack_required(Msg) -> ?no_ack =/= get_ack_ref(Msg). + +%% @doc Negative ack dropped message due to message queue being full. +-spec(maybe_nack_dropped(emqx_types:message()) -> ok). +maybe_nack_dropped(Msg) -> + case get_ack_ref(Msg) of + ?no_ack -> ok; + {Sender, Ref} -> nack(Sender, Ref, drpped) + end. + +%% @doc Negative ack message due to connection down. +%% Assuming this function is always called when ack is required +%% i.e is_ack_required returned true. +-spec(nack_no_connection(emqx_types:message()) -> ok). +nack_no_connection(Msg) -> + {Sender, Ref} = get_ack_ref(Msg), + nack(Sender, Ref, no_connection). + +-spec(nack(pid(), reference(), dropped | no_connection) -> ok). +nack(Sender, Ref, Reason) -> + erlang:send(Sender, {Ref, ?nack(Reason)}), + ok. + +-spec(maybe_ack(emqx_types:message()) -> emqx_types:message()). +maybe_ack(Msg) -> + case get_ack_ref(Msg) of + ?no_ack -> + Msg; + {Sender, Ref} -> + erlang:send(Sender, {Ref, ?ack}), + without_ack_ref(Msg) + end. + +pick(sticky, ClientId, Group, Topic, FailedSubs) -> Sub0 = erlang:get({shared_sub_sticky, Group, Topic}), - case is_sub_alive(Sub0) of + case is_active_sub(Sub0, FailedSubs) of true -> %% the old subscriber is still alive %% keep using it for sticky strategy Sub0; false -> %% randomly pick one for the first message - Sub = do_pick(random, ClientId, Group, Topic), + Sub = do_pick(random, ClientId, Group, Topic, FailedSubs), %% stick to whatever pick result erlang:put({shared_sub_sticky, Group, Topic}, Sub), Sub end; -pick(Strategy, ClientId, Group, Topic) -> - do_pick(Strategy, ClientId, Group, Topic). +pick(Strategy, ClientId, Group, Topic, FailedSubs) -> + do_pick(Strategy, ClientId, Group, Topic, FailedSubs). -do_pick(Strategy, ClientId, Group, Topic) -> - case subscribers(Group, Topic) of +do_pick(Strategy, ClientId, Group, Topic, FailedSubs) -> + case subscribers(Group, Topic) -- FailedSubs of [] -> false; [Sub] -> Sub; All -> pick_subscriber(Group, Topic, Strategy, ClientId, All) @@ -153,7 +273,7 @@ handle_call(Req, _From, State) -> handle_cast({monitor, SubPid}, State= #state{pmon = PMon}) -> NewPmon = emqx_pmon:monitor(SubPid, PMon), - ets:insert(?ALIVE_SUBS, {SubPid}), + ok = maybe_insert_alive_tab(SubPid), {noreply, update_stats(State#state{pmon = NewPmon})}; handle_cast(Msg, State) -> emqx_logger:error("[SharedSub] unexpected cast: ~p", [Msg]), @@ -189,8 +309,12 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- +%% keep track of alive remote pids +maybe_insert_alive_tab(Pid) when ?IS_LOCAL_PID(Pid) -> ok; +maybe_insert_alive_tab(Pid) when is_pid(Pid) -> ets:insert(?ALIVE_SUBS, {Pid}), ok. + cleanup_down(SubPid) -> - ets:delete(?ALIVE_SUBS, SubPid), + ?IS_LOCAL_PID(SubPid) orelse ets:delete(?ALIVE_SUBS, SubPid), lists:foreach( fun(Record) -> mnesia:dirty_delete_object(?TAB, Record) @@ -199,7 +323,13 @@ cleanup_down(SubPid) -> update_stats(State) -> emqx_stats:setstat('subscriptions/shared/count', 'subscriptions/shared/max', ets:info(?TAB, size)), State. -%% erlang:is_process_alive/1 is expensive -%% and does not work with remote pids -is_sub_alive(Sub) -> [] =/= ets:lookup(?ALIVE_SUBS, Sub). +%% Return 'true' if the subscriber process is alive AND not in the failed list +is_active_sub(Pid, FailedSubs) -> + is_alive_sub(Pid) andalso not lists:member(Pid, FailedSubs). + +%% erlang:is_process_alive/1 does not work with remote pid. +is_alive_sub(Pid) when ?IS_LOCAL_PID(Pid) -> + erlang:is_process_alive(Pid); +is_alive_sub(Pid) -> + [] =/= ets:lookup(?ALIVE_SUBS, Pid). diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 8d0ac02e4..8f9a3e3cb 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -59,8 +59,8 @@ open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid end, emqx_sm_locker:trans(ClientId, CleanStart); -open_session(SessAttrs = #{clean_start := false, - client_id := ClientId}) -> +open_session(SessAttrs = #{clean_start := false, + client_id := ClientId}) -> ResumeStart = fun(_) -> case resume_session(ClientId, SessAttrs) of {ok, SPid} -> diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index e0a2555f1..3187aafa4 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -164,7 +164,7 @@ start_session(_) -> emqx_session:publish(SessPid, 3, Message2), emqx_session:unsubscribe(SessPid, [{<<"topic/session">>, []}]), %% emqx_mock_client:stop(ClientPid). - emqx_mock_client:close_session(ClientPid, SessPid). + emqx_mock_client:close_session(ClientPid). %%-------------------------------------------------------------------- %% Broker Group diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl index 24de66d0a..36d743ef9 100644 --- a/test/emqx_mock_client.erl +++ b/test/emqx_mock_client.erl @@ -16,55 +16,54 @@ -behaviour(gen_server). --export([start_link/1, open_session/3, close_session/2, stop/1, get_last_message/1]). +-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}). +-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) -> - gen_server:call(ClientPid, {start_session, ClientPid, ClientId, Zone}). + open_session(ClientPid, ClientId, Zone, _Attrs = #{}). -close_session(ClientPid, SessPid) -> - gen_server:call(ClientPid, {stop_session, SessPid}). +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). + gen_server:call(CPid, stop, infinity). get_last_message(Pid) -> - gen_server:call(Pid, get_last_message). + 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, Zone}, _From, State) -> - Attrs = #{ 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 - }, +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 + client_pid = ClientPid, + session_pid = SessPid }}; -handle_call({stop_session, SessPid}, _From, State) -> - emqx_sm:close_session(SessPid), - {stop, normal, ok, State}; +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) -> @@ -86,3 +85,15 @@ terminate(_Reason, _State) -> 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_session_SUITE.erl b/test/emqx_session_SUITE.erl index 5b6fc477c..2bae869b6 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -76,4 +76,4 @@ t_session_all(_) -> emqx_session:unsubscribe(SPid, [<<"topic">>]), timer:sleep(200), [] = emqx:subscriptions({SPid, <<"clientId">>}), - emqx_mock_client:close_session(ConnPid, SPid). + emqx_mock_client:close_session(ConnPid). diff --git a/test/emqx_shared_sub_SUITE.erl b/test/emqx_shared_sub_SUITE.erl index 8eb309001..dd0e20ad1 100644 --- a/test/emqx_shared_sub_SUITE.erl +++ b/test/emqx_shared_sub_SUITE.erl @@ -16,7 +16,14 @@ -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]). +-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 + ]). -include("emqx.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -24,7 +31,14 @@ -define(wait(For, Timeout), wait_for(?FUNCTION_NAME, ?LINE, fun() -> For end, Timeout)). -all() -> [t_random_basic, t_random, t_round_robin, t_sticky, t_hash, t_not_so_sticky]. +all() -> [t_random_basic, + t_random, + t_round_robin, + t_sticky, + t_hash, + t_not_so_sticky, + t_no_connection_nack + ]. init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), @@ -34,26 +48,91 @@ end_per_suite(_Config) -> emqx_ct_broker_helpers:run_teardown_steps(). t_random_basic(_) -> - application:set_env(?APPLICATION, shared_subscription_strategy, random), + 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">>}}]), %% wait for the subscription to show up - ?wait(ets:lookup(emqx_alive_shared_subscribers, SPid) =:= [{SPid}], 1000), - emqx_session:publish(SPid, 1, Message1), + ?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:puback(SPid, 2), - emqx_session:puback(SPid, 3, reasoncode), - emqx_session:pubrec(SPid, 4), - emqx_session:pubrec(SPid, 5, reasoncode), - emqx_session:pubrel(SPid, 6, reasoncode), - emqx_session:pubcomp(SPid, 7, reasoncode), - emqx_mock_client:close_session(ConnPid, SPid), + emqx_session:pubrec(SPid, PacketId, reasoncode), + emqx_session:pubcomp(SPid, PacketId, reasoncode), + emqx_mock_client:close_session(ConnPid), + ok. + +%% Start two subscribers share subscribe to "$share/g1/foo/bar" +%% Set 'sticky' dispatch strategy, send 1st message to find +%% out which member it picked, then close its connection +%% send the second message, the message should be 'nack'ed +%% by the sticky session and delivered to the 2nd session. +t_no_connection_nack(_) -> + ok = ensure_config(sticky), + Publisher = <<"publisher">>, + Subscriber1 = <<"Subscriber1">>, + Subscriber2 = <<"Subscriber2">>, + 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}}]), + %% 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, + 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), + %% 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, + %% Now kill the connection, expect all following messages to be delivered to the other subscriber. + 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(5), + _ = 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), + %% clean up + emqx_mock_client:close_session(PubConnPid), + emqx_sm:close_session(SPid1), + emqx_sm:close_session(SPid2), + emqx_mock_client:close_session(TheOtherConnPid), ok. t_random(_) -> @@ -66,11 +145,11 @@ t_sticky(_) -> test_two_messages(sticky). t_hash(_) -> - test_two_messages(hash). + test_two_messages(hash, false). %% if the original subscriber dies, change to another one alive t_not_so_sticky(_) -> - application:set_env(?APPLICATION, shared_subscription_strategy, sticky), + ok = ensure_config(sticky), ClientId1 = <<"ClientId1">>, ClientId2 = <<"ClientId2">>, {ok, ConnPid1} = emqx_mock_client:start_link(ClientId1), @@ -81,41 +160,45 @@ t_not_so_sticky(_) -> 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(ets:lookup(emqx_alive_shared_subscribers, SPid1) =:= [{SPid1}], 1000), + ?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, SPid1), - ?wait(ets:lookup(emqx_alive_shared_subscribers, SPid1) =:= [], 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(ets:lookup(emqx_alive_shared_subscribers, SPid2) =:= [{SPid2}], 1000), + ?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, SPid2), - ?wait(ets:tab2list(emqx_alive_shared_subscribers) =:= [], 1000), + emqx_mock_client:close_session(ConnPid2), + ?wait(not subscribed(<<"group1">>, <<"foo/#">>, SPid2), 1000), ok. test_two_messages(Strategy) -> - application:set_env(?APPLICATION, shared_subscription_strategy, Strategy), + test_two_messages(Strategy, _WithAck = true). + +test_two_messages(Strategy, WithAck) -> + ok = ensure_config(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), - 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">>}}]), - emqx_session:subscribe(SPid2, [{<<"foo/bar">>, #{qos => 0, share => <<"group1">>}}]), + 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(ets:lookup(emqx_alive_shared_subscribers, SPid1) =:= [{SPid1}] andalso - ets:lookup(emqx_alive_shared_subscribers, SPid2) =:= [{SPid2}], 1000), - emqx_session:publish(SPid1, 1, Message1), + ?wait(subscribed(<<"group1">>, Topic, SPid1) andalso + subscribed(<<"group1">>, Topic, SPid2), 1000), + emqx_broker:publish(Message1), Me = self(), WaitF = fun(ExpectedPayload) -> case last_message(ExpectedPayload, [ConnPid1, ConnPid2]) of @@ -128,8 +211,7 @@ test_two_messages(Strategy) -> end, ?wait(WaitF(<<"hello1">>), 2000), UsedSubPid1 = receive {subscriber, P1} -> P1 end, - %% publish both messages with SPid1 - emqx_session:publish(SPid1, 2, Message2), + emqx_broker:publish(Message2), ?wait(WaitF(<<"hello2">>), 2000), UsedSubPid2 = receive {subscriber, P2} -> P2 end, case Strategy of @@ -138,8 +220,8 @@ test_two_messages(Strategy) -> hash -> ?assert(UsedSubPid1 =:= UsedSubPid2); _ -> ok end, - emqx_mock_client:close_session(ConnPid1, SPid1), - emqx_mock_client:close_session(ConnPid2, SPid2), + emqx_mock_client:close_session(ConnPid1), + emqx_mock_client:close_session(ConnPid2), ok. last_message(_ExpectedPayload, []) -> <<"not yet?">>; @@ -153,6 +235,17 @@ last_message(ExpectedPayload, [Pid | Pids]) -> %% help functions %%------------------------------------------------------------------------------ +ensure_config(Strategy) -> + ensure_config(Strategy, _AckEnabled = true). + +ensure_config(Strategy, AckEnabled) -> + application:set_env(?APPLICATION, shared_subscription_strategy, Strategy), + application:set_env(?APPLICATION, shared_dispatch_ack_enabled, AckEnabled), + ok. + +subscribed(Group, Topic, Pid) -> + lists:member(Pid, emqx_shared_sub:subscribers(Group, Topic)). + wait_for(Fn, Ln, F, Timeout) -> {Pid, Mref} = erlang:spawn_monitor(fun() -> wait_loop(F, catch_call(F)) end), wait_for_down(Fn, Ln, Timeout, Pid, Mref, false). @@ -161,7 +254,9 @@ wait_for_down(Fn, Ln, Timeout, Pid, Mref, Kill) -> receive {'DOWN', Mref, process, Pid, normal} -> ok; - {'DOWN', Mref, process, Pid, {C, E, S}} -> + {'DOWN', Mref, process, Pid, {unexpected, Result}} -> + erlang:error({unexpected, Fn, Ln, Result}); + {'DOWN', Mref, process, Pid, {crashed, {C, E, S}}} -> erlang:raise(C, {Fn, Ln, E}, S) after Timeout -> @@ -176,23 +271,24 @@ wait_for_down(Fn, Ln, Timeout, Pid, Mref, Kill) -> end end. -wait_loop(_F, true) -> exit(normal); +wait_loop(_F, ok) -> exit(normal); wait_loop(F, LastRes) -> - Res = catch_call(F), receive stop -> erlang:exit(LastRes) after - 100 -> wait_loop(F, Res) + 100 -> + Res = catch_call(F), + wait_loop(F, Res) end. catch_call(F) -> try case F() of - true -> true; - Other -> erlang:error({unexpected, Other}) + true -> ok; + Other -> {unexpected, Other} end catch C : E : S -> - {C, E, S} + {crashed, {C, E, S}} end. From 14b803657688287a1b011875655552d315e06d18 Mon Sep 17 00:00:00 2001 From: tigercl Date: Wed, 21 Nov 2018 22:51:33 +0800 Subject: [PATCH 418/520] Use username replace id(issue#1737) (#1961) * Use username replace id(issue#1737) * Add test case for issue#1737 * Make with_connection/2 support batch connect --- etc/emqx.conf | 12 ++++ priv/emqx.schema | 6 ++ src/emqx_protocol.erl | 21 ++++++- test/emqx_protocol_SUITE.erl | 112 +++++++++++++++++++++++++++++------ 4 files changed, 131 insertions(+), 20 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index f3af119ce..1d6603091 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -649,6 +649,12 @@ zone.external.mqueue_store_qos0 = true ## Value: String ## zone.external.mountpoint = devicebound/ +## Whether use username replace client id +## +## Value: boolean +## Default: false +zone.external.use_username_as_clientid = false + ##-------------------------------------------------------------------- ## Internal Zone @@ -708,6 +714,12 @@ zone.internal.mqueue_store_qos0 = true ## Value: String ## zone.internal.mountpoint = cloudbound/ +## Whether use username replace client id +## +## Value: boolean +## Default: false +zone.internal.use_username_as_clientid = false + ##-------------------------------------------------------------------- ## Listeners ##-------------------------------------------------------------------- diff --git a/priv/emqx.schema b/priv/emqx.schema index fce0f5c76..56f393cd5 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -794,6 +794,12 @@ end}. {datatype, string} ]}. +%% @doc Use username replace client id +{mapping, "zone.$name.use_username_as_clientid", "emqx.zones", [ + {default, false}, + {datatype, {enum, [true, false]}} +]}. + {translation, "emqx.zones", fun(Conf) -> Mapping = fun("retain_available", Val) -> {mqtt_retain_available, Val}; diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 44b0925c3..f26445571 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -289,13 +289,17 @@ process_packet(?CONNECT_PACKET( client_id = ClientId, username = Username, password = Password} = Connect), PState) -> - emqx_logger:add_metadata_client_id(ClientId), + + NewClientId = maybe_use_username_as_clientid(ClientId, Username, PState), + + emqx_logger:add_metadata_client_id(NewClientId), + %% TODO: Mountpoint... %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) WillMsg = make_will_msg(Connect), PState1 = set_username(Username, - PState#pstate{client_id = ClientId, + PState#pstate{client_id = NewClientId, proto_ver = ProtoVer, proto_name = ProtoName, clean_start = CleanStart, @@ -607,6 +611,19 @@ send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = SendFun {error, Reason} end. +%%------------------------------------------------------------------------------ +%% Maybe use username replace client id + +maybe_use_username_as_clientid(ClientId, undefined, _PState) -> + ClientId; +maybe_use_username_as_clientid(ClientId, Username, #pstate{zone = Zone}) -> + case emqx_zone:get_env(Zone, use_username_as_clientid, false) of + true -> + Username; + false -> + ClientId + end. + %%------------------------------------------------------------------------------ %% Assign a clientid diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl index 3f6c4f252..ae308ea42 100644 --- a/test/emqx_protocol_SUITE.erl +++ b/test/emqx_protocol_SUITE.erl @@ -60,22 +60,45 @@ init_per_suite(Config) -> end_per_suite(_Config) -> emqx_ct_broker_helpers:run_teardown_steps(). -with_connection(DoFun) -> +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), + [binary, {packet, raw}, {active, false}], + 3000), + batch_connect([Sock | Socks], NumberOfConnections - 1). + +with_connection(DoFun, NumberOfConnections) -> + Socks = batch_connect(NumberOfConnections), try - DoFun(Sock) + DoFun(Socks) after - emqx_client_sock:close(Sock) + lists:foreach(fun(Sock) -> + emqx_client_sock:close(Sock) + end, Socks) end. +with_connection(DoFun) -> + with_connection(DoFun, 1). + + % {ok, Sock} = emqx_client_sock:connect({127, 0, 0, 1}, 1883, + % [binary, {packet, raw}, + % {active, false}], 3000), + % try + % DoFun(Sock) + % after + % emqx_client_sock:close(Sock) + % end. + connect_v4(_) -> - with_connection(fun(Sock) -> + 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) -> + with_connection(fun([Sock]) -> ConnectPacket = raw_send_serialize(?CONNECT_PACKET (#mqtt_packet_connect{ client_id = <<"mqttv4_client">>, @@ -94,7 +117,7 @@ connect_v4(_) -> connect_v5(_) -> - with_connection(fun(Sock) -> + with_connection(fun([Sock]) -> emqx_client_sock:send(Sock, raw_send_serialize( ?CONNECT_PACKET(#mqtt_packet_connect{ @@ -105,7 +128,7 @@ connect_v5(_) -> {ok, ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) end), - with_connection(fun(Sock) -> + with_connection(fun([Sock]) -> emqx_client_sock:send(Sock, raw_send_serialize( ?CONNECT_PACKET( @@ -117,7 +140,7 @@ connect_v5(_) -> {ok, ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) end), - with_connection(fun(Sock) -> + with_connection(fun([Sock]) -> emqx_client_sock:send(Sock, raw_send_serialize( ?CONNECT_PACKET( @@ -133,7 +156,7 @@ connect_v5(_) -> end), % test clean start - with_connection(fun(Sock) -> + with_connection(fun([Sock]) -> emqx_client_sock:send(Sock, raw_send_serialize( ?CONNECT_PACKET( @@ -153,7 +176,7 @@ connect_v5(_) -> timer:sleep(1000), - with_connection(fun(Sock) -> + with_connection(fun([Sock]) -> emqx_client_sock:send(Sock, raw_send_serialize( ?CONNECT_PACKET( @@ -169,7 +192,7 @@ connect_v5(_) -> end), % test will message publish and cancel - with_connection(fun(Sock) -> + with_connection(fun([Sock]) -> emqx_client_sock:send(Sock, raw_send_serialize( ?CONNECT_PACKET( @@ -250,6 +273,59 @@ connect_v5(_) -> emqx_client_sock:close(Sock2) 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) -> @@ -263,7 +339,7 @@ do_connect(Sock, ProtoVer) -> {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_parse(Data, ProtoVer). subscribe_v4(_) -> - with_connection(fun(Sock) -> + with_connection(fun([Sock]) -> do_connect(Sock, ?MQTT_PROTO_V4), SubPacket = raw_send_serialize( ?SUBSCRIBE_PACKET(15, @@ -279,7 +355,7 @@ subscribe_v4(_) -> ok. subscribe_v5(_) -> - with_connection(fun(Sock) -> + with_connection(fun([Sock]) -> do_connect(Sock, ?MQTT_PROTO_V5), SubPacket = raw_send_serialize(?SUBSCRIBE_PACKET(15, #{'Subscription-Identifier' => -1},[]), #{version => ?MQTT_PROTO_V5}), @@ -288,7 +364,7 @@ subscribe_v5(_) -> {ok, ?DISCONNECT_PACKET(?RC_TOPIC_FILTER_INVALID), _} = raw_recv_parse(DisConnData, ?MQTT_PROTO_V5) end), - with_connection(fun(Sock) -> + with_connection(fun([Sock]) -> do_connect(Sock, ?MQTT_PROTO_V5), SubPacket = raw_send_serialize( ?SUBSCRIBE_PACKET(0, #{}, [{<<"TopicQos0">>, @@ -301,7 +377,7 @@ subscribe_v5(_) -> {ok, ?DISCONNECT_PACKET(?RC_MALFORMED_PACKET), _} = raw_recv_parse(DisConnData, ?MQTT_PROTO_V5) end), - with_connection(fun(Sock) -> + with_connection(fun([Sock]) -> do_connect(Sock, ?MQTT_PROTO_V5), SubPacket = raw_send_serialize( ?SUBSCRIBE_PACKET(1, #{'Subscription-Identifier' => 0}, @@ -315,7 +391,7 @@ subscribe_v5(_) -> {ok, ?DISCONNECT_PACKET(?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED), _} = raw_recv_parse(DisConnData, ?MQTT_PROTO_V5) end), - with_connection(fun(Sock) -> + with_connection(fun([Sock]) -> do_connect(Sock, ?MQTT_PROTO_V5), SubPacket = raw_send_serialize( ?SUBSCRIBE_PACKET(1, #{'Subscription-Identifier' => 1}, From 551d5d99d22a861d38f7ea35c3bf71883700372e Mon Sep 17 00:00:00 2001 From: tigercl Date: Fri, 23 Nov 2018 11:00:30 +0800 Subject: [PATCH 419/520] Change 'aquire' to 'acquire' (#1976) * Change 'aquire' to 'acquire' --- Makefile | 2 +- rebar.config | 2 +- src/emqx_sm_locker.erl | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 07fe13cbb..6e01a0450 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.3.0 dep_esockd = git https://github.com/emqx/esockd v5.4.2 -dep_ekka = git https://github.com/emqx/ekka v0.5.0 +dep_ekka = git https://github.com/emqx/ekka v0.5.1 dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 dep_clique = git https://github.com/emqx/clique develop diff --git a/rebar.config b/rebar.config index 0af3f6b3e..fa2ab588f 100644 --- a/rebar.config +++ b/rebar.config @@ -6,7 +6,7 @@ %% appended to deps in rebar.config.script {github_emqx_deps, [{gen_rpc, "2.3.0"}, - {ekka, "v0.5.0"}, + {ekka, "v0.5.1"}, {clique, "develop"}, {esockd, "v5.4.2"}, {cuttlefish, "v2.1.0"} diff --git a/src/emqx_sm_locker.erl b/src/emqx_sm_locker.erl index d50d16ccc..29adf3342 100644 --- a/src/emqx_sm_locker.erl +++ b/src/emqx_sm_locker.erl @@ -43,11 +43,11 @@ trans(ClientId, Fun, Piggyback) -> -spec(lock(emqx_types:client_id()) -> ekka_locker:lock_result()). lock(ClientId) -> - ekka_locker:aquire(?MODULE, ClientId, strategy()). + ekka_locker:acquire(?MODULE, ClientId, strategy()). -spec(lock(emqx_types:client_id(), ekka_locker:piggyback()) -> ekka_locker:lock_result()). lock(ClientId, Piggyback) -> - ekka_locker:aquire(?MODULE, ClientId, strategy(), Piggyback). + ekka_locker:acquire(?MODULE, ClientId, strategy(), Piggyback). -spec(unlock(emqx_types:client_id()) -> {boolean(), [node()]}). unlock(ClientId) -> From 2bd0f16884b8ff2f0534260385cd696bde448498 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 23 Nov 2018 11:01:10 +0800 Subject: [PATCH 420/520] Add CLI for log tracer (#1973) --- src/emqx_tracer.erl | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index 3d060dee9..d92cb4a1f 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -20,12 +20,12 @@ -export([start_link/0]). -export([trace/2]). --export([start_trace/2, lookup_traces/0, stop_trace/1]). +-export([start_trace/3, lookup_traces/0, stop_trace/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {level, org_top_level, traces}). +-record(state, {traces}). -type(trace_who() :: {client_id | topic, binary()}). @@ -37,7 +37,9 @@ [{peername, [client_id,"@",peername," "], [client_id, " "]}], - []}, + [{peername, + [peername," "], + []}]}, msg,"\n"]}}). -spec(start_link() -> {ok, pid()} | ignore | {error, term()}). @@ -56,11 +58,11 @@ trace(publish, #message{from = From, topic = Topic, payload = Payload}) %%------------------------------------------------------------------------------ %% @doc Start to trace client_id or topic. --spec(start_trace(trace_who(), string()) -> ok | {error, term()}). -start_trace({client_id, ClientId}, LogFile) -> - start_trace({start_trace, {client_id, ClientId}, LogFile}); -start_trace({topic, Topic}, LogFile) -> - start_trace({start_trace, {topic, Topic}, LogFile}). +-spec(start_trace(trace_who(), logger:level(), string()) -> ok | {error, term()}). +start_trace({client_id, ClientId}, Level, LogFile) -> + start_trace({start_trace, {client_id, ClientId}, Level, LogFile}); +start_trace({topic, Topic}, Level, LogFile) -> + start_trace({start_trace, {topic, Topic}, Level, LogFile}). start_trace(Req) -> gen_server:call(?MODULE, Req, infinity). @@ -81,29 +83,26 @@ lookup_traces() -> %%------------------------------------------------------------------------------ init([]) -> - {ok, #state{level = emqx_config:get_env(trace_level, debug), - org_top_level = get_top_level(), - traces = #{}}}. + {ok, #state{traces = #{}}}. -handle_call({start_trace, Who, LogFile}, _From, State = #state{level = Level, traces = Traces}) -> +handle_call({start_trace, Who, Level, LogFile}, _From, State = #state{traces = Traces}) -> case logger:add_handler(handler_id(Who), logger_disk_log_h, #{level => Level, formatter => ?FORMAT, - filesync_repeat_interval => 1000, + 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 -> - set_top_level(all), % open the top logger level to 'all' emqx_logger:info("[Tracer] start trace for ~p", [Who]), - {reply, ok, State#state{traces = maps:put(Who, LogFile, Traces)}}; + {reply, ok, State#state{traces = maps:put(Who, {Level, LogFile}, Traces)}}; {error, Reason} -> emqx_logger:error("[Tracer] start trace for ~p failed, error: ~p", [Who, Reason]), {reply, {error, Reason}, State} end; -handle_call({stop_trace, Who}, _From, State = #state{org_top_level = OrgTopLevel, traces = Traces}) -> +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 @@ -112,7 +111,6 @@ handle_call({stop_trace, Who}, _From, State = #state{org_top_level = OrgTopLevel {error, Reason} -> emqx_logger:error("[Tracer] stop trace for ~p failed, error: ~p", [Who, Reason]) end, - set_top_level(OrgTopLevel), % reset the top logger level to original value {reply, ok, State#state{traces = maps:remove(Who, Traces)}}; error -> {reply, {error, not_found}, State} @@ -144,13 +142,6 @@ handler_id({topic, Topic}) -> handler_id({client_id, ClientId}) -> list_to_atom("clientid_" ++ binary_to_list(ClientId)). -get_top_level() -> - #{level := OrgTopLevel} = logger:get_primary_config(), - OrgTopLevel. - -set_top_level(Level) -> - logger:set_primary_config(level, Level). - filter_by_meta_key(#{meta:=Meta}=LogEvent, {MetaKey, MetaValue}) -> case maps:find(MetaKey, Meta) of {ok, MetaValue} -> LogEvent; From 6fa183f847451554d42935e13bf2388c2655a1a6 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 23 Nov 2018 11:04:33 +0800 Subject: [PATCH 421/520] Add CLI for log level (#1977) --- priv/emqx.schema | 17 +++++++++++++---- src/emqx_app.erl | 6 ++++++ src/emqx_logger.erl | 42 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index 56f393cd5..3838db31e 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -387,11 +387,16 @@ end}. {datatype, {enum, [off, file, console, both]}} ]}. -{mapping, "log.level", "kernel.logger_level", [ +{mapping, "log.level", "kernel.logger", [ {default, error}, {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency, all]}} ]}. +{mapping, "log.primary_level", "emqx.primary_log_level", [ + {default, error}, + {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency, all]}} +]}. + {mapping, "log.logger_sasl_compatible", "kernel.logger_sasl_compatible", [ {default, true}, {datatype, {enum, [true, false]}} @@ -427,9 +432,13 @@ end}. hidden ]}. +{translation, "emqx.primary_log_level", fun(Conf) -> + cuttlefish:conf_get("log.level", Conf) +end}. + {translation, "kernel.logger", fun(Conf) -> LogTo = cuttlefish:conf_get("log.to", Conf), - TopLogLevel = cuttlefish:conf_get("log.level", Conf), + LogLevel = cuttlefish:conf_get("log.level", Conf), Formatter = {emqx_logger_formatter, #{template => [time," [",level,"] ", @@ -452,7 +461,7 @@ end}. DefaultHandler = if LogTo =:= console orelse LogTo =:= both -> [{handler, default, logger_std_h, - #{level => TopLogLevel, + #{level => LogLevel, config => #{type => standard_io}, formatter => Formatter}}]; true -> @@ -463,7 +472,7 @@ end}. FileHandler = if LogTo =:= file orelse LogTo =:= both -> [{handler, file, logger_disk_log_h, - #{level => TopLogLevel, + #{level => LogLevel, config => FileConf(cuttlefish:conf_get("log.file", Conf)), formatter => Formatter, filesync_repeat_interval => no_repeat}}]; diff --git a/src/emqx_app.erl b/src/emqx_app.erl index 4a39e46aa..977eabc2f 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -25,6 +25,12 @@ %%-------------------------------------------------------------------- start(_Type, _Args) -> + %% We'd like to configure the primary logger level here, rather than set the + %% kernel config `logger_level` before starting the erlang vm. + %% This is because the latter approach an annoying debug msg will be printed out: + %% "[debug] got_unexpected_message {'EXIT',<0.1198.0>,normal}" + logger:set_primary_config(level, application:get_env(emqx, primary_log_level, error)), + print_banner(), ekka:start(), {ok, Sup} = emqx_sup:start_link(), diff --git a/src/emqx_logger.erl b/src/emqx_logger.erl index 2d850561c..79979097c 100644 --- a/src/emqx_logger.erl +++ b/src/emqx_logger.erl @@ -25,6 +25,9 @@ -export([add_metadata_peername/1, add_metadata_client_id/1]). -export([add_proc_metadata/1]). +-export([get_primary_log_level/0, set_primary_log_level/1]). +-export([get_log_handlers/0, get_log_handler/1, set_log_handler_level/2]). + debug(Msg) -> logger:debug(Msg). debug(Format, Args) -> @@ -73,4 +76,41 @@ add_proc_metadata(Meta) -> logger:set_process_metadata(Meta); OldMeta -> logger:set_process_metadata(maps:merge(OldMeta, Meta)) - end. \ No newline at end of file + end. + +get_primary_log_level() -> + #{level := Level} = logger:get_primary_config(), + Level. + +set_primary_log_level(Level) -> + logger:set_primary_config(level, Level). + +get_log_handlers() -> + lists:map(fun log_hanlder_info/1, logger:get_handler_config()). + +get_log_handler(HandlerId) -> + {ok, Conf} = logger:get_handler_config(HandlerId), + log_hanlder_info(Conf). + +set_log_handler_level(HandlerId, Level) -> + logger:set_handler_config(HandlerId, level, Level). + +%%======================== +%% Internal Functions +%%======================== +log_hanlder_info(#{id := Id, level := Level, module := logger_std_h, + config := #{type := Type}}) when Type =:= standard_io; + Type =:= standard_error -> + {Id, Level, console}; +log_hanlder_info(#{id := Id, level := Level, module := logger_std_h, + config := #{type := Type}}) -> + case Type of + {file, Filename} -> {Id, Level, Filename}; + {file, Filename, _Opts} -> {Id, Level, Filename}; + _ -> {Id, Level, unknown} + end; +log_hanlder_info(#{id := Id, level := Level, module := logger_disk_log_h, + config := #{file := Filename}}) -> + {Id, Level, Filename}; +log_hanlder_info(#{id := Id, level := Level, module := _OtherModule}) -> + {Id, Level, unknown}. \ No newline at end of file From 93a079752f96d689fb0938165f204eadc963540a Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 23 Nov 2018 11:04:55 +0800 Subject: [PATCH 422/520] Update cuttlefish to v2.1.1 (#1978) --- Makefile | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 6e01a0450..9a5aaab76 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ NO_AUTOPATCH = cuttlefish ERLC_OPTS += +debug_info -DAPPLICATION=emqx BUILD_DEPS = cuttlefish -dep_cuttlefish = git https://github.com/emqx/cuttlefish v2.1.0 +dep_cuttlefish = git https://github.com/emqx/cuttlefish v2.1.1 #TEST_DEPS = emqx_ct_helplers #dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers diff --git a/rebar.config b/rebar.config index fa2ab588f..424505d49 100644 --- a/rebar.config +++ b/rebar.config @@ -9,7 +9,7 @@ {ekka, "v0.5.1"}, {clique, "develop"}, {esockd, "v5.4.2"}, - {cuttlefish, "v2.1.0"} + {cuttlefish, "v2.1.1"} ]}. {edoc_opts, [{preprocess, true}]}. From 09025338d4d98b595dd532dfb9e208d53137e324 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 23 Nov 2018 18:10:59 +0800 Subject: [PATCH 423/520] Enable emqx.log by default (#1979) --- etc/emqx.conf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 1d6603091..1780725cc 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -333,16 +333,16 @@ rpc.socket_keepalive_count = 9 ## ## Value: off | file | console | both ## - off: disable logs entirely -## - file: write logs to file -## - console: write logs to standard I/O +## - file: write logs only to file +## - console: write logs only to standard I/O ## - both: write logs both to file and standard I/O -log.to = console +log.to = both ## The log severity level. ## ## Value: debug | info | notice | warning | error | critical | alert | emergency ## -## Note: Only the messages with severity level greater than or equal to +## Note: Only the messages with severity level higher than or equal to ## this level will be logged. ## ## Default: error @@ -378,8 +378,8 @@ log.rotation.count = 5 ## Format: log.$level.file = $filename, ## where "$level" can be one of: debug, info, notice, warning, ## error, critical, alert, emergency -## Note: Log files for a specific log level will contain all the logs -## that greater than or equal to that level +## Note: Log files for a specific log level will only contain all the logs +## that higher than or equal to that level ## #log.info.file = info.log #log.error.file = error.log From eeb0cab3e33ff3f35ab1296ba5cfa94e68089547 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 23 Nov 2018 18:11:46 +0800 Subject: [PATCH 424/520] Update proc meta-data for empty clientid (#1980) --- include/logger.hrl | 1 - src/emqx_connection.erl | 2 +- src/emqx_logger.erl | 22 ++++++++-------------- src/emqx_protocol.erl | 3 ++- src/emqx_session.erl | 2 +- src/emqx_ws_connection.erl | 2 +- 6 files changed, 13 insertions(+), 19 deletions(-) diff --git a/include/logger.hrl b/include/logger.hrl index bae279da2..db93d7f56 100644 --- a/include/logger.hrl +++ b/include/logger.hrl @@ -4,7 +4,6 @@ %%-------------------------------------------------------------------- -ifdef(LOG_HEADER). %% with header - -define(LOG(Level, Format, Args), begin (logger:log(Level,#{},#{report_cb => diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index ed8b90649..609ce25d6 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -153,7 +153,7 @@ init([Transport, RawSocket, Options]) -> ok = emqx_gc:init(GcPolicy), ok = emqx_misc:init_proc_mng_policy(Zone), - emqx_logger:add_metadata_peername(esockd_net:format(Peername)), + emqx_logger:set_metadata_peername(esockd_net:format(Peername)), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> diff --git a/src/emqx_logger.erl b/src/emqx_logger.erl index 79979097c..b3c9e9d54 100644 --- a/src/emqx_logger.erl +++ b/src/emqx_logger.erl @@ -22,8 +22,8 @@ -export([error/1, error/2, error/3]). -export([critical/1, critical/2, critical/3]). --export([add_metadata_peername/1, add_metadata_client_id/1]). --export([add_proc_metadata/1]). +-export([set_metadata_peername/1, set_metadata_client_id/1]). +-export([set_proc_metadata/1]). -export([get_primary_log_level/0, set_primary_log_level/1]). -export([get_log_handlers/0, get_log_handler/1, set_log_handler_level/2]). @@ -63,20 +63,14 @@ critical(Format, Args) -> critical(Metadata, Format, Args) when is_map(Metadata) -> logger:critical(Format, Args, Metadata). +set_metadata_client_id(ClientId) -> + set_proc_metadata(#{client_id => ClientId}). -add_metadata_client_id(ClientId) -> - add_proc_metadata(#{client_id => ClientId}). +set_metadata_peername(Peername) -> + set_proc_metadata(#{peername => Peername}). -add_metadata_peername(Peername) -> - add_proc_metadata(#{peername => Peername}). - -add_proc_metadata(Meta) -> - case logger:get_process_metadata() of - undefined -> - logger:set_process_metadata(Meta); - OldMeta -> - logger:set_process_metadata(maps:merge(OldMeta, Meta)) - end. +set_proc_metadata(Meta) -> + logger:update_process_metadata(Meta). get_primary_log_level() -> #{level := Level} = logger:get_primary_config(), diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index f26445571..573b913f7 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -292,7 +292,7 @@ process_packet(?CONNECT_PACKET( NewClientId = maybe_use_username_as_clientid(ClientId, Username, PState), - emqx_logger:add_metadata_client_id(NewClientId), + emqx_logger:set_metadata_client_id(NewClientId), %% TODO: Mountpoint... %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) @@ -315,6 +315,7 @@ process_packet(?CONNECT_PACKET( {ok, IsSuper} -> %% Maybe assign a clientId PState3 = maybe_assign_client_id(PState2#pstate{is_super = IsSuper}), + emqx_logger:set_metadata_client_id(PState3#pstate.client_id), %% Open session case try_open_session(PState3) of {ok, SPid, SP} -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 068bcc6f0..b223dfdda 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -343,7 +343,7 @@ init([Parent, #{zone := Zone, max_inflight := MaxInflight, topic_alias_maximum := TopicAliasMaximum, will_msg := WillMsg}]) -> - emqx_logger:add_proc_metadata(#{client_id => ClientId}), + emqx_logger:set_metadata_client_id(ClientId), process_flag(trap_exit, true), true = link(ConnPid), IdleTimout = get_env(Zone, idle_timeout, 30000), diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 759e127c1..77d2c058f 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -136,7 +136,7 @@ websocket_init(#state{request = Req, options = Options}) -> IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), lists:foreach(fun(Stat) -> put(Stat, 0) end, ?SOCK_STATS), - emqx_logger:add_metadata_peername(esockd_net:format(Peername)), + emqx_logger:set_metadata_peername(esockd_net:format(Peername)), {ok, #state{peername = Peername, sockname = Sockname, parser_state = ParserState, From 78fdb798f8f85067abc524cf9e73c9e381025ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Mon, 26 Nov 2018 13:51:32 +0800 Subject: [PATCH 425/520] Update vsn --- src/emqx.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx.app.src b/src/emqx.app.src index 77305f6e9..296ac18ee 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -1,6 +1,6 @@ {application,emqx, [{description,"EMQ X Broker"}, - {vsn,"3.0-rc.3"}, + {vsn,"3.0-rc.4"}, {modules,[]}, {registered,[emqx_sup]}, {applications,[kernel,stdlib,jsx,gproc,gen_rpc,esockd, From d13c5f8ec1919260b7a1783cae35a9f4a76e038a Mon Sep 17 00:00:00 2001 From: Gilbert Date: Mon, 26 Nov 2018 17:55:48 +0800 Subject: [PATCH 426/520] Reduce dep size (#1981) --- Makefile | 32 +++++++++++++++++++++++--------- README.md | 10 +++++++++- docs/.placeholder | 0 docs/MQTT-SN_spec_v1.2.pdf | Bin 428647 -> 0 bytes docs/README | 11 ----------- docs/mqtt-v3.1.1.pdf | Bin 1506688 -> 0 bytes docs/mqtt-v5.0.pdf | Bin 3215041 -> 0 bytes rebar.config.script | 24 ++++++++++++++++-------- 8 files changed, 48 insertions(+), 29 deletions(-) delete mode 100644 docs/.placeholder delete mode 100644 docs/MQTT-SN_spec_v1.2.pdf delete mode 100644 docs/README delete mode 100644 docs/mqtt-v3.1.1.pdf delete mode 100644 docs/mqtt-v5.0.pdf diff --git a/Makefile b/Makefile index 9a5aaab76..a04366033 100644 --- a/Makefile +++ b/Makefile @@ -6,20 +6,20 @@ PROJECT_VERSION = 3.0 DEPS = jsx gproc gen_rpc ekka esockd cowboy clique -dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 -dep_gproc = git https://github.com/uwiger/gproc 0.8.0 -dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.3.0 -dep_esockd = git https://github.com/emqx/esockd v5.4.2 -dep_ekka = git https://github.com/emqx/ekka v0.5.1 -dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 -dep_clique = git https://github.com/emqx/clique develop +dep_jsx = hex-emqx 2.9.0 +dep_gproc = hex-emqx 0.8.0 +dep_gen_rpc = git-emqx https://github.com/emqx/gen_rpc 2.3.0 +dep_esockd = git-emqx https://github.com/emqx/esockd v5.4.2 +dep_ekka = git-emqx https://github.com/emqx/ekka v0.5.1 +dep_cowboy = hex-emqx 2.4.0 +dep_clique = git-emqx https://github.com/emqx/clique develop NO_AUTOPATCH = cuttlefish ERLC_OPTS += +debug_info -DAPPLICATION=emqx BUILD_DEPS = cuttlefish -dep_cuttlefish = git https://github.com/emqx/cuttlefish v2.1.1 +dep_cuttlefish = git-emqx https://github.com/emqx/cuttlefish v2.1.1 #TEST_DEPS = emqx_ct_helplers #dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers @@ -47,6 +47,20 @@ PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets pu DIALYZER_DIRS := ebin/ DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wunmatched_returns +define dep_fetch_git-emqx + git clone -q --depth 1 -b $(call dep_commit,$(1)) -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)) > /dev/null 2>&1; \ + cd $(DEPS_DIR)/$(call dep_name,$(1)); +endef + +core_http_get-emqx = curl -Lf$(if $(filter-out 0,$(V)),,s)o $(call core_native_path,$1) $2 + +define dep_fetch_hex-emqx + mkdir -p $(ERLANG_MK_TMP)/hex $(DEPS_DIR)/$1; \ + $(call core_http_get-emqx,$(ERLANG_MK_TMP)/hex/$1.tar,\ + https://repo.hex.pm/tarballs/$1-$(strip $(word 2,$(dep_$1))).tar); \ + tar -xOf $(ERLANG_MK_TMP)/hex/$1.tar contents.tar.gz | tar -C $(DEPS_DIR)/$1 -xzf -; +endef + include erlang.mk clean:: gen-clean rebar-clean @@ -119,7 +133,7 @@ comma = , quote = \" curly_l = "{" curly_r = "}" -dep-versions = [$(foreach dep,$(DEPS) $(BUILD_DEPS),$(curly_l)$(dep),$(quote)$(word 3,$(dep_$(dep)))$(quote)$(curly_r)$(comma))[]] +dep-versions = [$(foreach dep,$(DEPS) $(BUILD_DEPS),$(curly_l)$(dep),$(quote)$(word $(words $(dep_$(dep))),$(dep_$(dep)))$(quote)$(curly_r)$(comma))[]] .PHONY: dep-vsn-check dep-vsn-check: diff --git a/README.md b/README.md index f8872e8a0..8d03df8dd 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,15 @@ You can reach the EMQ community and developers via the following channels: Please submit any bugs, issues, and feature requests to [emqx/emqx](https://github.com/emqx/emqx/issues). +## MQTT Specifications + +You can read the mqtt protocol via the following links: + +[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) ## License @@ -74,4 +83,3 @@ Licensed under the Apache License, Version 2.0 (the "License");you may not use t 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. - diff --git a/docs/.placeholder b/docs/.placeholder deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/MQTT-SN_spec_v1.2.pdf b/docs/MQTT-SN_spec_v1.2.pdf deleted file mode 100644 index ee6020d23bb4f9aba17a2be2216f3c6b8a024bc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 428647 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%0AJWMD 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

8J1T=Io6MJUs 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%0D>c>e1`78 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 diff --git a/docs/README b/docs/README deleted file mode 100644 index 94f96fb12..000000000 --- a/docs/README +++ /dev/null @@ -1,11 +0,0 @@ - -http://emqttd.io/docs/v2/ - -or - -http://docs.emqtt.com/ - -or - -http://emqttd-docs.rtfd.org - diff --git a/docs/mqtt-v3.1.1.pdf b/docs/mqtt-v3.1.1.pdf deleted file mode 100644 index e4095f1b5e9e4266832bd23cb2c078cb43867d67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1506688 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

jlk4h|xgEX= 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%0Dh(!sRJkwfq$AnM-IDB`71O zP*4hR5-QmKw(nL$DaMM3x_P-#krRNNlVxN8Q%4I8TM?#+Vt1qHosm*Mr8MtD$;T6U z+;rr$D62|EBOVLP;|ZD~n#tlpT0!piDYY49c|Cf=&*o*|@3WZTBRuj1^WkUMfoo?u z41<&7Fjr24y@&uPFz$T94Q7O2C3E$nTw(|OFo}Y*l@>je!9Ie{j8LMCH~wjKdQgIB zS35EWL>b_uW9QTzfmJIb7j0OGBj-*Y7qN;0=R;G>-~3ZOp$cLmxf_q^Cto_Gl3ISQ0s0M%qj0>16hRmo9zTL{LnJUJ zJkg?bm}1a5CSpy5;iT(PG>d_Bky2u5EYgHMuO{rP+_tIx+vGD^b{Z|K(+DF4d_qy6 zpLfcfd8&wyTe@XVEele(d06?1`FT^I%zER!B4m_xZ%O@-{F%h}Y%)P$---~QuVge3 zLI?soXY8-RO%0=XAjHhY-vP`eC$~2Vcp65s8f?>vNp+063}}$4RNZWqN9Qg8^5k2m z=F77)z%5W_KkD%33A?)ucNuzy7*+Dje z1<&2fl36C6uvXp7XP=TH3kc^8(ZL8p^D--St-M$CE*?Y957{#58s3@nVyddI&rGG9 zFBHEG%=30mYvIF_mC^B;TPX|jrUf1;$~8(#>`TbU?{l3firYCm@H80TwrMVQQ3o@f zd=c)=`P9)#MOBN!vuJ1L8}aU3^M}0{JkB<-Qf|rkLgD7^D;F2WEgIH{-%eL zGF}nv65{k&rg%NsZL4F3JOt;KUc0Sh#0oV=b1s6@%xYA15;V#+2}MMZWgzYpbt}3boNw*4a!a0_7o=8n zVG+vS*@|fV3;-e1c~fWR5qu6({=vp728gL{e7{4XL%+U30Rc6he^YbRTG|D+NExUe z7Sv;CY9$`Hibkh_P#Qd}z5DvJmdwgL;F3EPc;CYTAMBhVRdD}H*(N{5CXZ&x4A}2z zHlW4x0I0w^a|TR(PVKhh0$hDO&x?o8F^Cc)U#nyj-fBGFZrpU`wuROBMk20g;Rgk) z1u{PH*)s*j$cZsXQO3#m&|%Y*074nYBA@-n+2_)Q&(zVsV;-^XA>}{l@-^}1*TloJ zQkFIKv9?dzC7$4!tB`6N;C^0VRZ~59bHz#ri^YlyqSIMIztQ{p$(XXIzuS#v&57Ta zDT-qqm#g?6M;zu26)Lys6v($NrhP;l%$L0EO7>3ao!)V^91)hk?)DKqL?o0Lsua_r z*D;O;Fb)d=mK9(ElcboZAre84PPkeMBKBemsVDQ6``!soDPY_w ze!rVTE*b8!xPh-YG5x4t-J!=@zW5RKAkomQfyJz&)q|pY#YKb%n0;}vpuzkMF7Iar z-1De{18kB&uYsLIZ?&LuKwF^RkY4XKpO|UhHo_>Rj^GB2*QV{Msa)ENW(X3fF;ZtH zo2PxolD*w4B71^Gs?;pb@)YddA5ZHQ_^t8EJzBXtt8%*`$FqP~AA(6|-m?+uz2dV! z^+X#{@w}Q(p1MVqr3wOj;RqO~l-T1*E~I~cXK_C;Fm%{!jZ$cv{8BTl-SOSD^#$p; zsu)FcMjpT%1bwyH(XGZ+#J!c4s%5pHq#l4L&$O?i3{w@qVWkCpPJx;`PcXlGg;@F+ zCR!>kn`c4R=4HW7nvQFRtHBmxa^=u5W+k^Y9K9%tn5DQxYv1;1WgDeauQZALjl~19 zPuFt~fh&_=y%Z~Q$Sl$p{vNb=4Cri@V5vwF>w63*J`44&#-~V!Hj;o7U^27`C`jR= zSC8moGv?iWlma`c=u)*B4VahY+L_~TrhwG&G^zr!x)9qNEPVA%= zUoId8{jO65eEd_YZ2&3T#gOV#3rg=78er+DG8YPT|%QtW57v(I>Xr6dTqUORcvf52w5XEhrK96B4fZ zcfML=Pa*~;{||sERS>B2#k*F7F)KErcjy6;H;Rm#d>I0-WLNpZm%Pso(JZCQkp_;EaUSLn zQry*i%+~hOD;>ik&NdBWGy34@*!9Qza_4vU?z7v*AJnq z6$dgA=hgcX-$bl_;FFux}D z67c1Y{n%E;l*)EWIQv?((=@!S%%0xrXK1YpM2KE*Q2I}!%*4{(d^?>h&5MqlpGqZ| z+t;{tu8JJSXiI??$$<&QJX(<6t%}IXi6WQE0`;?HzFMo+Rj6(+8L>5DK7lR=7O8`h zFJJ*Oh|KPs?5*i-p+n-UDi!G0DCux1<)4s_D28vx$Xt3wNe?C}%dRwC0nnJ>+A7e= z2!JR;EF_=kb&pmv9{WnLY44HhiVjK%W+Io_o;QW!y_++Ud#Mr}9J($ziQdY(d%1bD zyD&Y8=7SEP%&cu4Jz{f@2~RL4oX%9z=?k~6j_h16yI~29Rem8_TIoW#jY=c=D@Hlh zgJBn$%`P!#JkVaRFEn~X&X4;K)z6M?-RlYW+|QYfYD-5P!O13#u=LD6wx9@DGbVJaJwHY)pcBLi2EiN(93B6$+f-dxRXk_2u#$fx<$3DY(uer*z~L0Y+hC_ zE>8Y1?7#xBHU<8ic7A{L6~FA*#(1->+gyi3^x{5Sq@Y~m+QG-96{aR`WhfsM-fYJG zbr*r?D58+)j~!mpAerv|a$PaWcGe|*Sls)4Cf$CCenYQ9c#gH9w^0)xtI>+~yqf#^ zqWnqZut?>oQxzYlQVVG_^CMvKA*_sO-Xnd@tJ@DpsAk*Ex5$RjZ>4 z))N|%!!SJ$+t$lO*Yo9?LX1Bn$(xGF^+!0`-p<~~s(t9)W@C5#=S!bP4KvGD1L|ZR zN-j%fO|?wsPhj0)ZsaH8Z7p@?V4`cjVE0_$UF|Ry^lgqeQEm0a0<4D_%q(|Tcba?2 z3Af`lgH(UUqTh@kh*f=(uf597cE?K5)A4!A6NfBUEj6d$;O}PuI4>)>qIoK@JvWw#O%-Ukyy}ysK0` zy|*0k+n-zCp=%8U#KK`sp+ov$p@1)cuDfoR_gFJ#nb~n?}J<6Wpj+fBE z`GlyQaL(`S4XxXU3SKmJ-)&uo)}_%#Yk~JD<|u9b-}6)4oj0oazhoep}QuXTsKyLAc^Y>LWf=- zgo5*p&eQfQ;jYQ$nttXAg4IQ;Ho5B4R;0Zz+l$;J0Hfl|9`C+cibF(;h$WbrG@J0- zM1B0z)scbZ2IbAt+>0eS!RGWcb1L;Zkr4ZpvqV4Bcx5$Y(^K;|YB(a>=~K8&U@~|r z4e}RWmh|pO(05_f5D1xQ(o7`Q!IZXe9#Mx8yB!&`T?Y5)>iW!^v>=InW}V{$(3bHe zCx?;o^`q^kU@B}pYqRld#AGw8s7bP<0j(gFAE_{{;=~FT(wF2F8*DNuY2Q%szf{g^ zl0*=0%zeGp9=gn(8i*yX;wFvuxkvE978OhV&C@&iAKOV^4vx zakMX&l6}+@@Vsk!HtWb8gWG|+LeQXrYD${2sYa4+5k>B)MY2n#S6&L)j(3QN9bq9g zlQE88>-T!>X>;rMwl?MgLGffAM7>j&Ss>-f_2?K_*q;;_3+*{-uE8p2v6CoD`I?d74dNKjRC6fIynKl-g56<`bbbsW^GvVng7a=yQ6|m{ z(9P}8Vbv4?{^+Y}YA&dA=HOqeY=v%PF;V3Omv8R3y??`}{sG6v#c@6t7kQ|^R-Byn z9X+3oiF{|44+y8xF5vv`Np70nP=%{wFjMXMJEgdbNZCi?%n~mVkE;jsnBfo998OD7~3Z#SySIwF0J->$`JUWLL z*I_E?{>)%$^iX3xYK5h48n;D|oiTT_7%9)~eb!hQPu-z@6T;KCu`B*SGXwTsxR z?K4iR5lW&YpZF*2BnoKngIm5UaOqIvJcy?h4WxUj-&2mU?XPJL;*J9rz44gqSrGr* zQ$)tEGr@nI-~F@T@$aaobaej@_Ef;e+QG(B&*5Jkoc{y&l#%w2#>&6to-+QKd-_NH z=3h_y{#-@)&w>AKBjtZ!g#S6xTjxl{fm40MVb94qLz-1mhI2A zl>e1c`+vEoOn>g8WMrWK>Y`*~!DaqJ4}V?t>m>9a{hnV3m;V<{`$y;JU+?+P;gi2V z;bdWC`y-z4=laF<*tJIc4wZxr-xjg+^c(p;SiV~O9dfgZUD8Hx%3(H?S{mUt0<+OF zH8*F%ez&~eMy(hC5Wir4<)!TPl`X4PPn$aWteY}+?R1ClT>AK2-R%s+3f;dy_UGYH zhk@~Q#3qn#Y_4~zBxGEKJw9HVRg4yrrM8ymQwNxh2qOxI&kX4|wfJ1FZ(eLtVDkh9 z<6Jx}UM?hg5Wd=W>gaecX(HKRbGEKu+VFgQY~AZ*F~e$?Z|c}AzMigDoZd{IzCL2V z0h1;+ScWfNUTh)=fqWtettW~j^B2pP?%a#~!6C!B-1KmGxSsstka=9c@O-_3M*J1# z^Ti=Ea(voKtGiXyFhyv3t2LjPE9It8Ied{U7d+&-{?=M*$=*_4C!L5DL1*6PTj@+8 z@L92ai_Ossi}aRs@(&Q%iBSgRbOQcKJ!_RNIfugU4o?SXk0f{L)T5lRR1%LED>7mF z&TnEHJ*3>;t!f!iLIMTbR`P=8T7DBkf?w=cyg)zMd~7o4E^G-NE-KzX`ank~p?P zpWNF0Wh@3SaqcRLCq7!(Rg^cLx^OzKq4Fa(w{(7unGiL%$$CThTcSA=L~k@7OEEWNxnkZ_4 zmU1sxRcAyY-01P6!g^%0qgydxox6IHMG^PPr|1p0&SATDc;hP0nY4|oMk55#1dyiv z5;Yxz&3dOT=z;Sc^mP{`3Qyr@EHoBELqje7#x+Z>`w zZ_S7VDo`_3+3g}XIk$=ay4q7j@pg{v^SbA{ToKx!@un)-MTVzG);3ezJnExGs9fSm zwgVP-ln-6JtC-CiwX@TtG=eqSN-Y(4vCFy&euvi>>Ab-#k11s-U}7nN8(oC0B;&|f zZ0#)z;Z1B%I%`ncR(Yl_^3Bh7S>M%Il*g>`@d`N7q?%;r2qP?|UP)UWCq~K(VEq$Z zF~I9rJ?47S`$X9k`Ufz{$=F_7;F8})gEfDYfbDZ_$1~@{CDDbtZ+~MmO=sXxxMU*)9l%Qb0C`C@lR5v6Ud&X%y1fhcDnV(8^di3;8 zz%fZs;sv>#-C-5ZfM!qQ5|BR(A>b8DzC{qM@B37^ES@&8Y-vpM$Qc7l z8hIIcfqAYGt$%k_i#xjy&{M@N7epoQ_tircu{XBH0PS2eL|1Ec$_gqTCaj3GDkmcy zk#ww14i+kU{JkJ}9R1YAVX5om^)hPlhVk~}<2qc!BujyVIqCOtCByF1Fq(|)o2g#3}+*i~=8+Q+Th zS00qFypN6+B&hpsMi$c3`4JvFsPTFf$TqW)2=*0jM^Mb?B5us?XuTTAiN1L~?3ptO zT6x&GwL`3ghIZv$lOe4ET1sFweY+C7W4c4K zY6q?8n<_>P8VkSM4(x{uEg~NOb-*?tkJgV29LJoWrF^B_t1dTSrSk(_>!ZfLdn(g$ z3*w`B#nLQodxh-UjRl1eZ7i8Yjm;;T?F8+^PIe zjTx#Q;T+%R?*KS^nqdAR&OmPiZ62x?uhm`)Q@!dkgk5CB#FZ4Dzp1~1!wO7K7|?Ii z_uSY|*IGRWS5i|3lnFXVpY4{dL1X&Aqzq2bwnO+>pX#Amg(5hAD&Rp^Vi72(g|bGS z3o185)yrb@t29$!4dtcA1!L~<$uw^U zQj#0i#yE64w;E2#U|aVP?>S#izoJf~lND$E6?5`v0*+%uWd_Rl;GT@$A?>jU)-l}g z?y5cvMX1Noh-%XLc8)jz%f>Z~C#{zAd>aK2eZDfIHlW_~N1+6W3T~LIV!;}Y68nI9 z28)=6R93jKo~`c(C9}C1sLo6YgE1Mrt~xaEB8=iV+p92cfoi(zXyvpu7!TKt@1DRY ze9OoaA_G!eG8GfkS9&5cz)S?z_=>?CvuOAmDki9Wk4j8x_!HeQg6M4Ke#tb7Q6Z0z zy|%3oZ~!AAK|op=%ye3bNZ(M6ZVWlW_lJIJnX~1&DyubU{|+_H!mRg{L;-qcDX(|p z`u!4?FgxYq0L4WaQ`{mws+=>qA!DD$m325vagTu*8arddE39*edMR&j$CfRr)Epu< zX@o8|TIUodk&-Z+yc-=`eUDQ%Rk-@9fpG?#REkiQst^?dmq5r+RsYLc4jWGlUoNp` zE01lO9rM78rVL~$9t+X4MVJEE5h6JOY*a|f`xTe>`$f?&0pKU5oi42|agXxlFmS4IR+D{*0s4z%sp}5Sf7T-W+PnuRZ?eW8W z8+1S;izK#&n<}xm4qI~1LqLO57fH1`_1s>`=hxsA{F4szba+UG-%nXVd=_D{Sv01&)2uQ$} ziVL~%$SRH}URK+Uh;SV08hMMtxxy=__DWEJfIy#B>@fdG7ae1>GC^wVnz-r%@Md!D zX20;3b?d>>b^XV*5gCJ~cT<%_2Qr#7ylSGUu!!dh&FC@` zh2MI(1pR3=NKpa5XeOp~yWc0?iW_?pmE&12RIh_(uAtV=E};6Hn>xJlsruXe7@hqU zr$gVvh^V&$%*oNcc6vWQyUgw1Bow=PdHM=bML_qxA8n%vrT;b zWHw{)VST)wQH`fQp4-S^uNECfho4%)4`p<=IyW<{gTSVvja@se*;5-+&??RQmUB9$ zqQH2xetY`)dj_;kM0=>Ui%ZCJ$Z>gq@MGpbl-fMr3=^bZ4lWx59VkMA#2~ z9tZHqMg~?LP`JloO4+qb?X;eF);spd!3c`@>yvAW_={gNV8zL7g#n4_wg+O!&F%m} zeEKSxia-;!x0KGr`i86}Po+(oQ<1JPL__^n$CuuA@lSp9J{!sPM&N_<{MF8juzU5G#ggY0^#sgGBRrOy8}#U19wJ4{nkQ(gTN_QWO`<;67w0X z#r(2S_l0JL>P2wQ#CZ%C!d>@D?a)9BJjm8J5;X_(;$>#xeLes^yGr`cA3<7}FxM{z_ z-l0~~-wj9MH*xFULnAe5(xryO%HIIScO-{0p0Q=(o5a-)NEgzH#N-nEQb-aC`ck40 zP2JT{7ibUd`JQ5*IFq#PrS|K{V-zT*X z-5gS~#a&VB;@1ws?Mw}~zmH^sK}*w4r*f>4nef3QY!NVaG+&Ekv3%4EA+MQFm?EM| zd4RXhr%#j8qT?Bmzqd9=Q~2liE*yYw!RoBf6J>A+_7+Fng>Or)iBOh2CxcQTZ8#DJb}>}jkpl4{@TFJ*eQ&38=nmWF z?Rg}8bNXgY6gw`roWA^3Q1B|y^L||aNWq2qJ%?fN-7mlS?f0rx*TZ2X zcknWb2OP>c!Acr&_D6d1H-D-85U>o}qzb#VjJ)f?V@J*&Qb>>fB!Zf=)| zNTzt{sSX2CYed5I0k@`dIkv-$>Td;i5x3TCuQwAM1>X=3wFIsL>!?Si$6F2(>v&{D zqGAKM6IafwR4y2wO|lkpFT;Tuv^Sq^7r@jEvMMHv)sI!itT{|R*;-jiI(T=jGv-k@ zk{VqFiBQkdXse~q3yog4*Guq(U|)xEX7|?3f=MiZ$-yX}G`wMC4fx6PS=Fy@=V6wk zRHuG8!tVe8R=WZ|gvyDD8a2~yU)-+Ba(oE(KVjOm9DEn94bf*L66AlT1euHifd~mH zO@(ts=Hyto)xTT+Y7y8$m&`2xk(7H>Q|1ZvbPNwyfUuDOW#kLRfAO zBi8Noz}#_BToZenroZ_i$&fCA=rTxS(>ZPFxT2kCrUJW7|FgT|-FTqu;N2-ahQ6@lqas^QNi0crjy2@2+Z5iz`BcU9W4m;F# z#{)+VPO!2am5c8b?mZw*{_mDv8`K~vZu>iRugo-?uvg=1&KWf1+-*8Z!h(u7Vbn_z z9qngK`8VHKvh+pl|)zNxuLj;88dxvapVc%iA*UV&wOxlXCouDi8dX)-QvoSRnH zw)ej~d%vEBZwYwPteZ)S?=2{|I|?JukEz8ERaC$;u+Wi`l_>JcIX$Ecg^sO=&%A;P z&ktydO?4vWB#V5Js1F(<*69QAMB(VKjJsw&q%*V0=~jU|15K|ME{lV8tZQZnN{LL0 zYdiv*$tn%Z86>aM0)c46Lx-TRMbs;<4Z=@z^)axI`54INUdhU)V%Hlg zz9sm<2qf1*4F~xkt*hevVx#AyA?%E_s9@b=ZrebLm01{)pQB6REEM%7Sj3;>C7vKR zLAdj)h)qI`iv&%U8HH-ZUtdRot8K*Pz+EC2g$UP>WDDdRhmMT2Hs_2&h#Nk*1JQ8% zVj*LqF0bW?d37&b-4D?suNJ3&TEE9oD>~YZCu(!(yUac=ZcC+i6Afg&y$3XF9a-Ew ze*f{3-RQaxXz&PP<<7tg0crhO>*eBx)q%IAXgh&I$j*vG_6%^ij+tIZ6pTHg(9Ga_ z8x}PR`Dz@&R&9KWz?DOaf@KMRBo@X)u3WT~WIPDk;dsw3)QQU2W^PIDG+By;al&}F zfHk>-2~-ytnmgUxzblFv-eK9D+w~eFz+ief7b(|` zjJTs=No%A=w|s^6W^@W(P)^&3N|%^~q9G9yDB-&>paMn5=vuWb>)lio6Rg!J_#-^A zH0Hi}b^T^`AQ3DVSeZTNYLFLIgX9P=LV>-YjO|Uo43x{8iczhRcnSz!+sM(c?L*if zrZ{SV!>0>(7>=QG4i0!t4<2S-o2tvAk#QQ%oI;ZV?ALqsCR9F>f-TU`p6#>YhN4b< zM=&Oy$xIXSSpphc+rp5lSVeROq3+@X!LcOal_>^X2i_6QE23EDJ7_T=p}YX8bQ=@U zQ9t2|>wF!-*kY_kSkfnYanH6~ZqZoFJ}c8(Jp9=@lkonFp>BWk`$Iv`oecR%Zxh@2 z*VfguS4pTq*XORE73F>7?%>eh(FocIgR2(;6r*5ZpV&teFQ&ti$2~YZ9BUAoT^N+l z1Lc4;nnGOIOod^tsh80%Cl)ZxL)cFqvSVuLXthJ(6iRLtSqWx)bk7Y1i7&U7*}15S z33Z{H!aX@PYJ+L=6wqEd2gd96JvMHM5Jiq#6xjVL4sWIHT3~9GJSWo7$Pw5Y>Trsq zf;fG+JY(~Yn_p6ULhHcNtJwL=ASV&TGp_F~v2uz8k`VJf+hxo@_oN?N9T*WoeiQ!m zmVB-~LtO&qc$g31d#uY9&FD81AfGl|JMhBPFe25j3?rhEG=ze}lkMG;VC0u}y=zc zb}x&G&Y~A(IFp8>)9|Yv$~7mr?{VBjx#Qwo(5vSlDa2b0!7`d9=>-6o4H8}pt~ z8MaVS4I%p;6AoA{QDm;gmHvY72p%Af#zx30-An*tlHtHokB}#dW8T0R)mUUrLb&`C zk~PA?O{@V0N&&zbL(E&9i8qKw6=^|!zauXZ4sRVu5Im-w#Q?F#RmV3CGaA%Z7KH~I_5ax6dSWc~H3jukOx5-y~|IgrclSa-N4v3A!_{_2! zM9Yl4M>q({)MjuY^h(9><7l=w6AOH<9ME%DU*MtBuXJVwP*-$oa+`EOg|FqQ{IP7Lj*!9?udsl`bwg759+ho!k@ zhiDP$j=GQs0ZOK57|lC6iovlRRiX!$>2;}0J+XwsO?J07TYOvFDd7>W14zPdDEgLO zWw!hQBzc@UV>g(YZO$1rWO_GPN=b2FMh0^!r?1$1{dJMKsn>P?ssEP~va5AuHzkUzR!9vOutdpoZT=MhG>CNV{+p>4rh{ zQVdT&uSTNOC6_dilf*jmC-=dL&&EZgVRkIoZ?L5J<4AgiuN%lLkm81ejAN@-fGxTa zkB8I~_n3(^b`KywXeo!MAAj@`G( zy%(gYcrDBBTvVuJEv)j=pSg!x+d~TZZ~6<78tI%Yei{R=S3(CZc7W2H$2S8je4(+s z#LK~+Ko3pP<*V)#?jT2+tzygD%{iIc6>X7z|6GjbEU*D8uB==X5l#Q-%J{&zPXx~Pw| zs~m(L9bTr)HusO4Q9_XjM*gZ4j}#K+l=qY9rZG%n#|>AsaP>mhXyJ*ChCuHdYTA z0Z@`8ZE7GLA&_w8xppDs(d_0Myf{-4zT*Tf0!w&uT)A24z`jLoAbL75q4buW7;+i_ zs^y^!vm190ei~-0a{P3*K)*}t4#y;)Ct*Zx#2c@%NZsrpM3Ck!I~Zt0A3 zf5Xwn6DRo_)%<@b;-hE&0}4q0)xF2S@&yI_qlxc}KS<5|x8a*V8twiSeEA{^{+sL# z(_dxb|6LOw9qS*^PC7bP>aR|_|Cs4-<2V24)%%~O_zSd?k@nA3eE$^W|JFf3|1SrD zqM4PE0|7m)w2{l#S9u#NJ?sC1^ZXyJ1b_Gg{>DncK>uea!9Q|Je*t|m(*C);@1KMJ zJ14>44*K6)3Rv0b{$wejWBwEA@=N>wm!*J_p5rR?&41%j z`Cl%I=KueW*#2bu_%h1;c}L8D+0lQseMlPV8Jby}e6d0Qb4Rp)*%9+!cEt8&0r|_0 z{(#c{C%^UY?H)|@41d&v{>$!Bp|x(m#$5Y{-6M%XgM$&k@0(ge9J$6dF?-rlZN=O| zq6-c2&62Xv!cbnQ%dO}km5d88E@12vT)}!R!wh2^M&#uw==I^wve5Oq%cM6O$7hc2 zrjO3=r|ojrhh2_t*X!etyMTm@voM(=68RDN68?E2gxal0awHn`;Zb44;PvC-!Orwd zL>eF8;4GY|MVuxT5yjUx*EMag4KLCS9*-Ix>_?rC_p_VHw)ML3^P3(1aV zPA<0xmMPVRTq*L#0PW;s^2xUMq!+}Y#)v3C)h;H&EMAB* zoWxBwJ^lXiX4c~h^S)Wm-EG62=VWeLZGc8&w14{6nQf^6nR!^IOO+3z^WY?k+-jHq zz%~%mdW_j#B!jrM=mN?#XR-Vf4u(i}Qn+Ln$S0;DE$!p5$wF>ixCX3W!++aj7bfUE z?fK1jM(rmiKR^8tl-_(vY;T61cR%y`4~(*__r`bCI}(8Dx+{%R&4~*$1f-R;~s8-jN`a{$VR0qg2UVf}_&Xpo@weZoiFMpw$-}4%N}v?285X!Zt5B>N5D> z(WEssQ}D#d9pK1zNV74B)mGDhi<>al&R8%Ks04bxbiP6`yUo5`r6s^!@erV}VT_WB zv)my)zZ$aN9s~@=2JTN5QBI}WU)-ov4lijB-w_RO{jfa!W_$`JB1 z%|{$Js@-?qc<`KQ7Slk@ITUMrDkgIDdF{OFU~4^Vpep!}B(+>0?+Ujv1R9Sk-Qg8# zEz8gpHmg>@Z1Jsc7Vmq)Ag8uZu`og?dXa!iSpvF(3qrR!i6IBQ)v+DSzXU09gt72c zoSI)Wjq>){;o<0v@!@wPz~q==qG8n(@wa_5MIuynzm=puJ3eA^7l6`?MmTs57MnyA z#brdP4*(QO?Xll@UOyS_w8S0rdup22#4PQcskYD^30r=1T%aEjrh+pvPz{Z9<;5Qu zC??n=2ccko0Ea|H=)M%;EN_F=i{*jqYlq(5MVJ9z*~9FKp6&TIL`HZ)L>Xx9TQ zDNR*$Tr9;-(5}10ZzGe4fgsf~fx#`EKDo84pBD711@IO~?lnL#q_*(T*YG^~mvAmg zKE=)tyzIVVE=E0b@Z&IOO$ZMD$`e-@14%X`L&3h}P|*ms<*V3zXhpZUEke?r(Nlxj zE%PBtf=TU9d$>J=pm}oIPS7fCg*r3#lb`#FgHuZzJ%OG&nwTV&Cco@*garE6blvz& zMHCgKmRtOLOHYXn=jf%K90T2v1rhIugALOmk0I46`>^nf$gFGd(GY2c{WhbI!K0e! zgQK+kqKb>PCq$OFzvZ&b>OY5MO>AWDYED80td8Qcp}hQrsxHmA#*>o9>PSbsnsVLb z7kTYS&Oq2?SZbNe&oQxX{!M!Y$7RRe?f@B|np2c$XI_w+V>{ zZRx*3OQ14*J@f(~Z!9K1DEPxEnIrsjjr^BNdr*F>g&9j;P%_&Tx)dJFp3M<62-o7@ z0qMptOy70Iob^3JyHhusJ*8mK()_URlerRN4!tQcg5PH-91;_)6^ow?5ZJ4a(sY!> zfTIh-hc^hb-;X;QH!#`X(a@3a0bNqbh#yG*}wK8oP4Ux?ylHg(Lzf zEVvm-NQIOb{bnycK!sD>Bg9{Lc2XR_B0l)RZUuybb|_3(pAyoQoIyq5Ho@8?e%5M_ zNQHhl(m23YH$6A?T!yo`qdl2)^(^%(SwhNT0=xua*38&E4$WYVyavLe^jObet>(5x zqb6~6-lL}0{{STK=9!Fx604@xj8NZ*Ava$}h!Bn}%}!)U39D-9h?!V_OptJ(&yv?< zBGx!zNphc5U7ne!_?TFsk($QL5+l7;%RJM;RVj-h>!7ePS&pf!PrJ0akdaBG!$iTM zM6Gvh?U!2)s?|$OY40xOG#WkuLKa>ac`eKW{WMx`{MR18YnIyw5k;g_cO_@zZtg`` zl=U6p$k%&nQds!z5*4-iu=+Gvo1N0bSjFU-B=LD4sAg$tz`IQ8pc!F|c|+HN8Q>m7pYQv@75bXe< zojY_Q1FXLVEOC>uF4_<4^^~1{uEQ<88qjVLI`?F!f-7zJa9=^NPY1X7=za^D%G6*m z5&E5IUsRCSxIK;KA-M%_pRMVoy?e~wLOx-Q%cMOUlOCzQ-cuFTk1msxlEqzqY$zvE zclOlh8+CMyAaJEnqqV!8D^>8>I$-uBb4B6V*gOR`zjp%J`HGb}$3E2e9PA{(6C=lH zS0S_MIDj+d9W79@<8I}(Y+B~>;D2vx`wW#XLqlMA!zxE#;9iwWy&wzKF5!crxjc0fegI~D%^ zi15Ab)FIdYkA9ld$gMK&hdY$gSHqletQVa(tIa%(+cy#@i1l!vpIhzc zPE;uCG!CBHBog)`Va)|rb$CKx$11~*6y%k6`G-YjPpwD6-Uofpo~mywGPJk6mh(C- zeRypYd#FGucHJC78(P`u%b*=GW#c*FG*TGEdN}u1+&P)IQ3RL7COvwa?5GOrZna&r z$meaum9hqzEdcGz#nOGpiT?_2)P&MYj~6kC#r}}X#E(ZYM%4cMR`f#Ie2~k`|i!S^Zb+$ zjwDt7FR%Lj^XlGM(P@|mym-*{(9iDOsl}IEU@ic+H`5Z^R;Zq;SBaAg{0Qua)*-mA z=tm)*!qKB^BY&rm ze~u$=%t*8f>l*)-v(uD8lNwE?01Lyvu;7>lORuRd(QLm|F=1J3TQ#72*HQFf$Fk#f28(QQz;s~m$s>s2zu$|t;Y+_fF;91txZv^Rwift-s2X-`<|&_O`awbBKS<%GB=l8+`n zNJr<*^W&muN8jWA^+Gk>2cS^X=l2HINAP=6xVUg+v6>{kIAUeYxG}=~>c@7=C_1wG9v{GRnP>HYAN90{8z>#tLQ__;Vxg^1a(!U zn3pmNpLLTUs{apZZyBA(a;ytmve05?W@bi<$zo<^W@ct)X117_87yX&EGCQTd%csL z?3^Uuxp&Av)mvszRQv>@(Domv_xuXQ%q)3a@Q(L&@{T zc=wP!W1XK_=OHr=S0Y8+5i!{rG5y2G!SCK12(iY8XM473b7{a+>-dxSpkGq5dVAGc zI-4U^Ux9r(TL&MwC!)!J3fE7hjkUoWB`0#ufzwqbyV*%@o~3OTx633{r_nccN7HdH`~4 z?A$_``Q`xMyO0WXX&b_)^{xA&mbLlQSBI6Qx2aIQun8T}<`**f&kf%`iyPg8Dq`Y_ zu7c@?fmE)upF$)Uy)y>oSzEu?^vxlTMPU>W&MGC*V!g?<8U76Pb$%=20g9rn{Wi(z zNnsh3(k{^*An>5Gmq=jawUVn1Nxt7zsh#q(K6X?iqjkW@+vuC8gGr+<+ ze}S__-e2Mp}#k zgPHeDas5eCuSG9-foY9n9`x4|$n6E#%{|T8t3|ZnX5aRF6Zd*(-OC{$s2y0F(j|-! zvC_5@9?%>s4)(q)$rBp7=Eo&FkE$h^L1d3Fx!`Jcc-Wi~={ux%vP9YTGu*!%@zKiD zl13j>-9(Ij;o8Ao8g@)Ar*4r%2XTe_uXP;p!+n>7>9U6*Ll+f;upHDek707%WJBYQ zu3rxBD%xZcm=R2c74r^M;EzU_!)s=jiQ(jb`i6ckDJZ4aSL91~p}u#o?aFLQ+6(6f5=mYNZ$l86M}C zI{^tP;&JUE>`JkQh%v`F_c=}w1Od{ZB!!=AwqB&eh-0bTC2TFfGJQtrM?r$HEYUe2 zSAocg*n=6`0f-}R>WN9sL4o4ZW6|o z8QC5u&tkkXu%U8~V~tj*+!YtRXkKTq+wA1{4N0ZpW0K=}s2sAjA=2Z^G(w$gG)0}& zJR@wYCIOUZ#YnuxK7w0S01B0s9;C&vM4@DgqEQwT<`FduHXgH)d?l)L_iMZl81Yv2 zlM7$*{SToz*1FJS%nHu(n^0D}hr5m0vtOHQQLpa$yh81s3FU)9F=r4@G}`-(&lAt$mf;$=;w|#`VCN$s$9y1-?%zpsgGc-lUtn3WRcrQ4JX>ld&_>R zEgEU6rrJ!e%d$777~`ziibU=FB)(9xXKsYFfjK7KG*)c(Br0w@2p;G@@NslQ^p7Hg z)x=7B^gAsjBuld8n6Y$3cK78=;h8j!2|LxD+^pFZs}Wn6imMef8Xd!R9*%=Zw}(}P^hPODcnLZXrF!RcHG8QBHpAiy<|GT(C=70K`zy2Av=VWhYWdApK;Q!pw z%<%soTuA>HNPy|LivJ1;{BiZ4p4WeP$N!V3`9Es;zqp#2{t{sb0A>CP2Qd7_)yzu& zmk2{PX68R!$0|l+~|I^d_w*W)de{nA}FaiP$0rK%#0om4A8U8K65D6&p^+_^!vX5!vz0;)-(Xu{85rg3;&Nf_J>{mcIr2?ZxxTT3G=zzF`X6!zCg{yzq){%sikml?^y@ehECk)Gu*6OxUM;h*>v zJI7xpBm}n9ltxa^UL9(ijH0a6``?-$T;u^ zWL2d$jG{jPML-mnhmelV9ekLyzdjzdm&@$XL<0}m0^kVM)yE!^zx z%hq@oyx5wr-s{*MU2UzM-K(3t>l)bw5eK{5bfKZ4ntn z?3y)olVEAH>CjRRHk!|yo@F&(J2f-;K~M&>$RPXtU&yqRE6`Et32szKB$XFP%9T5a zQOq`GU42oBR(^2TCFi47GxS*do_LV_T0+6LK}qB6xk4c#5hBz)ibMQCh!~4X1ueM} zzPtNe=lO`h6%)QqT)88Mx{Ci}?iDl9)3>6<<5j&1VlJhe&ISoe-T{OK4TOSFb&O;z zur8R8RFJy%3mRq4UirdkS>VD$yAPXfp|a3NnsbF*ic??!Q&`Ky`0yf2b=*Gca}%&v zTT!=)(gOl)b-7Z-tt{WGZ>A$J+Sbm`vBMYPkL2weuSR_l8>c=raPKO@imlS_QhGim zTPNcQ-ax3L${g)82?*7yW4$H(;$8Q^?_0`48mc~Wn>{wXRL)*`vq6I(cBn(Ibm1tW z@g15J*18%HwcqtsRfn{f1&a!_ixrYOd4B+RyEiBmsL-_Qmd;?A_V`88Uga3SJU6~P zb>v$r4px&-S;2hNtdOtxtn9_Ob48>E*e^rh5F1tr6*e=sbty8Gcd)L?A%wtJKU8jv z_VNmuopDRR!%Q=2$R&otqRK4nl_<{I1tVA#jIz{5EwAvVWm-pXB}2{Pk1C!PLY#U_ zY}c93+ir+#@ljj=ojU2;U+tc}7n8F+R2+#WS*6fMU+vlQLhP?nB6}ew-1!{iU`ev>M z(K!#Mm%6+;k*7*1cj<4teimh`VnEF1vqo`If+@<58X`+gkZ+@_ z-jChPR1;V~d>Svq?us_jA|7r`2i0E8Ek)a*kXAX znAIsZsXeN}2Nyq0e`hIudZO9ui4)lQt9ne&&@>^NCA83F?`zweBvE{})7RZ-W_KrV z2m7ZqV*Kdgua#t8Vq1wVPASmuGgl5PE5UEA8hLabA;Ux>-X2O z=WkJ0BKPUd#K%+h#1Y$BYJ&De)Ah#mE{;y7F0fK>N)RuUH*~Cdhh8}{ItfmC#hUE> z2pf)MOVyIIZD0tGXI$ZJsPvI;tDq2_N*Ac~Wwv3+^}l6qIz{9a;{(=qnd z8#e2k4NZE5R<1aXx?&PFYF4}A&ONm`LYA;s=uxB?ybT+$kck}LZki}K1M0a^z5|Q@ zxv>)OFH_)MSL}hPdhQGPukceCCSj?z6d;7RTxIYlwO!3Kms6X{b)F@iZi;2zU%n}_ zDDp1I9FYj1f9=2(H(ad}>C1G+Y$vlNDOAYW_y%5fd9;wWzgX7V2I|htQzGG~?jvtx z#dTHQ>v}#9?u082a&k`^b*)|wcamxNxwUw8T+E~@NYZJ6yr)*R$5AF|9xHoq(YddR zQkG|{x(UZnh*XE0}g%ZfNRV$Yz7lb*=CHL?G;&w6U%_N62X8-$3>CWM6NxE;XU}d z_rfb4lf$A~8N|%kcK%yq?Vu+gDQa$=3Nm-A>2PCuTO7W07tt zLl-ET8?JH2lkEZfOcNX#b8Sp2HT)2p4rKd%gF>gwsbPz0U-qp;HKiJ*(a0A#qZ~)= z*nsS$VB;As5HWc<$_tWEUu=8AS|bd(QkSRmW=74k?Mt&=3uNp~Zjpf7SI|EizQGhN z3GAH=UZ84%y5*&J);06u80PHEtz<5iw1$HUjAYB?DXZOtRSOqpqmlT#dLT;j&7S)Y z0iSMf8ObQIN2f2j=SoWYB1PAwHR+myh|>8eTxETuCk znU=Ba(}+}-`eji*qdeei<3R5p>B_B{$E?mKx>UoYd>(B$`@B{YD~XFKt@fZcOxxLX zc@lP54LlsS#l0YJ3eg;dm?LXpwpjNv9EVuzU~dZdIOa)M8CqO{_L?U!QOtew^&*AN zg+hclgc5T}ejd14vGIk|PQDbY&l9AC8(g+q3I zrb+COH&Ry>PR$V?pXOHRHBKT{BhD($OwlSI9b*U&!VljQLi&Q;}UjXH#>JdwPr8 z>t?VycrW+S)AbgRYQq_Q(&sGq@axrQ1O4sYtHZrHGNMo>-1pPtgd7>(cW)n`*WsN> zA3m>}sGU!muPtFP1lejc*Rdyj%T-S9;7o<_V0fFa_oriM2+$qlF=Jv(yLqa5hsuysfPjQG1*i z%<4BM)SkG@Lx)F0@oXXyF*()IB~q$NRqkjkq!O@0M~I{$UuaV1X2d9sc|$3Rj7)bv zkKEqb)%N-ITs;iy&`pr3BD_p{YQZ?=Q#yz-z}TB7fWNsb_7lhbDk-R)Zu_;g?PN0< zQonbsf8dLE5hZh1*Mlvx*aVk= zNsSeuG8dRu#cE@!gKp5LnKO~&Q-DD?`Fxu2aL9(I>s(>#q$kQM7I*X7O<9G@Zjq1_ zQx6G6asI%fULT3oq~nKk&iIB>*ql@8*ZmQwZJ<)MqSw*{#GGbe>&);`U$3QA#rNU5 zpKm(2K;bQf*ClAybLO*q)XbWx#0s85ir$bWOFh@aDrxWrm5H$5R>M_ZCz#s6Km{$m z3y;=lmkGm-b0i?=M3_y3`?$w7c3g-KUaJ_CVd1xI;~~$Ufej}YZX|xfG?Ws+$U!{q zwOZ<5HXVraVqfc17H2yvqBaE1 zfhVPm+dw1x{*D)7A&5Ikx`Zi;vBfr|I8=LgR-Qgc3=M`IDTyBC6?WS{M~0JNb*GSF zK0J`mQRUq8S-8@w)VY%GOPA_2B2rA`m7pGNCszt`f1#^gHO_QuQ}wu%`2y&Y(l}#J z<#`tuJm@rW6)eKs^LtLQpN*JK>bj}_o9E3J-VR12uUp{tyUDog$QXtd)gKX%Fbm=n zkK=ZpQ`uG_XjOg0c*dK>KnX*wu3v*H3YgV;kY$&y5>_nhcL*kfnc6h@5w6--EPa*{ zPcKGN3tG7NNq|1Mx@*lOmif?*9%_8(dO_Ie_g6v1Zuj1zUdT6Ua%inTn48*Lst!Ss zbVLN2@4f2VdGwph&*9=OVm7Q~_gm}XjMu@9YoV#G%p6}$)zpZz0R#N397{TjT0$ro zNctMruXW@ks6*e}aP3++{OQ{G&<>|t@9rcYyI)Ea5^QLrE?llEKC2FRxA8IMcnA4( za{B8~J)FW_VQt^un?{rk&c0$*G#(6KCM33{WFxwM=x723Q zUX2!*?$0Mgbspc^#7^UWG8N$F;#kL;<=yVeB_p>|=7?tf4wBPpPtntUdMrdWCHaE( z#gQhRh0cnUeC5cMDGj<1FIC_MLo#Ec1S!)l;p@J1(4b_-lu1H9P8~M`BsqBM%4x*% zI8sLg84n&yDa90u@#kZ(5At&{{#%Oai>xWhHCwrWMaBb3NtIPyFwZHl7!pnGcNj|S z*z2g336Ue6&KuU5&c{u#1C{Sx49B~JSZhI?9Zeev++eX+g$+7+1-h@bS~PO2iJ4K` zLkMUOblX%H2^BFu-e!TXJ1~~ZZ2QDTJYzvXrC4$@;OsL;6fwosZ<|8ZKcE5~C=ULb0DLk&cS|gG@Sc+OZ z_y&)LSRA@sm1}55H0?ekHN5_Crn1^`%RK=vV?fI~pQC5yay59>*#D(uEC9&7{71I& zO#1$@qKkzk{0=QM-%y8NBuXn=J?C+9u2&(Haou_ip)q2rGF};Xxgwm)f^%a6!G&SuVtZ^EJD(5j;@i-6x~HG-%%S%6Ln`hdv|Nov{C$| zX_Ml(wUiy@E9r1pJG~A{1ytV7u~A`Y)L9!*O@%zOKj(CAejpu17H#sH>d~O)R0of& zfdRP5FAr#qBib=tg%QLa1%|bJKc8-U88xy{(FCFn=c^sXRuDb3V;aofDW~j){2!&p z8tC+r3L`T{iPrQx4QfH)%qfltl^HZ8N6+%y`XQ(KseL%Ohihk7ZkKu@6qZjon-{W0 zKlfbt0s>!Lw~HbcLTy>Tr5b|o{*?D+sMYRaib|Cvy81ClXpnfd-XU17AIET_7srrd zXvG29QEV@`no6~^90E9sSj}yvK$=s50)KW>X*J+c$HumE?0Yf}+jw+w{aGY?h1}mK zlQUJ_-%&I<(14vBg(YVD8G@auOvDVWR=i&@JW%VcaM%*hd$8fslfcWPr2#J4@E4Vs zQ1#)^2&I?qASuu5AsikJ9NyVA8HT{c7joTsj~sHVDn}@pk~88aI!&IHPcl1)ZSFCj zLs3f5=IY?!Ef5dJMFMGzW%bJ1+%nbuEXo(TFDjH0$WsBafEo$`uBmPO_P9VpFQ+QGqNAz!FjxteBypZ_t4`W+ z#{}|p#(VAMQZb-GrFKvmn|jt2V}jv}ASs2+v%sjJ%EI>)qNAKD`G7u4esEkkFswtr z3ui;8VelILf@Gz@#Zw5D{6*OuR54^6W6GUmkFJwNATFRuA~M(lc@Y1F$zM~zkV86P zomx#(TZwYxmt7FgS!&HAlxl(r91>nLRzFC_#>l;k0X4Q4q8#m9t|#|%`*q9;puYPD z_9t(#2eHG1J6Lu%RgRsp$Z&_TYdRKng@D3D76!lI1A9vC_*=#sHfc8>(>D@{>W^Wj9_IV8yQtqTDg(D`&}?OqHnv^aWn|GKV{JJ#0|mi>Wx zA%>-BHz*CG8pZW&@*1oK4QWC3p~y&PCF}&=K`mW6;vXu22%+7gj)p6=0HoS+3qL_| zSLby@ojmB-A+l-uQVkxf?8kk|q*5WS2-$*qCz@Pp4be?%aYL~Wp_lr>P}9wCk=yMI z`JV{7K`Ig>8XQhGw?Bz-h%lGEy>~UP?BOG{n>tSo6LYw=su_z(NOB3sgbilVY z23nnwM1`%pTCMplzhiMs`Yehy^^okoa8Yo{kB8mXI1d~P05sKmFkz97KvKQ5eV??S zL9!vriu5w*v)SocBMH6z9?@8PztcziOPD4)^_-7fE$fO2S2LnggQJ;s+;e5r!Ga{& zW0T&dgTN$uO4mLsu5AOWD+u@K;Dt}uW)qiz7{XLW6^_CX-eZa}$0sesEV=g6f|P|My_xCJ8R$J5O`s1e<>r!51y^L2N~+`U69q|2+5DHJO|5j03n z8b)Fa-xecW$iwQTyFnL+1>3VJK7@`2hUJ!}Di}wIPUqzjZJI+F<#@RpOk9N~GlNA$ z0vQAp8lSPdh?nmus}q(^Rb4NOlX~P-EnwdQFR^uUG?ZnaSi#}05MfSiY}1k*!W*bj zmXE>QEUQTonl*qO-6No>_k$4Rxl|JPvz+N(95e*9qee@zL!^XLGFfoDqq6iN{8A41 zIe1QE0&o5OtK5BT98UvPjGVmn<@hf#^YUfk;`s1&+?XsQgaAvH5c*a-`BQ7VB})p2 zQe9Xs=cq?#OO;e4&!#f&vnFiLM12me4LU3~0Ep&?ck;=rWNZxWK#^36vks^rHGE8|Zt>JjbLdQXj^ zUt=h-U%^sqJkmlB#IxeF(B@44Y01`H^kNs>k*4q(A#-!;#aoTunZ$!|D@zmxmr5Y zy}^p@wHjWX=srwBK11>uv}HdS%&Tf@kPG3f-=8Q5wvzDt`oHLA6w0&6`rEOiU-aNG%6 zvhK_pooE)_6>9brCT!1Rh6=ja+3JL#gF-zuZGr+{dRIbF8X%0x&D#3a=L?a$m=w53>3u+e+my8LV$@B?@d~#A{j3DQ}5dp@nnZS`SPff-x z`6^jYLxMD$Pyyen2xR;-K$}chKD(w7a$a{_kYN|Wmz}mL@}6SAaW3M+w3unGCaj*|CV^ap=#S!Fy+*5;UajzWSE}GD$1NewD}S>C2XihgQ;OrRA}~%8ndH_Xm%U9{Pot&Bl1E$95GvJq%bmPUX9alf)hIZgd{2nVTVhS_tdK;&EU{NpO`E6ol zxrnO?#x6LxX@}9ss^9f$>O{AIK7}4o8mMRe4!nKdg(Wt}uvxriLJbl*s;cpl)v7l+ z$>)>98qAp-u7>|+yR(M>czh$sUM-g#WQH%z7@u$J>6??;C8lqj&pGp_6?S8s9#h9E zw&vp}mgBAI^~UG6UoLOFe52moF;UbOkAnp+gh)+~>AwI%*J-1kOWds_-t7EC=JTwO zzNDd?s)v_asFaqKOK~^UgUAij+C+okcIlwawE*L-1X^ot@(~F2?&78eN^5QEmoZ%j zbryuSIR-J-O$``u4F{r$d5Ys{7*WnhRW!)C(4HR53tta<&WIWltB!e)=6y^4L0v&E zr&2A;g=Cp2v#X5WY=(cs9zVdLDCED<6gbifH3**NM{FngEpA(Y^oJBioEB+(lty5Z zp)f83LV7Cc>TZI#Un4Lt!d%WJD!n3X))m^&ntqK}%CX zHz6y65vIBdnL(C9BVU&q<4*^V@F$Yv?y!>z0U9c0d*x*@LiKm4tcL?f_`{SaF-E8q zr}+cPE#8_>6vp|fkO)7!u>!iTT|9p3@?BY*8PcJ9)@HHiBMag)V>TPe5B0r>fkIREGIW6*nx@*MM0}DM)xg z(phx4YhdZ!Z&seh|Ej`qOJqqmsE=3Zir)uXEN=*MqR|>0NF_amN+VzKSDk0P3`GMmm9 z&xy96Bm{4fybbmnk2CLgH|0X0ScW$F-?d;73(6ptiLAk|!%?oCWe0p7++_t`x@l?C zM(Ks#zSz`s)sZp4(qCVo;9KQ*tAIXcs*y439{>-^x0uKr2>UT7Ll8HB+5^p#gvO} zmWvZ*`cOl0vZr#1HfYi$BVetz2i*MKg)#n>6fUv?zMLG8YAn8$?klaN(pbWQVrJFg zFq_MPuT&9Ox)skCFd2>5=&JVYw8iwce2cVZv@@Auqy+wTwRzbJt4qyIl%2I(!FHdw zlpLemS6+8duEx4B4h>jHQcpncrL69!Rj~$)D;K~zbu8ufTidVdYF|EuAYbt`=j?^k zlR49FLPi@CcZFFS21tmXviU}32}!)HdIiKqU0r0zCU(AB_;mO~fbbR&m@{4|J}!7O z+bUqWYP@lFu4J23kKJ)-QK+Y;Q{8Q{(A`w7&l!=TeckEZQESg$pVj0yNixBv<|Hu! zU#p#faf(U)r3o4SWi@zJnZw|eMJ*9J{UQ83w75A&C)3o~Qmt+^>l;{oivfnZ@HNUD5H zdWJE@`B#B!1G2OoMZF3RRF9u@jKg&%(T|S`=#%TZn~&0&yYf%;guz++@2dv)KhoR|(^*D5u zfBfn{HD)y@Hq&RdJ3Lt%NvqY2(Vu8HKA)h{5*qVk@@o4j&l`8NgXbABaF=e~Aa+@L7rrA~*sz}RYSY@$ z3C|8HriwZzVkJ#ISL?+r`zSaSgXpeA?GIsf1P2POxI31}0s)ipP}CRA0OS#qk0b;t zP3s{+3@*l}3GsTw`yLRA1R2gkd-!kz3zds={hneYMmx)OHTFm+W;~=%^L72;0U%{q zXPI{S7*89*5`o~v&XMyX2hCv{Q#%d#7l*E&=P72IRe*iS8A!jDR54v|irHOTi`ahs z_!@3&%251u7{6sZeu?5$9TC~oTOyu@7x&y^lpsh#M4Q}5)8tbcHh{8i31T2pq95+1 zy*2L*dP#m8wR2W#SOQ_`_(Fn|M}g*Q+WK4#^kcNFNX}BQgD6bEQ>xyby>~dkN)i!p zyxZJ6X2i4CL{LY+ATJz8NO0301aAuJnjIH|%rM(lCYGjbD^z#m5ff`Rh>~c3iYhEJ zdtD8)F!5k`!<7~fIj$BTvvrA#rvonER;A~s+kh&YC}i;qKWd-Ncgt3}Sjf zqHwa63p|M?mZKBUjz&yW$N{joOX6%C*`qP0hx93lXTuiu9X9YUx?FX_zz`4= z5eVBzbraq#}{9V!~<;nlO`atxUt}a_A zB%+N~C*c`x+p5a0;7G&;3&tj6oK5YT^S4QGj5Q=&6ur(&yZtu zvlO8`s!cNyGGJ0C9WXAGg3XxqgwKfk{nrL)WVl(5OeF7942-k=yxmn`ttCf1WkdAk zBfK+p$c=aq%q!|JKwId>?!7i1`OKRLOq?PEo%VN>SqdTD9Pt$ zq-1)8Cz-81fLYYgwwO0hqOGn&QyiuDj2;}h33QpwbM?iz$8Ew<*~N=!NeB4r zU(k7eR&J4qLT8Js4sV}#;DB7p;p$n-dao#CA3>}`{md~(q=T{l)FJ$&y@~7NVtRS) zGUL4|r@epGuEj631p|0HZvMQ*Ge$I^m4tWfBF8}CaJF%l4$-OMcxiGT>k@Mj+OdYF zebCeHWe4xlOujX})_Z|_fYWlme8VZYaehPab84R|6XVz@Dz#nIX1pDxMr+|%cJud=Zc#$dCBmmJjBB~f2+RFNVjNFP2#*e zcEB}6GtL7+{A*&u1y=I8rM>6q2&NvQ#ic4zHDB1vC>7?Ns;JQNC}=DA8O?PO89^tM zQp~JC5j}zFD{dia8r!T9JS!N()fJq=2xCRjM{Pc}pZB>%z8dL(fV!Vc$htNsy9{$8 zU78@HUGjsV{emte@A4R@;JreLV?O+49|s`(cRJw zQp*zJ0nG)^#do{Ll@+qQ;@A$q_6>oDr#d6R2+&6p`TMH?GuU^@NuwaTZ;I3NDfgWw zdk$Hh*OhiZoYFzN4x5IFc@Nt$^2tD*GOn=mstdg6eBkno5f>56xokpYJrakxQ4)3T z6VW&XqD1DlP>X`y(bhoI}x4a(qd; zD4#@>ic2}vRA~DMsCk0bTcT@;6nv?i(iXcgddYm#Idf+G%TH1DawrQ*GZa$D{lx;~ zUcqeFsvVV=+MTn2J6YlPby^NLYNrNXFa`&?;Cn%N`V|skQC~>NTDJ8UDPAcN{`O-a zz#=Fz>o3bP4o?|*zyqOVPX6EwZ~jhP=fW8yTYv0~P+l(rC9uFPSSsD+9oW{R0o7C| z0-+=i;bijzJuf=Q4HLhdNkVcZUl>*{%NGZpC7}jZZq1D=bhUt+v^@7!dm-NlR!#*- zg8*P3oP5^Xd-z5{4>-AidoD5IBb~c-0(^n_V8A!24Y(Y@sP4K--f;QmS%SaK6S`V+ z1*dzCZys|2A-9GGSS#i)yZAy@YLDKVu=)4om*LgXG<+uTjas~cr8I31bs~Vfath2x zt9I|hH?A=82wBN>Wv~g%!=3{Ub)L*72+VJvT>)Z`0QLz5*ryzMjbE{7?`1_+`h-Tv ziI__0o7o~~_VymvIj6S)zr=3DTax4>?o{GPQe@-s}ipJ@ED{6FLq#mqcc4lf+4ET(Kl7KqX+U=!Y>;vqD8P0Iw19h{bqrWkDbS^WMaAD}wBw;h zBzrCg=;~3KGmKpdSXJP^`%Yr{{a#h2P2XQ90CGEcaZ6cE|B#@3piuM(Ryu~z1ph?6 z5O>BcS@ifA+)hG}Qv^mkU#}?IcuSpxXle!GAp-bMy|}STqV9%0AlB?$PJXP)76fB< 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

_^xpSAytS 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

J!X7h|V3`c~a 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

^RnJZcMANQ9u4H4f zoEAyG;P3-j1*)nSGI%%Zpi@~SS|(bQ3z4)*7yt@rjoj#LS4Os->aCV?0eAC>t|<`&Tt7mH@U)Sd;g1PzUUZisMEFe${rOA$sT8CxTtu7*{P9;$OQ z<*h0@2V@j+O6L+HjY%bZL&J;1FY(V%TY9iV`Plshifuw>xTeavsgRoFIcQ#9{9|SH z61haunCgIqaK#OWI4$YdLQrM#pt)k&ILFkJSx)4_b7dw6T-1np{8~7N>}vlFM-;{p zlN>Y0wR20wmRZ0@GM~6UyXpjM8TBP6aH`t>= zhk#n_^t^&xO~14%ZR!_}EI#jEXhedAo)3psQo8sLBxr|_ zkGD|Xv^9*LdNhdYyU>7=YE{)ZFLu-KHYVz8WqnX{S)6MH-$%JJX4EIx=bAtTT0TG) z9Eu-nsc~YId^`XA*%@F=hIzIj9lYVuvmdUys3lX?gwV&rTG+C zrNq_+ou4#YKg{h6c%Q-jj-u6i)>`(huLB@ znn6)Dd!@bh!&zxrfLY@Zc3c}ZgHN_sqgKykpj{8VI#{xD_iAwY=g9lFQiw`LV9$+K zI`J5X_|~uE3br{iU3$M5ve>~piFzj=iO9dV?Kyr(qaYVM7{R(gdnXfBkujCZigSme zzKRLir33%KInh2Gm{@Ie$0U9Fk7>zLi7X0#^a zmrYp#vr8%RwM1p|drc_9V^5-RKw^GU>hJ^bkpy=w7|-$UCu9aMu}9C;hq=HDDa6{0I10x!W}mOrbUj;9}K%WEoF-G?t7R(;%! z6ccY(Coxy<Bf#{EZAg>}nR#>&NM{zX4XUkDhv0XwQ}Zeto6Bqp5BRU0liSDM+z> z4|!68(;0}|$)a;0`gZZHvd-2*i(H#CF!|5(PuP?#;Wfl=RcGFdVO%c8TJlViV*Tb& z6!x2YbBd~C_g!4cm3@T>Nx4SD{%J|9tiDTYY)|*|KS@~hH?5cS<8c^V^~jsQ$Z8fD zDBHO72a$FTi_*zNXnXZvNo23aF1mDq7ZG@>CTxCqUg_irLroI>vS}WS(l$_rj=-R8+~e^FEJrE3gIr+quk6RXddOwt2PtJtM57q$5(h$`}= zE|Ze+SGVw6BJllB+0GxtSyho%5VSa~K@O?%ZL;Am{-lW@CEK1T9BcBy*tY^(rniSwsy?W2T?nV7x8Bcw+%DFNR1-vq7<{Rt9uA;@g{Aq`& z7s)&hX`2}v>~piBTJWMkIo}y_IVAK(K=D-;Rc%*rb=wuK`a}0+Yb`46s;~ #e@h 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�lIC-}2-<5XT4-8K~#KRwBCP5Le)>oYgr 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

e}$=FxRWIsN6_dk9J{xBi8vfThgf3=(p z2&(uFLG=*mkH#5xl!~Qv5oyFNr|3su$9d^zj5iZxSJAY>SYy@dWvlGM`x9-kmG z;R9M|<%|@6Jc=x9IQ^>V!F8}qL&SDcbL{*v`%(o2@eHklSuzqDfZKg*^HS&2j z9Ol3zgaKg1yx7N~h?Uf&oRq6P$GSyC1dZ3^VgXpaBdmRRQk$|!G|+C5BmG-sdZbBC zIM9Tc&(??h6Pp`w?YEKc-=JmGL*-%Y%$r%@P;mU8$p%s&3-4+-rK@35=N*M?DpVdkHh%lna}kogpN|g|(cM{;7 zJ191T@`~k0(ugI5^1Gs#4{Odpmt%ak=1k;gL$O1lMe2_phMx_Ue{qcLTk{M5ktLb^ zbC%sNjF_#&iMAQv{=$cNJ#;Lp({w{(BR~h=z)hOABSXO#O|8%upVBX_SfJr0won-J zMn^B+{}uwIt^GkMe`_ah@W%Pj%gY*PY2RgKS8W}VNgWeHjT*xYuZmCbiGei^J*KKk z7LI}03`m^lGmS_W!yHE`H7$JfAr1}yi)OqT7O_;{srUu?toTuZ|5egs&za$w@#n%d zW8mwK%|7CnUyFa@Q5tVna#^Ie8Cv)2Kb7tu3h*Z_Oatjr(!p`~ z4*MpLsKYgU`s>n98NvyN4QA$!6yP_jxgK|*{X`=Mc!XoRHpb^c9o0a79m9hPng1tS z;2m&f(&)_zfqi^<&$!4nK8oRgd65&s2yD!a5exQprZ9KV9gq=O8R)krqhwIy0#dK| zBP32D)yq%(HYTnlqc+{+c*N$`W_o0A9DC$PId=lpp;?_Gt!TxFWArMVXbClR{dGo% zMu_9PWs9(KqgBqs{S%&;f4%uS`#He*dW^xh`FuBg?xN zS?D`?aS+bRh#6D(n_^=Ioch2Pjf=x47bg_R2Ou{YP>sq?1YQThPiCnrlmp(7Kx{km zVJ>6_v?Da^O90^veV{51B4~Zw)fB-N3qTfClRze37qDp|wwWl%5`*h*NMHfIgyPx!xa&jHO=3{7u%^XFU=JQ{3*hZ+cV@j8qGa@#E z6x?0^7Go7$5-rUd3oDb1|6e^N{PVyMOs?Z%_p65_ip9zI8>t*5rP708$!y&Z z04o6GIrATvs7Z41r-W2NX?#rzC)T5@ykNj2mXn z=$^=1KVg3tg&y*5<1kXkV((sgG8ZQ5sI5K(9jRQjL4Bg@M?@cd!;mAX{(F~n2{Vej z62w^-qAxS-@6;`Rg#m%6CRLo#fM`>k(6^r<;%3eQD;NV0(V3$l%z)j_#z)4*GpXFl z5G@;b;s=Xkd(suKSwoMQbkQsMVtd2`$G*UM52x%L`D?dNnZK_`NNMW6_#p9MzcsOs z5Qaa(4msNgNkPd7ePoFpCm2Vus&JpveiFkcUfJvhrfUxN39erG5-f$fD5k?%xa}dl zbpsy>f0Vy)jhD}E1-^|;Qd3hd4&-;g39fn1??ZaF`!A7W;KC1P19qxxgvmmNcGypM z$Vd9s;aWG)j!`-^CHmaRlEYmy2lKyU=`|x`*N4c%OwQ3cc>9A{e1M@%NYXD~8^s3_ zk>9;l`ci{~r3yM-OkRo~$@^Wd^ROtCG2;@tP+;M@bT3S!4Y;Qn&w!F6U2Nak1C zS)r3!qO*QSUxl;#;7H!@h3wo`MDr=lBF4Qbx0|Rcm_wIR1DmRIt-q-dZX2Nh(TGzw z$~hmx9F}8Mg@*i0NseL7Q4RkV>o$TQQ4`vZV|E1ZUj!>5hO~vVw0_K3)XZd5gfSO{ z)KqJ9ym`T28t75eiyi353zs;?L@-1~(v`+cD7i9oiLlsxB*noaD2R|Bi!7%vQtICR z@=7UR#Zf{#{3b3`7V%#)^1Lhugu{SI2F97ER~BVJ%TWeshaOeHD9ajUVSul}Ie#w2RbMOIqcp#S0Qoue!JmVeQxW81cq9ox3mQOCB?vDL9{+qRu_ zY;|lWFa15gbKX0D-1pwSW34$>%~>_8YJJ91 zBs}oP3{SpMHcK-IX%cseXxk?17HN|;I8!Bv&M{L)6sqU!UtV7uhc#q0(LY0oJvD2{ z3)O$niBY6iRI$RP{(S_DDqajPUx! zy@HT5_Kbk~#f5;7ID?SHfQjM5MDhTOWkE;spkTVwGb%AK&Ji-+?CLT83UZh+QF)Wc za;IXvk<#D)nup^T*9<~(`7`bjgoGe4^qG=zo|5ra^6M2!A`=*X+0`KYvLFnTAPiez zVqN1{xk8|kBO(rBMuwau%o70O_z|o)q%TrzDpZErRD?VpK&5|GdPiL1-NNg8o_C(b7M1=K3lx8G< zro@4NBQXIX!2=VU_$I~$9qELOSqmh_1rwTa;kE&VGK$}^Bc!MacUE6JbP&axx%T*Un^^-N~vaUWljJ3 z`~p&qt0STxRg;KYbDPta4Odv6iSjg3sN+ zlJdCrq1-7p7)_=(a1uqOCzXaTdtOL5(jt}Eodi?fa5xgLV&rkDp~y5UiANRYEX`o3 zE{)io7*k$zIMVDohGtmBF^HIah4^v+xgp9EZ4pf~`a=!0gy36xPzwJ9`uBHzTT*m( z|AYJ*51I--=y42Mkjt-adrMJejvG4Md-!NKgi3ya@ffd$(5*R=C$mw3Hs3?&%G}d5 z!v=!y`OmjuCh#LPsf`Dh-L&6g4bjiqtIV8*4K~1Flpj>$lAh3Udt;1hQwT)K8%m!_qnc6(l#`xd z`x=asT9kH9n)=%EYsyfUz6YsR)>xn}@dh~)&{BP8KP4pL@>Cl^;*2<1708f%Z3pT! ziI|>MTQO`?LOhx|k5EV_;gh#?9B72rLja>M*d^OFX3=#ROgA3~u_sbO$B2lCOKTD7 z04s&&2Zx4&rR~_itQysXmO=q6+9F)p% zSdGpYd{N6>w2YE6Nc)secLf*%x{YHngiUpMgA^q?W`D!QF?6)@tE{1O87Ot5&kDiV z4swIi4F}kf>qa@+;crGZ?{3*4ZAQCp^F9Ib`O9wuo`~uD51z=oqRQ>*d_=sz{zQ0U z^^u;CG~E^Qh|LjK~_rqI3j$L zz!JN>&-*OW_3IMWq#zPHf5?n6O~z6z#$0-OE!2D%6HpY1^<&D(kPDy&JN%moJ`!a< z^Q8CAXgw8?JR!{Rm#RzIncB*DxMr}biRwyH1zY(rb{%;ImgSU-A$L7n1?XC$vw>$_ zmX&Q20ryWhqkt7ewuC67J<4(In#fOZT;t`~DPf3p;4rFjbSHe;TCzn59IF6Gmmc`g z-ui1R(m}^7T+;lrX;PL#~C%(4=zXCOiD!xNTI z@Pr+2l{ntE+Atb>QMG}n79z{Jh&v(N zD0l=TVUhuu6=Z_lfHSj#JHZDQt+!avC`{jdiF|TM=iE2|D(CDmmS5W(B((U(2qiPL zGe!OhS`&v%iaZvo1R_#uoJ3v7m+n`>70o%BABvWivXJ0Vin(w9n zsTs(9wnRI$!+f7kh>ztZDQA-fBsrIl1zMd@4?7I2gdu8(b|FS=pL$c+b1`qCJA`Ws;#l%#Sw6b+nD)7!qj$~lzcSVx1;dfe_)2m_(oihSXeVLd zSfUSu$))n1XR*luZISd+2C$f*ELn%)OlbmuK~&k?6~o}7xkx7Qd9x)ap?R#2WNd)h zgGO#DO8^H;M6I#atkqCft#~O*dd3WH5zai^q_GZ`I!nw>_QJHO2UF(^153Zq3`m-B zE@f1h5%H`Ld6=Y7ciEZ3iXVFEnbJxSdij~+N8pV@o&>M3Ti2O;GcHtTH zw4L0NpC<04t7Aj6I$L7(oxo&!E-fU7HM~9{zJue}QXS~k!#LOI9Wa;uAy-Ta6XQ?R zU4At7Xk9S`yA&!6I1=J-(N3i3PwI2Yu!Mf3DS@#H$dX^h>ae7hMo5{X_TwgvaHz<+ z&1vxn(f27iC8vHx6u@rf9MCz0sKjgCgz``@jB4M+c@b@4GW9c#$-uk?-Wh8d{3wD$ z8RqyUU=gL?mujX|UYDI%!<=Nis7a?5vS=Emp}Z8{_@ljMV+Fs0`*iZ9?p@QDEfjAo zTEBnAjLk3064?wJL>9#_ElhBdu_P5tCE}+pQ;v#~0yP$8A~IznGi4$)B{EF-0tl)k z8mc55sw5t&Bq(Yr8Rai>%3tV|N;C?j)(QnnITBns5^XsW9yt=$0ugtD5wHRgtN!R% z{!^HKQw^Y*OyHSJz?n?nGR+aVet>h$13L}jIu(%|%TUZYOQ!P8mm*5J#4FVk=1qwZ(;+g1=6|reI+kF##Jf zAsaD4n`r;_$l&!zzxBwVbzHbsPH+bRyaNE=0RZd(Kyq-T-9O|QnIvsXGudYF3CiP9 zk*F4o$&a{$ZUE;kT+WuYV+AjFTnMXLI;KYQ&6&*z$Dx`U%%-C6(W9a_Kqq9fHy~F$ z>y^p<9iJ*T`_91Q1%{B_(~ogA1BO4>({FUeh{huWf#8%lI4GA21j)fag9t-{s!MJ>eZMIuVR{7S zCfJKGTM}R#pn&c}Y0O=g@I!!V%}*&hPpb8jA#pCr*{Hp?%8Fqr;=KRmy&xg~L!RpM z>-1HYa+4zpDBq;G3vY<+Mj)O_)Z@)dr?#Lj^Y!rPJ^l9SHHPV2w0cCo4rl>eu1`i= z{&0RSG$gL7Fh&_3 zi9WM%h;P5m9I)SlWt83RG#q5Cgq)-d{iC>w$JC`*3qKU&uEU_>*sM9dyTml>Rl9$O z77XkUC{l@Cgh~EDiSJV~JV83=5cc6;T6p&M)wl9WFcy%~c+C}uvAoR!@Q8rTNNz> zO{CIobCGIsLSE4-eq}{}KUZ#a*Lm)9yj_)Dtu(KFySYc{mwGA7JYnu_c3aM{O{MNV zc_%o?a-E9Lu+`YhR?T*Cc@JBU(SCuT%8q_-WV`Eq2_O)|dTJBb!nyS9d|hndG-X+U z$k@1&b}&DVD|*(n-jvpXc;L`!>SruAe^Ef`cY@t6G zP5rq|n0UDGJ-#RsX6AdW^lG5YdV+VeN}a@`*N@MDW*V{dp@iU*Nh(!r+|#DY66W73 zM{k3A>T(}Wr$Nr00V%#z=R+!S$i{(o;0a2gs$oqcG?s{1_{!*YHg~Q#qdi;Tjn7>* z(m(^TeqkaZ^I3()hMy-*-OCyW7&dwoUK_@W4r2z%*cc=1j0typUagy7N7n7e4<)4` zm-?n?iSYXHPbPUaq_FFyAHxGTjY~h`kx7Qd3rMwTisG_GQ$(;6)f%8MCPvH*k%`g5 zNWEM6GNxm!<#@IsDt}r*BJE``%Y7-|J0rvClr)$}V5?D%us-)8OvUyPM2IBk@b|;(q0% z&;8Ik85XblsH79i;4k)^={I`6db%{#b7`V6SSBrCIJa{RwTP6Cu+ff|h{RaJwJGMT zZ#^n?EuMaGtF`TZj6?5Sp0LO9Aa#E)9U{N&iVv=l_Hw0Wu=jc}d}=1bSZqjA2fI%)Q=G%m=pb5rvCSbr%kdKb*zf&Mq*}(E)zNs}%cJ)=MKR#oS*V56 z?7)eo%^g#sJ@(cW#}D>E8MEk5$y;A48JaX?JcPwv(8K1AvD-fQLr?Fh8JEVFGw*Yq z{s_l%*AzHh-~B0LRx#-x${99O*KzM4^M798N_2GEh-iKD4zb_q7_*;9_T?V1t+-rc9Onu zVzf1G%~(B>sFFj(l&e0KnO2`ZdgyPiTRWdNwXCl`NofujZ;!)wzp0jW4S&_=5k8Xk zvVnU&REaAxkc4lGc*V)Dco3b(Lp~gOGbz|J`jF+9_ZI&^Fn}(M4180~+s$5}K2NWk zbgZ8nzCKD~;E%1Pl&6UFF@xK8wUj8)wYu5m`LOpnjz!vLR zuuiNF(5s^gh+`?()Z<$WSnLpUlo9p-2`7a#H;%OSR4^zM?n^w_i@ge%?=OL6U-_?H z&C7)Dx%SC#z~h(B7~U{AQ?Qi;_i2?@9lVcR*d=AUU8Nn3&+(!za_ET{rq|Vbr{`)N zTQ7udnmUQL+ea6iU$yK*9$ziHvuf||L82l`Mbo=%Ho0#4$7XJoLJbCs3m>UgpjmJJ8aCcQyytsRCAvs5|>wd=%sG0G1U|f5q4OXJ*Z#W z>itNI_6+7Zo^`1EzFEEiV6|zRORk)vti}`au<2}LHg$o=Us^cI%|fHX-L~#L9^W5p z63ZT0j{f1)&_ycwP->-#vXaBp(QSG;h<8uht-Dc<$~ecj*Y$7$IvtOf1@EJ(UF|J( zgj-+!npo3{@ZeEkY?U!x_-j$+(q#Nz&f6jGA(*fAS=2i|U`v{{GVeq2Ow2!+lBSYT zTH1GwQ>malit3&1E0mC5g__D_c<5JsYJ}Z#b)2YA(9&ZEK8wlvKB()nyb>Yt{8Yf?ywX zifpcIQL`IuE>#;GZu;DNc^7>@6+&hOsHq!o;_9j9G&B5oz6k5*d#aHZm(QwVRb%G$ zd96cm9a8hSNYiuH)wiq3LcvJ+B@`2@`{sK zg|u+uPd~=rIhJCRHHFob=rNJna1gdx7*ySOK6l%BDhR^mhvuoJ`Tx*bzMl@#YY*IoWf$gR|8z> z)RgYdMSc^Nrp4hYmqSRUk+(W5!~7Od9*X(kAWDw8Nq>ap^4mZO}zSxfSn)ht#WL;h}ZW z)+R}rL9IE$roU3RtaGv;$JAZYe@JV1p4Eox)FL2XF0d(+dE(@4P8hk~oV{TK{~Qmc z+1dxdrRE}}AqKzy5-@^YB1y{PIOSmR7F;B8_g#oXCL)_uyOEJ~fv%)~O#GZRookUA z!U(=}ZJ-n&)kmg5gLDb}E}3DrffeP`qHT6>=lhlI)l1N69UMa=FO< z(PzeW#!F!TBg!>zW!o}fQ^F-Bs-w|aK;~k{@)dCpMu^@gcf+t!0Sw zzESx=tLeAdgS9@;OP6=a);hXHu=Dl6m~GbOE=T)7k7Zmp9!`z-%U3q}cU=+vMsnFA znYC*%{l^J5(EfJbKg%!RCvAqZA^rwy(JZ)JFEf;MPC7a>RbG?j*Jb1Y=IaQ|=oo^$RPmOhY=1B_sg6>o%bKkE&Diwt;iZGQ zrKxY|>1Bi{<~vH{tm9*==bHM44+Ts(CqCzpzE=O{UB^`~lv<*^e{xpSiM87I`PrRh zg!Pu%{)UVnF$Ikoy0(qi+6NOE3%MU-=oIU8HjXe$+3KHZO;6Xs?tAAZ0|zQ(mSVxf zhuZSpDQMjvD4l^CJHKY+$~V3xzZR!qM2}v~>vR!$Hogwvlev~~5ESipNYYP@8Mv5u z*il*EZp9koL_5~i*ETkuSU}X#TCLuse@=>Cb!+Kf$Y3Mqjf>&~G*B#z7A)@re{w9d zH6t_VdE9zUG$*e9dAxL%PR(zg=2~vr@thph^&w}ES4iyFKWK;;6zkmTj;1<0YUlVI zG1{`X>@0LmTiY7P@7^A3GKxt>t1sWgb3bi45ehxpI2WaVocy8o$9hy=3f^O$E5B-m zh=l5KDKO5O!ec7+sKA@m9g`FDyqm+W5!upJmE(1DaB>qJh}}6Vw{in6*|wsep%lxs{abErJX8s} zF}gSoVTMj{2WMHBzqYtcugRhc1*a&W6UBB<`IC}KC`p7r<%&NX zQXk_-CXci}Xtls_9m%_w!`vAr57>N`C(%uos-Vwx_2XMsp6v?Bip$Lk$dYQT6BU85 zWBTH_|2&5T9qLgZ06h!UvmZ^mAGRGmWNhti>6uZxP|VI8E{D_%z`mJ>o%GM5-O5YL z6o2khr^{oywl4EC(3kokoc!UMpRn}w&Rc_Dz7}CK&H<^%^!MrKM zbmmyn!FGJID5=Rsgn`>2u2zh5r)%imFtRE9S?EE-%hIuGpwV8ZdX%6$rTl$?UsrX0 z+E`0G+gbA9a+f~K471(-D!g|dfPU0;-7Ab!Z562|H|pwTV7~G4wpTz;w?%Pb7cKXG z*6moE>7t^&7_|6@HSQ8+yu(Jv_TB2neDvZ}k9!NClcHSmrdl7Mt+;WtS_$ZcmlW>I z7utA}@?d@H{LO0MP*~Tg)bT-!(+^SRw;03b<-K32i`x3<709~Wd|0BL3W3?vC6qU@ zi{-X&cdYR|TsyGBGgF<1-vpj}^=2lJX@s!+xbk5kX4O#!fwqOYt!Hh>V{#*8PltLY z((dx6?ijyJfXitDO!AmSsZaMy?$#>PL(bOA zXQHt)hR2RL+FEb)DYc&3ZMq9?_w!ae#%uM0=X-Dq-eZ}Uno}CvkW}--NI;0G8UhQJ zf{I$@o@v=botL!xRvzk3P&ylOh@N!@HTvI5oxVD>F(FQ_a<&K{&xePWttqL zk45oAT{S%Dqep`M>7|`1J#hx@A+om{8_48$XA_s~j%mkcGdD}B!m8|7!cj4$t^_k%^x#{;>vrgnR(1Vwt6nJM@!?ysQDLO*~dnbx|J`xJEWf> z?mupSIX+OgK27NkP3pca-?Z<1(z#0#PsW>~m~=aqB)o`3m4oza>3b~iV47gmw=*t&yfdvV#uJ@iZ8&f=uC(ibVaxLxi5<#rmSA9+}V zts+nK8_22GizcR8118_!kNCCMroe(bQ?wb$Orr{?6KxY;)g@KP%)9hhwC4jWb*N7Y zDs`&7<{>WjxZ0<=ZGB#DWzUy}rPUHrF0Qz>N@xqzhOAK*lrawuY3odH58=8_C=R*b zZA(5+-~$k5ih?cfa~kcqGUdooCXl z(I@m7mACWN`3|ED7HhL-NNp-1^?3{`GU!hxz)91n+3GS@s{A=DaM@8J^Ug=Zb%)9G zRC#1@+j5Y)UbW=>jIZBx=X;t;N!uFz?n!(*P~V|GUYreBc$;=*;W`YizU#y3zB^hu z4GK5TM*VtZYQnyy1ZgG#n3%DF>eqT2lWSwlNlc#5S@3~bmvc^%<5yxK++Prnfxu$c zVyA&L)le5O1I>+?z|f_ncN6oF^EsYzD*N~gZ`)S*;&~Wn3BO6g#2BOqj(}8x!-4As z%LE$*69;=;!5U!;Lj`~cLkg1#1IH%AqsoKTF=J9=l(Cj~1cKWI*Vflz%Z~@+iMe-M_xWrmoXL9ggyk(} z8q>jhcUt~thK4P~-fM3Il=xsllz3?3=-;sEDI$JC{@W@m?D~~JXY+#+=FCe1Ae@p8 zBt@NAeo6ht8gS3O0OF+J;9zv}B=VkMIFIm(cC#nvsP26S&>+z6Xokm=rzynbrGkFmjs)}18sFJTtgoF-fe3}S1a2s+b*Icnd+Z6)1Qq<9oMcV z3VV9*fvx0n7|G1G$GkTuq52Q7rHz6JuyhR;8QRsP7VaJ|XqirWOHTR$liH%@_dgCD zGaVUtd|Hoyk7T1TDv|Jy46EKa^mGUSB!Jd{9l;El_!*t~)n9I{;P0*!%8R64#uqB* zF`Am}&w8rEU&VJ~KG}HJY{v{obv(|0sbwY|tn0@~?^1Z&G||CUYx}zRuE(izS5-OL zWW6`<{0^?bhzi141_qLkp4x_kHnuTxbaF5@u>Oa%HMD?(W@7wWC4`KG|8%qo8MO)7 zSsA|*5(2adnb|qM2zG5kR#vtzf{96+5C8!DZGU^L{|Nju@-IOq&VMBTO8{;$Ty#!kq_#6-x>&hgLqzjU+wkK|tt$CnTP%=(h}EB$ZD|IGZ-_?PBnVfpft zjrkw1|57afp*a5K>0fPsz5Vy~@7?{S|F-|R`Tvpp>Jf5qaQ=t;Zz;gn*k29*p81#l z>(75MVEl_E6Z^kG{7(4a$OHaw*scm4TD7i180wBVz^`V;fT^GeTx&Rz5yB=zlK=w~X(xy%zn9 zu)&u;(0J{__HELhkbadvVujo5tPr+;OPOdG<#U8|yc;n%G5W#XeERM--?e(1k5bIA z!#DMV1yBrggapwE#pF{sm-M)&H&v~>Fe{q-wL8mkG_ruHF6V5CW}sBpcsk?`LlkcP zCi28KcyzxgxJ<(Awil98C@lgG34m=&abrdCr~lpMocG#r5%lcnRe`iqdl$0B$i|V8 zUpo>krs7PtDDY0bNJ;uAzg6N>qZeHN>Na-q=UHQY&GYc)@gn!EkJfz*UMd#17iDu9 zRd_;$j~|uDR^flS{8|5h!NtVP%*^;d*nKSz765?xzu}wkgwjz_ef@O2$c!?;d63=i ztFZ1$OwBj|N7j0z;iUg*^fsEv>iq$6l+WVnJQrVS43`PI%07xSVW0Nqz+ik@ zHw?7PM8l_|tHfzKv5%1jYzHe$Ha>p8`b)mqEXiQYUc(@#$IjX$#IOtO*5Dv9iIXTl z)fO0Yf;kB~U(WMxHx3I(GNeXMxX>j!>^ceZg<%j{C$UM%SBlA=P2h7DsLGr?f2A)3 z_#yE+R>7uhDZR+b$A-ft>RuB7*@X;RG1bm&USzq7rb*}$WWIO290EBVkgqhzv&AIi zB6k?7DXyg_@sz;n0xO&`S9@>bf3{xvYlbWn6Q78CTUua?TJ?!%yfMZ5ym;M~N`WRF4mvQudR8X^&sCaH9 za`(#31iBf@CD8eK=UX+oIbq;u^NR_C@RFW61vPO0R^_;!?AbWu>1Q)MZvm*1(Drrv z)x}-b%K3(arRQaF!6mV$G?)gliDrgtpq#0m$2;|&AUJau=rSvW2vQbd&0O*OfnS?Sgg6Z#F@i9GGRI~^ zrG{!UBHNQj?lSaJVn%VK?P6jEaG(znn+UV*a$tsV;5$4K5J8X=%KZ!rMKM7B1eM84 zfoTnOj}1vdcn*y>fc8Zy>QNaeRrsj^6cieSTHMR>0TO)VG=RkjdX3+Lajly&>Wcu# zg{*~a2-O<6%)tzgTk?ai1(S-+4^bLWq7>+AZ)C9N&+8fcy!<;o~bE;+?WWOb0H8h%eX;89!tg zX}L#hpz#Xqla&Wpo|q5n6mhv%IaI!P{!RUgWq0`smM7q@z6Sr~ekRfh@P=}oT?1?n zYPH`T7*zvhkHQo3ggbb&qZj(=kAbN7b22pD0LB-jsYh*~@{0KLTL&Zq(L3@k#uami z{}Z$ZQsqn^K?wTq;1;+h5}OdbP;PySPtLPIbtJu@@1gBEXtiiABp1QUPoDwH`Ut!D zZg|(uzsCszI%^Q@3C_f)d^mpI0ds`9A#MtIfo|^e2e9uT?h?7dT+_Lwf1+*%yCQD+ zxyIencZA$|+F@QZzTsSN*@0a{;)?P>Uy^ho5Zq*cMW*lv#vtbG!+&nzqTlsK)ACFd7c8(%Aq^<@5+S69gfBCLV-#jIC zv!;6d_u;3`soWnXT;Ae!VoDKA9+;jz+H>IQxH+1O_cs9TuMUeL_MH(_zontCRCD0q z@x5(#)&TA8OWv5v?T}l#O8P%>LVJ44FX=!$OkDLG&^jM)A6I(5dE~%3WOp?~o~2eE zC!GpgZSFW8G8DuT$~W|0(_2CQ`DZu)&b2o3?pI&pW{a#{hw#t4gaqFDjrN0!8aHRz ze^`dZZ!_S@++Y&&RC!+*R9^mvgPixeGc`z7a7mZvY2v!PTK(wr!l>dcBY7=o7O)Es zJf|FfnY!UUdj4g55cP}rw>Mu~Ba91@6PQ?_U1N32x|%Up0qLhnFsGf_d?_o#37J?i+!9-VAxc>Jg2)#r z&*PG!;p>xI(tSm;CE$)=?h$?E%Zm6O9=Xr5QwZw|Ljjh1fE@%@%H|`w`(OK+zdiYx zyfId!si`FTtsSpQx|%7EQ!6i$aK0E&Ukp#fvrnwM0a*_6-I!*uwht1dkE?EBB8IjI zTlIx64ed4#Td(boX}7LULh+C+gc%Wm3_c>sLJU#eDh{PDbM#pdEvPr1Q}>bYTyE{j zENO2${lP_WSQ%-|W&)r41tN{qM11sYK45Kiu5exhl7O z?J{$Y;Tb7~X$&xf?baobL~vM&#Hpg@;_`Coj?grY%(xh!YH}Jyo#_sbq0L(8Jf>k+ zXnJVNMpYIas>$YU&k@q`MUN{vEaj(4RVB)#tNgqhlQAYT?UXpVPCyr(i3FGk3DtZ{ zKG4n@)s#d^HpbJ`-b_(V2zzhj7y~3=dANJwHjLoKIf;z@$!BRq%r*!_j+9^8Mh7>V zDoI?_OHDPtS2lsB)>yfswGMOME%2|H_!%49_ryUnIztsvnS*QK995`F7!Y17#ay98 z9Sndx>~W|fyAaq5SNn#FG02rjDct~ouBd2Ih}}VtZ~#>DIj(v6P8%LwV11KrO9HQn z((D448)0Lk->I04hyHq@C!~f(sJFg0xtEdRo&;(qNM}msa4yT?P9F%3JEo|EKbgNO z+w0vJt)DVWL_HU!IORBwDca8OCw48OW5w{V~D3$K*qUj^?DH$ zEQd?BkMnju8`8UbTBtT`!%ZzMt56h2Re>LBScPWJeq&Re2b#oY1-Jo71_w^K7D_oH z=}KTT#RB>C-^vZBNq>)N`I8vda2cVpQiH>I<#wv#Rw+0{sRNM+xL*soMlVgp1=3Xg z6o9sOpjoQvh;JGi()wW-h!|(Gost*bZ{{Q5rDIJKU&TVj;g$2MPyC{$&7l!=@m1;x z+|9ilJe=vrcaGAYq@x@TeH=5D+90(e2};fx&0SVaO3mMz4(0A~$RzF%nG?Edngf;m z>OsxoYR~#K*B|kQos~S?@~OP$TENMR8b*)SEiMyNHwk(q`d8d~9k zoO^1*ao7&KjvI2gQ zo7t$V{G$=^b+|rpk3>i9rc5{rC46xSa;&1Y@;8WPW~jBaAo2C49y6~%b6ojn2nw+_ zh+_6Y1=|qUYCGQ}+;esJla9ZNN7c@?{B99pf$RvH z&uveanU|XC;98mQoi|WWrdg?$Ok_JxWV23YOO#BPDv(t!lqD&Y1uc|?nk&*US8i#b zlKDgBm`r6v&rFf3Lmn?_M2tlOnrce(8AY!46#0-q85kl$niDz)EhexNL;$NFJc5Y{ zqlUniK01XXSXT+*6#IQbv-*%qI}BG`tpZoD;$kH>lfQ2{4LN;-`oOU=z_7k@E^Hn~ z3sO@xREMS>l{ZUoM#|0a-tK2j=mogYQ;0hF@9dzU?eE;{t}U;O%?oj6me$W@l_Y%X5vt?$ zE;h-kxj?6?zjMRTOkPfU)xtn`)QK=D_7;q$q+Uvdk+0lrQ!bruut-SNk$&mG4?lt$ zJQgfr0N?b~B>sMSP^V z_1nu1Msu@rM@%D9-JiY;w71hegx+-fgDVqB2+TkkU^<%22#z3m%?Q?9FrI^Lk9<=d zhe}K2_PY(DkRM#Wm(Ptr&Y$2R1!y=lL1*t#032V|i4no2e|Z;a_h1|N^QUjG2?yjS z5tI?ibM(94)+oVF_psWmn-CV{d=NYHw&D}T(ksSWx?&xUyRQ%aC&(v#)+NAzM#~#< zQ%F-CO1uaW@0Le<>l#OI(6*3Ui9yh_V{SGLx2DHY;}&zB5Z;Y-n~O7J*16@HuTQ48 z-i7uLI~KJ8Fl|tx*&vq&LUoTjb4EgZ{{GkB?3yEI81e~+5{|eJ&Avl1yVcOPaDtE) z+-N3y(H?$R!C)1=NuMH`Ay;i+=SGBBLcwuNq{%!&={;j=jHC0x)+9T+- zw=45!&Z#ZlLc^=k2+v_}xOZgq;>Xi=cUt88{jiLFSby~SXhqJ|b`}FS*TR$0-dhQ( z*>`9-0+uy75HKKx2h8#_2DS@_EFouOYmJywCC5TjE6A#Usnx}Fh<1@Ar+!QbkSC`0 z>@yojhSf64MkbmYJOHdG7)lQG*&B#Y5&1R@bw<4i04Ev6y2lQVZpvN8N{%!LUxLaP_~OXs$aiP zsy*&D`psss-O#-$+%8qkf|i-2wh%XSeG^RVlet>$Ae9&~^T^qyK-$G}~>&*+xvzQIYBcBDIrO9&@$qwy8Eguvf zh;q{$HOi&W-2qBhbNfJoqA7)1@J*NaNCT-oma`9) zF?xb&j_~ED$S!075@NMK^%E8=EGYpXU_?btyeRh*TY@STe!AxUH{6*92XPg#`V%T4Y zc&V0l?~)Mh1oCM#Puc5-BWm*eP;>DCWmQp>dP8yVRl@_z&Q~$^_Mn!akd~l?ux;{T z{FYLrez~UXzy- z^`{}~s;0)~#@?%ka`XPMbWaP5yj9^*G1y~%MouV%5+wxrFy@6UZ^}FYW}!kk48RH0 z^~uRK)JZ{P6yA0slg&CucM6Uzc|kD+sToVta^Ce*HSj3oUC1G@O z@#yfV=?KmK?E!M^XxM@veH>S);82Ymu{aM{1jqrhe5;NSW)fTi3MrHXs4+Y|G831~ z=9q?ynMlRDIlm*pg^6z8qFf{;#d0$@bo8yVj?Hbi+bjm}p|8E0mX=l-^ai=^FU0o! z4`FF!uOJ9}c{KVR`Lv{9-!O}okac06UtzyUJwt^dJxv@XUpH?acn`EwkJbo?`&ZtL z4s5vx_P8e69thV;^=^o?O8ocIo)pVU%d{2^6Gv3B_7SnMD`0P6;G)#QMT3Kx#|Sk` zlyYHjpj92lazYi1X+C4V^X2S%p{SIp87`Q4dGUICd3DxKOAy$Sc6QGCQ4=IG9HC5; zJ|QkmFRw)?ii&Y;%gNtk?LU*gJUux=UB~sVBdPa6W{#j$`TQ9mAv)oP-AeO4BAIg#+|pI+}Gh)^t}7x?AH(%R90%R`TjM&icnHa z-MLmBI>COlPJoMoznNWbVoBG!!!8$Uc4)Fk_YjUVZiVC>o*6_V!2;`#EjzQ0iHx^~ zfH8@g@H-pMPA)nIO5YEuT)01>s*+S>?<|v)?-V8pTm#5i0?-TQdw@Kwf?<=WsW3PvBBc*tF{In``uHVlt0DMGMbq<===i&Ky z6bzbhm#h+RTkDOMpY5kN_^Kg_(@v2CiSywAUTEC~k#FW8ox!7ogp5{g^(>5|jcaV@SP|@I{9SUJEr!1u80_rG6T?m9meD`F@g;cIyI z+dBvX)ot`PeTH)M_r}5{GGebW3K<#{g#MU9=BGF!5DvU2W-FEz6vrp&8aEuy;AnaX z`!SrQ;Tb8}%p2o(B~QMl3Ul;+ihMxS7d=;NJA^(={Yd$5`$?TCt6)~^9ioj6>DI`nakDOB)Bak-7DlAsi za=R{7Wf_-4u|s}ro`-D-rE^IO(y=eeM0YMF0oSt+$V~bv)G;MDPAJqdD97Ccc^%@S zHDyM6;j)S(Wz_m*P|CP^)!ejbGB-QZ#ScqPz}y`JCn$LURSrOTz7TXtJR^sepa%KE zJz3lUF?S&smGE$;v=#BvE9U9C^E%rkugvvxtnMD`ccGje23O=#^9qjqiJD)M^Q`7l zp>Hw07e)KW^F6tDsao+7o8$9=hfPQAn3*7YvOxtKR(T_OKy5fc^%%KDrc(6PG;xlG zx)+6Cmya)$EK z88!J@3-;n?G4jYT&vRkXaxnw6CRl|*|KDQP$}F+&R}3!m2BiA3yj z+6;I>%MxbC=U}PM)bZJ6`_>^TYc%#Ut?KN_lLqntQEh|6230bhpjtV-%K5Z=d!5qU ziXtuB9~hRC;CQBGrE{7VR!Lfs#vanGU1{Y2jWJF-=+cszQ?t0*gz^PLEPuh`%I%5Z zI%;ip;}>dgRtFi@W9Is-bhB-zgH?;lDSt5vyLF@eFIPD0Hw}>{PL`r({4`Bx)HPwv zNYza%vSi)00BCyVWdgS|IuUXGY8k07a(04nYRoG3CNN7&YRL-4DJWxf==1uCWmQ%5 zxpNoeU}vJLHl-B`s#<{r_odX60P77>$aTJ^l6iGPb@}vtnGphjgN&HRM+y|@dSKjo z3lo+mr@N-kOUZgg#kD5q^OS?Bdp0#1w&e`oy$)R4h=ZlstV?`X+nj#wv3=~H(jCqC z;X-8wXe{N%+5$0Cq_b)o_T#bkhQZ=oDRo6-Eyjs&{9Jq~Zoa9aWtX34|G>56p0q+nE97LBfi{Rm@J3WWV#D>Hq#EMLZs% z&VbQ$k1AgCwuj^CKq^JGG1iKnB6wQT_Dlgb&alpClor$ibB|-t-fnaUX^_Y*2uGg< ze)7u7Id{;A(D1J0+klTs=!r5B1V$Qmk)7344OTJQRJ5HFrX4YLK}T)qT>bd1`nS<-4B`jSq7zeABbMnGed&T)AhBZPRY6Lenl1Sl?u@ zOd}-2g2{@EY#3Ik-4&hM*E68U!c5Hi1Oz#By4El9ml z;LFXK<^-}H)jTqUw0$RCGnhHj2<+Q6q*yghlX=m zw9RLz;L2b=2Iq$qNA;KI>RFCsX3^tb<; ziyw#loOcFhkhLS=(jN>}ItTX9O~}khKFGhV8*%H2&ml#fQgB#Hq16>Ns^DFrq^<|~ zhi|{f-P`@+f!S1PCSn1DaB_rA2G+!EEOulHi=>v3@-p9Tu6yU_Y&bdPGR4GPbfW(feg)i3rX%KS z=i>4`KFC$qdx2!7LXAIes(^lR>aI4Qdc3)%Dx*e@3G4uzMds{!Z1<}}xun>Z611Os7a!lL^<} zI}-1|;AZ|{dem9hC+iEvjwlQa%>$z!)l9ah(cdIS1g<6I3WDp<17AX4Grg*j1HR9V}>>MkH3{#>Hm&H&_(F1xI9nFBKRx zQdcAlS`q64)jv^`knXLEuS9;u8MA$IPugvVT`B@MMXM+NkrI~`}ldM6GF3CGVnCEu|NG*&mXGeM_w(?-?6 zKKLLxW(vkyJ)guT`?oK9?YsqewbADkSZ);vdXD?QYkiI@wLD1EG zY3Xw(ksU+xWxyOLdBL^cm-QRh*RK?pE9bS`As#L;PLqPsnO#vG*N6KvvOe+sD0fxm zx}Hz`uhrv`LJCqYGK+`Y4bh^iNABseRsVvsFIX(Kflh zOd77D4wqfUxF(Ui#svfd+{9_8U#akL)CNCDwd>3A#hZ`6nsl@AMNtH-F7}_`dse+Z zjkYXyOi8w1k47u*oy_(NY-n{A?#!)K&LqK-GZw(vr0n`M+AC^2?AI`h)tdaVeoEl4 z(7T@0&xliKD*E#D45!K>s-`3B- z@$#Ne!aHoA@@bk%aeGYNk0i1j(*tmYt~nswPc(jP?iJCJt4LeWmdJgLn=bsWSrAl)6@XSi{grG28sWRLIQU25 z#{zPRz??j+LwT$ox|%ILUNRY7?11zS&|dTALMQ9yLc2UptZe(^9#dz}Z)d57yBPD= z*NEpuyJ~-)?4SO8hlOSLrt@gc3KR%jgZX6hoVp*)k$$N84H|73kFxW`v!b{dRp+HW zI-3MvR4LHAts2a3s#tyB$q#xhR9Kc8l|Urzi>Bik*) z;eRe4B(O2vzS?Drp4RJy4%$;c?G()Ip#=3um^}UR z9iqe};ijR$M1{B$&de%`2LTlqMgK!cdTqGL((>&ZbEf90_ z@aktOEb(J&cgR)M4Y3_>Z#z=Y@p$^h(3U?NC_M$U&d#4?YVNyUxc6-!rgH$zRh2_y zaw9FQAv7lv20J2Z#YIck=kptvwz%0aeh;=q)eAN&hLeY1*SLNXlH~;s?kxg(LHHLf z3*INQgnRr@TxpK&KQg8^>7$ak@39nvC8TS1SpB##A0sUecT9Lmoih;@j7Gn8V1`u#$YKWH)cv60-hr{So!;R^}6LoEO%Lw`BOXIktmr2&zPB4&GIdG<0W1YG;u`MKE4JHbaY zjKDX>ZyzpII{ZcRl&HUjQ*M=pCMUiHe-ynXp@6~dZZYK2peLGa`BCVPdLu}9Ec!m8)=z~%I7id0U#`*E~uOi9FMR}-auQ*6vY@lW4 zx2cssGpb_1vO%^0m+F3sByT=+AQ1p3xYCWf|0x6p<>!%8s;50eLPg=o%F4zp&;dVE ze~B)1slcs&km`L#6Arf1+(mg4zwdzzz0AR5-0@BND#X9@WJByD zjbzNDbDVM3BI zNUx7!>L{cK_VNeoPoH_jQrq7xI@TP5tUmn0d0TEY_zE16U1gF#evb{CDkZNQcdb?2 zE%qM8M2V8yQ7mS(G7q#9!nM>VxT|UM4)Nsr9Bla&9DZC-#)i@0usAcy#^TZf*n9V@zYCR&DB9YdSr&L58QA<>fp+gY2IprxLMP!m2M0b^KciRK*q$76~w zS=v)@fmMFa-L%oHoo=sa%tUjRVSVxU*RwDo=Lw`EdGlr(62_8VJ0s#r@ypff%Si_i?X~UmfDkPHDnwa$B=@hSErR^KBzW6U3;p~-_OVtI^+Eb2TP6=h zl(1-+(zwdiqNdy*YrQ;!%<(4GW{Gq)x^N=`HVEBz*QYQKS0gS8MBhe+uuxjPctnfzwR zl^ye_-z2;Fe!K{!e<7yz!J__d78b)iI?;YF5FIGnFDzMYxqYan1I*K&m2WeK%fMt^ zUs!OZs!iQTF%v)$XgwwninjZrtPg0!oeJ!!KMp_fxl6fwlAlf~k6Q8MZ;HZ{!O=PG zBsLNB8l{>)OFPHDyDz`P1GN}mSCzU<+Ao*Y(f3(Pd3QPZ z8`)1gK=$y>S^F60XS0pw{YrS^CyCiHOwq0?pJl$n1cJ9n0D%yv zyR)s$J0lUlzI#h3g6>%7dFNf`0iwaP@uuSnJ|QMI^%bSm37bnN)_Mft2tO;xSk)KN zd?E!E%pdUJ#tdrRl(k8pzh~7EcdxHM1%)mHQ@BXJrb*Xa|sM3S_;mn*Z-~Z};=DFkDDV3+oR4@K} zxUQ;kS}{y8`l%(;Tae|A87tQ(7<8BV-IH3x!q!jX)N#Ca6!3Q`uHLoK z`m1j;>Ts1oKwU5Q&*vKsh+k$A?B#|&j=dS|6QRs*tNxdLPl#O|cS5?`}*s|aUy8o7!TCYXIIVz_c(REa0F zV3bnG!=^+`eq)O2+8Xp>3&;m!)@2KbYL;&iB%`JpJx5dH;`@4xrEK0-HFEDftdDXvq*^AFA`IW9 zTa{O~);+E|6&}#d4BgX?GNKs1#f827Oa_*R$q*Z!yUR;s`4y%svE7Z%2Xc?55x(<**79P-P?#mSM=V)ptDo(4bF?v|8=jP%IlO zmpt*VzjZ8@bmfcJ##{@Toh4Lv-O02()Dja6wA?jD$IUDl@88KU>HOp^9lVC4`gNkyP{ZGoNy74hwwyz0k zHF#!%DfBMUiuamDm|y({6JRbDUOs*IC(RPuxNrVu$0A41Zg=~YXU0iPctxP}B8yq@ z8|(&}rg=pvYf!SXGaF3l(O)TPiZn!MO=Sg?tmVA~Ys-&bxvyKt4QRpi=v~gg_#)t- zcVpI>1fNc9J9``?Dz8<{O;B*C8e1#u$p$#_+--|kl|@rg4@sIY^!@xq6#uVez&{+y ze}O@q|AJXq-x(ox&Udcme}`GP{!9B`7zrE4KWM?fU;m&e|He`Nk^i&*&-S17ze8C5 zXW75({@Wt+zwniR7@+^8i2hGF3kUPR^!~B)|AQU+AGZI&R{kr;f59sM;nTa_fBW`7 zm*Jh0c-Q|Y*MIi@$?@M>|K9#n^8b+kyX5b>|CHdLwEtZH_jCIX8}y&w1S{)1jrG3* zFFgO2{ z2}W~ikvZjqDCNjE{EB_XW;8rYu`#gWlGFVr11QDTldqeyEcp;enfz&4qcHVoVc~eKc|TpGO7)W zMi%M?x^m$92yO-eK!#0*grX>93myHg)Z^n=#Ue^r9SSofCNk04>kDEhtc^7~s;MMK zs5d>%QE|nrzLhxFdSc;tGdQ7lt|W0Zsjp2GFYanL#&@KtugB6J zNhhzhPcd>M3IDNN3neC$H{T4M_W5be${XyzF8O>fGLiXjmq+w?#sH`=yHJUkUw{Q> z0quZxq-X^?hngO1((<68tgNg+i4)^L0@-W}R}^g_cW0&2;-EX5_u%txJZzV!4Qo_G ze&om=6c;ZXK|X%lKHD#D1#xdA<{NKbqpq*&ZI?nB&Tq38Zwz$9LT#=mY4K{+TAG^Z z8hV}9hj9ZDIE*PmcBdK3riBy(AhSnvIU&OpFim^}rF_N@4Q8vqHSKGpojn^B*DA?# zh9IqR%6yS(W$rJUM|9-?V-HmppDeq{3?;HNq+T?LaY=Y7b0X0(rgAwu*Nujv5dZP- ztLymUuEqE^u%-1%Nvu_=Vw8aXwsPCUXd(9We&Rjt{1T}=6S!Vbaf~}=WJCnHfxSFe zJnDkBfnBY!Gq=3H?Bm7la>YRBc8m-7d*hVVuX>Sz;0LX{3&Xh<$^^qgN+uD@Y3fKr=O-B48Pg1%LBM-3ZM6k@A^+| zl0mQ0xQowZAeI;;lQKbeVw&q3ZD(u*-G`@Xb*d~j%!RQsr+u_cz+sPRygHIbHSe!K zy#u_sbTR#5rQYrsdUaVn?S;9Im|6u$i--dQRQ)nCK$1|-qN}IsBL8d4pkz!PAbQo< zPPWGsa1M`Y%&^YYiuT$=`0rFR!wD^a{b!_y&Bb^e>x z0HUm{co%rKstn^OCv;a^%Yo#}qLclm`#i{x@XHjfgpd*bfIE#xp)PiV3yWQ1G0rha zq4NB1|8rtWr;NTt380-Kn~wAOYueb17TC(rddNf8oW)VOO0)6@f2+(zqrs1{^FQev z$gqsCy5x2qY9kq)+WLWph}4o6ORD z@_vRCK0QA}E)%iuTJV&o^z_ixn*ll;<&K+Z394(wtb2 zT5I7*)M#Y=scS&i0$ z59u2`hcS9%OQw)4YdkT6SrpwGdrZjp=TzJAtanixmktW~qF+LxV>6?+gt=qg6Y|wW<&Kl4;RRyzEZu z$;%h(*cP4Rzx8k=8?b~p@FKLGt(xjy7-ey?;cMl?D*+j#62(}>vJPoj+67DW1Em1# zxID4&D1dd0Q3uuuK)lc*ejAHMtI#4gPcxj7hM-X6y}1AoSZEQGrx(skBMJb^D3MDv zV2RL_6=={65YQ;Z88u*)7iiE(6k=fmGN=a-X`li0$$2{ArU3dxqcE(9LXB?&qBJ<) zS@gp_0ON^9zp+4t)bV*L;TSaGg((yRax~9?@ff34EUND;^5LZbxmcq*ED3;IoKYSY zW&s}405{E`42xv=3;;_;>FWSBjigL0)4(U1U|ANq@La%O=~%h}G#YQf=Oi*IL{-FU ztX~Cq)B_)A_yO0+WZL2MG^~ZgF}(~DtynJw-{Z+t5tFeLXaEKF-wl&{DT7oIBe2Y9 zVdNYckS?| z0(a$bt^#-6@J*SEG^}cwi!U_331wFlHN_hB#@Pl!iEA8;a&Md0UXCwXj(|+^e8jJG`x6 ziAh2y+y~HK(5xKJSJ12*zFM$EH&6*!BO911Xx0o*FKAW@&z5ocg|#T_kcsvFDYbB5 z!F?E(x~xMYmby%>b~s%@vrhO}L9&_(er9LFY zzV_|Dgp56*Cf|`To(bvvKNS3zQ;mJQHX&oHD9KGkjCOoF8?MdmC-xqze=ZyQBJPj^ zw4J=CotaL@FGOso)kZVXN!Ve*(k*jg3K6JvzZ!qovdxr=r-$-x!$*NDG=7jJyHyh)^61hOcRg>`bSr zNNKX{RHv!%Y4R#KcjNGhg91px6ygKopb~4!`76wc9M5+@zotoMDTfvPxET?b5RVU~ z#E{Q1*a;g{N~je#4wY1{5zT`x;|jO1*Gt+Kz~ zAyCpU!3gujUxI~eo1ucFk13X|o zjJ7T8Kr$~IV_n!;V8%t>M-Dlb@h3(akCV9}nm z5l~o3tOe1`h`3?dqOgjhKzaTVX`a?)#s5?ne+h!V7013u%7c`Wo&m#-zJE_eaj@UiU}pX=umP_xhRj+Y2gNwuMI=G@^l{ zD1@v+peTf-f}toxq(Z28_HiP-I5EHs;~~rpO+LE#&pw|}faBbDX<|;Pxbk+Xk)<3Z zZFun{oP2n(nm^^BvieWTAIt7}4j16qoZ7!Ulv%WJoV;1Iw`QbDP^u&n43XbPiw(#_ zgOzIx#l^xdLmhWYjgo@V9;o*W!c)b1RKm9@2*hvTFp$4qqTNu9{%%u%HXYCf3a2_O zYW;wVO{dN*AyFbw{a%R3$jTcA2qhGEy{wNdPK?AMV~Y(N{+->3gR`dQ$4&{yF4fj;sz4_x7;gST#4^|KwrRSCO9BTCS z=krsyS9f-RP$AgsJ@Vw15AHq8xz`ra63S0M_;%#>py%{0F8{@VP3UT#1rL3f$ctCF zXP{L69kx`~oqG4!beHpAFMVjek9xunS2Y6r7MHKkkD3SOIoYlm5T8->Tfsw%^Vc<{ zGaqOd5~=nfe<>)xDJXA2X#UUqCPD*GIRe{0-5Yxyo5VL8@Xj!Iy}qdST%)I=%f3t7 z-GX1?)+~?DHw;(*cC0rpd!^e%xK*F9pH+4kQmzVrMAld#Bqyt3$TT;Wwb>GZbQ3p)x`B+s$sm%va7tyGVhpa zoAU(S$>~hhn<5iDFu3 z5sMq2=ris-Ji+Y({86|SOm0U*Kw93Ba7_tqP2)j9cUlJ(tQv0R=Zcj!)*qTG=-lY9 zbm2+x&|b;#qY+7PNhi7l8anmVHJ@&SB(7kPZYNR4>C!M$+T0WjT$MC)uzs1t=)i!X zc#{vh^Pmn8uFf^{1$?JOriO?GuKM}m`CHk|{p|cqo7vKFnI`ubmW$2xV=A4@I#LSn zLBY}VPG+4c4{hUJW=BsCY$3P_wV&04({i2OBXiK}=oMW=Vr;Wu*4HwQpYJB}tgjv3 zwk86q(?Q^0r6k2iYthYm&NK>CZoFO@;hP9<=HXlt=p+I(yndr8(xwU_i@BlE#377* zXZ{l6gjV`>?c#+W94DKR3fTJY7*B7L4vDw7Wf|cH z9~c|?{d}8J8PZAEm^86>%!eGu28#U-M^i2!ct{mGs{RBQgzb)XZ7+%~{rc^m@SCbA5Ce6Tvt{sS%`Q`lSY&8|L7n z0=xoDBF|Pk#WzxCnnLfwl6t z`ua%)3K+oAE!}NpfL()8@{!gbGXPuog9b_h%$R>+H--UA%_ogd^dI(MuKZiN2@N=Z zz$HLa2(vIFr6Ak~;CDN3eKa6o#!i8jgA4bU?iM$IW+ujikAxlkkRKqL^8pVw^5bOy zdbhm+9y8W3jAH<)KPukmzaMC~VBw%(0-*fi4^T~DG=(W+pqc}mb4X&~3ZNOg5ehz_ zbfYYNpfi9P_eWrch7%S7!DsUdLveR&FoZ;rK%u~FLT`T1htY@fgYW#<`9Tm~5JnL8 z70w&l`|~sQ{U`2^-k-cbJj1$^xTD^)ol|W^ceizu_;13TeSiG4}dyjAb-W6d_ zey+R)x8=FTYk+FcV2^x`xn;FQyCq}5We;o5V~=l7V~=3ZVvlZ5T7%U5ft!RIbqOH@ z$r)<@gE@>j6rVqv=E=Ci@0Y3CH=bLeq1XMTH^QfE<2NUW-~T<)<6}2|w*#Dg1{9(B zhoS$cyZ?6J%n9Q2yJM>Mj^|czXk&lriO?(C_&xE375=qPd0~YkGPTMO zGCVNk&E|!`m)cOPz|Xhj0WrG$DxWb>nKnb;MxEofjVAA)~-)OC5hCB^$$e!QwJ z8DCRq(6jTD$N4X1&T7o;kRyxZ%k`0xY;^g4{*E;XQA_iXOTlW&V1<@$$R%q<_GO=d z{?0z#r2wXc9uLw=>HK}a^F%CP^qTh?A-yTxGHqj8W`CIm+D^48GJRyK<`97yhX-u_|_O_j- ze@8B*$Lu~T7#HtMWb`yz$*^iR)}BD*+8aPi%aDw-iOjk3)}_vwkqo@0TjqdhRp2YR($Ftj$s`C4XPLNx_?9vu#5RmT zW*|GcquxX;|&g2m7C$L6#iqxML4QCbJ*{YL6^FC&Kviv_EXxMFX(FbF={KJZ@#{}4 zY_V*qSj}}rDvJ-vS+o9wkgB4x!uzh7*U&tlwyjf8>?sw6T;%H*(lWxBJ=f_$+>Wq^ zI$oZp&e!>R+_M;6Y`I8W7C+B^lJ8-qP!@cRHDDsc^$3ScjTyWrPsn8paW?I}=il7Ik^n_zb6d9Bt}Tg#GwNy; z(UDELF!{9sn(k|qcqX`v2e|ukUMHPHW^z6c%Y5NrSXAw#b<^q*j-NCE?Csed}R0Sb^F{lN;s13ewHGv$|!lOGe33y7C^dtb!h}F@1{|Y#(xw+dj<>C=| zfxJw(#`hT1rQh6a>KMKEzk9~dhlXvg1G_By(Y)Rav+NYX>+BZdjT5tWIbV>f%JySU zt)9on`$qW}&oWsOSNY=yD&%lLtjoI?Bq^mMM!u9n{+l^?bPyhD%4TMki=eQu#igv8 z)sJ*)T@CsQsWz8Cq6JL@@3V73dr@b5k1sSx$VklbA!dZVDn?|MmwM9UfBtqVbM;v8 zw{o(vDq{k{m1A>ib$mWCWH`6q*5`}BP=`hr;~O1iX8U1%gKGNCt{0qLH0$Y!l|`Di zv>O1{d?P@k*K>F(57qrxu$i&hwdeSFa1b4i?W#JFkHHgU)@Z=YF%-cm1tmEto4e8z z)MWxZ2&_jRFz(+(Sc}SjhJ*4@brR*xKV^I_pOy&;!*{9gbDoCux{ zgPwx-;p`I?i#uXX>(-zHXPk78)&96#h0JBEh5~my<>rQcq}L)tWGB!c`dwSGD)Zd( z&+9(Nu!QV&QmyUc(T_xaG5jVNUp$1KNMB zt#LbQ(p~OW=OA$T0`spEe>1$JC-z3BM(`7DK&H{XG6L%A#X6Q2$pRODCtN2a6|+cI zo2bHq<%cv`*G4(>>DU+;#w^F{i}p>-b_=xcV?n=bqbBE0Vs!s7$+(z|*K#Jywk_-4 zon4$ujO+{TJ?y-_N@>P>$0=SuC}n+lwaLDVSG<%K9HDynmi_lX(*I8fZ|SJ{eoow( zVO=%GSZ}y&nM48O$l`e=*g3&&J}Md8E2vQa)w~^PrT!l1tl5WuIWP^dTWhGoO98 z^Zw30RbZJVJ;xtk^j{iR405K%@1fd)<)>dOjV~C6KLHJGgoz@0)hc|v5<0~Q0x3Sv zk0btIjpx`_T4r53(u5gZsn_{L`18)wd!ZMEcSG<;8Ce^T*l>dB6FaF|<=$$f&KK(m zE&O@Ze0W;4Kz%%P-k8-7*b>@hx0&-+2a@+?TG^{gZE8;n)*U#76zCj?(M96vyiP1FGCZh*I_UiVS6P;~Guu)Z^+tq2 z?wl5i0gc7KZK}Vbcc8V3X$RbBRcoW?!l?q+k!Bgw~v>DSWvy}t)p zOELGsZhb3u639BSo>O;&UT`BSY{^O7qW8k$&i1@G*$<8!kF&&jZ1 z3B^W&2J(CdgO7T3+8gO5YIc79%x-B3un_HV2sF=c1a_Vw zzm(q&%m^rz+(<&8cps4J-R4I*H}ZZH37GwCW!Ay8v;>w$lnpS%E+lH!N$F zKBao4IZl8L5HSy)qesm`sj#$zg}_!~c<;98$Hy$2+!CA|d7G&EoXxz|(2b>~IuRS; zp?P~{U=3wiaQb`+9=vBTt(9>zzD)zTbpDSg12YE}vQ1>3qq}a2u>5Byi}V#N>(d{= z6&Cj+w|c0P5YCd=uY(u}#!Dr5)__%Q3ARuTR9S6z%(Z+5ylGFCf}B=TJj=Yse)dK7 zkK_j$)PNd#mCX&j$m`Aknz}>m6vEF^nLn5sBS)gaq^X;mMoz`i` zIT-NVow&hjH8~w-8Z|b}T3-O>Qd(Mh$TJql3BA3@UpTSYM?7KenIc{58^KaPE0KwU zIA&Ut_AIn}RS#Ofrs-2X(`v7hqETwkEUK>bGfaWw$fY6rgUVQcw-wN&bfqpZLmvNocaEH2O^V9;f&2qdR?Y2cQBMl$u@{ zE#yl+d%A80!<9b17iCUv!+zAPTC-X>zM08f zZL-_M=QwPkK`)vc#V*g~QYhA&svOk)uJ)yg7e$4rL=-H5TZUVv>)yCii{Eme4!wy# zK8T(EIVsd^E^nD|ZI3_3RzQ;Ti7ZC;%q`)<#$u%aZg)L zN@&-W79%cK;xDtd7^EjOYp1;2TU-9o#U1RI4TFmW*?Q+|PKpq3tNoqZp%WA~Hw~BX z%Eu(QrM_jHvDHNU??a2JQFa10`cVj(@ml(xGrotZ+Lo9p>eBaK7Y82i`9L%9NR1)vr;@4sd+X z3^{n5%O7E};7ybq3~N;g@!-tse>HzhG%WTHvO;%r5a~D7i4lP#avna295bO+qc^QJ zm|FZC5`z$q^BqcjLC7bWeFrM^sV(MkjJoZ7;%$Jx&C^|K}td>3;ZfDMjOQu`sNVu;A_>cBtV^oo!*a9t4xTMnAc;3jbK(V>0V#P{K{GIqdNYfD`{}e z2!Gzu+r(z<0=1UYWw>cY-aJ!#t$0#U@yPAgBjhh=im%i8)?<@oQt-@g@+Dw>Qjo+U z?0Ze4fSt9|CL{f#zQ^rD!VouQ+}w*q>%|@W5%Z8XQ*BYi{KCWn#zpHT{4Q$VaO2W# z@TbP7(brH3>2{tAw!s29gEVkwXU zP5_&{q!p@i)z{*B3EqzYzSCS0+9>B;0PZmhSxVJ`hl=qhLMTU<>ghgciC4;?rZY!EvLTnH)8!}GtDAWFtae{cm&iy9ToQXsPpO4k_ zZ$)gXIwUk%v1-XhVx}Y@fljiL$j2gbonA3|*)mDln2F>`q1~H#4{QG{vXX7;Xi#%V zbEuQe_WZth(9dx`251gI(f7McDb~)~(^$L&_ty?OVHV3BaU8};8&lb2u1(P&#_y+5 zI9;xcRG+8vwD>!kjE2)JE2C_3XV;N3{3@QEyM2#-pwqoiy7Q%wRbC#Lm1dK~pnR9M zd#-|wqBuWiqwI28eh8XHEmYsAPq~EDBYD`4sdVg4)?+K*SV|SoD=22y0{6x@Dc>50 z?4H(5+kgwYg>D?#f*S%Ub;f#69vyq;lYaKTr_ZAgN8UaGHa#`4m^f=K2CqM{k%%>G z414&H1T|uswzhg7kg|1~T@3rkYNxvK4}59+qVvDK?nKt^NX zvxzY33K|~AqfvBCmHhR9F2Cm&y3@CBV_6)6K*I$u&9*v)%A>}kQ&nxX(sHTFNu3S> zzL(8$Rgw%ol1zbB6D9%WNRMw%H*AHba1UI@cC#(yrx1tPxI-bHwsI?A-(STwE|Y(kQ9RFh zBz33YrN2y#XEM;T@F_CyZzdIcQrPfTW;x8KL`;~EMLz9aIlx-p!AvQcUCM*0*|g~j z+`e!prS6U&p$V9)rc9d=af2|dSs!2Wm;F|w&EER&Gu~2oHq$}v%P0-;l;=J>hpA{n zje{>nwpOWrT6;~+(c@v`{=q;eE%Twd->gH0a-&u3Rv8z~clD4_h>R9nmepT9PbU$TJ>x0R3*%m5%C6DL38Wboa=2l{`&}08|u9uT{?6=YuQ}8 zJRUq#R5yd%8MGHJG?j*Z(mq?kLa4Cbn^m}>+F<4o0Xv>dG{(rQc1ymf}zU3odLh#@Y!Ce~HCP5S2U4py22X_d;9h%@y z;~J!Kcef7i-uRGwSKqw%=Ds^KYwh)`-J6!Rx(`+VQ+4Vb70s{{#O}%~_D(FQreq#o zk7do#S74`xsruRt5}Rr+Cq!*cLxs!1>&}N^Gi5&S4hHX#Yo)kOZ*6gHhis60e-ze$ z{kT8gsQ;L1U#1{trrq->4N$$z)E->w9%0xjGB}itT>hRo(BF>EZWqS70_k#cUJTgL4Y3v@mZlAH478 z%1LUbA-;aXTIt)&19#S+p;XEy$tO%Y!ksji7X4#xefC2M2Zn2#| z#CKyEh#O2whT}N&fM*v8PDznNW3y{OLyB)HL9*Ih}p~=z1Q6Q6i!;^<)&9 zkRijwvcTzWRyvn3cQ{9W(i2rXwm#TcKG?+4sH=X+)#ueAyOZnA<*b6{6L^)$yYdtL_JB> zXRM!cKFIukSgh%}{y`krvufx10*I*^9{sNSisXIRbL{8XA8~RRx!T$5@x0-IfT=3U za|-=TR$;BeMY6PhE8B8c5p_l;IpViCbUf=X7A#J)4xC(HqJ%xKEzf%6Cx=8ShoCEr z7qI&FP7=viS=DFrvnyCR2S@EIS)U@DkuU{_{8u>vKVqJPpXO2w*!YT!`V5~ZBgkZx z!&O-8z%|tX`Dq68N#U35&+&O}f_ZG=7|;YT0&vquKb^BXT;aNyn3)!$M-xmg z_dH&X@+u_fZP05xbZWe<$ShwJ>9+>Xw+XjH$3$s zw3Re;@?34;D)>1G;T|_|=mgI->S%AxZT1=S=!1pUO4rqpr%&q9(doUx4{|tSp5tC$ z@u;tHQNmxuQTXjwW1w%G=(5m{d+oG>2<2fxYDxt$fl(UV?Jd0XWx=mae-a)M^7c>- zpY*!%4f^sO571?xrw_(UqRGZIP4wv*T$uOFW%8tU&52a;%U&xWp<|cr>c2?FBV!Xb zG!7X=ib*DAWfL(D>K%OhJLD%KozOSbh>!}8F$1tNiseCi$>Jl%`f$^KV`X6DN~qsP zlyAd@B^9YrTP22rw2De(I-ob@tvUvog^;0iND2HugfhF_h0yO&;iOtfYiUsyq*{n; z2~mo?GJ&>~0R*H<(b?`uQw9aZ!WXzjMFmBmf+!Gg*lq!?kSGzB@Y4qJl}CRd;3Z?Z zJ_Vn;$7B2aZL31BhQ}A>kg4uTE{OoRkcy7UEAL?i?mgdD)N!HEq(0sqqxADC8FzK+ zmXcVV=}*ji-xHn22Y$z=l8IoIOTE(l&Ll?tL2Q6=I4&3~LOl{t3^3;CF6mC1Qj?9v z1M78LCIJYH2ZQjyFh(k{b=2oH6&9HRynL*P345d3?fgZQt1ym|907H4kE$qVlY{dr zXLohJmXdUxX&GjV{KT4xf%zSye~7#FlPYnI8G`V_MI!_v0jga|^!kH=>d@vAJDq7R z=DpNJr=bBgcyO(o`oay1N=J!}&S_i79W&Wuc<&?GX`A0&`MXDsQ(c35N#lD-s>jZs zo@hMfF=u+y@0s^96E8*vZnij1wK=n#UfzQ9e+qNkzC3e#b#{%OR{xy(p?8V8_tGNX zfB$>fPgDuHLCz(_6GuGR_$qxMrK%lgY9n0XEtYDJ;uY<#_lKoVlZ>F{@<0J#ls6>y zLk7co->1!o{@&!e!Gy!r2kyh8EpNb;_{4b5qE~c6Szhi7jm9p|2MdQx*V(&CQP2ke z*aR!$Vc_lR6$=UW4C9F;cJAt+vb8&KjSYVwPA+tFQLyhQC7J~8XhGi zeZF(iAV6^WF5~W*A7kRKL>k6$ZQZSk=o)!xE8UpN9 zgx{DgWrS}f3rcsVvMq>GdX%zYg3n}6dl!~h)&cBE2+ZvR#|X^`O7g6&Y)6YK2#9*5k>V2Db6&Qx4f1CY1Thu=1LFQSD-0K6~6HnR| zC-og}5~eR1wxq5b@Z!wd$X;2@=dmApL`QWKcwRqiSbc{>2s^~>zP3G#Oz0t0wNImx zB zG5vhy(=Y4<>WmWHyw}|p&kX7_7+yO@#B+@D8`B~{ooT!!9vCxL=mye!);$T>ZKY%5;j!OM2Sl+Zy(ffE->3{g?8}bFo~+D@ zdga7WVDNzmaaT>QM+o`%#*a1Ol2w1_S>Gty-@c)20*`6LENZPcI@_;WvQf_iy=~FX zw-`PCFMfGZ+{Yt5?S!Xaf9JGdgDUAYS0}R`?|sJeHoxR!i{5V>3Cb1tRcp^(-`aov zYrW*UQo+xd$}sW+d`wspyruWD`57}nycMbZNAWR!Gm6a*Dpw57P7*!DiWeHS9hsjA znvsuwWa{y;%xEN8F#7lstp>LVF*#i4S6~4B`C){uU;b=1!|9@V11kW@5kn&G=;TL< zY2UM?`*Oo+;AewwFaSE2bYeN^%4y6`z?Kn-H_{zp@(*$CL32(s;!(TqXE@ke`7!WL zdwLca5WV`ew#dWt=P>G@9e;k>EcIgT{c)~s-QGL&1t;_}=k#nu$3V=FSWLGVRoxHY zl@9h`t-ePDMqLkN7U;xD;JLj;a_n1@2Jy^;^EsK!gTbes(}w)5M}@zKH;Zuts_TP| z*6r6@lJ=f7pBLv7GJ->MM~K6S^>x8eow5V}`(sK>gt~ir?^G8)z9Z*L{j&I8Du+CV z9Gm8mIr7PkbV%80`clo4?*BL**568p4D*HGQ?UEJ=B?!D%ZZ{;hjOFN3&VYkk&aT@ ze!Y|pm&S}vg!5ju{_PEX9h7Vv_2sHU88VccP|b*_q>K?%#j|;E*gE_izS@Ka5O=RoM#d1Ys2Tx;{5OfM zO!h9^v~;Z>h#<2zQ(?~21V3$_IxZ&rG8mCVe9+pmP{&e0WtVbn| zZ!rC8(d>h9tD?Umv9B5GMPIpv?!ojoSnKC4X4Dg^bi6$Nx+v3TzoW`@-=C}7a894- zy)j;{brVlt*72MK1-7k;^#olEyvdKRImSoq-4!HcCE|Ak+3DDPM%A?Wx!K~TApU&9 z&?A_ep(^>@)W;)<9c|hc`dpXX^Rl?7+c_doO+2}wop>a}Fx&26HelMx+P@RM$fc1B z`ww6P+9A^!W(mF4*%u2$x|_9X`H&`iZYGk6{phg=J-OQPEf{@;6_g`{ql8QRNPkj( zV$P*=&|SRbT0_7wE=P|14`7uef64Lx2LIume?Mdm@Ygb>(C0!E6z-A?}_7!5o76KzSb@X)_uM;vfZlScwM&q?yeoQT%Hk(Uw- z!=ndYPxG8<=hV--V>m2I9Ro&&jG~(04qwgZ^@wY1H8N~QOxZ_U+3iIm>ZLD zvE&lyzDG6-cUoP*#jNmiRYl4RsAFe0zbIY6ZD%BQ>X`R*)smQN6 z43^1o0w|z|I=&&Nj4=)FdVF1t7%a0kN((y2$jzj=7ulhf5R|_O9hZ&s{=bOA2SO;| zY)((8R(3H2j;S->SeyLeIq+sQ)DStwG4jzUulOVpkIbzdW`VuD0o4Zn1sKXG@*8je zp91Qd#}iflE{qxaIJCp-eoxr4Osdt@6zzJ2wg+6_m6A$tIO9LrC(rQe0!O=EqKyFc z>X_PRa!)n)SltF2gPL6xrZqCT9>#~KjQ8*h5fL+2!k+VIYqIr)Y*dK7F0}}pxM8Wu z{5my_-2JHCayNVfx*LW(Oh3s9sP#->P3TyNIjNJ)u9J zJ$f44LH0`uVe#!h{&I*9v>w%-2I(>4{}W0r;ustwgXr~;A_DDLh|Bzn*YJ~`&2%`~ z3M@Xu&_l6XeD2brZOaAw%(VD4=--P4)f@Hf*9jQW3fzPr3Tm&DD+pD3Q3E3fc_PAe zv-Ja={7a}4r+Z(Ye*g3xDujfi&pjIPQY=WhVtu=Wn^$(bfKYe?mp)<`AqU*CUBbfq z+va#9T^JOQ2;bxG_Q(ka-2f(|v;XBNRlW`H;-8SLEMpQsp}yg$!jt}YOXOrwKp?z? zyW1_t71RirjLf!1`WGU=X2Q3nZnZ(731!18{Fs1w({{(=Ll2}Op8+klZI&u-CfP#p( z@n|5{|14a$dlvoDmGB4j88O^3_m`gM{3xq-Z=?NP3A>>0h~ZASLwY{&qpbf6(OPFa z3rdjPF@^V07X*V_HOOIQRf} zB`pQJznMb9#D2Sf16o>slDteio2Hg_*=RVscG<*mj)W}Bsk`!Z#7=vEgVW@cysEf1 zk<q#l1ay}iCk_qO34?gz;s`&QoOk^+A*=|XyoY@8 zXN`|EXLIeEk!_ZDx0dDI(dKsr|0cvfV7*etRFany{i`uz7r{f87ZZ*fd;5R1_joy^ zVZ!o+Ve4|3!60r-$kP}BQ9GLdV^q-4J(K*1b!y+Na3Wz;E1zuDVOkDq$*v6Rro%5b zEdl+{3|fvv1RbELs{sAKHjNQlYA+=9@V?sodK%q48;*rZG#=}Lox}IN!NMCKeDeLJ z#47QPp}4V4;fQLHm}%Y|20e~pKjr4c^0hkRcqc<=BH13?RHu^R$Vq&^ z4qgPV+BuG?m?bAe7os_Bap##Q`}j20j%ZF*{BkPB%gIppZ}w5wMYMZC0x7rE)XT;U zQ{qkoDX|q}VyyO=72qqm^X;(09XQ=Zw!H1O)p2t$Cr@+yh5wPNXZw-#oAKK*G z_#bB8G>VOK_?4kKGQOXA@o(c@zIksUzI{}|~CS|ami?5)Ht67{>_CRa9yE(T+m`x&d3zi|_>`+}k1c1uE694EQ)$>{04 z)WG+$h=lk_{!OKXyiJWFtks*x8|koTM2IFz_%-??PwXr zG2)BV*`J6{9KY3M43EBYC9== z{QZn*)cpJ?Y1D##A>^uV0Es^`y@S_t87F0H`l>6Q{h+1`~P+*X0i) z3BHD~wArSq?3q@6!;JX~^MrL7$08ZquL`BjFQR89cIrx;)C1kO;L}ip>ab<2jp1q+b%4 zw)tKXEHc|a6iu4tf0!+p`Z$84kTc%v;oPy1B)LqTFk(omDm?vikUS_T*a&k8Wz-t} zAR#3AZkIP>*jP$AdG6FD8B%T{4<@Eekq0GX7s&~ML<~H&I6xvfHA>d`qJqgvrpkR8 zBHI*4a*eBEsvmuq?PJE_Zu)KF9$@^>n7RcVRlDsT>CRS|bR}$d^uz5hS~Hu;>i4t3^VcXev=CrvVQ|a!q(Osq#;tgqDY$s0Qv16)Z<+sMD;3=&K(J zvSC;SM{kh15cfQ-+I!hKBlr4oU5BR!VZIXUuJR7-t^Yc~Owxdx+g!MVueuO|67K zk{%9s<}i)iNBhJk4^qU!(~!NUvWT9Nu&TLupN;j3@!bpU zYNr*^m?A8+8ie@97l$gP$P|WO^w3QWW#6p4h3DJ6ZZVb`8M$;Bb}9pF%^XEvU8Emf zGF&+bUfnlr`Sp=b=hP?g&3e->*j%VcMC`uJ;q59waX4bXl6k^W>1NARb6v`w%F_tY zR>u%#B7Mo=*>TcZ$KxZE%G^#hzDS2+wJ}l^#}F(cBRl~a=yMnK`^3@*E0xFEQ&E+3 z(Ps2z1|Mw|&zCkq!LIGgr@3WYFcn|~vo0QX0?ud%RY}LLt<|+I(ah&k&KaJv9x=j18?=M8LLSE4}ZtO4RmSqKC~ycUAy%yK9}R%eDK8g6080 zZ-)_$IKZ3IO*3$W){YJ%kE+OPI0%sIIz3@0xF4;vO(e5cntwJu;kLJ9zsC{!u-4zz zen{lk2|A*9%}wmf|~HrUnDyUVFuCz!!546-k55CtPon z*Eb?VWBJd_JgM&Z?W&-1Y3O+rz&%$wAP!Rny^QQ#>_{eR;vC+};R6m>c9v9?YB(H4 zBH~AHg^1_9m$PBPKvYMrUb%MF3`)Zg%G`it-pMz!WoQUA9MHK6&k~F6t^6Fb8+?);Q3j3N@JC2uwTIlO`Co0u8lI(RNBf;tGV5 zf%;Bt`&$?`(9Q`4WtwDmvt@WJ*Sz{QJ2+YM_RZe#isL=w!NXjQ%Q4Q3c_psXd{K`A zyQ_Dg$(h=Y&T|mgQHJqMMuIjG_IyMB(~4^Q%M%C76IB-4P=9q`($X{Uq@{p_WBUXW z<7>TIWKWibsM77X&zCzOg8bsnRtUj=M4{H0&t~BKi(jdHrS@ zKx^K(Tl{v+bCQz2phOGv(ax=m*^!I;F+{lfWxk(AH_cL+iUUml`ur30`c^QNWXot$CSDHb_Ej_Jm3jTyDQls&tb){aM)wz=to(%*UiBFrQz zjdT?@VLrCVTW>4i_j~nWl5R_Wbf)OnA?S&r=sq+?o2ru6cgQiHbBKJv>0Z9+_tn~b zGi6?y!%NmZt%f~iHS~K2-GU2&;rku0n+_$Nr{D0Il@U4Q5S;}PfybB{CYa1B5Osdu zqnl14Tm3Txa8M~-_mh7x~R}P*J5&L zVKBCeX~;-;S%+&CzJF2uBpAm)@5#El4WI`Vc<(3XRTIM6>qbaOfP* z$xo*oA0Y7wtEA~uxr65f@2Zk*YFiafrQBC**(>fgUIu^pg&@rK8c`VWHbq9{f8P!;G zs9Xm%$ z4^@@Z*b6d?(q0Yvs_D~YBhZe7JUg>JXGq8enZ7`Axxs}~NsG)MB{9!I*?^Ku!y=bmqfzo!AL=8y(1ABkC7_!P1V@&QRL$fLg8};luk;Ey`fH|6Bjp-PL z274%yG?{R0@2Fw!Zi!NQd=UngO87Hrm7-yZskv)um0d=@w&keuI_Ta3%$=|Gx3xI9 z)M#q?T!*uN>9?d~@oP&}tY(tPHMb${>=vuN{GDo9gb2s!B2-WJoVKY%&E$SID#cn{ zx-LhjW?1ncWog=})+yaaduxHQblR!Z$!k0XXeOOmP3}DRs;LB`WC55$nKcr~JB4XC zd-5`84iDsOh8H$_;xMm32J+8NcbeU}JOuh=WKTDjEH5ZDOO1+an%%iPN;mqN=1hRM zgC?2R```qwqs^hFIa46PkVz)B+Y!?;U;5S|=hpQmOrt)g=aAiBbhsAp_VO5aMT_Wy z3v4w!ovI$5T$41X0xrUW(-WDW zTcQtS;Ml2#)I)Ao=M=i6-HO0XLFNY!f>N0u(+JY#sR>iik!-gFT8ThVNf76Fku%Vu&F;I zylu%Z#o@19v$^o1T@hKA#EaYZHMXVtk zxcW0bwjKg;gFOo#h*z&aT&}{zHsIFzi;3*$p|N+R%q+Kp$=8}BVO2cN z-a@i`67n31H;H6zyE+5$#)4HaX7>kduR$1N3)AS{ixwuB3%M*{9O+mK(2r!tAQq9F zao-anuiOt>*#5F)z8B{tK&>@@U(T1Xea)Ba1911VE5G~+Rw-4{u2_WW>gP6)guR0d zel;mfo^DKX;i{OHnocnTGMinvmkO9+S(w#rFmT7dGSTSMP)Rcr|9KG7j8)3fmbf3* z%u~kMGqqViUo<`bdBCh<<9zzmCxpN%WA6C$2bwF z*Zsy=**w$kVa&ZND(V8UPFTNDyFlFdA7 z*2URJd{zQ%nG1F4b1h}nE|#iO%v<=X zV6CF@!Y7w5R!#Z?v;lTLqm=Zjf#CuPrWGrStG=bmH0?a*_mk|n%-Qn@v_-wdan8m&$Iz8{@5R2Hn0ra9y@q= z+qQ+#c;#Tf$yH>c&yGl|&2o1T{e)W}IeY-VaJ zCXD-OYG2%z{D7*g+Bq3AI~ktnieoFwB2G={YmLZpF-}S&I!y>IRm-t4PD+KJ@C)}< z%K<6@0>bjua+r*mqKF)0=(&-bsP4;oJ=*Szd38(f9^y7fA}^P`5|=pfp%MmKJAFh; zD|(MBST~qPE(mQ8{CtB51ar2i1c%pk%I^I&*Tb1ETJOzTc3^ADnrr;gZKca}BkUgD z(rVW(#k0XvDe9%kPbf;xO^LiMUt0OZ0shDq&JGr1v-n4dut~Cmh1lr-g!uiCLp)Cz zYmf5gtaTKRgC$GPYX{3LrEr~@&p>1)BYNdIbEaaN0T3K#lB9nFyWF?Z&9TqK$eWe@ zgeu9x{ygkS!ooW~@+eZC%=oy{(M%{;B7$Afwr;NPQgE#!sn+PEx~J3#^2GNlh>zTi zUFs?4{aa>(uR0lx!~+|va_o?zAmS$gPr}R}cHG3&tFhc@nH`^C>_Gom0A~Iwm>Vmz zljRNr7|pyIjvK|i#_oxi52c5hcw(8XN~Hys>lseg?Qa|_%+asSRjHof>+}$~vN6zk zAlg7jT&Wo(mPxFofnVU{_dEd!KeS@C->fBs-FFCJwRD^#&^7t>mt|Ga->kno7j~$x z0S&QfR&Xq)@ZwYJIo4BiycTSi1pxWKHon-;Xylp9-R%Iyg;5JoFi4Pu=5&B}a_n9J zckaO4%u=$M(P(Q4=SxX}##=}F$R+BHl>A?~+gjb40GdiP4A@|!|Q ziO}n*UeIfSQH)zXpYx9A+&m0LgkeY5cjP^VQPJGP@FObLs7}iBMZ8~tt?zcRu*Kot zpNFG}keowexDmvmxP;;5>qu##;A0L?A`>V9jH)&AXm#C|WTYi}Yq`gvJgPe|dCXPI zm;QuJ3dzCsjBuGhm}_Kh$l=>eAf)eC5!xkPk5{*~T1R@?4#y6}t^j(Vj^sGoz*`UQd9mK$^nZjs(^{MD-XhdYr`V zg#2MW$0p7GZCTjim47%DJXQAZf{l6&tXw6e{vy8lAQ{UZl>M$l7S*&QdYM5i>di0= zhuBmh?W$L$vk{yHGT#96l~_cssB{U2)L}PqZ=dcsx&e05sT}~{X9}$}H3_EE(bN&bx zP9YlK|6DF$^mnybmA9lXM{rhq)J-^DRvc|6c=1b!-HKy0D+;=vg=UjHH-!b42zClhvvXw1*U%3R6&3{Pb5K-&XJzuGr$-#C}WKI zgkH6|!{{i-Lf=>84rkiG}@%;>lg}EYy`z*ymPeRg9H zeJAxVRM$GCZ-?u=0 zcG;&t^gZmplT}AhJ_1j%lBYj}IkqeD)A~SCSj*T$;U6%SA{>+-?+J;zzem~e6K6); zgwPr&WL~;CCQ*1xp$k%llEijJpRD(|o*2Zz0CbQ<1&sDYg{uIgLJ!GDe{c00a&FAv zHCw`Ha0elu6)2r^OeQsrnBYQ6H>fZvNYk!v?}`tIOJvR^EhBj70Nz@G;2-nwZK3>< zNmM!;bqi&YM33o}Xa4Qo2ue!YGTueWK2d@)aR_ib+<)|UchI*|1>Yj~7dD;|`ugGq zwAX*s#DNAU?cbS8!4DMmcT^2Ftep=|*_(eYbjQGsIW-#gf(7qz9v3%i+}4`sD&#Ro^` zw@vfh#gX9u6u8BamGl1ufQRKISR9MBfayUwwhNulsMJ*HPU-F-rgcL#%y@2G7w1lp&(d*2r{a?f5NWW9fmN%NPGY~}fr;@;C!j!M8+ zE4(QEPi)w?#E{<-dBk5gr4U=|!HopK;Jw2n0oaLarz{F0ElU3=b|R(}6ddW`@VrjZ zi8YX!82eC29^M!`d5W>fM1*Mynl#(*u(wJ2)H=3#f#Qg>?=Kb;tL-SsU;G3&Bl;$^ z`TpmM+C${Ve}igbdW~qT(jGB_sjAdbKNU+cg4%ate6^sLM}bYEmvdydmN8zny+BRF zOvWPFw^P(1yBH!D@SvMNQh>4(TV^->(wX@?XY5`Xc_S0&ZvP*`>#A~bum0g~u48FW z#H*k*skqosdscZFs{TF^o@5$oj15E3|0q)Ohd#D$Lr_N3uZfrOf!H^Oo%J%nA*reOhSn9t)-$6EG z!%MzO#HLC(k4n_bfIZZs+O&kLk1qq2Jp1oaH3Sf;RM>8_IDG38*foKY7 z5a`1E1~cQY!F1t%{h+VBU0{;-_cO2gNM2N`#LH(P1u4Fso3uMT`>?zEOj+Dl2v>&G zP?`8uy!eL=c{w*srC-+9zCPfuqM(e*>l%yWPH7~BvH&B#Q;8{HJnb|S70^1Tz-|&` z;-7vhv@zb3DP~Uc-&LL$OL-~Sa8CzbD~T}~F;2ByPYxHLLSk~b?J>op)_V&Bw~BuW zrzj9d;T_M#vCzfw-8rqim^8nx_S<4aeWwp4Od(URq?P(Df7`vp zFec&b+^a_}m2RkEv`i2+E1lP83ViH^MEW(tCC(wVX-I*w^1@x_A>3Z+Tjk96MW3ZE z3@0%v-N{sByLLm95K`X1GAhXO-VIAaPk9e-G>Kg~Po|>TWgf0(g;va{k6^|S$etjC zFG(6HT|RN?Zuvzm6ML^Puj#kulqM|A&0f^7CupQdGfHPAu$tdDYY3K4j}%{={cBd% zX``Twa4J5h(OyY4YPVlro2Q6g{J#vfHsHI@Kcx|Fs-i%ys=Z>)!cT* z2!hC^m>N++1M;Q~{e{a7a~IpJL(w(H}D)k~`^w#F$K|?87jnJ5KH{ugzrW!tz1be_1 z29FIRYGl_^=%;Y=?`|^OxEL*N^<hNmB}hFH}Txva`81~zr}pF%sDPcZ;UCc#_%J{SlaGl4Id z1$&CBPrzQDmKpJ;Py=14!7VypBPXTvJX(_wZ)KB(iN36=fxgX0%1b9Jsb-QO%0?~J zY~3o5g(53f>sP5@$Eqp|ld7U(Kr(!xq0>yaAt#dLbp3Lt1GMApe=jAyaqwgRyBaqn z|3phI-jkU5^K)!{G@ULsK7O7=lX&Zo|XS`=2V0VRT|2!=|a8NOkHbWq85MXn=y-e3(cxv zUHbE?L1;GM-Yf$7Y&=nqE^kd`F&a+T>&C7pT~B8tMo)R8GQ08=8r8#-5mNu6xBF;1sD+7;ERP$RxvCK1f1gpg z!glG=?L9Jkgm)|8KyKYJRR5|wwE?*fin18mWXqNJ_d{cn!RB-Rkd-ykL4>oSM;^?g zwlFqZs^N~AW2dypPiqake-`oop>25k@7W4i|A?%I=YnGYW5p`#%H(00G?PQeKSS=n zd#n`~N52_*R~l+G)&tq_8cQGRVj)lYvt!^wQ;>xOsVqCv+R{YNv%>fkeF4P;A_@&cz3hPLKtOMCWyHl*>4oTGO*{UwH~b;Z&fA3N&j)^lS_z_>o8s4$jKX| z!yT@J`)w98J#R_1scitLB3mBNBjDpvIO28~38KRm5zz8jnESSVr0~$zmaEChihp_Z zm{E2@Xyf9kQf|^4tP2Q2EhA*sJaoK3tlD@nS5Q zl240R>2dXZH`U(y^a4rW+*sZ09fj;o9SJScNr-kMg)baHQ-nA~W%-Pd2T6e`$4Bj0 zE{v6n%U2m~qbIjfP**i=wNK=AV?9lh4`BYHa}_cPtVi^){9D(mXzF5qJP(k-E=DPq zs9Axjz>;HfMwo@WsA*QyLW#R9T3zd;&M*SgSpsKxSUpH}Ha}?Pm&BI;uFIwT5r>Yv z^+(t7b%3)(N;|ZZ`fONGo5IUt@~|zgd!5P9dU~t zI3%BFp%F`HL6{H2fQom;a|}B9J=wW6>iEV9?gN1vms&Q;!x`2jJfwvOhB!iwhN9)$ z13PE7Krs>VyrXUgc0L~*i_d90&NIA3F1VT}S0oFOmUTQwoK*&c9(6H92Fv$DO}GHM zr>Q#22r%eD_YyjFqXgyd2I*Wk9=`!)%UB*s;)>(9F`KnmY-(;OJ69$q>nh>HQX`hQK6BUl(`e&}@<3`h0FXY_Xyx6GG8nq|2Yfz8G zIh#@IscAM%CsIl&mQVMam&RFa;2JE-((~1f z=f23Ev>JRv$nQ$*Ui8#@mF;I`kM2~?e*=5?O+%hl~`xar+S1~J12InXn_lu zJfPSwa0DAnr*>@#*h5S34$H+knc%wKQHAZ`h4yrmZk|UW0e_tRN-ipfy zTpPJHbQ%?umQR;qPQKfX9C{bYUMYG-@Lm3d&$W4{A@p(tcj+QU!!~@rT)nhT)M_S; zzsN#BmtYC2Y5q{c}()>3lnt(MolFQFnzd*tk9o~%(d5Zl?6JEl!=KAtOaz+P3`W!=-_ zzSP4e_|(pInw0X>#TFAv90zelYxYR0t4EM(&l<;e=hNkrRpx(v%UOZaxvh%w$#n}Y zOB)BPL()qI+>NIBWttIsk#%tHO=Amu=pOsC>)Tf+&?`4BobG~`twy#y~}scuTqy| zo$ZIjhupI%hOsT_g{0rSE>?6=;r0q=r?bmQHgBHIz%>A8oetCj*C!wusE6L5S;fjy zoo?mB!y7{q)tmLKZ8_-PM&HFe|0JwGvV2iOs=1ZQa^p_ikm+U=Yr%uC0O4@&a{iMm+4y z{*mPh)#dTc6^YEXy^TBr`NGF3ateSS|Rg_J{m=~N;Ac!-bxDB*`Gs?2BR-BtescMzUVA~353Zfi} zp4)JBRC@-O!>qw9pK=9fQ|%Jn$iI8Nasl7OY24;>jM#LKAwT3Z#ntSj755N**SXX_ z_WV&32&#!rx$v2gZT3>Ceteu%t+uzf6MN))(5HOR(&%|i7>TnNetMaWg#3L$S5z5x z%()QS)?4|5!LE!lXZZukP%rIL&{6&G?Wa8l7u|EH_D?}2b7-BIe{$eD(`1U?wZNWi@U=QPGQMSNQ z2Y-I9SqEW$7mD&ig$AQ3txj}E+uoUmbC=_*{Ww=jPZp z4l}NYuLk!slH}L%XUS$#3O^I)Ecz4`+hGeh*~gxZOWUV?&Yj=DnS4KG45x zTPPJ`fLA%C6`6d?xzjg^;V-wjy=5 zv^sui=rEgT|2e0hGZ?4Bq)L8vpki!xplW80fFazgf3xbenX`OA272N8QYDe=C$hSi zH>Y4Q$LiL}*_`^FkTQ1dJSyzUyDp0S1-e07*#NOUY*^+Okdnxo-SEXj=*v{@5q^z+b=6SUfa@Joy|ur4R@kP)HcW@d!<)MJ}c2T@(=0dUPEnH zvG{4lm(O4Vog%pf^?pPCDGExnq%FtJ z@8hhBE?_2!l?@><6ugczrHAYHT{q@YLqV(S-TLXU&dH+{g?_N1#-p;x*fVef%JEch zWjKt%*zkB7{;BTG(AbZ6>M$nhO=%J;zG2Cu%g=<5i9Wa1p!E^4LZT2^gXabD~ZA$pFwv~h3_`%rjN z_IXaNQ0{LLgk6fg2(KG~S0v$HX3>auP*etM5uhP0r@GKNe-6*SQBm!q3g5Y~SYy%Q z8nRyY=t;jQY}{E9P%f+nXwUVqt9SLbv#?8d6;)&`J=wy^#ALf)hl&oQ*p?Gs$Sre~ zv9q@;bG5Y7v`bqddDfVn@8EA`Xd2)Jq-Xj6JPIcxwI+61U3!dB(3ZBFPK!GTiaF<(w z2$x6~V6jApq^J8rzJkQgy@{A$w9i#l_;f78ZL?U4_=;Oc!5bP&*8pr1I@Aj$TFB2R zV2)I3Ces#i789pI$5**J&(@dv$|Cl&%!Bi*i*HT0%ns#u3#Mzj1Sjk*wc=Bo_6)kR zj*Agv>MadHGTKvNkGRU>&{I=mXMWk(B=!fHS8`1~g<0Eis8{-rZmaT=Q}a8KkMK`1 zjuqB>1oyO0SzmgsQ^38#oyAAPr{;Dp_NjP%v~Fd}go{VfHpRvTp-0VS{?kQZeTM+S zze~)!xCwzgZ0Q#>oS-}}QduOKF;D}KREE%yGy}~StKSTjq@kf|1LY1*tF)KoBSBpQ zBS)*Xw$F`T+yn$jN$3M7{RR%o&tA`+Uc3ajF^Pa6#*l-#!?f_#*XM@^6SDrK+lH-|yNwF`DfvrZT@}d12Jn%F;N~X-f}BGs%@924wLdYf zeRlzyjK?7Rcw{e#82et%+Z-le>0syY)0IqGlTTCzm_AqS{p-Jj=!MD#_KUtuI5N^q zzB>wxE;0uQ_b`G9|ArrA$o}+8^=_ z^c;Q?j^H)m7}E`uA!n;4O-odEcDxp)8DR$)=ajTL1zbcdAd6ujrwSeR{dBl6ANc}l;tyqAO954;&ikW z=QF1Cd2!i%)0;xN`S|&BX+UMPY|F|)!Azyusrx-+3hKU&e$91rI;y=##$f_u`MPy; zpU?Db%<<8AZoF50WNd_eif#}HKiMy0v7Jz1yOPHWAMdl!Y2w-VQa)-up5a@ygjCV+ zQa#BiV)M=d42qQdwDjfR#3m7R8teqvS*Cg03~fzbAj#>Ce6O>R;xq;oY3GU!VIE3C zup5!pYLJI9WR)*xsSZ6?{lQq=^3N_FsONh4_!E<$r*koeE4i^AO?WZB9e?U2JCo}8 zeY(6r-7WMPW4k1>)w|KYcsZMi$$hiHfFl5g#=qFJ<}2}d2=DJ(@1PJ`d#{O0=Uoim zr5)PSUKYg}IagsmLGRFxLpk{fyI)O<^N(?E>&j7x3wK8L^4Z{R`+-b%Kj+97k#41K z8<$!=1&Q2Mr>b|%?}4Mlps!95WNmW3E1lx~vfPHBJr;S$kKaZ76-H`&txfhmebEIc zVt3vghu+9sp_+b71WkvRcpD0cz<%)?e)M2w`rs%`?iaN6zfWvh{24A9Ih@#mrh2S@ zsd?#p&!kOL!S_gn`tgW>*tjz}n=!j7#Rhq4hbLFkZlj(j;4KVa3i=^pS;}2y6jg0> z^_#by80({_tfA?sEV60;OxTAK@C&(*NKcIaxyFnA;qcss}u4$#UJb0D0e4s%F<*cHTZej`QKGa-DHulw^9NT=xr5uIj5+2@F$usqR*1M38 z(9VRyp;J1UZ<5R@w0U7(PNkeA@LM7mkIv!#SXlG?LCU!P^T*&D7=MecHP5=)xBUGU zQ)?a`gp6xyiP~c**0PXtR60Xzp5$*P*~*$nT_joq{dalO*>5S!g}UXmvK#G zTnbS?DeRJdv(&N0Qe4Yz&RnJUiwXX^UN#}>l=a>wn+*EmItQbx~H zm_JJh67Y`FeS&SIId7zat4fc)w>>qL&V5xW_s>w}H0wec6~Y-CBtGa)?dc0=bf@>qhTAN*i1xNsEir{H+Z!UV z+v>KINkA9(4(hRu1H!_)Z4$FDDfu^Ao~s#$^_-(d!3lF;ti~@sX_b=-AhobgF2fH6 zov_c{0zvJF#N%>qWd@a$cx8H3c(}!$Sp}Mc9POA+jocP7ZCj~Y)*l$JFOiejs(PVRMX9=r3zq!1s)IMW9h6A9=OVTED)xWf5!8!j_%5OHN+k|`s(0Eh2 z19b)=Csu`~RwGmnReUpW#&M5kdHkw?5-`?e6NdT!KS-e1+|dknavAL-sSh<}Nlci7 z*_euW`-$ds6RqNdA>xcd&w_$sBdKJc$qjhPwGv4TltNhiSSqO1zKVSd*t#TY@)T>9 zEXxvgOIG&IJ-V{zAE3JY$Iqi*T?boE>Em|FTSQDUwxQApJ)rk_9~j0LJ05ICFXuc- z8uS1oOW?0R$WI5>6+O)4E>t7+V7g{SPecDH`x$~JA(0s*N{7H2fc<**`14F&p58F?JxwwDyc*-m&7g=0*Nc{{1c&|bOs9~`XNN}NnXjIP> z`w6F_LByzyh-S~!`#Ip?kW=5(?ylF5W3b%{JK&T5fbgDOZQ($lG7@wF60{62fvhW6 z>Yg3b!d|Y46kWK;azvAe8|OvXjqWn6f$1`?f$wsTNbVu#AfdE`-@g{pMyj-3MV5U< zmN^4Myq!r-71+=~Y9wft7ZRVae*|C@@Ct5;V3+VHu@O{BC{<##G_-#@IM?VNjkzyr z8Er5)NxP~%JEr#alLz|l_D$^Jlg9e`oPYZH@lu045*C(c&^WXIq#t%i+LGpWisW&c zfM{5U71Ha2uBcviQrsv@d*R z%fwU{zmmU3Ku3CoF#P1}t1j)qB#?jKloqbO+nG-CUz~8@8wg>Y97(eAtjFPD9m2k9p*ABZX^MdbO*0 z23)!h6FzWeD(%Xo=mk>au1bX9BAH(%`)Tqqr&awVol$5*T;2;9pIxt+?W+zHvxcnM z#Ynddz&-gSa%$Mq+ZdQePFq3EAWRusrrQW~O>0{_8!A8HEGek?~y@5ngaevUJG^3sZF#4zI--CXjthMQ}!O3M}sF&h; zyiTGCWjr(G6JDp^{DL|{Rj6xmoMx-@I>U^$r`}k@=pzGc@)FHvc0NmB^{L^uq>cMKC^NQH1e;#~TwDPV!z(JwQz@9FCezm&h07-FN zmbm;*QX%*C&0lw`#J^TP{X}ZMn0l#p;m|vdkN(h!n6qwLiZd#;?OyU2Y1#CyXO6O^ z-FHRIYa5ufpYcQv2OF-9vpAu3gn#V=+%A!W9**iducLQ(ydP}jT+F(!&^BmB9O>F@ zej>gldu=+l9b`k^yWAh6N9jOX`}Q@szf=>yTT1hN2tt;q*e}mTcBMTIO`>>?Kr zngMAG`Rao|^8HGsY1qwTnua_X<(6jtNSB=rO&CS(W2qx>uMCu#)@;72pC7Kj8plC* z4dd82m%Jp5cl%nqMrn&~+svB!(UI;-90QGt|5rl4(vwM7y4qFC4!StaW@L6fLlznM z?<{Qfa_f9P|tzu0M8`L3h2t)~41 zwEzfi8T`oi0_c4ZYU#=>Z>yc&#Pr`R@Moc$oMD+xObO27S0*)~_{P@GXOXqtK5tm+ zvTaQnnY2dE1b4?a*p3g5|VtlP|?|jX7u=S8`Y~qqX0n&N$KEGle>i zWVf$DP1jZZfckye9^>zLGX7zGS(ZF;yLm&H;<5uyN_NhH6Y0tr8&wy`@n27-IiV`VKDkn8-vZ-iGk7wOLLSM(;) zypBC555BGwd#xL>UsvM++s=aVT2X`nzBPTNu3#o~aGsJcrS6fXO2_n^Ej?$emg7yv znQ_VyjsEb2oQ~{)3Su&FWn?i<`Yj{;jJ^Tc$_7o9Pz@i%Pl@O){tuUY2CfuGTFE1TE7{C#7kC@QH4ncaN{@tKZ%JqZ-4g8lo}(My8fG z((+Tu2{Pu!G~r{2rUYCZJ8&1{|K46Wzr^m*GB!*{6Z-ndB=b{hsBSR$Gtl@{eVwYG z|7OMVJqo|nLUhsL8L8)7(0=BH1MWsR4y0ep4Si*H1P#EtNjV?S4^ z<~g@ub_d z3lwPYVoS1bYRJ1ldfR|%z|(H{NH($Xnz~F*-PA;t{9RcaU3y4yIpXoWn_+3#Fzi_w zU!i$ZQzz_@cBE79z%|xoR&neGj~t;R3!5>Yo-&ur^1AXadVV+>^F^?QOUv0TALQgd zxhDK^Bd2+o)J^Kuru|IX1C$7i`!nM627h}GJ_IQL{Yio%cA<3uKnmZj4OV22;| zr2_~~HtcA%8@XO?2H$D|3kpQvTN0<4^hkbwof5=!QF^N6=gg_v~$S58;c`z?)e zJE&R)6UJ2;F2};aa@?zTsKd?$qx1~dNX@$xUu2_ZYd#15V>{uB@jA?*p282MXPW6c zn`^=}E|!d|UG$BvRX^)$eOf%^^0R?T`j#rpcYngizPr)-cr7+J0ETp7EjV=>-a&n?6-Gz78!f-$b?>cN%K7R{z zJ)p(Rbm&fXIkP1?a&2{`mF0|byv6;sFxfaXx7qkzMiD=-!Q1S<*WTa!Z#Bm)BmU%y zNw}w0|Kn;jPJ;UI)k?bRR&*wN*5aHgC!Ea+7c0@;1^0{fz>}-VG6_vX9j@3t)|T@}4br9Y^{qr(}JVSKpOT)Dyt*wEwhoArCfq z?b7;EieKhC*r4so^+^7Kaf12N)6sQ#lbEpphC|oi*_KM1LN9yjIvQjGJ(Gjm`V;LW zyQ4C8+|{c3r$yAVW4cBdafiCSpo{k@+OVcn`;vRf#d+t#gY);2+@M~Gh-m0q%X_MM zl5)-c2y&r7whF?LNuddFVq5YkDKKB=CG@uHT9~7}WaUUPxmoW-n%aAC^M397Cf(l& ze2_l=W4(M}ZRO$JDSZU#k9A%|8-tMdMa^+@dMRgVK525qDqj8y1asWRK1<&7ex)ju z(7($FBz*2RyjZRG^_)1GQGWS<{pxt(6Nr7hgY*KuLjM!gsP{7xgU5`2GTxr(c+@== zD2}NVOnqX&V5fV8Cg!b_6Fgm^oe!i*x$GP-;_`JU%TF?!njehLr>8aUNisTHzQCVo z)ZJMv+s3fvu{7@LuFI$@;dIG(xF24B8}or(?o$A=t`aWq;CHO(?brO(sqEog-ulig z;QtRSe1q8EzP6XJzA3(b{N~=Bb*7cEoE)34ZbnA`f#q+2{vVk81`Ac+VBi}Fj&j_= zHqUyv9!!2q_QUnC_r+E;?YBTXR1(afSLtzP8-BFks$Q%_r!@I&xfy>0jf93HQr!U$ zrvp>XSewo~ZA#AD;pGB+TND2FTdZ%;v-!=r{)x4DCwcQG0N~#E0d@TmFnoXhfIwHpmb->L+fx(J<-ZD$c240GQv58^RxbiYcz75 z&&spnHxR^TLrlNjTL$3yZoldr6jbi8f95}Q43GTov3xJe^`7;QZTWVc5I)Ie^_}w2 zJE+kDSiU1(ctj|nrvq43_((p7)e^{pIo|UEy2UO;8FzdA}w?tYmD`aq4 zeKfjfKP((mUzAKm{t>j+4fr;a8hzF~>%Zv~<8K=Cdqz!hb}pqy(|_ncZ}$Hj9RIV* z{<9jsEtzi%;P4%#qyN<$SUpesKvR?SP=CCo-&N^uU75lTFu5?E>e%wo1YXzcE_cR+ z^D*CCOD%?nZ-a+ZhnwqaTxd>ta@i~F6V;faku%p|+c(3NrocophW`s!g1rn-W>^T#p!s5AW|TJIDG z)vZK(aQb{z@2pAhth_$blaXKwkBSD36tV%M_*N;U#WiG*9CW|MxIT$Yq3lzz9d0th zBtK-T79#bMrgQm}MQuX7Zo-mSCsr-jfPp=szP(IP3V1xD-KPlaZ~R>cozMwqLWUe9 z?dzC(qD!grk=Ocess`e?c7!;OA4{fztB#9)h~O40_N6)_c`k&Z>rMXaHDsSqwH+L_ zo)p7@HLDtnVN-@X%0`cbvz_6hj!0}Zp*INn?M|uLmrPl_vI;J#*~d&De4%G0+_HiT z1|FLFuNSJ>J>N|q=mg@wQfTg^gkGePj%c|PYd{jeaP}yBUmcJxD!3|Vz?OYu?T}D< zz)-%Rg9O2zCb%~lPwaYF?DQ6-NFiP|oKN+(@_gBIeR;CzSj3=~Fw#g9rd!BB_i

    4(c!k?5;dk@Xl_n1qkN7RM<>Z%%UoR6MM;Onv@0hCddS^9-5WEyhW+Oj;RX?q$ z!1@OE_$xE9Sd$*xKKe>VYrR(`p4$Rjl+1b6^M-Xk8Kj?F-#W^|dq#Ewo39?8^7-H1 zy~6-cPh>v5fG{N_$R%bFfI1*gqICOu7@}fqm$j2bkghVXLsC7bE z3Mp8WC`6zNazARWSuouf{z@InrQpWRBpOqRCnsrAwPn&tE`wW$2POR_PgK-Y5Anj( zguJ61JCCa&u~am7ujj)(V>gz&`%AAGiFQPNr<&-S&WGr3Ik8Sv6;lbG)?UwWm(8F+ zX)UR}xFp0O^qN*r@)Lhdf-0}hO1j&Mg3<3V_)bLmj?^7>eN$Q2f9Jv0oqgPvY$NY6 z<8Z3<2y2b#$SKZMNyWXTGbMfO?XT(Lv)`r2$v8Cmtf;`x4khZBS}&ZcD+bT~mivFP zxtZJboYJRcZ@EE|M+J#Lh!bM-l1GjX;@m|9{QvZVltvkYsNy5&Gc6^E9-{DU-GPjQ zAmhYTkR8H|C(g+1`Vmk|DjRW2$MkOz(Tmv-0^E?(NU5CFt{Plh{wy~GART@4iR3ctG~M@m%QrZ=#h9QXNs2u zm`;YgbKc<>deQa-AG$g){Pc|z%;gsx&-uZsFf9u2n+%*ghI9bUBE=pLUKC>Z7a505 z0NIv2W=!SYrX8bwambU{tZNhWPSFN?=R2wsw?^-JU~?JoIosTI_rv%%k!VTaNKhDM zIhv|MQK>3T!Mv(@dCk0 zqN3!I|6d(7Y?hLW1^cG#fGZdqC#YU0ImEG^zy$3K}Es{|0!594H3+(n{-&WD^a%m*&fa8**_S z{JBXWCt+FiArbQz-TmlDaHj)q+4OvT*J! z{$KpR#h=PF%6MnE@d)MEV^M$SspDDVS;A7H6UFH@T0~)NLQyu6NLmR+{x3mg(MFgBZsST$E?E|Aa5m% zpXr#xf4 zPl-z_>&Y+oNX~{Ft9e){J>uoZrWQQSQKVR98rRFk{-IrpEo@TviK&rtN={{3rW^9? z(3N@aPhKNtUU5%TZqml`-Nx-&)ObL0V5C&}*)6OCVtX^iBKSjOv%*e@%WphXAyk3P z*p4g6B1Ci`&#C)p;TLF5@tr|&G zPE%HsiCA<(LHWXqI!YcBuR5dQ6dsez{nT6I??%QBldX+L!)GzLriXVs2fSbW{=omK zZr~2O^QWiRNzaJur>DsOQU9;?FY}9N=LI0;@IU?p?WCuq4Y-qV{{GZx_v!h0^sSvf zJsG~|vGgsX_hyJ<@x){co7T!E!y;Lyy{74()0gEP>}%9ltUdf{(e6aGoLLVv5bPxq zS`Bsvx4lM^*anH}WMl`$282GVTvCr}{Wsvn#SV#0EI!rSza#k!`sO!noUn$V-9-2P zXQY3|k?!4Vq@NK4zDBOU&MrRE7ybqSB>3uvc<*R4+FKroJK;k}vLgzS=0X-F&kD^$ ziz(3C<9gNXBnBv$YR!B)sb!XF>FOr*4DICOy$^hitUn1g*>9a~<4P64CW9J@z)%FC zh`{6}qKHrpeIqK8AheNN<%n-+CLx0NAq>O(8W^XOQa^anzBuEoGcq$HLnv6Ya&dB} zo#LTWvvQ<}l;`u!lufkj5(|!dP+y$5jlFH z48CZXFDj*@IVQg2pBVR?OZy77Dwi0IUj+A0=&671ao*q`Q*uXrhd$v_DR7Pq%e9br zM?TRT-;+P+nWkK12L2YvK7Q2#6-!lrG#CAjk@it5&L11}1YUez}*v!7Dyt=U>|@S}7Nb@Tz^eM^FlX z`0B<(o(m!``F4{^#)Cg-i!e*mJU*d}ICu1f@ z3W*1WV9vfr>9Q1ResWSKYMZ6f>2(}PA{ahLs-xQu5FDgyq z%v0ZQ#RwPL~ipHI_{7~8kPSt_B{4ho7znR-nG;IWGcNL}o^mcv5KG2et zy$RHY8SPryAkFe#TX7!jVb2d*T8(XRF=*6=i%O$716h0^&Q;M<3MhOuKZ0JVmHo$C z(UNX?OQZCJ3Ex@HcU={sHS&?R3w@M6NSV}nYx2RWReK73U>~K>hxVCdWP>#g?Ndpy zLa7aDWKkpgFzR?@QB@2gQl+Q5tcb>?p7pP@j?7r}Ehp8)bY@Z}wIM|lwY zW+p*nt_$N<+>4p%fhp-7`-QRJr!=%|x8ixlui|CJz_#ffzgC`5Pf~Yx2ZFL8t>y&3 zm!xx`2VABshx(+c6Dvk?=E9)D42{g0D{0qm@yaUsy7@Z@sU(s35ZeEHw-KhdJoT3xkp%2LxA>_Y2XN=82c8~q3+QW6 zX;BHcIU)S4c|l!`@W`Hee|vwco%TJ61^=Sw%5i>Y|K#lbg2*-{^vGVB1*+aezLQ?2 zd1Rw~ihhc6@t(Uc=bgC2^UqA2x=YVpVdQO43%97;&wAK#746z_#5p)|C7s&o3Wow| zxH5m*3CHp!B3KH*A(h~CO;OylV(yw|huDsR3N|3U%aYj5iIVqb^bJ&sA~zuMO=k2i zc2$Z#)gx_9SnldJS8`#Ut2O&0n~ROiV=O?>S20%=(hU03r&u3{O1y9{&r zn`3Q3P3CX-X*wYrVQmehino9^oO0v>m2z8N?%L%pSVpmzmnz>_sI^RgpO9-YmC1+O zNWvnWXxgDJ4)8@19plR3yQNaH5#q5@IiG7~3ys{-aV5)_Zm_x7A=yOZ|0&gQS6(PM z^H0`|Cf0^~pj5~cbjSJ_W8GD75t&%&E8+`eI5MBov-m_?F8MsvoZ@8n)IV$K&EZ4* zJlv?|JXgpT@kMi{P{?N;Tkui<^6x&e0YFtumBXeBg@G%(vf1FK4jAh@D+FXOr?T0i zrgwZi3Ez*#zVP&v?7or$CEP{akd~i9Kyb6~@WAx%^WL$UvYQ&5x!X~^oJzsXC9s(> znESxVjIg86%4dxM(~;STd(#&aFf4rX+Q6>9@cF)%btxZM&km^RpNLsf-6ra+@H;Xe zQlPI1GnPIDpwXdcer>6e=>=^?ZHV-2)J&qRHJW}rQ{M?}oxKQ?@RLLTQ(gqXUkhws z)i194>4d2jFTcRyP|zV*g;N8;1_}B`m^0WA7a)iStmfG!F(&(E`?M!1rZkB=1d!Iv zyY-h}qc1o`;N`xXh9~xotWee{_!qHRGGRABH85E+;Wt3Bu9#NN_!Y6)Gr^-nvohH; zId-{srSbX8UsBr{3OgCj5-;28^$-%QBiZRG%)I!%H+o9M&*df@VF~Q-0YlB3A7lW|+Sq33$i=b=~1R%4Vln zv?~09hu;OQ^Ai(-`@!#lK(G1LXKLf5M-NGxyMX~dZij1L2C*m>i`~MF!#|Sj(W@?` z(&&AZfT%6b)O_HlqS({Djx{9L`!4;#qHka1!x-^H6BK$D1Px@&ukp0KPgXmp@f5W9 z)Y+o0S{HcQ&fQ96ZV%S!2lAud0SSB#0DoMQdBM=jr2jR3CrUdJsjdiTeZ$*43*-jp zYVly0a}AgK}`;j9)PU-*QX?pKjt5+npG62wt6cHL5$*Fh? zyG}t=7xm=MwJ7@PWAVNk$$5q9W}Pld@ny=^)A;=mi6M}4Mddj1{@u4Bv*B5IFIl|#>r@0Q1a4x)XW$8r~nCNQ~=If zW~R=72Q%+rz@PgrPG_Sm=TR*`M*R=4NAtH-fq=ern;nDHcNEWGwul#NT1YO zmj&&Q&KnC-;zR^458zw#jI}vJZv5*UGyc|io9u_6(d;NovHnv=?3n6}Y+ZH2uhN|Q zv=c;olbgZPEYkPKdUro=vW`Ol#|8@>WlN&7e3gUoNmQon#z%)b*G#>hyk*&0X9yp( zcI-n{R*PQ-54PV!mP~K)^jn2hQ{CqtR!Q^|U~pnb6$kSP1}#vBTMO@1 zb*RoAby%-ab#Okz3TOeFjfe+|wnUo?!J?|B%y`;H#AHVY=LzZzpQM@9`%hN-6Dx|g zKOIvJO4SP-S`;@pm7~*3ui_clF@ySP$t{htt6&v;){&!HjV2SAZFNSKynY12L?GR> zUGQ$5o;Ysho`N^BPb9mRTVxJoTje_&%q!lVg1JqaCdOII)@>yCyf)4`V7jLQfwC7e zTPJ_Xbk>RIUg=tls}Fph#M4+Ck~coUm6k*3T>s8Zx+?ro03R{r$GQmL((qGhH}zlW zumv%7-bog3SNmev?NHn?&`c(Gw0R=j4Y=;;9om`|*?0?W3REw;WKMCX*6=WTeX?}b zGWxrweGpw@6{F7&+q#_r*SVAdx_LMSwd@J>liL>2EY^A;X3^>*{~%lMTEB2uy}VDJ zFt`G>?CeZ%U{dj1P5+6U*0oKu>(`=Alu!RLQgB7AnwQGae^*Ag(>KC+hJs9X;1GTHsm#lIn`!FLgXfmyG zze5Fb1Fk9M;!{VAyyl0PHYfy*K5{N(Sl>z?Seb1 zswagE(=N)_8Z*kOqr21>C~bFrz}eEPH9&KhcN^I2+Lhv$(v@(RntQJc6?3bbG;-Sf z(Xc+TO!E@(Sl*;gguUOj^5vJ%$$4advl8d}We=r-IvNK2pd&7;Hc$c)Ui~(KwBI=`=L#EJOrC~18rJ+*GxIr3 zZ*ig(UfbQ^&;Ptp+~&LSpX6fK^{XermE-BNRp?jOxXWknAaK9A^bym-Ltm$o**D1V4n)AmM zS~iMC%A0mYoM0YZAfH#kK4$ceJ-Y9*y^-1#e-PQli1`7@k*2e3JT*)kb)Qns#4hA~ zjVwhSX!EA+X5*_@U zm3@9BS^N`{s8FH-^9Ec05w}GnUDU;?MP3HBN~R`hdLu-X1q~itRj*1}(iBoe?|_#T z4n|(ZTO`3lE5)OqY6*5!CHhZ^!D5RnoT;bx1Fh6zi&E;2;Ht%ceL3)-WcQAT*gyYR zsXa4dc(&Oc&f2Pn!p4Q1qKf1Yzdas^bS=pu{`>S_Z+-=)JQRcw-9HF#R}P{aMX4_5 zdM`yKKQ~0A6awhrj`+uV)%eY64}gR`;}1KS7M?6ef$IzW+`Uejnpk)qcW0>!Di8={ zQexCr7u3OX1#KlF*?rl98$1kz@Hl@;3KKvbFQVA+YMXQuhQY3vxz9uZOUqjGZh(hn zNOPLqKxY&EEb_BIKNdao(%~so7LMHHE_@ft@LY1hWen9 z%v!h&k9b7e$(ZeaXI%Y)C0IJvFZ!DX018I8?2>aPnxUnzz4o41+QTp z(7|Fg4e?F{7TtHls5g>xR|pjasw1mQw$U$X;;ZfUwX=6ZwxRZjhgw-p*m2x%TTQsw zTx8i!IC4BOD2o$fY`|rOB((=!<~?aB>bCvM4GlCn%(^cP5Va2e)Lv$Q+L5$LEm zZOh8*3NFgqxSHu%P|mLS$qX%drbA31+KqC=(E~sS&azyCNG(CtbVB8?e~0+qS${`y!X)t1$&F)pJF#x7MaQ zuuOQK+GBaEr@vNRwv(`=tvU|J5sDh+)+Tn= zmlpRUc-$#57O*Kw*{l}Yq*)6(|GaimckiCjqsJIB~mraO!9c?|BPd1jMk@zJ}$zke6vtYz|Vg}#1T%wc(mJNTPI@pZx zm{)%l?l@o;hR|7*Y&fgBsI(e}P|tuXQYg@V)S37gzVg658t181)3@C|Cj(j>w4Kyx z+-;T+Uk%kxwSoX|nb?K$)iGLfle%|g^K1^6_z|3d$B6H2I%4`R&v97EwYsO5>zDx~ z#Ko*_mm%hRFw)X3U|J>5{LtW#cG%t(cXEp@g*PpQ={vVr+7{=Hx2cDbge zKSjAVSs`prg=UD{OY9m=^?qt~be!bU!Bcv~h}{J(@Q;uN_pfCq#G|D$OCAOGzhu{S z6ZRKn!^b-q)SL1tV>&~x%kZn*mD;tLQYh1EKV*lo!K`>zi#Nk^dLH+VY+bfBl@FYF zVv$bu8_u9D0vql+sMm2EPXp>(pNKG6W zbX0gxD^mCkgKaGjg%(Ynqh8X9p4Uo8Fe>IWmQ!sNM@|-MeBQ!lAfj}oDuJ4P%;Nmh zEN+v`_A)afwTs%vj{Tqb-rJ)FN+9`pY2Dh(JiYLmFd2 zkMmLISNix2C9J2H>0Vqyh#B&5Nw1T?$PYd7?zgFceQ9sh7osJZjj{10h^O`}Ed#Fd zE4*+VJlv)M+y6xwzxR!c}MCsEa%KECpCMd<@?Pkq#9zH+NaT=CorG( zo=vTb_{ajN+J}!kw?4N2_>2uaRs3JJ5!{E-HZlm!i+d&>=|`i#s~2>Rc9Pe%C=xXl z-Y1V9B;Rj4{AR6ak+!iq+47%d@qo^ZwYvnHnJ2o4xu@Ph-vak$=9%uIO`Xl&Co2Bl z5$~(#<;i1QN_;M3G2;+>Uj#<1cclGP)*bZ-_LSCr`DnNr5$nC*|4Y*blX=UIT%BYPO~ktX+ZJX8I9+C3lVQjd~wOi3Mm$uth(zB4Z%(m7$ zm$u2)^{u)ZVRx_kvb)P^@{-rjtJT|ALw84MS442br?jFFAoli4!+j_GG;`jkv!Wc} z^j22A-raCz+K%@+-I8;$zU*4|VW{C)iy^7h?VUgI1An8WW70d}N~7J%J7vSPW7Av1 z*)U~BZ_THG_K?xCXRAq605>a->Pcs1j0s<)%aZ9Znx>`sxq8c+a|7UXb(OIaUk&aZ zdF9%H^t5HEcg=(M$7be=XD-D+*(K|Gf_2C#3lUw#YvqN>RQkLfV>wm1gW`xpCB%~} zv)0A7__BIp@2`sP%934rS?z@f*MjQ2Gi#<14Imx)B%{0?Ye2ksz1nk6StT$uh*oxk zPrG$}@p3NcLKaE^tadGdK>qjl&NsPi);9jMJ$i~9$`Z!EaPD^l(Zz(>`L)w*r__k} zavl7b26K%oDul_h(0tAY$RFMV^=Gxy=tvnc9?e0n>B{^}#~Jc&PJ4N$wx&E+%pp&y z(;kDpxo4e{Z~#`FJ2yW{euwAWGZrh4L8qk#*2L0^N526Uou^CC?z1&)0h9D60)!kk zdXE^8oeLV+SQnI;y4@zEX-DN_9=7bco8OWFrgUC;WNVJXSiJLm2Ya_MvNNh~WVTD8Be)X znmZTgR;NN!oQk&UKJSrqTn(RnLhE>oa;{K17T#Wv#ttt|oVD5wT&{1NP96i3FBooY zui`o)-_Uvcw~Rli33)qc-xhd7+AUuj*5TVP0^t+i91E&@j1-=hKLcXCx8y#CRJ-m| zl#=H4q`o5N_0YQxZWwXEX?@F{54Y@Hj%*>HQ~W<)V*SLl!IpZEK2dvP-jZ{|U z{(@)p2}p6&uU)bp8a(n|x{i`XSPZ57sxOy*z>`vvqJfrD`}qA}^bZR@n2#uf=nYCL zwkp<|)VXeb)#om0)z!#XOr#Fm$Tp!TJS$d*G-c1MTQ>2zhj{QG*B#25P;;E~U)s3@ z&TxOPEVg*tIAJsj(L-=F5<$uyij+~?RGhmW9K?ofufaiPWL5;x9sB(=$An2TS+a%c zLKZ^VpfLj%rj9fVlhhX^AP3NOawfHgYFndorZq{vDDq4hDv_jEt4;L9dAP(&&8TEUt^b4`LAMsTc7({t~xyW=nofo#yBo_xZH_$=A=)|8y@Fq_b8(M z+v?P&YukVoqPJs!^=^#4pz+Q=MJw#`w44KL<)F?{bv^$k9p}Vr>yAUqs_rWv7XIO9 zlGb0cO|+KDYen=2k4GKnfLDXZj&sRHg?d0)!y;qt$P#0PxQpzSBEZnLqNnV$Sft=H z*(1lW>-Fv2C?fj)OHGY0|C9K9Awu<`^mQbH?UgT%ROZ9+Au35Takm*} zR(^|zIzm{ybe+#HKJx7U2cJM-zY)(>j*1t>32|DyBF>68YGZ0M#DQA3xa-xLS54xu zxL-UZj)=!=GhVHPUZsRSzJPuF_k>|SVm=})^yqk^fMt*2lc0=6wK=X8mv-qBnjUfOI9Q)lE|ZqWJNMb!6!vGDY6yWdIe1K*0t#Ai01g5%dP33%lw6X87Z3Fn1N!lZCnxGG#1ZVL0lqHs?n!lKBD8Zkjk zMx=?EVvaB`=8HvQsaPRai*@1_u~F2CMo|%Nr%>~Pn0H07Ob0VDXA6EGRY62JVpMVoQLDP1SK2)H|%G~4EY7gA-{x@OMV08bL6*B^2iF?vQ%UdLz2Yw_*QHyDu-NgdP4bdon27vm=NOfM564KQ{_$Tk=~zefIo z`8qQ|zQPPKL*%RYc4sq;q^F6N8D&OED>KIYm>8G~%s&t#^N-9w5)<=NW}I|j{}o^q z$R;A6!{?Ard^*p@KgSmkJ3f);;7j-t;^fQtGUCEFiQM@7T^Fz8TSzzG%D0jp_-${H zCG7h}G+DxY5-F|uN})!m7q$vbf?hBQR>3KFg@6zd283P0u&`e^1Z6}xE}Rn1 z20!h|p_ToGo48^Uejj&N647I%nBF-D9Rlf+apL(CTQ#6qz|EQeAh)`|_{HYm-a zK{Sgt(JlJK5R^f2x44)7527l)sskp$uf{fGzQU{j-}?6(P~xzc$73(wfW3Sp_VPsR zBx?g2mJhkJm&CGi^e=4$NCHOyZze+Bmg?bEgR ze#9Hto9nPQ*E8eLj~kc?W`exMzs+pNPzI=6X{!O$1GWO1Xq$3_xzJuDO#Nx#Iy2I=xx`*-F1J?**FFi_X0EbV3v+;5 z(iIAGt-VfI_|u^4m>cX{%-igZ!qUf}|28+8b_wCOd&Cq#x@5(AmULUpmF}2*cG(=VhsA=Af&VU+?wWVlcZy|y29?sXSmRKd z2kk?M-S$1^z4m=#eH64#&rRI=G4Nau*bjgI zVLojieX!rPFrT%LiPn!{K5xJ9Szv2+?=@etkDDj$Q-2hf?K2cMgZlu5`KtZeXTo*+ zocX5x)~7IUU!bs=9fRgY`_gBD&E`IAzGuHrArOg0a1u{pbGwhy&rYjrM9@ikK_kTq z2~y%8!sc_I5R#>oKL=aLeH!~~~i{g5M19B~v1d!%7J-amkN+);w{ zebW9v2l14nTs-5b`oumCN{2pyBhtvHK({^Ts1=S$$Hj||2K=0GZ2M!!^B>}*bV?YN z&U^}E(mCORbn#OdmnPOioOU!n7~9v5#Sg~H)iLgh!>~FA%{t6$$C1^3vi5g<@Vh*? zMmHR`)oXj(;a+=8IF{^yF$L~@+hBAIu0E= z4_z+)KlZ){I;!g0`_9arAtH=OQ~tz2V~Q9Ode0%@SnGix; z+V#F~t#7S%owa{=v+ut9p0m&1`HFtx6cyk^R^vF%!U*BU9;$pnS=(K<+TjJ1!}!%*$a zY9_=juXDQvwbm@6(2-~+t|y{{LuaDHA*cEgIu{)oYKV>vHAVA7*P<@SIfr>I;k>0h z;(Q9-j7|zG(WzXE!j`BTd6CEiSr-voWQXfqxC_-LnIo7ls$2Ygq6XfRTkZ0ywZ0_! zPd2!2hPy|-Jm=wF(HX7Jvp(fpxNme;xL!Ee#KjmZ668cxVja;nBt6k;W4#R`#RaSR&sqz z)X26qk#u2)b@(3nAUvtW5!o8w7ugZ7W#YXJ&o}lpyW>Y7qnZ^y7PCh7#ZM&mFOk~# znQ&Qj59%20J0eHo=dh=snik$1y%;WyRwwoZkrVL-)HGiIdEdab?o7Oi*XYQ(__atw z{APGh^j4&4wi3BE+Y-4syUR>vcK4Z<*}Z0VncWwAGFdP2Ebqe=b=yI!N190$P*gd z%zm?nL&p2Y1j&8kUDSRrDEEGMk@Rc(eO)W@UMxZF_jQRqS_gZxnVEvVaBs$YueSRy z-g71PU2XPQ+*dryI?^Zi80#Ot9Lowf$Fh0t zNf3{h&sfMm8guwoET`BQ8wJ}`KZ=uLxfrL^FSf>P#mTV=#VN7L#c8oZu0_TDV@1VT zF@JG(EL@xui}L!1HIVBQ?FouU#pV|0#^!T9F1E!M;mks7Vey36lH$p+<;8`uRqU_g zqS)GEe{2Jv6N;m;E$pY_xv}l6UpzlnQ@kj)w|GhH5bpuG{wHd4@$y(*@v7Ky5ZU8( zhGV;UZR|Ary?8^czPKuOf#a-rORNz!jOMX;d+ZAC)x>VV#@<*9*Ye^+akaQE-nsaA zyj$_KKo|B|d_$5j8%B;~~;Ho)_sBcSL%` zCq`1^Q(EJP_Taq#h@{7-MKa=AWKcW^xvaU7Vev?0L_Ci9qdbX>iO-AV#TP~#@$%MK zO`HP~YZUR3iSebZ6PXfU!7|0D7_5t|6^ioDzo7gB{RO2_xpL?409vy$Lr>Au^!|F5 zo~`HTqb8|(u5QyO5Kh($wPw-uG$y@B_iMLA)5CgHpR3Q;7wJp%<@zdpt-e98(zi@n zs&Ch8ChgYu>WB0?-74F1&~g2=o-A9venD^4ujn`Q7O(20&?z~A}1R#9vh{9pMF>ZIHl zPwhc`2knmUpkKjv(68b3sim#y=_!{~czJ_MvYv=%c4b7sQJFE;+>ZlFD*U)Ty4IPTFp-Q59merd2s-J$MOG|KA(idSt=|DyOQf1Amd24#*?dM8#rG-A&Q z&l%4-PlKn4+BMHja=bH}m@_I;K(#aeUm5_~?S~X@+hz#}8w6(=o4K$Bgzf zOgFfdM=+z0(u_uw3`3k|G!tLB2N;&nEM{RA2Vxcn;hXm3hTj@?D1#M4((t5_ZRR6W zebAHHzG-u{`JTa^;fbd8_l%TnZ2P7y^5lD5qG?M!lRT5Ov{l+#&s1t|YF=*I z23%)&W+j?d<(YGD)3(6E98aliWuj@@J&QeywHm^Tdz-e`v(mGswXJ7e*(zn*+`a`i z2ex^(d8!l5d)c!`wgc^3pxSdd@vgug&oOH2Nz$(A!#vHNQ`Go0axe1O7Z%IFRtH=b~tV1D?yC%YjP5=6hStpmh zjxpWAp3o<9eCSh%n}*S5>6#uykAfJhu*8va46}4Na4K*X(QqE#zX(q^>JiGF!Q{W; zo{@UoQ>o7*FI)A6)W33lDaEo|UqSIcM_;WO^>q}>6-DLxMlFZlyOiRjjC42YTlF2D zTl#K&pI%En=J>DBk6@H;nz2KkGW`T;xb!pnIkLrBixELncr`F<&G3 zV$vHkjs-q^JaPnbG~|qdq+|m>&>TZogp&T z>+(**+vrW+sh%>gn{(1jmPvDlcb0dKx71taS?OIY^N%yI(p%x3L9u2Jx_E|%cUHy@HUe?>b>PNde8cjeDrQ#vMlqYnIle;g3||p7zb~wf;zswPzHHxI8i7rlOn2sMg}y}`r-Vhm zCBEgpRlc>}e!dN!i@qw&@7qH080NA0w)<-6okbL(Q6695vS(YM*{@M$*b}%#^K+KQ zlS}bZ5i|yqs0vgDtyC!vcvt!wgUOy=p6a4n--KXFFpcw?VurjG>>tbuW_v4xIl)mB zpp zmxG1GZSxfdi#&Vu5x#8yh@d|h4n|Q0WhD#F4WP36astN!$Aa^Ni>S_>7qu?9B)A-J zqc;Utc`gRmay28o99$dR5UdJr@%IQ)MfVko%7xmM6u20y2`s2*hb8p+oid{?wlz8k(4kIS$6sqT2r z`@8vj6x9~h`cr8JH&EPM_RR68`!oE5{KNbs{A08t?^*V{XPcJe&+|Jxm;Dp{Q@rQ> z(Pvm~p0nO#fy}_*KtJ+>F)%zZ zGBDPAIFKK31ttZi2HXK}AYZf5b%wV&Fe@;Jv`hULs0geKtO>05FAY@c z8g^3y|9Qgv2RdQ;l?eW;h93NXlW|unhUbk+vU>xyDr#G(ZKqb_u9-^Bz1O{W`l;!s z+=r;ux$7W1PVF=`meo_cK#iW8(ln*X-AL_Vx?k-FaVzY&N=XBHH>nv_p`sP*DEvFG>4|YCE7Kf^1Q`p7?TPn@`s> zlFmtL5L9qhTy3j}))L=DbnRZURnhh47n5x}Q4QHuBw2{M+)Wr;J1Xejj(Yta)R(AV z2b9@?U8ce9X!w^S7x}$lB+=N#fP3?f(f?PCj1>P@5E}Gkd8Iop6 znj^^7Ag<1Ox*j7cB`Uj@ToeNZi@%s$*QAXKNh^ugw4?PMP-Q#X{CChcqUsK457B`R z=x{qa_T|WSfv%_A$d3+*?%5iNu1LM}Uy?2oUG9KRNt-QnZT@m(SLu4|4%v)%s8y$% zy!IeUrF%(t-qn`u>GJ*7FD82i(V)AZw=J(T}(HN`*w{eNE8t@g4Y-OJR#>X*%uO(i~8#3`Jy^$UqQ55tO53Q zvcBIG@op!dB=b-h%(M9&F{XDgsqt-PgF;C@UZFF=OeRK3`^ZZRQ7W{jwT>I$X zj=oa1sSWMufZDIiM>?PrZPwQ|YiuhuwV`(FYwLRX-&rU7y6;{KGu?OAz&7h$Yt15Z zjFnefw;V2+V^lvJv?e)d4RX->BlHaS*`emiJQa_|~- zR@NjQ1Fb=h&GNY~szL4Rct<*kK6g-$Tkj>^q0g;#UsO-i?xuU{WZPPq-KIvisfW-_ zlR7!V2VA4aQ~Zq26vTTQo^RgMj2|pz6f4eL;<=8={mXdT8#$U~9iurN&#_e|>Y0;$ z&;F8og7NvXmht+(Qr0<+KU$;5lfCg|&q@6qPjNGz?2V^58_zZ=#>SV*`kAPgo8{gt zv2U7nr;fsww1GaBiF%2CwXehNYH;fwCQ*lZoNSBlQA~}mX!HDHIiBIdU)Xce*v7Av zvXN~lzXNK&7j$<((y#6Jb*)s|hT8Az5_`1&&faXB`|kbM9`~JnSKB?7TRvOe#wYD_ zi}KHL*nLl4IgYto_gD_j>$9R3wbrZiBCZ`5WjrP1tkW#xkFQi$oHFm6lw(e-ptkwu zOcpi5nIhuT*}sE*43RTSQnnz}LAIYGucPkdAm@aW);i~eHu*<-&dDO*oU}$ei-`PU z?MV>oQI+r$kJ%X}ib~rw4(D7sPO0ytGo5o0(GsHNZqy>@Dk3_=IX4hhiS>^Ob&~c3 z&MidSMLl-bh&bi7(7BiB5K*1<*KwlL;+){TApGQP6#7nDJDfL&TErfZ>wls)yJ!t? zbr!_-u+C6yySfR#yJ!#UN)>VDN*8|Qd30sSdxNBnVWO71XpiWk{h^C%5ADZXv=4L9 zzRE@WDVHYv>k7&`M&oxy=G(H_9HyG^Vn&H;%vig?#Pq2sC*GXAeIy6)|||DXRg#y`@xGBs1( z$A3{Z)+)bNlul_x{fV-OvWaqtMzy)-64{6*5KSg3Bq}2E6NQPQM01Je6D=ZILbRM{ z713Iv4MbIvwh(P6sv+7-bcm>q=s3}7qI#kW@|}&JbG<@zLlWz^C`ytlR;iYkR)kWuIi9YIobcpc(dA_Br-adzpQ)y~4iIzQ(@ZUTNQK-)672@39}SAGRN( zcFKO%e%^l3e%an^zvVDGk{nh?vLgkQ=ID=qXHm;`xkg^PCR8Px6V*Db8t5%^7q?oN?zo=R#1qbE$JhhqT(c z&biS(!?{Vet<*Z=cTn5y+~=$%{)lWRoM%Ais5MY)a$a-ZbSW+isEeyRtqHw5{KW-D z{^CLsesQ5QesQ4-esRHyUtH*hUtH*pUtCDWFD~@JFD~@NZ!Pq;rdfw54_Sv=bCrQs zkJYb?w$8B5RGzj*tuHEeYpL}m=M;QIc{c+1l_-lr`y-CC_?=k}s0vmqb-JovLm+Ro!%| zy6KCg?h>ibvgK|5y-Hr!O4`uoomKMRTa*E|T53mZCv0bI=cqLR&)J%&U9;V^D|U;$ zi)gl5dv|*;dtZA$l4g>;uYIt6xb2*Mq_P` zC>hrGtnVq0SsSg5O6LD#>xP!^s^2D@O3`6xxgUII@I$~40Y6mDCmd#uLS6*<&%rMt z%r?IbJ|Dbzb_nv}$V}x6w^E$ux zGw`47LLU}F{sQ=+;D>@ACVDGzE^sDv$mVDNOC3O1FGn;0cfX5XVWjGl7*_y2_cL!g z1wILP6+HDOBq2O&hvWh9qw(x)SbG3H84WZ;XE=1cgoD8k2A{2tfIJ9(I4nR89E!Wy zxcd$DDMs@P=*b9ZGC$OG1vM6jj47<6 zTrw{p{4Yks1t}pkWy4MiFcr`0z-7Rm>TTS8p7{3}jc35u0FMA)Q44vFESG@WfmOhA zU=w4fUi7YEVg{l4vW0&`r3n0JU{{G%z;Ylz+Y_4maCZwNrywbXWSy9I<_Wh0tAORe zCLrqUyozU+N!$*s z0+s{W!ZLW{XRKpd1^gNMd=*#^JN4XW!zJ@!!fU`w=GTA`;B+7&!Q4m|dSbqAK(iir zQQ`u`z+R3GNLpZ_1r}Ohp#@QK4tLK%a!yLB2~8Nw!{&X6nZv*ca60fPBO;+*#2E7w zX>9E%_)D~>=RT0cI6@+g#dwgT)R+oMDkQ1M?o_r+xF1-J$lOF@oGaoJmMehE*C^9-In18*FGwIkqP0sp#0_FgUe`AjFa)>WX0H1Hb* zn%~9DMlg?j_bT{HmT$6$1#+(xNa#$N^Y{?*e3Gz=+ zi>`rR&MS!F&!}l#MLlFqLua#(@GGchWx$^B#%AbbAs0RX{|vBF)<9Zc46twP#ArFn zvv2tcd&7)cs=C=HmL1GnvY0nDAhs(JC)tSJ!+2_kc`HAwZbq;8Zcx;73s=qojFg+l zkyFQ!N1PpxfM15Yf0kn`L4Uk>_E*sV75t0}V{TyoFjB2%6!SIELhGY34*jm+_d&l@ zR*x#wfpT6Qkb_>xUjaS{d<1Vf3B27YpJ%hvCcKyLKFzaCnag~#M7H^atjTMM?}eB= z3P~F9wB;e5U)0;z5zo(|Zg1l%YCeXTJi?mh48&d%OK9#mhLgFT8xD&srn54w2$+{+ z>|v?{)rhfbWTn9U$d_v1eyku|t5f({^5=VaY5_860pgI~660Q(3iz(6KvWf#_9})x z$nmjwYKP@I>I3WNaBnG+S#p6hog(}LURludY*~L6K$G9H0k{mf*YYUymY*VX$MY(K z%sqpO0CyW9X+;0o!^15{xz?e#ukmVV_yg9&6!i8p$lr#~`+!fwSkCcI z$v8n}`*o_7uz`1v#@>uHUkgxGc-B^9jB7d9Ohd2^KE`NDQ%Pd{JuJTp&8x8dAK?E3 z{5aS@%$_G?%Qo~m%&2Sue-ZecxwFFi3by|gW-1dF>cslX`zYEcJdf(f5xy9^my2o; zqu_Y^I_|DU&yV6=yI?JaN2>mjy?Dxsua!=C1XQ{MzH5|mKf2-1N&@cvlJA`Ku?Zy%649;;pHHr7Y z#!05X60T66Cd;ERkK0s@Zsa2Mq1KpWQtvFBt><6z(j^GU2D%;$++ zDC`^s9)z8PuyYW0{set5^t~)GMj#mpiA(HSnOB;*CxjmWcR`*Gp7pzmoh{z7OCa`9 z1+Y`V&l;Zv|E%EI4&g4y(}8^;Nk#l%zX6*c!`d}qCGaiaCY4vfaj-cKHpju{IM^Hq zo8#c6actR?$(p82cwjFq>;?Z0_;+{|?>{m}9XK^}UO-1bF&tq@j8>fJL;O}9rH|#e9PXIUJ z*(tz0Ji7z(Pk@J2ic(8DkIPcUs0LX_tuybWOvk${v&_6s6~b2$c;Ii)x3X6)Rmkfh z=(l423`kN0uNv(Uun63x3J_k&kM~j^A{vyuEFmRk;4*-1RG}1h8d@G zv{GjJBb_T<=KC2fbZ-6Zea^(s-Z#@Mn_k6Esg`}BKFRBjsxxnT8~8sUujKuh`Z}s2 z^l5L*`=YPmSv&ScFF^BmeBM+Ka+NmW{AhZX&&z5z-2Dxn`Xythham3(3%7uqpzlVU zl=57d-eKRG&VnBY9LlJs@f@k?h>ddSzlVtWkkPyWIztf^DXeMwF=qC!;DfmP81D8$ zwB*8qIIC^~=Hp$Xu`75TD_|ab%=9re}4m$9p9 zf}P#KGqAHvRgU>$Ha)}{}!&hB6|{|~&`Y z?Z60jhO@B$%)^-!J6N?p;^$qw3!0{KuB9p@gglA{Sbjv}c3>5-97xv8kMWt*@*ZO5 zpt%P`&!<=N21iAmIv;)Gh*iMv zM9+8PuBf7;fcKk|2^F3%b8pKt#II+$`C&xzN5B_lE#=BngIc-_I*XzICNl1?$fNsF zQFBm%SA&0-D=@_sJ!Rg5c{u=^IK7&d!*U4L27uqr=U+PE^4$nDYZ#MuafBx+IBoqL zmhsJ`lc@Ipj?+y!GV~%c=pAIo6F{7fO>e=%8?dIRFVj;su=88)n~LvLDo(^S7rch* zI186BPZpYh$C1}fz~ji)Cg5@O_BX`;6mQ%CFWurjfaz6Kr-MKheS4SB$m+iWql_ke zIWg5*7V&zAEM5lw9q7M~XMc)JFK3Cx&(*;40_4Agem(G6;5th)W2dRS(n05!966mH z0^b8T6xMD*e+By7nO6t3JAbEAA2+8Uewa6M-^`6X`)YUO&OCA6L_BOVAL2M+x%#m@ zQ}Y+gDx7hiL|%{MHKJ2K=I#T`)Kkn`I-9%F-5Xf5cJhgsqL*tzF5m5xkCkN|XA{+{ zYT$mxj z;>5ikYuqZ+DP=E3&=`*}a>QJb4&(=5h3du?~0d$G+%( z-uX~H{0zD98Y%|gEd`d#>cLN)MBR9g&)uZ68uHcXKkAj`CE~ZE&rhol5&lm`H4XmX zf|33SvlEBTb5fHfeQ^%@J&#d+kk7~_d{s7yQ`;$=+76*6e;#1x$GB+mR@G6 zMV2Hybsal|U-Nm~98~FKp)N&@4ddA`o?QaIlHXI6?m zLL`jC*dI65(72|u9ZM8Amr-4(zJn2|y1b82srtVRFln?5Cc$wZN$ zUgW>>SpF4z!sq$4PNU=gJj7>q(_+{xk;p#m3Ve>w>E@llf8le4>3{RsEj{1?8_wY8 z@RXa+>87uO-;KN1(4W7chg%{4Py8(-jXh(@z}rUwr(?V`fWzh9l2_HL+a zle_6h@K0}iKdeU&p9k77J3q!e+IiL}dI^nhL6VAouEr^58E~}VaY9*!Tw8{d&N4n# z(Oo);m>U zfQxWm{tnK|-@z&DNu2L~!Dl6k1kyM9q4^=+*aQ2|fv`4^pH*|gFAxaH0=7oD3-WYe zAFS-;8S_z$E~wI}(6k0NpM-rex>3OI^VyU7k5Rzp0N5OWkv3tZP3$Z6Iks%cH6H{1 zkfR6|wg`lOiooZ?52Gy(_=<5n&hMxB{7$2A@V77ZDBkNp2K@;ZuELswS1axnRUXDy z)kg@YsS8Oy2Nf0PWjbH;-5&!}AsMBX5g$>xZv!Dgy;b{QF8&JqPexNMqq&Yz9SB^& z&zc9}EnkJr0^kVX23RW>zJi5Mff?}di@*?9yiNz%14%CU^9Ig+FW@VEi1|(yta{J$ zUGt;xa4j@*!RyR-I>^?NT)Zvx8GC@is|<6!N0|S z%LZoOegphtz;ga8xxD)(a6R}K$rVQmxeBS=01e+oJzH$HQlzlaV5 zz6^{5M*~j*OBoGDM&%kL*+7NSUHNyajvA|_dG+zdXeg-_offFA)lqQcmT z(Sk^`%w|+~!op|l0ZWK^b1mk2qwvh_SNX4v(Eo{u4Om-_K3s>+-{7lfI4dcqdLjN9 zE5s~+c7-zzBaKTm0@Db40!IT&akmuKN?AhwWO*Ls*+4~N8m|lBUy;a~E_nN-^n4m- zVJ(pPr&(_L8SY-gQ%|6WBEqj>MDV}iw3rc$^mP%>;5F!MgO`qp>cPHJkHcdkf0Mzd z!ZUlJzqe%!f0Y6shn*Ps*BDL6Vp9Pk@CN+040kb`hCYyAVjmiAq0bh0F9jn?f|ptt zDXs+1V}!2^KLLt3d|qN2o?Qz&tp7f29>d)?f$*nt8Kb@kd=2=A+g~C43}fsJ$=l%j zfKS7$O#okqo;TobZ|13DRFY&2?v?;o7~~=&eh+*VmUURr zVflpg67!<69Y$X7L$-c`SwK}WsPOz#up=sA9q=!=hq5QZ*NPel&Bf6Cx~PG8%TdUY zed>JV&Oz|kZ;zvXigjc?s?7)NEA=Vxd5|~3&UNspgvR@T1AyrwXQ6Wt_*+?>o*=|Z zVK@YxZ6c$Ar~t+l@X{#g90!WJfw(ndT_CU0nZktJH@pFy1r)U+1Nd`b4Du-WvA`fA z@I}bqlxqRU^Fqn92gKS@2z}HLW06=>Fq;8cAKo`8!48AMw?gC!`e72>TdkFc*uvr0o3%E(45Py1Kf$dZuT-$qvx-!EXlI&#M^y&|E}R4DfYi z7+jx(ib0#7k)DLLU*=9S&zPVgH|GuXSEH2%%tRheLMB-NYz@gwP2Js1tI{Ove+P&4w1=fljw~;a(Nbyv+4EH5X;fxR@OYu3&dIt=k`(YYan+^Y$eZ zjRra0o=s+dNlVf`gH|Whs%y}hi#?4Hu%g}H%uQ&RW9#c)ZV3%prZm(PwUU5|ct7D9 zFv^ZIBhIfFkMU|FV`wqR{K7>f8|3c1AKJ7etuttq@N0py&7ghOpzeji8T_W{1@zQ0 zy9y%B*Mrh#2A#psa1FR0mjJ#6=dg4(lfzA7SWq7<9eVzf^=9@ij)cPD)9h`DT-dTZKM*~YtX)|!9a6gl6 zz^@sgd5_2rw9PPHf^A5^3v?R#syJd+4bbx;*#H`HQ1>>_$ARBj#>0zX z!)dFemaJ%*9ZsDWH7Cn5Pnol=46_!91d~_AAUw4GLo7>Wgb2y_H*r@Nr<0sj+lv>tnUNb9tl1M2|K8sw9}1qK}t{49gQ2EGYSImom! z7}^QR3cv;^EdjbM=w^^S4i5NsszF%{bXk;s3(Noqt?&<^JAfVpYz)p|U|)k4tQ9p; zx*qgF*b{=x+rYUfT?6_v(C2`SfX{%B9&tA6`r0-EUk7#u{tU@P(C8OmM~g3kUJZM8 z08fMSA+)tet!j|m0Df`M3qenYPre4-2RId29<`=Rz50Z1@khtd_E})AKSqXjJaDo= zKj@DUIPdr)WoT{%YzyoTZLR(BLM`c!8ff?=bip4r$S(jMM`;OzvKTnu!nV60ISSat z_YYcs2o|pNePTFzO*FLcG5Fb@0jz-*;ce>@O5sVd3>px1&NyHiU` z44ebLpBq8*7I4;r-jCK1SB$wM(oqXh=lHY9@6mF|gdsl(9Dm$bfS&}61HxuT&J2x! zPTwj-g}oPuIb|a@ELa{Ih+3~eJ`*%%qTL_33w(?z!~af8@R6fzWGx%hL;Aa^WHtupv$84TVMt_XoY_O-2wCgGb`LD27@v^(h5;7cI-61o7q z3#FrgU4Rdv)=IQn6c+lHqrdhHU=7qoudPccMUTWX)WzsKrUo=#RMH zuK~c)(15->3OX^4q6}gn8uCXWGY|9wko3p8HW0Od8I+C%Vsr%jz&(Hy zq0Jx1yMgxso1k+6B6VM;ir6_#?2v2emwD=+r-jm?im(gNB@cnFn=(YWMftYvlfs-GIImH-9hBo2P zam+_?5%Q>OrvN_&&H>-gDxi4_IBP-gN9%|LMudxW)Iyv%e$4wlS`L{od7I%R>WE>lMgng2vpj`vZ4@k1=KV-)RXxX19$< z5XgjfN8l33AgXOYlbi?bj}%6e;r|dKgj)a+^E?KK*pr(P{}^ir`P@OQhTubq9)(b9 zL!OZz_+EpwSL`OUE3#Ramh$&8c(mr!#eH{2v)jNku#->^rBlprV>#ZupT^^{mm&;D zoC1He;hPsM$o$&Ea9SF4wi&c5f}er9-BEhdV5kS^j=;LmcHWmnyUl{#j8bIwD9pX# z6mW(HJ02DB+d8w0QXl&#nb;4B#oow#@MoI3VkR^YYy5U#_>`9tmDA*gBk3Eq2 z*bmW|^G>eWr)r$L+2mf(&CI>6*lK9&VbIM{S_kxU(7PbnA9NJx>L~plwN8VR-`57t z;T&_Pe+OjpqgDd2ElPLzh|)OFnJE1L8cKmb3;e2}E1)z4`gzd9eH#pI?e=XzXNpW5hKL(mvE(2pj`#*zpiY!D(t_>@qp#U9|^L zS}W%xjXBgn^J5@W$#BF)-)3l@VQdK93eIvvTVuda%K6O1r;)c;qjZNU)qOXk<3V#0 z_=TbKE!clcZl<<#Fs!`-+xD1RPIvU>IQY{LbpuQ-t18;+2f76GB;;h_2|||fCot8Z zzM3+45S*frISx9%L4F8UC1TXl&{kh)?g$GH8VuR+Z8<+ea-TF&XJkZv!!h6A(HRo6 z%#JaA2^EI?aYH+gK*j^7xgUq{c2TtA!87@d$E;!Kb+a6^H=7I#=YS69JZ)ku$Lw|% z05(MHY4FCwpxYWrD+9F>KwpJq8Z?XnJqr9C@Ww{)%YbtkrEi0?2io2S=K)AwFsL&Q z#>DyT+ER?Tv44_3qk}nOw6(+-l>$Bo3(p1_uL#x})5^wR&W_impZaszXw%v9 zJy=y4wfwwlG!MZp@p)>GxN2|K^xP(zehN`{8ZyWO?9Zc7sM}EdfXH8p(KLNr>E}jB z4g|j`JU`B$&Pz*?D@<1LM`R8#3Z?Prb#3%_KX5cK3!3X-ydDIW0M-v=OqLrEjHAhH zV@=M~@vs!L=_5o%7M|p0Ay%^x&si7?Ltlg61_i5F5pYbeC!nV_{8bD&=R8JdnL&vt zl2~QWI<=#PC)`HFA-boB>HA{|WfXkgN;( zG4N-?@*SY(ga1g*c3^kRzI1a|aB^Chx2$uTn>XQe z@OzM)Fz8C4D}kM{$!{!X| zOMxF_P(Fy>RRmr@e}|xS5m17E4R`REKALP9{oQKt2^MZh)6EfdkQc zElA#ux<)bqbOPwgp>^P21ARO6`1~@kzA@jx zfrfjap%UmypsSeQ%WL@`K^FoRM6HKmb7k1v7Na~FJ-vcb*cO@%&a=QZzzme$tnn`S z3FuFua|P&8kof{Q7W_q^w}HNFEHq!?(AJtbtb*tAoSh3Aelt(fb1G$@GWKL+EX4~+o9?9t zsXjeTt+b9N^a{O3?Wi-oNWExyARi$~w^Au8KzC79t)&({M$M?TZZ(Pe(_o!Bx=?rO zO(XC(2K_pUqIAwEO|g_f_t1S*TW9BI=vfl<+`UtDq+Qo2+HpGR*ha;+(H(YdcWm3X zZJQn2Nq219cEv{Ryx-pMzsEjTd;H^^n^RS5%r)2Kdg{4Zm~%pQN!BRAc=+oS2(Tr< zp)6Uhw=yiqS=_ntHCboesq;mLret1EOs_fFFch+@7U13?^Gn7mIeNsT(a0;|eitqS zkSigbN|l$P$y_U^j7z!-F64(e@pggv=ts=>P|4aW%bryUEtFC4OFNgNH05!u0(p-m zaDNJ)xSvY$pBUp}sN12nDF@i(-)IVx@_&z&=eLliFQ?~rqU0A1G{bf-ut^enA8(Of zQvt5Thk^}#q5NVBjs@Ohvs7XVqXYE|X8qeb;yMK1a<%pn z&(=(Z>zGx4H#fBaPj|3Y7 zqkTM2m#0>SP={ImvW&irx~yY(AkV@CwM@hX$pyzHYnVJuwk^HwwN1LsvTYF^o*&(7 z-K*XE-dor^$M4y6fzuA#2{+C{1;qqc=J9a4sWb?_=Xc%9(#z|Ytkg(p%&sS+N1*4e zXSl8O-I1Na2^juP8B=-mOs{;`BckzScMKvR$Y#dR79|&bZ^>Tax;2+F?drsSjvVHh zYI{1o=@HW4@dR-ng2t$rNQErfO_wj2&jQ;d-^SeLbO*ISh=w05gMEsFGcOPsgm3Ys zA0+iU zI`=~}(wZW_&3Ormzl}8;R4J+!s0>+HG_@#Jk}PsrmNX4(T3ffoXyW|E91ww+kt>S$ zYEZ3C$`QuD+c><>G-MA|J$c{8?YiTWHHzj|ofBA6MI~s`rk``$>^>=ez)kcX>c}hS z{!{K&o2*JOD-_L^@6lvkU01tH!Pn{*Zwh`k`UQUQkee}4yxS({HpPAN_O5h0=aY~; z=D5i<<-Ub?!N_wlc%zlN;imHv!*_%Mva3GQJqhAF2()K#@)^E-thF)(sZU8IQ`Z(iId@%FiG-S((9 zwR8(-bIeV7b$NXfsL}@?U<9E^%e%p&UBv2!`~YQWDN4!(?79QQ|70_AVud zJH2J)Dk&!*yIWz1Hn)f!bZ)Pq5ZWsX;wiJ%E|)^uNh?7-+oSRJfgEZS{7HyF77LXy z1Tli~bF zgu6X-i3*$1p)czi-2`kFuAb2@NWz(Cs+!9&r(S<8Md=9t<3*IjQ-T9b(E#uxyPMLkzM8g;0p zBf;o_$FC_=zH~}0tJ38;`Ze>T#s(T*f-FQ;j3f6WciNegbEjsYiw6&XRfQ~LX{vMV z+|Y@kilYx)bS)4;5XHt&8y3lxo^~kmnh=xGlfILBH}U7|Pb_=aa#7WTCk0#AQ1lvw;;^~$U$JhkimBsMJ{!;BK(%E&WNIe}mP$60fr7cTc81w4DErY%xIAy=b z-6{{M$p57fEf08NNUl)i{?S#wxi5K2e~+oAdRgY$8i89nXkXwV*EQt&``Us}O$BWp zvRhn6F|joxqoCxf%OWFVaADyad*G!K>w%a_8l;ihNWBZ_zADS4U(P zY??B2l|2`9d?X3vVV=yLWwZB8P7B(Hr^=j?Cnr=wMz=9fD_)YdPPeTC-$aLhr6ijh zpVFmR9-N-?Vzevgl&J$M@g_MyfYapogl}F%~izyc17K_bnSTy3ws4T3V zTDOZws~DDNc&K$rJytv}yeeyZ=LzJ!&Wo@s9G#6=ux!+nRFo?#$*9epfZgZ6#1Set z%+pmA3n&sPSl`#Zq!B9K&hu3iVwOSO)xIcuOU9N>EgapIX%=BBXx}HlM4Xn_EG#MO z(kn^LTP7;5jwRDB?Lcdka}Ft~8ggRy3R^ zT4YYv%avd7j0se^9J3duuNE&wna&e-nXsw7F8EmZ+xE98W6eWZ6f=}d&1-6_rk-dx zi)#PrPWAOQRv>k-b|=t^Ss1k_ULmMNG+K$g7$6f`$+y}q%TQoqBRHlxR)s@P*ox0+J?n*1ur^Y;+D)A#@5Z(MwU016P_6rJ)E>%nq6eD7u5gP8I{b~6#$P7n7Nr+^|%$9FTb*p4^%7cYS!Oh<9s z@9PCLfX9v_^4%zG$8-CO3n22bFkTJ6^#b6*W9h!}-AHW5+qZ-G3rl+etUK{-paO9L zc)lC=?RdsdIvn4P@pk-xzj!yI!9iv(s}%SY9Rgni-PQO_FDos0tQVf!en&6h_h*mD z&B%E>p50%35t>i~%gxApJ08Jb+y|ELhMMmNgVdOuFX4gF!$@g6e!^e88Io`X*KL2O z7a#~lxI*Z*f7A=$1&=-d##hgKHJsB6@C;O6`J7>DFY7DB6h17UBh%G*elI{AJQg2; z&w=i0__`NB2_9<|B;Nc7(1A=H3KEUYg5z`Gxf)LI1(-s_X2J70kX#K9^a4`BV~b!2 z+wk4?CAZ^$f4cOkfc|RO)?d6DhHwOqFM+^~j4~|w*A}b(4L@45*HlpOWjYf~4)@pC ztWa^5?RaT4>cdMm+>W};#i&uOwSHxr-so0<;FR_g6mX=g6{%;u<_4B zdnA(J$$(6rN?Das!!b=0SiPew2gC2e{5>1$9eLWr-75@MSao~jEtiewzRk2{=m!nA zYfx50h%2)(^@SI;7ppl0b;;RNF?AC@sa3BW@_6Q_u&+KH6z(qc9hlVGs>yJ-RylpQ z&uBUmmR!+GH#|C1cuz2!!mu z+~VTHlUef4q||E%oj+SQk{>Z@*N?s&gVO?;rj{3uZZYnF#3Rd#+}g>>)xR>D9VekP zI`IY|<+I#72mxOFr}6pM$eCad3S1}t(oA)cnmJ@#$v!i#=)8Cjot__sS4*N>1ga!eR#gZoWxCXi62uD6z(|%$j(w{`%X@4@Q5Fs zfNK{?@!)Bfd}|j{J5g&F>rtT4E~OhS!5>}Fw+g2qmEa@Eh@c!$+BKh%<^yCzVipve zk!wbp4mcJ>o6)X@KNJKWQ{`Y~i_OsPVonw0T>(t4;kv>F1 z?B;QiAw~jr%eW|FBGGn>xX5KAS$F9|2e|EXwC#WVLQfTGZ>3}D$FNsvBfjNYt~1;Bkf1Zs_1b2$&(oa<^!N4o3(>ytgSQL9t*h3>#lzGC zqGxH>*rxSm-Q()(x$gP<1K4Yx_ppE!QC;ln(D~lO*ViKNQ331Rx~bKp^V`j`$)1C(XmXR}MoNuYpQEf8w1!jaldb4;lcz>F>Z`0uLC({loFx#?7{d)z zI$g)8&5h`ab~l;la1M*;LqeA^(TZ9(-Rp_(`@wGqKYS{fk)?gT=XQM3&{2JdR{YOI zSjx{K_~ctjg$G&=QC?czfT2}mu-^*rg-k_w8?#tK8``$ z&~A$O$mPaZZz}mHr^euK3OdQ=4%nYF$CH+fQ^r%cSvVsK=(wdOQ;0^mZw$HBbW(6` zB06b&5grkG@PSx9pMVL*P_hR?wuHiEhq5~+cv}XuRsyk1m2bGU0LVSwz&-)U?@=9HHCNWeK>p1 z=X}!ns11HS7)S{$1)2hnfLK7%N2y0G*W%agj?wo`?Tc#rxpf2}5|F~Rp<`g<^yApZ z(bu7+L$HH)T^snBWyN!xe<#ObsugQp3TVvP*R`wT?CV_9*3*^_bmknJuy^FYp_6kv zU&+}E?gVTqT&B6M^X_Ed$-0+zv2JQyX1Q*5Y-`^!xSwnqGxl}tYTx_ZSG)JPr*Aql zj*UGybgsjSR)&Sy^~Z3<%ZrE=kj}*0^+&tK{}d6u5j7y?6ul9L6EOfl0fI?6#o@#w zM0QDAL?Xm&qR}JQF~eigW0S&?qLLz#Vv@q8qogCGW2D2SqopIIW5>h1u+wqUG13X1 zsaRm-a_d@R`5*iZDyDJJd5`AWx^=dE4~Onwx)U=U;a4ZBo;6k@7w)itlN;teI@dad z5O0V*!s2dxv89FTE3x_;JTcB^kc(A`>MgR@YMp7ahQVBxTb61qu)J4)zw+wS{t34? zsFYkbtik6&=dsC7`pPoH6Z`%!wzIPnE)--M+=Iaxx)fRyKDz8|8~OgQ;?X2mYDObX zewVQ&uL~wKP1_5Xo$ZVUlt;DTFMG}#XU)K-5fR;JdhsglklnL!?HcR?4)%-y=jUC~ zi)$$1l$gDt;pNxTx$l2)^*uz9FWY`cXL?TRelsIV6fmWOo#!TjD^oGr_2iO zS8y>FTUFaQllFxrzDe4&=NBt;7Z>6qB_zST7vZlL z1=3k{k*?dE zN?=U+9HHYsKKfj%!-}WJZdssKGNVqJdBQ$h8S}Bayu(->@ecaq@$~q6=kt8$Z3^Qm z_V*ab^7qb`DsLA<%+lE-k=lUf%W2;)&nGp)!}h|u?wcq2R$yy&4NuOD8orX<1ha~+ zz0qlzx1{)K0|kSxF0bek-jWRCV8_VVCB~IOSAY)5rAbY#T7TrN&2@xFi^o9cTBlo0 ztJ+`MljIvoPvg#d9qyV=wH-ONwS3?1%?dLNRiLh>^AYI1{r62dw)5OpRk!7aGti^* z4<5(~W3zl|X9KA7i1pZec~(bLWHXrI1q8Lyn11qC!u=+t0roWVY}Imju7KcwI|@sw zL)}+dWvRp%R1K;s(S(}U~_^L)&{Uwl!cw@`CM;_tG& ze0gZ;uxP*EfyG9M(xZ!lI%Faogn;LQqX3smgQWao$@TTn4~&TjGXTX7iw}u344o81 znqoF=L=TraISug;nKewXmya}3{R<{qWLPmV%v4SdvBTG(2d*K;U07}hJq+<20MeoZ zI(LX3K`GV{`uR~9H$*Q9{&mfEm@kH@?NV#;`t3F~^=AO7@eRZ?SVbJsNUVJFo3u}e z1>d~3e0J)9))#iSNVl;(4#vgVJ)A!o=UD@b;q&# z(7b57F)9z%uB^8uTt2?#?DAd_Y@2rjwor0*IN-TaA$ zk%F+E|GeqBg%gd&2BkhXO$8ih?r#}HgbzO^A_@i?n`8UqRo z=@_ilj{8@P+RA)@ono>wU_k<*9&aUko~-GowPNTzxo4b*k(^>K8!>_epdQOyIwP)x zOgKVw zAd&l6KLfpn9y31|H4T5WJczr`*ub}(;a+_h&&`gym<;EYaD&p20fT0u>UBK`0BGVt zt~l&4>_Wz!NNhu56xg5ro&{h?%sC2S%#-B*dr~Z3w`8{g-6|+_Z)(1wN)It_nAY55*AvKR7bNko;x8X^3Pt2mYy5xcW6+x`WLc%#zl(d*&y z*$p-4HTZ?KVkMO^(^#>dSKdgyk1SuYVFO{oZ7vPzpc%#L}P9iKFXliiaOol)>c1xtDG7 zq*N4O{4qxRbpm|mEl&YqV)(mAabe4U4Vb{{0^#Dq4L}TWfd=o+@?qAx!PY%6qE}+I zSf0HC=U}gOFv^BVTz>r9#1DSLRP{ZO>{RVN(4h)Jwh-d-d`JF{VS7}8mSIa~{%c`l z*a4wo$*{ff{R~EbEYbH=6rla$kd1#De$iw7xjnpT{6*lroqn4#jmio7Xc}bJi{X|G zI0_a04hd9)_RMz0iewU`1AqI`bNLFp?Pof5(Bm8P1>b_a3EKOXqyp1@jS}pv$Bc2Z zp+_ecvm3SKcJ1pOeFf-ieE~?hmkW{!@kf*}ew6U9C}FZe62d`}uTjEYQNjt>p;S@A z;8DUtQNnD4B<4}VEZCv@Oo-u3h$DvhioIM#LH1$q+SmF;{&9AD=c4@AQ-}JnKYwsL zC9h5+XZ#VEMnMefK0YPg{+ae|4%u}i$Z=aJExNl2yE+)dj<+%ZW?R7xPUpL~BienR z;S+dwU@pqjEi4I%I2GCktoC2z`)Uj}k=3)pZl*yNY*9f~#83|P7(egYZ^7Pm-vXP} zLxg$h0D6LSfPDTG1+)D+7Tkv+7HGs?7Q%MDbU#uW+QdApl;((14At2x zFECmO6y0ITP26tulm#!`F$gbny0s@3_2ygSy7TR^I@CHWl$3{AWZ$WuW`1$saR=PN zt?z}LnIazOT)(^dHHrv?IiVNwe9Oq^!Otk<0VgH&cWMxEMsE~yW;>5Q#XcUce)p3t zBZSIU$o6{z_iuC%c4l!BzsKf6UZA!^uDIRNQ?3chDPtf$0Q0|7`R+_UviWRw0O%u@ ziB|*gx0O{N0=oU3_UnG9KcSY{9Nj8x{7Hp`oJFW3>gH$qUPuEuum*{4O6i}jCol|6 z`v+GZ%q8HRDkd9Dtyd=LSy0%p!%+aqJ%Q!MS$kjVTVp`;aOQd*rO>cmk?Xfv1BlZq zuX$m-I($UhT6{20eH=JWLmcQ9JshwW1Dvld`Z$O!hB#2JdN>HK1~`37dgQZpc(m~; z8+Db3mIm&AHNnPsx_DzQ-H0g!UfQap(VlwdNRhSgs`_IXhGWTkW61_%Rf8N_f!3-a zRjrs>HU!PTxSQ8K774xF*Z1IBhcN78X?FHRjaJISjckMf#<`ha5m(6Q3+J^8YVYcWy%T+}Tg(i>{& z{2NZv4_K%?@KJMOF;eh7c)Yo!ADH;y5c(1E2f6l2s*(#FbU~h)UvRh4aj(T+RU%#ad;`fP47*0AB$G z1IY==3DpAS3MCWp8W0*V6i^yq8h{m`7QiX=l|+|{x+n-dU!ID*D0o%~pwF0JH!WgD zV-#eNpFaJ|jHD4=^Q$L(RWQe2fxq2CTKU>i2jL=vhZb*bgW0a_&}gP*!rHR&wN4a>iD27*=vBMx5k%i|+T}2|$N?w%yEZ zKnlU_vK-ihQrLS^ug~bon=T3U#({4#3hi?Y_?8%t7$P5BEoI_zB6%(4ZI4Oa75m#4 zE#4JBy8NGIC_F)6L)lSeg(K=LIrvgBfH`w|%~0Tv$07tXz-k&t@jxS#wW!vdx0zNu z(ZzS(SqL*u4@d&hp=o<3(rl5~{Gpi>CusiU9NDRYqiM&_JL;q!)Y&pevtuV-9^~30 zSaZd9xE}P{vbK4&#}juP9xz>z)kTlfug4Hh2;M;gV!3&8v!y2uckVBw-s#e?}#QgS2jbK!Z<7Mf}uje6*1d(g35pf5^y?~ndYmN?V@Ozokf zB79Shn-omV+rmH>^r0zB^hsO;3mGqtz;82TmAn5sr#17y1 zd+K-oQ$gsd`;U&MdtYVGcglO)OJA=pUQ!X#)Cte;z8qNJ>ts<(kX!YH>mcvere zIIZ)cC#J6Ua|Ol=4jBTuqh)YLWU#>iGRT4%VvP5?Io@~dDhA5E(NY2*&)LVDw_h(} zE<*jooqU1RQ)m&a3+=4w>i}sAQ$yxZKS&Z5@zgYzr(gYO9#54%PXTk-<(F#k5>DDD zPb}QRt$ZlV{c>~$Mk@&2VQ}Aw@UR4VIM~e_)kE@EvQbIDZXK8P7tJW_+C=emgrZV^ zi;0z%z|BNYtT;(ZO2|q=&B2ZR5RN>VBs>aB^2L9Qm5btdkV24?Uc7TTopMr2K-FSV zjS6mUfil!5h~lWi&b-VVzas!8(j zO#0+_dpx?H*q&RFxUjfjYNdsHGV$oLD}t)Td}gT4uj6N=DdvHzedki8EGUxs@96Pg zNy?a1%<#(2O*4Ob9+Q{L5E`W5*3p;SEoS@439N~4$olo|5blciEE?%p!4-hT7L&=Np9kGY7SFOGZi1qbRk1BWEEKH^*n&2lUUkm8m z3@tb4FX);aRdnq=+}3UR+Gsr{HD{M*8~=&J$t+DB3h9?xGw$L~!?UURWn9a0Kf~OU z4;IioPOwKAb*+@HBwZ0)R72hAp2DkJ+&o};dEXx2Zg1MBVw#owEy{Jdw%n(z+To(y z-5rOhhM&{AnT*Q}ziKilp-lXz7=&{qtrp6Br4p--SF zzfZ*ky>#SE-K!ILmLSdVY9?d3q2G8Pc7SL;nEU?S6{t5DXwT?E?~-$Yo|=)dJ~Yq9 zLjH&BIx}0EG!a!MQGB4mQzlUOnMr<)+Ic%6#W?DQgpHBccjf@(M#1)w} z&i=J|0=dDOw}+UY|J_v=Ir|1_GHWz!`Rw8k2FrYllEYKax1We-&GF%w3*qrh}~*>ofTTzve=Y=s*aKSV@ekL{qNQ zLtmgzx1<=rLaywbAg{r~&0y#nt82Uk?pB;pYyQ>*nZT5C!Bm@F%@cU>u31oS{T|!N z@3H>jZk%k{l;rvGEp+>|?srV1*9;Mxz|YocL*Az8(~;Yqiyx&$2kzV@EpQA=WHwu@ zpct{mL-xv?kcon#YHV&28Lq9OyAMs(lyjw zpjccI(*^cGP;t&aS+`h3pwiyPJE??)Zm~(icS{B=slLaQwnj0U2iE6(kCXm}BT7}P zxmz4H@4YBCIUN_70LhvB=|3MDk1(D~j9gl%Iw=gJYPyfVXqt?*zYiL|WA;d`QHX4M z=Vq%tpSPb4L3Xt8AD%DE5$siKaha!RWq7oXK_?fYksdDKH?L3gu$`zE>810sYFqDS zmnU|*v{Qt{sBP7EkgM&#I!q1x;XoSPbh&Lz-io6%b~MS6GaMf1uE87>F#fxUgvj7j zVKcqr`LlgexWw*YTTP&tm2+LUZ4$V8nwnIv^?YQVV?1Ua8(6cy6|h=mvlP;#wPZYk zo9LMh%V-_9W;kawHp9BdVL_vH*77@b)Qa{MT3WwMT*!x0o?CmG=65!#-~#U}({+CW zxo7=RDoto%-a#K>$^mJo`rhAfPYVKvQE3}wq9KO{Qg~8;!GIHMLz@xo$SQ-s(e$KM z*n6|oAsTyGIjnECv4Xv-yzT~8Md!5e84&+}M*`hg$4)6!wlmn%E)xO*rAVXpT zV6!gXY{7_cjQ2-|!-a7y4IS(1Z~!JukkGFF-{qkF^{`;iA8USW>t^xZySlYBuiK7f zch~ZJaWS@XA12p!AH0l@^@gUjwH_8Ed=x$(E{fgD4sqWOvS%`Sw{%lCN1IX2T{$@r zRv-FYR;TX%rjFE^i04mkGnYAAz0beA{cAx<$j?vAk>&#IlXRow2z_u?J&gk-WaI*;ZQHk#Y;ic+}_+W=*_*4KWoI8s;~3H za=UB|gAtAeH&hltTn~DIcNd+D&#Hr0Uj@F$v02*pj>XMEfYJ51twJ6py z2%e^&+cBCxLP1vyR$;kFy##9-ZK?3JADyU=4f!+TWn#oqRGiCIo#eu+SA7o7gXN&WQ)B#WAtw3m^aMWYGGzT94d`nnv4u871^gN4K7&;XB> z71G=!JJV#*;C37I0cC{hD@Ge2N(-KkAsN1f2;!rEki2TY|Em+p&ZvH4Jv70mMIGP7 zayEG3DE?P~F&v&@l+Z9Qi{s>KW0lZ+ zDy<80TmUXP)@7qs+fOf`cmIv zg^bRlaDvDREv`d#j3t#?)woVQtM{@e=T1R9PU1mgTOwKuosTb|ZL{e{&)@tnpuS## zqmtAx{H@4vX>rpw%i(@F2}X)>B|uv>y@eGQZ3~F|ZlC--GS{6r#L!}qrp8jiMp>hG zf6*6s&*Ros$M4n@`*{u!$2x^lQ`WTashKr;)zN{k;>b8oZ(zymGiLcXb+*7N}+xKbCu_H^DFJpF1)Sbt@icj<5r z^+vs8&$p2SAjxY`@zOl^x}=Z4{|#t`hkrT!sM8Pjk<52Do8CQrY)Qqq4~K28HP2ub zF+{Splv$+OYkvaJIHb(I^X;Px=P884}AQdi&IO9pP1PH9*^{LDgsb&06XVh9!7 zxYy87*z`PTzkRLlmLBt7aj7QBV=7&~R@$5%%GVMrB7K4o8DEDP^no+3h*sNNEPVwzD9VA0aY>BR!Ovqffs&DV|u~0CnWi(da zGjhsKZ>Uf$VYmF#m!PpXUP^|}9)%$)+Mk*(E0CPodJ2#vNm-3vOjzPEcYXZ1wyqEi zvBr>sUTr7YvgHBcf9nG|Y1tjXieXb-8lhxZW$A_naUryl=m@QDws!S^rIg#VME^vNa z?#K?srVg#BQOaT2-s6d(#=LC!L&G^_=(AvaIG!~>5IrW-(Wh@2OzEtIv$M*_u<6h! z>~?}a^Bj0ObWmPRD}{r1rAhtjlWFWCdEL}FZF4foYNA1_gUuVYj(66PXXV*>g7CRo zpH&VlVN(KW-AGRlNx_q@%I1D^M)ABnp>DtI&5~n|n^_W=S(~d~9=Ew(LABhhM4s}K z_709!-kC%u3Nl z*VFn;6f@fP7OfNWogeir*6y^0tpt38&of53zG2rn8Tqeh!%98wIlfpH&6=f zo1J`|r!H{f!5}2PCw)BXJcCh;8yz19 z)8cNX`lQSyex~5{;m?2V^~S=3Ak@Xq{f+N1tH^w}$b#?~JP5;~@lD>Bq%aRIWUC&( z#M$Qg>j+lE@v}~N;j*Z4nZrMox?+b0XwIx-PrR&ow7yxf}^2Q z@oFe_*TE*abwl*T+kS_)f1805rw5QeeNEvqVY2O4Tjd?u9+m6S!J>|9`xJdeo~9^2 z0mlYtzSXB6i&-sw!rRFZc52SXn6@8QMZ=cn9aqM+;$6qkvuV3VKs#pVo}g z><>jtAthN*&b4?7{U~=^UiWv)2Ib*VwcR0idOxL>8u5M{BHu1d^z|;acJC9E(rk9HVqq}~83cMCUTXF6p z1zlv-T+hjFi}OP;%nJhrKdp%;!ZpU`(VYcBn;SFhOKi&z>766$r5hK%#nH^0>x5(a zo>|5j*V5Zv6&t-g%P1?kz_gpr*Hrg8m=vFkHrYiL9Tvxh@gw2EmOd{8+YdV8zE-}P z&Z@oo%d$F91^ve0BUWiHb?cn#V10s}L2Af+Gb26J+77oh5-3J)KfW&`!1lNc<$k!` zH)yk`OX?ZT1C;I{tNVfBRmx~m!uZ7hmLgL96;#t9*HIyl7I`0^^Y< zzx>vcbFq=MLT72$*}i4EW98fjGiD&t6%4OJ*~rwqq1(7y|MCFM1jMVAF7S=}^s#0SxYzf~ ztMRto_ocq>sipmF`pHIqsrEqkiz`BMq@1Qs1IN;9Jk$pl#h|9uE2qReF{omtly%qN z+<>mvx9W#%EhdH>`*0W)NcGmvm<#L^~oz(@+ zP>eZ_=3E4NTG~*I13=j_vayMQi=Ww}{xW+lT}SP7q6A@<)oBg^!3X!s;o<@#xb%fg zMeZ_vG|@Z0#JQHZp`;W9v<6(%OV0jvjbT8k;&1(PpZrU4?N>w4#f7Vv#nD+-R5`=^ zlQ2U>E$b#~W7%o zjSwbMcapEzxvDL5Y2*zH=&GgXq?n@TVC{9$HzT)nkd21tG(8j&JddPGTMn|)G_qw8 zv7eWiIpwgYz(UXm@_-9LBee9yGJ-yG9%6C2cTdDM+80lH67cJ4&+bUt{fM-{nJ1k1 zD|!?D*n);J0P-v(x!Lup?WWQ;%o<4!oRJR-HY?*I&d*r+Tz=P#99bnu(1C8qUlqTj z=J-c}m48Qj3j7uq)od{HVK*NEfkW;w=LB-7EioaZ94HB*95PP(Kvix{N5-aJ3-Qh2 zJE=WL87^BCB;;78@X%tmSmrtY{^r zLFe;vxbbY83Y9!(>UGU_>CZ}42#T(Y^jX@z9Og?URIexmEw+#IHZ$XEh)_bL|C?FB^`f<`&7;JH%NCm;A@ z-)j&?EY=PyQ|D)}Nt0E&viUA)^fxjTY?$Q#0y?tp7VpDk>#m5Dn%$&!CR#$t1ylI6 z)e|I+S<{1nNO;%%=|SMDn*a*|RN)5*8oFArb7 zf+8vC46!VBcfHjEs=~lnU2bUA=rx?jI}~CH`IK6lpfBL+_!M@VA`4q?kTPF&TXOiG z{j*ejl!>b?AEA&1k)9Ov544;GyOEpplZ7Pj1kd zTL|_BDtDn4tAiyy*v9d4`GZFs>(IRHywk^>PdYn02l4++`+sWV;NT?w zr*X2d{HOf~XJ-D?{?Ew2-2Z6i`n35Uu1|mdBk>9U!T*)`x6jSS_P>1lpVI&8VEW(w zG5zoUKh%HyX8F|iN%^Gy)BDHXzm$I<6Y)Q@{txwEnNQAtto}zvoB02Hg8xk6KTnX4 zk5SCr%E{P)QOru;$ymhL(ALP9QQFwX)X9vPi;0bi`+p(K#2j3l>|9I&0tj&b{wHo( z=PRB`=nHL+*V-*Pz>_w%zHj5xy?0E+W@CO~-vN>><55&J04#_feR+YTB3W*7(*WGB zJZ?W``n7fu&;XRHPIyzu`}I-GBMNGF{rmgThe2*5nqvGUVuj(Oojuwc3H^=^ULd!v zfsS8Wa$XNgO_pj4)!H2u9%1%T%=a{2iPpKk>vcz7He2Q;H&G0RA=;i+p5jN=k=-AD z;WEQ`iP+v&OY6@_{a8)u%i~AU<~iQyZ^1(%Irt58{V9l?F@n!QP`9Rnu50rMgDcnm z%yN9Jt{*`XT{Qx#rct+m?1LoMNsUF`+UjR)r5Uf%MO%Tr0(eF73A{yf8d-Hc=>>Ar zhaBh0l#$<@nGy3MhBT@R4Vcc!As@!o)zzUJkY%RR-F=8aYEyF(@c*MH%b$Qt={ z0}~eA`wZRGc=2TP3jSRD+0g`PQg^!VkLE>c9OpJzvlS!Xvz=I9_edVMCctlzT9GpLKvLR+d=?G9-P|hI^#&!o4G|^- z)CXK$Sy<)XAVD4}i=~S8drjF2qU3}351`kV|BFvA2tmQH9m$F4|AP1b zOO|cWI+~!1P%(R{GMBGPwd?9?)=tRzaBk7zV%@Li~Kj?lP`tAIGWthp8cMCj&229qABUCd+|#|gjr+3Uy&xWwI(YZ_8!G;Ss=sDD*leXT%r zg83KwZ^El`Wo>P1uPr1=OP}(xmk*CODil-=y< zakr`mqzC-lsQxwQCnxWn$NMk#--Ld0#Tgm6d#^t7n(&FX1+UM=FY1-`ESRjxIm|N^ zqi%H7O{410%gY?$Vzl!dxcAb*SW7k(Wmkn5$tAnJztI0R+e=x-Jx%a0_TPlPrHa

    |7J%Z88tlU(+@>Wc&nYvWgnk%0m7P!2(=6v>)21D)hYv)|GcJj zm?~@PTb0i?e$wN{(FHYGda#d8MKy~u5#Ep`{17nngNA$q2yfn z#!XD}+B_(T%T7sW@*ISXgv#+)?FKbphn665yR9z=4o2A!EF@SEavQigs4MMFlOWs& zo(1&X+udzBqneoA%ta*O+c_fPZY#Z8g?Zs+z_JkkICR{zxR-jrKQy5= zKb!I7?f`liSfv!SS}aIOG1??3{LHOR@$ULouPi4pQ~Z)*RDRgnd7^1l==(naIY7q0 z;H7D*G}BrZtDLteKk}p1#f$&DPHr=1VE6uR93k_~8f>Gz z(@fsX$d5~_8wo9i$1)T0PW@YXXK>zWI8hz``xDsR_DuV4Mxdi-Wot*z*&5HD?!S)c z-qLusr20~Q^A-&{T#I3s=j4BM@}jNr%2} zvt#;J*F>u|cy+eB9X3|xvm3g#(Aut1)0V-1-O@j>``W+VWVG8yOM^>GrB37{v+@lBedzPKLDDN#(gRRCJ)3IVfe)ycO&#FqNQgp9K z*_+)S7WUSqI##6mv~JsWt88tzxBTzwdsZy(wv)u^Zf)1L*sMkCTXrY&b;=$2LFxRl zYIf|GUP*d=9gSzF>}p@%`c(dIb@y+!w%Afrpiy1#>uA;4bg+(<$JkpkVgGFl=i+fa z;myfzSeDAKJ$+?q<}JJSquJlmm&kYisYQ23_eXn%JC=vCr+cOC^2%lQGCNkL`Zg>N zFZJ5+(llFB9V^>y*c~x@TF6&Y6 zOI_B6zxLlg5y`Zp{>;i}7lg?y2=9T;m|fPsxLj3k@L9W?-O}B@cyUYVzN=}zV$AxW ztu@tWk61A#U!P92JfUs7#~(Up-lC57WO(&?ch;h#-fT?1spUCc`>UNswdtGiX0kL! zbLO|sIk_|oR(w=+FDN~}?*2QkrsekGQJ-l{KBMK%*3Ry}zRuQEXJ2>Uvi^a!r?;jO zt$kbP%<1dx=uU;ty`%c?ZAj)juU(u^bgyux>n7Xz);Z7Kna>o%+{7^$(BKj zKib^&k2aSdF->+&dHaa_?_Qv5d~aGl zI<0)C2hUpMCEfCpoAm5bWm<2p8Tp~<&Q(pivN1~Rl0n&Nwnsx{*)}WA5_0{GY6HR;|ALykT>5Nw>}Z22hz$q!fD;dliA!(k82L%|vIix-!{xDLiMgb3~!O zVfZ#@l5===ik2?8P{mm*Rh)UYisdWnlb5f!VZ~2Y{Cb5yakGD zvzl7IsBpIp?1_vjR@Y`G_D5QZg9c}&E?NbYvNk3jW>U5)HkYrYGkO+n~pR_h8Fzvf?Q1Nj=4tVZQ+(wWN5K%Vn#a* zEsjW)a$S8Ut%gUKBNeV14>nGuUI%v5r{tOl5l*b`o1RN0O|g|cnwsa7mX9V8@w0%*;EIA2;K_*sQhT~NFhi+ksBO5VxCvH=8)iSGdlQZGg;9U+-15I zy^4s*2HT1a!!pTSWpKNh8GOS$5^OQ=D10+9xV_lWq-hguE=1Xh>!>@q$goLG|_sUn?f+GwnNruW9VQ7(A#>KNx&f12!iY558)a zC{`&vbB9W2;Mw5jV(lPJ)Ob+P4qvfl@KCTZ9Kid-lW3ea7N=%1VNQ**=eV(;bJ&yK zXlHvoH!S$Bo#nO+?g;J-zOI40Hn=XhUIP~o?ojQwf(L?cYryUa?hWoU)dRbOD~n^( znIKmcT&Z2(3Cqgh0<&D<24@Gy6-Krsb3=o(&1s5NiZzN2iohHcoMny-&M|Wo%e8!q zBGCAD7vl|?HQR%gI^l&uS80|kE>4R}{cYwbmT0+Kd5!XxFcz9vuq-$|I79by zNpNa#njZILutZaTiODEtD||f;S)H*xI7*M!QRZfar%{?!&^+H3%m`Zb+-NKhDUM8K zG+OCmswFdfS1?Yu?%-f_cq_&RV@i!C6;flg(`c=S({W5ObELfyV+yI(j2@@Mf)+E$ zv;-6P=fiXl<2!=GG;W7!I*kfXw>WoL&}zn_ps`uaUf2#qzDAy1q!tQ)v7Ao8z;r~|a z>A+XE`?<=t{qNM?XqXNPw_g*Mx2t?m;rf5@-}3)xs{EJym;F~XrH=DoGK&?i|DnIn zA28MayZ(FrpY7oJKQWK`PaAh&kNmFO1{= zLVc)E?f>$h2)F*+|JU#+|EkuST>g{gy&luqY~C5}o$6Q8)1IsGcd0={8y+p6ep@;B z`&%`+rsW1{-SLb5Bc{gG>X%KkVz$EbANRknXFcJ^imeA_{M=yw9#gM)O7Vi?b;bLN zi1Ae}P`CrT{d5zSp&DxA5}P{Ii73wZ_+t$(!kzij#S*C*rJI1UF~*-ubOLAbB&I% z#P8EFHuyD#5zTLJ_pi0jnSWhz=!i_UZr$~2dA(X*uYIpKmnk+VA{ynZiLtbdVqyGZA}NO$}qvquqVZe6HeUZ^b>>i&Pzzc{@A_m}e;<&nx?D(5ej^CkYp zg^|tentT_l=NE@vxmZ{7x__?9xXPp=t?=|n_7-E+nN_(({&{AN!ZmaJ^EGDYn>Q8j zD}2r1^L3!}bsgvHo-gq~Z&oQ>&Fa;f)%F8s{8gF@tF+YPpQY(~mX@AZ`AWF7#6MGe zoT>UVwf|E8Z2N=3UtzxI+Yi@V|10KP#a#-0f>cgWY*g%0{8|xc0M5`pcc|Q=uLO0n({QTU?4PE4c$zwY zsyctFM&(o!)kObnIFA+=V^x_ucKM5SqQ#nei#0~O{c(kH2WRwZ9I2ZKe7^D+y(U&eb9}p29i6Yu{;t;>uBM<@ z%{K4P-g}xNwYk;aA5E?IuHrq@tmO$|{7Lby;yn%2w{#D`r9Vkh-d~L4z2m)Y8oc+k zwZX)--7zO7UD=MD~mu~Z((Gg$vzGL6P z-b>yu!~C1_`hS%3&%B518{d1roPScz-}4?1^LNYZPkK*;d0T0HV2}4up>A+yhxd>U z6bUO{DIAfpUm-1w8momDy;q90gEcDNFNTGdJlW~aMT_)j=Qih z>!6lg%st8NvL6ivRSXB z?dr`;o%9j+X!C@^Rr8|@LmI>VjxOk_*x^ohr|MBjyBT+?zLyjG-KoV%8T~EqEiaB7 zS<1T7FvsJWi96hbO}9e7O`TT@u|b)B=hfoFUOJnLIWO6%<7`s7)Ryk!;f43R-z!$u zWNNp&_HT-;`$$1oylr5&Grrg~GBY7J$Qf_eD)uM_6uwER%q#XPJe5xNASXNEbnm-+ z-?RJn?AyC9nD}A$hZ{fK{h@C@nD{~W2OB@|KR9MwOI(-x8Ix3ORy?Bc-Fd}X<6F|X zI(MFZKdD^h+CKswagWvd0(ZW-K(R&PxhLAD8h{gvwRM>}xuNcf_UDLuyvosG^X)2M zSGexUZik(J?p$R*yd53(M(uE?xRb-tJi?u<(TwZ9PF9B|>&%mN=E*wKWD~eYxij>; z?EUTT42{y^?xe!#bTZfGPSQDchvj6Ia}*aS)++Lfz}#J4dPVWRLN9TZDMgQ>H>`Qz zJxu%esJuY&h{7{jucv%3d&>8+r=WY;?=30rbk)fgcZ51Q!hXu!Mn#kEQlp~DbSZZ! zcPn=*_bT@)SDLq-S9G4coR^K`yzKm@P}h9dcIP)*`Hu6Dexu&7)44UA2P$=|Z!JdS zI=iIm1ORv4xx%ZwR zeUDwd+qqlGY))+M-rT#{&rNanxqk`o(1*%;Slthl?F09IIse7o8|HoP@56_8hBw8w z_l{Jy>(iCB-OH5oG-cgYZ)!OoQO=XSDY_&#H_RLEjR;#Ny%FIN2780T*F>GNtxuG5 zZMnY28>H8Tn;Yro-G{@zJnTLc&YXwb?}Y2$akqr|pOtO>gXR3~a(=+wQq;LkuG(E^ z5{i^!q9Uv4QbgSK#Z7+NoKGs}6UupxcYJuI$CcM-d-l&{x8M0*;YL5*@AMZo z*|f9pWmjoOVVzCe3zr8<+X|Nj>0G6AopXg|QrfxJSsUi7o%Q-9zI3~@Uh`&$g#=@N3F?Rc;=)?Se_&KaxTWc!`5#YuyRq3uP1YYg;ijL`CEl#a-gL;JnVU9w>HdM;#T$Fh&V+gKX{CJPLYx2P#@un4 z8#h(jHrcjKM@-3Vy2VLfe@l?Q^R^(Jy<-(1f3DQm*G_m=bB<$PDU z{Wsiu?8Ma(?{?bAeM+m{LD+*Z!xmF-kam-8Xsq4sLF4eZsz zAp7}m^qRt!L%pWb*SM)TY-GmG)p;?mA{>dRvTYG5=b&8gdllg{b-!F(SE-xpF1O#` zb9cHu<_1Mx;d$MK9h$A)(o&jN++qKb!ueI9b)5ZhIzKPgHe^Qca(-?WC|*%`&U5Y< zz0pVKhPz|*P>s=JHYR-ZMu#7dQTp7E3corjeTP%_pN&a?Y0X%yK^3n^kZ$Z7Q8BobVqEowdsLhu3cB(n8DV@KL$6 zFnCC2n{%UM|0!{|+HO==du{3blXICpk8@dZU67vD@0?$l*rIe^DSg4F$DCgM;%yn& z<9wl5KO|#zIA1Ucg?{}zy@f&c$QL?is26(hPY)mb)58b`u3!s~P3A+I#4<7S~16yS97Zw12^P_Z6zE!yl^m**|5v za}&IKO|N3DVx!`2MP9L6@vI`MZ@~T9?|yv;?l-#>zgG+>BHFT9$4q$l+2d;aefoWH zpS}Q1&It1(J5D;=q$9$PC5w}$W;SX%+HZz&lDbt%{W?o(-X%5WdsW(-lPnA$U2Z?9 zQGbn3E4S32kj`K&4c70w!P;lAxm%G}>{e9hMLpPbDY_NCA8S>iyW40UR_s!EX0FPm zigOj0D{fE>C?YyaWAWe#_Et3(Cd~-@QCaBfDlbfG*DJfSxV|!-$kjTr@H%Xnaw=5r zaw005oj@b+7cSS^*>SUT)_Ljof8(X^`lIvg+EJO_so0^n%5@tm)7L0Atg8sW2y17yhhKuV8daOG)ax+JvpQ|g z_Fg%zEpz1+e)=7hPufOwTCxUO=^f8))1N4;7w>SZ4C#Ftr<7A_K=tYezQMq?&7M zbJflsU9|l_Q9P9m&NWV-867?%eZ?V7nQZP*Rdg%XDmMOa+Pwt4s_Oc8t-a55=XuIN za&rj@TqKZ7CQRkZ6bjn>k8LE^bAR>f832~}`fMReK zoY3~KV*8hBtyMoDCvWX@Zy>bv``-V(@12u#_CEXUaSgw<);>3MwbLczRJ;c4cr{KE zJ7nx-qMW$<=>8IQ;zSpf=b@8$9l}oafaL_udKW1jVmBr#&S7iM$ z>+`JdvSiU_Ko;4=0X!$F&^6M54*o4?shDmHswp;~GhlN|UfUHhYaJHt__?b4DdQ>L^RO}wH))SMDcZ@*D| zznz~xr9EF{9!j4AQ9A1C#I0W3&mDD9qcqx^h+}*1fU)g^#$MMxceJkVnZOgwtfHfr-?YQnGpv zS#o2Ysfjvnf64nsS$(qO(G{te4&VcMmf>vVCes|AZP>U0$uv7z9!aMClW91aW+u}N zp0yqO&|pgMik*D$&b(nI&JIdx#YRfY6zLAlR3fUSc0#l8dFs!tcyz@J6u$jl01){{ zxp^hRKiZiZF;elVhd`b(PM6WaY_UdBZ*Zbszl>x(Pf7GVIvP_V^U$_qNM1=IlNl~a z(x`2R+gEaS;`DNj@x4oAc4qb$lwGsK7Zg3FUh0HL$cJ1{*Dk}S6!-mfy=OdSQcbd1 z#CXvg)Ytz9K}jA-IWpv02+q0cqbpwI6Sxsy(=;yM(JySw=kguVA&JaRil*;-o@y)? z@rcnu>ZK-7DC$6?B}=v>79?64?up2FLkZfQBTrJgMpwK{w13*zgn8QFz|%Xk49Y*< zOZle-dV2 z?sWzQdMQy)YI|1K>J!>0iZDT^q=FkbbQVtff>K>H0Ul5n#Xf~b(fL!sy81nQ7vDa1 zMhAap?9JDWF_I3xBk@bH_V~08zB4k0h6Do$qSztXsj_5`j5!~L&g+;4rmn@E;0x<0 zCoX`)GFOQI%`bJN6{Tsu1}J#p2n_= zo+s#aeP@Ho0;Bru1WnJqHwY`nuHPe_mcA21ePs|rBpuR;bA*siMaFU+x# zqSqThOTyP4Y#&Nil!|B9^-|s#>+F+}Y&Qw>dc~Ea$i`l2*xB}!2t6F4heCR=iiaDo zY?mX3L8kJG?Fx?CuEMD8W=btEDiyZ__?_Fw!5!OS+$9}S=UC{Fo*&z8iME@mnI$p` zMve?douznZUU5*dwkuSsMn+9J`%7WD6GC*~Antt+DLYZpbK?AReTFCz_fz8_B%J^o zd!^?e)QjzZ{ZCfjwr4%O z1~0>2=zv|YneIR-z*DfiPyPV_HizD>g{}151{;ibZFFsgClkG470$%RaTZR-OaEm) z^XclMa|T?7;S0=n6P;_}F3LRsO)!_vH!uX%bS{OL*&T2X*J!McWbN{vr;PMAxRNe$ zOs1>0XDQX_f;Z>{SU|I%O%8nDqZz7T5skDIZaP~w5Vo*q*xl@Qd=RSGd{~1o!W(Q0 zoMM~dcJ^ynov0??1|hZxG~(|rSO)jP5*qn2$nN?6W_-|P zuw1_gZiaf8EZqiX#jC9E`B9(7Z^J0@;#p&o8)!}^p^nzS5>~+-@SyS3b#|@4pwd-z zzX@)I3ETsl@I#|~HEc1sJc)u}am=E#mAJY`S}X17Q+^%Hqx(_1s?X*E5QdAO1g?PV z;U0L1r0x7e{MLUeQTz`V;QDHKj=26DN!2={)p9y7eEbQH!vp*#xPqU70=$^!mdiF{ z6H#LlA46f`2ABv9Vt#lR{D6Ll!n=LOSVnWbtLINWf1?n@P3IAkq}$=^PK^DP*zZKpaSv$>-~N8=dRr*>F8(z;V7`3elRY#XDMCZw~N7 ze3S3sPvKkm5q<#$kVkSeg1y8Juos9|r)UkAu|ve2Cb&b{q3pnizLTV45FCP9l7=gZeoN@wf%_p2#nNPH^8Ybi?xu4V zR1h~O(n_Y$cngRg6R70;qw)f?`PUM0+(>O_!WB53Wczjys4Xno@8Ch{IJj{qWJ3%3 zK>QSA1%3wYRA-c$3x6iNdK}8A{9ZKEnL^TCLulPiEvJx1t|L8~O`KXnGrSj8z(Ob| zo;*y~5R$3OzzI)KPa8;!E~XKW!cjJk@)tOi=afgltGMOs!6`i-v`G%O6NXQwSzHM(lD@wNyWvXS zP5rGW3bxCSi&{N@B`=|w_f*ibfxQ2QmAz{ax%l8CEW1H=0{oNO2L zoRzfXxWC6ISpe#A6TCwb-axzrMIuWi`ty=D_9xGC63xs@{jH(d%#_|Deu|?KhQpzr zFDdstQS?FmcSh+iNSE(qV__W*#utc|>F^aHgWNN!=QQ=YitMGI*1d{WHkl-6B5`9n z^*sQJsr0x1E^d+&1{rgyAi7N;Nq&!{eLdx?$PR}1YY?Qh{ehRVyJ>A-Q-dq$O-*ub ztNEv7>$*sWnrICRsC6B7@GW>yO@mi(+XelC|KXu@3G1o}FSCGnn@j7u1MTFkjDyv9 zvGfTDkMmAKh!IKJkLg4EM`%v#iRxRZ#+57$wiAyUh=Pw1-5!LEq%)U`YU;0&{Jn$L zeiPyFJ=F4kBmW{BB)=eN02?tt7?q>}&u9ITV-!>0kDx-n!Eb3+b%c%&^MBGPSHpZJ z{8uLac~J$j^_-H_xJ5+?LFOh2H0SXy`h&b!S6Wm}nwE7XlD168${UG9>+>dM-a z`%)m^epN!>I zR@O!_CFs2TBu%J9Trjfqy;7PAO`;$s8+X=p(X3%z`dV?94(lf{^zFCCojRPLU&-?1 zcfkr*#|LQ`M5f6>nWGf4jJMPlom*0I)H~*ynRnK|@ z`{VBa!Y|0I(QKZh+d`6i4*s}>wWAxhl_3J=Ro9+`ktd0jf~pI{$V za{<^LUoZkEAiK;PIbM=#ihkTK34L3vXEV@TdjyE8T8=k-n6DgZE(FsZI>x$m2cp^grwSKkg9@ zDZ+mVWgv=Za9O;BNvkX58FGW%B1^KWP*}HkhSVUnNF9y7;0oM`vWfxB!7`kK z&A3~9y%BGIhomMbrja{Ih@IU9+W~Zo6Wq^rG{3= z(XEBh$~O8Dp7rA#{|f&`KWD1H$p4d{+ckcSvvlN6f2feu>2jVbLX6FnmBHOHp$Q`^WPA zJ};9MH7uFW6g+cs*%!ZSxNAM`aUnl=?C*#5x1Si!-kz(!T;J0)YuTfZ_>O$^?Ndwi z&%STGR?u@QNp&jGa}WmOE^8**c~NL%h%q9q14A7>-^GVn?S(mxA_rr+u3{G>kqo+K zxOn+MWD_FD%d=7I7wlKuk6Tq+z{Ud1CR*hY-N!2x3S&O-tUT&RzhB8#!)OhM`>o7Y z17-xX*|svSc+^}ZK8ZF()J6>l!bM@~i!e$hO=C`kT%Rv1wKX@M8@Ia)bDga`yg19e z*c2bk=d!5j9GyQPRoI`*6JWYx1`Vq!4N@lv93yEHM6Rk7kXqSYa-cdv>ncNU1ZT)% zizLYbfw2A1=Y>etfSjU!7|Sm-NUs*ksY_?yyX}ej z!zcR2Up-^Yvg+Gf-Mb>+ZrJ_L*LW!9-MNSKo zNnBO98#I+Iw$^aecrGvIdXPB2Z~}X{oEEy0Tdw5|!pTg%AoR!ZFKXO37 zPtr@UN?(Un8~Af)Ca~q*Gli5*CMgTc8z2qM@${oEoaCDBVywu9<+d6dD_3f0GSUo* zVOmGeTX9eR@WQFynO;`x#0E1Sl*n9(@@Z6l0>saPsLwzeox!vvp`bSu3XXLk2NpaQ z_&V@&fCqeQb`$h6Ep2s3DYhb5t&WwN=bs^DM?)tT=&)PI?u@k5!%GFb!RqNCZt zHd#BYj5M~`jBHRAzMl0_7UMZSWC0&yg$GwSksUIV)gsiF5Dx~47h1nHDq%r6EZV9DxT-?D38HPhxd^(QC3Pl?4pPf?}4yS^9u`9Jz zRw>kMrl3-?Fd&v>6HEH{VM(<>*Y|{1k{30!-O%X1m`eT=G3%t@i=pQR-;&0$mRM5T z%M<~}iI>I&hV>*)xd~(=S>SR4>3TpNAS@>)S*G~Bezea0a@U#DZ@sG@MO)94UxaoI zJ9z&A95;8>ss(elJjH(4tN-}s+xo{?j7zZv7k51C{EPlK{lo6&Lk~XKxozpgM@X!% zC#?^Vo|vIMUU!v(ya-8*6v<<%G4W zOxmm7lW^f=i7-)Qna98VHD&4hRI~bh7o4m2ufqk%Hx!SOz%W zf;odRcL1v1fR}_aXlFuBnWhCb@)rgRJ7gj|G(b2Gnc^vnGAZIowV;E6#h%jYjeFxR zZ=rWUE6XH%Zx{0FbbAZ!gIl%uQ1(8{)80EPoBPkYY3FQ{@B)l)GYO|HarBC-kW~`Z zk`luNB_+`iTaf`WNRrDdkt`OFCr~he?g{IY(?=RTeex6vc=Yez`#}H4ius>>{ld%t zee})gt&6AE+;{)<#?9OAUAX=!9;(p)^rDXN%Hp^D(wHyTefaqk$1We;blnXt_g!;a zV|VKMg$p-5b9dtwVVNqD+zUc~^u}MT3E+%$oMA~|NsR$AGr~B7{Hm$xc&!Dm3g9Fg z#%!Z)SJ^l(BG#1$ILrRPKvquH?kvfgOMx9r zOUt5Y#zn|s^(=O0wJPxe*10TO>EodhcZk+5LT`c%ff&7Ps67uddX=EBMi_o|wc&9m z9?>q`>Vn|}6ap>`$H7kk;*$F^<;H?Nw?5mA?C={nNdK<;$>YH-17F|Ju6O-*^%HaF z5n$|?JQGbHe1v{|uYQ+ar#C#?VLyR+s70P%x@_N@ZHu=HZgh|-TqI>1QE6t}h3(9t zw99Ugwqcn}0V=^B8F3>(phZwT;}S8=B8q9cctzLgk10az(E;QN@-w+K?mY82e$sU7BlqgR6v5GBgy-K7WgQf+ zL|}7#77?`BvVutVlSN{4!^oQnuV<6<(oA?wFDY1zv^nD@&DjdRI10xfgff^ZM-@TeGXv?(`;@L9hac63y7SHChSby!7Uit1Wbs9m) zN#W&H8$Pii5m7LtzeI@lIhYsL(C~Rlj>E2SCSi+`9nKdq#}K0=A;w|;;r*?1zu4FP zCL4g3kEeI1;w9HFx~XRA9c1Kg+yB_Qcl9mYI_SlF?m2Ru-1+r`)BfP`SKTn<2LI`G z_uV;X5ixv5&k1>s{4Ru`IbM}3{6Cg!Lz~UH%p_6#5eVgk%0o4w6(JcyA^$S&X$fr# zb%c(Eq=2W~!&+=uWG%DKvGNuLi=YhV0EKxTH1W{AV7u4qnWw~4t+QB)a%-Ph`Gb>* z$jU(6gxgjtl1j0YfQl5!6iOS;mjY@cJQfDOut=U0*I)Tee@x$v3vuWNIP@1U4c*`N z-Onh$v*l6qzjlf+s`YZghQW*j&2-TWXw!KF|eaBExK;ucP6g`1n0D%2w2a2Bd6T9|vDYHhT{ zQ&n1+<(57Rt3EH#5$ax<^kh8Y{C?c$gn?KTHlmgxJQ7}yrxyKj+ro+Z8+uFkcYATf zv=ysw%DK6vX}bP~+?jfF$G4mH7xe0_!I|G5y#3|h{n9D?_;n=De=$~aS$vSGO^Gv9 z+JZynHpb0u6j4i>1hUE8h;ae9V1rrAipC^`M`1`}sf*%_yuRW$Z0JCAk;|L7q`#cr zBW0iY9gm)QpD&U-x9ah&y7)h7qM{`Bu}n}F&!rLL6y}hUV4KEO8X3(JFr`sVjBKHi z&Fc9hI}cPD?#tO>F)<{CGE&o-FWL6)U+?9g%2s{E*6tZJP-09uVyw3`p2^#^xFTyb zriGA9He(~GQllKV!R^c{_f<9m+&n^@vjc@`2pT3v_m~{n*RAZ`%TDcMJGy@*cXn@J z;@=xzpy3*6Bn}yIC*uf>aYXs{Mp~HBb1;vX3V9=q&SKcOZ(m}DtB5Lj#_nENw3CPn zVpbSeM7yMH91-L;$V^j^^3L3{m%S@@p8A2>uK`f()ZRnL|Ke0PPO{(#Hx^iMg=M3G z?$SXS4@oGw9qt88@-imL7KdelDtlE`_7QSzavX9nR^gc8XmGSRB-9*1N3NsTA;ENY zw#rzU+N?6JA~rLuAosJ3)sPpeFmy;Nb6W_qAq(_y*u^!LvUp0u2wN$5;chOUec=^&UlapX;^iRfpd zteK1nx=FR&98I)%KL6h&>LkUR`9iZt8fL7dc}Q}K)s^AI%wi%Gmm>$enbdKB>Czsl z@ighm=_aX>@Mjv~?8k(&7K#eD#_u}l$3tE`c+CB^oAD&-F!!=U z!XzKHp0E=0tO0Ael{4$Ig9<913i7)g__gC_2jdkEa?Xh3%rTFvK0M+3$;U*r$y8s! zM@#k1GsQz>je@3I`$lU<*yTK#NNHihG*#Co6GEj4#yV# zPQuuI`n?p`6n=(2)W7`T6a5SH%kSz{`eyxU{W{!=g?KYQajKKzAU|2P;rcuJp&tEJ z;X6G}x^sp&V+Zfy8*HgMsK z>#ovI>!0hbd-vj9ICbj6CVjEonR46nHy!?G_g3z(!xiCN%E-*1VZ!Yp@2Al=C=a#q~!E6j?UEQMKeU|Bq9L2i*qt-TCI{8L6R zFnt-J1(!XVH|VYKay2E8Ql3I4EHlDf5`;6{DPHc>?BQ?LIv_`hoRc)wpIzumNSZMIMZ$lVe;l!S>4v>}m)?MLaWuxT_0zff zH(T`oK7L&P8iU7gE5EKfZ>P8U()w%gDZCsf;1&|Xt@`q#Q}#VULg>SbN%W5EU+8b- zT;hK)mh=nSN&fth3ElA$&5h<30!S?-n8Lu?FRdSiwE;hp^BrO`fd+EkCWdu?T{!6Ji|uL{_*4~fd;YNLp!3{9v?c$-xOV{8a@E-=1hqrv?d*;?DPVPS4BKq6X2}OTyCy0b7Q}i zF)iJkCONZa02%=(g#v9^QjOQxbja}*dCR=zUd0SvVxrTNuBDkJTL!$9o${7Vc`L*F zmM5NLo5corZar^nTkLNIN)kRRQO|H%D_x0AnzQG5+cF6^7ZN)Ng4ifrS`mH*4Co=5 zKezMPe?YrMhxR?$z5Leeo1}KXJDI{=T|~+i%&sy8Wv!-xZX5@vu!;yeHtB+t!lpsd4039KES^_TADz!HW>Z{BvlfY2-m( z?<1=qHh37S{(!&8U+$mdm#vx?*&q)^YsiB#BBq7XdY9n~`4-5`E6YstI=l;Hjk21F zJj^Du$!j*7#7=!~h$oy%?qIU`5HQi?Ds+TUwwS%XG~g!kVcg?k1{?xjMp}4~^bpzZ z5QaiPopm_S;V}ECV{=*#iJ#a>-UtW9xBPwh0S$<7!-F6Aj5M|5h6N_BS8?6VHmb? znNddtw>K^@;ym^5-(k>kpna$6-p&RycvDPMuSA!Mu3x zBeLat@DHtzYx$BK_ZiurJP2H@6*+rW=~7|u57NC{#QmH_OrQO;GgYQmA7~|sDK_7i zz!dCLmqcs#ME9qt(tzmEgVAM&KOW)|q#Qr>fBxE(Oetyjr%dD0%Byp?|9!K(=jw0_|P`C;(b9;Sv!lr<2D(MT)~_@HYYhy_02 zH3})wMtaY???$WDY3+Y|hvk$Jt)|<%b5cYBa#EW~9Uj>2}P<|wkzo&C-EKK~ldnD1JD^?m(S(`FRCJ3a7txjEqSo}}R?SZx z#P9FFQVS1BuNDStm0ebW8(F?y;5}Jvt$UC8;sl@B!25s@f^W0)opshH=Easy%DT1| zGBTN4aS>af9YYHWN8}1!n}umKL=%(6k%|Pi7|gGf@nWiT=NladUih?Q-o=jLwLfld zOL->r)f>OsF(CdBXtlno_2s;)Hg?>(>ZxFgC;6&zrGsa+_#fZAXwxlUf3S|O89TJ_ z{>!fY8S@J70L?So=p`YtKfKxzNDT-MN2)_O6nrZvID)BgqN!AHih&^AopLlq@CRt2 zLW?Bohp`z@>2CF?Dg+$I9fF@^lR|QgNCAJ2zsN7L-I~I9P$BWR3tmTjyXK2afYI$@ z4&VC3jzq~VfMHJ&6IaTqo)TAUTsU2>5z^u!UD5H0gWNB+HfY#p0{f3?jp=g{u~%kd z3**+#3-iUYGg*)Ax#f~ZX-UPNSIEyl!n2no&~>g|t|KmSos(+PYU#Li zN|H2fwI*m2Db4oNoOtSfG``*8O?}MYF8Xp(i&6z^A{XQM27*UOZ6)s3xWtu8I2czW z7)oW|1t;zreQ^T*vno1|#Q;6liun$*wc?~$U7}F@GV{AfK0SN?yGu8JW^6Z>&0Dwe z;rp6f<7vaoFVdmk{r+!}ZBH0C)V{s{;9IS&(C(K*tv<2a{a|?I#2^(41C$oR=h39& z;Oq^As3zORmdr%@DU6+gY~=~;kw8VsCxOlI`F(;Qx_mC^8@rSvicsXIh4EB)D~^iC zAzl~b{OwM!5YLB_no3>Fzt2wHnz}1hjuR3+CA5{K!ca;FLy^{FD0!jL`?UA9u@_aY z?HV{e;l4{o=EzKiZF6`N^7H%<;Pz^J@skdz{NQh*e?Rlmz6DZev}Ez-wRhch@8mn% zHqnu1pd$~wUvPi->jQ@l{OO}zt9G%!R6doB3~2X6VvsH2hL=3_uTb|TFk3=Er@0{5 zE0t+cpfn(;;N+d zrbJ9@vJLF2l5B0hn#Y#!q1OUo>^AP`8X$h(x%Rz>s?NrI;#-ZQ&!~?ukP$nv-fNfZ z2)NZDzJ#h^RZZ(^>y~~p`XdiZ;{$lu;M)pp=w(Hc+_kulNEzeUA%2$MFc2OB%Bbd+ zY*hQ`|8Ql`u2oS8lTjssSUh>))pOOCU=(pT?_ASA)7LX#b#DLmvBGCP6wy7j**cDN ztK9mef0OAN@!V-`7o+}T_U&hja1sye*6Du#q-0-Y!@Q9{&~+ZrT<}j&;{!|{-`^3< zVM#ZHXqyeFs|YGrj8xp)W0Q8NAb-nE7wMfW#1Q?+g5X{D?e=Wfl8-J)YD?!XG7zd1 zo1QG*GE?Em2}nWQLHB*73JOU1&*WRADE`ylw{;Y>is_t_uoE%{J_QuNpo!lk=@wIC z7?h&_(ZdERlx=xeWM;2kKH?E6RR{~7t;Sj;*1q$za-$_5BJOy*G;X^v<`kAqjFyJ`muK?Pj=sHdo$N8*!&IblFN&9IS4AzTe+;$CNS5z5y&- zH~iL1lr~>)h#lANb6HF!_YPl|X1b<7#gGMQhmE`|?N(-AxJQu;w^vE(kDBh@L8f1nw=7f}_z z#Z|~GNTCrXtk-F<-H*;+nRr~Q<#W4^)}S(knpR&FMus^q0VqLqdlRnA(G3}skN z1|+O*6Pa4I2MbAdO6R+ZPvqwB511AH_zNF;hu!n2J6zv*7u~YUZQTCx#-a5-Ad2r7 z^qUpI+&jA8VC06D-0RW)B|dfIjuLa@?Q1Q!AN(O$r#27fjDVf`BGzE+KBep$Nd(15 zIH{RA0&3A~fuE+ul+;e#5-LBDHpoRLdhpK`VcH&%M2C>RaYnkqmWMh*Ip+yyG;!&R z8-)FJjVhUJm}*#TfY;wLMKbX8B1B5l?zu$$()uZ?QWCipx-^hF_oY2?rcLU)iX`qp zZRiNI`xPn=ugLs~mcAArZ!BG0jJg=UKO(QPfYw}1X{jc)D=r?`B1Z;p_Iq8dy!Cb} zl9U?W<01s40k0zWmaQHhbM}GphD@bQF;h@X4-(hd8QR?w4lHa0Op)vmcrmKDYqHTm zOuYvpbiIdg&fsbqYYOo(Xn1E-Z*qbWL+Pf6E47OD*H<;;4Q%RPB7!V6j$=?&5L>K; z375Z_aOpV=Jh31}2j}yvnj_Q}-2#0)I*!rix@GdksbpXqKJ>TQ;8sqxJ*3U>Irhy) zXHVrHfZ^y$ym?6zvsv%CwU`4gFKG|x3jyjES}XNc1r-hXT@|ru8+^}hr|lt|t`UlN zlek3kvBpIfEP9P5^$^TrJdr3Lw&#OZ>NV3 zQzNBo%X&)&%}-#ed=#SguEVN`mRvn1eqYN2z~fXmxw*i-o6c&!@;_(co?5V=1O;YS8(D1;R)dt=D}d;AbljwqIi`~ycdux?Ut*Fzz~nhC)dR{c=;eG zXjXPt7#~GmtH~3}|L6Ak7fN%|G>62_ZKMSodvXt{(fsV3^b+84(56TKM`cr00K92Z zu~1P9X5-9&3SAn>P-?dvYVTHET3c<3$*6O%Fzv=TycJQ#ZUGqJ_n5R)N83YA$y6w6 zXOmDiqt{m(+@$#&5B`K}DW|oTnd6c{kx*@syQ!5jTZfjD=;^2P(TtX%Tcvf6Lz$uT zy{bsl(QKPoV>$q=Z7x_I)ctZR_m*c8xn4?N5r%!(vE2VisF6?|d{FavJzsErasOlY5<^zA zyhiCn8r7PllHA%8ty)}odUn-UD}X|;QZ=1gHD0{$dsGQTFBp#gX%8t|w0pwH)Az3_ za>%$tX}FYu>Q`zrwf&N38w9r@=!^GsUUcXGAfZWRu^zK@ZqCrC8{<9UG}6T-=P2Os zxjEczm;p-Yx_?=oo$vLolD+&VAE%f9w5rf5a1+^fGkbg%7?mIM5dd!Q9b?*ben_bQ z{NQqY^PVQPNYe@iAqBPYmolDPI5Hjlg;b{SSzCCOWXp_oV00iA8yx$Hr=4&uo#~wQ z32iKXN(YMI*c&>C?okpbpbU3bFwaRyFo**a4`lHW$TjK`B=EfEyMD-S& zp|f^%RR8WR&4G|)Hh||TZ9|yL&dcjyWo%eHQqyJY|4tG+Hge0hwx!*swyTxj znf#MG2K-0jVjOV=eBVbNd)rO7h?7H`nk>IUF#w6L@>ufO70>m8nxxo!aMXZgL?ayM zznK@eSFys3iK||$#skFd$0-DC>4E#FZ}n?+9Tn!hYx>D_Q#&$>`gBkHWo7~hiWH1J z8m?8$jUF^)Dyf;{dVV+-Q^+7B^lP^fj9y*MVJ$isbCH{(!HLB%oUHb4`gl5w#&Rsi zI>Z|NADa@xZRZfdo-g)s{lOgS)iHHCu9tn4d+djfK#^V^8uO%|E^1z?KZ8J*^sOjN z!#PuJ!6vW^+Lo4%A{1~o!k!ovhE5%j=md-+if&$A{69+suiJFNe+pL3gjV*=x-%4@ zneIu9Mt*w>96BL3K9)+`UGA`8S4VVT>-?AOb*tk2OpIqpFmKInsI$7-?rga@^%@E@ z<-Xbd2I~A&qYk~rXKwX_JMc{Q%%?MdMX}R5_T2gKmVgo^%JtwoVXS~I3oJ@~xBst? zVy5XE1Lomx^pZxodi+eS!lO7^#$VOB_8VWbTZ>0PF)lTEai2O#n|&ai_dD$%UD>Hk z+N#oXh3xvuxLC4L=~iSak7mCODx>KvsQ3$;0XJNg#1QiZ*M60%E)E$b87H(i`|NW= zBGhWtx?btXd$MjWBG+xb>Xy?_=6a1y1wTHP*0|!g7@^f+t_VFGkcfxl#{8VizE0Xe zqJ1%Eo?Ae^+4I88C4+%7auG;Hp_PH5d_lXw^M#bqzPS7O;z!g9u&Oq=6wes1MRx1R zwO@*+-WP45r5D7m+;#K%7d6;4?m0*`al;VyBru}2PX~@#*pUPO=+t+MuU-Q#47EV$ zU}&8tS9~Yi*!yMc$4E%98Y=#f{cd-S(TavkNd}lP733Y!xe2|*ff!N>8O`` zgC)mcdQ(pD3H*JA*k0~=cJ{e@d*h^M-hYlVuC7D-gZ7+9HjS=)q%LiA0Ua^GkTnss zrY%3Z>42z?SjM@^;zkm8>H|6Ljgo`vV{mHx$4U3VFjeembtXT(uIF~=J_ZfNE1Om1 z7M#nGRMdJG_)%u{h!I{Y+cs1$OQB0qw^$G`LC!non-ANsYAUs*=k?FXRr@&K5a5TlC8`klSJGf1G{FzECof74 z%}9wq#f#EfZ*FJ*cAxws=)>qWknkExP#!ni#j#2V>~?Cs=M5=>^8XzBFn3VFB(VP< zkI_$PSki&eAE9Vx(?s0#S!hf=X5-&f#`Id~(Z1I;npKW)usSC_+Ut-yxPa0%8h=l; z99->0vn75{6Ca(I*xGJHXv_$hFb^vaoHS1>gMV$Waej7XJX3D7*r{PqGm&jwZxsGF zhoy%l6VyDDUPiP$`jf}QZ%I1etDWI&U+yH8fp0W}-DX@-m892x zoU!qbT)b<(D&I+-id$Gv5}7vlbd`pgUi>t^$zj+*o#^R*KR2#(MwMxlt#6BWE=LCVChK_@xLxrQ2VGFZKysee~ z%8d4={=9O5ZG3Gl$1{V1g=s0xTpPD4L{uEd0g%u_Aw|c5lPg_I{1k-m$c#@riMyBt ze`v~BS*N3Hzm&$rb|ghoQk4xjlqSch25jo1Slcm1OrPoD*=Ez)VEsAEap`oyH4&K1 z)iG)+Ec(`1AjqJ)sH(TgO*gkP*hrMmXUE+;*kf{gkGrVc2+DqmZ2!|xRBSHN#M9eQ z6sFPH-lxAbV=`rA!Ko!?KhYt*;WrwNDe&;ut_@h!!{b5cE9zR><Za?3K&MpZ03@<%Tt#iUam6{b_& z@6@5|RoLFezhLlncaVC&H|yupf6I*I@H^3V0gu{kjp0XD!Hd7N*auqip3HhRMR+?q zvIDF4r@6jNHO^Mq6xxb|w&o}5^}e~VF%kRn($REUQ2JG$I8VEM{M0;k83hI-gg5K> z%jQeDj5#IKIiLj6^iY4m#UBj`?9T72wiQPUm4(x{+D_gY^XMMv!X7y6YiWp8$+(Pi zv;HR-k)k3ji2KbOd5@V;U1S5qR7j3~Y2t1;I(_iweIBhJkWm(PSrz0 zQCrL8o1?gr{O-VeC_Pij3^y9b1d1*Ujq7(=s}b3cL0F~%p^jFFIt$Ona{3ULKLHB} zTaqb7f6){f-yFa*9fIxqE((i%Y3Dt>)K@cxYsSEsGX3#!&Atl>hh?xc^MGQ|+!RSh z(hsNDkd|M!X^9{k7Ko`7uNN=rEF{aNLv)d&y`V9Ecj)Ob!VOItO;OELsIH!ddz*C= z_FEMjaIg92jI4pS(ia3<-r>7;4};%AgWz<4c}r8-S(dz1jPL#XW%?Ck0te^m6IfG5 zTc{GK_h)meJ2!)@U&=0Yoo~stuBAW|v6ty^x#0r`Ut@_a=QGM8`w2a#sBGN(assft zLSeiA-W^`vbfZ0frZooC@)YssSOJ^h#oUilSfnxCcfqX-{$XJyKAp46@afrL1$#_k zTamJbEmySy$o%B#bB_kf%PuJM_3nhu3ENv1D-V?HPG@g7?2UnH zj^iWErUJmIqk;2a4&6ZMoE#6MnxltVysL&IR$oKoX?a8gtm-2ImbSCgVWRh?Dxar2UtJ;11m%>1)6eX_1yq#bjka*+~jEBqr0C8wCrxoT?gKcEQmTlfi60+?+mp} z*W*`KB83eU_yp#v0~)d(A}0PcY=uA2xL)a`5vJIEHWN+VwJX;4to2W$*1cnOI;cHm z>NtEZnLXCobe5(WG;wuW@?N!4KAO;8@MqiMkJ#|$EH{tkOObsp@hV=-U5PsM=Cm=4 z9v;-5M{9QojP;bX{v*;zN3?w8ytOKBrz^(_Xf-eLS!xA$HqRQM;$zHXgfhj(%FCR8-*Vz`@0jM^Z;)0CjNS_8(ei*nJ+fqyc< zY_Yfc^By<-1m@@pRS*bBNFzJMWIA)!3fst`H|lt;!*ZB~e#A3Itt-iVz<-WiSm1!k z_9%$E&T?uIfF}Pc*z`PQHqlFSSl8^6Dp};SdAX0b*yICw_gG|;S(2ZhF{=0`!TPB9`l>Yd-xhg*#h-lt3&k_4mJ{yGBBlzEA^mX6LU`eiGz! zwqair>UTYawDp|R37mDY8%wTK-{QePx|(eYQx+5QR=0D;SzZ2jL1Iwgju4qKT_%KR z?`M8(j!3kezFmFoCX)c=d%E%o#fdwatfO=wYh+uyC9>{%`Tb#s!FVi-;{U73~R3LN_#x%>{{A&3>Zt0(G5|2Re z!gYz{Bw8TP;4VqS?~j`lLjT={+=h?{{cH{C(lms><$W$EaZKgv2Pcf?2nVQN{`V~N zY@Ha$_(`yDG&;xBtigw=vxbO_)$GfuZ~6o zZ|Hp^+FnTSh{h0Q_mCI3TqBJw(&Cr87sk=6%BzsKAh<85qdfNPNEsnnIP`44!2;Qe zt!tR5ZRF?Iklc%D2=Cv42&2ZI8YD`0ArEg&Z+ya+_d-hIpZWh;(OfJ4*NmZ^vr8ok zYEHElm(R*)CEUlQX%f!QW5wU+p>Y-N;XU|zMSuyY=Mfqq=;!QITVX~yn(y9*ApXR9 zg?6m{Gu6$X><6`}k5RvlB_pLJvz=*1_u_8E*(q^SNX#3Z5mYyS0MJOSAQ8!+L;60% z;VfXPNe{w*eb2>)b_~7G3AQJxN2-Hi7DN#$|56XMQ(XH?g=x4tSk5E`^srgM( z)wRwO^2kKPOhd>cm6Hq<7J*zrto`JBIT4})LDc>6Ii`p^%###SbJ-X5#Cq8X!9+p2 zcg@6v(m;qKrb%v5g;t(iSd08Dsh@0Hc9893c9g}dCY!Lg3Wo}b0ZrcUqpjJk{ovLt zIuS$jx^L}^`iQrLhXUdYfl`iG#lFZ6?l`@uBs>^`Xu|m_hI^`L2a?&Qt7DZkyCUDk2o*7^bLR z^evPvtS!t>HGa~3G$HI>tm(+DC>5DRj4@L0GI1IbCiGE~(hb^0`E^wRON z$~k)LI(DUOCJotTIk0L;v$ihd23c*QL&jz5<(D+YDV^|&bCLca`&t$< zr*eXMV*dvtFkMkjMJEM)zt9f!zE9xIyb^-ylBAEIS*V`>Qz`6=Y~n{4`)JdyX-LgA zE_aZ#zrL?=YlCz1*oU)4Ekx-#x_+SOozP_@NZ(7=OG=!vl_8X_G-d=UKe)murjC}A zQT`^Y%#s!$8_%kZ3y4+zA~aFd=vSroujE%vC?Ebws#d37lx2uru`sW6NpZ<(l+~&I zSN5-*zqCe+lQ!qqz6Z4u-86L`$ick6(TJylK*LbQP;FB^p6-f{n3gJ44Ri$30$CsE z9n{FG%PgvAoNAyi{>D>R02XAIAZbQak}r?}wGpc`s>)7(x>xav<&_wg7?x{REZWp| zDBEjhRB=_dR#q=8o*JC`pNgIOxR-RuY}9yEC{<5abg0D5YB>H})> zRfV*ARh6o@sw_D~=Gj}*E6QxFb%pE;IpyhQqJe0&>N%wfix$?)R?F6Dy0uFc42uqP z4)YGnmvcljOSbqxg*7buoE8ILO<%?Jl8bpHs|cW0^Kvs6aBuB(En@A*Me0NQLz;)~ zt0GlLscnax(#&@st(Ca%qlpA___57*Evtoma; zff#Z;Nhh>a}m+bc@w_XlXwvI72y6EPySCT{C#Y0_nfz(Th>M> z=hieakBvgBp9L!3k9+u5Ktc9pp#FPdEJm>eT(X95%RrjsoCqi+B#i-G*8Y9U=GU!d zQ>y+``nYZ8c!9;=o#yf5Aabkmcl&o%Lc}KUWn;lGdm=J8W-;}eE_@(qtz=l|fv}FK zHe*;%hV16r_8Y%g@DU<8XG`U0(K zZyjf|?ikF--?}Z63t?}%ueAN|Mua0C?AzS>-ENAM$8^%JR*aiX*5Wgn??oJ$!?N4_ zX!kY;!ZJ6XT8x@mjOv$3pS!KU^Oo+YFI-C>cwWFfFPlOY(=1ifH$w0H*K5LRsc*3H zGT2X3Yn)q`1-K7=lEO$=2N*Bei+5rk-Y@GB(@VzF0ZWgs&+ZnQ#l%8Q=>%f^PWi&a z1KwC!W9wOImIMSD>j!uP37Ho(!Eca%ASvGB1K#*^wn!7LQNI5n>hl3%I`#h5iiE77 z!7m!R9=o(O4e9=JH(mVMruh3fUI+L3q7ag;!=)|tNN4nQS-OtRA_e-I?F<~^eMK-4 zNVof9ro|Su%EkGzDy=2m8YW9FaDmpu3wXC)d9gS-_ukN~C{JskH%D8RlZOdu+lcaS z_4#`o==^h?AT!iY{A0{`jX`kEVREGc-!_o-tU-T`%$`ErN7V<7 zjGWHL#5pBJN}iEuV{nYah5TkzZlB_(M2|{OQYnaQoC{s-BdRMJvhPzzLH^m{*&&%! z=s%Td2^YRWzW4nC(XRP|5$;v~4qUj|Nv2WeNC%I^om<9d?_?O3DM8j`DXx;g=y;Nn zAzbXiZ_+f%aMCPK5?>bCAZ>Gc-1=Cx;iMlRFLVB^ivLR0+rN<&j(e4|$+TmU<(=?% zWL46ur6;X!Gt`o~rmm+g?%N!wM0(ckuqfgtIM8~zq3_(tQ6!*NAc@Oh9&RMbOj?(T)?qSW@ z)G%u%^2Nf$!Hne%^O_RsnDX24%U`m746KM>a&qG5jJ-=n%!&{(x8Sdjg$yH^!g!B* zO|3|*#R&`(HBPPAo>QE~y$?H%4h*C2!9FAJneUn3M&5MX@IGhVwA^s6nsr%rNfB`f zvMMHyd_6HuQ z|LdS%e3&8l!8sLpe*A4=9U<$%O7o%HOd-9cGJS-7gzP+RY)=GW7*80_`PA+FZNqH5 zf7t(|#QMJXEnDker)*QPb9u3PMI#{w5WOWIiV>8~2Pd-azd2q`L3Tuw*}iEkj>AKU z!6ug8b!@?-imrTvj!G1Ug&?zCA%~6)r4ao+iuR365>i|^HZmGE`gbJkXyGq$adCA{ z7BaHk;X<;Ic-CX)V{W{U$Iaz-?|FFQgd_JOg{%|5V?TP|T|4QA8}k>R(ViaXo}0$r ztI{hrp3%pYD{SwOtusf)h(0@NjrbAoMEb$@gxuIBBAxw$g93{3XSp|uMjT5z7hYh9 zGK*d~Je#BJrZlRMkGylfRls<2Fb&yFy0sc0s`MtTA2ANg}6U(zF{yoD<3FJ*tq zcdESQj0q+LS6oFF_LK;M2@xx=B8&6MvVGuUG{x*}$^>o7ZWd&7ROu87HP>==t>JA7 z7%Kf+iXXfYfzv;23hXQW4W%xc1c1{>3xaAD{s7h2bNLIlyfsT~n*!V!!Etddn}UrB ze~$%(oTg;lJkWE2Ig+CJDfhTm$k`p*hU{%0kf&-l1S)kN1 z&cU#4P>2SvP!=JI!DCDt!(*%$LiNzMn8IV2&wSfLV_I`umm0KP`8JgE!1J2R4g09~ zHFu>gbS!o#_fTXoV<7%Ja%--=n;%ubX(|*+1oq&Amn)OfGSclVYL|C2j z#ay!VDVE0>8s#t3U(MS28I?e6x4B)^bg7m7>ZP}pUTGD{8rdLq9a_DEDnE^iXpN}s zdf7!*plq#{o_f|=?L(!DyMCU!RSff*)@MKl-K-Y9_3|{^5n#$1?nSQ0SL#GTKt5r?-={0Tg6hoL2asQ8dDiaJ zV_1QGM)Bg;t!@OU4|evBF$7#KJ)RuAIKP($ND7r3Ezlv%!?KYf7N9!*#2b^$PO>(` zU6rg$a>T$Jl8j>6qUK1)t&Rl`W0g#-*q^h3;`WAR_F|o<+N{(w0k{^D*@A4F@}Vl6 zlN0Ps6Lw#w3S97$CQXo%6RNIR?I3+e2bS@PjYGkvfBX*l?9-V?6EPNHBln@+WCDUp zT~xCv=Q4L@K8^gV`T9ydGN@XNn2FW^$7Gzi>Ap=a3@@Bz+zWo6z21^U|**U?drL;tShAZVL^OvpvPW zE{L$C+x{N!KNx^*p=h*bc`meSX}o#QuwrjDGSXMy&UYsrWCZ=@)8|O?<}xz}fqMah zu8VatC)a}0YH>Hmj^~oLL(v;TYLFs8ksAErESrmYG?c&$giYFE)g1;wu1dCKoHRz9 zGev+| zEsBA)#1>&WG!<0Y7VLn+3@YP`Hb4=34ZZ~;cVR*mss$Q%%{(n38&UY#exMbDrrgY$ z)kVWpKiGuOpNRyRM&jFlLz$?(y!<<~LA_e8x@RV0CRV{(O~fSc1gWvI+EPnKARd%f z<^v>SV1{l#s*Kgz>q|5RbgJw5+wt^dWJxY8m7wF>vv`FrpUhol~{qeq1# zC+H3T$?_L(Z!Wpsh=P_BUDsaPeEI!H-2I}x)4!FpAD7Q_Duy;ebiaQ_{4x>&E8_~i zxj-y$^MmP@c5LzmdrWYa=t?ApM9xiV%hvA4U)_kYg3o^+K%%`$6PlUwe!lKP&wP?4 z=KDaz!|Z^Kd=TpOl%S$ckSkU~mVdbLY!chgmI*G56m}u)X|az8!p=Rz+A~bE9Gf~o zzAKUVF&|^9W2o@_dl@Ib;a^*cwv(Se)@_J;`J;k)SHa%{pNgDz#c}CO_lR>J`$}h0 zaYYOD(W?4O`ilvC!&n=~J?lP;q(@qbR}C4Z^^yU>f%>&r8f|Mp44{J9ug zIjnw7!y5h9GMfd_FFMX-d$AdLn`Vy&*1)j_CSc7APn7F#moDBk&h+n3NmKG`N>lVF z$Kmd8o05iBXPd)@)H;snj_B9caCat?Ion~rr}l^T(1MfQlYQl3tx(KU6KMuk*ui|O z`3Me429TF-zPxR07g^hz##<1VcD|f#t{3gwQ>{lKuX*ozi4R*By<#8C8Er^Gd@Lqb z=DdGsi>$gKwu}bAQL@Av;8`z)#$?fds&8Wq?KVBEkE}gtJ~h7`oq7|_fG4}urKR(D z#mtV(uA!xt7tnQdKm1x57P^+?!?F~$np#&!D&wkE6xj!RRGH`orJpeWI>NnK^gANG zVf}EgjQD{3_x!u&Om;mDQ6Kk8ww?}Me|05oOe>Zc&K(4EgDygaC^Vy_wCyOje|ZD%sQ$pv0qr+6D))YA;v8C`n;=ljvmZv zh4;MKd4-UCIR=FoojJ>oP5VLc)o&tsgm{G~BOsVMW0Xg))A zkc`O7O8piVPC5IzsFMN@!qf zacgpGeQRWEIYH^lW|p?C5bP*Zy}?`e2k!f_rUYzVZS-1O3vIuXy@vSq8f~id)0G3F zD1F!#+*57XmgJLh))l0l*t}5LVz!jXQhRW$oB06VN7~I`FOBKW<)@sxWHzh3q-;TK zMii?KU`#%XqRG5&A|tF3=TaP@G}`l2C(+)T^RAK)zqskc!mI3ao}{zw%cWsJ=wK^D z(y-7?roRSc^{p|AZZFWX?Lu|~jGX?-&)+sYJd?tXa}#PTXz##Wz`~V1Z5+vie0m^o z_{|k@VJyLK`PJtM?n^2YIoRWOlOx22IYMRJvJD5ZZgPegMpzh^yg~Hu5VXJlQ|G z(EzctFoW*{aS(Mp7m~n4ke?Nu)SZ9*QRBK`ROu{YC@e;r79`qF#-iMtM*>3h^8HNA?=L%hsg>Fn#K@_;z_ zi6W+oAj4AnTB_#4I8bdgTlS#Eof2^+iL)}3y>3dgb7OaBhu*JtNrOG-I^l|NK7#CQ zNq!MOZ+aEoE%$S9Dj`ovDbqHk#SNI?vnghazAGI~PQw&!0$Mp~j$!;0h@_U3DoX+b z^0z=&xi|$!%Y5!)LLGsm+GRiCqDa|GZm>x^532)6OJ%}4C$Q4<>Z#YqeNa8?o_(Qg> z3{d;D3eS3#EXcG4`?Qsh@Qa;wd)al4*a!E&_Bo3xa}%{9#v`+G6C~J%FV~}9R(ouH z<0Ekr3Y|kfyMo*xtodF9F)r8A%^oa^SN2gr4Z-kTH8^#Mk7RGdbVQ&BAn7%aw%=tpIq6e<>`N;9_RSsEbI1QA3#onC zb|DPMFZgT;3bs#xSgQsmXmeMN`o`*%PV_+DzX7)B=vd~SKp-T)#B$E{-q7?8ZX~Ma z#yiZr_;-FK_ADD1m%DoUoRwK~9|3m-af>?pL^s}w8a4#ONxZP$hrfRJn#b zY2MVS=1HFbHMw7LCS|ngdcg8D9G3rkx#fiu0h*gWm|l>5luLY@OUh=?4_VY^;1~z` zk()qQ8L~Ma`m*Up80XSpP$1g5S*YU2g2BIwaAQgVfrO&>G^FR_W@2j0HK|{o!cv~t z!mMh(fHNB_4G_xOd*szP)htnohXawbr8)fC#)9>~1wE=!nLW4>%(2}lQ^|ysrrQzB z!!aK8;f-V}Y~^FsTOGljB^C62+FKo|7krsOy0CPW9N}Zj_>pTZJ zIp`Q_O zlCYI=*Si6Ulj=QvpE7wp`yc1Np_l_&S@@MXoM#5n%UAzQ(m%z0K+<|v^IQ>ApGG#q zv2BM)!E(sdrrdqFVZeVUh?JhN?W_Bn!k5a?f#j1YX*iVOP4%T8s?j-Pgdz?^?c`&G z5=W!ukCE`fqEv}|N&oukp0@Rx3I-;1>JP0c)~F;SRV)Zt74fhqmFF!fvqC156J3EO zwq?tBDo0LUuP@`|uTo(W{tj`mW3gjVG_f>5Y(CpOHo*<-Be}#6Mip$k~Pm)RofYx#G{_o2VVK?mQb6P~PA!{xH-CS%eK35z6n zR19rtYr84%?Ehus!d@_CYwy*LExDa}oGIGYxhH@N!I8i@pMIY?pS3l!_r5iYZmVv) zZkuk0i`LD?&8E$mH`9H-q`FZ*qQP0(s#-|DbeDP zB(rCEj%lePTb874o6{WQfRx%{OBlA{9OK#clTBFKF*QS)mB2xtQC$ZCi`teiKZlyh zB~9B-;J}*sMZ@MFB<+Y=m$OWF`di`s%2Dzp>|*g$qB1fiu5$uRG>s8nltpe@P8(P& z`QLZIAyoaXLZR-QHg@II@K`_@7Sr%l2=_r!=lz-I+V4gl$$f0)9TlsXcc z4IJ~d4PTyPJb`kx=1Xw4X}~32NBia$?P)#pB}Lm-;PRR!VelJaL%isRqGY;M05Jug zM=?RbCbIx27A`J2Hae!U1X^N9isJXB^I0f3DQpW?t}JmREbs#1t| z`h__GjfZH2kUMd)4H~SbA;v%dDMr-~P0w7Z@-hpy1{v1C^zcDzZp?4zFD=8H-?#RM zM$rVtdBVw7-yWw1gvPt^ld&e6$T9epi!F2gf)mG&a?`bp#J2xBa5xPUQR0a+4cRtH zn))nBcX7&Zf$I5FG=xJv^a$zMW_${ozX?)}9<|}uWBuECe-W`ITlnL0Rsm*7)l0K) zMffL;Lzf)_1yM!@9X;d~9^Mw<3`yfbI!^}h&B9{akW$wG{a{|wmpdUSX5TD+ilDh% z$EeTU?FwNNddDbJk+r7oV3R5=8QB;la$sQ|k3E)cIoh-d*a*0wa^j=|@4ZA78eYM5 zpmHT|_1FaPaJ)Gk*5KP0YCOj-xwRIAE&Z9c!YoB4od~ZSodlO-&A1djSuB5!8ZpS?jCQ_wH8ee!y2C2TnI6^HJ(1{d z)ZDxgWP zL{%f0UzCr_%4Qy7<1m-~Wiz~xTPZ<-yPny?*8SIqAxoEqf)#j(V}8CIeu4-131}hM zlK>DB_%NvP0n7krnQN~9d{^tgzjhA>(8yQH6Y4lp&4&=IMZKpt7~!c|0^9(M^0?x% zIKvFk+C0Ox!(_&DIRno3Q%-C?dTkOjJ~jm4)+CJ1>X}%}IH{;k)ahZtg-nN3$e~E5 zvUsJ>Sj&cZv6l?^=`Q`Ybf|}I)f=1%yVmHuT|^Zm*M@0hMt@WcG4t;yDL+_cJW{h7 zdnb}g%!_hKk4A^SBr*mr(1lAYKS=^eX&=|y@T1fX6Hu&?Yg3u5(Q#S^U9JCh!d7Wo z@B@n1DxWQ20mUvfA{L$9W&LXzE8rN!V`l6a3~fuwtTv8iUpus_e48>~SiqKef7!n> zDD%Gnr1GosT+E2(9Z571);RM{fSgR1EC&m`KK2Bf1nZyk)PPxe)6Cl}2jh+mYx;R^ zz-uO4<}jPWuk2CvQT8lrxOsH|uDmC68uO&3?x6Z0dt7CvHSfH=JX@B7W%iI8)w~^` zGxLyn(!3+*ga`m75C63+(^xmAn?p|^0A=#6tO{hRGqUAkF~nFkPE=`+b5kZwzS70%gTB6Po)ZoZ!sA%8z7yttsLA zz=m4?zuw$^_oVYu^2BWZ{r5EUG5{ggJd>B$6ITFJrXrjFm|OUXlDuVBkI75+$+Y|y zv;VkT{0Wo%QM-W#@a8JB$MS`49tzOQVqo?Xbn?|x&-Olej|?ct3NWtv)iJnCmH9Kp zWG>TU?{W43o#m>s$;k{8IB4}%d9#~UZla-Rn!}-~sDR62ZN>->-tJf^F6_aO z7W>#gP8HurXD{H@fcH^_Kc$;^d^|wPYEwTkLL?q~>pKa&u*uc?pE&S+J~{N2kQ>L)zRoyT7oUg{V=eTkicM ziz`^id{6$Bv+#;F%tWzi8jlnu)3uBbUym~4Kgfm_mM^%QX?5al4a0vQBCee1UWn5A z7s3WT!}|P@))N2cI>d!Z(OpS>V_@ubqaYD=1sC!PweanSbpga9{Xgp{M}(MDK6LuW zEcm)_Euo!3M&whdwUM$z3@9ZT3YCF*!;a}TI1essj_v4#+=9-A_fdfpID_r}%f z_x*dj&{dxj*I7^_=Qpjn=>xW-6od z$9=@P=|Q;;dawPvp`noVZc zO_ZdYlddV64QJO)8RjTWQWx9PowJg|yO1Vt50~_vYl~;+O_Y>HjEgT#lEurDCa(>b zpz4kSWnZj1w%K&0&8@mefRy`zW1FV24f| z{)>kfB4dhTjX)ZAj)x~Evj;ubmc;I2V}(E!XI$!xZoQuzjsgA^Yj84I@zAoHZgWN5 zb9Sj-NiYPz!K*L7w}xkzbFZOlZO8QsgVh6%uSm(}#2$(=kLZG4LjB1=jtB(UOs zzEb>ZlHQ~E?i%9}{j*v;S%Yj0I_cg3Nk#xS9tBOc1I*g)fazv#JhL8X`8oN~`7Q@+ z($c|swIX)MKW6C&TI5UZ*-4o0#qJ|8!;FQR4q*t|Tiok6&}-4sK&yjqzsH?Fk~d<` zJ8Yv{xq<<}AuVPhP^OFxG$+#xmpQdsXfBT1E3>s5_6t-tX7xn)I6wPcqD%cv7eY*b zOZYXioRsuyfGcA;7;#YyDRscS=PM-w@}5ay%&d ziF^8aEPxQRy}3_(r6xtzK$gSqj51(N-aWH!7pMIkdssb#CbyDkq%G_HfA({Z|FfAp zgUDBy!pFwV*m&dx=00WKNmUW7C}*%jDBfn4yu=+gl3)sCPcKl1#&?Ok6Pl)b#(6 zXDTZSaCumcS#*Hlwq-+#lUanIIjk-6woS<$2Xt}(Bfz$Z75Co`)LKve4v!^ts`vS)pRuFW#KDHkAf0c=?af^OFVwG zJ8y~=sc-Amh2P?38p|0QUJ>VG*)9I>asKA*lm2&Fc`fud3itP|V8ImoRrw}3N&HdU zaThiI{h@!&p-61{60eH8r>uxsuKxk4RSnM+v-6M$QEs$yky|y-DCk7drA%06inn|j zbT;Aa=w>PWQ=~apB$x0&*(#Z54Ro-+H{r4`awCY9+nEo3-)2j4u*59_~F`quY(;An>=Q;HXemiGb!H;>OEfP#ipEa8$ za+h+|bWyJ?`o=d>Wp8VpI~W~8hx%02j9b1F0?2+1d`o5>ieWa*U!Yt)?@X-}i!^TAGWmE>tkDe>dcgUNRV<)c=#ci&<-TvkOu zMwY9CSD15-WjY(xGn1BK53<>Z~_b z{yTGiC9?*S22a_(B72rTcdIGT+;(#Qzl&DICGJ);pk+rwrI-d<8F86rS%P9-)@+tA z&x#^$GI+6F7X3iXy3s|dmW_P>_C=vHLaXek-gVwutHQEE$hJ|xOuIVXwsW->x1#un z$U_#Pz1nmjnkMXA^c6ac8d&paqdPb)I6W+mhUW8_Q{ld4G>T#%U#@?aNB#4e%(0cU z8bM{R6_G)xTsW2#eQA^K$aAV?lpxov2re9}j%emgxMdlEVsVx%PrS*PRY&-LmWxPb zmx@GL4BRB#8)LlDHr#W>WtECDigQ^ppfaAxZyoVA40ET7%~@yPSWG%nZTRO572UG- zx%ar@={}D;|F~m{OJOZi8kEerN3FSX``x7e5^Jz1IyJbV_!o3$q7P|FvLTrJr5Fo3 zGh?ynD6qkpLsnD((U|*2!A;666cahwt$fpL-pm2ZhO=m_^arm{=4{LO6zxFy=K8}| z#B;u7UINax~T--}}Slf)Fw6A$y$ zDKT1oSw{Vj=|4UkR<-v>~%`EmrF8i$;_zkd(I zbw2-oaWswBfhM-`cLtL#@U_&PZO(7kEnnZ{H4&z#t#ZuJ?$yCfzt<13foYymK0kc~ zmY49d@j^Rizha#2?(cpU&V+nU@2-aMr>XAa^u{AmesVTUcM{oV`|C?^wf+UyYZ^cI z1vTvjCq$Q_g@6F(7u{9k6)k?@)#Ss)^Eac3NG0pteawcN^gH51;Gwk1rvv|B9=-#O zTrHt;mnm58AbIj%_9<6?xHa`{$M*91arkbF)2oEO?)QR2_p;kj2c2(*c{aaL`T5HT zHr-lowMo&}SEYMSZ&Ar`xYXafdQs`HGMWiam{%uJln{1DJV}du7IT3vg#*0`;%@_x}B`Hz8$5eMXD0I`Dul|wKSH{1{euyD5uNx$y zzX>u|4sBsD%XVbP^%tt7{;qJ3=Pew7#iC{&Wv>RQw)PuT%No_*RGA@jM|*Q;VlY;< z9qCbTj3d#t_zWZK8U}L=)c?UKAvqzEXI^GZ!oAl#4Gz}t9S-5GNb!p{e zM&!Z5pPG-}=RLu7(BYRax+2F7EZIFT$RCE!aB+c{v+tPSy_3h7MUSH4d?f?I?fl;# zk)X&0zjL?0PvT4Gfpy1sVgL60HhDxkn|U5ULX@jfFD6P|{Az}O3wl;oAM0r855NQ} zYEO@?d0@A?eC{=S@A=)A-H|~u5!A~Uivq$H0P5P^%X*^PRf6c#-Nx8E?l{e}-z-Ob zs4SP;fyI&c&^{wtG}5<;{6=>EMzrS(+?RZ#obbxTPyTS`qec*Rn-L$ zUrqIwQ)e}*x|n_?7w^Y86C>WvA0u7XmJW?RGc~J(e<=s41W1P22VD$Y)|vO)ss+!O z6CPrArMQNr2QTGaM760@VubQp60Bl&+jqRxCN9-oy40!aNrrPw{p>1FYSWv3E{;K0 ziN|+qQ-($86K{Ytv@v_h<*?*>*o@w2aiJ~^ZMvv9vyZ>_pIh~bODz|F4XRC&;Y8Df zf|%XRCX$a9tV4riCg8Nn4YRP?bz6U33SE@|pirLjU|ObJaf#ko67#uFyhf+xgs z9|uj0C)A`|G&QNxiRJc86Y64$b9R_CsV=7PjMnT+#KBpGiFEt(JsK~K;{51ya;R$i zGy7co?Ehw@Qp9OS7UYoM>>EyYg2;|OB3U|zducqg-!V>c3pCA=PI^r)kxqKB&uh>9 zbary^cxnRuhzKOF{DL0AlPj}Hb@!lm6Zdk-d;-oWo%&$&l21)hGpm{|Tg3}3oneVr zvkA>MCetz~o5f6DtZ30L^kKZ}~qU)2IAon&!Pv#AEIX|C4I z;--66wJ4Nsu$-#d)B_(hSL_B6=-~9!SZ(60$P>pqMxEz*@Al8D zMlY?Z0D4~cn=_rEidU)Bhox52CcWG7gm{r@)2MZE+4h8Q>VEdgip}YtNXF7KGkwe4 zi(QS&QX5%pF%OQdcg>$Qli==L8RN{CV}btIdx|I9uT#tG%Y%Zmg3>|hl#d7Zb8-ds zkw5AK;dgTIdM0G-w2&C?5lQkICTZfmdR_I+P+rH??XheERA_Px_n0JkEt45`T4N0N zlq7jQ6A^YAAclKI5(H+_z)6G0aQ91sDw&$G(^g`*4`?k4s?!3xn=C^@yo$hV+^VFk zV<|x;M*5mRS1rb}_r|ipzD8w+_Tnv3BI0-g2xXvK{|7e!26)ZmTkkz z(w7oWt!Lvas|(6x8IGsL1k!+VgXyP#UCJgzlj@V%FTz8!i7zCPjg~#VcP(l@XFD&j zDK-n%$QS!TO}{Ut#Vphs@HQZ$rf}sfbGmZ1yKHGQOP&V23rMEfQ87!O{;AqsrnH*H zPy^lp6wvIboMlgUtag_x?PS@|fOiA&G<_>(Kc>@EUwtVpXA#pF`BJ&Jd+&A@u^-|3 z@)KULDq5NN`^94V#mQ1G-L1OM*x+YHg*tsYOSPsMr8+(PqV60oju*QT2rpH zgJnUZvKzS1Vx!fhS{VYw(Q48h1=8E4+iNyyjVhPQvaD3Y3f0ZhskB_xNA>8*(!XjQ z70*H>TJ3Fh$HxqeYSYeJ3@BU9zXcd;z^9w-p+MWp#F8_X0WB~KSNZ@gOQl$~SoyD# zUv(0@*ac{Rw14zH$E{6VS?CpUjI&JY2edC9KP0JVhuzOEXpdPGrBxr2hjPbE>t2$x zikl5u)d&?a+rnked)SM*UVeQ4^SssrHLCUxf*uyAT*BTy{UcWKEAAPNOqRuIc*#*=&_)k!bB1Iu zP#Xf*$Oy&$A^pXU`gk00{~(^2e*Y|>zS)6!^#s!`zPs1<(A|^}7r3uBfJ{_8$^sJ z1K?E^C7~acv;i}XqQ+!`OWra;V^VAjZ<&DEAv@JfwK&8QG5`V9M-uveNt-Gr5GpVj z+`^@bL_$9FUqz#J(}ey3$WZWilw{$%#&W`B17z2~Y? zkbOKmCL88|{5JbJXxie1FVXO7X@)bQl22vw^gM9F?ROe#-`ro+G%U=Ah%$b7h&}yg5Q=T}<0!O>} zX;6kp&$0%JcF+&NTO0iTW`8T@Gjm2 zLTGv4yncs|5@Px8?e}2S-&Eg2oPQ&I36{ry1D3+J#xRwlvVOB8g(Qyxct>6mteJ$W zALgF)np=u8>CI}WrzBdF6s|R%aBxB)(a0O0ceL)PhLW!jF*`y&zj}p&{!Q}L8*Jxy z!6+M2c)y9RkbX(g{wBLZe(HV2iI0sG){DNd6 z_V#V zrMw{RLO$@1F*w z8g$m)u?EZ<%kHH1lJFekH8M+0n*(-dxD1s2|+-J9SJFNeoKWb z2T4XCi8Bmobf%)hV}wK_0L3ncR60}S;#xsK2wZUvAnndHxp-uda0I;AnUGRv@~^l$ zkaPsfI7^ULXX>wb0+4tF*w{ZIwa%35xL%Mv1m-w5kYCQU>v(vOFa-42iIC!px1zWz zkW@qjaHb*67gVBn9FSNwE zkW55$a5f=7FR1MtlCaS>lCXSXIpQ@53)`F~us-Y&{g517*ESoCwJigusR`YoKB=#oh_#`n` z$@t`tuAiqy-n4%ZfF+0~G1`W9B?K?(nD=uSt^#=TI9{Wu(K5 zG+324Mv7gHEk?RlRTL;0&4fW;Oq>=ot7?~)z^%$)8`h+1FpmpY>eCgAwdz03YN0wqzH`w zn^+4{gvf%`ta&NIhQKb?qS?U=V1PAuc4!&c##%T##1gD$&7U2%5B9MZ*AFHI%OD1; zLNmY?)K!D@A6ng^SQv8XX9U3;kDeon9>$zOU3v5Iz_7uarx@9xh^EX>-ije?V}Y zrR}3@8~A?o{#eEp=(}EiEija@j3Hu1b`rDEAaV`KZ^O9mgD#^zm`a=^=Ip$piVAb) zp^B4sW^zpEcNTOE`RGiS&^IH9`g5RB5cg-_rQmDE9lG2&4QCeR{#qB|DF~y0{Xl|m&?x(g_UV|IX?@`~He&=GyGK~--)!jL3LpzK1+lIE` zoy9(EKYPETi9*}%^+u&^~TJPDc^>Bqvl6j zZLfIa=Eojv-+8|_iXz({^u{oX$s{Mwd`D_UA;^DWyR}y66NZaZ#%sUI*|HrGA;5mh z_QXcSKF7AEOE`X(;m3xjZ`9i5=+plvVUK5AAfte-M-QZz-rCee;mEgW8@eD|Ev+3{ zTc>OI^WOg7;A)@nN=LIt5nl_9sTlM=<{wcGCO-{7Gk+z&4Zjb6fPKaqRg;jV+vwuR z;`rip#_Cj)n3Z4ebzNSb;m6J)&u&kmlk1fLTTQUqoNRl12YU;2YOz+r!`{elKfNG!WjB1-|cYUy;Su%p&b<=cbD7yqE>E_4GrgKGl@HI?%Rd!;@Ws6n-MOobc1dMXI>~|Y=`F(K zQ+1TY{nD@hTwq=Sug3ndTpjJUt{^y!GuczR-?78&u^eN|l_~GZZ%8kTM(E5wVTvyp{cAcXy7#{yh zNfHzs^@^O6`J3anamaUwV+e19&Q=WMDFh8k1qJlRCiDm`9JLh#EcLhYE2 zSMYoAUy`8jQ>8P$f5mMl_#+OJ9!dP|TBKYgRpe5nS0w0eet7-+sR-?J;`e;aU--W4 zS89j5`*o8%IbL?%rFS9wXgLK=&zx-RHGa#>4bdiceb15GXC|qCYQxK!^0dgLHI9p&sLvnQI1j0zjuBAgpo?P$gnQY3#>`f z6=_s6-TjiXEaqap(=b2~tw4mwxS(L01OpML5rgLGZ8T;VVUMu<(O)SKbT+|sz>{HBGa;CL$*?PD`Ae}{aZT}NA_G**^Ow8R!gKm0Pl(e^bw3GV1-#2`Wj;0j zHMgD4&1V0{L_esU$LWXA#y%it$)UUDE?bMG8j#_nZLY1K@u}Kxv7rW_=hjTx;$gSe zZaC`6x4K|=u;i%R`Rf!H9_n7}9^Ij6XkhpQ_qa{@u;bVlj(QYvEDM)C9y%I2W;nDw zvV`x$NsluQGmf0+W8!fDR>M8%AU`g0X68gI<*=O$0 zK5TNbGwJ5Txfyin@$s?mG5_xAPvjonG;Oj3eUSe1`lHg_&%d;u*XLAAJ~{`Pc|Gp! zD|<#EPErq{|K8wrDxcj~HJB7B6=@X7nV6f%npm26ZV&VU!ga8WNxMHis^8*dy$&m1 zK3+%cSK%0Dy#RJ$)(_cT=y&1P4^v*qc40UT8C+<0;W!P8Tqt*8oe%k47Yg$z7=VW77^ZUI6?t3x=#N^!#xPhRH5u{4tD&bS|{~ag2urE|mPScn5!8Qu~u_ z_TOA0KjKUDC0-Ie5=-ii$ouNw{uL(e&P0}SqxM=7SQJN~8$bU3dp^`V-k1?~)n@~`2*j^xis=yXxvKBLpe zIe&>BwbdQ@PC9$mNDo zG0Z=JzA&URo3nsq%fimgQ9%I95E^rSko10vkGc3EfHW!GoIp=8BdOG!Tu)g$7-~+B z==)9OGiTS6TTI0<|G4snKb75_^-%6d$g{b~ij-%Hp}D|{^mR&rxrC4;Zi+OFQ|NO- ziWy8$NJ=?H1I8;PJ(c1H6K$9Ll%fFRZvR}DVh0m$mvT%ofbq9WpQZT0#C;@bQslnz z`Y7fnl_R<*m5q`zzft+f7^Mop*nQ-nVV5xEJ9+Ws>2FAPpSx3>U?O)?fuW-?GCuIo z{P&E)vKBhNVha73j4}&rJ^h4?auNu(ezG<=-G+oSRl9V-+5?eym$X_t=tnM={)15I z(@vD`Ybvl?Bd;dutFziFIaIK>7ROhJc@)N1e)IrO&4+iW3mhqnRWLey{#7CHuc&)I z@qVt1HyCyH-D(`_96kjwXO5FX+2A)i1)$iPH@=A7hAqCh)>>4lFq$`-VU9Q(IA?8_ zt;}Q1;2_;>ZLpefWX(TWbZUdPn(PT~=S|$7n`*&YR>6Wut)_bxhZC#H6AY%Q0?GF@ zRRNuIG%bv$GJbR7EgYwE4`FWZuc+nGW_sPxsezF*+wLgT0J)hNcWi2f;h9T!q&#`X znNfGlJYe|@+#NL!U^TPij+>`&Fmvbr+EAWsX3!nO5STf$=l;eJpfj`Jt~3+(#!z8t z=AS$AhP=Scq&wCI@aN2_JK6@oYi7e8Z$sf`=IQE{m^|K0{}s9zFmY!03PlW{GBbCD zEv7I%^B2KC$aBn$Ut#_N*3TSYq5c9m&8%JF{!%!fdANEVAWu6pe1#DJESNdCdJ_OJ zmNL7~?k;kIi`?f27Sh5Q??Hh@#&CiAyyrp*IEUpx90-H8S5XgOImBB-%sM&N&{RXk zI?V!6ulZ0tz|zc8!%{s|)l5;tP(5hdEL`)cdiaRPt;~WHu~%+{4Mf5=U1K6+iOkAn zU^z^|<+l%^;E~%O$OU1!SvXGQ`EaT%e)r*W+VA%_ao8Vz)YBk^AnD=T4Tb8VV_8h< zy~VO(*F}l_{!f=U){?7+KGwpeh9uSsgDsi9(LAG1U5~y=7W5UvWyE@lfs37Eze78<;$D24*u)lY z7nc`Ic^%a0taCQB(JnV*|Mmu^)JAr*FTS4n;1UES_S||cnuK;~DVh}tL82MU>HkcF zaHYYb>HX+KoVd@Gbde!Ss!lU(aOs^7Au56aL_y_{e9`ps%SF*Vs|!VFgw^GbF6yOA z`~3d3zqnl=&j;MLU*!+~^|6A!!G*o)Vm%+i652}om>{&B#xvEvmEv_E=B)yaz1a`& zHijnN93bCHibvt?OW&#r#^8k&-#H6LByN4YV-n9!+%~%t6wg=LBD$j!2dQjp-0_O% zO>gnsv5DtQZ@b-ziWhKfVcmV`&f?fsxa025t>0q3W9iPW-?qCG?#_4GqP%11203jT z-0^qkoo|WUed?xE-u1gR>P|dAK)aO+jG~R;xg!Wnqum|8!w4jt+AqJg35+P%JGk`; z3^9gf-dY5P8}BXMdIW}T!hYU51V(P|-P{H|2TQ;bZ_S>+OYBYGx;=;X!0K=9o+Enp z&Tst?7bUFV*6KMtXm9h@>pAQN*7H4?la#?Znd!Zv48Vke7o5h#J_fM>*Dx`T#bScN z6?81IEa0jNRGsJ}a6|>OZS*QQp@Q`&`o8b0b$Gb-I|@t%6Z)F)_y!VBaB>3~F1XQ| zSh-KH4P9rOVLd$Zf>`N$=0!wKc;W>~V4pEB$?FfQDAAVAIq0PCKWOrjnSA)#f=}>4 z{Y$hp!AJcVT>?h^Xz%2Agfc1eq|6_*N}?A(7@9{Z&d8ca{{ZLxjNN z21UIA6J%HepBv@bNEHUu8x`5eSOP9tt!kj_UMlE}yHN+sl z#z`pNpkw2~O-vi*w3Omckig-IP(oemv6AM8op_Si!9_owO;$o{7CQy(NQxmJ|dKS&?5=KwHrj z1~S6g3`?rrQ_p6lea9GP6?_$F;jB+3J@zcf=7rcPljaTB$wf`=*s0r1;c-dH+(6m! zw=3#HV}dKdp>Y%tQ>uBBVsa(7(vJ){x3uLLKexh<^wSb$%W-?qh5<~wUCA+11g6}sfZ%{&M(rwR znP@O6A9^8ViMc{f_rl@S&{pb3hpy&Udsc#OtiGCT-B|>ANiQ-Y>apE!J3F6V>@#3-KiQQ4%aosW93Ek1% z@!hfAiO-Zz_)l-2h@YsQ2%jjPNS|n)UIRmB3$dB7#zOuSB2}QXgcwU&sJ_vBvmoiA ziu{Oa8>(N(Ux9oS>RTvo{fZ(~wvfvjDLd4%P$&xdQu5XJcdw%)KghGV2Ad}#sfWBR zArX=iP720;_l7$O119;pDVVS#n8G@k^e~v_ZOorwf~;V2-Cz=Pvi4vc*eem|S5|7` z2*CsLW*vh3Cxna>g7*0Cl`bUY9eM~S)2r}zNczDUg(TKEtHD1CssCFbImGS=t}CQG z#Pto%FC;s}xex9xq&>uok>q;&3N=(}OMvt3m)EcG-vz&ZO?>eU{Virh$o4msw-~Y^ zGvBb^Vhx2{enZMaX9yYnhM9#~76Si<%JiD2myMHh;&o40`!|{_JksFsZv?v78NsFB z$aQhGgVVo}=;AB}w|=A6#p4f--@4qlrM28?a7L{UAmScqeFR5GiML zDpWa$nlm;P8Y84&i%#&3av0AR+bvcua!^Aw`7f4+w^$@!Y)UG$@5WfqK+gIYDDqB3aNZp}MTOzlw z|4(Ja_?w098}0CgZyvhH&zN?7`VIUDL8s5RLEQcoWuI&Vm%Tc|96NmSo7-Eo-J(~R zCP>7+U$RhAO)wS1O|wwvicl;inbUeFz#*!*B%D|*MVM7ZsJ2D8M@6qGiZHT^-sqd~ zxAan3N8%oSM@SwWg_OSE#IHW1-WT>TVIn7mQ(^bvWA`KJqJM07V~=6byNJLCYpwUX z0U42c^dj^TM3&Ke)_{gcD0-m?w6Gp}pBvCg`(Hc1qe2M^3o{``LVq9nmQemZ-v3J= zk&)gK6r@oOCfcSGBv%g6*ya_ao(iUSX2^YG)h~46B}g_E_IDep4gXVU>^5N=@u#ry zZOk^py3p!viZ;@^u;XpiHiGBZ0=<*YSnHUG#ncGIF}!*=oblGNZhD^(z@y{!_Ftfj zVkY+PUZ6Y@<$o{VvOy5U;X%D_7f!c$|Nr%pmvGuGxm%3D2;(iCTb#g12~}oIigYOp zH6cwJElCnJ`YWuexT?ac3W}-(+d^Zt|EEZTqbfa*&L^v+sxgnvC+EUMgOP+!|Be9o z1;9jJTBu1UYr{khP(@AoKtA@xwM0@+K3Y>go>xBBT|NRFK%luNMBOptemEE=;H(I*RKk ztgE0rO7JbruOK^$zc1{rpgl^AQRVtiQRGm7_pJwyF1X9G8*60*55k=e9C=}7M zMKjje6tP1^mxxzI6hqOdHD-29SrObCl|_nYhK-kUQmUu8-I^vlk+d+}nm|7;qp;MP zTt7j(Fx{F&KYp>W)tXvAk-sqh5U&NpLDt~6b_q z)D=@6GIWGH7KWDlmMmv1(`<f0FV!tHj2+dkDr z9`B;Iy?6Xhxy#VT>====i?9}WqZ>ukfc`&X2k9S7F*2#OU)2P z&7g42Fsl6kgZ&Vx{ou-Sgj)l(NCOc<|2XM81Dh>}{cR-US&n=X4Az_2euulw`3Vv( z7;3PU(MIAJzqa+GjoL9$WGkVK=q#>ttFDdmEWvLpzm4oH{$Z=TjrJ@t)|rbY7;Q)D zRzUQv92W%tV;om6@!$U|vE)-P&n25E<#cb)Zo4;4ej?dcxHo}OT;^7(H@Q)Q&Q`iN ziBbI0R;xF)QKGcp&q_!vBr&;L?Q$ zLK+_UdEwxXwmEon5%Bm*Vj%It>=8v`aQedS5vgaO{=)7NwP*1B!tap-k>KvA-0~nY z+yCT#q<|#9K9mB;)icsQ?!`h=FiSpEehGY^4j@ra*ZjBu!1=>k39wCGm7xVtt7p!C z{P!PJ5ATQg*zP2T5@s9O>{LNQ6_hXXgi%p+DRe1{>hdfEsk|x5baY|z(Q)R4LTX8D zV7jUu^)E`iUliQGs9+DsD-I~K4FH>><6-zBF8GWH24_HKeL(t8bSCG3+#{W&ZeQ{{ zjATyccj0uO^^-D6NNki=lYW#?|2N|USL{ftE1`rd`zGa=kinJile$Z2;VLomT;!6d zDN-;2?yN5u?~>kyV!ThgFh?f`MkH^WqmTn+lV{Aa$rXl@FU^ra@(jtN=9nO0Su)%l zmH9o-C>wXq#QUDqc5@n#%Kt(0B&C~^=qW8GwVG4wsqiPo!|tL5>FV>Mx`%$ETz-lbOvp#dC`D(G%|{8E^b-vwn6)k%f(re!g}gi4FZdR;^<7@2VDxT4lT6#x_d& zv5RFM)`t3ti{&^mSk}_3QT$~})*7pE{N*mZ`Ir?+45|c5B>-Mf*>6pjG8^7Jpfzf` zI{9RYYgM71CO}J5iBD6(LsJE3US4Tlk$oQ6Tp7QwD&nEaI9cj5FS9W({kJlcdtUB| zS5j|m<{i!qCwpNyuljoUk3x1E*46AEMGiJ1tNEUV)HaN(AkQL08-dll>q0Rbj@6v& zq5vC-)dJEPQ-rEp5_ah1&Ka*R`Cf)tLw!9`eQfT%I}^2R3+)u>8Y z;Vd25bwze?;f{RALdsZzr!Yrw4xF?j-LZHLj@yxX2JVE@bYz|tKfqu6CeeT+;RL>E zG{wVkjC;oaP3^pZ4-Pl4XnpgI3d!K~z96F_9XOwF9<)#Z&hDE7E%Jhk`4)&5;=w=O zXNec7#4`T2x)KVIGM?4B;zPV0XT7e(o-+Rrif2pVvh_bCG_> z;;+?BVVqar`!co!v%74)6ZSXkrZFyXf(XDUFXQhR{587CH**trcYR)Sz=*sF3Pu<& zM-j~MSnPik(U}+IU1^z@psrk<3%#xsol9GzemfuP`U9M7=kD5H>e~c= z#%`X?@bfXR-Oj&axB31Y-2$7D=i@iK4}WF+xg>&VNAPxncG3Q-(Oe|SiSA>l_~g^} z;q66U_TLda$_EllWYhL1?hRk=-eEj`2_%#N74)g>m0!-?k^5svG;6|bf9*01KhX$q zI0DwY+j@4U1`QFz=w1@tmi}M;{k!OlC^%v>hgo-;P4A`2>JtS940hdi3(wK(NCbs8iazBB+EWE<2P01bK#G7U=3vd+rRM$T%^0B31ul?%-a zXJw^)(f=jp7k zp!^2m?ZofDFwh7S7;@zWGo2;20e#KiRlu`gU+_H`6)c#n4I}W8ky6v0|2(fYuQx9} z&&psB5hEKV8#@$T7GoK;AEOP>_IbXJ8?EjnIq!Oj$I^oA*+2XK(-yP?};6h{;N9 zWbjG#2gm@BQ->JhRkAa9#cYgb&q~AqXekNwc`>6PG{AH!luk#vF6HaT_G=ZHVnuCw zyOh?^)6@q>JQW&{=qDfjKVVqtC~N9Z`g4GI_M^_91(?VvZHh#>reb9dSb?jgO+AY^ z2*_0Wu2cq?ljg~6sL@e+rA!Xu{#Z(9p*$9vro~LA{;7N&Sev5Hqm`7M%%bwI{D;zM zrZAm{_tD6_O(l=If_i4Tqsm532gg72qrLgHN-quCGI3x)?!D;0=C<6n?6!=y5Z!mA zv2$jDa_oySZi6&jA1%#N%EQ`X+sfNA+j<09WOrnC=y$YsaCRIJ&hN!LJ%gEnECqf$ zXgiS-wbMO2DmzX#t+Te}e2NMR;&~yYER;=9KP~K963E{uf<rawc**a<*t-Aqt?uyuy6>Wqw9{RBzO0 zlw{OnRBY64)L@isRABTb4oDu)lWk>gQDKTXnvqPDDsRY*YWbM)n%j9ODN8y#pNrLO zN+rI9*Gy(;EDnd=fHD-vAi+Bc${U7WxQ&nYOHG1cKm3R zVk~=<(ENiQm`#Qu!(yb#90<}b%Um%R;_jSy|99jq;@XY@m3uZNG}SdZHqmfB%*!q6 zEUNIU^2?K^6Og}Xd{#O?Sv}NOnj)O*u`aPN7bwPJd$VWa?z= zWa*@tQ=c|3FfXu>;8w8E9;r#w6KgiIe3}>n%_>TlvCo0vF+x>SAVY3H^XrM+%yt$x z!~OdSB~T8Jm-*SKXwIF^{^W!eh~t?^8$`?9Gh#P>JVH4_IUfE;vmKDn{&SQXVTQrv z#CXnjPESjds*}3s26Nks-D8Crzzp`tU}SrldparYMp7GsnclY zs~hQvYj$fsS3g!f)>Tzhl>jwLt6?nR!0Op*Y?f3OmB9)m7Htwuk_!E*)vDEM;p&bm z-|G9S`^p$CH4=5T1^fl31>FUd1>yzn1(pTd1=$7KD(b5Ks*hD>RT@>XRg_hJRS#8> zRdQ8URR>i9RX0@}N3&c7+8TxmhAJDn8(*o*|6>tAseUzo<*9A?YN=;6d8;wCYO}4f zVMRiPLFHPNNM&c`L&ZatT=j6pa1~=^d8NWU&w|@R>_Sk5K;=y(UKL&?UNuLhQw42h z0b;$tX60tJL`6?^kTr|7tsRARwjCiKAlqCin?c&_w=S=;QvOgehIJ^{%c9nt;hcd+ z4SC5nUq7E2?=J5yA5kDn8+-Ofc4{`|i;&=?&Lo}#xdWern8Qa0vs%FMZ{OB9{=&T$ zht_~rWK(WjJD*eU)6b{Ar~Ie5r-7%47QR!iQ}R=vQ<+n~Q;$;}L%ogq4*d>c;gC;i znPsVmCBna^Dw>b%5zTTXc13p8cJ;@F$Hig7Rg|SU;GC+QqMXv4+MMDC!=I8~s$Pm- z>R$R@ayMUZ^lya9PR5SL;jD+OaJ!@3tIq+U;#wSwR)^`n&Ef_H-PySDzpVXg#2p&!U$@i%!YfPj*s3NE+s5Ho_!lc5%`kS?>wXLs+a23w+jZN?+v(eRjV+B$jUA13 zjnQ1aOxzUl+QpOB0{aw(L;l4}BNRerMUCN&>5a9Gd5ymsiyKoLn_U_R$gSQUP#jQ_ zQ4~=0{XC(R!3Gxb_eJ_>mG!fQ-}d- zu&K4lu?d>MrQylr&f^o~|Hbu-FMu~72GEDP&#+&%kJ3D4&ucKj-#mW8CpY0RNj;G_ zX*eM^`D8Z6X&Yf_T4o6qT^F4dUF}-$TJ2iwn(Bgf&2)`+4R)<{4R>w&FZ<8?Px!C< zPx-Gr((Mx{7>wy=>wU?fi(UTEi10y(9Wwm^({*KcROlYGVs81ucFK0jPR*u<)ucL( zKaM^ws1R*YWRYxTkwAZ*7-3o4vY0YkpXf12)Zf)0c zT+p4=u3oVDryro*(+XMbU0qq7TpeFsTip~MiK!V7o)VrBo)q5DH_fxivoo|ev@|4` zOR}pqsFADDsr^}lS36zfWXi2hnVNiklDtB{(%hVBRX<{s{)@pc;ClZ=VMU<%+)CnU z8i+Wt6szC)qm`_9)kI4qYKTRFe*R$I_%%GZStW%R?lh1Hv z1K9O6`xteSf?FgkVTw9agx<^HtYh7wUCuRe{~6>bj%Wm|Uccc=T;=8X#381VX*D@M zK0b}L-#T)fw!~L&3=Qs*^5Jyygobu0`H1|zJ_zt!aO3mj^B8PLsr#Z6!#Y>$ve)tF zD-GGMsUf>zt>LL5vY{H8tCBmrmxHH+*Qs-!Yu??9`!V&B*iyg}@{-vS(b8O&djQ<` z;Caw+FK4pO;s>QvE>~HIw}q-0SNc;-j9Fid{aaT z6IY8b--^nrBrqCNOIx);r7 z_#@BV(jTY&&B-9p%b>x~8ievZ-P^(_98pI4)f$yAojM$!9-f|^!cR|6k4vHNp(aoQ z=nT{aiU9>cnW2?X8z>w~4h2E=ph8d|s0?%vS_-v*?s08&j&)9TLOXjq`#a}4*E)wg zmpV87Abul$6MpM{s}Gqsxi?Pd641ZUV<;sw2l@bIgqA}OpgPbcr~vc^iU(DJIw>-j z@oGd=4rp3bA(mfR#sw4bq-2p#nY+j3b8*TJ4P=pHrA{ut71{mf3KAWf^v?2U7$N2i zS2QT&nM;TFUYYyy1Y`wr*-%;FOM~7|SeJbVjZT=7a>jzvCu}WonltpK?Z5GJi)3v~ z&{)zo7YGednIv!xgNP;o`^0V;Lc_UMpE!SlIw!>T#s5b6Oo&<;anXWQCam^Z|Mu&Q z>a6Gt>MZEY>1;0jURqw7V7$Nx_+XsEvA}pu{g3+QX}1&JdE9y62Y=Xl*nilr->OF> zP4&C=$10cQccp>sRW;vwpJx_rZcknDn_PeICb&0qaq#a~TdU0?4-jzmI5N<BbFAiY#{%6f$chFry464z)QIJo1ohZVM|UDaAD);PX~ z==C!0Y5qQ#{w}yC~<7C7*v7YT1Yd_Z>DAMNe8!T+d|Bd5>Yw zMvrJu59@`Z$+n-ilbMs7lbw@`(+4LXCkrP{Cl4pZ%X@GyxCuN7?gSTrXTW(q$vrnc zay=)nM-Sjia5ne}JOn-fr-K*4&ER|G@5-UdCDY6WSq1xK#aoO3;Sb1e1THI zR6)%a!j{bzK5N}a;Wd^uhBW~~9PF^DQf9bfj1>9_@Y|U&rZ(VI!ZyjxAZs}~5Y+-F7Zy9XOZP^O%y>?{$_D6eZ z&Q}-R3ks`)B`qLjzk3UX*bCi(0D~0}Gy|QCXaO{7$CHtVvITW4%`7$3dbqT^2#AQQ*m3kZx;$l0vRf;?x1j_G+}OlS0xDilT}Anq zvRfy;+@T~0!ZJfm}C0afH8NULrg2Wor8a*;Tf;=*DSH?#IJ5l-@nrlC? zF?pv86U1hW(~wh^8IpO}?Des#vx zxc=GJ-DC6oKIh_z?7ljBVcHhxdo~ zCn(49$MHw;CsT*a$IK^=Cl`ko#}_9ghiX5Wf7bkT{ONd0@-y%Dplurx$pmtqn} zt;}QCH1TTWRW0ZmQh$w#na>ETnugZvE|=SBHoad>^&3UoD|V6kUbv?2@cup}eAINW z;#%Ij0;JqYx0#YRT2uSY7_YZJbPsL}Pm`z0O8csI*4VsvG+YCrlF)J@!?x3%8u8*&d%LP(6}02hx)f<4*m-GW3yB18sKeb~LM*kbRs zv?ix~Hhs8{;6TG8N03&JIwUcUkBE(kzm$-VTq3Fy-UjuTM2w+nQGAtNi}OOgqGNM6 zTN!RogeH%cz>a3X(&4JtG}{?2O%$XUl2nY%A^1-2V!qosoSbM$F(qvn(T?j)aqYgF zJ?t4q7xpC#Ka4NzF)S?10$_o$!_vYu!+wMjhi!y)zShwwif?y4!?PGcvex=zjxPBV zj&k(HY+{?o8Fv~78W$MPye<|KvJgHqr@QP4v+bpAHEqRhoeK-?)on!r=iv9PP+$bA z-YVLf^Dq8jW6Edx_*$ajg6H~*O%ELd9UB@;6_Xls8yg$T7)ulz92+%b|VLd@UQ8;0=jk*oJ?VygX60YvDtFW`Sv$jXJXSA!fJG1w* z|81|SS2baMv+~@OZ(nZT@4q|4vkJAEviV}wWdGIvvwf$1oqdUYtbK=lvVDVnn|;|? z$XW4O&{@=3)>-RW(^=fv*R#&Ez_Y}Tq>h4)birO?`{tE1;Gxd4;%U*T=_$sk-0A9R z+ZpT`%~{!*QUcPWP7R>pV?K2Fzn_1{GqmF}bN6 z9M*zbG@MVi6$f*p_%ZGo&NV>C4`NrTJ95ZzGO@Z^|@wU6XU%@@uWh{0Qz%=cZ5 zr^GeMHPE%uwGEJBeYwfHDZNR%>AWeqNxf;jslExm*>UVk?OM zqFG$Ur!c2~{IhuB7|FeqPcE0p#o(CG{VShRZVi(%&X?#Uu8Qads;n#3^eB$Bqk%E04Fj@Z|-Q-1G#$5X-d|5x-LO&SM;p6iXjh6b~I+ zD}+^8&Ns-uKjys!&9dK$%_80w`n*3X@zGH;ImWuR8IG)2oIg26zO|5vu4tZLJtn&S z>Z8<=_fx?yUr7G(tB@*5rP6$vP1K{j-`7s3GR=>zb6;vW#7jFhH_IJ4WlKBNHY-Wy zU7yEAth= z(n5{~x#SC!zo85IkYC@%P@XhR0Cc<7uteRJ`wW&1u;$&gAR9&Om4^%<=b>*!{HEV< zK*frfdNNK!aX?RqxK2ZSlRre!4eiIbI&*+pX(0%1I;YOhrCt>Aoqp?v^ow&E9$?kc zF{iPBI{ClT>2vA?hZhd8Fad!^32lP7NMUb?rfo-&Y2mlX4q$pkTH5#{WFp+|Ariaa z#B@WAXL&-ZkX-?&EmCa@?AsHPUodvv?+DMGP!YF91e}7Pzz~t7@GXI`UYv-qAb6E- z6bMpWbZ1f>c4s46+3=Xh!PiCDwx%X0T)Nr)kq-Y4Rw&L z19JvaX+VWh)+$PATd)yKMg=b)3*tD8Yq$Qr6AQ{XEMm9VJzVW@bF)?IQ;2AtI&~$=o>vp(@0!mKqN>EST zpPWQ)Ji+Wx*KmqJN_E>+AqL|3i@A|ANoo6aEd*FADw(p`Y$} z13>f`VL>$D_cCw)>4)93{oN4j>3{%1<#k(LLW7_;Ip$zHgGs6IJA-Iy@mF02Q;>aW2httG+Z>b zh&B>-IF*sCHbPpMIR^&ZSg%WMyDxLq8~WZ4wQRsE(rZ)O>MIP?@mo>4|EQep2*ClR zv-3Wnc9x$hx`fIV6uUlT1k7+;!4vC4#)wO^eh^JT)y4$(vP;-aa{^>`DEwBXs2(A? zMwUy^9--?-o)g%;#OGtRG(hLPt)^EfizUQwz1c{3iR7KrW*Foa={*t8k{`~yK)!py z^6;pqPK$TOxJcn>S^Oc1({Ey@iVPh63$zy`A&ON6y zS>95M;K+x_P9qCWBl=BWyud-8EBU*VBlVi`{ESx&=>t3QKz#Y^hAIN5(u)*9c4ZcZ z$u?XNFq@1O7g7YaQsUwnOg3PTcB-P`CWIZbbwE>`-X+h0qv;v>4B>>3^SH`_eT5l1 z2X^CuoD69Y7}&Doft?J!0fSoJFoi^eOI*F$sIb9!iqG+4#! zv`dmw;A^Ig*Y+kII#67DX^sggd)1TbWxuSlpxEM_67kw#hcmoH3d0fP1Ni2R0TKb8#TU!YFgSW2&gACFTad8F)< z(t{y_r$U{glbieF(MV$`S8GLME|7eSPh(uE)tk(VEmL`Ft= z^U%e8$WJQAq&rJ2kFne(Nsb)#87y)s7x)6s452IhP|o%x=u4qZd8;MDmq43B-Eyuk z!N-L@<(-y%HUapBQst~RL0N@=81({meJlzz%Qh~?C4My=iJmalfI?-1jY;MG zj*R=7w1!$N5#<*rRuqK41}%aO6DF1KWKqFqL@QkUufLQzw&PJG=^)4`qDaNyD1yol zxJ)7_0*eor@P94F0M>zTG|;i4rh-#5JRCw>=?7R@`LViIg$JBjA=E|ht?07CN{jHU zEM$@kb+TwA8#q;>_Vd4684jlw{;~=@;Mc{iFR@DG#`>pi=cQJZN@g3SS+HOg%Pk1> zXbNrtA}WF_bpHLy=|o&pe)R^S6~PMeb(t0HN~A}@x)sVw^j*R4!#8{p??MH>(9`o( zMXI|=uvY+O`>?noj6+JkP(|P90`{)ZW5>Lx$=3R!zoZ4NE(LOj_08z5+tq)l3pS1q zTbp72(ib!}WB(~I$kLX)pMeBQ`3MynKbKbF1h2THlvGAZzDIoqHsC<}9NEVj7Azhj z1EXZYy+0-WBls*r$cMk^zqAHz%`}f&XR)6OKk{HH#?FDiZG7U%Z5CFHXZM2Lis4DN z&~>%|x}pQiP98aZ1Yxe*`aIk~Wpo?FBg~ZlUP~Vpr#I9YO&=4dZ@?K|A3eRdMEn}- zPboqTQTh7RQ{0Yr215y%CtEzakaDMd{(Yu&NqDcy|-mv$avj)64%(+K0vnlFI~x zYlrqig0LE1k8me9h;b>(ekqQK=LbLXP-qon-2LRB;>5}})M;M)Ojv;8L{T zQamJ2`G{W5sX<`)wy_rvai9iLdnIc>oaqIhJj(!*D?@ER#TBgpeteJV6^{V1Qa4a8 zzoHbtQ*!1UQ0rK|E zZeI+N{_sm<5ERM45XWkC{0;`t5yV38eE4Poy21lt=gk!}3tjkyF1@t7Cujhv z)J@VX=Cs}+kN~LCEK%w+ahOcE2h#*u>DfqtDFso_E?82p%XrR;V~iA+Ato7S$x(Os zam@}{()h^=&*mXH8M95}$iXD`m_;yuJ_IQi-czB5h9q7lXi=jHaYs_Zm{3xiP^(a` z98j?Ki&gbYoo1O(znfr9)&@R-=41|KV2cd7EV)D82Z4S$P?jC_Cv;NLB>Au;V>2E+ zQFRis01*TI#{_X=d}!}4s=J_iF$^^H!yoFSs;F&;bvNzm zbWwpl%q!xlRSmfw+F1^UHrq$os0Zi=!Z^HpkJq z!@;N^JvWEMK{2t*FUdZ~z)^XDG_lVw*DyzzwOYz+&6JhBKl98rQ3>ym6~B*y=b+o| z8H`;VYGu61sJrX0=b)Q{T{K_~ziFzQlvY?`ji>wLJ|(Sa&+q-WAnJB&N!>`zQVlB& z-B`^sPOCq{jTWu(Hp*mch@+j!M`PsP`}*Zl==9>f0<_Sm&I44u%hI&a>&82h|K!EXPwdo z5qqaq$oeumFIGJ-b>@%63rODarFSZlsjaedTAy#N$Q7=+^Bw%fk7p!!Z*2;59?SD! ztfn`EsOJjYaETP9l@}UgA^Zst-WO(|e|Ue>T$F?0FHQG5_fUo}Dz4O&!%=a_W;-XY zXou5Pk!Cv54L$etLcDCc0w|IzT3W&60`<#7pnN%5a&ri36`4D9BQw}t{CBGMNDJtd z|53mEz2!z^a1mcja46Lp>2Q~Qh`spfp9w9sUp#d85pOvAS!%I*#P|yLcO!EX)+7=T74GawQV}tb#gbZc=bYmrW-f7u;#@;$YX?a+eQ)bXQWQ&rT z8Cb@WoZM4Up}PX~lOG-q8j=8d_pEgs;CEj_0fP79V?SMO)C*M=dgb^RtIwY`%om>N zwcRy?7t9*9?KQ&}%y0FZwf%ksaGJ$xoBjylG@sCRebxOD)M!?%ZTlmv(fmx?_Z9m` zpu1VFw&jmd_ghEPb#2cd!MA1|9D!3lPo51zUC1w{KNfF8Ze0b5R=pd0EYgqePC}P+K zc`)Sb6YWx7xd^JQ{kaDH<73Ttmb|!n{PPrk3Y#VCUo4xwQg9921}d1(Ju5A{`6K+p z^oC)-;ZpK_vjl1iU^I)?|M>f2+2mEaDTL8{>^blE$7Po$F}=4-ADW`7Eo`5Kmvo!r zsx1kYq?#h_E%cUth|vAX`9fI#i}Zy!0$5+>1hRxkfcJ&aSrYY#AhM8NQt^l(vZR6F zU6S>P%C)du((#DPwZwt&UXt{PG_cTG((s5iu;gA+0Gb3Ao=XNE@#~f-OX7DCf)+|k zYH7Xy3-x;n`!w?DN#xqPy-`VqqHcZsoPW8qzxkEi>{TgZNBX&W-yTA7L-PsQ zt4)O5af<&kF7mq<%?2-c%fL%(BlaKG_uEy#{oZdce<4zq6qMcI2XH^c%BSvdC@Cic zD-rPHn{5OXL?hx!3NK1-C({6D4DK6R;t5lT9K9A!((kdW--Cy)d5EWS1p?c=OpG{` zw$Hn@&$kTEcRmq=ML&;+!%$xvgIc&~F46S&0>pv6qCD^94?^a;|X4l?=yNho=0yW!Oi5u|7Yoi}!H^$R3w zMD=kP=V}0uXpE*`?X7fmYh9*-G>im4PApzFf`~p&abLy;PJl2r$D5z;N4lGBXdpN5 zE|LQsQ3&_=9~3*IFZO*QhW?#6-UpV+1d<@<>ur{IBG0 zj2B;b@cWhH?!2pmXK4SCk5`M&$o^xSSNG45ufxL78`o3Mh_9nvR~OH)uOt4E>g0_w z6thuoDLY*F&$4a4O1QFi2Q!(=W|`sB^S6xUH8@-A9ZgebAXT3Yg`D3ih#TCxvO zs)gTZDMq7Q3fqBEGwMTOeL4A;D4W98ax$AJ-NL4FisLAs!cKr#hG~eDDs*OPvm(!m zvMBUs3AZBCjnZ79B-4%NDy(jjjqoV!ILwblFEp*q2WB2v#7L&X%0uiGDhbaSiaPV! z5P&6$+=#4}B$?6riSc_NzzC0(MOzaIJ$L}M0K=yMYAl)sjfwo5pO^$fFbhI5OEfJS zpyMQ$(hkvB3Nhf3sEWY2>V1zMB1Z{h8&zy%zlBY_g(b9w^A-(r#KfxhlgWXRIIBDo z4n+vQ9Ya6>1U!RGZAE?%JX1)5Nii;!R5Yte6)uh)*&wW5B)myslXirxiHnIAE7g*s zJaQAxT)!JD#ggbak_To(za6XeIMz)+s7{0dGrE7<2&InFO)sLv-xCo4nHN7H)`?YN zR({(Ot9plwW1=uk`y-wLw1u`SlZ1|6tc#=n^GYG-JVqm>u%t09T!027|}* z=8f1XW?TO`DVA@)VV%+`4y@Qy=%0}Dwv1B>Fx^M)Y)d%h@Kh+^0-Q5Z7E6ETbO_mG z7!#5+6OU33$|Md>5MsZiv#kC*CUmWSjk5vN#r|7i_AmgdiUNEZWDe2%+YU}0EjVkW z=h3j+1zwcI(KOo?GH6r%H(<>xvG(UD^9Zf&MlXtuC{O<%NLPj+RFYunewr&CU-tfV z@cSzb;J_b1xl(&hstB?)KL+z=QhSM(rXHC+f~hh^EszY;GR-V9B`s2yzp02@plqb@ znoWWkGwHqb_TO!!dYf&41v8nwKJKSO2cejY>d0WHNt(?}H;_d`tC>0Lh^3|cPh_=@ z_>YuEvokOOpj26)ey5|<#$)EUO!}b1l@j|cUYsjU!Axygo-3WvYz(ZlOy}l0`~%={ z=vPPXMvly2msxo`PAU1vnCdIJ+=m_N+fK>;qMnm;4DM2YoB0D4mXzRK@NSMv#5~nO zl264j3my}NSuux95r=F;OwtUSly0%cO0fYSun8*pYW6)wu^ja%hLi%)l?C&NXj&c+1;chS*FkwmtsOH=`mIpI~QwS#kh2gT?t-U#Wc0u`Q0d& ztQ;oG;Y^pb9L9s;?=GKnm~n|EL!>KN*`izUijV`(R%-qAVF3D>Q z&BN6$*=tOm!?}@$07k9&b41X^(S`s22TIMzdMO;U9_+hjUBm)jIsYLKgGYf!Su{TH zVY4c~ZlT`orO@x~ZwI@$A#$Gh6nY-;{UF$&>UsKZp?|2%-S7gXls0AQ})i zh#4gO?D-)2VEmx+;QS!_VEv%;;Qb)^VE&--;QpZSVE^b{DBk4lRa6;KG~Rz|{v zr4o@K|4Pu6{!2ak46PVA2S(ySoD94HL$ts#1&)CcS`fYlo`7Lm;2i>2 z!S8^iCGZLC2*ADnGgbZepTxY|^AhqoB-6+u1KJ{V*~lXUazEs|kwXUTe&{cwPug#= zLqd&=wV|*>yLwD&c^KZx^{Uo#00Lbg5Eu9(4#&ksGzZ9OW%!-^|0Ji41>^&;twOo1 z!U35qUp~g48V030Od1ZM_-9~E;SQ4nZ`rf#^ezYSpywArtHa^+ z3^>8-Bc%820nj?UR?mVHvOeNs&n>{N!~FwPUwS^kuD$i_HE`kvGOb=eCt-m%D81sB zECLX(A<|o{UWA_mXtqB7KMH7H!%)ED4{$cUIJQX0^UFol$ofenYc$vL{b7IZzAIN zL|7^c=Iet03ZMD@BYGD4ui#nrFI<~ZS#BPoQOf=g`)s2$k+SqWyt|ZvverDZyR@LP z(mdj=l%Mj)d6Zje$Y~J{xROF3ZvGph&TK`G6)RG~zb<-%P>A~9eY3rAq!FmS5u{PL z+cNnTR`^+Q7WtW0*jcfEM85w!S8au{5(C7$R(i*}--v$4y@>n|Y<1p$1-p-kxngBA zcy8Fa;s-O(ZfF2}ID_SeWgxydgY1T}F4jClyT-S`Nv9Yy*T3w*rFuzW*rKq4iO)tItwn26j1;HJMS|4o)tj*BpQq}!|FkC}BmQy?0-TI=An4lIdyB2Rq`jjLm;IG^i!x`Pf zh{nU%bA|h~X9UPe0d+u3WwGCohT1*Y=5JN6JBaR6N7kJ?k$4P?| zy)cPm=W2zwk+$Bv+eRf09@=(30={3=gwZ_Mf+NwK;gouluQ2_I2;+vuaV1X4kXUaD!OZ^y3b zJGp{sM|14WzQS(Da_n2Zx&WAN$ctyCF}RCs&+fM0L|4TAmhCtfeNVsFuik>DH!(=A z?|Z|4cU&b#*o3_HOTQ+@Ub2GL@}iWU;~hejw!1!I#a+G!@sSg5vAe zs77GkeWa0m=9W=;WxURQ<$Ay0B_8#3k%V4M`F`&;J0&hdwixNX+-Tx;anLJ##i75H zi3~$x$&;*vBIe7Y#F4-FcEspwTzO=eRb<>@WY6no+{nlbiZAGqG{8>9s+XGn{a2~e z7ZS=uN;Ft!bR1`l_*WWPYQt@cR2g$ss)S5+WHYJ-h-KByq+>MK$azvu+3JXP37cfT zK!HF|u>+}aMfz9f#K+>d*0@*cGDQ*H68Nwm5~wLdIjAONZbaVC27RNfma&};`$lyp z<2y^H9Og!uD{~MrCD95sn*1NcITYPr%sEU#pc$$xvN~n(2r3)Lx)XUH1Oi)53|xO= zq8TP0k#aJ}HqBdfN;0;v7^)ozKDN*Dq!^GeY?1QRYBCvbAmGU}P{rUWa^TofGUR2iR)%4%LJEyiU&!5sc1lGjP$q(M*+T{SG3~c zL=nGh8r~0ai95Scys|IkWy97kVL4O>{|lbZ{ilm?m(^SBA3Pn1Y{maAux0;8LO3hW z!9F3@Fl)!bKOyQmtIxqbA$~Ip;t*yOL7Wx)#=rW1?$)dK}p+L-N3VNTtH$w>w^{^^#kQJANg+t9>E@eNiut8{~NTuED+TK3Jtl{ znY(Y0XEOXVfPC=mL)*;xH`p^--^YWKxP@OO1U4i%!3tvuLKE)9&UskD# zF!D42zW+BqokRX@D5i=$d`XImBJ}^WeGvJ-sR#L5$kwHrrJv~^`F{tW!?ox0A2GG^ zUnZRw_#O$h<`L(^9+CNE5EuBDwrT&g0z(R7KS;#SVFXV7G?N%A^oQ=8QTtb98hG(j zphJXtRt}9h+OSmXP{W5YBh#>q`*6Yt`;P*u=b?cQbD`DDQxD-!$4AnPY@y=#L(@mf z_1QwDVCW^0%&#q6z!Agzy~mPuu>b!p%d^$KV$;ARcKT zJ46SyP-UV#W)j4QJ!U39i76_mR?kyvVpD3YTWacKVKkVZn3<6|06W1zhC9VDK592U ziLQh0%0lDH>N(vjz>w*k8LwfIs*~zA5e;Oj+PC@P3r3tejGEaC#_l?Tnt7Z?$T~$4 z7X$bW)ID?zp`5WiAFJ?eiR(JGqm2}PBsH2;>tq|*|KKY8ROf+q8{KG0q&;Lr@#Fnc zhP#DD#Nfh&8$*mb$R zr{Z4*MBoQ!C;Y!Gh~BN_gPOIec3657KWZE8i1enoYGVPx+IUlKwH?9gw~upvvV6Fuq4t zuCe7ZuZNHUnJLcF09eYy+6ljH{A+DxkHRT_8}VANLv8jccH6{xZQChv+tiE2KdI?m zjG!g+lH6TXhlS&k-d$XWCCQTXU8JAI$0e=1SU*eQB_%*Qzpvw^`8n3|00>0^_x!%M zm*MC5g9B(UQ9wbzFX^T7Lcly2eS_Xk$vhx;!`9BgJT!KL&`ut|Ri{6=r{9w*5Lelf zW!sW=((;+0B{91tbLa-jVN26t$K&&Ga!WA(_vp6o;WVzy_g5d<*?`OWwM{!;O^@xh zZaWunRloLW7jo<-xRz>Xb?jC1Qrn2(K465Q*hp&Lt`45^%65An+q`#1u8aR?Cfbg1 zK?oXZgWA0L+D>pW>3jJZxZ(plXaLMbRaeZSy@ zlH#|E#528gY3c88L}>X@u2D()hOV7K0(spu*RmkCyxu-Sr(v|A9H&vd1O3Yi5PsgI zhqvI9i2IfZf=3}}PL<1dAO zseFndIiPuveZdjNn!TF)GrhdJA%pBfWxI}kRvqqVzM+4n>l&=NA$Yc+{k*x|EDrzt zu50Y#>gn0>FNxw#LaOkV(yxYuSYedqU(w3jVC!cp|ACJ;+|P{uLz_1!&lIoy!Z*xh z+<(WH@7W^9$)ktq>LX9d!-wCyM6MN#lE)9zUq?O{ij;pS&!VM8i0m!EFO(@yqowsNP?QeU4Bb$PBxz=JfPp}g`~TxqjL`Nl$;YKs74PdeVx{EJN5bkO|B{Q#Bo&cpl7h=P z8}boK!s(J64gEhfi`Z%s+Hc-MqidszLSyK|{mZz>sy2?^FhIzvHI8vJ08z)np~!$9 zO(FDI8y)Q$a4s^K>_+aHNWw*>l2K|(<_Og3R-xGrv__=S{D?+2F>TGH;fkEdWJ^!e z$a!DkeM8Sw(eiCOE9!*){MW!#+s7++`MV54R%jE1A zGbd+~C(ZWsWz=Tmcocs)YgpAx!Ff@+9a0Z1y%b||M% zq$RuX>ND#H>Z$5W>fP&+>vQW3>ILil>VuQ6JO#cy8zkm*5!6f9)6|#MTh<@c}~2D)&9M`q&>Agw>`N%y1lGD zy*(aO12{6FA&{?YBIaPw{Sic{q7ZVSZskVQn#Nnx?w?Fs5^qSE%@win_|Y@_Z6UPgs{%&^lHJ)^AD`rwEe} z@_bwjcW4!L?3^0yeCcr#tjj%U#h_qwxO;Zp`G%K{I>2gQS%&E-H%{9y!&HKt_mvonobK77O^^UQhV>*D9&i}D2j9DcGO^M^xRweWi58rPeQUBn26RxZ6xX4Yrx?74kDC>Y_3 z50tgN*P>groNK5KY6Ac>T*8+Kzz~=7rJau2hWJ>6I#nYH1%v8oy5xhVhKhy;r+P`b zZUsOhSMZhdRkW7>X%DD`DTk>Tl;zsS#8MW;(u7~ITM$@)SRh~c04M}B7v3$rT|is# zUVvVpS}z<8dUsuzb=M&-cZKdX2#ZV>97nOO# zoJzw&!xF=yjY6R+p+cb|p_Lq#ERGo`HIO3`bv_@>P&H?l%I57 z(+5jd3nBUaD zEq-4-8azTiQamO4oWu>A7jWYucyZ;WE6>IEidlaI$oJl#nbUken>R2_d68VcucC!l=Ub z!WzTe!;r%i!Y0D(!#0umk@%5)k$jP#ki)6EuKy4kbOJn2#a3pq_NN%Ww9FWtnC~&( zF~4FO$|&jOc9UP&hp3%VPZ^UOk;Oq zFk+J_pHy+nESA!vW94GU*q+BA2m1!+1{(&W2kDbSy@$LOy*3}_AJ!gL9u!?zyYT!o z>ND!IT{2y=ud}W*5o8rDtkraq*NWM!75I`j%o671edi~BW+W*XCQqp^s}HEJt5eum z{ov;0D9Omm%F5Qw(#_P(Uin*!^ zn}1vpkSTEazV>a|nJGr<<}gm!17d}(>0Zg_9J zZ=i3eZuZKLyyrlxAf+V1yo|i8yv)sPVQKfNoU8BKHvF zfh>ecRy(y;S5?fD>Qr$l-HCL|t@oFIWWhZ;F?D;aPeC4Irazka4FD$rXp+tcol&!; zg1d5}<2p*ccI>>8q>}pLh#yTH-#F?x8ac+MC02D>bsn{0bi;JRbfk13((=u#%-8(k zik5oWQf5Nau+rSqZqty{V$-UVcv`jCq~mqtbQHA}rOsy9(;CtcRRVuT?X~X3?RD;z z?B(ti?Pc$^?UivRb2e~BbEb3VH6}DhG?r5)4@%jqrWYq`PF>3C>FVjM%5b~W*lO8& z+A`ZZ+WJ>+;@8L3#WkCZFc?@Fn5^5ZTdX^*+pps+$SLVf>D1^r>RsrNXp_80A$R^42kN0}2wx#N~(Uus_fD19RB zE9`^+gqNc2EA6w-P*&fq60d$*C0osFDr&-LN^a`1)3WEb^R!2?kGE5@W42?quc3s! zZ95A;D-(e8+p|eP@2RcbA%!*E-oLWK(`#3#`M8 z|L}!|QtmeDck55zvgj{LR4TWPzdL?gs;f_AX3(>Gn!J_OWX#eQsFN@m>1T`FqcrBE zR!O#!;gB2sa?-_DSzR=1bKn0t(hvPfcH_9azfQ#!iz-PXjTXI3_WF3de>u_>X zNk6BB&skwR%%8W)pX#%$0F8AO%BL%BVY7%TNy_9#ebrFPWoD+z_amQEQg&spsedKW z$%@&Y74yluC*#X}u&pegTcM$Aj8jCQidDeR8JR(KYTvCH@q`^K?|luZ`2>-GmGO1Up989{P5M5Q5x7d zW4|lo%;EXd1<9%SL+1(Ykd<^6Wjp#1`4IjP-;mFc#ZcO-Nr#Y&fGfxe&b7}u%Q-7z zveUBkKQKB_F5AR&3d`Ba#mGs{NzPTvF{W`bk&VWBy*9z&!{J}#Z{}&{z2~{-Md3l= zeRU&FhFVseWS?}M9GkSAT%Git44rkS}bZVdMpwziY}5Z z@-9LzN-ml&Dl8H$8UVfw@x`48?UTuErbDJ}remgarXwZ{cU#l7I*0FiTbiv44zYR{ zT&);ucjab!%LWSu>jq=%BIx#NYmC6zw!SX7Cb-@~x$Y6*o8VjS+u|GITg(q8KKOh{ z?XdrO|1(o&v)r28I{jMZx-D?g^`&}y-}3bBseKo0=X5cuHL{$1BRA+Uva32$LS@~YV|Ml>NnzxxnWIaie`cF~GQY(7`kz~ye#>nbB(*bm zhwfgSD{dsW-{S<|gVeWvlfG~FZl~%fY2O3p|Fz0Xx(>RU!R%dk35AUhNuzpGdYh}m ztK+Mqt1GJ;tE|U2cb9i*F&pX^$`>pbG8eQLR*qSVugsO)rzL+{IZC?9I|wM7J>|mI zd|Bx3iEoJSKPNqJRBp5Zstv6T&kdAL`J|ofx%5q~X{}i;O>{p_p=zZI_Ju-P4a$yG^Oi!A|N<%#jECGus2(ixbqwPi?<^o*o~h9;67;MtJ|wbt9z>l{QFO1o}->Co zrfR91q-J*12bf3^BBF@YNsV9$tdk8(lOZzY(dQ8Jmzod=>YA&Kv`(W87_{r??>5Go z?xWn>77)jl34h)SUi@;q_t{rqe)8;`P#%J<^=?D&L9x*sYZGk;dQri7gcT zn2$<0>d@mb;n3$W7H2$E$KTLa>sybp>?gvWF@{}BW%pj~ozxN(e`vLd?NY@8elM{H zjPDy?R9`q>Tr2p4TGwUo)5udd zzTCbfPpEA%z}s$?Uz}f{A2dh!Mcqa4`K26S^&z;^yQ7&VdAAGY9K>6Yla@oQ!Dy0G zwAaE*tO#RCX^CV>Xh~;V(`m{Y?k zi9=U|H`G_uHwa#?UN@pt-V}})j_i(zjyN?bo&lZ-T7&$9{5||5`~&<$ZR2gdZG-d4 zB-g|@pRYEqgo5MhQE1;UZZGaH?r`q{b&Q9eXQ})zYoAHRECESp?yvQXPPP#xVC#m* zCm4TR;dJ55BZjFhW{B#&?Jq0Wiqm=CZhRvFWf%6B_*-Q^S~!G2d~t<*Wm+VJ@D-7>0;_TgOvHgeAy@8l*m=Zb z#M|H`GPL4=lmrB}K-C7UdQp$zx4dLHyD$^`NVFLEZ;(Uk^F^nX54o+NIM7GKK83p! zs3;S2;N!z81W=15=Z{QdTG5sBD}^KE<4xNsdw+(V2*(x)offk~`-ZCv!5Eq*N?))r zEoH_24e|d__tw#EJlno!632F8Ow25^V`iBlcFYtrGh1dxDdw0dX6BfgnK_P`nVCVx zr{6jE&fGI=y*2aStfyA1sj9lVR7>jJyY}b1h)NvDB#fIaGjll3oaMMrajm*ee~vkg z`yN^3eYW4OkV@90>X9&ViS*xdX05+CzN!Ye-0zvb2atThqr@Y_gN89(1N`v)lKnKZ z&~(3OP--w}kbS`@EkFEbhlPa<^i^{X$x^9QsMNPtu`eyg_Yt+%wnuc%1$-n?ughIS zT16IQfj?lD73CGykia9iCP%>pV%EPQKKPSG$%v67A|mORQmZD3%qY^rLhPK6_#xP@ z+Bw5M&A!z>)IP<&xl)*0q7gx*paDrHpe1?onsx(z!)JryZz`C=o95}ulQtG95*B=j zpIC&L+_#OK58tyQF$ z#U}0MJBJwe2=_Mk+(wO7%~o9s^#jcnPAZF1V^R`> z`lGq$d`#?YEHD-=Rz8+nihtke&S-dk2&4Xe?9U`1kdBO!AVf_>3I(IVujPZlSeCp{-0CJ`osCP^(TYhyUREov-&BcMZ#3zC5`i+c4du(t;?B$i8#>c~Wtb|G?q^}X_JMhF=BU1!jG8(Qc~~^;hCH$EmrJzEuf9PeQwO$$(n1EQep9+N zx@0wR80i{O`GJIikera3kai?tXAqni5xX-)JxD!VFmh=M4}16}vLLcRzF@R~w7|Q7 zvjAKmTTm>32dlYCxr(`BVs!zANrp&9^ak~YS4RYg1&0Jj+J}#Z;C8-V8hBuOAbZGo zfIM3Hzpmm>cYBS*?O5}uGew8(U^FR}Cln6R@-T7@KaPyS#wtl};kIv%DDE>?PCGe}^`)3F3x_2Hy zMh10A9+8`FeHwk-eOP@Qea;0Ef)cC>tN@jv#FuHRwbJ>5`SN+FX!_H+g3qN>)_1U& zgQ|m^gRXLYUx?7S0=4~KJWxjcT2wjz_En;&bxPv}vf`Or?H94Y zhwiJ*_p3|@*z@wg`QwM!<-b98{{5cIojVQg6>YD}c>N+PUJx?_l{o;nM`mN^9I5ex z9(;xGT=&t2kvn@UHj{5d7vFliJ#8yM@C_g!s#9WP>-^(AlZY*h>J#GO|`!i@Yov zD2X-s*^umGR$Io;YY|WX2VPv?PgH*7+#;luIbUlf>0*QSKM}B!iuVRwBMP_@eyMJW zcBX{G|A8VJL_ra4O^G-pCQe}-T{XyU@{SF^b|H~eCT3?4$%Lw!K-0feY?gvBIzp1Q z`s0G%FVdwL;z9X5tc75rufx$>gCDmU9MPJ7>U=FHpNl>kl>6(@fy#TT5M4Ovv<-iW z;Sty>J{)s5D6vg{80eWR_?!7KYSy6ml+41o_T#^Y87XtTvx%%1UCLFoK;%G(5Zn6e z&hhpn+*J&7g0hxt5nWE4=L_ym?kAW)#P1K-xflfxXt^IkbMQiQ->3K$E293R>VRb> zEN20k4qa44JQNX-MHW0t2LOUCxVgYyn*!>APQk*L}zss5d$tEeCRc1<Vc)E`#y&FdZu-hT3mc-#ax3h7~11kUvxZQNW0xqn)PX_BPE#{F=wVnubPZj4>v zxXZL+uM*h#H}ouRDY|p4e*AZs=G(Jyx2~4W72%b3u2sadFd2J%rUC3mj9`cN5)LRd z_896LTKw2^8=h(XsEr@bqEa2`Z3l=OG2OOE_=*1s=Ja*P5&QrMNAC*W>|mvOB6@P; z#{~rLZRB25-Mf3g6a1VRpzv1r$-DJUX2^Ko-WKw?h&NgL2j=d%4Z`z?dsi4VoshZj zZr$s`=N|m@2K-4V0MH+`ZhEiyga>1w2WEDSZ|N)_n;@n7c@u|q*F-E_2$AZ0DVK|{`1Q>Ir1OBWu__Kg?u7U`$?Me zc`O9y8|6fbg+wh)&QFURxQURTWJV#}eqz0t=^?h?+;R{n;v^|!f9id+%R!$=+?TNb zKte_uAjAn(d`DqlFj7HX`;E@xN;x1S>m!9BIYU4^o}|fV|DRpGpNzO!{qUqrFnM~v zkVoN(hEw%_-U|8H`|a;}rt~%@d@qU-HEURs+$h;D#??<~uZR(A6_(1g`lphgGg-$S zk@J#Rn5jP*d)0CR=EbcMBVKjC(N+a>3lmYvf1(MQ>gCzwcYp;%>QI+sLNMHZw)6^Y z3SK0+i||uqU^0iO_kuQ273Ev0RzIJ8eSKxW2)rhLmV{JO;|tG{gW_BUgG@;Azahj$ z41UUb7ZAH)qRi%Jt(Fg0OSTkyG6W;VetDZdh{u)?PN8YSpZsR@hixayzT7Rv0%h=a zEPt$mNGE!9w33qeuoQd|r6C5HA_j#+EUA=I-{3pmSaQ_7kYj}uDP$G|qP#fgI1)Bu z3pltz21gd203vBLir_ek!59-DPv|Ix1pB+L_XeM2sSBg01`oDnNk5O0ArIwmvpMoK z`7w~I$AAXwws{=|ntYcfapF=3$F^~*1D3^4N!?;w29+Gqn-Z5LPHE7i5r=xVKUatI zOOsG2MAHl@Z);w@^N4Bvrbkurc4i&u5!@=XN&|>d_+fJ%>Mhq!HZs((?R5zUje(K% zh#~`Jq(Z+(oGN}FfK5xUtd?&ryAZ1s zTTPMw>*CX&JRLLHfM8*lGG1aSS?>dx0lu81{+(WFoTbkUit}m#f zGd=i4WaodEjhrNM&D5fCMfh3xzlSDKCQ&9(W>BWW|AtSS zfE+nGK;o8{9J!pvod4oJbH;%!#TlTuoJL_VVdP-5!UB~Vi5m3%&%qoL#qUF|I-?sE zIq`Q|(plRUL9Ia$Q)R4492(7Q|)21%m0pTQ$7(GHR$-X_07A@Dlct z^!nx{>!sr*;w9~+<)!DP>4oZ!)qoeX8FO288**E8i*HXW_%?ad&Oq5c-xDq~2ZuuC zeE~v#5R1GRGO22OTMl$w?LJ2el+xGor(8=D@RVVo-ai+!MWP`_V)Ang+DGT>5(Prp@t8hT2%Wy(Jx zFx58QHqkcYlh;ZM7)~sP1)6uMe3(f>%I_{JPdGf!aNwdR`DbdNY9YNos`+fzLxgn)Rnc%m)R%2wr&2kDgh{n`fGYE^}yj1DX5^I z$2_Hekb2GdfaVhg78c3_Ofl~#Ueh zK8Bfv?N5$cwr}`3sKhA5sOc!IK^mfIzNV5a&Om2YXLe_Eh8R6z%mdKAxg@aCm$sWr z7{T|ebu?a{Fs>qj0o*Vi1)4u0H^mkzG$=W!3Fh&G4-)AS=`rfT=uuja{DpT*a7&0r z_~&&y${oKgf-|D-hX{rR3L6j@gx?b}7G4%^6D|-gC0-tbsDlysnH@tso^|Xu`z;r5 z1XJ!3_aOIjlJO{<)=(o|g-MKqC1;IEu0xzdA?y)R$HkATQFpX`Dz0#@h=yX6IIjMN zeuaMKPBu#eDka2eS!sieyrgOBGTORf!Kx%8>cSzLVVHv%m2)RWJh?xqKbbnI07gf5 zl68{1lD(3XksXnvDzNDk=P}{6<~8EA<+W+HZZ>+Da$dUby$-)nYQMfaJX>0;=zwr? z=D)R9K1}i-HQbeS0y$|r{o+|&9vS6A_RS?{9~o;*W{K-mo=B!ODCQm|aSGy@Y8`8x zYMs+jlT-IF0~>-(!8Ty?7n7GG8q@qqjgRod(noaWRQ=u6ly7YE5zp0P8Lg2EQyO!g z0-JK+4y7Ss3r24*LsvFy8}T8g?wnqxz`yFd+BjOXnm~Ah9$-p21Eu42GfMC zgg8}#5!Dfw5xEfoaZ?XV4^yZCbmX#OX5`T3rVc^Fzl?150|*%e0`Y-3a_j6%`RUh? zv_I3EQI*k^EEZQ;l@-l$l&nf?qpIc3n4H*gYTKx_z=j!Wt;{-u3OJWiK*XlRAY$|M zbL17%{JO#Bel2T(_rDUiW}RlQW@W-}9hW@k9(o@}AC@1w9~K`F2AKi)kNHaA14EhW zo9Y{c7n$k^8-Ht&)IsWcDQl(FQR)U-YiU9=3LCmqfv)L+$y)ysr)I<2_+UznY~aoGKb z{g-_ba}sh=bkYPvwa2HM{^Ju4*z3O{CKl)pH?68O3Q+U z!lkPd_T#`?>|4&;h+9S&k{$Ty`VWrXtH`I~IqNz4x$?R3IpCQi8pJfuyuiFvF<-G@ z_ol00`Q_~g;0kd_q+X=pzu@cj{#)yOq`{qJjz5W&f=9{Rz7R1+Q^KtTOYFiOJ)jFjDqG7%EBrCorU;I^5!46 zakqaxzqYS%#%)q>nRB=DIP@ZT$$DFNTYFJ^kMteSbK6*WR zIeImE$8gSY&2UkAjpd={G3xcLztOIClNx+Vge(+d+D3 zYuh1sE~M3{jUccN1%y3IUh1NeTDfaD)=l2c8{~o%hIaG`2NQ=72e%&y1i-do1uplJ2ZeW3PP2J@e{i4aFQ`CvA~-SGxmN+#-B^A>;#)Enk(cgOvJuz%dHZ0b)>iy{eOof0 zehj@})EKk6Hv-rB6K?fihk9v2%qdd@Nh#Qs%tl8Y^OrVLD@Eusztc3n2r2Ai(K=67 zU8HLew^vv^>uYNf^-EI5IW{u>d5QVS>XQ`+6z^tIH}l3->%KF0`!z&C)ooJ$o&1|J zc^nT>#=9K{J~8o$34s=gmwP^qvDE3Kh4dLtL0@=56}iKzW|bFqcagnvftFj9=QE)n z$M7?+yQ?pH?F*RnFQ>*n)irH@`o!DW(2ro&C2>`DfB%k4J;E$SIe8-DGUL&*;TH6+ zZd!L3pa9h>IXwW$sV>R$dkdNMv)@70cR182_Iug?Krd*BG=tz4eG9qc7;C{EjW{dbx1_1as_Jg@j@nWOeriTQLL z<;2?<^e&Uvv?ucayyJQSD+Pa+lV2^ALDaS~>n$ITBDg#Cm>8~E#H@<~mU2Q(E}DP) z!TIMpy*9qsctPq77JdcYNf$*q#XL^^F}Z%eH_hO8<8!uojgQPIwdpGb(FynVT$%VTI6NL0X{XDWW*%FcP z$^+dryb=u6Nd~`A$dGnm?B!5YgbSpwJ7>C^Pj^{g^&s**t6gCt3*DT!id4gymIUDi zgU8z3@I=(@j-IWzH_p)!dCkoeT?SwI$vGXae~HgU4xqVTP2OhRNw3(}yT!Chw&mEB z+{2)wnQS<~as0Eni;rs(n@P(epHrDnS$>P#nw=HJ@qN8mSJFz;?H*noJyfU2G7`VL ztI%ZSr$;~uyBRl5aMMd2j#F}w*KrPd9EekDqGY0o8zzeGDpqw+N`Y6`d9TM{yAu@y zZqtn4Yl>u2SWmx;+0(t4EJZ8*4bht)XmRL;xYcWR#aB$uRxp*^3Z>p7R354@S+DQy z9Vr`+Gn@G)Ir5YpVR_!@rXtuotQ7X~Jw`3gJwQ#sZOR*yD?2?w->NZ0#)hl z1!H0H{@8$;Beg`5+q0X9JL}in5}QA1d2_Q`FHrfZ7X4U|HZq-=Z&ql_y#AWGFM^&| zTmRi5C-u2(=G}FT_A@x+{qhB|=ENFvfErPvmwj`72TzUTFNyxU(nj)p9Nv}q4K`D8 zN1cPy`TO0{pJxhaP=^Tm^x@jZvM}(iUB+-_x`PyuV>iU zPO3DOo0z-e8NQ6?*wIu<-YIG*K~h|OJQ$UOja)%L?>uLm}BQtrCE`R+WP9QVxPs)&*4?y zv{E`1k}iE@OTBcmBUHLlDDky0UQs}uAM@o;d+L(GfHj|Z*&qD+@3?VP9{iv9!)z4#XVNE_`?neMQ|iMNhVsb|Q}Y^!&7Fk2>HY;xu=?7W9v|E5(E}z0 z6BD|^+%<3Wm1^gvTInW1?r2ZGPu~>ZW+7h!Dklw3*Fz9gVG%?{C zS{C0!X!A?{!PVOE;r00{@e0u2Xt%>N3Q;j1@4dv&*3C?GZ)i%x)+nE#Vo1{8(*V2{ z8B-@x30gpRv02Nec18AozU-_eM$%9fskw?W>MwCWBoH$<9|#?bKlQF66Ev6cwId9P zGJ3rTjx`bf4*exGfv&5L<(g-FSI^oKf$ZO3XCn*~vK9BX(sX6bBhowBrks9?o!Z`d ze!2A=zH8Cz33e0U?|k56YoARrgHCKrB9D0|w39#!&!}&fP`M&MF^6~eur!;-*{S?( zpvBdPmnG3-+gdL^O+Q6#Y9K#4?G&k)nr-(QcO|?tcl;Y#K3gyg(zVNsI&W$4+jILi z(-f>sps3JMcboHZk15LsN<&_0dJ6g)l3R$XQTb$T*jM^p?PApc?rODD%RIq5YHlp2 z^D6OiOt0#(>owHUSCtiAMHX4$rD?$}BC5h2?(vz7-b>S)n5FA+=EH4Q^u2+a!H)lr z^~Lw`hUJZWW@=9vL8TAz$RLEdfCz-(5zBk?ajVxBfk(GsO=v$Pt{-w+LNd&+;Q*7mLERkMA>A+Aj?&iqW1`Y}#* zf%$9AYxrzof+qW1dE=Pscb~CdhwjM}%K_s&Q(VHUF*L)3>P%L{f#kh<2C6@7&ufgC zHhNo%@nwC9y-60D33qe02rUhR65IO(z|mREP_wpQXp`a zP=7ag?XKQ$ck_g8{>zdIP+dQL2Of5UOI@PPVCsMXVjSqa>pN#}bb&ojobq07A8paR zE*}rc%lZi~!C@kIMR&Li+P^5Ch4M-)6>{&Y0|}>!)k2wt##}p}Z>qNIX& znQt4X%1)nXpnZJtxBEV3xcYct&SreKj%usKu9Q(=#hQTMW4@OtIckaq)HiU!P13MG zevoM&g+z(vD;mS!%C2Zy4&nUF7~i6pB46L4q7eAs*X#&5;DXLmw4b2AjnS(ltljYu zNe+18mH2g5yLuzo7>D|`HcdXqpy8W@%vH~-;Jawmp_?IX0K<8GR5;(>p-_p^Rqh@> z?VVe8F!yl)7B~Rxi3@>t6pPbNZi5MokYP`UhitQ2uUak7411F1MCwi+tv~+ZCF?3VYdd$#b8y zME%yQhrU?=4KDE?l(kLo)xqKQ$m@}kKUP}tuLY}W(&{`#kgn~Enl;}H8$N5M-~IYt z>`^>bKBhrE7l5dw%yjcWj@%SH;vZy2aFKMH;O~OL&Xkonfj`-(9(${V1JSjwnVIM7 z%LV0arXp`i<^22ONj*#x`U5-vc*D&)ni~$q`hH<%CB)MsEgY`lCgdiVXRg#WSTXx} zGD(HIv%k|l`u46Bsha(3&I7y{ls12es_!A|_}wsLXIc*8FX54$b@B-nXodY?W}=(m z^3La?hdS@8rjC&^`D4$AC&H_<^`#N>avhc>YqYB)YB&78HuA#ELOy#E+|)rV&p!*6 zBbo>EsF@KO)`1zrOy>Hl9gTlDYh^tbr$dqfr&`{(``-HNmCv`ce}mvgC9dl&mp807 z`A(l2hbdeUwf884(^_-gz@whyZ3;0Z5^GjROFMf=OYFY@wqSC_mY#HnqT86CQ>na8 zo3DDaJh2_39++p4R0Y3ss!Z+$GwJ0qp}+lK$S6w~>jjT}iZ8m=55W4ZscIeD9X8PRi!Tj3irUAHfhL!tidSq5!P?S~l&_%Rt@k?be*}6+70Eq=(4A6dn!L z6(tfy3?&&Q1ce6$2_+lFL)eu~R8SO2lrH)V!DNT^*BebkVen^B*6%o!Ih_WBSpx>0 z294j#-U40U5*P~0?Ni0WLl4A4;PK=4<-`-H#e;17%o)7weywSF&k9%vvRJuKa0 zyyJR{WNOt{!}Fk9y3>nX^A(tUaMt$XsU7Za-V{G+rpgHEo>hz}V8m>LIM9*oDv4arkPHm`=(|)p&Ip|=1{~vodKwZ7cckAZg4;MHZU@Gm zLo&tI0L+ysh8gtRb>#Hzza|(?6zsb58*6FWg(}Jt_>}K>`JTI|EfIrmG~?FvBexti z9_VRi9`6s^`VOu#tTA=kItH#M!9Xdqpf&bxay14Wt%kcH&fGTS)Ff||lqCL7RsCl3 zy(S;A*x(B<$Na(XF-PzitO%kwqJ`y0mwzsN5;xEa8mT0>^j}2mJdMux^$n>i^1u?; z+HG?$gdQYh_Vaa9BFhnRi5W^M$eYtQp*1b13Mh-mjGSqVKRR`kfNwL~J2_r&IZH;I zY@a-vWw{%B&lP+FCgbv^nXTDO1BVLYnAlX(E1A7&G(d-rGx*wE@!yETx2oB-*F1P8 z=;r&^KNNaRYnMEYW5xmb)!Z3w&8yFoS6(%u6ihnytEst;2C#ZUKPf{d2#*B>d|!mV z9qIVlq;??L7&JMappJOe=PT1|agVQ^c?fls-uHyjt7YbEEqjyTCzvpRvqy@Gwb~@_ z=J)Pe-0Jz8_S^ET{iuI8r{ zj!|Rvm>!UgGEGn0;j_`X@G7F(I~T^@ytxVao{frRYHQ-;>}YCe^RHDq zBP&!SRu*ZbQJ~D?cP}vKefA_O!wh~GKI5S5aVP~(hJQNCvpL<|6;}KX+O&4m4jx+Dlp?h{JLi?%Yis=fbLrY@^Mb;Uq#?(OOs)MjQ z15dI|StuHhY^WjessgsRq{<$^Q9TRA1^U`A0Z8i`4y>t*DQ+a-gl}(tC4=x_ z_v0(*;i}i4roD^DbT6Oy0LlOsyiBBQpJ&Dfy_&n(5`98xh@0-R=l)K^@ycEE!0P$T zr`Wsxn>k{hx<+RsyQ_p~@FDZoLnfff&XoK*4{{%JuhRrNxG%B;w$Fwf*`21}+;6tY z7?P=o8iLNePqt{rS8nPXAkRA&&k6%8!DoW??$?=X4?%MSS9XzSfJQ#sFBRaY9*1Xp zoi&HMqIkq0(TC#3mrSzf?L_$up6dI^l{0rK$=7W=5yt^91xCTj>jl2akKM%{Wi{)b6fR4{?FqE7`ryRG292nQQG$+}+fBUz#3RSm<--xB<3wL$(f?3&}jp zzXVA)yDq+fyho`5^O6es>YN0yx>sL(edbnPWIFKBg3haG9*ut*KPt6Kg9EptrlPJ- zze#Qmmg}`_2g*Qr1WC|@C?;Qh5&^r*Q$p(w=r6wq-o5!h;`QYGC7^PPMgZZ>n;)6% zmV+V~`oUWu#`bpVm%XE)`$d%adp%kv&hA@#Wp085b@eL>0>h=g8+_w3F(B%C*4q!?2Wv$(P_rV2B@3c@!nrU8_exIA0iNIt`DvpI+!b7EZH)_ynl?Gi0pwpbxu}$PcKbAD;t08 zn>d=<8tUy0Y}fWB9ijPUT9cNiOPOeBu0dL&WxIpt27ChcFRJuo10*CJ&k5P)f-H-r z=p0<(bxWcrt6xau1ylI(#`Qk>UmW-B3_UIHAR`a?F9nG+H*IfSc#xkTxRvNjiax~HYkue(g2Evq?$%hdr4kU45aXZRa-p0?^`vJWIqx>-I~JvO z#0M#y6wg$oW=L^^cxq zF6%}19O(KOS_z`jmM1p1)p7T47agc`YHcaks2{gq6qkTKHrA05q!U*bt;e4p{t&m6 zCuL~Q`^;?j&hD;L1tV)WZ}C-t_XN1sywBO^sM38;#-C2hZ@+s!uj#XLHi0%W5Gyj6 zar+0n)`}6;JECDo@-Ta;vy5BuNkYYj$27tzfN}#@KvT?i&6enjZElUrQNf=K$ifOi zySIhGZoO}E%*DmRwoTlPc2o0Jk0=2&&n9*)Z3lX> zl>~!&L^u35!iMUGO5*rpYlfP((}%obD=1%+yg8v-ych%QY`l{Z`e}}Cwq*KgX|0vh zgeKN>kruG7J5z`1afMhQzPEc9rN=+eEz+*?rw}J~DRG0$4ms&q|;BElZ2$4{>QagrnYI#N9ow zN82->)XWC=4V0b0)8!`}Z_2gT2o0h9>v27MsoV+t^z`I;opPbeUAZML^K{qT#ZoSW zk$fZ9m(=`HEn5_ndf-ed9NdmoRT_5eW_<8bVubqP%q;Q%BzxLjA|+m0)Sn_86!nr#D;rSWeK;@_x|F4P2dTF_(&XnMHMs!odu2kBtdCcZ1COXMKId)lrn zAtirP11Fw-re7mr7N$L4gQGAt-*R;+w*?0mDAJ-+vfi-_H*0 z+jNPO1fD{Vvqjk((qDTsPU5pq4XSEcYFt`%gp$WiYCoAPm8qrl^lf$u+hnbVHWaB5 z{_bruzfmetDKndK_B88}teO_&V6CPdp}Q#O(Dsk;&30|M+0F*(-{<(WxZ18Y*Hv2@ zUG;9S!l}0(M=B7lU4RfX=;o@W>%KF*i1@5It65|9%Mc#TDA6*SSQcrN*M?a#(Bf=q zxn#{OelJsDrd1P4j-PUDA`0f=9vLiP^rxWcA>@*mA00hjY|qwW>}AD9F3HxS zqqMB^Xg;3|vDLCkoODNzV;DQ!ah)WQAqmNpbNS83%g;YeQUu)82<@{i;fmWd$e*;D zv%=I=0|1U|Wi$&H!u5ZaDlwTz<*rzAf#Ri`riZF`2mhXb&s{A>b=#~bPT#n%ld2Bk zu;D~Nlz3I}*=k9Ym5QbO|L^@Gr26|`X9ueU zeyXPde305-`V4s9p`6~*g7Bd3u(Z^tl`ZLMH0XqnAcIEoU6w`+Wg3Ab@mNR>Cqb2s z^=SdF_x^P|92`BEppk{#Al6`6wY%0OGO&Eg9#5Q1~z;YirvEC7QfWYnNA~s{5?89vT^{*G<(dfO|N$N`$1Hy7Du_7W6`I=oCbo1F@AD+HVlU;1{89gZ$1Vd1hhHR{XGo7WejG`1_x zn}}-VP0;y^8XDY0eGhBsDyF%-0>g1~x3`bood0(km|Y3m_@5I8CkN?2GYcs< zEBAjK0kQ*O+yC?Pf9Cu5I*^-#6vz!E1@dr{a&fW3QvXvv7Z)cf7bnb+WrNL}oUo(p zq@0{w{~Y0frQ_xz1+x5O7Xx`<$NnjS6-dhUubi-b-2YVfKPMmytYkJ=8L;yHwd4Vk z{`*RR?Chj$T>sRb<-eBy>;baC&Hzi#0;>VnzjCqj{5#FRuaT37?f=M|IG5EAohCx8iDoloceNa-1P;fH_CgL^*8BCN&4X~3R_TLY?~3> zwd&`m;5RR^2wAcxI!$Ia-UOusk4ITI=_R;s!|9m^Z`4Nz=D5*nE5weF5s4Z~HL_G+ zG1*MB8t=^OL=SG0A2U4;=kuTEw969@ph))7Z!&1Kjo9Co_GxqsI|{T+8l;IaKJ~C5 zxz~^T5|gtKBAa>ZntdkPCiwIf#|}s4A{H9L*nv9hbT`sgWtPK2;+aXwUcmRn-styy>ts3= zg{B8c?gx*N*`M0%A3I$SJ8!FB59ZI(T1eP>kvA+a#5E|eH`M$K7mngL1kn|)%)3mV z(!Vq4p)r2|ZCDk#&EUVAN50^^Na%Q+mB?yGjF0X^Fe~BmbIzRzYR~N`Ehcm&lLv2m-def{3<={cIZ`#}B!Q}87QhG2} zH5nlfqGR;)Ct)-eG!d2;{b=N-L9!2IdtU_|1+hqRNg;57-+%JI43dkj;@o_G`vF<(NV?LCinl0AEWMR1e-!;f%NJG3MkD1i8Nf{K#fx zT%zok*7qMT6|$&aXlq$A;GClOx^=caSO6Y^-?lP^e_d$W<6OSe__2&sHfU4H^1{ks z()t01tU;9T(nurR8<>Xq`r(f++BRChjKRlCC5=#T7^(Udk->jwa0I{N2lgPnA(bPI zbH7l4Y9)aO=N4xrf(K)%m>~L}Fvy!D1L8{sLxD@r{=FXPAfbWi(_1-S+tXf}&HJ?mUYOczJ_*bt zX~!@JkHl2`ppU5tD2c8R0ppejmBdu|f}$$|K`|BnM==!=U__Y2Xf)(w$G7&+w%=Z` zmjk>ppLkz@Pbn8;_Q;ov;SjivukF~jVIA-dtS^k#qO1M)u8qV{v_|$9Y8bbgY_0EJ zXv2291%D0ht`~hB<@s)~!`z#BIoKPp^|fY!(qGa{QV{7Ipt;CAjIFN5**ykJ!^IsA zih9p?xrPt;0XO&$?>cdv(VY-#(|^|4GI+)YMqdp(3-&)3ynM9(0R80sQFw$X6j6y=UV-b3^$a3(C&z=lxz#;)SSP_~ji`;hf4lRd_Y#O#ep8J)x zCj=dtHKIR&0INWk4kQJv1$A~>1{PSVw1jNq&=k)GEQ}^#eQ*A)TO!7hY&v74`rDP! zzqaIWXWC@Bhj7cG0hEx%VEtFOZNn70i_)Sh=B}OPoKcm=7&jNtu)kDGl|9Iy23y{H ze84(k%`8gR+muu#V%~&ZSTH>-> zg;C|}h6T$1UR@?ahGkmo@b{U>Po7q;8Vv6iVWsV`pg_G zToP1VuOsSkD zQ>Wiq-&_5;?7uq-VtX=*`LKluKsA~6%Hj(kmD zc5^xtN|);xk48&Mar)o#0$)v74%8C@7W>8Ml}#hcIDQ$xu9I%yMRViJQn-VnDlhHy40kkV!Ax4 zQTh73b=Q_Q`82|k;-!q;8=k-G*TQU)xzp(c)YI!GkW67AwtB1IE45vhT(G`OD@OXM zQ$VmN;a4cE@iyfG3WTcD1fFCqd8fhp5e_%$DqyrJ|Qzb1DswQ4RTM2+kgQBrx?SB4+p#5naGbl9w6$YTNz~c*L73CM^Kq%Lr56K{n*|9&vpW}ppMn4%2 zpFfu$mgoeB#G$-prc{zC@H{>(z=7;hiJqIIsWU%}V7%*^3F=4_4NK2z{ zJRx#&pfX_&vn7S$!y{!yP*#$iGdzD(N%;_n3?DiY&B^*9MWLc0X;9>(-|8qDS_Ijl z48Us*0RsjMoE|hbHe$HVNd(B;+~g0aj$lg)rKM$~BQScDKew!?X4IHrP*yq|RbHB` z9~LQ$48x%9p`Q##hDRWPb7H(!@ekP)0|tInXliPL?CfCT?d5`k9#sW} zBl0k&L&Xo;76S%;OlWU!hft_6F>@U@3}zcm z0iZ5fUp=mDTy+R)GU>NEYQ~O(a9JVXwZ{CAO)+5L#|91v+q!|LC6a-w1OrCgavVM8 zWMCyf@B?HPYx2)@J>VPsO{%>7g2EwC6fPb%JThWrNhy?9RE~lZdd2~veo_N8P63z( zGiEkH^X#7h%!T=u z3g^SNcpT^_<3m!%odNYYfVDsybl`C-V0H4i_lM9r zSlAl2wujo==C^e5ZTMQ-0dJ;bRtvo+y0&%%x&%C%Q6^djZjsh?v?7OU@vQH)qgk2I zvN_n7+2zD-U6&)=RgXv1Z#bCAZ>>vP5}>>p-o8GbY`Q##&8p6wG6&%T;WCw@7rguK%ORMnL5^c7BHaD$#K{K~<7WwV02WN3JntZ~{Ca=&m(<4l7 zm?=zbs1)iON`y(cHk4Nh)iou;*qX7zxSF6ap(a}xUo%rU16Li_n(`8%q_kZqEiV(w z%Vr5>+PJY>P|iT-Lds(|+^TZJd6qH@!mtjW@GG|CJ9*H~KOwF1^I%*DlsB z(XP_2)~@ki?7zx?b>yl|mu%W{^|jYra?R#zwrKh<6nllX+P|99S1HyCZ8-@%O#)Am znth+`;{s3WPjax3z(V^%PH)@R#%c4&Ft1&b_qk>9>MR<5rTmULcm=u zJd;r;WMtM0nHkkWhPy=Y;BhXzU8g%kaNyeMCc68K2^E^H$PW@x_cr>ik2m_weT{wv zSMkwCzwlV2pWoH!=XN*xNl&969&YsSeZ0uux2MSem_GZ_pnvzSp#R~Xp#Sl`PpEtP z{-{3s*dMIBc0Xe6dAQeVe{|EMoW5(*E>7#I>6zBEu}9E)B3N9B#XUWL?uqxvW~EZF zsvM48JjW5>ri-MP#7UR45gKQW?{X2`HjUq|l!P0*+Gmcxf zP^VqwsySV9W79wp!W3)Pu2~cQ9(3__T~b{~TbC57T|;+lbjOCHg{`hjqh&2r8zx=u zx{fY4mcDb5HA#fm3{)mwh!LFkeXmLHWi4_zEagaU@rbxzxKN1l2k0Nn$3KX_6I&l^ zkImt4pue#VH^RLrk$v!|)L{Q8JkGZ3Ui6B2VEx} zcMY@N1;2*}U?=Q`y=dD4*P``5;r>C#t%S{RHQWN*;AN8hrsiJGO%iwT53q9k$r^GM zWI!?0V%%N>KZ8s0zMdvi&^8ur)6u#b*28sZ-33p7Pw*NGvmpvgF*lv_&+g2Sjc_F-N9W84{}Rk3UD;^vR^UgapQhi;xGfiHS)rN7Z{Bk z5p)C{`A7thI=V@OV{{9fXiCJ95E75CMbB>2(|Gh|Q?n>*3W2C#wOYAmAf;^_w$0}W_MDcdpJqrO+-_4h2x8X4&g$vDRQ_p z8aa%Yt1gXHCoV@9UI82t45OErr z#7`2ZnHG=*rj=x+X(L(Bt(P{MoF+o78w4Vcpc##3MS(YKR@NlHu*GZt>Toz*8i`ip z4ba9!NvMEx*qxQ79!WAuoX73-dA&Yfdw=^APi%jG#`QJTjSb_fZ=M=!e~!FOhT-z| zbLNIU8!w2xde?oigPSgRs*c|0^|5yDFuTvCdX>aWE)VZ<6_5hHz*XR>CpxdY>OIr> z>8@73)pb6s<2w0H*E+YyNd)V8K%6xK5d=$b{Htzy!?d7l^rkI-u+nD**ZJ&UhrfOA zcFq(kuc)Z3aF&;Gg#`tL<)vOHcNlMc>Mdi&HB25`bNvi#2v;3@ITm=%T=&!kn@HAu zcab3%Y~0geel8ZkJAVl>#oa?8h}41y-y+h_@1SgdD;0prh0g6nFQ(b$9<;T<~s`Ml~&Ld9m>kI( zq3N2W=~M{Fszlj3zFE^OlU*F=pG;&wC-=s`K?#g+#=>zuoz}}iFY9_Q^B%821>&U- zCY@f%>!mN@^#*Rs)pGs094hPALHpMwktii58+=;MDyZo%p~}Ji=*SUBBpM8|XI4>O zRuIaS*b^+JArk$M{R;}$9=9)fd0yXDzrJkVUlyC|53hWm2;ri_C5`7ET*L?W&+VSG z`;CojFVe@q6dHZtvF7W?k6Yic*9gG$QuC9+K0q%bCoImWV`woqYA z7;}teK~>Stm@wuT3#wbqQ_Iv^wNsU>X|(B9brRmIRWd1R0}*M?lupf}$eVaxt}#zD zb7oERYn;g{Fu$hbP;dN8-9`@+0%k>(rb&eK#t-Ti+PB0^4wFgdR(XCi$fr zR8;{Tza)-66)BBA70F7^l-M9AvD=aoeIJgpE)PeYRi&l)AgZdO5h@#JRU};9U*fE) zLa*Iy?-%;QBsw@=k`QxcQtBuTI*73IKwp0)x4-v5Y*GIna%U_`?mWa#I=-6Q*569! zsh7py5$+KW<9w|Wdi7FC^{Q3*r6Wr#8}i4GoKU%d%vPrdrUw@X&l_pW;D z*}TS;H}K|+;)pYpbi5FCJTrX=p&3x6X+P7NqY!>Q>9h;tGEbz zq!EFh8wo0Sd=EWQxoK2l$cWkx(Uv)??6?!brsGhK9noJBi5zxfKSYTeLb(Nn1zdTV zv$7(uw8ZQ4gbL6(!9-otizA|`4C|S@N|MJ-Eu|$D!acE9UR%*!H#^!KZ6V%WW2O%^ zXRR9Zw>Wra-FfbUYp1l#d9Jc%c<0#W>!xzt8CAp2t-1DY^4@!~cWNgz5$7NGk&>UT z+GtkyXqmAu-!Cl>m5<$hWpu-^fO|-hcc}lihs%qHZl}GRfwA-~^lB6O^3EY z80!jkzHgZHrDVZG`)nO8O_u6RoT8XzZZnQyZoGnG<~QRo<`zZiLTQbO+4@GeZh(w*fDs;GLbxlW3i*;x3M;H|MB(Q8~ynhSuv}@*W8bhCJ#!<7~LsaJ=Q{NGdJ6_ zF(DAamkq|N%C&O6 zoXaU@-dc>AH?rGgqn9$#SD|-_T9yqHGhpF2iriQ~z{pj}8n?smZF&C!;u*d5q2~WpCpo9+6uEcI| zU{?&!s-N}DE&WHxt~>9TJah80xxacKwli-?qYw$of< zP5gavqWB5srI+g~M4~E^+eOfq|6Rk2n~v|@j8q`0PdWw97weI8&VM8IRU(mkt8_pEN(?ba;X+18G5qUY$C zft^^LNcnK3y_L^`Qz$p@d&Y^AMuo~!abo41Nn@{`eMjtH3tN|UEL=d;yViepop!+& zTUR|)UpKXR!o){9u0Fclwk)m4=gOSdwt(b6-b-@Z+s+={@bNikHB4=M|HfP2t)E=K za3QLndi)ObcpGHHE4ru3+2CBlb*O?DUE7DQy$*;*z0*aTrFp>2yi;OBW^er6)F|3b z8wu2r2AuUPJ)b%0TEFIx_;t)k1Rv`uAM;M%sqxO76W+-NPiZaT~t(6{~`Jk_MLDm_MHW8dsrz4$u4mc9Uexo7%CC5oa4>qg#NlakztLh zsYFVo3kvH4#Z6crkYpc_R38x52PD}CB-sa8_y_9)T3~%ZmRY}}(CTP)N{_3s6N1=y z&%i3&dOXa(a{O~%qpP^TE7sA~|2L+L_ryC;8F_G2A7dhlB-yehmn~>3w>6M6Y%^?& zrNx#-wzamkSz2zn9wMQTmABh{WmYb`j5n`SNUl9su?JaW(EC||<~LY~=`Pxy;4%=H z|JVoo=j){Z9Q=;|98HdK$Mi95j61JDE6@wL4Ay}eOl}z-#hS>$SjOd~S5=tY6e~Sj z>Bt}Go59XOcfzIEH<5&sltvu56Fr;|qA;LBJ5Yb25@fL%9Mn)zNi%Wrjt#N5Ha`%1 z^Xzjv$?as>Mxy-M>0eh>_u$H-u>)iTky`gmiml?BR*Y(DZEYi=y<{=Db!@}OX=i5m zhsO5AK8?K<+f$H3mOqg2B5@4$BI4P>m&??F+^kn*yUfY5Xg2W}DHSf+om?`hp_U$& zUb0xuXP2qhA4=1YjIE;q}Txn?&na&vhWjd_*{8-;~$dT3jmZZ;4PJoKYUMSYr7)Tc>By{@Mx z9y@#B9DCqD^=UpLE%st!HCP^{5qj4VI)KthP7_$1{dyBaW;xXW_t9t1jM!FT8M?k^ z{_>1WE%qcw$VE^5#bMoL%PA@xvUUo;_%>|K3+UbFG4D3%m!~VU$$U)rlp)F}WwO$t zY*k)WOd2sOIV7DcB4MS9R4L__2GXF^S>}_)mep{+Z09(B3wfGwWGDT7Ze=HD!6fox zGm*IgaaeGiHEU`BMt}~&bbQ$r=*C1&xi52N_Qc$x?caHD%5hV!{6*4(zxba$I2tWU zr2ouys}dP$U*CrQG@%bCLmT>|X*4NawK^CiCg#til!)iW;{7{sK#zX;NdGz9uWpN( zFdO|Buk_ar#Fus0cOqQW!|?(}5|;A$*^>j4W+DP>puid^{J%w$j|QTNbaF&>;8c%} zk}Qaz28DIUXYS)Z6!#x}Yhdj98t>3bHtFZgHj`1@D6zu4l56Eximm32TqnO#>@-`N zmD%RG7QUTd%b$mj-^}rf!~rL;w+e)XOrva`An5c)1vXp2P$AR`vjl|6qVNRM0<#4* znH0d_aqy_F(spug$Vm=<`hvu?j>*B!w7}-TkijmV#zbIEjCwZB=C@6^@gj>OA{+Wd z*0myge?nxFLs4K4-(|A@pC>LKB@>rV2ND60xEU(%Nb-YA?ftEIb4SSoPQ zIau7$E|BOPoCi@yTMg0GI82fdq4R4(g5uZvVhh*B7Vjc9awXYBTq1w`SNxKr{pekL z`LP2}4$~S?$bueXr45#7r`6Ju)#Nvond(e4Ol_uBCeu2JXoQpe#3PkSwNjIGE@_oE zl1|BDC4$7wCCw7YnTU*&Izg5QXJRu&ypAdPgjivqZ@x}koJF(f7LJpdKMcGDL(kyY1LGw2o|Q{r4@^itFd_B81a`2Hd|*QI zfeA^;3&{s2EHi)mfkjUq1L%7IgVn!!RWv#<{Y*R+j9#Pu**|U?s zALig|;_ryBi$}l*A^1km6+pnQwK#p?lhUo}&YzH zCWWWWESRmy z*Yb&$@6WHv=W|#Rki$AThbbY)qcOc`ECOxa@iq~eU+Agm@}_!(VY3mZof4apF6j$b$$yGZ{8NKj~##Ky;why z-+Ysp#DezNo4viU18s}X?pU;l1a|e1;5q-z-gm%7b#48gHoY)24809aIt)!fK!z%c zNKx!4Z750^VGvP_V%MmN_&~oBx>xM7+W;)rfFh}8lpzv?Y+)9<5=$f z^4`1eyT9-EiwE|sbIz`7?fu`YY{rtxs`g^ii;VC1yyI%eWn@6c5UlmXEJCY5MR<@B zgKsx&qFa_W%Pm*g&&FsYa*85EDiI|TNgSi3v_x#<>ETJKdA6#tS){^4SKF~WkBMLi zp260W`Jq7u)0lTL-Q`ua>%5Np3unjIdD?g;c~VXyKy(o>Tcni$u1k24Ni6eM0`phG z&tC~Yen#6yCD3-N9LB?V#dqCT3B3_w2JP4;>;G zPWvdcpzvhZ82z|mpS?R~5wg%3o6>dNv$NZgIu@djTR%O!-p9kGePd9VegwyH!5bXM zT@3xiq#GGRb|d55(rq%enQnv0{x*ZObz~J)BsI!jAsc0CyzQ$9mB=}YGJyUIy8}*z z1qO*h>%m2T3^bx&r78 z5}b$D68|?U!*86X@RaQlpecj}6~?h{lR_|36Ij0E1)*WxrbuHrfc@C4-{0arv3<^N zeWRnZL)mfT5V>mlw#>XCtLGMnLVpN?C_8vDz|;J;EZ=j6x(4lC}rhD}AOA8!G6}S|56oia)Dt9q@lm``s zjk7m-m~`eqbI3G}x5S`S#fWi&lmfN0KjS9!^z@B#qoh)0lu+vF57O`l{PDMQ283}& zt7toBwX>SXJG?LrGFX{*gr~EF;E@flID-wWJ#&OjgeiiOj0ndH?@xf);J5`m3bXiR z)M#Txo2qK#B)~1p}`^kj+Zy{}5SIBp#9+2~7+kDbN(PC~KQR3VY#@90B$M|6-8A zDiCu^;FoRO+`sq2-0uWGv49U2JP2Y#oKKA)KBUFmZohe`{U++N=i_e$o}1AKwxGQ* z0FA(nxNUIq)_6N6(utDZbZ<$MZIgr8RYBXKDdED8$b~n8T(llmCh$WZd4DmeL5%<{ zjvFt`aK)Ga5Ri}jCtfPJ!+T%?Yi7YLf_Z~9ZHvbb8(fisfF60-Cuhz+-6OBZAy2>H z#Un;83i0ziMD<_&TW;@+UYYsV*U{sijBB19mzXU%s{~aNv?E&tdg!_Dg_mi zB58$e9Md3mkZDmP60KMyrnLx)1zMp{Bw$3OR3@avG$qC!FUp?kLUpBjPy&hANgN=K z6K9GAlvpHXu=c1R?1?T!SE2`zO9+W_g;*kV5(Wt4gqgzOLZPr6Sz3h=_!sHJVPS=Z z5NC2=k@A-12Ps;x93_-DiL_v3S&@R+_6~~tv13F>(FID>A^3ruN4_aI)2?f`p^WWI zsq55@_BB)?-jgajuQ6`S7}SqQ^5ky99ReLG1#TEOQoxZbWxSoFcHT^o1`9M8(hx`o zDf8Ti23IMI-0DKcNylnUZsUTbl-NH}uQsSDwOS{3-PL*2;DD>iaV1?{9g}oSc(NE- zuQqNZYNBl?w59d~d**ou&s|nx(O?%*yq}fg)o4r@AXrK{6K*m>2zs=};`d=^KSc95 zyNA_YfcOqiWFctRLe^xmlSqWTLug*KO|(aJQZyaP_KVvVoN$p)F)k7cc2cMqJNrUi zWY`Kzg>3=6B{DHCBLi=oOb#??g))2GhzwM78LU7V#F}KFMargH#T%?t?Vp}LQpc&< z`7IT1r&x8tm|Ymey73_DhC!x~z{+R$Wf2Gwz&drV^Z*F^> z3jD*y<}W^5V=O1#*MYHjdt8NJ`n$|3+F~nYpQSZIe1t4djJ zLUdv`LqA6cm7ni^8y~;0(D~IJZe)x=YwJ*4P*j|aXWDKw(_R;930p!(rW*pHsA!vb zJH0NAO1H_fGw23rDl`+s<6PxRiO?ZM&B#f2gHS3HYk5zWb{97{wfr2>vQt3_#1710 ztqZ7_oEzva4}}j5GME5LKF+xfgv}QxaP&AHB>%dXhgXsprF4;qAz;mhvsu5joOP~( zwkgIHNWg@aILonf3~&+~Z_gs+Z5|lg@N7nu5SIkR1bp1pd1nV8<_yp0Z7M9|r>ZJD z8$jS#ZHjvaHbvN{f+eb<+1S6DjibJvliFi_`S7{v^38j5w$vT^@!*sN`R`@sHuPNf zAr&+GyWTlDAzh+`+V-!K`*xh}`02>097@*`_i^k-Mk~A7M?3Z;bEIj?9Q$O)L1clliZm+6k_}2*l}ei=XFNTflW2*JH~2QZ z;gE#I2)N4g|EeJ$*gnVG0Tz@4C^}rM1eS~gFA3J!EoJ2aioO^m0Sit7ijf3(E5Z9B z7ywhftkx6okGr=HKovp+iragzrDcgdu`2@YK{1i^{bB2dHy_RHnM;QJQM5Z-+JEPO zmAkjEjT;>nkZC8)2#M&Cnfc8EQbWde^*fWAdH(d#i|&qgVQMt*tI@nq<>q}agKvVf z-ZfU2$@f6uYMgq-w1-9NvXFZ;gvD!$Ad$gI)%1>bDuZ z0S-Z9Ayih1Kp@CK$^qhS>}$Zd$DV5eVa#XOfD1^$1?1ua>%ax%hiPGfi;EcKA_lpL zK`vrP+?p7NJc4kGVrWqe4k$5{#m7KcT3CR~qjak0OF_@-Sv$WN2!8G-A+Z6vd3#Z6 z3~hjA>PT8`B{(K^Z{FsLBfsXQr)@7DI3sKC-ri$+EZ;n1QSREtj6P%(sh)eaPw!k` zKk|piom8ur^EXEipXz~;YDDKxOcB#UI3hbuXz=U81S#}PyduFpmB~_Mxer$4+K*5a zIgC+^byt!J9v(KXiFWvm)phoyicBU-vO(D32^*IuOnoO_S90Pd>I89b%t9w;LT3pa zR)Pp#^TY;Sk|*uK9+qK`(y#|@NZ=$1svGH|0V!xe3L21t2Be??u4`b8Xn^Y)D50Tn z1DdJ2=i&~$SxaZR-tR3RJj`d1t?701eDSGln_pVW|^{@XqTsr(v zO~)@CN64TD|M-0;xnR-at#ZnxVoA7OKP32se^+Gm+M$(_((&+q;KJ2grm(sitr5B- zj^Eu)s5lPUU=?6I#JXTwJA?5n*2ZZY1vFCjlk5nYvb$I

    0po^i*kC8L*N{l?q51iRWO4s^#0!*zKs_hkGU1`<6*~rG71z)_)f35nB*XnHFID zU{*AUi7J}tw-?^eZ54G#-vVn5GAJ$l|JYYKntIvFCg%6S%^+K3)+s(S)nv(0uJyVu z?79f}MK7^bgkpbNxvn)pQE@{Ctvs$T9gnwgAyV*y(#=P$Gjzf?l-ro{fh&)&wSM?A zzwPUKv`)uaog&pw9Aqi#dbNl3&&zoLXf~Ph&3_KLXzQ#YPFKvqeFCe^>k?yiTQ)3} zjzxEkNy)*nXqxd`YAF3n}?>5j-8cF|w;p&XfW#6=@{EX}sgrFKtEo1CCKLBz!CbC-&u3 z1`(~}T#5#-R@>+5AE`%S>pNV>l*(@hl{ZF1Q#EQ9)@Lh>Uf^gd%6nyg_NP6VRxfc9 zjV^CmT2wAEd@5@BIN!De+jzEf)RnG{YdAVb#i6Lp$i*O45XtKpEpR>S77HS|jOy-4 zSjC^_E6hB1SD5VcX5iLkKDCG7A^*`QAS6(|Pg)eR2%kD1+hdsUtoWFYmHea9Nve?O zu8riBB>ns)EO92!vg%=wXGZeT6PvsF$IP)*O;W&Yw))Zr7vY?VDgTvxZ=)QyaJ!<% zqE%EZZUy zAn_VelZ9#zqe1k&;BeVgaRsmyAJj@qcn52*VDq%j4}R-8-K|+)eLnIVq>m6Vw{B9} z_O=7V&q+oqx}C2{jK2pIh&&OPvt>hNzfC#580kDI)9w9yBx89R{oDSq#y33c_hWv3 zQ^CjO0vL)vLI1ZGecsQxTz__LNSwE9n?6c2Y{#Y|A4g6%KTaK5xC zpaw)b>beeVu2G=iY|ekrFPHhHEJN#Lm%@HePo^#IC%2klYJ`W}C{eVvi^jUwO2nmi zLN;QsHx;&$$QdUGO#{wry#~?9fyQ0^F@%CO(4(hr88zN_xKt`uTS%2Ff0D$MsKkH_jZ0H>z^tqs`Vo0Cpbn zQ}@@S{8!;csQC+4#i=__R=IZw}l&txfWI*_qB^{%h@!KZ4I)S4`UgLjFe-yRrG$#kV3UnmR>{_&A_qz zvCOSOq7Q><6It^ZJLSr(1iangYim}?f3d=O@=>4h+o;&~IQIxnO`2`5#5QBQt^4_C z*{8+@);Y{@-(~M&$0D$augX@3OCo$(xALfJYW6{6z;PKrxLURr)DGDfK-F%3zA3_M z5+HXS#d9+?o7Yr25sD^D5IcpybaS~Fr6`3oTFE$3y=7{aRv%kn;u_Yqm#WvyTX)PDT;*?x21jeT)5QQ zRN}e!&q=4!8?Ci05!!}mAfeE1%RAftmv0{YI?v)`RirQOH-Oh$l3NW8BgL@SNQVXzTy)FKH zV8@@`KLgzyf0jGMnN&0x?kqBvOFmo}{Wowu-yU5!p9ucgl4hqq%R@_u-V9mO|`|k%G*u&2gF-^HQSV&?}HRXjVHMDho zDMf0Z2Dz?Oj|rO;D;vfW4F*}tuXiPzhWHKa;#bOl*M3XV3>Zykg&jHHRb-%)Gb?e) z^&sh&xT1NCcd2<%)0+RjrzU*tczqXstmZFU&;CPTZVJDUzl?dF2z>f61#E?-zl`&v z(N=N^=AUU+x>b8Ee^&Ez`F)$?@!a?0W3z!dt|Q-LD=ry@8e!J+X3dhU?=$6d^cs8G zBxM$}9P`>-;$!u~%2g!`D-&x<;CzX!Fu4PLR zvwdxyll_w~}PyBZ|_`v zcfiB1>2*s$RPOWfG~rW0@+$gHdQ9^k^!8b3)vrY4q~<00F~np0rRTLrDDp+)x$2UW zdR%sF<+o@2m##csXw*6J?>|V$qVP#)i-V~-+23DRdIO8V3S#{AIbit6i#31fM>MQ< zm`Aq!#L(l(Fm3V-$*1hnPo@<0eGVd-q6qL7TkZ@;;uYv|f06xtx2q@$WQ!DIayeXr zeykl(COEU-ipkpZ6J2G9?&qz)ITP>lk>uhzyFnC8F0XuA7JydKmDkuT?;!7;V#Sa0 ztjx0^8*<_N6sN0{Pd3DqfPiNgl0@?xvOo8(1r56X-7Z6bIG2If@DpVt99htr#KBig zbpB}{_@Nt?z)Hj5X9rG#TucSA19?=^g2{jzv?yUD*h5&@RI%d8e0U*RxLlf70ZslWV_w;mHiP(a$y~;fe~E37U~M5_Z6zbM zoaQsqYHypZ;wl5j8FjB^Bt;b$QmHsgPrkZpp$F`9uuA49C z${bp*=62?mGyO9gMKlnVzoVrykHGE>+v7RrRY-X}t|+eCp;rpI=n7#?G6Kp$&cVhq72f76kXi=@B{0uljnSDm(#^H)Gam(*# zObT)usJrC7r%MWVbx#vgAX?%$(K;H=GMP|COdr?JQ9P004&bj7-b2AJy{ z5P}kYf*uq>@#7XPi_G|gvbiBpi*}%N*cu^Q)aee34GJHYmGB#qMO5jH0pDJ&=Otgn z+U!LoXJ&y;l#vULb2iJ zJrR2)bUu>rz9GNCXnmZ1>VI~Ik~(OxTG;sUXw!)aoK732dbmBQY? zi(e+VBLersievY+E*WKg6$L}$vcF9193Zg;1P)?yILtG$5m=0&J1K(&FEOq<81-}2 zjoaO^sXD0jdw?Cx`U6uuy#z6)zcqg)c*jPwZ(!Ny1vd2R&~5=Pna6#UTBT%+V>JVB z`i$8;zWt`n!7=`$>Cf=`)%%yzX9+S{eflKw3HBPK{Yo^3?^h;P6nIIFWl|-OMvbAhY&14%1`yvx%A}Q_7P&e((;IqAKPbu;Qc>>}cWC!Y~ zJsau_&!#JNlHFuCq{F|Vp4z{mUhr?y@YzfD#?jN{Y1CEf_Zz3^w}Bz&8L|)J=g4yq z?<@a=qd^&@4ziy-my%_F*&ijJC(p;xfpQ>97$gUww83&Pj)vrg$bXR>g8UcDp(y7P zc`0)KS%&Ep`4@Q^Qs7Y`&z15@oHbkyM?76#i#*rK>*!SNT_Jv}ycPLxleZydoE(Qd zcgZZIPmmK)!hJFu<-^~?l|CdNLcI^mN0CSW?^ud{3mGJwCZ9q)SHk+p7vwCQJ6p~} znJ>v#sfT<`ERBETXtL{kYp?Xkr z)l>DPM0lcU$lpu#LJ7T9ZyY^M!MedWh3BNsRA=I>zN#GIa$c|En5~lCM#JgR~>mjmVjyZX#QaRHIPZXq8E=)$Qs|)H_DqjT*Dm zcsgB8Q2(Se)jjH7I$KRt6H()R>VC*SNlik|2h;;N`k;Cc=?|%gAj!jOGHQ85J%W;_ zs3|BpN9CaQf2)7vtf^`$>V>aN-L$WaPx#8z9lr7mN>O<#kGiUvY9`X*HzNgpGqurv zGqr)=oJ}eE4QbR(d(ZfU_l%UO_EhR*KW0BhUG2y1#}S7ojnDsM?_A)kD9-->?3^To zBqZUSJ!j{H0O1xw0wIKe0l5hXA#xKD5fKp)X+%Us%8NiD1QH^blkDYO5*2Amtwl?z zr8KpaTI97zDMgH+DI%a!ic~2@q)Pt3c?cBi4bZp$|NrxO&wkEiGCMoFJ3I5t@0&cc zQzL(XJnQ8Tkq3I3@|2#Y9!gJBccrJPm(tTn*(>)Vd{w@R6sT$(3ssF{p{fx=RZ~}} z>Oqw5AM78D_z?dP>gXTpFQ9DyF#j;56#8$XJpXY2aO&Y7;U7Ug{UiP8(Mo?K&v-xj zum5)c?Ud!8;GckFC;BH+rvFa=ok*GFzYBRL`zIq0)HzDH*MBdK_D}OqgZ2|*I&1U- zR_95q#FH9TxKi6mth19?VGm=4UC0XC%L;oqE9{Z1ut&4P_OrrHWrbb93fsjBJBd|w z5{_1S`c^3BB-YBqSSuH@R`#-19?4qS&ssT^^>Itq$7!?-$~d2uaW*UCuB?pnSQ&R? zW!xRQcr{YiVCw;09DvHKz?Khnj4_f-Z27E`d$3CG%PP4)tKgkn3ajM7 ztdfU7B|nb4dIDQZdXj#C_ZR`Sc9^m^#x2=;nOr=8e#}KgE_$n`krQzkrg? zhmx)ZW&8?yx-0AHJm~4)P%dr5)(y1(65>0sb%VlUT@BN-NSlaXT5$C>-7<=*GI8lm#o)YK(D7!l-5RTgY&l4G7#@{Nx=)Pv(_0U zchR!J4_&n!#B-tPn?lofr&ujd%flIaXuS~c4V53SsTJ>%wSLh0iP`{d0L5uHXg45z zpf(V?e~>l^@xj_K#0#~X5Feq9MmR>h1>sojR^%TC5{L&0+=g<-YvWPQ?b@A)PXZMr zf(oW!?^LisJlNn~-03v!KB)ic;DqMd{on+T_JH;P`9KRZaqkama}a+7F(mW*EoJa4R#zIA(@hnHk231d%}3i$sw~eMGWIM%*pji0eW}+$Vg98^WO0B2}bP zjA$)dBTN_R2)l}|WQaVGhxqm4dc?I3d0D(nnd%L> z2=@u73Q;HOC`;@Y`{@?(u6P$I$HZT#lX@#IWx4R7cIth&)WOx%)f8czD~>w4nz@?M z%`UggO>JB$t`vkVTrIG-y{kR)D>}N{b-n9)YVBI!T0lcxi(SPCOI+m$f9d)q4P|yJ z03&SzFDW|mgN`;+OZP9pNUgv~&`<8)ft0#{ly*|C`xWp~H}KLfr2hfb)C<(K5AoOB zuOq%61eFhhIzR*5Z-ArJ8+Gdu|Fioo#NT$mO@rNs!BjVbsg8oF-T_%ffvljm-5-Ll zV%=xmXDPw`vHN4BoO7R}ME5^IT*)9VL5UuhCmQh>&{rbps|iRf77XSCgT)~p?`ej3 zB6v&(k0p`xB!kLQn92&7%K9^vc|0keROoxfX5E<0vOP0BGok(!i6t?EHS^5%%tc(0 z*l^DR&{s3i*CM1S)++QY_bdk$lzK`*1&X^;nW+kxrjnSZyi8LCOj9nVsS6yH#O##h zdD`Pi8&{UNhXQuWf;>-A=8VO>18<6%Sfh|(M&IXrk7OalT?sNTX0T0y&bf& zB9mTBCi!}Xo&o*cN$*6xnNRY-CtaY$vq35OOetwhDgE{CdUqPktTKdIB_FKP3;BD4 zSb8$Cv|?h(Wn$?CV(AB(>JM(Y0mL!{dxz>np|cA>EICXpc}y(%Of21*Sn`=zazHGj zQRdCymR8IyU6@;PnOk}>x8yRn+`!zDr{AUDg<}=H3}Jd{so$gDgA~Ouy_jKc)W4&D z2RV!MBHZ2m`u(`08Tw4*c~E~4`hJ!^3&+mZXCvhi{SkzUc6u=F3})JC$+VNEFV&ag zdKB~IGxK!OSLv$|SLBn;C&;-GT+{CeGPeVLK^gOOf9-ToS+bOV#pKqjR@ zOiELjlm;^?4PjEsV^ZqRq%??0=|(1{mLR1+;+hpBwbb|Ody(f=Fj5{f(hz2(f%*ac z0IsB7uLtqIrN0FVI;0<>rutF+DB|zw?;%u-bQ3dDynaGI0XjOV{|)Jijy(ED`bS7P ztDglgDNhXl|NUf`uDZxq-`QByKglYSXB2~blabbk`qDVf6j>!RS;Bm(f(bx39M^(iKg~%V;XzTi`7~35u!W z!BnGgJ)=QZ@!m1sF^DU^if6t`_TK7+ityg%y$#nh-a7&DiQb8b-wD==XVyvvYdws( zqO9gjSstb=ohj=krmPW6S)-V;BvV!k?@I4V9Hm&x!>l!eSxYvumIbnkXR>PHjd&x7 zS9z-_&HK3bNy=C6-A1TrDxYbphxd^85E&SpqbSDL%-0Ozt-f(cdCm7%y2E$Eh@)s@ zjxmQ~jD^NRgugd_PZ`D^j6YDi@kis2i0?J_(ir1a<5g;F)Eo8G-Z){L#NLmLvq=Bc z_!KD(MgyfQ1fdKmq@eb)xpY&e^hqDxB2#1vNGwBwJYGsZp@da1Z9989fXP=2Y?u#M7UNy1rGc^xG^8Z_#^7g#MoE< zLT;fU^0)H0NZ%p<8{tlg5gGKjhx*Gu$v=TS_sM-URMyLSq`xQMLwH)AMww^jSsExm zmLJm~c}||AAxx43m?YEuH~Md+eCEc!%#Hn-82d9ZrZF+*GcjiS@9^J&_(Tw69us2^ zCdS_WyZv_~KGi>!1~NBJVQw7azt4Xkwe(N-7vZRx{+X2Pf5`t34M?d=IY8+tZ>GG7 z@ST)nl$mlo1xk&?Vqdm z7}R04Pd9oEUGMmvPzWioNNwV0TL!+VE34pnl^k zInt0tM#BO1he6=h`%uZ+t;}x`GyV;eD8wU+I6Q zzoFeM#P{ONH3L0nW5d7d&-k~W-|o3jUjAgcU44zGa>FZMx%YG9;Fo+7&}V8d)wcTW z)-~{>O&({O+*}z5k^04adD#H%!3K zfA@ChCxKQe)I1k%ox>61dTnU&nXtFMxh%X|&Z^V9F&l~AtJ%6jC^4Hb##WzL5N`GO_hPS`CF1~SlzMyNxB{g(q$9=Q* ze8p>lecK;AU8VKZ`?bDOwKk4^>Phg}*Z7u>*7P-F;T2uOf9ySL|C4t5%I(Hgmiuq* z8Cv|}o^Snb{xyz#v-W(&pKswWQ(vu%zx1e$*L*^C&HcfHuQ&XuVSVFo7fa>$)P2QY zg0i5!zo%5jg^;dz)~;gShJ=QT6n5Yub1@N@Haz@gdI@>Ia0b))EBU+FXP(Yg+NdwH zo!^Y65-wP&&#tZWMW4pEDtzU<*{k@pxya&QGyWESF6|r5%KkP!-=L*m+xe;+|FZX= zK5wJ#YCNx?2iPBnL~D^onp*#wT=3MQ$-(+)f^Ghq!~h;x2I)`NTb9DjDKFF`fKs{cCE${yV8+wwO&V#T+q* zT8Zz9?^0_qUo50Fu|$+mdr=`ODMMJoqD-|GHf6DoPiL`KtfMZXM*M(s)QZ@Yt5(FO zJn<9p6S_|PT>PB!MXh*&dWo%KEA+C-qnBV$(qO`Wd9w#iqgH zHBm=5iv8jM4OOdS(=fFWtX{t-QB;Ds~=juSyT^(H=>3(m$x1MHr4|)&L z18SvXnx$4criau@$2425bW9Jcm5ynS_pC30=J|ZSmbA(@#P=PteGmH{qhI50Zmj)At!}LSPW@L_dqu5otnE^(8*6*i>c-k$wXU&Nr`9#r z_NUaPyso{g{@AKh7Wo>tNWmUrLZqQxMsd50<#s8!UB+^|jOTWl zfOgp(ZB+S>3DE;>Gzx9B7xMHL*JE!V(Fc3`qSbccR-4DIHj7(rPxdtHA#O+8?ZRz0 zkK1k*`=0e+-!qMU&onU|?byZbxCysojoWb(ZpU%ljyt0r&!?v1QSoS_mzpLPqGh|# zvX>xdu~>>cC1~Z*Xyp~?1C?m&$=uePb6fXtTQ}IBEs_1%{9+y2eGYrIbrnBA>u-V9 z|1?77;pRmj_%Xr_=m`e*gkv7YVp)v>_p$l zgANQzc>|ZBc@#t5n z>}8kjN<{BULGNmgPk_!v zWpS^|;$GK;dtDRsy7v*DMBnSoeNS-Ti$dQUL9xD(zL6B~8|52E3BKFV8)LXPcH!Qb zhu-)iW%+)GKG~CfCwll!pl52TXOc^;M@~(Q0%I7(sa46TvvHeI#GVxkNT|L_G3cwm zp%%tAV;gx@kELYv*zM#oUNT;ymc|a__lWN_b|TL!#w*yn+t^Ks?1SNFAB?WXtHx{C zd%!q=@D1Y)XS|2>W5zL*dBXS`Qa&(FQI7F<;v!_R{G^8Kt%HN~4Y#~#rty(u7$HJx{y@O0gdPkXo^iDDpL`-*&A`?C6X)q%D&VImS`xY$O2hFx*R5lQ8QU6 z3rVfKKAgJA5po2zk|X8K`o)3*nE`d-JLzCGE~ zH=jLyd$6bPb?oWelRbU&+0!?VJ$<{fr*Aj*^vz*U-(2?e?aH3M-PqGNhdq6B+0(Z( zd-`@^Pv6e$>Dz@peLJ(KZx{CBZTcA-t*mpC%dB$~w#%_>m!li)vhwSVWh>lX3={)l z3zRL+7DL5QSPf;7+p>w}yF0S&P8aLNdW=}gYByuQ}J2@B=$?z*;02DFTqBqu#HY-8=Wa$fptz1yTxvj;*Vkv;>uE|ioIel;>v?LmF;y) zw%4uLUUy=9og&^4Z(wYA6IMHgt+rnrgw;+FZ^LS*z-k{ws63ui*><-O@55@Ru%&J# z{x1HG_{Xr<$>LM-DMDqnGhC<&N@lOqX^1zNKyL_WgqKa>|@>3dx!T9a`6nICOku^3H!Ijdf)NBLv3N}k5i2Ig!cqOW#!v>KlFZx z{HMLA5q{+Th+OOmC)g8CuqRxs?{44S6zwbW!Cte)Z|_^_GpRM({cPWleLto&-v-~$ zsf};5Z!_Y*@YN!&#)Gyz9`yFTsFFl>T^pY^7x?szi$R-Gtl~?vX9`RhQJVL}_gjk4lH7ayquf#;T zTrNk7@=a{czKI_8O-y8;JU9E~x!ET#m3{J3*(a|#`{XrmF|x%-N+vC8d(=+SK>Ths zsiz_!#+#|;aC5F%XN|=-VC^#VtmAfyoonXVm3EE2(L8CtV74^}m}AVjRx7K(nvGnw z$T8R+ZRVM&p|ntbXmn_Ls4}!U^kV2xI4ayWoE>^0Toj%gw!&NSJsdt4iHW!)nUS1u zU3dZVrG;{F%uF0}DjXM14G##92@eUc4R5%RGdwpmCtMd9=xld(I(wWtr`|d29CJ=v z0q0zhf(g$0pxgA>hn&Mfsm>pn7MacGKNMMn^Jhm&)Y&5eoI5+bHB=Lc39pZ=Ly1pE zo-fNewRVS)WRh)}TU9Rvd#Wc9?Bi?* z<^*Gc?Srk%4W`c;XgTKLa3ox2=i`1BU@NvV&8gNpE5_Pym0LUP&UU`N8Bc4ix!o3K znJUFxi{Iy3^Xx+Fs5uEw?Pc?vxxi{~9Yn5^7TIZLhTYG8!48?T%sp0ueJC_IR2VJ` zO$m()%?Zs9Ee<^qULUGK-R=(U3;V*!;ojj{{A4$UcZN?#daByR);`iIoQhldCthpo`^(E0G5&@_7J4_Fhoh#1UI?AV^{wV>Zg1sS({OLq)^VrE&b4RS%k9PX z6XqDSj7-#4rgIYK9UYp2=RXF|eMZEI)JDo9+afz6)lOYxa%5iQ`QWrrti7)~wb~aP zX`(d*6N0mYf#AB}^TDUhF;1ObX`T%34eoMQ2ag8_Vw-_h;sht7Rg|MItTxA3)o4Wn zg8Q9FaBOfwa8a;2xGlKDTpOHMrB_FV4@dS#_D0S`KB-Ep>Rgpum0vZnYG&2qsyU(2 zRh8=LRlQvGOw}7zhpNt3&99mtj;czp8XP(ldbes{q&$?1I!9ZG?1~&jkEsiv4sWOu zRk2m0tEN<~tf~psR27=p=zYD-@n*4Up?z#Jw^;4XdM6qE*X^{mW>^!f5@)zG$$rC5 zv~}wP>k~WHJd73)YfrSFaUyu;FQQKu*{96)j?YQ8MmoLi>Gl*$TGD>kK4A_q+uCRC z^G=i#XKuCnIN45~Gr$>w{xaS=V?Ad*Z9Q*obJoHd1k5eYEN8B>z$vz#v*$Rg9n0M6 zthe`}hBi56&QxcMv(=t&O|}-{s51~j>Gn8#w|UGw?PORzt-Vf}z0y3)7HAh+pGtcp zTb(>urnR`c2%dhPxik1&I2(3L^IQ+|ui?3)M9Z*g{8kmgvqoLChN>xw*3%DR<(|c+ z(-v%Ao^|QtIhF>!hE38zY<_-=NeaD(tp)uRTPnSettEYgtrgFBY^}v;ttn0Gq-9cX zEl10tK0LRuuXeq5J@w;xgZ+7y-~eruHi`ynH)~^Pkam}L7v0G32pOVH*QV1@ZH6|3 z3beV}TpFeoYb8{uJ*z!SBUtN>6a&Nn8pVotG%Mblp?C{uj3^X0(OBr-u{2I#qe-lI z?}CDTlqNH)&w~zph5id4s&&g6s@|VD!t%p4ehke)yCC^ehclCPA|GLTv_x|baZqK{XRN2I+k9Ej*E_?UC{~A3G|2P z=FuM79qo%Y=ua^xV@}em@d@z>w9oyj`&D|){igd(s`F?bjb8UO^)#jZ9^K=m1D+I5 zOL~*%CcVY8k`Cz^dMA3D-;{7f&(XWlJ9>BhI(m=ae{h255xuV$>VC$Z zKHxb&AL>Q={d7uyK%Y&g^*Q=?>6|`apHJuYpXooN2K`U^UXAqE^iMUR=AUR;YW|7V zS%8N=3dT9F2G1=^Nfy0NA16i`;Ggx`A|^t+N01*eyv0iuT}vq)J`)& zxgE7ID7TXqkzHgLtqPi_n^vvJR(l*w+edps_Lp~SPeE@?*S5?1<^9@D`G9;tdqqAZ zAJTToN8}^g9~5(HyFr^F?T@lbR%yrOld?wpt6VSFYahy|<+Iu;xlwM^&dQ(3=e3XJ zX8B9)pYqr8*II-8jeJp%+%8`dQEJw^h>?5b9??YB$ulBWo|XR;o#dx}m*}GACx|@% zQ2!{=!+*2?X3@`oi~ly!U(G-egZ-2IQ^bw_ss8Dr&|l<#K#cM~=zmb$;(y3LTa0Zn zqQywGZcU5H&V7vY#iIOaKXm(|%CXH7xwmDXbGkTs=zo}F#?wl-s5wOyvl zw%6M0&0Thpm1b{8*)z>~s^pbzQT7wodGjbruD6e&+>`ccd$qm6-nx48spX}2xF4t7ahz>I;q0{zIY)86Jx->1#%blWw+@+~Snt{cRynKZte#`-HUnn4U2Lv1 ztIg+D?J%D=_gW{+{ivBlOSdLkqfs*(Q7?zAQ+Ax))}E`bf0ct0hTBu^S>|!`1ABp4 zYi`5!O}AdKUbJGZH~9B{R{mpy&4boH>n!ST7w)avT5jiADLCH{a~^7Prk!l3+LLg1 zl~%}(vVB&%H3xauS-Iv8tHyd6;b7Bo+)jcc?VWa=ZP}abb9Thu;*c{VnD0F8^l^GR zV^LExoJGz&rvxR8wud|GaMbhmBfwL=^7}Qad)lRil=uCU;kTcCn3&uJDb5U@x zdE7d*DgmW@;+zSN4vupsIFrk_S#zvdXNS2Tb@>EF2F=sS(}n+sF%)A%OT;=-2CRNI zwp7Z&){=T*YeoIBwWb0ZMrkwxTU#1Mqp2O;iLC>_3oMR*`k-|O@;jT3h+Gq6g7p_Yd4Z zpe62S-Oo}n&zxQAF+6@M@f3L~sMPbg=W+UpXRT)~ZS;KKQ$s)XtoJ-kKjZiNZ1!yN z)Y32bJwCOb7d^Y_SDquDBlNOv=&kAZ`g#34?NzT|qgTDcD`=k=A9~Fzy^`v{%?Igq z@7vzP^d=MVpMBkY-RK~*?_12Hhm1AG8hV>~^sw<8;{Y8o-ZT!;dEMx2~tZ_yPt!azlErx48Sb`y9BwGTX@*ycXq`sO_9%oKkQ3Fj- zR$6aPDyt0)4-}a@OY7Ivn8z$P^r5?adqpM`bAdG$I%slPduv*GT$y`CP5F4};`M>y z=JwM1$HtaJD_N~d538K!vGVa&A8WQMd(D*6(`K19t*q8^Bk#^K;*!^)++-`Ka@A5 ztL-`Vd~A#D<)!By%Yg#gZtb=6%i5P7R`&zdmSXoa_t>K?w>_~mV!4-3DXlLbP(Ivp zm)1k2jl@+Gw10v*)hsGKx1xscZmwDMSWnAsu2wZ;MgrOD`pv^!FYPT_vAguJx_)b5 zd2ejnQ7_r1Wz8tvVlFW2%zE>rd3wb@oI(A*#oT1|#684VnNZwi<#8)&aOQ!_=bMwP zf-*A80!1sPmv1j?Usk)~{EG8dPWg7IwLR7}Jev(>G3x9b)ah1rWomY)0$j*t~|;*y5c-EuD)WiJ=k1ri*ise2~QS!|Nr*=|F8Of z%+w}zs_$!?=sQGoc0V^(Z8dzuC2 zNOJ;|MoDRH+33>R<$2I(wWU4H7;_sG%DUxaaLn4$YJ~M>Ch`w7CsZCQO(?B}Cc2Qb zWZ6D*U0LDk%F4FORt5r<85PqP?_9RBcwA-nnz+Eh5?PtIV&@vGq}3`uzqz9ZDo&yF)YbWm zPZp0WUWvPRm*n7%LlrYC%Zk%0DwhN*S1;aKX;nrl+b*G1Jp&V#4PF*nvaY1Ocrebq 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 z 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 G`TqstXdS8m diff --git a/docs/mqtt-v5.0.pdf b/docs/mqtt-v5.0.pdf deleted file mode 100644 index c3d0c97258e0c619fc4d44a0b80250f7def82b96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3215041 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

    tM#Au8I*~?lA@q*{f6IFw*Pw0_1D!&S&st zTaK-Ii|{%}wuV1bWNlhQIo&oR9j}^t>%*S%UmnwyCp9=$2TF2^+jk2-Lh#Duab6bP zjBRD@8>T+5PX+B_R?XjW!`Rz0`|XF*#EopV{Uv_KKUO zK0j=-0Mj_C+v`c#bQ%p7`$XvMpKIjxvMN2(PWa32gDsRz!@$By4l7c2%h@gTX~+^O z(Ra%~lb@&$#me&1?q;-uwE@T_6>QIoe}*=er8dNJh#3Y}_U>gd|3J%K2d46s;SEUN zlu^Q*7fs-&`}zSd0t#~WBEQLmMq<;=F!rdNAL15$8L3`#qmxMe+xbxENqV5i;f?)G z9zi+7F3WBEm3cMHhYyftPYsT-XfU>t^+IhXHBrk5_?K;a1`&Ue>6IH)El|JW+C zG^1+fOq;J#boWZe@;=FIHeQQ)S)=< zqh&uAdkeJl#e~7k81I-9aL_Sjp_Gyz*fzuAI)OOW=2e&#)k=0xyu~+8Y8d+10D*66 z(`MHi`9O$QPR!=h4u<39$D@CjI72HGkN)0Qkh;d)t^KAY3G~Shqda8<>*HWHDJbDyVc0DhA%k-Ix`$aB5`4-!WS6&1%H!nlB$L%mBLpDfzEeiMV6s z0k@h(m-$i!3YNtn!NkH^ZZ~5Gx^#99qxTFCQ!Ib9`*^o)EpW@BT5uZt{T>PDG3|{b zJ&tQ?YVp5|%al7?;tB$Z$=iCUKxaGgy?8TxCf&Ri(!E3E>pKQA)xatrh{ zT=hKUfqqg(TyC8m@aC5~=`;{hA9U%}Sz*^yOI5O4Ox)PrDZV^K4Ae?wwqM=7oZn%o z+sYbyiVbp6pdTr+(UeZ2mqg?&(RzV2@3Mw;Y$-!{g&K^1cU^c?JMn;gH>gxKOs~{WrQJd(n4*@_MN92-nVk@L~N5aJBR-v8@WAulh)5LkNo7@twYy)O)wM_ zx1iv;Zqpr**VvLW=DFBG$UW;4fns}#IM65ul?CC?l3R`Ldju<1np$ZUFCe%4rwMM5 z*v#;k92yl5Ct3lYJ)z~BFvC*f_?pP%I_heuy~^I-ackSea>rDcc4cA69NE%M-@WI- zwWKdTqdI27Jo4ImI#*_)sc%s$NZAD6TD3B(rS%IC=(DoC?3Zv@F<~Ob#eK8J#HYFC z`}{$#$UT|X6}Vp=P$v2v{Ma97?;h8IFoH$m-29O3c!*YVm>V;GV)5R7GS_4IUWFYe zX${csn@vhqeX6cZP*LDEPE)Y+oQ=UlvvRK54v=r|6)@z?cj&IQYHo zoB`4b7AO1 zPZGH<1NqGaf8p)t>4wc4US5pe>$*Y7_c!D;jKQl3L!ApYs`$vL)yx{fEbK(Gs;jP7 zw)u9Tq0A|A^K5KjB0}u8KngibgKa=rudwhQ=tr{6G!8nrLRaSCkoNfdd|kAGRvkNV z-|sIslhmf&l{ECWk`x}QF^ArVZ%Zobv9faX{Spl|yi*$!EoNp#4IG}u)92zpHl&AF z$o*q`Jdom35EA&^^1TjyBIJDI_ZT6MNB?{Lp7TH245@lKnlWlB7+Zfk3^D#sY+cIE z$ij?K!^+gvl8A$gQPRxH!qSz9nT1ir!Pdc9#nH&bj8W9g-O9vFSwfgm*vi#K(ac%I z!Oqda-pt;Wh>KCm-qp<6(ZSZp)r`p8$kxS-QAN(k#fIn`EB_bR|L-%6S^iU=@zo$R zFc1*XP~oG39UNgH5RfMhbRrP&zv(1DMOZz2!I0+>%-2MLJ;nt&ne(75a98E*9T}0kGxU!1t7A9d>#SB zJMRDmVFP>=HL!p|^Pd$x?}(fs6L0$fz(tz!CN>t~*K=5;-Alj;?KIr&V?+n`O90?E z(7o>0M;`=7$;nR5i6iSk@kJ0^NPfU?Ph#U5pD=^LPZ0HoYf{0?J+JvT4C7JXH4{fx zKE$=vz!|O^Kn|s9Y+MlZ2wd4az=xPlodO~fyA}=(iZ!qpi8XjpAMg}*Cd@4Iz6T)H zb=0S4lTDfi0*SgBmZn3P(YuSmPQh52P@l{xzIKMW)1P%ND+sZc4+qt06hfv zqyT`Y|N4XkwHvJgi2TfSdJJHq;|sL%GXq8h4RJPMmW*udQ9?eAKNpu;5{E)E8Mj)7 zkm6<&7oL{1V*^R%j?*TJ_cI{_Diqx>`Zx3LzvgDe&iZG{kEo6J)e zbNKQ0NSk7M(=R2Rf&uQ!+is;qep0Y!67Z@@!I{utU^W5YTQk(%-}5cwM3)61m~-{ivObg6pm=ba;kCEr?iiQOh?!GJ48?k(7}H!++g$bl)8}g0%>~mSsB642#4&RMa|)&vy3z6d$8>5lql?O1_G?xIp#%%% zV%=oRv{;vgwd*-u*b(uSmd#`-e znjQ58zV8kW(716Ba=AKtB*hZb#yiQm$&l%|Q_v-z*gS4FII&orTYZ4fX$`2f2pHi# zkXzj=SJk>e+&Hei8~r$(%0P?Rzs#C#&JhOZygl} zI9<5)I-kNsqU|tG%Q*e0m8fvEqL{q8+BB_&xHei@gw_5lC0UQ^hHMeCq-;Gpc)(NB8uXOD&q`*|06iwIQxfUG-y z>`~wP8>6aNMz4gULFMqIU3`f6EkBtSR)^|sR~uI7{(Qe(?k1MITs1sHAP5W`A;Z9o zdGmF@Z>}|@3b4wFl}t-tNtCq4|74!YQ{dn{pzUuFhy~B% zbFsX;fy5epq)xAjc+EwdO0{GJr(#l9qx78O^X%4!z<>FLs}u~9 zWw3^H-(9=p4je*eDuTM$jkmKClxxM%ADl01N7Zp!qi}QzjbNP0wq+voaQ_7Z$$Qqg zZ?nrPU!8A zvkO)N)YD^~es~EyH#PBfj}Wk`Jxv4saW*lyj^{xTgb?TdIzJhthVF`YT-+HV=NMMf zC*|nVLk|3)9p@lbu!$dd9OyelyROEB)BF z@xLq8?$^uGE9?9Z=k8Y*3Er;NEs1Bg!NV~3ZBp58i{~5X$$A=sJ|rSC-;u}Ykg?U2 z-@|%=O+kYXW8%{wF=l3UMaE;h2}!Y^-y~2l$b^M-fx=M^9-VqgkQFI|Hlw@%?F7C` z1^)Gr#w>23gFSQTt)YsYfcwYQZ2FT~T2L>LnTHfup21lR+;elF9Cm&+(|3L0mkk?e zoeBUN)kmZ`D1NKDnMQE--jMe%)u*4ij9s@8jP$J3Ce5PI0qug;LVZ*b^oRCM8}}Us zL#1)>Xz(XBe+myNliD=k#l3quue*!nrT9lZ z_AdrOdg|=gHgQN$BM9+$ZA#mZd`B@5!fP~qIGIMTTmoo{Gcr!XpowqtQhnlf{ zUHV&LJV%5CRM8!^)wmaIH|=y3rxBrGcDj#7*K*uYth^*4$7+bZWe{6^z$dYXX)2o9~lJ{pzw7^>~~mWNAgV4Y$~S4zZ(9hg(HB1=eDki22wyy9;O3pqow z5*~JRs{C8hWq!cZSB1-@(I+7Wk6=$2{{Hpp#fN|fTpJ762b3;t*BR0>dYPyMt0$fE zPZ^LrijNE@!z^-UdjYqm_aIYTJ6)`2CHa*X}3{2@&&{~qO{yg|I)?Ws6yR?+QM*1K@L zZAd{e>j7o$i@U@`0o-|NMECxSOHYRMGB#BeJrAAm%TlxM@2(bSPG(b}rmg`ia>^|n zW@?*e;wLd(5P>?GNFUf!I8`E6`i}(oL~na?4sVGE2=*8~m{8*8Rbt+lZRQ1;x$qJQ z5RL2wZqE^a$p}F3J4C;6~ua_>jc z6b;|lU9gC|r5pl`m(933BmX`fR+~laGhsBE-?_Au1KKSphPiTSNx#p?X>1##8d6F9 zUeuuH>(APD_Q~4^kk|!mXhn2*x5Iy;vZJNe(BJoC2QB_>t%ehL^RNS)3AW>u6@$AhQNqMh;(by$=HUqLyYeNh zq?>N~n1FL+!0svu9ODnb+L1wB_W|St_*N4C9VHAMFA!Ntdc&>0<3YqodC@SmpK~6G z=?4|)XFA$*6vG#8k6KuJ7igb4S~Xh?0bFHYV(}dTJ(*f;7SrGxEcD0rcyRBjGDu7X z0{cixOSk2mU%5IMg1yPb(f8U1W%&_l>Sa~qau89t z`V9&gKt-#IZ+xfE7&R2foN4_T9LC95t3rxE4*nyl*IapSMaS9cEB7erkgT?Sb|6HE zmu?V(2Igv=OZWEl#VPADn0m~g(Q`-PyTXhr_PQfKL#Qy{mi~EF;%aQ8->*s_uAJ}Ic`z2R;99Pr z8ar!QtV(-VfoYgoIlarlQhrZsYi#ZwBPkSq$fw{yE zdt*YY;=N4@JZ#;YV$6d3F*jBr^h-K^0lUt0Wqy6Xe`4ihP+}wMxyTixKc|)Zj^8*GE6#H?~-ZYg%wq4;DPfnYB`~4DmHMz zxUNT&=Kpq4L6o058X%S6)SD==ag~|Kv*uRxSU;?7y$7?4+JDKeZ)xi)@_WJ0K*O*L zLfC{OY%{8zQrbb8p+m&b2;>fum{Gyw7VZG!xqM|RS4|)zh^rJ!HXdum1Xtk^v7!i) z>OJ6_Rm_$uY=*4ouCy?7Xq!`E=r1hIul(8eST{dJjMrEpnYOuEZWZHbR0a8*5k0Is z6+au+RTU*=WNNJnJ4&h1u)2JRfGcv1H>YLdX_U+n)x2J1?*9_0bL90BWa!tzi>O!R z)$Kzp0W9a!Ep&7rKyB)E*zaoT|GWJ2Cn(!=J4r4htA{os`2&Y;)B{oC!K&LHG-ce8ol`o-MDPz&`wNE<`sfP9CE|o%~Y)o~f>| zKdCDRS;%M{-odP_+1iA}?`P{EzVh{ZKv z(pj~fWkpUqpn*RiQWa-lR{tKi&*|@(uoC59q9JR z{F`Wc(ti&M`NmWb)pEI{Y+w|K_}pPTp&|*#!^~HLHtQ5S@_1pbuk{A?IspJD2!^aDvBUO^Wrll11+U8D7bVb#`F0Jehh1q|#y$Qa{lE^Q0> z7G;N9r5=6P@EIQT&BTS;sk&hZvTd`iCB+&I#TMwDeMYW1P9To@Aq1Nqf_?8RB{+FR zcaVa{(jQF;>uT*Q^w666@^W`2pcM_Kt{C_2mzPJeoTVRPp&6VpeahIvBBJ$(cjdY5 zn*6t4?ON{LM>qNfXG$TwS!;*Y{02L%JchY^n*^+b-~!q4dspjv#d>LFtr?;vzaO{N z)T~8$n12V~RbKd)kji1Xiu_TpV6gh6fX>U^=p!rD^SJK;Z>F-5aTO)CQ=%V)AzA*7 zadSwDcw>#gCmYWhCDi1&CJ@*;)7*pon5s!_kMrk#Ql_?FP1Mv|mJG_3fIvGg!DzZA z0uF|*`A#TlR$u{Ugo6Jb81+K*ymb57&Q1D$L4m-h09KU|tJXOo6IK3#dI*?Um$Ow= zYbWyicov&U1NYAo!D%cOG$t<3IPiUl2U5FOlR&U_rkH(!%i-)%q>wKx&yPYq?J=d0 zHx%MWrJUzh^AHT$gzkFBrye+~#xG%2wp*F({yX`{1a~m1ab(YA2-dZe`F#u+iPOkr z^N^^9P4Zqzjpo(;H~vfK3LmTp`5o1@PD*i>+AqpR7`A`|qL7~L9FYWgZhCJZ6?84? z#;PooThAAb!XNgCHBChnFrpI8PNZ~0LqBhEPG5k`o2iuJIHRW@Ah9U3yub(b5hHC0c7cEO{MFCx7dUNY) z&jaxonL*T3$YjKw2>d*lk@irp#(jfL!{hYNdDU5cZN@50+B(&RAV#x~ztbBV$3FO7 z(+Xxsb&>gMBp4CAEAB4m=X5|?`1XWQ!s+f*pRSlts=>wfIHfXFZS2qB#z2O4F$$bf zSL>E{Xsv-C;oPKhRCG0?bL`A+u~|XzXFlj&2wFuYs6tNd+B%CW;BtL4JJ~;EE2;9n zY85Q72VdYhWLf+#ogYpfu^L;|e6W40-&QF(0+Sw<#13Q9PB)@cgQW~1BCI8Oh-m(QqUl^n*?n$p&%_Tggl=+n1U zG$Fc(wX*-2?f{U~Vc83c+D)E4xGQu>RlW%XW;HoTyss~ncZS{a^XHetda5pO*o@Z{ z%Y=5S8HlLmb=i85q?5}^G0VXG-^`ow3Lwq+Zh8_&7FI>K_~;%?9j z77&&o6_0Ran&c+8(8DETOd6J9ZLOqONqa;2w=XXN9f7H~YQL;QOBu}9vz%o_A|cq0 zrC6Y9V_?{XwwFfr^m}Hv1(AcnK7oT_m9e4}(nKD3fxZXuk$1fO8-({i3hw_uG7Fqc z|DSRu|47j&GyZ#C;Xk9wSA)Rt|Kd!Z73>fp(SU#uA)x>KTh8P^3f=r4_U!);&V-BU zKXWG123>4f-<-)ENA_T*H^0p>IMbk+=&Ece(?Y^(vYC!?-#&OC9{|CBO*un1<)7uHw|zVT_pe)>j8ii62$}rybzef>`az4!O30& zx&yt=1p#^>Imvg=7x*32o!zilSnGfWV3tQ-z}9g{f-$1yP|2+L1RzfgAhDrJ zntt|2kDyKP0uW)CciF=l-vN}2M0xjsqN>4Gz!LT{rmRtL&d{IsA307lfd1ZZQY9LW z9G0&!Gr5x}bnx>UF#Lmja1&DK?6?UlWDCGa=ctlN0Ps+H|0IOl#B%>&95kN116;vv z5KFHzJDSyM>Wxng5bbCJyUX~OK+MyR#7SqzAr%fGZCJu5FW5~7XQIqQIqO)Kz?wjd zN`V3K9N(M%o9O#5Nh7hbv3ytj{7-2lHg?wUYM_5t|MOpvM&kUB(n$Z48I`rNw_#NN z=1O&lSUJ9Lz(w>w?#jl(#=y-@^#AIvEL{Jh75|yvX7;8I#@6sKoZtV*MD(w(e0={m zAEL+FD8;4J zt9-%r{>|xH5UAj^WOu)pYWlx6kf? zFLJH^fafvV{^zCFcMrg$N1kf+WXk=;^R;X85P*leWzyxv&0JNV;so)gH6Ry#V&?br z_2OZ;e%juO;CBDJTM`{0Ef?_o?hz0I>reb*uueIeO5?^z`cZw|2(`ghVQ`H^!Dk>o zm!2gZld<5l@~ruwn_f*<5U5}Mt;)0rQJ*qwqLigUZ>Z^Sg8p&OGJv@oD-5y997CLP zR2@rn|C4WVWIv&-9%JJuDI_9kywPz=7#$n-SrSe@jRpx3VYc1uyUDgFu|pNEQa;c5 zeyMy3g`6LTGAJhakKR{T#|I6h;)!$y+yH4uHTZYgQT35wwyfGENc<`WhWtby(C9p()|UqABjSXOEpzSI|w$$V}&G^QVnVA?%S*Yadj=>&Arf;jffAU8yr5DwwqwZcEL zOy3NWM^raO^zZvTce^HkkRem&}H^a1~E+h2$bitLs z*KqD9tK*7XM5}?p6`U7dO3%8KS(cq-jMlnVPjhpb&dk4TfoV{k785P+0$q1ftS~Fs`G~6}DoYM7UXD0R|CF&>EWKzguSo4D!|E78Z;I+`IaQfs?3a%lal2<}4q%-CN#ZlSsfFPE}3b=9T# zDoUuOY>`e>XmTJa2;Fekw)Z2L%A^BMXkMd5Nc0@v%!_Jv!a_Pwar5(py*zc}5T}k= zT0m+DUVzL4sj@zWUfNGF&Oq@=aIj%!f5~7lop5Oh@TyQ;2THC-hiTG0`yPz5Ek^sxw{T1h*)pyZEB1(!AqA@xD|deylzEe^$??mNA&%`R zhvW@(XqBd+-xJv69H6&4&;Bq|jbThc^?|_}blGyjF<=FI8fToA5qmVIky4@~mMyzB z%J>yY7>x8X{oc~?jq12*yzz~9$rC8;DYBF_SqD%#3|XDHCBRGASRt&2yN7V(lLJnn zaRFJWa(wrOyFF&dgM~!-UQCYWvwY&poO;*Bju76QolTf4ybESNqmFBE z@6;x&2{P3m4GU7KA8;veWak|$stxykmFOK_Af6}kZ@UFu7s%*^E1DkR(g>`F71W>6YlZ9jX1w{1)aZhv6=#nIoeEGJij0 z1&#(ZfF0o;GPvQn|0a_NCDb-dVfew~ilMqLF{GE-Gx7`ECCP3Jg?(B_QRS08C9!=# zEZT1&*yhdg4WpsE>zw4`4br~`-R(l`Ub={wyOV<1uE9&zEEY-6gi9V9U8)w6oUx7h z3;tId3i`%N@I*}AYU4P(*9PaTK3DL=KFrmcNo`kPRbYkhIfEu~RjL!-{=4w<`V^u* z#gXH9>*RyWG$lzA*(!I=y!6 zBtS^DMnEC7>0FZm1u_Y&lkvAQ&RA+V_pA`4LX$PJjmcskl=orbId|`e`VMTqZkl-Pp{^|||DAlo5RT~dUsc>JNYWs=ws+bRGQyLd-dgT zh_xwV2Z!F=`>Leqbz?_da8pDSSdXe@)zPotr3i^#KOe?7^u84r!Zli|czHo}reNdo z_)d{#cd8_U$zdi5vl*10q&%E~lXb%uEp+%%+SQ$weVuX>tt4Ii*Y}#l^k^_BO}kf9 zALf^Z`MokZlqsaB{US*zwc;B!UL(7a{O1=RFLB@O0c?ZJhv+byDTaQe^r@?O)srW^lsd^-Td{-E&bDRB>FDY8 z;aj#!@wj)erLKL)_w7$E0CH|@cSAm{wsECvx;XfF5GOfp1azgp_Elp7%^_p6_@abgX1DLQpFZsQ3ME%)NC~9qX3vjk~+M z2Y1)tZh;UWxVr{-2<{HSCBY@Qli=>|?(R_E+PnMSK7G1(-|lhG8Q=YrH5hM=s-BACql^`{B_66mL1c{23Mpf7n1X#c3u!pA8K8OKE^YEGuQC8N-|(vJ9s#0p9B_ncG48? zTI^2m?kelH13$^JtC#jTX041qn&|-0 z+}@WBKYMadHac74c;3Dz_q=~)vei;@5)>FhwdX&n5irRzz+^4-*nZ&FI|S{Aa(=4uZ$&4|zRNC#gB zhloNrRLmw-Ll%Gk(A=A%yT(C^>c(ePsds*`>XAYYTw=UT%;k;>&`I4sTV5lwungW4 zy__o24jn}fyvu8|0zF0@ydYGF@!X{6YIaRCAK2Dnbw00AahHyrU99_UPdX)6R9ei-dhUMQZNxrn6i7p>K%se$_y|8~VI_*0kEBDIfZ17e{+K@G zCh_?$EAeP_z`HLyTFj!RSVi6EEz0U`Ruj z>{C`-EJHpb2X)sF5$UP8;e}0$oJDZ&ph^i-WSWQhe2HLe6MXm}m=RDxk?H<=0vbZD zFDNzA6&dNaW6o1mfjduw!!)3oh3Jq&joANbH?}@ki7`DCuZC(DMS)80F?)qC zy8bnsW{~T?~s_{Ur?r`~4jm z$7`I320L1IA(a-2sB6nB=Xbd)6Yjm29IJ(Dl&RwzP)>$?8L~;3V#Bf^$q&1cn zcmvyW30Yk{4i^4n7^_q_?Rk9UH1IfIiLq7ijdr-mV9Y;5%1vIz#|JCUf!PRjNXAnj zn0JXz(f~3&#T=|vMuszkDJaRuZqfmsQL8QQqZ?SG=g-u@T|Ft7j&o=fI#Wu6s8Q>g zs#l#iEry}};k}rJH(0=`tO=CFpblkG!wt2vN2G&dPUO8Cq%(YDUP ztbU8&0w+9P4&v=$QB0A()PU=h{)e$n|2MqPFk>VA4>5&yEx!F5NH?GAYnWSYYHFn? z1S&2S{LQzE?~;NQ6{|>nSg(FoIO*0;s%rH@dTCKI|-W+B5Iz0Xsg3p6GC`i6r z2F24s)FuNb;bY%!O4jvVO8VN{tVP}%?Q#U*;^)YDW|$FlQ;RQuUT8t*pyU>yHyIe62SD9>W^iKZ`>%S2!AS5hP{ z6UqfP(e740W(Wrw@udE}Ob<<%K$AS;R?y?0uMoKwnTAw`)A(uAq2>E`-GY@%Lz;zW zr{V}J*TSt!spH0uKWZU@BI%4p8%j%p$`^tYX|ZKXAHG_1PhrE9&>;q<7++k@82Dyq@K87!obGY}5Ua4oOe5v_fB;i_e_qt#}2cN4GQ8K0oDr`1KTlkUH-NJV;{R7tvvK}B03 zRhYyJg4;h2gIvf-H9%c!G7fL~ zcC7l1JZ*I%ZwLt@)yaV@VepzaOK4bo-bqnF&@GAw#}G?*l>e?0~T+s@nr%=egQ|H;9_5g5B1`Sg<=JU@Lrrz(LML7KPK2PG!|H1 zRiUD?&N)D--27de?OysCE*Z;-T!WkGs*n11dG_s$S*(DJ=L-6pitbTSweNH;51d}o zyM02(+7Hen*ht*$880W@mpQo)59Y!gFSB*mh_%lvA_F0W)O>@fDrpPT;V>iwjZW=g zj>?JTSRVpgPEe_RBog#=R2Hr=-ir~u!sD@sCo$3`4YdW90SQ4CoXOv}br`mPv{igJw@{-3)$^n|g+Hg54zK(k`s;+AM+HXe){`hh>QrC`Ie-5jk6L^dIRz1H4 z1Jhi=il(JW^w~2ECj#6hBZA7&|DBPhy*M+w3IF-y8va7xiqpg3Y6leuK8gKUSG>@S zT;0o>FeVnlmVf4fJW5z)3j<7LZG@hfhsZ^KGb6*~VA-N1%|n2k#FAIq{hK#Mqh`e~ zcygSnOP11y9AVX?5ClJ>tMYRbEc;!C%eN?8801S1eLUWhpX+L9En58+Pn9oByuT6d z`!+x+romedSUV?3SxJIurwYe8tx9QGY zqZpnqp@e}^PjVT~0~=fQ=_@Lf^`=#J=c{`dJEb4E=^IzC39tK$2%K4|;Ol2-Y|Q3c zU>)#NMDhjBj~4x&%~8>JnIEr6%@D2&03oMlz*i>FBdU+~ElANY`pX-so!l(|$>0e1 zeWfpcVr5+e&Qa0zE`c@aGx__73BVe0*u>b&>JQ`o41JHkC zEq(&b#k3g}koXML{m}z#5U#HQ78kLot{bgRNP~R3;?mVuzfjV#;eb%)2>Nn#Y!Trj z|8!~q)tE!__!8JQf(tX`g2qAu%%Mi3ObiCz{O}{`Yh&New1NbGwN*(~ zn0mI%$$%<58BuVs3nTcKzk;=YF8<#a{(pI5{|w>(eH5PYhQg;7#-K!S6mcGy?z9B1 zcd4e=WQ7?#3_+F+&}9o~`3kRuw|Ls_udpEF1FKl{J}{i8aBEKXJyDnS`-KP)Ey`)U z4A>LF8P@`UyV1oX&^2(h?I-dT?bAvo;9!gch?Lu1ANdgk(=NO{i!9(hVgvEflTT3W zQf$9d11_%s-mlunosF@ZeK({Rh6dWVorpaGufURh99tVH@L8Na^!3+xm8h}5XzT8| z1lw1GXN+cI2V+tH<1^qr+_7lE1Zo`H%evg3d?EU!$(7xkEH9|roi$ZU^|CkMKg#;<_ z1RO_cM{O?N0u{uN!p28$>*+$z+d##~0FDBDysbA8;~vgX+iSqQ1?fxl;Ug;evD2GK z!wetbM;|ou(wqfn9D)15n)C{E7T4BJrFZ*?QR5iz;EZgkxWWXM0P;7y_W=s6^u4*y z=eQz0X{SJ$c8;jckvZLiA6g_E4A6NQ*t}7_si4=;q7fG$)>cDuGPNK@$ZFZY4hK$x zloBmVhQHS$C0i{#*r$U70~6k-gm{m+ic%FfKjPR7dxIwmI> zCunF67bh7TD30>4kID@i$_%I{Q~UP^TYVMkE# z)wN!KTil$Vugbz`W>23Esg|U%1^>)(7{a#L;`w?++Buw3_1VW~&-HxlbLZ2mJNtDH z?&9PAe3eN`FTn1sh6N3pnnO2q@`9*)w@<3)_ImN8`Au)SvG(l_^iRGSrq_Pv`+h#J zN6Hsn%20s1bFK!xQ!C-7xnAmf?S1IgRdR(dXHv?kQag4$p5rad;orELS${*_IK^-K z=`(y$0?!#MGiNerf009cFz4wacDMrp+R0yWY$0@%1K#PRvOLfHslE;Aq#qv^Fq^4X zrr^yJ9AXzX9(2Jx3OBqnThp^M)4RHNZA4A&LWEuxO!O&=q&H12RLEL~bIejvy}?Y$ zP13=>Kq=4qqx6hWlPZ`7D>HAbtt14i%YP5*WgBpmM2Im8Rxh+fvlN!d0JhuGG%t3Y ze1l=T_5*fVHi*7`uG=)${8mUj_V`mSk9S?MfFU&om4Oa@kee4LzB0Rd7cZT3Uz!H) z8oK-(;;ydD)Sx3bN`FX;$p;^ipOS@7E`@lLXml=2sNtz`dDadEv@02saq9To+d{5@2P8Yv~Eo6 z^2r_csu$2#&_!YqC798@R9b5el;(HM$bhe@sb;8($s)qsZ_>`n)yiTzs95=a^ZMZW z`76{rTg9Ro=(^hdj3MO*1Mi!s7DQx!NHM7>jJd!?CY#e?-#6mO<8%SlQuL$Tg`Zig zBh+XwEkihq(i`W{X`uwkg4!XoG(pg9(60BQ;#NV&%@E`ofmskiMn1@?-#;eX9ti3} zecqGyZFmoV38U1T%oC6vBLhcC#mh-1C+bpBckfq1>#yQg=gbR5rqu{y{;gDHrasB! zF#ZUF5BtqY{%8a3qoWA+o>W+eS61P)8*Z=|#Fg!Rs7<()qc{(kh9NSK{#bg+dKwSQ z$a-ShrjdK$;MT!!=`m{7psvc9=L#g1L|I0p$r=~x$(rj#Q*o-R`267RE~{GlHYqIy z`pA1SO$MoaMnT?GN`)S^^$jCNi)Ln3=Bh4v@agL}6zqvmoRc3kjRuoF;#=81CVF)T6NvF&wn2O8O}Q zE>M&u{GE!8RMnpO9 zLeyATJEp#}!|$U@yNV&on&BX0+vYwad!oL(5*tt^4-2pnb8&nqC)1#24$+se$NL1^ z6>QGhH!~!VLnBC?EcH{t4QNAVM(R0%yqHA5mH@G!?U|F1(kZOGdnqfmUAhjuP#`X4AA zkf7PO9FDq(3W`TU=IJfSWiZkL@peMRqq^U3JFEITm^l54s*J1GR*&;nH#Di`MxMjl zs}EwOok%MGO+o*$*_1!H4N8j9ub2#9$O0ScNW%Ikzyr)#wx;?xMd{eQ*1Bqe7k0_o z$xueai`WyK@YGb}*I`*IlwwoGMTTWXJ(_8s>4Pitu;-PQw_N{TnskJ*RxA5TLz)Z0 zu+|nU{;g%W>nC{DZ2r?lBl}wH&c|F0B{LtW=+lS6(8;?GI;sXd{C9DSczAEh5nnTK zWce8(*)}!}6c4P{QdcJkz@HN5pO=H_S$(O9i_beN3uQ?yi-(Pd``El+FYH9Za0 zsYY2M)v1=ljXFesD2B^OHDg*4BWkuO=rY2Xv-)MYzTFWnpy2Tf1*r8Ab)df6@uw!` z+#Mi|kjPBq_}V?m7ZS4NkTS6H?nQh2_GH*72+8+3nvq!;OIOyLSUSKFT;lW-#29t) zd&8)-n>lr#95@YyyL`;r^@HMTJ)|-JbEJ3cHO!xM96s7hs-7@6H}xB=V^ZU8TYZf{ zYA<7Mk@bvJO1OSsGpXn{*@W2M_9c~Vy^UO%A`Q~zfzoo~mdAkTLWx<`HqaS7r&J%Z z{#gLQ5NdakdaQWycekRr z@MyCXy1NHvDA|q_=dRX^nanB9j+1$rSkq$LUhuNYJ~1HEuASBJy}no0SxqFIl+T0q zYEArf;!RAr?_Wi$^&@`$@L72r2L*ph^5ODyB$H+kg|ooNI3rEf9CeE7p*= zX~2rrzfOMT!t=@0QLlN*)KAycYq9a7w;|Ig1YVh$DCbl-P|e7$167hvu@j+yX%?`_ zfp-<%FMD1djx+xyb8)F#wz$fKa4AW=Jz0113Q_eCg0c3Qq?0wTy!;n2IqWyQ75A;% z4wd3-DbO#4M>|rlse}bPr$G&IU)rHwEc59z2UXk)?o2-dPyLIvQt)-m*CQ<#A#M%J z(E|+YSfVV^NR~;uXsix}?oGxmwk8h>@co+z2!tIeWQ1Se4Ga`r(Zk^S-iel0)3=Gg z3Z|P?#G{S0Zu{z8<)={ce*Mj&xTyd~BSPV-kVHR8v6k>46vL7;ncw}Lz8nCt<%uhmanIQL*K=%m>vkT!YjM3 z;b$Q1(b>s=URCy*s;g#gCu!R@f44xz$IhzRD~eJk?@tHa{zi&7f&+V=8|qn2vgtiK zyPX;~&k&UcVeyL4Bjq}b3-h>n>Mbg0gZdIr5!r0Xu3;n1;c-Mmo=WR3RB>AZ|&^}f^>^kMuQeW#ssQkkK{%(}=6@f8%9%Hb4 z0DRrIk!_`vB~=H=@1r`{ju)0Tg>D~z)kFSHrJo6YFEP1aW)=6tth{x427)?fD1~T4 z9VxH-J<*PRb?wCknK&$BTb7dH2whS%Wd&9kCd^mUp3F?i#GnDXt1tK%;8AYry**t} z6lA6%>H!~ZV{+6@Z(Rfy8ws|3+gt1-wSxrV=VXZEKFXQ@s@IQPKoKH>_% zm{%{9&mwrC*gP410w}&|VvQLJXcR5k!*&%1W%X_~|7uc}nL$PPh1W}n5L67m zkmZ%fG zNjQ2g@%)v$7M)gEyJriATQJ&=sW{1IDr;0G3U3or<|Pt}2n3#98BUCK-N47s(rNSX zhijQO7FN`3Y#%@Imq>0~Snlma7YuTqlBneT5=FUzx1JrpF0PG#oI)&;t&%xf`n^~; zOcyRtyDc@*=!yApS$LRpwNCg&<-Vtzr5>BYMTP>B)%12zbtb6P72l4cwqM2(><%U7 zP0G<4>N)k~8v#?aoVH_Fl;z3|=8zETiGxw^vFp!;U>kUyqzi8+oD?r9%LRBA?q|pz z;|h5>M)$xArys8z1gfTuCQd9vHC2Ew+kC+YynT!I&;&F~4A?%6bB)q*b@e7J(;qOjX1sVA*)#Qcf!~d^0Bjy0%61RSF)br z9oA9i>=W{8PWN638=eL;lw(Ex6@6sWow)D_hYi)pZ1&uIDr){Z+}x*EDmAyK4b_I+~GE?gh<2hAD@B&j;Vbxhr_)%-3KCm=&vvhowV#@`32kQBq5_FRI#b+!> zJTrRo8BvW|C1ssHh17m17PgP68H422F#gPf3ecTQ^i@c36`R;nSx%agiGLoZZG&2y z!byLRkZA1dF6tnSEXh)|DDSH`(z;fToLV_2Dv1}8?(1}>nJsq^yo0WdvWX3n!Azvysq|L_=tk)n8L8Fkx#p!m7iO1 zACWx9SwXEPLGUI_=3aMQ#kQ8*p#)1g*U$Ki*<< zu-;1LE4p!)d{e!>f1bTEJnMyLl>dE(wqfp5%q;L3r<-3-@eGd*53<8YzU8&QOL?v4 zCqW#K^(q#RBh@bC67BX#X>G5NxIt*g21y>2&WTqrhQq==BGtx8yYxYn(Z=V@H)PG; z0&LrsVmz-#%T{q1X!o!{`ZBI3z2ZgGkrM$UtuG*t(b(b1AI+B1b3+Y5)3UuWYVZQw zhJaSqE%~4SJ$@3&9mAnom0NxAFHrMv0KvAYU|}o)qhhrpr!== z7**nFcN*dTU5|pj(!L6}LmSgTVIbA8${8@b&z>YAq#vR!?uUv1CNV25g0#RuK7v16?CoPiIbcD$x&32_zo*bdk?vCu5C6i4 z3DRPGDHm{^;NuhRPH- z#+AT8on#OO6_E?h2Gj6e>9{o<`3^9c0~zanYh5-8djNaC~KCm6Q$q1F0J+6atrj0RyX4mDMXvtNYG|xg?M6$Rv^es=@s&&l%iV2 z*V$5K<&(%PuDUKNDD#`risaTmipB{OL9VDIya`n$uT3eM$bJ10J@gCXm|WUznT}w$}HtQvq|m{q_OmCv3X5vYF4%RNa*rKdke1p$4ZnvS9la}313ycEKWts^Tr(hGFa z$Jai@wQLtTHQY!cSibCA(0glne};qL%MtWk@6^6`R^z!ePT=*FJzEEyy;Wgl>SoB< zZEFs@u8CaO%xb(iFE8&xjU9)1p#|mSQprn(RG1N6*DW?5gKDJQ4HS89<6CG1Zdviq z_ZD7~?x-2Bd2{X7;xX+=WCGNZwBDk!<;}If5U$k|oQ;rZZiosFl;$0Bdk#B>TB0z` zl~FI^Fq$r^Orb^$=s&7UJ|kP#dPKVhJtEe}$MoQjkXnnIjQ{a|t)2@*(!*CK%;U9( z_xU}wDT>t>M5+(#Un(Sol%AO$8pz2dYSWg0S^HJ4ByBN$6B{LZ#Gu)<@I!;F!2GT; z7kJ)iS}B%0W0M0PHC+K_9-)sgYE^&pM?|4-#B=)DM1h8 zM1vW5{JfT&MBkkY<9d}1kp6gb;m{Z-S!gJvd@EwTn;ma!qxBRbHhQ z&QSbbG{o#BN~5Xgt6Rl<#^5c4J%_Hs4$t#LCA?^pu|}h#i*vE#;R@5qL=(nkB!kqb zI53HbF56*G_AN`N6er;0mOL*eD_=k8y|TFPZW^gf%2l1~NWa#KGXTwNmhK#?q)ZCa zq3MT|NFWyu!tTWhbn=9z>-bPboKyLi@VYNpjjhi<=B~Aci=zoeW}|9?_67j#1JS+V zUzDQ%DB=G1G08vFt;hR^f#;v**86kyf6$rzGXN0pew<>{nd6XDjH?G?nkEG2Zed}__$6mH1Y_U^q2^ph|YiSC!A*Zy4xwD z;D9P~@2A%ZxVvO`yfy~HUCsG|LJmXjR{@0PKH!|MTac!o5a=#;*zeCzc#oOnJb z;0bQ7RNvtO@!kypK!#02ub(f+wHYe6+-lq?h64P&wu!08Vqbtg6l_yt12Hzq?L$Bc zE;1w@+z5e{0V5LCjdJpp!dT{RY_Km)!$x%_sp0qxj@7 zRlq%Zv4h`p)#d{!#11~`t9!rEP$Zy)w8ZiTygl19V?t`}MI?I&A|d^V4k5!<*o}7T zYWiV|_0uL847@L23Og>z%b4s}QkxVq*~g{QfiAM;HaAB)4oI-F@$1*W5aIui?H7Fi z)b@)%SO0%lgna)oW>mX@Gae*DP|Rp|S_VqL zlfr#q?G6_5dvOnBPxJ#>W>dOM9%5cTLz>~W0XcBk#wSm(8qQh0=YXD2e-&U$mM^He z4)iK)etGQFZ`cBGeZLmXrT)= z$FUCV7jJJ>D0|H>k3PsYa_2xQ%SN%nb0>&MuA`a6i;Z_43!+WVwo{C63PV`G-NRGt zI!;ug7!3&?COW9cyxO0yFHs;Fk-Pz;m<&UGT~S66Qw6{(8V({Q|ufwlgpcQTh$+VTs^ifh-Rd|G66Z{#$(*Y@DnhH2No`<>DY?M_!hwZcTFb7 z8~@8S|7Qo5)T{OW1IRURGUqOiCrn70EAHrOfY#S=FM?JcQs(+}w@y5S1i&Xf&Lbpl zE?fb4X?YJ9enJ{H>wc`-`}aWQx7S9$J2chGEf&DAm{sIq{axzoc^Fn&zr~jmkp--| zYI9(-_}A?GeB@GEDJ=Cu`W%`wyFKNEQeAY5AAW zZ10jjz5sCC>YSvnBIGgE*k$yJfLDOY#E1+`=n0TolSc7+0oVz{Kon=aokLJL10tMU zO>ce}L34q!Q4Z>OB}^*DD^Gy3xv6OplEf^87h`b}^M!%I#>?v`tbsjYI5}=yN&GCJ z^L<*+&0A;mnTSk6ts(D=2zDfW>su_bK|KaP;Ju|%B&#NLHAY7yC}fFr*^2lm{XU)E6l1hI)cer+e0Z_Z7@qbO9N zQ(d@clx5RYfAcM(d@vX2W^#dHgS@uz9!aAb5*!g!h&APZWKVuH;S*}Om>1m(bgAQ% zQ;a)fBg__z0V5&j|6FqTOKFsb%ZMq%n5ALupqm)U+8rC+H+Q&KF#^yg2x5-i#zrjJh>Zzp+x zUkikZ%wLq`HaFZn!6#7!YNrThY3x<)N^{8m#3gwi5hsC(4dpX^x$B!%(_Yrij~dq{;%^Je8w zBvB;zF5t6Pm5aWuAnF>Mcj-gc5q+R%5@@=&(J4ElE&0Xwle=Zk!+|{aSz|6zXsb?_ z&Fb*AVA6zpC;^Rl%B=Lo&RTuIBEM@E4qcx^m4CZ{o{4wm8TUu>P9n@f?p#~?UiqGI zIE1{VVv-u|NZ&X4B*AWzEUEA%gFR{1*~BA~ltZDTP%FhqX`7MmBb|Ra! z5t@83(*z~`s!B%rmaBOr8e0z@A-nTOxiv`PTYOS=1fGii+z*m`nPwI+o448OlKq|( zMo(@((bzE_u3YFZI%rFdF7^T|{cJi^ncnRu>-66UrDAaNMI?9+k!CwI{cnw^Wk?la1s)4ZAo^1Igwuvk6;M-jQub2n5uY{UO0u~NGg3_&C zjNi?{QBa_)EKUg(HaW};)k+oS?+`R{_yR+%!&Ho=M8`*;!n=y?nc1WF9j8cp4V&zS zZ&c7n>RNF^5JMA@Brs3{-aFs|)=X(a{xW&S1vQfJ;nSA6`-7I4;IAIGuU`bI&7diW z%F}t@U9Phnw5BLO>~hu1L^`n9@o_ouWj8)lNoB4fqh~UTeV%98Qmh#hrBh~U&MBBY z-p++xo80U`ZF@t%HpbN@Y)!v-$4tnjny;HG&HMZnWjpRrTDJl;uha^r)J^^JB^aTJ z3ixUdhM?7mjO_3C=HucChL_!V0b#Jv)kjG-2n%N0_9AHFxiwwBMaWrxIaRl0ZagL{ zn{b9rYoX!DH8GSPL<7yCAUFGcPKRaOfXP)CT;p8Q_$Fug!=pfje*8WwEoSUzs!UbJ z$8&Q{kP6WaX0W1`0FKRFo?Gk-g%LA-QSjiMP70bDvbdzhQowwHyCNdXwCEC)G zC)-ZmDxAqrdgh_3Ml@1tUldUwh{)P45>CS2&R1XV%xj^&U4jPj6I=zKMVK`E+ly3K zp#I}2C#xvBx1rBX@Yy~+KbOGM}fNEcbz4^ zDS$~8nmI{18hqr8f?#qvBwVe``sUU`Jj-7YYezwh(cnyh`vP|#e@yP}Qacst06cg? zmc^T2CmpfmYHAF{SZD5c{BG!>+S?h1)-gX7L^aQP_kKYatHM>}(ZBBc?X?Uuc+wA! z1uUAUBw1&oX=f%UuX{m7te4L3Z$sAw6pVIHM_!b?1`BMi{C5$2~Uh zRtb0qam?K*bMG~n2MzXLt28M1@Im#_2xyf9y=TQ5XQjMfeSI=pTE9eB`SMXqzFo)j z=9yl+358!^uQ*(hw&i86csR=qUvl_rv+%b(GwJj0tItdsDRzr+3!@XHOIfawZwrgo zo$F_#cdyGBdjD_@&R~t1_SdS(4ww|8%Y>?ks9zO#P|O2sq3m!8kVVeOJm=lujArLD z>0&Uy;bAW)vL!qv^@ef|H>Ctx4)>HsNqRn#&VIAq!mL1!P7uPVwZtg;rt?GOxJHhq zrO8FmhO`{5ou}`_!}ler=2%PTa3jhgn#2zcf`|dy?)9H z#@x>!h1bQEzg#JPk-nF+qd9aXF=hWHV9W*v!IT+#fVE>_3Jye2sY%m&AItOj-E1_xC&x{Z&oHuMrS@o1gl^1AE^9!99Q?w zWbUYpImc2pb~-8vY4IdvDA~R@_hG$|?&>5aws^YF$;PX0_c?J_5Nho8G^Kd^hy6nI zo@Gm?O_PO#i&jPPz$vo)IyS!1+x?@tqaUogl0T17j~S<494G1n9XJ=diHXbjw=fjl zDG6u+U%uyqn-mUfSmK^U=HP5kplV6|c(lQ~j2E0)g)8j);aG$6h6Tk) zl9R2S>8++bMr5Zhkq}}2c6t*gS5%^f1A5Np=o5Fe+*BoH|MH(_*|2W9n6jA3h@MQz z5$dRXU%N;e-Uv!JF82w!%JicEeb_9_r!XC)VJk<@XLMj=hC!p^6G{7b*eJ zNfoN$NyW;P1r^r;^Od|J4_n`<0A24wzaRY=qk~3|(cvu6h@Xx&y)P-}XaN$VkJj@m zm?XMal_*ms1$8t;9}*wzU8po5v{;9rF`qLScN@REFb$`$5tF-;&|@1N%Vg;%YH`@N z7ES7}*H?83l^$`&@i+Tzdd}elz69GPC+VDIJ&P8p;5M_FHL-^;7ePpDhLy%Dq2`xU zUj{%D+9$gNe$VpjklyJjLh>_V^q7uE28VgHa-Rx}9~5_~ofFVnEhS2PkenWoP20EG zP4P(DC69TiIW3s(<+hf!-RSBw{v>O|v(yRZ<1z=GR-K{^gJRPLW*nJ~$l~hk4$*H_ zfv)o%l*!+208cL8PB~1Ue3jAi%Fvh`g#;^9qCHVeCgdT-^gg_+pN7F*Xw_xZ6_Y!A zdzRtI*XE03j%&u~s#aiZHtwkqwJSv>uA!dzM$MtK<-ABVGe#xX$Y3aoaTp<&se>@W zyyf6$>-(u=@Juc-5?~AKZp4Ld>J(3SZfNnXn$dH{4nZIX1Fy^$;@QL}$nlr8U_NJM+E>_uyU{L#51 zBPN-q7Chs5qUlqA3UxVkIOsxmT4@NHxW?e-#{MBV{HttO?%d*nYDA0n(gLSQqZ1rg z_TqdLVVycj#Jqf^)FGwciCXGKtl^^jn^dI)-BY8gA!Au?j<(9Px^2#3@(1wWe;;L( z9T|T|v}D~rNxM^0L7gtAceQ&Tyd|Anyb!chQee_~M0qN&nL~L7(+2iAE7})Ez8;*v zr1ISicItu*4LwN~0an4`;{VOOoSw?=0x-_yNc zMfugZf6?T4XTi-g^wB~LiB0cBzcNxX z$__Ey!<9RJk?=etX<3E^3GHk3}@p0(V?OfvMp^=UC2q!(1NR58%lX*aolVrHF=7cbYRzxnmzfR2ad7_OA3_XOd`*~~ zzv_Hce zZO2xW&k6SkTcW4uh;<51Q%JbnJ^y6p6jEUiPV+E>z}*YeKV&cN?czxj{8O~ky_%Ee z{imGoKiqBFdZeN%ebyaA666;}j2FD>X_puz8xR_QB}f0p_D5z5aOU2Jc zsee%DROgfCw(hEpY=C^ll5TJ>?$u;07W5Y{&3;{dmX0bSo_hA2AWLd)G3YcC7n2Hd z(kW&0J{j6@4E+2cQ?ED^y~0zHa!u0YU6lIL-mS#q+_ey7eJ}R_TBDoT@9MAWN1o=H zn=f{pFx4rN(;ITy80Pu;OV>v)Y;MFm1#8n{sh5v*EkRBk!e? zj&(F2cBlVTjv_=7^U3|WuOc@4?cH3%Oj}cYMtf1;IF6Tebk2y5#3*y?Pr_6mQI;V> z_acLp;^5BjSqlOAgq;+iH!8%ypMy5*UoGZ>Mp(_ClpAb2`vjKe4tlcncZ8k#O>l;d zG&9^i?(zruT+6P55Snf#xrEJS*CT(?9~~%RHX?2bb3ZeI{_ zotFz?)Vw+-jJgRj!b+z33F$>hX*DQ9Tx>2Kru5&ksPe6xy~rmkwzq1|w{~0F&f3}V z7mct9Yzbo4p)^JP4El`<<<@41Jby3ovn;2;q(01pGs+4m$~N^<-ko zo+^PKEV$?44b*_n!m=%h1%GJw8u*K<*#E&I#KytL%)>_p^7$}xv5~Ry@iMcsk#T{t z9a-7_s`CBcun7HG8~+E@hySPEW4=G=_dkt5|GD}<=*<2Z1e%Zc-xoLk&3lZ%L(rQR z`mdGm!g+TwJbgk+L!%zevJ_JX_W$iY?s&ZqX7BZdFn#+8@*X$5h=7~X3BIhuVPiJE z0rNS4fc6WpK3n&EHx5oQqz9VnK7zFe3q0@C0< z)49*<$RuA6df>(HLHG;&x>JOz3u!x%I0fp05-NlG4Eep{;VP7Z|eXcqAObNU$5l{u}KiGsM_%g5Q$`^!XWEWsh4;7 zm0CVusv?fiKlx#0(r3iJk>X18B1O`_dz)uxRRBboOg?ZzO56d%+6wU=4}Q2TkoxK< zZ{r3A7B8Lb`v9eL?=e8jX2F#93yvqm@d(1T&lUh&=4WOG*fp4{`;Fu-Ay`7g0hZFz z^{3B6K43{l%wJh~0l#pQp;o}H1$YYzAnx4+zH2KizI8^49qj=kexio`$qhD=Bomu* z(WVCK5d@k6#Twj0#~}PADB(oOtDeSLET#Z&?*f%j6V2WAUbD^0RG0-4?7ivZ+h5!T z{x(+i|I#u4Ki1hKlKRHZoT&hi7K3P6~nXpLyr`* zWoFA9f9Mf72Y_3LpE2uiUSjEaB5wcyR6)}8)?5PctP*eCfK+MPh+b}jNC*-r4d7tW z_thL0lm_th1WE%KJ_Rs$rT`!X8l*@R0HjEahaHe27lB6lD$F8P-1R<%QOef6Jw8JGpFz&t;SltQw%A7&xLJCT7W_`Ms zhv?zm&4%DE`V#$w&_wSJw6e1jy^0_XtUUh_;*5p&|Izl&;h8Pl-e_za9ou#~wr$&X z$F|imI!4DfI#$QFZ728bwf4EsKHu5v`R;SSd;fTo%$b^1?;KSFHO8pl7^N(h(EIEL z>N7Gkl7PqrQ3+%;9sI+D;iS|B-e;h5Ms|l$&{DBpHgHpG6Df>@nN4Df- z{FA5gU&`b^o!{#iNy9r)GI)FNc(BIIBK zXv6=0ewUN+-(%tbdtt!!{~RoW-Y~3_-f;_kJ9p{Ry9B z0!;6*-H%{Izh})Qv%=aDz<%(5a)7ct{Upl_h;imR(ShQKeHJO2BZzmD{+t3ut`9J3 z|MWu)4q*ocjdbh?`y>bq#v`E#Vp`(FAcqKc6oBtD*lqgA9q(a${KUyq5LmhGMs`4r zk3s$V@L5ZoWeyhuW3r3I7-MWO{OX61>6n=IvAG~%F<`#-`_ma4EA>4WkCh}cfN1~I zmRx4{Y3jY3bV4A@@&402u%LkQ@l%*(UHbEok&-@rGMcg4GyuH+?(@@JRZZ=LiIgt9 z*C^{T&?wjui3V>HcYH>ly4 zUEcWLBhhd=$7AZ~*G!BOY-$0zY0DNsr1iM>dpJZr;k`d+U`a{lHQ+~hQ)38mZE=hR zzf6XT<=GjKYuEC|h5!SN7hw$kgQ)n!*8jD%|2ewN$;rX=-wnV)-POb|LykV;V1Veh zIa+|@CId2zSa>4kSOIC&k21&+L_$t5WC&n~+Jb%g8~wI&=6Wx;@>3H#bp|?cfwPNc z-tVfKX-U~Ex4XCI#?I9l7mQ8)tpo_J{4ETK_udz6{pwGyOvHKU(#(kb-~Y_-*-!CzQ*fjJ?YZ#spWVOHXG%| z`{7Z3xch!Adimk?!&!5~k~dB_^>NUS*V)zWIAQnsm=*t3*`4=wt9)JJw4DD@oCH~uy560*;j*PPFfb-Rx{*_4UwuS3oDINjN& zTq`Q~Ed&)jA}8yCtESR`v+(>ylir?QGc#r%zb7tv9uKu24^7ig`QBa&SJ8x>yj_R* zJ@>nA_4$MmN{V?t?(YhY<1YK{SySE&_&(a3&tf{v3qQB?*DvQvOJ3H@gjX3NGTOK} zTw@Ox?Pi{^_*?itcXw`?x;@`JU+u6K@zw!{M;Q;-o;Ke-UM|++`1@bd@t+R-$;JE=JH>eJxyK)ZEMH8mOJ@bJ(_n<6xhapGat=gem>^? z_N{z%cQr`gSfBHebi%Azs=v;8i9xQz)$fO__mu9ce%0Zb(A8oIEW^XCFNONP`*F$C zFT0be+4x=P_V1f-dbf%#yU6?1I9xlB;wn6sLz<5leeZWcD~sE58Qa(G2ks@Fi@Uy@ z=BdAf@+(&j5Bq(8=C6s%sO@@MpqO_#rL}%M`ABT4^W|^8f_{FWe0*TJbv$Ti| ze0=-N<+vby#d^rK_c>|b9k)$kKy=}`DyE0*USPOzF9z~;_hKSo`1lrPzt))NbZGdM z%lczgM4J3fwMSV-deq8$W!*PUZ@#15ZFLtG>kUpu$5PWeLP3?-cu{~tfTz%YE zc$CIDeSMgm)$lx6_3mR5v7E}M50Rns4My`m?lnl?{!<6#LP*_B%gwjS!g_4k zwfhU4JzrtcR!07VmFL%%^(5n%19FbGug<%UhPLuCWnMBcb_88)Ihog|?s(lp;KdFN zkdHx)+o#{U&B0ThXG3#y+M7yZUVGgwZtdQMu6SH7)dDw*j>qgyeX}{$+NIUnduux( zirvzFUSHqox5E$T_MTr~Oi2GE>he6ju)zF`>F?HCEBoU6a6Z`2E)_K}{|m3<9YX&R zhx+H$;Y00PgT;~9^Q3JV&+~7$LrdQ`4Vy~RpI7Y-o_88g$Sto#KYqPu=E^mu1@ZtQ zjfc;lEF5O;RBMd*hJ(R{+|i?m1RED@#I}d*H+bSEuZm9lf)|v_v<+%;iEMon%IgFN z^GO4A{$>&qeBX9C`WJoaKat&r#@vp*#&p#6gw4V^^jwq?INX+x*e~#@M!+Q44}^r2 zn)zUvNx#WPFeLEaTg*VeH#oB1w^}-p>gS%wA$+5DoEWlyOS6Ah_da?p*d?r-tR)>5%};Vumf>ZUnXa!luvM_{@|=AL8DLRcl5?AS{3=!m^l(Kj*gfjGMY^rt`WFq``NM2z~#790wOp&@vI zRUY)(>6laPCh%1EiqqavnV&tTCO+_LYcE)EjSNPXl6i$UaxwQ@JPeky<iUg`ZeDXhcXBS{+3Y|NDjQVI%oS&qKi6eTNB_M@<*2>0^5cYM9T5of|X}rB(8^y9v%iAE`WQT z9`iX`+i$IA`B^dTm=StrzR4l|O2PpWauD|L^zrf4b@T@Hv$^EM<7sw`Kxk7~i-q#G z_(H3j9mW24jF!@@Eu3<_O};OmU9({z?dlbq7&BIS!bCcudge8o1hY>q$>|5iezS=X z-Pi+gD5gjQTH3xan(g?E68{e@J&s8Ej;|!SCZ2}Ub&|r;9Wdz99jfBeyQ6a>R)e5q zxa=;G7>zrq09c>+N}sbtn~iA{d*me)1`s`)I^bKScIJ8@lZw_p9lDj;8$>5#bn50= z!t}P8=pcfEipKYtO;sUK^k%Owzj+*xopoEYw28#Pna#9d(=^$(H^qbuU3AUplTyQevwYvj#NwO~jN-c~6vAC9z&iu91Pw36%B{d45+W zA?-R?D~rjAPYko%P%CN;WQ>DPiD(kV6B`kIyE0+6wp$_Q^e`Se?JuRR3!s!C{nntA1LP(@O9K2~XI>ng+%j z*BV{{Cp%yh8bereeV8COQWnC{Lwq9-rv!dppW$d3BxVw2e@1ocnr0Uj!enZt*NP!< z?1T)yGHN2Tn2al-n3c>7PC^;g(g^Vgz&U=!Qicp;NYY($KW}ZtO)YT=x2uic69Ee4 zdNOY#r-Qbvq?j>=x3z`ARfQ_G7SQd%p^XJhHW=noOWabbk*PIN77~;zeW{>&=?2{p zLS@V|NN&jR6(Yt<-ZaSMYem3@>GO~sEnu~)t2k{yrBXseYsM^&oNS=9=~qDQDz|b$ZPQr@)#C-ND_iQ~ zQ^JCNYt$|c!wy)GlprM#&?49ShNA$no2*W*8Wj24Tp?iDnz#~UAuY?_#KzAF_+G7y zXCP65ti_8=ts0bp*hpYJwpbItI!pFG-FB#;dmd1-CM}aHQ=^ZpVA3LOYhr+1S6JDSc0Ob4g{YSLh%M`CYHME(&NPS2MKQ**zS~I#VU)!Oan|0ia2ysMM`N*kGoVxm8Gt{oH z3`P;LGd_&0fX<~WUZkAK8EVPDTxw3aD>>Y>fX=H+vJBO}*HUT*$%_bHQdE1-8(4*; zc}u0yLgJYiKOw4kdVixl1kg>SV(z^XejCwg{+MpN@~Qwf$f`gBc59_eDEl-GPab@- zVhy}YZSaZ!rt~wr!UP3I(lEsg>{6o5-89M=%pl5T#te!Ds49e%8_^M38QLI)-t2IE zIQCg0EwV)f8)6e_ls`r$r4%67;yGJf;(#6s!xB*-D;XJWRr04`mi=_%4-^Q;R0E_b z{=0}044g#GpimPb!Xv4@XN*rRKeY&ha;W>Cwn{1%FTY2L%`ozznN;!ze(+ZZbLI*1 zd@k5T$g2}KyzYrUN>hr!&tc=Oq=yN!`|T*@$`Yqj3!}!}aJ!Hojwl%Kd23iE;2M;} z$nuVkP22KT3dJPUpUo0;^r#?PeZOVKN6Bv*-e*QCyAqZHQ)VkQoE-De2*M#b9FzW` zl%p(^MJ)(luO~5pGKa;h-`rry(ZdRE710qLidM?x(1mXm0O%X2QADqg1Y{1QRmVJK zn2u4^4@V6!ak^>n31jQ4-H~q5qfBcs_TZF)1wcOj39#IZBxz`Yv>MMJ`HoA-re3HTh8+#-cVz7kBdP-wXt{=~T=YzESGBf`?ObZF8Yrec3&b_7r_ zDA_k8mq?Y!9aIKO6Nw*iZjz}`4Fg_~+KHk9QSiJs;~=E=m;xX9<^%pEIm59g$O8_( z0&w!YH(@1c-vOB?W+pgDX2`DVI?v#FWpYrnD&`zv75pmM$2>UbOi3p)5eX+IAg1VTv>X8!k=RQQxK7o$G?2cb znrv9QsbfLh>gWV9F+B_%8I!XJtA{eCe$A1x4hMBlB5oFx?5 z!l>#-j=%pm?S2hw9{#m_&tw40chZ)ZmIttWC;wW$%a(tZZ(?@Su;`&E_0z&vq~cMz z{;ag?zbHe^ptRk5(aWjohVzG>rX@@s7+xy?<@Sf>UfA#>a>iSgw+%h;fZ?6VcIH%T>nRigDjEYkz2`TOnsqZ_r^)}d^q5}2uvCQPtyVK=oP zyW4{5p&C4LJ#Se8#1BQj4DbEIWSH;kKU-(yzu%Vh=RNc>f<5#RvN&p$P zVhyHQZLpUihV-)@)&vC~Uc7u=buAeeULKj=dn6ejUIE!OY$0577Q!^uiWFMzlZ!y8 zNq;Z>$T1MzbyG0i2=S3~6}=HTh=XEP zXN^f}?Lh#ni)uSEd{cfBI~qjQW}AdD&EM3B-{EU2#g*2jct?FX*TnFv?XXD>>lu`& zWB~&|MpS4=$S=!>tV#{c(-~iRYgq%>Xa8%#iQ10B!|qnoZw1#eEY3}Oz@*o7i0k~- z3&I*Zewp1eaoTD;N-H?P$^rBt{(Q0^hOdMgI=p|fP$r8G*E>&VAVyD>onN*yicoIn z>ZLqr1g+LwK&G&+*b zzec)FB##UOe<_hRP|do0_%GSiRYq(ipbTC|q6x;7MS#?g8uzwMC2D3;7mxiNBDfXB zt+A>Mq*vu#{ZtmcY=zS3pg1xZ3ubC%Nd?$(g-5T1&&THkUuEfSY0vdg?8dTqQw`!r zmPK`cY7)}(+}lP@xu@~bFH+|4^NuBU_|F3I7ndej;a z;2v!P=5ln8khMUpwkWD?Q))gQTn#kKj1IOR73h@(|^ zIN1X;vD)0Pt>=$Y*>>RK`zTU`{Z~=;-|+?P5R;F(bPk|^2W!VYq2nm6wD+9&)rp&X z=6sR+voLSsffz;TjS6O7MRA0?p#VA|`PXlT-{~)ZE+vdlFfHNUi6iAJOB3V^LTbpC z_wvcqWFpBnm~o}Mq5dW>Y4_jcCI0<4c~hPKkXP#zbL4OG>XRA?bVL0Ec{RbTc~)OU z8Rcf@&b?O|u1~W7nf|P;uM5;OukIhH868rc9XA;lA6Lt(%HO|N+l2kGxnQZK-6g?Fa)GK z7SN@=F+`+GdB6~pv%mq2#R*_6S^#5x12C2*fU(Gf>8^>l2Ah<4O<7=p)F3n6?k>HC z2b;UDudB~Zu}UsX3vbR$I12J_epYXty!8|QSTIw zmxhn2D2eJwYT_!;>=tvq5)k-|P48b^m89`!>9NM(BDvGM4W<1|i=(T&-Jh!ZVAVbK zo2mpm@_q|gOi0hKP@7~;e*66<9?#9aH{cz)KK95wF$LRiKO@z!G*4I69 zEnV;4=MQ6`e`L^I`uKJ!bmsc4GKcWVi+5@_Qd%CP<1*(oW$P2AEw!5$p7~t${MQau z!R4fI=gzFM=;rwSL{k^aySe@Qc(+?Vzi3nYH+7DmoHr&!aVdTsPE{;##crO*X%FrF zX(gEJ*B|@qow-LBlEiLQ7bJ3Tlu<2H;%l-T;wPACaeOUf>+&Di>-($gVP5!OjiowX zf3U0WXX-1HdvQOK@~Cj;%q`*bA{sOMve!9^r2EVYrjrb06sn==bv|acqyjS>^0f1=*<_>x zFN94brhUTUpxY1=hy-CHN}UH1^NNr|7Lct2mC8J~aabDVagkW#WcbLHBQN?*MrM4{ zX(ihblgOl{zrWnW8a)_eaj+<+#j(jBbA@&#XgTBom)s&U%4oL1CXh)2zWd&v?~X=C zdef;<3`VyHJ)yDi_G8G{%NNj8wj5@vp7yyF(3JmA(?dYhH(KJIR4(N}3^6pXgJ|ec zdEI$1wrv59)vW{o3Brcs^Inm#QCtX!g#%MjLMFw=WE+wxS<^I7RC4y@PoluFL9}rb zu&b|_!J66imVv8cq64d9qR8e}S~NzR*{%2#d0_7%RyEP2D}`#vtjuRAU+PN)t$VVpSV7hn`>47MYp{`1rR0)TnESH;QWrT|`@8s)jBV;3Fz6p}QS`;tHNk z>{|3RZ~}u3_`fdTnl6GiFx3P3$_UjprE8kwZ;KT0!Ui`CcOh<^QH-HCpvD$E`TVR! z({S<_to^ekCkYPF zCx~t*?FB-YlwHo1bi8axq3t!Zc`USBU14OA_sI69At!(|Lrf*A!*2x6vbIn?2^A3b z#q>s<#OS=S*ERQMI%L2#gXv3%aIfL;j%^IHnIVYcDk$%isW01gOvsQ9R$bRWsZwkP zDi5?+Uch<=;xzx7yit!?51hSgu{?+h*NDdr!UV4bx9_t|Tw}g15+Nmm>lNo>ep26# zOm|bi+^_pWkOVgD8a5hfF_Ia9hyb!D2>81x!Y!3PEUZ)^9I^CtBN!c=G_~3bwOUbU z2}8P8*i@9lFjpc;IJY1pFx6-qT@z&@giaD$nKp(-8M6rcOQ{B?h+M6IKcD1xc&tcf z#5S}swc*5YK?_L?;Hsvl0cWXOp1AmNU8I}IDdh~n);Bjx&az8TI#CL1Kr>K~_j8&e zN6SLO3?3&DIv|f`{S?9`=MDiqT6zC@`X%Y+V4d;Ed6h4i!>tR^{$p#Ccq`GzxiW@t zPIjqCncya~(O&r6062xH#y@ggq>O7?Fc79x-Aw>EMX|=eO+yHLZcwSqzx;V1lkJA8 zn$dzvZz_RIGju6&&1DdUGLIS*{W1o?nlk{_3<4H*Tn4aa2!J&^|HYbigOcAyL3+-q zy43BaSCx@u*7e&KFgxaM^u3_zjnYo^`w%MpM2a3u1+uhWrM+L?b~fX$8W}#;{rs-% z*$O)s-^38#R?TG^T~$mTiRl}`!F%V3HFgxcpqdiGtPB9gZ}U9B_??4EC%YlWr}qaK zvR+9Tt-!^p-LwGTox=SXMr)Q?0!(urVY=2U^B>ErhylQ=FJjb1;NUxReQAFzuj+5u zL5Ht_?B@{aYWGrKM!UKkY2eX2qXTZ=#b5z;owQym7sGZ!WE!1QYaCUAwt-dW7~^3G z?++| z;%*vjhCCh8k$5UwfuN+B2u|T>s1IT4jEp2+&}}5Az{{E@avQOGJXJKv(lqPjn*KkI zL5AHkMc|Iu1>Hzp2}5T=j68k;+;y45(c@^$B=rDy9d~t?cg{f*(H-Fa2N4?8VnI zvJTM2yWrvZFE2K;ig(?6%M{EyVb>YoYw@>NC++Tvt4glq(wkHgm0P+j1vEaP+@X}C zfB<^~;vO-4Y7i`v!*4-j0S9z5H+gs$95+6H!K3!&hbnl_@W6s&=hZ2DTGf<)QqUPDq!I^Sgy4}_=PAi z{9-YwTgG!1(7a-*-aA%6H&j;K7F23p31o`CONn!`gD76nk&x6F#0ko@B(?HNi=d$e zOTqMm+}L%biD&@rq`-e>A<5VG6*0a;7ic-oL7$Ok%7|<+9&dy=eEZEGE5~CGQJ#vK zBq?Nkn3Zt6Al&@3rcId!6scGZqI?x2sc~o;d!#F7xm!AL>Mgi+{d<@+C^RB9yhkjN zTXW0tiQNY4BmnCjG(!Gc8U*_bMNz;2e-&#@Gr{{8ZXs0uQqa6=17HK1V=TUefpkXi zCnq~Aa3CmUSilr4gY>B3Qv+d00NkTu4p=y z8XzD;E8rd^fTsl$C^}(w-!Z=r94H#lS1Q2o4bZ@FIo0id{N7r?0AgdqqXR_;%--t& z6x*LB0H5~{AcLr^{a}n%aKKn_0N+5o9)S9daU0^gxG$vv-=C8Eev5Z534;5tOpD{u z^@0dAfWZJo%Ig3L@QEIqofL+qifny*Z0qOxokBavqXxvGfyMyzYTPo5ZVYyQ@ zmhOP^sNIJJ@&r0OM<>(<(qMcB0cz;i3}#9uz!FA9e9xx}G4rU)!_I*S*K}bx#i~NY z>uUh<*RPsPu?6?H9oGSp_}-rFSqcp;qPSyVU78F`150kgK!e@uWsH&E~w{`BKkDry%1cm&0hNWR5ai&($9 z&ZiNowz0`Bw5Q9?#H{t^VLs^eLu-+}&|vk!2>RZZA@wF^HA9|sK2Ls(3|CX@#IZ-I zBT`tpwrwy~;I)ZaLAn)+lkCy!2$1Ev2T#}RxA$9V0w}dWOdyrB+Y>JWJQyYLm+tgIO|;^YOz9`6Lnsi}KjggpJEFsXN{<5+$^lfx;9z8|tY@b;_g7E$JRyyri zP+ux>yR?_U%;g=ef^M$apvmWZH%m}BzboDyWApevG!*x72#K!hm?BOvMZmA52=ZB0 zs^FN@|C0sp(Dd$_hV}JY7+H6X&o@~F^RBER*Vp^)=ungOQ~d6FDvqGjA7K;u&HW-~ z!89w~Fj%}1mN<)?>%C1d{pyPt16@5QRp<=vJbif1NFwJLMPAwf^v6#noR^Vpd&Of# zRErY*5~ep6I0v9f!QmcWGd`0#Uiz=Y-r2dOMh~kxs`ck@13kH*39x;1Q7yxP4)OzA zDw1$#$bq-EdgJ4~nS-1yB5?z5^$9kN|E-*Ha`-jVE+FbtBajMur9w42? zfD#un$zNo$2UQ(F5K~hLzf=^(B2WlNj0rn55N7d?2MymM-@M&=@@e;lG2EhhgE~zT ze`0s_Uvd~dVP%7PiYIo7S*slO^OmEk4hx2eibTN6i4>KH+Kl>`@Quqes~y0mZ=!ch zZZ+n$vfq7RBoi|Y4aNA}fOkEkbCG78+C0p@iDWKk_~xTN=4Esa=F}!Np5IM=sMyUb z6h+Q%P4A^k(BN642Rg$D0cu~DxiT&=w|WR2_@3#tl8)81NIhH?99)nJWx*R4!6 zvN{JuoIfTMo^_Xs6dWQzlu5um%p*qkWRwcwu-1ks$`wtLAhz-rcyrYY#(eN0MJ&UT zsV71(zfmc5IWqx+8r)EeTQ^f8k+;-9<>T^T*f$_k#-i6D3?lMK)G1NwvqRsk1=(xcc=rQJaU46C$UDEu7u z-6ubQA`gKih>Ed*StgX}yf1toociV3q=kUhgxnbmdz5wI7>YUC5>u=aBpP8m^qUaP z?hd2fa3b}BZQEf{8eHKDIPzGYB~DhZ5Azyo#^g>NXEa<{L_Pa@lz%M#B4f0cnb+RD zaQL@CmU3~J_hdk=*HdITrea;JTA{&S?3?)QoSsMxf+jT+tbST?1w)y3MkCdLUppSm zYp@x>jFt`o4~e~{XhiUdfG^HyMEFzE!S`XrLEmDU)q>(YDIlk>Me1zUG$w4gb$EI2pe9y!n1q@%l;&sRtuMq zp*lUapv}Gu(>>y>xHs=&6VEZfai5 za>Le}4It@~7Wojl%$p>axU65GxXon=fDbD%5#1*HJQ~i96$q-Hbo1hVtsy+j1b4N> z#XB#E$E$5o`b*eWL0($~(tprv{qly2Q5zth7B@Z(dvV|$t=oDlr(Ly=C!v8bM)R3k z_AWmBDuX|eXNTl(5QO#MDUVbL-sdGxN9mf0Bc+hqA{j?J?~r9bAG*83$!X|Wu^qfu z<<46zJLOR+12ME~QimwFP7YVnR2k?{x%SZKXzR7bN%N~0dem(bY~HpHRRtHm7r6HA zF?NXk7&}%wPgo%<@(5RUot!triwmx=*6Iglto1CYdCSNW-2-bNWDZ=@m&58a%@k{( z^0^*rNK?ZSaD*1vKqm;aanu92WK%9S^PVJA-yR3CX_|PIoFE<33`74zae2W=nTFOQ z-`fp%4kmDOn4phvWv*nEL?jCQ&_qgAMn)6zJcpNq`%}yktfm*r2zVJKZs5#j*wK-9 zjsp+fDf!W5FGupQlm+gElr>h_v_T9Ae9cM`(^^n#drn5}X}4LM(>>}aR|Gqc2-sRB ztam<@nUE+!rxcDSRvuXb!<~t<#NSgJlT4V+VRJQ=bkp5=-PMp=ZZ)M7s5>sZww<#W|9bpiQ(le$&#b#~zG9Nk(_B zJrp6xr%`RDyMb3^WSJbH)AA$pfI92sTF6L=fWk!!I(`S)*FQIO3O(!4wTt&uF5qQ) z(!9ei{sp7nF!NV`i(^Px!lzO*IB3RPIlv02l0c%ZOYF ziL^{kE6F?CexSoSSf<(gj`VEgWD20-nmFPuA-gz@$|iZF4Vt&(62#lNlL!mPpc=cH z+d&B5nBTC*BW7t&6{Zbt-V#}DYa%6!vF|qBH*iPagAqPutI=<6a@~KwpP5sK0Q1WX z$?dDQl~I;1i0+dyZCRZ{ZDzboB#n61=2THTqziGrfmM!l_}S_=v>{nn?kfPvTapE2q8V^b%*f228`cTDXTwl3?E-5-Q6%bN!3Z5?DC{RUJ9s^vy~v5_A>8O(q*W zpWo72(X`0pRH?;NkG%>)1SJ$r3L^U;uduw>Cm7qy87@TKFgeSYt8-yuFQmN&KG1wS zMfogVm61cbeZS`e-sYR^uUGVx5v7{bqO2ic*d!|9T~B)RY6?l6a-=U?-Y}C~C|QbE zhK+;iIV5lhA5E4`oj=&wzHj@aO@7(caPHVe`E;7w0UgBQ*qjbp9T2g;H~&Oj4d1l)d5S~Rdh^U_?f2!pl%Yaw)~@lrU9}SQ@b|T!ZW=2?c``RRUEzN_g@lW1YBIb z{T(5rh@tK?qlwsr7;wfdmiMu?>XKei6GjgxIsD?9%Ju5Gd_BUrv_Epa-3q`iFC}Pb z$%J9QtX>r|U`E0YqcP`~~R{uf?Z7PN`+y8cV)4IS%#0pFR3-0z-a{x}RrVdhIkg`4^t*6jAZa-cy(n zZ`|1EfViP)d^c4V7TI8R2>Q+?PSG`EiumPWdI9&DZ5{GweX%^>#%OynL1ug-S9~7( zS)V0}vPBhNNmkiJM;#)DNzAoUdruNVMAgL-QITt{+};uGxCP(sN!t?rrSs8z7d)&U z+)B*{@Q8CTqFfk>ND=`6#Kk0bxaW}#Q9ytp3jtV^W#JcmCEp}RiGTnwh7uV*RF7;i zu-{z+>Zzk#sSw2+gk9YV7RGIwb4a@l@Y`_SFA*G}pfn8Tw8D@mLWuP;XpoqF@G=6- z9n_#5Aa5tOXKy{kag^QpfNR)TI_aG{m|n{gk$Fj{%$12{u|wD2$omr6hQ&%!-=*^W z=sr7oDGF~fA8$y}$VYyU1RE8@F6!fFH>DetN3myxO^H)dHu@of3!-+61e;-(i0S=AWH2YoHvi^0+0NfMeYHMBMksF%z*_ zuX9U#VjbJM@1MQld4cR^dpVX2`Q+Qh;4TrT0B7g&@k-?3jPzmwXXOe}H_Z2*O8@?L z3=LYlnQ1PcQV`(qIgJ_9)6{>>B(zrL>9r#ktcw(QP?qi)B>qqav}`WDa&CA2HK;Xj zrIxt^k7TfUhQe|egCtEAy-%j@uD*vl2`G!XV689JaSZe`8qAWw!lOOYZKx=qU9=V( z1J&>tLBBOfrO=+rhD_i65KmD&CfqCRxq?}npeP=?{Qh6lFf^YPGYae7uKYGD&)Gowv{eWW3m$o0D4)30$2z&?3{0gRf$M}s| z-x#JB_4OxamrpssV1qU;uXvZ#lTY3fg%91)RRr3Rd9@H_f(3;I0fDC& z2vIQvA%PpeN`~Ou2=I~xz2bab>P?9FX|$2xs>U>fHeYDn_A)B=PRLh`DHrBn@GJm{ zV8ac3UD^5JRDh_0t+E(Z>hf@cUESbU;_!9#ye3P3%2pGGZf=H3^!ZWwlw$kR(Z3c5 zftQKlL$RW5i^qj&q#f-RxOcKV5^0|^>U*5 z#1T}h$#!4{n2A%fID<S0G2$5Ym3nUBe!ZD?m_*Uzpy z&N|zW>h~8X(V=2n5@z6qduR*fl#QxYEvYOYALk`Q{h5YTpjogb4k{*PG98$jBT7@MJbzg>$Ha86%l#9QO2Nv-5BB)MyZWVG4>CadV3}OQj}_>; z)UyevUXQ?GIDJ_=ny}8G2TCBuz-Yu|11>48bH}rx9(rw*74EL+?ZOPpsy;8u?0dAb zkDKVcYQX$)I%&eF`sNV~RCn%GopTc?wu{^5hgtnWVQ41!JYR{dMXb%TQxs;&7kB7n zP1=lE`zp~PJkPX^;qMSpp-n*isIjQ3bd2ZejQrBveG`87fzY%)zEPbY680&?= zeEUe;!0&&a3~X#m6_0pdh!*u97K{i4)IvUG)f>C&71Yv99%8v{#>re;!maW&*S4W< zc(60Lov;T4(W}Ywk`5G~2mR=Py!Dz?+Tb$8>+GHFX3d%HdV(?oqOc$VFD3bInN{#$ z)yvgH7wRrU%>sALNx_ANt+&SlrNe$-iu2`yi9HiA_C%lpGeMlXj$Gk)x-zhmiZ9OX*<{EK{>Sb#hKQ+)MHEGjO)Eqh$fA}+Cw zcvE1lO6UOjRg8|Og53Q=z;#uo^T2SA?(29MrVFo;y(3GA>d%uT3VJH=#hk?_2;6Sp z_h?#N8KkHSyH#-YWn`>njuixwC^LPqVO#@~LJJ0K8R}bx zK^S$fm%Yf+A~W`|aAEvWd4CXlC`I0LNbOZ|=uY~6SFT)Xpm{oCk=zP(mx}pHe2rD)&4qnq%S{@Vc4O*Fm77Nq zUYxHubO_E0<%%5GKk{>7+b;QQMdNB{L}B0BP;v!`s!*-q0gf{$ab`Lc70 ze4LgjX}yY74(FP?xf8tEK7&Ciy=}WTs9b1~D9x0hlu&X8Wy8)l{m*qDkt|Q>&AY$L zB(d>JwQeuQ3=1>q-T64%ok~QNflNy#PQw$|%0@FQp2u^ZjNNIp+eM!e5K%f?f91`q zGixtX=-RALk`Z7@K`;_9MA~vSTKZuo3vbRJefi8?$AyNgQc!EvB8iM0YNHvo(o3x?uCso zZ-=$epcZ(B04t4-iF`LQ`>oK#H)Q-s9NUrYxBp;N=wr9fLw1Z4)c*2d!uRQTO;cM| z)uU%12)*A~Tt(X#vr!Y{Q zU-D_`m)|*o^0^Ek)6N!8MHXvyw`847S?5xCNPZ|8^&z3D@fp(N)dPiG@)w$5MYg0; zZr*uKyqOmw5jQVGM(P$t2k%$~8eIZ?*1IuxeRhG23=!CZo2q(oD@<4G>xf-PXTd{^ zY-0-{SDRxOCR=-%E)%=l`H#BhU*OZy)=1_&kHjCoAv-Q3j|>-3D(XV|^Trm1f~(XX z&RE5CJFb|rwUlv>q2*?Li>vu}bil-iS0mxV*Cn@E_DZ|;M)=FN zdZpimc=&`~W@-P2(A&S)c>hnKHx5>M7S6vQDOlMF*_Z+7QOtl}jPzXW{}g^>WB{$8M4l?a(BOPA7{6;8J`&0AMelg99KQL&>vIxutm(st3${er(J}7+;T^&@O{CD zyGO_4;ha8SIy*053(RF{Wtf3GH3pva>Ct&90yZtHAGCU`{x9aV5)2FNJc9+dO$t=v|f4Ifo?h*d2wfs2| z?ds6M??*RgFid&=HK7qwtjEu1PQ790hav5QI>*lHD=QL5fo2UVMdYA1{lNGkpkZQ= z!&UCg<(5=P9PM#19@~7dfmKkS|2c#+xN8C zys85NE87`*zffv#hb)u_821CA1chs1V}c7$m6xo;#*3eO*-blx?sfX6pnQ|+7m{>0 z0hLI0*wESX6U?NwRo z%tl+*_FH%#u_H;+UxBNA^Cxdq%AR0;^$=J0*7OX=3y^Fl-M2{T!I@JTX2TRLl=H00 z?C=D<49D0pgb*T~W>8@Y8GNXGoBED3y=Qeh8J1OBTj&>PMmA7}azE@3Gyil~AB_P1 zPQzXW31USatf6TX33-Hj_V0iQSn8vli3Cu|7NRIwrz2l?=8^Zd-57imk1NdiGU;Rl zpaNNmdq0BhV^ft?S4LX9qH@sH0#M>5s`Y20cPsLC=h+=X!%Bl~Y;bZw*o|4C#5qsl zhA&vTZ*Wp=-%_`$3p)XTN>0m-Rn#`n||k<7aGu$hxAtliP3ldl;(}72b`M z36~U*|3V6k32!V{p4yZ6%_)wA0JK1uf!`K0JTC&8w$v$C8KMH+J7Y#X){Kxn3g_z=3N!lzIdxn* zMSLc=h=p&Bmx^`1jF{C{pOge`Io$TX0cLUAvw1o9V*13?3rrMTX!jm1}3VZx@~D%g!6Fw?^?!6*xI$&cjU3G z?>=xe5$3L_{z%kL#f>|ny(R~n+wi%rdR7mzkl*LGlz_;N=KID@YY7fvEuZ4w4TM&N zXuCtMjMj7$a~}F~XV9DOx!L1EF!0MoBo<&}UKrEP-0<@PD{HxfWNk#RZLV(SW*quzR%Xq)e_I7#%oLi_wghZpm zg@>?@Wmr?Ny#Y}|G$4v6sSx0La$5PLP-k!TRdn*Ynf}aNk4GUIk`Mn@^^subOX26& zb4Vzir5qnG5mAT7AMDU?0n_LKFb+wKA@fj&4<}I`*PLXzP#v6aelBDq(&d7VqS2y zZ?>TkQsNRohf9xcNuX8aH&=Q+(wJF-i@&aAabJgi!t4q+{kq8j4c&kzl0F}lf+ibdjVcJb6+q~DP{sa*p;Rj7?9gxGDH zO2Ko*VSSt=i{*GWbv`t{gX6=0?)FKaj@|1SLA1cJHnt{q5f`AWm3{Ig0oX}zVY?&U1rTaXUlnn7Ysy=LQs&<@1E5qq^FQFf3QN59ohLlS0 za8U-k0DSxC?X{9V*tN)(2Z`SeX{5g}Ih+WONK!f!sdpNRTZFaas@h{*3yQ$1E~bW} zq@59Pms(t;v)Io27dE>(I1nqy;G$JJM_ za|g1T!B&?^)u%SU6SHKNkG=Q>MMc&-^in?KN>0^OZ(ot3xSEugsLA=#sl(ZT7B-Zs z)ZF;ZRRK!!-o}`{6>H-?z;S)#`RHBFy^oUga&4x=r-QrKTKfmOd5Ih`KQFPYX6&Iq zq;WkeLv(LrcQgD?rdBn4@QaD5%cq=QI7-p@As-$Ox3Te%@)JBYr=4p)T1(-6>mD-_QIR z{OQLSZryRv)99#p^gZL^SbVOg#~aWBs67*P76%=8Ok}$5s>aX7^=;?B( zH=w&YHoXT>-y#-bvNRcw{L4@Z>7sDf?|cpeXII_IOQYMz&}6+>IaEwwBOlNb!_n5; zgh3cbS&b!I=v9pza^DYh6^>kyX|3{Il(dSdUp!Z9Ojdm>PAczBq%b$fTFzlWgmdD7 zn?;-pt-+1*bUyU`(G0oU*5~k^3&(m1*fN+T&G#-YF0o(gK#l5a_0+if62=!hS=yMy zs8178kvgCk@`V@L8vuf@l!$JA5e~oyP|PsUR^;c1s0b;F3PV1k@D#aFL=1K)7S9pD zwqi(Us_yEwM8Hv+9!QqX3^>Gi8O}}!=_oM=>$dFC6hz-tQt`ri(oTOmEr1*hvk6p@ zrQ~-!!EtpwCl!j>m0M5(a&8qjkz8A_65{O-hs&KO=~I&l&^#s2fEPDKmFF(T0;hLI zsq%({Z3WM!^DhwWetilH?Xc?Ncptt56I=k%I=67$9+DGWen4#pxPDIh6JC2qMVDM( zdvd1{tn4k_DsU7ujTM)(O#Lq9WuzlI`ik{;@&*?SUJL4s67`&3I=5B^o};HCRV9iq zNvWdX`$jO!7}=!Y=BL6hMz^*n87Y&pnhmz2UB+G3gqLNvW2Gb&dOJ%_h3A7#G39c? zv2Md#WR;B9u%?VWHQlZ33AFf_pV|@lD6&0vtt(bddkv~l+rtkhbU@oJWI(%d(-(_$ zm_RJgqH>j1Eemuzigp1*xih*!8l?Gr8iXf?ipF%x1 z(P6SwW}rrj*wMW7kduNXLl5qP9B|dw8U4{l`#l^t#ZN4`j!hj{WL*US++4w4hdE-~ z<-Tv-1-DA>raq=p1N>7umt^JHP6HZ!MX17o!_+Qff)`IFiRc31*PDHU9R?P3Yz2;i zbB(^BH%r$tmd~QuE^dZ(F2=hbD|#q?YY#;~UDesy44x0V#MH|HW4Z%wkyRxzSAE#c z+DP>_E`JYjl|mAJi<3%m@ZkUT@+-;(If5z%*!;Gt4PMr6T?6-w7@U=ZvBW0Cf-Ur~ zU$kmpf*Ze}$wasE#6W&+#X(^+_)56xQ zlZNfoW>zO2s5@x#+uBY7UCcHc#%3Qs*znW#9xt2r9%w zigKbl$9M1<8W5iuiysJ07kNRLVFw%WI0e3B((s}^hbsvnTN%wXPcGrDRzs)!1)DL< zXfvUzxa>z0GT^|HxBZ#^Y|5$GlPxv6^K|g!I#N`1%c`Vv)@1&*y?aK7jxR-5J;yds zs^30(!V5M~9HLf4?esC+gA{g~H*A;qP19sGuL9n*OM5>P;4qiC=YGtwiL!H;PJnK~ zlmWi3(%Yn0*`^KO>9+8cfO$>VH|31=kxf1jQI$<;s)jQie`TP(xJ{!k4uzc`w69=~6-SLyo5}nkC+` zm8vS=T%KVEe>lN<5RDwCmLs;oLDJs$_f+ojjj=Juk;|T~G&-^Iy}F-OghIw~n=*6n zsvJ5yF`cv`ZY^~CN#r29ky3Vyu+QVLU_AsTwFpe{y-Uz+>p8pR7Mf(USgsmNlsdM+ zm%abFJfZ)&k~~{;$3N9*ced{btQ^8`LiPcCarwtZ3bwA9kk-^<8*rMFOiUGzML2d> zf8?wO(XHwb@$4wBKv(ln`;#mvuogO(kim|&0*?m$+cl4n2`Ids{!UlTw*u_LVZ=QiGLA=Ia0A6h>4^^6>i`@goKC7CH@odQJ z>%WwQwCChsscD^};H8q{KGFozVV1*x{3r(}Y=w_mX6-~=eUO*UB*xrbPF~*=oE?%W z8TQ!6^}Rgse1JX?$u7xYp~3&_#8JSqP?%$ziH8 zW`Q;P%z>f0X*S?(OUG&tozp|WC{W1>>ZF}KG)5=470on>=pdK;okR&9nZ5kfsP$vH z`XB-1!7Y1Pd7K}ci1+L)&|6g6kR|I~n~&E513zTJ(Os;kah(g@{1qmWMZ0k`B{gaw zFm#3ep2D_8S42y?zLi=h4v`EO&>x|YV>I$ACGf}74Xt&jks6Yb8J1n2IQ_0|zxS#0 z8p_VGC0Dz1Kzzx~NW*HD%rD2^bAn1o?L_7r2AI70Q024_v}fZ3gy%9}8$yHo!gU1bs3x_Uy2c+?Y*VjuUh&-r=!xZ2Ko4UJ}@_B=qR6HGg;+< z{_2#?d{aSXi&nRNBM#}MzPNt{2AeGk50@|fw!-konZ|8m^9b2+zw!x}r3OnO?umpX z7m#~(vMb0|6xI2bQu>axIBxGTZUcz9B!!LvwGkCeVF#Y~4k|0Cq>%lzl4Pn-llga} zS}jer-*tpPXCzW{6U~Bz$W6eu<_;r#ggBi#nHKW0Fm?lo(VaNC?s|@EgcdS70+lQ# z-~=SbLska6dL`=N^B%t0BiR7(J;a*>+duGzFW~GYEYTarMDJGl!ow_R^I{A8-FmCj zDq{uAR^Kdaj#wPFs*>&)=2#~k~WIt`qg@&nik|Z%ozFm3_gYPH`}`{6LvU5cK6EUkBtAUOgSa& z6Oh}(e#-lvo~I*5tGH5}9anTSJX0K1s16lB2NY=L95)aGmvr5?U95Wwvv~wS-t`{K5Liu4`U1c5nSPt1{`XJK8U8lU`5)#Fv;4OvD>M9&>io|nD;w!! zfadF?L|6lR5|HYa83zC%?nf}g?QyBYYMi4*<9-=kp z_K%q)&eq(Vdol0`eMWb zh?9ZC$zTR9sw`Zt6O#!}59E1JbozqgS7YS=4r9)tD4$mZ`$UYRpgb{eDTRo&f52g2 znPx-&afi|z?%{I9Xm^m%<7%9rz~ADQ@ls+WC)?ISpM^(S@sPhC9C%JAQr@Xheo{P90GPci;|2=>3NLk#z?I!yS}I{e!h z`B#n) zRx{GCIQH{%g2Vt-0j|2K*zCn}m2-=v8)1!dQ$U!w0b9`+zYC4=rds!kVP$j#-4XtJ zEMdP!H${6$)mDl1_$qP#}?;^|FGUzWphOHI@+YZPyz0dt8<@mQg zzW?Hf_D|kE!Orl9qwl}4EdGOX{9C`^e{p92igILQ`nT|~Fs?%qSOCHAP!?f^h(NWb z982iaZbSOn1*3wg*&%UV%klUDXO{$C|0h#A>H6E)#Q`IO|@!- zwM3r%ZK$EEEqVr{7+3X<<+g(>AHQ>F;d6~s^r)uhAihMsjRla@X5}Y2b6(kldp3Tv z7_p>zqLo96xP~oUElHxSQ`o~=-k&`Abca0fzs%s){{Ve6 zva!-Ju>Y@zGBJI@=&!f%Pdt>F^=ni3`-lD`^v%FV$I42;z)t_iOqrM&=)MMF;P^7y zGXL33{}br@%d-2wqwN22(=h&b?oNz<)gb);-<`0&cBke)+nxS0`hWUc66=3Bvwy{M zz)Jt`E(b$Helii_-Amy?5P+vSKYPvCYih;Qe}jw1$|@E#*0MI}A5*KFQ%Q{lB6JZD zk-RuP(zP*xlSM3ytuU^E%lRw8oq#2m zuBDa`*=<9gD4U-6^9ol+&D*8^HL;bWq1{F;3nN|a>8Kg}nVObQmNTB^SWl~GoA%#d z0lqXQ|GPr}yjGamzxX^aER>V8qltkHER=i31n!qOWB7Vqp4Us(Mb=<)@;AnO!+7CT znM5h#b47RTV?k1h`byo-r_0T}{V{aCnD&eNufLn`LSn&GzmIMn_MveHuD;`%Xckhw zaWYU%+iv=_Sq;O>8J-3V610s zsy%$6fOvjRNQ~V>{h`uvWov=syWY8V%gHTtrs>bUJ>O-8=^)sql#@WV>H4=&YJjdG zdY)afCf2x9DYdm}z?6q%TW+glhBzeuP-d`!*0g_Ab#>c- z!8_S6upC=Z#RN9-Y=Fs9Gr5-&fIMRuiOg`!szirRd~47tzLb*Oy-E7E-mNFm?t1qE z8ASv+wBP!CfW`X^N=lNJZv3$M+}o@n(?##_D#Gvzcp1f3nx5`!<4bkO;#`>sOD7{6 z7R5SxgphG5U|}Ck{qzdF5Ng%==?$KZqysu5?wE*1$oxSLK14cdGR3b7G2w_FaEs5u zUaI2o)AoDzY0CT-t~hN6fy3ui8AFIvec)ZJ|Cchu;whd9ZQ)Evt_BA6%m4jAzv%ja zkQ<`|;8~G^np0TdYJWb^&af_L-A`J<8nPfFk4W!bhGnAWm_;p0?c-}9?O1%n3=&$7 ziPbd~O^xELfbb>ExNI&Fro*vc5$N{u&Eu3|umFJ5oLKcBJh@Opl`q6qv`mNM`m%qI zT9fTc7n%l*x2-+{m2_yT$(d79IX|YW&47Kh^x2h-L}3u-(OM`0#C{*{k*KG-LCAQa zPVEZLq%$-MtCS~eu3dDrS6tEw?tWufe6|LD(=^t1RjQWm?gWS6?yR-Z^pk4~ZR$TX zcM4rpnE_S=;_EDgE}MAg9)j33e~L>)?DB=B>Nnf5gQ$a_Qu&5ASy zB*oUDbS@vG5ZDtFvt33bDYPeKZ{9KiIi0O;wAz!F*?!!6e}?b{@~b0Q-d{*PQYRJg zj5DXR^E8AsfE?RL=2U4ER9JRwk6!@r#d=VF5EeB~dAr>a(-kn|@CYWVGZc0Zpr#Rf z8eAy`)r@fH2hysfCLt|Dkz8dtMzWT4^wdH&%4OSx&{F^v)j>!i0!2p_taHRr=tXvX(E?o0lEE%$+ky(}hvEu)(l`~9UpGpR;I zsZFLW7Vz@%DhLePakciR6Tt81b#$3lfC=77B9o+oPp8^pVZ^;7m{lZ^DhQQ1V^ig| z&!zT7RxR+8{A1*HN9~%vbG^*I!62d0dTGWf`k`(JHx?B*fvxZ9VGQC>iAA;PW5?rH zgiQK_duJ)hm_wDmGXNusO2T)VhpJZa%^<%BA#&u=!eP)02J_b22BBWshHjh^v5`AU z^O2{U5WQ=xq?|xwgCr6VoiL3ELL|j~C`Q#^RQ+)y7P3RLwe2@r!JymMrWY({+9T*j z$^u$s&3=V*RWbyC$`f+o3V1N>81?fzIw_2x9(Ygl_EVw>hS*!*S)hI5TwYQbp8%RkSuDM`Zv3ICzKg396Eb~@i)@8b!mFo%GM7zw&ZmQD z#Ojz4qMf4gPCs;hL}c>(WCQ0l2c@?zT&*-Og4~vrVD;)Qv0bkJ5w{u-1R-k+cWp{b zs1MyeLJ3{HM}tz4dBTn|Wfat*L0m}TRM+pv(wgg+-m5-JRlYjDg9`fYJp*>xU&E;l zf)|XWo%8)_m9+nUq%!XJpsDTJ45WYvA6nG127(ufBAMalTuO#a&|t_TQXzqHa=z}F z|C> zMn_*>>vAYu5XcHMS-28XhHZ0!FB;n6n;!G>hu2!7R z4@;=xFo6I_NZ>{pv#C1s_<*@pTy>scgT)EQ2kOLIO1XYiraNIWy#7cplfXQ=D{vcz zK%E_%bRE6lk8KS7dy~aAL*k{BMAg5GQeJ~(+jg=l$r|L2+olj7Jr$yE50~WeH-ND@ zTg>LHgx-UomZs-&r?`~ph6*tcFhfdy?jCUJyYLN_UG&0gh(4Ilo3N#OPWq^Ep{#Wr zF_UY}3q5GftBN{$&tZ#C?y+K4?L94{{=`Wpn4cu&s8*}~9!rt~pA3jCY*ji`xgHP1 zOg0%K1;{C7#`i+mEldU)<~9lH-oyvG6zZrPavp%~uX=w|nm0H!=IO%st-E%3%fva4 z(Th`eXBqL^JNONirYTR%c7@@ox(TSf6gAI`Wsp;#EH<+!FLIXMZQygU$6Z>)JwloM zoDnIpo>dR;h0s2#8(v471ry-V{{5cd5XM{PdMM$sV@EZu!wtqrlha}6@`U~zv1yI<-gT-mM72j>lvsTRH+v3vboGk!7s1T8@ zKJuCpzS6DS?|@>|Uv6nbSEjMAoRY;I;C7T|Q+6ndPl&0ctEy6=bY!;Oq6nrTxWXI8 zg_4)lw}PXYk)DP*@_EIb{4VBfjyHlgv`H8rF*Y~_@N@=fw|>T>^~G4A5Ehl{CJVeh zal{>28ty_*j%y{hDl3;XyP}mR+JM%%8WxV*m3<{w;$`Qe3Y>56L--Y@10e7M{)0sW2kY;%|L+@}eo=e#^7 z%k+ydkz1ByS#;@l^036>bj_8rC>i zJ-WW#XRql5wOG9JpAliK%$|T*+jl~_e$AI~FKm zCESRgA|gZ5R4rF6e&$qgz47tcK=6lPTi#Be(^<0f>dQJg>eCeE`$%GsF=N;`_3{~w zE!oBA5?_hma{j@;9bttzWl9K-)`Et1M@4@2oE4lj7zaic_3-+->$pYg`xE5*%lo=3 zmlTf*cMQ_<3%WdKK!f6h>CQ6)gI}$Pn`Sa);36p+^&qc>GYYVyxCE-5PX^iKU4lG< zAh?;myjj&Z<^+lAM`zq|s@)HnE@z2I;@L(guA1yEzgtbCxqUPs*rX&hxNMJF8s6W6 zQu3%15=FR5djR@64Fyl+uO~&4$krGYZ92w|)N5~LW-dt_%8!ZQk6ss7E}TM53<*gn zF<+jV+=a;X1su~1cz(-}Tswvsjja5E2t0#ovmAHeE&fT1r-AuPyym=&!dnNmO8B-P z0T}e^=i2+Me-%K+ES%vy7ddJftHUO?R1L~{?5=Tx)9g@l0NC>LcKeqzNJ+8?>qfco zWiD7|LH}EZKUk#eYi6d=y1aCxM7tda`U6(g(Tnq#%dhjRd2G5R+fiFRbe)Y{sPaXf zN>7s4lxE+)?1T;|;`cY|!-N)N@8INmpM9OdAK>K9#BOhr?gHtr-_CstqL(+P&YPDc zAYIDpZOoYExjY)qW9&pFF=yeTW0Uys)4^`YS31ytLVG+*v7otz*t@oZ(JKQmoRHI6 zkcfVV3aL5)JCg=GD=0WpEwc59A4d|r@fk|W7>iKfnta(gv{9%sUopCa+yC_mP1pH0 zon!=lxKO=#XTxS$QaT@yBI50BuQlDyF-1f1Z5tekbcGXjtG->h1w(8eZrg7{Abk)= zxQ`VJ_`c|DuBD%7rK}&{_!FP46jj z55K#}s~`|_GPbczRQD!6)nFwOoA87B+-1EpUscchO!7!6v5 zPgZmFG0otRHg}&o%eM?bKoM#qa=I_mX$!jX=k}nrb0fnlToz?{@rbE>5gbTxHUOEp2 zogObM%$`)eImT97vv(@d{m8XG@JzH323dbNzOF3f$_~+>q7s0TgsK%Ny65&;wPxQr zsfW+bXwfa_54qUVW0bodOhtKSuRmT=qIbba06RU5^-lBv-(yfQZHa|qSKKncno?PY ztLAU7#r2z2X~ef!^z$l0j_g5aMle(6m6`P2fw78tuH?_Gi5f`i7VFO?MW6-C(BamU z+B&;k`eI&$pd#@#VpZ1KkWoswT5iUa(cdy=A+rflLhMb>bG`dv=ozHDOY%GT@G@cV6q%tXvB;V(LYiIX)LRdiL7Z;7E1 zBxxydS1P)(j#zFN$BgSHFr9`)G_gLge7Xko_@0teWx}baB+($IYHL|fGe%gKsV+;Bilf&9nvYeBybUCOP;V%9nBdWP{7`GnO-^cf? z^Hkno0^H3cZMR=)Cf>w-r*4|ivAi0>em-sDoR|mf7Uos1nrZo#PXPuVc!!ezlZXVr zZ98E#7WV_JFiGzk?t)w(j3a;(;`;pug$7MRvSaJmk3nt-9XkDf1?__^Pq*?!6-`Jo z?W=_AJ5)KgQQl=-O_Q03h2@6AhI)xL_0Pa^lyTX>v6xw*?(b>uG~m-Svx7|lQ%-&$ zTui$N7|-%Oz^2^{0$D9ei!3^1(Oe=A#j{i3X5#!aqpRW6Ts*f$MxF)qyg>yRa(C}; zGRO*hIjJyOXTZ~BFSz(P8R(eW^Kp!?1?~ zhpoQCaJ+&k63-}0Eo;heVKT9>wjae@h;WP$S7f=s7-F1=~VJWp4Cf z@GVz<6`wiBVP3J=Qu3(2ZN~mwCK*UOR9ReSw&u0!-NkvK70bA_X}I9|0d zKS*7*-x}fTuXbg{3CACP@Nf(VKgT=f=`M2IF#{PFp8%|U%e0S@W=tprPM#U61ay4O zzdGl_)g`Z)&<=th5ocb*&~50TFv={fYQ(;ZsLs|gC}l`ICf7wChfLnBHs@#uwvYIo zLVdgrZE6eD9{sQree&mP-az}V>cjV+#cYj#a=`X~5l}MzO9ACyM*q(r0sn{S^^0)-rxqR4Y2|j+5b8;HE^9`eA zC_(|eySF7&I<-2g2W3~m*+fY#;X5Ji6L!|nT`JWs=n|^xAPns$Bd@$~o*BF25yzmmda|k7s z4A^CQMuQJTjY7Rdmt%)Nsn-9C{F3os$}j&i`v1$-%Jy&1*9&7ABtZlaM6AGc(8f^~ zhS!kY0f-U%fx~tH$J>JliUPn)Bf_xLN?P;{382X$v-DwHT+FiO*Ok^+YxVW}ra$Ma z_|{p7Z>m&)e?Y=_?=^6u(3Z2i)1OKh&@hH5E!CZ{GH}C_hwHmTYXM$tVSwFKTog5+ zz?S+9*N%ZKxq(+;KfgCzx(t|5xME}!Tb-Vm*028ly6fHB&O}2JZNbXf7zzD-1GsHF z;KHe9+l{@LASoxV$~uJ7;IN!*xxVbs>jRGO7Ok!U)r=NLFYmU>gS8?Fg^O?7nd^OFR9qGrgxE>R5Ez_2Go-@=_&qLTR)l=tQ4qz(BNf z@INWpf5He_JeqqEfqQ}bm2f}1#`SP?f{u$U~;rL_7|BqoZvwk(j-{C~Yzh#*J z80`IrYu`W1`5FH}ng24*|CiDKAMO(t*1zkMxGq^B0ff*{CO|>!_8>H@te%jhjVrj$ zz+lxStPmeiE#8M8ZB3D4188lx>CZlO@vF!jR_FdAgm%vk z+u|zrMDshVgDhupxZHgmw`*J5VC|JWuwBURKenY+UN`bIJ)Qz9P0P(s324D)64N-; z#4BE&-{alwv_M`f4D-g*|{P%@81XJ?ZJ6xPzop+pM{gnhW!|a|%uH zTv?WwC5HfrZLQyvm*E~e{Zk7U3kcerm>qB8pz%poZrzTeJJheVbWtdT zU#ZNR6zHsqUKeFfo1oC;sr2?))mMzy>7<89-bpw(96*X+(AQq()>IwjS)QqRKCIT( z$K0Y!LZ>yx&NcdmySTgShVvs|6p5siel2!z(+o2z=%`jUp1J?~9m4dt-^%|Sd$ar> z>E{DqnFMR>f5{}csB>tIrBv*F-m^*qR>NhB5d6?vjK!o%~u{`;3b z-%ky&j3_g~Oz@(0SeoR9w|i5slY%kUl#^1KPC83GzHe1nRq)if&G&idu!|%;`I|4_ zjobGQ_vf81-=7ksW!x=KH#fmwp>bW(P?R?~l?$oEV|zPnPs^LUXe0N1W9Sx+GMAE% z7k1zD-fG2@J|*cs4*x>Fvmrj@K17ldiC97rc~9IFf>hFG=l@E+l}kl5aYQdmHbrKb zI8iyX{dB1m-}FeCn&sd=(NtXLz*^6z68t`Kz}x{QDU9{;T_7+sBN_e1?Z%KbXngfX zEo-#0FrLgv92J^P2%&I;y?VilB|)pjU?vw$YUrgp5Sp74U`wQ<>A%Q#5vijmf-Ddl zXk*)(iNkX@xcq@&5~3fuy&C8d!MO5G&v<=F9yYtCW}baUQW|cIX!w|sJ)q_}ExYJb#3Q1hAR^g?JTk*@04(C>mG}ROY z_#Z9KgLJMmDp4Bl-quQ_h)21>LhCW9x~$BgFlA7hu^75}kPA`qr7gO*v$9J>6#v8BB8`mzNng;#GT(?#lwT>(aOyUiqCZ!YP2>X54gZn!kk4w ziaQ{R1iTUt1%6_dI(UT^@JAM+q2IrsD@xh}rCyDC95ZUbv|f5nz(-zh;mzFg|4eZ9jX2PUor~A|0JT9lan!O4nuYMbt=d*7(;;aN&&wg^@yG4f} zN)=ff@9WyW8?Er-h$s^Pn6o7!U|_sPB&&=Atc|IX61{~Jtm#j-XHOD zU*_^BVZbwj$oXNCx5fmdsR3jrYh9kxa5F!3uQ6&QG4jbu%O zP6i;edq39|x_e8R4USG>3vib3QP_IIH^aVA`6N@KWS^jEZ8b>^;EndW!SuVw7o?h{ zb;SeFxofgmRvY+Xtla0HOo_weZ*6h@IXVh&^dKiS33aX)mS?@vPz6PHL zz`B&Va|T~oy4=4Ur89B;mXyjRhfDFi<^X&n1vY7C=Xx;)UV!QK#{4Se);Y?c>Ex94K8_W8NV681&dFSH0v!R zNi|)~IsU!WLqHYdZ905J-;X($TFQL2)xP}07~IbGDrUy(xA z-n%VrxLROywSSw5ZZ<&>4Uzublsl8YS8#P5sgJlT7~8XTwdr3no^pB>-&kI5wH}-IK3$Oguan`l|#zC z8^n*?m#no06va)a*+nZm`k$to$3D?MxQ`St#U32%4OJ?J66f7E8ZdIZ=~cKUHOYmMo~4K^%cBnZQa8aVGLGp4_b}HJ-g8Dt2!5m$)6R^jL&4Fwr|$3{$mi=sj@u? zU&ry74ckYFGSW7wF#2knX)9^XSL&8l%!=)$6*F6WJmn)9W8Zs$`zOl7to{q{Cmz)lN%-v zGggjTAt%?_4>&H$#nq}3iJnZM`E1$~+OA!+J7TaHY%AdNGpMy!Z_;2iFEG?0YnwFh z4hzqHu0-hC(uEiWrH7((+lIxJ15d7LsgbRC?`!<;Hpf(W0Zr58Ce}P>IEX<^dk_mD zWwF%hlZe4seIB!v7y(Q7a0O!@7^} z=?#(vYjZ!}((_hy11>PJQv65=Vi1+H^8+QMrXfUN0(~1}L*DYa~t<-)!hY>b82j?PD=ldt9C5sCv$uo>6|p4T9=tkIc*A`Cx~hE>}xMQMG&V^YDK@siH`MoTkdZ*BHpau61QLmfeuC3xe_LRyC zVm5qNPK!pY#+rF75FLe3d5i+{McsZjr13eLh&87mB9#&0E1eXG6c-Jr_iwPlXq=&o zDRRHtl1P>-$Cjd*0znn?Wo38qi0C-R{xtN8ZaqYrMJ(YVd$8-U3*K~IwPwY6bJ?n1;WM(U@aGodcb6aU z;rFh|OFK8jo+`{cUYE4fY2LqIZ90D0WR8C~fIa0ymq{GQ5`R0n9&L(XOqxt6JHjca z$HZ7S;SO#L>nY6?!k8xIrN45#D3&U#1LtX8O+%wdO zo$>B0Foevj@th67`7MabD2Gcr%==}EG?C#n`Z84?*;1YqQMAvpnQJnlYl;uXA*W4! z)yfo|tg6kKuzoqvX!Rj^>s_nKRVG%yk=>^&D7wS$_x6_=I(3d~?IPXfwkPBcyJhnG zoO0b~qP31-=@Yf0eN)L5D;EtO`6ZTrZg=}k+)|@ZY#43S<${*5;_H0bs zP={C%88at0r}eYNUWkmLWNddXE%Ot=Vf*D1ZJ)>;16?~{ROF3qL8G)imkR$>LsRo_Z4 zEKlbOLKF#PV76!j6l7$QNk){T*SxTb)NHG-y<59aGXOHYC;J{xpY6>6l#_!0<_ahFYwZPar=PCc2!>?%EdLHAtb`OH>))U&6Qy=Hn31a zrMI?PBdIIb6rED&s*@J;) zm`V3xZ0>=F5?EuXRvYH48*{pDjLd9u_FJteXIpYtJ;3qpcGnA3W?R5*B^hhwf=rd*HD*OJn}I4H}AhH~FES9gOk7T2R)caWfN zYs<10uMNxbrx;kSoo8lAvTN~5JghqNO6gs9YY;dSY3sW4LZ1Y1*Cu8)$$)$j61Du#rC~F~hh%`Gz;bJ6cX>fuI*rBTixFcyu(rlLz4PJz} zzCH%;d@4-~Zfnxsfv6sTs%jB0SBsgnmq#M|mkUByRgcfBC?xT~);iiT>65ix2WY`x z7_Fe?(PFpQ0%o_@1+C<>tJI2 zTV4-#v@pf_s`vzSz)llr_En0%(~t&oMz22-?ixU;+nf9E-Pz+fkBFQ@^wXJ%U64H=g zZbo4!EFvKWr#pPMk3QmYY zT?ET09Aup|jCcjRu7T(??|YF{B(aDKaaw?-YSO>A1gjFCX0|p`m)@yl2#t(3CiU)> zM`yh~`Jk={-9ay#od)v7Jbr?fi9V`J_8w1b4z3wJF`l9C>CA15)cMKzDQ>-j6BG6& zxZs#pHx$t4mje=@P>9S29lI-tw5bst*EiXp2MO5xQ;pk}FoY;Tz{+Kgf zl_6HOTgG{z*d1@o62Ewg9#@^ggGJW_&6j~fg`)t|;+jm!nHa^$>2gJ3+X?KI4n|ai zCZ0&mQ3K(w&UPB1siZ<@HX6a*QIf9fHtP{6Kk5vXuoMh>JOuTgAn?y_fvk2gt=(me=wVwjG(X0L9eKb6By`sA_W^ZiwLuyHWnk*Uhx){)oh zIEQ30Do$zVS7T+w`NIAB5ewFP4LS>>5ys9zEAUs%WGx3ER%h8cTZG^d|KGk~`JT1_ zmokZAxyzS;>6She+Z6=aWxGjoLO3D~F~AfFHR)txlEOAnNd*x z(^y#k{IKEEoMgtGGw~o21Gy}!5K*GlDT!No`4*`Am+{*ykw@1?fnt&N<)!bJ{p!wO z67$b1v}@2jtKBIqHoGkRKMvv-1L-pG7A8{iaTc6()8PwAIAqtz61l9>9&%or7CF!D z^F3kn&fMM2k7U{dt-PsAJQkFmRfNCDlakvhlok(WCr%$c=>Zr08xulc7KB$UB6?lV zf5QmUQANPZHwHmfCm}Of(MiBo{~FwLBiTf?5#ZHU-@}hYILqrESF#o}U(G=+nX{<> zm3UU0Z-d${JMGEagtO)$_oR-4grKohzT=`4)!kl9b1RXnC9(mNNwbycgl}zlIrE`@ z>rQL)kp^6}hpp|44$NmS3}(j2Kc;{?N}aIton%!z^zNV{rl9y**)rZJgb zWZ=hIrFU&8x-_upcxR|J1mHH4&1W=wKP8yfT@1%eQyz#bVVE=B`ne!w5IgGanE3}G z@K2F4&B~v4(%c3Nhx2JS=`6VP5e^SU+v5bx&1q;2lSay~-g^(98N1)v=X_j>j@;b{fof^jU zO8}^C=AT9X5;%IRxP(3Nk8$6)E$8s8FD7q0>-w=t-@xMx&!bU0-a9>iAG-#g2qv*| zL!%lRp@L52Je)Tjz{@#jUh(RCeO)kb9*mZ}9*RyaaS1CLGD;4HkIZqAtPSs z7y{`JqaxlIK)E6+_3X{X74{=8P_x}IM*Hm@Jg{0g^KT}||4N_#3kivZiHU)go0tQL zWpe(Ol7PGPbvH=~O|5?gq{VUk~FQT%rv;L0C#?AE`31wwwW8h{71_yGYe;f6G zc4B7zA4F?5ZWab6U|l)c8MuHsvH=SVWMH{CflkzaC($tjhvP4*H3tVU^M6vUS^uh9 z|BF;Pxc|_;9DfY2nOT6)FEHqzF8tpF{XGKzuAcpiplobFi)$93=`_plRM~)i0RCb5 z(}%x{`oAD={}kiDiTZD3GxP84xPfFdaB!J{ye~8GqW-(6EPq6!|5z5l%<_L;OKgAD zK>tOq%s{jmIQ0J(^*2_{$@M=({a@q?ycz!63Nr)mOCUnc%?3;rh;IX}%YhYUW&(EQ zZ*%?MME$FT_OEhfWBTo2&cO+c%Kj&17H(oLR$$P-C93}ycPrappG*IZaL2*^PXO?L z=K{p>-i{5j{8!bpj;>h>TBbor84qp_OO)0pERvD%K?VXJK zeGJIEc7t!Azudn;SC+GlA(Oq7Op2j$XbSnRk;%FfichL=jF0vmemZhex^c`XY)bXh zOEG=WuVmvZ_B#hu5;gkUJ~v8j<@A9dcXPwxE=hb49>lW16x_e|${k&vbW)vMc_a9&${;zSZ!o9es9v3YWA4jG;IH%iQW z2Pk($IL>nK&lL75LdsMu^JEf@Nh_VpR1qd{VGye>u{z%nkNep5_4RtnMb(Aatdp@^ z5Tf5ldq!;1zbXi-*^ig;K^?IlS}UL()3RR{2)C5|E&4 z#mVS*Die-?l0t>a=%&%xvalW!n~>4-nKWS`bjT`kgo~K)IyI!C(Vi{-PfAW={1y;M zmg5T7T5~_KEku)CB}-*VtWOFhshv`Pj5bWgii`Va7{!2_sK62ZR`1H|jD*cRJ|)li z*FXN2@A^*q?2AkC7D`t(CYphSvY!}AV3CZo0@)NrHlm2VEL5jC!UUcU7mD~pQ&z_E zL6I21?G?~jA~+wOtEfgy!i)jI8JE=ylexFgdbW8mkN2K-7yCD!WYQK$lkY31m-*Z*JlQBDco}ca!B$XaZM^Uf4PAL(`Rz?jaAyPYL{wuJd`4 zUTfA$Dn5CM4vtMdBD0>sIT|xLx;q{Q;}VwlAfAL32}p`zsBMx`kpg-yEtR|G7R-00 zMhz#-mInQJs}<0>Mc(YUl&JNonhvgMHG<8AaQwQ@jXHquHrVm zzGvt2A~W!yzK~I`?jmtp1^)p52zB;wn8#cLdEY_S?L@nY&)eR6uil+B>_N{&&h#;} z>q|%jfk5v)u?WMXB_u-g3e`XL`CP7a$m>62TsZRhk|%gPVY0_ zqym=g6Jcr!opOLq;fYF91a4pX;A%3ob?g!kVg)NB*)e`e{eFV#G&?gLwI20t$K}E+ zUXDxnQ+(KPS&i{St*uKY{rD)Ta1^(PV-(5oh|uU5D@&&B)vJk(-Ju%((jY%W?0D|u0DN+yaxf?^<#!#iTqgTG-BdQmBmLDjS z@snH(8RW0~R2R_IzJunAPUH8EBI1B1m-GzLQZ_U99G~0Bi&EA+7rjO3lbme)leJLI zLW;<4KL72oZf;7u#NZ$%E&Eoy-EgWW_dBHg1iOuy`c6~?^*w4X$#~4H$3F2LWBI5v%PJ=P z+uL;b38vwcO$ojf3<>XNJT+f&0UrqoRAp7$hg>{hAo6tv(R4x&Wz)8qC9E{&kw29f~0(ZupLHJe|-r$;9*s`gr5B-1-{m zw7+qH;qvZd#3K--gXp7xz=?Eaw>wd#wXBkc!^K^gr|NI<0+U&fIIL1MB(R&My79&G~^WYEJf5oKbF|Su^2LSG~L^cOJdL7gMN^h44Pn#9|_n#!fo_! zGtO<76&)Rl64<@%B!?~a6zk^5lWu11zIJ3c$~OSy0xyc+cXXm^ENT}m&s&-;chyf{;`0iBzvR^qb2+8mP4c) zBj`f9p?pn#5=P%YoUAih>8OiQ1rY;C||S7z`GVK_qI zl3uIos~?Srq>3+yY4L-OR94lhk${D^SmX4P8NWu1>R0N%I<_l6VY1$$O5WrjwPoK78-7mzWOW=_MRmU;i0SmpWwDB-CIlIP z)k5Q5h4C)*W!~d!03Lovab66)9X<{;dO&zKxc_ApQ<}FV#L`z$Li5SXR>{}|ORYX!_s&%Aa5~~=^B$E)ff4y-8 z#HTNTkY=c<36wAuQQ>~AONt!Y6WYq`(SE#-WzH#v{Q_WSa$}Z>uCtniRkXWHqO;#4 zplEZxLux~o-JbB)XfT9L5Al0FkI*jkCY_i9B+}jI*qU1 zCOA$f}cj5~CO~@di4^I`1pe#0h8bXC|b*@t>?Bw_a& zqUMhz!sJI!r(q`7ttX6opMJ=k&YaP~=&5U}8?3k(;gnWS7k)J4>JY$~%q1Q){ql*k zDpa?I3bv#wPsNua+FOyve?TlULB4Zd&)95$+3@QhMfbB}!}{THvRu)(QdO!4FV`L8 z4!#Y)m(zmfszbiG%^x58>W$3#dXGqdCA?+WG6)yXQfYW}*;G?N+!EbH!(nQ7XWWPN zw?0#R5gn6>67HvJ)@**msrxla?@RC$_Dx5d6m6ctOZ*4{4o{Jn9fJ;pCeXv4V>>Y; zEAinoO@f<7T+Svff72!600*JolZgWigEt*g-X0sOk;Ip+N{-YnRzSFvXfiH>QAF~# zn@G}eT-5sL2$j1H2b@{6;SOg~u>nzE^t9#}JC`x52%$(2nIYc+fJrHk$L!}zQpiEj z^p1ToVl4aZFtt$pYVbac97cHI-vOj64 zSNwxxS5;VEPx?QGJ(!OYf)sDD5h7R6g3CWs1{<}wWsLXtscOtN%EOG+E}+u$Moa-3 zSgH{t_$nPrd{U0kT%waz*u^j-qFq})`-SN-&;bwelN3;Q6trUaD zkW0Wl5JsAa9)}xR$@gyYP#*o6pX>QVLKSgY8_Y|EOfFGFCTuK0)n%{`s93}T@UyVF zqG@rI@$Xoh0xW25O3QIfM*M{2uu*U92`#J9vfTU@F@N>NkwX*W(5do)IQ-Nl$7!XP z(8hz@ARiVRqQ6Lbpe%3`LZ?R(PAa!mEG*o*2ql(|aiN-CMu;rk)Cr{_3$)MVNv%2+ zu^(K~YoOZOjhHBR%rJg>zxh3-{aKNX+UIsb8882BQsu$)}D|0gn8aFt8KYtw^{A5qjps9 z@}Ou8-->3uqTE}hVs(NCU1+Mf<+W%;3UFi!%}M!%y*x_$16L9UZ zaOR3#YZWq1N7rJylMC=kwUzp_d%fNCq@fMJFi4#U=T`4tC2Wr1U}}FeSdFQyQ6mEj z%>Y3|vdzQS)r*ih$0W&=Y$NoFZ9icTjtA=jE^4PNo z0mE11E}a8EBp@aZ&mi;*-{TXZnzM9u@4U@{X5?mQ(Dxn>Cg7iKmgsTDfUAF{%+rjy zQ7a+2AE7+Cd)y~{=3}hbZ)tFg<3g*@(GY%Y#gj(Fyo7{oMscb%q;CNqa2r#ZqoE24 zz_nz|kYcR$zn{Sd^4gdzN|bOv#J+_??~~eRvoK)`3Gp#u`#aS|5^*!R?H(1u>%mND zT!^v`*FE3)9nH85@1H{N?~;Oae`T&o@Q2DU1+K#L!{7c zStrlQy_8U7{;Z__RD9(E7B*i`2U;HTgJ-c?41f#|ha(!(&Z99xiw zqGKz8kw#ykrx-{Pwq;k(35S5(vbgga?X<=M^v4w^R42*4fxH-8uStJcqae$df4aoDT2QJ-MW>i!4=fvQP*T?s#IK!6MsDaUIHZ19 zH|IB`qo(_vFGD}uv|B*n*K6Bv_~!>>)%yJIiH8X~9o(vJtY0=6!uGS@4#U%CJ*WjQ zb=3kYQ2~XUSYuAgdZApdj2qqB3vy$Q`65&|tvxr`T%3o$Ij{R{!%sE%!{oOM(lO)x#U^}*&9@4%*J@cB97HNXcJ1_ojkQEOeGZCag$ zsR*E&W~`Pe9i(t&s=bt*d&9o<8ctnuM=r&bBVJcHY>unc4vaLQq1z$*v$+G5<@VZY z>#1XjfyhOpzG7w(_+hKm=uqDMD&6z+E0F}ZK3zoN_+;dxW;UO`3{I_+hxW3P7<2S40G>}z}yw5puh-$LXDtDLpmrm zd;0_uSfYn5_XtUn{VgN6m!l`N8ikM7@Dsg!7hS%_@cityHN9^C=E*(XJsI+^xnE~Y z$olw8vAWJKgn{3VFCfN*tknDcee2}RDP)B69~`Ud&ia-#NW`H_UX@~Eaow0mHZ9@f z3TMxQ$1#!gV$a1IMf4cB?)D1!1*KJ*`mOMqvV&%LCbCS8Qqu`V}9X-O}1=utwJu$ z$$3nW`aIhZu*C2ac7w%2H9MV(ONbu4qwESN^DRY6#^!$Bdaua5BB94!o7?&b?NkBc z12!3s&hzXfXxsvu5XNA-I9up*OXVBUG_q&zzm~n&{znOljRD9#vvUKTP=8BMKxv4R zn1hKMDAxR)Z17K$+8i9f8SH=3Wq_0O|2cvG_mkQjENuT|a{FCZ+vx*$m5+kzo5&)+ zl(vK2^-Oc+=VBU%32VnXDT?N`&@^vFx&enuG7l4S*5|~DE$v&7^?D>QJmt#?smH&`|8#Rbtp^ z%a6QReL?{=C9wBMDr|?8p614=deWx{#Pruw5klP!0iPt%#hbF4>bJXxX~szN$LhnK zcYdE6fs1app!cdLkx^3l8HWJMMtHP}gs9Fn6j|q(#9}xC%117b#1+U985gu;YbXz? z%uV0IxQk2#pZ&s2Hb2*g!icR!u1%N;rT%U%OCn~TNO)xCjTCHB{g)<;z3rL~$*rOI zen^K%z6lqU`vAmZStFEKl!;hCr{q^pRO16Q4#oHa2hX?8@B%@p`)$z+MU?h(eJu5R zFF(>qF;tKazhW95eKn4khf)jY8m$mk5Lb&qk}0Ezcd|q9@{D|g{OslQin&hS$&!#8$wez- z@M3(^!|rB*#+z)5X^&!2_(EkK|v z_rC&0c-G7u1e19xFldakPg*q|k%*#V*Y!C|L1Boy!-b&rdl*rRIZ|G)%)$nHB2ql& z#Z3is4x0^O6cYI+3MA+SX}h&mxXuIfTew9pCeAJ}8%>CMAgFe?Xsm+OW~{C)Kc6xZR40Gg%oBtoDEG|=^$X`KaJGV!}844Qn zk5t)&sRu@N`moopLwd-RtO+QN3XN>hH@d(60&n?VL{6j&Cu>C`?EKwB#t@d%vmSH0g^UHMz6dwJ< z9xeXUArzmSIoS0*jc9@&hz7w30;)(~&C4PWWx_Il3*|Ad!&A+yp&E^>NHXkrLxMxz z_$WdLti&8Aw8_o6jDG@%6TRrl^d8cly%jn~YK+)kM^D_BG0tBANp(NE!B)%$B&a5T zSh>n`{)GG<&SJPAmHc6^>qx3Gjjv>W2boHR-4}0LJao)vyn|cvq2xPWWf=RmFIT+0 z(zo3OT!Dne6!n$)$VGPe@bWwEwzt($MDAA-Ef?312YtU-YfJ!}4xch6d*}vm3S+OY z;KyfEKk?v5oLynfig0OKo2Q3%%NZO&rJr&n-HEZvY%CdN+EQS9ejjqT6H(fLTHO)x ziPMK5GA3;eX!{6VdCH~8_i&VS6JGAYB$WGl=k^kV3jGqu7Bq_-AlCFCj8A5I8rQtR zBLYGfI|Zw8OFF>SE~&OmlhgTdA}U4iR@o-x^vl>rZ-KP^^Cz z=Ks*M=o4Vk)*+(S$`-2-(3;bqnEpo$a#u+rHFQD6Jm3d&Ew&b}nCrKB)=^-lioVC2-m&lwS{T{?+ z_2ndcY-R<7Olqo6`;M}mK~@7Y!O&~EZeW}0^)LG`2S9faX=^b;iz3;T zeI$(9b=9>DGtl*J=WPX!-QMbh99l)BX<}5ue)4R7i0VZ8p+|myCfpclm1P8Jr8?ONb9q3)-*UqNf0OxPNHjR%r#0B ztgq{r%>JIs{9vJz?`bvDU-R7|Ujxr8(>z_4><@gvAgc05P^$E z*r;MU#M5441nx2(Z_&I>GRwta+>1`yaWEpF1x?{P;gu2me&~=p3I_E?%!3T;y5|pq8qCz_Gc1A%h>Z)>bvY*re^xgrmf%Ad1G@ z{M#qH&7hQB2eeFvoq~&61L1Ne;OfL_7?i!0hr}Akl}vjK@a{|DFIqi9C`WtUL8d}= zHBaz)10b4JCpj=`2}7O`$OT0#kEkT6Q#7(FMfT*}f=S0WSs|)MBnwIiRFIby0f{rT z5TDZ_M}TaVu7IX(m=CFj)K)(#pQ2WOTlB$N9`_2BELxZ!+}*~lmO=o{_V(~Pc(WK! z5g57_WcW;~7>$T%EAHlO4IEuCZwROsr2~Vws)2MplV;k-_c4W(j30cc6=AU^LTUY4 zG3MRw`NU$>XX0=ZL1)58Qi|Ma0J*bI-dd0i7BMDZu7*A;?2hRCl#2siwq9T5!82D} zuzUpBZ!;!Kht?*UrDjM#UX+_*<+EBhFqG_Kcf z#O`2v@Rl7PNRkl|m__}AOdc=DDsW-%Ad_X0uJdR5?Dd_Mn#3pGQE10=fw+lvG`?5T zSAc_aqO`b_*&+H=8k>_S(?cs}?UU1qM6(pRpom z!M4?W(9(*{OoVKAgGm{zoc(naryia)_QczPGo3gMrkdJ^F){IDR*Ih%Ya2-*)}(H| zT!?s!Inu~i2Ad||M5QRL9+sFZ4O2^Wl#Kdwk8XME#Rguwr9D5ZS=U{*Yu7(xnT%wo-J-;%C!b0$BLwl9u&`5{g{4{D_n`rB8^BcF`yFA z55VBrQ^Y=4bF@r?&6*>L+5WW0i;>HfJ87MMfwJWC=J;uAfB#XQ`16wwY1~Mb+;K;v zhH)Q%;j-Xmom3__M(f06I2VNlNA1Dj1czcDZ2guixKH3nsvOSUXMfoyYpsLRm#JR7 z;Ur-Ar4e&w06LEkUX)a#+(}c~>|lYnbUkR!kKN%S=NC5oM52rZCtmbe>E{AhaQ-VDr)ne#*gS#fLOW^F=bNsT?`M9!J}Ho3%=>vZgBj~m zUrSP~xGqZ6Ixvk?_EKW`hCTO1zL#fSZ&9kgJsOEyxfZ>W*=9AeZ6ac52K} z$B7HkDCqE3Jqd!7*;Ya=e6}OfG1@h6iH?u!;cz_+T}@RYur?mDYKg`T>)}WllFOh0 z`9(Oa42gw97WEYof#XA@fOGD42mVArE3E`2Nyf)ics`s5oR6xe4_3rixYnE+#i@ma z>Fahs#1k>km~*2kR;{D~uWKiCy<|B^`mfIc@*hj5KBk~A&CN)aroX^P=-YAB%BADW zx68>miNsnAco!hmI%lvQ(W|Z!a#vQ6rb4SeX1eC5VB)x|&{8qSO6#;~HyIsDMMv6w zq14UOq5y0v(HC>~m!z!WdMfS&L48@QlLuF@Y<1pRZ;V=+%Ex1*o@-AjOq44dAWQk! zDlWzsNnQz+hY)cWM3LNzwik8-EoH$Dv(>;xn643x>`(-CRT12qAphAlfNoZd?hE(3U3tdoI2QW*VhaM;=V7d`vWEkp z?@!WpzPaCtGqic=<0I*CaCCMYMz)DY2t|EAYEEXSuID`VR-4#?H9ZGT;hJ+)q;~z- za-F8foSS)@xAGjLOI$|mZ$jNr%fQdK`8wfC{AP(fW$Gqp707<3K|5{%>RbW714Wib z;T{w%gLDvY0+-mx0C8H{RTe9t!D@?5!}4?LSh#AOME$;?t*5@q^duA2`6kXzg&pAt zQCj)g1lsErVW@ehwX1;Ox`8+T=fZflogK32jNp9{@{L#GOyFS##=@+|1a(WYls}4g z_;p~FnF52N8CdS7Y(zBw}sMvGtN{fQ;F!x@b zQ&^)T2N_^94-od3l_k;!>MOpHVv{=9Vwj$r1qg<(F25 z{_9hvmv_FUGaA82!)4f_GqC`{hnrb?&4S}2$#0a?g{X`?WXUU=vE_+pm5#UUsHvY_ zVa?cJHzCcp1VwH9eI{`rwr1k4Zc6wEBF~*ez*xJ?_Oh@vp0If*&&S; z9^&f2$hPaRXt8lXzd+^4N&e?nvIXIQLZ9iJ`&Awwb){V%iF`@|&K_H!e4WEm%u2smG>| z=WA^w_NxwMhk0hJ^4s*fKi!-RP9>Fb2TX|?+CP$6O0;rcBGHCja3l-l;%pJ}#&s;L z5unNkp4xMY2?g27!n<951h9IGPnHF)Avb{1Z)>Z4EgZ~<2eaPf8(uv`RnCO1UD+cOwmu5YP&grg**gCx%Zh}ZVu%nz`G?tmiKMwX8;C4vAp~F`} z>}WpZ^{=c7y5XQFDJMWX;!k+>TyNb##x}C-o#0nMFeW4CxAA8ZLStIxj_zjbYQ*s+ z0!-c}M8QB)B8%A<0*FO%X-!BP;#m3~?&0j;uC7*(JU7Y(S%*U0@zF$`0R=0i2+~$o! zNpRQ$lM_OW@;QMO$2Jmo*8I@$xa~}*kUPj@bC+q&XEUy+9J8lkCYm({mILiUbP;s& z;G&C*AZl(FbTUqn2E}KQ^(iC+6%{!+_nnfRy&?Eno&qQ5Q3Ht1FOfupI{cuq)ymUT(QbV!JM<0|C<@>r1GyLCt^mPeDX6z9^>b4keG+|GBjuv7w|<#-=VSh^bjDCb`oyWTl*6GJwM*-xY^S0rbiOfiL5CfU zgv25B?nEOBZjjNX*1RxP)Ls&&t!9d!3*8ce8~8O0qd8fP$)6-;e#Ey|%$tO17D9R1 zBtwuq20-(DV};*lY7;aqACKp{4xS2~#TH(eKdX1DK@+ZwLA`s#zuCIs9&XEHt~8UM z_EU84!>o!aBLBh;BAVK%)TmymeelzP5cl|urS6vx4cGCrG*9wEtT9J#TuwdECs^BA zkl%CL6 zzXP3Xg-{{b@{|QkDx+M<6k!rcCS`-C%VD*eiwJd8gPw11+)pgi)X#|yd$jjewrP?1 zzIsJjR9U+tN%pB4t;v{d>U?z{g!GG_#p7I*9Mj9x7hoqT=twJ_n)M=j?nWT61)d6) zYO7o)rZx!M}N-iOX{P3r6+PddB(RdC?%QNo%k%pC7I zTENiHW_%jj9g(@seL2CCpIY76p;V#-kO$?*Xi_2Ko)DK&h1RaugCee)xr)(bEO$zK z8jM^K!|F_w&S>Qvn;zRv2oa*Fy<432{F&MQvlWH-z}utq4ndeSR}%;*n-p5P@*}<*9|2&bJ`rjTO+(lv)iLW9HD1O1R6mJuPSEqG$KZip z`I%X|k8F1n*)%5Oh&zrY7!G+88|w6!gaGtz3%K)F52;r__#!>D=l!HcSu4EjcOpQE z&-CA@B!8t>{XW`(u7*zl6xe?E zAE=K3fJUAy9Dj=aUrGs-h2{CR`?d}jWRPdqtb-6U?ezGAVUUGkzl}ewxKKG*wagW=XjUVH*to^o&V++i?NQN zh{V)rPxQg*8yh&UdqC9zbBFaz3`IwkDzDZ+De0**y6vel+*IvYeryVr3*J`Z$TXE0 zBCR!%;$jtS?uTHahbz!d%c}|D&QDP$<~t=J^`~Zl?KG2; zOvQVu%L$=>{eWiwtBn2kf-)zNNBggCssp;3u^%K7e6YX0E5`12>uP(P`2QL2~E$fIFC~$?cyhr`PcojiQM&|4LRzbr0&O84<_GEyEGQ|ZIZuxzrWy)!QJ|% z%+poV+**uWmyN8y6%D_=6ix8+@%yTo5M30Dzuz1Hn`#1%eNF7%pC9YbRRXI~-c|1r z59IWc974>dJ#ewaf^!IQ#QAql!o~N+2+ZuTvc7&-#%is=s!`vCe5nTLG=46vb{R91 z`FY8s}7<{Yp15g>`y8eELF$7&5~lRet2!T6~K|KH3maeg8IjPjohC=b(dLN0QND>23XD$CIhN z4|s(PQB5ewl}pDdn&qz4w!C7`o|R(vWU!Y>${%Ep7ur#~y;hFK zvx{eSASHi|Rf{htd3=7fS4{W6c>bnpD2!Psi%4`KDKyG;9Y&1_mAd9p>b-pLE4}TB<)Y^> zlmI>LK(4kb@d7v4qZE#__j1ab6m+zDq8FY);N#ZOR|&eI_YSkVw1`0W`>X@q zp@^@3U-R;@4U0U0T5#nH*BbGS&sU@>HQPEj_M!|BP0rR`{>`6lwaPQ7?7=*GIuvw*HI1Q8g&s153e;VZ+n zin}iALgkyEUH)ACTj=Qy(10Nej(UO@Q-vmW#~hn?5Str60#Wpxk_RbMGc^Q?xlil* zfL$L0qUz~IzuLjv6TZqhnU$VlXEDYYOK$as`h#ECY%K$KN~e8>3de{d%4{1mk#ZWXp&p zw$8!tg@Bn`kT0p{IkMB6C7sPtx|`Mr-5%Y4+_7b;t?H4*kTA`;-aDmz39fv~sMKhu zw45KYOyomuyoX0$l*Mm*&avvv%EI=M33=@$<4 zf!M?t*EampPPHID1(Ne&-v$*p?5Fnx5ja_h%Y@n(t$kUqX@PBI( zeFSo8i-fIlZ0pl(9*^WELSsx-+k?}CGoJv)ey~Bn-3$X&1E6}c4Cx8(&EMi&0=sx@ zk)GH}-Z9u+zS?;9Nsc(&GJpK0uaPppB*7oTC%{(5VZunRKCk56t<1nfdDeDZtw%m`xi}u@m);i&E>%CxHSg|H=twO&$bFGp@j8y+dFgKra8aawkfr=R zXtsEJESx_UlW!t>f?0JDA`>~YA`R6h+i>YWQouSk_8s34=CeSzV%oPy4IKuxbz|A@ zWF9d;b%?6P{1mkaYunj}$yqi{5-(Y4EA-RnMtnep;0e2cwzi)+QaIvey#^sSlqRbE ze`tHhAj!h0L9@_hblJ9*UaJ zb5GvLbD#5=J#;)quMB$$$sb!^3gR|Ua`M*WZ!XOgSdrlPd1AD8tKxCS02_0! zK~g5FwLm^y@Tn_f=t2{|`u(AvJ_m&c^rx`*oJ(My6`?wYKxYmrrE3iABSAaGYo|cM z)r%%Mv~PpmfRkJ;K;teV-08Us1)4953KFf7gM3D5x>#~q&f?~PNCy9JGK&U$Dn38@ zjMJ~glcVgl#$#f&WnB}v4!aWKH0CEGo!Jh3ieyKQ(xMA#Bq$NgYT1sc5hr%lY$tGiHhw6-7;yVtM5lE^+M@jdky80M*%*B|B8UjM<( zpxt*`etY_YQs8IH%HnX4OOV2$@wNXOI!U)x^5|p|ehyQhW)kTZhFu|<>>iqPHUM8k zpimWRYJ@EbLGHNn%i0&U|E_F*a@KaGz_Dz8xoG(GZ*WSMDt&53%#KvB$%0d(dgGEf zOSn5t6f{*4TDrnXGk{=Qca8m~(FxIOXHk$&Saf&Y#xB27k)?_;ymHASIghV8em;w7%ZYLwX zslT`Fs6?B&0p)#TJ|NE?D zqhrhVt^{*+37g&-%=aQNxnmGDn0&~&O+&kz1P-wyoz8qe8}8oT%$d3Zf-j0`9iU3h8-RAm2rDKkJfuyn80gjDR6ew-=T`%B`R_`qQKsX7u z<{V*kB-T;<$C^Ymp9?qd9ra}`K&}iKX6MP7Dsu=U&!4m&nCNB6ZcnCJD3M|2XfT^% z8?7;(M4xw`&Ax~zm67f6b0}7ik`PrdLeL&60as4SI4~VccGCuCAZC~((o#xclb=EW zU(eu&PB{u|28pT7j*2PKYi!HO0vNXI0~nA^Y1oQw1r|(bRC9Xxm#DOJ8XjZz=}{It zSZW+`7OF9HSmV0v-r}n;j*>m>C~}coM^CjwGVDI&t&8_#R0V~x<)}xnx6Jy zLt(T*E?PR(zumr^$67IR1%RcNrt)sVqw8HeB%Tn2~ z2x|3rg*`W*dl)xVXIwSK^0Snh`dq%(bcCE6nHJ(7tODnS3J0Bdt(LQ8Yy%t3JQBBR zkfmEE9WH9`n;zP0f75CxL`P4J(ZP3q=Xi|Iay%NvN=ZG_m;&DBeMb-pt1R17J8n%^ zP%DK+7=;^w;?9a(FA`5yW{|s|1gyu0;TnHO7*JH1y0G3Hny8xcty~ESGcEzd7YwXx zEA}>f;5or4(r=`^?9umeCs%F$wEKgvROd`jwuBniVDb_AUKNQD_*mbzFv2|peX5$o z6Vz$!O__oQaDi>RMS-UCW0p_?BO>=`*BTu~XnRiARx~@0bNxpmq||0Lmod0cjzdBnG&$?Pf3p>3;9uYmzT>>} zj*_Hv4Qt^{9$F5|PJo2HDbqX`Q~BnDf#y{C=L^}FUf%yC*%lX&h}I+3bTXL=EjS~-ty>2 z!)4OQ$G&y(;(t|d4v_8pS-m5!Uuq0Y01!A(dxW;7ljuYzn zpUUoL=)haVEApJRhyLFpp8Dch^8M6EED7*jc8W=spA%jIm0&_NbCZsAK{}?gJ}_P+ zm*X2Oi?8yWnP}fZ_cpcCWsyiJ(WX6M^%Zn3hM#-2H|3rOOVq(LH7 zn|-d$(&rmL3EoSQyacg*8lT5biiA|Eh{b?cNwp?L9vv1`6fZ0odj_7FuPlP%kt}Vl zR~5Nn1^#_e5P(TY#Z9bOfaGD@q|{nHy+~7FQ)CXTw;OfN2EoXeKRPNq@9doighwWn zv6}<_mOY2zk;KNp6o~h9_KJ|c+g@|8ZcZrChAVmbKC4tZR=R?|&n)osY_xTdC1yO*(2+YiWj(z?3K&8LiQuweqlJ!mSBp zoe#e8fU@4?0*1g7Yt5v&U`OTB4C2!?%Ux8J|Fkt9Hk^ViyPspBdE!35gHC3Hv#`Vm zu{h_Y@7VGs)Rn-q6khWeg_&LCr^l6$Rn(Q-XhKuzXH?7hrzEp*`G9QO)AO$u`}0}q z1&TauBsxK_5n!U=6-kZH#0EFsjFkHZ8p(fErkOm|0TrIEoBFpP)mF*!4^F5F&O124 z4&pSVF|}Y28E?5I-wQBTN0~n3zr>3ye2gvs!!(jNwz7YGfPaw1_W_W9eYi!=i}pq&eTukL$F~{gs(vE=EkJEoB%6xzJ|5bw?<(>KJ?4 zPkj!#4}j6$Wcr;3QdN?ooHTONOp|gD$$FX`2;6I&zZBmP{zp58)7(NGLEKh7$_#e- z+;YTDALTz5=HOtL)+KCTsKa{y@eWH>CMB$?>(3{bf7W5(*3z`jDZ@@ddI(Qegqwp} zn1jd!c`+YQPN}dUXA5Z|Zn$D`qm1GC-IWe@Khtz1$IR7(#tc%xxVYpPYmN1H*Lh%+ zG|?32!i@RlG{QFr8A53c)Yo9mCkJ@CxPM2}M7DpCA4`d4JGeahf~!w8)Ny#!tHUf| z-8PRf*v7nFg&A|%u@p61BKb%}FCGm9BfCd@a>eEr^YuaX!i@E$W+?%{KzlY>)dC8v)8$e(W!bZ{a8?W^~| z38nAKmhW#2P^$3fZCxBhr=9$pc_{J70E5@)ZT5t@kz3wgdxz{TsEl|Vd)-H*ck|DC z(Da6R3kW4h3#>f?&7*C2S~j zSO3emC*dX=pW^Rwa{PQ*$$N`GCdVdaHFd!v2Rvz@#J{wdy$8z$ZU^F42^}J zHs*aa|7FzMce9dOEzWzj`%{2qr)`<3gdN?^-1LOOSsRF zz3QyqX294pN7gOj7*y#$)TOIRZ&YcscQtAAhxA~In$b{MmqZaU6BqE)MiVnHA`b8utwRf$eg5h=)Cs7O z1odYVSWFBeW(Lbe&4-E?R;IA}Z5t%Ah8WSv_Xm{|IJ!(OAVS1Uz=#pmg0V=s%v*ks zi@93B^f~t#kx-bJnZKb7x|#hwCy05OLmf~a%&JVbi{tzRqTP!lc)_nOQ7R;%__}=E z0B=X(FgJs{Ad`on!Ucj<6nTLYN&b*lRvQ~6p`11jua;5`P?clZQAjQ)_h7Io!`8z&HR_7CdEGiY zi$8@p0d-IH6~?_21O7*SDj9alxMJY)ITgNO5K#v!&5&KL?!*{u_RvJ5`^)#?7a;O! z@Y!3r-0lgj10@B=V95r`Wnb#IPDM&PQ_&e!q{>}c=DBYgG zh}U11!Mqm#ZJ1i_77Q@~dr<5#+t~ij>Uad`_@L{0K0t^k23&qn#<_NZ8>F!rnNcjF z@*3WyCiJ=?vt~Ervf6*GUf=;UIF=<{&m`R(CUF?7v1a-3GBiO*B#|42#&B%S-p^^c z0_-qNjiY3Fh@Me-282lj>Qi`)7z@v7+P0@$%+|5fSG?*dK`1c!LE{C_X~XkMa~`zy z!EF-yEOcA9@<)cp;xwkA#f?!-+n_k{gOO_}(m=*GLf8_w&g;Qme=Gb!*{KBmQIy)nr0H3M!ayh6eUh(S}|~+DwDj| zy$JNLAtdm{+Tnf@!QLMW3V zR)L~2bCkRYbrG~b8%zz<`PpD4aE0mFrMN+S@|_eys4S5*K0W>mwE052!ESPb1@?Pi zQD8wyOrGuj0Gb=xxq8@C82H#uq0uj^G$i;$tF$jd#<;5dtuVM>z&+A0UW|*gez1%b zwaaZ$ktzJlCMOm*MeG8@)*}e&OKElN4F>Ky!acC9M{+6SBTt%SF!mK$okht$nM59m z({x)tXuL=@Lmea7vFx3WGRCGOfw_}+)c*IPnmHsR=UTDG*XJ1F_HD2>|;fE|>kshPEpS)7=X0!z$3n^Fh|}${x>)8<6lkZ^@rzb?)Poa;X@9Qo5v2bS}=u*6@?g@qu5@I(4ni? zmD#a+-Ue}cKnOPy9BL&O3Q6QE39H}+dA%h}Zm5XN_}2F!+Jad9grI&ynx=u{ql0fz zYkJOH)|#vBHkJA-y+_Bi^)B5}Aa%5ICmE5^o<)@zVgZA;RPj1jaACntR8#4Gg>P+e zC~)xxw_TM)_};5b(34P|EHLd=fs#+K8IJa2$SxYFIK$=1R4rk#;XWG?RAmPrK*Wm> z%Lj&K@z0NBVRKGOY#CLW>vN?QMZwvl1=`j=7YNvSOE$052;nv21D2%!fJY7xhs{~+ zG`bsL#OSRm`57y_yUtklQ`mYk145p%LoSpjoiI+~gdek~<5pZ<>XX1f0~Iph-`RZNQclUAk7&o5>S+B_-FR-SOs-%`wSe~jFoT}hZ?CftX8hgOF3Bb z-}7H#+C?ce@oZIdb<%>Wy;-5Eg#t17A7R?}Q>xL+-(g$iMIZmmJO1R`>va!` z@)~ou+uSR*UZjqx;+B9@X_uq?c&($q0I1U0K`~beUCtyKtx!c%*H8j1O)kLxYehnu zDBm;fPDsR(W9yw#e3ivu@N-OHnu`gROfQhY4rR(sMK93#p;Rk}?cL=EVv&;#?K3&6 zVbETPux6+v^F8g@byJ@Tw9q0^s8fYOQPtB^W_8)(S=JV_*`ZLNw{K2%BQxpmfrOJI zLV&yc6ddIs7y*dC717Qktoss0VIm7n%S}fq?S_5bag7TJtec+_Zj493;Q zbkN_6da+_zgoqcisx`V~NP?w`_w4BO=k###_jhyUc&i?sZ}50XJ4KWV%@!<%%kO>f z?Iwt5FKzXH#xG_h48Bp~$t0IzTDGoA#+U2gfqT$?32c9rigIJ#*x?n$l6)rj^8)`n zkVb2NEKjZAX#(NqZwxISL;npGQ~#2uvpKE8AYs1dfAvwE)6rWd&UI21UGo%y3wg$4 z{ir8<$fz39`1tTLQb4&_a$sl1IyinSqo^+w0iqfRiYKRp7@sEu1^@DZ-HdXS$i&WW z>7!9ou;87{BxtTD5w99Q&XYp9+sxsQq*tLFykoOdR4{;mhB(q5Scik}u|nkdoQ$JWW3Khg2JIw58Wu4Wq@x-#pR`~!+e)}bcl=)Q`HaF5apNxk5odH zH>wzwIH)?&B`m~dsQjlIARQ-b%#Wa0U!g5{rbKrWPQ&9T8&)zguW;wyvo=7k8@9>I z6QAl$sH*p8p1tfriUo!sts~2QjYdPPW3Pp+{3^De?)eD^xom`y5!Rk*C52 zbr%Bx@^t#~Mo4xO2X_jNRBF?Fz6?L?<(xh-=TNIWx3b-VfIvtDG@6=kmj1mxLpKD3 zYI+=<&rptc#n6tx&tzJn(T@hX1yW5X(rohgk3XhNUZh)trBuR9^WwQ!ROAwC}F{VINZ6fQX8mF7(hrM1us#EUO?=b+SJxq(1rI=3GNLFuGskOM?jVmkEoh}x%3 zT!|4~N-J3#Zw7G(p5_BWiw5cv4ujsRtf~ zY2geOn}FwVFw71V^JzpUeR|5A;B}@)1OVS0wD@Aabr7WiVFcb~A?AC6@tM0G-~7|k zL?vROokomKo0tecV|d2xJFS||!@OIXump=f($AU! z+M6KWN8N%Q9ArYPvai{qT7&EOF(~=A@3mLI$FF?~2ObrC!F9nuiD#QmG4saWUz@hQ zfN}M`yZB4GWaC^QPV(H`SDon!MXl?7j&NW-6CDJ~x=IKARbbw4dG~fFSk5OD_ur7$ zEe8RQT@k4aIrDE#R5OaqQXlE$TPw5NDATBVLGbDHTPHAQUI$ayHrQZwDAht-DBH>X zicJe$j=I0mbJW zX|$5D44}+EtS|cpF)ER{B<}D6^`%>0#7-CxaK>B&-&r#?sV4p zvc@nKwr2vbC=_Kz69uls9L@*oYgCz3a4G{#V-@f3?y`lBoDGs|5+S+$xJ`1jBCZm7 zlu26Sa7}6GWDeiCe+ib*4C^j}r+R%)W@^(#zuUyV#UDNzzS7kGDF^(?$)PB^-40H9 zhQYtgHTJK<3@)XrE77p7ixC~9d<%1C#L#F5>h7?Z?uYDiAIT|pBD{1uR>&Z?A^-vu z2p3>DwjiBzY$h>|MNp4GQiAtC@s*S^x1`Nf=7^`2@2u={Ua<>0FeEd|Vz;4BP$&wyg}u^cZlyIAUyf ztku(xjK)Yz%*JSo^SCTk4~ur-0V~_<@Tra?>=AA23}K%eOc3LozZV7kQeVjtYi*h} z&I(*dI)ViB#RZ{6IC(1&`ZO)g|1!5V{{tiSa5ywU-9nfrS9F;Sy--*@`zPxmmm)%) zoISC1@y`gx$ma~>5skHx&H?AG&}9>Vo*yhwrdBPEk0x5jP=Uu>kW!c^RtS@`tmx+} z%LE0JRjnI`EN?H!NDU}|{_c@tP_CZbZEZJ3-Q{l5T6SDx?9Zy!(J*Phpix9QrifFd zUbDaj@eIH65ZCfbb8^`4>m|J%=+6G+8cG(+-0&Yu0L+~E&nZW`jjyXl?dOCFsM<1S z6Jx__M;mY*RTXy~ZKWQOnp)8CI34F+S~$*bzB|pf&ow9Bh1RG`kK$~+${F2QullsN zyzp;JuyIJHp{Q1T6eqY44){KrPgem$4KHv^E+pPr=YX%7K=S&RqN<>?X#D)Tq90hu zl{HSHjsN+d#kYbl+5Hn6uK~@;IO+B8QLcqFV zBD12L;`t^co>}j3i!q089QHFKT<2+d|LbqyR5G-xaD(P*yA%;>;A&vbN zwt-!4x17FnWix%$p<%rHFn+XdQBCz1gr?a|CS;rofx!}ym)tXgktjf}?~1m>F*^{r zQ9dd*F*zwWu`{77t16=s&UJHy>H08!PWhB{L*ogF(}Xp+sV7?sn>mw-`*I)ty8u39 zTBlxaON)<&`WH4^BlXY&pYt9U_!DwN2(<8wPCXFrCbT)H37h7uP~rB9Vx_F1IB2ke z>Cbf)VI93=8^cUpxy^urd2OCjw0C`u$m@2+T93yjgai99vm~86lVe89t!q}LM{}JD zBOg(n_zxCEwEPfG{WX(`g2GYDHePAj0yT%=jyKw|A>cjsx zT#PVXNDP>B#=si?(?`Z9NVlj)wQL!z0FhV!jg+>1KCA2b1b|vRRYFsL2tSHVu7S7`)khiFTG0Ot+1Y@#r z1TUOXlrq4wV_8YmT5Cb~cMBn#E7atWp>G?e!FTBxIO zQ094hk|!R9ICq@Qb2#Aqq##b)WH{BP#a4s^EN7B6^9VE!yCA^PM{AFK!>sq!6pD1gh#{IRivQ9@Yy!fDdGHFLy1FVS${-QwJP92stT z1|3>*79CzvIsvAku6R)oJ^2uafrI|VXpmf@zw%na1FHSSbS3mU&>j_BJ3nA^x_>-> zap2=yh;|j4OMAK!Py%;ppzy~@krpU0tYOrPb!8HQKAptMa;y;)l+>@#gb11s%W;{I#d|y$v%Dz9hIkhy= zAuqD~OVD^+c)9C!zz1A#MN{@}vQ6TDSSYG|-f^*we{N?oXtGV@7Kt$xY6)&TG^t}| zMWr7uqCTAenkf9iXtW@RFBksIPb$S2K=CLQDlfWM>&KkPD;Wt-Jpf(I8K067M4dF?z4}Ak7k3%xTTC#)@&_&r%qmVL?}P84xB+{4&Y3PI!+eE>AbpYhUm`A*6&ufv)&8;7@2^?%29DOSdbm8Dp#= zXRlIbb!=*GIBVhV;IJwQXebdZMV2l(MTka)t^IW1hi`OZ;Wu0-LVOG2(N@ZTu)Yz@<#3q{R%)MLXy;JNA=C8#4cfUo<^Wcd8DroJ&(0wc^`kdEPO`!K!tUeCwhV@SY#>{{V zYigPoW{#>Is5F4IV;?En5{8cg9QL1ssb$$vU2fZtY%gx{M54$^d3-W>40iC8Vx!74SN>je8nWBK47kj*y=^qpBNI)C1>sC=}aRs!>90$Dv zjRsuh_eQhSI&Aeu`Fnt7i|s`hAYNR0Jf1kN>32M;Q)ad9IyzUmBhtgwKPMo#|2KDIFg}4=YuM{p0&FAHV zve7*9`i^`TziXm<-)gdn<+8sE!58VgM3t!N7&pxws$4nLrWjJn&*^+(z-WZB%9x>Q zu~>Y&+u{pkL{t84GO4VL(W1=d#v}Q)bIf+O4b}$TUP@|9tWlp(p0t}Mp&Wl8LdFKa z9n&##=+Pvs2l%iyWxb)2g}_zh<`Yop?Oy$b7>xqx_)Q+h{sHFzKg2~V=m$zqAe%pb zxG#x;I{k??`{I|te`1xD`(-dDSO z8<2{$o6b7YzEPOSw8(3pKb|qe)H!d$6azy(d#7UqZYm0~a5oAZP&J!&M2)7|yoW9u z+(EctUx`o;{4@!@&Cd2Q>tqU-(=vP7Lp1MC<*$&rT;80?d#A;I=x>^9%451GY|W;c zR|TP?QtJK{O@u@U@ZipbX^a#*5GF2&TVmO^LzGreLLq9iC%}>y|E!WuV>=r#9jA}X zb!}p~j<+GxmtC;ALppS*=lyIbY9+i-th&e;;ZIx8X`lvF!F2@hm%Nr{XTb2)Ddu!Ad`XpM@UD%bbIlQ3(7E{M~ZF7 z%<)F41j-lpD5vVW;P%`YA8`?;t*lRD-#vjDVV>E_G%9W0j~$!~0lYL5?VkLmQr?x5 z3v+c|G)FZnBI*?NqiF^6VWKg!En78X;JCQf9(2jj_tc?$HhU7EGdF~LGhi>TB0iaEC?HB*drhUNB%u*E%LHrSaE)|}6tQf&&0aEb%Y zsIi5B;Fj6)snK$KeE7h_bx*-+>7HfzTZ-$}vC2tTD0>uNhPj5<<=HG-6s6CUcU4ScL+ge4qeLW4d?b}6@B4ApAxh^1(7@ow@HV> z5Nl=8=St^woiHH>T;wabGgj}}d*V@$d8?z5Ptt*oD=U!s4RCCh*;H+9)kdJ?Jwe)CeGFxYW>5k;z zaEQNOr@OvLo_;W&*bQ_cE$^nW!NV6#Fh1R(=GMRGoyZmF1}uvYd2TWo+S47r!LxKHof z(NVB|@T4#?Oge02J1@w7Wq&VB2S0i(vfYd(Dx0oao|SL*sIPC)2On8Rohl=wI!(tc zrai7);8QCEUQ%}-5wPdSvzpVxx7LJ3{HI}I0^;cLl`}yn=;2%3rxWR+tDdT1)E~R0 z<-WG**;6lJog!B6vmoPJQG=<4>It-KeTL+aj>1^2%#z)Km){r1u}-B*sk?a?V_m)x z3M7ax+306%?U!hdo}SSVMq3OcmSx+SZ*N*#Q6z>c`yoE9WU+{j=?Sfuj@L)C_Z7z( zz+d=Xn;&)BnfdQzZ}byAWz;>&xsj=FNN#i|?Ve~i8M^k@;^xKJ4S;TUPr*^K1Quh$ zx2f4ImJR6fX-oHxhCRbhf|`W%XXD5RfDT7}k|O9tU4=fH@4rwO2d;fl?Go=By5*Ud zzK`ZtzeQ@ShIP!%r?~@>z__qY*`2lxOHgI5}-+wl;pUHc2AInnMiEYev_sLX6s#OtN zQJ_drz{yWawm|^uurt`r9M-F(&!X6lzsGgHmT?uVU00q}pKUkMvG(h#mREE)qJw|K zkKigPA#?yA0qc=@R^wd2tX6kF5Ql*ihatoL@igMJA0Y-@6dX5YeDT)&KGOV8Iwk)% zKKB1VH>V&fKrdkK!q&NPI(tHUT3^&}&32U6Jpe|r` z>djU3EX&}=3-His>LYrDe~RojWmnqg6aWZZ{`COv7!7*_>QRUL1}T%%j4{KMwQD*z zKY0lfLWa6N#gf*srKVe$o?AjbVe$cGTMcP z&~)ukKBvlL<(1j$`-XgPn`Q?>U$Hkr+z2@G zFYvz09p?WeS^rx|`hU8mtc>*k1>yang9UA^Y#kKs^bL)F=wKIfL*xI?$@*V_X>+Te zY^MLAke!T$jQ@l8{(*ySOn-pl|4Fi9xB&q{CIJEeCz1WPWcB}IWdA#omE(U*)&xd+ zA%29>(N4*rGYaL3M6splshS0>H4*zsBD5>~-yUU-jQ@fO+VEb`nHqv-rV%RW&h9H%=+I-7yt9R$HL0Y_TLwWY3@yzO|h6; z4)T9}qUJJMlO6Ye)7k)wJD8=&BkSADtUi*-EOC@)_xz> zGEoYrlj_#!_9lo!|C=O9C7Rq@6bY{q>)9H#_Wj5rXN#@XEgpyKrvB{g?e=;4_z?mE z-u~v+>UM|He+u34vd||{30b@ld(C;v2RUcVm$pJ8=X}gB1|ypH;CBb!L>8DzQ%SLc zbSo9T>QbV5$%6MjDBw2v`+Ov$-Oc*=dG6r^!(J6a5gIbd|B7qR%^mZ4KGjNTeEJ+3 zs3ne%9H4cG&!pHwW@R^~^5ZtJ%Q<19nv8$0J`hw`GV+djT+ zoD}8pV?u5S1LE?xUG{??kRrvGG8V$z?kg-Ng&wsmAK%AFYNG)cWgHkk;0oi)C7Auk zJZr+z)t;5R2NUN={#SP&OGmeU-%uIr7+2<9qgXEc_EbK3BI`F52I6=NJ6dv$qkg2e z5znk_l;R2M)h&om&0c9icgy2DViFPKXlnAmNm8vhbO$jeRg>q&=VW9Dttmzyy6NR|Gj8MK7E49xlH3;^ z_?_D@cfs)(beNBH_iO6rL}B&@DXAWYuU5YisVHd({-!(4bNTBmiM@11WVIs-8l*$E z8QrDdv|V9oVbQUWvWlVs182dc?9--;(ui4p&teGZMJkE%RZjv2{^e1qIl*fI5@#u6 z^&w$-%an*nf?Gi%n-SZSPT}vbL~}Z(=D;c4ex=zISAr7PGVGLxM2BZ@I#LGat`3Qw zfJW5-xWr)^b>GZfKla$24BVw6#xaN4f)+w+=K}dee-)qBVN2%z7>2>eST7_n55% z1}7++ULN?a6c8I&0rFwehXG1D^%xncUk=)fjh(Av>goOoI05dn5a3#e6c`)at-+N> z-BMT_YI7T!NQE;}wTKxB%U=t8ahWS%bgJH=a6y^1Pz zK5wg8@`|V1vC<-~F^P=QOm&Kp$h`urrU-l4hM^srQR3w?^0y*KA=jO7j{*{mbFgFK zD|1QwKoa8=1{{#v%*4oEULib+Sk9cFqldfVi&q2KMiY)T)~Ae&Z@(swmz(c9*bR{C z2l19dJn1Jospln2#|HMPy8=ya+n;QCuc27afYnN)LorF$k34-zPnZ1P@p=@G8Omp+ zJ6j@W)$o+5q04{sb^4zv7l(``H-sa=YR2)kw--IzI%#Ixk*KwmJE1 zQdy6-7Ddt0cEqSYb&Mzug)9z<;9yr*hm`3a)$*5hDl!1GZV0wZtOMOKjfAKm7x9E*&g(SBRWX{ZLiGWG{6ZYP= zI!u_S8B;og6e^_>Tb5(E{4%>i5yWUi8)H?VP~8I)Now#OKcyHeK%t7<>uXz5$k%}m zY%gKquTmsMSaf3~cTEo0mTd54iAL4XP?Qk zTa}hL43Cg5Q+d^L1tm;q5k>OuDuDFEynvdlASW{^5s@ytGS+(e5xoP8o|*>!;&L)3 z#}msbXUbnypoE2X?Ms(B3m{0!yY&mO6HO@rQLzh`Rx^EBmevNE%S{YhuU!w}LHl3k0bnG-u!{FrYV6Lt&-Sl*61cfBB61Y9RSibAR#{AO_V_ z5}oG^%#i#tb)otA!8m?8#XHvEQi99qgxOn9gga0oeGIXSJo0sRZNU9_Vx-LsqxpScOHJYC+-qAZluJ_^tk^VqWl?(^Q%D7j zXkw#vOGs})MZaM*)8weh2gvda+1%yI;_DJj2kByCois~Hb)cP?Yl-lFk|`H8la9V< zz6-OP|~w*9ulj(pGhV;fdNp)T%8ogZER3!pd`Ud283a4H>scWK5|p z7jl{WB?bA<~m8P$U=^!(Gr6q2I9%L63epCPS+y9XSzhBjfwYGa9YQ-_^-(`02O zB@5cYm>yp-QS?Ijc9oSrI-V_4frcsma;*R}*_OY(0y0asOgSaDAEdwf>|$Y{HbK@7 z?%~Tz_;&I8iKIu!eb$AjFw|^jp#=0DEolCP+k7+bWoW_xHS>H8$rZu(XIO)Ch=0UT1wHFI^ z%SDU^idNL_n)(uKovPA14h8RAzNKS>s?`%ExQ;!OSZa>KQ(6HTXD z3k}?mx5eWgs=3%-A!;RX8G6ppv?9IvhMwO(*@q5nu^02QOLSK#u@{wlEGbWoLf=+^N_@;rBI8GBHH`?fBGZAsAC;&2#uQ3wzsW zdk54zLe08?#zrPj%P8+nvOhu{hLDvXpmz|89jZfVeP0 zn}xs?|GHnm2>>LStXzCQdL5Ia>_e@g+ie8eg`gN!x8Qa)6u!d(k{>@{Sy&s>baL~L z&jCxu<7P;P-XYo)5Kovnfpe;5@Ea)uzm`GBn9?)86(Ylz3V#G0>wDm^ZYm8aa&4_# zCWFp|!QLvl=dd3j?R{v)al*f@Zly2t-RD;ZA?dE9B@o$1+3-hTZO9!yg~HjUfSE$@ zD(VI<49{6~&Yl~{AaO0%tsP{{g6IkX)Bj}1wS}|AI=`%xurg|ywL1o!xC09n%)%PSqPN(K5LhqU+CZC5w?b5LI%it= zro?1#3uH1Gv8`x`1i`(oPn%U`aVsL4Ry|KD9Rigxh|Qdh!%@KWpIc`bR5geNmSV9FAnt2$KC!l<;}#dy(+$$s_ThE-0qzmo7SNqdvPDzs(G#8c&?xM=84S~*nx$C z(XzhEsBP?!J~r3D{T@>5<^17*>lY&Ql(a!!9Mfj-;dj8qXtzUq=I$GT#T3+a zsGE$?zXP9o;8w5`oEH#$ZZn>_>(aVS+uuVz7NUKkuLu;dB~>~O<6w77z(OaJWS2-; z6ArvXzDxp51x@;IIL~eMXzA8Mln8Q#Ed5@SkcuVq!3RQ4JnVuc=L6=N+1?)p!WY>a zk15WZqb+%YYMRr%gtABZ&YZ8KRY9X04mgU&kPvd&s^fs?tJEw0YmhudqJI~|=@ zS-4tDagFNK6^KkC?0;GX3~Q6dLUSlV4Np1H+FbtH+O)_@xKp14?4kzajKV?XiTXHV zzbSE$w)_FjljQZ*KD>P^hoEmKj&DW9fGW@Yhd17Z6M=GX1ahZ0BON>e+YwZBXiw`H zbw)$&6=$|vZMxhC@<~{JVUpxV>&~IT_e9HI$qV!0#QRZfw1n(}ivt@LOdMd?gYoJ( zSxnquf_C62N>$OnL#wL@1CD5dJ&9k;aU2`T?a&}k-8NK1ml|MqAo;bU; zzZa4XJCi@SQL8VX@BTI#a3EBa)g9WCOc_Gkbi zoPXUtv)pANzt}8%I-+cRFf@}5`5fCoM%WL4fgyBYw=v zjnU=J%INL2E;=ijzs;bmYbvjCPEs=?UtDLdl%vg^)iQFDA#AV{X-rxiJA=V zygTbgD7{x=N!u5=37ttRmjn<#d?Bzf5&!h;bfUt=l_A}M>M>JcHYRRJ@AQUX@twRR zZh(hOq!h3s%b&yY{txEfGAynwT^GgO-QC?axVyW%L*Wj=-7UCVaECx}4Hn$pgImy0 zhqd;3&Rx5AuRh(kd)?>$;!iPVje5R!j;i?@n;=3z#;1T>eNt<=j!_r_JaCP>5#c9s zwJt(Bp0cR$q`NOKI7&Zk;cm*Q(#F`LNBaTK2kQ$zl_ifLf-YNkeqxBiAPPV(meq{O zY&?Qi=zE|yX{;B=Sn$t8-6Dnz_SM$ZrD%TDS-6|A-8k8{rRHE*H&2KRbQbPRXFlZK z44sI$2=$!;Wh_x6D!;rypsZLIPTmZ)cXWiC@f%gB7SKsDI6R_C3r{W-%2h#H!|t`Y zfTycdBbN3nw;pK8G@REE^9ua-!);|=-q!YpJf7xSfUS&}{dBpS(VaQ{q5-D%tC_YE zsp6yAl(jSkl5sRc-b^A+NQ5UDfMD+ncxjN$m607c?l9+si{@&D@|qZ(z&-=vSr@;& zlrzaHgJk7fNzOmmE7B&|B97OAmebh{Z?V%(E=Y(0=8ndafk}#Wl&M z5(ye?&CWj7aZDp*Ii2r?$uPph95z|Iuyrs50gLY~aE(xQOWW^9?IhkO67Q%x3zVtE z;A#h_-$oG}$&m>SJieHb*1wVm{S(^ssW`C?mUSD#%TYCCqx#VIn?j5Zj5izY{qX=V zWI>BP&rpADY}<|-t~_;}=7eN7K3`wO9d7Mx`viV9VgtdXnajiA&)X z?WoH@2^#=6K>@F=C?nxigHH3xxwemv=8qYhtwxezvl0bcq~*R4i>^b=-9si&K_Vjqa*91aBE2B9`T^sB+KRZ_&xE?|?S=K} zU>I_saIa5@-pGERC4#24mSJSI+*Sc2>?G9SxoHPxR_gbfjD8igv=9C=?1nkAii%_Q z_x89F7xt*lrR<;8oC^CSeKtVZ-EEMvVcA-+w>i>y)yf_*D*St&=rqdaj~fM$0sTm9 z^yj4WhkEsC5=N{SbrY<$n&lXv=Q@l=CR4Uy8U^)I{?;QMh=l$*HR2|F71M!r4bYF@Z)qfA6l@@o0@sx!%BfpV#;h4otd zzzE|^^b=#KZ_EBz5kOMoeyo?unja1qF@(_*XE9#y5-rjrf^zNX8rkR^S=sS9JG%l= zE@f!3C2VQECc_3{Pvu!`nN6_*zPf->HOl6ywOiwHy{C33=#lRg>MUxTqkCP>N4OF;sldNzc1f99 zM`egUST(-EP1qg zoS4Zk?~!mxm}uKDnV_ZzZFiM14kN+q`-uVx6Gn8WO4YO+qtOIKextCE`1@Aur7bmm z!x)$~9ReG8ES>UN8Sw>p_!Ht=ETswE_g!c$l zeYuSd#;QG|Iu8E*`}>{cE=KPiBn{5*U!VPf^=ZGDF)z05|K!>7|1D9JgN=!ki-e1v z>%9Sxgq4?-={tT_v^x?wH{V8Dg zAXZTPptev|pt4X>WI_Oh7sC%9V+6fEMR-8dO$M>&MPK*hM-BWKe<51 zPxdX|6U|73l9@32?zUot>$~EFBc0FFV}kxFfS7~+n*)!a`OJR zPg0s(zj6Pn%>VE4gZEBpw*MKW`TOesJbwP+ z&i(@YyoWOXcZ#hvsYe!E@-`bf(Tpl9iap+E;{n zV$kkVm92)rgZ_AWQ!n8jz0Q@^!%Rp#mQj57Q}tS#KEF6#KviwqMJp6#S2Z_(VLsZ< zv36jZ1&m~a!byGzLj6wNHxW{F&rM^p{CZ*HZfY}!CrXEMbsJ&p07M~BqFs=U*D0H} zBFAdq6~K5VeEF;!5Oi1X=}*{qu0IMY{tiAkSlO6Z*-6+~xtZSQo`ai(iIbCrjg6Oy zgX7Nt!o~92n*RqNoWEtM|1CiN$oc=BtFp4cBaruZJia&LvhwmUvAv&e+^p~K;orIH zANlw{2MFhTl>2|5qDjJcqIURtoPu5LVQlME%`{&SqgMHaw>$Jt+{Gp z1pwGMMAtvO z6TVUS&zj**f!@D<%!$hbNd;fe@2i}4!i3OXiL;S)GuT`^p}Us+$3U|g%FCC)%bYKW z@@Q)&qBjQCPP_N7awbbAp%Iu*`#Vi7C8 z<+$xlD_Bsf`e4L=%m7<4@Xyj0xB*q~P`3cvX6;(&1)x}#JsQ9JTlT?-CKzRSL}cTT zM#x4EXnC?w74ynOO1y(OfF4;&wvR5H0>j9?M|lU#zp=E9aU&-TGWAswHH^q42C>bqSZT@eE_UErE7Lhq1q5t|q~K>#UjMZG%r*^GF5Ge# zH2Nzdf&}4+2Kpp9+zIzp@-||RQ>TxKKK-6l zhEm!q%Mk&I7@M3HUkjboK_{zp=);pMMMl03kr*AeZvmQqi$$PTu>Q+CxGMLTpa2sU zoa5$kDJWCL6NMvIr zR4mKF%i(H??s*>vrrd1kjsY9Sr!2$n&{OsR7`BMO$yT_Rcw|M|pwDUmEa`v;>t?1z zydK4%T)WrzfVrSl`u%ekGE1<^r|kPQ4aU)UYa8Nxk)rOyVpfe{;b}E;EA!`BkZ4ta zqR`&_;6P)5_|NTIWKt0()z4ky;2ri6onQnJatdu0 zLgZtZFoY~#FB^sQxKmdcgWXd@h>qbtN^U+Eg<6Y3)lq{atw`yc#hkYKE$$Sd&yX(wI-ZEE&ngJ)A9B)*4L{Ux8_2c8&b0S}B`N7TeLo3uoJE<}02uuyk} zL$3FJt1Jjs6(5OZ+d+I*JB5k-M{Nx&jN}+4qM)q^q21~-yb!Y`jUft28W1-rDqziW zDnHmxK z0IvAH%i9K%gT2RCUv_weL%QfsI!E6P_AHIGIwGYz46DzFc$0qw^VBrpaQ{;M*h)5T z6^{BP9`_p#2g8hLLINjF=O?Ta*OhJA-gA^L)<*RxR08j#E%tXU%f|$g<@DV%a+yYW zaXl3CO%EV-G%qu()+BxH6*ijX?Uy8ns!q-RFlyA1o?RoQ8i0U3745Jtb7WNV&wCUlz z{Acc7f6@#*6oKd;gA^pP;m6C;I35;O(#*XqSaJx**O0duizq`RqEJ!`9MF$c`4nLF4_N#6HiF=m)dFUoEM zK*;owb_7iJ$n+eIR^w>XH8Vb)iKEj{?Bj@>+@=<*4j#X$3LM#MBpe|47v{H=I)(JV#Mnls6j9p-waT=KaEKX)@fjda|{w~ui>F9UX^az zwcuVG@Y#6QUX@+#x-zM!$?2WvT?*llYN~d&7aTLv7tc;sGgOI{&H?UIa`dHu^^1jZ zOrH3^c5kqwI3;)PSVwi$nzttKI6g)Do#~t1b9W{c~6uNZaGk?vc!>TY{^p zvH}PY(s)|JWN#G}{Rlse~1ItI#2wPOraxmRcW^Xr<20&A1!v!b6PoFI)c_F{U8u=LW*^A%+w21|c4~-%BP+C2g?_MKw=+%AXg?swm-v|SoSqUb@-@sz z*cR)kN0GjsGhI4HKY_Ut`|RE_ZS~yWr*cY6)bgKfx0RbyY&R)1=gD?E*JP#MYQe?D zqtz~zCWXmN(hQ#3d=F@+hBva@j~kdSp2 zTk@A=|D1^04GfnQ=@RoK=VCg&PL^iXzdK%RSmdUxpH^!7bhE)V^(h6F@&a-O911uN zXCG+0YS)wN=@(BGxlV-}MNvJ-1Q`CFI_6v)4MB!8&MV^ZiK72Wh6}1(sj*r^{rxa! z8Pf$IAsNT@;l%czm?gHv+g@h=DrBL~u@Fs1m3^ zlRO;X6{wsW`3^a*wNNQ?j!Dbld*loI<~pZ)T2-{Fs%`WG4-KX;7+X#e%k=z{Q40@( zhEq_`iZ=;YgdlSLVlGAf@HDvYkQXD$8gAk9!|)D1$(yd|>7b0L>zr6NqK|!($SsPQ z&3N)F;@8&PCqA-R_1PUzqaN|E#JSHU(lq5YgFj-M!gjYLY8yObD>Q#J8nSmYH=d*^ zU#A$@J3pI`WH~A(f(xx!V0yIMQzLZxq+_|Z!tHP+xDeNno5Inbt-0%(%7$)OI_zX+ ztPuA5_ILWt=)sgh{OYOx#pB1@q$IiNMpI|=t&=qI{a3GEymj?bE;S#er(XZ7Nk_rQ zSzG4?TcQR=|3O4I>5#Sle ze3FTXr)dX=;oGNocQCB|g$xJa0@D>cXOjMNeY=UQY4Zk=it!4yQSR9TYt)*n*wcDu)AKr?s&>a`DN-9G(IRpNHcI)E6%;@Lwh{h?l zEHQr65{$a?6EnJrSm-;C=6_Fx!k8gx0LdF5bm$|g9+ zSRsyrYK)n>Ic}^t?t@@{O}a^t9vszCtqRtQIyxYaXlaVt3LNpiiNbg{oE2x6N6h3} zUmW^nawc!n#_=XiXjxx3jlheA%O4Yb3(Ay_xxs)G)V(1`6HzkD9wa+)+FEG?!hCCJ z8A4Q4IkrInCBe72OWX*aGZ@GTTD9IE-2n{6oy2pO7U*R#tTU3`` zFhd!bQ9g(Zqx)}**< zz^$U%VUP*XZ_A#7>`{)(m)!)Bi8cdaOF){IrPC1 z@m1~k_>^;j3ry;Qb3FTS`mAa+L=LXash5))(fY|8nh^*djTzu(B{;K~ctU=z;4gQ@ zAkrHXz^0BV^GHu(FRJU!?4`Vig~})F=Xb$sImRmq0KhMVnU_FajY0l3^<2w zh*0&D#cUoxZ_ z)@~}(A`VI+yl4LPQs2~z$Cgvxco(1F<7)|_UKx=Mc6Cd`dT0^`5-y?lOhoawfUUY& z=9PzRoW4fbC)tn@k;)WD9@aA?wyzTE)iJs zY`U;oeLzlwbV`QyD`tNAU4P{o-@zIgZ!QXe^)tk)I}GuNNN#)qY@W@G=^ZQwQ3E+%=DyktI68Gk>&R|ISnM4k__C(|{ z1{yKt0Wm{XA24LmDC5%V*gxAN;B!d|q|2>Z@h?|5d>&9ll)@*#$~d1fYbtm5f3*LK zX>n|fnLCLh9XTtQxuF?e(PhAyu4!6J`dRisr1`5N5AZTjY_Xx`e2&wO*D*K?Ei?{) zI975gToEabtIaceWgsYbt+U+mfrtgGb?;~880%X?Bqi4;!G`+9ymRhf;o9|G^%(v4m1P)7uo3_uB@+ z?$LDudSH4`(kwqDS&C9zZXS-fAPE#NS?3>4ayco}w9mYzr7cRoF(i3w0^sxLpog3E z$U9bfF!8db+kRTeO8Gr_5EhqB9By7XWhKt~Y&9T36rH7frS)*2bs(7c>Y&=rv^ILv zupcaqJoS(o>abz|BIv(ruJDtRhDv(su)*V5ChHdh?M2owQ-NdC_NsGj#+ckGaMkcy9CO0LlZrJ$qb1%)Ue%6fkoI$ro zUsVX>Zm$=h8m}00{6O$ZV&)0{YI}*MxLFuX$n!O>;O3`TaH;ZZREES*eMb}s{UO(f zvsqRex(!6v%3G$96$0gH6VeEb`Kc^HypN8WommvNX1K=(6G!AP@|CB`$rJrUF&B(XAtzBjNQ1u&7K!ReEtlEPt@hF{>Js#rJsv>4KG zprwh5s*^D-Nz;5+i4vFK5M|~ioFbe)yO?~VpUE`!+&p_Yzc`D2rSJY|7a|22t_%2K zTn0&wG%W8~^&P{eABiDiRZ!iKq?dzW3m-;*L!2kVp*#zpS-x25M)nsxACbHT* zf!im=i1N?Vb_7)Z!n9#w7;piO30i;?yWQLgE%7htUg0Z=X;(Y5hPjc%RXAMNDdrZqXrL`L0SgOQ z=8X~tC8m79Gq_7->Q^yqIWy5Mpy(!;{ivn3hYG?Mfya;JNzgU9qr{d0nfwNVwv0qH z>IcxRclRiN0-!%i7yb@_INr5IT`c7u?=a$KV&i!4Gv;LCVE;4xu)e?Ik908W zI}7_abTIcH^9ugnMOofSXtsAVHrsDk<#<D5AIwFS_nuL0*cCjs5ocn+i z)B?d93-@XM1gZa|nt_*<{cqF^0xK?S(#eiVI>QNLq-yJ=rsc2C$HPRl!w(^9=)1=d6TykNvDQ7LO zxROd)s8(w8gkBDm#-5*=wiLTA7T(K}-(HL5WmPQ4j_RiK$0)*p7Rho+7FQ0%;+hos zm)ng!e&156UXG?`_oFv4-!_ci|KB}*0+{Ldc6l_t*^3xR^(e&syX`!I+;7(XciVZ} z!@KQ#g(2?xH)!apAI^pXRnM4_f;LK9gtX~0I;Cz;I! zUmTdmR~!ROc9t?nC3dhnMinqFAn|k9FolU&dA9zJ?Oggd+xa@isN?miz4J7*vZW{% z>PKvV7UD6wd*1k2EJLU

    @EA7zha?IS3{jHpd2i7WUl%5FjzLGa;zLD?~{|{G&xJ-sC`PoQDYa2S1d+lu3x2#FQ4)~lv>DD+Mo$6c{{N6*~*iFhQ|X##hZ9>OkR>|LvI zx3wNb$ir{*JR4t!!yk8m@@L=!eSNXFsTv)CdJhSC)_aTc&wyGl98%{y;G?w^qQnF= z>1hpw0`ZF*0Ap{$eFb){Us2z1s_zoL0Cvp$ICT1%J4PD=qBQjGq(J#g%=v_l?M4Ny zd*F{*ri2bji4vAtqa$`6WA8|n_*aJEqjglG1Y`<)u)}B-zz6?IXp%IV84O&_MU zCt+=h{ltpO1~Rq~uet#=@ea@xY$NUfUBilSbLTR32PiAl_ji(6i>I*zcsA0rg>B&t z$PZ6RY`ht7#n!9aLCt-ld(@4d^mj=dD=6$o6t;-s&)^-RH-h%&{rO-MOIoBwX-$io zvNcWUa?v%RTaM8VskH=RFHLJ*)Ve;kUPw*Nx>j^;=@z2t((K(1Kuzd9U8kg;#OpD& z0xCDV#h?QVckW8;d03z!E4QRZt+l4+j?JyDC-)QC->ZL2asJ;;wF>H44EC3g!%el3 z+G-xvNTYTu=7@iZdE!yASQLmiX&ioAydyUGJArc={gK8Rux9i&V6Dim_7yY8)+t&h zXnCxE3GV29jE-EBx1J{T>evZYd@d2Kl@+5GDpjTLm0qRyRi8YTe(tEw9QC=La;vkw z$hqSh@?TGO9ZsD;-epew_Qae%dZHH37ikn`i)euFDtzQM@!?d^oD8aPIB|@arQ}VW z_KDu2zseU*5o7bE@)XHWBrm1pLkuRnidX$dVel{IDSE0IDTi2m3)TOGLu@&bJBF@5 zz$sxKsQUwGO;+|zu#bTKZrEMeSHa#D_94p7_Q8H9{F}o59PF3Fz7+OOu#bV=Q}#p6 zmHp6*@P7jFjK|k1@F|4PB>2>W&!1p#0DA%K?P2eRkXvEThCLr}Ai~r%X_aW$T>$@% z@E>c!DF1b^_d`67z}^k^>k!*|_)LM%R@eu_9yek`3*ym?*s(p~3QBXO8D-~FZ@AL* zVWux6pPw4BBRyf_h)wYzmS`L$qcp{|MUT|f`wiwflwL3TlFP^PanU(2&S|u^ZKF?H zp9kwXm_C!%C%*b7`uv!2JLZH;pDSx!(0UfF^*oyyte%S&myGsi?uQwsKFQWvi`j2Y z_`-)@WHhrHZjW-k5o$AJ6sHDpRsr>>+nyMkl092`bR%$d?zpH=Dir!mrNHqDoDF6=+V zw2Q!9yEBhgb52i8dB(}ChQ z^H8#3g+#3+pXbsa(YdF<$*jQIUH%Ssy%zcl?0Q!~YsU}6U$1dBb>jS8YmsMIWpYjZ znp;L|WY>2cj6g`O2OaZV*$XfOU$Oq-dRO2)_+tg7wZcoirUI9d)Y|U90fc^93-eG3 zJ`#!5)N!vxU5{%_|FkS_6;X?NsWkFYj|!<)5o#RJ&*3ZG2uO}ulR-UT zq{rKH#Fg&VC@<{2$hQG8hZi`z(z`nH7C`Kf==~yj8Q>!&^(StIkKWCA6!ueNY7u)W zW_64H!0b8b5l3^FVGSbnE=OO$=M}WEX0dQTg@s!rETm{Rg#EWaOU;$m{$g4m)kl2$ zYejx|VrovU{nk^h#J9g-OdPuX1!-P-AhJ6_Hncu!d#3)QHCIqV?{XcW-fJ5b)AICg z(e{`c{iviRSuCdhR+qF4Q-8T7^(VmxXW{fx>KhAiSAq!{ZoyrzEnghAVci| zU>~4_0;n|-1$2nETosRsB^rmJ0FP>4lDLbSu<6pL%Z+HK*qghJO-tW?}7a_Q;k1>YYg4x1;R} zi~C!4hV>n!})$! z&5RlCSmU1n$idn(lEYi@q($@Kxhqvu6zI)Z?bac&F!B z=EOmdkGhxDqb){H-8W0DJS^RAv7;=unq2Keo(qwOLd2}#cZYUIDMsHwq|?$j4WZ>r zJ!(hJI0}L9a^PEtJRG(vtnk@D?5gnCJ(Z2^$p1yS5jBtD3hkmdndb*rGP~c%e*Ib6 zfZIn7V=HeOdE?D&``{b(wn^_!eX?tHJ6~U(u4aq29YQ%VtB(wfUJ=t&BK$gQsu`%p-ierkx zDBhPU>=u^GZfAF~drT;cRb{C(e`t?c;MFvX=*I@J5$skr zmi?aH%lj2-Sto5@# z04xIBtDx-wRvVUc>u|dsU?afhfa!p3hutz@nB5VuGhjEsOu#I_KErPuIK&j5_behOFwxcknWk8FbAoVrLm9T5hQZ)mrPJLTT>Ze*$?{_ZsWam-8 zbUyVL?Wm<+Kz-hY)PG$>ZT=GK8840dvoxEazO;HdOtm+r;F)E@P#I9k`_^UkDq!R2 zd5;WJ%78ZYqgAK}tcKiI*2US`WkSdxG0h*M?~BwN0=j%p19Z!T^~!)sez!?EOwO8? z3BzSV>{7C`%7Btan^ab(0ycB!;1OY2sd3h@OxU0dC}_UWtQ=-pY23h;vQ;$W{*vvb zId)Cnh-ZlUqJzj1gT)vzLCg~K#Zs|KY!qLLy>hgiA{WSIa<$wfi{w5l*{WqVvD#W) zt!!(kHP)JFJ!CDgmRYNd-^rI6f7G45pv z-mEmch_1M?>?hSyt__pd{kd`M#tA* z+LHM1Y3mcd?~MOmHx1uI&;!z(MwJdUKkiO*#y&J>%weYF>vlKhr`oJ1jQO}RcWU#@ z*~WZVoAnE|d6ucs2Bvfxn3``mHnH3q8ro>M2l=$F)l)tl=Cw$+O;NDrbySYekcS~^Zi+8sia~#|;6myt+ z9NZR-#np}W7{a>cy~bQ=OjBE}Y9jO*G&ku&E0!!;u?(iQ$QW8bO<+^lES5)W%>wFq zS5V)(j(XqC)c@|H9+*)d9Hw44&BV9I#AniJUCo#awVB@8m=A07+z!T^tIakcW41MB zfiX8~^Smy`%+qFu>02_=joDwD=NrmDf2c7{U(~jjF->}H4gT$V7;~#O+nc`Rg4)J3 zeNTr3s_n2lzSKMHOmOducVC#A;O-LRruaMVjsJd8=LC1Jcz2Jic=y#T-kmuz!96Q} zT*%CWyJjgp%3bU}Hkr+253_kJpDkg_*-EySy~{pgTbb!cGINY+dhlzqjA=%Qo~9Ig znp(WJrZLAG)5O!uGp6bBdzsR{uDUS|m0UN-m@jKHtDZ4Ud|9U7yWWgQ*UvMiq4sPu zvSfFUFZJyF_)^MV19!Djd)m^VyE=45U5HrVwxx$kXg>^BCh-z?W=AA@b5 znZ`7w^IKEOzcuOfHEpP`X+!-?uiej#0{u)I>~BVc{%hm09gskS19IU`p%qenntdhC z5cEH0Bp8^j%^OY0-)L%TkZD(g%-DRB8M6o1H>SaHh@p@nyJO}VbFhIfz8-|(#Xeqeb2_;z)eyHA2U zJ0ZPMnI_IupT* zgPF_+u6CHmqV{mr~PW^RhHqN;#OzpyEWaKw8mAd7aRJcCbLysC>fK$!axD# zt2VF4oA6e=Ex(v|<(WL2_vb_T2tJ07;}iH4K8ruh=ka{Lgf9p0-3I;v-@>=^@957J zd=+2EH}cJpTG+#jg(EysU8ITzqB*2MI*D$gr|2yPilJhp7%Rq$iDDY7E&7N-B1eoC zcPTw1VuqM4=8A=o3VBhi7VE_(@u?^hyTv|8frMpEnI;>_7WC%2D1UEMbz$YEj@RnE8RFFc$}&hFK6; zM&=^2Jp*%5;8~dYf#+Z@4lIXR5O|&mGFOl*pF+O?b5Y<$nE8R1U@i{446`8c3Wd%O ztR&ZB3jHd~MS)c?^8>HJTpU;pvmo#~g4y99RdlAn+E2 zE(okg{uc(`Hu!BY_`PHBd)MIi9)(^Q*l4hP-(a`NVE12x-3JD*4-H-)8N5C=cx^U# zeFD5bHF$ky@Y-VV`rP2P)!_97@cPo=^_9V^$l$fj;I-Z0wF7wVG<$|2iVa?eG+sfb@d|Q{S5RoY zf>PlXv@~8pTjLdUG+x1g#w!@qcml7YHjdJH+Y?E@M>f5IuCed7`)Cmc(pZnwKKKX-r#ouYOjOA??QuLM}yx* z8b7Aihf3E&BX%*_C2uZji8E=0=u9*EY@Ab!VdH77AEFiiWSY~@q4oX}n$fSO)&55` zpWjViK4sh49vWDZc{QHO8&N;qo_FRwLT}SJaY^uMYg%Z7u}u%X16wBfy=!dKL+_F8 zQiR)RY|}&U!=}P*GPdcV|3>569F6b8XnY?;3$ha_p4~SMbUJ>j;6aSn(jBzbazM6 zRkNPZ3XIt5481nZwv_|jpAUi&$ruA-3h8a#qHA#JpY+Y z>0-}j>fZ9cOi72^QHlSth}(|QHzNQ!n=Gq90{eOm0n!q={_ zXXEQ{qF=v54Bxodkxx5>`5y7?c6%damI|Zdr!d*-tBPNL6?;`YVvkBoe5+y*KTyty zQk_3K_lZM_QYDkg5-UF!^BJ*S?9gGtBFw`QVXTySVXaS~5#~7zvz*Hyr~0Gf{qE7SIz>tz6+tBw_X4Q?0;>QN6%3xE_!*2 z9rj)JID5Q(uYI3A!JcSOvM1Y9>}mE4d!{|he#o9}KWyjObM5)|Lc72&w4bt<+0WR| z+b`L#+OON|?Dh5rd!zkd`(yhv`%8Pfz03a2-fQo-4>{5aI4O?roZ?h+Bp|G7de+Wy`8?!jn1f)$5RSZmV`ve2_=WTP?b>i zP_0m%(3znIp+=!5p=P0$p>sp$huViahAs_V9=bBrBh)jL73v-88yXlI=56-2cwc(k zy`A1}?>ldgx6do~g>U(eAM}%b&#&TF_Z#`m{1$$?pW(On|Ly#jh!u9%5&x2@*b+OKg#>%pQ*;C zqsIO&ACdnmwZ>kPZ%~cB6RokYm*2`i$*J-$@^A7V@}KhG@(KBrd`3Qxx>+OF$qjO& z{IA?BKa*cjTiR|nmfzZq?WXcib~n3+-P6vpd)s~Of%af~m_5SIwa3_Z+H>rA_5wTK zew@nsX}hVt+gyE&)I zdd}(2FsF^(lWcvQ{?1MICn-xqRwx(>g~Fj~p_-xEp|nu_Q1ejh(0QSDp$kKogf0tR z5xOdr8R`|v4)qE34-E?Cc%OP(y&`Xyx7R!1Ge6{q{c3(qzlq=6Z{@e~+xi{+NBu?q zWB!x=)Bbb*3;rwqYvG&2Bg3P^Q^HCc+%!iNGAZhH`KEkZzDE@GF(_)A+-4}MNx3O%solhW z&VIpu#eU6xL%vPsJNEmAqQ0_s*x%US+dtX|9q!mpQk0_V5=Gq%imD5W8blQJRH+oz zD%2*_Hq;?>aj0{sYp8nyMSbFZ?tSHb?fu~G_YV0fzVBD{Yxs@*v;CI-x&Hb71^#@0 zpd{XPB>*D*^L|PN8ampo{+iV}CzJ^n8 zV^e>V=P$rQuAQT@1c4Bxi8(Ue0gpwjH1cpubRr{cg>r(Wvh zU4>C+LrdlNOwbI5*1t51`H|oRxu4)V1BVY?)3i5+2;PN^!;swb2$=7QBx zm?6 zJJ+z&onFp$tgdsta|1gwWq!(H)*xknh_UAGb#5P)?$`4hu=f6i{)Ox!f1UphyV(E0 zFJfK7R@h?Kh3^R8!Lq^+h96|tqYVEE_$c6KCU5l!<9ddtk>MVi(!vGV!$h662`*r> z=v5S{`PVq+=qC!J$$mlO zfwSrsC%ha4t>ktg&rbk81Nb`NX24J4Fxvt6E!MGX43-_KRP^oe&9Fy0i27`Gk5pUi zmFjZ4GFom&pJFxkGJBV3DV^=1cQx5}u}{fX#Fnyk#_qEw?_Nr|AB%icCE8xe)>FL~(VGu_49g&$ zSmN5k)Xk6?C%gvK*r(vsn7(;YOc{I}#aQ*$3VvP+z8#5{`{3sEbfT&(qV3L@mz20h zus){LuZrm$$pO?3fBm@i?IVh{68`&MTXH!;=2x2p+SDUYPgNtx?+iQARa ze8&1V(fZIid*smS{;?!A-}4Xnd;K5%eg1y`pkGX(lhka_8A9{St7)crEzKF9XX*q2 zI%#ocGLW6)HS!vJO}wUFGw*D#xp$7&!fWZZ@>+Z8-nm{I?>sNVJKt;Twe#A07kC}K z3%!osMc&2UC0-}*Qm?ajnb*a;-0SLH;dS$_^tyXjc|E+Vy-e>Kucvpd*9)gL&6GwG zF+ohA{#vjutge^jdEV(>9WTwR>(%ql^y+(Oc>yozC3`7e$a6j43wx({RlKTRHSbif zx>v)i>7C})@@ji$c&T0kuc6v2<4u*0K>iK?f$TpDU(J(vklpPqbMJR2x_@#fxqo&i zyAQZi+^Oy~ce*>nebAlh{>7c;{?&cR{hK@6{k!|H`wutIeZ-yP=DUmC0{3zEX?MB% zqWe#Gp1Z(Z%N^lkR-? zCHLR%Tka-zr~9hA%6-LM>8^HPci(i^xgWWk-A~+4-7W5y?pN+Mce}gG-R=J1?)A7Q z-2b{Sd(XOWyKCGx+_m0w?rZLPcZ2(m`>y+*yU~5${lNXu{n-7?{oLK^e&H6mJKS&G z@7(X*J?@Y0KDXGjJlk{J{q6zxpvSytTs6PBnrqQh__5Ej44V+V^ z^hJL*fT-d|8Y^z1zG@1aN^|u^)N?GReq$wjm91j0QP2K5^(1ewwd_sm*WaSv;Pv>XwI=n7Flb^*K z^2U53pUG$Ohxly%Fwf(2_*_1Z&*uyHLY~hH_~X2gFX2z~XZUmcdHw=_iNC^M<*)JA z`5XLA{uY0mzsooBP5eXtG5>^r76`bP8Tvk28ZN7SX!_0;exD$^iwwO#Mzmc>bp0eK`)Q)=WklP{iSkzvZNETm;Zd{()efd6(7LAd*A1=z{FJ_l zDQ#X!kQ3_6E~CEXaw`8T6i*k8uvby;Lz8^m@C`&*$EI?mg$e-{;(W&pET9P5V7H7p|zqR2&r#^_%_FIqE8Pjk-=%LX^El zJ*J*fFQ}K)E1IQwT1uv&$^lmzp-bWvx)9J(XQTjN2f<8rO(phviokQo*`E&tY zM4zWG(53Vhx{NND%GAi{3K9z2HPBKd@lA(%^UQ-h6Q?0k>R_~BsDtpKgfRknK92Vd z1V`#F)Ya|@hlL}8y>L`GCOFht2evKre`^~bUx$HfK~pVMZK1G8SS5G5=VmI}*-2w??AQGLj-*M8+H^`zzuCgHVzfPP57Vg$yBF{a2Yt_EuJ z)fTADP@AbXOKrB=9JTM&=Bfp&g{aLFHVEs5jY5L3N!To;39E$|VU4g#*eUE4;)Jcj zHetK4Lr4*J344Ss!df9#SSKV3@j{Z2ETjtig#7}E{svT2I0k)eUG%RhN!Cz$Y~Cs} z`|G6kCZw0-y)w-tr&rdbJQcha$;(X0wQ?zmP=ly=wW(@>YC&q#)TXOVRr+YPz_)!g z`L?c)R-69DM+%&JDX_L5tO4J> z#?EUEk&VPMvww8`lSoJ6oZFvxg7^D@VDtebCK5mJCL-oT1bn|wV4!4h=ojSR8B}Y1 zg9dnq#_#xtR^TDp)cT0%8ZWU1{6s8xintH>ii7WXi__pQeg=>63;2xO8n03KuHV4g zpvBu)`u$yGS+h>=;H}lCft4W)(R5DP1jIZ5hbJ%7f)s`t{$|#X3hwb;2 z1V{p?Z*ecZaW8#vFMV+@Lu$8A$}=g)q<5mjCqDX5nNH=^Mn<)Cz`1Webvj>ruh(Zq z8?Pb{Yg(&ndsn8p zBgNvJxY~b6^Pi>G5B~20>9woRv-(y#U(X}2S-ZziQ6|Z~qu+4*Zg}l5P(nX_$&CBM z-o(Fg)<3@1`||enTEsW3uknnZI@W#493WRK>G|jc|Ff-!tM%PHASF5IKQGDu-QGV| zCdm0G_d<2*l%v>x5assQsVC?{zL9snNsqbKe<+{-cI(=({rcRxE!Mjf6kRh%+NaEo zf0w$~HJtjYgMEthxj_0z+DM7>tf$ia?^4M_S=YhYP>C-jKGFNLOecqV?ehP`*(A@; zKIgLu?cd*Vc3{|7-8L?{GsnUD9JFz_DrZf_H!_-Gy^U42Zjmb2XsCJ|$GpuzJwfz? z>og=a6Sg06&IAuXa+xm*|Tlpl=Npd?-JIE4-HfKHZsf_lomK)vMN zpx$yHP#-ccgWOj>1$2ts57bZY59%)u01Y5>k;tdY13?3!OfvFmP)-=-$e?)Vf>C_} zWs-rCc^?!<@PxlN{g##Wlu;xnF|K}^lK-Zhrr^6s4OAb)Sq&q~RN+h+p^&$8=upVn z=(qYF=g@_8DP8g2p2ocxbH;)h{Z_x?aLCnICV@$2_A-Z=Q%nw1$dob_%pK+t^OBXY z0;|R9vTa#Iwkz9|yp4o5L2erECRz=dB*YZ8<}( zE7yxN=PbC94?d#=c2h-E`dwt_Hu{0Q(O*L$dz&x+#T)__mY?J0~u7>Rzv z67$0f^TQf_h(G#}0L+_Nm^ZUAZ{|SWJfOa(SWbjv_!?{vt~X~6wm&z78v)jWv*yNw z9ScY11lECbflIcaUo#m!tsWI{efG?MT3pxR&#M+W4VpoHn3aZEbIZh zi%a7UgH7j-bD3aIaoJox*gQBl7r~ZrSGWqW<=joK8tfhJA@>aI6F6fW#qta<;{~t{ z_(r@oSS? zD1VC22Ajp_@r7Uu_!9mK*iycnzX`UIzr#NSd!K*8zXbb2!boHkE0IVVNE(4{DAAJW zfNdtxle7ifTGCMht!;@RdDj!HnWUGbKiIyKK@tnFLnW4yv0z6_Y$Og~?IbP|Pp}>m zUr8X?0Lcu=T(EN_^CdrkT_g#YM1qZwC?v69*GSe&wt!8LBuRFGO_A)Cq=P*mIVw2? z_Jkx$k_R?NQXnY-TP!J+l!GmkR7&oEy(PIXc>?y4X@GPF*dXZ~>3pyu(nZp6uwl{&sRC@Ybd7X9*f?o|GzsiBX$tn&-y=PMJ?#%m zPhcPUOlc1GZ_k$&W6$)9(lYE-ULn1O{lTlHkFYQIGZ{sCVaqs~3h6;D6J(mC@3Tx> zrc3%O%Ua6XlOD%1eVH-oEiCIQ`&!l)Y;U;chJqa;8zCDF)><}RW(U?*<|Oj~>n8J- z1%UOF1t1A9eQA-e_krmR}_2<$`IGdTtJrJR$iP^?@g7v!2?8_Bih zx?pwWE#>XOww3G4jlqiYu5!5kDkt{{B6vil8j^rk{w8PAW1=zf+PjW zP9!^#>_oB)$u1R!lld~#v=@O-op7oww>bP;*qd>}FKSV&Q7ycX|$g4i%oYJwL3 zeS}yEQSq2~0&*c+^brH1q!^h(7EvNA;a7~hQLR`Bm%lN{yP z#7N3;GKBy&F*PpUK|=H>eZW|7Q4+iwK3%_lkP$&5#286I-LQU?5UN-)Q4;(V|9+Sd zYy=LXN73_`WkMU?UGIK?5JChIqDay7XjXh1{@w6?^biIFR3d6ojp$|q8=>9Me&`TP z1Qw!8(e;>S!W-UQpZ=dC`b2RtlES+o{fHr?2sA`mqV3Vngg3&w5&fVcun4$BouckB z&jcyH`Vg_?qVLhq_%{N(QT@0fJh4BB07L>flC4NiLK6mV!=p~_x_ddY4N__2+qDm>~LfEN9ih=+cp5Y{x%I5BUt0OjukD&7ynLFdx7w#ADEw2 z@69FsOIT$Kr1T|ErH_?vP1AB3jvnP^ny)M1JN6)0Nq2EEuNRizjjWFgj$e$dPYbpm zovhCbmfwtw54? zY>Zp;fWBpaJv{EvDvweA%@)k4DT)M{4-ti|Qxo<+BDw)4x8H$WQ})-Y(i$PHqmaj~4CZficr zXALDAfETJTMAAnu|8@i6+KyO)IO?VXw$4U zRoV$H&yV4#Ev9FEKO!4xHA<({{C?+Rs_NxqIh8?Q^^WLctKe%&Ua8A%q$&cv%C4{V z(4z8{E%kMC>TIvZ$9><-RW+j0rBRFLJL96;=$Dx%KvuN;b7mTJ=l3m@vkLBmHDa&z zt)W?_2OjCA`P3#UHF>BrdfvM;*3Mn3v z7;$6!3*({g=%z`dc?1`L^uozPQop{RK;(bNr2(Daj$%NU#GkiF7($n~OLu&*PRHfo zkF8x#-NVwhOZWR6u<3O3w`%lu6A}=UIE_RGE5l zxny9QjCbXpjGx+Pl{kH2mEexslYGu!GU_#Q^T@0?dRMg6Mm%#rWOAUOkL=sJ$|CN@ zh{0WM^R^;B)rI9r-EAf-Muzdn!2fvFLKoWOf~2}NYZox9^u+xVK~y&a3eY!2aj&JV z*!zlOarzv2G5*m~Hok@}39(;JuPre@@=jvqn)>l&%L(Hk+?UK((ZkXGwK3yXRP&gkTxlEwiv1uqV$*P&)itq`2fX@8G@1Dis`O)p?WOU zOV&hvEL#i7Q?f!crSfDZA<>`N#l% zDTl+>v#n}!na>DKV{ec?vK6RfPATRwj+q+Ydo?!PrHk`f|$8h^BIb;VJm zB!-h?K2(uS@Q5kVvbD0WseImgoV-DHm5YhdX68HsRw&Vl~CXMd4h^Sw7FC;4EK zwUEP>_i}Hpu*SGw?uc@5Moo+j3e5Lj9oy@&E z=6@y73}mS1%FpUKp!tJA8-ZpaoMbX#uP>NhR6)ZKT%%K-aubfd*XvC0DGA3Alrr`H zT#(o6-WXY=l|wVY#uK+iX0wrepc@ z9L^DlBemfV_HUNOG-w^8?Q`=#=0IgSdf&^JnVADN#tQes&DqQW)Bz7#d^-jeq04RC=h;ZY}rK1t=y9i#*EsD(5GTd|k` z9fUI0Bl#nQHA70B6(Ew)=2_>R=O2nOWm$49`K5R_!drnIC{CH@1?P$8FN#NsIb;=N zopLYvHzHa=nUEc5PPyk9=Rb-;Wnps*^KkgNB2#@_0UwA?$>+0V8U8ideN+8X!%~A> zL6~qKC`|!Q+2;A@3FdX@?}~|wQDtdzwfL!oI>KFnAAXslpEAwc&Yu+{7qbN0CTqu* z6wd{Zj-P2%?a7g5uVzb^+hNP_HwZL{I27A?aOAM(wPv^Gw&t{c0po7_oxTm?4u8mg z$o-V1JGkXll;nvw%S}!z{qI|lTvRrW$Iu+DH84_4r*5&6>{YIE-ljlD)a#G*+E!uYS%I3q{t%*{%sucaH6&n_O-%e|w6imIilA(kKZ ztyx8#o4h>WS3*!6_kE1Q2CI(bP2!%e_1&ul` zHLy?3-l2-H$h<~Def=1ae|U8ai7^gu(P1nFvWg*48l7-XyU4+JNoV=`%6>K(D@LJJ0$MqV&gkhqJkG)b0pL?WHh5mrSv3D%AV zP6-@EbMswxl{ACGU=;s?A_e55Wx*Q(?`4OI?;cOU-dfsiD4H)eIpz zXQz&QNhw9*tuj;VzQCwzMpM#8IqFl_C%Y#HyT`dRD<)nm?*yT!PQ#zC=*htzlF+T_ zDXAC@@ap7IqDLJf`UmHz$CqG7S(d6#P;;CEfzzi**2pCOg(XsTlH#rjN%jd_PH*jhF(YFEkI+jrSc|wcY)e!vyTk2VI`k zOy0)N<&xR9I7ld-pCT3XTUKgKg6as(&OD--r681&Kt9Im`Q!#rm#$Y&R}J=)LHkQ_Ot{5=;UH|a=gD|ym!rz z35?{{%J3jXc>X5l;uA`^zUd-q+)rR)R-@OQJ9#t0B! z9VTE3rP3Zdv02|dbjAR3LBy;uD}9P@UE|8nGazG$3j|x=R>FGrO4RMV93?n1nQrGV zjtUV;45K1zs?ZE?&fbCZYE!3{>Na_}7ECO{@+0qxI!#x-8ob^lo-A=~Q;a(AXOww5 z_~RTmlnSLG&x#)sJc5eQ`6^08T+*Z>aa14nI!7X|9PZaw23bf4Yx1;@%~chWkPFnc zPSf8awrsgnZk%WCs1C7HP18%`UHgwd=POPIQamfB%59(CmHXC+8OHTr#2V%#9(fAW zqaNNGEJk-vYnYuLSHdu{qqL2&td&VnaYih{~S(7djBAlntlv2e!aSO9l+3kwjyTO*VWr)Dl%8k)oI1yxK3P)s=!XkI zT3^;Iz*MMNvSkpuD!8kBO7*wo6T+Esya|1Km9|%)Lb>fNE?M5EBCDGCMfkHO?v~y1 z)z~$(2_tF@5yL`#W)H^LI%dOjfx1irLx5QXie|07=h65fgFq8#u&eg_FD@hO z1!wF1BoD$^WMk6fz=6PL zy^8YML{7|TimAGdn55Yx^)JRAy8tt}x3EKH@*J0?wKU8|2?%WN(ON#XvafZ$75W*s z9Y34hRU4PvwY>mcpN*)SuoZ8Qm_X%yN z`S_snG%q*BX$e<{JNQ~)MS+?3$Ie;hLr@vC-&JclnDax%YL4RvnRm+lYYZh53yz)6 zSao5OCNCKeWkv3THe3r!ktFbw!-jGh8qOGuAEA3fD3aJycUnXIx`K8Nk$$(RznJ5P z%>W(rQsKm*l*zahuIcHVM>M4H$@?pxjvXngnr575Zc0Oy=9_OLZXz)+&N`d}8Phv8 zDLh5j3oisdhPdNR(b&TXRam(Hi)w{EX;N?_)?R zL`Pv%!#zBRW)6H=;g`|tFO^_$O^RS^);l-ND4RH$&iCVc@)~bji0f2vbS{C&YSEWf zZBW{i=tCqNwW7RXWqJB7wC^Ey1Z=)=8(E?L%I$TZ%W0gDv#Mv3V%6F>uzEf6%Tvze zW@D)tBOQx;-M;!oS>h&dv&mrAa8RZ$A`l3}V{Sx->8|jKr3$6kScAJ#7gtL&esNzV z47ZDCr6BGikg%(8O8J7Lu6wj<0GsYiC%SF|s5KemP{WVI4MuV_+ zPo<1f7;dci3T;_)PAB8FNIx|h3@^L;4gXmsvpUyjEC@?fyq8^RoiIW={GnCVtBsSJ ziQQ-3H?GYa3~-S~=yJ>byEoXu%r-48oD}rM)B3TTh6z+=Tis5&Wjad}HJ7ln5^~i@ zH{bFXU+zG{SONuRVB*Wju*}tS_kR9}5-JW%s~DFdDKIlTqRY$oa+H9sQy89(@Acjk zQ6@%N1EqBg8w?cfR+d^Rdhi1`R*o|Eavq(TaRB`BXvR$mRb$SS_Kqx9VqQpQar)U%w z7TySt7`Pl19u2LqxyVK1yy_}mUVKmgh|BU~^xUVSJYF-IT)itYL@J+ZUSuWMGPO{Q zn@iNpQpFWBZ_Yctk2r5sMj!f!az>V4DhX4zlXphd4%7yDSK)>vtDbtn zu54}y6ScVW2M*E=_In-wv%uWp(|24Ess{03HMyEG;wO8SCflJbTkWB!7HeTcaS{C0FW3X-=_m%Jz;jo6p8;x0NQY2^?6o*rz_enOOG^V9qGo=CLdKhsEy!kzxnv6V^PU^xrT$^9 zu0n(n4w;UwK!gEtw+F2v>;-j%$73#zSPU2KuyfIqJ?=^tvu$#<*1hWSrR6BYRLxWe zX=}sUtZ$tv#ThWW8=@uT%ES%{>5QIsY;?Cj7+A9?oLpv{whg;LC>m7_#7SK>?AnTlItEPgs+Whtdm4ri{a9%M8%ZW4IEu!JX4`R zaC~{eJd*&tPc!e@#8Y}>F>~qpgZ-s_Z)oYB=#ulyAGL2ZCT+=zbdYXtNn6;r+#75< zg)rnMA+C8*m^Z(y%RP&syv06CSTp!d#t+OeZ2y1 zfHH@Rl=VP6gR$dQb;TFZeyZd+UkqML2%o5~PkO1e>9){zd~#Z8=EYNO&i7fYMcA0; z<0B15Ku;BgXQA1rFLBZCdOEt zLMo2BMeSo`%@)n%O5X%B$SL84y%Cp}OO8Cd<}2%F|9OG?LqGw;(<5s=9wQZp2Q$@W z+KT&GKMkw_tl90dTlPrHW)fE|lt^L0<hfbX~Bk>sAju%CVu}-JO__zNiafRZJ*C1$GzLDyAk@y}t1E#N*)u)Efy@A(hR> zM6m;3F3S*qKGERN5a7_jz^o^Tm3i*WiuIa1mfh4v`~DPyUS46Z>1uA#a7b5T@aU?i z_LrlxJa<2#L)+6hq`8;M)tEM{+$hDyk6#`cOXYPMp_WNGsj-sIRyG_(F87#~uAAS{ z4E?^4pP5>=@iZ&iN591un%S#8PrXs1_DL8_w7_)ej3L8}X>0rH3>hGWS}6&!g!O!M zo$e#iI?H+Sp)iyd+mzPyMcBn!fS!ERy4hDNQM^c>xR>(dSeNC%Dx$lv-e+;j(mrGC z+)?QhZqnxIyAsdCc>`HP+3MV|dTGC7aGHSnxOK0}_=8A+3Y)cc?&*GHb1>$LL6jEG z)l#)2u~*l&V&!X& z<8x)wLC8GaR-<9r)Ohmri$!t9M^Fyh! z-(17o>lB8pCuN`2AaWOG1r08pw!`hEO$Ht6Ayo^Ld37@z-TQ3?_?csSbNsHnqq)KL z_*y_yR~ub(pJBu!c$!h$WW{q2zVU43*S)-{X5xnj8^M0$Eqj*Sk!83nBT z^~H$iub&N#Q=pBwcpz1TAxnF{Dxp0wy4Sa>ar(pjTypP^50_|aWBYnF3nN1_xRK)< zW1Cj3c~xCG>fTAqZH_;~jZ4kSm(nrB*%T_{xemmna@}vJ>o^WXrGkj*Q-ZlRZ61Aj z=#MjVwCl$>4lkI!^$Gz%U*@2CbKrTznFJ)OxrUfH=_E4 zQ0>ybF6z8`97bKc-bM$q7_3OIROmI?xtJRa_$`{$d5@tp#h8-v+YPwaSRFt$m#3)wepT>QE>D!sAe)S(3lR^lTK*E+Wm>knsMixtGxhcs%W9T-?x8;5 zKG~Ne(HohEo{tZYX?J$)SO26~qZg)vZX-BvvZn;t|x(fq(1b%i-`H0)gBJE7;! zB%Mj3#}jtcok@FJQt6lBYf}K-oF%P>`f(%6?cIGr`X&w_1Li&rt?~EegIh_5Sxfp! zjYJsAL{+K$>j9mT=Yy5gL15LL=lRs;CD+Hy*(^i}OVzlMvbghCSEDLh-u=p}wpXjR z9ZUOYz1G*zHdUC+*2h~x%Lt+c%_Q{Q-PRUdWqmdOv}ny~2jP4q78aH>ec2jXE2VDLK9WRe5GhKtWHfYWj3OBOfq)K~ z;ABeGX=fnCXI?})(mH(DTS=&CWH6j2{49Xl@?sx)e!?*PYU4Lqu9hb07ncp65r$b( zhN_Bo9QZV|Xy5#RA=+^~eII2%5I-7LJ!F77G-G;F=;RDu4seQ|6l!d;OfY=|T19F_ zGufV$I)Pu1w{u|#ko3Qhr%w$NNrVGm?;Bl)BK;|sZ^rHRntssb9gL*l*Q^Y29uYc296MJ$UOE z4-Vxf{ncmS1~&DA?IJncqBKW-}1@1{D>FrvF~<&j&EiEXLano~RBHLn0#r zlTP*c$;=++aR^N=HHaaKM=EC74@E#YZQBf0hR;*IU~H*lXsio#PMnG%_$Oa9$MRDt z2_p%Nwaj8`$nN3K`RC6+8OD3Y+4b~SifJYzXOKaU^cE-6ZpVX|>^qC$d3;QB=8Ngu zs>r8x5@Y=eO=aFK{!t5(sdDe$rBqh8J9Pnb(p6N+FV_~f4`oV2H0_GWh;`{*&1`r! zyCW9jIYZj{rxHOnpY(QWY#vIcb*Ul8O(;p=nYgXKUOaD(c|#sUb>yD7)F-y9U)-Dl z9o73Y4cr+8kD;8m$~1(mTK6O!n_GQ$K}Afr3jw&BB9SQtU1bi3UUS#aQ}IDb$Yo{a znN&|1?|1&g6Bg%Q+~u8^?1Tohu?|m(HW@7(dH9;=t~z{8F69cjJ7vQP3Z&wn(QHae zu`lOq2gfNMLMltrOffcEhUj!oSEtc~LQRqV9jkVvbRhsgK0k*{pdkz__3i9!4gOgC zLt5#YLPOBeGc(}P;{8LbOELb3`X15Was2i6uf_jf|84)<_P6}k@LB&E z|3BknWo7#h_uoX`v0;>`#759j|5@BijA z?SH)f+xuTn{|`ui>;9GcJFfp{l>Y~?Oy3s&nbVw{G{Pq4_6D{z!sdVM4FnDJtn>|N zBn>Q$?2Ym0ztfX(Lqq)gm~c+ljP5b*qlF5*@`S)%6R=4ga|H95p&$OU@w*Y!+MJky zvQ{o#VAHb}l|8u|&Y9_pjCPdf?}yf&kmWNmSlSh zIv?u!Jll-Ns&wx=8+T){Y|`gD<{(Ir1S87Iw0~(yzM64nfm*=+=LrW>_~V$hBM)b2 zTjgnM^V8G#!PSPqN`mTbJa#-Dn=fg56?tHCiM!_y&^Ig6|J^Cz?}Yn5!$rqH%S``Y z*nJ-$O!V}u{}aA(4R8&`g(se;juo`VKU+XJP;cy%p=2j`+kv@&7{$bWwB2O1hz!B+ z=5p&|5w+T>%BF25OvWUiqxJP0qs(4iqbOnXC}ZMO3eI14S5RPqJ|3?gpIvY6TTYiP zYiviEZikyY5xhUTA^8AP*K@)5*bB!^K!!Jdz_}aC+sr(xX4$?~Taz!cGZv7sHP{bD zLHwAFd_iZhB~4;X+?oZz!;X4-dR5C{+o|-IHh05aOex8-Lf(bZ)Bu6o^F4?aw^O=+ zCX~MUf#3DV(=K6BH}Hpb?~SsqS7~W#p|~}>H4ASp#2BoS$~<*4?Z?d8d2XIU9^ zYp8g7!1~3A{!73U@**3^+>%g zvA)CzV9hgoDxH%UX#KGiIQysRjT_#7>!iPx=q zcI=(~PJw(cd`o!TXw@-#DCrScHWy*~3Zdo(Q(``O`iMlBt2nV0A2Ok5(@_g)w!kaY zapn1e7t@Fz{%zL+ixZj>$mT$|~7X5u3%rLl*mzJM{(Zkf`d7L_r zH4m~Mu5GrUH>rH)kb}sw?W56>6$Cs|He4I+Bpd!OzK~<6$zbYj0!LqpW`b59%v_G< zuq5k>GB~d8pEHP@@EzTdGnno09^ESKV4YAO-LUXO@Lofr7~W`LKEZr&L|WeXv_N!d z{LzTM&H()QNOC^_=%9Mx`tY&jpiOUBg1y1|MT7anb^%IE-OBxmfL^#99;{nql%frv%NpV7+L`UAsq6Oa54)c!duwn%g8uAxD zLxy|92NV7w(nZk^;puarg|I;i7si5CyCIz>_=H-Cj$j2D#5?4LxwXSbrGp(LhzzjV zMMneOgCAl@|A7cc+AG-aJ}imPT)Ho99`&nyhZ*DXN1#k|9bH8 zV8do`HWofET`mF0-*6Q=LgwBtyWq*N5y27FKWqRP@tJ#g;JyGL2de(y_oeL^_N#9z z_p9$P_pj;1ECYn;(Cz9P3!4WL@2XV$@5uiSZH7uDqz*z3rqChY75`1_30{^BTmx(= z(vE$@S{}F%Y^DRftJW;=4tVLU+^^h>$mZ0mDY^xG2Dj9+5$r_h3G@9T7s2MwGx()G zkQ;{OA5R3EkeKi+y(Ilo-?=Es*MpwX*I}OsKYo}CyE2&bxk8!?twX>PaD%~y3~l2( zquu;*2D*U{jOx`3j_TP9j_PCXx48Cy2ImA=gKz4CxzSq&kRkXW+8w$^;*5A^T!yol zCh-Kas7ACQJ?CEsp2Fk)**5UiZRw11qq^+Z47v)x_J?Ly>pAJ!>KTY4K# zF{+o_8Q=zZIV%0b`p3!7Yl7XdYlK}1YuR@=m|pyTfouBRvFp%hfOfztf=-Ak{39l3 z?31M(=zf-MseYCn+TB0SkT+D$m^T)z1f8%~w48uf1f7sqAy++a*W9~r*8sbq*E!GB z?qDJ@+z?-U?<~7**SODMH-yhOI+0=fny#^TovyidpR6%%7~j30 zf2Q-j!+RoZ`7>NI%<|B^qkHmA(paP1P^KepdD~gzoZMYw&UW1lt+e_k=1@p`4 zfa!F5lV|Nj#`dF-PjUe-4s?$1l>Hgy%Z=UHg*6YR)@-^b&#UqFFW>C9?@uCJE0t|N zN94W7r1m!>jxVvwEk-aV(3E$N@0ElDO5tnTw-;F(EE>?dT2+6*VWNHCm5Tl1mbTTO zpQbJ;9`FtyFxH*Ck+8nkLR#OVyN6|eu6PJknr?f3a%21J(9H9lusoymeAFp<*!o>h zl(|p72Oy#BMzFJL`@!J1FxH`@jN-=9Q;l9QUyKe+-TGUS(9?vfZK12IrX0_uT!}z*yyR z6V1{%?*7maKtsnn&_MO$5Y5`}X=<+YYMdrTrL*SoleLQ`=V5_&ivCogv-Bcw@^akj z`q2-yF7vz{goTHEP_zp}Z@s^*zW)ubx+PLp|a(bxWzB=|N1~>M}3_C{UiDlR^BFQRYRq!&5A_GhZZ%eoP z8nx+gE=Z_f-JQv+9K&EEuyY=qzOSE0lCqW8<#*RGm$X;Z_h;vn1cL_0K|n&=zB)SF zyEr(>v30U@uu*0w-={C}KLR7#FBwV%Z!D%yBNFHGbSIc7a~Pg}(2ZXK(vP@MA0mmb zG3DP%RV!C@5t6FtC~PbukpowaPDsYvF9BXtZ!yiu7*ALBy-(F}DAD1VI35X46s!rS zFzoHE4v0R|&Y)NpO^G(boSwOJ-Jj|&XU~yU>+rFM%T|L_mggG)(HfE8Hqh}Xoq=Xj) ztrvH&yl;-yBf8+T7SQPbld5b@NmdKxNO4B%1UkkW-4c{My<;iZ|3#}VSuyI7SP>K0JjWc|w z@|u^?%vQ$a=Bvor$bhejL?rbkoaccSc6Ls1)Ri6J0y5}%za7L6P#=FM%7)A%P7|8@ zYVnGR&lk^GLj>+j=i){#&?X?58gVXo%+@(Sm@pIjbAyPBMWMT3KA+X$!^%gA#HlEHDjNCsK)K&{`9mXwaF36tLQR1R#+5R#obN! zbiDD@bh&Zr(_CbE2=vEpi1+lKvsIKdv}Q^IMk~;je*2ML=3%OGUK4 zHw_oIt1DUk#eSJlksF5#he3T~+{db<%Ne)J8MTX9LVE-MrfWyLLwUPHayygQCI*dF zM0}I*a0hZGL_)9BPOj`PB#0FL+-;=nYfIFw>A|@vW2=-eRtcO|Y3UZBVGDzG>Q#J1FO*70WE`3oCU+YA}n{jYw}SCFRPhLYa)7MsKhn51j9Pjw1fr<-;_0r3oM}92wFTJRX{B%cj5fRGY zYg55yCN==rHdKGN8ea8o;c$cga9eO* zLbcOCpDu$Mu6aam13%YuGct2ZZVGPjIsoMJb@H8vDavpZg#B)^3s1u2eTba zJC5j<_9gL)Z=DJUrK?Z5#+e@-r0y6K-5S+=z%{BTEj`ss6bcD7G%BoW=Y<=!F>jHk zcZ_w>g&-PTaEfb`#o>c`F=TBeaz02rN`=`};fJy_kQq)uSU-4`VR&2~n@3X6v4{#p zX2YIb=ZP$2Q?K}MKQ(jziEGAmf;=%Mm$3Kl7Td7qkJ*x2Ko_66&q>$;n_ZXJML7SQ z`lUY3b-69^t!Qtz>H}r0GdL}y>^LYw!VxlhAEa20VcT1vl~;PGmD|Iz{mBd zCBA2*&fYVibl)p*k^JJk*#4b@9y6ku*bsg86oJy zU{#(a1}?#(KYxtl{KG<|;)$}p@9wyOX2ywZ8_YfY)dNC%wh3sORgni5K^Q>{V)cog9;PaV zRpxGFLLXjLf;l&1ZP>CMCR(r90M(S$kK({`|Im)b&aC-ioy3yH$(tjT0sodh9o)+k zD{D%7YHHVNzv7+i9pqE%6Tp*UYZ7-`JH|Q|KDH#*bp%1IY@csrE>?y3N+ME>X*OmS zrQLjXW7cC9kzPhMno?7l$Si3LNh&lhlzJJsiXZd5^LAKJ83EbEk90&#tCwaM#*R3h z!6}|mViSLc^`|OOzF7{8r!ctRh?@pTOXgBsigA~ryqICn$2;67pCN-d+&0{|s#Uxtj>HXc3B~LYGUvNF+np1$RwE^v2?G5eNj&0@!NSjf&MSgEN@ZAEp zcY6TN(U~5vjJ}{RX%PyiqFaa94Lm6N+w_Omlt4r!DLH!{&Ugu%3h3K%Wj^L3aKVbF zC{EpJWXEablEP&NV_)KGXS1g&(0bnM?(nA)B(bdvu%>UKmZcaSS=D-@mWI0Qmd zej6&Abh3A}?>?TqQ_TwrGBPv_9h8_5ftJEeObi88Y)NX8#J(J+j`$T5A-1S5(Q=;O ze}3MdZ!SMD1p6?O!e}DTyFTD2wz487rbe~AMU|M|<4iN;&?UuT!U}aXC{ap$fhVPC zNKQ&_L}3=sLSc|Ci_j7$PvR8o>$|5(H0cbM2t;A9DP9n6&52u1B#ICXN|BzfFVC1< zn^DJ|mvAkf^sz40oaTa%mw_ZFMtyEu7zinwaRJ8Zv5HHD;_Pa#tgLKEpDNW#K<+s} z6{Jk`G6#Ayo{#!LIyKtsMUK(zv(DQ-8=RW&BGw1?wa^$k>9LS8rDS!4bD<$FeD^kz zx|J0peOi0WA#h&gxdJ~=i_Seu5-HKi?c#ZRs~^(F-cQcSsfdjLhm%;!@ z-5`#=i&%*=k1w}#>UcBBy~BKausi29Du zLd4Sx`uz54Ju)LF-8?7p2^ab<|Nagbkl%kSyHm}vV@xWZ;O0$oVEy~ia-9#Ec8RYl z=QGY|n9B#p9eI9^RO0Q73KSAWd% zGd>#Qo^`)H_b?L`9d$6MbSZe0Y+F+tZAFi_nuA0nzqvzZcjJB$jBU z+SJO;qsP#>NeRi>Y&7+`#Ei005TcWKf)N%o7f8Vc;%KbLU*E%>hXjX&MK`8|lYY8K zX%lvbsO*~Vw5tvd9vo9Ys85~_sJj-TzNV(}d`wuxA*-YWdKy*Au)$^gW7uSt#9xr2 zBadL?JL+6}E`g>lL62&QBO-xVFbjXZogfjo0=~a1hzOu7WJr{X0bQ4BI~oZRiwT!U z5RlM|5XdWNRUFg}^zR^fgmS(!>dEfrT%5so^1eSxYz!1v7iZO4NOx5%{7rAe@e>nO z?rdI(CL1>hb~%7@nGi5hE45C|z~-BYsVmn+>1HUa<6LN>ZOW z7EKo$C=NdFL#c;u*(AW$)jK!+h<=?|KF0yazz?fN$fiDl5SdB^7VXthb#R{5VUj8@sFZf9@kG>)}}D?iT0bP^TXO6k{2&~#!JXjbXf zhmKycr_5-Hy0xxa>~55G!q)j^JYEdI>R6beWA_i%KB2FdNgLC!`z~k?9}5as<<{pW zjfZM|wB7HvvF_7Y(qsN|aP_N}BCBKui<5F7p{Vu8Qs`Jwtg6G1a`0^t&LY}BEpO#1 zi`P^jM8YU5tw70{)}fo9Vo|hT!jzj=RJSaJ7anJsS2x~>&xeYDV^}G!Vo{Y$SX|~8 zP!fo!^bE{%HjX73*wnYRnfgsuTvDoKRB3rSK@ZmQ&?>B_nLt}mI~#18uMaatS;?&C zFRqZGG);IM?tqfB{M)P=3^-6YXIinW3~_J)6Y37AW753-)RfYExzVLe8XY6qeo3)Nx(7LJVM!hDxoXrrcREUs`OJghJOI-DdSzIyJf$njXLmp~NMtrc6Wmns(PlbQu7)brjLCAmejfKk zIrH=0t4!d~#Wf8^g|hY@9ehSYXDVn^hZvd)fC@7jR`%nBN^r=`n5SkD$`p<&oBj0_ zJtkFo9)EmLF)3p{EOZc=_47qI zyru?12n-mUNI5V`@rTnX)6OU6Q6`n?!FrKUm1*%Ap<$1!S?PP)x%^mdFRF?e2c#3m z8Za#`ps3gk&QngtI5U-O4eeZ0uP z6?tmUUoB8C(@?V>O#juAH?2|I_iR3x^FvY=w!xXKyvNT7@o+RJqY+;y#G$cjZvgkF{&+!94l`Qazb^ma@FW zA8h4T3q*EB8YJ&y<4F$c>G@0S8dej5-@H!N^|6RzRk! z{_IJFQH?k~rwe*%m!1l;tx(f=$bJgDilOeQy=%U!WvUCD9^T8uDUPb7LP2 zgv771?aM0y9!v#Kpu(xUjR@<~LtX$Ebg?20y3KIRqzV{nADPMnHe`UtQ>uvOjWsJJe=U z;FHfE9UCIKA0%~sBGeA1+to4H#j!E_w1&osnCnssAJaoEj-40G6CKPs-y{{bkab$~ zw21E{d=ARZ72nP^G*d0XDTnFk$&F14`sgFdyIeX>Pq1v)8mbH^hPmAyR|=RBvGiuN zXx=oNP#IZW>FLc>Un}pmGWi|K4AqitG>P^WhAWBAYpS$K*!0B=AIxmZ^#v&wXXr+Z zCjsqVMdRibp02R{Ty-MyDsz^|yfoSrpFkcDOrY!Btu{`|6Y14gjw->kpd;BDAL|Ow zm6sT3mgu&!JGFaGQ&i23b<;0Z8TF+0kk4w|KrR@}4L_B}-ZxI8E7M12xSE(E7qpfL zlR5$dDtumftbe)Liyn`tyX~IiR_GbNK$r`3aP{mgFvhM9Ngbmfi9?@%XNo(|H!5r3 zNn>JXWU6h@_S;jW<$9~*vQX$NMk~h+llgf^)Ta4*wn5#e-EeWRN#VL!&E>1QGOiiP z@?kQ~^@@9du`&i)Oya0nZr*-5#kks~xdeeCclKL63Ns2JQ9S8_MI6mGV)HpNKrxeS~$yAvoo?deCByV^OTZ94=1XvPr|& z5q$ec*bln%Ql6Hhl{Jk_Ucs0Jl5^cHOrSZd4{RDOP@X~)7*Ekdq$cx z)0t?;qH>c+%2kXIu2Ac)EH08i`+)UQ3b%_E_e&pkChi&;-!K^`V*OJ8KgQlVD6*#c z62{%#8Qk67VQ_bMcXxMpcLry0hZ)?xad&5Mw~uF@_ljj7kp&F-y^d zs&+}U(viiMZQ(~bt6d7!V-h9~wL_;?B1#dqFqafV{&?r7p2T7bqbw!eQTH1|Iw07{ zC{b128l&{k#9H`GM>))rnGKr?TjcBiPSXSDS9+S0ySks~qdII;P3tSt%${TOeJK{Q zC49I_>Bg*pZZA8Mx%+VuABi@SvM~uaVak^sS%GVcB3cpC{`Wh_&g16ulBhVeheB8 zfJR7X_oFHr7QHE7qo*k7t)aNCGV7P0sjj&BP+p%$Yazy$YMOn7gq5lycyIk$B1I@rvunf`E|6#FO*C5ijh(qsp~jpo_2Q|fZon0@No z>9kwFUJ2sEz9vES)A89zh~cjBYu))1??W&y@bQ-EUD9zB^w?p?`}z6Y5kby|Ju-I=(ykFp8k|S%`upK0XU2MaGE;tiW_#lQ($ovj{eTpYcXfQ5m`In zEqT={{K9bSf8F}?)#GN>UYMZ$`p|SDJZJ&MKlYN?-u1USH>rklet zy}K+^4H9)`_2`n!GP9lZ<_+F(yOh3uvdS8t{JDW?o_Wc-$uRap(mG453&sj*kvh2{ zx@@FOAgnTVDk^biNoG0rR_>C+c%v8L`Uht~;fK6ei&a;7S0V~#Y(uBCp5tK9ent9- z`@5fIpUjirZ1%zhLpRe!DgYB;zTx08M7|L@3cf|B%l;m_{X_VAvvdRfG5jJzd-WT{ zzFpIR&kNwhH&uFQ)6q}6+kusiTX|)sjeCki1=D+HI|z!?f4{y7^!2jacbYdpf7fc2 zc+%c}Zg_#=w-~ZQ`=7?)&M+SjxtFJg8#zPpWVmdDw#6#xuc?2adu zy)^eJ-s}{$X0QDl%r;*NiVXqJtq8|i#0oNHve_^eScz_*z9+pkP*f}8-93Q`&r`_9 zWQNVJ66fI(ao+CodT;KhHeagLIKy)ZZ!G0gpDKXn9gdUfT~>cz(A>$-a@8|Gx$qUklWjN~PaTf4 zEs<&dd_LPOB!o!A%KwD>crfV|(biYN?5;n2>~hbETYtJkUc(v<#w4iGRKLH>FgA|1 z8ISkDXJM!E8or?d!0K{8g+x1RYkyUJ@S|E-|9q{HSi0m!Z~0i<+Pr-ct1l~WxytNX zoZK_qZIg;WULG4>a_?f$=DqHPl_tE!I=Yze@;o^?f$bO>+OCK)Etrep>po(+Up_)0 z!?5?`HW6}@+S`%MkJo9itBPC0KVNk_S(4=$UE6sc+yy`BO$R&rBiM*fcat%cOV7(7 zEE=iOzgMijf5K5WGaX^z4(;3O)h z=g@^pq!|i}T~4~egpk36yb$NmN}>u>uB+H$M`6 zoR6k*c@A=Jc@F%7-|Q!LrhLoKbMNi5g!>vxr8`#*<$P|Q#zQ;Z--ZYvkF8Kt`@0yj z+Lf46`0SviECS#Gm;>#rtXP5m3%h1BqaN7hoVN2CmN}zLt|^oqU9@7@x0 zJW0()BR!@3K=xco8S01AOx-VvY3+jA*{#j>n26AzM2N>6ruTk^NBkJ;!d+)=#o70c zuv;#IQThXB^KbzO1N%mZRj67t6KLtAm->~vF~<(4LUk&_C^m%h`6-)j=n8LDjZ?aX zf(6&ac)hRVWX$fi=+a8+7}@4#)vP!^&oTXeP>(+6OrUU2Q!nY3JRQ9}p;YkM(4j}G zccXp}(I-t{_&$ui_Tom3P3 zHEsPW5!V_^#^Lz8b|dCQ?4HdW-oG`NtTQ?YAt?jmdHJ&p&(hJM}jjZ615fD@ZN?`kTFd$6Au9E*okGhK}1y2?T zUHFNbObZ5ePDQ@d>&N45_YNCp$0KPTq)760~tyX0(i^W2KiO?8}ZHO z&hm#J=XE~bNBSF97jHE!PS$NTk3(C*9=bA$oB&1W5B1BZ2QC|I-2>rEo!~}F@bx}kP7-`jF9gDk}|57#J#1Uxww!5NO1P(U312&G&h&p)-Prug;R9_SNduN1J59ycEg4y zGi~3%zMSPg;)L&2-yOZ^NG&jMHldkPn~v-{JBSa#tT4h_4SepW|SKCLT_w?}qdsW38o-Q>GR=p)CUYlce$(mu@_ z$?EM_*lyfkv7w%3p#Sa1u70(y9dj@Rr;2|T?!4bmho=|c77WwsJ#=Nz7Wt7X37@Au3b3E2#!uTNE?!3ZgAeV*T&bIdR_H zdGFtF=Aw?Tp#)?T3Rj0NURjDh06NrGW6A4dODBdqmWq*&t~!c@CmMJ_X5x@)e`e#= z#XjJgXb_+UeD*{{0d0mK?>-iLvJb2Osth-QV3>R-h3NV#+y7DZ`b6#UI%kVOC%zAR zKx?PWY9v)Pc=_j38Kwr3a_}`n+qfD0*^qh$K6f5Y0ax?vM|bNoeUrb~?gR3m6okc6a_XwtY#_MINNTf$)s zoHvPY0qy}nWLTFC8~VPB%(162ZdOtK%L`-qiVxC)Xocym{Z!jKyAY0HR4^%wAKdR0;s=30RyHA59OLW__ zb5r~sf6>voe7_z#ta4;NMZOIncHQARP4j{Hoo3RWDqkxqd(y@V9|{A)M{#2M)yedF ze4IKmaXQE}2x{wx{+7VEuN^|57XEzy5ShN6)xq_lL_N&L877+WP1$%3 z7eA6tu1asOy3_bhEL%DtCBX~fbd2@)AX0o-m!5fUVjt7)0!5(NE{RVqFRg{E>bn(( zkh1K70jpJYYDG(?@3Nq=9E3<`Pub?cssr=W{8$a zg}|hSr$`Pn&(wT?&DRnO5&B+MDYkL{_K(?T?eJ_jSdj54b=NUB3yn25=mgM+<+u|1ae(icWVtwh44pbG@r2oU3wo6Pnwb{q-m6w)G zUsPydMPnl^`YkBi((CkW%( zjb&^h&>&auB8Z=Tyt|5CWL9=|)3?m1^l$|WfWDzT^nE}~sHN`oF(`Y_s9_y*&RWK3 zsW1U|gNN4L z!0r7co0&}M_IzziLAzvUl(AO+T0^a+3t8?wwiCZF%Oej9pCEOQ-BaW6G)cL8K9x?@^HZ=-8qj zlO zOBsX=CGb#|_3mq$_k?H@RuCx|FH}W;u!b%>G*y2zU!OWzW4Pw08q>n7wMzGh?pem3e(FxY{d>%86PRDSK=!Wn(qG z-$N?Go9>1n1{Js9d#PVxn8!pDJ>qlZ%jsTH*FZom>g+Sps@KzrLT=SP8~@VXg+v8` zlx@d;2>kjt1m{qx^sKs==BrkNb)})@Gsf^XJeBjDL3@v|%>YZowfOlYF;C%-U(1R>3XeGAV`6r+u#CHQrO>vMWg7;=3MAkZQ4j`K!lp>Efi#$Owf9eK`R;${XzSXgoJLu%^WC-}8~&in z3Q7oC*2-N(gf-VKceqWcu|9!#ELz6}wWFdSf5FQw!zb<@KE@DxFdSZlxxqJN0VkJ!wpX*}Zlrwp zn`gc?UJ1TEXrj>;c`d(#27swGiyC~j_3R^`bqNDaDq0O3JAi$Ds-El6k1@Z}L%Y%{ zFk!8nu7jL^S4}3HU>|fZ=a3P4ys)3x=d}t;w}c0OIa`vjF;*z2i0lPc zVDV)Pg5N&#pXhu~e&Roay~GAzF!dgfGSM5FMl){jOD>>u+5%I8JUgMgVfTdb#J8+H_V4~WaiO+!ZxXQWY zwP~jJVmb9n&Kg}%H|I=h~d`Zwa|9|lkJ39;K|Cqh= zTxbKdh4*29dp9@p%iQj16ZaLoI!Sh-KvKbg1kmEdS4JVNV7#IrB_UX?)D|r^z518d zni>gk`>O11WCa?&>J|^aEk8TYRaCYBpQ15)x9&;ZL-_17HGFyLl zZT*=50t51)B(Ap2aD6f)q|0UoW+mi@Nw_`g$HAen_qSYwL;4kO+OSu50x%O3em)AA zS%fc{@L8_ra_v0b%7@5`69A=cI~-YQ%&JEGEF&m*^8>nFv}}|b|9qcz6@WmxTb5e8 zdm%B$ogPA78%=DI`%+Bn|BsMZ1IXWagn>U|#_rqgaDMS60fPN)(G&MDOH zuy{V5BqQMHiQ)Z*ZqK0GWiYGl?|Url!+<&akiH9ZX1Ew$7xNz8^#+oo53oK zYMZweSdTXhL(fr3*fa(`uZ3Gjq_IWN6dOIqn=w@>dY#DkANPsqWwc*ss`U$q!0lks z9dv@zp?_}GefNIx?qX?A4tojk*#Mo%J?{m(JU@~l>O2jO>l2u-lluaL{IUn3a-fo3 z=0iV?ZS}jpIbw-j`v!w@*T)*anvoyS{H@<&W_yu2rBBDF41f7&Kr_zda|viu!IIvO zngAUXlX1_iGK0wN8Ef!lIW(}|ZxL&~l|a=$<(pv{y`_A?oD<(9nzh{t#i&c5OR6wN zg4g(ag;WbRM7O9SxSJdMGF-H%c%|3tF!yneWOm)Gge<$`sh?AK$+nZhe+MIOg9nvTbMUMvj|i z$n~(Fykf~NC5IdGHiBlvJWJX9&ReMs5!;nIz`xI!xhR`6jyzj3`tUj*ZYHMGfAB=& z0tJbG^@r?4?C!nwJ6u}a7|R*$wLMbX9jt$lmTAR1_jTsPLkRERarmFZRAI;yqz3b{ z@3sR1n$v>8V?h7TdG<#3BelD62PObCBX9T)sr0pR1=NwOdX{sW(vjO@VANpP3GOzv zO@{%a@1X6iy$wR^q}dxsKxg@EwOe_m+1tF=tR79zc0pVh|W)X{Q=kUYrav3 z>?h_oQ5_foKy%;y?&w(RE;Hxxk}kZeR~-6LXQ%^Bia`fXGO%)9M~qSBfD1^eVFpNs zU-Suz3Y1xrQ>v4LmLZL|80Nwb0WbIP0^6dEi)Q)?n30O#ta@w_u9AAe z-?ec5F3g+_M}^S*K|p!ui!$^JP6=#*jEQk!JKBx}$)O(I80tDf3B?I%omgn>duD6` zh>{j}04P`6QNUaHTkDamc@>4`IvWKEjzMsS1se|WcI1`i7MpFD{5{OEP^}L%1Y-eC zZ-f;HY9;nWIYwQ9R_1C?6m@WnmBc#(XU|U`U^$0^j-JR}bgd!lj*n{NeaQ5@leXq6{5Id#!~$IElN`vQJP`K z6srnBT;Txqj4-ZvF>3rFIOU@F(=H$?g>oTQ^IBMCK0wJCi$bvtu&0K+hqwkHRwRH? zM&u{k z#zJgY1iVOY%u>qBK1D|KX7JL&mOhtxPnq~I?DAVfjMGO0_S45q0#N4^{22&|LizxV zdE-p_5VZsSI;g`!`mkcf^d6u2y$t%$%R&OgEeSs0RRwvI4mgYg`fy`K`Jm9TH3wz@ zdfoSDiZ>vP;ye&(g|GT1;L?inp?iw*!KjMZk?FV<~H_Z$w$rwcN*fzhez+gfdC_uR*V?{uf*4tzRc9SDAL9R&31 zdO^4un^5Wnn_%iin{dAcyF*WEuB7MJw$;a?9e8v?UwBI0dukOgL#h;$9pJ4f0Q7BY zo&@YYuI28%_vP;W-1FWUo3Lk!ojVW*T+2{j^?8lH9>Rrq39QGw0IdhTc(H+{i-1HSis zD!xJM0`eF3O+1mlLOLD4!kky%Haw7A9&+K~4SJz|gTH564tn7RaBM5ibL=o3KzqOv zs1ty_8_=9~%DDHjlwbC4DQ<=P7yL1L0p^WxgHvG)-2 zMB_JlVd{!_A@q=3`BDRpk8N!$dw}1wEQh^t*+{&D=!v~E02p@I<~OdmD?#iPUPZ>r zBr@dVne+K$p6`|k_}@_v0Rlq#dJ$i-6yo{t%g2l9_JnMrj3PsxL6u)PU#=O2?^WU$ zAW@{0|1s?{};K`KV z8ufXRI|R5h=l@E6zRR5`yL`)%_YZ%*bI^*;%`Tm!#~=yO4MCbKWZ#n$l2MWed2>SAw6HBOV+rTN zf-HTKA*3j&1SiRt9b6r_BVItybF%mFqa@fbb^Y2=Vs&`{BZW_-Ymi-3r1+d znVG?XuOyufaps|udsQOK6m(l2G(U@9A~g+>{crk)fQz?8 zP$Z>`m9>Z=wp`IcbmnYr#A)&h2IG#gw1^c?t^`P*zuHJL{Mf}QW(`0U7-5Q4L-Ulm zr5iG0nM3XIQ&2R!qb68#Z&lx73Y7)PYVDC@&Rk%`W`9}@TP@`4U$teAZ7HO4^?rgy!EFLjx+Vdt6TPF@MP z<(!$rK{j_4hck{5#{pz8jZ1a^oaF-Ykm(;Kc1sV<3(#YVmFuj{7&2OH_+>%jB0pC$#x% z1YFL?8ykVt)f%3POwz1H5ks=1ku%?fW{q0BD)X9*!AwxxqJ^WHxzW-ZEPKKB;yFKX zMPaGlvG&Ul(&R;(1_YD!OI&BOCWYn!1eW^f63`={_i0XXaYca#yzV>Au2~> z-sC-_d9vy6>eewcMk5&!>n5$n6{Ftv95Gm|QH^RdnlxIoPEQ~TKL=x+3Jy&h66wxE zlRiwRnJ;huE(6cj4VLz06YbhX!=&X>ppD~7ZUd^c+lqPmf*0OBMzl%20cVNx?Bt!< zs8Jdu^Pk69vc@mjV%9I?tn>B#zl?dx9md)$nF##%jn1Hv0Sj+VTLss^ABPY;h!x9T zPrUC<;$@8*HS9U#D=r3XE4B?{LrIgnO?T!vEr<}06mT!cE!~7ct}|;Rn?JcSp~Q9W z!0Mt0lnC8}32eUNB)tE?X?cSl8jn4G{KCO_^x%pI&#HUUta;^*o%@d~K}Tk9Wvgax zS~VxH_Sbwh&c5KOdi58&fiwPfS`kFgI-a`xuZS5mD@kTgi?R6`&Q&&!?udi6Bka^#xz(rg>5(@mjmlH%>mnodl!&A;vWZH-{l z&F5Qu8TazP%-ieNp`pM|D_8jlIk%@hO;+sKQDQiIt0xSYm<1A$x57I41g%m z#+4`MYhe&pvh#2WfP%d>(~!#oEE^WYAKWItItOh@1WUQ?NuTc7F$a*uS{=A~ciW|J zRYA6r9N%}oYut2@*tKe1I*cHM_X-Vnqq!{O8zjMl;yu0&C9aU7Pu+QP#53`5JpbMQ z+~)RW{wL7GSM1zp()$g~z8v1u_m@xp%7uGCH6&=wtTaM6;2>dxo{!T;&np%If^AA~ z=@{=H!!W#Q`}7cdxsPp4?7w_p6!x99)R5!XIZx@ehYx=|%{Cx-82JXA7nt^$7dc*{ z#+(r94eq$-C+ys$j9YO3pGNZo>6{OrjODVhVb;G9Xac?T?>yO6ankZ7%1|E-T&roiAfsYnFy1Oa{63zl%poZG1O7$vj!sXPylXVyRN)z=;DN zep7@21qoB+xx-5<)<3vYr$q?m0SYV)G~B(UA;{6_Ne6t%I1TibpP^0ICHXGfrxi^b zM5|(XaqON2hePV#OT;Ho{+yp&%U`K_cDZiM@L|0R$YV0#wmo3O1`aH! zFo1)@Bq)FfXYBqc>}@kaK!7p8z|DbSyx||bhkP~r7w;o)>cLsdO)w=&`^-Dhg})57 zU`6cD=;Ngk6}j%}vmi>lDKecc*CK>@79p~xGy(ffCCAbDT0`WY$MCh(A^(B=)QzeC z<~lO>zpyc^SP_!bYXH@c{$=Dpe8Lm_+4wehwh!rv&fo_dhB0LdF2Xa3AxlV?=gxP^ zAR*aHYZruo-fr6QIAuX^#1-p31@CX$t>I&lBo+#zU#soAj?X?KTdZ|(fNq3^%FraF zXD4aM%MkaYuS?O>)5_P*j*(+2E!#S%pgqIx5Ng#aI5e=@ClWx&Ft(faMRkn7akM)A zJ*F@e{UTbg(7#=^$(C}Hle3d?`%bd5ztVv^r_s~E&QA?q%pkP{$J}wNF?flh_Fk-R z@nLycP%3NoKOIDc{EPYO|zcr!I^%rLAmnIkp~ ziRpDx9}4XtY!xi*!pnD33Y0_=WY~QTsS~p)Jgy`b;*{iAdI@H&^3QPDe0fR-Zp3Da z=D%>la)-sfjK3`ZRI^5nEYFDtE#c(YaagF_O?kLGCS0LI+i8O$S#h0;nv%9s(c;k&;`p@t;nA8wMopC6A@<<}A-j`?8lIlt zzX3#|$(#4T1W*{_mvbifzQ-CsB@&K|k57-s@3_zjmU)5KzXdf!-BcWW>?%GZHG@ta z-MqVL`}JNSKlew8k0>C~<0_`;hx|Z(F1~$bfnS5X5%NZb3@ZC?$}MXDk;UJUsY{v$ zlmBiP@r6cX<1{qmsKzuK3Iv4uTrEDdY1HG{W8(`i9$nuE+=utvDC^`4vY%nsuY<;d zf^1y4@>WRiyUg|V+4%ka^O$=6Wue)WqhFkR+Z|9j;2ee+j~I`+>p~a)j~8&2cKqOG zb+W;`$=&XCB`V|7Q%Q=zjFI6ipj6Am;xaNZ5*0EMmGv12(bI!Oe`;l zQrT{kZ!4Xd?28l>EAsm%MXP+lRVa(^=45iVAJBicG@w7I{~`HX9W_eGG=!RpdWvCy zW?t?ETvauuN;{{$$k_U8h*50a;P@tI#?0%s!t8`I*P)XTW z+qYXwUsqR87r*l7gJ8Qk`39{PK_$HaudnT@&=2k}GUhp*_{*wqcDeT)?%yOOIRyU~ zVMD&9o(7H0E~G9LJ9Maxe31)l?uO_2!=R3e1%*H1cslCak1FvM*!rscmO+R^DeSTt zDr}ZMs)J9Jma3poX0KgT+y3HcWdXAU{}PQ8NF27%fV!JCjXpe2L{Sg4@j)!S2!k*~ z1t@4N&~OnNJVY7;cpb>!!SHp7F=%=5Sj1D`EltEsWIu|dKv)YXLr^G#C1>DZw?#Qa zHQg^vyQ8#dYW**o6*Qqx3zRf}XLr3XK3H#VcI_DjEU8uUO~k}SS;krZyoLoRmSXJr zU%zj6Tlyeb!ZcMA+l`EY2#!^J@I!PR&qq zvsKBWrOjdPb}<0GXFU{)udK`Fr6Tq`(l93~>8R*4aMc&|*0o$`L7J0|3Ip-RUpsu^ z#C_f{X~mQs1JPXqh{6dJbTqbc(k9iMNkfA*l9eTE!qX0*!MM|!O^eyC9Q9r$C9$2L zZ74IP)1qG)Z(ujo%x)odDuJ+)iXw@v|9(>+rcshTkv$vq9F1#KlejOJmfv?{1Wcn* zhZMVuktn`37xq1=Nem}BSzdc}b9ph~^^6t!%=kV!mTYjwlGSiAq`icrr6}2h0x1o- zE7q6?d`7!YeN?czkm)3zZHAtd?Ku_}gRE@Y!qNsxytsyJP*UkPqnizik*lOOGE1P> zqKuMyeu@T5;@-(xAS_AlVD=irY1+`f_5|1%I5$Im58&W4r?9n1AOGX*#QSFLHoi&fs8{Y_QA~Hnkqz2@KQY$-oQNZjrkM zE#8w=YWg&EquGHLBgxvlsbmb9lYJSy`G;7IWMS;0&7TU#=|VPZZ>zcN)g|awZ;H#fcx?spl zOUL3AZ?*LbmXP}RB8&^2t|Tn7R)AR$Xt(qX%j@=#RzP#LnhA}Dry&P$i`-UYTu+}J zQL~&Jn@SH(<>2p8*v>%}S=;#Lh_ky2S8L_PepPF}#gQg6)B+kvwalQ7sFxMG?qSf^ zgyy+5fAUPjmKibqef*KYx*00cLWuNH_>LTh72m#^<$kT5!=Fs(%^Q7_xF+sgx^9w9 z9^(*=#c$`5Zad;_sDR*%&0$hu>{C{mVS~BtLk>4xsSEpa?l3xbnOwzRY4q_qN>gLa zvK2pOleBP#O4Cfy=k4CZh8CK-{6wCYRk-+>Rj0;>4^0jE9ULB?pF< z>>Z9u*jd_+&Cb}tf(}|BLtQQE5n8o}JKBCOMl8fxc337|eUwo1;(+}$^~a z=4;E*{=T_!r1^_?8YIk)xt_+)#3Ly%CRCQ#_9qHJHDloC6V=Yv-XgNvKp?~VBKk+s z7ZzQ>_!InDO#XRgzGD|S&)uINwgu&n@{8fk@hRd;f3G!^qi8`@cju2oYlPa;U}xx> zmF*5}KTEx{eTaH^dZ>Igr_JL05 zb_yNbU*B{T``GEqvNur)u=&Yypc)UOlqX}q-`jHPuA|Zq429LJ;q*JxLPQr7>#**u|?3 zdAjm*Jv!K>BFEd&n#Mye`#AeJ!+?;by%5n-x%rsJN6dSM*LPB!cbpElp=%cwAC%g|~{#(^}5DA;&BwXYp|Vppp1ioBgYF%0()bg_Pq*2H_r3R0yp;=@YRz~VAKkHnfcG)l}J)kOyjRll-%;51~POfP*?it)8FD& z2TwpQy4Hx&R}Q>d9~;ir(X4HbV+Oq*nj=?iq%LSQ`(^qJStG+Tq5V|)CP-z#%$T2P zl2H~&6WJx}(pdLtvK_{TP)_RWXVRix26sD(iiY-1G%`2mD#AO|-u6b*2s{VD+&1Yx zpgF)edI4GPJt4P{ByxhX_1qbpv+ATv2}-$K75%CxTMW9I@}0?)4%wMHTbY8WFV3Xm$gVPWPZ=n{Ut6rR)MUuOCR3ja zThB_J!?4f4LjBYlO>s+et%YjJ?wOv8MM8o-ZWS97VCt4l?~GU9EOzabSJOk)>yN+K zu#$2V;Dy38Y;q_0#VtfmOxp)_CG7JK>?X*ggP5M!X+V~PaK_|M=qacHd}~1G!KCzt znt~jAL%NP&jsiPmhVVqg3*o%Oe4w-ohWb=Uu-6}(CBZsPeVI7APwIjzWD$_*5P!($ z5f{FUG0eXa;_C~gN7~=kzC)@%3SNvmSBIhXKNH$LXQ}zbopAQ+$ z_{%ONaYM8nx{CPE7+h2>X*MP+>ibYqgT!S;H~be@`;pS zi5cc4jJJSp>M^o;)@g(D3#HqHa2H0=&)ydEfc^-TmPnO?L6i+mC}>p3f#t%}W>)!fpqf3IDv zQ5Ws{ZDnA`sNN&aBiN(tDdZ{cDRZ%;ThzP#A$^sj+5S|J;%w-w^epZ4Lw>2oTW{_= zO9|PW=G@Ahe2Mo}t$~a`kIPP3G3u5jL&)=$_Og^8Vr>F08~qMDvu<0Yj$}0sVNUro zo2S$o93~W-aFPr%ZDV8n8Jab1`{D1MtD2T4F8Y9M>W)j_~7i5^7`O)Z+;Da9lD ziApUx_TV_STO??E+Ox(iyH2mW)4o&uH89gjBL8Vu8>cV(mf3iOFYrPZJh}g*9eFd{{R&`6fw}|smg6J2XsHhUo2<-w6Jvrg-G|DCWUr~p0Q$r9lWFCf z%z_P({Wis=A0ES`Nd;pV9;|+!_P*Cm8Y5&Wlt8HKuH<$aqPF%mV%3HRP__wj1ui#y+(loSV}Gw6bLwYR6q&{v9_kDBGNjN z3z`Ea3>G`MkfH>(`H&+XetKp_`E6r4Nl`HAd;V(@hKm`$zwnl?r)0AG^mE?jbDpz3 zBnu!i7Vka&V*>UVNVJc~g7Ow3E8;Tv*gsr!)_@uF!XR$1b_TdBCAW7Xq*F&mqYK7p zM{~Xp&WLOt5#IozApKhJ&=*gPp)UFwuj|MAstMkE&!t(u`Gey@b?tl2y#96a)5ZhI zK99=m2L=l@8>m2^*Ejlf;kzkFSjl7Tr+0|}CN~n$oC6`=O?VGipw>vF1GXL=k=fWM zeI%x;9;pM)ps++d*6cgH)$A??A%1I{0(y2Vz(L8W}E+9$Ijf z&iL)(1WHfRgsArjW7M`YUY(sFaczA4ueFwK(V~%F{q{pG zwcK`D%d%b#M)=W=lVFWGDZoy_&liP`m$=d$3z?c6{M z%S+&S#6n0zP+W#5I}RhpGo?pF4@afoQ4LlRO&L8>vouuW{?&e_lJyQu;?Oa}m?^p) zs@DT(d|5MXhcS@Es9tc{8@nr06zP$mU$n<79BhAwqPo<5ORKD<=+pvTDOpvqS=Cm= z;WBmE$W@h{Mz?sK`QBx|NWZ+~*0P5^11?yT%dJI%4-ix%@Rq<`tIh^}gV&D!hBdFU z9x?(uA%f=z?71xV?n1NBT#2czD5?Eu*AkV$NR=;oi-b2zhDy8KgU&8?c@heP|iS^spA%EXav*?v_o< zgvb|ydZFLagB7Ak6PSE|dWtlfjO;w(cxP8d^X)+{?%YIt2Cb?TgqH#v-aK|5xNCY-rfiY}O;*#7! zj00Fq4j|X#3nj){e+UTEAc<(#y9D z-6iqlq?wQIR3yOOsVuDzJH&y(=0O-A6Jn33$DC=#XvPX2-kh{g*K*8g@1pk?%Qg*b z^;xeXZ;oEYsTpSN28;|^c((tDSS(5RPxSlT>xoM?{jOP{O3T0t5eej4%08+x8JkR1 zqwCD~$7@&3vdsRIo5Qxk!S+h-k7{Yeo&Z0QR!#8?wg&^I=$0mSfVFvapoziSkP9n} z9F7p0fY*hk7T-fl_Y?ymO!d|2>LF}Nz(19Qp{e4?C9K@8v5i_cHv%GkX#52!1DTf@ zftHd1@>2=&7H=As$gtl26!o)^4|( zQ_qM3pl!RPPgTo-uEMdNOT33^k8w}yTcBbV8Szq9u(%}vFGl_Ki&Mt9wLudXJwCHqLL&^JV#SoN=JyLqN&ciO}ZO-CWq0iH{k2 zN&4vwBX~4RVH7-~*c2jQ~QnQ>Wu*33XmB$*u0;&P-ABovuoq~qk+nJ8_#&dk-h zh8k@>*s&4`aP>qiWZvz?!QW4MadbO@EIxK=)aR zC+@`%zp>!3Fp-WEXTjRZBwW8v5`+FOmNW~Blt?7XTO|AgSLVVVAo#+b7gjOPzhi5;$OoBM9)w!3A%$Z(8D?5$=pq4+$b8YU z4|tY|uPK`fo6>W?h}x&{P=s~9Jc#U6!i`9lk>(dxbmu6rqnT%XK3)iB`?;n|nqcAo zTdaA2b2XtHtS-~Y;VRRla-vJGS*PmNS8)q1U*se(zX)|G1d z+M@p1ITWSJ{|`Mt!oR&>*8SF6O9C&}WShfTZMU&PFl4e&G7_y!b)Kg{s?JW3H7ya! zsU0XR(Q!y~b@D>8O-Ev7hGl5-p@9j&w3L-KD?07OGw|dcb{(OexU_|Y5R(BXVF8!< zJ!1DHNdMck8=k-A-bL4K>iy`)e`75$^jkY{!6&yi zZ5=vPBIkFfHWn9ua`LIvKOUU_#xx_~@)@w)Mu^vJh}iozM=xKkkGN^fdg!5`D4kb6 z(}J=kruv#Jjp5sinv1xQp+|NNnt>YC^{(|{!R17W_HVU3$r3|(eiR8Z7hSc^-+O~L zE)+DHTs!ir`5jJg7k{qb1WXw*Yf20Sop z%gn`RnUU4ittbDz|3NJM`0>{V|8LIG=hfS1E&nn3)zYVC{9)$W$7qN0Cy3TlfF~0g zqjg;%t(Hg@JC3`Bn=UPfI)FEbW?n$b1kfU8lgRNwgTNx$R2_g{J%-;$3hs+b5ye4V zmrBVheO__uZ}4{aZt-{sI2cugKp^l8VQ`^)SR8jfoXiU|dvnh3|J~57@BWOx*}r@D z?^B0Tt5OeuVtBCq9J?Ak06hwoDkb=9YEG znf&5SBi1xL_`7YhpDnBCGvb!lQg63D^Vk=-%Z;~fpA%nLdq)!Wp4V@~Ez72_e2X&h z20(f}*w2r;qQ^C*2VcXJwZ+(NNav*YTfl$>Moom1NDzP@qR(pJWT9t)w=s87@rGP3 zOXy;$Gz>D#G_1uuMg}WD-jE+KO)($0>Z|ej;SWO`tp0>;yJagQtJ`*<8uAOz);~U>P z^zHCTkFS36fs>zaxp~OeSv8G)suoP`j)z;CW{zL|29z6Vm@U;O5))cKXO zj+1XUJTd*Zvv1fuSX=rS*sJ}|5Ch*p#sZ+DU0RKS=Lv5x2%Kn?S>B+?vcMb6oT$p% zWlVyQko|&e7X+EeEbC_oWG8|JGZdzbD*p^AL^OmrK7o&FiXeboXOKaD%Dif&JL*v{ zAa`|@N*@=fY^iF)AY4F;H$$3$ECESE7w0-#RyM23Y3}stX8kCrRn1hgg>jiJ%!C*m zV)6@kp^&Nm;$JThOuq1cxEg<688Vbk=8jyd!MjuANpC#wqx+UW2N5+6Dze@%Lo4#3 z2>O#YFrS8$gB~h#>K$i%XR=vaB-r1O{YOYSf5Cmg{SjrM zoh{o8*hc9HB1&HxP$1S+YF`VDFE_o^9Ri4T+ubDJUai(Ji&a)t>`sSAwVErH`y^bs znQD60S}ZZ1Vpc>=P<~C(?B*uLS=3FyB?1;=KBa$ltUnE}b3nNbb!}?fg-%|IgYdXu z0pvinnpT177=z5>;!!w=EUK+MRLbfpB`EL~J+jz8zB)0r>!Z{UsWa!_!`FO;L#~&y z58jjde)r~2_CIiuuvw|pWn6>1;kz*N#ic)5cJBD*Z0hsBpMRZl%>+Q!en6HP1<`$4 ztt$wV^w9&d2kqVLFWOl%HV27_C0_t%`2xWbToM!payY1}8GHqTA&eu0*ecaf#EcBy zBEyIrHGu`qlEzL=XdcD@9*P|RN<1ye^o*LpNGr%K6+pr@X9^pbay;H}6(|U3emTs&LSymOB7Eq68+;615pRM~|cRhtbT@s$C7g|+1v}Tm|FQ%Bm zf-XHXByrUQoG=JG5Wh6;oIFamGcDwHyM~%uMoa1EzC_54i1TwiH;I~y;?^A*i6e1Cfd1tZ0 zWEbKW_nmg%sPvupWU=&w`7@iFW3-T?hHJ6scX=TYmaM1K?V+MxF%4LnIx3;BD zJi6v9JoPV+{IojzMBvneJ5ygj`QeFI{@Rc9+ns!7=;V{X!3Z1hzTY(*f55T*vFB31 z`Q+ojFUQwVT#QDDH|P=ZsaDI11_Q?!C4f51uqK&EQU@Lc!EfN~@Um!RVX8(V@f=6M zK18BG0etYhCn3fY@n9u2$M`+?#MTgiUCe#P`ZU|WjJgLF{5$WsXNF|cbrb0J-s<8-pd_H z?j%z#)sU8C9kt3rpvu``6$Wk9M3uuLuI30-Y$sasi(>ld4(p>(U@^&J@&K#yNta$^f1*RPE#Gh89iY-7@XR$hzizS5qKPdABtZ6X^jieYPLQ%xdO1 zy%jdU|HQEe*of5~!_jfny%-Wx7-I&q#OW=J58IPt@b@33#--kw1|IH1UQIr8`C;+} zq|RLG<&luROZ6V^Le2e4Cj!PJ98CIEHh~MZEJ1hZFM-Ub$^9@cME@Utr-`qV)vt0U zbz?{v2{)Vij{F<=`qI}rSC5C7yae~JM)^W@P>n+T! zY`tN&STEO`=347*bG?n$JG|?eHI@z5r>q}XKlNtq^nU6kY3m*^BCm&|G$@-WZr|0s zz>A71`C<+Y4xB?_5SBnB8Zd!71y54v>9rE&rL4oq3=U9&{IT{Eh=U&%@Em4X-~e6# zR*+4KBAHc-MX}i(4y)7Uc01N71`#Usn_D=mC78ewZKMO_IN>-y6bn|)V09Sa+@jbm z9I+^((dd^HyCf-8u)7_0_yi8S9pj0pG#MfgQkVb|tQLzT8I2HQq6k=poOF<*;uTWS zH5$-4YI2JXhv;^1;Y3BDky;py>7yf0AGIn)jVUG4PU!`SnJe8PeJU|hiMQHIyl)xd zCYpe;wunJ)Ge;&v70r3PisW#slFR)IzVcAY0mY)H?&y-g(WCmWh9zpe;Uu-1r<1yt zoJc>RZ}F&lmcQyGO<{*L3#D`0=~AKI*T~i&x%|x%BspTaoTc zmHM)}O8;=ZyVzMab}(Y|FI<)m*n?`&D8~fKdN866#?z@Ed`P`WQyJFXHw~A){9{AE z{L-w#1FxjczM9%PrQ94n0+jdR7pdC0dVQy}$DpDR)IWAXF9ULN zNkyjbXzdXr;T&`nzlQr`D;_td@9PcQ_P&_<^!}5x-&@%5_G9rM4xCHvBGd6Y{CVo( z)O+v0wEj%_puMzc`;s>JohX8+bMrLyRViG5sat`)>lFl+F%JWwopfVB{UY7k!d@3&a7=% z&~MY`)V)i0;{L~8oYiysy{UEFk-_`sOgOfrk2#clj{JMX?)abyQMi@k?PK|F;C(mh zioVzE;|lA!>E6{|w%pSVIFRb)5cRv79yJjz_$eA__f8uCaNzT@yjgx{-imxKq7+&R zTz3>+LR>zATP|$!E((2x_$t(kRC%tAvc+AZOxRP%5T}o25a%f@=u#X_;2xT#fGH-G zLZH~j6ch`hugIA}kTZiIXD5Q3*g2wGxTu?qOJpp^O8yIilIfP!1={$t&|b`y7Pjjb ztv}Qd&!a-RfPyNYHf_tHx-6qmBY7EcQH8B9CgvAZIGybKwadq>nRS2jEveM=QwR4x zwcx@16nKf$xyEf5_H9j_f8wFV>u()1Ve-Jf^^3-iTrg&7&B)!Dd-RnTHvhhI2!-Is zsg3N7c<}yDF!x+)|7$-s*XLYcy2MvUF*q48_-DYN3+1A3v^ztz1(8if&=?8RoT^%_ zW~V#XmF0HYL@tnLbLP5S87x`>m5OACcUjF6lc(edB&$nY>&%+$4gp>qD4eUK)MbvC z0kekOT+!v~5HXNt59W!1q9EUmCw%{WDrWBK-?4;p5sdpkBzY!B{jz#%RX@+vFm%zn zYgD1U#-(`&BrR|-aV|>jSZ zJye^NLsix0_Aj_efcpycm&2$FAlzh$6;Vvnm1>!qd_mWhg`}JRQ*(4Qp>B>8MWLNr7CC<)vV%#HK>}2@j9wKf+REwAjih|Z|R${i! zvl459l(qB}i@H-#8{wZVAD8SmY?-iY+<=~gW2YXi8b12ocJ$=G zQ?KCYW8og1 zu{jk8vn#`?aOykGwr9_!Hae5n+t2YLRp*d9OGQ<*#0u1)cY>qdaXVh@;2~)Jk_R(Ex=I??^Y{K_oDI$Ff`n-gf$! z>&T{gC+8JEb-JbNw$#NJcYohl2m-HZUiqTUGv4@!DY*Q^UsLT5&v~_}?ik!dDx>X; z=n>5}01qHH;2WSe>XiM7_PJV=L&hNJuMlT|V$y&x_7l!d2wlK6=(61iV!g?r8BZCG zU>BJ1Cr##pxJe_Ab4R6M#%CU{nsi_Aw!zruZuPXey}E8nci(?4g|CvmbjqPC!j`yG z=Z-&JqD;JoNN-*KE6i=%`TLH|VNTFGneKElEQk5{@kGrXap6H|uxBh9=cz~ao<`K@ zS%=npjA;e-k_!g|YXMahb`q4KbPtOIPO>!mEK7xg$&CySSudF2J~IVVJY6tpbsle4 zmOJz3ctkWSCX*`3vS@|+NX_=kS$@A?l%eRBU8=+3ce(&5E`OHj3aVME$oQQOlT3); z?J_u;G=Rdg{c6yazx@*!^hhA=AJbwI{97Z5`NjZM`t?mZK+I4Uk zKmJD2`G@WT+q*Euf^?0>&S_rD$V=0ip_lh;`CZw~zo}t*fs9=|fA_|^%Yy($?XLhj z3&5KWe2dnL4N8?kw!diuW-5)nq)K^^G)Nwx^f!-|M#~B5C-j5;g5zIKg<@YBj1i>T`vx!k0vcc+d8mz=2L7nT6>AMI`!)qvSRCzCWE0cP$^5nn6A%e@Y!#?` z%Ji$iKn4f`y-RiFgi?;R@~ZmmFfz2i+v4=m5=|fZ%xK7Lc@r`tT>o-Ej8`|VLbfY> z#gM8+sosD1U||2+6?p7FlLKxe70jyC4R`HY_ZWWR@*KJgjiB-EU$W(FA5?&Ppp)8& z3B@?im?wqhye_?PE#^xMm4-2)86h^-rAT5+BKao9jPm^9NR+XejHTX4G}^^zg3{XL z%ni7(YosmU74nUx0Y-9-HG{rR;8U8vB*^ENn}dEdRt?V!lW@CVvsh!uulncu8UHb| z7^G7^9QD<#c(m?f9N8wHXmn)oagW^vinN-H8H$Fdg%YhMVr}W07Y8_px8|U8}da394hhMvGtmhh^ z^Cffd{4vuXUD>aG%J|)rSC1I-T=Tf~*IQ*JyS96EZt2Z&_0hdI*326q>8o z;d5qHRH8Rl4!vp8)6~WTz{Yfi#D>D?vNm0>g#7fthOWz7=-6O+%5vDk7g|d4s`Caq zMmwfE_$49C0H5dB;3ln%FPGs1HbTM}*)dJ{7%{nl058}g$mkB3B{68N_F?2J@m2e- z^L^vvd_Gz`%19)nqfe2*<(fy-tL18R9hyY{&|`G2E^bEO{Hs3W{QqcBxI4N|@rM2r z$F|J#;fiQ{Tzc(+0zTP#rT^HuBHNs%_sk+3mE~m}J9-ob991CL1ljTrc6=ZcsXHrs|Ki1|-1~U?ns*h`T|_{jK5*3i&?sSfr>5QHEsq z9HQ@Uh-Y?_ruV*FS%PiN>@3xnUDL?a7hD|aoje%;GNFS5e%|%mpEqFqh z>#hll$BumrudFI;M@aty)!jI|El zZ8po2WEW+W6%YlpVzdW{WO`c}E5EIpCz{ATg%H!nVrI=d&;t;?+1 zrfF+ePwP9oe$YevKYj02+;`V2R9a?pvx zC`7vpV2}W*6!IIt>=P5Xz`-9Lnq% zVurt%pZezbqSt0ThSB{m|3kU_FLwRZ_<>X|=;(E`7rcV&t*gGi_1!hkfa z0@%*{z9@IgA)NC+|9#7GHD*hV#TKg-yd7QUX1W0y+&q*}A%%4knxS{iS) zHUXYqi*2l+nx!ngBJ=O@)SW>LtTq*vPW5iVO z4aSJ-5Vm+DVI@sY<`hoCkzC5yn@?mu;j)F-wAS9`xqVbwM6$h``R<$4Bl`|_EF7Kr zAGWgA&V4r)5ezCzG}1dU24#UT%sdBjRf;M}`l~_&hiVANE|E$QZL8#G=eFf;FJp2F zipx-rIVY_KRJ&+w_7=*rAlUQtc{aPY<;WOXyu3|@E;%S~mp_o{78wF8Q_rHDS9q_Rs2d+dGmv{fwj?jW-e%%yj&;D#vF!Xzj2CSBYb@5$sd$!?Z2XFH`paMl0oVvyDljMh zfNMjg0VMUvAYvU&Vc`lD7sxVeRWsrvkzo`a2~!57qOC=etwUVhfU3@*telc*Pt^tU z%=aF9{G;dpck9nOvY-CY+xgzE?dO`Wr#ail&3Sp{1Ix#3Sss~h8a2vfon3jpVgJBK zy|D1%w_p73=CiLa8MAGs9o@DvGIhpQ$Zojn=!6H}B^us1%%(>Wn;A;c4HIaX%R!gU zrAIu>(M*KnHH}oa=CX-f%x%(a!gBUi&8sGs*3t$fX(h~aWE(;@n^)BXd7aLy7bR>? zS+Hy+QdW;Kn#YEeu1U}D7ojNfL4A#WoBjhmE$XN1TlDRETCYEgwnmE_BqVLKVSG13 z;#ByaptbW5hV|g;lb%aiHos6c5CiNQr;%t5mOsx8%p4>8Awq^$|c4bk_is6 zSSN^e43oGV<+Y^p4>^hm+I4L&-oEoe_g$yDGwa4J=xOt}VDWls!R)=A6^G*YqPzBG zmyTK7`%a<)BkxE|B-u*bnuEcp@C@}fb)EW#qIu#onG&ohoz8FPuke(M5Aw(Om-zGi zO`Zh|O+yNU5GTkuLj zV%*hJSLuOEm%g*mx!?Vk;P7~i!=yq;3m2XP!p$$E`C75m%|w}IdKtZ!K0-gja6Ao- zl$Uy3_)PdlV3zVbsl)6gicVUXdAz7Im0}Y`L(`=8oYeq9qX(Q`OJM+Ok*ICLdLHo> z;n|4=)!0TM`k88@lLgc$=4X1e9^=MfU|)-9mZ4}3(rUfKV*6n=w$E#LLF46EiQ`z( z90&^ly$;hB*;|HYmDs+(pM__m1p?g~WFWJzMxhx>Pv)b9`-do`L5C;UuzStON5fMGM9D5jHP5YCH_T@&*#WNQ{8y zw}1KjNlf33HSiyawedyB^G4z^vODD%^D`C9PefZJ9CAhG|v}tunSMAiI&!=T7DjoY^jP7m-t&bY$UpU7N9@D$!`>IIFJ`?| z_^Qm-8X7dS4UOhShHZr`53@8`hB8gqFkZtJl8H~(i=|Y)A0fY=KoYve%C*F=1~Z*b zugfjD$pbfZx!gvx*=vy`i9C>?#UkanSi4K7Q+PJ(cR7h!RCpHtJgS$Bytzpdfkm_+ zYymBg>Rp+lvk41Rm*_$+^27y@=x@3O7Yp#lxJ8#FS_sQgxv~1AVrDAmET)?KgIE1f zEw{t3lnR3lS&WicZW@xvdF$m6$1Wf&y8tUPVNPM#^1o!;!>`9)J(bgTEY`X!ZANeF zu3hr}Qx96kRL?jO^R75`NE}@{_1R@vOX=K~IvSSFTiAMMXGuqV7J8#0r?h&(u^-3d z=+Z=2X>`F$Pb7q-9IeEADT6Pf_1Rn*hrB3TQ;x=K8c?Hd5$e)xG(BfJkHzQvnxC2s zlvT#efkLvZ>{V2(T&?U?QiO{5L8l0ll9W0N6HyaFkj>M{dOg-!h$zIY;Y^RNNyMip z7OHSapuUQ>Voe^wz8<*=o`ro;%dow%4T;cHb*AFIj$bQK6Swf8x}c^S*u`r$MLs4W zP9&rXaU}sF>EH!J5WfZ1SVCTX%D24ljvWUa-KXwNn^?Q|qwr!n_uR^b`__%v7T<~< zUl1%CbMdQ$858tEY=6we&@%ux*mw?@u(o&`))v#1LO34I&sv>@SSCG9a#C}oxoHi~ z23LF9d>1>GfosL}(q_jd=PMMG;i5Tga%zok5G@Lpf?SW?4LFfo%~9N%tlYL?w&qZ1 zn=m!3;Ng{7#K`b!nFui%NaR@HZF!^ibl9!&}~4e(L`As?G(qX|4xW&ED5h z1`oi<`zOA9|4ibo6U%<`z@ekT`I{%SHs612#-ksQy!byQmeO^2eJ0=m|BPnUx*F(t z#<^+BjZ9TqnX8tbYMhwH?8 zU>#g6F0rv14>X$)l_?{WDak%X(U7TXU##phXia&*z`Gs5(c(akv&a+8CO!?`WWtKp zZiTawyf-j~2U&T9oq{aky-~vyVW=v)u^%c?sXDHKq$Z`hF-+KQ9r9wRHA;%!i_O>= z78@y4K}v}Nl~i?8nr<2lKjVrWn0e&ls^d>JoL{=}q{-en`OynI=GU%o8;o|?EJFuESLoasI=@qwSKR7f>rI2*y&(ak({H-A9$08g`etNv-t zJM{n1|D@Pllt)*$)@>AiEXdRVY4rl_GLC+-{oSeQtjq^L2-Seodqs)#9) zm2;0XDP86ymDqxRskhdo^d*pjGDhMQDXKOcg-P&u(agtYmQSA)tZ2DdIhW4;X!CmC ziL75H1`-WKHc!KJM`3ssg1?^9`iUlJaT2F1VAJQJw(OQC# z?qs=;l7=u2U+kjx+Md!rt*6IRUE1AR%CF1zWO*xev|Fd8GO?t>XBx`c)|nQN4Pm~+ zW#JfCzBZGTLyK!+A#nrnu*t3FqF_5WDi#rc=$9Hd$Psf;&X-Eofhjw`6j((o`qT=o zZw4CPR7tv{VCvVg__Y|m>>R-CtYgrNq?B?J2eRtq1gZ%JlOfrmh-|hRkR7TLBZ&sG z5-Cw@X=a})-qEmeV|GsBzw*Y8yZFk*pV24j?d#?(DROSTQr>XKJKbk@?10O)(^ie2 zKRW35Z?@;R*WLNbxx>2os~bY0Tu1rb(%UvnKhoUXta#b}j2>i800-C|^-nS`GOjmv z8;=+swmieXoN*!Jj}|OaAO-A3VAd9zbXYo5TH}`{EC+hTwdOvU02Y+qD>d=Dvnahs zzgGJiO2^2W4tNYCZFV6>R*~P&Q~X)9zc;<2mjo(f1z%tLn$$Jm=T64`%c@Pi~fT{M82@GL_}kdhbL__d#ZB z;?8~XQ>(4+EWEz$7_ZjTxmXG%qU-D$JAY7nnma9APy5WqVZE2%sk_^DkUJgd?MoG{smrY9NVdYV1~C zBeQu%7t-2o0^`WD+BJ+QH(80;GuoPJ>`=5%x3}2a?X(@+nZ4~9(&+NlFy6SpLZ@u#xThz6}jc~uri1f-fr8!J$BXTxCK zL@epEQ=+}8$gdLn^-}ip7dIcy+J5Rj^TY{L9$4zJX3jr#^_dI5xp&dnCsA8`&g@|I z*hzObMDBs_VKWDUC$NUmg<&dy$D)(XKFX~dFGO|qx;@-(e!K3+@M+-;B&pwmmG%i0 zfE4M5)x>bTHVq1^zHc?~%L>@EXaX8hgD^DGuyUbzXupJ1JKPTuys7PjQ@#%>D>64`5$#Wh_)%~ zIfk|02wsl^|NY$0?9iC;Ne0TdPq1&W+)ZuQ-mUwQWtVNc<1qg#={o;M;g1$WhGKep zN|oe{vDn5FjRu2BuhVJ`X;xX5Y<7o3CZ-fCD4|`JbpV4!mXyp?xm6Iz$}NUgNpi5Q z4j@?i&_W=gwlj_lS$3G4OnvYi(4vLC7X&Drh39%v6T~ueVK2$aISb#6@P?G` zUEHT8)@wroo8RKiF4YJ& z(%tKZZvN^B)MRAL6b6~j~lN20g6t;O1?E^6%-7+$BFu+bftTZyY zCVmcrQ?Xi2m3h)=dJTN{fd?>bwqhk%#IT{j&!h4zv=A;t9k2s2fFY{vEV!Lvm}C-> za?W%nYdIY1S+c4%9oc$g=`EfDcDSbi>t~-5O2xn6D=ZzJl|()eBtKA5y^Pq>2B1Q9 zAygc?HGHR_Lg)Xt;sdHFvBUtPwi2flaYsRTAAJrz%8=^ooTvb60fTu9s)c>VQDBc_0Xv>TaogA^~gf>!6(}nKa@V@bZGTscv@1ObDMP~R`QANcm#b+mEn12f@M)FP19Di zJsqZJV7pT`rGJv~MFz^iQ|$qnhCcKt&?7Et&iqL5nPBEi8ArK*^XHUCBu89FUTU*>Yev>hvpnql zkDd4Itu8t^XKWhWY)2n2d7-t^p5xDrR8$$4cXZ8}F|K58S!sF~S6U_alqX0sUv;SEs!p?xoBC zw#UczSkK9LWoO{ufjvL9wt=tX-{PNv~Fh)VJ7 zetBlq#Ob3a=gXea3!@_{>#PSHhq~^$r@m>@h$r`Leu{cRtSql93sfwJRgt_%NtvZ# z>6)3NVrkBq6;1m#V)JDYm`gX)(*TF9B^kJY4_h%2Y{yK%mP{jP0gEw9-T=0Ni_z94 zE9z&>y1i-RmWt}tYx4@{FU+1&r_+p!(g3SqAj6$qU6`F+SWPu$lom^(Xv>&3X?Rihg4zpQz4osPMimd%~JY!kI8OECBYf!wS`Ab9msW$@CK zOT;M|3}_>@@A2a?Y`vMl03`FbLxHW zd-vZzfB*gO-{*Yq^HYD1`ekuxY4Jnk@V9VDxFnlgB_bjGdLdj=5=Jx0G43D_&|QOL z<1ZGMhC<3k;qzYd2>KEXHeFurIYAGR4E;On7aS-cKk z9V{*`MegLF1cxvFNPg|3(&Ewpz8Lzl0_9bLnRem=SO%lia-aq2AP~*y;`i`q6%W@@ zyC}4Pf^0wn0iD4oSbf+E9{BwL3=Ui!Ak8XbF(`4!lVvK6m^~q`l%%(k1Xs0g7)JGz z6PLcxPln-dj;y_-%qg4HBU(CQY+@TkBgVoV@$Qz2wT*?!k<8^R*a9cuxd=cHZi{+o z9m8RH$}$2Y2!%i=L9Gtx^b`U*Edy9ei`h>v4+Q{%{U7(oL;VCtVDNINGQ73BzhABN z=ngaTuu11G+IuL*?DWAc=prv@qWD!|CMWqu!#jl126pS^6 zO<@?kIuHuR!ULg@h%drKC=7KQQaZVCsSF&&N=uwM=vXs-QC;QC+1;J+8+&3~aDKz0 z2~(E0-LqsmZkSd%sfvf&yN*|ks9D(EIJ2?_L-2Jx-x>@-2gn9Z(Q-?mk%OEY z?<|D+KzgQ+;}tn_KYyJ6l>d^a+u=5B4}1b?(r%H-@|hT)Yy!dGV|7fKd@R;M=1+D# z92^j@hGImZaikzTYxs=SP8710DpgEt5{j=m4y_sCNBtwVFR2;3V)1?dT+sPedrkQK z#>vf{6I;^Z}~|YkQs(xFn&2CUJjY?&ic$s>M&_O_T_A7Pc%DFrG0DL(u^%6H=(O*#q%#e z1^|_K3&Uq5hL0V1KoOV_%_Z>A8BoCCum-a7gn-j&2;}KNhVP0W`u*-~pV{qW%_)cw zZeE#!1vz3MVX;N!rq++4F}R21SppigROgVa&~SSQxU9K(NtA>f8*ljL|uE9ueQVM zBh>O*a<2v81@+MqZ9u292AGV18EAE8&L_>+t=4VVUD44-2_BPPlD?2`O0-VWNh0z& zM4wwBXLXl5V&Z^;rr`l)StZ4m)J9@bj;P`6!EH`0@bP7n8kbKg$WNpnhc;LEuz$p+ z-!Hu5{;yiv-)SFHjt;f7PKuQT62Dh>`n7xKHvQ#i2mW(ZDK-SUZeFM7(<2ps>C9;P z22ZyKHD)f(L>n@?Gtfr+ZaZp9Tat!0O1mYLFTz|FQZ=FsW!dYh(VN{Gy~7R8qLYdn z2yciYXjQJHa3&>#3VE{vsH}7pqC#p%i!3Fj4ZZgMZ|V>IYFFE1D=McS{I#KF|NP=L zTU+j*U$T}S`QbbFCthC_S$1^ceeu^<-553dz*`+V@16A^KJLe;LfF)0I)Y(d6P2(n zPMA+0L=r^2l5WkMh0Y#qp5m$XV8#Gy`fC&{!H_kDy9q-wDeS1E*yANRc8U@P|EH&C z`o+yTPyc27I;yTbmitm*=LDeifdzB~L#Y5XM*USb$mUpckUs}HYqSLb+|I6G5v$e0 zqO=;T73KhITAE*Uvv&N)d;K*)M}SjFur{nr0M7%1sWKK0hQ%b}#yp-;RAdxYR!Yhf z3ysULr)ci_0q~Do;Z>-2Wa%Rio zaLb+b1zL2hyleBONVtCd1i!O9-{u;5d$r~C8wFFBj;dKOZDhD0Ew?)4x7@r5Oo@fG zl^zK>V7>V`wygM*AVb;8GLmQFO?1&EiWfu=9*R!J)+P}E0>kv_fGPmGXxL0cTGymI zqC-9%hVltLd_)g_q=#$uuuTug>!D8%^&2TThk_JBU^yPdGH21tQ9(PC67Z3~V{~-4V65Hu{ zbS@t4xOp_Oos8y&Mn8D-b9$>XI$aq(2g9>MfoI0e4=_BnJ&=4j0jM%Q*)0-}w*Yd# z7c;^Ta{=px(#_kIC&^xgcto0VpX}F%@2$%HI{bb+(}(Y68@W$*R>Su;<^2oz{y6?b zJpVLu{|7w&1?Fjd@A$vuKG~&;%vc$p>bk@#-xEusLmx9Im|uV@FatavUA%NU+&KxZ zp9I%s!Npl{y$5z>!X=roA_L}|VWkNDA_NmkU`ctov)(4n7$Mv0HMTlilZ~>~Lx!oZ z*WZ@y&&iqKcVRTh$+pRIiBvQq8WmhceyYBFN)aRQB&)oJ2;>@tLgJy2DSR2rl-NL$ zP{Ks;#Rviz`WdyUK`cDPV#v$mB}`t))WmFtRtnn6hiGXOrP311Q3eV#9(?2sOq%3G zO5BNYU)55@D|bH8_KV-iRSOPY+VjSL7x7x>gw9zl2RdiDPZq3y;=_GgPIi=u#tO+P z@y6EN_MHc&%-Pmhmce^!S59s|eY9N)j&o<{POI^~`a*TbqPY{xO>$T)s`pkljdFV@ zubWxW{?zi);O%>x%eU4g{?T&Z^6BpUTJ)jLZntaog%!~;qca1O%blfd56;6oapvZA z=C61sd>{D2*z`5uRZ2^JZttRjY27&oy_RhWmcQluORSZ@V# z3=#knMs6NtPKfic>>wbar#uzIW9|v8f8?z#JAe0B)5c#w*NI)1@102TM%{O>Gt7t; zhnr{b>6n{)<21F0rcd`aKleZfJU;)4bz|y}T;2Z;obk;)6YqI@tG#^etca^Bz3u4I z^5C31>)}#oh2zxgBQ_}AT1n;VuaurA9nsNFx7!Jj1W*J`K;-+s3|S_vPA`%wYJ*WS z80(B1jVRw(X+*LS8VzQX32{YEnG%i2tp^5-gZ=&eL`V#TRQZe7tB45_er5Vi7~L_` zQ6FKMVHsQ~M|okG3lBpvdQ7k0eEjf_&TgAxeeMFRI33XVSMGRl+JYz7kI@@WQBsGtsNfJ%#O|Z@c^G#4TL6ax~m$T8??L^KIBS)&xSc2dj1S6zM7k~1I;z&_} zGdtTZSuBVzvfIrjR1^S4aU`L^{$KWszr-NGR2Nc6E*K`5fRSPl7BCbeZ`f4qP{BhS zfvPC&P9CaPY{(!Q{PV6!l%NLCmgQG-loN8wbJWTXE;TbyI)TI!31{-?ICm zsZ9?qt~3}=U+6WMvd1qPyY4wMl$tcUqqO#pkr}r@)K%MFKX%2Gy!_h_t}NR$;RYTS zT~|4F-iUN{7{Q?4o7dT5rVnFKXLQs?VYh&~S&SP7CrPc-X-%TOQNLJ^^fJk(b7ldA zoKCkyP|ctRZj{0y*(eh5g@Y>Y5JZAWXsBotQ@9twBb%6lQ4CV78mGW?L;TGVZ;HCg zBNjA%I0_{$eh-D1z8j~LNc6@M%Ja|h!;y$0@y46~i)G7;N;i}#Q6JE11&9#CvJ_1_ zc|qcNp#ZZ2o(OfC_plmD@L;A>6Yt0W6%mX%ZnrX7Wotu01t0LlJy_a?IZq^P@xm;$ zFX!68wVe36oX-Y6XZl|J?ui#)r01!8#l~X)?f_hGy4!>T zCNUTc`kYHJ%K42(-rw%rh6&mk^7%=)wm2BrV288q&|X+18O1#&xWokKVB$AOytfEj zW|*0W32|N}3RNr?y4p{ex}ubaf&-+*WsnF`jDNNPF?p4Wp#7T{N!IYBxV^>eBcAL4 zVp6&)X~{;|e}oFZ`iDQjal5ZQ)m+lBzV`ooxNG9WGw%BI$MdKULbr|ZjOjc&_w$!u zuI_lEt?iEUAEHHY&tJZZ{pVK)SHIu$P(|W5?|QQ4J^QEEio5Q6a_-8LTgSciR^oK* z*&R~?Bq>>~BUn$50uu0pebMF(xp2MhZX0T}Ew-UuM(8uby*gN^gMtofwdvZuS}I?F zS|MFP4G>z)X0J(-On|UWQ;XCtAyI;oRG8)#F@jkn!E9o)T?WqKLRv*2Vo5Lvxmb9s zQXVp#dWT*)Y|^-))zo4yOiIHfZ=)&kk>{rDm{*oRv31Rz(YM=jUisviM6o5@FmBS1 z?x^tA%r2>3Fs(Az{76sbswb8$IC*H}?4t9}eD_;;Jg}s(D)z`*OKMtcbHcM$tnI`Y zP>ZQ$r9xS|q7zlh3MVORkgUSDkQL@LUWS$!hMvg43QSc&x`jqG;vmnm41g4%g_QIo zo@s^OtF-@bDC=JmRv1z}?%zU$=r{aywH>gMULQUfDdx;#%O*ASo{1A9J+E`T#8=Y`G{-4>9 ziR!E{!{PASOC-BZ2B8vrBc}aTB~ZjlcZsGn-<_9}qtWX1UX37W2s)5Bb4iga!)B!^~hs(T*vCigfNY`~cfiXg(BQS9+D>yCJb9r=F9D z`^dJsJSFB{S68)a-XvMhn1AY{w8>j~H&5OGnLuX0)mlYU&(Cii-7$BJ`ND5<#>@%V zK28Gh4T0*h{wYgqD)zwlzWe)vXS!-VwTmZMoF-R#^;FN~&2tDR*o1ACWf+gLKsi_w zowBP8t}la&%itattjJuFiMlfOWS~hIFxvvtEYNDsHKVl}*rtKAG;q8IYC?OnU{(zP zVohFNMX1J6DS7ic5L?tyQ0b+p}uXTI};#+^ElIl_c-n``ww$ zgy6rgzkh#y{l9R@J?C!co^$TG=PuvOH|F_w*;{t5nS14(FJHURH!RU6GWtq+s3kS8hIYV0D@6o^_44-|@X4|J=F$ z>tB{_`0mODD@tb;uD_|Zw6ivo=;Mbl(Ql*s;6gM|-f&X^?k(6-z;$Kr%H#$!aB%|O z9KS1`Ym3KOCY)fxamGv|w?~Dys&E|`H;P@HmuD!FK_h94uQg!999bnvC`pbK$;E|o zDEm0*IQB7BNw7u=iWl+4{y9j#*~0EE!Z6c>-yQz&fo%_MZkRQdI<{`YODCygZ}0v= zTmICl5Eoyfiof@s=f9nQgY!N6E(DzS>&f|k{0Xp?OkooWLs4jVd1bgEQUmAVNFh=X z3PhDzREeVMR%#l(U>aeZS^|w>SWv>u&;}t`EP2kN4mW5b1yxiOc*En|qr(D5q9(96 z%Z-RBImO(f{{$t{GXlNOi4Dnw)un`ZNj{0LB76=%5B9-uhRQrSf6VyAKXZ@0l%J=P zRDz&2$IOa8jBCb5giWJfY?(c8PFDPkIob2l#@<3aY6Qu;Kr-^XN)|W~F4~=fohd^p z+>J5VDE}}HZ;#s-$Hm3*GT1pzOOtXek%@AmET_=bQ%qe;B0b4}g$BaYG03;4O^ zJ62e}GkWAtWxY)~DGQpjzdnK^uDZG^F1~bq%{b!P61J>ezH?QMuqmy2U5V%VIi~a_ z`PqdDVHaOcEvZRKEy;)?tH*c7KM`IOcA!kOrMwo-`}SnqmW*qX@$#8CdPe&UZc8lQ z6@jle;UPWV0RMH^EJsCwzsNLZa+#TP5@n<@YB&wbG)HAZNR3YnCpVX=iK3RtA9#Ag z_LB5wddjN-W%|d2%RZvZQpujT3U5S8BEzG80(i| ztaT_3Z7i=@6OHSl@k%51NjoKOw&akw0rmB2u3Nyncs$5sJC9Ye^1=E@T|ykJmO5nC zA*4Jz(pz+zI18fJdh`x^ZOGXzL{Q8IS0$u?I|vLguD~}8{c_jR6_1X6_3&8vJ0I*m zxwXX7y0z(JVbj8^A07Dqmp@x`U)A;^W({KBI1e1{aCqa9DT`e&OsX?<-#%4wr7(s zxdvT(O&B+M7(;#~K-jZu?3i>XN(<6 z=5+hp+bg#m?!5zZPrLHk2bZSi))&oez3rs?-k&-~{NpfA?P92W5hrQ1WFM+AV~tmX z=aaiTYy@k-qMlcW31NbeLY=k;21xl}rCPFZJTGcB#4$e-;+Rhn*UbF#+Q}xvPSW(1 z%1Pwquql!BMX`e6xO?=dd*t(x^<3lnfbHniH>M51y~J)>Bz&37GtU0x0P29!pKQdfrdA0cu8{u%nA*U$*D$b zMo7zPNv?(y=%?vLy_yf`fhj@5icq|w$Y;CO(i2V?)dw7LgyPr6UO(G%{KD}8?w&u7KFp=uH?|elqc^#Xu~~{7d{KHR95 zqRd*dU>;M8pm9mjI5W8zvTo-4Zx0o%GxNi+8Gv&CEW1HDZ{SN)$|(w`@F`>%BBP^Y z3W~^wUWE^iJfj*tfAb#U)RE8s?2}ZOizrONkAKbId~tw3aM2C}6J6LLCw7-{XZ>FH z8){`o%e&@dyjD*M+!C_$nHPrB!*Em>?1eD6=>iU$IS!lf zCh~oIOeP}7`>k(jU>0(`TH>Y4&cYJ5lulBMfA{Bq>=mklwMkS1k>uT%1%hUHtV1~V z$md7#eZsM${HlwGz(x-7tH^m^>-fiFGIuS~_Q zGIk^mBwnA$w70=~QbJpeDT7Joy6E<8?8HUI`>HlWSq zSFH_;4V)p|qSM9c4MF<=;w?X(%2yH*vHw0Sg>GB(i`X`x9RI}q14rJX|Mjbz#y(s< zf{QO6x-Y$EUCD>Sru-FG*H~}qE*)#V|ITw zpIt1wc}Q7eP0IM6fxkxBUIkfo<4CPYTeA8Vw4do@} zns&rg<;lr@1Gp$en#}R)>rHtx)7SO7=LZ`MV%G60SVuIR60Rv zz^Y2nFUkjMig9r5*Si^%aIIWf+Ed|0bI#x0?jOvLrA4csQI1zRE?;vuyr9VsS`5HYiO|5Zym z{oJOq@lJ=+VxjVbQ~V7$p=4G0B4>kT=91n9>+X)?%IzbA+aGbwQw@thxUI(BkdaX7 zUA}f_TSmjqhdW;Qwa6#tH_S_jwUo_HDzu~;BP>gM>Kk|2<`+5ktk7-!j$BlmK6`%l z?8wN{`t_yFTdY|R{S0*bGq9kW=?QvS`D|6W*bM$&q#qsTxD@iMZ-UUouj4ty8_6$6 z2^O*h9OZbr1){qz|0aZ-@?oF8R@ohc8Nfc$Ikxrm@zZ11Ci746XD;UQXYMEK)^#wR zZ$qC+sIxq$b_RXn7<&*k%a9UjGNl*|W~0%dHezFwVXuKxUiyEYVdaXv_!Ue<=y^{AT zE)Lmgz|xsJv7Nuczk-_Ri!EUVEr}dJ3uZ-Nxi#*4Lr^Mht37fvz3+w2! zQOQY3=9H>5z7#3n22wo@9M48Cln-1X;5gVV&4`~-zzMJy5N5|$$8+&r>=TFI={f@8tgW88%|V=kTCBx~Yu4~OG8UAoa=DA4Q7dYxW>y%u+A zH^UBBmtu^WAqgto?0AD7#SuTF<*;sI6T##qPk3?zd&d%G-=N&F7|Cl4anLS;TyDks za}`~rr4^^B=o`>sSEV8kY3Z8^qalxKNcHsS8xJq-#EexRTl}aIHaDCYHzPrf zcl>6d0QSub#_XB6nW&EMnO9;8ugkr7KWw}TtUJ?83l^6exi3c7XRNd?iOAbr!M%s@ z_+4ND&(K}i&hkds%!eVh1QSREc|k9{3fD^b8wqDf^QBtpZt1x6ru3l{Ch;1+K#*XM zD}|8{iNlsGa4jY1;XodykyMI9=7kAGk$XM*h9@dGc`6vt%qHb$4Z#m%`ZhoE1mp8F zpN>_($63c-fBt3s-PcF|fL|TU=32R#V{7oi(GSRP8A5j-(f!5M{ zbf99W9e*@>plhUS?Bq*_%(5o3G#@Vo{r8V0;@@%m#%`NapE7gle7tddoUDFav-q!E znkIu-DSB(T2}PG{FcJigE5}%Pf;+1Gy6-%wojv|3_Yn6P4wL=`T3?T($DpFt|5>Jc z$uDMepTg*UI=*W(1%(|& zoBI@cd>damzLWMy8`9DwX!!{DE|k85d&hT?mI?lrMNoE}`y9%Ci-(|$v@`{H?FG6| za=(Srv-mck8_Z8KNV%U%`7T}yQfh}ej3Q`LS{@IS@1>OA!&foNa)9y=x##&5@deuN zIc_~Ijp6;Jd!W=rOW(tyyge1`ZU)$fO#jh1QxrQ4x2TpXdgc#dmQw9olV z?|{-|TKW!t2ik%}dX7o-Cih!@4lO&2$CWa{Up5TwEVR#e@uyHmrB(5YwDeyo3)i9j zfpBgjfER}C5#ED8=DgfEe?+)Kcte!MXH+XDgoV?A!~{2d7$ z37^h*eWq{bxmkD5x;rs1@w~hyNtNVEwj{riQuhxN?EjC#TY>QPUm8*yQ-^2gOo!<( z9j3!{m=4onI!uS@Fde4DbeIl*-;h=^9sZ`FVmkbP6>ebRbb3bmE$PQIW@T*qFNQZW z!!x&J-jw-87N4cfiqA^U%Fimzs?53~t0il7R%h0REN|A`>Wo*~zjqW$%`KQg(5n zZeha0tc4{DYyZK*@`a9t-i14+!*uwc5)gfF5IzNQ9o~*qs0i^Wb$l4&vhlZ(8JWj> zP%7dfClwV_ykvYY#HCQ03Ux@%N{X+d@tW~ZP$txW8)DPgR}iN{om`reODW`13b{aG z7~-<=Du~xWX#wq}fVL{2t>!`fw;?W}aVd?u zlGp^LrO=k(RWz=pG2x+%YM=~i_CQQHFQdJbK~0iVOYwSI$4c=Q8m|DFWuW0<)JA)0 zqrI%8e6E6cFIq!!gj)EIAm+&`ic99;h>YavlI^_`SBdXN&c4r&2n1F`UrZP4O(6{|LiL{y~aoQ##KxoX~lS;OZd#tAq5f4w6e9B$qCz zzAmV~E@)hI{&D#!l2JZbz+CWLGHOLV$N{hcxseP0d}s*u)0|33c>pJ|4f36|Mh=uz zfPMA>v_f7twD%z|O*sHMp!Q~n?X;#ILM@~^AlHEgA*YE_cR|1Ye(Hhx5YQd~Ix^68 z15GFD1iTaQekk(<`jG?N=YfxxQCc8Xj4YG~8&K$nx-#@@gMJ8&PPBp5T?VNh$R(u% zz@s-H6VZc{%F%aOe!8eGWV8g*9Z*7YZB!GXGAeZ3Oj4QlG61EWR9=$m0@{Pn)$;L_1E}&P8>*fVOwg8VARf_L(vsjTz!HE~{XTF$l>mN-&9a58c{BN^SWzBN2k(r?GpCL}BWe;5)oOBNR z*w~U4%=XTIh zwLu=yP&d@@m*p~=>n5!enQZn>cnfbpV}5S_D=YC|xbn#}Cef(((~xHdthpERlyUM~ zjf3tO`q+v&Vcp-hn)|Kt@+&$Sx#qyk@dm8~%6KbQ;9z}qQ(Jbiako&JJ#3{_d=K#q zHmZMRoc(sJ*i}FC;7Si<9Z*(QSHMziXkte**`NIl#v-6085G%(tkK{~S2Ri-b*&s-|wx6q^B*Gb#?YftUnER(#O zU-y$+6Rj!!G+1swUlaSyiCHtT68Xo_LjAp)_R+2CW^wZ}G>9=eJV{{3~8*h!?j3MK|?0Q=c%D{hxo8nb41a zC7aMuuulnX<)ywt8LJK^_ta~y?J^_k2}tjyR_dbEm06^$yumB_UuyyW^{p0F)6ynX z1F7Y(T3TsN9psUHs})MxAyoyrRgjYoHCkACIvuIybRDXNdTn%#Q)pNrZiMto>f>sV zOw%NF1=Mc@TBL0?T0#4$25K#|rj^oefZTe3)vO+AQ3*M1kS6$2>IIel8ljbPUaDhj zqQYq_J^DFofRq*J*Rs+Ipj}7l5&lX4H56|Q@L9t+t)Mz0lnKpB;HaLa zNNyXzW~kRf`>&ufE8I3x8ET+hk)@h)Kzhz$vMTk6e%e_H86(19J%kCVDyR->DK``P zs|45#oD=#>p|q8*5KYjsib~o-wO7q_Mx?E$sR^l+k*cK95Umm&Q~}-qe@g?pv(i}M z#TrB{RCmj1`9v*6z6ussQq48dlrkEXG}TJSij-Q|SX!wZlY3oGEwP%`s-P0L1T3V6 zTCc*r-#V2(nu0h{`Xgf-%!}WuL3Kj-Y65#K!tGmPLk{g|!J?=i6*CIFDd>&_~ z(U4GX4cN)p__=qx`iA5T zXQu4f*a5suR0j&*#x1RGC;Id_yhLwg04F*i%>%R+Pz_`_p%|!}Hbqqq4-v-=T|qqH>mTsRb_WrY)ah~b^@nN{jLkK~s*?c$f}o!sXNMDb&e7|M z$#uE=`rOn6n5r#uhs_JTx?PHD>skYYkkRAw^_S-6I$Sw}&JE6fhuvw*aeKORNh%j= zuB%sSX2S4Nv+xqe2$`mShl?6qy=@3Mm;2n^V7y%*%@St^&isKvlWS-Q$vL2kc%l_4N8?GB{p32|#X8 zfdg7~du*T?yG8EuI2^=!I(uxMZjhPi7<2_A3e9DAhtKA85vABbm;FP2JNwI}APm{O zUN_7fVjp&Q=fFl75jvygE@vNTCWBB6kydU26CSjA9Is|lj_eNNDU@-TvYtHX^!1SJ zAak=Yb0ggQi~F2lr%K<1mPheP&;y-BL_&+a(QS8j5$K=_=^p@ryghWn0_lzcGI_lu zlUWH!mwWUq!X`HF>zamd$WT|Nd=d||B)#}`5}|6f5K8&k+;VNQgt{&X09zr zP!H+e>jPsVE*B=RGSRtH4V(bguJbjV26Rksjg4b|4lT1ZwbsjIJRU1^bP>RKBK%^DzDAvaf8 zTk9&@>MN{rbDOofsihjauL81-b&WMv=%u=$y0J9}dWAf>x*Za7OKnAcJ?*NZ4Y;>b z{wkZAS6b_q*0##EP4!jPkh!E9IICDvU#)ZnLRHpR)HPV-s)~k+rPZ`m6Hu|zYK*(( zwbe8W`m2Edm92G6jYN#frp8t)BrPDNwKdRmd0k7jMXs>cwGf5WSet-6Q75!$qC}uw zW3@tss9Fvg5vW4aZ7tOkyi`?J)B~*+(m1$sjy~jOyON%m$a!NkksI6S-q1CPV;A+&PV^DQf=jA_))wj|ZRGRQ<6ppEI=Tx@{f|fF6UQcm$H&R% zK@n=;p0;o$LK?#5Z;2z2mi_npGW|CW|Ip&`u?lN_U0xnS<;rKV5z+z3!}Ea}^#Pbrb2i(9L01xM91D?jG1D?TW z0G`Qb0-nX60Q^b*Nx+{H-a=S-TX-As!aKqr0Y5JeB2K(oJc@YnF>x4S@p18&fS(jk z0{&0pD}cYMN<~;TTa|@))g09x!1t;i0{n=2F~aI{btU4}RqCq%U#E5g-m88W@b}d3 z1O9snBP?-}h})J=(p9 z*Y4B31lkgq&NwuaYCw@)k($X1vfK!q?-p+be2)m+iu=Sn0so!|S``n7haf&IJ`C|A zAT^Qq=MWzkp-1rv@d?03#1nu&DLxJOGgKSTf*yzlGMKy^NNNMTL+t<=yVPA&Mjm(; zB!S98a=N8%$ms#OFymW;w%})x&^hGkL-E}n#|C8SadddlqCT6?1?LeWA9;_LKtF@8 zhM7B&gOP|NWC-M7pr%2%)p~`@ejQREpXWzf6wXpS3IpphB2#C-7fwPn_Rx46jdyKu zcwA^7jqjrI0UG~^JmW$S(D(?APm&l5G>+Z4VdDmDp>ZLN%V=ChXN4Ae)c;>GjL>Cb zj((efK=LgC5#$MjG(w3Wc{q^;MW9GzMo}ml#em##C>|xC8E7V&1$AVUgpyGTN=36# z8tf=C;2e>K=77(hn(9<}9B5f{Sy+K~qC3$+G>lH6*U&li1s1Rwr{Y3fg zu!p8s_J*cc?GH__y)iW1FNCHyT?kEk^N_>^rLP70*q>G*v^O8YM3_k5ekiCtRJ`+W zX!@?A(De7LNTL{{fA5S*>5-r`jMx8uji!09EHl^v%{wSCSjb>+D2;=6)3g9K6%BTi zrqntV%U}+JTNr$f){aF*Xc4Tejj)Tl3RXK8tS;A}U1$%i5%-}(=mOa7Ef1rcGlM4K4DuY)u_>ux2Wcd#=dJn#)z@ITG zes&#$Zz}LG%Rjt}!Ltf{Xg-5G8GMCGoeW;didNB66j_6>L%Y%KXg~T9dJr8&Bj_Z0 z9=(FzLhr+hdI5cnIjq4EI36eCES!%^@e;fYTNy>h!I63fzf|DEKVpz+;E{d?zfj<# zjN?b|Q{Yh}gN*Lc=N0%^6@yH&$Ho*m%ry3RGJ{NW$C$>Bv3fsenmW!b>o{w7{38WE zaTSA1j*(UdUs2#MmN9rrfhWotJg&ee^BFu6YK>2x2({>6wbHbXT4v4^R;Se3&fqf& ze6EPWBMd&Pz+amgv@^)0{x#Fu^YIKa?w;R%>3n*gN%X=j2AgReEtIE%Z!IL>n}Z!` zD_R4m(mqzFi@`$*e36afi%c8;yof=@=S$5D-lf2onO(m;%;48k&Dqn8>(gwmo?)}> z%mG?^0on%s`WEom2f$|^2Csbr{Pv6Bx!(cbeI9**E`tBofd`)fKHLIcybSz!J$Ujp z;LH2Kn{NYuehYZ?1Nb05jEC_Fd|0$=0=PRE(K8C)u7;R?Aju8L#! zeTH%V$_fR(s$%dx1->?-z}K0?Z+H~=FH93}GVk>p=9hlM?DVa33Vi!T1-?_uAhYY= zGRrv2baVC(3VfG&n)ev}-{}~9PJ!>UdGmWVkIpeqbdFivxepci0kfnJm>hq&QGx%O z$KYEE{E&H)e`ERo#^%jOjK4o-F~~gDd6xe%q1f9g@-7i@lg@qhyVozeUE^9uZOp924*ltI@2S1kX}Wuf!-&+muM+tJcV z>H10O;FTFx#?dW7$xOuHi4}MwgU3SJV6f{;+F)g-WB;nnt*3QVu#<`h z|DJ{NQ7J3g$>0cspDB<}V{i?Fw@)z_G0$4@td+p>g*731z{1W@o`oY*Q4>!qP?f{r z9tJ-P83i0xKPlZGn$|^xruD}{(}w+_X`?(Ty(css*%O+M(oITl4^79+nv}jJG#%R* znvVM_G@WoDG(EE>RC|eOlhW5sN}mmFFPDa2*lL(*YsMEZi_BP7<4f7>8DrFlwwxAuAMMP0J4P*5fNj=7$ZeYX{1OgL`ELPKvOOPK^{_?!Q~>&$iqM}rZE>YQcyrjkxK*N zCpQTgehLH;(aWQVnDf3l_eltdul{tr-tk7pTzj6m=bCH&t-03PhqH0Z?n&+vy6ax+ zu2UL3V?CdGrz#EJ{oZpv**DHN%U9&9@-_HA^;`Y%{se!9ztCUeZ}1=SUkWGzParWc zG>{sY5SSb{CQukC4OHWAchD2e4sH&f2_=Q<`^WSjAJ~24uj>;yviq+Oo}oYR@gI22 zzwz`3-kSdB`qw1b5)!x_kuV`)dBWC&gZ!yI;b200ze59*0f7N|11=A^tnN-MN!*&) zIdIf~z`&Azht%BzM*&t0JUAdPDL=6!Y4xC(fh7Y=h!@lqgM85Vc7LU}{i`6gG7R{KHNk=!ASq_+VGccX5M6&?~E6P`d_%Ecb>$HKp1mHoZ&7V1(xR@&Xd9-#w! zLaW$Y>?1lwx9Asx;t;H~!^Atp;n*RL6jzIN;@jd*@lWCbu}M549uq$hPl%_)PsIN% zUJ?H$ekOh{36dy9Nzqb_)Ju}3Sgg3cr9M(W$sxJ1^7^HK6vXrh*?vw79et>oQ0VzXzP|B1ZlCq>9O5>y-N#mu5 zr3um_(nRS|X_EAqlr23jyX4VwihQ4Zzx)H7>mHCZ zdO!;9K%m`EB`+m~-3ZcjX=OPI;I7p1fCnU;YnyzuYJvkPpgD@*(-Kd_+Df zx5&rkcKL*SQa&Z0mj5cBkvrt`@&)-L`J#MD?v%r^LaZ1o#ad#mu~D(M*yz}p*j}-6 zY;0^?Z133k*gmm+WBbJ_v1+UqYme1q9kJg>HK8|Dj19MnpM-10E4Y4!>*wKGNes72 zGN4bm6}5)mUE$=nO3-^J=&cj<4hec^1--F?-d90ytEdat$|J)C@?GIFd33l{P60j{ zumtcjU@2f3U={AY3Rn&J4PXsuzXg;-ULStDL!Zv8{Pv0jPy>Mb0QUoa07wTs0LTD5 z3CIOJ1z3mPh+BAnY%hQehy}y}h!zj%1LzCr2T%ZNc)!q}$Gc%wEx$*D-bGOdE6}rt zMYXUf9TuhE#-dtSR11r0VNoqCN{2=1uqgc-Y14;&{B5n*_$?pwjt?_B6pfGNyM*5I zXu$&A+n{?JbpIH-e+=C}hVCEBt8njCz-qv60BgYWTR?fZO?Xh~18>kfLg+mkUX08URDuj4IVK7u83VB&=LX3 zfZ>4e1HQ`E2XXHaz(l~KfJuPI06l$>htE#~@&V5Pegc>d_$iJgcGM5Z2*sYhh$5t;f}*cv+leI5Z!1Uw4cQsA8ee%lGXH^d8% z_~^|o$Rm2+3BB!v-ayr1k?9B*D;G+8{R2z1H2pF ziLu|}Hvw57JDY1utq}jMEs2tbg>BO4uvZ$_c~N?#^Qbf_>=iu!&#P76?_6I-|9@K> z;!wq}p^9G-G)q%D5 z6|B9lfU5&s9pLIfMjZub2eRrYvg)YN3q7iPa8`q}8l2VOs_xD`t2iRuBh$%;eU~$ z!mFiT;kzZd^Qwe3j&<^}Uaj)R@M`%Dz#qbQ!_G|kt?=LEKjQN?;P29D`fGVahfQU$ zsSGxi!KN~J|1i8%0KXQ%uLZENOj0l=a!!Iph(@dA2B3Pw?lMGXx-kFY{Ii$Y;>E-bDLg-qKy7e+D zi0OSL%8MGf@ zH+pcLJ=LSmp2ESGDlb*sSV%brDaRn?7^ED7lw*)`3=)n(!ZD0}3{s9^%wvKiAH@v$ zDBOY7a%Xt0ybo|Pyq3O24Ua@NjTBxFFB7W5%jEaMtK_}mO1UwdFCW5{`nBSE9P|?a z$dEtA_yZuP0o)z(p8$J-zmMxaT=!%C{{?s>@B_FW1T+B-;qzf!Nn$ggg(Wx0ZMYr- z90z;=Xa}4CoW#9TkoDJalY9oB&tivr4%ZIAdCZ6palK$#C0_(wf^{OenuGxArx*Hp zq2Cec*985VpkEX8^Fp^K=++F~nxI<~bi;`la2U`GXaTeW+5pD@$06ebT-yOBfS-hn zGr-RQIshL6E&wh9J`Ptv4^xAzf2syC9GjyNzmh*shYyf<xy-^XVN04z5*Gu68f?paF zZje$jhtdG|0Up5TOk95m7#E%kYnH*91Jc8wKLUIbXph5!C-6BB9(W#nFG7=@;kojA zLKORMAMpK{JM2HsYQzlpP5@5g-YMK8kB54U-VhnR={BP`MMkg3=p!+56Z`OIpuK=7 z@yMJ}4Uv(z^V~WDp!sy1v!p39@^(bB33I3cv9HIx`7_Vn#O`_T#U~$hi_Cv$h)5&K z_hCfSHx1CPfwh>7Yc3+32Y4DhQvlBbrU9M<6rlffT%QO00x$=BMSvHPlV7*4M8ob5 z>2Y}a8DW_8JUmSqdDhHE!9sd<*Rpulvde#u048ySI{t3%dthS&N8#r9S0Dlv90Bq^ z+59YL?sxZiJVxojC>@+#k4NUk+^?alMH&yQo@6gH2{yKKGT8}u0r=d=9XDdv1iR|H z?fMZi%FLvOE?baAO%Z#jYA=h}^E%=}HE0=Z!c4=pOB>Us*X7f~u!v2k*`5*DWxb1C z*1O1{4$hz|jACX_J~XI|ZW|*GsfJPP2rUO}YqfWfYUj8JqmaL4dC7(dE zc3w+vUPo3&){&+j>&VLPb);(zIf?$K(4W?hu65(an$gs~W{ix?%7*To&)}>gts8ho zKFs-0$+cjznPKGl|CTFY5@vQ5W_DIrTyE4HuHO+0fm7Cx2hPi#4AP#5R;hpL zu45Rqu)%AUkyUOP>~7$gX7EZ^0sAZ9MK9*mGI($#vX5$0#XnVRPo!!dfTk5vCcHZy zUVbq};?_ zhm}>7oxX_u435R1 zE(Ue6VCUy(Q2-fGYd%x^&d=cEyT3`>)OqzAeUoPAZz!U|WzeS#`jqjvf#31j`j_-6 zg+8Uwrxf~>LZ4FTQwn`bp-(CFDTO|z(5IB2w6|jH)`%}^F?KD+ZpGNG7`qi?x8BAV zwHUh=W7GdR&?oXO-X>`C-{1+mpx*Y|vrWL?1g!>oe}nHugnl=DQ?&{3Ch(eX_RLz) zzVe$XcnuN@AhF;Z>1Dk&i-g{{&Gn7pJ@OlX8&6k1lK+U$+mI>mB4eVvo|7VXu6^md zgJ}6RVSro#*aWBzzaqbl>o#294Zk8-ZhFFdtE7F9v=5T@LDD{<&rQ!*Hv!%R)Pys@ zSq{$f$TQFK9?v|>yPtW=H$C$-dz(JH^(mnZlq;ZI0p$uPS9)k`K4CXM#og{H>i<Zr+QX<-OPuoO^HV$IgDs{n%CBkM+Lwga8lcv#%PFapVtk zx888mZoMCD$cGL2upytGSVf*Vy~R(P%qLA-_{kDIRidX!$8qljz}K;Qg`zH=k`=0o4C+vq!&^}V6%+}r4y4_&iv zt815@QMWytS-STyn*j9h@}~LvBE3c37CxThg~7*^bS1VKm_o)TQb z6yYUdl<+V3n)(fu*j|-LfdqUVOR11@Zw}pE2+$|gyo)OxGzX~r1XN3#G zT;Ylk7XF7QiGzh-i%Z0%LbbSBJS_Y{JSLtN{!8k#h{ETV6wCL8uw{bfG0|elw>&5I zvJ_i>DfY80vHV7~Tee!>7W-S?v3x2Huw1eHP0X=;X8BB~ zNPN}$sCAN9VSU{CmRM*+M?iJlvdN5jz z9x6Q?eP?u*^plwMm11bTV`6?(ixNZe( z>jAp}djX9RAleZ?Tckh1$q1Z_@LvR623!*)Rp{myR2v`;pa2Yj50JnzNN=-`Ij*1% z1`G#`hK#Wh+Wp@GG6CZOlOizL^plzoxzhlJ-9U7cHVyY@0g4#R&-11C`d#UFU6GU+ zC0@}Kml9ACm7&TAB}GY9(v>XW6L2>hlw4&B+5%;UGFzFaEKo|7rA*0ERw`?hb@*JN zRDn_r9^$V<-*o!7y}QO^#lA5fnK|4wvgEmcxS2NUc>O?h1%~PkU(?Or9&QXiOlcAQVi`C`oDzxS5dZtvW zo7Eb1tGZ3yrS4T5K{JeWscpc0+F0#=EmIq>P0}W7`N|oz)1W{6rWIK6Sb>Lp!K7qdl(7(@tyWq30#- z3M70*tFG6s+a+q5v%7`l^JH1=QxjIH)rI+jL^-6uSUZW=ITN&TR_-63zve)T*F>a%_LD%&odYgVy zKc|h=FY1?}d5V4wd9Lx?$#Mt|n$@BYj={R_815LY);PvG z?uSJsdI{mm8sL?BGVo0JvjF_Hh~O;39pfF7)Fj7bM?TFd$28!D_LX|2R_~bQD1x_v zt9jZ%$9#LbR^(XZSmIcrr#V(D0moX$2FFIn7Dp|jpaHLUykoCp`5PU()f{-6F9#A1V*qe@P4mlhbBUQ;ZvWy8vwqv)E ziX0#f6O3Fn!6KnMY~cv$oLv$OC`+?W1XICPgP5d z3Zu$MH>!;~qruo=>@oI3{vpU|G1|G3WKaduFEZyD8pXI^A8LFGO
    `!ORDj8x?R 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 z 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�Tx`Lyb2zv!;)7j>1jCW*U)y4oomQw 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

    %%=_v@VJ=>zdHo z(Jr*LQc1P05`si6|Cgivd|7q8x;bi7V6@NUV>fKX$G!lg1>CA`h0&t-DYN0PFxnh$ z)vk2;xtQkv(z`!y>`_*USGWuzUnqWj+>nl+ZvVD;Ui^2SzX_why>TX+jv7#d#pPgJ z5h0sSIxR*zfpWQcBJN5g;&DKWSlsV(`Tahh*XQ@fMZqPC0?ROQfp-Z4pG(AJej{q| ziUQ9X9ZVbvY>q~Wiou`>yjfVtFvuLAv ztdohWl@kZFIqU6 z_f|jK#3{Vw-n7Z>78HI{^;3nHomSDX28W2PRd>xdDZEo+=I`;?1tsv&M*&5!d-g0~ z+)iH6w|CrtdgvlO3zTS~)}$;9g%Dy~_i_vAdl~0KmSz0!poNTQAS&RlP3g z^#<`p`XgRM>QtzDs@}JOpbx%_)T^hFIzfs|^>ym^I1hl(qRGETucG(RUhoiGY5X_p zSLjuzIb^2HPvD;;3px)64k1QiL^p*9u1_xn27L7`pQ^+RI6wAwb4;<=$mhQ_{yqfs zpCdc+gZJISSn0Q|isQV;aS-WTWY&ldK8Yh;{@i$H-|P-jeBRqR=>+nIdEPyKis579 zc>pfwX_Hu1!U}8XL|yE3d>p%wJ(s;0U&>y?T*Kasf5C3Vo7k;rD}EL~&%TLw9s%0j(id=w?e|BfVhyT|{&)$d>(jDs2)-;cQQFSZ06IdVsO{0l9XGWs1MM^reE z3Hk+;$4R3-8goR7LbhlWj7Gb+2yEqdTbtrVhthcGknWwcSJkGM+XpjTB2HO1nl8Sp zhGXD{7f^Xll@*U7DQ?B37)^d(z!&s|e6%U4HmE5ztN$0u=H`Qxyu%M&7Zg-Vo#)7koQ-9i7{rrv} zOm2(ZSUh>zPRx;d^v3R!mtXQyF{Z?-?>+VSB`+7FN*qU?BB(M9x=b+Jyx!YK6qf1V1g zNR|cBLpPpGP}j~7w*COj#9LrX8Ql1YiL9|d=CfvuvG5?T5R45{_0VE9NT~_0{{@h zy^AjtP6v>Qjoyl!BhZtb(=r zLvcZt5@FRHku@a7cny0(E)5HDDC*X>VnuEK;f4Bfe`bD1WP<2M+TtBkD4?rAZ_TE< zSFGci$z(?6G3qMfG!DFu(HH%5LJxlb{QDIXYvXv&Igg)l`}^v`i>q%B&5019eeVam zKVACW-i9Uj{8w2vTRdFjhSUDIdeRxoE~>tBC(+&CjK5FAWFb(5fhC-c+9GFwHQtIPloUnHDLfu5=T61c=!ZvH_ZPm61N~Ck+ETD zLI~4E;8QmNLPTmH>Thj}Cqx%*Oti){EE?SqthA{bcF}@{kS>=M@Pb2UxxW*2_G3w! zh&Af8u@*+CHHox%^p-BA37e@2$sk^I`InU?d-6^X$GL9ZeB(1`Y%K@9igIlK+FO2b z%8gl$xAO8Sm))}NcV|)$v<{y6vky;g{PnKzt7Yzwbmc-jr!tuEHXH5BLW z_|cr3L0V4}X#xApqQ7WStB>_Z*_1L#Hs(^trxvBoPyIagyz(dce`H1>qL7FvM5y?} zF~-G(<5I|rLxBj|j>#9p6OGr>qu9U9+9mQOTC8$Frnp=UC4=>?1_0TVm`^Bb-oqxvz{8n2AM zu33l;z}<1iUV%H8(=^SvXcwc>Dw7r`i8IB8;$rbK@oMp^_%?Ac{zvN{h5xaOR%~R< zVUwyjl+a6VQ&0gSn!?!1^lqFPao8x{S#pYnOMKiYU#c!|BE} zo!(Bpgql$t$B~V?=doz95kd*2X&dguD-IHs@+^XaV`V)U1t=u49x>26l(_8tcQ@Sp)yiu(zk0{j=d3#`>Qn@e z#Z`Ue!^rQq-uBRSSN`&(4zSAC#;b-m!7AP8ibq%jaX}`0x|XI)CIj^(YqQ(Wb|aVD zjojcOZ62%Jh7260&bC^3CugBKn{@~*5mWa)>R}b>lfy{i@GRY(mpGEW8tdjgx-}7F zf=Ek`!Q>-65Rl-u$V3(-;>HuOWLPnF55W|Jq52>rIRx26FHv=~Shw<@&v|*7vk1t5 zJ_IKGq0WR8s0pvqju!5XzJNYOpW0|I9dKuIr)18ej8+FN`y4KLwfs|j3v-KgZE{`a zw%jIscXE*0VHvUwWnQwpkP$D#8zWScD+m61i$4~ujQ@E{V=O;3{%4@j`yb_*hK4xl zOE*M^#y>%c@sGD8qY>iVg-nCS#EPkuDNq!Rg`&wGtKdIqTq-4TYSFOETP#Vv5+zme zh-PgG7r9;8BC9lijZBkdS6R=^*+OO9G2Fkhn# zEu#!IOlnxx`&_n?TZYTsq|Z(I+@$Xa!aM(<7Xl3In1Dh(|I*P~%YYHx77wc%#Bq-|dfVbN*RHLamu}c_{)PA52e>E#E_y&O5~v0DX*n}Pr_C9Z zzdt?{H>m_9V_7I1G8A@4u({P1g`!z%$tJUI!Y7;yn?4Y}iv2A;nm6u1xQU=7xvN5i zE`j_%M$Ir!Ik?N@8f<#H>9r>MG{zoBs>7DFHn3@kX`p6Tp~Fr)NbpRv6adgH zk)mW*hXAwe)IFLdUUYifyt~ZB>~`ut)OrZIxY4m;J^4BM0jg=kaA~BT)aW_{T*+iU z8l&BIhs{BmcyP)h=i+FSF_C6LM;ff4BS|&tjuWI3ah@h26qABZ2NW?b0{w$1XDS@T zuEW3!l`{me`td;wgSu{{b*XxEx$2znFrSCoT9fJ_@piNuc1#^O?UcJu-?9F}Ct7>D z)U~Jn*VPNUyfSa|BwOCV&91iF&pG$5yS_7dSxbca?Xru${j0^djQx1k<`1@9+;?-K z6y;>zV-<1Bhv~OpT>I$t*9>S{2IQ<~01Rg$J47-~8tdF5S(z=!By7VHU;>S$L910! z{D)Ek-LtqFAJ(&yMzI53d5^6*m^+}*S$H*lj=`pnqGO9Wjw{rD z&F79gt%^55_X-$(oI*h;2?jy&*B464<^FDNUpFeu8r!F5NbAu|jgB4BqMHur@r>c8 zq&%!nJw|wdo|<`$Bwh5x3r0UhQ|WW*SrCD()S^2~zqfs1Ggw$Qj(8Vh8N@TBj-hTr z*s_qX;QI#MDL()L}`@ATeVC(53a;FCEJlZMmWxUe0F*cf>mjecTu)4w?FO7 zpmaK$#}t*ba20BUCfV26*V(Ce?bx2;?RJh2Sa>0pBBvax+LBh)R5}n#XIX>ZWiqws z4luw{IYrdFBRVeQeo(Iv|6wQ@ZeVUUkAs#z@A>Vtx zy*?TUPj+h~rSdsd7{XH!KmODJZz(~b@|4EGwN4%*zL)=uALnThUv{7{Z3zgb7F73x z0h6GwRvpu$YmzG^exfO(h%<1~I zdqTVLs_IJJQ8WIIw*8^>1DW4ip0U1V zF|J9k&fFfpHL))9V8ZnM`0~WE%=dC@ENfiX#@8g6x!hUYa?1tW1^flV1)_OIcvj@N z_{_{ohq2i?IXo#cDN#yK&P;P2!!c|@3Hu|ygfHzY#GL62a~bzU{MmwGdic1+#o?9V z)r~iWH--npOqTIWN(P~TL@~w;#sQ`=>@dU{9L?cmAf-x4l?eudP0h^`LrF|5;k1Qp z1zXA1Yg=SnYBSp^_)0C6OCX-(DJOpe{}jKIe~;hGn|N<)vLT4|<&nJr0!qt_%W677 z?6kkWE?e%OMb;OAp@F>g5QwYAp7rF*2FMTYaB5L+@#vpkQXRmXW?wpG{vFp=U!7AE_u%EXEGpb~+MH|8BqFEU zWB$b*y;mKOj$PcPT{;Eu;|`Gh#UT3tn3_$C{Md4h^=jc7@fz24?lqw`;nk7rldDr} z(l%=YPKHzd2-zab{vtUTp?VlkfY`g$n?hc100le@McQq~l%A;xFnrz_3QAHS=wUKJ zmZgFW6<3{3>~w~ml+&Bf27@?^ddXr{ZX52x%%OPXkT8fV0wL%HVaoq0{ROgU4W}(` zhtuw~Ijv6Gq$ZMyhD0hsn?%7SP^L&CZHeb`*d5E`gfo-JQ6ZGC2dX4lT2J9XL~^)% zNV1VEQJN1~qAnHU!;`vX0>|hk`1In3^1Z3Rg;#y+zpLG(=T_Xb`0=t5KPrCR?CL9Z zq0H-Cw0PDz7p?eW!BIpitABOc&rc}M@5>$sN;wbYQ2=?gVL|gQ3N1A)HyL=VBO?d_ zYt$cVi^T$d18V}D8*m0oaF0RuM6n=zChR3nyM4w|xh}bpv2Fm+QG%pgxqk#qt8PtWWG!V( z{{ib6f$*mMdiTLf-Jh%3Zb{O zT!GFTW45q8>j8kP`IOq}QphcOeeq^6@8oll{i=r#Pd|M!7?Xdm7euq042kPNDw$M{e2-xqv5_&RzWzaID) z?+MuF1u?eq!clV(AkiSH*z`FP*AZL^0j{J(r&ACEworofb12#urJ^Y{8cnEyP@zqq z;O3V0=H|BcK*4I%+e{11Fh)9H^|@;wCS#`@k|{aml4Z9m;LA7YYzJlfGE^p&%w!sp zfqZ5BTFoE8DC`def|zn)QU*H_0{`TKE{JCWnl+eEL!n^MA5bxA&+z+wogI|Ht@@~Z zA=$1L3RbI)7F8RgCObO=!C;`HJ($$cD>#%~lw6v8D7hnPOlrwgYf=;1oXIuGoyqr- zd!esF?b6(V5MG3-HTV_sg^IM_Pg4{PxO|x=iD3inqJzESE8@H2XCf^sUB9jy=`27~ zeQ8+cy9%|Raz8Z75WL9TZZ9E2dM^jMq?@g7wcRCR+InN8wlK<#5S|1CNoTW0AWRwg zFkLDip;_hr|Dy_i^EQ%(#~1dOQ9q9182>d|GywuF_+Y^D%}b~|POom~Zq*I>1yUZ< zM#`7*6x{VPVN9*l^%dtAfu)9S74RbJ-LI?hSeANSN0P((L94-m&?`Wzvv{cnqy)2m z#ZNs;VJkNId|2|)R$kXKM@n!wcmVQ5rlw5|xFMBoXvky(2@9=XVs0^*&9ot4@w)Uj z;JL@+0!WVsNjn;82}B~nKp^g;uz-WL-38c(#EhCq1l4#P0OazmK9}nAQozP9*DTm- zv0x_P4`QG?nh&9@mS}Tkd$WtOOS5aT?`BO|Z=Ny)1s}Ow6c&j~#Wmtyk#>q$RC1Hf zJxG!I3AS@JznpjTcvbN~NjH{u9UA!L(&Zp-<=Idno{|!pz zZ$MPJAC>zfG5qg!;Ru2SDnV0A#%{W`CSy`g*FC{I)DqAMC4}2`ouEAjieF>Q9~z!H zgkco=?)ba(cj?U#%PRPQHvYWx8HEydOS|Q-xUYm!X;d*iEB%4{gYc&Gmi!U-kzn?6 zUcoI%GX1RZ59dD7a65aGZ3DH*xQX3hd%^Sq!(2&SXS|-d!UnX<{Zr~zBhz8(U|QI2 z+a#_mUhTP3bWbyAh%JtOT62?L3&8O zUtvxWXG>*y9e0zsR=Q38g~Ch|r@5y|Gvwoy1f{^DhYDDXi5nbX+n3Vx<&*k;41Z1gz zqz!bUtZxtL*I$I~l6@j&(1}6G;^#|3WqjWN9J$K){sBQQNo0A|;qsLvSqhY7Qm_E; z13+xx)W_s#tc<^Tux%Ac``6^i){i1N+WC^3+*dn}Y7QG$a)<506fp<|CJ8)IDcS2s ziX1t2$yPtgB(G)X#VObUg)K^I|9X|0=rJNYk$_-8oPj_E1#>G!mZn|M5a{n-{n<4y zR$s(zFJANMHFH1R{>VSEdE@p^sp(8ty!F-hH@$?XSD%09)SS_ z?{9iVeIxW*_+9ml+-E8e)SzivXnN$*>{X%F*`I{&3O$g0F8q9Ccg7yvHohOR$f>iC zOr3>Hu#T1^SjS{{rp`LDp>R4AF-3FP7?9<5RRIzrQ#lkY{AHJv7` zBs4@_j&js`&5qzff?P|&k5sHyaCvaKe+>}K3U1KIAOH15*GG3o_eSX`ag9!1<8Xo7 z%Tb)tIb+F@v01r)xIA3e_cV3uyDD_#(PMTH35nVX{2U&s{a?9GCz|=NaMH|&Tv9(Dd(9|uh;U>_!iOg{4e25M}`VcexJM5_;4#aex33o)P$wW(Y zJ*2~PYbdn-5Ff<{Zhz~l+ZJ4Ly+&FW-1flI>X#p!KQMdKWz`ocOLfMPko4>i7p!ZW za@!aBnxf~)wmE%EI_LZxJkjHTO)kKuqtW|Xy4Z5OueYV#a*4E3TIIdQcU{*njt0H1}S}WXlDpM{84iWcny+@O(zBpj%#sqoXZ`sxus~Dbb*Oopi)TX z)YH|SDy^z1i1^Z(Ks1A_diqXYYQ|(o*-|rbj1*-y8>oPl!V!g>JT4Gukplrm4n$;+ z8>nOurVyr4&H$bD$S#jZ_DE?ps;Ci{YBQ+TXe45@St;^;><~PwjdGv-u)J5MWfCA- zJ(OB-FLLj28{E(y824b$5WW&g)Xssdo{1ydmq&M(N6Y$}PFdgPIYBAOzd~VxSpJIy z+}9IqIwqB_> zKecnVZIM*Y&+a_KcD{6e{*2B$@;7we8?6YHNTs#X^PKQp3^>G?bQ z9DmM5W%$@;EcEa~PmGJkxJYYDGmf;j_6ULyjI_EUk=EuICvZXB?83O20u1If|Q1Sq)dHOEo`WxP|dDlc_ZISu1W;dwNQpouybTpHKD>*cM7t zjhu?H(PlFS0yaqsVA9PyjRj-L*lS#5G#b6rn$;Ne4ko`7fZs&UTY3YF0!ssifO7OU z{J-pd3z!te)o4}syq`TiJw4Mivok$Cv%9m;+4qB4cN>>~X6bH97P z?|yeFw(3-Mb#>LLQ|FwjI#p9dmwJ2}6ObmI4FD04^>M-pq)Dd&e0EYIk^UnOX_^F) zTw;gLE+`8*o$nFCA?jy@Q>B_VPW4xS2Z|`C@R2so^7F;AI+z_er>Ga1n0!N6CwpQ``S%)$8)NdEB7;dW=5efrdc_vFe4S zSO6Zj6ORkH1+9DtYao0cLXQjNJN4k?1ZF$FgHGBrUlRYO z%%hpdZ=&(Dksv43;vZ%BA%|qqw%OK3G^vRlbM-)eYv&cpJhgAuuC*CEdDI3 zAZI%8!vp&)%te_#cAMcdZ*{0f9o5XiL zp1+9E)^g5j>ww4|FuZ-8HsPeU9$~oq?+3`ojenp9APD1@OdqqA)}d9Eht`3!^Hy5| zw~^&w{p;UkzdrQa#(}jay%U(WBl)WzPa8*)m88GZ*&%3icY-v&oZWN^}G1O3k*;yv36?=m2>4i|9FVIIo~M!y<`$rIb&SM4u9lm|1L> ziyR&HUc6v$QOqjGfU7Q0OW80&tl4TWh`9mtj44y$ z-9b9x6xFJzPSqlDr+|r`1}1t++ZJ!WkO}F;XB>KGsMD8Pah1I)F)n#|vNQR(@@V3D zywCn(>|7Ew>SrUOW-Hl&SIx#A=pzL4C!)5+hbYuI&k_Qc-uIhc zY|8v~?+066$y?F=m-p|!`+f3f_^j{kLz%z4d@yt7(4*9xwUwGy9{%y~e)nVeBQ~wj zI}>4KmZCFzV>Xz+z;y8|wH$y~eIFFQS9~&hGWN&9KNOo2zG88-JgIO}@eH}6a8B`3 z_fly|;VqIwB$LH8USYQPa^JGTg~eY5ji#W)`GO@}311leA@=}xPheZ{QQxDGH(tbh z+>#K&h_$$+uwT=s(PmGq#BASfFoplsAB)>vY0K;#QG93g=TRmaEEi%$q?30PVRuoq z=*}XxNGdy+*UvE8CY{#yNX}~ZtKd%Qwhx+_WV2>(4xLIYtUx%CNlR91Hsx#0nhk<( zg94M<`Z}1$vab+!hW%cTpD{hXb^punJTd>sG#}^jcRhUgNaky7Kk^IK878CTXVIWP zG;Y%$?tS>!_^E>5Q#NWDW?wym9b_!r0N8$#+*|OlFrTn1Chh2?@~%%yvG{ zv3d;uW8xsf%uyyXQ@S-9D!%zQ@sr1|d~>AN#Ra&kzufq4<^*;heiPefO2@ZsITpkZ zJ^X5Gty}VVxSE+b^xE^lIsV6ux9)uEHlmaN26XZ{K$&&uh+3#RrW!XHzvsBIdWU0| zV^7)3Wyi{Fev8}cILvX1wXPCX<&tN9V2DOmIj6Q09 zIb^@NW5~v{tF$Zx9ag-g_6XT#s*|jMmvVE~^JJ`v^U5T>7AB5qaLv`j?N{sOen{`flb1 z!wo>0#i$lX)ao%pkHCBod%N)S*r~#=V`melWhE;r7Ni!`Zg5^-va0q+C7Ws=D!Hrn z$&wwlFGO67g>ZxUltEaHMvGNpP^7FnAai~h`qCBIQXP|RWihm+$PAMe6E@*wfsAFF zjkE5sK4WFw)+yGjtWR5yT8-9VePwJ@{Lc7}_%m_Cf%wt*$@rPLAuiRG&KpFmlv@x3 zDCi8L0JfbXEVV6b1JFE#N#s%SeiZ6Ii-P@Uca>T|i~rlMhz0dRtlUyXf)Yn9Nvsf4 z0}DyH^RUoi^|@^e!R%t<1Geam4fTYJF!gnOZOtHiz#BE$mk9LCokaH~|2$!Rv3T?G zpML%IPmkYx^{d-%JHi~kpK^?S(?^wGnFPf+fG@eYbmTYtFy6Ztqs+vuufMTn z>l<&tSeOB0VFiqZCcIj$*dF{^W(?SeJ5B3McjB$g4*Ur74BpMy9yR^MyvMlLe8~K+ z`9#nhw0QiKZ@Y!4zzA~#g5VD*o{|(*c*^Him6xZgloHOS@p~tB&b2z7HmkyE`m4Qg zuCBi})sT3+K2=>`UsJ6#VPbAzC`l%vyPJ@~%-JkfSvnEGASWJH?ITc3u0Bw8w2G=ZAcOe_<`o3iLI=NW_=&>e;b(YbnIK;5D%?Elc z`wG*Gw>}p(IUP3gYk^n6`wUD&Tn2+t_DvD6AQfD?abS+e_0e8xY5VH*kf~nYl&=H( z;`QEh_M>$KFn3rvhDu@cjMIG6Cp3fI8H= z`Mx#2@A-gPIA#$Z4_ti~;plw8x6R{G0tgs6!m@|srf>&1mXoCX+S5Z>=dbm7Ykb$> z`hH33n-%qWod9+95$itSp<|g+wZ+}&ZgMrdN4Q(u7r9k;yL*h4FLE?E_Jnqo8;WrQ zW@d!vo9Bnun%9Pn4d$Bg81tC$470Jy(s&UaeJ4iX5o23NjJT*(Y4o{CQbgwQRQ@gg zB!7lCAfDq@p5@27c;4+&e1%auu#v(kj50Q&DA9;g*ifY<)^fE>?buXpZBHLV9aV|&5aEWg@rzw(wHCkUK zZW0;so1%h7R5KOqDBnfu>|Z@mx9=IyMkz?wz6nbaWIO09q( zBsJLup>}|>!|S6nV5)TvFqF7QJJvKfp2t~+&bfZSZ28Ffv8FX6u4ouXt(zX0TwA$t zDx=FnYmY;KBlYhZ3^GjKC_^tQ;Q5J z4?yM1fy!qBl{ev=)yCtd;}+(i>7a#q*s{yC%fhZQZ!$9r%$=6b5c@#rQ4@1rbT{6^ zu;J*^D1)$pVInYIH5ufK`WW9>$>$453O`f^dF+}Da$(n4o6BXlDIQG*aj1|hWQGVL zXZ=`B5Y?ocO!x(S63H+F@fO4kW{^odO#ZDlIe0?C5)nrn6-Reg?ErBk5m_|A;&e%* z5oh07&?x>Pqlf=3IV6O_MvK{EvM{Ew5eP11DbQq5DV0T`+-{cuuYUM!NYKb@6*W)n z=m3t}peu^!P{&{@I!7GMoOS!`tEV(wLFwTmy3PJyR$R7WRlYRRX=UT=(It^vFYfzd zKpM@yuKfpne;z_0AdT*VSxyU3h#mRyc(sWa4Wi%|+1K!E_T$WNjlVM=x0{xkmw1>i zrpvIzvc$I3x!lv`UFf&?Vyru6W$jk8BZjEeoYY1`m!F2J(^vlt`O(=bbTtU8Ugl;s zz{gA~v8hubPq*oS>8R5R!}>ct=L4gd?v>KP!~Pxp1KA|gI}%9|>M_MB2Ef>J2- zpWVX=F2Vmo|HsJN|M6~T#1k3N|2oKSA{o}IT_i>l0`WX#Il}9XwAlslEH?1W#Pg8D zGzGl9%`Dh?cnF>-c>Jvb@w@_AI_~X1sPX{VYz_|I1dn0e(H5NDxXwF-Le^Tc^Qrt< zX-nqx%Lg-m!u-LPv3JHtJ9d0T{_xX3&z!-Y1LUfKGr#zk4^KSw;K>tYr3>_YGJ47R zq6)RG+U9O9hQIoXOYsb*!`X?UPfW|4Yw>lZt1IolH2vK6p7}lN`^E27e`@-~W|7!( z_B!*A*zN37tVs;hAuFW{Bq>~=h?)NWmqyrGl5(eg;WSXHD;2ZbN@946 z!HlBmLQ_%9ZNZjcZ8>tu5qE*M|6nMPYVsG$$Ovu8E|r~bp$h-E>H8PnLe8JgCLL8| z1G$23AUkDDdcC^;ch45bbA3*GoGiuobfu3jdYyMj4O;t}_|&@V|J#~O-%B6;NL%;r z&RYU|=#d{O*>1xe zc0F(gpDJAM!sXT}wx#^F{4M-FriZ-du%@C%567cXC9Z@+z89FCC;;(d@)tB)B+RqA-ohOtv--7^g2CQqtjY$mTYmF*D|11rp?h zJ3ck8P0r55O>wEb@Ag2R3Gk<6YmgpEp(nJdk$(1U5B49=-eadIgur zhqKaF@1gu1X7l+b*9`N>!(%4iD0yuzZ@f-weBc0HOVzv;#JuPTH8Nu7AKN;kE9eCc zIv$+$WTuXG4e#+YFY2=JX#WW|1EXm){I_)E}iv}E*C@>7lfwdr;5Yok8& zz9YWV`rA=|^q#8w{itueXY2kpinn`BqOsLFD(dx#zShvFTa|5f``z}L-Z@Q6nwF+F zG~JNCrRkRRqrxu1c6)knl)2PWRuV6&RxfI)3kKXSv(M3t>T0UuhRO!F%fZ?Z>ygqI zT@>@g+HJkKeh({G;>uopj~Xs&h{aIaJfk@_B|^6Q*+_6~_4IhU#1~TuM-_p0s>0@f&;n7cf5A6Nx=ec<8G*?sOC|(X1 zdJFxnz9>qEnxnW;29F;FQL!x$MgBnRMI#DYK<)+8EltseC=y0`sMlq2w(SqIG7AEBYW=N*zi4YQqm&cAl5)8IyDAso={9>L(L;fvT;=m!;;-Z9p z-i*DTOmqTt&9-UU&9_ZXkFC1t>CyA9dh^v+H(Gq!-B*%7zPpLnzFJ z<#12?HSU#3uc49g`x7)Z(1r@e07>Y<4~>22Sj-8TN`yQp7=v+e$gGBV}0mZ1;g_(rwmN&F=Ll$VulvRx*|{Vv%p zgO;DhP5cP&LS~U?iLfO8G-P_h%d1fwKgVc?%Np$fKu+83RDDk5zqsv+bK2= zuZdW|MHENp>S8vI$a%Y+~gw@k|zGGxULXaYFBWzkoB+`tTTK3vu~U>o5-%LH1+ozwnASg*~#iNNwL^} z6Z?etsK1xlC+_iE5W_JW#XH2O#h-~M#EfX!!92qpWmt>B;tLo8z9ds(NcxKXO@=1l zc*A(#Ov6lJmT#6cE4dIaGc5Kk@-LDWC9gAFBK^mdXgd*PF6+<>NI%uYAhiqo-RTK1-$rnDm_%TA}m)E&TqUrLy4u@s># zl3G%hsgu&`)k&P(j~|1%iXY#q`o$THTHRd7kbDfu$EcjA zj_JjZsZOJOwGaD#DHyg0X(L%ysj4Tz?xyBC8ZFbJ@EICH2{eTFX$XapP~}B`ol*7G zZ#3RvWRP)+kuko6KSm{aW9qApj-2d2eM;g^bp*LS66p(kDsg=s!N6(l;n`2gLlhWn zAJ=xag*!!nK5FkoZ|XB)*ZjP{g?Qc+?)KH zvDf3T*SuW!a=Y7t0_Z-4{Wb$SYBVQnMsw7dPS%X+m^4ni)4)!>X+9lG%jsZsx+a}S z$J3>w((UQ`bX_{FW)0?wD=UlJW*h5zapfMl{r)zN*yDtV;an^xIvhqMVq!dZzuVXi zDY|dSolvSor<&Kx<+0bx0-M-AqHih`#D+^MVuogM~JV2J%3^^DpX4q_7bt05+FPw6IcU+w1OB>b4Hr9k-M zu24(sIYxDAoNFLvc;z`WJR7{mMTqTNgxKOKxwmqLbej6G*AsBM>uQ0=?SfEKAgF9u zrQ6tjtXg?{nQ5 z;>BzQa%-I%yc$6YwQ@vvuOZ4hguEWM_x2y$%?V_s^`PnmAl?FAjCkigB0v;+tXYy? z$Y@W$C*nLtd!{(E1K(IAxP#Hmmt;`fn%S4xPeokji%8JzEy6cuo=AA%`6oo;b>a{% z=p;kv6Y?~HU&-7-FTABzd(D|wwY4>;Xokt{cnf_()&#!59&J=PL<@|nZJ8Sk_ra*D z0a6?pKmk6WlsRMmdR*_B;#B=#d;h0mw|XafCn$^YV$U_+Yn092&B{K{OWqfhL&|Ry zmlCkLN6m;+A5n4T($pc5o5rve{T%3v* z7boIMXy_Aa{flHd^2(-^Znvq6dkoj#FT zhOWXh@w8RC-6$ti_I>_6N}tq99-D!G^G=_YGVm$Pvl^KJ0e<~Tc3q0_@zV&ge*%j0 zqkmUzF0yQiEiN1LQvX-T+5b0WLk2SMF{kM9IK?JXx*6?FxxHS_t%xqn@Qmzq2~MZW z>0n$~bTSU?a?8jM`m=0z*l>p-?Y6bqx^1j2D2W~20!bWDOG=@!~7L8{m0|jWtea%-q*D z32?PF^9|cfadlkvmMVCI@+;*J3j2hD!{vn)bpdk2n+t#f1-vNn{8ZVisPmEQjUc4A z@5KO!kQWT(N&$*mw=ptFoP=aEVAqY^8g|RBPzj#Z0u8%5N`pBh1J4HJzQ{a>YVm%2 zM8Do%!>bmYi(Cj#XRq%k2+rw{pnk2!MOn<|E*jGCPER9gsLdI~QnWMo)p`KYvehX` zQ|rdH*M|K5P~E6&E|VmOEsN;#R=l`rf`PRdwR^;~kdKssUNRh2=y`c_LBX!p;iURl(#Y5S~2YJF8dEUpRKu4I3J2 zRxahmX_qsm5v`YlQ?8s73C`KDbk3Zm8`y=4%~e`iS)?pPsSn?1PQCHg8-y}bsTB9t z8{8Y7W&pW22$=sz^Wl^ha&Kz+hh#n{Ka6N0u8?1*F4jY@9{Tk#``kPvJ~SNqd}zFI zNcq|CvF}yY)m3dJ-d}60YpW9k$~4x%zn|7tSJyJrh~F0^3C#C$dHZ%&)z#F{%=j>Q zmYGZZzmm*b3B=w5UKPYLzp1UQJ^>MYF94U5!ma}kzf_Z|?;8)$wyLT+M%FWB%mDnJ zy!KvQRb3?j+Rw-|>h3P|3HpR_Q`gMg$$Z8hG9(Ps!EG?SW&E@0JLV~tB5=p8f40rA z&vVRh+(+F>=NRH#i`-rAF0R6ZboZd94S5BOGi9bYKAQ%WrTd8|TdMD`BT*wnT z8NRn*Ou?zh-O;7;D)~d=Vs*-6@!rHug`I_+MHR(ANR}l3Sn^TXD-}JJkEh0@ew+Gj z)t$p`*bTd3H|&Ppup4&6ZrBaGVK?lC-LM;W!*19OyZ?V(^#j9h*bTd3H|&Ppup4&Y z=ICDN2(p>~Dolar$BkiuzP6~Sg=1W&oSjfxoqj=C@F)v-B?|z@cA^I(? z%!0f;(L6Xo^WRP3QVQ>&a0P{T64;Vge@ouC9C_)EY+D{jGO9sUs2Vi_G#xEQT@X$} z-DoBJtwq)+3TPj8OrU3GMAtQ051Tz2cE6YeMipKz6#X> zRFsP~qHL1ZV&ygR)dA~3u`GxXVX^8blhA^TdCD*Z4T4f+$61=_BX)?p|>vDo1HXYkKQBY zbVRPC-|3-k(%xO5m(r!j=h3ozXsuU3uC??Td22qcM;60Fv9(rzL+iWM=cFvmwV^y0 zALy-fvC~82o$$^Ah?nan&YEseC!5=j_-U zihJ_Tatb9Of5{-~`S#MaI{dS^&8JOL(jrJ(O{sA$?YRXxYW%hqWWRoHz0!F!Kw6@; zWi9<)mRqf+^n5L)A41P7=@_1O-j-^#H*XMCX=AZl_qA4OFyRV4I&UDg&Gu%t9Fmi8 zpbOEc#z$A`Ju^^xc7!a^vAmjcE5d7*=y*p*TYjoA4YX{mFx}j8!)33}? z$~-h6VTbaw{{wxI!_Yk1c7nGhIGqcY5X+5-Pw8#=syFa zW>qy`)>M`g}HcF9)%FTrS;GnC~ZK(YPFtE>(Zr5s&%=(%;zOqAExvkUqeU9 z@?0IV<9aYH{o`28m+)FE^Cit-njXN!dWy#t|71UAr2)~;R_d7P%Bw*q^~pB}@Uj$g zEXb1`YcIf$HO}8jTacA_BL;Etc~D|E}|d-wNa!=-h7Gv&r(2EzKus?=+h}^EW~Dlw46p$E`~H2 zxac%^ayi6CL+WTqDTW-E>CcO4PtBxqXguVbL1mm)#x(FJLwq*nabuB8<0Li_@=u1c z$h%|EEc%TxP}*fQ=QLXOBuKpk!ZCV2@#w`N{`%|w`t{c{8XEXJQP;E`d^I^jvsMkP$Ao5ckqbk9Juj_>>;@)} z>QDQxV{~&;&pNJYHM0?wOl8i2Molb7xvv1XqFx*8znYC%*S49Bp$_HxSZbLDwC7Sj zR=pmzPX{ldb42xPLQ0LQn%SV9X_K9aA1<=WX? z(b5t=mti)J^j=?OE3uZ8y`-#iDPjEkn_PcG{+?^R2<9akYQ&_1?wm;=!S} zVziA)#g}>1=@gZhsNv53p&qrqb71k{;y1u;9PAlT>z77`X;1Avy~Aq1QBL<@Or71^ z)!)0gvtQ*sqRt>XRfh+c#ui6`y5nzl#-i%dfk-r_#_2d3+SR7s#nFM`Xr($FjjGWl zUC~G+8d3Xot{RCBFOKyN(E+kPBGGtfZ~t)V9LVg2lxDr3PBk9uj6|1o#@LGb66dU#+LNP;}F;N7Bf`B9B~xDs<8yoO?xPzn3*%F&QNSHvUG90M5W6B zZA)mILYF{ii+c>3y@^hH2Nw4)jnFld(0*{B|1Gt+_XT}g4AmjwTtEGFezkl#esxmiqiQ3gU z3|$8Yk}EYa3B^6}_)zf07o!8EZ}z^?I~0xdc9sstUVoA1UPR5m;A{E?OfOp&!&EUU z=HX8GFy}eLYc$asKcQys8^oBXX`;)b{ctR1ZnK+GQ<@yr>1?HwGt5>q#tjoh(dzYB zCoB^wQM+SsI;@_>J)N=FF=lFG*a|a>=IUS>6AaHEj6U0;$U|eW;TSrB(_r8t&=DsJfyi&+Rk}G)B80j$<+P zVv69rx}$%b0jR&#*Q#wTb?vWKhilb_Hnla}($P>;TcbW--G==0CF-jU?e#6Mw5zBQ zu5NChueQ{w)y?zOmm8XEO4QnUt>N0XHnkV(xFR8Q9uDJ#8 zl?HesgtoV+v_mecp|*_*n_C;6Q;%%*?1rX>_W31hT|;{_m01U&)oN>XxV>S{D^1m5 zwe^*7YfD=#y03xQ=7#3FFnXz-Tie`TieAA}Yderp+v=;EnpjuWuR!}S({D~o>-=!T zOZDw)eM?hKEts=wp;`6prdqu#3~EkOb;H~ewWfM*^-Hy^RSTqqSv9WRtM#>vh5oAX z|D5)QmS#GPIW5iYVPs1%%5ZzK>8lNGwIynGxS@?2q%Pb7@zk7X(ZYnFU30B2g_>G5 z%?PT{{3~s>DP3x6tD7LJjW#w`E_LG6GRPtz8jTFFNTZ9qB{&gJ^dbK%77HcH+j#U6 z(eJsXQR#cq52XJI_`Y;l+Vl9H`|&;ZC$R0~d+vX`dv5*H!^d~s|1s~n_1SxT-~IT$ z`|*AEynIpR6|1zyZbj*mttOhUYoIBi2mRl~o1lLy-Uar>90VKNVh@%Ub+YRUuCE(3o`vT znk4$|YuQG>3oSe4r$Fb*Q$Xj*=tX{7E&%;>(U{h-U` znV@IMRiLZo2GEW2%b=U&X3#Bi3+Pt)RrEMd?gst3+z)z*{Ff+QDgPDd5&1iy-?2PT z1WU2y^F*?gDAhzzW-IkXQW}&-&@U@(pxc#p(61=-K+jitK=&$rL{#2T-T89xL)o^cxVKV_T&{gaHJf`o%sGwr!VvQM$Y z4)$XE4A7sqyFqJq4-xHN`)ts2>^0!m+Ft@)Z-;&C4fZC`bL}mlTkWl&zhwUs=&-#F zbh~{%=mm~KA~-(h_#BZOs^ewQO^&%lbTm7LK!4fsW$_eUJ zIlc$Ze#b#@{=oq&Ilk}2nshpySwwVBc1{MJ?R*M!t`jTA`Lt65$K&*X_Bww6`l$0f z=>OxKK%JjCe+AAp=QVJC?Ysf{H_n@&f1BA&gv{47Unf##Pv%!Zug+Wz`m32=g-^*z zf2U6y*(K%?sT1Bxa(dv8q`#l~<8o3!^`h zWwbyP?0Pyak_?i`X}Z%q!D*RHB3Z0|nitSFM%~n2uadlQ%UqQda=x8m!k?4hh{gs; z4|^_W&$aBil|A33AD@v!?0JGc&$8zw_PoiS_vurZz2uE0ZwQU-8D`G~?AgVhee7>L zGNA1w=w+wl`qX&~v$4oV{^&Us_RAuZNj7-eP1NufzilNL@|H)(Xz?n#FyjZK=EbZycHHuk&UwxF%iw!qeBTVY#g8@27W z9kz|xCT!PiAJ}d7eEST0(B5cYVDE!dE{ zWyoW6UwS94yvscLw#>A=aeP$%2+K=Yt2tOnI`1UMXF0xTmeEo1FpFm9a;$P(!f}w* z&Le&@3n!#zoFf(?k{Q5BW))dOHjpi3JK06{l7r+ZIYCb0o&P?$LT>QZ?Hs?xaX-f^ zI9|i?W{$UWyoci>9G~L&0>{@mzNh0SGCAhjKjGzgHpgL(yEu+>%=>?0l;d3-A7G<) zkw%;b7qDnJMpls3WF6T=M#(#5H`z}PlVfCzoFNnB61hfh!P7+eS~k4R1%K1vakJrb ziJrOUPh92rj*heVO3upRxQJsv$9x1?tsHYpW({z>isOwOa|>qe<@i00xdpS{=lG_M zClijdIp*tOGPmGl?pc#NIPT$iCC8gMeuv`&9P?E;`8>zhIKFRQjqRJvD?0l!%PZI` z^I`uPL?c1HYPc~!k-ih7& z0QT?W*ul?|3*<7nPHvNXf-E?M9HBrc5=sQG5E5#HCZSzeC`5#QAug;G-WJvin}uz{ zPGOI5KsX{C7siFN!Uf^7a9y}9+!JNdA?An$Vv$%Pdc}}fBXa9KHNx=*j`=!zim#)m zj&gjO<4YX#b)3uRFE^j#QjYoh&E=NOn3 zIp+Q3aev6;{*cH0A&-wI@0O0Ia=T6C`|niV&s6UJQ~6q%%EvXek7K?9r*gla%KMvo zkmIqmT|Z{_v;2!``CI1v)5bR!-jwrP<~QfOA#;9y3&uG&H3lD)A9r6d{NrlVA=(9e)=qP{`s`i;qx(bzPQSqf1xlff6ttsF=3wF5=UCT zCoO;6-2MxyIbS-2^Y=KYu0MGG<0p(|s`xwccL*x(h#G>KXSRCBRqE)&x=kpl&MLb1mo#LjmAwQce zFXrWCG!~QoTKa2*cx4LS8IJ$wxB=e7YrAP}=kreFou7A>AWnbLSw`fv+RJ!tjsA7J z#u`%wW%EXPAWi20o$Dk@jp~jE>U^iTvn_6A+jWtCFmC0_!o?!Q0S?oq?vVE?7E-ACO*jtF3-63i0p2ov&XuSN+RbMr9JOR39 zAom-P`%UqiWD;vbf0@84ev9@7sXruw^|bXg5v@P9{**}83)YK7w*JEU3)H5)*;sFO zE<1#_HAMgQiy-F}gI7i(CSDGkp$^W2A3%$*KT^M#2XQldZsM}(`F8=6T**1JA-S2< zn|P(bK_stlY1MY>!OzAXC1 z5R)0=JgidDC03YtR?J7HI72Kla)nz&6z+=kCf@r*5-y8G^qROhv8A;=h%x^C7CEGV<|_NVJtxmp$NG&AzIB9vP@;?fj=Pc3#BIRG0fs+a*|vY@=W~Q;EyBi z6NrhwiC7T193Wc(S4}0$Q8LQXDsswHvIy_i5tbr=1Ew;%-XdhXiAUF4E0W26Y`q0p zO4!f5-l$)ElMv>Y`da_RBdtQ(@(g?aNbgyKr%JP>)vVWc#BOoAi8}^4t-@)sRm?K+_d`yFSRot& z+%=VKg%@oX3&pT#8m};d7K*S&RK%cIWh&{0uUtp!7p{wHTK|pEiuRvx;(F1?VIe3S z7EPlT3gD0LAh`h7O(h~qtioFCDVK$7#uE6Bf_>tKFn~5=!f8|4ILQ#gLYSPvbB(Z9 zIAAK>3qLp@c!eI}Z4-YB`YIsXgaT;ZZz@@dzC`G<8c#pFu}YP_25H^oijXgaO#FIy z^C`RuPN7YasiYL%Om_e;-2nt!vP=;4Hzwg6-8)5`j;PlW>yNBI#(V0YtY`3UI%hpc z6zh5Gd6HrMne}JHYW=zO=VX%gvi1KGoAoL@)4{yaN!yFx5r2TU?lEzUJR|-H>u0)n zUc5xg#LMC@NhSN#>x;??Wd*6SecAS9Qf>d7eLBvT^z)X+N8SM*zN7qz?<=W2;%^PR z(6{-?E1%gm}+G|O*eXH)8{bdPFgI#e+py|bpL@fBRND$^-8Li#v2F(V1BKivv?_-ddteS(}| z?_7PRtJyjD3fWKSEx|Fk&?JG*XdcP%R`fIE z96EBvrJPd`T_`x)D_fF$QL!m_>!F3HoKRL7>hHDOvf!o+1+_1neEdZE6XTJkPL~d`Y9rT{C z6k5($;QN*^N+&FvQkJklF!)|jH!XKk8p>BK+hm+DEbx2FdCU8$7V;_iEchZFh2^N_ zxM5U7@&Os=FZn9k?6mAQ=+|NCkVoYmC_9C+b#&~>RzZ1{jQ6a(pVF2AOWe?|&{8OO z$vr6Dg3>}u*fP(cV~u=Q#_l06M;WnrEx}Yv`LbLrUyyMUmlvYPJWFB9BJxQjhy0$5 z*pq$B4>K}*+VgEgdW z(l(r1ub?ysrQ4*_(v_4Z@N^pE-9gDJX}@&JATuJqD`FLj7ueVj7$i1Ijp7CxC5y*U zwn`c`v?>*2BHq5@E=o&X(ker{Y$;pp5V88jbtui2>ZGnzOPpD+i`W6ge$cn2Vrf>Y zCEi2@;%O0kme`6O9g@pXc7&an;RA?WMe(Mj7|M1arrkD{Ev{zg zZVA*uaXh7qut$6i=V*5F7JI~P;(mk7CSjvkBVaEQjzh+XnA(*rVwqSb#KmG^1xnvV zX+&IMXqhc^h&CF*BI=dIY_U#Un9@e55HMnTDis?Inz)2>h;jv?gN-j$+eRZ-v=D_8 zVu7LlUUG|I{}ZtP@e?@fTi61PC-BClk+Hx};ChRdFv~3yemK3Q@8QF&0}(YJ4`KA z!+QM|Bqi>~(reTvYf$N%=p7!DENL5 z18*&t*UWi|nf&B8c8_|?cZ@y%k)04{5xP4s=jWk%HkN*zs;HI`1D8fFJDAo9-lO*Z zH`I#Ov(_sgac4^FeCv~+$k@z^7zy>-#F@E)@ggj$*>2*s;zUV((Z~z2JEdhNUNKq8 z=FiB>VbN4`lJ}^V*p##c&HWdWx`F)$dw!EX>Gv0zzHb}!ZRGl{GU(gL^&K+ktMj^8 zRM?#4J*vJNO!|I}JvV;rET%k2-_>45?MWZA2mK6t(9aQ-+1c*nWV_E3YzN5t6fLRO z(VdrmO8zd>qlxk8J@5v|t>+Q0&efPdNWdCdW#ChvrS~EEEOqme&o18iI(z;PLcdoj zkqaNvHw*pLN6&Bhu+Nl_eE9dK{OS*tWZpqC{*Xf=?9-0arz}s|O0>QPQd zddqt&(_7-Cq7nLeevp0xG~^(n0cc)G1Cis_%FC74DsNWask|2w^=HT$$_(Y`{7`;K z1+ACroav#GkOqE5C?_;4R1<0pwT3!E3qxI@p3p!j9$FC^39SjO4{Zu<4Q&tY4DAl> z3mw!sT>g>JvCzrTsnFTbMCfAZO6WT0-wNGDx<6B%X`7ifGk0bIm!|h#1vH)R5Jm>6%$wc`dXCuxV!L zOz+H~F8LuNV*HQ%bnKJu@rQ6;qic`;`Zo#eVbEGBSK2DGDszDgDqVOMR~7`t%F;@2 zWw5d;xUjOWvZ~S+9I4C-?nKR|%5dd8loZnx6j9TQWCKr6W@#x|Ra6d@Vvtb)x)hu& z)RU2DtJ2Cvs2f44F2(qn+$S6D0dkm)mhPUU;wWhij{JG2M>>mnH(x5DI;(s7z|bgb_VML*Mm*LaByC5Q7{th3l0UB0k5P|f~Ai* z&Jy-!;p6n*AdbLjU`JpVU{7FwlHMOUl$1Y7d`El-^8Xewr67J+{4Tuc@5FtGr+zH{ zm?+{I@h8yXr{Yghb3&XTHuUvt`mPoh3NiXi_=NuJ0K~tVfdFU?GzMA&9f5^`u0T&< zAP^6%2#f^Q1l9*O1-5>qrP8gQv=_56EEcQ_HU;7ddj)oLi>(Z<4z3N{rNHD-pz;8; zJ*{VgkJIg8dO@e8J_%hOrpJch=HO^>M{rkgPjG+mQ1EE*IMZt^csh74cp-Qxcr|z< zcsuw(@K7a!m`X>mpfbBMkL3$1iz;VSmL=dL?%qGuPUgMDpp~zIQ6&Aj2N>%C)X~6k z#yjel17k=>*^1F?>1jXXjG{cnGfGRzI?m)#DJdDJ1Lp!40+$SJM|rdfWzksWCQX7g-N?ztPag<*#zg2!0aKA!M(4ej)E56DfXYTE(Wdsuzw!XBH##c*x%@P<7sDFpQ4kdjw`SzjaN~C$<>B%fDC2K8}!=$8S&>G9Y*^GK&B-(13e;4ZRL8&ff z^=C8M7vl_CeaC%cNT-3%0gn4F_%3;m`L6nI_-^|?@Rj-ro@3rke#IB`jrkq^Y=0g~ zF3{vXhMG5!#{7ADN|Tn7^#PT`q@-k=L(Qvx1-uKaXWHtjzYukcP^wG0{m7%eDCDrw z&Z2+AR=VaBL8~vvmye|Sa=f!a=lG`kN_?8H!Z*uT<7@P_`a1B;_vHE(`npiB$2aie z3Y1J|Nkf{2l>g!iJ*7!Y$!er>n3SXp)W}EKLZs=eXS6~-j=Ho5CdF6ti1%cl;(g^E zV&L%hdt(nt%MG_#k8-PG?n`Fi@Me4S45^Ug?9Z|<&sGKw&wkG#LpsXw{!iK0T|s{{ zbz}{n$@AptPfsCmk!QviiaceE&I9&)LLQngLT)y-Yfcr4uJ@*OsxciLzy!(CkW%o6}P4^uJ z_cT$nYMEM&mJd+1=~@W`O{>sm0czZLv_`$I)(Ys*7HVBuk2c`m%OK8S1=>&;(bfRg zYnxDSE9z}Wo1K6W)EPwhv_o0Y|iB3{HYR1vm?u!UX7xCOxkJt{d{V z7~DnvzLx2cwObw=Aj^}>pa9?k6l*0EsE(defY%e`P{p7QG=(P6O^g?&;J)UmqL9dY z=7FZL=;HxxA{?qb#ehCfgu#%fN?So?=<`7B>sbbvrNlQr&hR3%p*! zAHDB*f(+jE?gbo3tzSw5lIs^V1+TV{g8Q0xFNMVV^&a+AQAn&)?|W=bM!YAGAEN8l zJpWjiSQ7y1|5%IZ<=(@{V^3jwLn|%U*Ku+^zpv9+AFCOp?JJt5uXFD>WSs$=_pJ23 zkNjoO*J%IVo7yZd`u5(_8hs*d>$7SViT%iXUaRoF4_>DGjxPsbW#`R&_f5|^t<`hR zXZ7Ahe!5=Yb1qrG1a&n)g=d3r7NEwn!v}qQt)M#q3z6?az6bOGAddVB>W97&&t|}= zZ;fY{Z@un!zD=Gn-&Sv)Z##VLDE$36_MD@>ou1>`KHu(Cx%<4%_iZxlSNXnu-q+Y3 z2=E<*kEi^01B0}+fwnrVNQ1j(@a@zg;hVlAo(sNXY)>A?o_s!8x5Ir{>++q{I((;q z&-x}{LkbrG#}G55*4AF*+QQuIWx)3~?_u8+PpR)Z-Iti1DXi1|!FLO~P{6wYHaM5S z?gPHN3{K$PbVj=lU*FAO!gnA3KCbT>9qTBEn3 z$`$uDKq8+ukL3=}9tPQ-s`5NQq5j_2_W;jTyc1ZS>Pz+W6qgt2z}Nka@);DgBksfH zW$-=t5&UkP`-dz2J(Iq_LzcfhR6e`BUMr!HIu~iXQP!N=-^<&z#_|Q^&*5)Z|B=jb zjycbh2KKkB^Vr|6&ZmE^C;mpfNr?1}R0f?qlAq*CUy*K;e7RNr64@ZP%de7+mJ-W< zCz~z1EZ-Fzmc5qaLYC!(<%ICO<)k7A#TbG`=u)hTQ-~;8O0LkS-6d4l$qpK9$(+-0b{;*q_;+ z`Kq`jbAD#GIGVX4^DE-JL=ZNM#Vq>TKwcw4Q4S!VkRla$dJ;%c$wNQ`Q~+iHY5gN5Y0_PR5uL~ z-84jW(-6^3t7AZ>A*!1e1|X`Nwg@~#b<+^lsrMKN9H-NXyzsC((}q5>eYy+=)0XLF zFN{4$zpaw@@4m9>UuDxZy@;*pzo3ylB6mUBDr>1&{aMVwvzUQrZvo#;0(tiSgW#gS z9)Nh;g_&_-W?YyV7iPwVnQ>ueTweC;0y?h>fSGY&W?YyV7iPwVc-s{LU}jvft_$(D z3-PvVHOFfKh`C*xiQq!a?Ly4$+65Xhw+k`1>k#Opz=*e9h__vcw_S+0U5K|`mw*v( z|9|Gb1-^>n?EBfBGfT+so`et}w{zR%oP!V{#uzC@L`o465dkp*A~yjMF-3|grhtfu zF=E6>(IQ1eM2Z+GAkv5!5u>GuNRc9?NDUzxmIz z&s=sc&-`a+vS-%ewX}1J-5H{6VrQ_kaLR#}od!qQL3TOpIVYh+iFb?7i4S9GFsGax@6J*` z&U;F{H;MSN_)3mZ72gz@&-%Q$#bIeI#o~Ire>}`F9*fV7FOM&bS4PIO-XHroz9e>< zC131tyfRi#dhASWU%VoAgryU)o$>P6$K(qZ|ND>YvyhU4qkh;Q-+cik2g5)7FFuHC z-zsqEoB!e&TuVJx7)yx_j0IWBjU~he#bWPP9`rFIpJw z5$zrA7abTK8XX=T9i0%J5}g(;iO!17iWN0&xdL|3z07u^uu65STv5#1Tx z6Wt#@6g?XKIC>&_I(jyyv*r0?sj-Y$I2L0&%#U@8^^Eo5zgVol_cth}-YCp~HIiu0 z)~LZ64On9F8-sLB!47UD^@_)Nbw!}=f!c4ZnOt!epwVvbo zEt9RK!`ScTQgb@xRYqq5XutaM+gIU4^OIY?cdmGvItIQRC`*r&bO0sv_ zJ1NE9ZSSVG_B-}oN(FDI|6V2km)%L-BMJ9N#y#5L9&MRhaHuFjL8qZ(WBu?Y?vL@Fnn}jd&!kBZiH#NztY5GQeb(8eU)p(9MD|?q0zuc2+ zsK7a`{<352a$ULUUv_SG{(owl#pnj?nce4{rhoRJ>|yMWJf|7!@Z9;|bXJRcpDKqg z!LK)Nz#(fl$1Kvndg)_70zO(BJH@d)LD@_m36C~b?r~2;%ypz3I22W zwEi^P$+POcYW+F=Inwp@`g#)jMtvjs^d0&RlK8)qf&V+T;#jrn~t0m@~5R`ka2O zGs&6iOivx)%v9md9A|z~iBs+@a+b6ya+WzOoi$FCv&pG;YMfeUm$TP7;2d_2CGAMs znY73G#5w7F?bNrK(WpOnf6hPYbha-$F*`ZCeRfu|KRGoyBim^V&+eFXlta2C=VupW zchByX6wL0MG%b6;x%+tKYUyVW&Ms2r%O2(W=gQaUH{RdN?_2#J*lypuy&Bu2Mf

    BlQg_T;oeq8M2{py{C3A+ZkxQM!;w_J%7}!MyR<1ws!cA6 z#DdErx#7i;{G`31&5>@Ao{>JuT_XKCY(QvhWKd*SQdMLmw?vP~*x>xgq{!6Jsif-2 z^vKM}oFLylGM`;}WKm>^wKu7%%>=$$7AgqsYjc*RmF(6;sv?^r)s0uG4$ZGxv`e$A zNKIqA_=iVon_o4rAIh>TvNxr_DvLiyl`L`~Qtm$*-uYc&|NR#7Ut|-_tl+!){X2X5 zzoc)IjztcKhewWu7Dhe^eP-21PI9R~2^HDhBVR}A9iNk!bTT~JNp{-X#ZFd=p4>gT zC|nj=qnU-&UmMzGubH)6+1JMb~&ZtGH0%{fWsEDt8^Y~bJm_4UhFJS+Uu+epK#VX>q85z z=}Fbj=D_LDL8q8wZRK3HBi&%m<;|pB=8DigMMw&IyBC|AWQs#-QnStY3^BY~($iNxpRDQ~-^eSFc z_swNXJbxh4Tl!-~E}q z=FFPPZo2E#bw;V{xKys217FW7;#Kjt{eHCZSKI0&IrTi=TYaA*@d#<26HR0-s|$P` za)BG3)gAHuS&3Q6nJ2T_dw!PZJFcHr+a$bW=E)B8-0Ma^t4kA~!?Oyqx@Yyu>MQ23 zKOn1b*5Gr#7w=o%nEbYuS8Z;6=kuESJ?cV?sGvG$w)G)6&2#ND`(>7VFE=Z5Lgu{8 zvdoIi#hFVpSFl^n*DEsDv3_ah1`gi>w~cii-|LwNxJ(bJ-zw{sdL@2OoY=IasAD4P zXyUC<3w5+n#}wFAThuWXwP{BhPB79N-%xIy{H;#=2A!3$KVxymx{O(#JCt!WV@1Zt z87J7C=8#!@eU|mp;D%>Z{6G97H(;>o=9L zmop#nQZjm{muB=!-<@8XzCC?+M(>P)>FeE}AhV{_4f`MYRkemQxzD{%Oj&T6I$?*$ zuKKEz_kSV1Nc}ok*!Yd`L%|N9|cZ==3mGy0?kvvZqRgw{2Z`1V*UzwRx51y8}!crFGJ{F$oE1< zN$4GDJ_p_oG@${R`O4|%{Lt!Br+Dd~BXebq-l{EL;I-dYxCxlv4 z4EQMUWnemR5AaUl)j+f}jYf^!{=E?hD|Js`(=G!32zWoRCvZCAY=YdDwlY?eI@|v^ zh%*@aUXWjf9EN-W@EvGwfc!q>2xJ3T zi+nzX{2W3bhvo=0PeOhP@+`=;ko%wvDbSn`jf6Y|a%aeQLLQ>zhG$X7pF#f)^zT6~ zg`5w05aiyF(azeXkjE&wp)>GvXhtKq>k;QlXhuOUg#0LE+)JN`9&f-H%mvN|;`@BM z11tqT1_T#z?c7w$5obB{3Jbk_mLq3{<#^p2gLZ& z?*qdA^~FGp6#X~AKLIa7+Jlg5G14CgVsz^mgE~%r(Q%53dRGM}Co6d?WU!_F6;ioe z_a1OKG*^HzP;!BG5U8(k0s6qY5;KUn3J5k3;2bd#4+f(5 z1(;3T3j8q;+%DWUxgK}{&}Dm^1*0#;eKlwT7>UP}ZE)J<$VcI;Ug&b%WjWF^)`Q#i z8yhYF#u+bG=hW%b8h*q$MkXnn_wi~_|2bQMrk98=z{?q@a88;&i|2DqpDQXDX9{)7 z*S+dAV%GNp_GP@AXGkUY2mX}t3f~aMUfg4BrM_zzN2*ig^iABBN&jN*-{)w z$rt-ZKt9Siqe0~`7%@ixCpC;hDivp<_*8{9Jfl!Lh*^)A&mxCS>KR6={}aq^c$~3| z?@OhZmmr6?5$6D~7OB1jzO33rbRuOkZU0O>x&h8*Q@jDVsgCF*KvKd3RM>^7kTEQ zUVVJq)j4u5s{FMIl%YbU)hg8cWpqa6tmnxSs7sB?Q^jY^aD^&oz0%8nJ8~#i^+Il^ z6mm(pMtT`il}YuT(L9_Tct({(Sg7eKg{wB<-!j=F3Dsy3PC*4;N6@kgVT6pr$# zl}|NlRD?K1xJwyYEQZ#LNsLpk{lh9P+F_L2&L~@T{a)mWHus%IyP;K4uNvgm$Gtn! zR^VRqe0QsUL2HMVEQ)0+%LfozhE|H94XaU-BDCQZeQKM7HEe-j^n8Q@-9YL!OR;`MBa)A}7 zb*{1>4(*BDYB3hjs>q=TJzgYB(5khFQvy5){Xyu<(W-6>qmSy{mPZStFKUpg1~Csq zlLpLH7E86T+X@v*2Z3km*P^CtRZG@YqkXDj$*Koe%H@c;TxnDZsaEM}u8OafB6Nd7 zeWp}(-z8sHvy73eS|0edd{VKQ+^F=h_A+3VtYbV+A;+&k{0j6`CE9!~+IB5Q$^mHH z5#(B3N3^GfwpCVIhIckAd{r1J7V4$Wk~{`ZSc5jtMVwp}%A?hQr5WhEUFg}KxQmLv z96geU7N}AAYj@SRQO}3Kw%~zx(VI1LDp0lU3EZ(9`6Q^4@YtzBFITB{g9VPl$UTGd zw{?3{^%rY^XxlSxe58%}W}!wW!5ceZu^Fgo2FBnLAS_LtE~;0n^X1t#ro(c^BmeQR z0kDu_TDAnmw=+~P^9VwZs}ooGE;?|Y>UXqxf^4hu1QsdeyTH=wmZ|oszYsPL_CWmI z7;C#Buf~0IVON#){m^&)FsHC>`)=Z=9?jRSUVWeVDx8ZNR_&<|RC5-OEM?WtsglqR zCHp3;5#*Z;W-CymO!RYEOPyD%cULmiBK|?d`KfAKQP7}v=XRivHY=I+gP_TWCP!&h zd1!}G2z{;M88;Q|Dywb>WL2uoO4i>uihydSeqW7B(H^P7>N~`YMJfjsQ!D}QRi*VA zDD4!Kb{R^0MwOOB)y#WT{2X^r#l4n7?g&{~!mB>D!_v^8PV(D>9`rYaAgjH?R~s%t z9Cf1JiH2V&3sI*X>nW|e1C`a)w^FlzO2P!-V9a{?s@;6M8HbD03Vjd3R@G=7t-i<1 zGy4Seml~}%`p!cCnL_yhWc6%tlRC4QH3&7-{~pI1Gl6#iE0E6`$aksyjZ$bzpt%}y zEz0c1JP5stGf~-(eygEooY}@xj8_x-5lfOEK0|tKM4o24cGM!(jfdVE#wdUFsJq+o)TuWbo1yjEE^} z2GJ*C>`dfUyF$$A4GSO-0J`!j#4G^*8F7|EKU&=dII&?rh142T&7L1Y-v*kMz>A@G zS7#4EgXeq=>mwbj9u4a?Z3NKWNxKg6$3X0%X_-K*o3v+vy%aY57BW^$8upjSE$vz0 zgFvidX&Z8O_dT#jq`7+|Sik5$fd(s0?PefWJq_5S(@=g5Yd69U8@c5~91ZJY&E5II z+O{5R6CEYg&^|g!sJpvNSkq}(>k(FSnp{1#5!R}jyJzC=(7OGL)uo1#>+TLGc1Ja=RW_LeicJCj2^n`Di^ev5_@YS>5BhbVo6 zJAU0AZ!n35Jr=^wi4Xgb;wESYArw2w1l#jXK`7cy!%j81`y1|#hszt+q87NX54-UK zt=fS793M(3-2E%;agn=+fG^Ma+?@xofxzB7VUI#PfzTc(^CZ;Q?d#tIuR;9okWq3S z<5b5UtnT(B`b$iO#?9w$Xux2?-EBkv`oLEjtWjHzGPtdTU6gwCX~QnyejwVp0qs_g zy)1I~$6Q-QsTyEEJgX@Uc1Y^6>syDBUWapb>#+BtW7oYNU$d)6Yt(}`>X2JKMr$4R zlk2fR!}FO!*it?A_Uo`iUWXR0$NvuM!2@+)L*53&7~%D#!UA9?lpK80Pz7{%!rd?V z!NME11F@&nfE}JX%qBdKDmf1^i4y6UC?_#y+L*DFI_ajd(`eO{G1I5gCdwjT-^&Jv zDgUYg*N3V9kb!-|H1^uTj5CQJz$gJq} zpFldbr8J6gxzDG7dM5z-)+D&x>EzIP)P;iTR59oi$w%!dgR-d;b)^s`tN(c-Opr;E z^3;E=1`O>NruKuczADTqRhSz?qa4qV(gowj-CC@51Qq~$0S5q!faAwcxp|^i0bC4R z3S0qP4O}q8uyh({q|XH|04@Yp z0v`javEx(!>E)I+et?|Hf9=>7I&crCbFXD`&j+|qLTm>S?%QncktkaVW(M#3Pv3^% z1MdItLhC!R^>?6P%S%?DJpbO3MtF81jCS{F2Da$!(!$r&I}hK1N#BX7bRkWm5?Vk@ zXfO)XEv{E#^q%{Yt=!P$n!}B? zEv`@Sb!+r?pao5jC zbp1@f=XZDgtf8Jiq4`|{c`dHH;kxO!ra4uhhv)b8{K1|-3b6y!_$B+%Y*)o>S##L7 zD%iS~vwcLJ^xM5Kj8Tvd;VG14|=^EZ14Fw zo?qbkeLR1d=TGqb63?IK`HMV%h3BvL{B54U+w+fj{wdcF>7MV|R><-Ee9!mpAM)-W zn(XS8P_i` zJU`X*z1}P+^n9;13q0E`@OragrsvQ1{7TPX>G_*Hf4k@J@qDj03r@QJMLy5>tm&do zp5N2+`+I(o=Z|mR>V+-FQelf;E39nJ2!&01Md#}i&EsD@y~XvM7T3p`Uw50_yjQxF zwz!_z;<|;Eb(_)Rx?H`1M4B}a+Ue?-Hsot%wX$3JTP5N5G5=I=KWHHyw`wGuR znW-1`r;*(ICEl-q4|AwNmPjF8|4MWqU9|EvmqU{Q4c}A!f=aaY!mdDA`zEpbLz8|k zb}C{^{3alc*(nEi_yW0}MEX-m^QHAX$25>8tueLI>0dQV&0+f2s(hSB2J&c+JknJj zUKzNA`ajg2_0xDW?F*fsm4(51CWy9N8g-t0jFNpBzHDDQ&xj#k&=>Kg`8uj!fcbKL zoqTa$XWw~vyYUO1*8ut{mAC#C=i@^@hWbtu=a$O%YAr4oSK>`Di*GW=n5{Rcy9#{L zLL`br{C~~DlThQgU)(2yj(A6|7rr^2*F-1ysW{jB9$}+a^S-F9>;Duw^0iQpreE0Z zh_6KU@zR{+--J`=O{mgq>u9eCim=EL9YwC_BJxEcKc~zS^Th&D zCKiecQ7IOS$HX#za#J}4iSzmmU}zmZSMr{#0dpR%8^pS7R2H`*`SzqeoVU+W+4AL$?M zAMc;wztcaP`)jWVsQ2nbNQBfo3nH8QJ}%2XT`JBK|Cj(qcrinc%IV5MzN7H#EarZ$`mh& zmnciTB3_|@cul-ULGgxogF@nu;*S&-Z;Q7nBK{=)M2`5g_%mgTzlgsuo4haHr>OWq zd_Xbrk@$$>;_u?`)KPpYKBe=-=i+nfB)$+|P-ovD-yq8M4fPGB^JT8gr7n{HsH@DA zd6XyfWjV5O>U!6vPRa>jdHu(PNR9e)Y2HaQ|_d( za<|+~HIdsXZWbtNZ@tL&kW13Xqu61B-8Cis*y_Se48Ns+z1p1BbA%u%yIO9InkU*3(Z^1Tj)XaHuE;BFmE?+r-#ft%sXh2d8c_NJ#5Z4 zXH%tlw|O@`V%}rkLyOJ(%=_q9<^$#fw8VVKe25-3A2A=H$IM5~N9ot*Cq^c(Y8^I2MMK5srxPna9cjkLmi(R`7fG+#1bqLt<=<}37+`KtLU ztukLTU!$k3cdU14we_y`EjUcpde-{T`jFOHA6XyK zbJnNUr&MKqVSPc*TVGmV(t7JF>nr-5^-t@cw88q9^)K3J)me44$<}O*Ua+MtX|vth zZcQ)RnRX^^u|sxn*;H-E?Kr(;ceXpzR=ca+m0q?h?Mm8aFR_=A&88J^j%?!ass``ET^!NN@Sa`Nz?2|4sgz=xzT@|4iEBpXHxL@9<0&q;lbinCK+( zM1eRZ%6T=tL@X7{#Y(YSY!I78wb&-Mi(0W$>=t{)esNG77DvT#o{v5er+h27L|!eglOyDfa-5tfZ;=nlN93dOaruONMm{e$$`|EJoJUN)F8?6klJCfO z<$Ln4@^A7(`FHub{D=HX{!2E9 zpzYW(yOZ6;&bNPM|JweI{iOZ0{agDv`*-#W{_Fgs{A2yU^xwrZLpkOI!knOCPS7zM z2+RgP%mxyyZ-DPxf$0;#^sT}4iC}sYOmBhFZ7{kYjGhEWPX?p60i&mY(c6O2Q^Dx% z!07G4=pDf5X<+npFnR_UJrj(c1r`s0#e-n+5EwcPhK_)t9WZn@7&-?G9R)+jz|e6p zbVo4sd0^;HVCc?Z=v;8;`QXkj;Lfh#&OC7E1z^p5u;zte%>uCIMPSWB@a4te%WmMy zAAv8sgD-y!zU%?MyaasN6MT6o__7!H@-pycZ}8<$z?Xf%mp=tx_61*F4!-OMrn~}7 z*&j@KC7#j-fb*^bs|^IJT@6+n1P;3f95xsnHUu0t6dZOfIBXa=>^g8*5jgC6aM*D0 z)(G&{NbuGT;H^>MtsB8xqrqEaz*}R%TjRi6FypM#BNfQ{||8SD!%{!&&<8<9qzp-QV9`B*17iTlcbVl*Q!;v$QCMExs|jbAEK0K-zhCB zrBaeqLXrvzMTwSAY5COu%p2F8&-e5BF8}}c`~Uua?&BWkp6AS&nRCwjb+$P(o;)9( zKQD;4n74!%#aqSOz}w7A;Qh)=<)!oX@b>Wz@s9DbdAYpvyi2?S-Yxz^{$u_#{tNzV zemnmIxIYCf0aw5is0uU$Lj}VH1_GgAyI`jvL$FtHP;gXmQgB*uPIj8?99d7R0aJ}1N|8c`lAl*L=8}(A)rE3JnJp(ghXL z0~OK-6*2%7G6WSe0u`b_g@m9&#-Ktb(1J4sEiwZwG6yZP04=fvEgAtWxRIbpqd<{< z0Yw@Oiev?fGzJvO8Wd?PD3T2*(l}70@t{Z(K#^=gktTv7*?}TW0!6Y1MVbtX$*H4@%_;O0@u#$_tdr8 zsiHxtVnC@xpj2x?sn&r~tp}xw1*O^mN)-o6wGotR6DZYYP^vAUR9iu*;z6krK&cWz zskVVqC4o}WeR?&FPxtt{MmYq}I7+Nm!3#m1;JKg?qu*~yj{?8|2C_+d+KRNU9_=gkbLf!~9Q7pW zH#pNP9Ye9}SSeP8)nl#LN9+eq;(c)yOa@cJRInkK4rYj%Vk0qY%ocNC{f5p=#QKcf zb*wMQjbnX7ZafFIX;^9;e~gea;4DI}5hn<_LV*Ay_{{=2u5*9n2r{ty*x^9wm9Y4-RVvfS)MV< zR}|j$A991Ba|WZ|U`)p_fId3U1nu>K6_2zL_TuMsZQb>s)e3u^Kzp%Zo=_K~Fmv(f zSXWq2YIluY=|ZlOp7*3s7?hfTv~V+*hVR89l1rO3Y$6CvLwED`zX z_df6HzG69eOP3{-D{IUF`Q5M?$TuJJMgG9i14e&Y5Vi)~JK(~* zfmp5G7Sjf9F1QW@@~uRnqVAN`|9?!$7;x+U7o{qX)a%9JY+@<;d?M((;_3Th1d`Gg%Lz6p(3$0eNVCrI-_!;nyBsF9)Cm7Yosg@ zxky~4@A?<-L9cpIL*bd|+B_uoxJb}_;(-C+zVb65;gPiK8f7pD-pYC5~xJpq33z-}PH55lRvA7`B`ejGmjVmLP$+8*Yo!Gp>2)dqqs7 zQ2phGTjKgSJugMC^4~A9mpLwvtKjr(JpI1DIElSf(B~6Qz*#Ls-{5Zc?|XwfSUXwq zFF(J8Akxs0dn{3g$P(|XiuSF-v{A`yz$l^-#WRYtjI#_I1F=~{Y-1rd8;H$btVHya z1-hO>h+H1b<90m}fw<5$w@X5s2VD~mDgdg5;$$P$Izn3%Mn7u+N9`jPJYhi{T3`0Z8sy*(Ivr3x5=1&GK?FM)l^}u>iAoT`%|oh6 zpmJ0S@0Pub+`HsG9YHEh8VmQ<2J%TTYiEvH~e<*K=EFEv`at75NCck4U5& zWHtF1xsU0$s**M26XZT2pCb1uiQ2~GGqM)BwPYP~>yUyIWIg#Dx%AAvQ+OxA*De}o zVoo%%ZQJHAwylZn$;5Uh{Kd9y+qP{dC*S|ud!Ms!_RTpL=c=pMTdS&{?)6l6SJiso zIo2Pwpa=W`tauMq+aov-E23SNF*b!;v^Jpe;)I<=}Z&KR<;i!fJLG+Z9P|0hMYZwM)85b747pkR{ zem-2|v$r?b=uUz=)26>4MdJ5F4n=#|0%eLkT7f7FDyeBQ)mT^q@9hAa6Saa;U{T6iK*La9(HSlTvzmBB@NB~@cu~RSo@BH`)PYzaWLw5#NNfh!Ck34o;P+U##XpM z5W(P9ufTBEXw{A%`Y^0B1bg60U_qd0k5UhUP(_Y%V^w))dBwT+d|zR9l88}lDzR>F zi$G3BlmDBv>~26mc$j%C1R;zdDi$ejkd+?76T#Vld=^RaSXsg} z_;^TCI7y74bI4lEVLhl7XcK~1ANX2=VPC>G?0t|}a6v@IVIe~309=z;5kW*yu71Lh z09+`?RH0}PF0lO{LAa2P=)+1-Tu}R1vBvO@xWf*FL)iNev1o$wjKfld6ag6~u{MM& zg0`{=RC@QygcmV`yvq?nTQu{uy(;QR2gEbxvP!jts^1nt0DMhL|O!~M~jhUo~K{MU_QjR*$>!?E|-W7h=j zSR`R$Q3=W59WjS>3Ei;w@ngLNFPVmY2%iGh&10DfO$A4>_CaHz1@Rdrxndm&58wfy z`CyWeK`LM+5P%UvqyS!s+gPDee-+RYqgV++d{B>m!Ue(702P*DW5SpKUb9#nLN-Aw ztbLeR3c)PKVP!&v0AABrbHOZ7k3qr)L92jyW=XtQZbBD$08&1_Bwr9O^eyhN6O<*$ zEJ%KUkRFH!=ssi+E0`t3>@cC6V7fmmlO#*58Q}!{E$XlgAp;rAFDotf`Fb01IhiJd z76FSC3(%$%TIgEbVMi&jiwy!DMN)c^xMOz4C+G`Qy6wk!0AYckG&Tfn1jHCIcCuKY zX`F#3Xj(#`9BWdw!i;z(7+h^kHi1(8`+2v z-v1e^4nGT)4=EWzD)d1q`d{}*UDiD)cZwyIeESjAh455x9$OSV6bcyu3LDJiM=TLM z5JnU&lsw1+bOsDKKqwLb56PS;R3BggK4XR~K^QHFi`|bM`n~H6$^mj0wqG@Vx&v^D zv`VJceL+?g&<=H*F0={a0ltqAR1aziK08QgAJ7hYn=G^%P!Dct9*anbC&=wTeT5u% zjMnEKM;rNa{C&W_kN@W1KZniZvzu0T4e7i^$9jPYiOc?R%u)5n_m2AeApUWP7@1|h z909}VB8&NuRl&$Jj__~7T^W;49wColI|v2&x`*<|vjHiDHzN)hKNbooL1ef;lDK#` zbRNk!jQS13M80quLPUzyw27m(W?!d=8}d@1PX^T$Wy9|UP#F#`Go+%i6Tlg_tCwTZ z3}qQjXh1)HPy9;3`gJ@}4Dw-F%g-kyP;_Uk>NMiqKkNe>PLS&09 zJfwlhG}{SVQw2EpqI^`9!hJ!(ZIQy2XmBqZGSZJ)eiA+RqDC2F3C+{Qm8ftpyn#Y=WW0D7ROcMy7AW6jZ=P|vO)WjQZx)VAo z>}_G^%0IJ4244whn~67b*#$?pA@VJ|>WPFHHTCvg zyNh#n^hO6;@8Mr#tGG$4i8HqFq*H45L?5~= zGjw?OAh{!gCQ);$Y4fYAVufc)xgk_k(ZmTGT89=-7MOL@#mat;2N7NL?3`e5@)G2j zV{C7+R{5`@(@G+(;&@%@F#gT6g?!R^`ayfKHHb?Uob{ykO*jftG#2CW)GBYADreH? zG-+sETym>;Ar`Y<+a~Th^X0l-e%VBK;J}bm2VA4O_b4$2`&9j8`*iUo`hxl*?+W#c z&>!!bVA$E(N%@>uCC1f`8p*9cZ(->x^qO1-zm)hPY87)Uw3zt}cB%fmE8q=peXHjK z^ipB9;0jil9+4ij626)D_e=elCb-?bex(@dl>d)cBb!BEHIk3^zRNdZ;eZ+GvY-_h zbVe>UzA|v*n#G(ey#23XE3wXRmgQRxXQw;AV5dY+u0^LS!S?6k!DX8O;=*T4i73Mb zrroR6+o|X;1)CKga*HtmK8l|X)DL~lwxB!i?f>h&DZ>*522x%hQ|);2cE)ogeQyc>{BA?{MHzfieJ{4 zZ<_v2VX|>n%&^j=zjcQ5^0@$>wY0*Tuz9>(%$~RzQ4zF1GBQu(TL7=9MATjjKvl(^ ze1XYjFj`zVA+b5=2tj?CR}o7|(LpsG(;jLS+Wi*U&GL0@eyJYz8!b!6LJNN+G9yd? z@nVfEMgZEMKp5@;;IM_0cUY|-R>p4>GURMJ`rt`x>VJ8HvM!j^iay5#`GMok zQ`f4 z-IjZ`qbngB_M%B)Gd1!fnxGhYS>8PSz`oz>hE+`L>__#_aQ2zlC>o z&ek@)?=hqhcP!B9WZV^&2E`1c@tBFAe$ipCt>@7xR@K0`xSz49dXT};=jvFjIM(VN z{A=dv;;4wu_okh3-+OmB4B@uj*wCR)-8w3vg4V62GUD9@q)vas*ct2D~@ zsu@{HDXn%`g@)$!2q%R+9ilsp|4@y zc$g#t&j6@MnP) zh%^P0?wY!slF!Q+^Sgbjtitom@mD(I9s@g@x*+t!cMosYUlQFO=XU#4MA;&E&-KA;O;E4&kuJ+<39V(e9`*w zAuc2Z5-`jU% z2n=?YzZaaj9IjSCDCk<6kP!KPbD{{9Jb6s`aFP{lEAHRH#`)~b(UMFUy^Be$u~3TvU9Z24*l(T8tduS z|G+=tZE%?iqqCl^VZ(LhUGymZ)fHFpMMuaK2fKM~NZ#;vc3L{Wu8vbu#xwSlKlW%N zXnj&va@{y&MdAK}#isdcXMPY?KJsIe;SBL1t(cN-wXV|EY9GnFs1&XRKcUEGZF0$K zRj<{M(Nv*PYhPohcJ{ti$=Yw?mUOZdR^I0;!xyKE!{jzGTR$JRs_f45&mop{oJbOHjfWlN!YBTG4;JL$$ z_nBoM;NM?r?EP68}t#0OvTaKDc;|`MBalEm4UfP2eV{h3B_Ua zf5sisjE#rh@8T&34Ua?XlLK(&<6=d4Mw4+Eh(eOT!f|Jpm?~)&{Q$5^T4P0a)EN^v z_Mhbk<6))>mtrm4yG8IM7*nvjG`MYUreeZbhNfE`+JPARauk=GB{a?F?v3lMhOY(Z z*gUFPWxF*)3}T&II*(@PpN%`xfsraVYHfcJpM0k>V#wG!*Kw{HU$vZWy`pD4)yCL$ zD~NKp_iR-)=$dL{j^pPgHfs9Ff~&7Q%aly} zmi(^Q!>Vd#bH1N?V1{*BGUeUl!9RHLyQue=kYnj+d4ttEs@h#CFLp+nbC*fd3BOO{ zgS%qLqtw>ROMP5p!1OlX$HD4&ujOx?cUdjl$Q=n7lHgSF4X5VhHh2l#x$v3``RABS z#n8;BMQ2f69ONeRHT&VvBB#Gu&i!WewezEnx9DF4+nW``!N+c+X-&ul=fL~=I<7?5 zE+4x?k4jmB#H*;#SkuqPkLc1s{$lE9;u&DnR<-Sy^MMDMnj%HNOfn9BDVo}s1gU1o>?QMfPyG6t z#Ky)J1J;0aL~Sb*8M-T*O|Wooa19e%ug3C1H54ly%Jm9G#4HQCl!gN$-#!0}A7?P#9cUIk&0@j6qoeTp3MS zCO4*Jdi-ClNqzK1V8=h5d}>jNH*F1gu3yg0CNOcgOBH(ps%Np&zl#wR9fS^WlB`s6 zPHuT)T;VC8!M9{5$?-p(9 zvr0%>7idpf(;Sw^_LZx*`(oXB8kbuGOk%jN^8_M2HcnX4dpuDZi3y>eaJv=2FfF;L zcq%vS@b(IMrrb8C57Gg_QjjS4%C)y# zIfhS3X$PJ3nX8bwKrFg<$S|(7tva5pn-h$$MqlAJ5EhPGZcDc%akWE?0fOE`NW7!- zb&T0aH#&cpd$*3y6xs3iS@u1$_c2|X=39ZE0CtguzTrSoQ0%|E0KZ_i{B z)Aap(`5Y?$*_b(w?QUB%a@jO!=;kAxX*&nMuX9vZ@&33sLSu*i?Rs!74B?QhZJSv5=g!e@RfeYVmJ-IayX;iPZBL_StZ?F?)PlVf(JD?;rWzm z*1~ad=Lgd9ouXKv-{(>p9m-A9PcR|%9}uO;u@iV_&p6~&M(#lF=VHmtnfu*0Cl=Pn zW%ya$`Q05y))?cG@hoQtIU@awv;iQnbDkIdUQ+Pt)= zusnGA^zmko&C?JEF?#F!lDQSB zREjwPKK7CZDJAGmOX>r*C~n28 ziowgb6h6RBoNEP|yntOibQpGhIVh*0WCOTrhh+^j^D}!-tM~<-)E-IURL;vlVB*}} zqDa#pBvy`e0KV=5E?@pPiD@bqm+Nd7>yJO@7@BmcswShQ3XI}QQf+rdnN4zCiP;K|enV86+{T=$T_IQfb~gPK z$}&{a%iZ}4M-oVB?EnE`T548a8O~+EpGt5T25EFxt%zm%IMiNEc~65IA-HoN>V*6{ z3r7sUp!!w*_~1q0MOfi0omguq=JIDN)_L=hf2t$ib=gK=6@NgX{g7o!ajUeB@my$n zl|X&4J)R0%mGNe{6X{c-9rkZ3o@C};9lgVXnRZIa!h3$w!Dxai|GQD$N(JrFlNdvJ z^WVO?e-cRCiXLh%nzx~UF|OA3BqZpU-gBx?4UGC%D+sPE&>CBpI7akW{Y1mFHyf|d zg!>X@H<(%@%w%tIXfFuP;~G00E{4ir2y7m#x?-r?Y!>l(?@g2+s~c<02~6tTj`It- zMxzfqC^*y*=vncnryb_DH_Ibg62ynmM|2pt9coYR_b5JgbtuNGS@n2jJtj(G`nath zBud|kGP7D741W*j-k}690y2V{E2EPvA_ManuBs&_aP3J9=R4Aaszm0mVG zypkCRZWriHuWRhl*z)|wj%hWVeI2d(&dB0M?X~D!MG;cp>!{V8cH$4utF}104#k>Y zV@5ya8*1=&&rXveJZyh`7(B6390eYqh}06lVOi7K6VNzWjW_ibydCL`h0A1jx~C^> z{3&B^noF6C4{OxE_nObLP6-MGvug{c`H|eV{xf`;D>2k3quYU+{GdzM;lm@l+O3w9 zsCd7alT2)S#9- zPUb>MVo2#-!4pR#(vt&(x#q3%hq5OkgUt%fF!249i8n;V4%q%)mEPg=MZb%;(`*~? z$bK?Li>D;X9du^?bYK05n{ncIpHKX>cs*SFra3E`s1|o7IyqzK)7{DwzuRmLS-hdA;(Db@wm6!P5$fc=F&Dt<3obx`;TjK^`pkQOVCT8ptM#pgr_1q5&Rjktxn^Y z)!{FGo=Km#y2*u2N*hdziAb`>jH`hx5=oB{Hws3H<0arg1o#8;+8hfrj88aHipioqNc|7CZ-IsrgrAe7KBVJoXqSj|Kp5_kcF9x ziItI`9|87%cITdP?gQRe)=NsnNE(1l2ucc0b&msuW+eD#_MHx+`$~|>$1IZd0Q~kr~AP0 zjj1}j`YIL=XCza>R`G54=z!@jD5g$<6ef|`;*o^X>zHWqyR1QrIX~U+1iU z4!$tPTRN;Ir8&RHNb0V{wF&l08Xn-zv=}^}fH#qHv$-8er|R`Du8p1iu{8+8ivrX0 z+OF!fnnAHgG62yE@jt(W}{(x@y5 zK1_qscd9pbH2&U4Nt&}U*l4qPM^c5FN155YgHv$O>-KxyMn&>{u-|UbMVy_9SI~K7 z)X-f-{+!g%Xew=}VtYfF_d|W@434sOvEPbihuNb-{m@rws=g`LZJXID>M|_vDs#E1 zZL~uY@q(@ZdY{b>F5=&L0R^1UZk`p=Qmy;^K~;IW+KelGmYZk?`u}Wvour#XbUA8D z%*6=c+Qht#v;G+2EM#P!XQ>v6ykfWixCtR>9P}w{a86u-4W=kyur^0zht~qdtd@%; zgP+YBODl!DhD*&#yVt%4pEiN2dF5RzW<*my_Y_ldE(LmJKb&k1)XFb-g8WlFxMLdE zd}Nu2-eZ%Fj=8amE7~lJ!6Y#aYyDQq+?lHRx|+DgY5PdI?ECsT$5wmhd&2*&FmCYt zbg#bgbe10C7jhD=alx<=lKOF1$UD&ZKI`+$mU>IU{z_0X^i-GZ=b~?fQI|SEYxdPl zrS5R2aJW$G^EJ}>t|f~%Jo!@ahWsCp>1{~v>4)`ejDQmn&y~Q(_$lIrskScD^0%sU{H#^ouaiQbj!hn-Ro~vN zM?t(Yy-E0Y7P^Opo3Gw~kM^xIUC>3<*&MzqUSEhbU)kt~B?1XGS{ zHD^SBm%^5Q_gX7o2RRG&bMm3d^>%$n2Tj^;AXYv&&1Sd8kgW|^NyO_R?IPAf!BQuF zSHoOUQJqU`GrxLEhwNi^^vwNsdCJ)|)^oT3RgUoaw^j37%@i?-dp4)x9l2Dp-Kqcd&Q3Emn1bvh(dW!V5wDwbiEd54ehwqSzbdgjg3q|V*qv@iOQ{+lNytlS zOUQ_kyL9~@#i9D3Kk2&cS|%kxs}&0cGdc2=Dd!Qiiv)fi8}3p%XxxxD(W#e_N+rvg z+|ADL!P`7;ciFsG3+TLxj}KpJBYGf`I6{KPlSUu$)ur1kpS z@!ny@*rZYazAP^LJzW`$_; zo%-U752C)Qgs!^VTYL1RL3LgOu+ihkq*#cbtVl>#sb{Lh+57xyL)OCX3XBad0f#MV zI$(KNUO>QF+B`N}l`YS+D2QoGbFP#GR(;e;V$s=*sLHb_rY7f5^^ZEIST6-Cu%JXT zwf;6MQ)qCtK_~NYdiFs#!;(MY-a&1vAa+U!X>>vp%qt}CI`IVa3nlzAq(*bR~nJGRSM}y^uM~tc7yo6Z~47T zgpXV4>4(v)I4FD?sE^d09;t63b=A8B(fGg04ePobOqO>EsNM^D^`(7-u7B0sd7us% zMdYk0okw6U=JlW?sc#rqF)7*ROh$hIvn$(^(= zk(&W?ZzQo1igAoiAGLrSN30zT?cDZ4Yo!k7Cr(eB9VmQNCvpy#x2D66!)r8+Dd}5_ zI@R93!5kYUM}eT5VRubfq7=6qXeS|ccG=n$AiJX!IY+h^sQF4Yr$D4GJK@|!$-*8! zv*zBko?FMmBVXI961+35#WR~IFI10*2et9`Q8Xprke@X6wXwHQ z&urIcO1?keVV_OExxp}d82kyK1R#8o-$CG*GH)?xzzRW64HrPxf3(9NuZ&oPvA~@= zB^UFKjs`&GAT&cv=beiTx1rEE9xAh+hdSNWXiPrF8(q8Z-kqRPsqi z4d_LZHPYcpfbv+Q7Ia$xZGur4dIf+s*(eSjuD~L0*Mv$w-Y5Wl0x+HelnNK8!pSdA zG0H-B0nAG?sfTj`#*>Yb(OC*oiNvDN7XTTQ1I|>!0NDhfba)9sHqNLPJt04pasYyA zP=-l9yao^%XH<`#mtUL+6b^T!8qLR|8u&pK2uPG>(g{b)$08q4r1~qVL@ZW=9#nuu zIv_|Do{vQ+mW%#Ta7-ZoR6)JS9W^`I=_a^j^d``*%P%7WVU1+M#lwBUGPwD|*s?Ma{UsRvO14{sJ zvVjzUH_^Zm;5=cMg^E9EcM9N5JTL_CCLIU?yi0{6%Nk^}ez6nQXkhzIRXOO-LM4ytm$w2qYcUKEH&v(}fFUo)JKv$5y z`GX!KbCZhRR`8sO9wU7dir$v*t`Q!R@2(gQo!_h<{+7Q?C6U{F=Y?mAM@C+q^K=;L;csOaN&g{kOccfF}56O8K6kE9)<(eVHobOV|I zn|PxDS!;_1oiX=k~Mj;QvG5e_f6}kItrRbNj__kF}lK#=ZmFkg+wylqNiSd!BZiHe}t= zzmkK$87{};;4zQkr8DtIu)wT}<}F3fh_ThCwWKSExRWf{W~k!sQF2t{?wO~hfj>pZ zsH&m>W1-3MPEw>kq{T@VOf$sPwXwy)$NQ36kjJDzNYM$=yRdX(%Yz(oX*T2qvTML&vbz<}PRDH8K3l!Egi#T-iXN(@%qA(a1C$BA#z9`XxW!n{?c zWD@x9xdrb@W{?t}_zF5nUTJ20f%YVq zmO1vsm(Z1C9P0YmC3=4u8^Ta2rne34ln3*9_0u&!fu}{$wB+3T~>5l*l zhMBj-C;XYVeeaNCTVPY@W?G7lf<$Qq+)Q#8NCmnWBckw$Mw`)}Uiopsi?=SUhsoi+xFwc9>mm`@b+d z;?}rhDBxLGCCRF^GaZ{NLc{GZU>VgFHIOE(5m{I0Cw9qkw%I7d>6T+FvC%y?KN zrV}K17P<&{U{kqL0WP4ZgCC=pc7|J!&A-S5oPE(Y{a+;<11jkd6Z6<7f zODa)eNh;HL#gX^3%<9UdWn0!BVjim)`gLX|Tn{wsX5BovNi&~pY~ zksk^N9m70P4vEY;)Vsu9#jAl@=20JyP~wqw%JEN zIZdjb@as9;kp#bfyn}cnZMj{4{9H!zhVush()X{5d-^%652g>24gR+M1^xIdcb>ruzdQa7| zRy)I*V>`n)y=0dGoO?EO7WgCKm*#U;4c$FI!Uy{ORc>~D=DtsFWA|Hg#mZZ%ZKoT@ z=_ld^A1p5zM4Q{ro#8gow$K^s75HMo=3B*B`;+EcJ5K=pw%+p<=%x53;=XMN1DG?I z;Wqsk)g}*JS%;g(qPu-l`GZ|u`Q0YeJ4W-*X2@pj=8^cL4is zY|~w!m47>7ZFR#Mzjfkiw%c%<;hWK$A)2w8e>~8OdSRwgcusLB+-8|)U2WKOj(AnR zU|gkHHM#|9o$F3TbeR8H6)Cc}M#{v{`lZ!mYdc(l7YiqCz0uljtewa1ZR5cy$Jk5bLAy8CRZ|9yO^b1h zeT$VX932APS|e+Jvb^4ECu_S32W$KNM**z%=!EV@TO-4RiArFQb@D21l8kVKNW~gP z2nHJ+eC8iSdde&Ut4(%+d6ji!tMx_XQid$dQkqJH3A!x2^g3Ht3$>fb>2X^YVYY#V z-0x2`&zpKhi?w=1)tkiWd0S=Sgm8+#9GjZuhE)Rpk62G_I` zafdR>ctCKpxoNb+9g;VXz^o@eeNX3cw8!(?Vwgu2`_kF9tlly5g6jdQqJoTU;OfRy zOHxjo+m^J~t<%UAnO|J_8H(&>rv6Agu2d)K0@+|PtD#oGV8KEE2eg&(1Nn;+^##41 zn}@FS049BBT*>+n*B)Z5m2eu~`ay*X0s2>-PN8{*es>|{piA2Fyo2G~ewv}L8fTld zH&oI1k6)9pb<>e)3z-?&>UkgX!kkL)fUsnKr#rJ`en%b7j#6%*-9|cpt(i`mL!`TQINe5Ovl#gRGc3ydl;m?SN7&BUC7`f_HAku&udj=gpIK`8~q`iY*Q z3{zXsi(e%;ojRo8y)>OE4McRE$<~$_&zgBgIW1!bwzDgbcMYFSzrG51uFp7L<+4yV zMxhpS%AqGgI(@khVs0aZM0vqrt zD3Aj(41Me0DF|{5eh=jspeKkJi(mrkn?n%`^9;fe3z`pZ=?^Ul2>{`Ug(n52Cqy|0 zB?Xc123-zt?}pLMwx;Eeb?-5C4TcI1en;<{{#4F$_;6H$fzeIq9 zzixM5w@7zTH+c6)w@Wl$SvVnGUIs7tU*h)WPV=tj^c&@M+nlPzb zwow~9W&DI%9LF#)lQ*NYA=1t-z#y{c=l>U*9s&5lkj@XVVzP!zu4Qk`sOVOwxv#a5 z-lva52cO;x-x~E*ktfi#cVQ*c$DtYd|!{ zG(HQBcK-Qpj;m@c=8P{k@<1t7G0h#SwusMxhG_tw9}^i0NU_=z*yZz7^PQwQ+B?lN zJEW!~o4?u2o=vIQ+J?{zQD7<))lcENfA+fVFRspi|@*CHv2A+^7Qn+miIYqI3@2&&_=+s zPt*t(cc|noH5GKthx>53=$+`B_=h^@{80 z*V|(+#$2X&<{i#j^6TDR`SJejM{xLJ#U3*8qAuRFCzm~{V`Kf~wn5X@r7TfL&X0gO zyjCO4v)u9=<}(u)zdknJDo`!jnCI2S4VU3{E$4HWd)C%v;3+y`dKwYxNX=4<${rwR zk`ginuOiTG(_BDWL-c;xrh9Nu$h#-f96&i9!V?+hpodX^hQ8KOQd02g1&zaAHp!&R zYcEmaz+!h)0KLW6T(rI2_J%4A`*;)R_So6BbhChOF4g1T!Ekl4HH9WC+pw_~SPK5} z)CjMF@K0OCyrh^Q(wYi^dT@fyqF)zBaszEy8F?8^kR`zP!@8Meu7rq5j|m42;WO)H zv|EoW2nwDOw-TOq^yVp2omGD(r>AcLG#EQPoszHbXWZdw1hiRk0wPYL%jfI~_&s*X zqv$`Os{9tT+rOqfrDkSGV+tIl;z6Yx*iO(f=UkNqz@vZS=B+eTd_5-8k_wImJW!j=EX~xOL%o&|co0VVTCvaIdHv4kx?tgP8$PPjju{C)w z_C!^(&WNXviP_?1baS?Kyg`eA)Coj(P@C}`7vpVm>(o4)rRXRcOuDrxx3vmvUT&jx z>)SO+tQ6jSUZ_P0Y}%oea|U5+epIN{{Ti4+Y3cAw?k*z3L$9Ic&apjMWwZ# zMvvfGQQN-Ew!c1~>&UfR(NDZ$ag-^!tY zqLmqyZZubEJ@Ht^;ms54%`@HSqjufFyCBQHKHt*`fU(NQS6)J1rc}kDCD-~Qpl8JT zOEq)GrW@6k>%6N)A(@SB$!S~k;V_b)D!Cc|gx==l3`-el`y#u$-G=kx1OFK&y+(zM z2@CV+lfSd)uEUp~WWOZ+*NkqVu^210%RHLBS~hY}BM~V? z`CRd#J2jeu<0~~aDQP$pFQ%LU>F6(egO0n<++ccV_|v*Md$i<~iv753)$;XqYst|h zf#dewXH-f=*Fb0G8?+C}tNOS+YRB5lQqt1c#z0$;1D`GAVS9U)m|2`-Z5H^i+jv?l zPk9l~x;)J-+7xBl!_8g@T3unOFk}1Nx_nXi9$#;V7RRBge?8YX5v$k5ACxT&pELnDZ1$2bPrIFSrjyI&kio{2?`iQsXkm%bQ*A-5@f= zcGX!O|8logJ3iL?-}U8+B*;9z;l$_F<%;E_(i-iIc`;#DS;?d!W1YTNLz4NBy=enY z2UIK^iuQ|Jju&Y39|(A0jG}aj@lA_IMx>-pmBxG;Mv^iQN1W3ZP9DpBNx~1U*Em>a z{yZpD@Hv@v-3RQwdEM5s;IWzQnOFMm1*T+icpDi?Bax5`MMX4uZxpo?K1}ELCEPfh zdCDKWqlnGXz~Uv3`}V9W0F)EmKKj6@LM~;gko{cL);bhAaNM?WMQ@?&-FNBmM(MIp zQDCG+NhuvT`=Nm%VWBW&Pj^=}GjOe2m_h}FI5x1r9&uyqd3%)d)SyHBw4~&CJ$zkn zf}8BTb>oQEW;?HUh!1yJM)@BX9I#jn5_{rd81&<{K<)YUnO*!}45ybCGt|a|4u7M! ze84TQlt2`uUB$s$WF}^MJ7}0hP!jkwI#!QmWzbPv=abVOj$&*Cc%V9Oi(2Kf!5elupW(ELkw1pm>#%&wp7$hr1TvPg!vXRPQyI9Y425$j5K_?;IT5`N& z5wwEBV#sTyEq*jy&;fxkN*7KvX(BB@c_t#kDO3U(;=frVErH_p6kWEo35Of}^X8dR<58=i z1k;)To4=_^k~*W^Zys#ys`vst$G<%>3;Ro2cJj%B)iu}VO=tK3zzY-zGCJgkX;`vv z@`?Cl4Y(?+8CdZrk+4Pr_j}u~tyRPzD4H^5=1eJ95uuIhI2IXz56NNjV|KcAeGxf% zAve7FLGdQ+f zkl9b|pnJSq5G6)CvFHWP6KTvi$>>{TrI5M&HcZI6)WfZcjPIf{Qf;Z0gywbO62dmI zQSWerPU80K*BPf|zp9oPo}bVR_5*pu#zB42%e#~(Gy?g+m4fTj$jwvelq~aU^QBl3 z&t@^lG2P|*y-1Uh1~(J?fNgNp?IHO;XL1p5X65DMa&uPj_4oetu3XYa{OZ=`ebb^%lvxi65 zJ+albvkAGdy)ikpnzC~82Y%~7Rr_l03V~!3C|-;RirBQY6r-B_{V>k1DldXA84mR` zL#B?DghZ%AWBP)9{D?fH*Eav$y6BDD3h$WTP&+OW^gONv3nzM3GYT%RfV_?Wi>$AJ zsVru<9frZ(-3J-mT?ZT7-Q67y4uiY9yTjmqaCdiiw}boT-uM2Oo4lmS*M8k?cXyMW zHeKIZdy*hd_;ripxTM3AFlhXg#H8%x1bnQN%!K6ZB=jMuaJZmwq@uM`#7wbIsitOBE%&*)IQ$6lQ%nhep zyp)EW{ZGoDEf>MYPKKt;n&#U}ZVTxKR*qV0FHO&ucy+ka z1w&dx_4=*<)@Q1zYD-OIwECVv`T3w51RE>ONQ;H~bqk}Ef2l2o`=>5RXRvabtmX=* zkz6=VsKv5!>HpwyY_V-G6NqxU3^($*=0c8+kaGCVX4)_YGmG8%oYZ^jA*Q$!q+o~b zAHABcyOv*ZpGw0kuAHtmHqLF?g*zccJm8?gjG&Q`!IXaU|6%-jLXk^VYz^QTIW=|N zhzXRBit{J^QjHW6#ii10NY3BIqK?vg68$AwuIwGFDODAtC{-n=mlJ-K=46)8krG2R z{ouX!0TBN%oBX2eVlkV{Ol!9S$RVKG#ko_^J=tb@0bXQ?V8DkE`IRs%(n63j)L9MQ zsD$c&@QF{md58OPC>nW7{>v8Fn)dbLHSsl2P}spm{A4j$4|E`NyIdCCySYz**&CFJ z&BE`(9f#Y$x*zdrY^*VVdqSCQ$~2e!q-}(dG>Ov{UYbyU6Um4wD;^B4XgARKZCfauO!PL2>$dGy8h-bSL+0DS&_` z_cP6H>tf9rC)`b1A=r$-bz>qu7QUg|mEi;07Wm@j3e@bTv_a&O{f z#6LLcDh|=yR2!glkg25UWbPMAh&Dv#qYB54!0n20Y_7{gwMSfTz?jM4W1th%40)uF zg`-E1G{+2uRFg`8W6@^k=x8sMDaB}~o2U9v?;G`V`1JX21P{;K*vsU&Gry4YCqKj{TY zs!QTOh5zUM|1zJ6h2HKzKE}inkFkn{G)WLXUW7l6vOtW;U1oG835CCBBRR8E{L-3) z-cn}7Qvuiw}!z5vtM_+nW zPn17Oqtby7=gH}I*H?;6!)YlL_~X}6C6+smBBT%M=erX&=k*k+on$OifRTLpuomj5 zlKyF~lgK74SLCaq9f-s9yk&_ zYLYHYTD_t%W(8OTrDrvMy`)%ueRA{9#Q!)<&FKDDy%#QCX(!FslSDr-28Br?10s%> zhMt>m14*D!-j?7MXlaV<*k(J|hJ(oY$02gRSxI)sNI*~~Hww}$iiX~j-sd~>HK(R?S%7$&Z`XHYqQ7S48i+{Qdb{qHK3@AC;U$8efxDyK^YzkNjX=NcAnb{zn@VP zoYFQ7`@&`U{$oH|1(!Uy(Kg*~y^Ua*U7DOJ5fr>coz(j>QW;*>vKIQ53ekx!|Tmg=#DKZyB(t@2oJ*?V}kp68cn2r^p z@wvquUZt`T>5Iv@O+NZ@AxsO|cs2+6(t}RZndBR^_r@&6Z(N zV19;;deT!&diZcu!15o3&Fb zQcrNEC}=T!37@FC7@y;;wbyAkTL8v$=Vo_5o3Mz9Z$Hc{(duO}*Vl}Yh*B$1FXW}L z^~jQjP|%&>dGa&E%}P#e-8UmtD=NK(BnGfzu29`84GJf=z#KsT{e>hc3s3|T)#x}v zhxWybc$Nj_V1kG{>9f>FALUt2df&yiDsr5+aqp!1UwPtU#D=GYSzRY!Zvb8@M{x&@ zf952T7irNhv@TkldFfk3&kx&qbF8}$e)rO#?m35^<09V(oej{qrG$>?hEwsU=N0Rq zouKHM?1hVp!cF-04t*(eO^|#xkop0r-G+-59A3;{mGC&a?C|&C8>m)EI>~GAf3lw; zv)s?f-p*yu0zPh8uJFK`ft8CC7LjY|RyI#?=qpdDSg$|JKCHV@~ zWib5RX89AM?R5P{CM{QoJ9+gSf)4#IvuT!{N3-rpm`MKL8#OGRu`jUb_}!23Z^M@J z@B7H#(ck4R3pCM@LX%Fe_&SBXV4A~K!(I6e%m^3esB}FB@OL>PTaxZ|v*c*@@_|sh zk2FS^ESG`2!rIr0nU}eEK{2X1)#Y$wEkT-8gcdGFqB&bk{gX3>j}PJzA8Ufz87y2` zvbU`AA=oda(T{oAOGguLT7Mc{!abt^JU0W^OOPU@NbH=aQN21}GP{70jS>fgJa1yRU_p3M=5IFIO9cR8Rzw+A`( z2!6O$^UHNxev~sl!|JvH9Uj3pitIH0wkYvoj-Y5tJ+1MjvnhA&iy0Vp&`(~HpO+zX zmq;%;mbwT(I1uwKFLL-(g$0ryww9|=^Q!OzIP+Seu3>v~bo!zTBlFsV;PUja@8Qz^n4T@}5kDSY9 z5vY~g3)&?nAKd=q6UF*S4k2(P5~BJodbNT;o}Kg1Y*Iq2xGiCp&>S}8-;_1{OHUk-x({6a>OmPno$xs1V$*Xma*)#5L4!`50}`BoyTF zsv)&3rjS*3rRP~~x*Z$sf<3Cr&HGzS>*JgZKa=vCe!Tw6|7iK}dV?KZx;xPzVWlI% zyUBLXDJoT9a{xLc&T=PKl-x*}Po_7qv4+4_E*&b$;$FwW^=q?n+2*w72UnvkRpTms zR!R;-CKbwby-UU|<{bh*)6~}lDMjd-zT&}pNxyzdZjAO%&3kK@ z@+|FY!gpTD{PXreJzU_8K=>l7`Fk5tp#Dz4=~J%*R$0CGr9C66b#!@Ak3AJOK-9ZO z&`)#su|if)%8E6gy933pW#jDEpNc#7pbi2{n_)cjV|e!kOEoh_OqKT@%p5HjDB&!F z&EE%nShDisj*beQr_Rs9RhC6G+lXz|J+m4;i+?q7ZbhCr5vn>vPvy=_xUf~tp3>j= zzeMfBQ4}ab1Ipv{Y0n?ZwKovemMaFo6WXaqHF+$os32H2PF)~~0F@R(mnuBg(t(6A zO9d~DiVJ&DEq$HHIfPL=H)1;czJxf#93=g@RS#|YM5R?YMDVZUDjsQQkcHE z?ghr`xE$mJ^Gdp&ud9}U3wL8s^F=Qh_Jo<|#u|!dd8Fi6*7D6}Rejxkz3Lw$^&tWl zX9pcq7tQ*Sn$oVY+|CE11ATfZLrw+m7!6qK0{~TOrYXXR!0`J=mI+N>O=mqOJ8HLtAJLdYk!-Y7ehCWbT@# z+`Jkjs|~7M(bHa&yWlmi87jciyhMF?J6SsHI|X*MS^6{d^UxH*dpDYs_-!jslhK;j zCI$bue{$j#Qyb~I8c>L*a4qjlckezI0bqBDyj$irT0cXLkV# z=+007Zi0Y>ITgg%wm4(0wN%dpC`w{`pm?om!tISrPUvhZw+l4X1?^oK+rx?gPD)$c zbtxZsYhzpQ)cxXlh|z`y?oLjWRbEazRzEDQJgWi|-e*fE>;S?o(iRjg`^oh|a~f?0 z>wl0LjLQ%$3fkzUdp>}d`fb=&(zx8Uquq}km^LP3h4%*phA97cSy^h>KT4$8Us0Ho#x$5$);z7n_0L9LSL%x&jENoaMpvlF;%$ zGy+KJz_zS2$yD=HB%-iB`bdbTw6!lB<*TbdSCu_i9#ImJ8=?r{#{c z73IIGvXxX-Sd_B-E-Wo?w5xlwLbKTC%e@3=R8-vguB-vA^$n;i@i;0ADhRirh4AuV z+)XnGfM)(;5+`1{g+9Gjrk(D!UVn~A^MXZqPB5NW^N^0(B?b?t{>eP$EsnpdQ}eIt7Y69a)q?n7Y@nrFWu z3xAS`t#gkcF~<`b{}dR0T6!h!BmBgJ_VVUKw)IEr_?cLfXO39t_09(WP2&v%?2r4O z+kq{u%r}+dN*B(biE+5HYW#Fj`|WaSKHcBqeV?2U=H?_mx=%*UjP2c5oZCPe8dNdp zPkJh;f3lm|_DKC$v1bc07}e7tjtctSXmbo>>11OkgjRN_wEm*bxm`i748YVK<(~gZ zB#1#JSLYuiZs!pKAdg0it%Ol2L?5tuXe1t|@L`$UDm)k_rUo6{L#{OaSh{N)Xrg`8DN1!k)cs%fOJDI9X|l9N@=c_h?jBByEoYLUOtbXn2D&4n&Xhw zld*uHXcw*1lA%y?9*8HGQWa5>Rt!HbUZWoYIOz#&P$U>PSN5jxsnnI+(;w^n?RZ%K zxA~8LsNsiFx#+F{k8AIwA~_e^X4)}bl82N`f+Ex%;yJ-`!cszWxuXhs&ZOd*;+bIu znW*Ekv6oDldSrF@FVzI&#!$p|g81_q=d*U_!fSd0X~#k$Tu1X^v$=;uqHCh-F3k?l zM=2Uzjhp;EQSo#Y$$FZn^i>e4Z=*E1Fd^rBMaQf$*Tn4FqyMIEy{KgUOJkbp~X@tA`3;JE$niGVtx>W;ySm$l1)8RIAXL=`K4hb!~qWc|l=pmq%3Q z8131?OiZw~6y>rAAv!1&X8a+IK`x6CZl*S+WFX`%lLi}c#EHUTfd7h%9ocO@5VLA} z?5|~j;c?M-{SydH^c6_wz`vj0skVE6@P#61-V55HyhaE9=n9Ak?Ek|FYu+D(2_p* zstG~!|s;sGx3b{+vnS5Z!I>?$%+0t{ulF_K8)o)jEPbRmSpEQk)*a<3$Nh8?ixF(Ni1(zn!Wr(Pp`I@Mn3UJ_fiek&Y_R5{LUdu9VB1DqY&=5{*c zGBRt@JG3S=0NOu_bCPw*C)%B}Ks`rk9AVD18vmM}y~oBvT4G>2qvWz^OQb#94tFm? zU;G!i||Q#rJrDS6neYZLv$_^jqt1fXZS!e z7&w>`)*b1 zcU95$^gCX?6uqjw-wYD%;{=W$*4kj~NVSDp((MThkaoZWwP2djE{V@=Zauu3bOPI9 z9??35-Jqdh))3Kre!C@JR|gJpuF3=gdhdu)J?VFldkq5J10e!M1C^jlp!%}^cKFw% zPvh(!q@b~*dO%lGz@s#w>58|c+T-j<_Ob;&B0mIfe3ZU}^*(YIr$gVu;60HR(UWNl zFAG00?kEK^pmY-UbR@K;Tz}gU>g@~6K_(=BW8Q^h<@r1P`oP4d|F=_d z=q_U3L8gh{!^o6{2dIc7_+qKz>VoPboKZEAHR&~e_WXHJwNw*w*c94-zE&#Go@lWsjPHXU%_?5QQ-|VK*we5$^gW7l z4Pe=EyjhJmr!#iMVfA@x zKyCizz8M7R0FSg|T(|Is2~W63d|G{j$4{)tcQ&s3Q{98rrPRfrY<^Q-=xKO$1yt_+ z>;?Xq&$cO-D95$CagdF9VCj!!>A#Kc8&xuV!Py}O%IHNmZ_`|-nep@2v+m9feD~;E zrTf^L!}P#=vGh8;-gD|9^(qEyumdjKfE-qQgV(!RW>x1F0_CI;o|IW0X+fy`8U1mu zVUn31_*e|!IygKs%GVmyDFS#|CYSi55>J7yq=`2;=F1xLFgf4kqM7+2qB!BG!@Qjh z6K|e9_y617Du*TKs`7IkuOvdHq#w-e@0HY-FwaTs9S&r%vqlo!!HHuQP!Mrs14P{_ zzDe;o>UkgxGJ~Z$2`@*do=f63@vHl@k`FAxSM6lfkZvuGE0!pRozqsTFWYcVIH8{d zFQ$|x(!@_DN%k6Jm1;EU9I$Jhtn01Y1l1g9jM=Nlb?)U@$jG!ahbryE6*CJw0wS+J8!Fn~Xfgvpm!Bn&|*v5osg%A)+U@!z( z111cG4o^i`hkkUoqdcjT_;v2Qzu~~m0{rco=ctGTU^tpoih zxkqQT>ILse=UBIZ@%AI$Q!}btLf)o777d1C;g65?v%5EUqO-M%ftQb|t->R5)zeoZ zuB~n;cu)oWn<=Qt!#B){?Y63&xu#|M^AVKtG}>}|q7mTMgtUa>uijo=sW{xLUH zGfS4t^@5oQK4qictelAEs5s3Nbd_l)e@qzecad486zDd^X%~OoO4fUVciNJT!lM#A zp!o|XpFp2}+ub;Ku=UUl=_tC{(RS% z_w$4)L^!Nl24`uqF?za zvL6^X6%N6r?>;M>QQjI+ip`aM{OJwEa88^Y{7;|=*n|mHTb7!#QqYi!Y zn=-~9c0tebKjw1bb4qaSg?Nbpr{9@gzN1KBT4N}mPeT~q{dWq1c-JnV*O+z|3ORK# zPmcgrZ3%CH?P7RsPKtGuM!j+i4RE)sB}9@ewz|T;l;^%eymy-;P00)0-udiUeK5B^78*x?|G7xl{P-%`S%o9@7jcv~by)-$rFhl))WUUX`$qZDFH zb_2Bl)9OxA*|~(}wL58JpF^v^Q`cROW>15v*Gco2933oau}bda>@L`^`5-@jfq#Z&>lrX#Po3J>;|=~%>YdbU6efbPZH|p8ihX~T!>F31H*d<_ z&D7)5m3OexqC4!wwG%iD?7VV|;hlg#@m`!C4bs~nc{`2nvT%+vZ$1q4w61C)sKiLt zqXCSpHb;Kc3c1Y&^LE&<(~?Sf`fpAB)z}WC5nPXDhu&Hvo{U5IJ%_LxeGMLyyPJ4{ zf_aRxIYVaJ;1H@%616Mac%$OIztytUO|+1sj3*%Hra3Tv$Z+@e4t@`ML}I@leDMO; z)8B@>mkZDW-E+)B%^~ozTqC|`drjMJFZ>{U^<|vu7N|TTDqmbPJM;ATD1q>J&HZ72|hkC0@m9|C(o4cFAs8jc}F?fhTE>VPe^xfh#J*b(Ez^Bap-WA z_cws|tC=q2HKZqBv+&fF_0p{XUiD*04#CWAM{bU@Am8KfSUez1q`fHH5t`Rg3L*_%}oFuF>x5ncHe4wreoH&){D>yh5*?aZf)c-1}ZS zQP{38V9b6TrJO8%MjgD|d+womzb*4g>vgqv%jei_Q{p{oK>7su36ws@ItX4+2XPW3VO?3Nm~<0qYyFOKPUqek47-TfN%rr7!7qo*c- z(0GZs!;)8rp#ejqF)4Cm2eJHC9Zizt&(8dHqz@Ba#rQ8MK9~;f7*ZYDKs{!vE@WHk z7oJN^zUmj1K)`vaGbc&~{(Yzy;JxJHXX+sTcTrjKtPeqFtXFIcM9`;2J5;o?x59!;sk&BiX0p7s{Z#_r32;IeP=F$s`LO=6?}~7 zH1m7iML5j)uptbHr;_aWd~e49EJ^DwY_mq}6|mWFEkd}oSx_5_*@;k6g*1iFHOe3< zn;L0YNg;K5oi9>eRg2aJ{7#0dgv(%+*_=Xw9?KKB6Sx-P7rhf-0|Zxu7NsvOv=%AA zFrqq**)UFx>|~PDj_tGdvv!L(Aw5!v=fMofT%7@H)WtprKgp;@=`xllS55aPga6UN z*#SK~ztleYb-Y)-7YmTKI&P_(-n|NlsT;>JX4h9CiR&@3l}K6y9sP?Xk@-opOtJi@ zMyp(_oT9YCoY;slAY)_Jt$yh|i6T{NoK7)r#q2=>Z34wfB;7bV&1hEi5YZ`wb>*$t zB+a?DWAifp!@P@2B|e&Kmf*m#WKVJk^0~w!yP@JhpkyPq>Gwzc$auN>3dagZ5|}k7 z-oL{7+m&ZYNW1~DUuo8?cvZ%=aLW*q?Cr5SrTaCt)|K&NgS8H$T$Q01zra_1vPaz#QC`_8nEwh?H)0hs7x?6K+{DU`PS%yErtBg$gMO|T>G|HhtW~Cg=gmg>Q zr@>;SDpsSqSe`kxs-D3d51}ECGw{!3iQ1iZ&~Vgzv4=a6sWQQZ8mk6x89|c2J#M2^ zdpNnSND@qIF!`6!ier=ZFXQIgwM2*W*Ynp=x2E(eRf-xm>nO$kOGDRRXe-=JvT6O7 z29H!&wfEK?%xO0Ic!?1jNM{PgH8R!-%t|A=$a)Cc)bk z!)E9fzNCJ?hpZSq2&T8LtizA5TiOPJ9oBHcpB$EQ!Qy-=0P`3#CzZtx1lJn042jpE z#P*5R#l?<+)Cn{D25XoYV-M=Ru=`=v!Qf4UDv>8m(3XE-_fph_*%P*h>iiJ?iX1R} zg3K5#_8+A;Na^NSX1(1yj@W_r^X&xA4e!iO>rlQX{^D-o_HHa zGE)BwvHcy>kYw0Q0W%0rgU2f5ds8f3U~OfDixA0~=>}sooq?1-Z+qGXJlZndIVP1M z%`$B2uxecrW7x)|ZAip2!8u~;_@%LH?|?O(7M#-Z7c?q$^wQ+f5ZD>B0hLliR6``l zv^LD%+q{glf@Hwmp0j~OelC74&KSTqOc<#6VDw-_Wh7uA;OE#BWfK7VK;?;UT6ba1 zqg+S1j3z(u1&HsVZ3?*fE4AWu&Q)6c#F4S9hcGyql8b3{L|keony4@=qUXMW`j-yO-bZ`8?)4 z#vbDp?G=r+PM}_(&vjXESx?{Z=~MJ+P99}oHDjiEd{$*vMg2SpNSv0<$_G<-Kr^P6 zs~%A`fF1J5v>C$0c;YQ_g|^9&3XJKBAd})BYnLFBdRhMB+y8mH@YX?Zk*Zy&oJt=; z()O70uei%c9e&J!Z5{mU+Sm;p&<%IZeIDSt4gQ23y*okpGh)An?#BRTt*A|wh*6WM z$ePU-`Km!Xp#{2=jz~chUg)6p#+Q==^`gcb$pZc$898b-88vFfmUPv&JeHU6|-qZi&{}dvuWNCYUl$H46CiY-=aW=+rl*39MBc$ygvJr3E>c%lO!wi9<53mt$it76^266Ab zq+MT4rPTLr4C3_QB7kJHpnum%kAWs1QtF4fHRG?n%prj$K3{GvgSfBx_)yY1s;lom z_L8=bkiHI+LJceNKk?9kLO#bf-2X}_SVw#+t31wIhvb=gsAH<5#IumPcuYbVQBI6m@*c;pU;8Q`>6aeC>Vg6nK|Wxq6mS zb18BLa|FR z%YC0hF+vf`eS->lTqwKx-+bvkXAC5#^Z{ zfto?_%5OhL^qCR9`->e%s%TF!Ep_)@=?=rY2laiRT|Yv?5Vggy;|a4Dz|@YtKRD0g zmuE7Qy-;U4WwFw5)OfCmNLP|9l>~ov?E06|3N3Dn=bKk9wr;`;F9llGT<6Vx>lxC* zYT|L^kA|8DcpskTWW4dxGsRm3tCL1`x&X`)lX+%~v_`kL%Q-JD;}B=fKTd2qw-$=G zR!r%6Y3W5Pnu1a#T*AxqE+MWtD;K{1GG)Q2^63CQ?;m?M(Ptxbedf{1y zDa&-7sCp50{e(9cISVbCbZAcf_|gv9sUeSbe`tGObgAWI3@2=c-ekifEXt;O?C5^C z>3Wf{hejDDEYmiHRvBh2f0)@wmrY?fG2K5K@)vii(+ccqM&m>3|bPaJFvRR0Jn|`tgA0ns4_#r0Vce`oFv&$Mm@@ zL#7PKEBzO0AQyYN=P~rUA(x>u^jWO^s!F+<`VgN`0loCg2zq&h&yWw@%{5q^0p8cH z^V9;}G1qA4A>j7Nh*MhgPiOM@25mtex&TcYO8b| zi`d-Ztp{%HV6F%A*kfJxc)NW=+CkwRv>kWj)6L=7MsO$lV7432xK?vxwj0g3IRrx3 zjcDDdxm#@pHy>QMd4Awtb@PB^-&{7+TzBlQIo#DYqvVdDo=JRCiFU-FnS3&Hwke;d z-q1gTat^ydEN`>|LsPeKZ=_v6;eFEdP$<5X-)_#{AU~tF_PcJi-q^ePu0Wn|{Jz~p zAn`Y*a$<@W zO96}5M(|aki&%gpyw8c;(+N#8b*+t?*1VHmug{L5EVbbEL5pGrVA|9a8p0Bg; z&tj<&Z`E#9=EC(N8KBi(u(5Dy@z@BpDtr<5px%5ROUpnr99|b+Nn0^oUYA};Z#A4= zm+U36nCxP#qanALf?zDX@~4v)Z;S$jkw%*_YFgL&70x%R3Q}3oZ0ac>Agtb*Q2@kO zk4@G^cnNp@Y9F<&Yxk0Rh~+a0_tJX!64>5!yGwdE_T>^pObpo9fBuJ0x|JY&@amZQ zCg?*Y5Z`wI&86r=D?TE(Z_XtaowT>__RPU0t4D&bg{Nm?>$;nzk`Q`d0ncVy) z=|k5Q=eghPSp6n@nc$nUHAb`_1EOrzBaDSQ_&S^r#)%&=xoc;~QXWi!65m9#le`4!=})xYja_CfSbCZYLlP ze>=ss_G?@En9{SPWYCOHeF>0LJJq)ieC-2SnlpID878t0mkv1&n-6twd7N^dU2i>a zp>D-*DV@69r{7lI2HqBUt+F{7($yx;4($$QZe4Ga-PeFdn`~EUj}uUbWVf{tnDs5> zv=g&*IrkARt1k=OMp=!M8b?)di=t=oE-P(@I*tD{ma6a;?G|OuT+fnS*4m6V8ZR{- ztDqKz&*JWvAv0FiF`G=!nx>MQwxVkh)*8N|>A?ZqYqbG@VZcRA)@F1g%d!JNj3*&$ zCAvAMwtjhPRpa95-1^}b0N@G9n#(qpYbse*y0Ca~_6+8m%Ql&6I$CzRaDVW6iGr-U zn8SVj#vfGxS(J2(lMlK2nA0z1zJ(NOJcEuHhM7-}Ph#aE?Tn$b!>I`rvi}{4S+Ik} zBqaTJ&UnR_yxk@Ed{LuL>uA_E3#(!Pw?z)wXb&Gf~!g}Hc+W0)lCSR~6mcym(C zzcQs+OmMe++kVx@)=XJte7I1CB%A9&zC?_!MsCAzuzh5Q*$r2O-St)5L(d}k#yvQK zzfUjY-2aSBkYW265%yhc-TnyPN=sQt^1GuR5UiABiWMsQW5HLboO3?YzpbYS2@mfI zA&n#<-cN#h7t2YC6J|`>O9H7?Mckz0po@>qFdzkXq~eBw4CJmx#I5c-`)eD?H28N) z-KY}GLYw8Efrqxu0m~`&MnuXz+(h`?G)ri*Q|)H3QEuzqw!HL1iR+ zJ*Gyzhk2S653jzWzOD`G`BR{_c3()UcIg{Io9kn)jmZ z$dpc2hhrP6>S;f-1EE;BujLaY#F6_s~ga6KKj}H z@Em-swSyl!#5>C9c6_FIDXsx_T66|g{y^beg?cg)QWY!cfMB!UBzBO zTb=M6^7Lr#oo|y^9eGC92lOyYBpX^CMAXr)?N9B=>1(||zdKckOaPfVRM@XgODhI7!+Xs*!WJLG76n6NJm79CrszJRD0Tc zbn*kDr?F%GuM|_KdJf-(bknR|@bh$lOy>rnf!sciVvdtgweznFAgkF0S^RprK)sS;nr=*;P4p8c;$#6$VYk5%kvE3yw4c%=IV8Qo;8S6Naik$SaEj3L&Fx^i=4 z%ewU3S{Uo^d^YuXx(h5PY4>jLID8QwmDwu3vQxzZ)n-dh$6WW?9=3cj*||pxw6(&GtmOkam)H(WVP?l*|9m_c_&(LH& z+&O7`A&d-KbH9y)_&Ua8zxRzGFM3S>-evlT-`#6u!5>uWHz3dEs{Pq6kzASFd`LjmB8hyXdffog|$EgbuW(~In3$ZNQ1P5Eo zXN`oO=Tef-`Vnt#s}rD_qd9%5RMEgzOAyCa8&gx9y%Xd{lF3DJ^D~o6bvMcWP(5I& zJI%;jm&hzS%H3$%;bRWbi}Ti0Q+RmU4*#$EUaSN9 zZY0_u182#l)kDb9bU3`<`RH#!!Ukj(;_BHW`EU2nK2(94Z`kO7~z#2bxYouT9`a@D<RzU0xWbJo{UD_9wN4LqmYmv|MV)rkDn`iwU)hN`qW;E`?@E$ekxdbp4%doX)Z) zC;ysam{=)u-3#j_iOlzg;t>rfReG604dG-Y><(cHm0+@Mxzl`BhA5XjOOXQh#WGzl zWrq0E0l9nBfZMG@eff*j`BTh=I2)iL@H^Pu^s6xUU9XtYS_~V75buj&1xb7D;xfF% zahy&N^PtNfVnbhR|LZh_8A3rW$0RCdpMEBIrN4vuEm9A<3uz+{YE_cM}y-W(Q$?fz0 zKpM_5=w6qG`?P5^L7C8OMUHO=62BoDSHIoQ-)c>FpBt$JceqWkoGW<@D)8E%czh?E zCR6PAXR90$>9tBajXYX7!kHKlSc-{LrkWEqGH5;S9#M?3^-Q!lt1`w zZ;)g5G~58$e;YIUQETp6Yw1BsnURZf0}~oB;aWBE=*`AORJH?>N^jJjal4C^@B=pL z8sTfZ!%7Z3s@b~_m8b&*8W`a`1)?f98I_0wX!r2ci>hZ1=+!eU<(&3gcIE1>@5$lb z_3!Khc<*509M5wKZ_`)uZ^J}|PqhMthM9dW=qWNoR5`M=NS6N2=0{u~Nl1Oh4G)%= z^56O}D~W^YG)UmJYJ0IeN5l1((|35kkEz1AyMSFvSh`?_m^CrsJ}6&SIvKGxW@zaA`UOM-XA z1SUCZBKhx?6Z8z9hOdUMYXu-VlM&1WKI&pAFCH`aav9)-=man+`mELAX$-(uYx%Cp z@Pp=#Z@0YaszdmGA-;M5&ZQtUP-&t%%5W>9aG7z&HVCC^k*%yq7pFoAsrr zY)6t6*TcQV9ITomciFAE2CC~Y(3?$il-8rG3R^lpaWIT<>PxYuo@u{6mBz2lec$Na zkPmBg0g!;)jgc<^!zj#2UOQQ)=pjRljw4Ic7P4_&+Okz+-n71K;!vJuvM4!2oQ@+$ z)7H-t$v4%Kmv6z1cEJs@{D!mqCQ^AXt#pt@DQZY5ie-8hjBkVPkJ`5==VaLqS&{db zM)r3Q6$iSQ7QRNDVeR}9vpi9ll6!@+`d~vnVmalnjffXx-g^w#1z8*?d0{ZAm}~?x znSO~bstimhq8hPECeI)?1BQA;F|-E$k+QQsAm!Cm$c=+xzB;k$55gaWkc1F~1wL7Z z(rm>zb5S^h(niTlcrF*JMj5pm%LZN+=d52uC*aNCyu{x%1(BYIsusq!-PGn7b+B5ZmHDbnG$S58 z)V4`AlA1-8vx^a`I2DyDlhk%9HPo6(m7`!Z`cU8f9Hy^KF@ul5f=b|LcDJa#AYOBn_%QLO2N zSi9=fZQOqqDAzXrTDuC@ZA1+Nx}=-hnN}_-4#^r;tJM)TPZy2LUgL@gvH!#*zv$2& zdHngHp?kS%-2VK-!)HMfV2M`BZd6JrtQ?b8Mp&;ja;r3wP`ZC#x{qKHJM>dheB{P5RarAUsj&bvtnu7X$}G>ptZT2Fq`zdkQn3%?%F=f6IMKqB^NPhj9j!%* zXU~gHdq*yfq3l7N?(*@+xpc1KT%2kSy3V?R*d_T&XkAKcWeES~x-GsLl$6b(&+^b^ zB_m6f%`0|2JyDjG(jc_W7Fvxer0O+7Zgn&0!|C)*(Y0|Dr9k9F48>6KMCK$YfZ|Ar zw*J=HJgsC=EV;qTp&0Pv6~W!q(?&7+Jp{(|F6U6}0dc$hXFH!-pYJl>h<#R#cvSX+ z3Oe!R+!sXM4G4+uZIX~CZ$v2KRQA1Iya8b`V*U^6_B$n8$b7B&SUz6r!A@`C+;Z)P zd_w>;wb=8$*9G@g4E5i|mU*g)*5sp2+ujrX?jWTT<;3000HmV=K36XEg93%kfX74d z0#Ie|mf)<`J2kbWW-JHERO~HvrG6rBv{A$ky!5q*ZlaBcgM|%Fugb`9j>fl-nLOZi zS0tm{N*d=7hsLaADX+P*vgkP#H=W!34uA(hh?C6hNuF&^?@WHq(-$sq;zgW&_~6J% zWHV@EKrnY7@g~krWQbW-*rYWurnQt~{`MXJs#IYW8bQoa*_~-rssw0A#gG7kmDfF? zs%AV$$_9lO0GSnZ)3RbRV~C|RqwA@LMCJQ6{GXHm%o(Oa1>OJh3hSjnN0fbcAvlG& zS7U-Vnq%FBo|fNp0azj{T`5WAa-jA9R^4@}QARkfv|ZKU*|S9UCqEvEUi)Gzcf z%qAJZ-+FrD-ZAVxpJYw3A%XI8HR~a0haLKE%;}MYrceC>4rhQ$b|4N6*5rgcE{*cFQSU>J?>ZuTM?vdik7L@{4Oql#zrwjm!C#)|zPE1Os_yANJw1O+%~Z{q?(g}W?{bb)tIjv&IlFseRm0iOg+@3e zcdc0;&>!^_M3MYhO5%fsQdO5k!K3s$xs&@}`Uy24U;S8Uhu=rMy6Iz$oJ!zBsg)#M zN(xm`dR5SMuuC|bQ6;PN5qsGcv_FsLHy?=w-!;DaM04=!j+^3f*<_N8bsv~!6PI8|e^tWt1pE z@^V&_7cPxHKrRhT2&eKILIoOpy9o;WLaUM`<%B^mS_luq zCRIt0lBS>r6n+7z0A;+StSE{ilJ4X2Big0*#4+f_3O|F`NmWv$v?#(-4@EKCSfqftqhGQ%kQQHTO!AXQ10Qm0S{6cU20 zf#qIOZWQwoYOr`hpfzUMHaSLUf3 zU?CPr0!Zj3#SSv17$%)bl2WCR2NV*3%z+tRQZ5wZ5uC926rc{a)iHThcq7Hkh?owJ zm3(1SSR?sNs1y-~m24q1sF`#oR!V^4TyT$zq6W)rT#O&{SfnsG%#~^eBqfP)3@pR} zDUi;jOIc840}4?=EI^3oP$qDL>ZUl%m3k&wN)zK)qL2t=PkMs~Y6)-OCkI5_QO(SX zse~6|N?*sfM6}EA*3<+NQugqnZxq3Trr$x`Kt!J*ObW5^&)4yJz))<06Y@(OFYwS4DSPzLcZyy?Q(n+E zFw|%069r{>^i@0vD1l9IOiqULkTx_(${smHM^PZSheH9zeCQLCz#)(~Z48ShXAd3v zia{W2$^@z-Wse==rPviT6$D)a2YiNr6gUxbck$9d8m#m^^27)^8un$eV$Ad(rjcQC z)a=PaY8dGfri36XQugd2SBhDHDI$mfXzn>AN5K&>b{DS=G{;KcCvS)tqhVhaJHSqt zHLVUCqhx=F5n-e&nhJp)Nb3WKK2V?vnv#P;fOy_R6ckb6OgHiUKs+4nGjiE*Ci40| zu@r1=8Pl>bUx_^q3b5A@JH==?({21dFa<~Zf}AnD@>_k6m=Ns^A7~O_N)1W|YIqMZ zP*j9hzQYWF8aUeL^acUM8t%JIP84qY zt-f1~8+%#Gv>?ouqCR4X24h*ylmj$QS|2imN1-cd$_T0gHe+wc3)K@Aw5v;OSby-U z@EX<~SKP3s@UE!J{a&^7r3g23r_oq7L&J7tDpKA{r>;zdkz3uWznHLUX|51AGu5)c znESD9H;=RmovUD;yosFy$94z>S?SA&2v;e_crL&-WaB%$L=U_U895nvVra62;|6*h z{p`n234S*(-_4hIGl0K<>p%VixBvKkkS7iDIdpJyIc{L{-mR)w)FMJ{2+`DHOy@`o zp&C;FXE28mTV?3_JG3u^!x8NwuZwiFz8*fsn+#{3L9DMA3}i%4oYxTpvWHNq7EsMf zu>)k-gQ+;A^Hs9M>B}2htt@V5CJsunnZrlC*ox|?m}82pHg$5`n<>uEZW|ktm8~lC zIWS)e&ZDuDHrnRlRqSPtjYnDFsSVdpcL}wNjHK#A7Nb%X}g!w|am*8AWaPE1e zU&iBJsu;h{Z8Fg!9EvsHDy;D5?cNO<;1?Pks?^BF!4jm}n65!ze-m0Y+0OMMi? zgC~oW^li*qH;xn8b(qTY8d7#AtT`P~6`aYoWi3_>eV=&vF5|49Pg773WVo^>Wlxbu z)bDUWiCYt#?)9$w<@weMsBf-p7|9L;%&eT5VWvGP+z(MI8k>pquFX3Qi0x>*e@>k) ze^4!pL$Flz)ooPk0F$a-{tc~dC2jTg>GuJYt;Yqe^42MjqLRauwbO-`Bb{ zniRPY@+)~aHjHT0`_XMGK3OUjsd~ZY0F3S-snH1-`@?kL{~gr#LR)Fru|;jk-;T(Wc*l*!5M?4NUql`JSUa>t;S=3GeuB=>%FXK({YGRGw3D4T>o zST&->CR`Tz(LyDdusB%q?HO=8}bLu0*gf3J6YuQ@)cW(74bUdoC2 z5bcijblPpDrAHOm3+X70YDcAv#YRVEfO1I1Jc@EO7PDEFP~is672C7te?Wg#wL_n#GvVC5@+YVV{mVrEBUU^PhI%S}?rRw#>a4x*>QEX1%y5JC z(DW2>7kWz46g6@k8IWQiE?0v%ktZiJnSVE@N_2g-w?2?D8|^N_xx!{80byCTLM(ft zYEfc^88co_|B;J9JTd5l&htqRmv$EZ1_>Pm_>pEGCNVizz& zP6yrgBt8Lk{<`MkPJ4u@TH!=v6l6=^OELaz59W%}N}!$b{mN>lPO*Cw@#S1V?k56e zirMqqujMA|d2U{J6-l@P<6G5-$A0be> zN--mS6e1<9^d6ZnJ4}%wM*)pez$YCtA0L#@Vt!3X_I2U7pNzUt%sy>b3MSE0Sji=5 z7EsAc1!eFi@lkk%P%-CsCdwsfC?^zCS&3;5&%JY`lVyJACrO6SxY1XA*9YfRN1$=_ zey3g6R)W4L*~q9%8j|0j>WuKod5{ze&d$Ds9%VX#o>!H0PO9}~#EUXSk4f8U(7z=i zUyjB->`|?o{KACJ2_Vh!0MV2`&i{V&V=vgGt$@w2KSD)9!Uz5lS%Gx!lH9xL(nXl{ zvQ`*{)<70OQ#-lIve=No`~KT2MM=AjiU8i5=Y>AOYojMz(~B5?9*( zV}+er(=?r0@!EYuIh-yMV{+2zzX-$TMa7)g81$b}{l7|+f83wpFMKO$Ej)<^MvqSA zsbqfv4H%8@#S)j@2rmpL`t0$xMWR)sRijp8_+zbMu6fq&{(r&~PT08wZ)wkYhQt1#laB5#5MY)iMBv^%FeX=}CHpxYy;<6Tandx;Q+ zxH*XZp3CL?KV?TxM|k~$3nRPu4gLH_6HgaU6VF7*>{Y@QcHmX>mAhMsEj96{4uTWH z6TA~rZ^k*wIgTW3Z-F_+M(#$IMxi@#OEF6mOI1r3OIb@BOI=GJOUa|rsfnrTsf#Ji zyc)61-}ird=JnR0tuxRzw(9MS>ire_zw(Ft_ETuB)kN+MZG3B4_AERtnS=6kRhlEg8D&Roe@$Hm$6!P3!#j`Yk!Sn|52=u0*Y@Z5?YKYa8}t zEj6o?=a=Y?NRZ&Q?h9Uz_9q^XprFmP^5Bxu<>*p8fFj?2c?Tx0o?=51Jwhg3-^A6R! zVWYU^l;qhVbE80b#s@m+4Soe)E`m=!K61RY3YB6Uv)-jXGWsnQX13FShaK4E;1aD+ zGMH3{zHp1nDXWfs;h~n3R$WQr*q7&0%{EJ;c^b+)bGt~YB)=i?3PhnRKv70em*y&d zEH8j$uaqLazX$r`8j7l{O>$Lv{wK^$QK4nKCl(M@+GE#@T!E8VPI-^{mRVQ7u+Av+ z<1pwKZynZIMyrU8e8WPsNH0`qyjxIAewsboFmtd7`1{qOD>5Rb&pc|6E-e&dDYqZ&rM2z z0=%+*>nLw!J9XBw)OqeE{f>MgRd7Y{N&Mpvzp_r1u=&LkA~%g})kA`6K1dcz4dYQH zB%!*p;;0;wQC)3wlmSVpt~xz}Kys>Uh>k)b4Wzk9C!*f9r1@bdMvn#n<;{}IMa2_s z@8+BW@QmVVt9MRL&EkohcgMz0=d>@0vwQ5VG2gx9$|Js7B~1@}o%~1h-KsdU?y^cI z~ zG92!`v3ZDjZhH=UE)k^YO5PkUxbc}+g{pL@lg(>FRbS@+<5EIX37#*oPIz>H(n2k` zw8zzN=X{$)s?<;CyqiSr)H3H#ts7RV?n~q544`!@B|^$i^Cc&N{ACJdvnTj&5{L5- zuKwA1s7G@8N*2dIAhj&{Q76WbKP-8&Cn}IemV(g}1xU$7k9h^%>G!^m zN;azs009c}k5{UJSU9{}K@;51+f(=9+fE92c^4~l`%PB(=_m{h4pEyE0 z#w1)y0I8E-|1P3$-x9~7t_@soVZ~A<@?KD130~xW_o+mp19i+w$m}p0=9;RG&h@mo zd6G^WVELUvf1i@*IaJUAz88#K{~2!F{wFZjXvV+RXu{tkELQ!NWHl)q*SIa%XIUH&Js)g;)e1W@AQPJt5k7QuO4~NLU)ihQ6EbSV<^}zuD>zmnvvxG0}0i z=Yr&D7)xLsx_o**)k=Uih8KBCqX-hcb4~hJ37Rlxwg%T4B~y^%}?s zv`v*;R0fIwMNm;jQA9~2tOs3~?wEdEcj006nS)QKQ`JIm{Yn{TMxYI5H&d0EOEyu7 zSYSSD`Ch;WHS%4xbYvQg?TlJEs1z{iT4g2Sj&pMNw_VG~D){^^El;YvG%|4U{RtSU zxUHv7n%!T*uB^l+ub|T19nH}u&Cv$RjC4N)=4E9$6{^~&8{+M`pbYoy!EGZ6KA{i? zGY&H(kyi|#s3|QZG?m3yGM*}98K`qG&^V-wm;HVhv#HQ8hq^?t|E44MN{Wxoq!5-ETV@?JM^o@0iqR&>IP#LLlISox34lhErQBrJ< zeckX1AdEp<`!+3&oDiI)kn_#s8*zV!Br#6uUzg`^%tM!ZXejXM3g+>+rhM9>5xLQ- zbaeP?j*&>oK_d!fxjIVg^f+^$t@-wYAfkYhA5-5g_ta4AKV|T$2N4BN+@}jhE`dBR z1hGTTrILDMfqd`HYHZ)(mDJuch_04WIfEpE{Y*-2Di#>>-t@%+``??MSYXk6gIHgU zqTSZ`1-pg$^k~PW2KA_C_On7|e)C^zUXwg@TTo4%I4-(@k$}~QC)wmOCL6O`-iaf@ zOyv>ZtU6}BML zd>}O)1=aeI^FsaP$d@*q4*le(HOk9CPCFJS^&>ik3&SvCSK4TxF-}Q$pthluXJ=Ss zpxR+ZY9LAWm?h_7^6o#%-pEF{KbQJ0sjZnCJg{_xyS%qQ0U7=%&Iqkp6QVia;%2Gi zf4M>s{V{phgQCCn9%Fs^jKbf7Xck;QVJF(3oA2VIL|uAv_t_erD{DjaJhyq4wv>$1 z@ewg5ctGJT*J^5v0Miv2!XM03V|&SDbdlYZ=wordx4~!bbjjp$k&SJ4nPloyV!A3| z0|%SX`PWn<#3T?j&F2wE9}+2}6#UDir`?4uB#rLyjEpbY(e2lL%~2uf ziX$Hm7hn}I9#}0TaIgRker@A?z_U>OFSaBT{mjD-fjL6?96P7mxi#x$F!513<;)h4 zneg?r$dSsdP?GQKvA9yUbS(keC?v$XJICUeIodw)HbJalUTKcA#PQCCsahuBs(a%j zL0q!1OT_C%eovODZ-|HOp6T;|ruN>z^WI#OXN5w;dVD35 z*JDfcw_|#VQuHF+3r_;ktB8~ef3%qUZ%|SrZ}?MP-cI+uCzxV~$-Me-Za8fo`TA#! z3?bnkq8jO0cTaT=T>~)*Zu6;hc~{Q9LYnARV*AJFkZK7bFYSDjVrx^S7T7j{vSUV zX>6Bva@ndnYss=KC-*H}?F)-vPq;=NXv>N13LWITuE`S?_-Y90U}`mFXTD5kjLmZj zMvAR=OTT<7Yv^Xbphjct*)QpyWM97V?1>(i{nYkkv*s+LngM9DbKz-UY z4!`s?o_MQ;fj5L8_;KQ7>B&h$vd7%*45l%JOT|~AD?W1IR+z^_YT-^aZ1Gtr_`||z z39pWJE1uDwUufj%?W16_`P{h@K@_B{QhZ%4hnb$og>SyF{;UY3Hf}Cg375rIhp1*; zw#e->Zcy%UpZPNPm-staS+aY1Q-;Q51VX6i+essn59Xs(b! zv1a_EcSuJ*u|vlO$*5G!zXqX>+G$f?upnb1%pWcvI2NjBf@9fLhMlixn_wI=P`QreNW zs#Ke>IMl8I1?6_z{Tb%B(|8h3-d#I9uB{=qRKgF3>w!ayakoz#S$?bkc*1uL(Ftvx z*)0Y$1c_xY_OuctJbqdgZIAWEPtB#s{gKJ{)H|H#%%x($cncY zTbkoOF+Z;5M^`!1!;; z|+NCGbr8g46q?5Q(Y(a;* za{jLl;AiI>Q7cYWH)oVOn=u!P8h5zHG9kjPIpwLPq8rlcYk@W5>Tx15eK{NL%o5@` zME7Ie=Fjs_ISd(+E56DZA(dUnueNVyYdl?Qy_&11KV7;GAaZ-l*Vxe$&S>SjUR;zW zw?SSJk(oT>ZA+~lfHuX*h2y`2Uk-M)2JTaWq1{d9oi|Y`E{^9tM5t#Mlc>pnHp*Q0 zv4U!Zl$EHbd+OblAARhOBNRT6sgd22HHfWkv9@{G0}(E4;wfPS@hSKvdSepNqD(mh zd{FS{(SchyaKb96em{3cBsRn!T!Vp5o^jye5j9ZC6YBG}fZ zq}ooaz(8rDlKz!8*{$wH$#1u?9$lr*=WeA>+AMqCdEw&%nIEN1aOhb>(8q4La>6W( z3DqXf*SPXGRIh)f&LFQ~GlnsihD;T|8*SB>c(NUXUjMXcaIYfyLX#G#@IZthtY&)($lk=5#71`_VdhyKbJszAt>S5GcWIoyndCElIP|r8uVxR2 z#LdqxoptZE;k~vzyToe0@(F9bSDjbB66Uuoj+B52?NQ;wtbH=nsE0!uo!8dklmL75 zQDL6`o5}J0NvN<#QNW1sd2=vnOt8Itq}AA?E!lVO>i!}e&qd(nKkh@C!JTmpCp+?z z^2`tqiMd$eSpbQQNdNJVOfmZ_y`R?^AAJfLZ`^e~Aczd6pYi(`{xkX)3{;oHEufl% zOHf7{`epqCr5ySA3aKbCNT!!^k^5XuK;kf4juz6tW`V~p(c*6m|J)}v$0&LI*%^3w zSHJ$arfD#->fC@oVv1^MvQqEX$aAg@vv_jRf>l8u+f%BNUWv*H9P%z8k{hSTgLJ>11?a55rkB+aY`{ zQ(3xHiPz3vt;4Vz&&V0U$x&GX1LX;F6c=>mph!=%_IkB3#!K~LYy+-=9kUL;4LycS zW@w6vnL}$!P`PJC2bbEo?x9^i;Rc=q(L8kHW+Q)KLtsMz(*5-JDL%uytt0MO!lBC} zUdL0ioic0F_mHIMe#7Zf^5iJ{O2H+?CBRBqy5&(J0IvOnqW5}UkY zfvHbRyt8Amdt}|PwqJ^KoriC%b0*l!j?f0Rwwbe%nROwB%x1J_CDO!>y0%$GOP8Rs zDqnN7Vz9#ch->ei-khN-UsJT0VC_}av7okky5&~LZ(vcwF@Fp3Sug8sN!DEbuc8C4 z!Y*i@yVu&G$6D98>AY?QUCWNPvKdvwhw&l}TE}HoAv>iLw7@^5gResrv_Ka0vr4nL3eBdDwDq z`|+^QwwX6t>G}@$jp8G1oyFw6TH0#nca_$!VYW~fNTwss8#A%EvWNoW5dFuYvMhYZ zP23&F1MPe3`={F3zLBZ>?51#BNnpKl*A<87gQ=>-+;4{;d^$|0w1MixGM_@d zW0@ChlIsMBN|`R?k|bZ+Kh&SfoRz%|r@u}fSlpMlw(F@SQxn%ZFz+Vkofyd1pg&Qc zg7OVYS$y(H&!P~$#FkN)37^n5s2s3;$aOUxMsmE-pVsC#tnbB<+CRE-Jb`!T)&wrI zOLV{P(_3%j-$p6RcBL3b={zvMq|B{c?!)rAnHpN>77TY-^W0w<0&tsPJdZ?W!2wgk zQY0^3PXQ$gcw|Zzo@v!v9sSCEOJ#(TXEMM1gjgUx00f7dIz!;F1;mkNGqJUq?Wu%h zS-2yxL52RiK3_rIA@Hq>^8O9~+siA&qLLky<>wDudRR9(^q3Ud+-_ifGd|b2Yfuw* z(+B!IXr>rOYVS}O>|!s#Fl{Az&2M_mf1+yE;jQwxhba@>>0d-ZabmfTE1d1h^aOwIejz7wy zb)}Q}-#%&|@4J;~=BdB0o&D(wzWJ(q=d>?zx%(O^l+U;VSH0AB`@8KnZ`0L{bE8T% z^Wg-aV3YP`q@n;?v$#EMD*U(|gCPLa05Tj6K0*g|bw@5dkJ#knnO|bGc?YKlju<_wdt|n;mO;e_JyA_6JFWd_ z$_L-;&V{c_s}zqt zL$_kVS)o?kZd=MlCOcp6B#U3N6-yjGUy5{H+M`&l4?99DA5)goIAZ%>U4~#R*(p7m zPs&aD^#SQf;N*DpHFt1p@-Q)aaBtc2sTQ&BV4JVO6>|OTobv%MW47G{(nRV6GS9NShiseMx`w@?N3Fm@_K|`M z*hv$DXS32_c?5;REMZ1H-P4N0J69_mCY&s5v@$L?ePI#hOv`28bZ%2b!1a{rBuJI<8VmK7~ z#%(cl{9|n%J~7ko>fGn=tLcpnTJn)B)?+Yt^)>khrEoYgKTSJxSMUF)GcnlYYoj~k zJkaDDmZImx3@+%8u;KfiqNF#PYKi=QG9;z%PfD+)bfnFLfKCEyb79dEcs6pyeWC96 z*6A7e74$NT_59_0`?u#JF*zP>@Cp;yUS|%-t;qJ(v2>YLMFmet#G?6Ok zEoxb`2K83H+UBomdO~4U`VF`%G82%-on^)Ov8b3AIErbx!B=j+YGxqp*6DXZ7h@YV zt`xT7bJY(RZl2%hKd z7m54-IG2rK_ghb72JVcK=yM*(P<Z0Q-xj_p5U=DH^JL%^JdSuwdv=#c(II)${{R-b~!V+o&Y+veU7kt%QEyMYT z3Cp+#T1(!O;``GBz8YI!Sl zm)g3M)Vfp1y3?xk;Xr(x2ghwXzcdO@OurHSB)>=rpE(XWCN6MZ#K}iK52rjsB+XKE=Jhjd3yqmXfC-gxgpD^$IzmKjsG0JHAxix zVP@zjQKrB6E#ri*7TK1k;wxZX@}eYg9=CEVGN*u4;LiV{X;wfEnchL^_^3yzh_Gp`#JqkRa~;0ru!{okmbEpg?!aM?TwJw&dZ$WkoLAL7OA z)_v+ql+}?Za&B|sHn|ddIH&L2j)h)+qMz9@wG-xA7O0gC)Ch&|dthiIKSK|MKl|^K36co*&4uZc zDVEPd%|4H@5aL|9|GeC2)=)=^VoLVL7kIg8N&8YXI=w_73p(w!_A6?XX@qX=U zqhPxo?vf~4L-vcU{B5~cvUN9&buZ0HFAd>e8p3WG`#||Mcz`7Wz!D8$i3+g9033Y) z{6X_}6u8U9zstq2HU4I6OlND%YikT|YfNQp{KeLo)7JR2tufPT-#5rA(RNHB@`j}Y zr+lBn(o=g_d)SfxrShe+_utGN`FF-9*?m_n**&=f-s)69Z)kB(XmNL_`QK3Uo=|hK zmIJXCJdu{1U-rzQ_RPWdRiXA(zwE1m?d<~X?L_U|8Ed!UP7i}F`SA0%I4zJA90zvZ zwtNXqVj3|`wj6aRD6a||iLLmq3_!B?6egNZ3TMEPkZh?BTvLKefGiX%$=X2$A&lN2J)SKR58TYl0@Kl#p>s{=dwo0MRcF&+JGf2Sc!x_TH~ zQv+LOI9K)b_jo)obkogX>yOoq2%0Owj3nL3_WoN)bG?TB?R6fO8LxyszwBwnT3miM z(NCRcO`Ypaovck+!&OautWpTkAhK*iGWfS8{$JGuMit8^`H3JJ{16)aV4BoGn$#eg z)G!*&5E{*3n#Dkx#UPqRB%aK`O!@9{qVDm`yy2?c;q%;KlJ}A~oSi$Ioj0tXJFK5K zyq-JUB9`bQmiQEO`I0;Qk~jP@clcxXi|E$tPs^K6mNz6*drnh(ZMnm{dBZT#M6ysC z;ScgT@ba%XoL&s3_q?X}@TT0T?3o|a$HN-&bh5n7Dxi7xWB&K_cm+e-x+Ln^_#Cha|Jq6K-oR<% z>k$cgG2hFr!JhO!F{spkaTQ)*w9%)pt=3C(vmev>x#0Ev^piO=n@xx|Qkol~d3LP! zVSnYV(E>qQ)3`xmb;wL$9d=+{&tIvCAs{x3?{ZfDrF3|+zUQSF=Zdc>hlu*H%B|wu zdhL^%M?av#M$jSjFl7aPSQO9gvkZKbm8lZXkqm}e!9MCF?s=0IDr|Y!wN{Z8Q8$P= zTD}EQmmsr~xP5HNRbY3S3R1iIusHr`GInjR2mR>ZiGNxPJFbQ$5noF+ABM_hI`uWD zYo#uIm9E3OF&c!1;GXq<1si>S(#s0w;?H^$ztq!$TzlL^CdWQHAAC<3n1+9aeUr~0 zw4@o&LvZQ>M7{j2Fm6yw)Ah6dG5%mkG(ZZ_Vnv#{`m;H3vsuv4vRUfUUbQujIR^dp zhs(8l<_3&$Do|O#%d{Ax%5fGO)p@wCAmv+z2jS^1|OUNZvOo zn*9}UGQg+C0ST@4++V`lh^AL**lt~GeD zS+RZRsc(Yq@b|6knLr%YqRTBLWDPrysJElXuji%bV^2YkNsq_7N+AAS91ww>pF~GF z5`mImU&>G(m66|E%6JkLnqOy0;~l<{UvtD78g69#`zz%Irbnbtzu!(*AEj}^zlJo5 z;s^rcV$UCA*e-pu#yMKwa!Wwc-IL_EC#qW5nYj85iNZJ zJNnn1|CG+~$RgtUX?HlTtNt0hhg5u_^(1K#*c6B!$pUAAhny4yO86RtFSAPGiQIdW zwhmlfRF>r3ROzS9v_z%tA(;xp_9>a-d}}-QPBO?`s9XhE+d11gNsMa(YnjerPoYl< zpI{iU62WcIHYkaZl!R0sbhUJ)l|Z3tnt-iM5!Fb?gp!CQF0On7bW+apRaQwV6iLD> zqL)v?HKW&0!r!N_DWzgi7E%^h%X`kxD3u_{4=oi%05U5_v5S)^hoMAz=aZU6Am+p2 zV#fd}8JIF6i`9Ux`0Hh*N7UZRY}1l=c{};wBT4GMpKNI|k$J{+$=JR9jttBaQPXM? z(YPjMm0xKGl=9QrJhL)QNj_z%D8OWa>eDw}6SBWe2+)nv!;8p@hIWdz0D`t;aRPu@u{Li0GN{XfcC@gl_M zM3}H-OY=Mx1SMno*z2MRqi~H8*1u^)sLuf_v204qv}hNUIqF1hbBX$C>LTp1GyBaU z+P1j5Wvxpz&C|>~xYsC6m;!z1*7OGWHL8r)p`O7yQ9S*ba}w>$8^z7WB}>xheLgz? zYgQplG9{!s*|Ypu^_6KRQ@PGV=^}^q&~P4hS+e*IeB7S$+9+HnpkBFCq3d4 z-G0R#+Ij~%xuV)xp2|Mfh{J@_Wgn}5kraG>#Lbt#V~~l?7fEp@Rfha;iD5HKdessb==?l=3Q|4(kmX9r4 zlPqeKNsccuROp!(~XV9Ach>7)7r*Jo26X_%f+kQu8`* z^(4{NE&@{iyd1&|qlk@?bef!z(UHuwBI;0Zm_@?FXaLMTk~BVE<}(LibPS|cowxm> z)vXDtAz0b8o(|zDqA~QWpvRO&d=6VDN&UH5Me6erTE3)+zk%*^P11+IVMB6Wz`9@W z5!5jKQ!sUXuBY{Uu|*~@1)=JL4sIi%rP~x?E|=(OU^BwG^|8Zq?2eX1Nu9^OS;3{} z*~{g41=f+g=Ge7IR~=3@~EGK=QW!&i+42bypjJ^0mJ>(48-WX%*m!FnmOP2uWwJwDh#Jwe&rDoubSe&Lhav&hyIa&)d)A%9GDa{UPwfA#?-v z9QhoB1jP^Mk>$?h$n^s?70sSZJThGCHgW5ym{1BH95af}=SE&jo8SewDpVbeMj^|OQ=z#rB1FL(|E<6` z(pz#&5s5l{=pbn1vqYWZ&b1R=4R(h+M_EB@6gaBNZTWQ%e~z$1)QDjzJoTFUQ3NRn z9g*Y%|7Ta;qfb-Hxh5imK^t)V=ywWJZK9WO+5dU79{J_Mf`vXkVjlJ8#s$72-%aKw zik1WlA@bw85+1ou5#$EIWh3*Wx^f>8PnC#(5wlVGFVJ4-I&#UC z6;1ucf}{@z!M^jFQqQdrT@0*7&_{xhx_&&8n3B(B4YWsG|L{n0q%oB$Vu-ko2w9(b z3v6lT&Knzw7@7nnf>c4XAVUfZ3Jc0ZibG06$AmIly)L$rLk)A4EI*IYa!`l8w29t1-eLpX83FL(<1m zm?YofsO<=LKAdd7r#kg97yQc~-5c``^9VoJFvuUl`>QMMQCsfiuTG3dxTCP#HW82D z3%Hjf$l?XU%U9P;uId-~ySQy7URU@#-Kk5FxFCNF@6UHgQ@gpsL9eD(rc_6CQ`7&y zCn9p$dC?RXD1^d)Ot@(O-@2zC`U~rQCxT!k#ZybUMM8Z1Z&P;td7KYam>|i|Oy(i` z)W#y8u#7#2WPe}zk5K;n`yUo2mNq$gVzi#6KJqKBbYEa#Bo-Sg z+%nVc+x)Ri+Hs%y+v&^Z)B2@9G~4CGRWrN1j9eCdX*~flO(!R9t}x1?xf^ug^+xXyjKtK zxO$lTf125J^U?hyG0us;I`%1(L(DfA^-8bG_Q{hYb~k*j`a-OR@vQZRC+$U+2O0w1 z|DC)wlL_&KH(1V)TgDvNxp)Ro0;KKUeNzDSgLG@oz3+3ID${4dMK>;Cc46+g zba2|>xN$?@vsyNpaY_d5xatAN0p2Z!dESGhOT|NF>$va4dzwCn&Yzu@ou;p>E@7>+ zzRSM-Yi6C+o#vhP7u>KkpK;;z_t3M9r+0$V|HM33KBPQ^JQO`7dnt4Xb9S*!W^R^q zNR2t)k{yWOCTJU7@XY7DvvMW|mD;Z7BN9$zDdgw*@LYfV8fBWkODYEARJ|0ULPI`5 zf=aI31+y)#^L&XX&BzZ1W!R-L z*o2Y8$uPq4k@|$u!oi=y1Mo0e-#eef!N}oYwD15d%s2LLuj(StdLqxKfu7nT&zbLu z_yYg6z+I!lU0;6K*@wFhfbU~P>|;gfV=Ui&yCBAzXs|r=#OrwbD!G|Y|CjJ5f^Hb@ zUyXb0v#!UDajZweSpKdX6hsRW(%{2I;KpagnsZP54N4IXp@qA&Vz8yR-mGyLqF?zj zvb^wYquOj@lXb?ZVAU~!{G|0`NU@>yV{-8!=ZnrPyL{L5U3>ASwYTW(B`5RZ9a)CK z++9L3E~Kgk44Db0PEQ?YJb)dj--6AH9$0TP__L)PjXtG1Mqy%=-xG&Bz9%ay_d}x4 znZsc_iKf7!ZSVQ=qg3@@!0@f660H$3I2<1;9%T8AFjC^=jB;9w6Ki+}aawJy)SW*Z zFTY4i`LZ6!IIZ(6lO0Gk(^u2a+!(f(3V;H=Gy#?@-|;h|#=^$>4|)%RsyQ|5{<{3gsQ-HFabWEd)4Jfhrf2cG;oIXoqvs%GX8)ZhBXg`o-c9BJ zdMjF7%ek#7U|&S8Kf^3$TKYNBZ7x<1i= z;=@QZ3{0nbr>C)NZ+y$lKpN{xLwet-0CXH+-TK{@KER!yz2xDDYt-7`_@~oajPl*U zrL!K6csX}6#bb;hIOpk6IV6pnx-9=_PNKh|NoUX9irP4=6s{+Z7FG~!?1Mf~kD<>P$yPd*@eGHl4`#(+;RHvXO3SE=at zuzi?KlQ8H{DKHdUpLoXPkgUIFS6|VtUp)FwaBCO)AA(!x88yd#c72Pl6iW)mDD@3i ztC|o)>hR{s{ta8}kLsj~J*g%E?O2e>J};kvLOjks)w{Vt`+t#bF8gwPqmN^QzIT4n zU{6S>4&$>zb&5^l<732a((Ofl#+~)^%T8eZ!A)^0DIrV>e4+?FWU z>37PsM>uJI$q;4-u=tPa9Ue|U)w@HE1&b)NCW}iWckY*ClU-xI>9b{%6#RD6ug9i1 zhVKVV`6Fu@)%ouI*WAa1f4I)o`i2)NzC&K8CCHQ#{>wQ2D^qBA-Uylc8e?b_7KLQ= zmx4Y^w)cMmYsD#*>ryQm{|a}r2wdYLEoy&SjVLbTDo?#ioGwiPiTPKiIs-xK|79_H z>e%8;Tq@Mzi|S46DvTG#?;jx=!q+F*f*}jMHu%$Z zx#~G9CsFoQB$wS9$LnmXmN76yfuRjE@iX44n~4UL%0Vx+v&gYv2sK`gR8e0`^G8S-}%Mcf+*Vpcs0*AtE z`U2tV6RuMm>8-2YI`h;bH+)k4k4@dOW5QE6{Z%f}rcZLP-JF||$pu-5_Bml8g4lp- zwKo>je;AYDqjfY4!u)OOJ97bwk(pZZ=M}1-;%M8`t{8ey=gO$5jOtUqr#mYwVAI$j zCrbC2CcLM`lA}RROok5&>s(pIhwG58ePi=`V~gtC`m#6p7I4(n`j&^)l5pos9rGudb4Oz)TvwXX{(lWxI(xir`QNvn z9B7_7i!#d0?B=}6k5pZuUhj#Nzy$&>ZjW{R`nVR0qWVM?qw8wS(#k%4RdfIM>KzT^ zJ^*f#I}IJIvha&L;NbWVL^rp_?h~C^NT+7<0X?Bpd2@Hd$Jv*E!>A|3a|2(kv~B-8 z_aP3Wjnv}~%SrzD#)`3_>==}7eyGLQ)>G}o1Fd>otVz--|DW<&z+AM!Q1nocfl7x}6|J!YmEr%8&yX_cTCc}7RgedJPW7cEJ<$eyu`Gi&rifQI3YZB8c&q+W7@$7~c)TTw%aeFT)8q9U zOwkTf881{@q=RuqmaQ$iUf3i(5$<qgLz`$b*>LX^V94bz&DfvQS{g-%kNJ(7wqNZ$AXeca!r3oa(z#>nd0MfI`_I28 z^V1==Kl`^%LE@%W~&t3i>KnUz}~d9!11H z_2DYI|97fZ@|sNQ|5rv>mYo=+nRol20#ER_=6@&er~e=B-ZCtXcFP(LB)A24NPyt( z4uJr{-QC^Y9fF5Ia3{EXqm4swXlPs-cXxexp65Mt&Y79_n=>>2zUx{nRxO(1s;ax2 zy7#?nZ+Z1?(9gU5$PJet`~LIoOqxh3zW4A&Rh6a6=*TC-Lj>v0ds)6iUtLj0!kE}9 zS1IVltW47v*Y>pb+MIvpy69zAGAF%WQJ4_@kB=0PqlqFrux}cnga;XfCd3w>4+eSIT`;JzpMW7kFS`MxuEu-~x%7&({_>=1Q~ew$Jg z8UZNcGUu1r89zD6i3iC#?3rBb-`1wUVI*GCUSaK-x@QAyE&uvPHP8_l2`mjp)o0Up zmd>2L&d$^qKC$yb9}#TmTv629in7f{%ISbpyQ-2qBDtg;$FQa;J6|n)x${x%@an+R zFMs{4x=0n=tMj??Md%RkpwKtguRVdi9=_Ms29O+G+`ps6KCxc2-j%-OFccbZ9e>_N zXlLM%c++t$bAxKw$R&SDSk;->ld`CMoJpEIQp#_EQF?-6Zn#o1G?W(H#?xpI<=KI3 ztg25V&*7}0vt)82vO8?>i)d#w;$WNRYD|6Aykxysb|qAZMz{1RM&y|Wf5wy|{ifF9 zATg88uE?E#vxF)!*m3_|Z79caA?!-Ubq;)S9Y=KE4COdmnxCob3c;R|wY<7+*m7yE z*=x4}BGXvTfm^%jXY|IVFBHb&Ex8>n?8vw>8th%U!TeceC-fXUzVFA^Jr;VT9vpjT z9j~z31A)=EbdTD-Os6L)QOOrk-}Kk9zv*wUCWLnhp;v8A_#AU_JL88z%$DF;M9|Tc z^-Z|;3Gk4_69eZ*zOfjg-{9%>%8HWN;a69Sw<(gIy(33B>lerDO+a~s%}<>qI>QM& zm$tL4|H9w*_46a;24$~u4Ap!8sZPW$4f}G5N1C^6ku_bea2u5*m09~y?H{W-KCZlf zT={(_-}^p3y!WTuWazz_ zRjdk%=EVziO$(DZ;rCIxrPxNyCc2{EP+ZJwWKo}XR)5E35qF(iK z%Od*0tDHpKOMtu(VT0v2i*G7jp}vez?~ERm`ba>D zUsjl_kDR7RpzGt*A)l>!o!0LlTtx?w#BrX8G?^L8;BU9%2`mu>c5scvpJzu8OYIHp zs)lPBq8qvP@=yNW4*{IlI%yS4J#nc*>w`;>9rgIACFi6}p!XDUq7P=l%Xq)cq$W`=C{!3zD-w*mgXL+hQd zK0~*a5r?SM)z4RB=+!V~2RN(;wUOW`1; zD|oL31Xw<;qiO&FHS6<;<#Lveh75IY9UGh+jknHuok4&~f^qaV-NDZ|tVZ|Wh?2OS zt}i9&?GG;}G3R3)^PLOp-X@4|${7|JBrh-956hX19*+r7B#Bd!`=*K}^+tF_MVh@daDcbolE-aaG1rCD~r zuR^xY4c0;^FW8<=uYmnpqBO6rqe0GP!d3Dz=d}{U>$TT%u4nbjxQ1RaQ(%dWt_)6k z?AY0y%zBxrRXy*XmsirkST*%mhp8M#R8O5doiEk9skCBy04DB`OgWu@-$HZ-uQ9Fs zrwnm`0E9856?2RtL%{L!lE&u)d_(S;}OfdxTG zkdVo?konY>cIws*2cP`DQeh7JGvFhH^K~zJBDe1$m?6X1ZsBrS+@5)SSq{qG6$njK z8FBzF2vkToI*F(UB1AJ}2VJlH>aYm%Y0A-0Xm(K=$P*5^!q^ zpk85h`@Xt)+j6>A;_<@e(?oFaRj*V7uJ~GJc^tuSUfI+3Z2xw`GYQ+x>lsd?j7C>W_HOjP_CocR;y2A20t^B|0EvKD z$5cnr4a&Z)0fs1T(k1~r`y~kA2cQ-pW&ew-D}DWFck~kVO748yJK1yAyUF{;3(Z@) z&2&YIZAyk&c_h}XkY_r>WIKI$&TwGcf6gKnDpWmJl{h>C7}3;(RcUiL8+mfy83n?;(X}yPH@FC&IoWFb=%z(_M)l@ zY5OJ-_AKlh>UPCbmyge^fgrvsD$1JuS9k#FP;_H{&|NHFXZS%=%|5OpIjGRpjb#x@ zUV5Luv&$ZB4JwPbSKpq&l|d8)mA3tE7+_0yHhW{eP@-npq6ex-Ti~cQQ06tZQ+}~P zGd7;X@(Ko|Fqm7SGH8MI!umd4E?xFu!IHtT~f0}3VB1#Ob%)2Cb9step}l^$ zIMO)jfAjEKiZ_vXKuf+I*eqqg&Cj33wawr`xRT|*g?~x>;S-B0=sN9s!X>BBm_4&# z4?|Ynx&JdrcOSsen9WwJjDDLFERvhf{(Y`KI>k4pJ63WYwg3D1I z3~3#|_6XDV!IyirKKfRr17dBhjVy#fUFBt}vv78X+-YKs4!;4Q;I6DRI-OyWCVuN6 zZF9PW6O;VfZ~lUNStlXE#JKeY561DGC<7|>9wj@-;@z+hlTatx1({yA`~|CDygGp$ z`P3-^!fSgVqhI*Md#kLaqP`XGR}CvOvtH`@aclQp?`Fp*+0~URZmzN?R9^b;wN?*s zEla%t&#LK^U#u1sly&t)S_-`~KFLhF1Qu!mFv@v z`bR+W-kqbo-jZBkgVUM@b+@rphH_^ zG(M$R!ZWXtBw~liQ?E3YN^(}HB)hPY16gskzLT~IAM`YcGdor(>N-Oy2q%R<_Bqw1Cr-4}jaUGa=Qzjh?0gdm3PZ-r3Vg zIqr1Y8hn?8-}NYaq2Zw+d<5#Rap;-sw)E=zUdX{{t3I(0t8O>R>nE*Pec1??NwavM zxbW~hV4(Pq?P(Cf-xeF+lTIM&URf0$v}w7ZVjvFN-rLm5 zX7d?=cGzrMK&3pWcC$%)*Zi`$a6b`cmjuK?PBx*XF%37~GIC1BVVg^BnYS?pSNf{q0S}5UKKhvlxac}nu{aIpu z$afX_t0_y-`@r4O_mbcPT_DBuL?(UvdW-aUwfkODtMquidxb;e+QE+PeNjKV6dAPD zbc5F?NzVsMYq0)ou77Qz7xtX~fu;X4;?vbaJ}|2S&VtVSM$?>lCB+CNG@w9Sx@c+O z)A?9-3k?Sx>cZu{T}FNW7Az#~^L-4Z%?R{as|v1QT88cG4fumC7o2R>r;LCDu z3wH*~`e)?(sls4U>q2}RORuQHCo9Q(Z-eUNt*&%y$&1&&&A@tVG|DdO(u!XCrp!W|i(U4vr?WwKj!#67jRt=FNGYq>LK9s;tPDR9VmP6Z zt`o#uW{4w|l69vt6(63{O|IpK4YjXJXa_}U&isJ*Gx6Mpr))$G?bVhp&>*igng?)i@0$v@R&QuJ^bcq z|Flvw(m@RdQpw<<6e_`^&sCdQ4sO~qi!J>ivw9B6n z#b&z3e7jrLtRaM3i))LeLCP&Bp$kC_%QtB!iKw_$aH^3fv_DmIaz2|ST+e*0lXP>% zE9iL_5=4plWATGa^T(^OOG?ZDW&63+%OgUeJcpWjgOHNZbh2OULNkwuYbqDk3@dic zpF_8cw2o~(E4P}l+@)M1xlFl46s^od%BQ<3zz3)ORfu+WUKh!hWp3h(t$Veu;sXx+(E`)4!W2zncck0{jfz`G!G#;Kn``MiVEYBkI^&Ynl9kJfpS4u- zmhSaR(t_=W?g+kqS1rRL*mi@W{dvuTgVfYX?)=v}5YZLwX-b3p9DyQwRekN?w>ww7 zAqr_cRCOONdNY;L!Pg|QDw^%9M^AQDqMe^;!yHQ8HBZWX5vj4Rd1N48vROML1YaM) z6~qSUCvk9>Q5;O`Hp7?IV#Zfi%ZK_>dCO%-9)jj~9?Vg=xgFL7vb-^lEqTELZl`m` z##L&Qjx_Lmg}pMC{2ccCK_w4`S!r6F4Mu6+TIrU1pnjQjUfZ$yPav?|qM+?;{aaA1 zTsE)o%w+{=*t{A)3tzx&b7YQhBY&xWDP!r14Q@n-^&%z}*F1DgDs{#ee64AIA`g7A z(i73UX@huzF2FnO-`#E8Hs(dmPttsCu4j1SoqIFfoLk25If@*{YT&f^zdv_Y!+`~= zj{<+d(Vn+%j{jce<8rY}ZU*giu-~jL4SAkj^pDbIsB;3J9^3AiuuPshbsC0wNMXh~ zVd^U3IO4EuT1XD@H{tsA!kl|y;xM&A)|sxQW?2F6`Q5RI+DN7k6%yXT|JeH3?mRd5wKpn2V&$S6M06ojj-<&vc_RbdxK( zo0Xg4_HtG|C@HgIEUX`x@s+C5e(&Timf0`UfzK10P)}aGtTnNLRjD2Bj9T`c!~m{R zJ7~0Yg~RN{_Vo*1V27#TF3ZgGNAS|*-nXikn;CJCEeb3`qdK3c@9_T3j@W@Xsuy>LhdSXhVs>j z@AvxOSz>Hu+wvS$8;DX2)EBmWKW*p^lImAZ5SwfVzIB`ZE;FO4A5u2;EiI3v`V6Sj z{-90w(>8zF?kBY84N_c7er2)Behbyd{jWa@wa&8MX$Z#+Rjpso9JsMh{3vbtJ~Qpc z*6HN+q05p=9ZqwAe1X-g%qs3d{b)+{uDy0diH`UR!OC~Wsx)R&8H&8IH0CJ2SPKMB zq|BdK!7t@fmDYG&<|+m@m3AQD%^dkMtu?7>&XH+beNI(XP%wj24a$?PRjn#csj!U1 zNnHM(w$u;1*!`*LV;R=_#L&75!uf$0vk&P6@2v_yNE5!V?|Usf=u9pBZ~;BPo=sHB zaE)ER?0Mo_~1!*K2ZWzNpA?({4fVwplW}7e~_Hsc~=%ShdRv#{u$>ras`aM zZ*=!P_Fr;MYjDdnwAInCPO7YWJZT-H(f{~W2zsw5g^;C(<8(ntF@5IyrFqi8eho6t zTZ{3uI@AEg=bL$pPP|U!7hL*T|M<~&BhX7|gKtO7;%#DIsJ-8OM@vCUfz$Z*81)7X z3g;~eEIkxIv<9LFRyFb|@4=AU+sY4qC~M}UJZ~L@aSl39piSQ1L*GO7*9ks;uny7Z zU*QOFbwM$7)wzMDtoc-VSDt(DIcZmGkf4R@KCXoeDhrm#baa{YP2?L=XhCQnWNz5o zcMe1f$kb3iuc4ye+f_80D^irZG_kP6kIyJ;K`#~-@Wb%7s9tQ=Bwk3n6fY0C2~f&! zZlKKWf$*6y!iyn!wjz_9xz2Aa(kE~=!zYs8R6#S=p&Bx11178OoxEmmDhpO+FNxX1+r|Z;1Cy zs^Y}Yx`_k$NZ(LB;E0UHk=!59R<>BQooIxaXzZA1B(l(!x6sD@wF2!c>56j7H>aOkskZ`j} zY`^P!J3+0!SY`T0Zw3<~1W)ge_HUxX674^3X@89fOeFkhiukKOC{gYE6o!)B7Y2D@ zCAO~wFf_Iw4C$VxfFG~P>OO494tij+rk!~|wSFwZ-dsZBvHT9gb+X&qcslv4M%OX+ zLf~{9$j`MoDl*zHY(`|&_(Rbnyp3+-JEME3Yw-%Z!!^U;sC&#}xJ<4Q3kZJ`1%>uD=?w`KJq$mh1}yg` zuzLlH2=DyQ+K`0_PthZ8cvY|UJ^&S0TxZ!RI6ku;*9Y{_Y)}Yt;gn8CT z1EStc_xYl%-2gGy&w!ZVKL1bUgdg5cAWba3h-tiLAon5Bn)PE6YbVz; zx#14f>iP=I;4`WMDgd6u7gr6+3!neKc`y!X9?4<>``<$m zpS=*8Kp85gB%og zChQ;VOA{EcT4({yKs|bKCY<~o+6n8v-3FseH5ji_XaUt|JsNR)to)g(oPUJs@IZ&w5Fmv@T~kq7WLM-SB^)tu8E zJEusi15EsqN9kx|(8&SUx^ECj>jJA2B>z7^y@xY5Wb%sWd9uGD0k1BZ*^J3>M6Wxg zJ%q&@;d(ll#T(Z68oB=vKYE@GuWr{KjO50KvT;e#zh{nDM8zT}R85iD(%DG6+xiB|)n_%$r z<1Zw3JPc2Qw=}XDZiGTeb_4GoDYh?CcOyu4BZStDSuMP|W);hSMLy2zkk%6Iw;TNu zj8z#hU`c-6>G0>3{J{sPO1IA|TZ0eko1?-jWSfM&B3Ilkgfv25hRA-6D3N=(Lrdg0I1$xiW6OIzzOlZdo1F2mwW zM%ODru;}v!&AW{C?+ob-n&Gi>gVGTh7StkO&t!V+nIu8=?3N(QAA1zCQzIs6 zLBOZ1`S{>0XT6j`_7vK2`^E^2YEj;`2w^ zCU?4j18L?uv7WozcY+b}5D?%^c30>vyrGvsmI@)hxA2)R?~ELyDDdr}9pzX=CgaaZw9ey%={9Q1Mew zqoT3nhkiqT)b1`r?cXx|Py~sOtcTQ|jEA(IB+e{oOlxSdz`Yr}I+Ii&p{Vk2-kqjf zKElSIBw=xD4n%Mid*%a!cox{L1Esf%_$ zolemG(EO#a;}vtoMAnBYGDF(p9s z<1_17as*K3(G=5>BDQ;y8#bZWMQcvcjdu(|)^ul`e?Tzi_55}0wfUcURg8BWwVmV; z%o>)C7eY{%BKA`DZtKSUSMHoet>^hq^g9ELQ>$%V&^LMS118wDNb=E;`NT6`Q8XlS6ln)KUD)DzpGjXx0n$uf2)x3&aaQ%h4&Bp?<< z!IIoK1D->IBvfnp+~`yglZ>D^h)PBn$Psp|SCgBNbbyB&cL(C*1D)MlbZV&+Fuo?@ zgOlB3;zNQ9G4g@Z?@e|>G`{$7awkia- zo*6F%d!Lb>wkcj3;L&=)qivu+62+~v@W<+>#LKVKJYvXi;6Eb1M3}R{0sEb+p>an0 z_}cplzKH0>Ne}x|G8v3jjnzF+9~&p%<2q|zME83{*3MxQ?D<( z2G7+4Mf#L?r88Y4GK^ZWjo{=>=+-jZFx?UBU1~C%D<6N_0UplL3BF+J zT|JM#H;>>(0@*j;TnTD#UD*y0R1=)!ut+7AJKK-DeabqLN+Vu;ABLDvt7?Qdsx(l&^^V-s`}8*vMg4@nS~pdPnrS z{&GI`=Y^M4%)8Ve3g2)nH7@cLAXb}KDm(C(U#3>XiJ~1^r|zdt%)v6ni*Zu<5GZvR z?ECtJ=l~v3K#ij3{J#w$nakplL@b!^IO;@pY8pa+TXpTIaMlS5; zD^e(5+b2^M^1V@v(BzVbA!?+vmT5hc?Vwxl9-)E@AkvOXgir z`<+B~d~!?g#{DsXS~2BNt$1>Sm=7MVho8jP;@SPTPf^@qS>fJ9VK>Q93|j2!+U-&3 zs^a-W#fpW>Vcx0ynRIu$ThZ3tYNHPrGZXJY<7?ko=lm@eBcintZn$a<(>g}$c~uyL z8*cg$BWJQczS>XyP;b%FN1;>9N30u&AOD+ir!BU#4Dv zpDBRf=BIa9xOq4Vkr$uZDN8x^qmu_#?yq=h5vEp6zs_T3AQAx8ch_6GcnF+Zm!_9Bnwb(mt+VL9nV5Ioyk1 zrpSII-KxK^;sMW@@XMq>nakC*s5tE;#+yGDlv*peqC{4TxiI8eqw7ZPYJHjQPPH0S zaV|__o5?8ImfD4+OIm|clI!gnPU-)lnsfn+fp-z0sk zS-TQ()=R4=7hWb->r!+p#?1DTtJ#vfqkvD-w?&~8SI|f_Ur%WyX{bsc(3s%3GczR? zAArOGAQb8sU?!v+=|`-q>9#5V57N1&Tc?~Emu{pF@$b@CJf8^LOwX^j2(iZe(nBeZ z>dNDX-QCWmq@Zq@dx95N$^Rvz@oxg;*U@uq|7$T`PS+!D1(b!%3Z-b?MK9!mOjHgo zYz0Zz`@NSu;s{T8nns&V$0{ahwpax;ixP*gH{(|WR@NuHiM-^K8fwdbnpV749Mz;7 z*3twI)vEn3tjSc?638gxO8GZciSTptP~>M-xWh>(&D14^gfnvBNc8v%D|y1e1<>rw z4(3Gd^e+l6w=z*BFC);L87vi7jx;)aSo3!nNbbQFs^ zaEv-%Jos`P5$i6_>6RV|Aa)NgoD)#gJk(QPC2cW^6Z=`CX=r~KQQg%2-1v%hW}kQA z8}Kr{@wZ9Nm0b{5E6a{ zZx*V)c_Sr8-=>Q$!6Kgfu7JA7A~7EtLB)vlFzTuL-`yO^z_+s>78o+2cw5d%_vjH^ zPgOUqFHLATl8g;dol5#3X7|I~A~W`ypYh1_?>M3}_BU-hqEz-Zoe`^S@cQ7+Zg>Nr zRtI8kxt2oG&)&-PF#xceAa0M$Z5Y^P84M~q5`7gl10q1J7Cz+SgxpO&ulN*LiE1WdTbijX?Ni974+r{bmcCr7u068}mANw%LE^|Uxxf|!4Q zO%X1R*JCqBJzSv2c;!@* zEZwfNzoo>K?A83ASc%(8Y0{RA1c@EL{9!oo(OFr@^`T@@e%WvTTDd8^MRVV*B)z~c>Ax#3m5Z`@pQiVU z(>Q*Bb|oR%O;$p`kM3xXoc zj6%n>e#`%!*m0H_W?>kpCjvSBzXkT4R!XF{1Ko7`A1{-5NbO4n9^wCV)6WIfnOR-G zOp{+9H_hdHvCh~cEUV=$HtyHIb=C4@-wKXkow4Faara;ZF1Gt&+^e`{*ExIDjqmgC zmL||-6r8SwyUF^Nw#vKSlpG7rEElyrJPXOD3(m|I-QTO|XYU^<`}&qv$zNSP2`yu9 zWve+QYCU&34;x^S@+56ct?ZW7R}|R_ym;NtiY`7T$kI`XPc|iVWQN{xfgZBG#XY9w zER&4oL*!cArWz6|GhL2;SySoq?P2mVRn`)VFZ$scOw_)NLy_|}q}*gVYUv8|dx5qm1aT10fcm6;r4 z^~TG1F7cKt1tEgeGjsb2bxxGZ%Aik%|BY^H%HD+p5!(YsoVO5?{9P=Mp}bMniT=0M z(B+8NK)`@iyMJF<21ZP5MVFS%X$MUDV>|xQ*a5;(#4;Y@(NAMh`>jAcIQj6OS*Ubz zZjUV!3YPNvG?hc%JqBBE4c)i$J)pH=BxX=lj&iNgR9Ex&-AWr3T;Hm? zE58z#0-N+GlohIiQ^1e6i9|W;B(!G5DY-^rK>}Dd1#7T~b~y%gFkA}Np+O9k>NfgOBf~zIj963kyFckW~k_3N`BD#ZOYrpEW_RNvbVk3!>}3 z)D_2YoZ=F0x4N)H*mT#g>$S(~s zdK-2X>c>r9?!I+kSWxB$*#D$Xv=&)$Jgc^=FFzAXBB+y{wfRJ27*)0)*38Z;V|v7> z)2w33(1VCwH#AeGI^)$kNLe*%*t$Pg$=g^KeMDbT|Q!P2e68C{2F067``Z<1eGt&01r3uroi(Zo=nM*W3Ki$ZBB)%<7TO`uX+UEpz zwe@4ml&(3icT3hjwaPGQ54(Hz_K=?xva-<}mx}_Kf|0|oDp(p8JZl6bAL9xstLOq% zd7l0->-7x)3ke*g*unYHf(X;CKW^jVj61^HCC-0n2N&!4=)5G4NA<*2!Toznuvb-Z zcQ6(z7V4`&1W^-Hvm{`!(Xc4LxGH$3Ec8s16uM3L6W><_@As&X7J}J_(3EVnj5;Li z-&BRAV@kJo#K4yKi`TE~W;b{)@AEZ=rqku0s%;fl=hVI{NZEa&&@mSog=f1mfa40M zBSl)IB^{uDgs$reDi#q#E$+9?5L~|WT>TT-2j&NtH?H>&yidNmPkt`cLg#vBma7sE zg6cn06rapgfcP^^H=mo)hI|!Pe9w;)+_QBaHW$o0F4LKw%%p+yQ}&IQWhZ`h1^VT| z>xXP~_oI=1b$P(zC1Ug9fB^t7}$tnAIF z8t$=&4aXp$e6J)#gzY?AHt9|$y{iFVCp~w8iG_*uTlQ8)-PxIg`Xf=*&U%}J`J+!& zb|+O2mrI}6nf${;kVC>4z-Uu{WNh|hQ-4(?YNUS5{%P#p{o)r_3mKho!X7={TJ>=L za?$J3ANZvvVYI37DUgfSQ_@=mzsQGt#|XGnwh@=-{8)jof%fcBDxdL$m6xdl5X3t| zAEG&ea6{ooY}b&soa)i_@JAGtl=7hTw;F&?bqyJtyXSe0q--fOLMWsaBkJ_Wg`VS~ z>dw>rYLw#6teQYr^V-DlF z+?h&S>}0wfcW>o)1nR)Ffd~qvn=mnD%LH)}#k8RaG3As(aD?uzC=h`;pd9FI=xBr% z_Vgq;ts^p;iU^{c(Jg2xiw^Jm*{3HFT2k1nqHJVkntGEj8|PHqHS|S8Ni)Ltv7PDK z*~dS+3o14ag+c8eht5}e%VX93yEAJ4A8~fa6lb)XJM@&@X9-@j?lf@_-3f9GuaEdv z`y@b0PyM{*$L!BPqI7F@65SQN{$Y|F^S{z;lw>(!%u>`6=&~;b>iVAQPkg{P<7dya z9;oBt>G=_by|RV9CrES`grQeHsge^V8|M3m_Pd8-EpD$Zg)K)%cjsnZY{Dyb@(B-I zAx63YxmjO1bKQyroc$Cyml0NCP>D3j%%=mF&zbu#4f{ZqD<;LKyB8%0J@$*-CDZl{ zfBYjjvVpU4g0^8$<~HBBY7OOJDP2&TAWe4MjBXZ#t!!YZVUS>se8H}4mh2T5*0F=h zsmjY_`nEU>*sf8Yb)#+bl;?66UhRYKsq+V;hgaLbl_tlzkhardF2tJE6ZOYVJj?a0 zF<)$^^e}NWyIuj->cZI@C25~GhS4q)+SwZ?dE$}x9|L7ylp9!My0;z=0MKs2&6L~c zRqVm(0%~;)ub}j)Ls%{6+R+?+{asHl%Z`@psd4ucAhqB(poG8H~jwh6TH|= z)fL=A7v@Sh-BS7wWHww2&v-eU@(A!*O|@m`)f#FQGbv=jun7%aCbkM}vW0u%%JqGl zI_6}REap$dIn$BTWy3}@tTt81iwI;x87Mo= z_9wcdImlKQxCXNu8fIqY>=UKnk4c>K&s{E|d zDps%kWtnUrN3qGmEy}FPqD2hEkOr%`nQhvd-?L1!AxF4OvnMC%zMp6KjNdT147*%0 zxlFLkuwfT-4BoZFceno4_l5y>i9<9B%6$yrJe8SnobjtKpw)ayVB=WQUB1OTzjY@Y zEg=iNF(tXRhp*OeAQNucu}SU?|IDd1qh|)9%Dx~43A9s@af@dnP6{I9{>4#w!F1T7# zur@+qZeJU@X-pJNc^7Lcct`#O>oc33QL|AseiM2{$*M1<7d{%px>qD4OgkdUPQiLHs!^?HEH#F+g#Kp~*9nuP1D$mTL$0FikA!pp*&0GXb z9eylVQIkFSMb=T)U#p)W(ZLK^VFQ2?pE6CTqYohyPqkid@UZkG52a50PcVsy#w zXz#c^tYdwuGeQ46TU>uCib(-vtyi$(thzWjMztC(1l!!TZEK%g*dhVu3{Jo@UGrEO zqrYKk$H3?eym_JdDCgd?>h*(;QAACHj!5c+hh1q$uV_kpv2&_-Xbv1kA(e@Z=-Ak0 z);_NK?TwU7hzK%9fiYNOdF8M4Q4jQe&0gj_0kV>WO_)JlX1sH}Y1 zlKnejt!&B(V6A*vmYrVylYW)479Lm6$X^v#%XlzEYQlscC0>7VY>nSqx$>UHm%Xf< zWs@0u{x$mwDISvoqFN=?m`CxHTJycSxN*%hCAF5spLT;%TqeM*x`l$nN#|?q_Ulca zvc>N+wuAeeCaalsCu5rPVdXZW*Tn5=jfw3NRX@!JrPxed@gv2xhi|@Z(pbA!aqt1F z79o}GL_SHu<2Z#w7G+NcKIM{5N?(U_7B%;7Qzttpqs_B+Rcg{U7s&!ge60*CY<`xBSBBXFjI8iOs9 z2L$D>_g!I!CY;oDpHor9B>;<7_h!4TfgOZ`k0!?}mhO^MoExZ;?qB^**`JLdxIhY9 zw)$n^#gSG1T>I0Gf>SyJh-79X;gW#lA_}RWR$Qb>Rzz&9`n|P{$AVqS?4MAVVT#!M zWMz7o(sGn?4y?SIuq?_{WP)4S=(OiPB^-}8=9k!haW(Oq1=-Gm7%Gzw?OKlh4Q4Gz zW-WaeKQd*SbY-ei^lI~W8~;N3Zmg8pg}3|4Q7n)9(vh9_lI_pdipbdWJ+&6sNXL9U zE?zdibHeKvhg})I){E&Y7?%`GUQ2G79tfI_*JO>>iT>GrX(rgIiqy?E_=e%O%=?75 zX!&ljyC3N2NHnuh0+wrVlWI^d)m^eB2TC?5|H>pgnzkqs=i^u?nUsrhv#M9l%fvWR zu_zMZ!(J$vAR#6jci+0LBwGEs!N^mYyur&;nXvKt9G1^aMZ!35>rkSusTAYMg61}z zL#7V+w`pdki4@7fW{n)l$q+=Br#zvDm#3WWQ9V(j4~S2UJsR18o%QWCeh%Q_$9!Vu z$9jT)iM0g0B{o#6CzeF!HkbEcCg|5|3%}_J78?ld&)aZ>7L3Gn#E|Q!xy6&~`(rx3 zCX?~!;%u<<|2w{od-{JD3B_F5J1itUDt50%_$#vTi<<7?`MjI-GU4Ij%%8t_l@ zP>y0xP8TBT$Gs) z|4c4G|KonXnnc#(vxLW_%wN#JKkEO$I%ov%-7Vs5l*>~yk)d&`-h;V;4pOYap`z5{B$A-PsW zcET{?1_^73U{D1eBHZ`@@r{Hkv}}Qb=X^FO#=Ps6$z) z;a-zpL}7g1dK;3iX^cF*pOy{=BWc=4*%}3oE;MN#a9S^$WiIMoQeDs3w02!3?m@L* ziag-NhIcsth{e0{0BHSh4@S4~=lRLg%}p+%HRO?w%;&~TTfZN%uJO$A!=>*STo7qk zM``{Y+0|gdn&BKG+Tzyi3w|{4vdD&tVWburG|t?bFqY1@KwO_b3KLr_G=CNq6F!={y@KKh6v)J-SoU=hNA2auKvP~ zEuE_ow;dDvC*(F_#&-SzxZ(I`=(?TS3~LoQr)o$ZWib8p?PDn@aXmyH7+FC@5PT1u z?QUmgw5;&EQ|LB$q_s~T_j$ilU9Un&&c&^A06r$;o@Uwd5{a z9swQ7sMF2oID<3I7LBo`;Ur;p2gP1>#DzktJy%v&1>cHi$doI@t}1(eR^e9Cf-h!J z>j&i=gBJSS+)tNMkELNhBi*~YVOk)L=J3>qGkOWq*|PtEicS)Gt2xm%Q~)MP7=DKTk0VeXO<75& zUgxvgkbUq`XNK{X%%2!B-x_HG;wUbLBMzPH@Lt4Ws~C`CaAe4|kCh;4DxmF)zO{gl z$zfqu3$Njgo-r(!L$W}q$l<6D?5LkXvkQWb9q8t6XSCXNwR0DkZYM!>spLb|;9@-J zH(dsl6DGTE)Y>hD8(_$U(R4nwC_~UwVB@-7QlQu%EGaN`5JwN_?Kk5CzKB_2=4+ZM zbXL88JG1&Z5BNW{y#-JlP1`Q~gdhO|BsjqWgaE26uN|+}+(J1b26Lcee$C26uN@ z9QNPmdC&X(-&=L+t5c`yR9(Gu_0{+E%w?})0UBhE>@f%QC#o*`oFF>nHk>=GeQ3yisT~y z4S@wRIR~*O51&%_L;v4XQ%$;>h>CM?6Iw-(8u$0l;y**N4RheJYX9%)IyKq__UL~i z&?Qu`O_)ME=N+@0iN|NnE&DqJWz3ip%)Hvi#~2d6X^Vq*`Yi` zH5&hGCvMTFQy}Aj(y(Q2!&x%Ht|MX$An9Vr70s|0v%_1H21>XZvLjQ2P>s6&Ta0s{ z6K}z#)(ZQ@BqElWlP4kfJD>JI{y*guK%F%tU{Du-ENE~8HR-j?Aqg88GZ}!grWA7a zrOz4fmxxwPkp#Zua1N8&40U*mpjrX$sOYQHUT< zgdR`wOH%|Y{D>KTj2(Ui5h?_*#!T4{?UPkd;ECCn)mx*oeDlL5OJ41j%d^3yBN0IZ z#ZX?k8+u`ZV|vu`$_C;r5z0B|gSHyJmGv9t$s%*nzMkMtKN|Z!^PN-^gckVIU+~<9 zj4lm?winamn)ffdGfDn$tWid(_sNBGnqdU$v1np_lX?q>ARHpw>)=@0P(*P`y>Zme z7`jn)#LB?6kvJpIt{VxhkdYs%Z4w^V@c!}|SrSxS5JG>eMQ6D&hh)OPVW&nNgMYwyz%@3pMSMyP~*-8o!ojO_b2jyVTs4Yo#q%p zzg?;f#6JG*9|*CJ3o%9tGGd4_{);rbJb%&_2%VSNgvS0K(nImA7VM%5#1eYn|1%09 zia2PriDGi#{${;+I%+G>g!(el{9<7p^or%3)pR~>9U>Z|ub>2{vn-fxFSGF2qkY-c z_nBbBM_M#+NU)naXIY~ziYa0aufjc^$*D5PJgfE8ujXj;#?U*D$r<#W-=pyQ)DWgk z$jpnI8N7KV>p>PaA#~wN%a7H+mwoqtE5f3~CLVn^OX)u!Tt#T-)WZ9CjFxFdKX|_< zAolEc6&0{8Z&P$geBH-hlG^dZao$^20?J!2=&Za1r7`K16bVphz3wd$_RhNr5iP;( zR4Tv1@T_!u>MGCVqoJG1HGlA`3L3~Wd#{9< z2tM>v`IP6Zdb==nQN6Y`B|Uz&0>M5O(Nb_+Th#JBQa)IgTc=#CknZ_wJ~LHxhjipp zF)RI()!;Ot<*Pq!sDB>W{v=ECf5ZCOh(@g=Oj9}I9YcPQY!`l)&ihRig3CT*`oLAM z>0)>=-Q=%wa6t7`ByWD6a&&EOc<`pYusIhonY)u)q0~&Kq^Wkr49i(*Ks%~G}rg#3FGXd(d#E0KL&jd0Qp+K`=eGN38BvJn+VS-eijZqiq zWRuUt*3y%?@ulelnl3|xKhL^!6Z&b8`?@x~f!zDdB4YiOEU5VWjif(Fk9%yAc;~<^ zh8L$MyVOg9KTE;RPnvbK^+I_h8LTu`Ez%Zhx4VEhR{h11MJorhniA7ogH()M9yFb* znBSnN4U(O_1EsAVuRj6j1<7u4oujb!$90umC{r-;Q8RON?!> zAXo**u%L+w@kjU~5Tb9b6SWU}{Ev;=?xXVrCbq;bc~#`m4NKKmXI-u5WNldV8`f2# z%xv?52y@+T3xA0%yrEk_OwInm9Hw|koq(`gi8nP`wrFwmjh+*AvDX^<`N*OH>yXl% zdu%)Du`a_P^Rc>#JpAt;bBaCuZhhJSpdRX8Vh@`tX2gpA@m%3CjFjI1_V*nTUVj!) zovz{}h;0G=ab05p3uXN$Qw<;s`R^T}V_ir5O~?c>aznCxn4cK=NaW^S)9cfK;0-Ys z^~wfQ!2Ca3po62e8^bXB46kzx3w9MiAe{Nq$vc*8rK&G%c z{n3Z8nu;8Z?CRC+n^`*NkJ~-vEN_Bd9jzF(x0X34LQiW}wdPtM-%0unvmg;x^5BvA znx4~21{1O1kol{z5R(Pd=wjMLpDLVx<$XQTquEaD)GXc4El9Kf7QXN%>=wZeFM1B- z|J|{1{+Far==n!p#8_Q6N#FlEMtqXble|WG)R;#?4jb7tLk=1P#41#Ux0#iW`ilE@ zzFZU$mV?7R2wx+y;79G_zHkM4OK)T<-0k7M{23H@yaSWNj<+&BpK7DMZ!RYI{~w56 z=Zu;I!4X$fT2Wxe!Pv)kb9V?+)SnNyVdfh47(jxfOA;E zIdt=IxFNOl5u`dDNJ9il8&SoST|?WVq7)?hxy+KAzPD3B6wksQ zc-MflkF>?{Iq46A4jXFD-SZ&Xw*>1a=A{9g{(CQYBiVG^U>iLj<% z>zN9S34XP8lVQ$!qSDI@Z{m2TR4r*}bBYtCjI<_kzjgMT9XSLZOO9V_U^-*gmaN^0 zl3aU$Fe=kjrh&KbPxw&6=ukrbhBIy=s((hfh(Q$78}CBZ=o(8xq4UN!PN534wqHP{ z!W_;?!}SHr=I7;Bm7wV8d=5JzzDo4?-_&=O+Yv;1=c;QVp9x8QZ> z$lxQ6&WHp}48(%BI%_Xh|JA$yN+^F1RUHbl6S4*=9g znpO*Nok!aTMJ%@@rbbj5rT2!fH@TkVv}B6Oa)J->yaqL~;!D@iJK}#tPA<(l*e0Y< zKnln+fh)+=S7EMEoV~{%rT0vs@1%;S;=CZ+mY6u>2M0%4u&P2A=o7f;{owNQ=|_=v^6NjbNwX-IO^tfA7fNo-xuxs;9>$tl5B%TCvG#$Yox$GK zYlP2MST7vt<+BBAZy3q4`t8)0Wy0mumoz*Hl*($Y(l9!ht7ybKu-Ksy+Hb#&Z5aP*GQ}jX z@ig2)=Jt(w3X<{4UXOloNtcI3y4BDA@ei4yMfa_MGD{8B^3{7$C+u6#fSB|?PvCZk zf5?VSC(~_wCu_FF#lOCLzSg$IdBO|2ylK7QuLNth>v94B zv|3{ffBIur$CGZ>nI%l~BY0gqiU^8}Z$(Eo%-iQj0(qP8to%1v{G0$Ku+Or;5)|sv zx4gE55_Z^9- zW0l+L(4i|;uQMKRH=mpH-BO4O;ceTYyxMGj4Pm3qFra7c+wqinGEwtEyi0fGg_p*K zo6gorLkdlvGuyn~G(VW0Q$a5yi#cm{`5NEx=bon6$rt@z*5^Q?so?X#;PIqYnPFyH zS25LFzKV+7F*8V$o|X=2ey4cG2x~iLbTUV7PmWcJ{L%R@Q*mRQM7!zMed!{-U|NFaUoycf?e>vlYx!z$;90= z3pk%%fHRz3c4#tdhgbk2Gi9T9hv@SbKg=VGrU}O<1D*>s%?IieG|k6ti7V=N)Ojv_ zO^v67s46(XOF_cV)+ zfe*{sUx``ff+*!z#ARDUE>N6l$Ccr$QomJCeVdBvnCz|=(ae9sv90~#;)ZxyC3d}| zX*J?#ym|H4Q90Lq(2-^HYSiNvlMB$%6c}C$&FaveApFgneqwB}YJ_vzOlOoLSHLx| z@l9f+E=SUQq%L2QhfU)=qwTzTjvzUL%0)kz2=sX?gcXz7qk)mjij=>*H-aLTwa>4S zQqkoMqcrpCVhTk@Ch*p?O;iM0lwWj_2|-mZhktBb=-O>2Ak_z3Y5rb59r$d2iDsv<6k5o5uf0*}`q@JnuA^*gGVRJ-QP^589{;zK**CC;N%`DF&iQ>R zCRQt4oXF0qlW_a90&0E;7^VKa3_!Ybu?9Itd3q= zDM6rnw@%{{L>oi%l1?xL)8%(;ANgGx6L(mtu)A$!Q$p>0_6`IG7>?4vqyNiAZuxUU z7#>Oc=$_t1u_|*x*r!Yyk@u(Dbk(W&IxHWEBKtn%7TboYnme#@hwE>lZJAY1H?8i| zjYO=9T5k{oKRgQu#+WaE>0l2c2K0Y7z|RaC*z)*5WPkUSLz_kWYYO4nhtde+O9$5= z+Z0}f49heMo*~QuwM&F#%QQw9t$l?3HuWX;mZod*u}K;Y&nRRd5FV{!+>AVyfcBVw}jhhe+;zTKU4_>4-{2 zcgLV=rSxJ5TSF=4p{7A0w~m(Xu`)pkdL-W=rDKd-iJfAUTmg3}eT1E3{h^g&(UFpl zLFb5)tEf7LT%$zxKoRS2d4pM1)6xU8AcqlLpaT!KAdLvEGY>uG^ zv(_)MjI-bKcdW-kk|`$G0OL5@fxGv+_cf_pVgWox`EsA?4%(Gr^|@TT4&Ve_~txI3ZXvia?pm)8nLrFWx?ge zC6&-3Xi>{>iXLCZiy5C0U%p*AxE!pM0ZoIZwalvP;*~6w0n2vG@}N=Bjt^i@6VJTU z2L9@~V)t?kWqGN3d1Y~_+6GmfVo>>ta&d~@NqxzTg~ozBpY@WHJv3)!j$h$oxsoMZ6mm_cCtdHN7=c@kf65Hq%9%fQrbC5& z1>h>JL)!aFd5;5J#Wh2862SrDM3VM(ZM6@!PvrVdbjGW@%Y&1SGF}Za1lPC*17?%W zRu3edjZ>YuCU~yKAJdT_2=;zgVOL>XQ-Ah6%=!08Y%r5z(|RF#_I7byVCm2lx#_eI z^|=-IKW4(`0AOKaVdA6-QDTwQ6#Lxq-0|`IQ>9brF1i(YWAuH=(!>dv$;sLYB`NTf z@sx4*$ST9V^gf7Zh}5uSC&~ttB(XeoYGPl^Ge8PvvL9?^PCjcoYavhNS#dlyo_20* z;u-fpjZqZ>kecJAVw;ksesJezqq`Bc5HXatP_oe3cAdC61ihT5A8V1&UADLbL7481 zF8*ajz&>K**>h1Bs`5p6nBF?2dE%`Vb0gPV_t>)CRb1<^SxD-kRIT)RfxCNaY2*nQ%0;qcgVx4gQ=6_r^JdOhbZ zc7)8l<7ouFXmDLq^@espF*T^#a<${x2P6gq*_oKOez(wD4KN;^HsM5T-1eIf)Rv)a zLARjgpiPXe$a2$*f{VGi2$v-n4RdJ^O^-CF;S=PpeNN%xaSKarqMtlUK%X&0;6tWW zJGM8&OHw^8KM((s8~cm!lc~Aw)1SMmxl|%Hs$TUBIiB1MsF-MO#OJw^2q7X+IlExX zzB5HzxS1B6^tnW!{5ZDAP{o zQb{!COOcmNz2e#4RpS6X=nGtHClr_X{Oy=L5ra1Sc4)jd^p zm~wbqV}--dv}9oN;D^7?tOOXJ&W2oHXcM+nGN&g{v0g4PVZ}G|RSJbB2I#@>9h5a} zU%8L>lzLC=>_f-McQHGj3m?7Z1akMP+R1C6#coYx1FN?gTh7iC+V|0Bh0tI(AYfnU z*?FMb{Y}Oymicq;F}U-b7Li{mB*Mm89!iwiAON$n^72+z%=|Nvezx_;`uAGSt+~yi z*&M8Bfc_LSStzPovA``t5hoR`=HmU7+8xk~gwFw_~TmMW=Mprz(iWo8$0?YXf^ z3~S4P>UfEG?uK|VMrdgG-QJwN>=X2uJRnR!2L&LNX4!(w7`eNZX#@gOJsmm%rCvEs zjsdggFS(21Jus^`l!M;SdySnrFs(uR%RzzJ`P66O zbe#LITlSk+qFj}3t%rAeIy2U=SZHY`Q|Nmr50~IsrDS-@+whS+z^pUd@8H%f!cZs3=$DQ(? z6-kAS4|}zT&d>*A@5(cEu2pmGNNzl6`mtAMo7eB~zLF znbV8>dG!U+4EQcxBrBk%bfUdHak2;Q2MCMGrVM7of3p4D;?@Dy=8pz(%>+{sz6 ze=vH^m-aZT!v%)4_WK`8VaNJ=1E{q1cRMIog-Mxk==+E^tnnb&^!;wO4GXkRVNv>x zdLgHx%^>nv!6);JX&NG_yn){K=#o=0B8 z!?@k5or%A3wLv+AxlC%#_#!`_k#P<&L%IxuH~q(=%A%Ht-1V9e!H}U26c_|S*YGn-AGb>aN*7sT|bCO@ht?H{h3&_kK~54L*xvI zNqFHBIL$A0dH^rVj#3nB&{i`yaf^#+AX1vKFluU|_-F?3z=M%KFz^lDB3T}?SLd>f z_zppmZeYcocZy{tadzrt#dhYHA|$vOqbU@y3EK>+jlK8R>pE0UaDyvA3j1B&R^op>yQ;PFZBcQfZjPo(|e@$2( zVYF-~Bs`$LX4^GUji6#cO#(1-6DstsEctmA68b$HuSUF?5?r*?B zi$Z~Q!GC%Wi~{)l60E`b{i>$-Gan41H#~f`zTH=swYxOkM6BP^#Wk;a`FPPR;52jp z$rwwfyNQEkpYp8d7B#>o#*X7w+EXRbehQvZ>-)TWhm9!Az>9~+aL0g!`z}9(HjEM> zr>MwBUXeQlA?ydFi;rn9Lt=VfhqP4?>S^sLCWvo%wGSzuXU^5NC(Un6EJS1=W*MM<9t0{T~9 zq?9fMDP7;sRo`$_vrri(5%Em~ktesw7GhR^un{TH_9Y1-8=%qYj+|8aQwa@$4Jpuv zYJ3&1d-p-O;a(&wQvA7Z)gsv9Fyrpi&vG+FG08Hhh(-H?Ro;&n_``xdq600!%M}Df+DGG z%5QxV^+Fb}?jU^%Ri0g0HrcUUv}zj$!5#f6Ra`D522ng;bGq}qfe{7dT;h0?7NaiX zfiILYJF0l%kxG$DB7C#@voRI#OMRc%4cKFlZ@RIL{lRt?XzIxyQiJ5D4W{L;#ugZ6 z?uC!Vz6_fDz|W<)j|kd+U7dkGY4cTCVsaAsgS7C2?Rpk9=l)+vnWAp|d|0h%Hg&Hs zRH&ZWSSE8+eEsol&R>7rMP4tZop}YHS_jjXD zA3|{fEreRSl|Z-N8}^4@Ocvy>Rq!&1HEa9c5KTPD_m@g9o46BTEK|dJqlA52s;{M|^$He$ z=|H@tOWoc5MOmek*a&}f9ka&A$9?Ga%}HTMmUd85;E9Yqmfa~&o;vy znS}{4;>hP~ZYuTu?wgkU=Dp^ zug)KSPp=$&t0OvnyA39(Lqq|D{j)<}p{RT$9qfodmH5%QS>bPVeC6Yr`Ek5RMOwD9 zElXQiw$d+4J0(lI|A&%+Y#}UK883GT|%}zmCpH>z(K6_ML+Ml*~0p zd+r@xI#r{FO`!BGy*rl7aSqSN%k9qKh<1s%JG7Xaqd_mNf~t&k6z^$)FpQ@Vyjxwv z#sjT8T56tS6f$emQ6*&_9x0a?V1lK_7%JG90 zx6&6v4e{kb&bj>cFRmBX9xsR>$Cx9e{Z^%>zIz&&Z@DJ#za%~Fn1L-^5@cS1s`teP<(drW zpq9BUoXGU#pz>gy!(Y1eTgAF&#pqQVMQ0RbFil%o&we+h>t&viOLS%}juOLK-`6hfc?6IMxs#vEF?&c1geoMhH6Z39msZZ2 z@7%6}lVox4}4(TC)0=1p}xjxHvwy;p;oygF-q@>yutM6Q6`E$6KY5KlznI>mimCHhfmtKYW8oT2>4HYleZ^- z(QMvMV2jV(w`k|6AUPIX!jFKrq;*X0*7+n5`DfdWM!y9k`a-nL5l$QeVfWbR=sN&q z_kFF#yL-D8xsGzmj?{UqOy3`ob2zN?7;nw!E-lbS*OB=43Edu<`BWz++hg2_z|glV zLgyQ=#zX-ukkac(K?w;ousHx_{zm4*Epn8uNBpD({Vo=y_KQyQ39E(k34aX!@9 z%FG3i^&hTM?YVC#eD4R-B^RPo>F9N6Y&Qqz$Y_3Lc2ZdGLF8jo795yd*g zw>-H8=y1k_Y&-61wIe3N2j`<^;z+%J?KB>i~wB!xPa%6H3YRwI>lj=rALoor0dXiZL zCWqxQrD(4Ad|ZOWuYUWkkli}o<6P5|7ROI?Tfj!aBcU2yZZ~OFF=G5tUTGm$&&+M) zwhSODmx%_K;q~z-zavWX9sb_MUi?#>Z7tawC)nYR>Z&iT+#tnS5J^FVX89hy$i)v% zFOkA?ZJTYIi)zg!slwImxOOc%@3+>j2c&GyPxnk}-R#Y|ktaD0qJK1=H4-le)vJ!3 zGeR=W)*pWd#!950spiP&2GL&SYu0ZjYgT>hDDQ4200j+g+~tqahRz)Bug){%KUMR> zsxsCZ<0e+`oU6N)u6lEYs+jS@1*`+MjzG^D9-rlqY<^ezw~A?O?$wc)w~wq&Qyqhz zFg0RKv~==O4r!pcQy8=!1t8~1^ikETl+GYldvdImD1O;Rt(`m4et!=EvU?^-HPdB; zq|cZe@UGA5JU9B9#+x!`6_tDqU{%5V>nz*`(PA?3Naem&JD+^2@&eEP&y7uW3lRTq zbSC%8!J1Dolhh*?H>!&a{!f4pUpO&8`<9n?jg4|898$a4vY21Z-e1Ns-lkbk&4)9; z@NVLs9}YjcSBVR5D17U$KswMtU)^O^@Ft(Ne^<(f@1a3&DTAG+LvPUKtV6KR_dq@T z&f8qTrRO7OSZYH1Y^V8sWm}|HO?)|02wvyelQZL>T_#%RwX@=OiBPs+&(s(nUEjo+ zChb93oc}^?N!&9+*%o@T6kBoV%U@vHN@D0|Pc%p(S)PN3HrA$MTKYv&iQu`Xo=ss- zUJtOKA9k&GBa~4->CZBOr1H^`*pr%{)WKPN>Vm83n~(tuf8kL=OD*QA#iaHA#I`9a zzd>hrS{-|c4za9OG8gfWie8-++ID!H8Oa5%z%>;c29@LCFzN4`Y%M-13I)m1#z#;r$+-DB->1}By?e|= z@jTw^)jTL@vib;ne*q)6_U$w+NF|(mg)v7OC_i&>3U5d!PMd70Z>hTpI~)uU0f}y> zgbm_YeRpfQNy~K|x!rtc4@{^_r>9^<%tU9uzW|mjgw4lE0Eu(ea08$WyG%Z;iks9^ zH^ay&3v2vkH}X2vHQbfn(>RKQyg+J~!5-fdkok3QdT&?Wp2*Xj379zTyeobsvS?Jn zs*_JX4jcy_&|IzUsxKN;0M>Hbcirsyp7r5T+9xX$YV@yDcQl6UGVykQg!hny_M}

    E^jA2IIsFJHKx17 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

    <8?>uYKA69s37bjr~J= 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

    0C-;o(XGXMMP8n-+)%1SCIHh4; zGpj$Mdxy5+=@V`q)2Ke&rGCWVN4-jvo|q0gq`iN@9M6{HHHj%riI2?g9`#y%>eUIo zkvA7Cf9^Yo*|at5?(?zhE5F>XifBHbKY#X%h8Ke$4eGH;&Afcm+{tMVmrP2Yu*+uQ zwUKLskIpnVfBho1#(ah43OA>r$M5|zVDU-8kW=*=J#3QlKE!=YHX3EAHf~~Gq3ck) zg*U1)weA%8yIFE&CqLN}{qTT4bw)ZJ7pUW;q5+Kpxu-xL zsK5MX>u+{yo!wlbl?yJ(_V`Vm^1JtR)9;eYri<;FF;X|j-clV}>{~oIW6-~K6A0!l z%vzYN9iC_cV>iLpmUDLFJ2|@m#-H))NF^`cMHCr)qQ@nF~Wn;MzJ zJV!a(PVKHcZ|B?YAMaSY7)2HA8of9&E`HO#q#8GhSB_bjyQcS3^NkQ5tM+|&Z^b;l zc#XN9n`S(4vk2c%GcI6KMvZ=`V}8cn8*e_gX_gdrzxT(+{7Wy#h4#NcmcMq9D{m(6 zGH>T}L-iveA$wYHo$f*&x!eDDvH8n8;Wp3n-CooBf9^FY(IcSRqMPT4m&zxmnpWZmaaomS7%9{jDL z+B9kTo<{Yq{KC8-hh*KoJ3Th^*S?q61^EpE<3dsbrtOl?u(B?# z4F;tFkER>!YdAf+wWx&&>vV~tB#0SaY>{UEcizwZo3|rNx}}-@-I+anRzm#b#E^u@ zSvG(1&15=;^2>Fo_N|dy!_K!&cgu=-R-?aglelkKuZ>=mmyO!k*Ew(F7O1=HXNM2b zJJqlBnO95xJ^!8Ivxi!k94ojt=iK=-J-f~_jP;+{V~z3R$;(CvRm9WJZ*aVKvDI`H z?(1l|^_B84;)zqCsC?_xe!t)U+S#uw}y9Jr(brJ zB&AmCe5&m^@3T^S#Gv}M^`b?2`#>fzo4 zN^6=;AHOU3s-&WR@zC(H{w3+SIV?}ievA=Ml8$QtONCR-tpBsSvp;|G)=4MFG1{F~ zQuyNuA_j$bNGORCtQwsdTZx0j+0NDumWhAW$Zy@PoBFKw+yABBqm*3TN9R`B8vnoC zM`Lwm^6;ykc+~9Y+eg2n72eOw9A>q&M*nrWo!VZ%xZaj+?c%b1D^InL`#3et*(<_wh8*lE&jhQjjcc8s{s*;k^ zOKka{mnU}f!(B2}gIy1SQguGQ}p6a89G z*S%avb*T||*Y?Yp`g--4vbc!c0(ES)*Pi9Z!4a)3f_JegE2nL@Yws)cy67>icFyu0 zjjy76UDMc?<`rqxuff1JM!#Rn_-}@yy;T-L^Gy1FQ4d^xG34sxyhT}`Rw(vdn{K`Q zuFn*=`U4ZgR_1vj*?*Qr1WB;p1*9T`4?-C3g9jr5ABQgkyc}N*xDYP{yarzbcrCsb z@H)H*@Or!z@N4#44g3@2fe|7?84-jEp#oTyPz9_;=m6FwbOCoEyZ{d*1_K^KqyU~x z%mzG%NCiBPn1^s8jd%QL4w_Y_Xzd?-YfVG@BzVTz`qOb0&Wt#0^BP2hA08gQX- zJ>X)+5QHgCQVd6gVua#4z(tDd5w2LQcmVK0#go84B??5CXt-!BB1FNWV1$dtiFN^} zT67TbA<<#LM?@z;S|hpv_@?M4;9Ft?gozEsy$~UG5%)&8xR1CG;J#uv;P)511NIOP z0PHFD1neaa20Tt21vpw94S0$;25_u67I2(64swVWUk7|cd;|D5l`bJn>9W#gL@3p% z*l=$)Naq2Lx`O8WfIbh#2O|<6g8Rz!Q>t|o=vy?zQ$Xu*PTwdp4>XN}afjaKZ|7Gb zlE0I`6L2+uAK(N0Ll7V3pN04$zYgLn{6>&o=U)eWgAc9d-{jv0e1{Ku$iK&LMkMHr zmrO&xkKlRw55O1b%ZQ}w=sIq6mLgKPOt_321sF%gFvT!LDu%;|V2Xznk3)Py@dR*c z6l(yVgc14PP8yxT>k$=_7%~YNh0aQhLCKNflM+!zOh{53%1Me0i9%IK50PH|1B{Tl z?~u_($Ue|-fDv*V5io##-v@pB#=S8@v2THJnEg6TS%z`MM=CN*AO)l-qMTzrCyZ*XxNtoEm1 zG#6`gaW^iu=VEs*_UGatE?&jOTe$d0?3CCk*hwy~<>GoSzQx5YT>PAi-*WL6HpWS= zU+s|tazswZ8TCRgs5k0^`u;EQDQ;YGu805g6b2)OkOA~G^e)mtx~L22fIe^xVU1>t zOpqxuLtRlf)E$|l9>@aqM3%@3StA?n%P(8RdUpjFdj-UPU#AG8Cg#3;`xy@dO<-pn z#eG%yNlML(hHpd$-08dJq|v2K_(=7_msUYH*igpJ1{usCc6mWpLyS=cJ92rI{S zV0*D6*lFwn)_^r*PqB}|ORs1wvVs*burHB(Qi zHmaS6^JtzbPnT!Hv*6kCTzKw0A6@`&3~v%InwQ8+;idDI@N#&Cyi(p47)K0Y{S2gZ z$U^9K417}owV!H(o&=s>Hpjzw!=Yb+2DnJ+h)Agp1f7x6DSlEKVVSqP8NV=mY*PRk1*Ex2#rAr8O2!;;5o%zv!Ik zj_9qJC)O1+Vs~+nI7XZ*&XKl9iKnEi#Bf-sRHU>^>5Nj7(pzO(*+kh~j#5oh%~GvUJ*nEH`c+L+jZyPa3sXx|TdQ_J ztwF6#U0K~)-A_G2JypFxeV2Nz`a=y|!$6~tMvz8=#xjixjnf*<8ecVaHJvpBG~+av zXjW*R*1V|RbENcYvW(v( zO*hy}TdZTCW2@uMVV+L2&U;;2*Fe`+S2_-)ZiVh4-AlUCvBbK_w6fubRN5uGOIeo# zUFxJYxwf0LP?VmD-T=J_y-d9=vh*RnCVf)hTz`l(tv^wJp?-yat$wRCZ9p1W82B5^ zFjy-~?=omK_-bfo=p#)VPBhFg+-6v3_>oJKMq(pJqwz)=vQ)9r8KYKXZE4Eb)_9C@ zy73lS>ZI{=6Lph5()S=uf@CyUM#E$@Mq2-FvUzDfM@oCjsO5MmwQ`fuZBlCEEv1aC z{Sw)EwzBhVW#`!~ky86aDRmelrH<~>-gHWp(Pk-i_L9n-W%j~_l)l4hd`m_j%jkP4 zHId0pRAtmiMlEF2UPfJI)LTXaWOTfY#>i-jjAqK{Dj6-7(H$~+NJh`eXrqia%V?{N zew9*FMH$tV(QYy-tDmX6jQY!{tX`&(GMXr(b7V9_MzdwKP)5sSR8~*Z{W5w&Mr&oX zK}K6-v`tFQxbLtL+nc(w6&c%eIN1D~?0As(I^^~;c|X<@idKlS5fZHw<-+RuqUaJL zMOQ@ih$p%xYLw2OPFE(#u4MnarxM{nI6|R|i6yY3dPTG$k|JRTtUxtFi8R#&`x`Nr zyDFEvx+q^%fHYxMeiCVOCFyb{>2W0)h*iX@$VjXq)`T^Ie5v2t)1PgRC>Yk>K=BWv z;oNFbdX71FhC93NfW2iNIPrWu9|?qc!aT%&r>=mnh8-uOm($B(&6`8#z@8?b&WBZS z5nT%F-;MM}I6GIEi)i60*p~=pdG~>9**E>!+&yGGbKtq6L^&S&ZN3ExlkxuNY5_fg z=W=n5tfb*^jN!@|AmgyD_CwxsJZqpX5bWi++AufGxL_dTvpKKg;$p6T>>dSiwZ!=A z`LOqr?k}OnFt53qnQ%2T<@QWw(tL3S#={Zs!{zBIDL}u7aSPn6GZ$+^ zIINkx<#=}xfwkZWx0mCdfjs1|-3#Y6Vu!F3a@=A>VkaP!W838TnTW!+a3KrJm6OCl zKG|3jwnUCQ3i3&THD3yrB*zC^iA{pwg$2v;%^`mu2-a{6kduf(!f-(yv*;{AZ(&WN z$p+L8v9g@35okNQgIUXQVgH3%&>7T%Y0L3h%UcVf3PW=I99X68hEM>wAOND8@- ziY~}W#)DN)<3bo5yX9o8zJ;M8IUcKT{t)E)kJC37E`0Ald40oSmgFPE%@3)r-~W%a zkiD{wi~o?8Ou)P)+=+CqR4*CN91+A0!kUPa^&~R3@?+f37!dgiwQBF^_Q9?z^s`; zSg`lFI&r}t<4yQPLY-jb`0j|p&p`0TzYyAT5@;!kM04{E(JSa*!B4HEbHO(i&;^J`7t)1@Pp_lb zAq9E^y#Wd6a(WY@=`Aob6}j0cVz0+b@WU{JkK^Z%KK=*jPj|c#Z$=Wl1%HJ4z#eHg zaue(m>_h#zdy4MDvBF8ngS#6y_&>G)Kes4<$n44w8?lj#*Kqfg)R8XNbJ89d*wF)g zyttzU2GD{>$egRM2Up(#Tzx&c`ueiop3b7Ppufs!unP2MdNX(bgUyp&O-i-iTgJHq z-cB7ol;fR41oz2Pog5deE@mLdDTRC+F-19E7SNLrFtxJ<@{H5v3m*}vU)|JhitWn*Cq7tiK2nY9$G2K#aK z8N~4{;jo1=Qox7(;A+C5$N8{m8TY$a`rd;7KmGl=F0$J85&~;`q}t}g-pPcO!3_u) zrqGE?!f1ZvMioPCo%lG~hMFLCqjL^mYYI4fb6PHky%5sjz_|a3{3C~G&_E2X=$yk6 z6olCQ&_cN!*x!|K<9vUdTn^@-hZtPZnWv7}^^|)D@88n`=rMXN*Y8qYV4n_2p9_h( zr!-2OUMO>Vp~C5fD);n&;qKzwae86@Q@tR#QI77^;=x?o*l$8JxwUk9Cq9K7LHpTX zzF_A-bSDW9G{ywl%m#LU;@=_rPb|j&OnafvY8&>KJtz zVboj5YYxx8(>0TLlc;hEYD3)s*(9FskE^I9JXZ?#z0?tqcu`+FU1don@XV+gAS+{^ zigif-A9LRW6~%Vud8@h_`yqCCJi-vdBZhGZ@fobDuIm5YbXT_xA`<=>hB(AH3`4TF(n(ar{05_6dd=@VkV;SQ`w-1;x;boXf~*7CPf{ z8iXtZ{Mm38In_c-oUT#>)-ixbUdZ2NZND6+vBXfqpQ3wteiC`d3}@p?74R)QW^Dcfr44n4V{zru3~78d5AWx@ zk(*`+8R}vsv9f;3BLeV;K`$Bd4c=Hu%vjm{O&-yTFGr0jhTOQkakj3<7{Csj=NAo0 zae3#l^X}uGATJYI&-1tVCvk1>;5*pvzyNBDpNZMRUE~j9wa@kkd?Vk_kH+aN<$Cxa zhgif-LC0}E7L|;=$cx+&KA%I>=7x}4&$q{wOyjEgWV+AB-rT^a@gcr8W({ZO&|unb)Nys>0=dX$X&zTsVx>qo>ZUOO@sA5}{c)vskY=pe5&sBA z3D*(p9WISaBh^^5(^ez?agDK3WEnf_1-9}*{3FX;X{;PuRpekrxs9#!cyfmm_1srm zNt#b4t+9SAIGJuHoB%Si}GIe z3FW`1txGpdm`1k zhL=z1v-r9-ymZnO*Atbm+!MXgo}gFXA+hJT-(~5a(-eIYw)aGwy;Zus$Kvd*((P@I zvp3AEV>`UEDDPG6J+;Q(|G?6o_q>ZS&kn!2*$3&@-Xk`A53%9<*i~(2QDF;<3Lj%5 zlYP;b#WflU2bi`mFdp4ac0q43ITgp1ffi6QjNy|9ZsOZ4{TmW_ z6OpO+H+v^%{NJd9+fuisnUD<K$?0-O6W-a4a(|NW-fomT zmhkRwl>5^J_6bZbA{q9B;|c6bB=?O3_9c?*NMK(gxo>W;kNzT$f0sQc6aFXCWR$Fb zcXrM7-@bcDzvkI2%@g_EEQZLNcPeLGIsB{DHTqM?rj-#=pMcwyi2L&giMY#^8@MLI zy?F8eg@ac7vT36YzZ^|u*)O{jai2d-*tUJYMk}aXdcx9Y6LH@ePQ-n-u@@88^oz_y zrGIfQ5%*`85^8egCaCC+Ml{g#9;=u&oLEdilvep%?t~a?skjI{{0 zt#1+28MOgA!@Uu>H!5yI!#vY7f$RB-yU3eoHPoYyLlqB@K1SYr#RBpd zaOiSt`lUVAF5Rw$k7gW@F*42~F?%Yq_dKcieEM(tL0;|M<(ih3j**Hoaqkq6O|e%g z6WN(kR+z`OpI zA0Yzj{4q)5f5QI+R?PALgP8cY`L`i|2N85L>hz!{jyuS;68cw4S@cQUo*KeY>@|^! zy<~6F2SN(c4_fBe(fLv6{V+Px_GZ4SUXu8?Iv>z4xLU5B-6Q(nu(;iq2>~khq?B}( zbdplDsIGpd)$#v^Lqo)c^AYuL{G$QEJu^F7^rdKk{^>1XwXbsXI@ z0Eq{<0wQgt<(Ll28`^Ve&*eQ=_gvdEO+jAmS412sWzV7YJ5roW2(V>$ZSlk6Sai4OLD6}fyNb6V#TlGY z#<^HD&v+MbbsoICphk=LgELAGI?4xm}~X>Wj|d+!_)?nc#sg8Vn^BrH9&zhKu@k-6$FfohTX$4HS(RjnHG) zC?uIIbPVW6Pn^C{Xqb#>qnfCmm==_$vVE`trHZZ>&4w<4e=BsU=uXl7u$H265_{YE z!8~bAdxQHKDniSl)gr#gRAh$sETrb5twnj@@RSZ+F4}=SIdnC&9K2IxFY*=zi%N>h zLD3@}l1vslO7x>TPG36G+Nd^YqI%-Aq*GZ~u*^!;6*U$$fqw*Pd(m-SOZi9B-oPP- zid{>)l;C8Dgw6!>f^z5#&Yi)L;3RmUcTENDA+oDx*Nt6EfhVEDtx6@nCi!Jzxr)+;xrewEloOFvettwSzQ8K9w0{ z2bF{@jzOk@9&!2t=@D%>jx$YzVO_eGbkHZk8D`qXf)9d^AvYHE1?NFAEkXL$<%7S6 z{h)m)5_`;N{8ok(f4Toff`fUc{$~DF-Vm>;%Xfnz#oy^a7kBjO=gyC`t^*7y{vH02 zxMNH|@A!~)t#IKPtD64$z3|ybf(qY#-&0?s@2Nl8m*s2nC$sZ$-*LYH>b|c%%)^zy zq>%bEkaL_JO}Zpx!DYu9eVs9F;5X@dqFVf?0(m%g1Y|0q75k6kK&{9V{6_=!fHx2f z(6_g*`mX*%?Gn8`q(a;An|_e+JO2;Z(iC6Hb-MQC+R5)>ES3TL2*BQGHP`P)6K!N|o{sgpi8dC==YQxg^Y8QT_aF2(_z(MA{B8bY{uBOg z&}E=|{Ac|AxbE=}FnY*;$$!~@)qf4VY5xr4)BHLAZH9Zez6bt8{}YCL$fH;UpJG|Z zs{e$>YbMRCrE6yYfR=^xR`4582GH{0NwR-Nv!m=Pp!E_`44N2pqm)ZM9Rn6*~V;Ld)*ejo?~>I)~J>H?*R?E{v)uBqESaXY@|4@ zbuzr{x{>Mv^lS7@n|2b&dv8SSK|i!6vb*YrK=)+Uj69eIl=PtgM%v#9Hqc1OFfJ)-(i&m9WwsrF2JuDyW%X8{4{ zer+(2tfdE1{R4pvoHLj&>_8i4qCTPfP81OzTkiwwd^zGXf$Tu8))>fNFTdvcVSM>O zf&X$q(Z+E<#@T&oV_0_`^D)|o=&Q9pHxs?58|3x;#(>j*FW}TBf$N|nzNqv4%UXHB z2i@z>`WTG*?8^|jrxE#I*QYo5cp$_O@$m-O-w^ZVfX{!Mp%i=?XK37@Xb4pL%K|kq zUk)75dIKRwQ#7D2$^!L)L!coOXx8IY#MdL|vOuejqZEOTb+M-fG=(060$tEWaSAj5 z|2Hu#vhgz$9XpYjrC-P7qxmxoFS!n!hWzQ+*bAKX9|mG$(TuTXPLvN@!ejL|LlgBo z#JW0+twt?K`$KD@{*5t6Q5W@JHip;5vYBYiirnwWI9XhWNDN*tw;DLl##1aNUxF_N z<6^=9V!=Q-CIBz-6aPcpTObz80`*!+pdP+_A?nl7xCLLn80X95+E`!|*FAwNz(h3u z)A0!eru>%zHxP$<0yo!=@xV%am-#HZ&F@8`4;8rNBDzZX~nRv_b~(3*l+PX&`A{Sr(G`&cjyJ}R>~$b1X^d>DPR zem*(O?vWl#63vfsu`tLGy$)uweq1LX%<(^=h{V+3Hh&w#b}c=)-9Hc%aTZ}0d@^W- zoo@f7pay@4To>cI4E3xA_l4&v8nc7@g9o8E4s^Z=HfS#*=a^lAe2Vvrfx*M5kD>)O zwa_sTUmu{~muUXIU>k#eUxLTNV+wuB>W9HtkB0tbj}D~qgcOAL>!>8;6mj7$JRl?X{|R%W9u1)#o!zT{lz2u(j{Tv zwKTCWTE51WRp#X9D@ieEe0$3q~RNeCGsi5uMEE;-!Z&v zc$b_OhJ+#VU13JJN6xUXUH*W5=W^KiizI`au4gcGr|QbxhQg7XA01>68`0`uVC#rXm7 z7+3(7fEB{2gb+0eNCDD-Odto?25bjJ$XFQ&ts@S!E>3l?xn4utZ%4t|??sVFo~YW| z@;cVnNz~$4zU_dGz76~MKbaRh;KkH`4>14zGV|Yq?7hNYWB>l#a6Gmfm#~ADrU@s_ z;EWx#gg%hab_qLZ2|H*B{w`tnEMfO7^8j|wGIq~0cF%Gauob|rRL0I(#?Dzr+hs4| zP6M2YmRs{0M-b%kXm%P#2UH;)ya$Y@DaELFA1J%xMZG1U z3KRwTK2Y0~?Z~&Hz9UMzGH6*fiW|7y}_m&C!(lWQ{>85wcYwQcaud{FY zzQMlP`|&M5+A>Bm*q3;-*w=Kk5!q*W`u9*NoRjQ_l|fiRiHX zz`t-UMPZ+tfX2VDH-7dR5s>Wp5(wA5vTYi{%7AwEb!8;#AePl|2A=$f5-e8;)2KSAYOBUStLHQ zY_`CMt?=Pp%!hX~AEsZec=I>NZfdcMS?pmJ`orr)bMMP9Jy><#p9PPSux zu^sD*w=g2!>L%nfh|h1q)8B$uzXdPekrxK#?bxB`0Xt`7i127x9dAg}(u_1G-IneF z52Yv4qO>fn%Diln&2l=9EF4?We`)+X{5$BsWqz4(!p{+Bc;iovKP3jlmSrLspCiH; zH-CQf=SkA$?`{4b*|cRpB8+LvBqB^Qi!htbpEiG*q?orM&TKK~o3|6Q`E%yak=M#jKJyShV>#>U;eou8-DaW;K0`)s)I=N+Vp_<2Ri2*VNnuL3EKa2`?ckNm!pr zh)fb9lZ415Au>sbOj5CaO@Rm{?Tfpn*ssd~+p)fs4l;031K|sFY@cGFIwN!x(qW)w z9j}e>zjJM)EagQIr#F&Eq+_~Gh>}vbZi`jV>s})twzX#spGb(R5~8X!pyS{x0RARj zV*L?qzYZc@j@r-a*Acdl_Tg0>*P`n^3SM9H@zQPnuDC;#MZ4$~gJOwTE>?@RVx8D1 zHi<{XcJa8_DV`L2!S4hAoH!^BizDKgI1c`#cwL+oZ;5xr`{E-=J{6yd&mr>yGUbv0 z`A#WWERj+rLCO%lQa1E=O1V-#%2tcLQi1qFQY0r*pA?cxrAp-1fPX-$k?N5ilA5Jf z>8R8pbxEhB)6!Y#ymUdj2!4k&DqWE#q$%l!bW@s_?n)2D`_f~`FGx$$icI7rIYqoL zr%{fa2~Li@P2MhxvQ>6V^Rgxv%VkLS$@}Gl(gV3cJ`9-_xlKMMpOCxd9%)oQBTdNt z@_;;~&mjHsLC|aF5%iB;enL~)Z*)$fJx1phaoA#_J!vtE)sjMI7pWtfZbh2M()74A zGV^SeCXi;al+r<#MrR{w1!*}_%t=TwE6HxmOIzh)q*+M4mQJKW%Sp!RWt_gKwlE#E zoI`rtGKk#REcTK)OtdShw2evzZ4sTrlzg`CawAL;tx3-+=cN^_FfT&S=v%qU6=g!1!fNxT zGA|!f0Cqr$zwRmzQ2R}^`kwMwtj0QWNm-Fj)3vTtuclbG(siW@Pe7VU*T-U?x?L45 zovM|trIjwRldh-LeR7++AFJboVy)Vs9+ocXw5YaFuT$Hk4n?4CRF5IIgN_>Y1ocm~ zTkTQXlm+Rc*e-RbXT(NjfsP%upL(e}p!U!nP={#$D|f|5QnPwVy)3P$SJi8vr_~us zGyb$X2l_VpVOYJVK9t?+ZR$(vJ$jd6&l6_PqFk<E*zKl~?Aa6{|^^5=*RRYr3Uc zUAAUP`PQx0Joy+MWonsx!n(sMtIKr6$tR$v$7;v@Qq&=9n4-_KJ3EpQRJ+WxZrQM|;US$ml+6oz!6+ zmJW%>ts{_`u#T}F5NQus$0=={R5L+er%?qxgL`3}rL^^ysa;oLR!T}D5!J*K5-6t&&7E!dXC za@&f!U7EKOT8ljiQFqjyB2C2mup-#gM7uqc#whr(?!ERL>b=T>I%MA_O@#d%^mb{& zF47ULPSc2FOBKiLRvLTlZW`_F8jZ2={B4*E;IZ~%^wO;O!d?dc8Fj!Kq>&$b_92EN z+S~Wr56aWFrFfgICE_5Z?G3PB_hh--DvRaz!(uILzJf7`QD$#pcDB*B*pJb+*iYEI z?LAVn{S0Oe1#?r-o;sI>u@9+jVu}5dbjW@g{6_nL{VM2dbo|<<=`4eB zP3aj*+vjw;OFCq$#GKV`ziq#VdLPEyV1GjAO8X+6m8`S&WwF-2>flwY!$f`9c3Om| zIn3gqBi;H`dhEyoAO7sv8s+CvzCDN1wq{1R(Y~cVEG^j%Id&)y#5>AeI@391@s`7G z9g)-2fiO+|*`7l^8STZaXLsAX9bS9RXCFF(juJXYLvy*D8K!NOj%v`gjygJPI~pBL z@^;4&Mz_=^`I8m6glgRZ2$tsaa-TDs1$@24vYXH7gE_5|kvRg-(1^>lPQ57`#%51q}< zR;j~zRB4q~q%Oy>bXqKTb~wAFE{vvT=PAUW3Fm2N2rHj1=UL?5cjhvg^J1;@g7YHg zH8w*zN1a!k6V54n11y}vnkrwKciwQ`bk3`b&b#7$brGdHZ41r^DEk;?7o1CKnRCTO zP}hPh33qVBm4bC3#Rl&nf~#9T;p%an z5j$P|SRYPVpSuPqt(J-9jwa~zLeG%vlIybTs_U9-+BG8`pgkvRo6R~K?IW-hE=Fn43cK+#R+#`_e#4KaNin=5`7t>X=yvSmy)jj4O$DBB6X$L*29<;S8 zH`T-L>+V_iE%zPwefJ~xQ};9XbN366;7RtR(sd5A1oGlJPlhL3?DXVfopYOxWvng^ z%Wg%mW<^@z$&a*XmuGY6Rc9K>C%d) z6zc(!p0$N4g%vNbww! z=RD1xR?ktoMs=1-N8O#C4mHWsWu5e#k|rFpp3@j{KJ}q(Now_+l{y@Kp7YXa&jrs# zczYMtz^75ysOO5@;+c?-Nf!~TyQMDA6!sdeo*PX5CgkTmcRde~K4$5H=YcfuS@NuS zi8slc;!X2Rsa9#;n@Lm85_T5--W=PKcbj*+YY2SNc2Vl`TIqQ7T5XkHw^zez_N=#9 zTJe^7YLqVTKJR|lfcGF>+1l#2_j((gQ_f1Y%zN0|;%)OD^Pcc_%c8f(krhod=`O~5 zMlSaDdk4Hj-b>!g-mCUi?={uxo%YUn=e)O74e7L}#(Ph0@IJI1Li)r}i*yn7E_+uC zdH3-`Q=u87KIFVhW30O0zN#Knt%d2*oEEWz!a-?uf-CsCfI9YgI zYAu{CyafyIr~_-X-7kDp__Xj@;q$^5l=ca}WM8T;!?uCQ=k)n}Az!Jl z(pTd<;HwvheTRI_zE2!6ZWzlh@ZKdPL+OF^4v8wSeYkAUnZAVxe^)4-4lQp~M4L^~HJ|&PP zA6;vOe-()J9((=}-sx-ATCG;6HEK=Tk??bdaF2zbB!u_uT04ECAHEmbNjX#N)t_3h zXB4!Q)~B7*2JLg&ur{KNN%PvcXGNRTu4}XOw`_&_5~AoBnds4~dce z-??n|f9F2U{_otjO+VT6fb3-dQ!c>%r(99;e@mVt`!;uPK1piXfAITq%6Q5Y`?`{y zY-9gXEJ(gYvPd2I22ser!?BO_kZ+SOkn`j}k>4d3aD0(mBqL-$`8#rze3?v>x5@95 z`{aL-uabWtPs!KFGI^K$3CDBW$-m|7oRf@jLGGL6N8F!rz2qG}#P24*K`HSQ0R+LOM+{b5pH(hs?BZCWR6D|NcWniew-a$RJr_vY7g&H+SgG5i#SXU-w*rHCo7 zrHC}GG`2BvX$nXYa}i_9#g-zaND(7aoTSqGKKI?fF8j0AUT4jkwbx#I?X~yJkeLtbA67T(x9hj7Tz#lM zT-~BOx}$EBad=hrgXdI#g>PP*>imD>C5zP5l10NdmMkr4C|Omqwq!%e=8|nCJ4$w! z>?=7?(p+++pep64q>}opPQvwrF~-SWEY0_aB02%gr{^}=ECRH zY?%vn64~YINtqX8IT!AeIj~)QLT15F)kK*CZ>mqK3sNT0NB2uCty7he^^x`JQ<064 zjcRgaQ{;#0p~#Do7uBaDFGXHb4@Z6+`LTK=@=D|tRUO$Ec~yNTay0U$`fTJkk>ALi z!W;Lzzn&X{@AZ{52^{pF02~!Kq3$d>RMJv%tmI_LnUeD*mrB}7uEkW$ie<-gWBp=- zVui60u~^KF-5DDjyC*g=_F(Mc*p%3`*sR!`*b}j*VvAx+V-2xYv9+-cvCXkF=8dt8FzS3X1qTcGS z5}?!W^vc!mCDLt@;f0SifHDGQ*|K`|^HxxvBeMQVI+ir<`#u<(4PbwyFJ%wMcFJ0n zRW?oRf(<=^cjvjW3&4hS@NLNi+K~ojv%EDNe&IR}xrVY;fnBu4zw^w#ihs!8A+`oJ zV0Y|r7r=P|NM8bgZJqb6OGnZ_r)1v-U*K6N+HRFGfPb!(U8f(#C#W&DctVPExK@fE z>>R(&vj-qH;Gdwjqp=luiVD5(E&K#qQQKv)%diD8f;-);E9~)X3w{Y|*e^bj>!~3a z&!7%$M+{+n?a8I(xFgO!;=G^^`sUZ^Pj6Fp=5*~#rmd;k_e07b+r8Re7HhIslpYG6 z1jA44n_pu*(tl%qc)P3EIgY6S`p;wAy?zHZ;{tIL)cM`}k9+n(e>tDh`XBtp{9^3; zW9!}H_js9gN+#n8K;SRL-HmFy{evCvBA4m!4^TU1+V2cM2R;j8=x^0>rIPyX^zr)r zes$oNOkX>FJwM&3W{$)BYVBGFvCJGP{R!eQ@Vk>Ao5#4D+qF)`ugo{{m;LQJ(~qfk zGUDDDBG=r*OZNuxjyPv*?(ym%?t|Z{IxX&IjC9elH))GF^d${Wy$*kXRTvOvaPSzpmPf!OrC74^8zf*HmlM{f>#IgGMK|oRq0HuVL_dWBR|Js{p?ahw|^B$y@;h6 zyNN!ib!G-}gy(XM6Rd@Pen;+h<~s`m8Tb!z!l!lqx(_=YxlRt|UNF8`V^BNmolWWa z%-QNR2G8xN6YGrcTRz9b8kFHbKG_q0Ip-bb%b;(t1AueztdCFnL}xrZp6fl~!}C4w z>7MswFaEGxTTa98RNnFPMk-E%+$nP)KFhnl4sv}k7lN41%rWsfVhemO`91zbrwze* z@u%X8@Qg6EzGddd%pAA7>l%_7%iY$+jP)!T%Rd%uLiJ>J#%3e~E>OC5h#U zl>qL;{W;2fiMfLQioJ<7iS?;5!dL{p^!LQ@5ubR%7wDhxgXfHRc9>c}F@AWyD}L&t z4|T@AyUTuCe4RLyXkl)Gem{;5jElgZXDqd^gIsrG|!{(A4PD_sBl z-GA}@mnEvhm|*;)_EFw#xu17i9^&1WPxEfeBUZlkvi2F?L;3eeail~q!rc>n1g>xO zJ8<_zzms=QzRbHPPx9``Kk@F#8s0tm4ey>D=iQSNynAw*cTX%Al7Lz{6 zqxibK*1nT?G>^6skLS@O`FuyGHs5dkL;N}_G}L<9@J3$ECCzvFYVCZdXN>p~F~1z3 z7ZFzh8&T4Gh-VS=3kiA)@n&LvJIu&tTa#!HzvR`vft)RrsUYT82-^8gFrD8BG|rPQ zCtXQAg1DM^IWgr8_TA8d%?YIGhxe$b{U~YL+0Jv@srkK?#BBFHTH8)LJKm*jO1z8F z5!^A8cmnYmV3S`7)|!1kJV&1&q@B;vOOScW_ug|H_k*3&#ulIB zm_Ely0Dgk!<`CDAvx@j6+vPXL4E9bNLz-SSIMyb|-MH?@l8m@n2s$*5M@N$8S3At@ zlzfNyb>hd#e~h?*y||0`CgP7$=EJ1lq>N3@|04b_IbS8#i2n!qKlIOqC0}6c-{jG6 z6F*7LFG&BDnBPM)8QUh~-&{dGm;Gpgp4~i)-Z0M)?+3PWyqJ-Z#OQeA0(`#XGUM|g zz2uF8;OmTbZJ;?z@EvAfd4DT!EBJ!(2gvaK;JrhS3FpryzsuuEnol#XhJ$b2 z4;hQ!bq)OsZNS@rK4HxPr{lNKW(@`HaqyKfb&^)O7V}ugURsNHyPq^)g*OcNA!2L- zmRJBjP5cINJA34PLoJUQ_*EkrVVPMW`258Vytm^VXmw6|+DZ zZ2~QQp$uEE2{F@BrUusPOUb!J8=835)1mX!Q%n8SCNmrQH(yQR*Mf zcHxa(q}ymug7jO&gNb*c6}^Z@hhdbBD95+T^OfW(=>_; zAY-GPI6_aZWh={wA12;HJe(10(PL3+*npycE8z9bad>1Pje~ z+kXRVL*Ig(p~Y;mofxAtFmxF`wGg+3q4$u_L&!f3heq z(4Tlz?CH2jJqq>+=Uw0k_aPBx&d6}Kdj?rLQbxLwIe#^>aTsfp8^*T~|JAyceJ?;C zNw@Lamu(@eL2c-Z;15BAtc_S}HgohbBb5LBIPOMd*PwOmm))pfPf~V6vWxS+Wv55T zX(FCa+(!E!<U$=PCXrkRpda|vk`O&_)sUu)>_jxZgsqo=BQ)0QCDfK@j>TvK>(Ap{{maOu`V*Y* zx?Bfn+5=`E;Vgik_374p>`$zIps{1oXOVLmE$Xw#X#vOF&h@LMLsopfk({}f3z{)84_(o;!K<<4z2=x~IZS5xy^ zo;8s&6Umw2t?9HX7algMIU>2#R$)CaoPG3d1v&Yl&tV@NB9=cCtrg$=FS8JyJL*yLeS<}N6#G4V5 z?f7D7efTOOD*PID<6f+W>k*&Cxz;ZFA%}j;A?HDif{Fef#SGH(sV4?W!I-n>NY8@2 z><{0ix4ok|q@iIJN3$G!iH97n&S>jA+d5A_Uk5F7ffyWf0$1C2;H7Xr$K@S=uHB6* zBY&hU}Mc8YJhDjWw?eM(ydCpMJ+auwb$cG_MJ=4QKp*O@==43=g z_!4G;IhiwJAAD=IVO=x!G4|TgFY){|#CiAuYU_)60c`A~&ZXf6@Hsfzd>*~z&0p}f zXUKoX>k*Hx16F@z7TU!)ug8wU*os-K-2(e%uMYWpVY!*~<{IreX32ium`C4M@aS>R z24s7Esh<-b#C0)!G7)V_23X9#U|f9AoIIFd=;U?eaadGs_n)r^)l zFJ>6|o2g+5wXI`Y6RDw@+A0u5vcvp5b1yvk8lqQ!jdQk*S*VTKrqX&rY|G&qXwi++QvRSvUdHCG&D?3JV|*lvYKvqu#;Dx>o} zF!_6k%ZZCl!2Ygdzt*uAn?0IQv;_H3qHa0X7xcwCO`EU69;=uUHN}r%T+hD9 zRc|%*TtzEZF*C^&_OTs0tvNjF404OL2Iq#hF!ERX{^vNd#Z#0l^!&q7o)5nCXgM?L za_#{b37}=|V;q8({QP&EUx>hQmz0Iy;i;iW-`MLuG8G2pVLD=5Q_W<}yaJk=n|MS9*MpW)!XD zD6eEsM^eu$Z{?*=uEF!N77(Ldqc5{-HT)TB<;?2Ko>p^q^riQzJ^RVo>17_SVyC>U zLjGlr&LMx!f-{|^BjA~`?xTa{^C|N_? z%&5Dk7KGJ!TnNKc_v%{*#)5lC!!bCZ2v&RBX8w5-4INj5Wc zjAL4Xc_%vM`p&BH@@MEd#1F0+Wwv4$cPC`zird>gV()aye2Q~q8ZANojBpjc#%OQw zJt^ZGs%6H`=AJE|)^79Hh{arAyj?D=`VwfX&7-+59mI3Da_4g%I|13L%28`RXHYG* z9rN~z-1QBhXJ&OwX1sCtx_xqs5@Bu_)FOF>Z05pUrT5@IAi|ay|J5_54Vl~p2 z{Tf0JEevzrvBtm>X%U{u!6<~b(DF<4TQ&Fjm%s^g6vABV!x5Y%IR|GQgN!wYXGJ(e zdDPsGcnI^3in-t4S0i_=VdQ6X9%a+FXE}?9d-2SSbc~rK$Is8m+c~s>*CV}l(}ouI zmpj;ANqU1jSc~^JB04x$Kgu+6)b`M-0lY4l&TL%I3{cM%B-^`w@$#Oxs!}GxITD37 z$t`1;CA@rinNfO%v*R*#Vn&?su)n{`7%KxH~$@Oj5?YjiXWKnI?|%dG@-=@96~0eP*hY{+jzd;v6=F#*v># zPChw#%roO?=OVT>99MR7?ZD>|kQ8}DSClKl0LYjFI4_>!(UV@Bb9_g0Jzc@sI+}ji zL(X=3ppEnd_P5Qumh|P};VGe^LVwDf2%1@O8~b=SM=IC5TBIdMc?Z6g`j;{u9%i&G zhBoUVWO!pSV`DL{K&^*xeQGS`HR*Kg56taf1>M2xCUDHD7Oy^V2T$@R=IoNtouJ97 zqRe*6RCRDy%zM|zfc1}9UlX14twGG^h@Y(-m+$fF+YRAbW0v_9(7Zl<)e_CdS3}sR zy-Gg}4msGFn+HKd#%xE67WWkOMruk+4W7?BfR zh+Pfy`5x|$s=02)xQabaOXf57KI5%WTv-l~|4Z*aoBk|Ty3l748_duo%wPMiPM&sYmQ$8CS$en)<Uo`b5&QC8p8FO# z|7Ls(D=e*AL_8XN{VI=s7uZ~jUg*z~vm7>uULt=kCGQ26F!kG@UG}B=_jEjuIH~`W;D?Q8Nn>TNF}@>srT%@v_v_dPZq)y`;IA2@Z#%KS%m6r2j(tOQgR< zdMoLzJohf1dl&f!$Ugu&oB$n8kpBbne?WRI>9wSvB>g05o3u@O7U@~E`DNPtGWjm~ zF8jNT{ar?WJ^A&ZwWFZ5qm=m=Wj;nd^p8RRSi9KwT|Bpn=T=ev4CT*Ieg)-MkpCm{ zf5iT_vcIjAxk8yMq?eOkPMyto1{!*Z_(kHofXzqQ)}!>#MEYkU=`7M&Jhz52HRQLD z-$MFs(sz5Y5#d@%orRPyq@887v&1PgBFwUKFwQci7fD*16+Pnq-N-$(v^?9sE-`7G&yqz6*Z7pdoqvtiLi!cj_D$OMP3mDh7>tJqqb0&<2|q&lN4%J) zXFl$=LK)q|w0s^dpGTT}lYCPb978^ zbT*LQK=~ZX=aA1nn(U)N8G|xrE%~*iIlE17c0WV<8OoPZzLaz&=}LOL4?W+9cDB>b zcJdifVMbJl5fx%Ynbp);P5$r5{~ga|cD0yYLyU?Lqr#j+`8jNtnbTnAG&$Ey&NYK8 zl))9sx|MovCI1WLe}ViM`7zp-P1~}m=K<<@fbw6Z{8z~zOa55O-%k14$!8ujna4u( ze~A7!w)5QW9GCGNm+_Q;p7PIAeiP+4F&}PJTU6@rQ0*P+9(#wq+umm%u$%28QXjWZ zNjYMlwJ!*L#BLS1BIP=YqdO77IZmE4z==9VPO($wB!wRBR5;_ENzP=Us|BVyGx&bk zdCmf7v9rus;j9)u$~-CSST?e35vrO1T0rd5!i`E-y)*twa-MS3SJczWRokSD;$KXb zt2fjUb%#1CyW04e14VR*;5mF3QEX7nKRf!6zQdOqH zDz1{Mm)KOHZc^i<^Avz%~AGB$8xfrT;Vun7wwZy zKW7l%-5Zm3svK8Zf!1B=#ko`Tc;%8`pv5numj&+!7XC!xJqTSwKkQ6#Mu1Ajn)E>I7As~tvusDss(0o{Z_nFHoI9N-r2oeVCq>ULyxmy*aZ+TLIt`-r zjOe`l%pXm{2+8|+5u^Da3PfYt<@&CWJw2lS>|fyMw}gMhPJ>V=|n zpL0O$zZ8Je>>QE0O-AIT96j!wLfg(+=K|ZO4Nfb2m)^F3)8JgO_d0{nTjx5d0MLJ& zxy$SCe+cU0dKde@g``zpE_k>8uY}ENAV8qPRr8){CF8g*#o@u+Fn_9)!7`c z5PNDRc3a}(<@`zU$&M?LB;yvZ7ML2J5uYvf-1xk}zW9Rp;`p-o3ORSR#BDu&FFsF) z_&PanV|)w!51$Lfx5szd^W!-Jd*V&;gYkLs!}c19^#J^ch#!rgh@Xz1>(=jjC!4@% ziC+Z7FH0LTYVoV_c8*>^%P6|0o8|V2Z*=>*`R)*RxI5By+;VqJ$}f#hKam-i8qfVs zApt&O)VUMfO1H|bDZ41MZv^K+i&N;<+AZ!(p)9vfqHv~L@6NYRx(nSU?s9jfJ>Om9 zu9x`6>_6#lvTNO~_IkI`Ug_>~_qzL?Iqo5E7P&3%G1`fUjJXrMR^2o1`Lc6%qkGB8 zcH7)*36-!C*@@glzr>(KA!BEWH?z?P8NquJBZBBhiwV&H5bWA*o(3FC+?g0l52qr~ zkG5czq~-y6^tx~Qy za}w9>OG!N$N#-Q;k^@A~)@0Nkmn=f2EW2u-Od`t1CCi*xGMOBmtgy?Se#!Cn%H*Wv zB)u1#)CZb@#JmUbrhxa*U% zITMbFj~bIr$%F2G$<8Mw;*gEAnNKAbOI(~!9!?%jo)E8HbF!1Cljjoqk{6Sgg?}}9 zIoUqSbh1ZfC1)g~qxvNFjp{opf7B3`;iE>5az>Sp8Z&C#s0pJgWzOvyRppOC+o+mR zwe&lpPiz=9ldHxi@%1>nd{o`2dai_f|NF0TdiZOc8LBSz?QT`trnJ&)DzEek-Y{Ky zy;zrWwD@S5UM3XZH=P4s@YZS4(X!~sDI=$lS5#J1a)!Rg?}n- zgw)*%e*tl?x=;2WpO9UIr5+GJ_m&a)jJic>V%a5H#J8)I?i7pmNZlm$L8G*EPia%> zL4m^pM+Hs@oEA7Ia8cm0z*T|v(xx&~Aj>;e)<>YPU*`)95g0BoQos=?7Z@WjPGEvS zr9hQHjbGOa%=GCxfqH@Y0tD7NkK;jrJAz}Atz z4Lw1PZ%;bZXFy;}Ce&rXE`hy%oBMs4L!EY!-y(3#w|Sg@>_l39!uNX`@Vo#;Fc5)r(!uk%d0^{&?U1XZj(n|8}g0erML0Uz<9|)^^i)nROs@ zr@&Z&djux-FuwoU_51n#gVq^-?&iA>Sf~1ZgF4g4fuC+vr{-5?9mH~JREq$?M3unQ?x@Iy2t=IM1xpb3l)^tR2kj8Fl6yx#672towTXN!6LT z<^6Kajq0>q)MI_4IU3ZC@1LN~oS#@Hx2DxW{AA?1R9)w{zsZ-Y^yk0y-F~n$j_ij! z<0+NbC3o9XI%VKL#D_htbKQrX_N>k^PmS*xueRrO9UJ=zfu{r(`8qFk`p{ll>a9T; z{<9lA|JbX#_7!#jaE`rJV1wV6%>vs5b_nbi*e7s6pjqIE_d7MOGV?_+4p^tKHt+8t z=Ldbv%pKEwpU>}VLvWsbT;Nn%%-$#m-YBPK#%g9xIxBD?ZH;ObxFT@9(}zLZDV;$) z3D40zNbSXf{g}Teb0PvczVAi1lULfgPZMwkc)FZuY1e&EeP_R%B0pB-w^Ll|&GjXn zvQC+2my;A2?c2DrJ2(|SjQ43oH{@A+TCtoxnzcEdtvGb_(qA`sg&3ruMa1V;q@t zj?6>ns24Ae#DsI&gJ93fc1(Y+tnqC>mo`Q-(|lQ#=BGWr9~%8}lK5~i7FW|^t;+Kc z{M1EXRcZHqclv%CzQ+96>g%sei=o=icuDP>Yy5Gl^W*VgY3jNlH4pqfQ{41*1@Z33 zYCOyHLtMrtj=bL2m#Osni9KID-)jqR*~{9R^^#KmeMQ>+id1XRuHpW$mZQcfHOvtx z7XUp@V1huUK$Sp^{~J&%Ff;8ppw5>8)CWdX2z(flWTl z|9^n30*$^MyL|ltfVbC&{XQJ>p+$`;I$Ct1=ycJ!qKieBi>?x17Tj5`7PY4+DZ|WR zS)hmYA?`aYU&@eSLwIbsFEeu3NY)Nt#}k)c^a>e zmr%v~Nj1Ki)(C$m{6Cc$emVTIx;6ak@Nv~Qd?I{8<%ds&Ppg4^Gi{V_rY+2RE$d%Y zY1WafBg*N$toJe%&z_q7sB*I(%YIBHv-f1bszzo1BKsHWuK&W8G%Z(~$!8F|)kl>o z$`Q!B0Spj`_P{TCA6~IQnLx6KW21ZE-v}xM#;cDG-7s|Xe{$J2bcgspN54)|7+U-+%?TjKlw6MkC_4F4{CMn$s@XT70@^j_Ngc~z8M zn_a7hXE$XxsgGpu&)%;_{D0Gn|L=K3WyuImRDG0IC^NHAW?5lls*V)yBJ?VhnN_%7 zg$rdy6}Bi{ct+tdWfq=PA)&3pGb&lQse2i8ta}+y*1hy3*HbCn+P(C5yO(~Kx|hPn z9?GtavRBIfjB-dyOAqB(Mmbq{GSl9HlLk)eiP!(0uBGry;hFqJ-Am#5?xiQW5A44l ze9*sN_cEZOd+F)ifm6Ge!b|C&u6kZz`9T?4@ z)qezd^mRt*Keljh|6I|tc;Mops(DdqRmnt5jY+_6+J6;v(y))J{4^xz7oBT--C65h;Xh; zoijL(Wx(Jl>!O1E!NtO%!*rCcazEx00ZJ*pS1>Q`B?s^DxiQn0G; zT0GOzSBr*-{zLtTqUuLF{@?6N`b$dJ8}&wI=&$InC{sV6A5bC2jn(^+-cuF+XL^f% zQJK;f{(pxWn5XnKz;7)Bz&FwWy!#7gdu7lSujHTbO5UZsOSx{YTQJuzWquj&m$?~b zo>#oH2aOH%1f`q&f`Y{b%L-NuoZuC-7ql4^v8Oe#k1ek*SU1S^OY5L31siX>bXz0J z&3g+rdgbQ5UP;MilvFRGT}!6DfjfP?lk?M?YPC|e4)h_|)`a=F{Qa`QrzzR7|aehK`r(=R^%=1P>@DYs^OCEYKu zKIj+i5{x;ie_ek;=D@f0Z>wItYkN;qS-oHA{esL%$*cN${bzFQRsH|6_buR66xq6~ zySjFg-Mc$42qf8t7-Pf$F(P6_#0Vio2#AP?$T)}*fkDh5A|fIn0`iCfF(O99h=_p5 z#E8f+4mubkA_8JWLhEGF_~1I@IrsD4?<8NZTD`hzRn_0se?NBj zs*RLnzH9!IT1LDQ@d|apF_D8~qN{tkn@xPzL91z9!%-MtN7n@Ng9W@S!BWmrrAn*D zmL*t}R=_rub5}wy*wlZumMU+4LNE1yn!g2&ycK9~1r1&j257H)k>4xj)x1mB!QQ|G zCI4LA)Wqk3Dc=mX~I~tI5it>7mak^56bpQ2zS$*(x@6Y zEp}{FO=7m|CTw;F#1|)KI|G~wwcfluaeVYRXFyB=%k-FnsLI48&CAtR*JW9wN+eVz zj(2=cX-t9RiyezRJgxKWxd0 zFJ?oQQ>rY@%M+@Ug>$jRDN$wOQ=R7V#m&p3ik(v4p72vGOxUdY9#f$D&eCPuio|Tr zQIeR=eJx4MjqjWg&UtH;)y-1%$ER}NeX7@vFR>i$@!DLf+U%^1s_{HK=?XoTE4@}X zvS^eOs-nlaX{x69vc&AfHHk|SUt{FUxc&iX6_Drrs?F(Mo4K|XT&lNI!tJY2EmV29 zOny{Nd|7lF@5iYz1zeX;)#CW#2dTP%VV0Yw5L37*#D0k-af3oWabw~ZZeMlcuK3PB zM*hc^#0?7h#Jvjngzky^8TlVu6k-*P5mUWL{3@n^_%y$;kXA~y*)1-)xHRJ=DlC(y1_io(NXoCE$BO2HV1pD76$uzElgYy-%<5F zA;GB#4oFByT%y|e!?CLRrFsz@|&sz<>R zy)VKUpJIhfu*AbTA)L=H5#|%-6GRa&N4!jizsNs_O#dVPN6F`(?|+ga{LlCoQ>1^1 z|2c~GKkr{gvF?>_XUr2H{!7kF_G9*A)Dvf?UVMKu%c#VaG_Fu%Muirmk^WOlnJp=g(JqShcrqQ`0Qpp0Jd-l&}u4^AcMWw!%K( z+Y{T8x6KMm@vREW=5%=2ys&E4Q$|1lE&h5j z|G~9a1?|UOiMpqys0n1~MKsb+E#?oltNd>Gz}T3W0=bSyab9dk+5LxUX#3vi#j%~E zvwk+!`J;BkEQ+c7ajNpg)J3OZZ2xj>6e@u;UacB5p5`{Z{T4Y6i=mfhk+rb}F}-7l zpIX#<^p4mO6BE<{*n`+*5f#ZGzszxrk$d3H6#inkDj-k1tKp&^pIYyt}+rCWpGp$4);LsWV0xzZ6s zp3ziWw?HC>ych@Bju7hxF-xnw+7ZcW8$7(`VFG)Vie)OsA!ah-_FJ9dArCD~L8MbJ z0{>3vX^`cJbB^}=BEda}>ho1ZAXrelVd?fKp%xV#;$>CV>Ze*IIzg5}nrNrjs{Uxn zRJ7`Z*G@0~n2r_STYWQ#7Op@GZ$jA$C5=t06fHv8?eHA%Jj0R)9axF^)RFTK)mvFw zK4lRDX)Nkh`Vi>5X@J`M@Q;W$jHDOetPpcnDoNYWuVie!92PGkSs`);kaJZ-JUT{F<)4DB zg=lRJ+S36&Dpt~b9i{pqKca-0Phl@#23doxcSAmft(PHZH9V(#V-H)~LM}mFCPv^_ zgy?g?d2W}}eXyrTm#ZJxyVnTxW;-Z7#^oj{}- zMz0LW6awujKuqZqRn7>^%MgzF7sApS;`Ymyh=Cff)DQQ?hb>)o#(vgtabnIy7${zcis8z-m|;Ax~f{{Lr+&%eKgF2 zqV9fX&JO0QkdXgMwvD;DuSm?-myY%*5VJO3MmS$goo&p%{rPS!k1J%XLC(?Dwm%gA z4OH=-6KJOxxiRwmRZv8=aPNnO%gDXlj})LYmu&5)cS5;XV3-l2SG*f5J!Qq_wMs?A4W5 zQJ)1Mvo*SCUg>eWh!nkU^V)Bpc`1AkAXj|H-J~(Xs~?)*=~VyFFEyQ~7|#x-4{k%a zB!i%zKT++UD4o&on8%mH@m6<9(SYdD z#K9{cSAxczV#j+eJ);B7XiUGGemy<{SuoSD>ccnA63^=hksxy8-RK7^;|A;k=G*lX zI!|Hmioe4yfAi(BEA9PbaIEk6F7+KBBGH}Xx(Q0q3r(Z(wj`-xl!#U&1`-4*(#}9- z-$Q6DsOin6F4801IB&Nfsw*MymlK@w@#NE*u^?YW`cSx6I`Y5Og(^<#uijhj-k?<% zpY%U!?9y3;2t*e=f0E>4vB~mGMI?s0;l?Ar#u%1|16>2^Y6w5bpqMgj%g+#>jED~% zsd|4dzP0bsCOneTJnbCDoO{4@fj^o_HwvmRi*Bzbo2z|~pTZk>SS)(_V_AqNx;@&E zB;mR=`rwo)Z5+Pm*zB=yVnz} zZ{-vTPe&?x_Hl&D=vHkV8$5cIi^q@-E9P+MPezCv2y|DFfGo~=bDKfN-sL2w0zSa2 zPoT8NA7>vvO)p4@J5FBdxcvaDNI4XJ<);%C?p}Uh!P>}%r{kUP!9_58c5_*N^>OGO zHrBASxY5!^~dhI4uQ6j?dcK5YRkt<_4x_o~b=!Ps0t!k-ts~F9{tPZ)}gu z2h`>@JrYQBpX3dQ%5f8QAmR(Bm00Ejep8Df23|4ohzQp?S0BgAo&a~(VrAQ_P7>o~ zMXQgx<6?3ifZ>J;-=(B;uoHxFlxW$5^_acC-BW|&4t!dEEKW3I7B{6}_ELX1u$3;B zZAqCdNnItQPU||Lm(Epp{n}7`Rnoj_tb{z5D7L3)v&x!$uJpM&OWK$&m`%7ggHvW^ zb?-21;P9hidCOW|19lEa?lL^L>j49246faENE>3^8)q)l4Th#=eapyz3}mhM&2O9$ zO}V%AA%U?HNC4#4k#7)32sLCkb|9AFE9)<001g~?b4$`mG zkgqmh6=-SbC10uCzvhu{kp{-nz^uTE0tc~5Mo0*~KcjjS9N+Q``gY+dvx}b4MNXF3 zuZ|y+n8Y_Z>9zfP*SBI5VoDZ`TzZ>5tP1>mcb(fwHzNUCQg+1pK1d zk`>Mj?no4-073&R4?Z^lJTZrBiHS=dInn2763#rNX;;C?fxi@)+2>bZm}+I7x?rBQ zuypKz$`XNf;sHy5j?W7&TG2 zWm?P!CVnGlN;yHFre_jR#H2>5K(s|ZMkRE5Cm%O8JKzG(ou5%EU#ha9%EOx}YX2vF z-TV)C!KtjAM-Z>D4VHc49;0&Y*R?PSjtQ$c`6Zy$6p?-!)1*v%wC9P{M4XDg1E_iq z>E?ljQI11NWq3h!lsGvqHO^mtl3T0P+V$%)BXO#&+k1WOw4tLOrObmAnkvQig7VLson?E478Jywd1o#+-AR z3#uE6tK8`#*rvKlv{iner}`+oilF86R6Bj?8z(C#m50!&Wc92)cUF^qckODy(l)I6 zbmgJ?jGaeQzE?oobQ`Ld@8z#uxna5ioz$kU4jQW)*#U_I$_OZIj^;_uWlE6$m5VQ?R}ktX2p*j`X*K6?KX z{G@hA*!3R+(#k8H4B`l`!(_>rdtXVLXCuj&$8ON<{r&uAr4Iv!(onNqLI2a+f(Ac? z1E-FN-=ic0%{I?UZSEN^66E~5F~ZtMG2+1Gwf9z;9W;9ar!iX%Jfkbf2h9F11r0Br z1^v&6l1EQr8Lg)Xp(VIY-D>@V%pYXqjRAJHe%XuL9_TE4tx-D6yyUwNHz)*EL2bl*d~jTD98vts1w+; zoCl&Ahb)Jqvx;rGr1x2r^jW0z?V!%6ygEh=k+6LyXZzk{qYD83d0Gl3ME!`1u!oPi8S_~v0ok;-Ek560Wc;T#_gkxs&3#_M7_y- z2|ygaC^Q-xe){{YeT0|t(T;nG(yG$Jki`ZGboRFL23a~d%`*NP=Yc)4ljzpT@+%B-NJY40d17(UUNEt&kI70vGJ z67k$(sEF$=7z_f~sBJ$VZJ;5bmHtyMWQH^=2dqS5gkpCQsT<4kXv zJ7w1MNZk$EbVofUzJCz z+zoU?Gw2fmM9m*Y$({5~Z0aji)7wR#5%})gPFt*PPONS=tX+*#CVy38U3`zSYW$%6 z>~7_2?JK=8t7I?tK1(>X#-Tep<<7qT4F#*lARAK0=5$RRc%_>bF4F7qF$o}(c& z*=2U$mNYbH(q($*E8qTXW23cd%LZlDaE=C380aIXmL4mpnY8!n!;yCM%{Q zlQu+2erEFvh<;N{@mp9@UIXN_owzr%mMl7~Kq#{)W|%RvLPF>V<6LSCnI4u}kqXRm zv1Uz%+3>j@f;?qg%=BIkB?kBK731o8D%8-xO*$t@LhPH_)>-u<-y=Aij7yVT-hxqR8lgpUP?_M!&3agA%12mUkxphWCF@ya(@t0Qj6+Zf4(${YW zN!5cLkdDEQ0nVMJOQQQK9|ilwvmvzYkW2FW+NahhRX@t3ZK>=7q!`bEQ=8lR$0dcO zvBj|^#(DobgGPgfSK|)*4yz6`Sl%*|4vP*4e@lPc*Oiyym*VH(XU0c@$CT$xy)jb^ z?9h+%qg)n0Sr<|dWj)NfVS#k4W2t8eJV=@1@~QRnT89;m>f8hwv=;I8y$h$uj?p}B zqf&FOzEx{(E4ZAaa*l!aaXLHTBk&&h%x~WhvzghM^{n%ROgHGt((~Mtxz6-PqH0yzUs0)vQ&sT7c>qX&RoOYOhn7@+E zMZ;a%;(6#x7E6WPEM?2zMPs{MS)25RsPDfUb(TsiZ)$It^-O(@eML?x$Y;@8=F1wa zOWV~fW+o_R5s%)RX;D?iyOwTad~u5{Vmn~5l8WGw&c2@5nPhyPmTH+Sq%)0-%(ca% z+S->+CB-PImyMH}7B7Ne4uY&$qFM%BQ{|gkM9mdB7t|XI`4;pWi_1-uhVxGA0*{6G zO(hmUmu6H8VZcG#RJnFji-pvxByN@v;E-;r%p8O-Rl?WQVM`P?G`=0Ni4zxdF~4gwh@HM->r~Q4Th=y?H|ExQIk)evI@bA#&|lrtz2*Yt}g}l5&n&7LDK0Z%!tT?cZ@qBkM?Bn+g;Vn3+&DNMVdD5>Jpw zm~ws8e$)m+z@y-3^LfsB&T(09PYOJxKcAe5bu_}3WG6FRRF0Yz9}Bljt64PHXUy_2@-R+jXLfM==ev#SjKXe!_K5a~fFoXGUNJ9+ZD(x?N114u zXa{b4I(s_6ZthdU;tuXL-nA+CIY$&=JQ}7{qJ0p7utRh7kzyjj31Di;UZsh~&UI_W z6WA~3ieVM+o#c((egxhDi>I&1ol*kFR*vn97*07n2<+U-SF9IzPaQqRA3+WhV7Vpo zwJ4utf%SSd_z?`6ys(cL7i4;-;qIHEDHOZm_j#)HUm|*ymA=k~x*506la}>R){5KW z@b-MKm6#5F_L1j-P{e4Sg+0%rLYY(mk`jQSA{PBDim{}2031~wS+a{NY_X$1#0jlI7oE@1})Pb^#_T!$2 zsRQX(%=>PMfQ|glb7sRkO(q`C&Ao`!*M1QW09j&|_4=C0r=^YOW*C$ceCx`wP5_J|P2g>jU$W*^cHV>4k zexK|PpX~lNdH07*>rWY%&cc*vwSM?ZoOeytc! z9QaS}I4O3>^T+lz3z{_x{Dw77hBe-uhFhJ6w*j4$KRQnl0_ENZ%DoGegAe3?1E2m5 z-up8$2Np613Ni-~GW0X9Ej+F*C9W+tt}O>{#YdkTrj#;VS$2eDHk{+0xmf@YiIYWy zIhpod&;8~*Cs2dxFs4h7$tL1Of+v8FO8-lBs9;3LCHhs7r$igoDh^3_Oz&E+#b)*) z?t0vsW_qt+CE7x_EZ%oG7HJO9h?yegkm`bRV^V5e<}uylP=IqR{W?02ZgfKaHZFIx zFME9ajw-h(_CRx;*!Nf@vs7KOoLE3RQsBMB}rY(d;>J>&4&u1zs+;aYhHi7AgNn? zhkU1eM|@{(>rVq-Q(jA6a|?$)>pt6rE?}2Ky^UYZjRXgMEhEcKD-5SlhzHr4i8<|y zf%yZhy@sb^5=6lftRQjVqj9vL*|NR0brqbqB%8PVGH=N;zyG#o0<&f!&@jK$FyG2B z|G+RmKS4NpNi2jT7vg{Mf@N zg%I_wR>T^}?5XPLIteyKoal#N7G3sQdGJvvbf4uP2inw%+-T)4(znLWfzic%RJ-$8 z$FKGaOC;7ulO54)90xkei#Y*Q&NRUQ6$-oDOjS_>rAj0Gx8$Re9FtM*%MVs_77}FcH+AgSOr0WkFr6}XTrY@*@NAqH zu%9FQ^UpPrV%*L>-w_)G_xpEk?VaC!;8Xv*W$n5B5<;Zfoa>7>3I1gD`pGIpWO456 ziKFD>ZUVL@?qn8EVKb4)qS?6YuI(zEn9rz|JY}|r{NeG2bJ8246bs(qkXzRZIKY@iY{XX^(qt!Jr`38xbaLZ6SrF-$JJ|`j zYtoLJ=o?gV?QHFArGy*CqtCsYQcfybuxy~L_rZK(!uL^pA~csJ6@{v;t-dIk7K}C7 zc}N-H(J0s2tF$Y4y;GlKAKA4MpQze;TDMcH%QM_Z1SVsD-gcC!XG;aI z>4@ppT{t&`?rSj8;l%OH@2#=x>mG+cG0b;S&flVPFJQ%V4=S)+v_%Rl5+{^rX9zm6 zTK1Sj&u65F+WFqqc_MF5lFEmSf1CwOXwFhu@_m=+cqLq1DzaomW$!YBqB$zroCy&J z@V&XG*p`X)_E(t~R~95jKhy(>==arBF%inM;&ujj!nLX9V~2St)H@!VvO3#2UBh}& zO_JP;Ls#>*yC)|e5tNc=cV_D%iCN^Q7yc~tRN)A2J$SixVP%n{oc2!jo7pAvC@oe( zZ?9=I=RaPV3UDo|vkMRZB`xh!^dE{4R$d};I=25;}aKSDG zq1})gXTs~_!{WwDy`k(J)c zFUYcvEklyXd>)r$>qUG!`gM+m8>24a)iA%fFxjx1*C?Hq9_mQhPf)s~H|Kxta2i*g zKv=v?`s7mC3|2y1buf2<75cgmd`mFMK;pk+oqi@`M?0yKWLUFxem zuo<>CJsFvGJI;+yHJRq9v)Cx7Fe)lVyvCnNwV^Ce(QLD|HyvSnw#Dn#t^_SWR+qUV z&su|P`{ym_E$2n3qKG{G#c&h*DD5>wQ&BtvDTvNMLD1~lNorPHi+sdYa;us#CTV1> z0O-tn3aD_S}BU$}>IC^vGujQ|}0?meJ!B7|Y5t>V6VaTK1q zjBaS_cJ(&x6ueUHR)=Y#WgkAMtu<&&ufaf>s4Uj!99o-^aMY3{8p$FV$=qkA%h_#O zX*)PrRL4O34vsc7^rIy*Et%i9_|FuBVq#AKv2WpNgGChy>4WB49%IiQQ_s;D9xPkY zdM}Sazq1t&-`lOs#|!)Z(2&LY1?M&&qYAdt(tf+kxu>M_^s0Km#9kGy zSlMv;2bnxsM52YTENeyX6|4GWEl8H3!sqTbopzmG$K$XoxH3GHP3Gw|r+GZfcTjL@ zD1J~&rOeB|Z)E~)emTmFaAjlf=(7yZ?awsP5j@&QaGP0MP_vx?;FFkyo1${~3 z^HP1Sd;c-5Atv2WpydtO;~Di8U4$wpyXW8Eq*9`Gn7vji1ZvgcPNw_^xRJ@*~K z#8f&3v{eI+!Hd#h+Ym0Q7V&J{zxZPgCCiHmTitS5LsX{WgmhGc#ih%##gnW$qwz?l zRl2kq5VcFU`ywA5>_OvZDfI#E=JZQlrO|2%JsG>uyx$e%dJ;6s*6oWz*3eevGD*8{ zE0VW>Qe$hgL|W$iLg~^=)m)~ty{W(qlV`E5F-$JDQHt<|s+Nn66d zPrG8Z%TK9Y1CJA%-d5UyRzrQS_Gw$@6DagV7;H?(n5M9&)Lc`G%J-ey!(8KwYz{_% zNC((@!rHlXQgrNk8V)mR1!_Qo0}kuX#H!gPsS#9p={ZF_#p$BCxU`jwgjFfK=1I4f ztm)&4Vpq7t&*D3o%3r8+m8;(ssvcq`K9?309hZN8`bZmg(GW$hSnh{^LrL6jAB8Du z64d#l8OqM8Nd%rCR6h?+bISorP$d9)6~uQo5^yMb?*lq(ZmX-r@b zZ#&tmD4fMFpR}%RsbD;Q&%5Bam}SD)wANUDk`MjxSS8^N@Sfzq;=9kPDW1K`EnaeA z{F)FO7n>TJSwm~&DA}r(V@d*Jb9a7FMVU^rV^TX z54{(-LAeX|FYwRvPlCn1$-QFx*LO_4HaFgEJ<#y$Fitrrp%TsRT_?XaSP%PK@OSxd z_+Q?Dw7&=eG6B^;JJ;hoBiEnS2MvvCzR%+yBl?{nd-QQlWqcU5kq6Z;S-Cu9IEaSj z*5+0y%J1JOT9trKTNHxC@@Q6gB3b+pfV_a&pA{dph9tlK1No8AY(RyHDL(`ldlPB< zX5md;0EjFW27u$I?fV6YB=bLzQ;@=lO!x_dcgEs;`l^H3F$b>)`taW&%tPpcR znGg)z1^7A?Qv&iCQW%l82(YJ>nIQaF7y$Pl$8Q!oB^myKoLAWW=do!4DvW3lFct;? zUkC>7!iPE}QylVEVfVme^KHFY58+iaQ#1&1EPo^{9)lR-9m)0+21^}|DI-K501Y^X zTj+=Q#Xgfh;#s|eVZ8kY@-`MOk}VJhtMlh=Qiw1>TL=a&_j~7$+n>opgi!+t&9^^7 z&|}FX2|sXua!vr<3a=t~WrJdb1m4|;g4APS00iLgakotX(E)^T+NX+yp73a3hQ;@NZt3%ADuxU8Pa5-@6674+bj?> zK#MR8yykb#AGSY{^OM4e)cw%>$vF|E9}5Fe6`%{(+$Tx$59D*gRe`z|1tc)CL1RJ! zaQBfQ0#Z&PdnRX$ZAyq70A2_NZu19cr0t}1+z$1Ss50JhTxM_;DuJ3u4)dnO<1VaO zoTQUfrodWugYkJ%H$HK=Iq@gH{hD7Pd8r}qs(C6laxHgr|4yOWcv7c%QJ;hVBXap~ z5k~%hi@2cuTV(e?B2WL(BKSWdIaHr#&lTCO6vuf#{*UZFb5PWmg`ST-I4Eo4tZN_m zBWje$bZ|@Gz%VTM0wWGR;d9iNJS{|F1&WFda;shP+}kg+=ODH#(0D$g&7EZ0z2yE* z5WCN}5ub0@K3(_%apnSFfTBhyBvPy-i2MaLzhVkfV}kTmG`l4xhb2T5X#cIo1MA;v zwm$uj8rY`)lEu8^eh}R~DdHXH8i0pkzU>veC!A$r`WEso_CsXbo6C2wpc(r2S|96T zLE@wlLfFi4a4@<*9xA-Kq5Au;{>KA`;SCk67kWmlc`$h>Qrb)Dxe*&1i@Q}{V!}e^FWV{R zx&*9-DD~2U`-$4~uQrkt6AkqTtb=yR21Adog;sa-k=K$cBG@Im=63N!mRF8^cL-$*CUu+obiQaW26WP2(0n7N$&r z$9ChW0KmXRK_aeTln#bz=(XL#dZV)M{V!vMcqMee%b6XRufGwApK{bsED+ zT$gO~5A>_Pc81e1Zx-=U!*BHA!4j_NXqlY9n6GBR5nXk-Cd1INtBBjQxW_ zLlYAdo@Uy!o=fmYG}P*arB(B7o#bP}v4Tb$@{wdw)dAk{bDg?XscI3&T?!j0KDJox zrw#i?$v-$#=C_M4IaZDs2Ofe30Y0pT4sn_D=~`v?-)#!De>G(%wiWla96O|Hv1mz1 z6t`5+O|u>A2{p(eCi`Y_OJf_puK!`5nx;vW$*tQzR_zh!o3`MNe{pZA2s0m(N(JN= z&KELU;{?`)9LG$pStq**`AK89U^vI=?$mjt;C-j{SFUo^()mo@u%-&G*M-s!Z#7I# zOzF*fC>Fb`lrU1;YV~E8jdE4(jH-qFf0>SI3*;^giK-_oRNZ(B_)zOC`=o{lR+AxGo6-Fj#(MnA5xy_uNI~HJxq9k;fk!Z^e4$ zfXdESO7aFYcK#MqGq$t{t1hj~qCh$b%v9_hE@@9bwsOYuO-pT$T72p^i=sD~1=KxI zM>nOqVfgw}XqaSi*z1X|@jHhNo6qiCuTbu#62a@vdR?zbj)9zp)1=nYw>^iq`zlMX z+ka)*(hN&0xYuT6*^=kY3MTj4Iya_SixsHKBv?mCo915xK#*7pAK32Yic}vJ%Nh=g z>acT)drhD8hsi5?Ln}*|Sbuws>rz7>|2~xl8AQVhUH7qOzwMyyu23)51jIFEi;N-E zo6NIrHgz3;0NB;tY3)<4bXxQbkG}!?W4DOUc1SF=5TT^9?gCaHarSQXSE&8`8JWw9 zp_GqnVm`NV8(k)^@vy(n=vx22G=!cp=k0TI4G%c^ZEW*IWb*BaTZCQ3)lYjZXzjLb z(tGH;sq?wU{G2hyRTn_3^p38;&Wf4I1SKjB-WSHhJ%3=sX~9iH@I`NV|M10?=GttZ zWuI<*XIvvUCZR|%{Xt>BY5esks>mw=kmNxSFp`d0@TDvVAO8n@6!1*=bt~5@d}N&$ zA=qw4laqeyN?g4?CWe1af-59=><}xdVdk*@Q>D%99-4OE5%P%iQEly2+&;C#bxG&G zR+WG2s29dw3K*iv+;4>*qg3H>QS^SbHfQ{7|B+ba)&8W|(f_*Sx8Nyt%dQ6@#jjbl zs?b7manp(BZnA@qZ4a@3Lc}*;UI^#>&&$+m%LOQsxI%i+UdLXaJ5qQLTBNt{Le@eA z9etjn#ipQtFKSh>`;@u$O;zZ_(3&|r{M&m%mJ83EO!?&2y!Nu96W7Se2CT_8!AO+9 z`xoU4fq!qRy(g}s(0C`@UJS%W`x{=*Q$aUg#CxGrTe@yQq6$~nVNbD8<3|LlHLZhX z@BIhCDH%Hq(ekt5GW?3@AjUI-%er7Kg@e-@;)tja!k8N%`E$Y@ZC9>Rk|d$mU2oHw zy;}k;+S8&Z(Ky+@Lr&s&`$g>*cX(u z17N35n0#08`wb0)kTg>4{~E4Ow14L%IuZvAnjW4N;g1DAbWd#~-)Cgd)tjMuu@3D> zYmu(SIt(sJhi7qpa`yQGZw>2&aybKnk%QuSV6xs_g+12G# z70Qv>^+r_dXZTnfbCSxitP_DmNAHkmPRw@<8FrooX7U6S`&C;Hzo9ir+>|dJ%D5@` z+91+(e$tN7BCqJLoS3gA9Z54a)7F}$w~1z7)3)5v8mjO)Vz+5%=mYj^J0xoBz#h3r z4|>JB%|DD=*9$bXRLH0^&IF8Aw$9GY=2Y5d6V+jyfZXez3o@-^nzHlSys)`32P#GO zdG>+(@YI*GcCI^?h?vcJvB_ohSyR@mnA~Nt`JcGU8bxh#AbM&@D~OeWv!Vn!tvRi^ zXCk*kYk?dhw<5Q~x1#Ae2stC;f#cZwU-ymn(f5`1AID>ID#v4T`*Zqp?Zy>ysB)-s zPsUZoQTMGZsjiWKBLT62s6b-iTOc|R5s0^l(*EuwKtC8NQZ4+LGne~3K9<9o!= zt^+th1hkhp_$`yX3Tupro!V@J0jEbORrnFha}eLC<+;MDw8-T-5D<(Q&XUt4L3z zKybtWmLko0b~EE?dOI~jJQx{ifGb6iy4IPZFQ9d!V zaIumyk^XC}L(0O=1e^IkVP*YC;D7jkTK}JQ|KESZJkulnKX!!;=I4KQg_G;wPP4Of zz()V{|C#>ZT;*h8`PVia%*_AZ_J2$MN8s z0k%f2W@2V04yI;|vS#)cu9l>n%-mdT|DItc<=|lDW@8f&K>75a6Xlt4ynvXXI@5l3 z;ahar+`3J}HCfBm+La9p+bK2}j9sI{dIjt{ltF1TG_E z<2>=i3y!bIL%8v}@i;CfZC=c9PJhlQ`hswy`U1cmzGXt!eQtBUHth|oI(~u|9j|-G z^<&(R|8z4wTzx4Uv_A3HIzE$#d%l-{@ru|kqnY!ocylY;x1gtt{FdSV?k&s{PE@W8 zx=)5$w`?j;T>uk&c|N0y$t`k)=rud(WfI)}?E6}Z2nE8Kn?;qr=Bl|AD!(>Ir=iG~ zWcO_2$Sb(aoh^&Zro6QXpo{Fbf;PKGrrX5BU2O!wEhSG04PV7?VBvW$<92+wVd?iX ztx33gcD=D)hZPeA&ujm*0C2Qezy<2?($84UE{?I)r#h@V(Qva&2I?U*>`RKsTz>ytQN2Eq77%YA0)_^Ll?Yi#EbyQ7))*41GaT zhrYf9^CqQ3jvn3a9PoIP!l(RouJ10(ysFbRYun{r){snyo4oo1`bU9Vw`W;OdOAKa z>Ji;g$_|B#o3oeYl*2SmzgKk=x07531mCV%z6(*-(LWn4HhD$8^s_nNTvyL=SJ_9g zF;X_yqt~e`{h6~@lwBIVrJUB2#C_&2L$+@}HKaOt=+>Mk1XG&fk9_DHL#217ji~m( zXurzzi4e_njo&r&n}6o4?|I_1%t60vt`xEs#dQq|dXg-s>)o$46tJw;f z8_|0;Ttl<-k9}xsjwKMRh${V?3Co|wVG!AJ|f*BT(>GIp34s)Mertdd`8 zK@G5`)??o?e zv6`Brx3@Q>iH^PKTZgLtKWA->v8}oR5&6Gm40agR)XaRg5JJ@M!;MvRb$vWrSxlra zWw3&00;8Uvx*oOD|Ngg(y&++qu$;c@iy>6sp_7)Ck{Z}J8H}PCVN}CZN8^BAA)VRS z{d`5z`|#f~_+qe!t6(JCd@({Xm@li!URX3B%gQ=TKV{7`{H`8C&uWn(;LowTo?sMsMUc5XXG^KffyWWC`|EdH}Lf#>j!%ccKbReEm9#^ocxs-X8 z$$fZ#zT}|=L~b22r-~M#uBM)loS1}(BmQp9g^0DcbZEN4y{US1x(RB|leUzWe<9mX z^N91?RMxGc)=TrLBq^&jSUr^##LS+Vx}mqSBxfzJKpA&u7_Y*zdbeL5wdf^GznXQ? zf7G!XuW@Zg$#7_I8!bS<`@3D@(G<|&&dJfoa_af)5yx@R5wFqn$TIPLhO_UIoytsB zqbaI0cWN_@DhWbGWz`gvtlX>Boyxw|PndXKee zmSa({-*mWbxFDzneMZ@nek#uCS4WzYHH-YoJGZ-Ms+!i5^eZ$#PP>Dfi}zYH{up@m z7U1gd$49UQcM%}I(j7f%JijwKj3$4ws;|DdxZF~wibYw$v?Md6s$x#(8v1e~-|r;< zXWjkx*ve_z<|h>Wh7JJQ0%43?E3U~OQ^R!BjuqxtNbBJh!s2vWDXyhU~U^M_MP?TWIHd-Pb zq0z$~^}6Y{uX}G*>A8uspM_}d#x$?JJfpc~z#Z&HU*}Oo4z?D5VPNiPc9O4f(j4m? z(IaVWJ`c63vl(2HuqAzva4XlX&eLq`25Z3Ih=iKuxg2AZ$2-_5^cfTJ znUj2}w(xmujzBWW57SJcg9dw8=c*%E+5V0h+wDx?R-sFcDt6zS+u9x0oGsA)(qh-{ zKC~LBW1YOnK!9e?{oC82-d;s-Z}ZidheRL|;TO9`e>}s+c*xlTczBhfrH^yZG<*;9 z)K$)=3C9IV#(ub7rVz2cdDjd;*+q1cvj2hmvt6{{N=6tZi?Ge@>U>+4r)$cp?P#u- zB3E8!?nH6A|Ni`KF&^;<*A>g?M;(W20ThG7RS5T@r)9tAqmYBquZ1Dl^C|?Et2lki zB?A^a{wR_^mtFn24m1AXCucqIqF6fXj(^0?5?Z-^W8iuIIr`mt&z<2JZ!)2__O!3o zaXr7vO(la{BmMoZbmy5`Q|s}C1Z`d2xTg~^I=O`4l?xWZcOrBh z@WvBLvcvhVgXW*k6-TRQg;%v39KDLoHb1Ov9)sSr26AJhx$g$vFN>{n59T8~HXKz0 z6&L<>eOmRt;%pBwI?Ue5KXi$$k_5~Bw`>{Aoex>-_txlWYtmbTa_lglPn>#*;!&^aOy z%y;xaK8ax4p)HG>LLiT7Hew&pI315I=h$xnU@TtiBScVktyvAfXK-Z_0(NVbxqq! zne5ONh@i)aKLyRokKzvwLkS4S9l&A*-|pJ|E+~Eq;#}$kKe?aBjeaIU)*p-trBGPJ zYb(g=3(P%+JZx7P`kx!mA2zJ_`jq3;s}xgBk&}T84LpN;2i@O?C)jm+B)YY%XFfr5 zeLJt3FQxU2Y8dDEn6t|zD2FCZ0tHjbF{`w2pBCvg{&Ws?k_2kVdYzZA&viOn>oic@ zB|81&{K*;EksiLz*je6bkVt|=JOUp^HObzYzl#@a6aDd&dF!V`N@{r3y8Tr7zJ{1A z?L4TQC^527CXjddX?;x-;uzENi?(6 zd@jY(7W)V5NxCyka?^FB;^{UDZ2?B>h}K7l)KEwubNtT zYXMcy8F+{6@RsqGv3#W9JoFrW9RuO3=>MoR7@!a%gNxp_a{gibv!;rCf>JdsC;KhX z)yuKa-It$UCh*3*unT1rMSd{aF0xPpelLv`(*~0OGaV}+^>c3Z9IxX$-%z}o%nlK_@$Fx)&=Mh3~>orOW8G6P~C<$lTc zNP*%&+0$h#dKNUA<7>Tm8$ySe8cGfRu3hl)Z@dAb&cSs|qKzxwhWyOJ?BVFtoW=OS z4?GSlIg^nn1J@abc8=gu`3 zu~{9iW;O%sRH?8KUR=Z}vTHWO5d5WLl3BSEQR{}eom3Owhwm;3_9D8Yh-2QV$T!V0 z{RiO?Xnn2fp0+iy;`?2i#IZ7{nVEuw4zNV7*f=%GJCT!G5VKEN8{zM$` zQ)oNnZfHa9A0(`oqo-Z1KPk+`3$R#n?9nb8S~D?NPTWPJ;Q##N%#uPtn194gn}SDJ zc|?O!RCY`8;&-L>%Fdc|cbn7mB!`}o-!NPCNA!Zd!Be)u9}QMyCg!nv4-h2}-}>BP zb>{wh<7hs;TSSyfFExf@E!*D3j`KlncQHp@#^qVV6{DPOT&?AT9r$9+E-ZcHIGd7} zGx=)5r9tIZ-JotLW@q?d>^WvctJbmHKnjw6!u z5ZU0@U#~^49=x6}3d1HZF_FB#$6l4(mpvcL#=G{JgM%+ES$u0Y@XTcFuj*~nLn z&)L83*Pa}L?+u<$8a-YVknSJ-INDnDUT(Xd#=yQN6i+0UttY;-uj-9lN@>I=uU%X( zjME`xGcO9SVU9=jFC9S=_c#r++n3q&$2UZoBfBdbs~5r){3i#}{xSGszc*^T4m>a~ zb2?y`vOF+lmPl|<(3(~*(Qko`T3*%jW$VS`rb6O5PWLLPAiL_~jJ+F6fhJ3(t-@0W zof$kx@yWxo+TWq7ae*17`sBL0ek|4B*M2+2G}wLx#o8-Sdo{<`fhn1WgN_V;$HuvEMk>DQ zvbxqm^woN*NHgz`_(dMsdB?6`3entnb`_LFHJ!nTd#XdZvWo2ptBIOe?S0GoP#slP z+VYOUE@vta%AMJZ*1`aa??H)wX_|hSpMK$GlH&pP>!+wbtc`j7#7ct7m8wg+?iGRJ zp_FCC#H@XTf|c)++>Db%9gt&{_h~vfWyv_!L%}Vt1{aK|XRmO9az;BN(6j%f72llV;>V-Gba!zcXw%_(Bkgy?jEF+0$&`8OQ5*BOK=SViUxNN!GndspWhp}U%|a*=BzbO z&aAZ`oVm`PYxZYiTEeG1;Jev6yUkv?EPcF^%0(Is4^cid#;*>FA{Lo*Hv+pG4Y%#@(g57pWc4c?Hz}qSDs?ORum11Ao4E^%!K^93DdaEwrBj`Ry z_m7#s3d8`^W|&5D`A|;2;$lv|p8e9l#=aw)tX zsPE<3Wq0iZT60a&1KE4~2H&jB5|zoyc>)+Gbbwo zSPa`&k#%vmziyO}c-J{ML1cUD#Ph)#Ba%1N%`O3_8DAfNd<O}n{d{F&iCK=r>F6Q*VAhsuQDqd?vV;_||{ z<7?`3p3PEmb4nk(X0NXn?E~V2@Om1OmKkw+2;-jqut4#&)k);_O%j+s%77>{>{&FV z1_&FScpqRIIcK4U4l$jsvjnahms6`MTBtl`772}7v%KT$*44EN>$e%h4b&G2EdGSb zSG}jeSiKW2Y905dhikg*!3yKshcg(7sxxWQsMJVLd_OR3jy;f;cb;{!hva`i&rdH%d>vW20U!S2c5})0foY8`iV>LBOwDV zj+P>%ukU8)NsC8O%4j*T8kO~FS0Zzc`1HdPOmTcJUa7e-LRe#5c2Ht4p-<2)g={b= ztYrF!lB>G3@@WVFPb|=MJp_gE>+tltt7CT+bb7xBJUqSXYS^WE0&X+%ryjaXnwdvA zMadC&bud9inz$3Iy-63J1H@mAqT5irc~iydMw*3)iXh7A=n}GZeWHHfhJt=eU00y6YwUX?lf?vzR-EG=<`=`W~fMru`Gl9S|yz$u#nJ zeae*8T-uX6k^w1z6y$SL7_&}w8vEE(f3~;ji*sebDc-YZDKvJ~itS3UWE46aI`@uUGC zt}qyFI`gP~Gvqc(RQF{5bTZdg71UR#agt)))yJ>qQkPMEpyiuMXECl7<@8pi2z67j z$+Aoo7*#>kbt$(`j0B%6BS?Ph%ojPIg@|(ROKElN+zv-*ub9r)d70Se#%%rVCeO&p5dN!zRQ~;tRE7b@&coJLu!E zwcxmIZB2$ny+!7qyRf6p&A#m8bNFW8aVU0!yZ(2$8_i5y5-{>QD*MNT41$6GKoK%7 z1b*D!8#rEI0G%)tAMxpR-~7(BnaD(S!RW{Z zIR8g+Rm7=1$~Wn)(O6&AMBg%1;9}R#D)HKH(G<8+TYm>@c5TX9)!O`-_gcPV%ZlNM zpq~L8zO>t=?<@Pbbpo9`>aUYrF758kT9G`=tE1@JT62iskofBeZ6Urufit;I0>c7S zKCTw!^`UnTmo%xkQN z{{^w*G~_;s)JQlAPghI^$HF`ch8R{on{>8_!|2p19SvmO3Ny_jjn#ydRws7_QjYf8 zS>ZP~53A0(ZEA^XzQ!)zY1S1jV!0%{-iyJ7u0cSj981aUiH5fFrJYvsi3aP)t#4ub z53^J?g7>XgwmzNI<>&s8p@{gk*0b%Y!pT65+qp=XC}qo&W2Jlc7E9Q^*uFscllSfe zi;jK{n}QrLcd;hv(qg8>b=+ej{wj}a;wA4gpAe^L@ITA-TdIA0>*&6@@|4ZPlOvUn<~8}>IgiL^{->RT|7sf|;v!dTK+ zCv*&}Th+J=|2tsvY8z$0)}FJnfnX!mGT}!oY>;OdaR;A|m))n#YIpfQSgq9xvwGWb zWT~U;tJuz5kfDpsu#%Va1)sUb>UDltXSwQV|w`Hn7vLCge73F>C~lO)6do7y^wv> z?LOVOlcIAQ$vPVn8x^c>Sp40n%h&G0oobgH_-H>KQ6X>_Ik|W<&!AKuo0YwAywtAO z)FcPho~*1ADQ^ytbs9}PSw@6YAJ( zb_cDK`7iksTyrf#YO58cCt8{X6my;Bp5=~!Vdsi~S;Sg7!dtY)J_0piqKRVMpIk3j zoKu{L|KWMD_x?DV4wwiJK4E1a%1a!bCX~u4$H&<7d);``|%cXfB`{(l}>e6lN`(P7J_Vp2+wb=Nu8zLyyUQs9kSaFr%`uFcY z61=VDD<>86*zt=%8U6%r2pW2q7Z;*%Pb`w7Hdk}`J@aRz<8o8haAY+BZ_c-6$?_n~ z%hHRU0*nrpF-h{ehl1$~^@U(s{sKtcIgVE_m(k+mR4B-QS!bCvUZl&`C!Ft`^meHv zRh4xX^vFkTWbWqVgAgIVIVY>2(sphCjEtUU2X$FGzfD8pD}8Uhrf)$PfDtEDEV zrOV~{n`R3sH`jPJEIX$irek{u?)MIIeY_?UP>=JYve~ z2Y(#){CF&?`|snw(5WS^5-Ba0W~Ze`5KrZ?sHaQ-Ygd91N{{TTvHm~_Jp7%_rR=YI z^V0ESQ&F{Yyz#iKY+F2GanWnrhE_^%Rc83@np(k&*}nxKCfqGeSgoVfz!`+vmX};f z8}l1DWlEZ5%KQwjunfi5QDbw)lVz*9x9`wMLvgfqDrbsOLKjBufvagb5rT}*u11%w z9i_RQK>r;5gt~yCpita(m%cL;w{L9NCYht8R5{?uR=1Hx)@1%uT-+~`^PSSbIy9vD zS=~bCCYPtJeh2{5VA@ADlb~g?N1fN@R?8`vxsnErCdX&g<4Vr6ML}N@u1j5=NPiLF z&38Lc@~%dF{T|U&(m~92b^5WDqB=sfb|3i{03&rJIz{$#e#AE6JsEWI zv5=fG&Hh7OYjrKUfXAKi_E2@MohnM-BrGyYY2;<&-$qpE2(Ihv^}O*7LED1K?$M5d z;Nm9zBhq$qN4%R5eav1yHGFQ_`nm>0#hIb;f2R{Dz%92vB=0edgTx zO2ZWTgA2bptb=PwBnZ9VHa(K$NsrNZ9bM%wG`FYJ|D974+2O+_cK{~r?p6B0WUtiq z>)EQ3Ys?OUD(;l{0cf9*YumjET8g7^z^HoOiy(@*;nh;u#eBShLw&jdRzFPBY8C$Rd<$S)|L=G<%Zz#~1pkhEv!8o1 zaeZ^)6Jf2-V$I#lM=n4tXjmd7_;!CJ%$JsY4HMvh|}mWkRY3lgLIZ-eR+Dc7Mh0{4HVBC!c(cTfS!BxGQk$XJa+tg&`mR3>n5PF|}i~ zQZOZYNNUryTSgEKZJX{;#sO$Aa_?7uEn1NqzHAU-r-Lo~HONmm&XVKW=lzleWvb_crf2?m82yDt*d)jAA8bGCR$uTqj1j|!Yrh0w zQhUE^?Obx(jXi@R_QhgS-LZ7jVInoru3BW-qI< z8bWT2Z?IvV9w*U;5&t!&?0h3U*Hodyl?3$y5J0guFI%uOI_%rG@MbhzE*Hc3{C!;_ z=Ri)k6RKwZ$?NeFk9hatzgunDd-=vOe~7B6YLayN*!JS;F@f)1zW*zntAAiB4 z+Tq}eR{qbBa^UiTch)&#`YO;oSe4_?n5fEr=^a<7y9(hN|5wUWyGeos#VFtXlZJ&xjAtL_bJ>0Z{O7Uu;_qGM1!tEl)JOkw%yXToQDcX ziWbw?{Vy5+wfF4Rx*K-FUKl%-4D?UdH$#udj zY3#KK@8uWxo7ow(6&dfTo;dpIID5nb*d_cr{^ZX4AFBMI=#hp*3xn%ka!%I+CD6^5 zv(iHVp$4iIwARCbTgkdK=34nAq>j?#@VED_Rr|i9O!52~rhh%sUW}nOnIA9$qg4>% zbKNG+>dxvuV%(Jh-YIzNVAj7sk*sE z!Q;5Cz3KJgY!Uj*$-sVgfum#FJA)GDho9aBN4zA(X#myJ4-%^HMsZllzU< z7kYFO*iq%`LP_0I*%_W6N%z}ea6m9JOT;2nW#iUm==*kpK6M4$g4#C#$D-$cLN5S4 zJm<^ysM|`w(Jb@blUjti;qbxacHBe7MQ*wT3BLBHnQ71J^~B87?99w&(Ujd19%}}_ zB)%4s?KlOtvOR^i;*tx#O@*lV_%_MWD<@VP>apRi#t3z?hfgW&N~?eFRau~r+3DFD z4ZmN5>zn!{+b5bPSebilHUa3}@eY}TdTc6lb?oSSOO`YHh?M)T>&Q2`gSsTOw@p~P z8KbOCF~1#}UJY}({8cc{HrkZ%fNZl%`{C8=3d@6uJxB`P*h$YYW|KvsEA`0}qy0V7 zs#IoD%u=lt*=)D4RSj_V9;A1a`f+IGxbc(P&j^>dyU@T*&0fB~n8LdHKViYMI$odP zi*2hhpL)6{p!X&ps)WflMKGtJ}fUHLF=YTZDuHAFS)m(kWtd$uMvywiWwRYJph2N}+%ro+dB1 zxoo{oMw)?P_o+UWOBW%Uem&+KzKQqfWEQwi;c6Cayz{2cd|cKnQQWp9bkY-PxWaKY zh!4Zk6Zm9*GzCh`n~Zg7t3Aeg^Nm<^jn$3SPo%W7HM6slhh;9#rT1Otnw>aLSlvxcnM~u668IxV}hMMi+1z%5Xo~4bd_z4PD23npj4YN?UX#kJ`*d}7E0gu3&zV1gv4dHwh z3Xxicaq@lsk>kl+MPCGaKacccVc`N4;#oXr#Umk(rZirD`C;~M;j3T;tk2i$ZRp*3 zOgy|#7dRT#5qIEkr6cj#E1-Nx<$nF#Va)ZBzFK+Kd$?)9Zta5rQ@9%YLl|k zu@~5&53yT^K77t8&1iFcu>&@dBPL24aqw$7xub7uBiqnRORm!vbVIh23zmP4MNmNuK zCX{j6{`C)qt2sc)GdXpDGL4LRw2aMpHy00xylz?#U$;K5l6)HQDSe7uj&GP^ZE*(K z9p*$dahhNTBVg;lgNM=UL>I}Mt9MCHx_?XJcLRbDqqr1t5<&aoHRig|#-MV}x!`DI z(%fzX08EyLh_N|8Kj|=fl~fK*JIy+&xOO@}+_)@S8}Fz*hhI0{uAO7Kq#YVH@}F1o zIV-MA3v~_k`1EqOKJ~m=4r^;Y_5ZJ1qI%3r)=$>3Ak}%cDPtmVuZq`OIfHUb-_yRx#p18&f`*#QGhJzaJR{Dg3Imo2u zrw3xrGyJR8vklO`LoMC0hvN})XYQtc&_{PQAmi$1uH;0VUSYVK3M*7-=&V+U zet_CTNubNsDn`YJU>_=Pplcv}+Cs5sT<0yHZ7Mcty(KC6*QH*)5|U+-FMJ zdlFKM6?`%t%Yy`S$Yi3B&sQovw#(@wSX>o{ViIY}^m&7?8;7-5s?EN4>Jd8!vsIhy z*J#Yi6^b0I#@g!Rqh5|gHHh_g*S^j=9#Qv`fR5xne4Pd+?!_w^(S`vkKb6KV`9(94 zmZnEd(ttj0T2xZx@GcFQQAO|S!qULEH{)s8Mz$|lEafi$w$L^8c-1yFive#cc}6nf zkG6RRuVU4Zm{X9&g?&;=W@}>>Bv>V8<;*v=E#uJm$QZu1MUpDOVC5e*#Pv~+nJuow zbcUpLu8ZkXWM-UD$)~GhR(y#4PtWR{!|Ddhq*khD-=VaXBH2~wF{sD9snjAS@8jfx zsK^S%&-XuVj0aFeCokW6LQWNniS+c8=oCLRu)1G1eswg|uV*q)_#ps!ro)Y~*WX~Y z-FI;Yx74=zAIWjrI=F~8?nTseH=Yl9sMJzHU9#OKPh5iqE<*+v_tRi<9rLG|O|V@Y z?qjRw3e(-+ZjsD_q=Ur-45Kb#n4;{mTinT?zhip0nKYT-znmpr!-;cpe z&Xoay&Ku*_q3nRu;BHm}FfOzf1ZO8R>`%oRf_`bkwimIpGj&s6>oIH=qzt$*@|J70 zZb;;OV%=?>SGl99qY~^Qqhjs$Q)HNtpZUN#2pdiI`Ns|ti`gF@ogG*#Vc_`miZoi& zrF^LM;FIecqfR5f*kP~88S^`{?caI07xmMhi$RfNGonJlzMVH~x-F@moD972853I& zPZ$57d+Ec78H6vpu$xc^@w7`b*h3ZW=C)|gmvOhH>U;YKm<6rr_IQS$`P7D( za1Z&fR?%k%K8BH+++GexxCo13vj_4z9~u*-ef>0>#;E&A>53?HKsRr=_BM4s9^BDN zC=bzfsoXQKvwpR0KnQ-XuiwpdW3`j~xz#AL7$ZrFO-en+uR`mj`IE6kFeES_j(kN` zlpsekj`@Yc{mpA+Sih#c1tm!d(e|fekniGu7gFPSK=bDm_t#_a)8OR9leXvj$KcHA zIkVLJQ9m2XILi5a${ zt0Qk5sM?asN)Q?P2UqLShd!&`5THDo`bScu9yF9bCEBOJGe+1@)JZnMLvgh}MDS}29_NSa2^>q=s$ z;<8_0i-pO@MUuNzQbyY({n^e3zb`yqEVFE(j2@nEzy?!KV#_PXmFMOkTaT5g?Mfhy zDdZCo9_YxkNcaV`!tE(2-lF=C zjvkH@|8_IK!{KAc(lj&EaI#u;4tDxya){5brHyvPnpJ%+$Z@7phUK{w7i^~le;^MI z+;xBId?BD|M1g~rsamy!{`4TFQA${Yot0%Ds?~I9g)%iWU=2K8d(fb;-xt(2yVOy} z(-eei2`FLnp;^tKu#pHIt!(o5)!FKyne3%bn75?-{M!7J_;>ryw;;#NBFIr=6j`Xt zRHQV&(DQnKgQx}QwBkL}-W=Zl3R1D1OC`D}hit;ja{26H@Y2X&3%eQ7*Hqtiicwgidr9fBdKU0snm-LFQj- zmOMOiQ{lDo&|Nm_SGDg>pMJ?9Z#4d*+keCidhjOueYWyK%eT7k552Eb->H3F(Can9 zzE@%@sy?y0KEiB$#lnOwunOuOhy7rJU}oT-uxq z^~GO^a}hddRv6oxh4I=i#yL*SI}jYL7TV95meKh4VYa@hLFM5vhf6GYONKRm;g74T ze@}6D&Mdt5?Z~j~K8gSBoJKS85&)~b}U;=h~ui5-3bFYnmJZMu#@u zv6wfUR>U{@#nwCHg6o@heSsx83FKN8#?tbf%lgeOdli5F8%BMw8JCCB&L>uVTf*NG z;+QX*L_7D$i(vd4&sedRbq)~j!$w7SeZ#+sw=c4ws1IYM#8$wC2bCwsTIs#U8A6WQ z7cbDIAL!<(AwFoT@&C{({}HHB!4^!PlLaK3DU{uhP$L~LI5?nLDHAMi{6Uu@naaXm ze*XCOYWvlkh1B7h(WHR6(_P`QmGIHWZKmHop?RG3nri>8FB9koe_;&+t0QzZBy{5M zui>Ut!tLKjDT@qJz@%YLh;6qp;D1ZXAvArqVzPfZ3j@$n!D=x>m%dsOZxyNs8m{g> zG~O{;{$c^<>nF+qw4-d9nsRD}D41*9cx00@i@YD$pC;Jm+JFzrduJ7tw98J~4a!5E z588VZd(9F>v8ghHK^^}14|Xp>F>FNa;7lqPFp z<%(XgmU2*nhb46!{f@2psI{+}qN))&_QS-nJe&h@L_NzoS0X*}G)1 zv9fm@)17jVIc;82Qgd^N=e&~Cm$%ocS&ARfvWU1&WqC3w=p+M4$jqNyXU6Kbd>J3G zwsKnxQd;(P1mzcq)|W3_yO>zf5kU2DP!$AC!^8M<3!K-6MN4RRX56V5n#+k(3!=mT zgnerdh7xJjXku!py-|XLT1o>4N3Z|O*-~-*0Gd;|rH1{&>5(A$(V8`bpnB*(-eVUM`{#ws>I6u`5Q3`Im z)W4xYzAmSQj>UbuzSKXj2BsBiwV6*^1W0N>uA*FQNEA7PE;gwl z&}7l12Yd~_AD>uYC>L(%yW=>J(TOcM{M9^XAmLFIWzfAe$mY6)DI%;K=h;qf_fJpm z)zcKWvVFtprrjqijb1P*m}3rxJ8IU9jH*wKJr`10;4>>F#!P-37=&*JDE zXKk5b(_A<=Fsi;0C3E^hmMz2w%ugRUh&NMD1&0obi`3gVw# zyb@@;W%ruNC5JkBVj8*e?kUt#ej!1kVAz${?Xf?KI$7|DLT#;ZJ^I2rkQZ!l<;3Iy zZLvUMTv0JSX|Bg!^xsn_)6CFF-?^b)zB$++6@{im2O+J?Ya0iWS*QAlbiqW z_na0i8f(NPX=!>oWNbkLM#iF;X!!76@wQ~vY}_T^akyTJUy3IO+jPZWU@+(lS-FBu zH<%^AGh?Ls_hsQhvNp%xjA!k_L)^Ps7{O`25&4EEC<1U2$X5K6@AOMDzF+){8)_wK z)*>hQ|2?k#zFaC@E1{osO#9G-H3DW-b5642E^yXyl4~xE!Y&lo6_abOjbbm@*4dI- z1BjU>ug7Km;#}~+9)T_|EWjo8n?oiF zJ(4pv9g^!DenAHbu$G?S%dvK0n2l~dFYF`RA`rm_p0uKpyOWvMVG0H!_`&OzD8XhA z0puIoMaG78v_zEg#EXCK@*e1$ZEX8I8^XK7{i@VCyo9@I!WOEJW)nhUCONz>6&Usu zzuw6Se|RlCU85^*X!Nq2k_Nt1h*zPK(MAG?0lY_*&t9;kz8f`6Xw#GeH|U>04=u+4r#%l z5cdzo_q>CS+$wDEu)l@c5-JS5bpy5jSYdD`rFgDYuXX%RbkMm@?)ZI#Oti1O7-L*y z(iZVKiFH(W6z4bH7SH&cG9<#;Uj-z%7y9y^HW1S~r0(7~kZ|H&@7^%`l;0oN^&S6k z>?-A+1w|5wJ8|3L8s`F~ne~AI5YqQ*C}C9VW7K2&W87ocBL=GRvF))aX#Pb%*A`BE zqO~P?P(jwz%z@JtFZzejn2)k;+|wVTJpg~W2h%^=RI6Fde((~qkRIZ(D5?Xu4(eJ| z3ZC!?klm0i$5IJ-_)q|10*5{p4Eln7CxooC`HZ=ZdDpnsxYq= z3@yVUR(tXuDSpvyLxwJER?a-9ye(Ox5a6D(M||6)m}PUiO2%ZyFeGgp3Tc7dj7z4M zj88&1A)Mnb5RX)AJ?&p9jztEG+JY5nbCWD};kx}*b@RRsfB45Ut;U;3biK5bnjrdi zRrSsd^9DD%H+sueRP!7#0*4x}9{)ZOdvObpDOHPcopHlj?ONShi$uLdow7l$o{6rB zzKziaA;+_i{i1c+Fr&J{+9k``INNa-7DN8 zh={<)S9n~5w7nIp2Ri78O;ZY!#S_U;tMd?ZG) zUc~<$<4&~H`L-~;2z6%ovx6A-h9ceUd}P=I-Le@t=Bo2f7+*E8Sh(HyY{aX&o^6dR z>A{HJDDV+uRvQGcgY^I8is^1#k6E2&C+h&&Gg*yN+;N;d>Z<(r+lkct zTPG9m-PG2`w2cW9ey{u2;)kFRp=<%#sfdC404?npt8eRi^Jrs#wDbg0cRjZ@#SRV@ zy>pjDyd5s{;6)t1SN2GbEsWyMe|~M@?N>VNY2~TGh57U<-xO$w*+VrOacq-D_X_9>;l%E-uQYN|?UUdS82i!~(FxcF)rWovCIUu z#{<=c%8$qVi-CZfc%V!3P4i97><>A9YyJIW6xx8huJxP13(?DmNeHjU1SDgO*JH19 zI>mg$ZiG+)qlGlNLKJP{9%WG0dRo?oJV9Wa^~UAya}U5U&qY3g@J?@yI^(*Gho7zx zJ@%o0w%Ewx2*$I=Y?k}Wa;dVVn}6C??ou9QcI+)rpk##Q3`%gsVHr1fDwk}4$nT!J zNs3l>8`ibG8`ceOx7oWWoZlAd9wueJd{^RuwBj$tzlaR|C*vbF^v)3n^3pOkf!|7K zfVB1NXWEkbuM$?%{WdHDPVO7 zWSgJR#es$HGB?qWJ}NZ_ElzvQFGz>+Oa}e97TJvB174&)Uiim?5^OlPPnef2+W4Lc zn8!Zk6RZ5Rhoo^*`#-#&;UYI}o%KgTx&tjTHo~5WAoLfv?d+hR}i;yJMbB9QqLc*(7>cr2i)vhPN6EtOoxdMs4rCR3PoxKMYvtqL;`+-N$>^^q07U855W& zhV;Xj!NqYQ6dnpsfV#3sUEqsJIj4o-0flK7FTuW0ckkjjQTD3h;GKfR1c5Vm|G4t8 zKtsi1$~bRF#_B=v^jI0IeJrWOW11Igwlw&iG zw6`Zt5Ik(XXER@i*o)YU za16bR^h%GsVVDuGH7fX)Nt8z|BER{ykVtjlpa){deHSv{{Qi@UK!?6s zO23PV*wLWR4A-BOwEibAabIE6^sB%R4jF=Nx7q@>?!BD`i?z%%`$TnW2T8FK?-Lp6 zRQmt*Eo!*rjzuMqjs%KS&RTGixwP_J+f94r!9ty}Jp-o@{d0fjZ3{-Itl@dVSie@t zDoakM=ItJ%QP4Y``)=yL&8q}L!nR-k>GXaQ5)$GN;`I$a6D}B7Af0CUJTK+|Y~VD{ zSrO1>eZI)Q&e7Z_ez3sH1=q7(eYM;aSJfZp1;K!&9}8>Lo|-QB+?uaX5hCNX7kmsu z1LY`=MPJpP+C~S;0ggp~)s|om<@EK;y~8%JV``peax4PYrpQ{b#`bD2p)K36HBVMA zNOU~ISbMG)UoYFZIOYb{23`+dXZ(iqk}cb?G*9w5=Dw~CES5%An!m0K#Ou2j{;+I= z-aKixAQ54Kqzr+3d~Tjpbj*c5Pom+x*vtDuj=75q5*~j8!)qz(0+j~@^f;D3FEcHF zu1IhqT;gh!YSw6$YR1;za9MO-e5FU+*weBwHovURjs!V|9OJ@%d4+(mCbN3eA-Wut z?)+8n@Y>Y1Uk$4Zfj4wt%L!8wyK45Sp~>-6Vr-U%6yU|pxMqiLzoJabD1A!uC!h9K zm`i*ahvU7&ABW@ld3NERQ>qzpak@dzE+;F>GoR>0X(u%8ZTTNvfmk6@MDCW{c!JfJ zzGiFgfwpGT>w!;lnDcC`)A*oILSw8$vnI&tRe3yDD5i31dy+~27 zAeog|zDT`DlNBR{yhvHNDqDktDJ{icO9wS8#jhEwzCA0$|7~2Acg~StXIuw+&b&#+ zs64gIVb=Xa8tWWOlfHQAe5u!w9U|NR3#fA5p?$9e%|1^*nWTi?o>@OUwam()O}|UK zl-PlONxe$ZpdiJ8SieZSNXr4EiM&c#vl?*8bHa^2AXEI_Vl+|e6(6wjI^rlp$@+4A)tckuuPoyG1Qor>y<@!qGzIXFn1FSp zCCE=Er=I5So@@|SkMAF2FS-^Zp{ms;tz*00?5n%Zfov|5o|M5wVLBpDdA>SHxOeTD zg)k!}ksCMuI|XP7vMcJU`bN?Y<7xep5jN#2S1dZoznDlDBy1?Q*~rKY;A=)!G@Y8f zb-8|QM``-sm@MR|Q)O(9IDWTD?VzkR^78l$=@|skV*Ff_ZJ{1zO0bZM^I9(vnI7$i zn7GI>h}P%QeH4d7h>eVO0F;GdG$$N1$py4PY}(Oo5*2Q74stK zyd{%W9`iI+ms~FQybDg#aq&OthBEv<_a@>Ea|sRgB+{`{CG^HQ?vK2X^LG90VTmt? z_t)2Ak)$~kZiZYm(;ic<=RFqd80iWv3&X$J?-k3IyYyX)C^`QaK$P3Vcy1R!k~>Ly zZWTamG7Pex#`YEC2hqBadJeR5*6PvJ<8V-R?D!S%hTxDYzA3T1wz?V z7^U#SE}dwtjMiTe4yhOMSKoi7`Nb=c2YJa1gP0}&Da9+)$_aFB)DC1e;C^q zCUjUn{r1!cam5V^{utMKD`x>NNWCICO z`<)~ozZlD9Xy>xe%%IPo_DLM?-K(?3*R1Y_?k=wipB`n0=3JWwVqP0r(Qe)^DIr{X z+Tv&SG57E<(@$OZL$Xsn+VEer76?dZy_on#gM)=x#lK`PG@rW}PXHPgvWWMRHs8lteG0=}thxv}}og0Xn=yTfb=&v|d*FZE)0* z`~;+Vk+w!xe%s)vOa0j~tN-={IuDI82yS`AyGv?IhP`uVl*O!8?9icqQ!O}h3f;Mv z35`FxWol$tl#)MVr48hBRg^$rWneTmxPDoMQ`?Le@RcMqM;lwr~ry_d4OFD*Z`5WAky1 zl#3IIYH@s2Wbl#iaty}&U7WLJs{heoh%j^gvhHKb#`Y4T{wgZUlRWA-G_!y)a_O32 z^JU0yBo}(aCfQb=JqX_Q|098C9U{2Y4zIV;#q3H`o<$x6B0e*Lg8YM-dNl)IByX9b zMD+gLe{VaoZ>cp{eyN%t;{BmoK8#N^>I?O*e>&Z=@bqxvOXJ79geV)borbU45cvgl z?VfKZr@>b41p+%BJ5f%$>$QcoJGED}A!ea{;SsTz6l4tR zYfU{`uHmqCN}Ud^wmvXlTQfLdXNkpu8LFL;YiMBg(%p$ze&9)`1${`n|6_OHD(0}Z zkY&axnt`;M(9n|0%`qMZqhR~x?P_N{nNt%96ho@R!r92F? zzJRVE$f|Ys``r0WvLwxko`v+85_ycT;*227;A;P>>Ry>FKkX~1Q^lm%<)?@r%T@co zk9oY$3OA+4_M<_OBUwH1SZY{R$s_jxg)}AE@rZZa@xh3;?;xfv0!tO|P&IT|3J%2K z_YsJJ0i|9aOS|C|HVQ8NOfG7sENnE8c(aHq*(|3SxsnBid(=t?PD!NHr==NjLCBtl zMhFyg(T)oy=k|H?IRBlZ=e}KWYf(CBYFKZK=P~PcqQMvfnwq`tt4`YfJm5fSSCAFc z$i8X&jgDO|H(X6S++_PVAg-)#0>nqA(Y6#*!~5uu+`6skzf5rt6d!(Dj}fASzxX;$TU`t-4;v8HJ5XI&BQ2E~G6)zU~Nh*G%u{2yg+ z8P!(Qz5C+s?ykk%J!r51#ih7gad-C=id&FU+@ZJ>DNv+EiaP{%x6AvU^FQZ)yW@_L zot?2iuFO5xvwm}~XXTLvYurn)iw4@?YXuIj;I;fH30k?SZ5E`(8WEZru!>51s={`eIf@J03^zFB ze(W51VBG5QUNVD6gbx@-51*`hJJpkd(-qT7?6}R6x;gpdFfT@w7p~R^!>V3U`294L zK}=FCUg=2$8gb*>I=5h@$#d!G(O}|d{MG%#eVU*R5k$GiwCR+ea_fJw?*PAYzDAxs zBLB+3pfE?6y;j~Bqf*#tLxdY4r&S-MY)%XCzxHTfW1HgG7hj`)_7_gMMakTk5Z{it z#>m`X+jQ<8kjoVpy2C>GK`mRMIOii?kX#Ml<1(Ib*UpOyG*r2f0>P<>mLjk=`A92> z?6-QXt+I7}(estH6n53_?dPMI!tdH@yNajYyCzE#5K0TWVRNWur~g5`^=8vecppF; z{(|DAbUN^~B7pt5&VMcn=K*QqJ30>CU7&FbReM0fRPoPypZ%vRev$GD=H*Y2f(a)jty>Z;|0xIJ`%z&%sGC@x`fS|SQcebmPm-?>J^9~1 zazEtKbL<9U%Nh%eR-Wa8}Ih zOH$&W!obBr?LNrY9^SAFcsI0%CuMUqL2h>?&!&>FcO*BTFIjwaz57?yUcOEJs0p3Q zF%|BPC0-Zx8uRSo4)HgqGBf6W`6}LnA*WX|)e*kz!nv^vZ9=_igS=v4)C^1qMS0i~ zUBLVCeIvXaY_e4w-xv6TzPEdY|MwE@^5Oed7v#tDMD?Qf{`r;PmxTRuAdaYFy@5yk z9*WsCE5R!-A})(1DsUJN7bk}c0^(C2#*3@?LRUwJ^s6e?yg~}?rfK)%Pk27qod}&GRA6T@iNb~ys95|xp7iXou#OvW-rmrk>;CrwvRj2Dmv6O z=Zl^uLrfvZPE-A8`#%anJ7r~D0nf%ZT*FI9nC!xl; zo$-d=ont0X=HlF#Q!7u`VaJ_qpPzi{RyY#;mtNtY0 zeQO!-sH3W6h8p3J|48vLnVN29bgSPQsE$N}feyJk<}4@wF+gl-_M=)H10pfR^Li zliujZU;lB4nVVUBS|<&fV*0LZpj@U*{AF$j{+HLU%%?&XOm#IS%Cwxq4wjoLehH4? zje@Xs%z0!*+a0S}(|s|kvs9p5P&5-7bXlj=-X4i7PN zzG2g@O0?yb3b2k)JN%gDZ&rk(tGi*BHs5M-iN2y`xqBziRQuodWqpekTXCmqfdcLV z!2%vZ^5sP0M3dq1Vb`Q}(mK+Z#I{7ABp>)UoO|^RGk7V-LcAR`2YU*%4nY=n7Iaw~ z*&NEbnnUjvtst4z;`c^&9W&j#in@w`x%JAYS-m+2uinRio6yIc$BxHne@%(%p6|i5 zcMNtX&9^zlQJX^e?uKR@^1z$+ak%-xEGJc-%Orb+Fp%AZ;2t@8lDxwAh6#Q1YW}gr zkHhrCqT9-r-3`C~SvU!KlXNCY3`<(}UUazs6Ly#-VNS#ki-Dg)m`5^2vygE}MG;?? z6c}Z$$Z!{C;bjrGqp&0ByYU~zHC}~CB8x1}GjMgzt{ws%=D2naaP|)IeR^ik__1z| z_C4A{89}Eu(x*10@(K+NeJngg!tUN0mMMhnWbls#0yc1etTfwfy$ME`=#bNQ(2PZt(V>FPA7*3&9TGbV3!z|iO}d^P*8-OVV-y#&p^T^0j0yxfl?*6C zHk?zCHHoXb&5;;Fu-%h%;lMp9_IdAHK|lW(H)fbdIgzOUkKBLq97s0Vu<$dt;?qA} zr!`)(@_v7AQI|NFIWmzK+CaJ%`}dP;WL#r1?;{75;m3-3%);BNX)-Ja6A6B}tTjo4 z&hJ6Jx4#a5-M(0fx*?xcQQQ4kV&JHA4+2_AQ%)!bv=Z#W9{p2j&Hvc&a*QI@uO89; zi1`$Dv{D#zu0%ERbnYn`ZhNI1h{UzU7)WaNyKsl_eX%)iwlDC^@RlAj#3k485bsT= z14uxit*g!9ww*2jpwXi-A4(I*cyfRkK#C9+h&o^fkRN9e+rgjOnqVh111u#5&StPA zSC9`?(DnL4P64O@36|k;KmbcKGl&ZUfUE(6SWaJd7BZhrX?qzUR*>M*woIrw+KEQ6 z<0w}YO9o4l9Slem{3*vf0ZBL6JGV9b;C|N)A((H4w3p~~tD%4_?8TX8m}->Z#P5V> zM`kp^in{XmUlnl+1YxZV*SF?TKm!&_BsPP7wwk4jz$a-1%%1fV$wDhM%9f-^i2x0}J} zJvp-*mhC)w?Cq+N(Xh3lk&wM+nIVI;0FGj*=>7Acs1WA30=f^({yBCMGf0pmz!Z)% zo&VR0aHtyOH^3;ifPP7(Ims>n-oR5&ciTilLr2*&5sC<*1c=3%GAzmK$Y1l$P(k$K zuyDE+u8C&E0ChMgjE@{Mk`M>TI_?Scqr}V+z!is*K@{Mb1x1+wLE7RRVjbd=@w+VKJ&cdZ&@;d~{!0WD8A2U* z7*{*}!b7Ld5KBOpp+x^D&KMvG;D`;3GsJ75>xdprsSrq$0+B;tAgzGe2*Z=uC459Y zL|jCCQUI;G(e|E_hkzv&Ur7{uBx6I|)rk%v#?}*)9tpnjyrbY=1tL#&0tvC`fk$kr3{_f5hh!X9$IMFiL zwH$y-1}SKEzn1sWS^DNY=e7IZ>DRo!E0(|f#w?Dk6L;rVBP8pJG`3e|Qz|(cb}))( zmtyon=IGT09^WPhZJG8QrPi842HD9~*fAf=aJd5tr91VA8i$Cke=JcFU0*H1p+5qh zvk_muwXTSU?!tA~>DtI}RY}-U4mwh2>NR5hx@=rCkIzQGWEo}M2q4+PNNv0eo&^V; zG69pxjT&^+HT~xO()W>l+n8C8-~S%*X361Bk&}x2)s`i_+APRmY$xzZX7<8KL~ko3N#Pmxz-8 zcMT?Ae(?k*wvY#ronlwpdqB3w(me2n!IlP+e*$*yB-4)9nMLj2bFwwI1pHu+XLI0h z9vh+^N4lMTCJ%77?Ic2giDgnJ+~;!%d4Wg3R?oFr6WdGujSZjJ->I2e&;I8Y%p9kX zOLCX6UPVT$!^CmdsQy({=h*+;Gt54Ge%4C+pPvsk-0g)j92=>(B5qTIU-m8&%;!CA z!#BG;(yBrm&iQxhaQEw#r|HoCIr-8tR;Sl|sw&d+13Oz8=`${4mfQ!gEO>?I4@jI4 z|6{y`Ctitp>i4A@81y4Z-%@hs=-?jVR&=bI4pRCmc#S9v6mu0`_6VMJ>jmUq9165?dV0o~*q=@m;I68GHupA$-GX5pT&TZdN%#uR&XZQ7I2`g;6n|dn zMA(8|>=A z{Tjc>i%tY(KU5X;NCy2^Aoa#ry+p9cENzw=-_r8y(5l8NHAQj9+>ti`e zhwD{u1?aYGd`qWH6DKK(%{&gIY>;v+hb5DqEC z$!}vv?M6tazw|D|{4iUDYfp#j3F=|W`~LMN){V}a(i4vQum z+1;m@uWF}1`ps@*gWs(Y*Ml7b=PREj5Gr!V#g8{Ao>y7yQ8Pmk14Hw7MJL!}F5pau zeflsi!6*blc_6de9p}_u(F*75`eYYbvmXxaGvK4hDgDn$ZeL=Ce^>Az3Y0?>dNLLG z9Xw@|CM$5a%WxCOOf*Z_9De%Q|141unuR8lp_-COfJ%@Zu4R(|TuG7@Gvoq~5Cw3i zg4C>%LegtR+E!UwG}-qG9vFc?-TX+!!_u=PiDaavWOAC@qO&CNWP@G&3PrCfn`>UVG6 zv*F^STv1f54>#8dMqCm4#IU!Qy9aV}oG^4t$sOsJFfVZXvv>S0z(*{gwYm>6x!hI* zH&#L(?h(Z!(|A2hceho1X78uj*2eu4nHtA1CzUhV-qgIOBU0I>0UFV%SsKqY-om_5 zn_<*vPb-dT$fjLyyE9R3H{{ofida<1$AOs-n|2Xy&n8&O64?~A!lj-@d_PVP-ED)X zmqVOiem{2JSbnJzG9*EEr}h%RCW+`f%n$9|Xadr zYE1kYiVx9Gavy2c%{>M;I^^`s-7!3WS{J%|Z3HJF};=P2Lc6)2bdzQ&wQ z*t_yeoeSp(A!2_x>q~YWg71$`*_7yyCL0(zj}tGP+Cukby)HevSLj(a015YNcszuZ ze;3qGd$cRvk8QMhfXV8o*d0?I6bY}+6 zq6phADuC3AqQdntNl6mG8AA|6Qpz)C+1w5D^{<7Y^AhIO#^AB=Q*6HFyGx{TRQ4zC zl#PaWz}ioeH{|ia@9LOGy`ktwDK*%;lj+-ijte`02ck1&h?AkyR>k=uAI3tI@DuGK zmW9~pXM#ncFpI+wYgmBjF|23d0RTP@2^W)TjHV;m4rN9YVh$LLQ@}}L82kUwnUDA{ z(eKha6Uqc}jH8Pa#UH}GVTz|YR3ea{jbq7SiLO8gv4iv=E`STbY8+myKh6!qp?td1 zY)l0g2oJ;yl7bLG_!$r)h{m1jpS zF-SZF%!9HjR}4-g%rAw8M#4;63o!M1akV~za!Uk9%zBAPVoKD=V+FhWKqjZ6XNU|z zI-pepSHX3cV|M*Veo^(5>dwv&Kue(gO?*Eg&HSYXZU^~@z zR!{uTX9@kJ^BC{RB_OJMl(Xfmfp{5s@^ox{+kN{vATFr4`%iEmXA^a^-Xiv5)k(~W z;<4h1z^!3G{!7+N`%C0Y=}Wqai=l#{RYx|1*vFmhGJ518m_ufgNFak=i9T@dz4R~z? z2?EsNeZ+s6{6YTltlB5yTWXmt4{`9!iKT0Vds{|~0CAt}c>R`Kzi>G%HzuropRdbVy^qD>)X$}Z+N#UtScNK%IKN;c=Be4j$Na3h ziezXIzHFarwlf)qTzLOmcJfDi^zjcdf2Oh5?TV?1;U zKpguj>zM}q-|`z3)d3=@ScvTv9M%Kd+vsGokoh76_3=5>pf^)6O}=qS3pQ(d!T5L6 z(OE0C=5+7#JL=6fP|PLdY~{Pd+y4>k{F*HETYzvSTCF&Ky8Kc39l6=?(f|F5$3Djz z;y>WG(4x^e0B4gcBtSGE91wS!41uf_UKCywZWO*3O*~D zu@-S1ctf~e^zqDx>gm$65f%Suxf=V3$B4^_e?xz$kuICAVuib?1ZT95G<@t+Jfz^; z+z80peiJ$&BO2tNlufDfV5RD8eZsoPJ^yujfX`t%TrR#T@ z)w7+Pr6S>!;bSt80r<06BA}uWT7XQxB zF1-bRX7oy(1!)zO1}YbAv9qZB3FEi#UX{s)-wG;BANg8r9$rF>n;yqYNNDVf zN@c)EKr%Y}ic)iMG?0wezO)nsRsfRG+gF#mgNK1RH1?2EWiS&kht9sT)E-<8%%Qa} zFEs$$0CVW=YfJsWdq6`P`{Gi0FcHv@&i;F;HTWygkk-De^dtBc+-&%VOxcmK=1a5b zpLMXH(h`gPm(t(hR;8s6_PM1eU~i=*R{PA-Rq(CS5}SQt=?fT(V4+%TuTaNqa1;`~ zBsSl04ti=XN0PLxZQuVmKB`8UUD?p=5MVBPxZiD1v!s=kJX7f$abQ&wobiRV_te~O zPO#szLf08lHO78U_Z=I&S$_z6s-u>0AjJKa8(T7^W{_}6)WUK2n%h?S(TEBCrPG$o z>#5L(B&0eDqylV_xDLQ!n}qHM+*G}gSA7upQ$Z^+>&zwaeD{RRfpL0j5KF4`f`&3o zw5v9kQ?kb_>qL+FN2sgn6NByv_^7+$HS-?dK9h&?lc@Af-AHz_cH3XoZ{L?m%lQ)d z_trmRf3yUFZZHKLvKKVlLMkO!QUYHBmzAZ)F!*@eT_L|%)*?19^oU1#?-kj#TaroU zw2YKnk_gSDHG%7%k+09tlp)XX?}j64-zr~w8V=y*bW2)`5A`RX!-Gq0ffz>m!C%M*-pqSdXt0kfxrq2Ii;b^4#%%-I* zq@1{xJS8|BMZAEN#|QtFqKenHBtpu}s;418SQ;9q?8D@vJ4gmw4UI zkx}j$pcIP|_sa28F%lszb#z@eP+qAXCGL@OLWrh>l^eEw!AEK_~KO zJWkN7$8MVE|9T%Vl(WH0N@L(zT`e)0N71;1k@oJ{0^?pZEs+K^Ex#+dy>CgCuebd2 z5}*-TjMMrmz3O@Gi8D`ulgjdsr5a85A6^?igTIm0;+pmG;X2BQkRp;YlJAZiG_NXr zn@c-Bo6-hUwW1>B^l%QPPhT&XI-#l+E(jNsN?PL948N5%7I5^*%_ z4vdn*2BcOTJvE#;jz3D(Yw{U3DR}7K+&Pdwk=c^m>AZBT4*RHNe{JN0j$d2u%NxFs zxHjlY^%z0sQi_L~nham=1LDGJBI=#lohYMfn(AL>&gSrnA1@v(a68rG{u$!Buv#n- zH;~8v>t#2NjH;@4!zZHZHPVOLE)W?&ZRd%Mptf^g^^G~q-PQDsiHU?FhkoM`wM7m& zo4YIOue?9j&|e`Y!j2po#3M?H9I`aO0O*Tzm|v*sixU%ZMGlSQDLOJH&l^(IBncZX zN~B9?luJSSbV)vRNq2Nf*mOxLjO>!A2~z`G3HA4e16#TE z_nfyD47U*0G;GVX6s>^)fPOlsx%s>4YNR}=IjA*KUc?+^s;`4XLv7dK1RyV;Srfc%Ilef7}!5u+?XzVmL7ssH6`Li_Jn>@SM|>Dj&6!9#3G*^G4aA9`j@Y_-+IvLY@>bD2)DI2od zXZX7uBjm=p)-7cT@HYv0=z820_H^A~o2^Z#?@X(|)bV%-d2nqi+LGmMHf*h#^9d9_ z){Vb)+4`DFloaw4@)MB}t3@)&o$MkJ-$@0{dP+10&DGd8{4ryJ$4C#8CABKTAR5Cd ze!ga$BeQxxf`n66f#hE@!59m?b+hI$35#VE@1!&-S2`-v7(=nDpk_IWb!8Z@2@4-a zdbVt~b)1W$>G^81OU5Q`CLMBb*7NO!aNq9uobPoU7Pm;4w%T5&HmK69xx8FsZq^TUfAo$y zB9iBF2pGN8S+aXQbFuT=1Yp*F>m3Rpe$yzJzzY!3?;^%Op;eAyq)hYF`NN|;!kC}x zsr83d`5R+?ny2m`5#@bGlho^!(gZN4atHfCd}$T9R=I=oAi6Xc?4sPkagbcv2|iWs z;5rB^jR8|B`?HVg=k1mp3i!+&SNgs#m)<*3=}BC_HSWH-+9(dSd_34+<6r|;(tWQi zbpXGn>X>L@Dhn{auzvqqx&%H{s$~0Kz~G@w1a1biGtOnsN`o^P9F+yZPK*NSR?Oei z7y^{Bz+}MB={pR+48REtddfiznCz0{OwqOm)t3$&J%3#+HG3mwTgz20I-S<&yoKhz zbTGyOA>b^AvMd!x4aX@vF1xx4y}ZpQ=`{&8gm2w5 zbrcNz1aOSA~j}DotnR5whQUj&m|CL4g+6 zYBg$@v)(*9Un=Jzrw#$aMwFJPb9oEm(}BFTpPbB(Ehwz=?D8BZ5@X0T-e&&lcN_r+ zd1Y1$ICRA$!FENyXpFz4?dj63;D>-LtVr=~np&6y)WxNwEIqRJ&PDgAjH}WY#46EY ze{@)~kyQAKn|US!iW{@n{;Nly!&UeMmQk zXH;dO#7O@tdtg@-iOx7B9fzaR6Nbl{4hPSu!jg}XZddFnsA&- zNrkPLfRyDz78XgxtoWKw6A9J&Tw}MlqXTol&GEZs47CUE??`6%Kga2IQLVWrnn!0c zO%V>gKi0OZj`AZ~!Y^H)n5;Mhv&7@hX>1-x{~t*B-;u6as+jYu_6x zI(8ja-X?rx;@VKE6Yfaw-^-|v5-(=oo|m6sA2T|KK5X@tJ^dZH{>Ta^E`z^;*cFC- zju_lW&5!t78U_X7w+s#n5^WJ83Zi)tBMLGu4IUuO4hP;shQbbUO9oB}L79fMG*l-Q z$uPn*6_!_qJQZ;%+*=yCQ3lHn`?C!4H11AVpeBrH*klooC#;DKE({DD0yj-49Fnl6 zw9qu=HH?G|*EIPxY>$3pd$&Z%Dr|Hl+4OU;If(YUb zMqfnJ2%!xQU*xq2><#*R1jz`l4Yqrv9y$7ou;F?F{;;xQR{jX7~ z-ZOl1`n^qC2#O_9#OwsuT_kiJ)-#R$I9Og!iV+%CFM<)?&yCO;TFi~n8o9|0r!c@m zf@%4QJu#x@lelVV95*V{AVC(|-%pNNSbU$DoZy>28U4dL{3JLLc4E$X3?numo`sG6 ziTNKX(Ew*X8+$m6Y%EPQg4Sp;Txb7jJ@PSXK>ynN(4jx|zq-Ltpdk16zmFhVO#h}E z0tKpK|IGV9qL1`nxWVP2F!%p@A5XMz{ReJHd8oGii|@mVzTf}g25XE$+&|=oYK)fI zzw3r*jH=h)yW#LY$Wccd{`W#FFhT=rZhR|nZ3E73Vk@vd@4l=;EAV#%L2gpwFxUff z=iK6ODFc@0!s4(R1KQ{O;_y=g9_Qj+FmGYRn~c7sreS)U0=|@Mp+cJ+zT|6RKARH0 z)RLjtoAmc2l3^N~eD@SRpj^}PAD0@;!b2<8v18>mr+VT8jV>5KU&wFh`==JOGPyhA3-P_$b%p< z9E9Q!CJQ2Qh#LSQ!~;MqWJ5*z1TluizUiTOAUqWXRu;mt;&~QI>mt=zB$U`i(-6rr9j^ zjUxQCG(QTag50zwKL(~E{j?H4T9Sh0v_3ypk|O>zfFD&&L3>(m$+GX{qu+q6nE+DnvfiGUr4Va$9?dIK5mgvNw=gCQG-@9`D)_Or#! z*%*0Y=%!wzwa?Hg?r0n7ADWRRxl;nUh1d$pvL*5giI@fRaiU2*oA==9Klp$xrhn)G zmej`yFZEI`@wuBY6tTJ6G!%()Zs-*8gKj{IWO_G-(M*6||9UguK`alFMDORZVx3JpjCkG$eVSEPi-Dyw5JGbaCxPPKps&Wru zcvNq=w2PgF1#FqN3!O$hZ3VYWQH3IJ$@p?pg~x1}`wCNqDQly&4xoiB^C6o3+EHmhWDE zj*ds0^OiNt`gJXmpMs_zAvc6a*z?rR?9O0k(M5tq>0DaDZD-CU@#Y$$CK6Lk>#_NUh|Ok*J3$&WMpJ!WUd&R7%Sv_IgOYktObf%u@yI8==sWK+2H;# zZj$|WT2iKfgzs&kj-OvHtmc)o+1!6n^cVBnzIt8$xUgeB zM}AQLOP*Z*VBTPUNxqnwinf@_@H0wmH0m5Bziuvn5$>cPT=B0#})Fv zE2n_TVLQLKyBDels!yC}+~;3z2Zw(~tc%9oGWBYQYhP=>*V@+_)JD~s7$+Dbw_|xy zZi&u%#UH6Jbk^P(OZ}MhbUY;5{kqvYc9OoV*2e6@wQBmq=*Mz<=MUr`%pJ^~*xuA9 zlD~o)pMZBhr)Lrq5`hwrzKPdadpv`dxg+i=_*DB6rvYSsx|h`_q|ofs?A^u5rn-IU zDY=5PT)P|{M~iia-d~MOF*)L{m;F`MnK62ydJw$=y)-@Qx?NM^u3CSSEB{01E$2g% z-^+uYaXz6wkUPzHhmEW5JHNt<;FFj6Z|NEw)E?A8YVyKp$)q6TAiO}Wprt3zYu59A zN%$azN7cLReZF^-gQxF4HEmq>2lv1BcSpPjDe5cpy~BC#Mq9s`+=ocvD1_*rQC3kK zscE!Ea<|dP=pqE4>2{1qcT=Beu?^c>N1bL`hRTVa43AyiTZg)*o2O5UE#;V_Yq3_z zJGic`pySi7=CI~8PR9R^>lw|D23^{!XB!oO<6q4==#8jc{P?;8+x>ha&P&hJ&zsMm z-Eh{p0s;5&=P#RFbYpMakC42v>YB`5X7Ek!%e@F2Y&l zbL1rI7rc1}V>N!2>U4dv29U)z0Q^tF-EOP?CvF@NADd|oSXsD}O^`*9HY;K;_h)Y> ze5c@7H~5L+4k(!#$n^kpPJB}Q!PDnk1`#W{>J{_ zt_==y|6qSBRDH_=aSSpIidm`*|4G~>W#hxL?V7mHG2r?u+GJiHV54etH1Rx9o4v{x zV2d?DkuJjE;knm7L7IKVOKi*e%Q)u;aBkN_T+O@oF%O@emQ`5a|zQj zzLt$276`CAYR#`yBRa1xc{}KJnEdv>S$b&6{p;e>3Ev@Y!e#>D)LzidtQi!o)%INV z>h^^98uTjhEb+4R+V;Ztnv3at2Re6RS=pM~OTMxVsy$`UU$3r*-Sh-<*Xew4GYs zyT=ZTAy%E%0oG4eK9i>ft5N~JxAVt95hjry5n7R7B8Va{9_9PhqkgkJ)=%aq#Jf2= z^S@huIj0{Ll1~?FjUZ8ydwU*1*JgSxKaVsgBl-zkI_xj*{@7*QFWV0jFIZr6bz58Q zBK9-8e7Q2(#~!85k>U+8e9e3Q@vwhcxYs-WrlYB;sWaVFWY%9FzKKYWR!*SGmSt35 zyrIzFt^S(7C3*tAuKqhRxgfgEdClQu)?RyVn)%D5fLMT~k>m4+OO|++F_tm*G0q%) z{_jw&nI;IaJavL3y!NMaqJVrc!p@D|cXc19x7mB{(1lbtbad|jEc7lex72+3*23Gi zoPEXKHRhIyuh3e=-BysV)VtK&lDp@Y3{Tmzycd2~oLk!W{E`t$%Z0R__S) zx#BY_!p77+`fGge=Go6!yI3Apc~vEQ3zZ2aQmwD)^t(5fT3TT`3>iw+T2VTTbEDLy zqTgj4z*5Q_TDh~dUdj>{>g=&!^$!&*OC{~4t78T1=
    mj9)dW+Mtne2x9@99Aq!v|L(@w0A7nQx! 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
    (#($I7G2YOhd$Uh0 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
    FGx>L3 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 z zMuQ9LYZO^UQ|Jz-bZxx(nP@-*1LGC@clRQ!G=!+d_u~+|SD2=!m-Jq*8@IP$kkuzV z_xaj^e7}M$1SXX+z`HkP!9IwIn zP#ttI-^x`~#J53h{qYe7zu`}b-#FP|B8>w|rG=S57?2)iJwQ(B7&yyxft+?F7opFKKjl^fVX2E(qoB~*2#R~1H5V9CoO-_N8<^P z+&`m-t*l4vmGv378}B!tuZ_bbw{Z;o8uO+fw{~Cpp7k2*@)h_fP(JXH-Wz;i&em`E z-MJL>>|0sumD|OsvZuy6r@zA{SeI40opwr7O1sML%~Lu~gYXI0ZQum#zz;3)AWhfva4#K>x0=-I+OyFCJZ+|F#B@4;TXX-W z6Q?_RALevz-vpiI#WgtmGVi-66k2^VPydcflfX1XT2)pazRsscD|4ST=H1pxJi7l- z|HQ8EkNJhwyLUT%EI)f5UeEi0Yaq^oj((eO8V(&0yl7VL=ex`MJ$BT*PjhnJN?Bd4 z>6`hRvR#{BoZ3E){-c8A6t7&?7xJWnWskVdmC0vt?UI}yZoW`Hr z4|DK|;Vt&6{1vXV z>Qm=jkN2%VZ%nkAt(YIa>vp^z!2cWhrwU} z(LZhVCl%ejx;#Z%zYY(*|6@X3?_WY)agaYcEb~0p_48r6_j>?q(Y#01-J|L*5jWJL zoW`42r6yuWRHekfdz)H0#5x5lSI4Gar@Ar@6SIUerERFM9gieVh(fByrcMxGFKW&D zy_^Xc=}<$S)wNAc)6N|?zBd?N46|!2bN_DDI8*gkHRg`(-3Eo{Gdzd-Km73e+m7c^ zkBGa}B4$kL&M*&Bt5e~Id1HPqw#MlCpeGrx{cq)uXAyNd_$BJ>5|^i>al2FSMmwV$|JGwP?|<%$@Jsz4 zVVR^Lw+%T2mq9Wzp zFLQsmV{+#(Ik@XaSRN&td>%A3p5M%5x-^X^vz&R6N!39UZi6-q{2p$JakQG&&TsMj zy~f2L*yL%Z@DY~Twk7kC0p~!}zE_ZgI_=-Os&u`Wf!qIjk~w-}8O;DF$Gmxe{6F+L$yAJz1+se<`+1^GGCzc=lY97cF;+7!0VqQ zEb|;5bm06D8Qdi(TUb~stZ zE=#vxevYtAM}FYf3Yl+jVPj_(5l*%f&wn_-kFd-mEYtQOueD9(p;I~n zM}m&+=xnf#h~`1*__zmMA7Pn}6YyMg=(6ql?KnVM{SlTqw*UEUoFQzxA13jzratqi zc9zhlJ00x^%j`%-2UCu)OddSPM~<+}f{(ToI!9RM5tdolLV*@XSmqIyX_sN_nb{}K z&wYetI-??5fiil8WgcOf_7v?mcX0m*%RIs|kFd-mEb|DIK znc{2mWw#r0gk_HNkDbFj!ZLZIJIz=m+ULVokFd-mEb|DD+3A?l1r5x3?FCYgT9WH^05RDCpAP)8}4_ zbN=dYfBpT1il5caz4+DN{FnFt>G7}jKmL0E<8NLS*xW)(_YZ!xM)1|;KAY;~UR~~0 zarYX*%6Oi}yVc3Px;%~fPg5iK@?LI}k588}yp*PNFBS8CR*^o6Gy}?rx3X&1+6_ zwTGjBv2^p*)s8nuOn8YAkC^Z^M*D~fX9yiJ;lo!j66}a!lESFiG$&V&nDFvLy^i6M z_C=b6kwXB4B>X7`z8F9;GbQ)w3~I6PTg-bY>qgAPh!GU;ob6qVpjg8H4omfRhTd85 zOL2i*2$Iw|$yAam=XfL%sy*+5IZCtcoS@`G_N;?VWqKqsw3lD9dTC$A8S~-|OL$na zwqgo@MohT$fLSV~fGl~Mco)%)N2%UFDkfYKAbB(-CoAdu$8m~riwT#;+kwVT@s)P2 zWI!V(TzaO`l1jell&A!SVi`~E(!^I30P;)xB&`t4|Am+8m2e4x`q^W-i>V!@;^3~F zDI?~$W9FUgCr?eS6jEYzkC^bz>To+t9I(Z%i*=V51TM%@;2i!2SJok3aR#q44;00O zf0p35R|EtTOy{bY_$qZoTxS z^h-H#N&>gVdOHVRxrpNBL}VAU-}s(mZ++#L7ZVQ7&p82r;4CH_J{r4xETtO@UrQGK zu|&@Bxbw-*vp>_JSWWkd|56E#zsRd%nNO)I(d$t~1)e$Npjo6wL6 z#9H|VaEOvBKbGSMeH8mrJUufMU+#1?f5AvO3fM+fyarj~bg%?&b}Wie-}--*n)H-& zsPLptLbdK+PN7M5pZY0gb*EgguFP2qASg8j~eO^+s;4r=$rk(Z-PPvH0 z#CmZ1{>W=2Riv3Uo;erJz)L*A8{4=~G4#p_6k7{W=?!0cPs+UF53FTSlhZ+$63liT zjrco!#e-huDkye*EAbo7j{Clxe8!*r_P()}I?mtE{SN*$(*G4}URu3kzDg7N9E(2Y zXia^){0PbGArm|Srmdr>fDXNvSxS2U(%HHL&;vgHp&Lj=HQ?JA(h2?or7@Z;fBWhf?1`LZdp5jV$eOy^Po(+{bsj zKE)nwvHS4__+rG8M*H_AiU_B8jvX91uyXS3qdI%;<&EdR#u|eVNIbWhk_6R8+WJ=H z=;h9zo*f{gF@Yb?h0f>-m1M^5En9rPv$vEsXa?wNyRZA_ z``$-U-7mHEe*Y|!PUW;rWb^W?nzBH2s$Z}^}z1^AEL833^5R;m2h#!sK+JGMJ! zezLR9auYnm$$fbqU+A;40w=mH{&I|?JaD)c-Z}W@`Mb;C-o`6m_(@4L%be436 z$A{wgLfLe*!5e$43`-+J&1c3ZJ^nuC5}A&_>E^feJM{ed*ju!GTF>;N@D@HM_evaZ z{OJXW+=|OoqNJiL(yR_$<@Ib|U;SJ4`}!;Yx>xvK^q7vY!}9Dmg=MEB__4+7p#6M9 z?0I@6Wh`!R5@|dM@i)uJtx*miW&VXPj+E^3F0~$~+<0Kk&c=K47j67=%dwb#zwyao|BQtFD2&cAO0uRWdGF`( ztzTY@`WNAQh9NRzoZ$6Z!>T|*1sU)|el-KAn5R8|WoVrgYV(A4;sU5$vIneJ?rZspq@Tf4yi$>%GskNNoI~t0e#8dJ4%r z_c`*vz$^JW;fw8AW0Cu3<@GP4_3k$I!COcFLtlcuCyjmNtt(qubeL9?Es>5V3t{jY zeUUD%OMNa=nfY#7< zsn6{``&SA**U<`2((_~Fm7&}dil;Ryh&B?48!0ZOgw(iBzfcIkNQaIlz5+AiH>lsv z(Ip#jJ=f)OD3fe6^ZjAolu+FL#!t^^rWLYH=YnJIn0eR1j|2?5%8Gc<$ml`(A2abq z*OgFBBh6`Mr2cdNlv>_OJGG^Z2ulT|Xndnk*mJ7+?pNlhGKCcj^L@X|2N;f5q)=uh z>s8qZ_7jz@)baze`rA^)mo%Rp)-^n;BwVTC8~>26@C{&wO+kT(;6a$iXq*V2ZA zd~hp$y?i3$dZcm>(u1CW-o4U|m)c!%m9Yx!ZY!f?1q&&x$Zru&u|bDtpfVN;mBNQK zO|m-p$!A#M((>#nW;4p;l?k(zyCfl6Q9Lj@9yXg3k-kSCbxj|2zf|=4?3x@Sx6H4? zZfTQNiZ8U49gEdUzyTPQ=lg`ymHehYxXvNP|82hS@R)4KQpGpZc(?0pAi7qzDcG1} zMZ%&A+`;>@@wvv7sJ-ydb1)~}*zevSdAl~ups(?*m0EtJ@~>cG#hEIrs8lQDH-9~c zw>VqA2D8t`s|1tsV&SXdnE!xx8sI2+;<{2@W$LXohVfPeC!4;?KID*sALdgr04oG* z!>p3K7lk78sB14NYS7)Ry2@fHr{d8v3c?JKv7+T#pml_4xd-?Y+!x2;I?Pri$oDG+ zP?kg2fVUM$tfr6W$NOS`+#dx=tv&0?opMvb6VQt%G)~3F%oco=*ztWhUyjpiJ*0zF zVoIS(oyIeK&(mQp%(Fm)hnTN2!|vxEs<=R9etZG8^uMx5D0Aq(QWmZi_2Fd+^S3Ow z<>PE;Bi}bKqcP$^;kPnql_5+1RCX*(ue(4xd?&uyb@wIPcR$Lmuw+T{^J>b^3iE@% z0#*8pALL7|{80TRuS|$FrXXN`6;e&%C2~%-QS*r6L9Dem`s`rdtnfeByH3MO)9c$- zxS^l^NNvGuw<=fZKzq-u`vj-JNr zVaB+o{&>J$E8<3OBptI$$!YSY!rQbrxW`KO1?vdv_*8-ng&1o^XB8d{H>3RF18#Q`FbJwB9R!B-h0Wmy zJRn1?K{6$|5vMc?K*Acm&r9pteh$uHeic)OJ@Bwf7s&?vZ1COz8MFuDXvh4*Ro(k) z9=kY4EYHuA}?P1oA;zM>4Fe63I11vy8V=!eMO1 zLcoD%6LnfF81L&@e*6)zqB3J_5G&F1{8%_o&?<9p9&Z#C*Jvkf;GOt%U#~D&?>Qe`traqKa|H_KjNc%Il>^&{{l~##7#? ziVIg_k4;p0I-Dzw0vB~%N%RL_?8!}z;%vSd+Qv6{WYJjHrtUgPpmd_=JwfimCA)H{ zU(euMDMXwu-L$f7BnmH9#R2TKKrEbSX4bqyRPE*9 zoYAs1aUcH2NRiU;Av9ayf=&ETTFd;CC7%zT#*fF)N3E{!p9;m{?^uj6wrzy74mz_o z6?|%EGaWn4U!QneE2|h8Jl(QZmUUanAR_=Av>eZuSttx?=4=ZTVK%T)s~rel3y}8B z3JN6=hZm&13hNdh5B9BOw3k$jlNy8BD6-hvnW}NRHg~W{heaX0Id z4AgaF-Z_11Oor2`#(a~XWO6hK1Fd2-3)?EK4NuGjcK|VuvbtOjQy^5`Rwp_QBc4XNAq1kgFCf|aSIGP3 zSXs;~N?D1`O7xOrq*BiyHc~|?OVnp=z$9sx%9>?|n|rHsW|eUcN1Aca zbz<1l@QR%Eg?9c!FM9{x#s^@cF5{K(gY_RTGT(eK*;z^R!K3IfQQbH_U|kh#^?YT& z(5pG>%MZ^RE?_=*T7?H?GByjkC#>}cEGly{?E@ZL_jaa?23LZ|)&UO;gW+Svs*TWE z!Gg;0vP!KY`GerCeJhr&;wyuvyH+H16e`9GQ9oV>NAM<^M7)9?5w&;&?t>hA&4`j8 zY53_7Y17^cHa&0j=@d_NDU?hDf~yreG^h5hE9f1^2P~t3J633vLL~8_c~9caaD*7B zAzOoWABQ9Flim2+u=cYno~*f%MXA_Q1+cTJ^a!?mlpsU9>}6d=E7@4!@4JI9dIMb> zQ!+exjAM8&8*G!tEEc2Sna>uC#!(-vup*u_C$dSsy2`qmGwxxHtd#$STWC5Oqoe3U z8C4@luE~l>H%JB<>M-tTG9j(mhqL4&8I@kidbH-`wstSWF;-+;5iZcpP*_E0=^@Hh zR$xuhy9mwrlL5X2i~FW)ILiasefZA$q!m~P{HczUCS~r$(LfUIz^lQD@M2}qfa=3suH3K4uFnF*airSog~I^t8!((~vJ?HGM{t1qL>E=h>!+%@Yc zKH)wot5v~1b2I-6`Br2Zn-`NFB;TdZLg2^$IizjgO*d=@LRH_qom?&I? z9k3dA2ARF4n0;y;o@zjW;#Ok|b+rdgBES^#4bSu3kPz1J^xa6`*t1m0-|_u?leT}=&|X4hKR4a@5yn_6LiyxqKIw&Q77*2do;PmX#S`3hw*t|8S~nok9Pb$=HL9n z57Vk)d?(BD=(0780tv%Xqb2>(Te>qYmW0dp zPj+qVk*@JzrD_Sc)4RfEXYzzJ3tDW-eLX7N4*IaTH8*oQc6wS| z6LifiPLtjpo|s>B_{oyEq$~O&4bW)v$B4uBef`F%V=J0|_l+fhLAUWmFO4sudcs4J zCcjaz`%q0U{SkZdjnB?A-esNW8xW*V51$u`&6n_% zaaf;i9HYebfHPdNb};gDa2@N_yf@x#o~QB00Z*m02Yj->qEk`~e6p#we#2|$QqVIR zN!uc~l6%R8G^Ox`ZHrFBz5K)W0E{T=Xy*ktqMPJ%&5ub3a-IA!Z>tjbYrVTehuq{< zLkpK-0PDb=B=)cj&A*=0AI(R9=6%9FlsvB6-AE^IJl9HGg)Y! zpuqjo(~J*IWQ8;mc9v@zg6npah9o7|hj&~5JS*;Znkv4Ooz+Y#vso&q zIj*BhwA6L{fd1jS0^o5ykTHIc339lw>-uNADZz_F;>6$^r=%C)JV8#0U5{WwR>3uV z-XLfA-G?vMWoW=X-+62DYiMqpg=oBxGh2iXNsrP$N*T0lE-yb?;LH4ztv$DW^4jdZ zfAjGtmg>De1^rHOnR~t5ZwZ#Y+QVFbUR>sNfy$>a*k64-IPzLCK?+tr&o-iuzn=>?|Aial!e250M#rR#Tk zT9WA8t{D1Ewr+65?5s;n(mM5gR!VyXqt>u>N>9?qun}69VMF3&;7^a^<LgV&F%t7Ut2XUVZkaR{gl&;7=E*>cr58cqLv#0r{ z5H+9cM^tjM>+)2QA}ke}+0ejP3~8U87V{;4Z5o-=<~V?J7`3OP?$%A$w8Ks)rH6iF ztzJ6u5tn(yW#&Ig-W|1i=>dGZnOWJM?Vt*cF%PGh|ApJ1B-{<-zo5P#D0#r z%>3p@T;?;5O{eyf8|`|J_2Q9k4?lUuwsSvv zIpQ*D)po4x&GOujs1es4G|OuroJuCDn-x&XOP&&UKc^^%0jj&e3O+ zq3<+6m;^GY8@!IV%p)%Ih|4_UGLN{-BQCRpFr8%dKc@_ixXjL?vn^<}M_gvEfKEvr zahXS4=FlgLz2X}>;xdo8%n@~S#AS-#I^r@};XFJ?T;>s%dBkOMML8FE#AP0FnMYja z5tli~6C81wM_lF+mwCix9&wq!It45$fcuv}{P6ofy#K@Re*eR-fA`R@_douNQnjjB zexi8ph}PVL9=~1z_Ij`1JF4Ga@3nBBE5Kgwr8-X)V6RU_I{!4)ZzU+ZzY~ik^~aNh zMCJ8(yn7Nbe0BN$!{g0d0d_#-8|J=O^4;dX*TQ|C`^{bt_cZsLy&CRc|Ne(RJpS;9 zf4ca0|Gx62e|homNW*Z2SQ|6Kg)!{go6?FYvj?@G)1=2B{r_m}sV!gdSWRPXh5JnmS?eePd~ ziQOxss#Q95uopZ1mVn-Y4t@1!2K4U1#9uD7dcB7aFM}UTwTq z&T;yisF(Lk)|sxIB^$p-`#1^exZ?0}ICEYG!I3}Ch~&d77nK<4;u^~j;J0ADQSXuZ zSl-z3JcC&I7`EqJmcP52n&V75c$6ojOg&}S6y!H@MU^8<+8pP8eMkG`G$)lXLSK8GS`)(xSW`9 z$;urye&rf1H%B=`qIa>aXilb3yaDEJG~8K687zb6$X!>?8|OYp&H<;b$8*oI#A5kI zjBGhidS3r!#ThvcNA7~^OteZe(>hcu(`RSw%MmhiE=mQ3)}FuDxRQ%lBff{kz$Uj&pC98h83fW&W;+t!3U7&22xm!*nT-k$^E0o zFaO7;eWii@nSPE~a?v^T`f_TG*eUBEzV>-FjvBu*{-!UK`S%$rStRKRvgomEMy|86 zUd9vI5=V{SKNB@S=Um9Td-Zd~lD~q7h(?kqt+!0TM~&Ysha;A}N)GUW&i|sn;b+xb zDA#@77CdmFtyh@!it6S&M=bfz&hha@_@2T0h$Y`4m0#|t@r#H@p75i_uUzhO9Lw2W zZhXF?5v4B{T|FAtM~z?PQkDbRo^5)ca?Nw5RSPIjHeVXYbRO%GAK|F+YrlZ)RHe@L z5~Px7|G-asYDbOVqsDJ8v!lju_3O&?UZ$0DGs*C+wnq8r?RW_eD?Es_zi!Y6*+4BcG~)=@$0<0eUziduYDyPhcQ)=vfb zzO=o1=_Rc7=LC$tIS+gNe4zE4J*Kru9|5CBz~~V$dIXFrLUa@#W+WC+RRHI;t=b2X zZPF?`@^Ew0zYn%?w?K~)X$a^#0!BMRe*}y&iaJx5^rr`7U999A}7301WNGr+v zn~#bbor?X6A-@tIUwEwN83f~CcYM5S{OLZplW!yO!zsX4eLe5XVg&92|E-U}SpmDQ zwm?t@T>P>uD_3^#~X(!GZNS0!G`0Jpx7xXn#-gZAG{Xc|?s49Xr&*w`%Ji zwp`kpmAb(l2_Qy5?GZ5g;wa&RE|JE&yPS&!*!@|5j)2i4VDuiHexZ0Viar8Hh1`q) z!wQF{`76>!hj@q&rnjKwU%<~1FlrUp@VE54j$~)g)9KcgIRZvsV1oZe-$%e`ex4&> z^cA(>dF&+qF#a(Kh~SA6nj;YktN-n7iM!ZgB`m7_kv_8}5@7R=jdgyuk&pIzz*R!N zBVd%B$}hmye4)VoudO)ze)W@nOTzV%+@iv0alO{zOd=W3+=I08nG z;=@Pr;bi>{|K5kJaj8Sz2+iRh#5o^$Y_B?d?>f%$P=GLiQT9u_N27E!pB2#m{J~q; z{rfjhu0QZeevG^G7QGi^d<2Xh0i*Xk*+;-AZ+o;;5qLTal0qqD(g-y#2LA{cJpxAU zmf7!-jKPo6*X;#4fO&Pj`CIJWq}#IZQIlY?&NAFfJ{V?k&Up6Gu3J3Z2LJG{K6P9V zYIXmERy(%dr-I}AA7SAWPxS|Z+>#|p(0cjiNf5WA^wjhBZ5!D)5|ZXhVh!oho^bP{ z`0%?{qywSDxj%xj?8veH#H!s`mt>%Q5|G6C?bh8TUvDlMijr{H6|l4Bi4V7T){ZB< zRqSkmDhVH~y8S<^h&$Nb<6SE-(5f5(qrw>gr5)uXU=*4h0i)MXFrlN!th(EFlHR73 z9s#5AqMQ3S_G$|dMK4FdD8G`*G?MDy9s#3Az~~V$dIXFf0i*Q&BVaVEn*q)MYJZ1L zZy$#(bOel+959P7L-Yt3t?gk({yAfOYcU8!}RmgKInL2&pyj}@a_u$p9*HeCbD&qR9QB!!I!>^ue z3cuY!a;ho(cCWAeC#fm?aC!Uj;p6?){nL$2*sG_HmzR(4AC;c?@Z`?B>#3&j+w;n{ zKVk0sT5NOQi`2f%eP8#}+~4i>mH#Ajzkm0>EV}&EAKpHh?iH7_{lZI5P{9uOPxIW* z>+XI&%y+Et2|XsfdkHswiAVm_U*|E(106PG{W&qpZ_k5|zcBbSa-9ahli9aaA@HGxAk$Ss3}oB>r^}?JsqCyW{R$ z@OgxKZxy^BKhp07Cf&YokNV;A^8NMQ-Oc^m2L-*~KitpN%aPyZ#m;F?m7Z_+k)Kz3 zzT3b3AODXJ|L@~N5V_myOTQFj68I)W->D<$e4e}pj_eDn{`GHu|M$NSiCeFen0QTfYEy3*@GXP6 z-O=VBE(B41XLI-R>Q<75+qd7n^UswO3Rib;zPrA@@2md4|L%_QT_T5bK-}(#@s&B9 zBgT=|;7Dt5q%}Cw8XRd2)Tcbs8jOR_8Q;~eU}`#C^b_h}b6LFcUo``(d;Ly6e-1!< ztBxf&%y-YlC0iUlU6`;?sS&>CfI{Ki<2eTag-|#e_|Y?Tt?hkgujhGJFMmcI>}y-Q z9~^oqhj^}7Eyz764v<4ticz6}r3s$`xM}5>l1i{zU*FQSoB1=2h=37UEJ~&d}xzHOL z%cP-`wrO)myJS^`fh&O{;g# zRCOvAk39o!QxY z+Dk`JnERb@42Nyxr7vwkZtg970G>FESJlk3Fn!PY&XLyOIW2)hP-@5pxZnz+@rsu; z5Jy^rQHQbZCEEpX?j)b8Lyj+^I!=(%I5?m;e!mx14t^ck@O*grCOUCUtneay7EM;u zaPU~zDxHJpgroZae8-JdF~ywkuRpxKbmbl@*uu{poN9Y3zGz!V|J5&s|D^%C^_+AQ zcwy%PU-QK(rsKW%`4u|*DChV`Y`}L*6EyhowL{-m9NuwbyclOuGD-c}uH|bN7S99} z6z^EM3QEQ>`0lIv(kFhQC!G@`1yALu0zpiVY1FCxPV;iU16zINC{q6hj^>N03qWismyzvS9!dLJCW7zmy z$pFUMyh@XF3Ip5f&cA-oAO^vj*G! z4hBMt=&*zL;l(2ydh5UEe&LPdp2=bN{ns(jLAUkiV88Eu=7~k9U2Y|9} z4m&yn%54%{G2$m9q$6ze78dO6qZyv+VjHVLd8>6-l?@)e2jM~p$ZumOdAwmZ-5Wn` z6ugD!g~xR$)zx;VI>y`j_{z2}>RE=x;ab>rGce27(s(2$^0j(Bg(DAq2NTnPS;>32 zbLN0=&>@eJd}BRKb1}9t%F|f;+ab?Zqd+6@9b-EC+Iad;c#UfB!&mi;3+fm@X8SyX z5ug!&Dx`ax3+O$=$HCv^KDnzP#r_tYe|{hAN>ih5+}VBJj{OOs?Xaee(ZLBuJK8>d z$x5L=H1qoY^GL@1`{n(;rnTOBAAO-8zfWI1TVLbe?hoDcbRRmM{<1>gLLYEQ?&WHhY88r=C;ePg5#O4GuB&HmB&;)%C!KA`B2 z4lKuC3=0RqpU5Au@-V(XeqZV@jv6+V4l0VJ-(`uQ?7>f6$LHx^TAO>e$LGV<3$1;w zSKIeabelYE9Dc)L{Edbi-`wH($=?oX8{G>(AtkO+?8nWX!5`_a**o~);E!MK?N4+U z77aSudFIQ^-k@Ln)Hu$+1~*Y`;4~3qCtO zRtN23Jj)!z`o~DJ%CZ7JH$H5C=M;GDX+_g5=pkPU*S|)~r}c!H;jJ3SiLrN2$oXOD z1tx({SC0y3g3tIEedd`iKMARStA2MF{+s-c|Gnn#=W{dfosQ7+XVQ9<+-~SebWQqV z`d2nSmhmi)v#;rWcl0EB=BSpL9lq;HF~|oTqV!R32hSUxiSv=X{`;3YuGgcSuD;B_ zetBsG=yO)TV@$*1$bQ@Cz#1i!PI9ksdhPG=!c8tD6+WQcm%0H-0F+FM{7k+)LG*JJ z0oVFo^5*p$Y*DGV(pX`q^rge{FlzedrnQK6JtZEnu}c?LAJ|V;#^V|Fv+|$y%kwex zhRF}h=0)e@I`MzLFq+K6kaM3(w?Y=a9_vrIV#U2Yb@EfR4re{F%E&Ps^`2ZFX<)Q3Y6*8uU(a@O zJ+!D{F${h3nuV)p+@iE1>FTZ5T|L2+?2G!7tk8Ae16%*T@2x_q_?v1KC}kvZ=j@Y0 zlRb}s?9-AL-aIRy?R)8g3JrBr+HJ(`f~ z5Bh_iuEP^JYCX_>*XcZ5+;co@L;E^|+1R_a4JHRFchFtI$Q)_4S5^AVnovY^9plSnnr)UkN|=Kw5d$ z<7wsI@Nh&?AlurQp`LsrjDT>4Xyn%Do$G9AtitoxLhl4O8j0Ykc$JZqz=}>9Bs%Hp z`pK^ir+vlBvgts#`vvrs;_r#=TYqr^D{p)j#rRrbG@$ieP|O2ur8A3Rg6DZ6T=&+% zh@l&&8$}0&4sMw7V8!rL@4rWHXvlbPQ8i3~^)Lmb^Qd3JF&_5;f4Vdq%&LYtp16bM zFbBNxO7qFjlRd7#xHn7+v)J*j3rW5+NSGC%tr)u~{2zXYU|BtESNuI8f6r%?sfW!o z57WFwS9YDxif7*YbaMTFFkdJQUXqyVubJhs6#&VX&lAwKS-3CQ)Es*r5jh;>t9hmM zN6WnACxZ#q?s47veBebWpvQO+jq5neE&U=)_5MRT2u(3ejbqV(VTUng&cdPx&jd}F zZH?UzpR7Qr*cWK4g>429upHi*pkX|DP+&Ii56t#Gm~DJj<7>6?B$D%qL;-J*&wCRE zTHS_Y1)xfpo*I?Oa8Lb>@$`>%gl{O}AxZ!n{_6bR=)(9qo}Yn&`EkoQ5}|KNG{Wd~>B zCk-U8KwnKm>TLa(u)!tHU+;!d|n71HRav zN0gcE34Xk&`uA;f7H*4Dtq7i-ZHmen!cXRlV%sf<@+?|jk5U`o!=SjDYy22qTiNc? zme8sSp1}})qszP~19MRgyv}oBHzP3l7mgUM9hp$VUnm;}v|o{T1N~GL6!pE!nlXkr zhVOY2rK8lm?>%2wag+}{*2;Oh?potyh_MVtdk1J(2DqSkeHo?xlXRW`i8G|#Sp{&p z>zc0Ry8G83;HNgv2lu2RSQ>G>fo9ZdgKaX(cU?zc?ZdQR^EAdP7(M!yb|gKSYEGM&!6QpC&W)u#RQd#Wo!|gbY(PZ&5@60`KDj!Nd zcdHhij(|welh7ALoC5Q_olkVnTWk*sENLG!3bT$Aq3^=pU4yvmc2n(Ac|Ko=k>Cu% zM%;J6%(^7=aDvv^Q#h6TC=p){e^Qi0{@B0qNbb9j@yRJ3-8bI+jIo$IPOTr%&>!v*#^{CiW0o7&M8ZsgKfk42 zgjUgvB{c9J8ci5snXMvSGy=5>_--?UHE{$`^)$|CRAAu&=h2f_zUpC6paV54*Kt^< z0^&vH673kZ-r+VZ0O>harN%>dGq`rwGK3EAH%GeEk zpnD^ML*C^#4*lUBJ#5?G))Y;p=NgG|*{w6bxMFw*JIR`OoR|FuTr^vHckr@~LmP>A z=p_k9l6fY-c|r6KAY&$4-g`Z1;o9u76XM=h#I?nE`yT&)h#wA)n}03IL)&!S?qK#w zcpa1RH%Mr}`HW9b{K)tZ4Yl#(h#+HSNV+8K*x5&8gD2}RB7n1ln!zXi25*ZtEQB#x zl5~+YCeQ@gbk^{WZx0OdCNsx1?GLm*$#`>vPiP0X3d7MN2*Lv#Wx&Q*FNuOQrQz$s zQt?Ww|8}el0>c55oKB>WPC-kb@SGnz%t^YUP5ZI-%eoOy=83RkIG#}lMC%HI;S_72 zUeejnRq6Rnke%|BR)9h2n1`oA$o81Sg7>5tkopqa1h+I6kRxTn0YfojY!nYBtDZo0 z+mW_gnUt}}b~T6E+rIS^%*pnEBi6jsHCRZJoCeq#A721Pa1DEU8(*w&<1oCB;uBMtQ<=sutZvdcB@YebTGhNrNs9o+Uz?c$)-^rGZU za-5GDtBbv@k&{*+Zw0{!YpuEWqEu&l%q;0o!-&J+ zCCJT)QK~T;jKASN_>X-*U#44Gs~aO>&4D0V^fiig22OuN;C#mTf&Y?VaXhxKK9VUk z$23pZc?vt#>l-6UObOMxT?%5KBd=4!LN6w1sq((SPx(0Fb3^k_}=)E0A@(bkB zJUYP?pH(ymd~qW3A;A_8bYcT!KY~H*E{?SM6tis782d)LM?9+{Av|Sdszgof3*3q& zkrzHWllLhPT7yEj0VGXIKiLs+tr801+g2(Ir>C)K5XR`Ri)@KuwZl;GOou|H>8bHi ziH~S`d21rY`s6FNvX(Lh0rtib-ll? zz|=#K6jmXCynC(RIM;%^`hNWnEYvN`$)>?+PW{4NoM`Oh7%%NB4F}$(vszI9Pou#F z_0_qXEOB&)Q@S?Z{7jty33RrKV+D1lc(rAt)PA0;D%#>p^=z} zzWg5R5*_<%J0F%D>g>aOj*5kwqgKuyzj|Z=t`Sq9ohK@?3>6%pK zFR*O+!1HOhuhW?<`iJXa|GdLQYURhF1NJ0dANaiwaP2VVv!cDYZeJ6_Nj`}mIQI5n$E|4~75s+0f06Y4-V19P3ngAW0QGGtwo)*f#k zCl7&Z^QWJ7KTI{N?uY5AkFL!^_pTw8>)A=RZFXMYyNyy&!;u5Y& zMsh36ArD{W}wJ&k0fkax{#v z$u*16{r2$qD_m#Qr_Q+^?^}P~m}oOwF+Y6Q?RY(c1^%P&SVOxM`Rwurk@WiGz4G4l z0~?~f=?CWygTMZxf3ol6zZZIW3Rsi|U_=zZ|HJQo|HH3;_t3BRKmJQ`nV(ELaQhO& z`l$f!m$p{{m3y7&pBb0Ahq-+^N7nN zT&H2>>p+%SjLGTr_Ko=sy$}UrH^gnNk8xXT|Xlc#x?kD$&4zDTO_sF6gS^6;D|j=0RuO_188Xj&$D$xYKvqHB7{_&wUwQaeR3*4q=EI5^@m z^BAYqM1TJSmYc>A&=>k_SA=Fu>mPPLt6K|AJDT@Dyr}kgZQA@1mzi`)Zt$SQ2M52} z<2zHQ>Apy=eHHw2doDhn?UMf}+01sy((63YCC_Z}c*&h1_peq$Mc(=j8Ec=@kwixO=T71_w5+?J@|U#w01tg!*cpw^PEiCH3Fi$2iT441gnSwBg|D?8D&qJa0I={*0GA-1^1W*>*>{>63g) z{)Y$o>c+ali*0?u=!nZ?N$&ap{};R(dP4hC>06KsogHzR=$>2Wh|4_UGQZ#x;YG`* zm>!>pX8h!n+<(ZG_9&0I%p)%I89(5L{pw*a=eghPuOlw=nPzs{`IG-5e~ctPJ85&r~hX=yp&tdzG$D7 zy+SYJ0~=>qj=0PTmgHz2PGpswqJk5Wfniy zDX$|g(@74RLi-3uT;>s%dBkOMar1!NwRFDsh|45I?L;`!We2U8yCW{snL7LbM_gvn zik%WT;xdo8%p)$dQ&FAX&!=+4Wgc;vM_i`!^iJj-ahXS4<`I{9#AP0FnMYja5tmt9 z>=Bna;t`!CJmNA7zS_^-A90z*XB=^vM_lF+m-#DPLybkB*%1yu2n1228 zsG_=8=ln^Pq(_m}{f$vQS`Fc=i}BA^Uv_U)?pl76t?2CZ)t73We@r#%Q8)Bwl}#NH zuKOguUeoz*FSR>rI^QjYcJKB=yQfmTcYDFwf1Ogh&((Fm|MBs0)a^b+w{8`7JANcV zh`28S4G)s=IM3x^>;2m&fs2o~k5f(OyFHG#Ala|a{eG{rJLZ1Bm)LzSGJJoku=}gi z-0x2-{;NO9+@J0rudl8iBHs2)5k|qBDZfpJUuLP-@}o=97jA>_=Fx4 z=I#;3-%^}*pu>i*!upX&ctmn@i{6GKr z`1BXIw-;A`q4u!An-17rREPNT;)jdBz=Ur08@HEeXTRZh4~v_f+wLx}&pNoe!fQ@F z++D@f_8uhleYqq1!ug$-;gtUB?2e+!&%Fs8KD*-Fn|Bf_4U~u`C88I+`M^y z=l<&YYy?suyxh5t$Lzg{dwRL!5aQV#!`Vl0|L*c^1WtRs+_|j^`QDqjk(WEl-k;rh z8Nt;hIG^27E&TMIE1<^lo_oVPpYI6ydb#6-7Sl+ z8fJO!-Q_3R&XMHwu5?_>yZ7`n!qU z^uM$3N_euiRbqX5PI9HS?VKz3tPv$ow`uHThBZTSYNH@8VWwzi1}T%NP_ zw6wih*G|{>14n-?&dJ=zq_29{85=k;@DGWJFumND71HyFkD8Cso1nT`Av|wmY(Y z9-l;SYuo)Ymblx0{P%L0C#g-=$zW5%Hp zyUuGQVd+8(r5T-ZQrjoKA^LEh8?A2ru8d#HbMld_Z3{1!;$p#ZZHqh-27k5ocfZJm zySvL7E_auA(pbN=GoD*yRQcoA zxs|%ey<7hT43PsyC$m1Se5H^FfW{7Cp&{>la|Fn#JzmOwW0YrHCx;QAa+ zIXwqbvpz|DNXj)oSFWt3?QXVlyn8>(ibT&c7MkcXk9YD*&TuB8797$x>!Q9Pa%&o& zw%OM3oOC41a~SX1w(yO2mv^&#)1PD|^SgWjGyc81T4e1V?RmjT+S)?D+7>w9$+0x! z(4Cm0c`VwNd1zbc?XJ29(>$~-YknuM?u-Z8W*ghwXgW)Oi(Z0(3A=m!Ty3*HsZB`L z@|@GZYun6=y(?qQ^4!9UJ$HXK!StSMe&!|GM19TuF(#LJIH^9tz`NqamUk(8OWVSm zyt}NCw%b_;sqGWvQ&wjA#@z~Mw(~l-`x#I9qOMn1F@KkN$bh}{Cokzl=lTj=O*Y;ME+q17 zi&xWTp@lN)&bYyKv(Q%QXJ_7|ZPx32Lou&qEO*QL@)9oi!p*SIWv+)A*Gd6A<9I=_ zGh9fDWqeGvg|fK^e7YIBQ$v0_WGlH?)0Xd@{c; z-*`9MOx0bO_0!rGUc!^H!fLh8Wi03DpTDdA&oVdus|5zC&dj(WbK62kJP)@seb^Db zT)+Ejqb40)#<7*J@v1ENT%newKS;U2l)b-J$TO~p6&D6798RgnD-Lyso9gC0e|Icc@3-D-b_C8+Bkw=;YMw=)}?Ls z@Allhf26IFCG)$BTF@3n-<5=Zer}Q7`eVGzYou+W)t;khFa43MOPhrBYulvT*57O|)b?hw1Foyu zH0hYNDF!!vqa2WH+X^cQF(#c~f2#bS%%GX^RNc_Y@7*_elIAh1>$Sf7X3~**SH{fc-C2J0-0dVUYm*Lt zeiv9?%Vf{pUr+pWv`xIdwm0jVr~NG7kjZChgKSHiB2#PILR)Wb_slTRwye2YQDZ_#u2lRs8l3G=3RWy+r9 zhc&DG;ryKP3NuVAt~SG773F1a$=~%|MJ$$YyqWe5jHP;!%X1X0rET7$?_Ik<({s#^ zNpIr(yS`rV@7}rWd3@R?{iSzj{a~~$W4>Q}f;~6yhmN)dex6%+-90;H=1uqPlzDx% z%{k3fHrZGoShm&@E# zo?O2%+j;dj`+P^+tPgoklALACv+uQc?W`@&%{n?Kx_VyobERFIaMyD-4X6G}xj(~7 zy8TI)?;8+$dG~g*?R#$aAB?v98Gh1E&pN{u={xIswaq-Swpm7LldOIoANz8dn_>cM z+oIFx&w-VtzuEt1ZqkO$YsrG0d>K9W>aZ6=sck+FGTPR4Bg~eyES26eJ`7=Pn|um= z<9^{y+7_C|I%ge|TZ)LAzoF3Avc3v*&d;&@7FuPMO@4XKVru95?xGVCjGWigSjzA= z)33G#2HK{*Mc-sy+Lraj)fRlwHu>DH$EL=w+*ueeu`?(G((y%c+X^7 z;cV}7`7T$%+TSXF?L@C@u58{+3rK!xhY2tIYdCA; z{9oD#pS5k)o4k8B;}wM*f1lpPoo9Y^Evex==GtbsXyf%=`df4}&)v=VSKH*@bsm8o zHNpP++F`2+|7x50kT%xZG!||1`l>NH`;cuc+9a6aQWpHRjnjPkh7&e3ztZ2l&)GNT zGpVC(9UnDfqCqMgZEyar<0|WO*zEEgKD)NfI$>Fq7Wi@L&-{v-IG@!pkBT_W-*BXN z`7WNbKKBVY6SWJR%MiGHW3f$BZ5N!>HuD^+ErOf-<0@UoSEA?TxkXRZ-}NWnb+}@A z_Y-sDf?vNepC<%oMJnd!-U;29=YgHh@{QYZ-m?*Lf$h%orLN7sCC|}nrg>Jt zhox3p)i(PmJm+A>3>Rl0XB;<+Wi9z%X8EQ+gtYWGpSJ}U6Is7IpY`jxMbEBn;cL9M z(^-SwozE?C<}iF_Jmo`K=uO)K7k-rm&e|5fk*{Uu2ij)az*vlCf|a&qZY0lwf7+&f zI&-s?HPf56+2`xIRn}^o{ZgKDg6;MAeAN+-wQZJpH!yAXtJF60A#I3t?vDaJ;gIJR z+oiVIx2|oDKeE@e`ed{%^j6ypQ*ED^?{c2OH=NK}#tb=@xh>}zdUx@+TS;G@;9@&( z#&OTtQ(2zl6kOWoGi^$rsrEL}pti+#UE8d~X`Au)~iHrhTh7IA;e_~tVCtHtUJ{Tk*7y zOnSB^hf|jCQqq^U#b45^vz_eV{(Mf*zQm2XZBu`h0bJV_{n@+o-n;%dLM9oYZSk#Z zTjY#34&!+&%h{B^F~<*Tb0l^7F1>h}N99WAZ#aWC`;K3V=2Hu4A!#dw~HO+x%rGp@6NFRwJpAf+LpQTqAxtruIa3cR#kq1A1=Pg z9&NLK=!RE*wpVLgaNKkkK5jdH;iq@Y`7q3s*qZS^|JuABd}*^k(YteuPW{d2^hew5 z>+_uC4AYwPn=N>tZMGM@yX^no+~2Q!?cSEdEZ=bFW!<#Re3IAEfuGmo)3(SSZHteh zwi#cvEpw}F-V@elqi*@`vJcjCv%RlPV8Z;a&G(7lb9>Kmi=(u~?5C-`#f%%Z&H7qx zvwfs(kv+A|vIk>dd~-1JRU(7g^|BtU?paUN-+T_acjt3`qixptJ-7Jlw5>Q~dvLZjJh#Y3E~7;@ zYI2Zqf~mIIm*KfZ9@RGMwo$_Dv(&c84{eelPv6xx>t>#FP#z9dG9VfwzdT~_)At8khPN>^zNdgYrCEG&NmL9FF53E z%{hK>w0dHq1t$+UcalD`=bA8TYOsD79V4U;AfoFw)k?jExKrJ zv+h&d%*QEQi@jRgtfOmNbdcI+dqLac%dKtZ?b;UGMcblF)i&!J+8jig;Y>l8D?CTS)flbff;LcgTYs-51E^~Fk17Xqgb0p6!14yG~U+w>A?@Zvmp7Q^n z5-ldnSSQODF)gP1-IqkG7E-Aw)0lkMO1FD+?+w{16%|7@CW8>hGR96!hC-G^q{*6{ zP-7`uk@SDQ&gXN!>qUim%P z(M(y1EZKTfq8lv9FSa8pD_H|cS;<%kFIcjkfwB_(Q(1AIhBDlIG0m``q#wX!5<7 zyx)ZZF_U-2yr(RmS9LY#OBJuLSMd74!WsIIQ_fOVb$eCb`4srG1v zgZCC{o(H^BC^!Ao!=9N^nb@7@y^Ij$OjP2*m4msqgem(S-C9lW6CCYidSO55M zUW8TAKVFlrs`H>~rW*s&wbf&5lIdCxF*TTN3cFNQRo9sk+q+}asgd!n)o17~RV@x= zU+txJ8m6RYP4(C@y2Ta@iYID46$4#j(^V;vBv-Sww2_1>qAjW!Y}0CvwrI8oS=!9Y ze)5SkBW6=jKXGbj>llZnVf3a-mvLn3fQCZiD6^`QCCCXYDov2v-GI1^!RHFX*u z7f?qtCDa)wZtlubXY$MW)N!()`&_-$nLOH7Qs>i4t_rB5q#mVH=uk)fLTS@LR^?!&Rl_XbwEWE_#G#CvEU6`wmC&W? zwwf->v7G){3i2tn?8&FpmSrim%JSos>dvjDRB05$!7Qa3oEGy`j?k3lC^awqGG&%h zMN+1qgwj%J%kytNtyZ!*TCEuKxGJYuLaXI|mR4J^CA4mf1}OA5ROD#2g3O~;3mA%N z%}T`5s@dKx=BqltwK|~Tej%;e*k?-eX|>$X(}ApBWNEbm*%H2Pi@qvlSVXH8Wj?Kz z`}wrmC$qF#fouV-#k)>2jq>)(%?#`|wIWmdjf?bI@eVu@?% zMhUH!X<1q=TYsF3#35Eahk7)oq;ik)G5V`@!sf4|?H-M%&3A_gk8-T~&h(o7;NEG% zdFPVTE$(r8!v{w>_~16Jf;g$uP!IFo&UN!X3Ejx3#x2pTgj3?^uEz#be6U>9 z%QwwUX0N{PW=__YxKuF*<(60}Fau>v+%^I(qI=AKR8x-<%jSa%T^^I(a7t_wmZ`i} z!M#~&G;@z~vd#y4$#RJ~C+8AlVoRA;q?o(ip@qlnjdMzDjoF(uoo+5^Jfsh>)V8M7 zkGZC?rt`txtr4?RVkt5M2Jy@qipj8>d#qecF4L4)q5t3>u1O{X63>3PjYZ-8` z7sfQoI4Yeq-yLe2RoJ%(Ga2FD(RHBicWBESXmmVMwRQ1y^k@7TmN$nuCQRuLwrGvKb%em!YNcL5Kj6csRU+K@GfAJ9M1N;xLUWTt^&|g^-EhLV56nW<&GA7 zskEqesaO469pvdqKSSYkm{-FElY9!PvwA_ZS(Rg&umo6EW?8Md2y;i)G5j> zIqD3?&b1=IF^fD#53Gxi9#hC^?UDfC9!FEyzot4lFs=JsJ1gR@fkqd796<_o$gmjRGp4Ee@;Y)W3%t5i@ z2lCCKuWoZ~snBW+@;Wr*BHQzF&=7NK9?YdZThp~p8~+&1l}CKp^8BuAZfUE#ZrrBr zlYx~k$Mo;s!(d^&Ze(>;_mN5ciPwx1{T2h=J9D@7OPnH@?8vs2G%g#W{gGz;kI9cr$8e@>}J+qz$0-g;UH)$F*FO z`{OFterevO#3Bps59&JK*&?d#?>x^23??g>Amq+SI1^03PqN_b#nyS(qKE06U^*Np z^_O5a8MCJ5U^u}lQBJ4s&!wHr{rg=2LN~W}_he_mcBA8$bmj8ljCSTZ; zsQR^$iIos+IHeBJ$buf|Wh#ZIc>^!hu_givSnNqM8_5D8p%1(OgC zLbAb?7e#QcJvGEx`w2gA_AFcAl^o2{R~F> ztDez=sr(pr2`N{;D15;veZC>7UUD?lpRo=6Q2Jr**PXA{eJV?LpDz8PUmG zLeK!wn|A7fR_fuc6rx%8o9A+Tb@+kaI{MCl^w~x&;et&?7hW44ZZsA_KSXCcZNUGH)dJUL5D9!PT~u-Ne|c= zK&YAyXLP@|yUBK4 zEl20X@J27FBV*4S0)oK|2xfTB+GQbH3SDL0oys+Rt>;vy@DC~JbWY)>c+GCwV9MwL za}qokfoLy?2r)d(vv_mIK742nRG=xmP($F#1`Bi+w4f?_q36?JmCQ7Q7H348(R=BG zu7T`E4(N&S8hIN0;u(Wy&ODqX~sXj?h^uUEezmTS1oK#M$L4QU(p?@q& zhSn5Nz!C)H%KuoG*Y*cHlYV=}30BhiBOscP7VH+fXOIPGElH_gSS*rcLAiDVz(@FE z%JqID?OEYB=Yq2!MG_ZxnP!*18GpoL6Kpyr{yKOl9OXwS1r>&$(uiX15ue$L#7r$S z!sr0Cpn>8A<%lMM6=+{84GQg$ka#HVOuBjm9=iNuEDg=_6Izk0mSyJ-KFThHd&0#y ze1gwGY+L+BiwwVo4=$fv|1A3kBhz>h6ahMzxuW*0zDVwrVPC);6Kv87u%Dn=eilqh zvxkQRVCaSIw?jF4Nq@53fIs%S53IVuK;~xtq}5H+kw$= z7Go(wKzlR?Ouh6fjz!6hqy4H=G)Uagh~~f*EhWHcs1J@BdeCX=i_xwG#mhXfL##ZQ z;Cuyb1Vi$)Ax}qT)TxC{SVDlyyhbC=9H`EZiTjb8KLa5FA_z{U<|^+%+-BCulECp(@VEO|3(+lA&-=RzIv*aG&LjP!vUTPgOWncqvfzJju4N}Se zkP=)4HPK%TgsY$(IKxs2#W2voMma_#3E)4$;5ZQxJ~Jvb`pcF+o~Kq&L2*S~P62CW zYXvwU$x)rb19hG#O7@D>wW z1hvqbWM1ol1;}f%VH%nZa>D^F9)xZviU<$z&~YdNKk20i3c+=uv!X(D3X0JMWQAP0 ztj~&Jpb0r46Kakll_*aW-67XBlo46!PPS^pHw;TJy~O7QlSmcfk%#mNc8CtR1M;ES zNs)mjvJDl#iOw@SjP~KcKYGs#I8f{aeol1agFzMrO(`Ga25)LR6;zyV-3;=bdWnGb$CfnXf*&wi9g^ASSF+qs=zOel;vfDCW7-L!5C5i zI{06wDH5v*ub|3Un3&H$1PfBaJ5UgPg(&Qf7C~Dy494P@xCcDUKr7k;hzj5((pYR9Uisl5(m-NR0}c>dBM_ou zsMCw~GV~PP6JOvOJjC+w)6!e8o}C0jJWwftwHaw@Y@-~A(HSHTLTC(1%#6GhXD|+t zEa8U6fQlrLG?okr^b`2UhUI0@d)XNJ3g^&!{z4axkL4%uV$pHE2e| z#IrEHM2AE?ZK+<@rlgSk3lb?Oh<31Y`U{F^aK){vR~}dz!#RPn9@#A{2N$6gInzhU zT`iFT5^!;^aFV5Ka6>_rVkRsSo`DB&5_csXgp?H^QK+h?%1zYqA8SFqY6VhbVpx@Mw zj-xB`+em^Zz!6$!f&z|a6l-xOQ6k2ojDzALY{+O>ns)Fw=(TF$Z6zS6V_@w+C*$v&V-^fkTBRs^?$sI`(W$9>_P==QI z4SFlS6S@OV5-mv0cU{Mxkg>*g0uOMV5};7kVq1#Zk*DAWn~}@JQh7pAskmm)8tD`3 zprt4UycU*8pU7nhkA>JgD@$QhC32Ae(mqTyLchbrv*HTgS$stj#!KQ|k*=^Bz8Ioa zhcpc!;9gK3LKHP6xdZ<4G+8Ce<-<~@MeG)J&`UIBfx-rjDj9Vk0q(;qD7RrEK|CsV zpfk!Zq5x(rhbjm&v3=n&@fA-h{*+GOqo|8%BP2#T9DNX_WIUh;u0Rj53EGuqM;NkMiwBSi6M>dATw!?PiO-**rdD`G!sP8 zKlns{k(c6S{YL6U0obR;((<3YQc(6mn;K!jM~zeg48@L$*oZNqpK%1OYRro!LW59J z83dv^`E%$Y>i}Q40aO%1$R}`>dl@&I`{@T!1^>7rl#%D+P9c;~nVOx^EyTee)u%ir zJi+`hO;nl;8+?U&utZ)2n-xdVW;B!f7@vTUJcX_j@nebf#POK$2oFL$plC{nN&f|y z(5Ng8;}j^6KS!~3j;c^p)`JICJT7^n`#i5a3BFEug1~4D7QoS~axg+;MqBYdk^n8DwY*zhC@(_s8q|%?3^P_hCXff$G@_$jjf)k20qbO zB7B@7_>0vl$^eOwj>bUp(sBt+kU?TZ#kw0=z*e-3oDiIljT1-10nIGn3B)D& zJUxv?z!&qGKFZ*Pso1nemu~I?8i>%5LkbQNXA(j1AJ6eD-h%8t0tA!LL5vT6V}Jq& z(Lw9@3a+BF2$*p^o)&3>TgGzH7kI5P1Z^{?gRHcXN3j~76dS`%`O%0M*})HNA1*Ua zcJvEf!upZAv_;@f^d!8X1nUNeG-gdfF*Q>+ilPXCa?Lt%8z}0GH#DA?H&I>qDxee! zX_d%=Iy4eyhJyQ13#<(0qUGGH7k(l`G853M=apBG=F@eE#R8zz4}`)!v;rGZG)hgH zpMVC+(u!%Z8px3r%g;fW#t?qeQLwZLBT1uQ&9mpB5A-=&03K6hEj1& zUKhmRDKg9$q+;^4QEtZV$Onr8eL+0+7uvPZLA{hzM32U=w4g{A{S#UU065(U{^POe zxvW~AR1q-^$j73YAJ(8QQmdWFvD zIb#9no{13<7uz6^OahG+W@(;4QJ@n7QYk!FAC=)DzXrYZU&A6{sC0nGG%nOkCb72s zzlpCh^oIVJu?LZvSptFrq2pwB7=z35$;`kF<-C;*BNEcsgEr_h_bZBkbRq{q9W;SL zKp~$%R*(RzmxnPUE#a#>2FG*b)zJ$YqINSPGjt@c}(kJ;CXa?@kD@B2fKS3t7DNGW^F{=d= z6eZxC>ARv((2W*_3G!mVu!(>%0%VT%3K?YYNM0Tg-qV(lQ~NQHwU7qnMynNd(|6q^ zSE3nX5Fbux2L+mF;w*Ft9}d5;LL|*S%8F|oq5KaK7c>wT$`c@MD8{O|#mE`N!SrOA zntxH=RQ?fq5yJd`cBW4C1fOk+#yi94=05<&>=snSdkXhfA9ziY1UVCG6EM0!7(Ht zedkGK+?4eoazjQM%gTbhU?x#DrBp%-WK0z`;sF@92#aN1DyJold#M*0DJv;ak_ABj z5hwbiOcHS(8YU;C*&}X1`_Nr^BIwnua7wr+U&a+IkN5^j$nSy%8quN(SU7c|+hi=@ zn&Le0n6`+COmwO_HhC(LQoaz*qdnlR=2V1dX4J-K1OR>bK4Z~%0JI7c)e8EdnFO#; zF(1D*W+umo?1*5g8|^^5WNF|Dm?v&Y*N6bHTE(egD7+B=@L$-9XyzB5L71OL_ux5v z1SQcAS*tXgC+M%T3+Q&3n1iT9(H(Y2IZ>0r8Tg6tv}!=nhv1f4(Izk(p21oSNtqb} zF*C*yp3)F962>aVz;ofl@J|}AVD`*>K)*5$jFRAqMzX{#)J^YDF8nomLDm|%lOGVC z&&BWt#tx?Z5%pj;?4t2LeEWqnof4?LzK~gA{}t z4@Q<6nJeZXF5n4`$c4B(hL%z%yhW#ZmhqRUG1e|GCB0QP7i>1%C%Q4?4q+}j1^wio z)P`^keIR#1IpYo_srf)gyilqrTv-L#omr(qDZQa}xQ_Nfm+_hEhqBDd1Vf>C&;827 zX}k)L@e7z4@|CnS+Lyf&|L`2KwiZq*hM|wjo-rGacQCmpgHei5kcw;;9?&SIX%o%p zEk3kCyNX^!jj~CKK)^Rjh}RX3A~mge!M2gIiJj@YB5&}APh3NM8cnKhVr|+&8>t5# zvf@;JS~D$-uAtbA=M;}(>$-*mk*>fu=3()&;-Ja1pkIuTKrZOPnZauuqEL=@K?*f4 z%Tv_EXa0j`{xOS3?aBlZA>fJ0Jkm$O3ONHPzyfFijH6zfrca0;T~aJ2M8Q`GnW=@b zD1FA4K)K>vGRi`FC{S(*OOq!HcO|9~aRJCoy(C9@(0iZ!ZAk1_D#-{c3CF^JBe9&TKz)q=&i7v z+WDr)$anf*puOz8(*aL}`J};Yl?ACy#(n)2xO@8t?-O_@RFA zMC+{7L&kiHrrJ-$`gY;#8^Ze+{G9`kc`F&L*=(Y1)&9E zMoM@TjS-2|u@VEwLU(v%=0+LcGG^i*Q88R4&rJu^uOwAc6aCYCj`~i-i4}p?#HdgK zGH8yNxC46>&Des*PKL*lH%0JMH~>?Kq@}xH4t$jUpdYe)gS{p|hf8J#4MY`AYpn|c zg#z_L$PT)wpR}%#r1J9AOpD|%4Qi;b@Qu6h_UMMRAI*R-u#dlr6*RLUo-v}=b?DLf zMmb?@2wO#)<(&B;|>$DEfoG~9WpW+f22%nLk z#vhtTkY1`^+=)hNg_u09q@~<7Xe#xD1ISh}0wEFofkg;QhL z9Xvn$B7-J-CW_EFMdNNffa)-9(-X?kG_E5!IHcGIA3z_80N^Fj9U4St1fJ0zH7fgW z_~qyYcB@%pWy3U6n~?sa4a$*;8u3x1K4!g>n5#N)Iy-X$(Y&eiGul9snt#&jC9N{z8J-kED(cp)6!VFwpT3G}^$1GP3=Mz92O@2$2)a?Y-p$Fa zoBA++uw4BD@j*m-O=QL&6LHcA>WL1K)iv!fk`x6d5+N3*9js%Uo|j~u_N1eV>QO-I zqd!DT$|<<(M;U8qCY4nqiVKOC_)FJ`G8iM#Tj&vXDe7QmhW@iQ4h>U;K!hh7qi^Cd zIED;_p0Zito4k>-B7Ra^&~`L0=e@<6)NMHl1&a)T)Pj&(R*iYLJW#wt9o zRS5KhzJSZpW#hs5#&@(D-4;%vtk3`t&@|;(Kxutfedr_KMKe8CL<(oDw&UrGiMbk8Fg`;)hmQ@m=*19cU&e&3DBC zgl&2r8&f_~ajNK-#L0Q9JnP$)MZj>pOQ;rvOq*}~_h?TGb z*=Q95*NI9DigG`A&UZM8-fQ-;_y<0hlp!^7Wea}5gHVvYM}?a(%%&%I6Gkl19&j>y zOQP(@F#Al{X%q6b=Z*WOI2LQKm+feWO2HNk?eDO=DcDF-(nxNPi#fW9Z9my`nK!QJ|2g_SXNbTT-6?8EXqZAmyEfF5!GYKZ8 zun3jPTS&@VNX!-xQfL}hYv`fGi3UrWY{z@$4GBnRDMiA7jo_T&34hTd^Sp*83<vBQr<#R-a?`!Ipr-Rj8GYeYS9ntnV8ZjZy_mfAt`Sm(elpn77{I1WkG}T zUrbFpvBq6JXpEhObFB;_q6EaqW; zv%H0*yoIE^g`~WNq`ZZsyoIE^g`~WNq`ZZMb@t^gB&-52Zz1{rU<*mXi7!5nJszk> zgsmNH1~G@|1UMth`CPbz!yG_mw~q8281ZN;hMwp-FzRuSrzv_Ocg9NvM|QB2WKjBy zI{x$?&Dp74?T2pmL+`@9D(1)%XT!@jp4eheGqEK1NmmW88{rB2A-TuM%DQxor=mw? zye{26ovcpjC|BovnIZk0lS1;&+UZeUGg?pa$K2!NOIMxV%7G)63%P?|Da>O>ZCy<| zKDyZ%+qE0iWKiot-UggmblB0yHyONm;If^2{qE?#ziZv**k_Jwy=40(Ehinnce}kC z#%pdrZ^7`}uRZUWBm0l?G}!mzT}C(Km`%4LeN>m+vrcU99NO&(Q9pE+QOG&;Dtl~{ zIk{7E%${ZBo`&^v9Tb&4#A#sZkwv@!!k^MRggKx)$-DakfkcL_r?FTv5oc>%pye1W>>x|tnWzo0YVuZbA*TzjyVx>6VZ?5`)_b&1ovZg4 zDQYsDgSo)U#FWGx}n=;74MS**2DUNt#U2Xq#O}FaNW2&okxO|jn>kU~7 zSdu(>@5a|3=>1JY`|FFY4I1q5aD(mYjUJ4qI_lyaz*maAMsKZV_E7sQOKnv9-(hJEz`mEI^*gid(d&6Et=|T%GsDPxck9+QUYkzo zh(&XZFnhK0w>=wuvf{Js=fu@*SLr}|B@pc#ulUf=nhd9Ck`ZrpXo_D#AU+&ti&`o^Rt)m`?;oV@ItCbKrQ zp49%?x4zr4_0*tH|R9@E^@{XY-v7auj~yG}DZ4D-CZ-Gs+Fe;izJ(PfRk_BQxuyM6~P zZ9HK`^Hn$AQ+vpar@TkcZ8K%NuHl2D$*He5?b|YOP~T%mo?Yo#(YtZyY0s`YI?$m1 zf$t^9TeoV|R?@;irYc(K)IvqORN zPVVsD=kMG*?fDrY&zrkEvd^ZshIbt|q+zP-=3n>Qvq^2!Ar0o%9T{oe@a`j1zdrS= z?gw2m`L*AkwELqIcdEVQi3@9D>t?(?SFIaYg@8p^DAKl1 z13g#vd2H^~KUXwqcK%KuOxdH^V@*4~^W>6N^S7IPPS=G!X5IGk`pHZ8>~dP`10xe( zJMifXTkgN|vgVPNmoA#~R_9qQyS=yL6X%c0)GV5I#IKSMkM7*6$@X0){OY|sZ=86! z=hss^oZ0fM_^6wIyZxAnyQdm#cjv|f_t}5u-#*-R(eQm+HyGc@w_@g>E^6NYi)Nko zzjS=dnJxY_uhHu1Z*+Wo>S1elI&<-MyLwvQw`xY?G0)t2fA_U}T{^YLtBc+k=Y6qq z$g~BUXWe!D1?l%ruGuAa(Y{^pd*aJ62fWt&lF9u#4cYJ1(_c$QZ$Ivw<$(deUEOr` zp?|-y?)39-?%&~7U**o-C$``F-To(R?@e}E+T#8RC!Dml!D}sFz2Lr6_q=__kH!rD z@TmjVPg!u%+%G;Dvg+W#{!3R~^5DRa|K9reE7$a#_s!|sw+**#fA9%Y``q=&i1z2d zb9Bq6PI#-=?DJo$eR=bUOPZ{`_Jqe4hVEMR@`1PRcleZ(9{&BEJsy6dZ{paO(;r12 z>GbUdtzSGe`Pk4~6RYli>(=fEcUbh@Kl^vx(CUQ2nPJ1aDzq^x9pW5BtqAk1Y4KxvxRHCzjlG%B}m4I=|gH`~Bz9Gw*sXy3g=; zDqm~y$xGgj+kNo%J!>1E(dLiSK7FF)+Q5df-}>ITVEk`=y}oHRbk=XDb-SX^63@~@ z_n5h)_u;*J_8R_n^-WiPF|p5~_bi^BifmYU%%Q_)eK6~or>=Zz<5Rc2*Y~r5HJzq( zeg1}*2Rzv3`Q6(ea@m0Huk?E5m{{-mJyzd%_J9E=_y^PsYIfYw$L-c`kG6kl_wjKZ zuB|(E_59@xj{nmseZTx$kAYvWh^;t&#cxgteA#T?l^r?{yy?W{4~|@SZ=Y8azi)io z)u+w=YQk52PdxFJ?yqzkH|y?R<7U=O`E0vYpD(%OFP;7!`Y`-O=Whl*FlWK61sA?? z^ulAtKY6nEu)WW{X!joNlZRYC`_wDeO ztQ8eEFI+Zv#E=o&k6Ig=eac76qPMNPbeiwj%P02PzqaW;=Z~BH%5A-0ym!TWBQmGV z9`VV)KU((Dzn3qodGY8KOJ2HVd57Cyc>K{Jdmh%J-IepEkDW30!Y_|n*?Hx#p>rSV z`maB3yyB~#SNS@e+o9oAiK{ksh>TtE;oV~=jlHMtr4Pevj$E+7D) zkGZhbh3P&2v}eU(V-JhXZajPJ?457j`-BB2+;i(Kw^a@K^KGx+cF(Pmp~L2N8U2UK z$>HO}gDa04I(X>Jk25Quz3Xpx?Vo%+`Hv^xxbOOvU$0#C$_uX?vHz35*(-MYm_NMx z&zIg^`ov8a4GSzd=iAfI+G}Iw#@}q**y1-W-fl6a#nKkl`;Fdr+{MRV9GSCs-vj$L zzv_xPZ$wviXg=ru=$og;ALu^+u-cKeU%$WR{kCIoJN@HR1GUGDUHRT=D^L3O5l=j{ zV$*{=K6UWJ$!mLF_~h_@gP;4ubCa&#e8wx|(&Ii}cc$-=n)6QXx^(hQZyfr@A3pu~ zjgyY7yk^ifqmMjk!4nJq@a8FR{`TA(&pmtLRp&mp>9$RQP3yl|*y7HeXY8jdt$d)yAIfE`Ce;w>kzp&bkXj6?t0&$ABCTs zu;|?tHQzk?`lBCQaLu&2Js&%2P>+t&?wxjNk8saxk9+&LQP=O^CwNWkt6$#xSJ%a! zA9ei!PYikDnd!0cw2o~u177&s@Vi>v^?73DyMH}3@Xl9{oilaksl)ad-|3OIU9Mg> z`{pa6p}WHCk^}k=IB~#B@2&gu2mKaYdh2C}9bI+wwSQ}M<41i~U$a}s^rlNc`Fs0; z7vA{B6`9!Y#=kmc^_l;^=cWU$xwgZk!{6=SwoRzb*nVpUR|W6CV&yeY^j>-Ci%&Lx zZ}eLay|Z!MOY6?s{*Ckek9=PD=@)l@vDZFt>@zBG=HvyB*2M2<-G5MAr7eDt@EHpPyP{XN$6@!!02*t*LvKR@vK@Y9a2JmjcZx1V_RJug1D=FdHP zoxWt3SAr)Fy#3IQsbRB+J~e)3r)NKZ>K{us1`j{=t835x^6XQ;TNMk}p76}LKkhPM z?aDu#-M+eNWyg2NUHHKfSAW`R{Kkfme z*X%tO44ZZD-^Ts@k>y7nbIo_VeRIr7GmoBm?}uOc???~5?A@o2diwAaqPM;L&v(x9 zE!>cpc24hcPrWqYkEJrrtW@)e-ld`sEXM{N>RlS02CJ z@f#zntABsj>UT5Wy+3N`xA(264!!*QyT5!o{QP}~uk3bzWYEw-mwoE_bn(h{w;p=e zjhiCt-#q2?H6I`L%GaYWJ7e%mpS=9~@KattWz_l)KRxmI3toKcKWBc`_w}>J4qLY1 z#xwu*&Bq&0+;~ij35`B$R58BarcEzCGIi4E3trp#g#*H!9$ekz^IoSmTJF8++B=SW z`Q;ZbeQm|K;I|`&-ZS{~naOuA{`eGM)!#eZKKASvn!Y;ihTrbcZ_ha&RsZGQudZGa z{qo+?Jyve|_n-T{84axN^W-a2@A=@lDRT~g{gXG({p`W@U(arS^bGn6KaMb-}2fi}tv`-LZp@xIKJ&-w!*Ux^(@kfB#RHm!5n5t=IlpSNqP= zXNNYu|C86QKJ#DyX|eg)3;*@X>2JRF^uHow#y{U^`J{I)IA!Oj4_Mvlmhb-k&2yVh zy8Wal57_aD1>d&%dP!3aW>`QOwmdHR#V zvj#VM=&14k*!uU)}V|rrM2{{jK7;9W&!^IQzB@nN7ESeb!^^cbpWPG--dsoulKoj;!dys_P~QXwLS2=y>@>6&8aQUIKRP{YmeG@ zQKO;TMH~F1@vUo*-frr_XFPa7=d0%QXnW}{N6r4^y_*|MZa<~xpZiUk^yy9w<^~(B zJ+?zMdgr(?Ylr@->;B;Q_M5k>x%Sin^^d0W z^D8#z@k?g$Vw&SrGR&r&Vs`#$n-d^glf{eLd{Ewiy*yGYkJQQ|wem=bQY(+t$|JS%NbP@Xq-JKG?EK|_Wu)e=3CfMs{;P`v%s{c8Y1o*vIH+J6){G&G zc~hQ-{U4i#joB4LMQbr)&dQBKiIykU{|~IiFt%7^cjatKIh#_>rj)ZOb^x~FT|OV4rl**CHz&3AX3YxadTfq?r)j|$$c!+B%&oftpQ z+fKTu%IqrNv5C5IW7ziCXJpkVZG`O7zfXr=@!Aod3f{Nx*E#7Kua{Fv_nU?J?Dc!Y5k1kvqxTn> ze!H8{E3&VADeX6>sq?-}zdxP`#QpJ1D47WaGO0|)A4;WD(O9au-}x(+dqAn!YbAWt z8Ff8*OAz}?DYRnF?uqBUKyH7tFC*aid8er0RTTk_J$H^dALqI2I6;o$^U_ip#CN|act!MxjgZ2*OWn(q*s@gH}nsilC?Y!7}C zFN{Umnk+9IJuTveV+94ga5`GV3uBrtFMQUh>1{pAYmE2Dd+QUUER8>{w^r5ULfzhG z-|CjHyxAAK<&@XH?yL|$eO4=Z4V2FU7N1-l$d|3psvWO-GViXc-&75L{XD(3%9P(* z*8~epHTzz)yxzL1U({Qxcj~Rh;QT%s%>8kR`ehD3OCMS5m8;20)V0yVK3b*D>!YjE zg?%*M!s!BCbZwMRF2M82VNC(P@*X3Hj31V*-)c)Pgxf|KHm=Na(DvK4$AW%m-+Pwp zcfIdD3-Agj+XedQaI3J7R-JyNk5)&1avuY7vqgQhFJmk0qgCp>KDu~bDBFP5yC3d? zi3)7PKM8OG`OxKx$-d;QunSga^Sa<_Y*80-n&$Hv?I--;ldGut#%MwrN0WaDa7>`{ z^Yk{SLV6D=)Ojj~ocMc>Cmd_Wnr9RVl9W{SfG7`2ofLaVPMlf zN~o&Op`b5U9`d&j_?_~Cj$0K(%-P23`;`BX3F<5x2In3ua&KmUV{ao+>RCOgnjoh; z!Ce09*RpR_`N`8TskWJ9O=f`>VZjhy``j>ZC$QNah;kpjzt`PoA|MKs`BZQ1s zPzY?Q4}q}ymwy)!7cwxHy9<2@We2p*gTXLG79iZB7)2KEogxD6P&Vjxi$WCD>&dh4 zD9*kEackZ<%mMav!}2p0EEP*|O9(NQ~WKG#O7jLm7vu`TiM#3$8s=@Cg z{Cla*H-;_JyO6buZKPJ(@?&hyQTzQCV@8~|qAjC5hoSDPv$je>vM-4(=5zhjR`kdz zPNA)5UlY5<8j|;p*lnV`z-8wlQ;x!lJbnIto)^4Gc8hp!M*5P{wrPvW4_F~@t!|sQ z1lxzVj9QDr+eocduwo6#=X36L>xEj8eTC~5Yen|ut=mRz!7E+AkGDB$^EIT1@_hB$ zB0lH6e{@^)x!~QT-$(iPStX+$VoN98RRrAy?=0Q2j#xC?MiJ!c+qOlQ=0&oueQjGP zR`8}!XJlaCB=6>;?F(LQE;{?B)od<0>JCku9DMeLraxaU+D+CHh<$$!KJ4chvvf2T z3&uj3cq|nNhN96}z!yvS)2VodckuEK;am`#efOs9t^R)7z{btN+gEJn<{sSCF>meF z+(S0b`uX~ud&9{0_nXvjI2sM7!iiKk9gO?ZflwxqN(N)WfIk%EZP-QqE)3qWgwo(W z?DzdZ@SgWl(Yz5&(C5vLT=GXWL4UA)w4M=-(HTc`b8&iopYmgOqgocy2Y>H~#;A+E ztuS`CuV^V^N}e6djOrWaMSgHZW0b?`4nspbpz#G5+67C8dA?w{z27Y_=%4i1YL_EG`u#n>fA|a&* z3_Ghl&9LC?g|J4d*$env`$SYJ1A9R;M%gPd_0L`j1w+{@QI7q|yF%P&pNT2~kb8!3 zD0_u-h4Zh(a#z9{rDof~>t>&cDu0^0l2=Xf?@&OQ_KDGbeB$+Z&;D94k%f_d3? zVj;~(?ZpV*@?VM(Oi9DuU|7l_9;JyDtLV1sKfBEH=?w-y~tmG zDC~`C!lS0l(lVJ)(X$cdD2mJE z3X963%D@$uDVQrNi`7FjzN+ZHND#;_DGQhIMWp;rDYLKp%>7?c}_~}7nf;R zQcxC+1eGE#E>rYXRL1moX<5DsbH7fO4423w8lklK*{IJ44i>f%HS_RAWg$6{qOx#3 z&xYl)itdZ#n=;p+FB%bG72U`A@ug)79*XXZfgmMirS!$T%3c&c8#B`^#bp|76_qhv zUs4uS{-x+XGu>TOW>PmrWoS&vvoSf(g8O1Hr?|hA$rl&hN5D~9mJe9m?+?Yi!~`Yx z1$C51QGL-8yNr2bB{Wl}tXJW)%te;a>r)^_c}BMP)&)ZYU}<3onYxiZK><6xQTc z(S2bp>?kTDOe-zZ@T%y(Xt55_Ml3(b;C|#0iIwst>eVDvVSQ0=DW9W0oncdS9}9F! z%Zf24?8P0G&>ZDdrjloIG^J(5_(y#qrBRA#L19YEihU!`Mv8GmWm;WRR9}=Buegjz zr?jltW(Y03%3~DP7c&!@#bu@ajWOR)@~pquC(}lt7>g+jmdX+rP(m{xQPSU7NU575 z`og7l84H*4Di$fVBUG(KUt-bxu#>jX$yl_cZ!F^|kuM86@?$^qEO4r29tAX0R$}Lb zbtU#jS+S1~vy4LXy+!qfG@vdjV|iLhS)>>zsV`C-mrxch#%Glk$4HdLiesO!*0L1a zhgL(Bmida~HJ>+cPxI^?=6lUSsUUnm8IXTfGcd5gB443!yhj8`_vcK z!la^SBbt^eE-Q`?c(xSBeVjvBY)6!p(!vt865iwXOLT)3mU%vH`te#w9e@^pMwOVS&a!!}*yc5uZ+4T)3tys$eS@wjsieuBMk#TdDrWrn) z9%qwhJ^{8Vau$^E99%aAgW`!=4>^p2{B1o6y&oeEWk%qpN|!iFY^zz~D6qDR7;&hFtm}^~U~)yLu|0I0Mv$#F==)6uHDj+!aaRLmYGe+d!RJmZTZztQ0x) z(6TctMFMkI{FavNWOkNOx&A3w5aIb*vOvMC;1&v&wb*X6IqF!M?UrS!i`i986)trq zTVq&PN*(cfDRseuHC0prS`pl4GYCA(^r~^0L&p{0wG~;j5p#f+^-L>3Fx z+$=-0#AH~B1ZQrUOR$Np%!4_C2UMH=1>Qm%b}`?{bQ2etdOUu2 zy2s;p(UcGNl3gS3tW{^VnzQ1Y4<;KbW%O_&t%Da_%zkj}!xOLzO#`mbDRGd94{jqS z7pjUZPxxRTSL(`Ka$(LBaIuMXW%iO?>~A;7c${ThhBEt>I=iXDK1N`-o7lnJPECNzU)dA^{_nb-@uOS*HXU` zvo@C9ToZIPD{m=$(Aro&?Xze8EBB93&Go^4r}(s>sBGlyv2KtL_GS~W(`x<{qrommf=uY~fBkFrO~h!m$#E_Y zu$sBUt=L}UcpO`ktC!i8g?T#^g09#Kl&~Jhf-ABS;Sa8Pc087C_@{M2WH>zYvw6;u zNgh)jU3PBJR4GbQI;l<&{{55c4hA_jM03l58I`vO6+C2ZS|iiJ-#4l5K$hw*nLjq^d`zGqGDb`?75f0kj$3mqy3H87Lo<2L_F<-k zaE7d|^tVU1$%n8QJQB+*4`<8sdtsnrYrAe>#gb_Db9OnVfA=27U&QN1R#$Z&nbedxQO2zw_no1KDq=Nj%G^8)Y5t@8tG{d(@D8^KY zW)N-ZlEx%#C=HM=2>f{2=onl+#Tl{8FG&o2hi8!>)R6dRK80Qcxi1N2kk6c9l>V?|)$~^cL?ZaX5Lf6! zQoQ6lZV`Tl)e@UNbUn`XIKAO7lYd^f9+F5<1S9yX{jluSW!+2?vejolDK-8gjda*2 zN}T?R@@zeXK*2PGAzyl!L?YZ2;kvm$+D}Se5&5WWK&?kCyA1%V=0H8p7^n)Vw1AT~7PgpW_+JP?mMQhR<^ho*vpZ$DC zj=CRylGlmEL?`7@-Dma(`;kc)dZ8OWvVqyuFWwqGbDk^VM^N?hymSn@;D1E?=Xq8o znEJ(MsSBNhb}|E?LYR1=tFVW z4*jHtjeMou`av^TD`xD1byK2O@}ymrs~?7s$Vc>I8rgoK(}o|=A$<#F?a!5y^eznD z8PRF1N>-@);j@gw*h$*xsgXa#q>v9J8My>xDd8kE6!X*RFN1veDfu~a6|!uZ{zr@+ z2B9kg9Z76ddTja0B#-)qor{l--bK&^mXHat_!o=B{TccZ$1>uw+enBGku}t{aNOXH zFsI-Lq~H!&JkXipH)>Cvq_aUQJuA!CH(gOVEB0jX)SqENP7J%`v(a@w*J6<%7#R zAI!iT00NzWSHYAcAM`>v5#Smc1-&dtg0>Jijn$a`v3CUyVcMb#YZg+{rlaSgUG1Sy zswWI*64E2#7tc^H^kA3NnSpjqkQlo}Vlh*nq$hgPOew{}De8CZ3+7P*8@UUfz#D3Z zJFp}O)}Vk%>_K=*O4aQTc7}e_7M#FO(QcUM@KQV!4~j$|SS8s=E^&R2NES|?$(@_> zq$zhKr{5WU$B&@a(l@wb^a(UDxZvWU@EA*kdctX1Nb{uHD*SQE3(NG)lrveDp-++` zlWyi|Q;tmG65c5R?=npDqA|#y7c&6)X%0{stv6#pNs4w9LCI!>W??j0_#_Tu zK~PGBBZ(qU!ayey3TI>y{tPWj2l)qWOn(|2k}UwuaGqEOzR@2=JzT{zhIu0g)QS%7 zAr4~B3SUnX@H>LV!UcZ@CqN6>QJi-l;FZB(Sqbco&{5(IF*_+Q6-@dQ!dh%pK!#4! zrlKp^v8*dd-$P-rFc5`%!d}S%{=su1D?Uj-60`-fLpjfe<3IwD2c^O(>>cXTJjq-= zJPAnp)SN)O=_^s2I3S#ehcf}%$FGX&H1VLq7@7g)d_M)l$td1EAtm%`e}NJ;MuhOp=ygp3n?`NdJSPc0GpE!vPT zd?T`fK1vjeNkhS0v`6Tv_KkHQ1D+*&5l+FIxZ#{ED~04D*c#Tw)9gt_67XAygSAQ& z;xx>I?Fsdr-k{Oa27?lyJJK~aBd@RM5G?^gkP71}G$KK5ag>XGih48|E%Yb26JG+} z$ts{6UL~MT_mBoK6d*atUkD>Rz=Be64)3J;_z!i5K4J`}TZve(1?WKY@iVk0uZ%V# zA7d5LOe6?5s2fa%ak76n!Vj7#97F=LaAXUrMu=Rv9j(EW2p!=N%7rc_uxM^0zlwEH zpR|Rsi+Uu=u?%66;up{hJ`m?2KNQ+TOVU7?#Agbi1oy@LvKHwJ-4owH0IU(-BU4p8*imP2Q%CQqonVVgd1!VmV~DR@Q*<};BU-N3q& z&?zjy0FhZ3odVlMBbpw;M`0!0frTIf(L`-14tP#~6#=8o430ANN7##3rXR3V{y_tA zeyB;MM1qWMFltbb+Tgjyd2l(6J){h-$Wvl>2o)4(U7mD7eoRy`qQ#2IEMV)5VT2z1 z4&XQFo8T{q?*bWU9XpdedE*Z(girW!v_sP8PF8b57m`376zL)}h-8!uzwoV)EHuTs zp&~&|NQT}M6R-*aj1ww|P2454Lw!IezJh(&t=u@zY5cE|a0-d~z@H>E^8P6q0R5xK zmpUG7I07$FcF;Q1A?U}3ITL%ZNKfJ5#03Nl@wB(@Jn zgidIptkbwnP+vXgxBQO0B*NmOxTBbYc_(zH6l!^`qdn!s<+S=tN3 zww%x~0HnJX^ogly&<23U-e@D zSUYv3f*>Vm?L<1*G@qymeT?XSw1ROimIH5OGvJ772KneWZ*4YzJ;quJ{md0r%2ES0dvC987bCxL20U6O5vSO>%T$KgK6K zLF1q+F03P-W`q?nF%8&&EkOj2ijSrTLTw@+>Vz2h8jzpk8D%oSW>F7cq>pi|6M33Z znPL`*L4h0S4boF2i0R<`mHSjC zPlzmgV*w>r6c?I76BaLTi>{#)$X(ewR9jIA+9>=6H=##aO*n;h$&+Zz$2vILhBLGT zI!Hc9CZ>@BIKf}|#h8T_kgqJ7CuFb4f+iG02BkqpW{L?xn{WaLobxs0)=Jc)QfFt1jraca)Ik% zB4i{+=lMam<&_{V2@DhK8Ec_`TvR`z=AS}EG78`tR3K~FxwM%4Hns&_^iEflcTr%1 z&fy6_5UgI-0Ol!TpjK)&sG$)j{8dj;UHlsK5WjOx7@={4^4rvhBo);08`Q#&Q=9UP zTG0l^D_WAsuri^j7mq0&7LFkYYEeu`R}|@CqiH-TqY7wcDK(l;<_vzwBLX{uE2zfj z&>Q&*IU+`Y*dP)yaTb4(74flfhC7Mjc#JVG_=*pvEk80MW=1j^7mFsifvyn+!00em z1z~s*`p8#}4)KCumdVqI$3kMs0M*v`g)x})gBmsOARp+XX2umx_8C73u1m8t`sJ_k z`E&#X!@6mYenJO01-HRvI0RC`N=6zW2$3_m0agefut(S_>%bP!IoN?2V_%9~r2Etl z%}F>QBt-&%fUJzstSV6ihGioro_dZyLBKe)3QH7jfH}0P zFd)r8uv>Tm1q#L(8Gr$JIumzm!M5y&Xhg@tGGNWx6fAr$p< zRXWQU3;zN#!!aUOU8M!FNf8wDHs~EXfD}xuhR&cp_!c|@PjI97p~wvVQRZ6uMU)G_ z6mz33*gA0qR5F(2htU$F5a`2mgJmXO#P?8XY&l;u>nMISP4EAWk-IzT?mIE>gIRZ9_56 zgdXr?v>4AKPbzIjCm{nnkxePGNMadaD8FSy%4-;n;=9pEOdD(!77EpnKAwiU83AC+ z@~ZNM@?IJ_YQ&92!#Vj)S-gBY`44(8Iiq>-*l3}=6I7abm%eI#NSR;U5S|0M6Pbav zG|s3;_Kh19?}!P&3IGc`G-yRH$wO)Ef%f8)amwML zG{PB1Xtatt;P9lmY67>-qQ;ev7<5^71j3r?g=oeQQIZN~KC7N+U zU$7J8jwXO5XeSvoSv3C(IpIIlm_FiEm0u$-WPE=}7)syxt4s^_qqc~eWO0(6Mq1RM zNE)IsV#Nc>;>ZW+e=jOQyvTQr$3W9CqZ@eYg*Gq&Zt{%Azi?VepvYhP0tHA6UBVtA z2XGE6K7f-N(Zepe0sfOdAulr`BsU<2<0lL}N)Iqw`C6ny{pbu=l}{GFAtLlenMv-3 z+hCBq3%bZ9T1~CDn0yf4MWZ>z;VCS{7#;|egvW}$$TNdUpdCmj4ikrS4GYD7lt)q~ z4-1BGCNe>n6w@f5A=9A`NXpE3(gSe``Waj(TY>!Pf#!sg;sumoH}aF{fMyUh#|3Ue zH(3d=j7$*RBL_$S<*8H|V{?OYJPZev4N=xgo587+| z37SA81WUmi{0k8%C<`AXeePp^oH{jx^2xH~FHkpVr~ew!YmA95VVRNydWbg5dJ(c} zl9Y*!Wo`HsV{Z}>ejUD}RnlklM-iH097P|DInZ`-UNH!mEo)G8L=h+lt+Wm&iR~DR z$lGWpNuzVcU6O7oo!sfg+7*8)TZD{}f$|P` z9Vle<3=+{(G#PdZy+Df;b%=X#1Su04$}buj5ye3ax@Aldi=kC=r}SClE8+=ol+Pd# z#fp?=JxB?ga*zngXl?+h2o}&k@>5(#0wGWisW9e3S5;<^1B}pEn9K~H&;o1(-33EU zyi0^n^d=xugcYJaER2x>^n(lV$eG!k#ZCAG`UUqSTe3a|Pvmj6B_NF+6V1U9 z$Gs3jiL zxB_&QF7q3iAzc#{(KiSYdrVz$0No}2B2Hw)gl|(ciPpe>te+e4Utp}_ZbgQgcL8M; z7a$jWCVbauUsiza!CQGy$^~#xr~#3@6r&ZW#UA0FG+PJ)#?Y4V71TpM@}ELx_0q}q zYP_b{fw%#MGV@+09z)vj0nNbV|5iV!5uXAPnzvv?iw!U;Cw7rzqR_;C z$j(TM7)KE{H=|vWmdTGO5+nYEpOk|)nspRL@dZmJgNzQ!LlR%pQ`!VW3~JG?q8G+* zpfnVM&L&twbV37V?P!@;NFzRSweT2Sr6y%J$Yf$~j2rPFa(k@jfM57(N@Y_T{eins z$&X?)jloPjDlg0h6avmeEg1!LO&k=iqQBB_+Sdp`GiLa5gL1Ts*2v{zI%-8eLU|pU z6Fw0~@+?hr$>hYKQU)dNkY%8QjM>B))gUh`{nO#v`fDig1%%O)Min7{H79HMGGqISkm|Ecw64YoE z48?Yd?KQ#z%}_WRP({)rkOp6(d!R-^8GM1)T#*k&NANhr&FGJGLsmn!jjLpQBtJj{ zza<3aDdjqe2*GlBK1h)VKpK)RoR)&nZju%aZgB&i5p2cJLK!_r2FOvomwt(d;u740 zH@pD`9n|a@lE?a~1x*xY!w}8XDG~*(c^0}UGchwh3jiStFJy%wP%9&oHkq{{*aA<% ze)(b$4@(qcI!Mi23eO04HRDOHNpcZSh4J)FV@l8;KSj<3j3?_U-9smU4HNa^8|fW1 zP$xN6>`AlqNEFYaOf96LtIBDiH++>h*4is&hR8j_OVAqaaPqXun9vjH7`~FI5Dk$H zD?)? z<+uf6bOS~US-~s$dRPD_$d_76+-rm_=F#Zauw@~Uz(Z5 zPar|9T|uhow8=f94d}Xb818|qaDoq56oDN!!9F;ue7Ul4%3UHtv=^;%?l=U67gN$Me}vi zcJ8B4ZIa!9gPQr$b6^GEk)<$BErW^rPJOs(Yz=FdMbL`88sE+R@-*m`z8e&BJsoo? zRH7UXZ6PY^N4xkg$`xgq*>UyRXHxNP+QRfR?B&pw zs;%D3VfVghf69;V<#6^$W$!HSI1U<-HuZU*=1g7hzsns{t7u;*d2TLw1&n%mFcKWT}U;*Ld;rUXJo! zj`Cg(%>4s&JALcxOp>f)d2qk9)eKnHaUw7TsUaR?TR zKnjau6gkHL*}FK`ly?pk+{Lk%Y%(n@qAq)eW#RT3bJ&uz;iZD}M>suZQ2LBI{+K>? zwI90K54{V{^x+Ip+Oa~(muSG_o-2lQ{T@qm59qM$>A}t(Hgq|377-=tZLM1*YQyxO z(2d;gp=~@x#~k=%N$!)b8eTWT!?8;Aeq?1`y2ew{qcUEX?w(For*vwmb10BK1}pC{ zt{&AjqxBSj%soE7baf6Y)luX|Kl0BwwTJnMfwgrt>GgPJ*Y~a{L-MBGn&SelZkkgV{HSx@|7LZoF!JIg)-4!R-{>AC=jV@^uM@B1)8>hTOm?=7Lj{~T^7l@ zR@bD^bcAkqxMdGX{7I+Q{g6o6`@Mb@2JmyWN{FoYck*CGh zBV^C-;)Jw9L^B3uxzfL;Iyo@S>B7AG#5F!^v+N~t@w%`Hqw)56ojqkvku5o*${2!f z8_F3|+kWajJ+v<|t}vG6v0`Xyb0Q3gkiwvK`MJ9=FP)*HVCtyk8*+Fyu))~xdx7|Jv3 zUzCCQM&|a{)v9+$@kOun$l)V|P<-xPMW`{RT9JmQu4b$P zjc(PI)indh#FL!*&W0V=dBEUniHjnX($tLWXWTi1AP zI>mXWb|8{}#HXWAwk@mCoUZu;0q>8fbe{fli0)}4lj)u{@p1L*`ZjZX?(QoVE=)~- z{Gz{Z-n@C(qTTm8r_t`?&ug<_-`3wQY|`zumaWd7zRyW>*8BQzxU9tk-bKL~pY5j=;X5BiCOkTR>GMuL~jJ1c|>|J!uE_XCqJn*|6QXN-aFm1<4bH3^{d1Cy4sb5WM z8t@L9wBee013Ry%ne1(}=%5*kKVIMYoq4VAj&9s_#`aCRAKX0Po%+V4Ce>Z`$eg_F zn4c(#?6Rpx(HWpaVqR{8pZgX~?EaeYoZ)DVu3Z1z#&?&lU;5C(Snd0B0>clD1RC7@#F#^7RSx>qgnduFomat3I3gCCa_qQ< zi#xX*v0b|fwd4HL#`o#dYC?L$v1>=BsYJr}mH;PkE1?+h)pkUBd@QlT%-B+P7umpuWeBJiF4f zqIcuY)1F;*bf7{11K&w}u+BeszrPGv{8eH>ubt*MzI*3>i&u5~uGzTCzR&hqe9H+l zZd-ot-G5&9>)QDv4-U_N?V?@xzoPlRP50YzT54A1C;J}t?&hx-KQ(*N*=?q`IeYn( z<#!I*@M5daW`_djo!sHQ&)>Os+Ve9)o;P=SWS>oM4evT`NW)au&A;xqXOr5dLmJGj zJ2KL`;oV23etqg!-4D8C@@v06Y4=Ab?o@lp6BpLR*3Ec($fXrm?Xz*wkyljAX#8ZC z*cVgYuf1W$`E}D*o!Iod8BMPoI=SJqcP@RuVf^gfE?pFT{vYG6du-Z_<2{{bq#xha zx68T1_H5Fq>HMxe+g@@=;`~t;Ppli+#MkrYNtdr$>}m6D;_9xuUa;%}-^nxQKf1xY zc+CG}?@ZuqF8BCPxRIi|p;DSk%5ulq7b#12LUyTi&Y5$_HVl$Bgpd{_Zbfk`ZPqNM zg`{jHOUM>w$&y4RBKf~R&+q(Z&Proezg?scOddANA<@6W1P zZtQ@vqf0z8x6r7`X|H};W1;Cc-M7};;_7|H_EmU! zWS@QyzSO+hRCl+Mbq1whxU+ei!me12l_g#u*yh$l1-6#mJZxUC($mj8(6iIt<(C~D zy5QDH-|uO+`$})QmAfB%yXDs}U(@>O{q^S_?^n1|pi=skZH70QwWxFYgF70PUEb#N z2IC(5wD-nSogXWDXmXqP7y4)I-gx=cORpVz>$^A4toQDcrqLTW#t(!R)%a=HH5)3# z-tRCqy8E@yr`EZ$+Tx$zXkPnR`8I769XoB0ojNkC)TqkKRt-IIUg3|&el+?a&ye}u z1{|APdS*Df?TuO8>fdwbu~JtpI60(5fp6M9lv(Y)H$#;wS3hfDuv5>TGvlpF_d0vv z4KEZ}-Q%$pQ`dfew0D&Pdn)(Ox?|g@>n7Yay1+Rvz1Q)fbXVaihZcTNB6LRE&x`z2 z(^d4$HEHF_58mfmRr`%*FWesZ=b5DkU(x)GHiM5$I=@?yYv1iLv*aa(ADL8S!Wnzs zy{c!at%Z+%aofJX&8T_Vu>yZPxA5e&vxkh>dqutB;poWF6$gixJpbW3SIlbQ8S_v^ zTC~oiAKfr6?WzVBwHVQ@ewWePM?bf$;;u8+j@Vka+?CfY>#(c%LzDZo`n$j8KUN=p z>Dv8|)thv9ug_O#;?UBQ8n1bxbCH`LIas&s>Xv)2`o6;Ep$lrCa1X2)tMbu>vlnlj zRkO;)jTfzTSDaU%@{$#^x=bzC^})*bUHa8~ch6cCx~S8RZd*$n{M1#w;GQpLA1ZQJ z#oI=Fv!v`H@3Fo=xwj1)aJ9R^@$wzUTs`83r<$xtTUnvh=oJmGZCJlSr!O*JeER!A zO)AV@HZCK0Y)j(`oyP1L(|GyQ%TFwywyWv4EwgG2t-a=jjV<1;xaNZNtH!sev#G(R z#^HwZ>+PFzZ;KXfJT0Ao6fIXs?rmczrLy3X|>qojYJ@S0LmOriw zuWP;T;x67JXU%=OTFsU(wqN`99f#*M*&MyO$h2qf9QVV(ADXsrzp2iq8~TrVtwI0M zSwp`qxcj>mk3CW26aU`8_cf2VdUL{pF$+d+YqYTOfTg#)s$6*g!xz*`k6rcLxL!{k z9)7st(3d?;hd%dS+x9QDcwz0NtQ&XFt<-w`E$ch>eY4A;F7x+azP|D+?{sP9FFv=X zfBNP#=2n{fT$k6^A7B6E{u2Aw><{mMVgEhrs=l;v^`y@2Iv4JG$Um;jfz_dDhsTa^ z-}vO9Cgpk;oBd$_ahs+!+%RX|uFi=r<2oPw9;v&f=9Z2fCcRVpt=mpK^+WxM?rQf}J7Z#W;>l{kz6BVKj(|g{s^Nmk;u3Yls!yUZ~?)$0VJ!MXGJ8|)e6D2M#@kNQDC03TmytMlz z{U2@pXmG-XO)qa+eBx6RwuQD-D?Z`%(Dq)DH|xx=()*6yKYqFY%SwHx_4~S)w|C>d zTXx;K<6^pIPv~fC#RkCo;-SdVTqX~ zNB!-RV#hArdr_#yOn=ehXPr~2#QvfiiZ(4e{p|ZMh@L;@ycT8Fmf3%Pwcs58!xxl3 zZ(fB1fu#c%?_8I4{JqcK+cWIh5tHh_U$<4g>Lcci7+Wt;fAURV+|>2Ca!q{CUh~Yx z3(xsicum*mE?d%W$wv=`10$+eOte`4e5Y9@W_=gkvh$z4ygPn)|GwcRhj%PBpvIy~ zwVqi$?xm+f{#k*;u@=o+v~Th0uEUS-X|`bO)bUjs^=LHt`SMc^G}-s;`PJhm#~yq+ zz2(R$+n!2<|1n_m(0zA*GW*5Lo}FB6@U=UeSE}f**tgmKwmp2WKegrAB@MTX-LSOy zuI`_|v*X0!PY>Tyc-sS>Mc?)L=KI&aFLTkhi@JL69ci2aHH_c;rbt3yratDC!c)K`(39y8+E&??wINApP9X3)&9rpHR!kE z+)cjrEvHwgp3!k!hvfrC*ZBCm<*%+d;k&li50mdba&NDncZUPL+kDjjwsTt?+H%Xi z>6tyYRNvWuIVZZ*j#2@sRbR! z%z3{5%Zt|5ZT#%d=O1r;>*z+K=j{E#Gb7$%{LT;Set2!0(6o(j?6}9h@K|)jeGU6B z|Fp$z`#$J+>HX7=_3aryzT(l=(_fu8wZe?jGZxHU(r@KK&ym+2cl5uyG%hi)(6@!E z4rq4r$y*PQsO&d3^AG>v3f8S4?JIrqT-RRiP zN5Agk?(uT9>3#2AUu^S;7q0$Wv(gg|WIi$Hhi6uVj?C#^Z_CL~9&ffi89bc z_pBN^;o8p*ZomKAw~zifuJ|)$9=)y4!i{$=Khk1xeEf&?fA0C?&IZG})?Zxe^~yK4 zy>5D-U(>zSd#yaW`Q@)_eY)zi&$qtOr}vJPA9pDB`oXQw-2K*9B~E=j@~ut%wr~CL ztzgdqYYMF$ykl6Gk{@2Suf{7se{y`)$y=x2y7aO$uUqg_`Gd<(zWDZwGkzX(^2pqK z_Ia1g{&r8jRb4xV7Iel%nSVTM#fJymj%i!yow@^FJ@wAMx$D1LeCpWA-t#lFIvy>% z@99Oazwk|;?O)&h%})!a)tGW>&9Dh23tcv{#=iSEpWJk^_lfb(S6y{xV!#XcPCJ%3 zIqk=L-amTg;PBwV=T9H|YN+__hek|!vCyGA%M^GxqrgAge(_^u#p=(ROdfRhXIEc- zWsORg|6HczXWNIDxa+|JM-J7!WO1Pm1w#d1Ei&~`qk_Y)yzA}DYEGO`uhQ6a>yA6P z>!kui(udZ6yxHKv-<(}wlCRLA8>@vvGyC^E)Zv`!Uu`cksqco>6GyGCaB$L=3B}%A zvZ7<1w+E!3Dws97SBre3X%hnHiCq5FlvBVSLA6s(j5AtU`9GWh`8`=Y6U67~uM?>` zk(v{!Igy$ZsX39F6RA0oniHuxk(v{!Ig#35EK*B-t<)ZB3^IuHXT5I`$oq0Y;D7pZ zfC&^8sdeCW-jtF1fHm}=YvkU;Eu6^3iCmn> z#fe;;$i<0VoXEwAT%5?oiCmn>#fe=0Vv&nIXe?tf>+lUgd$eyKNm`I9Dh^ysRIkU0z5xFhj6d$GHM9`+lI`r=-% z+mn%C2^6-oWzWI93)%$LD|?JL|9;y8)^@+M$E)qNUF@~Fs@69BPR?T+|kk&rLy4~IfwcQhV#1zqk?BE}|I;S45?=34b7n7T++zWom8 z;dl5K{0`^hH>9=Rxc=|;JNz5?9Za}mzIZ$uPIw~;f7~5*`vajETNnodVOKcr&)e_R z_Z{lNA;c~ z*?=fQ8B2DYyB%cZL`<_RWO zUgK2uO?LIH`zD5^d0fdlJg#K*9+!P7kfcqT$8BG8+t(O3x1Klae5oK_@lT#f*5OI6 z&z0s$uE_;q1NQUDRj#;}B$3B!*O6R5iMuT!R-{Moo{_A0kC>KgxPepF`VD5@b{+*4E$otPLA6MCR@D?i^}B36%@$O@ z%U0EFLG`nupV7`dm55MQub^j=BDuOPM*fCwt6_FyEj0Oq-8QthD%7fTa67GPyY@Gy zr3J#-s~EXGf$Z-+`gZG@*xVuLJNLLlpJx%j}$rQa#7M2A$b<3Axf$UQH({iN^rYE>27scqT=(X2RznyRu@eV_>$)X?sRwZd};@c z8MKPYp+J7$ay?|5h|^QWSU{Dn+wxE)X|?iD#TFGkkh=I}&Jv=jON-`t*p?DK{F~O8 zG*-oKpII@*&D<~j8nm%i%yfgk`ptDJ#3{>4B$w@UgFpIBy$d*HZ_eDOcrcjzK{Vc- z?TuQE2=O4lsbz(Dkl)l(2mt;<>M0M>T^Mao?j=(_tRJY~)Vo+=jTMJ-pW-fG?z>pd%9FjMl_3s0 zcXBHCgSaAWVwkHeoDxs&Ra8B^YNy}SMqtf7NsFH3BR+@xj$C)aLr-o#df=gcQ_BK# zJ$4L{M=*7xrDA_%MONeK*ixFR6gexh=Bm%KsrCcEMG-8MYd>MhpbOr+^(Uo|90zpL za&&CTci~fUGD-hDO%WykaMvw^l6?FR*;BTD&1nE7;3fUiBA?dy_|3AxIc>nD{bdQK zMQXosOl|7s4T7w=%x~60mOd&9^V`I%T93snv8^VCB`ma#Su-GOFP54NEt932 zMSZhNPrAM!Y_8JtGv0oeExV^H-4+tl_M|_ZxAdgU2pfUZb~aHaGKGoHirw^T#8;SO&WdM_ev%I1*wT zXLkI~2xbIAk!bGN&Trt?mJ-Wl_Gd4)o9Z%Wueh7zAKOdr=BBP&mcWu+d^3vyG(HLa z`exBh-z@D-ecjkkh+sc{nxKJgsgt)!LjDu)*>AwcrT|Nf`t8MZ^K?rXp6r&(mmYQn zf*5kv(GH}0{h1!`dp{9|^?RVkqv52VHCpP5^4o2fbl7xON9J{DDA(4O}VDAL0HWTe*fw z9eDyF?)T|B#{ss)CUVf$;?%$|KWuawNcf`KG8jminI?Mc)ZWp#A^`QqAF)+*nx%ms(q zircR?h5|v_8>E$9YStFsJ~ofh_V8@n?F-XpH^&jaYwPonHuF|510gRBXOZeK{SUB+ zr1}{N&>Js{2-6O20}ej3ReFFDIA&?<2rX7k0qeS(eo9a3!zD41MV#^#ublwLcIu^@h~nApBQ*Bp1jFq-LuL$wgHCg>R$~ z)eqY5_rf7l5ABkC!2{&a$OHEpxry_O(28({&r*?+fc;0Gn+|ZFtVO0*9 zOSM_|o9A-qYS9C^wdCEy7VXppf1p#g+)5VwWIOS%XlKCm0~ztre$#K8&Oxs`h~$Y5 z(2jPP{^ZalO1*|olwst8c2kquWwqb#zc#hy-VC<4h0f>=G^N~tiFPpiN58yuiL7GFIqsr_5!-(&boh$B8AA08veNqRyV z@fnR59WA*-vfP0n+N$lI{OFg<16HU<@D;jwg)BlD$N;7YZSwwrblf4#p-jp$2&hla zMD(mQ-yFHo2c3_beu87B_DBGPfoHxzh+4QCii(HA7_TpeCJ+wu8)4T6zM~Ub<{9^& z`~1EP+U!Or^cfbyagJ5wsY5g*C2w$3v<`#G8Ac95>Ps+4qg@~(jqxRbvalETAW6TN zFU&Pb4ps9vf}|$Aw8!*M&&RBqCtPGi25!22=(gyM)R^am4@o*D?X&b7 zY>#6_0^&CGmKDmD7xA5Q=ohU+dL zE{j6?^yPk1f;$6cUr*durxM1CaUUL8sJC>hkCw^7Q;(35c2qcYV7 zv70s`QBVb&#vy#57L_eY3sYX0dL{T_xfkA$FXoE|kv!CkJE%jJj>0GtxsoP$xy;TG z5%4I2j6^)}&6A)ZAO!T_yjM=TXhlZB_+DX;bQ4bqUGh_|p|UiE)}q1m1@xyE!dZ@K z7hLo5EM`opOfhF1qfwEN}TPg-T zQBT4X4GN#xn8V0#SQ-WO=#}v+A~FhSYY+#{;vB^pJW%KHqNJ~Iof5Dy2qFQ2d^=$* ze86~1a?o^XIC!bL*t!neBPqd?fg4`NC$X7uAp})~+h82x7PgQZ3gGuqdx#bVg$LkX zMDR>Mk!%{|hw3irHSPj3C1=M-JACD`zRQQ9P4EexP;vyWgm@b74!$C@JRi}W0cZu6 z13{M>$uoplRN#XjqA%o7JD?88hh)dp3?wlm36ac#4iRVq9iS{FLRb7Re(|u8S2t(y zy!m8X9^_b7Oug{aLgXHQS=thoO+kAS^qlBU+n@@;z#RD?LX?DhKs(W7xQq=Ef1ngR z;!fEaG+Zr*1I8nQmXaGwtI-h<$9Qm{nCv*#1iB&Hq8l2CXAc@7lK>3ZA2i%SE09M+ zM=Yz_qIN({p}k}hjh9X#uaaizP!#Hf_)h=m3HnOQ_=LN1)Wa&5r?91%F?tRiam~;c zY$y^cxr0zpT@i|G1HL*O5sF2T24O$j0Lp4mC!Pz4glwSi8PG_|D!$Stm1nd=-W_rW zH53uVMLB5}5{tJB&p~vsBt|V!A*S?)_RGQ|ozx;r1ZO-6$+jg8LJ25=RG{^=Ox7JQ zFsNg=FO&(uNm(~AG066nv>qf9-6$AZ@hLQeHnKX<1kL0gVS!ozfB0sk0NyKVz&AIz zB8)|7)js?;ED1Io1VU~_7vd&)qm(Q8aKSHb<%t9chqkKM_=pk72U3a8&#`b2-I9h& zGJ!*|744N3hK8VH1k43M4BB!9JVw?2eVfrI#ZcnbQ@;3{n(3WXC%4HNPCg|Q$dJ`N~|ywWK2jv9$SkuV60UE&__ zFhN_99uP%VFa|ex9(|UM$}<(}$g?0|r50iZ+8L*8&=l$rX98fv0K9#IK*?@^NeLPu zpG~+!C!h@?%|91hqFSCZ5wQ9J{Q~TjBm01F3!m^8DMj8KXwHYMD{qyc)r#k2kHiDL znj(zDCeh0{(m?a!M3C`!{1W;j?+|I@FO;Ca@|k>C4dW|wBP~*`q8=71O3BE!JXv@E zjXY7fkL*&v&_ebMqNq-zrO*-301HJmFa=K#?Ze*D3S^i1Kp(Um$|8Y?0A4{Hjg3Gn z4-|wOaE#V~0|^m9UNNmh-Y5sjL<6AG3Ni9ff@)$jV z3}mp3njqD1ogLtkR*f73bXFEdwJ0{gB4dAGxBL%T2SmrjB9z0upb9z(lBupJdO?(8 zJR<~9G7r!r7t&>@C<{o7p&GaY--IC2HF?Cc-HJX$eNe;LNJ^Ak(Ne`2@^|qD^qf3f zumxm<|6nt10aN7#V~K<_Af6h)UtvEuLhT7?jJ21q#hv7-P!?qzXBiF`e&(B^XtF-K-$XHTip5T0 zGPxs3qBI@p63S3Be?f2gcS3i-Nvs8_Io5Ub2_7r96L=_s#y6)#X;6Z85pl^M=4u@0 zF#zwSxAICwHG|f0A72M4MJS-PuuSr#$R9iwV)LvtC1h|@_6wwv-05!se^yk%I*YDw z!dOYHE8G=ULl@I%l_5z32)GxNry=s1V%!0Jd77*e=Vil){_TDOwCKKtAI`8rgwo zk{-9vhSs2yvRbqmCyM?-C;AJ&H`@KeoCi^ z?-WN+t72Xxkv0e=l|jIplRc+BWF4RjH-L&l2-yU#axZbSxu1UERq)Fdp^PjScM73| z%9LzHw=@p=s66E{p$Y1TY9i8P*q|$|2TNo{&{Czi#8 zMpzL10eMqGO!_atq>akbDEos)Mr{Z-W$F2-vGSPDg<7k)3ouH1pL55_uD1~CLVsboF|B&8=%noKpGsC;D@K&wFw;1*=$Ig}37 zmToHI5RGHhMt#y+u0uarFY0qC*1!_dDYVOl6@$aDMHB$1Bv;5NQfg!k3Fo`lXa`k5 zJJ}a`W3uq_SnzRViIG;Uk2pY$NNqqoR1As?3Si+b@+G^EYza>l(NVABVtH{y7|K|g zxPxop6Me0Lx%_$>5TlKv13X*)fPGivkTFM*G*aBic z>4>~hO42w8ZQxv7J}p{9b0o#Gb2LoxYzAe^2Jk%iDeoIUR|p}QMzU}kj6})z!4pDq z;hb6oZ8b6?kjGC}udu6%PQYt$PIixDJQu}+_$^312oL#GhZ%!{Qf}i}gCpVwG&R{< z^%WVW{qPvX;HeN4rsc*?OSp;K;SU-G`hs}sFKrJCvv^iqQF|1>QiD8QyOLN~nKF}4!lL07at=R^-YfE#p95CXCSqR6 z6*8mej0PZk#z%l%F7#bKkhX{w`;>u!%rb`b0TrSx5*jQlzXBC>fs0_MWPryM7iuIE zUt9Ly_*V&@r#~k4z%#?j!(EQap4jddwwftLl(U9d{t5o3*DUz|In>-d~J1x~;j z`i@LdkQ_DWfbD=oX52#0A%}`Rv3rV2#B6jA-y9nRKO|4GF|--DL$2fn5`ThBs#BOG zjAK*_BFIa?IMa7|p`aTz3KL|-fMMeSqXh6=Ia829`VQx10iiv02|4|eC`bs>fZRy6 zyl(ogyJSi2uYzd8+xMA5?wO*^h-#Pf!ryNtK1DTfTn4T7`rXX zDyCBJ40Xuzz+q(!go(1Zk{EdwV1ObP`R1TGmJ(E_WTZ4=bPxgINZCug6nHHU93O`s zQV4AmKJr({1)Y#JD1i8ycne@dg9Xl5Cd1#jq?9VaGL3&occ1`dM^+TifdI6?l1)g8 zbisG|L2|;41tUuh&*gLA7x084av?5{A*GZFZINl7CH_)tjJC^4Np6+R1)B}^@or4q zA`sAp11=}YCKSHKv-V5vI^2W^Og!{=?%3*b)<)O8JnqoD9fx& zFfA1AxnFrW#jDU5yMUU(UvW#(zVsFUhv)FM85<&VK)u=#9s7o~lR-gJu%cj;JQTPh zorMNe%GtQ_X7GW9TGT7QN3BsdNgfFJ#u<=W-Y8tt8!zZKT$X#MQB--};1A!p1~(N= zs%(61>OvYR2O4^@;2dkDh3JYFn|MzCD7vm|7!b({bYmPAD=P|`JPYzggamTY9*h~Z z#vlsis28MA<AcZRVFzJW5w42oC{EOy-e33RcJ&&;m4o8o)TprE2;lzZXQ3 zEs-tY5M-tlVo~~xEurP|bIB+R>1ly-OK6%b8GKSkTBC?$w79DB@TsJMP(ogvkw)ZL zcqyySI3@pte&od<8I6$TM6fBIdiiTwtdRt)JGJ6VQVM2>mI4SGyQJar7QiryH{(BG ziSo|mxTTxQjVtoTqR~$iHEZmX3=3y8HY>}`BScurHDFCBOTL3_33!1nYBXA-5{g%N zCqOj{6U0*_Nm#{r2^Jl#QB8`b={uQgX%FX#EZ`{0%MpjNd{99Blpt!Pna&73g_cl& z3V2rT7?Lg@9v$IM&<;s}3RGrF;2QT)sW4EIFKW;Okej~9>t+mv^5C}dL@m8m973uBQhqDaLMfm;jVGECk|6KO zPRnK}4@s;`o0K`gqmo^veEc+INLCB)5gwtjU; zfF87fAIb+$^q!S^NX#d13e7|aR3pdAgvkrXi-lhNBJQG>#xlshf?~3u^7S02!bYa?PD^jLw6GicLgg@taf7Q#b%o@T4WXU=DPY{2(9Fe1pBlL5E6a z1Pw$LPU~G47)lG&3n4q`qJC1lB1z@tDVZ9{UmDa8+I{?Isrvm0B_|Z;1bavNCXopohVxQ27Tj- z(1jLD50G8egXM=_WYDC~coB+I6nA3*REDXWo^T#X<2sCkLh^mE0rU|M09xYRAwgtD zpc(a1qO$*nUY1;-w;Cl@HcTV6QOQ5jpd6W6BRVSTW7He&XFyrvNPZN}DnCUg9;2mr z7rzua$o`>SauCF4q768y@h81|q_>QCh9`xP^15{^r}>80PhZt)^@x@r847XVGht49DSPZ5qHJ#2fjT>LCFhqp%L7veARN!Rf*h>_?m?Gbj#>tJMt{_}1e5+)A; z4^KKq-$Y~ia?1QGLn#dwRw>5;cXTYA20-~{FcNI$SflpjfpljQUEl}E4Wj56?XavA zPs&#ms!<0yKA{80XeAOMt>=hiq#D^4P9dzc0U97_%CUgbI#zkeBgbm9i9eyXuA|#n zJ7|su>)2=vgp#GBJmM2wN2_ETbu0`c0^?XxCfmqZ8nr5Z!8%CqgsaGza(ME&&@6K- zEE9JX9pUfOYRMN=Q1pj9OG(LOajXbamfnm@LMc(xXex+A{qRQkEPCj@bB=$P`6@>A7*ah#mD>I1zv)?M(F^6+M`7m__=$uxR~b>p0TIpugkM~X!~ z@K|vh;Efrh7Ti&{d;&6(Af!MFiQz-?k^4s(uH0vIq;c+Fp8e$+@WAi8J}vhwU~9gT zHG2ZLA$w*iesa3oOhsi9hC7_@P0lzao=6WQ&*z#?kvaj0>9u*QF;kwbYKRKC&fIx> zMm1ADm^H<@T(efpRHtOaLcVlQz@7>l$et_=dZi=n{9}oNP$<92!n{|IoMfU|E%`Of zp7qip&F!(~KczIx!^#T3XjslnG{ipLOv_aRn3rqMR`!Gg*;GP6vKyRAF*ChN{rk0s z*|T8Pu;i?s9E$mwF#3yzrOs(F({A#et7RrxS%hUmY0mVT-#P8Zw8<(aXNpaJ-%>km z=3CjF{`b>v)FI7wbEe$@;s6$dUpO3sVVQRlpFoD70%*sfAz^_TQOZFBBznotnRbKO z#zd1uz%kOW96_+{j%e6{V`tipGwlX<7}r37Fb*p6L98gJ8HZUZRcG1_K@6UYh9?LS zSy{qyUvxBFH76 zA*ACIsnLs%3|$dWTXeF*I=vpi>mwkyB17d(NtzH0l1jGdi!4%}aGeNPsWE5TjgnY| z>q=ch719M90%ioVN@5XOk@C{ZGS0Lcy+lrstf2^!IHVesXjOQqfjSdd5v58#NryVq zZk%a1&a@j+rNkHvyOQ{DrrkKxZm`Mx*ASC4?S{b~4IM!c-g{sOP(z*0v>RvIjRrqR zi{te=({5xpGzjZVyRlvvcc$Gq({7w;H_o&h2Iv_!)r(IWW_PCDXvD*rcH>OD(W?{A zv>Ux+>P)*~{J~^2ooP4Dv>Ux*OKO`RXWET3?Z%mQ!+T|nk1^2WOuKQW-8j>3oM|^2 zBzLCW=w)Qyzi_7Am>0U8X*awT;Y_>XwP|PC4X%OX*bTaoB!ps8?zyd zJrN@Ry`WMzNUK`UpXSSVUmvrHj9t)g+9S&BF=I{K_yfCG@upnLgqkMt9-aDhPGf^E zwh6kUTc3DVTGe{pB7Nd@;<3z(IA^VmWb92L**(tQ*r;A+R(Czchq=eiLG9X^cWJv7 zdmEq@@!pwzvtn_xRgX0X#!O%8-6ty^>3-H-1uM5I+UlBCt^&3Ho>rw%>!NL!wOn1Y z!9N-`{l_&GZ~W+{YgQCqQFc)43oBlDMkH(c0}DD$pZq}M>zj8?D^T`_bMH7S-XkMf zkiLo{T1F;mu+J4qFwx!@aK%Gj)>X)04q~4Dip+MWwgmk-47EkKYAQpMTd4lo z3{9OB^*b19@A#A4c*~cZN>w$7q4wmYKcAt&I5Vp=VzG?4R*wm@M1jkjh{agqB9RD3 zgR#5}wYU1}pc!KPXZKS6=bDF=I$bYk`>E_r`)qw><-HWO%TW^}@3zcns#$XP+dPvC zjT%@`^PfyLGrD6HlivihoO7vNG%}Zb=vSyG^&zjo1Y^pVXo;5WhW0N7-JNEoLjaOiVk{jCIk%3>qgxs^LHi+NRsk1gn=X*o; zvd`+7DGwv9PgY+!7&l~g%gk!oGZJIFbbt1auPtwk^zNz?wl3GcqfhVV@vIv%yZ6lO z5og2o5YOi8vg9I3?Dh+bJa@V4;xp_&-`6fs;BW61D44JJU?kNNmtR9(Beyn5axf=* zi_;v;3FHeVW>azd`Tq$?lWJczo20gB{NIo?2f%)nr1=}OTGZr^s(|8}|BH5$(&KkF5V;ich*=-NJ<# z4}I|PKTn-H)p79!W$r67f@ zc6Q|H0X4s<+->iU1Hpms4OqEm=tVczn(}?^p-=XYe0zN1p!fRCPrK!@@u&I~8~Ef$ zoocSQbn+|Z&e(MAlQk!_7)MT1h^hLj)GJldq_6?gWzY0%tBS3FRveW{!J{A1wohuY2j$3yQ_-T$wD zO&wXJY^BT|=N{T}bk&KSE03;xXJNSamlM35Dg?a+URu)gsxjSKoip%~Uej4?VBmG( z@X#CkpRufF+0F$k5A5CFGh#rKCglglkKK5v^N&4?r`7rD%}XO)Tm4*PbhVCYI|~kc zzvkDz1rLue^rNf58*wt?9>2 zmc7zu)U>ttzxMdyzxSSh$CZKkTOU5J+*8FbDR$|ZBQnNxJ9tUmou__WwtU>;dn-Ov z@!qvV*UoHrY(x2P$N9Yv++Jw*A~i7oGgPQ|hH};7wuiF zU4cn`t`A;w#%tGS{Jqx?b*^}9$kwZGz2Ln;XZL<=$;hnm;Za|-8(VeaMJE7s>-&97a*(qmUeAME<*pgwmLb=QAs@RPfjrB(bX`b_QfhOHjvzJ2uk z_l~)i^{f(@xU}{AQwoJ!raj%{{Yk?guUho12hZL!wA5Me7pt*j>5B663l6!j_QHB& zrfobrWM%1EcV2UOaM0GvKO9-M+?MghgJs7qp743iF=cPqb>@->yC$+0kGSrf*t^|p zmM>bk*1&Ui&73mm$+W)@uXcCYdm>$5y1H=BK^J5cC^+-PMvjxO=Y+(P>v+E)F8;Z+WuefP40=cSdMw|i8Po*&J8z0RRB zV~5w)KA@m;rPog04mlG^i@9O-%4*5Z#1X;!1%rM>!XjfJM) zbl+NUi>vn)+gIV`k$w6-_)_y~Q{CN4))|z3;m+o53cF%8R+e~uV4GVH71&yK^RRin zN>4xYK+jHlmtS^t=z?1(eZQyO?km0JR_=c6?UrA^d`;`8_t&3$ykFr;flBFDwi(`J z)}qeo5AJAGc6po68;pDK)7~3Tb$+bqp~-FDU+AB;d*kI(FTHl?t?%AEv);Q)nnrKj z7(WnNRO6>%*KDW|d%wff=6zi^wl`*VtAEd#$4Xtb;N*}J1-@zbP-eCF-V9Z$T>Y$p z!A?DU&WyJz-RtauH@r|_b&tnZOkMl=(cV=G?5W&8>yB-suA6Yz=mO`w^j^n@(p`n8 z99sB6iO?BsKQHoAO;^z~*QAvzKX{*ORqZ#Ly>NTrpJ$dHd`0s!+6+E2>HKa*u6?)1 z%#xQBeq>US31{qi_o|+ywiZ76#clikHlyZc#|r%I+`^O7&K@#i?-liihod7yR~#H( z^8APETrsPGXUszxY0)~5essgQw5u9i)M7-p`dvnEAN}02io4EOJ7R0ya#vorti!J2 z4^8gV>hJ!V|5$zarEB*;R&Ubby*^)|i9<_IYP{x&&P8s1iY_thc2jn z!acBJtjb3h&R)EAR?R9GH(s>XU2$H4%1c(v>N2%l*9R-#cj;H}-92kn=%P+Lx@|3S z@Kaayf_uK0eW=J?6>l5y&62W*yvO?f ztsm*q^vLt|TK>2$ysq`Si@SJ_oHh69YBgKF*naKXcO0J6WOMZ9BGaC^bKDODe`wmi z{iZsbZsdgrY#w-}QtB5h5h%etNPNy)ss56>s+|& zA^*582Udrs9UeQvedCjZnw0BZZ1#iw$8DO{aKoH+yE-SjjO%>xlLM;{e6n_R)`mvw zR($%(+G^9+fAC(r(p5@SetPaheMj{jd8F=^np-+{nDkEVw{AP})DQJ1x~tt^?Tm@h zi6^TC`!3k~THnEaXZQJZZ(#rR>lPpB_I-tYD>rQUSHCth`aS&Rg)`i5yFZAPcy#`Y zkL};Gcj^9B>$guH(0<|ecUPyas~MfS=JPqVW?cT^>Q$~!dQSYJ*y3_mP8t2!=w+QJ zu4(w(_-_B0SL52JZ(Vf#;)C7|!4lqaWix{ZUFBS}d<|U{eVa1BT|G9jzsFNO-u!sZ z$~mh(T2^I6w`bZ<+;m;{tyq2t_I-EZkOB+xq0w%bh|+IATIbz;lMv!0(-F7`p} z)ur3!J-6k@ExR|Z-*jEMr5Be8Pw#ol&Nn{YxpK*i4|nt~xbLTa_mnx&?Zm|=PL#N~ z#1|!omRMOL^V05@^nbMVqrnLmHod%Q@rh4O*cRGSt@wo3L)&{r-mEjfO7A;*|M=zp zFDvz(*6-_H-rkM-ZrOF`mRmo$Zpl0APQHES@+;qsO|Cz3X{TmwSKYE|@H40G+SEVZ z|LendyBB3WaC_~QLtflgVcRX=e7)_~>$^SM>e=qs-@0JQf?KwC*?#r?Q|`Za;l%q_ zot$>kd-CY^t123AbS3o^IDWy zTW0_H)q->U4_{FFym=K41eOk5ymMXF@%KJ^Z_lu2M@*{!e%)5}s*jj6Vr;!Y{mD0d zaZ}gl$~Eyld(AT&FFfa8;Wb^KyKG6jB_BN$4veT?G0|fE^POgunDt$B%g%rH^6vQI z{riTO9Nw|ifEtS`)p}<2xR;&^`DXOmLL-*bN$?O*|dvh-H%^1ZodD!Serf@rcYgT;Q0e5!;Qi(hwFcE@s28opM3H`?{}TW*P!2ub2s_gx13&~dPc`_9hMImUE|~LmcP2bbNxzi`Jx5-9 z;I$XKKDQw-H8!L2ZAb4v`S6i4D<*#UdXGunM`iAPW%$(2n>)|zb!5qmC*E7}bnAkx zPXza6-aKpH&cx4OcJ1)fymguWjW@q`(u45E?tlA{ic2EVH=izb@vZVKf9-I$JGm_-2K+^ zuTQi;(YVCGLf;muI-uFflN%NdAKZP|){^Tl3)FafU(xRx^eVL0_2T3iH*MUwe(ctD z{e3@m?l8OUccWuFAN{(EyT{AbruV&feX-3WUby;i%}P%=kom-%AD&qeIx?qwy)7p{ zdA!;7kau5`rJIJ&-m_}xglj)Lxc&Zb-#+@|xZ=;0dGxkE3pd`i{78$%@$nzl|GDRn zI~xq^T7Pk=*DK%H_PXhTeogmQ@3r#i=9j;!_35h5KHvIApWZuGe%zth>j$?!bN5?c zl{oeB$hS82+rIU~w}L$ftSPj1@Qz_!N`83Rz8bIm{K@fECvTm8>(a~4yl%ly`+2>s{``bPBR(0(dTF@C6W&ZK36(1gKJEm=+cj^v!_0&82=C1#0 z@u_1cd(Y3z>Ugy5zNZ(x{=zqXwts#1H$N?yR%6PkHNz&9EOgn(8vE|wd~(yt-Y3RC zUv<@)i2*O%JMCEF`#X^VfEK}g&i~|2``^Aru z6{|mMGI`M1pIv?Vl{G3|{&Sgf3>~Hq`n(g zPaL(n!of*fCKP*f$%>A3-X4&Cs$kaSUM=#Erc?7Pb^!ZlX7Q2>1Tt8d%;E);sj#Yl zI05o|vUnzlx3d7r{yLGG6RA0oniHuxk(v{!Igy$ZsX39F6RA0oniHx0wIVe$@{}iX z`BUGw2-py>qfDPBz8Kra0LYC!6AAQ=DvylTC55DNZ)U$)-5jl)qRuCH2)_XB^mh zW7m0O*Lh>td1KdkW7m0O*Lh>td1KdkW7m0O*Lh?2FZsr9FmB+vC&8bzMTgSl1o(*p$6~md95ua?TpSjh5wr6g=iNryD{ zAx;muTx^Gsmd3`f=>hg24u{gx+#whc1XoxnBH(tXd)O(R6*+RfGY}T&*i!WCbPe0G zqsiL8*nPf@C#^aw%EnO-Yv*Woi1uap_@}*9*_}PiR}bIVjyC9J^;tyJ7hzBK2s@Co zlP!D1C3m(p=Yw8$V-2_i8P<-|>~#@mCwKO8XIJ+yJK?ggE%*C$onzpDz1{V^*X&#B zW{*Wi|E>L1yuK0u3E zzueSH&9v3js$S8CV9?E;lVR#*$6AgPY-k&%p{)C_oovl6!tvxz?pozG9!Q8jY9ExO z$H54F4v1gsFV|xMsOAlu>*~Mg%BPn}?!JKOk9HYmcX)Qu<&)N+_WlTbfT!?@R_T+S ziX-9=*Fx+(?Dp&V5If5U*-@Orp%E}{_+{-tAJ&fIrd+SJ^Rd2L^{JlI)EA&0?Y0Sb z*m;v;eC+XS_8HX9_VHwWA@~8A=m+)FXZ8~Me{|=26vynmPW#~#JD+H6d(qp_DUqZ% z$LgPvL+$$vC>XvXD`ux{^eV}RU?9U6LB7q7?g8yIEj{wW=CpU&*6!+Lv<|KZV&RLoxoX_+N-Bo(~b3bkw>G~ z5lfC}FM3Y@c--i1fW5LkN%;&Jyol02+QRKp6VN4mbP;!8x5rrnS2{_r(KP56WZZ zW?u$nCL|QW2%j{D-XgrlzDQm`OiCBuV^AalSJ{g`7zs+Iku{#@5kprv3teQ%M28rZ zNr10zc2>vo(0)-g!p4;eD9EE+kzGO$EIGl(P#^N3aSXL0(ib^lVTXuf;vEy)t^o32jh4VsOt3T)w zazR~2?!Z0v@%A7?vMMol-IRS2FD?FI8=*dYp+rAOoM5wb_(AD9ubyKW`J2!&ySQRQ zQt=nO$cz7ID6RKUyXXnER33Duj{z(s^tH}o$7pu~t)V?$*==)PHjWb9cnjz*7GL&_ zatu#1fIbg=#K)jLabMUI^}D=op@VIAEILxMWnV&up3+By3p{J_8$C4q!p_4>(JutC z(eC8)oK4bCxG4`nco1QkYZ4{TA+l@5om; z>5rkg(AZ6R87j~8g6jq!L%I(D&}T%?i@$s#uex7yOe=+(_-n|U+&dwO#dqz04IT<_ zM6Uq)h`{3`CF?Wo4~C3=#{0rY@u*@-vv5>+079c1)Q?>QkBlGYPd%@8s$b|iGJ{Sa zFBV-5&Zg2Uo-8*8T_fnXP|@@=`FsRgMi4=8F%VZfXsPII{2S;meh9~f-_q}FJ1Tmr z9R@#m&hSgVCH;T~l8&IkBl*9=Eq!9$psg^Kw!qg6blScTyh7ILj|w*Jp&jtS$Qk-5 zxiR)c?Loq+2P+`l0NiU^k7wsd|yTAXpB%U`ZqL->J`Sa0E@!H$yif z6U)DVb+S+DS3)>NKjlZ~!9~i;!9BD+=mI~uA37(HGw=wlqh4ABGE0Bi>4)}GqK>gQ z^q%V;*(-c#+F)=um7htxuy9k}sYQ2U6Z8@=*ja|laDE5N+Di8avw(uM!Dd;lf! zlaU4MJk7VSqsL$z@=P3_{gk{0y9A5`I1Cb{To30w4;8StLPvDd;u3r^K8#g@g2iOC zpK~37&sBb&=Y^d156^?A=oxx!pG)Dda6{K+KeDgKa@vP%3S*3amVF%#+U0{hWIYt$ z`GrBI2E2`I`pEALhL8f`wB?Vx@qy5_!yA*Gl##_os9c%bZEEfzx)pA9u^BfNcj)&M2KqQ z1kTGEDl2T_FX<~fMayKD&_Cix07~(V^w8iUdM*2k{{`NtzxdRmv+=1G)%(LufYD;o||EsnucPn!rogTmN z%Al9xDJ$P@{7z|r{4miLE*bp9Qzd62eM@=2_Gn&nplq{dI;Gh5CTe{%5qa0}@5 z%m3w9xT|(yM=2u)zwobNAp?w^7v3v6C-cPpR_Pc_p4467F;|RN ziFB(Zm8aOkjfc)Lc9fEl4dOCAZ~PbICE_zE0~O=Cp&7JQ&3Y=LyBOc$*!(3M$+7B_ zFHcTPEeB!b>5Cp2@*43_#7*&rY#_&~Suu)Qu48#l_$aj8#j!rc7tWdtCfPkom2{IY z;$D0XT@UI${0fd`A^egr`Bd~=_1|&$HG9)$p6Q&SDAQMQQ;yaFs1Hc&M!=Sq& z1@2dU&{^3NdISftwMpa+J65~)RA6Wt34MT`t7>x48? z7Mh3dX?%_Acq^bA!YxfSdPRNYCKO$$Jo1(r;i3G1ut}OpG=+M1UA>_$+M|prnylv$QH=sh^LblA z-V!vEJq)2kQd9Y22D$W&V`bTq0j^6AkOFC+qHyF~nhvc6Cme^k7UCW&do0bD&G0}Z zBwy0Y6ZA}(Eu=Su=<_^uVYCGaVf=yi@Gb|^0fC5?WREmH$oLMu3gOptY%~-421Ce~ zY=h;uYWxz)aE#?Zcx7{_g&dC%N#k5JgZ41uO@BjpFi4N-3vX{AL&6@R2S`G8QJHV) z2_u)vb3<9(j~t+Vpb^MI`I2HdghbIJA&c;i%qR3f?~zH0Ku>6mJVbI1X3Pi4Cufg* zX-os85%n}?r?GC3O>z+=*TeGyBiTggkG^t%g`!2g?`9}Yt7%hIx(;15h9b`d9fUy^36EQ`ywC*Z@|G-M$_qU)Woi`5{g239<7$kKujf3K!-S>1=vFFM5m|? zvP#F$eDWMfF=RwWX(DotHgjDH5EhhzY)}EABj3U}07yrfA_=~OCjnl|77MF|4e~U= zCd!juOZPZ7=#K1hEPsPM2y%t7cxp&J~b4;7@NsxPTBFMc98G_QZP!?5Qq+)1|jUq{E3vv=edO&HT z`xWQOOA5TRjs0pbXakyn9@r}tPrS}Wv7>hX|VWPRW+{jrdq zCL@J@w1Kf$BZu918=?NM7egU^G!Fuuew zZ-?-!CxNJ9i##{COm-1u%+26p?`!rpI6@t&nX}hE#bNjWVEg=o?5#Z{)j+korTPC7WV2q9|S>DN=f9 zCxoIlp&2*py)>+>A~2x}RuVf9AU>mo8mr?i5v%~+!ph*S_=L1*ADAm$$J)tmD6->> zdPiNLuOd1tcL8-l0eL_2XoPCAp^CEP+3VdB#U{`Z6ft9)Xfmxq<`j`gdGJM$RK1ZS z3YifFgHh-p$7nJ#Xe621@yO`~9t3@)_58?F1r3oX$tk@>Q@|1E2*%P&`BhLy_aF%Z z4Yfu-fxLYhY$O;xr3}%UXT%liOdw^#eR;@u{Ic8X5!j&UO)(5QfKFj+z-oC|$Pm5K zo1`33g0w}W4MGWeiC!Rk@+{;H$ibF%ijN~wp5#`{FXM)DyQ=`}PTur1UNCag=1&WD5QDHYsGn$@(VKM0IC#qB# z>XY$Ypfz&Bb68d5(c-ztU*^pTqhq|41;>Q|WHE)Y$fP10q8jSQo8?~AlHQ{m&`4ek z%|RB(9)JbJO<;=N?7-IqhoKp=0Q}NkaG7?KO*CE-x@|lk%A`oWA1ALwSwN(~P>SoE z!FxuE$ps-m*hYE`zHm&`fdr%NaFmX7hU>J$L^gDmtO;_-MJh*{!Dc*YYLZ z<;(Hi>OW0Y&*ZTwCX|ggpU?xVEFTy94jm|!H*+bURw1*Z6Ra}ymo1g`fu7PZwS)5X z{tJCj&(TlgmC7rS@S_nLIall)S8_!1!C(3WwKSq&m8*<_G8tC6v=Wa?{1*2i675dD zuZc0jUL%*_t|B@#5r3ImWb*Zn1>+a;k<<@#SJ(kxWSznHB%NeQWC76v5XkUT?@>uh z6sIKViTr>TPzHY$FT|Ed)QEOk^9!I11QT!33iu+QL+AxDX}aXZbb(XKhTw~eUfFUY z{FY~fhT;W*7SPkCH$0;dWK7Q?XY$;^5BV-2h9#$>yspCv>Vq%vFF7uiY@gz0kr=s6 z%AvB3*89$MLhTaY;EyGrfJ>6!=%=aQ8Yh$7itmztm~8lj9Ey+TyyiA=@GJckb{RiX zQ9hQ5j73szl(jy6J7hsb2P0pi1Gs_=!MoGd2fqwo{vCbLOM_RG2YtwzB+Esvu`DU` ze4woCw&E%2pL`XJ8}g=DO<5tlAzCC458py)1enU>Bae(7)Sa?5k~yxRGgu)#g|5jr z)%65e4`-!ocvZ@Ga15$TGbx#xsRIvFd{9Z!N@b-LE^rMSNTh~5Dd(ke6-D6kb~r}& zq#^Q*Wm%AGjujOsAEx}8+N^O4<)f6p(RJm0(K#JsFX$6z(H}hz(yBJ~MCFPX93uge zB^?XfqTB(EcwTSkaZG;z16?;(27jGn(F1fh83M}3CMny^U%U{m(>Z0gbzM0EvfDh5 zesYXAEKf_vY74Td?6#;R-;-n2XRr(0r83#q`1i&>Sn%%^QzkBUC^}bw8GqW7^V`zf~!$VZoiY9}aNVSe73&1SbO-tlkQYgp5Sb7R!Wb>$))+-08 zWBAXpJWgUF9fNktZwM#o3D!@P5pF6P!S_{|hWwJjrE>XB9D~%Nlqh4oLNrZzb>+_# z`KooIoJO#SWT6yV3gxJuXSj~d;5z(=kF<@3iIUh5Vp&m6Rsor&K80LRR`$b;s9~+t zT1j2rA8{k^IeC#t$#3|9b3y<0_C)GhjoAUYR*xQ;eIkAC$m~()j+j0oS^d*I0av~g zXTv$uUUO#b=JzdimTNd?mMfC|$LDAMwt27N)OoMY{7m@`&iqXIRnGiOlMlncGVhUL zsTGG<@5yTb~6?-ZM zCGSOysb9T2=4X=ELvIw7%U)}6LU~i=Se^NqcqiB+XMX0dWgi%`ViXXIohxtQ z%+FMY)25lp<0^_GPSSXce#rW!Tvw7P&vkN8InNWj<+-k0rv0OF0A;GB$M!kp!IIy} z9hvLogN?Us&O7rnl^55K=68~POpbTh^y4!*UZ`ZQ;$GqzWdOlMA{e4-(i{ zrApRmzdQ3Y85`0Fltw?vm1@k3_nFN*0l#(L{4XUm-!qDHHI9t=m;aUX!%!DP;2JaK zZ75Q+&iqV^Hn~UEGg2e((FDzka^`26(P3oUnV)IK^|7pa z+n4tS7+B`L0LI1j#w60jn*-!p$kjRXGoAUFywOGMrZ-YFS;U#2>CDf>7m!Z?PRLI4 z-Ux4m>%CviXmsXhI`cD~`I*lAOlN+kGe46AvNJ!Ecb1*`na=!7P3>^zXFBsUo%xy0 z{7h$lre3#o=4a-f&*{w1bmnI|^D~|KnR(~E@**fXHj?p-?VNu8sxv>+nV+e5WS#k$ zdTGR&pZR}zerA4qcv`bI?L91W?{eEJt-7Y%GHom9VIdU0nTepx)^VM*wX9vUXqR}*+-Iv<^Q_ER%eXm})U@0kblW0M2h;RF;AX>HR+Y@P zqpeQnw8hed`dOKMdm`3s2;3?w(xZ3JNLIW@tUqUN;8dOXop;3IE$Y`!%R^Cj&KA2V zGrL)OAX&lhA++1xK(|AhH{eQ_3Ob0hhqB_&TdD%o;j5yQ5nXqnNgVeG>W|{;`>u8xKvw&6J<+NC- z!z`P`@;uhEOH7#4vPCJ&0p(prEII92;U&{Jc;#9@y!4~REVXcs7C6&(x>`m@3ve;5 zUdwaoMRukYXqhZ#TxppwO<#mx)-qYNLrVbhVu+c}Nd27WWlxr_;x1>qUh9?4)W3Qe zPp@ycyss{ZfLq-j7OsvsPyG;dBnX9GeAA)ynJ(2R(u zhCs*-H;E8LilGHi0%CZSB3J;G-V{p^P?{n|Kp+&QNRg^^47}gp+Ivpf`{t5_;Gg&N z442${_ME+E&#YN()~urRZb}VB$wd+tD4m{CR$2-uwL_;QTvTE+ZY1Uy%2r8JrG-=) z>Cl~vRRK46lE1!OFR7iN1=6X3f24OazURyJs$_~U*UN^$KE=mq--C$@a6 zUehW~i}K}qv5+aRHam2sHCIA(1qb_by%=k;IEHE7(PosOAA6)P*Xzsms(z0p_{1Z| zm+R&9g;dkNT(8QrsaP3|LZ=OuQoza=V8c*>2hJW-FV&eYsv=uGg3A_2qg$e&u>8kd2>aLDn+)KH+;S8?%xb`*OWeIULGwk<8xUB3lvr z8+@2mj#g!(eYswhCv))e;ov?T+=qkva=nxf{uq?&%|_A7u{hC1353nG17SZ*d}g*Y z5O)3{$XOT4s$gGVuGhzB%J0~h>m}aG$7kXrl%-I*kIz&bUX63xI(~_Y$J6f`v3Ozp zC>cr zz?x?C1-vTIA#gK!tD^46S;ea=?!FaYUr|3kJ`)?QawVXSV3VwqD?xr)MB(Fkfv2B5 z%lTfVQY?Q0{%!KisFQy|@kcyoalhnQz!R>Zd<%+K;|^c0*ZJYc|N3&hic(PC6(Yq| z>M)8}@a1}yLnQnGKI}GX#?R_u8*qv~ajlpj;-{oHKx_64r4FM{T-&~abAzZdc`w2mGGwIRkxf@5kM>y?*rV%k}#B%>Lq&z`9aaIa&<=I0{rooA5Ikp|p!e z+oQNd*w+3{tZUlJK`OpdV%(lFtglJRYwy**+kGM%j4ScYv19ZmAD^bDik@tQ;ydAm@cCyIc!?lmkl&y?&YxHa%pBd7YkIy92iE(jXu9pmOC10*LL%1n5 z)JncwFGhdL&k`kuKBLnFqQtNhDk_X)0xT8Sk!j76p)#-xfn=&g-%+G4dJkSzT$8&E zZJ+Ngj!b6;TyvI(4Vv7F@Zt*Bb#xyLjpVY_et{gDSWHSK&Umnykd={W3W8mEglp_= zYG)5d51eg)ujIz$ek>-m=Zl;TA^(&;lkwrzMgLZ4xlFn~j3>M`OKv^(04j72-K#iO zmR$xh#oSM?T%*?!zrncC)Z_z3{t_}UDi0)i`M7RycaYO~D};WK<&#nRa=m1&Br{}M zRvA{Yq59TJ6~#3+8muvtlgHXp;kwTHGzlw2Sz(2Jd}gVLmC(m$l0&mVCe{)(1@-AP z6YGE|T}Aw0CtR2LuFO4kC?Cp(@}YE2DPcS0$o27=WY0ji_3@c(vcaYx+k{_}M;DJ( z$r6==)O7-mO*knM5g}AX=r)nJP0k0%oey*M@tHn8 zQ@OyYn}Fs9!lR#I8vr$Ay+T(5|HxI1T9j9gaju0}MQBDD6KYsIo8{$k0#@^VvCerj z-Ev(nq)lgpg+T(+t2lpVT+jvj7nH0*5DYS)hbGr>e5k9+$fQ}K&B@-O^ES~2`CQZF ze&AFH9g};|7|FFPI@MTcsq^lJ&b7I&>zbK0l`EF(1~i#v6lrFP?T@$B_@2xNj94_A z=Bi$7F!BsfmKj|G?FZcxNTl(rkrqoWmMqk%@xyVDXp9WWCY`y&V3gN}wL$;Y6`Z7$ z0}is^&>x;QKrUnF4Zsgb%0ABRNJ97uvd(xtT2)T)hd&9@4Io80vi$Pl8muW?C(rr; zCpcZ1Hw%{1&Vdbc%k?W+G3uRvHNLNd zOij@MG*tjE!F6Rfr~evqjCC%h=|R+@Y>?s-Sj(XcaF5l8j=(i~5ypX2SnIeSF2r73 zJqmO9o>8(p%FBkZ{eM}VezMwK8Mz)*We&iBz2@`69uF1|>W2I!iZYlw`b z30(k5C|$fsqrww_^%bSpQ5%f=7T8s)56wy4(2+0K>&x{*bBMhFOhJlJ$D>k!lHj~$ zFkkoy-9qy@$zA?H6-ZDh9Et*iKw|!)KT=Tux`G&dz)eG%yba}eD? zjmnSMjz9GAnFGVe$7lNZOdp@=<1;@d@tG<^Nj=%__)IGd8r5GNG9r_5Wpzj7QdU}1 z1!zYUQu(M{nRr);03} zbguumwySpz88v)FiaMey*qaLb8|FT2?>_9*UA5EVRNaghyf|;x72mbuyjfRzw^f`s zr(jb-D=N;*titR6N^#yoCa5>F87wi?MzvDSU@Qw7)oM`5)TviojTb1+c9tYva2VuV z6tNup>AwPlA^>1JkPU)T4KqWoS;?nmwyp%tGUkeAtU$3XsKbK+ zqJv#5a-znst++Z^n9{R`7U4r5CbZdB;)4foOBCJKO^CKs3qANY<$b8Zch=2aAnX&q zaWK(EVHjDWYe$1@85b+fMX}(NOzx{p(Ey?d_aUun;bW#&4pA**yE7~m-b`a*t>$Z( zr|Ma1Xl6@wY-r7Ty-uz0Y`Gr8!knYTg+iisP<=LA?#W;%D(6$FLMGdT+W?z+Y8K}T zJ^6G{OqXcS20eIAU~}r`gt-pX9-u^bzB7#8$H)8lcpo3HI4U0>uk$G%AFqS+itLd( zJ;Ac+s6rg=0k=a7xF?BWIX)X3D9O;^(m7%GW`st~xzboLriAyd3ze zxH)_g@-^<`kb^TG`B!j1#$EYLDDIA1@lpt%4dd`IS#ewBca4o$?h`szTFCmvGIBF0 z{3;lu;wWSRtKhKP0&C% zDd*%#)vncEqrHL69xpv^;XE&F7yfY4OMPPDhJp<>O&&F~uJxFUR>R$f4j?Uvm$ zj87-Z7F@z3$NUF)tKDB09|-NF<~JZqs!k<2)}vol$q3`xIkn(>JQq0s#>*AHQP%ml z&iOfW_3`oY)=<6`k*(ro$pZlVtPZ`7ELnW~n#2bjT){N)h;}{!A0Ka?ig=NL-|#{3 zA0CN^2)uB<^^4)X!tVa+7faW{8w`phu9;H-%g5mB7aRZZ^^1LcysuvzkDZi~CJ!w< ze4_YL=>YA!eEniyzgYE6eSAEcvi3j7S3Fkq*Vivb7f(j5`}la_39)V3U-6NBH2bI; zK3nK^xcSL@(Dp0ryefDL5_w>I?l#Z;?|$;}@jgC&J{V_C`^sPDXcr zd7S$#zmH{6!r~&!RE_VoA66zIMds@%d|r98dX`@}kqlZN{I=!3s*w=&!L{BepE&%- z@e?NsgHISa`AxhR*Lt&TNE*4W=@Yk9*@Mzt>#r<`wBn(f=2~_}S&!uNibWk?Rldhs zCc7qn;Mh62#=BOwFJ**L|FWqmN;-_7;Tju~uV1Y7mA#mi(JIJ>%*tr}ef?q|A1})l zJXe&h*c52q*Dv<*@!0%{(Shr7*5l*j@&0BH>f__dlYq?|{ed$-#j+B)0*2`9r^+Xy z*O1R-2IcgMC_06kAa9g$EsR~2E#1e**RW{_BtAag$H)8lcpo3{EknX`r_j=eSD_nt@81i?9UW&Cr>BYKz)2BS#1RgS&c3JPQG2LQ>`;9w=Y#3ui{1( zeJF1&JhQ{NK;r%sPpE9EimT!)dtQxtw*>4j!i*;O8aj{UFhZ$84Xg4o8A|>U= z!L@QU@_y}Bs~W$L&-C$`7%%a+4dYnBbDh5sUk#ptO?Hmb&ZpS`hp~4U`1nle7i2v)9}kQ72TsaYtc=Ihj*x%8Wl<*17x)54#RX$W5FeV&l?hh0 zIm!MC=koQ7<)2EH1GA@+%K%B>>I^H-f$F7G3j%2%Z)D=4Wqv~jh%RM)tz$`*-BKr_ z`d)bz$d|1+Y4Rrc`o&lz!7`*H@ROIgJ`*Q{yq7mN*(79#mS=hEM{<~xVx%jTUkK8Y zEHw?Pbw~T&?oYmcG4^xd>+2W8;m{t%-}oWpwY>JjAK;_95IT=gyn=nNjP~w(U%!|` z9#m_=1D#suNbpCC)o8&egq8H@Hhtp#AJMJiDA9A!&6T$R&Wgu~_@;w!|9{Du4>urJ z0@@L~BtH50%nnG+FFuJi@$s1m2;{Sm&!mz?i+-Rzmw#XSFz|tn+xhyrUbs5J+S59@ ztPDZYgQ&y-Z{T#7{%i0EC5|D_5BnCp!r1$v2$}5uD96=7BQgH;LHvsobl|0YH_BIK z`z`EM@@CUKm;5M3(9NS~>}tactkAYcxHs(F#8--}3R9#QvGCs9j!% z)b{b2ijov$eEnh{pDDiU>lah?jNMKZnT7)sk-2bX_Kna8yxZ3=R@OdWzu0mhvENgS zxU4knWMrMe2VK$azJ4)#H0{-7n~`_EtYG9#ef?rI17E+GlixB)K8Q7DFG6%ujw}|a;5u=_nT7~s zJYk3+B2GwIztF5WDb@~;m}K^Kcz|VyJ|;UnI~%@7|KfXEisS$z(Fk(V#jI zgd5_|RfF~m^p$PJ3-W5~5B9+Ei{1|fhvT3>#X0Is48Kp#rOMa{8&t|5{Q)i|%0X5p zy8%`q&Va1$1izVw`IoY_vwr1k#?C+LSKh1DaQs%kLcGB0V_-}2@tHn8)5mA(TvR8o zg!bwr7+*tG9irAe*Jb4m#Uqw$Bska5o_y$3YeqThi67#cEX!Q`_)H(4>Ekm$Ch?h3 z_03p(rYlLRg2D?_OLW+nR2JbJRSLCAws}|E*HscN=3RZ?sFH2LRrmd`RI+W3eV8ca!u%<4cw@>`h^^>|CnBbz zL%bHM6?%n(DQ9YX*G0 z99k28p3?utOg5D&1^n7SwL)W3U5gI0a0P+eTySJx?=+Pr9z%KbzRIL2lPi6VNww*; zY){kh#@8m)8ly)Ji`RoMxSGJ@x?W&Q7$Vq^4al9s`-kki-QJFVU70UDx!172ViPrA{)MaE+>}4PS#$8I$FgG?-F)rlMam5z z8(N*XjU0Aw{P3`tg|_AfUPL9k*A*4k@NmGB0^TXfNUer^6^#Rp#P$KI zs;F3f`BI>-m`{ar=HSnyCOxdEhf``iG~afdwixdD7`0OFBM z4um-!7AYhh7j#cR9=kAg5`AW@X65T__CD%9zF>c25+ zVm!&bs<)9rL-C!;%bLbNK_QpQEJ}a8+vf)GxdD7`0G}Jc=LYb(0aV|?=LSHKtAc}7 zSw&VN$8%^}WNTw@U&D)mb3Lo$N=0@oAr-`Qn(u4!Fw(wP(x2xWdBU1-21_eRFV8hO z$!{W|abQscdI6_L#BJ6re-%j*vQ@4*OCb*e6?0HT8dgo7dhcXXV3ULf&_tW7qqmUZ zUm6`9agInPg<`fz4UP)&pLI?r%Zk93ZwERAR$ckV@dr;D`ao0QER*d7ng@T;AE{f5 z4;Hgv^wg8LQP>5^%G~*eF7+D?D$CfEQC40Fp+6#L zZJcp_u-^!0cufru&0Z>x{3ctumC;j_Nl+(n+W&CY#Cyp%Pjq-yzFmp?eeLG|d$pUP zI51cHordTR%JsFIIpl^~pqtSD5t^q`m%&B3B(zUq*MJwRBEvFPLt5?d6m}_4Gv`HC zBMeV5`GtkKCFG@~+REHa!VMRP@{(;;gpCcB5{<%5Xf zB-eZ|-&mg;z~=_|c;p5sMFIS=_{?@JYkwi4?Y7Rq%94VW!wAz46Q5afb)yH4&n&r0 z(YnpYXZrX|AD`*tGZk;(<1^cOv-t<33R7cKaZK_nHa!+S1Rcd9$v8o`I1cII?BjJJ z8pT^k&qjBl#*W21X_t?(&E6REg*}f=(@q80KcmN^?@MoHKVat(oMQx>197c=faPcd zKXo1Ti)RBn19{4#8KK6@OAK!@cKH_Nh&`l2o+;FPv?cD+$rE~%`IW_f%cCl+6(G(n%x76v%cJ^OP-pB9NLAb+aj9!1A^ zaq6>tOgJgT$AtKwHtQ)-9}O8+u$l_a)IjHaS9GcPIoU518*XuR#-A_*67u#$WdoaMuyyrDYJc5#mpUqs@yYu1w$wM1mL z?g!3I=mxnX6On9A$ekjRu7L%fcv5+Fa_wt3_ZQdc<1>AHW`BC!;%@WtnLa*~GXu^S zs$oneA}@QDyxabw{l|Z<1>|6 zE{j=0ZXBFrW99YnnLa*~To_P?YBT%zOln(UkMr@Fa0fI?Vol^Xiq9w~a4JUZ<1>lk zY2ptTM(z0cOimH8sLE4Mr-kIa#5Sr3BOjm1=_WR4yrs#Ahs|H7mnEzc?6pbO#6E?S zOKkZ*J`;MvqgeSBt>7!5YG+S}t1i8F_f&-C$` z#IokudGL$VqXs+I@SK?4tn+&%?o}bJHH#O+aEcL{^EmkvDOMLhIHJ@*Ci0o1le)M# zbWomGoSSmu)2sQp%HNf!Uu-B;Nw%CSHQN;`U!aRmC3{imOB=?g%FZ3uOlCwrKGVl% z*74)?@tNK9=i@Vde5Q}jtm1`=ZlgF1&T+_~z%?hHTq{z#Ok4*M&)BVfeCEd_J~Jw( z+#R23)lyx-%@LWDD~dWIm&!ynQ)3UfinWTC966?xipp~Gxt!`Z1hnKzbn1zn4ylZ9 z1cY4bUph6K;+|1co+ekNHEJr-)Ne^vYYhWLhm0CNfTPVSD#sr|!a? zR!TH_e9#SEz&(~TGmY(X6!?2~GRGq2E1G`7;wZLx1$@bSXb zuQjv?@%O{TN0&QEd4R!p&~I$;?K--xnYUrk@u@SoyR7wG41@2is@siyQOWCa#PA#< z1&;Vys1~#cP$jP^=o~Vn#e--nc}>OOu*(+ZB}-lxvXx3M2%6-`q}qC+P7bAXwNcH~ z(?O}xsKkq`+pI$HsR}ez$CFi{3qiJ3m^oPm+TalO&`|}N7|Wh~CRqi#NOkBSYVTY| zgI(Cmg0H=SSPzM1%pcWAv;f8@uB}!{AW^^j09lj2LPPKfxP_LI8^=i4+tT)LvM8IpAbk4b;+NkB?3@mUY zoMhtRyvd@~HE*(nMb~cLWC(RFHgCt#7aJL2_=sLGz`RLp7Nj%HYEZ3HETUK^L0GNX z%%vOYe7;($#4E?!YPK;Z-(N9x4*LcoBgcTor4S9Rs_E#yJrvWls_Cf1?e<)`r$C`` zRb20)BPT@{LI8xgA^Ie9K`WR)R6^_sd+3A; z<)(CUCnh*DuU3(Q@JFJ(?{x&uI^6Xj!2=}&U&+{E4i6jw*Zm>Ww})QpS;y@u!UAXN!+cGma$=K&Ry(T`d>V%^LPf z7M@6bk_!{f4~b@S^>U?K!7623Dj3asMDL`jnup`FI~+cB#dDx z=)P>kF1v9&qt2PW^xyK^0Lre5nF%m4PV#JyG5Hessge%O4*3D6esITD+ z$yV<^xThjaNfjfM;DdWS0(GhwftL@_46#K={bXG~S@-sA1gT=y^^=R>TgB;xQ3XN)`nYS2k`?c*)UEtCR=pn&jpSC6*pQP5u-PX+tqjioy(8-26%?0fJ z!$prhV0h!+ups7wkZQyEF2dfPbe|u3-3+tcnETsWl(LQkg}NZls=QRXVM`~9@SGOu zL=m!sa&WN5q^VOVYTB_DcT$Bl1Z05>AxgOJrs2bFP4c+q|o7!6D zJ*BM7k}zH#Q5uf6@OVE(BXPiRz-a*^@4&N|roY{&UANqB7r1|4WeNkxhdc!mpH`hZ zeR3l;1fg(=nYi1!Ib1#zv9V<`DdZm5UDy>$qxfwOKhXY*`5weW866@;cGsQ&qd2Mu z?LoUelWDbg&7uP-7JTZR%son2hZS_YNt1d}h9>k*8a|<>Kb5}e8h^x>ld0|Ys2j)K;tQ> z%sa`X%PL|^a%c5S*$PrdGd9=}38Qt8t&nVvB}f;=e6q~O(o+$ZMe1mk4aC)xumZkR zJ9)D6l$)^Bt_tZ_vKo@XGJ-Ns=X5wvk}8!)q|gQlrUJ&SvOg+sLkeHmHjbh?bSF72 z1BWjL%pXS&C0SAW8>LktJqT$#ID+R`UnMQ*for%aX`(q>RT>NX-r&ixKYuM9mPAB~rf ztf$H|;+9};2c(GsLV!Wnj5Qs7ue39`zVc+~cA#S*mv&cazF2>aM|c6gDBZ5y801pR zd|5Z82Vx$KpS;}s)q9jSB`VJz;RE*?-jIeZyk8F6=AMu|jt<95N@exKSV=!byqU%< zcLsZ|4c8DJq*IBdcLSrDe}uQf1*IWtr9I<1D;ltLM(wm^Dsv9z0lXTpV;nZ0He3{_ z5T?N*(Rr9g7#y&1DR3dWpK((H1#Bb@jOYW*1B0}_Md6KMHHD|bw8)@1urYTKxqCFg zOru*RG1v1vr)vVMbRXk}?jko5BtQ5Vy+2$h!G+$02TJSa(i^LP28&P*BWrjZ!4t@6os((cl;RD} z1ysOTrG95cnyf2yOA7d=rEvoc{f2ETkVJ{z!nAKn>SY|z(!Yu8!J#b8oE5Ey9uy?s zuzAAAED0Uq72tFec4Y~INZE^Big$-8sB20YFV01ZCZ$1WvZgRIIGG@?wATDm;FHp0 z@mvi`oMtlIi!FhXOlh0V3d}KRQvQ3!H-fqd`EH$(*BbU z3M?XVKclr$q4vB|Jc&$6`VXwC;?subLr-E-L zkCf1m6%u?yeke)6kGK@0FnAgc&{yY=;GCrt0Rt}~)0hJ5&mGndzXcx1hz7^vc}gFk z)&on%!su}43h4!Uo^M(yjnC*vbfGlq;-i#9uzr=ooc_TsdtUq@f|GL_gTCSI7S^oQ z2HuiFEqu|s(*}LP=9ESVtbq56n;>W5NewF^k8EzuIUG5736?5RqWu6L2NSIW^4whm zb@3P#o(%7xlmgox(z+Y&q5&`mq%(afeWayJH@PLgu5}YtLx+qBxUnWhiY}y~OQpk7 z5sDfR1>2zrg;R=Oi~d;z=7v0yYyf|R;W=Fi`REyU9%fsmpSqr8wJhjEW6*LlMlcGf zAk(;%8z&I8@qp9x7pQ<1pbVs55DJ2m9R#MbWvo2!kfftC`UkIAb?)PbLLKlUf=Egg z5>;lv8gT-OBdZ5di<8O=u~|GisJZ)DB!(C7`NOTWCZ;tj0YV_)H#NqM}~(t@lwVgkmCW zT2&Av1N7+y{3V?W&(kZMM0!O7uNwNbJ_Nln6WRG37w0Rk#E$G90$N&El_ zO)(OlAx$AeW#B+voK;#OIK_zN+Qmu&X{9n{bm%aQiFIO4=$Vow$Vq-H$sV)>e?mHO zC$=--MwJp3N<+8HQ_hYT5YQ@#OAj^t0YZfXe0p6o8i(23-Kvre-ilo@#S+D{Qrl5P)M2YH}ngQd) z9?vQxL$p3XiZ$X+^lP39%cD^9#i$HzlkW`QL?dt<)8KIOksga=18%Ih;08siz;}}w zqZI&?GTY98-{MwEyDFVpMPSfXX3E^-ho-#5i|rjQ5G1Bw#%9$R^TNy-a@KHtrpY@4TFcwpSMU7 zu`()(!*mTB#3g_P*a_7$GD&x|KqZ*1vHIdNl;Ht8wbOtj+vwp$f*TzJEB=a`0UIQr zU=3w5L-7KQfc4>%VF7EeT?4wg@fGn{#e~ug`Wuo&ItmLE_=4mBCaj?C)Szyy z8nD6TStW~9s8z*`kaHR*8YG+r^##7Fl_CWaxUQK8EREzCYc1x07RM}wg+P`Oh*OE< zaSb&HAHYHG)T#qZX2M%3Gz84yAL3==G~z123%SjV#4)A85x5NWv3=L^ynsokj^H)!z6$WD9wLqR*Bcj-A^)5^8kLu zqM)sGm8?5I>P6Zd+?+p>u9T#NsWc18ed_~X_S=9BMW&#+nU`2ZxX)F{Ti=-Mh2H-3!jNYQX z0TLPc!*ZK$jtl^GY%fAT+zVHMCgEgyR~5QomtvX`2q>I~(SsMFWuPbt$cTZO++fj0 zpkC3MG-Ana?LGv3B?pv@q(|WuBh?;5`Yzglo|9$^Z-FyHo$zMH0#B6|j3g4Df#Yca z{1)$rkMMR54uEJ=x)yh0r-E2yagbgF4H<^Xiw}tdUaJ=!0_(69K5DxLI6O2b$nh3N z3tt4+k>RX4vt+v3Jt_UVBqIA~K4}-tVTJD3Z`MuPBQS*0u^kB$Md@IdxD0RRH{4tL zowz&9NlOc-=33v=FKev5otOuFofcT3x`no+wX>dLH_(hkCX~t((ndbfbHc#1@TO1U;G&TiYKLi3MY_J^p%E=kkrs=)ka!n1}ai6xTBkZy*0Qmqk?*& z03+mGab@!hcy%Ss4S5zr_iLN7Gsv( z1G_MoryT{njWy#ptBw``eQGZ)`AH!>(FgC;4g)x9rwYT6?kJ579h331kKnBgRG7#Z z#3f}xK%0|1XFOOPfD1Rk6~!SW6Zn>UvB%r}%mb~0e|#e@Bgw^`;!xts^c?PP865CY zf3jl&6UYzJ1f{W516M{5FOd{c;$_>l@K^n@KY>H?6u64U4<)K>fk{kYgakn!kTxZb z$^6AI86&o7cr8GnFYSvM35c)xLOn=O>El{Y6*)xjgf}7UbSE4bjDZ3wU=_m^H%5|5 z!)Nb?j1YC86X|KB8mkGcP1H#Sz!}87wF}aip-3=JdZBh(C@VliwihfGV6N?~V0F@& zfGF@n?*-WrE$}r?`$V;T5Bwy(cwbt34J09xfx1-CkrifH1OfPz@CqCSOAXh+a6Xd`5bEF+z)tc- z+L$D~G!}FmNn)@S>7y0kjbJT)pW^ZwC{lyI6&}Z4fi+=xKutS3-m87Fv^W)(VD`4} z;5+yeb49~P7{Y&{T4@<@B34IxAW3P71V+c&TX{`Z46Rsqvlf6AEaRXTNQlN&l2X=K zVLK9pJGDjQX($4?*k|S_f)h`LrnS3_Y!{3H4V`tU10nQGGzk9VIi5vYVBKc{;Yr{i z#0R_~Krsi6gSR6q_!gXHf!W6+Y1x6mw~TVZ7bTX_83xv@e5pn`dM{hiqG$RBQKTWzu2Uy&gNugy4ejS8 zP1F~%3RcPpc`KR)eP}1FWTw1;ZVb3!IrpMR0(1Jn2gRP3T|t=7)EO)kz(_eRLl&^- z6OBk4r6-+FFb3Kz1v^y3aD>H@a|}%TS;FMFpLftsuiz?rt~i8n8q7jzFpQG!gC+## z$^y)YfUPAPfu7NmH7n$*b|>&__?+Y(*Jv)<3!=Ax^>BE=r+3&H6p*55I#L2YqSXMV zX1!C}VF8pAPo3NKIyQaX9Vkehuw>jOo>ec^bTFJo5)4 z4!n{vv6~QHfirr}C;;3u9f9RaL+{do@P*EfNnE z4)B=vg*wSZ*OvS@{fd$?nvd;0(9Fy=17QS?W7%O3F3QI;12kmwmNg7bNP7?7!JN5Y zS_DH!a}cZ3>5w!XSrlXoh7Zt+gt48L_-nKWM>9~-8!(;Dz*f~-(oiW+c0cCbR|yYOe80}r+LMDA%x(9MxStcUPPGKMk3?!YT) zf$TrwOnRqyl6V}&J^_NX1cWnlmlg_lrVSOxqQT$5bKl?mNIC)(2Ulnhsr3o99L(ixj16`WGC5Xgf)w5n8x z_?hk6_{;(@K4hO!G!g);Vu*SR_@R>oc%O7WerwN+jghrOgQah<1MCu|!B60Mf|hU% z4FIZ@o(c~I7J?u03tG{b`GsT<&u@TxDu0azqVWNJlVp6BRZ-RjaJz`kfmR~z4!WZq zt;zTd@MQ7iH6ZOn?3P}^CU`b51GEs5EHfx#w#N}a;vK_yavmRRsgC;tLJ;^5yC=IHt>X^g}PYq znU^?09lVnrSGF5)07_GeUF5bPt38!wXVAgQ!wSn{Af70BD~yq50T0lQMY=iM97zdR zr)RLVVl)VXa3$*{S_*3|4ICYZ8PW-36My8lI2UjN*MI=%Yx^x28x$<&jAXL)rRWYE z2~A5D7UY27s+o%eX+H-CU<4s<0#dMxb(bC_CEO$!R%+HV)?-(>0;t2$O>_0dSNfhoRKArTzW1RWpR2&Alnj@ zCP~IR$s(;&L@Zi-tNzfbM1ep;TAg7dcr1P?sm?hi|B3ry7XxGrLXs22rhDGYZyT{r z5|Hk^6L2C*oiC1x6f<%XE^d{}5nLCzi zQ4j6xELc&9mn$wM`GA1tDMSSC1Zi^{9vb0JxE+`P6nL5Sz<1oo zOT`0)`GN*BfO9h!Y2BQm&>yR9nrKMZ+J`VJt)J~b#lPUJj7k~~Lt%!p0g5_gSHu1; zSxgJG1Wh4fjE~8FzxKM}x*&dSr87wZn*ikH(nSJI43BA7&B!azqh zRkT(nES&%?0li0hD*7tppasnlN{8R-CfWX(C*Mfx$HFN)Em8#Cj+FyWrWJCVxQr~6 zwwAJI%D2EU!?YmjmpYe}-U_zR3tXPTvzrnp0pDevmdubHlD#ftLUUnWP$v+S-lQFr zq!!vEYXrr@J6IOUdL%R}${Uy*l0@f!k_alwF5EyS01rmM5Bf$oBr&N8sR!o?&(rR(c7pbm6^s@5Lvh4mFkNh{{WgYn7AB&!@Y z6fhb5Wf!kA0JJ!f3c3Rl2e}|SF`Ne-3yp)j*aL_qgK^@alG|_vaSN82RYIa@kBFuY zl^8=7cLzpxHp>2%Jrn=Xih(M2ZYH34)##F*n4ivbG;(D48RGz=1x|O z$-@n`HxZ1r-ZTME@d1E>CN12B=Kx3H5BMR2IuWsL|q$Wz*} z(KB2gIEfF)))F#vK@WTaHnORTAuFvT^MvK=oKO2w@g-UHB|~JxXJ-erKpo=|I?Lo? zS#ykUqld6!8kZjPCf-Ccpz|Q{j~qk zIfC#~^Wsi0QXXQGxLPgQuHmLaPaweBN=HB?Vm^R~MWlCj(Xuz_6W@rtFk;aGxU2Ud z`GJ=bNQv+1FBrgEkO1n#-px#C2h;eTg#$v;eUJgn5e)!XqTPW(SVn*u@1sXq{|#Ot zUO=}xC6+ZzC$&}KKiD7}nMNZxYS+i9H>+OS$c77^|lFlh*Ac%m`pCK&5{T$X=XC`G~IRkCrg zI=U90hJo_QcqF`;Yn|F-2hyDpcVRu?+;9|KLmi=%;z@V`dljCS7Xt7DHt=QPvPp2R zxCX1iZSg6Pl`#MVFikcVxU{a-ANa_%#%%jfpsnwrZKNGAhk|u&6odXFNk@O|PxL)h zCE2KJ@i2B^Tno!28#znkt=hjJ9YlBHtKgYzc+$C`EV~vj)9Pw>guc(Hg7fEqx5mV^=o6hx*T$a(4|!Q}t^UvsbduBHT6zF#o1TZpWRH}ds_|>Z zm8_We!M(WFU92e_4s8Z`A>1=brqeT|8*S3%WaEW9(kE&qwXLbi#+Oz)rdloVz;@VwGS7oVNpza+rmU@ zTWpdW;s?iSxX`KS&UQ$LkJSM0WYdubFWi&n%q|z+qkXEZ>hjF;u^MOu@O5^cNK>8V zuyb-UOtxySeXNF$)j)?ro@n=kRs(*p!H}kfmL)r!kJUh{M%K++0$Y{#uhQ{lq0p{Gr`p&B*!}TWb~WkxvPG~nLJMXez+bS-p4T1# z-CHp&;a)&z%J>|>xwV~;v5lQJy@Q2_K2D|3D{b!}uNitlZ`00@bz$F(wuluLtxz@v zY1ZKn${8!>V>NuNhCGkiS;>A4uCPCqb&it~3`pqs!WOnR!cS~yK32oWYWP?U>}%{X zIICl~>|-^2tOhcf|8*|mV>LJ-(0Ky@;jo-@Bb|@=SPdVmA&WU12DDzCEBaUso!t3Y zjnK>3$7=Xk4IiuFV>LL{<*ZHKGCG^}u^Mc_IpFZI8k}3AGT=49!H$pB@Ua>`R>Q|? z_*e}et08Yz9f|l@4IiuFV>P4~_*e}etKnlce5{6#)$p+zK32oWYWP?UAFJVGHGHhb z$1hgHil?|hh|bDE`BbZbOh?h-AtMSYS6Zbzn!{RMM+;O@$m+4^iE*h#sVHQ1LZZhP ztxSrQ5OG7^wJ~Y@)B`D`rE)$KCQhX^+mI0xD^nZ88@1j#^%p!m5+ieu`{|crFU9it)ap=g{Ux&061l8U!!u!3WqPT`Pk*g zZZbAKh{8l$?7Y`<``k9>?v-}@!p^&YVUtaF`_rDA+_Cf>Yt7hejm_6sveI|iH|LDM z?1FFZvdyT2Q-jw2_R5E>(3n(@I?_i?DJn4HX1CyFg9@&iPEO!^ZKv7R}ah60KXhA7oP5fLr$dZGIkX3kP>4HMP zRhSBnz&>2ao*~@ALcvxvd?*qZR!shbs>+AU?xwcWAsH=k2fem2!4}@#_|b5WVw)D* z+#{-XX+e}99rq{-6&3}2!WRdX2z(L0=%$dO!&0H(M+3g79H?qNx4~EHAnT71e4PXq z&5oLI184>U`ShRgkc)QRj4koHT1hBVVSCq7v@`{<#6-+bb;PrOh%=BA_X zyyy5ezqH+juMIo?d($ibdgq2?Z#wMy)R#{?>;1!)KjwRX8b9=o&t34db(VZ+tM3gx zXY{OBwzxQb^EXz$e5u>Uy!VOv)=!@}bJ-c^ys_5Uo7=h9ce|v*|>CmVtT2YD#QVMdxq@IzSTiuXtN7ayH zR9JG1ja^t5%7px z6ct;CY^Uw>BuTc;Le2{yqb@z`uzknezS{4enZ4nL8(uSXlhMO^fB4#Fzdd379ao)r z+=jD8Rd-!({jW{k^Vn<8U;mq{?zifmQ@?P`Nhj}n#TQQg?U3hx_`^$1S!S)xdMB;? z%G@{adiTjY-@Nm;Hw07uaZYah#--e#i*K2{(HRrRe(IRD550_R1;>0b2#()v`jWQ| zUF*OlHa}*{^z6)|cinZvV;XPm_R4{8PhK%K{GY%2T;<@g?`?bfmItJsT;iDD4}CE| z=hU;7dOJPnmz(dg{+-JlbKiX3omc;7%JmaAC|>`_sh?iw`zx-!{O6XPSwCaqOKb1&mcKhs` zkKFX+O^>|y_|8wXY!`DCUxJN#}|0*{f`^hP%-EvA_@an9;?|bHu zbJu+L=54+|WY#jbZWp|E{6D7rWY!5&Pkws8<=>mN{MqA z^gq?}hJE_PyHCt~_4Mm+dMka~;z=J-`t z`2F(RK5^?E8(zP}ao-qr!-z93eeliW?p$rVuWhnk>DWis`{OBVtuyzm6-#TKdGk4c z8+yiC+dsAJEhik@?7Mm97e7_|-62CaTyE*@j``G6S6q1P_fr3J(w0Z8_4UfZ7k_@~ z$;YlzAGE|3@2WZUYTVd!rXCA%Q>4U#@?NZO3 z{MgoiIBAPlK6%7#OME)D)^$(MT4wT}uK309S5`msq!AC_{MhvL0~7b1Ip_T|uH5Uy z##3MITRAv&?P1s5^7`a;A6fCVO`{wa; z_CNo%XZC%1gWNiIK7HD6#=Q8`P4+tb`H|PYbJ)_G6*ueIVDFQ5z4FEbdro*_=e2I% z`)@nWKH;xZ9(@16)0TVXg1vu#L*dG&A6)N}&uw-5{=fUu6(fFk%kI_P9&Ef&zH!_C zoVdvY8`pk6?vm=$SN-jh;Tvpu^LxJ>HSDbo_ui*@!1%{&@4tTHs2?a)siv;9v7-972FJ1)8RZ*NZ7V$d_2Pw$)X*sL#}bNK0lK6UX;2b|oK zUi!jUZurCC@{;@fZJGZJO)t0XJ*jmzobg=xu3^92<0oG&{_nD@%~*fbl6%j1{rt~N zTxP4^O}b*GwU_?Z`OBQM)CyqSm^v6&C(QTVPwdB1s zAK77@4Ze8WxTjV;`GToq|EDnY3wOVI@m9~DHsbtOpUvl&I`{b1&fn#p?;g0!m%jDV z4r|>#=Gl#2+xX$*=L~x{bIhi-E&jB|)i*zK<j|_h4uj#Fqc;@d{zp~8Xn|@{H%eSocO75*`|H(Xd;?bYa z?D)=xm8{(H(vGhJ9gS?r;$63|9kI6XTNsrt{Y!{+w6Mjt+~5wJpPPl z&e-Mlvu}U*_Di4I{a<7Hwmp8>JwJJH^lvu3XO*6f&Kf=Zp&cLECD`fu5zk$CetO^W|61bd zf8TN1cenjZ;o0JAL*E(ut8?a@G3S)WcD`Ykqi_9cdW$uVI(3y1J++N~H2cu+zk1TE zJ01Vi?C!_^=%#)4yLj|Z?mfS6x2Lb&Y_I#jeE$K{es$2X2VMXCdiQVsv)>*xwy@&0 zLkpKZyyUf;UHhYhesTXh_kZvC!O!3GeDM5Fp8xuNLoUAI?(+}a_rRqOex)$`pcn2g zU;65qGc&t=@7P_}nX>%VCrqFH(4{*)aLs*B9oRf*_JJ?`<%PRn_{+U__dT%leRurz zXZLP-+5LaGY2Veh7`*w}*Pc9W*0fVz-(l|1xd)6p|F^?_{grpW|HjC3Gg}_D<&x)C z&z-krY1*7;ubMVv+SOD4`fTy}ZSK4I^@*=-{M?-n%>ChEdtZLossC8x^2~2Cf2a-q z_VpK?_WayuZ+-r*`yapH=>2YZ{C9V!?i*UY;-0@Uzy)2y=nfT-hbVF zX7l+;-=FlWKVNg_HFy2#wk__Mc;0^JKJ>*YKfC_d*WZ2QjOo9Ad*q6{oU-95jn%%j z+K?@#Z4t~~X7;q%D_ye2-gEZ8`jVerI%(hUT>9vxS6@;ZcfhsV9rERg#})S~?lW=E zar=xr{l(_oKVSLdE7z(0q4x7zAG_{HbKjo(^h5VQ^u={<{p{+&Ws|@BT7fN|H~I|`R#r4 zezWZD8~mk-y(z?1-&rUNiH|5yg=g?D_XS5B||QyXJqe$$1a1 z@u?pM_Z;`bKOTSO;4A-Ko%`hf9-4dNjo*Lcq?JxOVAZ3y zy>YYc&bxc|#osR%t}MP<8$D|Dexv{T)T`flW{){%UUJqJJ5SpAf*)^q;S0My_k+)D z-I#agOF!)ybIOH}eZLue;pm5tf9{CCTz%2HKe%Aa8CyL$YO_rXn@-!~`F$qkfARgf zKe%P5xo19b>xxeu^0(hU@$RdCef8^0KlaV+jsKqd@@rSUw)&cnt$A?nh~wtm)K|HD zlTl+gJ>olS&c43z@7mr|AGqw28(;YG3-f}VgP#T?|M1x-ws`e>-#a1q@9|&TdE!Pp zoN?KH=Ux54UC)1K#EysEvGPOt{l;9j@z(VNW{tEV3yysK z$V1yE#0#j|=| z_{u83I`GZv!%tlDhoe_{c=g$<&N<+WYkoZar#Ifa!!AE~?=$c0vj6EjpMK4=Z)7iT zj63VeKko3yt@bWo`rt30_n_>&^3^V%bL}mM-T6}X^{c*l)kO#Y=z-!TwaYjE%9}^cJN5O|?>P65znFCXA+vg) z{n<&E9Qg2o*B$!$Eth}yraR8wYl*$yEj`!!r7NF%viaUW4j%WP>+b6>*dtTx6OU^l8vvta9-)n#}7K}`4_i%=dV-t=n;j{o7xedc%W<-~RgOn;U2SapZfG-+pq(6AvDF^Qynt ze7AkRcv)->*a}GpBx&EELJO236K4X_Dm6lrflx?3o>fw10 z&71P>Sw9|f*Rsu{e{$rdZ#Cy#`u5j<|K_qYf*CVDbJ>|cFR%F3S7u&#(NeE`ZS_H? z)(8FXK7W6^a>w0|?s~zopM3Q5>us>@X6wDT`bv*Je$wE>PZ;$2D?6-x^HSrMC=dGi zGMBuv^Aaa*aQJW59eVCLBQ`s8)d8%6EZ;>WPz(SO%5tnDFVySl7Dx&N4*+?VHf zupWADbg!sz&9CUk+!v$l4%Rz(lDDRKYl^p~cx#Hcrg&?Lx2AY&inpeCYl^p~cx%eX z%$m~a+8a4;`<;v5x%i!n-?{jmi{H8Uor~YO_??U2x%i!n-?@C8b}onaHJU3BZkSsk zola+q6w+4gq5exvO1UC$fFY&GLwwbN?QhCyj(6NQIkHW(o(YsM z!hJ*9lGZon459q(Z*tl8H`y`=*sW*Co8fw+WH(N^t#9bM^-VF|F*Yhow!bMU`GtEX zQ|_2hnR4*$uT>$nP|UFzbbUo_B^A-`YRjfGL9#8IjX#@B=h7WB zwf<<+5p}FT3XLb*f)1ER&lYm=F=r{m8DHXTno?hh{z}>8n5ku+xUXxiqxY8c$^L>& za(sb;^tO^UXmC~v6#Vas{6fE zo6pAS1J0C~uWB5}@t{~t@=hL2$6-KQVvT5108SSSiqu$6%(s-uChyB6WEr&{l|wMr zUzgEcB;Z#nEB~VY zYw`PH35Jkpqe4@%Ejc$5=*4N3!l?;*E0$s8@&3vbq))V!iwW41*DW!=a**JOpi4ql z6oY>7KFB6yV=^=aUY)jA^l|d6SnKmVR>+Ua?>C}<= zE?X>fj1Y%uIhRf7O|&KSCKCT8bVAw^{5hZsS^OOV%5q$mQRO{B=K&f=90us~$#=l7 zlKp`&F&G5sK5<&5Eun8ngeLFHCe{rMO30p|luO7D+LGfdXHU+pOr_Cyf90IA2z9>~*_ap$IU$p6rA(ZTNE+GCSo*=M zAe-R3aHiy#DZ`$CL6A%E-5@9@_&se29X$vVIyD$X3dwF5(3X$uyCt?b%1PJ>E82qi znAH}a8yK03%QD*XnM8jqd3-FHOgf0)NA-Cnn;crR@VPVMdw zr7%WsrA&cQC+^E+V{62HWED-^m)MhVUoJToiWkLjjr;OQ-uU?P@%;k#72-TcZSnmt zZD?$X{t`MDl%JI=co$sQVaIs{_vPcfSZ#4WS;_;`_ms^zV%p_rj zdX<0ydx$tpvHB$V5N&e*?pmWDgJzO!i}OUR7HrJA?~^4XjtAv*DPhY{TYP+Ki|d`V zW#f9g+TwIhTVmg>wzw`pTRv_BP+Od@(ME^yxg})^_a*dG+7f!5+Tu13Hk=6_Mq4Is zt5REhJ=B(557dLidZ6&e_z=(L;yi-3gbk&PRW!bTD`(N4WAs5=LT;%oF5{&k#pMxg z7{p?5rY$a$xi7I#QCk8Ja$sP5d{`NXBI?3%HlZl#iME6c;69i{;y!jh$+iScbJ)I; z_m$)H%R9<(8BZHKf%rRuY+N>?#F7y|abGUZU$~Eq!=x09lOU`&zZd4&$D| zl5gZ;i3wQ2*AlSGrQIYN%DibC)`c_aB=9mdbkSt&3P>KTd_D)R!tJG*qIo|9|v5vem zydIt!m)bg0YgXzt_My$7ny%!V<$AGN=g_WPg#l(7wQW)hbPs9NDYDV{T8i`Z94G2r z?mN2C95eNhRED$GEHenRY)_e5>6>a!3|UO~h@%xY+GvM8My$}4NIR^}GpaIPH2_9c z`jpZi(A%&(3LTie$r`Qd3x$MCYqFNz8ubYkt2k7aMbO%|@1?@Pq^VOE#~ zXV%c~;BvA}Ed1!)OWLDzsNOj`w`Q%;nWd&{bW#}x7@cAF!YwQ|JNMpbbh6k*Z)uH= zHn-UA(Z%SMd)(sp3p+U^Ei~*33A8KpgLZ`^+GRM)ie?clZDC-%%ga2vuuE(9CR-4< zwD^t0fyXA^Eq4oenRIJ3(eUz8E9Sqpd1#ANQmKE-dsgcSe1-27?@R zaOr@RlmmNr1S?z_;)7|)8#~?B&7GwRa9&#I;X!VWk2>gKR*w&uyBuIT2ke+!1U>fF z@d0-Z4{trUy5sQL3n#~i@cn!UUn_?`>xK^@5%3|z{FFJGFl|0WH``Ho*b*Xs$_!T4 z5&@Z(=w11UT4;%0!JavM7n|7VLxfGKe1r>ph=!5Hpda3Z3Kq4oX^ftee~P_U2Ey=z z>pS9&DLQa(iJ+b`HyPaxdApuuRk04F8ipTSmy!OELZ^$qlDcg7NC&l!TJX!-xqkSi z17PbV5lyF@mcj?urJJRxzk0l*j!vlPNH|av%__C=J*Pe4yKR%CC(JTwAGCE0Ke&6` z5CCE{WV6fLO{R_MdF`CTmS|MwpJ^=)VNmVeO%74a_!-_50UGDb2rROB=&NkR(a~ld zqOs|SzH?Y$y8*k~VL`Tq1={$AU%7WU9Kg2TYI3iYDah`YO)dNo%_tiQNR~c=k@@%7 zJu)uZ2P+AvQM;7~YOW)<^!QdpcbBzMvgh3Lni()`aa}qXcde?v3|AE+dW2GZas$JS zHte|Lp-_ZI4;5OYDm$#r;G%*AmST2j{3o+fwVt z@h`kXT#M_+R40wl;d0U8GA_8$&5n%~O_9;fj-M5^`?ps(RPIRnD$v*I{=QkL~TNQ-78_!lk5TjIP4nLWs>x4-M>&xv%?YiZT zl_>|Nh6IgjwNyt>Yc=4! zBm8U4TZR~0|JHi9v@Kij2s;<&dPjIZ@OoFuwE~Am=}InJ$=0jcYOdC(W8%$J3x#Z* z0#DuZ7FG^C@7Q`Ph@gAkEltnn9Z}rkoOeXK1J1iq&z2k2dal~2*NV+ru2HX)Y6VP& zRSr^X*-CugPD0F=z9T--TJMNmbl`a;3nM&_xX5Ci_X5y&E?;Q`nOZiel`GXcibgey z*GLiX_)IAc&d2Ai)f{lWTZO|TzSi0axA45R6SnnE?twovUu$id2cYjdrwrL_qt<9P z8ihJG?q*PK2DN-0FM&WOOfftc*1CC*fCIxmTU&)44xc)5%v4o8qtTIdt0rdIZ5FzF zw0)3z9G7xxu+BFnQJkQ4a@K7QoPrjav{G!x!w1;-x(|_z%{WC5j}!M*BrWmpidv$r zSSsU$ExJM^B{NRiW}LLeeC{5Lq^+F`XuM{)4nMeYIBDy)klf=QchWZFge+FnsAnf+ zGfuXe8;AAmw8KnK>&I!38K;+F#t$Fw6yFAraB;!{ zsM>w9PW}G$p>G7OO3^rev_4OoHgRHXEL!KVhjONe;2Zv9XP66m=*>PC@*VAk zTo1m_*4{NEC&#F{-FjJnpSm9E6axFSAMDzWN9u-y7Qh(UJGK|N3>Vde3l@LCF&omN z0u;=ONuY4(-Wq2=KyhTt_6+DbsGwB#WT75Gs1qu9EA?<*n1G6DBq413gGIZo6Dp>- zxarOxDyEFM+hS0O_;)wsFwzPKBdCDTNvI5rDyDjbJ#<0^AA}w`KE|N}>GqU^exR}_ zxYA{>8~`es`h1`gDHJiNbmIy~xeE=I{&5BV(8Jaz36)~82d{B!?_74{Y9Vmjc+uad zu7|_{-E`*x7W%o;Kxhh%ll zRXqk;A;7J&sA!nAu>1bQ)DOFhhYw-hO1 zt=*DCqP8_8vaUwErPk5@;05Z)N-|EqnK&d8Wk*s z-AHeE_xivkr#74yZX$PC)TC%JnWyJ_u0TStnv z3r!v-e&}6j>*#TSU1+(^o}!t}<3L+(){9O2;VSr)q;u60Cqe0244>q{)KU}rwJJs? z-qyS$OK*qOAu{+ zFi1Cs3=scUXWe&31yX5PKPlp0+=sAdT0+d?L&!q-5OTE?K``OXolH|c%+ZVB&HUfL z<-j~*+zdaI<-k0FTu%;Gj@xy?%4uBt?omC+&KwY6HUasw_14BTFlt#We?Q=BD>8&q zhO-V@XMseQyQc6RslZus%&(aKXo(){0+!?BSQ`camK}GwDSjwm*?F1kDFRl@A)ymk zkwvT%GS2^?8?jp6LE|uExC6q@IoipbUikrYbO_oOX^Ut|GSL^l(Dr=&?S!$V44`zF zunc?X*z@6v+A;%l8cn2&!&7X!|C8_)e}T!M{<_*;M{0>D8}?24!^_C zg>=jC)-^%#jfIAaV@d<#idog?1C>Z?=*AT@zqI_D!Npj?qnt60xB$jDG3 z0IE`~an9VxH*mqnWG#h;b`)36pEJdJp;)XG^F)LK^bmW{Dch!1Fayl#pQ}57N^EJ3_%jQGUPL$gkYK(Z%7lX&a4?7>qKx$jgjIN!gCq=$rqc9 z1XWsav*&A!lztd3@G4XkKEZp6#VYxO^fry0_ebO9BkNhGo&78xCF+^OK<>@!nlYrK z?`sBMW@yjL>wwKyY zH30t%AGp`>ror=ee@W|G&R2LJ;}%?OoCM2&0UB>3?1!<^4=EErJ(W|{{$f?t>*--dGu_hm2-;MIT~T4(;rQMBiJRqwy;vt!7C1vUz?6Q8h%ns7yMwhL;RSfdxHMkTk?0E?FuEnI z3Hr40>N=ViCZd=t6VC^mIJ{WD;ENDQBhZX^82vEHJ0 za7u6%r9eF3R}7xve3>_VfboRuitt@v{KqqtaT7VAbpSsIh2xzOJb{eX8HyJiL%ajC zh{1qb;aorkjLlZ%#-VHmV0#WeBAVnocun27jjlR>;B?Tscn*EimvKP-rKLi9MwD+? z1SeX1!{*7GLGz|Kk$4(Bg;xMew(-EUUaI6da?aPFgbetYg%&c53a&)VTAAmVnPAUj zsLHfgh)l`rIffcOUtzZ32YgMiCc{>~Zg{{hiu*_rL;r9+aDq_~EXMjo@T<{R8cY!n ztid@o&J2*DE9R%?8}KTTX%j4NYD|Xr!Uuf^XSa0#Pc%2y)A$$97(Zi-@D#9@+!7&O zLP@|&{b}_y9=XJK$%O{+KwaRi`D~Ntz;8y92g~cqPNVgPqQS>f+RX<#6Mm<;lYIsF z2}cvBg?DRaO;!bNt+mm-pjcLtkSzQVSr3*aY-jeg+kJK22L~crkv<937raa!Au*u6 zK=2Luq4+32;!=#l;Ms{a?z7;W#T$z?mRA;^z)p=w?Zg0a)(vBFcVo5#mW+kb)xedI zKQtnEaW7+*oWS$Y_@n4Nc`!xrHf$1Y!XF|yMek5)qDLu_xJoqBxKH zRHD{z;6--l82$y#QQ8zL;C^m{QJlqld^`&6Qf$640>9+ok_hVe3&w_i0wa_ii06UN zXkD0cgFXKlynGLyYFHGYUp=nb1V7&UjMF^X7h z=o7lpq#H!@3O~hkp3po}V-k6LCKq@Zo(I3gLHHK7gJ($WmBS!|Oa0lRq5o*G z9nB+#aG=Hxe#KVG;24$3D?ADccq=dRfCpqbnlpT$!IMSFU*#J-;*C6Tp#^^i&2TJs zAb1Noc)C0su2f%E^uap~87-L%o@wWSMq(yb;1OXwKv5G`)te>c*?RAe#tP=5u^)Uu zgr)+=H6BJOfx^)`o&qY=7aoMLLOXWlYyGYq2AQxEHlbyYYPDQVc86WTEZ2KVIAOZ$)gvB7I4^+TlaGG)uF-ojPfShhN9R&u8u{-V$q<1h>BDuYS1T{3ycId@b?&W#2`SA*i$b) zh%w%w><-+a4b2FA(wsaC#FFjm0cOTvV?0PM#+3gcKNO(xgL_3HEJH*9+kP~NCC~(@ z!0VAr(VZ;Nsn1wnEaSkEif(YC(`BTGiKI6gMpyhRHbXZ=KIt0IWQ5`ZNq~L$&7c6= zYu2EL$i*w-SU@ZJ$lr)0#Bdl0+)6LZo0N^xuJFMNoskVdkR6~A!r6FP)j@m@t?bO<|o!u4(wkc%Gp20jMs(y`D2mNmbF0f<}vyo(%3=mMRJ zAb6TABvdH&p;bJRcA0l^3?4;F;f2V7POzYvso~wS8oJ>vQ1}(hOW#f#1$Wvb!$Tvy zpA|jFKtbRL3*!rn+>L1z$FQ}?fvky93WaDE3B*)lJU-(M!UGuOOj#XWG7@^Vqfzn? z^a7WOX~Zh%OEYCf8S*=P$A$&T0m-)YzYSE8tBJ3W%6OQR&tD7+Q=&Uq5WFH3en*eY z;y@URB`$Fed>AW^LtpcH6SHb`jP4Z!H4gO0n_^9T;293=M7~HX%I08m{!w&| zHms)koZ^veU_&$FIARi`^nnJ%#}YhKO+-Q}FeLv%$4DSARn8Pf)Ii_h0D>kU5S64o zB2+XjgKcH9=olSwFSDWoFS?->OwYF{MSU7CMUOBDTqv!JqWGzTST#@@;^n1{%gXhU;AWfE8#PeugB3H{^4iZb9{J_cUV9JuGN ztc%s~Omsx#;TxfjoE^v0Jc*c(MnI8#hO^0E7#9N2m)gzrDFKqdVPwkDWIK2{8?ncoA8#Gh>w9)e{vU5f|r*C#{-Ga-}K&I)cKH5$PGPctzTh zTg2!(jRZAN!7^?Fs&6Q>66IMC$b`1BOAeci>#X)%y zK4cK)qaPv;xK=Bgg^)L6XE~_C+eZS1(ZJ?Q`f4$ze7aM>7RlCqu?2*peu%Ik&!3eOBm$ z`AiOJu!GHn9;?^}Y4Ax!Eo3H%VtnX{@nSFK%lZiek_F(OnoBEw^4>=I2YqUWfgUwe zg)o#mDq|zZM1JNG^opQDA~J|2RY4$|Q#?l=st)Ld6JSL#gkl2UaxU{`b3WrBtKcu+ zh-DPHI8zKIR;FeD>=xnBkJ?im6P@6GxF(RM!iHXv9xhQ7!DppW&sqrj))-S1Inhvn2cY9iHxe!sQN=jMsEO{wsd{8yoU&?d|dVf`&_R&39(LR z!oVN~5Ac90#1$K3Z)Nya^^eZchP;e_X-j#7e9%~gc?BAl4dH2I#v?(T@RLnGWU(*aY#=nr3wL zt9h}qIA$2CSebbT-@#9el?<#kJfL3I(HuxoS|I`Hu)Wp@VzC%mvChU8s1=m) zoF1BxkCR8E0j(?$38W>(JY9`PpcnHyW0b>TYt;!gyUeUzAcN|NvDj2|C;U)J8rN|x z(Sqte285HqAkK%raX^uSb^M49q;9NQ*V$E#tZ11zl?nLEo(DU@K;u^hVyR zUgf8H&oFw%cIX4&N6XBU{rCb)ct2JbT14(-PvQ%X;N9?$=BzPDOv|(lP?RBXT&qr; z28;UV8=B8Ea`a_l6;z6Z^h#zy8=48TLc#gK0xv_kpqz8bBhfi+uzP~JUUdZ_pP?fx z9)P4gB1|k`&LWnb|qN&D`lphL_{MintKxWG%Jy@@jY^LVi5Kao)lw{8M*^k z$^w~x!c6K@oFtB8RSP93OCUHic4eWk8$F5>6vd!nlL6xd*jzPJm_h!Ig(D&DeF8LWx$4VSF^9SAl^b1;b2vI z=H;FEnCg@sRdWEdSW7v%YFC(zc!ENRC9o^kD8EzJcY^KCr7uguGf6ju9^^mhlaq zM}C7P6n9|*&1iuF9!^_en~DWmQ=SJO(-%3B$xf+ZfO-X!P)2x)#6C1%#etgIga?0jyAU>fRDl@VGUQbrf_sS!LLU}g&#G?gWD)@{`Ob`R^RL52A z1`Ob7S|cWIOS76&X>>+ARC%zlDhA?2#akgpnFSo68H;jr*qlfStJ5+lLpnUwpIpCj>8CP1lh!o{1kJcC$NSFfUlWvL2P)i$eGAw?CTRs=>jg(`iFc64WM?g zqWK&QfCPSQqEgU>-IWI^2{#dpDm6A&&Ou(l6`GNYak&hX(k8kE(_G8^OVSu`SCkTN zRn3K)jrPfI%)CRK3#O2t`jh$)uYm_@7aV8afhD#6%8VCDm4&OSAipzrsc@9h&^uZO zJ;-HZrpBQvvns(zDBW|u>TsH`qGRF$ZianjEzS1jujD^mN3N}PPvtO-QPnd$Xs3b# zQi!5(lrj{oBA-PEbjnem$!5rb#ai^M>_yV3nxqT_e&Y!Fy0THMraNBnZLF;HPOGTO zyx|{y<2$UW*`(Sg*QPJfNITjFi3rYTt+X(^LSi$YQ$C8X>pKF7u!7!Lhb78NgQm^` zzL+7wT*yN(L)QdEu^j!v6zW`_r>u$J`45@-OC5pMRS6*m@q$BQZ6P&Ay$Z)X@$8cVVUX>a0`ppV*JO7%D>uMNWWC^YqJ4Be7Nzi0<@CE=endAtZ$mw04Q$ z$`;@-nm6k|aEa>9)VSrFiYJLB%lR4GcE8P=h3M+P{@}y7y-=9Sd?|MhC+K- z+hn4Czt%j2QOSO0{we-~S&>Q^4x%tZ)c}pps4Za|oG8))Ib}13$5cN+9vF@}GQN*T z$m?{U2<(_l<+RQxWkx0S&E2$>_E z4&Uk|)&3bL-ze*+!l^nfQH0zM%b_Qe6>^$bMiok9OVu-VZ-GIE$%2$$YF$!!D`=q= zSRUb-O^HdsyQAV+WRKVgkA*uhi()+y8jI2cgCmkC zhf+i!rfF3}GY#~C1pJ|W_(b(BAGf~VhCC6j*iUQDDzw9O#CG)Myu4h8Gy#+ zrAb<1d|J=Z*vUBYBG{T76)9i_^aQRs6N@o;*ids5=~(vW5c(7kpcFD`VHeIpkHQc5 zkmnojH3>ReGAn2>s(4!Wx?oTw&?v<0u#3h?@0v-fE>Fw!Nd3~VhQ^BCIE!cxHiUkV zfnHD_|0-9|%7%2tj9%X(kLEY33FAZfDri=8g3pOKpx965(jIt1H>#-LwL19iP2gQC`kEVuzjr{d_ zc{ec?tkXL>^UwKM`IMI6K=h3LH2=^#f^eyEaVCh=9b$^OvX*Msu&LmQ2C%Ji1X3c# zgNiUBtuu>Oy+Ob6jo1Z=^N<2YJ^Ih^%?0n-7aw$QOotbT5Nv%KW-Xq;*#5G(ghE&$AQ#s6UWc`d) zQfov=0x~rHl^)2n1re}u>-}#2+PY~E_lL_hE*Kw1WYlD4{9`gsIsu+wh^nsXhnb`# zFqsIsF#X^if9ZNz*598ns;nLW(jMa>TT)FS^Zrrh8d^!^t`X&hCm&;M(y?+mRsL0>ln0BeRO7%p`YfJ?K>5vZB;3qrt=dxu z(wUj;f<0hv7)77)4u6#5O67`THTqy>NbJC8yb?sn>-ofIPz|=lQ-Bp2&;dwOjRlt0 zXSD|&`7D{u{1a{KdwiQ{htBa}eKsD0q7><9kNJtd$Ey?@^;sOo42;i0nPMYrY4oc3 z3(-M-Ctd|-s^KZ;!n4e0aha^E*%A3ZQVU;bL9;*bEGMOs#b?bh73uK;dADTZv;32s zOrH%uOCP#>o6l;G>_96yK0YfCAZ^q2_?YUE%2Or3EKbc^{lQ**)>+sTh9jFHUI=?8 zl4vVkg<|BU>n`V!1rV&hDA9aKIb!<1ov8fy#0?VT(=*?YSPLd z_yZq=hw=Wp+aAKm`q-3tRwMI~w%bm-`Jl^x^sGkUiReG`cAOipdDUYo|NdExz*D4| zX9l0u5Dv++-~(_+wG5mIU+@7@kXbI=qj{>T>bf&4_^bw853e)xB${e1hnZ6#hN)J~ z=isv%!Dltdp@=7%J(1Ob4<;DOw8*klhYLQd5qwsIHBvtw$Qy#sYG?&H_^d|oS&iVc z8cO?v&uRpp)u5I_UQTYV^>60E)KJXIaqw9U>T^`!Rom4nTJTv7-50F8sf< z$pxR)2tKP3d{%?CJC=ij&uRpp)d)VTq5B)Oei(dKBlxU_?jxtT&kYN~XElP)YG?&L z_^d|oSqZP@L7$2`?SVCc~&E^Ux$C+H|<0H`|a>}2<*nb zDZk&(*KBX)-zl6cC)S_bbHl%N zR$$MKf4T{ae><$eX}4}oW3~~#eQPTJ##Vs~vhKWjbBoPe9%k`xdKI|fk8Gvs-xEt$ zpn0|lLG6ELD^0g0@YsuaTx~IsYN{+=ZZTJrvHNJ8kdc-{I=7E91t#?YArA=I*Gs4*JGk;(vJ1W~ZtDK*GRo zL4H4DucY6*8IMce@R;AZncHmhnkm3v+bzj$wvE5p#Gl{f(ZB1CT9DqF`g(Rw>JrKZ zTOhD&T#_##v|9Z*Pm-^$&l?}(l+a@p6+VT67N$DSn|~*Smc^V=^1k zuNqS3o;-ok`DG7m?bj-_CeMoM-8O6@#gorGiq3Oy)oQnpaGASXA+cB6wPkL`c5|P@ zz^-intsVYuY-ZRp$3>sjvfI$P#|J~TY@dmN| zcec9yR|d62^E55n$J#A1KAv{vS@T#=OmwW(Yq9F(Ot)=s>o)Q1rjP%HfbRD|e-r*4 z0K?cpD>O7~ur~e}@RXYy725rK4&GfY;PN(}UB2nLH$Ba`OyGJwB5F28|6Le1p5&M0 zKX^pc?|zvBy1}sjW`8%gEf8P>dj#G=3t2;F7A<7%>y}jucZ_-_V&0;KOjo%zSlI$7 z{*N{_^9SQUs|90G|3#Tu${*V4+i&*3&>Hoy%EVb*{w5NP;R7fBJp>fUdv1v+_DrAt zdkM&Fu=!Wi7kD;W#N}6C=Dw)6K5NYj0bz2cRmgeE2QQI1k4J}y3?WiOA{2Sziu znk7?t(v#R-2Liqv*)=J#g)gB_e2-r7J$*fsLfIep&RNebi1I%7V6JycTOP_D_;D^W zB;>9yLbBa40WnDR!{whbufc6VB{w-Ib4T2)%$vD4?rnfEFbZ6s>2H6pN;BR5rb+|R z_5Vbr2EG4Ur3T9Zm1I$AAnM-?W|^A{->%XuxqYKLbs{~9z8LnIM*C4@@d}plxWsOfSv~9(Tn2}$N{>Sy}*E_8)RP?1Bg$BM{ zF|9;}-&W+Qv!{5u{v(TZoO03H;?ku2pIBChjs4}Wdpwf|M*LVg?&yIN&OxgNZrqYm z?CIJw&qb!Zk?i^9%8J3O`YsQBX3V7Pee(`_~ov1$aV`J8~J&)<8Qw`XH>4@mEwCAJd=KL)766;FK+yNg*)-5DfZ6go%WE~ zYkEC0KCachgG%(C$2&p>J??g=Jeiz*T}1IN*(wi8Otz&CY}&NkAm61Y&vd!mD_>~c zpFern)4kQTTH~sB3O$%@(3cS>!_r1i%5m8e@^R(nWj5v-v@PG^nTr!28N1%nczVT@ zY>|$#F7JrFd7BlFF4OGEuKnXew>8QYk^1f7#`cgFr4K|OIcuB#@as|QE<~p_yk~i? zMFpC#J6z}5-N|vyzHPehgEnL5ZXK}bwX+W-F7H~_v3$?y`%1o@ktoA81}APh#G7A=8tda8}5^=!uvIdS9qpX3Wq%k9I7yYVbXYW7dpHaGxFfW4j5} zrWU)p`iVEIjm@>Tw)%iI#<(6j~ z`cmYI`s3&Byf}1Yk=oB!DD51)r}WpOikD2El+Rgw!s;nMM2s(9=TPo7!@9>NtWJIW zUhfw@BFg2-S$ojEhZf8n{6^>lBdYf+{-US*>_>C<8eAwQB-?_krHhpu_wLdAR(CE@ zA!J|<>$Y*PjLz5M+}#l+Ck!k;F8?b_a~vDFujW@Hs+_r}-@0t~g%)3ScxVpScw8?4l*4mi=qd{#to(b7geE0BW zy^GAteWF+Aqw7muOiAlF{oIjuhs)YaZah5Zvz8~{uh4q(@dit;^vzkxQK@R#HY1uY zT-l}Sumg>YuW$21!->PbPuzLE%a}Z8X0-WoMfk$QJ4?@bxJF9HFP>gd|BE%vqMzL9 zJK$l+*c9i#i*T4gjh`dy;P21Q`o%egMpC4X$Y~^*EQm)>Y z^V+rKLp{xTE)dyfCi8i@7fqek3hpX#SAX?MB8|U-gNrQstU=4{~_@(Mq*viRIWPYB?6JG%N0ho0a#z8eGeYkfntHTM{SofyQLZwlMijVzbloxw z8`{Q?j0ugd`|3A!CWb!Juvk=TT!U`o_K$mKUByG$x2Eo?SF-Hm>pC3DH*!W&s|Ug( z9@~6&c8%j>>Q6s=G%PH~)RZFAn{0W#ORlG1IaROt=9WhvIahvnN?PPq>!6C>D&IV~ zc=euz5mg>)vU01n;wF+?PySQiTWV%I^5fz& zxn8LFZ0hMX#n0F;_4&=ZZ}`ARtqre~>oES&)H-iA-4MF5eBp5$8r5jjpke19<7Z7i zH@Io}#p@=>m}?#uk;(>h7l+KXrfQ!j)dHeDbO4Gm@S>wtQsPupJO6I|mY27= zx3%8(P&fPeyO&O`9?^1E`>mgKJ-ejo?&zm;&7JoA#0!HiG;7~}SKVE8lE*J~a z<(F)Sf88+V^;+MBA9b9IxYFvADQV-=M(u07qRGIu&snNGIAC<4`c=J;yfd-)n`cLy zZItr9ty#)DtJ=1o9rfPU=?PCBURtU3_Gh+t>hnpr!QGZ0FTK6;2cLIq6`pTtMEJbj z*_T#Y`cAixwqM!)#_{~ew;XpLfA9E<+p5i8v3Yuzc3pCIKNCK&+lkGtxo0P&TAzGl zaMO~Bc^3~$p15mnqa91O9qJO>ZDN;G-<{Ze;=8Sz6LvJ-w&D8^wpO3F{i{{&id4y8 zdGgYcea7|~b-rGDM0%$V(?5^=^x3O#UT84YT75wE>{FwsW>k0fNjtiz&yYTglfFOd zIR3=8)#u~Rl|Qy|NBY}++syAf`lkoyTR*dY<<0-<@>ye!rypH=eAD**GX}O_vHy$B zq1z&&7i{@qN$vTizuvsb@?Ec~KjvLsvh2)pW5%uPGIdL%cP7O>wyai-$sJcdvHFyK zhcmx@V)1zADN9Mq!mvh`iebCrf7v`C_IS@Xdw%lml8s9?eY390hPY|%r|x<@@q^`` zF5ldLNb;wb8{}&;s@y1FkynaTtJ0^6dt$DMeI^!|^I)5_HjC$cFt=yB*XHh>yLgVX zL#L&+dpr|2)Y00}HtwkoZ99xR8Jqs?!gm*z^nT_2aP7Wj@1$Q&KfG)EuE$HReW<8= zUaw~ke*FExjcaC&?qpAU>9@Ww7QGsG^`Wa*^FNgT$NVYzH|CFjxJQZPS6jd8obq6^ z(#`TseRIk_S9 zJo)avw^IJ&d3NM!mS^5Q0}4goKmNX`qFaj|zrVV3N%-hOMebWx{)A)gpw$PrC0tpx zch!;MZ>3Ie@MXPL^=qasNu5yN(O||?KR(s{osvz%-l{Nd=Y#jY?cUP;oleGpS1Bo{eX`TW;owrpMm8zosu^!m0PGwj4Ec-b@4Fj( z_0WMTXWw{ZnEltz&o_>Hq~7>>?WZl?vFZ40^&9rxP;ggR`-rBjTn($y`@)R==ll1*cG&GmZ1YX>vjwBhq(9TYYJAW1ng^3d9eI4(=~@G?W{;`8 z`{iXRGxJTVdg9qapLDqxz5BqNuLc&{U36mMv`*ufyqo;~%B}U9ymjsVD@{6%Ydmhr z(F?Zuz7CTPeqHbD8f{#2cYb`}MeB-7(Wx&rN?!kc)U(IF>h$n{xtIF%@?F{RYU_C) zE}K(+ev$cUOV{+>c*=Hu(aVcwb$@4vV~%%z4M9gMy9Q}+(PE!!3!zVqot=f8GrSym&x&PUEx z9a>E~9eR3Q`q??<7tYLZUfkcU@9~pWc3ti<>4mo6pW3;%bGN_f`RwB5iTS1#ef8O-6+2&8 ze?DrpZ_?KduJyWnu;K9T4OSQasPdC-AD`#w+w5q~-WxCOe*fp%-*4Lc!=8_m5)W+r zwnN^JPVJf2@6(_2U;lR0r@Q*@-}CjS&RzqzJbzAkmF)(6+VyRs>xk1gH)^XluDG7^`^Cv>`4{Mh7`AH8=vY5&Q7r+-VETWjX^EyJf2$Wdxktz!dr zXY9&IygKRKYMXM$4t%fw+)J?;b1%R6<;C1X+(U-kKX1Z^u6)m(Nu4<>$C>AghK!C0 z`A6FyFMBp@-rID>;CuEyTDoklN~NzAEwFe0i2N@M3pszLUWwH?I%IQ&e3)y_na0^h zlzri|QV~<9)UPz5V7-Z_4$Tf3S~aD?Yt4rYIekya^spRfo~-V2ElBQlro+88f8L*K zdY>Jer;goR{?zpJDS1Cxv!PSn&jwb#o-JWU@2ES@rp*-iwwcS{T5<}{thV1MIfVz7 zp#E|Jn6Ai`#-h}8~#rg)*b#k(QmbA zaAE!bfqOB+0}JcHYD#e3JGky0T=x#Hdk5FOgX`YGb?@N1cW~W1xb7WX_x_(<_cn9R z+bRaZb>QGSaBv+sxDFg#2M(?S2iJjv>%hTv;NUuNa2+_f4*b8h4(!mA%Ym88t?x~# z>r2$L^zOi$o&2vz33CVD{1JFXN|-zI!Pjb$Ep@*C!PfuQ8*pl?$-txeH{PDov}@1q zdJyvF+i;w`Cne11i+0D_J+a|FE3ZIuxV(0Y-QnP!Q@-%q-iE`gjQ-iU|HTV{TsI`~ zE5qdu+FBaZc zWVeJ{JT|X3Yq#2*VODR9&Bf~xco)yj7jrnW-o<XEke&)M#9D%3k1G;cK z1JLW3^vqi53B01*<2PozuVk5kB3=q9MAf49Y^4u9=v(x55KO) z5m@-vGHE70c&(gyf0ZYxYkbeTUA_9plaMU=1MZl4=FQWZM|aa(tZIh49X@s<_wPu~ z&f{S*Vf+^!9%f~iGhU0qPG?p=Im3C?3vXt2hk4lf%)`$1cCPcWn}N-4vxl4G><#B| zJ8te=%}(aw9?o}$Ih?%5)oyd}&Ok?)cDClTgRK^A>_7=h>4CreVIwI=Y*;kAnPMa- zd+8v7l?P@I?JF!(v@wgp!GhPYc#Qmaqkr+}tywW9~T}Gb{^<=l1 z?>&6)VKn^9{+yP~c5Du9z=$IFH{9W6XG->!VWePKJI8}>{>2(ThgFjJ$1BNiv}0Gl z2*!@{jL?fkIEj}t%8raQvmckSgGa}a+sk{|-I81HLgQL5Hsf_;j9KkUzZSmd`hflY z{qoi-2e?2!$)oe=5iQ%T=mooAcY8Sa01qD8amc3revnK1LFos;%$0_}DJ+G)T z?Mu&s5^{of2$Yf@e&Qc@K+lMV{iFxZ(YQ69EP4T9<}>a3ae+NET-qfgn-Z&xwP zAH%=X;4loioXFwDM}^~nK3%de8nGH2_;Ke13B1`yjKyEKlik7{F8(rZu@*eSR&{tT z-?2xR_~v(i;2kS_pey<{_5l~-2|M3`D0F}}wI3DefPIbp+5=d=6{dc)H;wocUN<=B zU;NMDN9{P!Otf$${^AofB}>UJ+Vj}zR30QB z64&{UlM8X&Z#n&BN7FDXaRgWkZ{S{j$Bu(RM5fKhmq;kGMd3L?a+hdN%qL-x@7!kyVRCt=xZ6|AyJfIVM;~IAC47xZi92D^f zqzL)p;ERZYRq#TrWaCT_Y_3!WLYw>%^SbbbSQtWa%EvKbktoZ^4FYKgiL`4l9w)t7 z!t746N%nQ154=MoLWhhSUO^Z515J?8*x9{YWW&F)jMHJqaT$T&8EwPM`76JOp&w;X z@)@z210;)|q(M9gNy%_zQS52M`%NaqF5h^BEtVeL0E)lJX5or(d`rFnHKTdB3B55M zWj!1xGCDvl(n=1_ArE5bd65VoZFVOfixzCL1Oa-$k33F|-2n#+=cw1oK;?s|+N&rP z=4GD`V|_zpV46OaUCEE-U15wp9Oe$QYj1mTuk3*S&^ehEzX>1F^hHL7tYmG{fOx{=h_%x{aaB_L$OoA>%HNTbzG#I6A034W zJ-ELG>B#wD(O5u5#!S}BSh+eD<|VV?4Dt!gW8oyQ233l1accAuOCRho4%!(jd6Ek` zVlb1U1+0eFh+=exp7_a5_MA&5i)QW0i|E72Uyu4hDi|6IhrNLhT#F5FTaB(0f6I2K^)h3(*O^rv>A1>&KL#1-=qY2_{QHjC3D+!+q>p+81R z_QftM=!gDk#mTrO8}>zSWH!jh5#?eGRXIE85qqkC;~m(5Ys2877B1$kSHF+0*sWtlvk5~p}{avLnIM9 zq9MQq7TMiENM=hYyo>gP7UnJ*ktD}6#6^nbuorqD&%=HI8mw^y1fnE<=KzurzBpgr zBD^p>=}lP+{qToTXv9PT`4u85)|+fflA#fN)`4#k3z-o*(G2ZLqwILi8AKJs81#g# zg+gLG2<5QJL68VdTEGLI&6#B9__t9-~p<9Hfg56H18u4}SmjfFl;k9aoGN@EU-=W)4}MbWLEqqjm`m(}8R#89lRa(3F0`TC3cCo9ifClx$b}_Or40qjPxOJJttbVVuqUlo7^Y zQ7io8MaEdg1)wV?g&%}iP)ruUSjBkwwU`9!NJ}xW06vY}ny+#xw(_7s@f7OAPqoLU zOF4;hKw73PX-wl}yo%dmU1`S$cBMIRZ`@KD5T1$Wfey_WOf|&G)nFOSpaM{c-=R-f zS3uJ&5fQK{!1wzU)ekJs5rzaBKnHBWbQG?ExlFqZSGfu>f`bT@h$&ks&oT2zKFQ}m z6X}KKbjlQzNobylg+PjqD4J;Qp~{UiX=T04yx?rE!*@9!EHFbLHz4vWW5L5RqZ;T! z(-01=6Dz26!IJns8WB5zM0uwPo3Oq{&QHZ1MM;dsuc{R(r?4>qMlElUme7@IXPS+I z9uTTJ6W+_eaIx|ZG;1;&WFs%+OIQWFDZ~+B@qOYI`XKAT=6r*!F~&QYIV#FAB6Q1j zL|Og{3H*eegmp)m zk4oh-WW(YJ;-~U@&=2b|?}V-WnGQb9Z?ps+ojM;>Xr60yBcFjs)H2M+xGiW1N$HoL z8mBOTqp({twr13@Jz!;g{Dg0W1Jyk6czg$Ka$NZ#(E{!H#I9t<(P+@eH{`wYWUgQq zC2mrnquML{JFWJLqy)6B56-yBFM$HWde9H*m4GXk+TnGbCu40^RI&T)+@8E~_t zM=!#o2k*q5W>%)0MKypJdO?5KE83g^_tUDWpok~=PxD?5$WJ*>vrViG*OEcO8{h`( zDHFtX2>z;ls*)#0mcP0AthyBXKqfq1(H5+M3GA+F9ne-*0vg5N@Fw!8s)?rXE=3Z} z`P|CNr5SqCtXF&Qk!x$F08j8Q`eM#P57<{8%@y)j)h5W9uw0lBWNfCK5VoP3f$R<+ z6GAYLr@&l79*ls0Xci3qu)4BL#;)8%wHn2uXpF!pOa`tQAoXTQ2Kr$Xs%*i+@^RY5 zw=@G}jv%|Bbq5(T7Gv=I0o#g72Q zLM}$9Z&Y_tVglxf1TY9*FK>YJRDYpWS~jeq87KPHNP#YK4SC4l`A!_6d4uZPw1*{? z)T#c1afqyBfb;~{q7Sk$MvBSsGO?(Ih$#$<$FKvfC?{kn%JlG2ayrc_kd>XrK|Yl; z^r46d?TD_BnwZ0A6e|>nm;vI0Sj6O6{EMx~kHs^bNe;(l%$DI-VlaK#un{>kmeIUe zGNBEyMizjgskVX%RUsK8A2mB9--WYGokltq6UzobTk{v@V8RD2YTZFG&`Qh9EBw`G z;v~E-WNG%xzpCdm5Eu;ara$ed9*tzuHoT06U@BC}OalfXbA~tI3h@K}h&tsR_yU+i z9k?<6rOZXxr+sAhq5&}}7JvlgWz1&Ph%zud8#8eg#=+`H%s9{nj;YEvFwd_k{;87nm81k+JGqdXSowL9uQF?!W+6Fu59-0X@VPA_7-%qV%E6 z4E(5aEqtjWsqq-J;OpcSNXcB1Kg^bxg&-f18!j{XBC&^tbQPl^pAx9dd;`gd73d4D z0nMa7vNx8K?l0~cn;8JUtf;D3sOY7cqh{QAG@4V~l*cQkQ~zM}vNOm-#|DLpPDp9;UB;^QAys|} zLqrbjPG$zz(mAspjesyH-H{W(6%ZDFXxNHTQisyq1&3hlfSgN|Op7hh8d)6e!|R+N z@4$nVaVp>e8u1J>G{oWmheZ!ft(%6574A$^r%Z=fd7Oi>}6(z)CQz*;)J0`;Q+T)ti?LCZ)U^rqbe3? z!{9}gNzO*waFC)4SmY~u^`ar=SLz%?5TW@6G+_hciMUZw82`kMbb~)@O;DO;G^m2C zO&%)F6=KbGL@^Vi#Sh9dluLmz#UP@KW^>BJWAG5;crcI`9V`2y&I~8Pb}*eZOdig6 zcqsm(I+7}Rcrbc1nF&}@PNRB;T!%4WDYN3q2&5(CXL6xx1@>nIS`%WEB!y7Mu1rO^ z)e3^vxZq9XrYZrKQ3*nO)ZiGuB9$6rZf;nPi_w6pA*xy_5~wCeu0o7On`j2jlfRLx zlPRG{@PKL%k9!c%DSi*Q$!0*UIG*((^h+j!cPRf=|ME|2sOnFcf|Zqzktdsam9jA6 zJIAR0fL;?%KmwT%Dur)|FJz#wEPB9Bnv=0UPMexSS>;)Z7r+hK8NX)qnqz_`JX4kc zhoD*Bi;>lmtW0h!ZzHZ4f0K!b>*yU+3D4k18Jcn&WgpBrK)W=r90bmmHz+&e5G;qR z^o}OU?U;)w+Gr(7vvcKLvV;WTT=6Vz!qA2n6GGAMNN4Wz0xkKK=aY^ao9_dkfS(@s;pG16>qtc_#?Zc4zfYK9JHuS&a~j|%0E>t z!p7J@bqAsj5;A*+i5MwJMxA0W*dm5Dq&+l(mB|bh7mba`;t&RG85hK3=v8+-pmF?_ zd;%WjcNmDn%9Q0jSP7r%G_Vglnrd$KK6AHye#xHfdA zolD8)(1_}vpxI8$#Uf%+>Vud{D<@h7Q$?$nt6PsaOlNB*G1d)17TWA36l7EpWGGijPDVqc}=pXOrMB*15tGrv8 zq1IhsS>*-Tg_w!nRg04s;Ctv+5tQR1I3(1BNKuN}3ew__Xivx%gTOKLC4PnVu#e)W zm|3IrS9>*IQ|>_C0HDmem##*GKJ)-Ga5+U!MFB=6xfH)O4q7CpAcWQ}n9ags`(omN1>zKvg?>CKu9-4}_y*uu4m+Zcxd@-*zGAuFPx;9BFt*x*1IZJGIP|8wbnsQ71p&|@XZXM>j)EleDy>`N zQH)A8N?}YAs|RJ`h$mbU!?ohV`U^6GT(i2OwjdwyA)CKlG|&B1)BjlI#5GG5lllY(K$$?qzt{FYrato1tUZp@@DWOY{+Y3t-`t{PI;Vn)eGQzFIStMiR*Z@09_rjNS zC@qP|R0R<7wR(o-@qStXiQ;S&qLn&jqOdjBA~(lO&P>dLK&ZltSy2em%E^Q#=8;$y zPGNMcJ%Cym4^I?h`k9)w6s{5PYQ>XUlk6g$isKoZ=9I8Maf+G?98c9#*aH*LhRJ$~ zjf@T%Xj6Bym|C840wRklwTK2*Rnr1Dd{i{ny;rIXQF}y}ur=uL*J)KTVI;yBv68Hi z5y*#Cp+pLCvw&vS5U8%nf_Sy!s_wcX`Y{UZBkw|5@FKFM9~=wItAF_!Hh@d19iTx~ z%3w_eZjl(nK+$4W_)4)J6`%>KCFoZ(Q}h96GZ-{0J z1Ip&jdIGIc1x8A8Q`Ld73hR{ENbB-&8CFo+n*VEdtMykPgzcHfVP{1^=5O?+YMp8( zIs#|ISE|u!Wm5HA)m}7jRwYoCYSr_#)~Nc55Ud%yau}@^5DRe$v4z%3qk{-3J20I3owWF}wcdUS(z@e8EV%pz7cLPM$v;oXdd*$zm?KNtfZK+DpSR-eJV z#%mHBgG*`2VB@4?8-plC<6blnpp>CE~vzsX;vH827q zSa()>CY`}PXivUJ<_Mn)#V{=#CZFJw)-eG~`jzrH%?}s{4T@ED$CdGA z^Hbi5iiIGtDOwjT3-6jQ>z+66S`{0hU2|U>+-vSL)X2a&+=oV0FIN>#wM$G0dO?-{ zd~+O?WN8n^6@wwY$-~VIM6)te@f7P~58VX7( zLy)MEXy(Uf?!x4=+UGtpKI3tG*1ZT?D^Xm~`MMK~jK(lC{ozG?ulD5G_?r5o63Ayo zFxXGmGjj4&^r@T|ZGjTaH@M!6N7X4=LigUwo??30fPcA2xjUa>EM&(A)h+=6PZDC# zDG>x4$v*N|j^S;5Mn8O({v=b@KjN9JpZ&j+PTk??@6VHegt1HaZO^r2ee0ijmLu~y zqT5ajymszCdzQmI?4l>B{>-x+<`I^_<5&OwS&qQdM!{z}U~7CM_$-I+eISd0Jr#?B z&vKC3GY4VD1fMBB1)t?mYz#ij;m<^fb|wQMsw!3yr4@-3W0dofb(rcGE6ZeYd{Cam zH^FB)g3of?%sH4JkZGE`SV#_46AM1eLH1@mNm>d%%R%Lx%sBWgNAOt=m862ta;VN7 ze3pY`f|)T@6>6O9fdZkvF6S?D&xUtIfBn}sJ_b|vns<+svuf5quLIenBpum-r%zwT5JeD z%MpB*Lra*p zKR%&{uHqkaj+GCQk?}8Rr;NbPEm6M2_&y0}L*)o?s4X=5-D#to&K4%T}tZ9r3;onQt9Z6 zKRs0G<3l-?-jzM1#EydT+IS_ishfMSm~A=&FyPj=C@_m-|D6PEU~}8}Y|ej`9TLa|C#goScx4))NeO+FXw-?1i%)3T%j5M4r-5kP@<~r( zcO3}$b7a?~#1_7UI`KVv#rO2}ObT^z?HyB>TTtbF?!jE|l(sySJ@DgPWJt(eUxZ}4 zV-8`^s`37F_BGfIs3gF?c8evDBm{VeLz$uTHppa+X_=7u8&!%JnXX^WuTl;K!sGvq zN^g?bKMQDPN4`6#G!VA4sPxWW7zk(`V8}XkB0Y(|n9xuL>UW)6f~Ub#ps#@XZA9UJ zp78z~NN2%swYt9NyLx>M5Kv;L8=$ zN>uo5MV>l)ikIs@vRKC{7p*NWP0Ig?Wp&utU+%icGkIXdkCo$&9ys9~v})kSEh)vG zu08WyWXc=Ko?ou47`&?Q^3Z3-OuF7T@1Qrn=^U});Ta#4%)YC}8xd2Y#-6D%+p_wl zg7b5%YkBRin3{)&r{*3q%-D4&FGd_# zT49mv>V0E#=BZmYpWQNI-;g}~Yq`G27e^+RD_3qwM1`ox__xnh`g~ZKhK1vXmK)n5x@n#=&nG=Kcn)9^ zS4_zk=_u>+j@X;GS@GyH&7SPqKQ44zqg)ZG-yUvk4{1^QK=hHbw&@SQ9<}a5bXvoE zmgibjp!vGPb*|l=9M|mIrt3avGj{IQ0gGNc`#|FIu4Ns|_l&-;VHmEe_k?3LFUmcv(HIKEy>>+O)UKd*Nx9Dk+ z_YL1X-1^+O<*P1P*7d65n7X$0mosy?TZT?<`sMTyuT{%)_pp18q!hmU%e=J?tldy< zdA6Z1MXsnne(uhTLpK(w{d|Se&cS<1e?6*r$@EG2oW&=sp7KM)_~LaAcdGh@VttTIEu=GmboRu7vs+Mgt zqUpkwU8)W{(75>eHa|3+IPCkxo!7gJ$#Z5#n=e;{FFd@n^qhxlq;&k^=>_$_Sko-} z$(_Cvu9dZZ8(v{YdGD7U=0qP}^uwIGWvj2g_Hm2IOXb?MjqTKVzxVq2;f2RmUbiXb z>U}xCo$$@Lk+z}B;|5+@T4aGcdf&$j;~Kn}`%>XY(lUnT4>{d#WPJ5ipSUVju6g$$ zXXjqM7Wi5f>3z?jI`4&S?m1?|oUK1xOso=eq;hgX*L`CjpYp=Ekb7sZ>NK*dCFjgD zE56F_%HH;eT)#zF^5otUTC&`bW0p;kA2)yRImbV87a3BfMfNsB&QHHTE?12&dM+qX zBIhg9b4|&9+=pzk_MWx0y=r(TuxOdi7Jd}NF>YjQf%RauY!=Zd5XC$?H zAUxu+&1YxVI6kKS^s`69!g5SaDKfpume;%Fdis@9^@?w9dGwKU<#(r~MP9WIs_3oq z&4Y_q?^zg8<)J1kw^}PM3#q(j!@_QJN_HPs`K5<{Ue#~mCRee}2jceRKlQz(X0{_g zEmzF4}8?x@JhK3;~!0}^Jdcxp&QE=9=D-UjYbU`cK$Jb z*5q@8o0ea^Zeon{QhJl}oyQ*;-(>yd^;g%=J=E-%mI<{|BDcJ^GwQR7TMAWuWKvY! zT@80NaW`6C|Jcm_QBiGdQ3GjGdpQ=71>B(cuw}!NSrCYP}@78a5 zd7FD%>unEpv!B0v>E!AWEoZgg`dQbrOPcPEemd9OY0po*Fz70CzQ!w>3|#x1rOJZ?Mi;7I)%(ah6MMgT zcEs66Dev2wrM$DMZTs0#?`@r)@Z{m8m0E9qW_zbTpL84CZTa!i+be(YdAC;K`Ibh6 z&)c1SX{Dv_bo*%gmF;gF&wqT&arg1}j=#9A+UymZr*~=BC1>|D;S;-^*zB5nc0#K4 z$u|Z!Et!~i@v!8HyXH39v1Hq!F0tJvb~*LkiOnaz+qyYnN8@c9zW-or^?BRBTGg&d zmHd?_FCE!uY@bo*>!nAecj_?x^Tb22-uo2UO2KHF|1Bb!VTnql@|s>9aWL z`=gHIPi$L#KJHxkV;gs*zumXZ{Jx`qdT_q=GwWC0{I4#bHRgEw(Y41nZQnm*VEYyO zzt|kQEh2ismLHbXo?rUw&6_OW^_u!)-qj_`&Kx&p+`2APw={ZZQru(9YSozBape=M zPuX`k^V=sDk9VH3l(Z}iYh#A&so7R5nuE!HU zSpMnq&HaZYe|ot=z9yr}jq(+FrAW0ZeX6)8=9<`NVu3jiwn=NVc+LlNd$xOR?%uhJ z=QulbT3WluGjT&5tsQOSp6bxH!?=^N>EAAVcVS8ISKbfT?pyXw`sMV)ySDFoyyV)4 zin{0ZdgkEA-yht#X4dFV_OzFN>-%ESt8rH!x_UMLL-~KqpOSxL{`iM`lt_NH^{dV) z4>l{^EZ@{Or|fg3SI;-)BiH`ko=@s7uaej`@$yf{f2!1HZr_u=?TJnLq#t@dz2kR} zulami#%Hv3k1kZ?zGdZ4IMxnYeQ;aC zl~sFJ9U1;s>huO*)@xP2X6lmE3H2QfW<2%dQ{CSw*);5}3e$Ezc<bQ90~fx0X+(h$oeB@EwX#y}X`3g`e$y4c&~erq z)gr2W)c1$ZzILQ}+Jrfisx-T z{k~f~tJGUFst>7gutlYc;T8KdKi;-y*hg=szqO`O`h*>8^BwB(!{-OCp8fvpi#hkb zY+Lzj(&=-H&J``TuUL0`zoBWX5qbf9?EyYbCl*fYG=9mu$?vb+TCd4l*Y3a4q~o~8 zujyBI*-Laz3|w<*lRy^@9^8QZSmndpI&tSYsZ#lHPY*R zX#4%CoqIcX+uN=C#iOU&w;sM@ z{m=a_G~4@PpH7?8X7>B^%E_zkuQtg)D90~3sts(Ok+Eath#@_O?BFmRU3AKecn`_6d8oC5QdirNiR3zmD@BeD!2EYtQ$q&+F5Fd*0ot?>%}~^CDAD z#J|4e!n6&p^GkZvPtW-7wdVU>_G3-g?i#W9$flGjHTIs`Kj4?oE?%CPZ(7k;pG{h^ z^M&>2qgMMSecj+%ugeD;4)5Mzb>WXHKiT&2d5*r#j@InG@#60Hf3E%groBJx`8X-@ zz{YPoo@xC){W<^jZ%2K)tMC3jUw`WCHE>IgtwRnB?^fXJQpak2aP7M*n=(4i z>$tX5?#I)9D|c#r#;ng~&A&E2bO=k?gXYZq>%hsw?`dZNfd-spX|H814^JnUnSe>IoHdn}px#pZ{ zoNYwe7d|T$F?CA)N)rm!n|SKb?2w^VQyRS1e8`Z~_k>Ij%W>w(>MqxUJ?$i#1RT z3zVF!fmNixTmbpwRXk>DfqGDIrWTy31!ro(nObnB7M!UCXKKNjT5zTooT&w8YQdS> z|L#mJaKlQLnakgLbAa{sn**%>hc^dgP89F33~Ti-{ok?-Yvz!*&tiis_y1!n_XbdR zvZ(*gjaw}mTv-2q;9d;li?{h*aNRq&?j2nB4z7C#*S&-5-obV6;JSBk-8;DM9bESg zu6zG)t$SOu-r5_yV>fumZt#xX;2pccJ9dM2>;~`H4c@UEykj?b$8PYB-QXR&|C4v@ zS_97-{_&lVSxgPw=G!4O%xbC1OFJw!duS;8GgNh0Y*v;^Iqt0LV$*K!kPWrDtX0{w zT#vWjx+<6T?%B4XmcUCpZrK`|-m)%L|86(sHnz~3;ckbIy~6F9T* zjE`MbxSC!WjW#J}C-Tf*=^0r)=)|s3A%m0M#G@Izq~i55NWu9C>UQc}H+$Of?%2#- z>Urlnzq4}(8>&eG?10R_9>$CWd}h2lmz{6)_GNvq@k_7#s}1?s%0PP5{z>e7&wK3o z7isM5cHzMe*wpMGY<3dX&dRb6-?^OZkj_2`*pIz2*a3hywuPf3@L}x5KJ2vSM&tf= z?a1swBlO|#PyPIh{v7nFC;bB zd$}&qugmBG{9#>oispEXOvm2r!eumVI2oDrCcR>N-b(1B9h>yPU!%`VJaVqVE&I~@ z&qwbruJh2JP=#+AIfblD{rKBKR@wn}l3C}|68OfiBtLet$$pX#dr41$ovit+@f#e1 zbB)VC9^}MNz;nP3PR8ZoGx`@k{NH0CGmabmTlHCTO77e3LHmAxw>c#ze(kpdxC)a$ z(_f}M4v)sK&wfA0CgwBx^W($@p8WE09anq9t?&VOG=A;z;|O_;J&@V&w+;})?kff_ zF2fhmj8FaOxb|dGzt}*y#_zS4k$=Z)m-NVIy=~kE1Ki+~Onk`B;)7Vp2~U6@=^?N) zG+O5ROn%}x?SOkc*~_sQ^g#Ra13u$#Mn2y3kNw~xGfwRCXZ_DGgP?5onxQ|7!H@V6 zy@-GL5`7zag&&tbXZkg4!hTlTvpF1Ia^lb8${1U$xCTx039a#;#>^QWxI}wB|Nalx z!)iJXqw~kVLo}3Oe&-0}$sV5i44Y@!F>#M}56wnXPJ zjN%B`(f*HKV?HP6b1vs0qj5{w29651%GR70%ZF&j!zm`d;J?_4jRFl%dC`amt6IWh zogSw!4c2H6V;H@Lp+D{1f-dmfaJv(Y@+NY$?lU`HOQRk(UyKzaqB_MF?0VC`Gx)U<(p_%$sP5AzAqs-@l#z@ld3<(YthK^>Zk`VL>ML-*YHd={WYaMWZ9( zF&16w-TxdnIFCUuc5xj_f~(ANF_eov9&aKO%h;mBEq1HeArN=`dPF1s_`-gW=t=%) zc){cq@+0xScz`&MEu}y9994A7yq=?(`V()O-3d6uW5;^LDfTVaScv2|aD=Rw1KSdv z@q1-GcsenRy=ZopFoz;CIEO%zO;bC7-d^~=>eMXqQfVHA*u9i z@*DI|JU2Wp{+55=h@%008UElpV=v{F$~(fqtNHt(5*NXS+HviF(7cB z_{wRp&4orD_9RWA8lRqYW7++B3Rl5+kR zbU=0kN6-}i3c8hp?}BY+|10>adXrt?xwP(wi!p(R3Lly_MEQ;UOz|SYO}*2E?!+ea6}klusojYms~W72ok|=_ghRT7#{yiT{JPNJB%taI}=*j`D4}$TF`7?psEo0e`}eO7#Z25ONXz zdfM&`r)VGL>K}p=(0$S0aiV~9Lk^)D@>xS38srjwqSxx3w8aLff9N^a;u-c%`QbSh zg6Ck8zmaProL1!?naIjjPkE1PC}z1dH}ux>+8{9R^Igal^`IAtzK0iqlxd%6q7hXY z3*YL=$1o%!r=VBj|3So{FBHTw+CO3r_#XZaXhNH51m9uhu;wCs^q%*i6wIU5;ud(! zPQhFywuK$0Q~ux+1YeMHP*uOsn6`+wt4|Yu>tFE~*3lB^Z#@&q1Pw=heRNd5)db%A%VIm*A6_k^b;K zIy2yToKBX!5#js&3aC$w7$E~3}kSM(jckzV-J(m8yojDCH;^`rar z)A|y-H0iEpxHaL4_X#d z{m{1q8~q8XuPwTLU~z^Sh)r4(z7!g$KAS&VXnYKnGJfJ0DYs$Yi@2?|j%xS?m_5S* z?G4D(Y5WGyv)a-=C9UlH_x&ojuZH*GFY`hD@x8Hk=I%24r;h4x5C=jFq7`hSeu)ke z{v327{xF}R|F0f5=nuM=<%xK%KZ+jr`AOmiT)++?r(#}WHyc~h|L_0ScJ*&%|EqV# zFT677WjvMS+s&INc`yHfws0x%6Hk?#iT+56llmk?UJ-ZbR|2HrC-J*i&W%60cyG7` z^tQuO`6}+(F5__GO8A9;o!Y%L|B4TR4GliiKJ>~gP8yH%zHv%BzyCE{?oZK0UK!^m z^lGq`zD*h)Akq@+*P!oWC6>r6Qhvwfcesv!^)iYe5AVZ4 za$>d|gwfNN9^HD4cqm*$4Wm%6t=SmGmV2$|gpWeY3tanKyUAT7yTEm%`;l()MZAm8 z;d9Owm-A|*7NTAG(&wV*mha#c`6KL`BWlLHWX|ww%s6PZ$UJjR?9R3J%4`iDC-is3 zz}Sjb<7rbKnlGiO4=z(4(F)hL2b%I6>E>E~l=Y`6hw?0$c!?*87fz0i>ngRHCy=t1 z*F}Sr6RtsT#`WMQHjuA6f%9Ut1bKtLWJ@X6c#5na?`sdBJoUGB*nYC>T+3sOsMlb6 z;_)u23UP*lmv!8@WH=xbJ!Xn@?8KDF0%-?u*KZ1#j6fy7F- zsAs-AvchI(&~MQb%c{Z%UtAjx_Shn*L1sPT*B-5o}`=v5Nw&`z^?NV#YdiiZG-8uT@=1`ms}jNvGYXKFz5ALDRb zJLZyTj`Sdj#`{POrRm!-=8Z21m-&XgA=gkk#=t93o%(3AIabRzr-WvCoy2NB7s-%L zyc&9e2Z|n%tB2p*fgTtNN~9LOCc0M^MM8!guSXgw3(Z6K9AD!(-U{f3aI1-0Z|Om9 z!sx>C$XhzXL-}Q~iN{7$SZ%Ymd{6rvTW7?QqIqBZK~oDnM>NrZWRxD+<4%oh#`o!s zqe|o=(YpfIWp*m{CQO{T>l@6cEjHwu0UgG}9hd&>_`SUo4QzzqmHbjtuZz zJwOW7KBI8tTuo=bfpEfgg=ZDsiR^J`zBWTY5y@A2`GTIA*+P0Nh(6y#7e-r<5axT( z9_Df&9T13UNqgjYa5u<6|MBa&4$XwV9IIpd=_hrIy&whUF=NDsd<8S8uPRGZcNVppw% zQ7GS|)w&GCREh#R#04$D7V;)KMQxB(dZYQ|Ignz=h>X%i}1cPR)AiBBRP$ z*RoItbs}|+FM}9}me18X(MQ%h9$hQ-;6osPz!M{>F}BeoSAwTxE1?U0VMGMmYMbE` zG@*T3BqgB+0*a0mXowv}P7Bbu9ZEVPOEKsLmV?sqVZdvyX)`{FbBcJYLJp9*Krybb zMb(QmhIVWeNwO`-Ne$@%r9=18J*^^q5Ch4ofd9xgxe9PothDc;S?X+z#~?M}BM9So zO5iXkh)Tm|eI8UHP>P%y^)a#wirPcql6)ugkhm60kQu%UAwt9O0^|sd(W}75(|fEQ ztrhu^dOV~8Ss%Dde-fmp$w*;4+Q3+B$e|t?v_wkeV!@`+EbOFLW?tZmbDQ82cW97+ zhwM`ihKL?XMOvwE3PNE6&-V5@g=UA4Z?p$Dd+{)@{gz*DMkj>2EM?$gHy(k&L8aN2Tmy0puN6asRLPq5=af2 z2QMHMe7F4|j`<9-%I44EGmQ}h;SE;PE06@4g$R~`w+f=@kvZM~{h*|Brf|lHAhs>A z9yA1NZ8Khl`D9EGx^DiO@rbZY`f|!j7uR|Vkd&A7MTkzQ9qiDQSZLZZ~INR1Ke!yV8 zDW1_T^;Cc{3Na!BFQUg_DXq{pnISa~<0u14hrWS?^hV!ZLi!JVRyJicq9`ws6qO#@ zIrE6xL^ED?UK&=`2uxJLreg=n#Ame7u{uVeu>y1pD}%SvCeosPV6M83wbO1G*>T6- zQ5WcIM3>|)pe`t&_oGK6s%b-wvh?hoJ85hJ9YK*8+eDLT4Kim$qVnL2AgRvCkwP(| z5Ez9Ha*ZY-}@F3_Tt>;Hi6*NSmlv8?(rhp^R5samm`c+WJcaQ`@!`5i? z_4a9SNHBUz8Pb|>IYJo*eG?upFr=WRA z2Yp83c~k9*7$`7{OaT@G90AjPj%4wiXdmAS&l}vqG^0PvAb}c5$;@wsAEKfoaCK;C z%t#Is88HVJo(UR*PmNyFbDKy@kO<0#3KUa=qGC5p3r+8ixn78OJ5i-&*r)JZpfz&B zcUaZ%Xz^V1mzg;cI>xLlI4%N^#S~+aNh2Gg8tTWJU({8ef;U%Hl;rUP|MLIuDuf!}MQV^8lId|}$kz#T|2oScB z9)mAj6LlcLXgeIGA@MSDB6HCfkf5#hL`{u=0%H_cD29o#&H6BrM6Hnb_&adlY~@3ic-5?wXH?> zecs!D$Lfsr)BZy_{bcR0NRH=1|6|@0-^c9h=vVf>z8rri{imt+Opnc&P#Yh=p$Ar3 z9~b)$9VoSjUf=;?Q?vQ-3BRJ?6OGeC(XM*dp$ptH8-gz?y?W(D{MNHUL-B$@3+S2A8=g@JGUj*4nVviNq3;4> zByuX{eGV(A55B;^bX+QJpK-G!Ms8C%H0y|8z)Kw0g!@s#?fuYz$y-i+1E3gHdWB0W5O3(*KL)#D?N zj2-k%ZH+R=6LbbE^9${pIqY&>uGsyTaZn&+fqs2lWXe>EChGC1~j;iaUA=D2_OS3 z1u0if;Hz>CY``@XBWX1Jz5z1d+oNq_K1 z>4DxK*FdK^1PVhp!8|{dB`DBqLWyhV?lQ8_9%U z#>bA!_}o}SeHM?%*C8*)&LK%qiEC(s1;ax$YekbmO{CguWdWCw=g|^$ zY4fO<)|-Qb5b~dEJx*dGuR%NW8{z~#!TL!Vanoo7-xqQSePnQ{T;GXnkXlMfneYnH zH1q1_&y0L+os@F~i%1qqp`}nxzL6{C+I+tJhmW+4hDk|m2(he`(<&g-)JJVJ3n|4O z+a9cytySs<{>Y7i_w*u>l6Ua~=Q`Rytxh#5pCQCp2 zUdz{K>Nn);Gxe+R)U-}u1BM8{fqQB!$dLF?02pYKug}!)6_@l@6aQkMKNo(IKCg5H z?C_JZ9r|6qkGG2r$=7EZi?a>}WGA%3s^sf4V?4t6hxo=gPocK|@j<>m)7eq^`b_+Ow1>D}Pch;~b2E<7_Ql=eIQcd1s z+{b*|gigdy_}m0N`T9&_Ph(K*KQX5NId>>upJ~39=+nF*5wN*I@?yRDI`8WrkOw%Az}B?Kl9U!VMiWqA^G`;*NvMU^>nl;U!O@%hYUiJr!>!I?#>@Z+Hl3g zzcTLun&s;=-%UOwdB^l@zCP1&_VBhtqVn~bnQpnhQ~XcHI~t@a9WOMQYuwwLPayZ| zC{w;Zvw>DAPQCwk7?mKRWS+cHLjRw9eP$y)pb`mcjRx`>^RmKz4ug}caXXfiO9W7_{lejTopXsVRXCydx#`T5y`b_5;B;)$d z>x4AmZN5G;U!UoO&B*9tf%Em5%%x>sM!r7N`S{LG2Pm8+&cqkzYVnI1IL_!{?9o*= zF(b!WjZAZ4hG)J$lXDrBaeP+HsGhd%csLt4HzCM$sg!%eRSBK^6GxPPC zfI_}LGhd&Xug}caXF9g*%=UbJX1+c%U!R$;&m1_{e_d?VZ)Xi zI&Q|~ddtvQ-^q&4aV^7Ka!aV*QDP$$*Ce?Rbi`N}dwT5V7?*|aHwG z+{^r3D6v_RUyN#LeAL;8nn7*wI2;yU4Gg?gI3p>{>Ny*#7y?Y z>+ET*ac*g$r`l6078&L#*6P*vp00Wm4YG4om4kO!D=Vi{QVk$tb{Oh=IfymH!s7n5 zg}%XwjDd@8e6VPtjSp5O?RfXAl!KrDhQ>E)V6R|rzt6<|5b9KY+IxjdY!;;I;X;ka;E6L6d60MbN|WxF{+In z6)W{psnp)nQ>>K=m5w?)HgTwA8_SM6yIKceRBNVQ%{ZNMsGM;z>vqP+UQjM)pdAoi z&iL|gB7z;ynA_X&RE!(2xzgFuRV&nc*bJ(tT&c9z+q-JTTCK=NIqY>%uMO;Xi4vmmawy7}7XM1f-X_5WW}N)}G0k8k<7IqIb{e$h0wivu~4) zl$r1PkZ?EuVYvH44E=*PBuEKOxw816E8Ew;;HDhiWR*l5z=m&qw?E9&GF96~k#qkkTeOHvWWxUW^9&Z1Kd1}?Oa@J zMgor2WLz_$!;O=y$xY#S9-9`>8-6!z2En96H#cy4NNgA5mgF3ELbL6&+s^QtB_Y|S zY|&p^-dm0vE3wU>%XI0X3kGSEi$r2ENp?sz8xGR^Z1ISjy1AKx8|~3v%4Z2?_T0^> zC_fe>_HOPL^`@KKc^He!f}P{zA>42Sy11yvO|smiHaqN`t?80_Oiae+Yq6=8i|?Tg zTko)S9h*D&er!(121K@#`;D7(v87LJ!X+K!uxGBNt%vH!2e%~}NDs||o6jj@F4JJ+ zO&8DbeH_YZyWKQ__Ok67({b2N(G7*TP8MONbf#(W&&|t{ZRQ4UdWVE1@An<59gBWx z7kq_IY5(2-RNnL3<MuHb=|&3PIkb{JU|g+dRkScko5Juq9JUPbQKnFX*{+Ocrgrxs%&@xiJx%(ZQBl zWj3gHn>d#Xu)xG!;n-XdU3QDDh5We51M|`%HO53Ew|)?V*mNs4gGjdpKTWzr^~zIK&o&VWS9SmtukMN}Y;}hmf>>^}1YSr!H>qUfGTvh$D*8-(g)F)m zoyRSBaHor*kv2kDnB zk6NV(%b8sW4}QD3m>a2*7m(j*L$=ujdImmGSGu_X+nVtmTg~y28^60lD4Ot0wr{WA~De=CIr5V!?O`J0US1`-@oJL3}9i8sS>oUcaslsTPcB)vH2O4 zc2nxuzKlZM?u|{?lqfln{D72hP06-n{G0Wx_i1b|$M#{p?{oAK7*TL5+vIH~KDv2? z+vQS-cF$Vinj5^iJu|YTeG%{D3pF|{q1cH$(l9It+enhE!FPb7(H^AM&9mKn(=E=} zHdCI6ugR8{m})nQQ>xq!ovi|S(`}=*V|pNz$qz{6v{lM-lVB#Da8Fsp%F^?YK+2&- z&tgJ@4HyaiaTEy`!Yz0jG!oThj2y)R+cFkhDKR4Kh>O^sM zvs$!-HbR6}P(Gq=`tIg}K18+JA?mTvG}ykk36k=zYO%alTmZfwYZw9O3}V@o|s zaLTDZ4^&0l+>$~a!7{RetQu}Rcn54_t7JFWcFby$`kxSp42YhbcaLq zjZM|%02D#Xpt$uaZ|aDLi694orw9=UMxC);B>uI?jz*y2^a`r8Ejbv6Y@+4JBveq6 z@gpqD4QD-8gsl>J%MG&G8k?sOi>GW5D&a7G9^`{WQ?glPdvRo}ol54S^D|aWn!W zz9wFwkOqRl6&8Ub5pFE%7TEOD)>A7|$oAP#kMBH^#*NKY(Jor&J8r0A3*emm7h%)` zuc2I9$P}Clcx0;!uhhErYtblq&ljt_Y!ke;XPz0T2BVuDE zB0{&xq&z9gA_v}Z6Zd3`O^*dr3ZWAc%I)w$Xpu;YJfPXUiIrIyp_^{5?S}pyc16RXpQ;r_9)g)G^;cd9C7$gV6PLDX+I_&`7jXYhmyRDZx{!FfFf*t%a?9&FO@w? z10M$|)>5K(Fx7)Uj6{%daKTo&O)(xR`sD#D*l7?^@6?0!kOOQkw#(xT#A8mCp`m1&08bJ8M5l~~u;Ac78--IRG7ARLUnBu^vs}8X zuYwqXK^RJ!DX;W%B97!rokN;%FOoRLcni-4e1Stqihh>X#Mr~k4K<>{9&rOb{0`ma zePqFn(BtT6Jr*=9wQ5Kglm>8+G;D>-8$E`_SYCAU=qa=_%qFNGsN$B(5CnF?EdG=_ zct(_|%yZxqngo@gl^b9ijUqirsCg#0{MGw2TO`^rjwt?u8jz7T$I(sn4GW9zdz4bQ z5d%Eu32kLNX0+2KKv+3?1a0{aE6aaM0{@zZ3~xe7rawp{^uaP>8{jFHOTWl|(sHs_ z_%&=_his|E{EN7n`9cb57Y#r^kWmqlHiHo!Gy;;jg?6{c%z)O3r-M%Omq*gLT8a0O zih|shHtGyGV#y#M{bs}!`P4e{ul-a8P!zT!hS&5Ov`1L!AOC_k%7I5Opz-Jq)a1VY zA=U!waeNC~XLE1_6l60;B!fdTpqvtcH5M_HQj||TLE>l^oAC3@V;%5qs1qHa6l&!@ zgq4@{K-6~oWyAsmVj-4xj&IClfX%iZx+srvAP`I>`UMw24N;X^pdb8VsS0m^{gfIR z6mgRO8TWESJ>@;KP;eWp#m978X^_`r56phh13VC0h(vH-eRGRy^HR`*HlguaTVxHH zfbVAO5N*8@q*43^H))SqO(=zSX-V9Wj|2Fq8_G}<=pcXKj0gMR2l*R%C0kF!Up1O9 z)K|Bsc4KOODM7(AeL~QNtcOQ1a5#-JkEVcJN*;)SexOtQE7#Fe59HG~F;}B4;&=l} za2X-g!Q*rj74%J8O7GESL>N9(u1EJE0mKOM1*&_53LK;J{2<#}B^t*z*w9Wt#Q4Ma z?FXB`^PjFF83k~SR#2mQt`vLlBZ7lG(HfqZchNEN02%xs>h&-#EkJk-l%pcGm~!>}UskxQdP4~_7MQz9B^EE3BD zMBDhqc@vc}INYC{y?sJq~XJ#&FMU1+s<}ger7PZ7{#5 zgovB?t=MQCNssFS3zSJ@5P^keS|l;kc8H=L8KTS*W8uPr%uo!E)u+@Tne?DMa95uL zI#`>%<~MrEkB7hTqV&)+Lw+bVc!QQ)4{xW=A2j|eKG+q4*CLJ#-}DaNvBNtI?~l4hVM>Xe>E4b1>U`Bx<} zuMrZ(bx0(t4YrDfq8i-C(oi-L0J^MI)fQ^Kj2!ih(P${A-BjbX>CQ=Fgp4ndJZKzJ zsCA;1;qTH{$A`@PVur9B@Ey+#tfg`fH$%VCT4{$*U>;ZRo3g|#qDWeYw4xC79UJ}N|0W*5r|BgmX^9K#6F^9`iS-$ z%Mw1b$Mb|V^MuG*$-|b28ORHI0^gAY7nXZ`l%rU9obZ@PKz=5D#HgBIBQF%T--8$A zpofoXN8&g@tf=;1pH$qZKTt~VO(oVJFpHxN@c+z{LG#^8(s&FsEfd{9Q;+fj6QCyF z82>_PkwDL1d7%Yx3|T@SX%67bp<4WfUk5EAmu7(fDNkal9yb{r367lzcvKEhTWzgJ zA$qWhR+ZQmG=Ul+FJ>lr8)}0=S{G!IN7Tx3L$uE0k$DUpnwKDd&okl$p$d7xf}1t4 z8uZyQK`BdbAO&8V8Y<>0vGE;NENry+pqHU9g^X!~ur5Y(`r#Zdsq*rFJOOB|_eGvr zG^7>yIXv<(6t}&KqwK&3GkIt*bPLY}S<QG06E5-!W_#H<@gv1m<=&&r6ouXj024H zZlMg6$A9w}L=P~J;3`sraM;w3T#D(jEM z2IS%{n2UYq9@!tHH|z zi7cU+asoL-*3@2@Y)Nt%uUKuvu7tkHBJ4VJ_dq@58Trve)5p>KAm%{Yr9AW^sw65m zKcv=*=CqaCp(MT?v53~jktCyY{Vq9SLwHx5rA!bya8c``U7=aQ31ffi5MSY+$1%}M zd;^3UjYfyj473If@hC%c29N^Ed%!7qKj=@5(8t3H{W$C`Ut)jcJLJF{M9YyD%j8WC zfx~MT860ImW9c370x#o}hZUr!NHXLUy+8{N&_>pv6ZF(Wa zhUi)ThT=fnL-i@}o=i1`f$-s3&hd(p5as z;(}ICPH){Kv(0}Z&8^s6I3kL=)sp#N<<)qm7W6S!#vz~Joo$9%X0~XkS#WHa)PWwx z|45WiC=DNyCtx6CG-AQWgzQ90$g%!4nP7UUx2?=ZETwmXUD1a_Na-QDBxsAT!j;Gc zr}PSJE&iBs1?Z|Q^DjJuyWthlHyR>&qAVzY?BajnClWDX+w>-p8v26v^CI>OjMeYf zGj!Yql+`bQFW5}zZnUo!pnK3(3(9=~juz6auu?=Tv=)8TE<*(o1dO3B@fFmAKiW@` z*3wd@Hns zo8kMxyWkc+jvh8IBVBSU@*{d;9+2&(IocYwgy$`?CWaGOJp6pxrqx2a!D+l1tVP81 z;k^*Qfzq@PbPi_;)`EI}(P@7RhkUtl19q7nz)oTY=VDlIg?V_V}eD$$RYs8wW9)hK#pj#3%3 z#nzxFPVG@TjD#kMyCwAKx>|8Ly-|I4t_K8qim=(WZQU3#z+1E8rUrn zl&|n7=!c_PT0WYh1%MlJ*F$NgmovPu$l?|+U>U(y>?|##=b~Nc0mP3IgcJHCZQ9WX z{2J!lkrjf_T9r&`B94T!U<$o+OxL*% z9zHK(CP?kkZ^#C?>l_vj6@V|&RE(!@#+0Bxc8Z(}7*Ezy*+V9PjqrN0jr5K-P$oH5 z^vO|rIErO4Q%lp3RdZU%4VPMD=U$l^BKHU_L2INV$oKxjXvXHCrJz zD`+u>KpsU7qSe|}XI)|a=mq>yyJ#))BG^(7iUsGbUp<2dU@4=SP|!>nsHx!=#OMZu z7Foe7Z9OD_5@buDuaPPA0JG_g$QqFb_|0>EZw9! zk#vRZfXcN@n zpk>+&0Kr{iPxMEbO8TU?PFK|}@WI%t4n`1dA&Xie$FP8JV}g;oyW^(kG-EX+#a-u4DNe0tm$&4|c>{ z*+%z3WRE+-v;2$@hX!5}nOUtfATGL4;pgU(%ntH^ZZ9+3uQo%Wf70#~5~ zf1pt~cIZ$tex)C0d_X@aP*n9W!_ejUSM5Z|lpxU*ste1?yYVs^05~ll)Qyie)HfO$^&slFj^mectjvZWs&g|b>YWC53Qwq$Nk)1 zP!7N6@9BpC_m2;ib)*uRRjYIk%$7<0kV<`2Fhu_x{XvuTl=%-kj3v}>P{)!nI=m4`SdbYeD_7Yx-an=kbq5+nNp$ALQb~S=pv}#;yU&GQMS6&ASrIwDd z6bdniLtS7!iF=k0y|r`p_bt^Pm1cc8-6wE7XS%C$2~Z{CKWq zw>~e_LQ0G`_&)k$c1lh-_f&q0^zwk;e5CKrHT+tRfs?Uf-c{p5cv(x?=M(DeAb zRICnZ&F)vrLvYgKe&ZsISQTe6=?ccCYO-}S<6A)gWHrms-FE*xHL)b5ja8wpUFlb1 z-=dgOwK$}`EvXdf9Ky=fXaGbW(w_azsuEVp#{GW9WQ$`K)v#<;42x9Kb+Rpe4QsC! zn;Vv`ei`4=TB6cQ+L-s+uxxeAq8iq>?B!iHESBD7JcwmE$zq`a9FG-wDaZeXWjV2o zFDd5TuiA{od07+x@v@w3sad`(2g3`@8PMd*a&Sm-A#^M9WjRD?gp&lnm?m9~d|8fe zjwyKpo5b}X>>xEm#v#rW#(gw#g>5#?h#TSLcoG%-*SO5d1fR}xzAOhH3>hO3F-*lx z(f1|P!&^3NB&nxMWH^okgQJ5il2GJ*Cu*C(FrYLW=a5*wEQfGGTH#ci2sKUPpc=Hq zaW$ZiG(_U)67J!^5(kNO$^Cp;j^T^+rL z3PR1mY^

    tJCWo1kRV`n7wp@fy13n63Un5ktcKLSl!37m}yR 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

    6zw5DiX zYF-1G4{XIw)JpQmfZ7xsC-9lTXkymPd~MieDvmE|#)XELSu^JcGLwD#7)QIp8Bw&( zttCMH=8E>Xj?697PQhf#?5Qs?s3sg0bEa~zVC$y#y+vw`1$1kDBvcmouk|v8%}N~k zoG00~z|k>F8@~$g6_^x2hBac2*}vPk7M*2wriE-_#tM#<{cca zNj($BB_rXBPHzgET+HY*_=fJh1s z82T0WqJ*u z7Fwy(zrSxEgYcQt=8)g+gDCa)0hU6CwKju*SS{;2pJoQ-q{$I%s{m5%2j5fLwJ=#$# z;pGERkG_IFrM@JR*Xf_rlInX=r73Jy<^>HW*D%2Te37 z!p`RO5%-L^TX2vd^V*5d`P`3=0n!;vH zVY8CQcbn6cls|>dLOH^fIhGTQWD1)l=X11*0Z&h1v+6WqxpaO^VY5(-DQp(+H9-dK z5c~-nc%0^FeefRm4O;x7)o~$ZWoi1z^|B=svSqr1b9x` z51Lmx4i=w8Cc+&{vz6@Q94`b#^G2HfJja$0Q$MdgGm`La>^QNrmPKE_(R4+(61d4# zG$dG|q$CqQNeSh12p%3PnMnnuRhvyvFZDn#Sng2(nc}^Xu3B#sw3gjZANlbmyc=$8 z3Y#Su5{ule-09BJgv7Es64EXiUZ=2GQ`oG~12)?!Y!*9a!n;AV*eO%kER?>G6jRtN zKCiTEcumNcwWG!&)ipXB|u?x z2b@2hf!jFjLA;^JOQ+zKZ0kSVg zn@%D=@v315STXS4IgQoRTEZVLxauy)B_&bip-Dh--Qf-^$YL8+Q`@p-~1#$R$~Ogp%g3e~WKnO#!Z_Y@t;# zL!dyyxEd2UL@1H9HzX{^4i5oj5Z6%uKs0Kj5Jp1AdZ)1UM(^_4SKauU;3wu?uZ>0+ z6RmpOI|b(roe*>r$CE&h>8+{PHxms1l3Gc4TDroF&4pSJZuqV6H`YqVG`K_W6kaNJ z_*k+tW6tS)I%qz_^3W1#ud`F&x-xSH!`B5N*KlanL#`7{7kVk>{!3D8!e^zcMvrr?zrmKekF5J9rRUWj`@nTjje6_Z zDQuRRF}evhHHFP8{fCjj>p9|scMNSv+@YXILN{ALv0a6UCY|PVr%jpXn#Lso#?;3L zg%xC3vQz8{t1sG@>=9lO(Y^#l87~+7R< zI%%;SuiF!g`TC4Mcy-`%y*Lvgpnfl|=4YF$uAGDDsw+S4Sh;Ne#2avMzrcSJrYlKl+;9uXo^{TMp@cwaJxra%II_#w~{!X{(X)l9$!L zc5|1pwRb5(kbBo7|X!gxVR(}{W#)jBL8oQs_JX6G7w^25LxY& zdcKHp=eh=5lo*jR-p7T)fzXMJzK=-ed%|V}(%sdMOAR(E5lrT5}Raj8nocWuB^?a^2X`ve8pcl_nqJE{10D)uL?xI_=Mao z6?D#XOwb^60<|HtiuMx&V~zk&jc(Fz?w>dDpRuXBPu!Y=QWzs7)W=^so3exw1~KtdZvfyzg9< zoi8yV^FI=QkwNtKT2>wtm*BUEq#&#~!5VyZumzz?|E;!);SU}{E}6myh6;a+e_ z?{&3Bi+d+fB)NMNoe@J)T+_LXX*cwD@Q2=nE}tHV*)@78=W(EwzuM*WpVjWL|G(+= z;s8D{JU;O!^%Jp*UN)$;ct$VmqJ#dfT{N1QrF4TWtV( zfCj0FLgI!|`0(BM4FDL|fgGT}TA@==zxa;|ctU~>wk}56$itC7@$mIjvb_t7rj%jV z-#39kkc5LY3xCd<;X3b&4j)(|z=MKKuot_g70(IfS7b9Xa9xM-})`-ouH&D;WPkm220 zYo>bO7w-?8FEg1n>dDxgTqlegt%$pfHR=o4Q@BXzwdcVWeHgQTTho&( zD+rR&DEzDOqBn9KC{V0wv`!#yuFYQp9J&T+UC}(L4@!|e9jygt71u&=xw6Yy7Of<_ zUo2_?8_B+G&j+#;h-|Wkd)`|w>Bj&s09zC4E-UE

    G|A*+LVtQUh7CMm-P7|1Zl3Y1Y;=L?3gO6i2|sSXWdOi#15^RV&0aO`dO+uB zv})to4dA)?R_)sM<83%!#D0pai1QaY!(I<)ox>OV zEdqd-3txBRIu;`0t4iTTNGG`5VA$5|u*nXeP}h~Y+ir$eLvL<*j3=0PxS+KXb?ifq zUXWlMf$Yn|qv70ET&x>1PGFnR6M<|ZAt&T7x3Kgttso&d>d+62YPhMmjNwpmTp=(C zs1u0{y>Oi`ayEtwFAeFoxI`4BI$|pXp&Nl`POWJf8*NpP=KRv^Y96H*V|rT|8b{q?P3Byen>L z+KVll%xyFx%Lki;6c_)A*9`A%)bW$38<_z`4U8MD^+#`_CC=9p=jf42}T%3D)?@z$v9Q!9%R_=}tB&Ufvq>q_gS=Uq@IlY7I zm|R(H#L1NvNM~|o%|ST8F`>1=*|NboBcqoIo;kSe1VJf05gHu)3li;6O=x=cos-Vr z98sQ+gKqQyy;eB7x}YzjPY73?r=nAeKDu|EOn6vWtAopJc=hNYf|^QzeA*q)$3;BB zTcFwtzuwo6KE`kL6MEZ8;8g7JTu%Q=xd3QG9>5_4zeE2KIl$?~LgG83wTec^ffVsP z61w_@`zoBAerWF$zg=8uxPrjUOUEVDNKQ#w@-{BP!%G%o3?p5n-C(fNKmF8h(B(H( z9!KUVG+T-nZ75AIY0HWyiI$G$Qq{O1)wpvi>cMnkVbfY!=Rlq7K#~_l+a}fLd=g?%jSU z{|GBg+6P=<@cwPxL~E_FJQsXm;R>@)G9TT#E8aDJ4({7;!J}D2=I%r9BUc6hWM!s5 zmXw9)G3z_;+xU*=ifmtIpPKRP)9s<%;0N_4JQWTqAq4DfPewM){?kAG$$E<&B^HC| zX3P2$O)IW2HiB?`lPha{P?8*ojylA^_=Mk=9>~abXi%dX4MF&NHEFy3T^5*LyPf;d z5zuaX-u`rr8#cyiFEp^%ZmU_YjIQ&(qFv~&`-n#L3V6Th;a$6*6%u=0wBhva(a*XM zjp{mD*@UHVo#(9p8Vr;w>o<0spo!2WYd`B?a%H`7+GUSM4yDbRTv@G({P_j0tStfW z(D|`5?l8 za_?OE>h)^YKmF>prk;G_F`?`8uV1{B5cb>MPY?e2>aP;Uf6@Q=vj6ebsV8jZ%I#lx z{6WWGaWOl8^4*nFw}1GfdRr-X^MnMppVHe$_t;O&e_I~PeV1D<-ad!;x3kZ1Vf^7( zvjqL|>+<*JtrBW?`IO~u{W@uA`&l`p74OIAd6$m+yk8B!O=9swTJD$kyIsxiNj~wL zo7&8b-iwdJ;=HBF8RQek6I)2Xdr3bD6^Bm*2g8csKhj8~4TMv(FOeiTIY-4YoF} zlNcc-Ie|mQ`7oUFJD<;&4BUC*aYyB~h{@lNY9w6FS}A7^M^ z_3!@6pKku|Zu?*wR{ctNOxnKc7Q^Xo6yu>!0Z!Pcb5er(_7*={Gk&y(XO!z*oo)(6Yl?r#a$kh5G#e{0j<+VryNR`PSN zST6co72P5w6-}akbgx)0yH_lWZKzv)J9Vz7q|1BnNB`0y|8=W-1=8JC+HBd;ct5%U zI=a=r?6Ui2dX=vCm;J47C(f8Ix?5NMEeX|Y>`09LkN#FazQ>gEB7@dZsEU$d*UG}a z@z+VUd~d2f5|dZ=cR&7Mr^+1v?fv!JcRyU;@7`|zuH;KaFLr->v)z63>Wg8lj{2&; zrEZya9eeitc$uzyYV)`z*B!MuU6Yr6x_e8|D1e zkIfwPE%hJU*2myu+hjd^{RupAcwa-G768sj(cBQsvn*|zn4*|Wz|=+&R^YqduPeNu=IKfGcyQh5OYO=Ho$&+HGj%uQezH?n{_i&BwKQsIkbo8`6 zp&lY~S7z3t=e^l5W=qZ*)`9FLz_FE`?QtEM+cGnvJITBrH!CJX@yZLH7w7=fAn-uw z5}i$;VF`aCd>fM&h?w<~@Fszi#qlEM!-Po@4#nCRPAzoP`&&E0)%e1$JsM&FwyI~e ze0R+K6^=!lwU08|;CYFj%dj7qg0F~KH1siHSM0<3H;^@cA)%jHuc@DPte2si8P&uN zH4FAtJNIMOLw~hroZJt*F6PhvebuinvU$SZB@9aqR7XcSlsF@%@H^%1W1PZAm}m6k zzcBnxAee{wKMlW=+yx1j#ud$sPvLh?VIx7U7XOkkL$DFX5s^aVLg%u78_Xp8Hxvrs zr|6EIpK}MORka3-VLr8M9_i2f!{{_R$Btd_1qY;dr|>(B^P&Gv4zQ{~d!6b5!Xlp-n;3lFz%*zyh=jhz+M(KSeLEi)b?z|{~ zOzMF>AD+4erAY{kxS)37SsVxf(9|F20|1=!M6C70Q|MxU--6G%g%TazFURVxM!|G) z%G#TKC=H;V>uzK7NR;C?_o{h!Es0V3fa4*d%Rs=HruhWp_M4)?>K_V>9G zf>PlH%W=GwI`t3tf$Kp_obzBwU9WH(hv&n02F4#yQot|y!}aEoGt$3+!Rs73gui)g z-}B*%+;^^bqZ&u6{{2pO=MNtJ{o!+LmJi?mndj^McIM;!<{O&a7p#u==ljEVWbc6n z;e$9Q8I7W-9E^%9PcSOSBh$GVBF@NPFS$%tM^si=NuoC8y(m+IVu1%{Uh? z)n?-O@8tC{68O^)kwmL+VY3o~C6qmRbwp1OzlOX#u7=|_9)C0SqxX%@hn_$)hRD>r z(;xg?lWzjvFIx8a?7!;Q2g2KM>7$;T!tYGsciyw2$eUC6ot(Ad+Nbb4;a{fkJ7HRIm?XgHHRCwV zcRE%PFtm4Axs zaO8oNW#)4~@~8(T z(`%nHKDo7Gi^=*;mP9f@R<=&Z*){9m%2Iy{zvG0M!tapIrtmv9sqLDd%`(4Svj)&A zkZnn`aaPY8SqqY*Ep#63-iMsH<~r;;y8BpZvh#ChKs%v1`WHDnXGkPx@c;xBKsHbU zbSI|``-A>)u|heBiKP6TN6~BEq6~A_obgzP;_8POtk%Li#nCDhFrm*-3hiy8)k92j z=6DOozfdHB;9bju8^``=_-L3z zkD)uL6L(a<&@8{$i*RnqSj~cswB=qb$A}<>V1kK+yBBIQ6cLA)Ap5kxbe=+gAe=cD zLQV5@s6RdqJxAeiY6xpcHToT*Nh9n%m2o<{Vduimo5GtV;Yrv?_sdSMS=WL&70-y} zw*|ueu|Mk<_R{wyC?#61lT-MeDf~`_K`f4c3coXj-@*S0Nt#eQG!HB@RtK5{l{JOm z35GE2Fs_rHCOq^Mey1q|A~u9+53dd`mY}s(kTW!xy9B+8gaMQ1zSIdUZxCT*h+sl@ z(I7bN5_~W&`+H)~KGKp9Bnq5~tVh=3Hqmxn(>l1O2Z79DX{HX@RX{KyyM!N%`Xhl6 z_X%_ByyN1ZgF1FZvj&+4S0$CF^IoX#1UaGc*f}d5h&gxtBWR&Et_e&V!ijg%zeVFB zL<>xmuh4s^Z}obcSTYQPd$k4Qr3Z2HvMSTN2F4V@M&&ttwX!gI-J7qSl2esFuY8(uoga* zeQ+EJ1C^lPEc?pQ6z5{}qv7B^Z+!>Mm8ww=?R<`8!Pv15m2Z@LcV8rLBFXas22 zvA9V%SRMyC!&2YL={gu`r#hLO<;o5XRyR28pu8h*8+rRR_k+vIezwW+2kbN@JDVxj zvEz^#aWugYK^~P>h3*pH;M((16hxProB;;K519$-Qo9ik3&eLcE zGqF1GYMse?<1(V8^mM_0S!TbtB*7i?>bz4 zxF1{(ZF1;lu5n2Hd*ihgFOjFXqDT*sE0FN!4Tu{i^=K~v;VbB)Q!m~Vq#Xp3*G#>~ z_Zx#qA(MAzp31 zxjA>@DKAE3EFaB|T<2JH6?EI~CyaTL`NWgIyd>MX*zq5|7Fny#Pl?^41g6_4Nc*v*+ke8SPO6~Y+6=C^GdDg9C)Rg4|WK;7H!h#XWon7 z!Nr@$7)~bd%ws<8pZ$CC&MfcE;^+IZp1d=A&u6LKj^z7(eEd6kXQF=ytE?ad3iTHiRlCKuYE}E&W>x#KSSCv#Tc?#mno8u!V^sB!U143$;)F-DT^^5+I z(BnCkp5C(hO5NfY6RE#$VSsW@pt zDm4#$XGY#QNy=koY93NXQoQjF!iDR zaG%jaUP!*_nl>RbWS>$0C0}tP?cYt{cc_zP z>xS0i2qMP?ALcj*1$LU_BZ7a5HA<9de4VJ&q?Ne*VEfAhiF5*$77Uv>2Kb8|j6^I~ zi9eqxkcq>58;@N+Na2FSZ%mzF(g-vBXd@}X^s>nzEZa@F!4H_q{16>o+GB=bebSMk6OS(9*@u*4PZX7bL2gJZ#p)hwv~cwfe%HF;;o zr!m-Bf=$_3N;^!JNk*B;JCohg$b@b-z4?wz7}6)cp7Ba#3)kexj1;QLk2SI;X|$}Q z(Gk+Sdpdrtv0x=b7NIfsLv20dzB9^w5@WnvV;_r!OagP)VqBzIEx_M~(<>AwJvh6Y znJQUY+zhLF_$MB!!!mYQWPV&aF8O%x%r5Qm&^z-uC+Ew>tWOmCay{#FkUoF8nssS| zhYufT>U{NjHOuukQkcGa?eX^eG=pg+hwd|Be)ZSu|Fc`ju$uqcgI&>uqCSglSF~2t zHTk?1$*M_uKY5TVCll&vkp~YcsoR6H0WS_2fx99P9+KpCMIKzAzH#G0{~dL8IbY23 z-$~JhqLbb)=PO11;8gR=#azDVlh0l*Jm|mUwiX`r-*M5>gZ?`yI#<-E0&dMqbq^45 zQcDtD`pwfKwJK|KCapav#VN_|q?V+>w##YJMp5VP+dU|o?PCh|)|M=>>^>(CUU|@e z$GztuEJn*|kq1xbk*ulc^n;{uUOt`1xe|^y_u=RKm1W8OcK6eRf2vp$b=`Xf&F=K9!O z`r~?-x$VEHhL+iImj90Cmu5gwH*AfF= zmI}Mv<87#KXP?o5ig0+fLJ)ZSUX>sH7q9usAIQk(U+1b{{$JeP-|p^H(l4lQcH4(s z_jKXQySER!w-4`TpZz|$qCcN~mS{$N0+K1q+c4^h(7KSY9vWmB9`wrZxxDqOJn?dN z;qTW!-aP#Mhu!S?%lqA~_D1LW{%_as{$uw1>Snn<2RONObgj0Fo7HY}qsvan8_M0A zo7w3+*3t3_W6+a^HU|Bwc^reD@bzQRZ~y<2F}OB(*Sr04PStg@UfuH2xn0iZi^X=s zjr3+c9D_b{e4+^a`Mo*6Yo@!Q8Vq8;U!`9E=r{?rEZPG8^f(Ed<8AzbuJ^mu7Y@4q zxDkIr*E5^HuIoSD@Al7DtJ&h2B<2hNm&>J%R2j|R&Yq#-hZ|VC#~V;jk9Tm5M|T{C zhi72aAMZdyJ>DUhKi)ZCZ1iBh?D}vumzDj{ht{u1ccU`W&8Q5gIVz)O9hGe^FaZ5` zcJJWVKPEnU*=WD5=Zo{S&>Tz8imxb3GyaOm_RK}b-Dnp#cW#LN)?TPw1E_-79Hp_AUOw#b#g$y0zvh}ck zi3vSyFKD-6d&)-NOEY^uXpi^@m~{HylnvjjjQ(I)Zvl}zDl75vc&wI-#d0&~pE7v_ zM$cX>#_e51Fdwvc8L4$xwi?WlcQAF0>QTdt%Z9Xb8QyjDj^#33=IMJa@xlJ}EG5mT z-oasz>2CE*sKG?>M0^5BJw^zTe$H3*-0t%|CWC(R`M8>~HSw9%jb0G$LQU z-QUeTsQq^RV)otav&D9Qeamrjwcp>2#uWoK-ELq>sL@jpP-_Jg(j>uWhzrNr7 xZRT9`+?C6}e6!oXe)ub;Ng}f9PH}AS9 + CONFIG; + _ -> + os:putenv("REBAR_GIT_CLONE_OPTIONS", "--depth 1"), + CONFIG + end, + CONFIG1 = case os:getenv("TRAVIS") of - "true" -> - JobId = os:getenv("TRAVIS_JOB_ID"), - [{coveralls_service_job_id, JobId}, - {coveralls_coverdata, "_build/test/cover/*.coverdata"}, - {coveralls_service_name , "travis-ci"} | CONFIG]; - _ -> - CONFIG -end, + "true" -> + JobId = os:getenv("TRAVIS_JOB_ID"), + [{coveralls_service_job_id, JobId}, + {coveralls_coverdata, "_build/test/cover/*.coverdata"}, + {coveralls_service_name , "travis-ci"} | CONFIG]; + _ -> + CONFIG + end, {_, Deps} = lists:keyfind(deps, 1, CONFIG1), {_, OurDeps} = lists:keyfind(github_emqx_deps, 1, CONFIG1), From d08ed351be04a9d1bcb7eb0f15cc9dd22a8cce66 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Tue, 27 Nov 2018 16:48:51 +0800 Subject: [PATCH 427/520] Do no execute rebar-clean before distclean Prior to this change, this project support to be built with rebar3 and erlang.mk meanwhile but when we want to make distclean, this project would execute rebar-clean which would get dependencies via rebar3, it slow down the procedure of distclean. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a04366033..76e048dec 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ endef include erlang.mk -clean:: gen-clean rebar-clean +clean:: gen-clean .PHONY: gen-clean gen-clean: @@ -122,7 +122,7 @@ rebar-ct: app.config rebar-clean: @rebar3 clean -distclean:: rebar-clean +distclean:: @rm -rf _build cover deps logs log data @rm -f rebar.lock compile_commands.json cuttlefish From 5c291ff23ec9b4499e50a849fa914f67cb68f5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Wed, 28 Nov 2018 15:12:22 +0800 Subject: [PATCH 428/520] Fix the 'route_batch_delete' config --- etc/emqx.conf | 2 +- src/emqx_router.erl | 4 ++-- test/emqx_router_SUITE.erl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 1780725cc..7876be59b 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1928,7 +1928,7 @@ broker.shared_dispatch_ack_enabled = false ## Enable batch clean for deleted routes. ## ## Value: Flag -broker.route_batch_clean = on +broker.route_batch_clean = off ##-------------------------------------------------------------------- ## System Monitor diff --git a/src/emqx_router.erl b/src/emqx_router.erl index b1f2e783a..8cd0d4d42 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -137,7 +137,7 @@ pick(Topic) -> init([Pool, Id]) -> rand:seed(exsplus, erlang:timestamp()), gproc_pool:connect_worker(Pool, {Pool, Id}), - Batch = #batch{enabled = emqx_config:get_env(route_batch_delete, false), + Batch = #batch{enabled = emqx_config:get_env(route_batch_clean, false), pending = sets:new()}, {ok, ensure_batch_timer(#state{pool = Pool, id = Id, batch = Batch})}. @@ -191,7 +191,7 @@ handle_cast(Msg, State) -> {noreply, State}. handle_info({timeout, _TRef, batch_delete}, State = #state{batch = Batch}) -> - _ = del_direct_routes(Batch#batch.pending), + _ = del_direct_routes(sets:to_list(Batch#batch.pending)), {noreply, ensure_batch_timer(State#state{batch = ?BATCH(true, sets:new())}), hibernate}; handle_info(Info, State) -> diff --git a/test/emqx_router_SUITE.erl b/test/emqx_router_SUITE.erl index a35da9c5d..e317ec7b3 100644 --- a/test/emqx_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -64,7 +64,7 @@ add_del_route(_) -> ?R:del_route(From, <<"a/b/c">>, node()), ?R:del_route(From, <<"a/+/b">>, node()), - timer:sleep(1), + timer:sleep(120), ?assertEqual([], lists:sort(?R:topics())). match_routes(_) -> From 309f3560f36be695e146c722c6d8561fe02a8b57 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 28 Nov 2018 10:42:53 +0800 Subject: [PATCH 429/520] Bump version to 3.0.0 --- Makefile | 2 +- src/emqx.app.src | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 76e048dec..3dce566d0 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker -PROJECT_VERSION = 3.0 +PROJECT_VERSION = 3.0.0 DEPS = jsx gproc gen_rpc ekka esockd cowboy clique diff --git a/src/emqx.app.src b/src/emqx.app.src index 296ac18ee..1bca0118b 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -1,6 +1,6 @@ {application,emqx, [{description,"EMQ X Broker"}, - {vsn,"3.0-rc.4"}, + {vsn,"3.0.0"}, {modules,[]}, {registered,[emqx_sup]}, {applications,[kernel,stdlib,jsx,gproc,gen_rpc,esockd, From 21ed012a0c79c22e32f7554cc35b7d9812671bda Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 27 Nov 2018 21:22:03 +0800 Subject: [PATCH 430/520] Add an elegant batch module --- Makefile | 3 +- src/emqx_batch.erl | 74 +++++++++++++++++++++++++++++++++++++++ test/emqx_batch_SUITE.erl | 50 ++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 src/emqx_batch.erl create mode 100644 test/emqx_batch_SUITE.erl diff --git a/Makefile b/Makefile index 3dce566d0..bedeb7250 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,8 @@ CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_session \ emqx_keepalive emqx_lib emqx_metrics emqx_mod emqx_mod_sup emqx_mqtt_caps \ emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_mountpoint \ - emqx_listeners emqx_protocol emqx_pool emqx_shared_sub emqx_bridge emqx_hooks + emqx_listeners emqx_protocol emqx_pool emqx_shared_sub emqx_bridge \ + emqx_hooks emqx_batch CT_NODE_NAME = emqxct@127.0.0.1 CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME) diff --git a/src/emqx_batch.erl b/src/emqx_batch.erl new file mode 100644 index 000000000..ffa9a7224 --- /dev/null +++ b/src/emqx_batch.erl @@ -0,0 +1,74 @@ +%% Copyright (c) 2018 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_batch). + +-export([init/1, push/2, commit/1]). +-export([size/1, items/1]). + +-type(options() :: #{ + batch_size => non_neg_integer(), + linger_ms => pos_integer(), + commit_fun := function() + }). +-export_type([options/0]). + +-record(batch, { + batch_size :: non_neg_integer(), + batch_q :: list(any()), + linger_ms :: pos_integer(), + linger_timer :: reference() | undefined, + commit_fun :: function() + }). +-type(batch() :: #batch{}). +-export_type([batch/0]). + +-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), + 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)}; + +%% 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 -> + commit(Batch#batch{batch_q = [El|Q]}); + +push(El, Batch = #batch{batch_q = Q}) -> + Batch#batch{batch_q = [El|Q]}. + +-spec(commit(batch()) -> batch()). +commit(Batch = #batch{batch_q = Q, commit_fun = Commit}) -> + _ = Commit(lists:reverse(Q)), + reset(Batch). + +reset(Batch = #batch{linger_timer = TRef}) -> + _ = emqx_misc:cancel_timer(TRef), + Batch#batch{batch_q = [], linger_timer = undefined}. + +-spec(size(batch()) -> non_neg_integer()). +size(#batch{batch_q = Q}) -> + length(Q). + +-spec(items(batch()) -> list(any())). +items(#batch{batch_q = Q}) -> + lists:reverse(Q). + diff --git a/test/emqx_batch_SUITE.erl b/test/emqx_batch_SUITE.erl new file mode 100644 index 000000000..c4c69080b --- /dev/null +++ b/test/emqx_batch_SUITE.erl @@ -0,0 +1,50 @@ +%% Copyright (c) 2018 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_batch_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> + [batch_full_commit, batch_linger_commit]. + +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)), + %% Trigger commit fun. + B4 = emqx_batch:push(a, B3), + ?assertEqual(0, emqx_batch:size(B4)), + ?assertEqual([], emqx_batch:items(B4)). + +batch_linger_commit(_) -> + CommitFun = fun(Q) -> ?assertEqual(3, length(Q)) end, + 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)), + receive + batch_linger_expired -> + B4 = emqx_batch:commit(B3), + ?assertEqual(0, emqx_batch:size(B4)), + ?assertEqual([], emqx_batch:items(B4)) + after + 1000 -> + error(linger_timer_not_triggered) + end. + From 76d5dedb3b7b525cec019a1bb7d04ab5961973b1 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 30 Nov 2018 14:29:34 +0800 Subject: [PATCH 431/520] Remove simple log handler at startup (#2000) The simple logger handler is not removed if we set the `log.to=file` in emqx.conf. This might be an issue of OTP logger: https://bugs.erlang.org/browse/ERL-788 I set the error_logger to silent as a workaround. --- priv/emqx.schema | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index 3838db31e..1001ab5a8 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -426,12 +426,18 @@ end}. {datatype, file} ]}. -{mapping, "sasl", "sasl.sasl_error_logger", [ +{mapping, "log.sasl", "sasl.sasl_error_logger", [ {default, off}, {datatype, flag}, hidden ]}. +{mapping, "log.error_logger", "kernel.error_logger", [ + {default, silent}, + {datatype, {enum, [silent]}}, + hidden +]}. + {translation, "emqx.primary_log_level", fun(Conf) -> cuttlefish:conf_get("log.level", Conf) end}. From 194dbc02c8bfcc6db52ea6cb8809225c34177bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Thu, 29 Nov 2018 17:56:50 +0800 Subject: [PATCH 432/520] Add batch commit for metrics --- etc/emqx.conf | 9 +++++++ priv/emqx.schema | 9 +++++++ src/emqx_broker.erl | 9 +++++-- src/emqx_connection.erl | 8 ++++++ src/emqx_metrics.erl | 49 ++++++++++++++++++++++++++++++++----- src/emqx_session.erl | 9 +++++++ src/emqx_ws_connection.erl | 9 +++++++ test/emqx_metrics_SUITE.erl | 4 +++ test/emqx_session_SUITE.erl | 1 + 9 files changed, 99 insertions(+), 8 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 7876be59b..d23ee2d5f 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -479,6 +479,15 @@ mqtt.shared_subscription = true ## Value: true | false mqtt.ignore_loop_deliver = false +##-------------------------------------------------------------------- +## Metric +##-------------------------------------------------------------------- + +## Commit interval for metric +## +## Value: Duration +metric.commit_interval = 10s + ##-------------------------------------------------------------------- ## Zones ##-------------------------------------------------------------------- diff --git a/priv/emqx.schema b/priv/emqx.schema index 1001ab5a8..70ded106c 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -616,6 +616,15 @@ end}. {datatype, {enum, [true, false]}} ]}. +%%-------------------------------------------------------------------- +%% Metirc +%%-------------------------------------------------------------------- +%% @doc Commit interval for metric +{mapping, "metric.commit_interval", "emqx.metric_commit_interval", [ + {default, "10s"}, + {datatype, {duration, ms}} +]}. + %%-------------------------------------------------------------------- %% Zones %%-------------------------------------------------------------------- diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index ff53554d4..d684c6273 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -321,14 +321,14 @@ topics() -> emqx_router:topics(). init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), + MetricCommitInterval = emqx_config:get_env(metric_commit_interval, 10000), + emqx_metrics:start_timer(MetricCommitInterval, MetricCommitInterval div 2, {metric_commit, MetricCommitInterval}), {ok, #state{pool = Pool, id = Id, submap = #{}, submon = emqx_pmon:new()}}. handle_call(Req, _From, State) -> emqx_logger:error("[Broker] unexpected call: ~p", [Req]), {reply, ignored, State}. - - handle_cast({From, #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}}, State) -> Subscriber = {SubPid, SubId}, case ets:member(?SUBOPTION, {Topic, Subscriber}) of @@ -373,6 +373,11 @@ handle_info({'DOWN', _MRef, process, SubPid, Reason}, State = #state{submap = Su {noreply, State} end; +handle_info({timeout, _Timer, {metric_commit, MetricCommitInterval}}, State) -> + emqx_metrics:commit(), + emqx_metrics:start_timer(MetricCommitInterval, {metric_commit, MetricCommitInterval}), + {noreply, State}; + handle_info(Info, State) -> emqx_logger:error("[Broker] unexpected info: ~p", [Info]), {noreply, State}. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 609ce25d6..3516ec8bd 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -154,6 +154,10 @@ init([Transport, RawSocket, Options]) -> ok = emqx_misc:init_proc_mng_policy(Zone), emqx_logger:set_metadata_peername(esockd_net:format(Peername)), + MetricCommitInterval = emqx_config:get_env(metric_commit_interval, 10000), + emqx_metrics:start_timer(MetricCommitInterval, + MetricCommitInterval div 2, + {metric_commit, MetricCommitInterval}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> @@ -228,6 +232,10 @@ handle_info({timeout, Timer, emit_stats}, ?LOG(warning, "shutdown due to ~p", [Reason]), shutdown(Reason, NewState) end; +handle_info({timeout, _Timer, {metric_commit, MetricCommitInterval}}, State) -> + emqx_metrics:commit(), + emqx_metrics:start_timer(MetricCommitInterval, {metric_commit, MetricCommitInterval}), + {noreply, State}; handle_info(timeout, State) -> shutdown(idle_timeout, State); diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 8130a307e..443af5a4b 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -18,7 +18,8 @@ -export([start_link/0]). -export([new/1, all/0]). --export([val/1, inc/1, inc/2, inc/3, dec/2, dec/3, set/2]). +-export([val/1, inc/1, inc/2, inc/3, dec/2, dec/3, set/2, commit/0]). +-export([start_timer/2, start_timer/3]). %% Received/sent metrics -export([received/1, sent/1]). @@ -133,10 +134,8 @@ inc(Metric, Val) when is_atom(Metric) -> %% @doc Increase metric value -spec(inc(counter | gauge, atom(), pos_integer()) -> pos_integer()). -inc(gauge, Metric, Val) -> - update_counter(key(gauge, Metric), {2, Val}); -inc(counter, Metric, Val) -> - update_counter(key(counter, Metric), {2, Val}). +inc(Type, Metric, Val) -> + hold(Type, Metric, Val). %% @doc Decrease metric value -spec(dec(gauge, atom()) -> integer()). @@ -146,7 +145,7 @@ dec(gauge, Metric) -> %% @doc Decrease metric value -spec(dec(gauge, atom(), pos_integer()) -> integer()). dec(gauge, Metric, Val) -> - update_counter(key(gauge, Metric), {2, -Val}). + hold(gauge, Metric, -Val). %% @doc Set metric value set(Metric, Val) when is_atom(Metric) -> @@ -154,6 +153,44 @@ set(Metric, Val) when is_atom(Metric) -> set(gauge, Metric, Val) -> ets:insert(?TAB, {key(gauge, Metric), Val}). +% -spec(hold(counter | gauge, atom(), inc_dec | assign, integer()) -> integer()). +hold(Type, Metric, Val) when Type =:= counter orelse Type =:= gauge -> + NewMetrics = case get(metrics) of + undefined -> + #{Metric => {Type, Val}}; + Metrics -> + {Type, Count} = maps:get(Metric, Metrics, {Type, 0}), + Metrics#{Metric => {Type, Count + Val}} + end, + put(metrics, NewMetrics). + +commit() -> + case get(metrics) of + undefined -> + ok; + Metrics -> + maps:fold(fun(Metric, {Type, Val}, Acc) -> + update_counter(key(Type, Metric), {2, Val}), + Acc + end, 0, Metrics), + put(metrics, #{}) + end. + +-spec(start_timer(integer(), term()) -> reference() | undefined). +start_timer(Interval, Msg) -> + start_timer(Interval, 0, Msg). + +-spec(start_timer(integer(), integer(), term()) -> reference() | undefined). +start_timer(Interval, MaxJitter, Msg) when Interval > 0 -> + emqx_misc:start_timer((Interval + case MaxJitter >= 1 of + true -> + rand:uniform(MaxJitter); + false -> + 0 + end), Msg); +start_timer(_Interval, _Jitter, _Msg) -> + undefined. + %% @doc Metric key key(gauge, Metric) -> {Metric, 0}; diff --git a/src/emqx_session.erl b/src/emqx_session.erl index b223dfdda..8ae810bfe 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -377,6 +377,10 @@ init([Parent, #{zone := Zone, ok = emqx_gc:init(GcPolicy), ok = emqx_misc:init_proc_mng_policy(Zone), ok = proc_lib:init_ack(Parent, {ok, self()}), + MetricCommitInterval = emqx_config:get_env(metric_commit_interval, 10000), + emqx_metrics:start_timer(MetricCommitInterval, + MetricCommitInterval div 2, + {metric_commit, MetricCommitInterval}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State). init_mqueue(Zone) -> @@ -624,6 +628,11 @@ handle_info({timeout, Timer, will_delay}, State = #state{will_msg = WillMsg, wil send_willmsg(WillMsg), {noreply, State#state{will_msg = undefined}}; +handle_info({timeout, _Timer, {metric_commit, MetricCommitInterval}}, State) -> + emqx_metrics:commit(), + emqx_metrics:start_timer(MetricCommitInterval, {metric_commit, MetricCommitInterval}), + {noreply, State}; + handle_info({'EXIT', ConnPid, Reason}, State = #state{will_msg = WillMsg, expiry_interval = 0, conn_pid = ConnPid}) -> send_willmsg(WillMsg), {stop, Reason, State#state{will_msg = undefined, conn_pid = undefined}}; diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 77d2c058f..ae03f61dc 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -137,6 +137,10 @@ websocket_init(#state{request = Req, options = Options}) -> lists:foreach(fun(Stat) -> put(Stat, 0) end, ?SOCK_STATS), emqx_logger:set_metadata_peername(esockd_net:format(Peername)), + MetricCommitInterval = emqx_config:get_env(metric_commit_interval, 10000), + emqx_metrics:start_timer(MetricCommitInterval, + MetricCommitInterval div 2, + {metric_commit, MetricCommitInterval}), {ok, #state{peername = Peername, sockname = Sockname, parser_state = ParserState, @@ -226,6 +230,11 @@ websocket_info({timeout, Timer, emit_stats}, emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), {ok, State#state{stats_timer = undefined}, hibernate}; +websocket_info({timeout, _Timer, {metric_commit, MetricCommitInterval}}, State) -> + emqx_metrics:commit(), + emqx_metrics:start_timer(MetricCommitInterval, {metric_commit, MetricCommitInterval}), + {ok, State, 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 diff --git a/test/emqx_metrics_SUITE.erl b/test/emqx_metrics_SUITE.erl index 8e601562c..b6f2d42a1 100644 --- a/test/emqx_metrics_SUITE.erl +++ b/test/emqx_metrics_SUITE.erl @@ -29,11 +29,15 @@ t_inc_dec_metrics(_) -> emqx_metrics:inc(counter, 'bytes/received', 2), emqx_metrics:inc({gauge, 'messages/retained'}, 2), emqx_metrics:inc(gauge, 'messages/retained', 2), + emqx_metrics:commit(), {5, 4} = {emqx_metrics:val('bytes/received'), emqx_metrics:val('messages/retained')}, emqx_metrics:dec(gauge, 'messages/retained'), emqx_metrics:dec(gauge, 'messages/retained', 1), + emqx_metrics:commit(), 2 = emqx_metrics:val('messages/retained'), emqx_metrics:received(#mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}}), + emqx_metrics:commit(), {1, 1} = {emqx_metrics:val('packets/received'), emqx_metrics:val('packets/connect')}, emqx_metrics:sent(#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}}), + emqx_metrics:commit(), {1, 1} = {emqx_metrics:val('packets/sent'), emqx_metrics:val('packets/connack')}. diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index 2bae869b6..d1a7357b1 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -45,6 +45,7 @@ ignore_loop(_Config) -> application:set_env(emqx, mqtt_ignore_loop_deliver, false). t_session_all(_) -> + application:set_env(emqx, metric_commit_interval, 10), ClientId = <<"ClientId">>, {ok, ConnPid} = emqx_mock_client:start_link(ClientId), {ok, SPid} = emqx_mock_client:open_session(ConnPid, ClientId, internal), From ea62b15c874bc3c5e98ff15185c4382b5951dbc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 30 Nov 2018 14:14:28 +0800 Subject: [PATCH 433/520] Alter apis provided by emqx_metrics, and use existing timer to commit metrics --- etc/emqx.conf | 9 -------- priv/emqx.schema | 9 -------- src/emqx_broker.erl | 7 ------- src/emqx_connection.erl | 13 +++--------- src/emqx_metrics.erl | 42 +++++++++++++++++++------------------ src/emqx_session.erl | 24 +++++++-------------- test/emqx_metrics_SUITE.erl | 22 ++++++++++++++----- test/emqx_session_SUITE.erl | 1 + 8 files changed, 51 insertions(+), 76 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index d23ee2d5f..7876be59b 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -479,15 +479,6 @@ mqtt.shared_subscription = true ## Value: true | false mqtt.ignore_loop_deliver = false -##-------------------------------------------------------------------- -## Metric -##-------------------------------------------------------------------- - -## Commit interval for metric -## -## Value: Duration -metric.commit_interval = 10s - ##-------------------------------------------------------------------- ## Zones ##-------------------------------------------------------------------- diff --git a/priv/emqx.schema b/priv/emqx.schema index 70ded106c..1001ab5a8 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -616,15 +616,6 @@ end}. {datatype, {enum, [true, false]}} ]}. -%%-------------------------------------------------------------------- -%% Metirc -%%-------------------------------------------------------------------- -%% @doc Commit interval for metric -{mapping, "metric.commit_interval", "emqx.metric_commit_interval", [ - {default, "10s"}, - {datatype, {duration, ms}} -]}. - %%-------------------------------------------------------------------- %% Zones %%-------------------------------------------------------------------- diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index d684c6273..b7e0e457e 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -321,8 +321,6 @@ topics() -> emqx_router:topics(). init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), - MetricCommitInterval = emqx_config:get_env(metric_commit_interval, 10000), - emqx_metrics:start_timer(MetricCommitInterval, MetricCommitInterval div 2, {metric_commit, MetricCommitInterval}), {ok, #state{pool = Pool, id = Id, submap = #{}, submon = emqx_pmon:new()}}. handle_call(Req, _From, State) -> @@ -373,11 +371,6 @@ handle_info({'DOWN', _MRef, process, SubPid, Reason}, State = #state{submap = Su {noreply, State} end; -handle_info({timeout, _Timer, {metric_commit, MetricCommitInterval}}, State) -> - emqx_metrics:commit(), - emqx_metrics:start_timer(MetricCommitInterval, {metric_commit, MetricCommitInterval}), - {noreply, State}; - handle_info(Info, State) -> emqx_logger:error("[Broker] unexpected info: ~p", [Info]), {noreply, State}. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 3516ec8bd..3d0ab0df7 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -154,10 +154,6 @@ init([Transport, RawSocket, Options]) -> ok = emqx_misc:init_proc_mng_policy(Zone), emqx_logger:set_metadata_peername(esockd_net:format(Peername)), - MetricCommitInterval = emqx_config:get_env(metric_commit_interval, 10000), - emqx_metrics:start_timer(MetricCommitInterval, - MetricCommitInterval div 2, - {metric_commit, MetricCommitInterval}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> @@ -174,7 +170,7 @@ send_fun(Transport, Socket) -> Data = emqx_frame:serialize(Packet, Options), try Transport:async_send(Socket, Data) of ok -> - emqx_metrics:inc('bytes/sent', iolist_size(Data)), + emqx_metrics:trans(inc, 'bytes/sent', iolist_size(Data)), ok; Error -> Error catch @@ -219,6 +215,7 @@ handle_info({timeout, Timer, emit_stats}, State = #state{stats_timer = Timer, proto_state = ProtoState }) -> + emqx_metrics:commit(), emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), NewState = State#state{stats_timer = undefined}, Limits = erlang:get(force_shutdown_policy), @@ -232,10 +229,6 @@ handle_info({timeout, Timer, emit_stats}, ?LOG(warning, "shutdown due to ~p", [Reason]), shutdown(Reason, NewState) end; -handle_info({timeout, _Timer, {metric_commit, MetricCommitInterval}}, State) -> - emqx_metrics:commit(), - emqx_metrics:start_timer(MetricCommitInterval, {metric_commit, MetricCommitInterval}), - {noreply, State}; handle_info(timeout, State) -> shutdown(idle_timeout, State); @@ -256,7 +249,7 @@ handle_info(activate_sock, State) -> handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) -> ?LOG(debug, "RECV ~p", [Data]), Size = iolist_size(Data), - emqx_metrics:inc('bytes/received', Size), + emqx_metrics:trans(inc, 'bytes/received', Size), Incoming = #{bytes => Size, packets => 0}, handle_packet(Data, State#state{await_recv = false, incoming = Incoming}); diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 443af5a4b..ef2be7e10 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -18,8 +18,8 @@ -export([start_link/0]). -export([new/1, all/0]). --export([val/1, inc/1, inc/2, inc/3, dec/2, dec/3, set/2, commit/0]). --export([start_timer/2, start_timer/3]). +-export([val/1, inc/1, inc/2, inc/3, dec/2, dec/3, set/2]). +-export([trans/2, trans/3, trans/4, commit/0]). %% Received/sent metrics -export([received/1, sent/1]). @@ -135,7 +135,7 @@ inc(Metric, Val) when is_atom(Metric) -> %% @doc Increase metric value -spec(inc(counter | gauge, atom(), pos_integer()) -> pos_integer()). inc(Type, Metric, Val) -> - hold(Type, Metric, Val). + update_counter(key(Type, Metric), {2, Val}). %% @doc Decrease metric value -spec(dec(gauge, atom()) -> integer()). @@ -145,7 +145,7 @@ dec(gauge, Metric) -> %% @doc Decrease metric value -spec(dec(gauge, atom(), pos_integer()) -> integer()). dec(gauge, Metric, Val) -> - hold(gauge, Metric, -Val). + update_counter(key(gauge, Metric), {2, -Val}). %% @doc Set metric value set(Metric, Val) when is_atom(Metric) -> @@ -153,7 +153,24 @@ set(Metric, Val) when is_atom(Metric) -> set(gauge, Metric, Val) -> ets:insert(?TAB, {key(gauge, Metric), Val}). -% -spec(hold(counter | gauge, atom(), inc_dec | assign, integer()) -> integer()). +trans(inc, Metric) -> + trans(inc, {counter, Metric}, 1). + +trans(Opt, {gauge, Metric}, Val) -> + trans(Opt, gauge, Metric, Val); +trans(inc, {counter, Metric}, Val) -> + trans(inc, counter, Metric, Val); +trans(inc, Metric, Val) when is_atom(Metric) -> + trans(inc, counter, Metric, Val); +trans(dec, gauge, Metric) -> + trans(dec, gauge, Metric, 1). + +trans(inc, Type, Metric, Val) -> + hold(Type, Metric, Val); +trans(dec, gauge, Metric, Val) -> + hold(gauge, Metric, -Val). + +% -spec(hold(counter | gauge, atom(), integer()) -> integer()). hold(Type, Metric, Val) when Type =:= counter orelse Type =:= gauge -> NewMetrics = case get(metrics) of undefined -> @@ -176,21 +193,6 @@ commit() -> put(metrics, #{}) end. --spec(start_timer(integer(), term()) -> reference() | undefined). -start_timer(Interval, Msg) -> - start_timer(Interval, 0, Msg). - --spec(start_timer(integer(), integer(), term()) -> reference() | undefined). -start_timer(Interval, MaxJitter, Msg) when Interval > 0 -> - emqx_misc:start_timer((Interval + case MaxJitter >= 1 of - true -> - rand:uniform(MaxJitter); - false -> - 0 - end), Msg); -start_timer(_Interval, _Jitter, _Msg) -> - undefined. - %% @doc Metric key key(gauge, Metric) -> {Metric, 0}; diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 8ae810bfe..12718b9fc 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -377,10 +377,6 @@ init([Parent, #{zone := Zone, ok = emqx_gc:init(GcPolicy), ok = emqx_misc:init_proc_mng_policy(Zone), ok = proc_lib:init_ack(Parent, {ok, self()}), - MetricCommitInterval = emqx_config:get_env(metric_commit_interval, 10000), - emqx_metrics:start_timer(MetricCommitInterval, - MetricCommitInterval div 2, - {metric_commit, MetricCommitInterval}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State). init_mqueue(Zone) -> @@ -425,7 +421,7 @@ handle_call({register_publish_packet_id, PacketId, Ts}, _From, {ok, ensure_await_rel_timer(State1)} end; true -> - emqx_metrics:inc('messages/qos2/dropped'), + emqx_metrics:trans(inc, 'messages/qos2/dropped'), ?LOG(warning, "Dropped qos2 packet ~w for too many awaiting_rel", [PacketId], State), {{error, ?RC_RECEIVE_MAXIMUM_EXCEEDED}, State} end); @@ -436,7 +432,7 @@ handle_call({pubrec, PacketId, _ReasonCode}, _From, State = #state{inflight = In true -> {ok, acked(pubrec, PacketId, State)}; false -> - emqx_metrics:inc('packets/pubrec/missed'), + emqx_metrics:trans(inc, 'packets/pubrec/missed'), ?LOG(warning, "The PUBREC PacketId ~w is not found.", [PacketId], State), {{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State} end); @@ -447,7 +443,7 @@ handle_call({pubrel, PacketId, _ReasonCode}, _From, State = #state{awaiting_rel {_Ts, AwaitingRel1} -> {ok, State#state{awaiting_rel = AwaitingRel1}}; error -> - emqx_metrics:inc('packets/pubrel/missed'), + emqx_metrics:trans(inc, 'packets/pubrel/missed'), ?LOG(warning, "Cannot find PUBREL: ~w", [PacketId], State), {{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State} end); @@ -506,7 +502,7 @@ handle_cast({puback, PacketId, _ReasonCode}, State = #state{inflight = Inflight} noreply(dequeue(acked(puback, PacketId, State))); false -> ?LOG(warning, "The PUBACK PacketId ~w is not found", [PacketId], State), - emqx_metrics:inc('packets/puback/missed'), + emqx_metrics:trans(inc, 'packets/puback/missed'), {noreply, State} end; @@ -517,7 +513,7 @@ handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight noreply(dequeue(acked(pubcomp, PacketId, State))); false -> ?LOG(warning, "The PUBCOMP PacketId ~w is not found", [PacketId], State), - emqx_metrics:inc('packets/pubcomp/missed'), + emqx_metrics:trans(inc, 'packets/pubcomp/missed'), {noreply, State} end; @@ -607,6 +603,7 @@ handle_info({timeout, Timer, check_awaiting_rel}, State = #state{await_rel_timer handle_info({timeout, Timer, emit_stats}, State = #state{client_id = ClientId, stats_timer = Timer}) -> + emqx_metrics:commit(), _ = emqx_sm:set_session_stats(ClientId, stats(State)), NewState = State#state{stats_timer = undefined}, Limits = erlang:get(force_shutdown_policy), @@ -628,11 +625,6 @@ handle_info({timeout, Timer, will_delay}, State = #state{will_msg = WillMsg, wil send_willmsg(WillMsg), {noreply, State#state{will_msg = undefined}}; -handle_info({timeout, _Timer, {metric_commit, MetricCommitInterval}}, State) -> - emqx_metrics:commit(), - emqx_metrics:start_timer(MetricCommitInterval, {metric_commit, MetricCommitInterval}), - {noreply, State}; - handle_info({'EXIT', ConnPid, Reason}, State = #state{will_msg = WillMsg, expiry_interval = 0, conn_pid = ConnPid}) -> send_willmsg(WillMsg), {stop, Reason, State#state{will_msg = undefined, conn_pid = undefined}}; @@ -743,7 +735,7 @@ retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now, {publish, {PacketId, Msg}} -> case emqx_message:is_expired(Msg) of true -> - emqx_metrics:inc('messages/expired'), + emqx_metrics:trans(inc, 'messages/expired'), emqx_inflight:delete(PacketId, Inflight); false -> redeliver({PacketId, Msg}, State), @@ -783,7 +775,7 @@ expire_awaiting_rel([{PacketId, Ts} | More], Now, State = #state{awaiting_rel = AwaitingRel, await_rel_timeout = Timeout}) -> case (timer:now_diff(Now, Ts) div 1000) of Age when Age >= Timeout -> - emqx_metrics:inc('messages/qos2/expired'), + emqx_metrics:trans(inc, 'messages/qos2/expired'), ?LOG(warning, "Dropped qos2 packet ~s for await_rel_timeout", [PacketId], State), expire_awaiting_rel(More, Now, State#state{awaiting_rel = maps:remove(PacketId, AwaitingRel)}); Age -> diff --git a/test/emqx_metrics_SUITE.erl b/test/emqx_metrics_SUITE.erl index b6f2d42a1..4b0a99518 100644 --- a/test/emqx_metrics_SUITE.erl +++ b/test/emqx_metrics_SUITE.erl @@ -19,7 +19,7 @@ -include("emqx_mqtt.hrl"). -all() -> [t_inc_dec_metrics]. +all() -> [t_inc_dec_metrics, t_trans]. t_inc_dec_metrics(_) -> {ok, _} = emqx_metrics:start_link(), @@ -29,15 +29,27 @@ t_inc_dec_metrics(_) -> emqx_metrics:inc(counter, 'bytes/received', 2), emqx_metrics:inc({gauge, 'messages/retained'}, 2), emqx_metrics:inc(gauge, 'messages/retained', 2), - emqx_metrics:commit(), {5, 4} = {emqx_metrics:val('bytes/received'), emqx_metrics:val('messages/retained')}, emqx_metrics:dec(gauge, 'messages/retained'), emqx_metrics:dec(gauge, 'messages/retained', 1), - emqx_metrics:commit(), 2 = emqx_metrics:val('messages/retained'), emqx_metrics:received(#mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}}), - emqx_metrics:commit(), {1, 1} = {emqx_metrics:val('packets/received'), emqx_metrics:val('packets/connect')}, emqx_metrics:sent(#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}}), - emqx_metrics:commit(), {1, 1} = {emqx_metrics:val('packets/sent'), emqx_metrics:val('packets/connack')}. + +t_trans(_) -> + {ok, _} = emqx_metrics:start_link(), + emqx_metrics:trans(inc, 'bytes/received'), + emqx_metrics:trans(inc, {counter, 'bytes/received'}, 2), + emqx_metrics:trans(inc, counter, 'bytes/received', 2), + emqx_metrics:trans(inc, {gauge, 'messages/retained'}, 2), + emqx_metrics:trans(inc, gauge, 'messages/retained', 2), + {0, 0} = {emqx_metrics:val('bytes/received'), emqx_metrics:val('messages/retained')}, + emqx_metrics:commit(), + {5, 4} = {emqx_metrics:val('bytes/received'), emqx_metrics:val('messages/retained')}, + emqx_metrics:trans(dec, gauge, 'messages/retained'), + emqx_metrics:trans(dec, gauge, 'messages/retained', 1), + 4 = emqx_metrics:val('messages/retained'), + emqx_metrics:commit(), + 2 = emqx_metrics:val('messages/retained'). diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index d1a7357b1..c45d6ba9b 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -45,6 +45,7 @@ ignore_loop(_Config) -> application:set_env(emqx, mqtt_ignore_loop_deliver, false). t_session_all(_) -> + emqx_zone:set_env(internal, idle_timeout, 100), application:set_env(emqx, metric_commit_interval, 10), ClientId = <<"ClientId">>, {ok, ConnPid} = emqx_mock_client:start_link(ClientId), From c87aabbbeb2a36ab7fc3e39dac9eeba961c70a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 30 Nov 2018 14:17:01 +0800 Subject: [PATCH 434/520] Some forgotten changes in last commit --- src/emqx_ws_connection.erl | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index ae03f61dc..180edc8f0 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -137,10 +137,6 @@ websocket_init(#state{request = Req, options = Options}) -> lists:foreach(fun(Stat) -> put(Stat, 0) end, ?SOCK_STATS), emqx_logger:set_metadata_peername(esockd_net:format(Peername)), - MetricCommitInterval = emqx_config:get_env(metric_commit_interval, 10000), - emqx_metrics:start_timer(MetricCommitInterval, - MetricCommitInterval div 2, - {metric_commit, MetricCommitInterval}), {ok, #state{peername = Peername, sockname = Sockname, parser_state = ParserState, @@ -152,7 +148,7 @@ send_fun(WsPid) -> fun(Packet, Options) -> Data = emqx_frame:serialize(Packet, Options), BinSize = iolist_size(Data), - emqx_metrics:inc('bytes/sent', BinSize), + emqx_metrics:trans(inc, 'bytes/sent', BinSize), put(send_oct, get(send_oct) + BinSize), put(send_cnt, get(send_cnt) + 1), WsPid ! {binary, iolist_to_binary(Data)}, @@ -171,7 +167,7 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, BinSize = iolist_size(Data), put(recv_oct, get(recv_oct) + BinSize), ?LOG(debug, "RECV ~p", [Data]), - emqx_metrics:inc('bytes/received', BinSize), + emqx_metrics:trans(inc, 'bytes/received', BinSize), case catch emqx_frame:parse(iolist_to_binary(Data), ParserState) of {more, NewParserState} -> {ok, State#state{parser_state = NewParserState}}; @@ -227,14 +223,10 @@ websocket_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> websocket_info({timeout, Timer, emit_stats}, State = #state{stats_timer = Timer, proto_state = ProtoState}) -> + emqx_metrics:commit(), emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), {ok, State#state{stats_timer = undefined}, hibernate}; -websocket_info({timeout, _Timer, {metric_commit, MetricCommitInterval}}, State) -> - emqx_metrics:commit(), - emqx_metrics:start_timer(MetricCommitInterval, {metric_commit, MetricCommitInterval}), - {ok, State, 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 From f315994eb9fc82b07756c10370425587a057983f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 30 Nov 2018 14:24:29 +0800 Subject: [PATCH 435/520] Remove unnecessary spec --- src/emqx_metrics.erl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index ef2be7e10..2d8ac9591 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -170,7 +170,6 @@ trans(inc, Type, Metric, Val) -> trans(dec, gauge, Metric, Val) -> hold(gauge, Metric, -Val). -% -spec(hold(counter | gauge, atom(), integer()) -> integer()). hold(Type, Metric, Val) when Type =:= counter orelse Type =:= gauge -> NewMetrics = case get(metrics) of undefined -> From ad8b547519f634a0b1980c40c6ea7aa37f196533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 30 Nov 2018 17:08:25 +0800 Subject: [PATCH 436/520] Make batch committing of metrics more elegant --- src/emqx_metrics.erl | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 2d8ac9591..fb57cbdf9 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -171,25 +171,21 @@ trans(dec, gauge, Metric, Val) -> hold(gauge, Metric, -Val). hold(Type, Metric, Val) when Type =:= counter orelse Type =:= gauge -> - NewMetrics = case get(metrics) of + put('$metrics', case get('$metrics') of undefined -> - #{Metric => {Type, Val}}; + #{{Type, Metric} => Val}; Metrics -> - {Type, Count} = maps:get(Metric, Metrics, {Type, 0}), - Metrics#{Metric => {Type, Count + Val}} - end, - put(metrics, NewMetrics). + maps:update_with({Type, Metric}, fun(Cnt) -> Cnt + Val end, Val, Metrics) + end). commit() -> - case get(metrics) of - undefined -> - ok; + case get('$metrics') of + undefined -> ok; Metrics -> - maps:fold(fun(Metric, {Type, Val}, Acc) -> - update_counter(key(Type, Metric), {2, Val}), - Acc + maps:fold(fun({Type, Metric}, Val, _Acc) -> + update_counter(key(Type, Metric), {2, Val}) end, 0, Metrics), - put(metrics, #{}) + erase('$metrics') end. %% @doc Metric key From ddb9eaef7bf0a4d88d3453c7335bd4bae3e9e10a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 30 Nov 2018 17:20:34 +0800 Subject: [PATCH 437/520] Improve test coverage of emqx_metrics --- test/emqx_metrics_SUITE.erl | 5 ++++- test/emqx_session_SUITE.erl | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/emqx_metrics_SUITE.erl b/test/emqx_metrics_SUITE.erl index 4b0a99518..ead7e98ba 100644 --- a/test/emqx_metrics_SUITE.erl +++ b/test/emqx_metrics_SUITE.erl @@ -26,13 +26,16 @@ t_inc_dec_metrics(_) -> {0, 0} = {emqx_metrics:val('bytes/received'), emqx_metrics:val('messages/retained')}, emqx_metrics:inc('bytes/received'), emqx_metrics:inc({counter, 'bytes/received'}, 2), - emqx_metrics:inc(counter, 'bytes/received', 2), + emqx_metrics:inc(counter, 'bytes/received', 1), + emqx_metrics:inc('bytes/received', 1), emqx_metrics:inc({gauge, 'messages/retained'}, 2), emqx_metrics:inc(gauge, 'messages/retained', 2), {5, 4} = {emqx_metrics:val('bytes/received'), emqx_metrics:val('messages/retained')}, emqx_metrics:dec(gauge, 'messages/retained'), emqx_metrics:dec(gauge, 'messages/retained', 1), 2 = emqx_metrics:val('messages/retained'), + emqx_metrics:set('messages/retained', 3), + 3 = emqx_metrics:val('messages/retained'), emqx_metrics:received(#mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}}), {1, 1} = {emqx_metrics:val('packets/received'), emqx_metrics:val('packets/connect')}, emqx_metrics:sent(#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}}), diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index c45d6ba9b..a04a0b82b 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -46,7 +46,6 @@ ignore_loop(_Config) -> t_session_all(_) -> emqx_zone:set_env(internal, idle_timeout, 100), - application:set_env(emqx, metric_commit_interval, 10), ClientId = <<"ClientId">>, {ok, ConnPid} = emqx_mock_client:start_link(ClientId), {ok, SPid} = emqx_mock_client:open_session(ConnPid, ClientId, internal), From fde5fbe73a8c84a1e4807a1dd4623b3f90b389e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Fri, 30 Nov 2018 17:27:47 +0800 Subject: [PATCH 438/520] Align the code --- src/emqx_metrics.erl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index fb57cbdf9..caf862146 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -172,11 +172,11 @@ trans(dec, gauge, Metric, Val) -> hold(Type, Metric, Val) when Type =:= counter orelse Type =:= gauge -> put('$metrics', case get('$metrics') of - undefined -> - #{{Type, Metric} => Val}; - Metrics -> - maps:update_with({Type, Metric}, fun(Cnt) -> Cnt + Val end, Val, Metrics) - end). + undefined -> + #{{Type, Metric} => Val}; + Metrics -> + maps:update_with({Type, Metric}, fun(Cnt) -> Cnt + Val end, Val, Metrics) + end). commit() -> case get('$metrics') of From e15e5d1f98907c97a9e41efc266f8b81ca6c4e5c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 30 Nov 2018 17:35:42 +0800 Subject: [PATCH 439/520] Fix the atom leaks in emqx_reason_codes module. --- src/emqx_reason_codes.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index bb0d8bb0b..3dc73a5d5 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -28,7 +28,7 @@ name(2, _Ver) -> client_identifier_not_valid; name(3, _Ver) -> server_unavaliable; name(4, _Ver) -> malformed_username_or_password; name(5, _Ver) -> unauthorized_client; -name(I, _Ver) -> list_to_atom("unkown_connack" ++ integer_to_list(I)). +name(_, _Ver) -> unknown_error. name(16#00) -> success; name(16#01) -> granted_qos1; @@ -73,7 +73,7 @@ name(16#9F) -> connection_rate_exceeded; name(16#A0) -> maximum_connect_time; name(16#A1) -> subscription_identifiers_not_supported; name(16#A2) -> wildcard_subscriptions_not_supported; -name(Code) -> list_to_atom("unkown_reason_code" ++ integer_to_list(Code)). +name(_Code) -> unknown_error. text(16#00) -> <<"Success">>; text(16#01) -> <<"Granted QoS 1">>; @@ -118,7 +118,7 @@ text(16#9F) -> <<"Connection rate exceeded">>; text(16#A0) -> <<"Maximum connect time">>; text(16#A1) -> <<"Subscription Identifiers not supported">>; text(16#A2) -> <<"Wildcard Subscriptions not supported">>; -text(Code) -> iolist_to_binary(["Unkown Reason Code:", integer_to_list(Code)]). +text(_Code) -> <<"Unknown error">>. compat(connack, 16#80) -> ?CONNACK_PROTO_VER; compat(connack, 16#81) -> ?CONNACK_PROTO_VER; From d9470f365f9eb2de261b3344e56b4411ed486d54 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 30 Nov 2018 15:16:58 +0800 Subject: [PATCH 440/520] Optimize read/write concurrency of mnesia/ets tables --- src/emqx_banned.erl | 3 ++- src/emqx_router.erl | 4 +++- src/emqx_router_helper.erl | 3 ++- src/emqx_sm_registry.erl | 6 ++++-- src/emqx_trie.erl | 9 +++++++-- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index 175271306..49a698384 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -43,7 +43,8 @@ mnesia(boot) -> {type, set}, {disc_copies, [node()]}, {record_name, banned}, - {attributes, record_info(fields, banned)}]); + {attributes, record_info(fields, banned)}, + {storage_properties, [{ets, [{read_concurrency, true}]}]}]); mnesia(copy) -> ok = ekka_mnesia:copy_table(?TAB). diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 8cd0d4d42..2ac656d56 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -54,7 +54,9 @@ mnesia(boot) -> {type, bag}, {ram_copies, [node()]}, {record_name, route}, - {attributes, record_info(fields, route)}]); + {attributes, record_info(fields, route)}, + {storage_properties, [{ets, [{read_concurrency, true}, + {write_concurrency, true}]}]}]); mnesia(copy) -> ok = ekka_mnesia:copy_table(?ROUTE). diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index 5431c9900..7bacddd4c 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -53,7 +53,8 @@ mnesia(boot) -> {type, set}, {ram_copies, [node()]}, {record_name, routing_node}, - {attributes, record_info(fields, routing_node)}]); + {attributes, record_info(fields, routing_node)}, + {storage_properties, [{ets, [{read_concurrency, true}]}]}]); mnesia(copy) -> ok = ekka_mnesia:copy_table(?ROUTING_NODE). diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index 659b3a92b..b503d71c8 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -69,9 +69,11 @@ init([]) -> {type, bag}, {ram_copies, [node()]}, {record_name, global_session}, - {attributes, record_info(fields, global_session)}]), + {attributes, record_info(fields, global_session)}, + {storage_properties, [{ets, [{read_concurrency, true}, + {write_concurrency, true}]}]}]), ok = ekka_mnesia:copy_table(?TAB), - ekka:monitor(membership), + _ = ekka:monitor(membership), {ok, #{}}. handle_call(Req, _From, State) -> diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index f5dfa93fe..569de9092 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -36,16 +36,21 @@ %% @doc Create or replicate trie tables. -spec(mnesia(boot | copy) -> ok). mnesia(boot) -> + %% Optimize + StoreProps = [{ets, [{read_concurrency, true}, + {write_concurrency, true}]}], %% Trie table ok = ekka_mnesia:create_table(?TRIE, [ {ram_copies, [node()]}, {record_name, trie}, - {attributes, record_info(fields, trie)}]), + {attributes, record_info(fields, trie)}, + {storage_properties, StoreProps}]), %% Trie node table ok = ekka_mnesia:create_table(?TRIE_NODE, [ {ram_copies, [node()]}, {record_name, trie_node}, - {attributes, record_info(fields, trie_node)}]); + {attributes, record_info(fields, trie_node)}, + {storage_properties, StoreProps}]); mnesia(copy) -> %% Copy trie table From fe33aeb3dc19d07330da7f3a750904e054f37dd4 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Fri, 30 Nov 2018 06:52:29 +0100 Subject: [PATCH 441/520] For git older than 1.8, there is no shallow clone support --- Makefile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index bedeb7250..e815e0756 100644 --- a/Makefile +++ b/Makefile @@ -48,9 +48,16 @@ PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets pu DIALYZER_DIRS := ebin/ DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wunmatched_returns +GIT_VSN = $(shell git --version | grep -oE "[0-9]{1,2}\.[0-9]{1,2}") +GIT_VSN_17_COMP = $(shell echo -e "$(GIT_VSN)\n1.7" | sort -V | tail -1) +ifeq ($(GIT_VSN_17_COMP),1.7) + MAYBE_SHALLOW = +else + MAYBE_SHALLOW = -c advice.detachedHead=false --depth 1 +endif + define dep_fetch_git-emqx - git clone -q --depth 1 -b $(call dep_commit,$(1)) -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)) > /dev/null 2>&1; \ - cd $(DEPS_DIR)/$(call dep_name,$(1)); + git clone $(MAYBE_SHALLOW) -q -b $(call dep_commit,$(1)) -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)) > /dev/null 2>&1 endef core_http_get-emqx = curl -Lf$(if $(filter-out 0,$(V)),,s)o $(call core_native_path,$1) $2 From 14dfe034265fb8bc13d2c2b9935f4cf15394ded4 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Fri, 30 Nov 2018 10:22:18 +0100 Subject: [PATCH 442/520] Use git tag for app vsn --- Makefile | 1 - erlang.mk | 2 +- src/emqx.app.src | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index e815e0756..695cc6871 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,6 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker -PROJECT_VERSION = 3.0.0 DEPS = jsx gproc gen_rpc ekka esockd cowboy clique diff --git a/erlang.mk b/erlang.mk index f38d22653..c5d4b4f7f 100644 --- a/erlang.mk +++ b/erlang.mk @@ -1186,7 +1186,7 @@ else fi $(appsrc_verbose) cat src/$(PROJECT).app.src \ | sed "s/{[[:space:]]*modules[[:space:]]*,[[:space:]]*\[\]}/{modules, \[$(call comma_list,$(MODULES))\]}/" \ - | sed "s/{id,[[:space:]]*\"git\"}/{id, \"$(subst /,\/,$(GITDESCRIBE))\"}/" \ + | sed "s/{vsn,[[:space:]]*\"git\"}/{vsn, \"$(subst /,\/,$(GITDESCRIBE))\"}/" \ > ebin/$(PROJECT).app endif diff --git a/src/emqx.app.src b/src/emqx.app.src index 1bca0118b..ce643634e 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -1,6 +1,6 @@ {application,emqx, [{description,"EMQ X Broker"}, - {vsn,"3.0.0"}, + {vsn,"git"}, {modules,[]}, {registered,[emqx_sup]}, {applications,[kernel,stdlib,jsx,gproc,gen_rpc,esockd, From b87e72861a73476614cc6c9181cb64e0bd2ed823 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Fri, 30 Nov 2018 11:02:02 +0100 Subject: [PATCH 443/520] Fallback to git clone -n then checkout if git version is older than 1.8 --- Makefile | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 695cc6871..16435bdab 100644 --- a/Makefile +++ b/Makefile @@ -47,17 +47,19 @@ PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets pu DIALYZER_DIRS := ebin/ DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wunmatched_returns -GIT_VSN = $(shell git --version | grep -oE "[0-9]{1,2}\.[0-9]{1,2}") -GIT_VSN_17_COMP = $(shell echo -e "$(GIT_VSN)\n1.7" | sort -V | tail -1) -ifeq ($(GIT_VSN_17_COMP),1.7) - MAYBE_SHALLOW = -else - MAYBE_SHALLOW = -c advice.detachedHead=false --depth 1 -endif +GIT_VSN := $(shell git --version | grep -oE "[0-9]{1,2}\.[0-9]{1,2}") +GIT_VSN_17_COMP := $(shell echo -e "$(GIT_VSN)\n1.7" | sort -V | tail -1) +ifeq ($(GIT_VSN_17_COMP),1.7) define dep_fetch_git-emqx - git clone $(MAYBE_SHALLOW) -q -b $(call dep_commit,$(1)) -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)) > /dev/null 2>&1 + git clone -q -n -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); \ + cd $(DEPS_DIR)/$(call dep_name,$(1)) && git checkout -q $(call dep_commit,$(1)) endef +else +define dep_fetch_git-emqx + git clone -q -c advice.detachedHead=false --depth 1 -b $(call dep_commit,$(1)) -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)) +endef +endif core_http_get-emqx = curl -Lf$(if $(filter-out 0,$(V)),,s)o $(call core_native_path,$1) $2 From 295a9d692efb3843268bd89f84d7a4e5565ad419 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 30 Nov 2018 19:16:48 +0800 Subject: [PATCH 444/520] Fix the coverage shaky (#2010) --- src/emqx_broker.erl | 5 +++++ test/emqx_broker_SUITE.erl | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index b7e0e457e..ca4a86c87 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -33,6 +33,11 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-ifdef(TEST). +-compile(export_all). +-compile(nowarn_export_all). +-endif. + -record(state, {pool, id, submap, submon}). -record(subscribe, {topic, subpid, subid, subopts = #{}}). -record(unsubscribe, {topic, subpid, subid}). diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index 3187aafa4..b9bfb4257 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -38,6 +38,7 @@ groups() -> publish, pubsub, t_local_subscribe, t_shared_subscribe, + dispatch_with_no_sub, 'pubsub#', 'pubsub+']}, {session, [sequence], [start_session]}, {metrics, [sequence], [inc_dec_metric]}, @@ -76,6 +77,11 @@ publish(_) -> emqx:publish(Msg), ?assert(receive {dispatch, <<"test/+">>, Msg} -> true after 5 -> false end). +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)). + pubsub(_) -> true = emqx:is_running(node()), Self = self(), @@ -193,4 +199,3 @@ set_alarms(_) -> ?assertEqual(1, length(Alarms)), emqx_alarm_mgr:clear_alarm(<<"1">>), [] = emqx_alarm_mgr:get_alarms(). - From 61030c8d1058fc1a9a2d70561d17bf0c7153f730 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sun, 2 Dec 2018 16:15:45 +0800 Subject: [PATCH 445/520] Add eunit tests to increase coverage. --- test/emqx_reason_codes_tests.erl | 130 +++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 test/emqx_reason_codes_tests.erl diff --git a/test/emqx_reason_codes_tests.erl b/test/emqx_reason_codes_tests.erl new file mode 100644 index 000000000..29f7ad081 --- /dev/null +++ b/test/emqx_reason_codes_tests.erl @@ -0,0 +1,130 @@ +%% Copyright (c) 2018 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_reason_codes_tests). + +-include_lib("eunit/include/eunit.hrl"). + +-include("emqx_mqtt.hrl"). + +-import(lists, [seq/2, zip/2, foreach/2]). + +-define(MQTTV4_CODE_NAMES, [connection_acceptd, + unacceptable_protocol_version, + client_identifier_not_valid, + server_unavaliable, + malformed_username_or_password, + unauthorized_client, + unknown_error]). + +-define(MQTTV5_CODE_NAMES, [success, granted_qos1, granted_qos2, disconnect_with_will_message, + no_matching_subscribers, no_subscription_existed, continue_authentication, + re_authenticate, unspecified_error, malformed_Packet, protocol_error, + implementation_specific_error, unsupported_protocol_version, + client_identifier_not_valid, bad_username_or_password, not_authorized, + server_unavailable, server_busy, banned,server_shutting_down, + bad_authentication_method, keepalive_timeout, session_taken_over, + topic_filter_invalid, topic_name_invalid, packet_identifier_inuse, + packet_identifier_not_found, receive_maximum_exceeded, topic_alias_invalid, + packet_too_large, message_rate_too_high, quota_exceeded, + administrative_action, payload_format_invalid, retain_not_supported, + qos_not_supported, use_another_server, server_moved, + shared_subscriptions_not_supported, connection_rate_exceeded, + maximum_connect_time, subscription_identifiers_not_supported, + wildcard_subscriptions_not_supported, unknown_error]). + +-define(MQTTV5_CODES, [16#00, 16#01, 16#02, 16#04, 16#10, 16#11, 16#18, 16#19, 16#80, 16#81, 16#82, + 16#83, 16#84, 16#85, 16#86, 16#87, 16#88, 16#89, 16#8A, 16#8B, 16#8C, 16#8D, + 16#8E, 16#8F, 16#90, 16#91, 16#92, 16#93, 16#94, 16#95, 16#96, 16#97, 16#98, + 16#99, 16#9A, 16#9B, 16#9C, 16#9D, 16#9E, 16#9F, 16#A0, 16#A1, 16#A2, code]). + +-define(MQTTV5_TXT, [<<"Success">>, <<"Granted QoS 1">>, <<"Granted QoS 2">>, + <<"Disconnect with Will Message">>, <<"No matching subscribers">>, + <<"No subscription existed">>, <<"Continue authentication">>, + <<"Re-authenticate">>, <<"Unspecified error">>, <<"Malformed Packet">>, + <<"Protocol Error">>, <<"Implementation specific error">>, + <<"Unsupported Protocol Version">>, <<"Client Identifier not valid">>, + <<"Bad User Name or Password">>, <<"Not authorized">>, + <<"Server unavailable">>, <<"Server busy">>, <<"Banned">>, + <<"Server shutting down">>, <<"Bad authentication method">>, + <<"Keep Alive timeout">>, <<"Session taken over">>, + <<"Topic Filter invalid">>, <<"Topic Name invalid">>, + <<"Packet Identifier in use">>, <<"Packet Identifier not found">>, + <<"Receive Maximum exceeded">>, <<"Topic Alias invalid">>, + <<"Packet too large">>, <<"Message rate too high">>, <<"Quota exceeded">>, + <<"Administrative action">>, <<"Payload format invalid">>, + <<"Retain not supported">>, <<"QoS not supported">>, + <<"Use another server">>, <<"Server moved">>, + <<"Shared Subscriptions not supported">>, <<"Connection rate exceeded">>, + <<"Maximum connect time">>, <<"Subscription Identifiers not supported">>, + <<"Wildcard Subscriptions not supported">>, <<"Unknown error">>]). + +-define(COMPAT_CODES_V5, [16#80, 16#81, 16#82, 16#83, 16#84, 16#85, 16#86, 16#87, + 16#88, 16#89, 16#8A, 16#8B, 16#8C, 16#97, 16#9C, 16#9D, + 16#9F]). + +-define(COMPAT_CODES_V4, [?CONNACK_PROTO_VER, ?CONNACK_PROTO_VER, ?CONNACK_PROTO_VER, + ?CONNACK_PROTO_VER, ?CONNACK_PROTO_VER, + ?CONNACK_INVALID_ID, + ?CONNACK_CREDENTIALS, + ?CONNACK_AUTH, + ?CONNACK_SERVER, + ?CONNACK_SERVER, + ?CONNACK_AUTH, + ?CONNACK_SERVER, + ?CONNACK_AUTH, + ?CONNACK_SERVER, ?CONNACK_SERVER, ?CONNACK_SERVER, ?CONNACK_SERVER]). + +mqttv4_name_test() -> + (((codes_test(?MQTT_PROTO_V4)) + (seq(0,6))) + (?MQTTV4_CODE_NAMES)) + (fun emqx_reason_codes:name/2). + +mqttv5_name_test() -> + (((codes_test(?MQTT_PROTO_V5)) + (?MQTTV5_CODES)) + (?MQTTV5_CODE_NAMES)) + (fun emqx_reason_codes:name/2). + +text_test() -> + (((codes_test(?MQTT_PROTO_V5)) + (?MQTTV5_CODES)) + (?MQTTV5_TXT)) + (fun emqx_reason_codes:text/1). + +compat_test() -> + (((codes_test(connack)) + (?COMPAT_CODES_V5)) + (?COMPAT_CODES_V4)) + (fun emqx_reason_codes:compat/2), + (((codes_test(suback)) + ([0,1,2, 16#80])) + ([0,1,2, 16#80])) + (fun emqx_reason_codes:compat/2). + +codes_test(AsistVar) -> + fun(CODES) -> + fun(NAMES) -> + fun(Procedure) -> + foreach(fun({Code, Result}) -> + ?assertEqual(Result, case erlang:fun_info(Procedure, name) of + {name, text} -> Procedure(Code); + {name, name} -> Procedure(Code, AsistVar); + {name, compat} -> Procedure(AsistVar, Code) + end) + end, zip(CODES, NAMES)) + end + end + end. From 95446ca83780481c7656c9cc04ed41159dcbe06a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 30 Nov 2018 20:37:10 +0800 Subject: [PATCH 446/520] Optimize the route and trie modules. 1. Use mnesia:wread/1 to replace mnesia:read/2 2. Update the router supervisor --- src/emqx_router.erl | 15 ++++++++------- src/emqx_router_helper.erl | 4 ++-- src/emqx_router_sup.erl | 8 ++++++-- src/emqx_trie.erl | 14 +++++++------- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 2ac656d56..941c004f7 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -34,7 +34,8 @@ -export([has_routes/1, match_routes/1, print_routes/1]). -export([topics/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -type(destination() :: node() | {binary(), node()}). @@ -45,9 +46,9 @@ -define(BATCH(Enabled), #batch{enabled = Enabled}). -define(BATCH(Enabled, Pending), #batch{enabled = Enabled, pending = Pending}). -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Mnesia bootstrap -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ mnesia(boot) -> ok = ekka_mnesia:create_table(?ROUTE, [ @@ -132,9 +133,9 @@ cast(Router, Msg) -> pick(Topic) -> gproc_pool:pick_worker(router, Topic). -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([Pool, Id]) -> rand:seed(exsplus, erlang:timestamp()), @@ -207,9 +208,9 @@ terminate(_Reason, #state{pool = Pool, id = Id, batch = Batch}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ ensure_batch_timer(State = #state{batch = #batch{enabled = false}}) -> State; diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index 7bacddd4c..c24b10715 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -84,8 +84,8 @@ monitor(Node) when is_atom(Node) -> %%------------------------------------------------------------------------------ init([]) -> - ekka:monitor(membership), - mnesia:subscribe({table, ?ROUTING_NODE, simple}), + _ = ekka:monitor(membership), + _ = mnesia:subscribe({table, ?ROUTING_NODE, simple}), Nodes = lists:foldl( fun(Node, Acc) -> case ekka:is_member(Node) of diff --git a/src/emqx_router_sup.erl b/src/emqx_router_sup.erl index 004b88bb8..2bbaabc18 100644 --- a/src/emqx_router_sup.erl +++ b/src/emqx_router_sup.erl @@ -24,8 +24,12 @@ start_link() -> init([]) -> %% Router helper - Helper = {router_helper, {emqx_router_helper, start_link, []}, - permanent, 5000, worker, [emqx_router_helper]}, + Helper = #{id => helper, + start => {emqx_router_helper, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_router_helper]}, %% Router pool RouterPool = emqx_pool_sup:spec(emqx_router_pool, diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index 569de9092..79f6042b7 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -62,10 +62,10 @@ mnesia(copy) -> %% Trie APIs %%------------------------------------------------------------------------------ -%% @doc Insert a topic into the trie +%% @doc Insert a topic filter into the trie. -spec(insert(emqx_topic:topic()) -> ok). insert(Topic) when is_binary(Topic) -> - case mnesia:read(?TRIE_NODE, Topic) of + case mnesia:wread({?TRIE_NODE, Topic}) of [#trie_node{topic = Topic}] -> ok; [TrieNode = #trie_node{topic = undefined}] -> @@ -77,21 +77,21 @@ insert(Topic) when is_binary(Topic) -> write_trie_node(#trie_node{node_id = Topic, topic = Topic}) end. -%% @doc Find trie nodes that match the topic +%% @doc Find trie nodes that match the topic name. -spec(match(emqx_topic:topic()) -> list(emqx_topic:topic())). match(Topic) when is_binary(Topic) -> TrieNodes = match_node(root, emqx_topic:words(Topic)), [Name || #trie_node{topic = Name} <- TrieNodes, Name =/= undefined]. -%% @doc Lookup a trie node +%% @doc Lookup a trie node. -spec(lookup(NodeId :: binary()) -> [#trie_node{}]). lookup(NodeId) -> mnesia:read(?TRIE_NODE, NodeId). -%% @doc Delete a topic from the trie +%% @doc Delete a topic filter from the trie. -spec(delete(emqx_topic:topic()) -> ok). delete(Topic) when is_binary(Topic) -> - case mnesia:read(?TRIE_NODE, Topic) of + case mnesia:wread({?TRIE_NODE, Topic}) of [#trie_node{edge_count = 0}] -> mnesia:delete({?TRIE_NODE, Topic}), delete_path(lists:reverse(emqx_topic:triples(Topic))); @@ -108,7 +108,7 @@ delete(Topic) when is_binary(Topic) -> %% @doc Add a path to the trie. add_path({Node, Word, Child}) -> Edge = #trie_edge{node_id = Node, word = Word}, - case mnesia:read(?TRIE_NODE, Node) of + case mnesia:wread({?TRIE_NODE, Node}) of [TrieNode = #trie_node{edge_count = Count}] -> case mnesia:wread({?TRIE, Edge}) of [] -> From b2c3d8366da17b8829516a7580519136090ac46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Mon, 3 Dec 2018 13:57:37 +0800 Subject: [PATCH 447/520] Add logs for malformed acl configuration file --- src/emqx_access_rule.erl | 10 +++++++--- src/emqx_acl_internal.erl | 28 ++++++++++++++++++---------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 3fb6dd7ef..1d3154735 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -34,16 +34,20 @@ -export([match/3]). -define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))). +-define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= pubsub))). %% @doc Compile Access Rule. compile({A, all}) when ?ALLOW_DENY(A) -> {A, all}; -compile({A, Who, Access, Topic}) when ?ALLOW_DENY(A), is_binary(Topic) -> +compile({A, Who, Access, Topic}) when ?ALLOW_DENY(A), ?PUBSUB(Access), is_binary(Topic) -> {A, compile(who, Who), Access, [compile(topic, Topic)]}; -compile({A, Who, Access, TopicFilters}) when ?ALLOW_DENY(A) -> - {A, compile(who, Who), Access, [compile(topic, Topic) || Topic <- TopicFilters]}. +compile({A, Who, Access, TopicFilters}) when ?ALLOW_DENY(A), ?PUBSUB(Access) -> + {A, compile(who, Who), Access, [compile(topic, Topic) || Topic <- TopicFilters]}; + +compile(Rule) -> + emqx_logger:error("[ACCESS_RULE] Malformed rule: ~p", [Rule]). compile(who, all) -> all; diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index eee7e6c18..383967030 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -46,18 +46,24 @@ all_rules() -> -spec(init([File :: string()]) -> {ok, #{}}). init([File]) -> _ = emqx_tables:new(?ACL_RULE_TAB, [set, public, {read_concurrency, true}]), - true = load_rules_from_file(File), + ok = load_rules_from_file(File), {ok, #{acl_file => File}}. load_rules_from_file(AclFile) -> - {ok, Terms} = file:consult(AclFile), - Rules = [emqx_access_rule:compile(Term) || Term <- Terms], - lists:foreach(fun(PubSub) -> - ets:insert(?ACL_RULE_TAB, {PubSub, - lists:filter(fun(Rule) -> filter(PubSub, Rule) end, Rules)}) - end, [publish, subscribe]), - ets:insert(?ACL_RULE_TAB, {all_rules, Terms}). - + case file:consult(AclFile) of + {ok, Terms} -> + Rules = [emqx_access_rule:compile(Term) || Term <- Terms], + lists:foreach(fun(PubSub) -> + ets:insert(?ACL_RULE_TAB, {PubSub, + lists:filter(fun(Rule) -> filter(PubSub, Rule) end, Rules)}) + end, [publish, subscribe]), + ets:insert(?ACL_RULE_TAB, {all_rules, Terms}), + ok; + {error, Reason} -> + emqx_logger:error("[ACL_INTERNAL] Consult failed: ~p", [Reason]), + {error, Reason} + end. + filter(_PubSub, {allow, all}) -> true; filter(_PubSub, {deny, all}) -> @@ -100,9 +106,11 @@ match(Credentials, Topic, [Rule|Rules]) -> -spec(reload_acl(state()) -> ok | {error, term()}). reload_acl(#{acl_file := AclFile}) -> case catch load_rules_from_file(AclFile) of - true -> + ok -> emqx_logger:info("Reload acl_file ~s successfully", [AclFile]), ok; + {error, Error} -> + {error, Error}; {'EXIT', Error} -> {error, Error} end. From 520a5e02251edbe244d002edda3f1fe0ddf4b15d Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sat, 1 Dec 2018 19:50:17 +0100 Subject: [PATCH 448/520] Download erlang.mk and use git tag in appfile vsn --- .gitignore | 1 + Makefile | 24 +- erlang.mk | 2741 ---------------------------------------------------- 3 files changed, 2 insertions(+), 2764 deletions(-) delete mode 100644 erlang.mk diff --git a/.gitignore b/.gitignore index 80e3bf42d..7a4e891d1 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ compile_commands.json cuttlefish rebar.lock xrefr +erlang.mk diff --git a/Makefile b/Makefile index 16435bdab..26bcf22ce 100644 --- a/Makefile +++ b/Makefile @@ -47,29 +47,7 @@ PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets pu DIALYZER_DIRS := ebin/ DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wunmatched_returns -GIT_VSN := $(shell git --version | grep -oE "[0-9]{1,2}\.[0-9]{1,2}") -GIT_VSN_17_COMP := $(shell echo -e "$(GIT_VSN)\n1.7" | sort -V | tail -1) - -ifeq ($(GIT_VSN_17_COMP),1.7) -define dep_fetch_git-emqx - git clone -q -n -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); \ - cd $(DEPS_DIR)/$(call dep_name,$(1)) && git checkout -q $(call dep_commit,$(1)) -endef -else -define dep_fetch_git-emqx - git clone -q -c advice.detachedHead=false --depth 1 -b $(call dep_commit,$(1)) -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)) -endef -endif - -core_http_get-emqx = curl -Lf$(if $(filter-out 0,$(V)),,s)o $(call core_native_path,$1) $2 - -define dep_fetch_hex-emqx - mkdir -p $(ERLANG_MK_TMP)/hex $(DEPS_DIR)/$1; \ - $(call core_http_get-emqx,$(ERLANG_MK_TMP)/hex/$1.tar,\ - https://repo.hex.pm/tarballs/$1-$(strip $(word 2,$(dep_$1))).tar); \ - tar -xOf $(ERLANG_MK_TMP)/hex/$1.tar contents.tar.gz | tar -C $(DEPS_DIR)/$1 -xzf -; -endef - +$(shell [ -f erlang.mk ] || curl -s -o erlang.mk https://raw.githubusercontent.com/emqx/erlmk/master/erlang.mk) include erlang.mk clean:: gen-clean diff --git a/erlang.mk b/erlang.mk deleted file mode 100644 index c5d4b4f7f..000000000 --- a/erlang.mk +++ /dev/null @@ -1,2741 +0,0 @@ -# Copyright (c) 2013-2015, Loïc Hoguin -# -# Permission to use, copy, modify, and/or distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -.PHONY: all app apps deps search rel docs install-docs check tests clean distclean help erlang-mk - -ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST))) - -ERLANG_MK_VERSION = 2.0.0-pre.2-130-gc6fe5ea - -# Core configuration. - -PROJECT ?= $(notdir $(CURDIR)) -PROJECT := $(strip $(PROJECT)) - -PROJECT_VERSION ?= rolling -PROJECT_MOD ?= $(PROJECT)_app - -# Verbosity. - -V ?= 0 - -verbose_0 = @ -verbose_2 = set -x; -verbose = $(verbose_$(V)) - -gen_verbose_0 = @echo " GEN " $@; -gen_verbose_2 = set -x; -gen_verbose = $(gen_verbose_$(V)) - -# Temporary files directory. - -ERLANG_MK_TMP ?= $(CURDIR)/.erlang.mk -export ERLANG_MK_TMP - -# "erl" command. - -ERL = erl +A0 -noinput -boot start_clean - -# Platform detection. - -ifeq ($(PLATFORM),) -UNAME_S := $(shell uname -s) - -ifeq ($(UNAME_S),Linux) -PLATFORM = linux -else ifeq ($(UNAME_S),Darwin) -PLATFORM = darwin -else ifeq ($(UNAME_S),SunOS) -PLATFORM = solaris -else ifeq ($(UNAME_S),GNU) -PLATFORM = gnu -else ifeq ($(UNAME_S),FreeBSD) -PLATFORM = freebsd -else ifeq ($(UNAME_S),NetBSD) -PLATFORM = netbsd -else ifeq ($(UNAME_S),OpenBSD) -PLATFORM = openbsd -else ifeq ($(UNAME_S),DragonFly) -PLATFORM = dragonfly -else ifeq ($(shell uname -o),Msys) -PLATFORM = msys2 -else -$(error Unable to detect platform. Please open a ticket with the output of uname -a.) -endif - -export PLATFORM -endif - -# Core targets. - -all:: deps app rel - -# Noop to avoid a Make warning when there's nothing to do. -rel:: - $(verbose) : - -check:: tests - -clean:: clean-crashdump - -clean-crashdump: -ifneq ($(wildcard erl_crash.dump),) - $(gen_verbose) rm -f erl_crash.dump -endif - -distclean:: clean distclean-tmp - -distclean-tmp: - $(gen_verbose) rm -rf $(ERLANG_MK_TMP) - -help:: - $(verbose) printf "%s\n" \ - "erlang.mk (version $(ERLANG_MK_VERSION)) is distributed under the terms of the ISC License." \ - "Copyright (c) 2013-2015 Loïc Hoguin " \ - "" \ - "Usage: [V=1] $(MAKE) [target]..." \ - "" \ - "Core targets:" \ - " all Run deps, app and rel targets in that order" \ - " app Compile the project" \ - " deps Fetch dependencies (if needed) and compile them" \ - " search q=... Search for a package in the built-in index" \ - " rel Build a release for this project, if applicable" \ - " docs Build the documentation for this project" \ - " install-docs Install the man pages for this project" \ - " check Compile and run all tests and analysis for this project" \ - " tests Run the tests for this project" \ - " clean Delete temporary and output files from most targets" \ - " distclean Delete all temporary and output files" \ - " help Display this help and exit" \ - " erlang-mk Update erlang.mk to the latest version" - -# Core functions. - -empty := -space := $(empty) $(empty) -tab := $(empty) $(empty) -comma := , - -define newline - - -endef - -define comma_list -$(subst $(space),$(comma),$(strip $(1))) -endef - -# Adding erlang.mk to make Erlang scripts who call init:get_plain_arguments() happy. -define erlang -$(ERL) $(2) -pz $(ERLANG_MK_TMP)/rebar/ebin -eval "$(subst $(newline),,$(subst ",\",$(1)))" -- erlang.mk -endef - -ifeq ($(PLATFORM),msys2) -core_native_path = $(subst \,\\\\,$(shell cygpath -w $1)) -else -core_native_path = $1 -endif - -ifeq ($(shell which wget 2>/dev/null | wc -l), 1) -define core_http_get - wget --no-check-certificate -O $(1) $(2)|| rm $(1) -endef -else -define core_http_get.erl - ssl:start(), - inets:start(), - case httpc:request(get, {"$(2)", []}, [{autoredirect, true}], []) of - {ok, {{_, 200, _}, _, Body}} -> - case file:write_file("$(1)", Body) of - ok -> ok; - {error, R1} -> halt(R1) - end; - {error, R2} -> - halt(R2) - end, - halt(0). -endef - -define core_http_get - $(call erlang,$(call core_http_get.erl,$(call core_native_path,$1),$2)) -endef -endif - -core_eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))) - -core_find = $(if $(wildcard $1),$(shell find $(1:%/=%) -type f -name $(subst *,\*,$2))) - -core_lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(1))))))))))))))))))))))))))) - -core_ls = $(filter-out $(1),$(shell echo $(1))) - -# @todo Use a solution that does not require using perl. -core_relpath = $(shell perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' $1 $2) - -# Automated update. - -ERLANG_MK_REPO ?= https://github.com/ninenines/erlang.mk -ERLANG_MK_COMMIT ?= -ERLANG_MK_BUILD_CONFIG ?= build.config -ERLANG_MK_BUILD_DIR ?= .erlang.mk.build - -erlang-mk: - git clone $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR) -ifdef ERLANG_MK_COMMIT - cd $(ERLANG_MK_BUILD_DIR) && git checkout $(ERLANG_MK_COMMIT) -endif - if [ -f $(ERLANG_MK_BUILD_CONFIG) ]; then cp $(ERLANG_MK_BUILD_CONFIG) $(ERLANG_MK_BUILD_DIR)/build.config; fi - $(MAKE) -C $(ERLANG_MK_BUILD_DIR) - cp $(ERLANG_MK_BUILD_DIR)/erlang.mk ./erlang.mk - rm -rf $(ERLANG_MK_BUILD_DIR) - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: search - -define pkg_print - $(verbose) printf "%s\n" \ - $(if $(call core_eq,$(1),$(pkg_$(1)_name)),,"Pkg name: $(1)") \ - "App name: $(pkg_$(1)_name)" \ - "Description: $(pkg_$(1)_description)" \ - "Home page: $(pkg_$(1)_homepage)" \ - "Fetch with: $(pkg_$(1)_fetch)" \ - "Repository: $(pkg_$(1)_repo)" \ - "Commit: $(pkg_$(1)_commit)" \ - "" - -endef - -search: -ifdef q - $(foreach p,$(PACKAGES), \ - $(if $(findstring $(call core_lc,$(q)),$(call core_lc,$(pkg_$(p)_name) $(pkg_$(p)_description))), \ - $(call pkg_print,$(p)))) -else - $(foreach p,$(PACKAGES),$(call pkg_print,$(p))) -endif - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: distclean-deps - -# Configuration. - -ifdef OTP_DEPS -$(warning The variable OTP_DEPS is deprecated in favor of LOCAL_DEPS.) -endif - -IGNORE_DEPS ?= -export IGNORE_DEPS - -APPS_DIR ?= $(CURDIR)/apps -export APPS_DIR - -DEPS_DIR ?= $(CURDIR)/deps -export DEPS_DIR - -REBAR_DEPS_DIR = $(DEPS_DIR) -export REBAR_DEPS_DIR - -dep_name = $(if $(dep_$(1)),$(1),$(if $(pkg_$(1)_name),$(pkg_$(1)_name),$(1))) -dep_repo = $(patsubst git://github.com/%,https://github.com/%, \ - $(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_repo))) -dep_commit = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 3,$(dep_$(1))),$(pkg_$(1)_commit))) - -ALL_APPS_DIRS = $(if $(wildcard $(APPS_DIR)/),$(filter-out $(APPS_DIR),$(shell find $(APPS_DIR) -maxdepth 1 -type d))) -ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(BUILD_DEPS) $(DEPS)),$(call dep_name,$(dep)))) - -ifeq ($(filter $(APPS_DIR) $(DEPS_DIR),$(subst :, ,$(ERL_LIBS))),) -ifeq ($(ERL_LIBS),) - ERL_LIBS = $(APPS_DIR):$(DEPS_DIR) -else - ERL_LIBS := $(ERL_LIBS):$(APPS_DIR):$(DEPS_DIR) -endif -endif -export ERL_LIBS - -export NO_AUTOPATCH - -# Verbosity. - -dep_verbose_0 = @echo " DEP " $(1); -dep_verbose_2 = set -x; -dep_verbose = $(dep_verbose_$(V)) - -# Core targets. - -ifdef IS_APP -apps:: -else -apps:: $(ALL_APPS_DIRS) -ifeq ($(IS_APP)$(IS_DEP),) - $(verbose) rm -f $(ERLANG_MK_TMP)/apps.log -endif - $(verbose) mkdir -p $(ERLANG_MK_TMP) -# Create ebin directory for all apps to make sure Erlang recognizes them -# as proper OTP applications when using -include_lib. This is a temporary -# fix, a proper fix would be to compile apps/* in the right order. - $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ - mkdir -p $$dep/ebin || exit $$?; \ - done - $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ - if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/apps.log; then \ - :; \ - else \ - echo $$dep >> $(ERLANG_MK_TMP)/apps.log; \ - $(MAKE) -C $$dep IS_APP=1 || exit $$?; \ - fi \ - done -endif - -ifneq ($(SKIP_DEPS),) -deps:: -else -deps:: $(ALL_DEPS_DIRS) apps -ifeq ($(IS_APP)$(IS_DEP),) - $(verbose) rm -f $(ERLANG_MK_TMP)/deps.log -endif - $(verbose) mkdir -p $(ERLANG_MK_TMP) - $(verbose) for dep in $(ALL_DEPS_DIRS) ; do \ - if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/deps.log; then \ - :; \ - else \ - echo $$dep >> $(ERLANG_MK_TMP)/deps.log; \ - if [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ]; then \ - $(MAKE) -C $$dep IS_DEP=1 || exit $$?; \ - else \ - echo "Error: No Makefile to build dependency $$dep."; \ - exit 2; \ - fi \ - fi \ - done -endif - -# Deps related targets. - -# @todo rename GNUmakefile and makefile into Makefile first, if they exist -# While Makefile file could be GNUmakefile or makefile, -# in practice only Makefile is needed so far. -define dep_autopatch - if [ -f $(DEPS_DIR)/$(1)/erlang.mk ]; then \ - $(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \ - $(call dep_autopatch_erlang_mk,$(1)); \ - elif [ -f $(DEPS_DIR)/$(1)/Makefile ]; then \ - if [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$(1)/Makefile` ]; then \ - $(call dep_autopatch2,$(1)); \ - elif [ 0 != `grep -ci rebar $(DEPS_DIR)/$(1)/Makefile` ]; then \ - $(call dep_autopatch2,$(1)); \ - elif [ -n "`find $(DEPS_DIR)/$(1)/ -type f -name \*.mk -not -name erlang.mk -exec grep -i rebar '{}' \;`" ]; then \ - $(call dep_autopatch2,$(1)); \ - else \ - $(call erlang,$(call dep_autopatch_app.erl,$(1))); \ - fi \ - else \ - if [ ! -d $(DEPS_DIR)/$(1)/src/ ]; then \ - $(call dep_autopatch_noop,$(1)); \ - else \ - $(call dep_autopatch2,$(1)); \ - fi \ - fi -endef - -define dep_autopatch2 - if [ -f $(DEPS_DIR)/$1/src/$1.app.src.script ]; then \ - $(call erlang,$(call dep_autopatch_appsrc_script.erl,$(1))); \ - fi; \ - $(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \ - if [ -f $(DEPS_DIR)/$(1)/rebar -o -f $(DEPS_DIR)/$(1)/rebar.config -o -f $(DEPS_DIR)/$(1)/rebar.config.script ]; then \ - $(call dep_autopatch_fetch_rebar); \ - $(call dep_autopatch_rebar,$(1)); \ - else \ - $(call dep_autopatch_gen,$(1)); \ - fi -endef - -define dep_autopatch_noop - printf "noop:\n" > $(DEPS_DIR)/$(1)/Makefile -endef - -# Overwrite erlang.mk with the current file by default. -ifeq ($(NO_AUTOPATCH_ERLANG_MK),) -define dep_autopatch_erlang_mk - echo "include $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(DEPS_DIR)/app)/erlang.mk" \ - > $(DEPS_DIR)/$1/erlang.mk -endef -else -define dep_autopatch_erlang_mk - : -endef -endif - -define dep_autopatch_gen - printf "%s\n" \ - "ERLC_OPTS = +debug_info" \ - "include ../../erlang.mk" > $(DEPS_DIR)/$(1)/Makefile -endef - -define dep_autopatch_fetch_rebar - mkdir -p $(ERLANG_MK_TMP); \ - if [ ! -d $(ERLANG_MK_TMP)/rebar ]; then \ - git clone -q -n -- https://github.com/rebar/rebar $(ERLANG_MK_TMP)/rebar; \ - cd $(ERLANG_MK_TMP)/rebar; \ - git checkout -q 791db716b5a3a7671e0b351f95ddf24b848ee173; \ - $(MAKE); \ - cd -; \ - fi -endef - -define dep_autopatch_rebar - if [ -f $(DEPS_DIR)/$(1)/Makefile ]; then \ - mv $(DEPS_DIR)/$(1)/Makefile $(DEPS_DIR)/$(1)/Makefile.orig.mk; \ - fi; \ - $(call erlang,$(call dep_autopatch_rebar.erl,$(1))); \ - rm -f $(DEPS_DIR)/$(1)/ebin/$(1).app -endef - -define dep_autopatch_rebar.erl - application:load(rebar), - application:set_env(rebar, log_level, debug), - Conf1 = case file:consult("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config)") of - {ok, Conf0} -> Conf0; - _ -> [] - end, - {Conf, OsEnv} = fun() -> - case filelib:is_file("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config.script)") of - false -> {Conf1, []}; - true -> - Bindings0 = erl_eval:new_bindings(), - Bindings1 = erl_eval:add_binding('CONFIG', Conf1, Bindings0), - Bindings = erl_eval:add_binding('SCRIPT', "$(call core_native_path,$(DEPS_DIR)/$1/rebar.config.script)", Bindings1), - Before = os:getenv(), - {ok, Conf2} = file:script("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config.script)", Bindings), - {Conf2, lists:foldl(fun(E, Acc) -> lists:delete(E, Acc) end, os:getenv(), Before)} - end - end(), - Write = fun (Text) -> - file:write_file("$(call core_native_path,$(DEPS_DIR)/$1/Makefile)", Text, [append]) - end, - Escape = fun (Text) -> - re:replace(Text, "\\\\$$", "\$$$$", [global, {return, list}]) - end, - Write("IGNORE_DEPS += edown eper eunit_formatters meck node_package " - "rebar_lock_deps_plugin rebar_vsn_plugin reltool_util\n"), - Write("C_SRC_DIR = /path/do/not/exist\n"), - Write("C_SRC_TYPE = rebar\n"), - Write("DRV_CFLAGS = -fPIC\nexport DRV_CFLAGS\n"), - Write(["ERLANG_ARCH = ", rebar_utils:wordsize(), "\nexport ERLANG_ARCH\n"]), - fun() -> - Write("ERLC_OPTS = +debug_info\nexport ERLC_OPTS\n"), - case lists:keyfind(erl_opts, 1, Conf) of - false -> ok; - {_, ErlOpts} -> - lists:foreach(fun - ({d, D}) -> - Write("ERLC_OPTS += -D" ++ atom_to_list(D) ++ "=1\n"); - ({i, I}) -> - Write(["ERLC_OPTS += -I ", I, "\n"]); - ({platform_define, Regex, D}) -> - case rebar_utils:is_arch(Regex) of - true -> Write("ERLC_OPTS += -D" ++ atom_to_list(D) ++ "=1\n"); - false -> ok - end; - ({parse_transform, PT}) -> - Write("ERLC_OPTS += +'{parse_transform, " ++ atom_to_list(PT) ++ "}'\n"); - (_) -> ok - end, ErlOpts) - end, - Write("\n") - end(), - fun() -> - File = case lists:keyfind(deps, 1, Conf) of - false -> []; - {_, Deps} -> - [begin case case Dep of - {N, S} when is_atom(N), is_list(S) -> {N, {hex, S}}; - {N, S} when is_tuple(S) -> {N, S}; - {N, _, S} -> {N, S}; - {N, _, S, _} -> {N, S}; - _ -> false - end of - false -> ok; - {Name, Source} -> - {Method, Repo, Commit} = case Source of - {hex, V} -> {hex, V, undefined}; - {git, R} -> {git, R, master}; - {M, R, {branch, C}} -> {M, R, C}; - {M, R, {ref, C}} -> {M, R, C}; - {M, R, {tag, C}} -> {M, R, C}; - {M, R, C} -> {M, R, C} - end, - Write(io_lib:format("DEPS += ~s\ndep_~s = ~s ~s ~s~n", [Name, Name, Method, Repo, Commit])) - end end || Dep <- Deps] - end - end(), - fun() -> - case lists:keyfind(erl_first_files, 1, Conf) of - false -> ok; - {_, Files} -> - Names = [[" ", case lists:reverse(F) of - "lre." ++ Elif -> lists:reverse(Elif); - Elif -> lists:reverse(Elif) - end] || "src/" ++ F <- Files], - Write(io_lib:format("COMPILE_FIRST +=~s\n", [Names])) - end - end(), - Write("\n\nrebar_dep: preprocess pre-deps deps pre-app app\n"), - Write("\npreprocess::\n"), - Write("\npre-deps::\n"), - Write("\npre-app::\n"), - PatchHook = fun(Cmd) -> - case Cmd of - "make -C" ++ Cmd1 -> "$$\(MAKE) -C" ++ Escape(Cmd1); - "gmake -C" ++ Cmd1 -> "$$\(MAKE) -C" ++ Escape(Cmd1); - "make " ++ Cmd1 -> "$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1); - "gmake " ++ Cmd1 -> "$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1); - _ -> Escape(Cmd) - end - end, - fun() -> - case lists:keyfind(pre_hooks, 1, Conf) of - false -> ok; - {_, Hooks} -> - [case H of - {'get-deps', Cmd} -> - Write("\npre-deps::\n\t" ++ PatchHook(Cmd) ++ "\n"); - {compile, Cmd} -> - Write("\npre-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); - {Regex, compile, Cmd} -> - case rebar_utils:is_arch(Regex) of - true -> Write("\npre-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); - false -> ok - end; - _ -> ok - end || H <- Hooks] - end - end(), - ShellToMk = fun(V) -> - re:replace(re:replace(V, "(\\\\$$)(\\\\w*)", "\\\\1(\\\\2)", [global]), - "-Werror\\\\b", "", [{return, list}, global]) - end, - PortSpecs = fun() -> - case lists:keyfind(port_specs, 1, Conf) of - false -> - case filelib:is_dir("$(call core_native_path,$(DEPS_DIR)/$1/c_src)") of - false -> []; - true -> - [{"priv/" ++ proplists:get_value(so_name, Conf, "$(1)_drv.so"), - proplists:get_value(port_sources, Conf, ["c_src/*.c"]), []}] - end; - {_, Specs} -> - lists:flatten([case S of - {Output, Input} -> {ShellToMk(Output), Input, []}; - {Regex, Output, Input} -> - case rebar_utils:is_arch(Regex) of - true -> {ShellToMk(Output), Input, []}; - false -> [] - end; - {Regex, Output, Input, [{env, Env}]} -> - case rebar_utils:is_arch(Regex) of - true -> {ShellToMk(Output), Input, Env}; - false -> [] - end - end || S <- Specs]) - end - end(), - PortSpecWrite = fun (Text) -> - file:write_file("$(call core_native_path,$(DEPS_DIR)/$1/c_src/Makefile.erlang.mk)", Text, [append]) - end, - case PortSpecs of - [] -> ok; - _ -> - Write("\npre-app::\n\t$$\(MAKE) -f c_src/Makefile.erlang.mk\n"), - PortSpecWrite(io_lib:format("ERL_CFLAGS = -finline-functions -Wall -fPIC -I \\"~s/erts-~s/include\\" -I \\"~s\\"\n", - [code:root_dir(), erlang:system_info(version), code:lib_dir(erl_interface, include)])), - PortSpecWrite(io_lib:format("ERL_LDFLAGS = -L \\"~s\\" -lerl_interface -lei\n", - [code:lib_dir(erl_interface, lib)])), - [PortSpecWrite(["\n", E, "\n"]) || E <- OsEnv], - FilterEnv = fun(Env) -> - lists:flatten([case E of - {_, _} -> E; - {Regex, K, V} -> - case rebar_utils:is_arch(Regex) of - true -> {K, V}; - false -> [] - end - end || E <- Env]) - end, - MergeEnv = fun(Env) -> - lists:foldl(fun ({K, V}, Acc) -> - case lists:keyfind(K, 1, Acc) of - false -> [{K, rebar_utils:expand_env_variable(V, K, "")}|Acc]; - {_, V0} -> [{K, rebar_utils:expand_env_variable(V, K, V0)}|Acc] - end - end, [], Env) - end, - PortEnv = case lists:keyfind(port_env, 1, Conf) of - false -> []; - {_, PortEnv0} -> FilterEnv(PortEnv0) - end, - PortSpec = fun ({Output, Input0, Env}) -> - filelib:ensure_dir("$(call core_native_path,$(DEPS_DIR)/$1/)" ++ Output), - Input = [[" ", I] || I <- Input0], - PortSpecWrite([ - [["\n", K, " = ", ShellToMk(V)] || {K, V} <- lists:reverse(MergeEnv(PortEnv))], - case $(PLATFORM) of - darwin -> "\n\nLDFLAGS += -flat_namespace -undefined suppress"; - _ -> "" - end, - "\n\nall:: ", Output, "\n\n", - "%.o: %.c\n\t$$\(CC) -c -o $$\@ $$\< $$\(CFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", - "%.o: %.C\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", - "%.o: %.cc\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", - "%.o: %.cpp\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", - [[Output, ": ", K, " = ", ShellToMk(V), "\n"] || {K, V} <- lists:reverse(MergeEnv(FilterEnv(Env)))], - Output, ": $$\(foreach ext,.c .C .cc .cpp,", - "$$\(patsubst %$$\(ext),%.o,$$\(filter %$$\(ext),$$\(wildcard", Input, "))))\n", - "\t$$\(CC) -o $$\@ $$\? $$\(LDFLAGS) $$\(ERL_LDFLAGS) $$\(DRV_LDFLAGS) $$\(EXE_LDFLAGS)", - case {filename:extension(Output), $(PLATFORM)} of - {[], _} -> "\n"; - {_, darwin} -> "\n"; - _ -> " -shared\n" - end]) - end, - [PortSpec(S) || S <- PortSpecs] - end, - Write("\ninclude $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(DEPS_DIR)/app)/erlang.mk"), - RunPlugin = fun(Plugin, Step) -> - case erlang:function_exported(Plugin, Step, 2) of - false -> ok; - true -> - c:cd("$(call core_native_path,$(DEPS_DIR)/$1/)"), - Ret = Plugin:Step({config, "", Conf, dict:new(), dict:new(), dict:new(), - dict:store(base_dir, "", dict:new())}, undefined), - io:format("rebar plugin ~p step ~p ret ~p~n", [Plugin, Step, Ret]) - end - end, - fun() -> - case lists:keyfind(plugins, 1, Conf) of - false -> ok; - {_, Plugins} -> - [begin - case lists:keyfind(deps, 1, Conf) of - false -> ok; - {_, Deps} -> - case lists:keyfind(P, 1, Deps) of - false -> ok; - _ -> - Path = "$(call core_native_path,$(DEPS_DIR)/)" ++ atom_to_list(P), - io:format("~s", [os:cmd("$(MAKE) -C $(call core_native_path,$(DEPS_DIR)/$1) " ++ Path)]), - io:format("~s", [os:cmd("$(MAKE) -C " ++ Path ++ " IS_DEP=1")]), - code:add_patha(Path ++ "/ebin") - end - end - end || P <- Plugins], - [case code:load_file(P) of - {module, P} -> ok; - _ -> - case lists:keyfind(plugin_dir, 1, Conf) of - false -> ok; - {_, PluginsDir} -> - ErlFile = "$(call core_native_path,$(DEPS_DIR)/$1/)" ++ PluginsDir ++ "/" ++ atom_to_list(P) ++ ".erl", - {ok, P, Bin} = compile:file(ErlFile, [binary]), - {module, P} = code:load_binary(P, ErlFile, Bin) - end - end || P <- Plugins], - [RunPlugin(P, preprocess) || P <- Plugins], - [RunPlugin(P, pre_compile) || P <- Plugins], - [RunPlugin(P, compile) || P <- Plugins] - end - end(), - halt() -endef - -define dep_autopatch_app.erl - UpdateModules = fun(App) -> - case filelib:is_regular(App) of - false -> ok; - true -> - {ok, [{application, '$(1)', L0}]} = file:consult(App), - Mods = filelib:fold_files("$(call core_native_path,$(DEPS_DIR)/$1/src)", "\\\\.erl$$", true, - fun (F, Acc) -> [list_to_atom(filename:rootname(filename:basename(F)))|Acc] end, []), - L = lists:keystore(modules, 1, L0, {modules, Mods}), - ok = file:write_file(App, io_lib:format("~p.~n", [{application, '$(1)', L}])) - end - end, - UpdateModules("$(call core_native_path,$(DEPS_DIR)/$1/ebin/$1.app)"), - halt() -endef - -define dep_autopatch_appsrc_script.erl - AppSrc = "$(call core_native_path,$(DEPS_DIR)/$1/src/$1.app.src)", - AppSrcScript = AppSrc ++ ".script", - Bindings = erl_eval:new_bindings(), - {ok, Conf} = file:script(AppSrcScript, Bindings), - ok = file:write_file(AppSrc, io_lib:format("~p.~n", [Conf])), - halt() -endef - -define dep_autopatch_appsrc.erl - AppSrcOut = "$(call core_native_path,$(DEPS_DIR)/$1/src/$1.app.src)", - AppSrcIn = case filelib:is_regular(AppSrcOut) of false -> "$(call core_native_path,$(DEPS_DIR)/$1/ebin/$1.app)"; true -> AppSrcOut end, - case filelib:is_regular(AppSrcIn) of - false -> ok; - true -> - {ok, [{application, $(1), L0}]} = file:consult(AppSrcIn), - L1 = lists:keystore(modules, 1, L0, {modules, []}), - L2 = case lists:keyfind(vsn, 1, L1) of {_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, "git"}); _ -> L1 end, - L3 = case lists:keyfind(registered, 1, L2) of false -> [{registered, []}|L2]; _ -> L2 end, - ok = file:write_file(AppSrcOut, io_lib:format("~p.~n", [{application, $(1), L3}])), - case AppSrcOut of AppSrcIn -> ok; _ -> ok = file:delete(AppSrcIn) end - end, - halt() -endef - -define dep_fetch_git - git clone -q -n -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); \ - cd $(DEPS_DIR)/$(call dep_name,$(1)) && git checkout -q $(call dep_commit,$(1)); -endef - -define dep_fetch_git-submodule - git submodule update --init -- $(DEPS_DIR)/$1; -endef - -define dep_fetch_hg - hg clone -q -U $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); \ - cd $(DEPS_DIR)/$(call dep_name,$(1)) && hg update -q $(call dep_commit,$(1)); -endef - -define dep_fetch_svn - svn checkout -q $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); -endef - -define dep_fetch_cp - cp -R $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); -endef - -define dep_fetch_hex.erl - ssl:start(), - inets:start(), - {ok, {{_, 200, _}, _, Body}} = httpc:request(get, - {"https://s3.amazonaws.com/s3.hex.pm/tarballs/$(1)-$(2).tar", []}, - [], [{body_format, binary}]), - {ok, Files} = erl_tar:extract({binary, Body}, [memory]), - {_, Source} = lists:keyfind("contents.tar.gz", 1, Files), - ok = erl_tar:extract({binary, Source}, [{cwd, "$(call core_native_path,$(DEPS_DIR)/$1)"}, compressed]), - halt() -endef - -# Hex only has a package version. No need to look in the Erlang.mk packages. -define dep_fetch_hex - $(call erlang,$(call dep_fetch_hex.erl,$(1),$(strip $(word 2,$(dep_$(1)))))); -endef - -define dep_fetch_fail - echo "Error: Unknown or invalid dependency: $(1)." >&2; \ - exit 78; -endef - -# Kept for compatibility purposes with older Erlang.mk configuration. -define dep_fetch_legacy - $(warning WARNING: '$(1)' dependency configuration uses deprecated format.) \ - git clone -q -n -- $(word 1,$(dep_$(1))) $(DEPS_DIR)/$(1); \ - cd $(DEPS_DIR)/$(1) && git checkout -q $(if $(word 2,$(dep_$(1))),$(word 2,$(dep_$(1))),master); -endef - -define dep_fetch - $(if $(dep_$(1)), \ - $(if $(dep_fetch_$(word 1,$(dep_$(1)))), \ - $(word 1,$(dep_$(1))), \ - $(if $(IS_DEP),legacy,fail)), \ - $(if $(filter $(1),$(PACKAGES)), \ - $(pkg_$(1)_fetch), \ - fail)) -endef - -define dep_target -$(DEPS_DIR)/$(call dep_name,$1): - $(eval DEP_NAME := $(call dep_name,$1)) - $(eval DEP_STR := $(if $(filter-out $1,$(DEP_NAME)),$1,"$1 ($(DEP_NAME))")) - $(verbose) if test -d $(APPS_DIR)/$(DEP_NAME); then \ - echo "Error: Dependency" $(DEP_STR) "conflicts with application found in $(APPS_DIR)/$(DEP_NAME)."; \ - exit 17; \ - fi - $(verbose) mkdir -p $(DEPS_DIR) - $(dep_verbose) $(call dep_fetch_$(strip $(call dep_fetch,$(1))),$(1)) - $(verbose) if [ -f $(DEPS_DIR)/$(1)/configure.ac -o -f $(DEPS_DIR)/$(1)/configure.in ] \ - && [ ! -f $(DEPS_DIR)/$(1)/configure ]; then \ - echo " AUTO " $(1); \ - cd $(DEPS_DIR)/$(1) && autoreconf -Wall -vif -I m4; \ - fi - - $(verbose) if [ -f $(DEPS_DIR)/$(DEP_NAME)/configure ]; then \ - echo " CONF " $(DEP_STR); \ - cd $(DEPS_DIR)/$(DEP_NAME) && ./configure; \ - fi -ifeq ($(filter $(1),$(NO_AUTOPATCH)),) - $(verbose) if [ "$(1)" = "amqp_client" -a "$(RABBITMQ_CLIENT_PATCH)" ]; then \ - if [ ! -d $(DEPS_DIR)/rabbitmq-codegen ]; then \ - echo " PATCH Downloading rabbitmq-codegen"; \ - git clone https://github.com/rabbitmq/rabbitmq-codegen.git $(DEPS_DIR)/rabbitmq-codegen; \ - fi; \ - if [ ! -d $(DEPS_DIR)/rabbitmq-server ]; then \ - echo " PATCH Downloading rabbitmq-server"; \ - git clone https://github.com/rabbitmq/rabbitmq-server.git $(DEPS_DIR)/rabbitmq-server; \ - fi; \ - ln -s $(DEPS_DIR)/amqp_client/deps/rabbit_common-0.0.0 $(DEPS_DIR)/rabbit_common; \ - elif [ "$(1)" = "rabbit" -a "$(RABBITMQ_SERVER_PATCH)" ]; then \ - if [ ! -d $(DEPS_DIR)/rabbitmq-codegen ]; then \ - echo " PATCH Downloading rabbitmq-codegen"; \ - git clone https://github.com/rabbitmq/rabbitmq-codegen.git $(DEPS_DIR)/rabbitmq-codegen; \ - fi \ - else \ - $$(call dep_autopatch,$(DEP_NAME)) \ - fi -endif -endef - -$(foreach dep,$(BUILD_DEPS) $(DEPS),$(eval $(call dep_target,$(dep)))) - -ifndef IS_APP -clean:: clean-apps - -clean-apps: - $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ - $(MAKE) -C $$dep clean IS_APP=1 || exit $$?; \ - done - -distclean:: distclean-apps - -distclean-apps: - $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ - $(MAKE) -C $$dep distclean IS_APP=1 || exit $$?; \ - done -endif - -ifndef SKIP_DEPS -distclean:: distclean-deps - -distclean-deps: - $(gen_verbose) rm -rf $(DEPS_DIR) -endif - -# External plugins. - -DEP_PLUGINS ?= - -define core_dep_plugin --include $(DEPS_DIR)/$(1) - -$(DEPS_DIR)/$(1): $(DEPS_DIR)/$(2) ; -endef - -$(foreach p,$(DEP_PLUGINS),\ - $(eval $(if $(findstring /,$p),\ - $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\ - $(call core_dep_plugin,$p/plugins.mk,$p)))) - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -# Configuration. - -DTL_FULL_PATH ?= -DTL_PATH ?= templates/ -DTL_SUFFIX ?= _dtl -DTL_OPTS ?= - -# Verbosity. - -dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); -dtl_verbose = $(dtl_verbose_$(V)) - -# Core targets. - -DTL_FILES = $(sort $(call core_find,$(DTL_PATH),*.dtl)) - -ifneq ($(DTL_FILES),) - -ifdef DTL_FULL_PATH -BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(subst /,_,$(DTL_FILES:$(DTL_PATH)%=%)))) -else -BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(notdir $(DTL_FILES)))) -endif - -# Rebuild templates when the Makefile changes. -$(DTL_FILES): $(MAKEFILE_LIST) - @touch $@ - -define erlydtl_compile.erl - [begin - Module0 = case "$(strip $(DTL_FULL_PATH))" of - "" -> - filename:basename(F, ".dtl"); - _ -> - "$(DTL_PATH)" ++ F2 = filename:rootname(F, ".dtl"), - re:replace(F2, "/", "_", [{return, list}, global]) - end, - Module = list_to_atom(string:to_lower(Module0) ++ "$(DTL_SUFFIX)"), - case erlydtl:compile(F, Module, [$(DTL_OPTS)] ++ [{out_dir, "ebin/"}, return_errors, {doc_root, "templates"}]) of - ok -> ok; - {ok, _} -> ok - end - end || F <- string:tokens("$(1)", " ")], - halt(). -endef - -ebin/$(PROJECT).app:: $(DTL_FILES) | ebin/ - $(if $(strip $?),\ - $(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$?),-pa ebin/ $(DEPS_DIR)/erlydtl/ebin/)) - -endif - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -# Verbosity. - -proto_verbose_0 = @echo " PROTO " $(filter %.proto,$(?F)); -proto_verbose = $(proto_verbose_$(V)) - -# Core targets. - -define compile_proto - $(verbose) mkdir -p ebin/ include/ - $(proto_verbose) $(call erlang,$(call compile_proto.erl,$(1))) - $(proto_verbose) erlc +debug_info -o ebin/ ebin/*.erl - $(verbose) rm ebin/*.erl -endef - -define compile_proto.erl - [begin - Dir = filename:dirname(filename:dirname(F)), - protobuffs_compile:generate_source(F, - [{output_include_dir, Dir ++ "/include"}, - {output_src_dir, Dir ++ "/ebin"}]) - end || F <- string:tokens("$(1)", " ")], - halt(). -endef - -ifneq ($(wildcard src/),) -ebin/$(PROJECT).app:: $(sort $(call core_find,src/,*.proto)) - $(if $(strip $?),$(call compile_proto,$?)) -endif - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: clean-app - -# Configuration. - -ERLC_OPTS ?= -Werror +debug_info +warn_export_vars +warn_shadow_vars \ - +warn_obsolete_guard # +bin_opt_info +warn_export_all +warn_missing_spec -COMPILE_FIRST ?= -COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST))) -ERLC_EXCLUDE ?= -ERLC_EXCLUDE_PATHS = $(addprefix src/,$(addsuffix .erl,$(ERLC_EXCLUDE))) - -ERLC_MIB_OPTS ?= -COMPILE_MIB_FIRST ?= -COMPILE_MIB_FIRST_PATHS = $(addprefix mibs/,$(addsuffix .mib,$(COMPILE_MIB_FIRST))) - -# Verbosity. - -app_verbose_0 = @echo " APP " $(PROJECT); -app_verbose_2 = set -x; -app_verbose = $(app_verbose_$(V)) - -appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src; -appsrc_verbose_2 = set -x; -appsrc_verbose = $(appsrc_verbose_$(V)) - -makedep_verbose_0 = @echo " DEPEND" $(PROJECT).d; -makedep_verbose_2 = set -x; -makedep_verbose = $(makedep_verbose_$(V)) - -erlc_verbose_0 = @echo " ERLC " $(filter-out $(patsubst %,%.erl,$(ERLC_EXCLUDE)),\ - $(filter %.erl %.core,$(?F))); -erlc_verbose_2 = set -x; -erlc_verbose = $(erlc_verbose_$(V)) - -xyrl_verbose_0 = @echo " XYRL " $(filter %.xrl %.yrl,$(?F)); -xyrl_verbose_2 = set -x; -xyrl_verbose = $(xyrl_verbose_$(V)) - -asn1_verbose_0 = @echo " ASN1 " $(filter %.asn1,$(?F)); -asn1_verbose_2 = set -x; -asn1_verbose = $(asn1_verbose_$(V)) - -mib_verbose_0 = @echo " MIB " $(filter %.bin %.mib,$(?F)); -mib_verbose_2 = set -x; -mib_verbose = $(mib_verbose_$(V)) - -ifneq ($(wildcard src/),) - -# Targets. - -ifeq ($(wildcard ebin/test),) -app:: deps $(PROJECT).d - $(verbose) $(MAKE) --no-print-directory app-build -else -app:: clean deps $(PROJECT).d - $(verbose) $(MAKE) --no-print-directory app-build -endif - -ifeq ($(wildcard src/$(PROJECT_MOD).erl),) -define app_file -{application, $(PROJECT), [ - {description, "$(PROJECT_DESCRIPTION)"}, - {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), - {id$(comma)$(space)"$(1)"}$(comma)) - {modules, [$(call comma_list,$(2))]}, - {registered, []}, - {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS))]} -]}. -endef -else -define app_file -{application, $(PROJECT), [ - {description, "$(PROJECT_DESCRIPTION)"}, - {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), - {id$(comma)$(space)"$(1)"}$(comma)) - {modules, [$(call comma_list,$(2))]}, - {registered, [$(call comma_list,$(PROJECT)_sup $(PROJECT_REGISTERED))]}, - {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS))]}, - {mod, {$(PROJECT_MOD), []}} -]}. -endef -endif - -app-build: ebin/$(PROJECT).app - $(verbose) : - -# Source files. - -ERL_FILES = $(sort $(call core_find,src/,*.erl)) -CORE_FILES = $(sort $(call core_find,src/,*.core)) - -# ASN.1 files. - -ifneq ($(wildcard asn1/),) -ASN1_FILES = $(sort $(call core_find,asn1/,*.asn1)) -ERL_FILES += $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES)))) - -define compile_asn1 - $(verbose) mkdir -p include/ - $(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(1) - $(verbose) mv asn1/*.erl src/ - $(verbose) mv asn1/*.hrl include/ - $(verbose) mv asn1/*.asn1db include/ -endef - -$(PROJECT).d:: $(ASN1_FILES) - $(if $(strip $?),$(call compile_asn1,$?)) -endif - -# SNMP MIB files. - -ifneq ($(wildcard mibs/),) -MIB_FILES = $(sort $(call core_find,mibs/,*.mib)) - -$(PROJECT).d:: $(COMPILE_MIB_FIRST_PATHS) $(MIB_FILES) - $(verbose) mkdir -p include/ priv/mibs/ - $(mib_verbose) erlc -v $(ERLC_MIB_OPTS) -o priv/mibs/ -I priv/mibs/ $? - $(mib_verbose) erlc -o include/ -- $(addprefix priv/mibs/,$(patsubst %.mib,%.bin,$(notdir $?))) -endif - -# Leex and Yecc files. - -XRL_FILES = $(sort $(call core_find,src/,*.xrl)) -XRL_ERL_FILES = $(addprefix src/,$(patsubst %.xrl,%.erl,$(notdir $(XRL_FILES)))) -ERL_FILES += $(XRL_ERL_FILES) - -YRL_FILES = $(sort $(call core_find,src/,*.yrl)) -YRL_ERL_FILES = $(addprefix src/,$(patsubst %.yrl,%.erl,$(notdir $(YRL_FILES)))) -ERL_FILES += $(YRL_ERL_FILES) - -$(PROJECT).d:: $(XRL_FILES) $(YRL_FILES) - $(if $(strip $?),$(xyrl_verbose) erlc -v -o src/ $?) - -# Erlang and Core Erlang files. - -define makedep.erl - E = ets:new(makedep, [bag]), - G = digraph:new([acyclic]), - ErlFiles = lists:usort(string:tokens("$(ERL_FILES)", " ")), - Modules = [{list_to_atom(filename:basename(F, ".erl")), F} || F <- ErlFiles], - Add = fun (Mod, Dep) -> - case lists:keyfind(Dep, 1, Modules) of - false -> ok; - {_, DepFile} -> - {_, ModFile} = lists:keyfind(Mod, 1, Modules), - ets:insert(E, {ModFile, DepFile}), - digraph:add_vertex(G, Mod), - digraph:add_vertex(G, Dep), - digraph:add_edge(G, Mod, Dep) - end - end, - AddHd = fun (F, Mod, DepFile) -> - case file:open(DepFile, [read]) of - {error, enoent} -> ok; - {ok, Fd} -> - F(F, Fd, Mod), - {_, ModFile} = lists:keyfind(Mod, 1, Modules), - ets:insert(E, {ModFile, DepFile}) - end - end, - Attr = fun - (F, Mod, behavior, Dep) -> Add(Mod, Dep); - (F, Mod, behaviour, Dep) -> Add(Mod, Dep); - (F, Mod, compile, {parse_transform, Dep}) -> Add(Mod, Dep); - (F, Mod, compile, Opts) when is_list(Opts) -> - case proplists:get_value(parse_transform, Opts) of - undefined -> ok; - Dep -> Add(Mod, Dep) - end; - (F, Mod, include, Hrl) -> - case filelib:is_file("include/" ++ Hrl) of - true -> AddHd(F, Mod, "include/" ++ Hrl); - false -> - case filelib:is_file("src/" ++ Hrl) of - true -> AddHd(F, Mod, "src/" ++ Hrl); - false -> false - end - end; - (F, Mod, include_lib, "$1/include/" ++ Hrl) -> AddHd(F, Mod, "include/" ++ Hrl); - (F, Mod, include_lib, Hrl) -> AddHd(F, Mod, "include/" ++ Hrl); - (F, Mod, import, {Imp, _}) -> - case filelib:is_file("src/" ++ atom_to_list(Imp) ++ ".erl") of - false -> ok; - true -> Add(Mod, Imp) - end; - (_, _, _, _) -> ok - end, - MakeDepend = fun(F, Fd, Mod) -> - case io:parse_erl_form(Fd, undefined) of - {ok, {attribute, _, Key, Value}, _} -> - Attr(F, Mod, Key, Value), - F(F, Fd, Mod); - {eof, _} -> - file:close(Fd); - _ -> - F(F, Fd, Mod) - end - end, - [begin - Mod = list_to_atom(filename:basename(F, ".erl")), - {ok, Fd} = file:open(F, [read]), - MakeDepend(MakeDepend, Fd, Mod) - end || F <- ErlFiles], - Depend = sofs:to_external(sofs:relation_to_family(sofs:relation(ets:tab2list(E)))), - CompileFirst = [X || X <- lists:reverse(digraph_utils:topsort(G)), [] =/= digraph:in_neighbours(G, X)], - ok = file:write_file("$(1)", [ - [[F, "::", [[" ", D] || D <- Deps], "; @touch \$$@\n"] || {F, Deps} <- Depend], - "\nCOMPILE_FIRST +=", [[" ", atom_to_list(CF)] || CF <- CompileFirst], "\n" - ]), - halt() -endef - -ifeq ($(if $(NO_MAKEDEP),$(wildcard $(PROJECT).d),),) -$(PROJECT).d:: $(ERL_FILES) $(call core_find,include/,*.hrl) - $(makedep_verbose) $(call erlang,$(call makedep.erl,$@)) -endif - -# Rebuild everything when the Makefile changes. -$(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(MAKEFILE_LIST) - @touch $@ - --include $(PROJECT).d - -ebin/$(PROJECT).app:: ebin/ - -ebin/: - $(verbose) mkdir -p ebin/ - -define compile_erl - $(erlc_verbose) erlc -v $(if $(IS_DEP),$(filter-out -Werror,$(ERLC_OPTS)),$(ERLC_OPTS)) -o ebin/ \ - -pa ebin/ -I include/ $(filter-out $(ERLC_EXCLUDE_PATHS),$(COMPILE_FIRST_PATHS) $(1)) -endef - -ebin/$(PROJECT).app:: $(ERL_FILES) $(CORE_FILES) $(wildcard src/$(PROJECT).app.src) - $(eval FILES_TO_COMPILE := $(filter-out src/$(PROJECT).app.src,$?)) - $(if $(strip $(FILES_TO_COMPILE)),$(call compile_erl,$(FILES_TO_COMPILE))) - $(eval GITDESCRIBE := $(shell git describe --dirty --abbrev=7 --tags --always --first-parent 2>/dev/null || true)) - $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \ - $(filter-out $(ERLC_EXCLUDE_PATHS),$(ERL_FILES) $(CORE_FILES) $(BEAM_FILES))))))) -ifeq ($(wildcard src/$(PROJECT).app.src),) - $(app_verbose) printf "$(subst $(newline),\n,$(subst ",\",$(call app_file,$(GITDESCRIBE),$(MODULES))))" \ - > ebin/$(PROJECT).app -else - $(verbose) if [ -z "$$(grep -e '^[^%]*{\s*modules\s*,' src/$(PROJECT).app.src)" ]; then \ - echo "Empty modules entry not found in $(PROJECT).app.src. Please consult the erlang.mk README for instructions." >&2; \ - exit 1; \ - fi - $(appsrc_verbose) cat src/$(PROJECT).app.src \ - | sed "s/{[[:space:]]*modules[[:space:]]*,[[:space:]]*\[\]}/{modules, \[$(call comma_list,$(MODULES))\]}/" \ - | sed "s/{vsn,[[:space:]]*\"git\"}/{vsn, \"$(subst /,\/,$(GITDESCRIBE))\"}/" \ - > ebin/$(PROJECT).app -endif - -clean:: clean-app - -clean-app: - $(gen_verbose) rm -rf $(PROJECT).d ebin/ priv/mibs/ $(XRL_ERL_FILES) $(YRL_ERL_FILES) \ - $(addprefix include/,$(patsubst %.mib,%.hrl,$(notdir $(MIB_FILES)))) \ - $(addprefix include/,$(patsubst %.asn1,%.hrl,$(notdir $(ASN1_FILES)))) \ - $(addprefix include/,$(patsubst %.asn1,%.asn1db,$(notdir $(ASN1_FILES)))) \ - $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES)))) - -endif - -# Copyright (c) 2015, Viktor Söderqvist -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: docs-deps - -# Configuration. - -ALL_DOC_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DOC_DEPS)) - -# Targets. - -$(foreach dep,$(DOC_DEPS),$(eval $(call dep_target,$(dep)))) - -ifneq ($(SKIP_DEPS),) -doc-deps: -else -doc-deps: $(ALL_DOC_DEPS_DIRS) - $(verbose) for dep in $(ALL_DOC_DEPS_DIRS) ; do $(MAKE) -C $$dep; done -endif - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: rel-deps - -# Configuration. - -ALL_REL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(REL_DEPS)) - -# Targets. - -$(foreach dep,$(REL_DEPS),$(eval $(call dep_target,$(dep)))) - -ifneq ($(SKIP_DEPS),) -rel-deps: -else -rel-deps: $(ALL_REL_DEPS_DIRS) - $(verbose) for dep in $(ALL_REL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done -endif - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: test-deps test-dir test-build clean-test-dir - -# Configuration. - -TEST_DIR ?= $(CURDIR)/test - -ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS)) - -TEST_ERLC_OPTS ?= +debug_info +warn_export_vars +warn_shadow_vars +warn_obsolete_guard -TEST_ERLC_OPTS += -DTEST=1 - -# Targets. - -$(foreach dep,$(TEST_DEPS),$(eval $(call dep_target,$(dep)))) - -ifneq ($(SKIP_DEPS),) -test-deps: -else -test-deps: $(ALL_TEST_DEPS_DIRS) - $(verbose) for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep IS_DEP=1; done -endif - -ifneq ($(wildcard $(TEST_DIR)),) -test-dir: - $(gen_verbose) erlc -v $(TEST_ERLC_OPTS) -I include/ -o $(TEST_DIR) \ - $(call core_find,$(TEST_DIR)/,*.erl) -pa ebin/ -endif - -ifeq ($(wildcard src),) -test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS) -test-build:: clean deps test-deps - $(verbose) $(MAKE) --no-print-directory test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" -else -ifeq ($(wildcard ebin/test),) -test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS) -test-build:: clean deps test-deps $(PROJECT).d - $(verbose) $(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" - $(gen_verbose) touch ebin/test -else -test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS) -test-build:: deps test-deps $(PROJECT).d - $(verbose) $(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" -endif - -clean:: clean-test-dir - -clean-test-dir: -ifneq ($(wildcard $(TEST_DIR)/*.beam),) - $(gen_verbose) rm -f $(TEST_DIR)/*.beam -endif -endif - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: rebar.config - -# We strip out -Werror because we don't want to fail due to -# warnings when used as a dependency. - -compat_prepare_erlc_opts = $(shell echo "$1" | sed 's/, */,/g') - -define compat_convert_erlc_opts -$(if $(filter-out -Werror,$1),\ - $(if $(findstring +,$1),\ - $(shell echo $1 | cut -b 2-))) -endef - -define compat_erlc_opts_to_list -[$(call comma_list,$(foreach o,$(call compat_prepare_erlc_opts,$1),$(call compat_convert_erlc_opts,$o)))] -endef - -define compat_rebar_config -{deps, [ -$(call comma_list,$(foreach d,$(DEPS),\ - $(if $(filter hex,$(call dep_fetch,$d)),\ - {$(call dep_name,$d)$(comma)"$(call dep_repo,$d)"},\ - {$(call dep_name,$d)$(comma)".*"$(comma){git,"$(call dep_repo,$d)"$(comma)"$(call dep_commit,$d)"}}))) -]}. -{erl_opts, $(call compat_erlc_opts_to_list,$(ERLC_OPTS))}. -endef - -$(eval _compat_rebar_config = $$(compat_rebar_config)) -$(eval export _compat_rebar_config) - -rebar.config: - $(gen_verbose) echo "$${_compat_rebar_config}" > rebar.config - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: asciidoc asciidoc-guide asciidoc-manual install-asciidoc distclean-asciidoc - -MAN_INSTALL_PATH ?= /usr/local/share/man -MAN_SECTIONS ?= 3 7 - -docs:: asciidoc - -asciidoc: asciidoc-guide asciidoc-manual - -ifeq ($(wildcard doc/src/guide/book.asciidoc),) -asciidoc-guide: -else -asciidoc-guide: distclean-asciidoc doc-deps - a2x -v -f pdf doc/src/guide/book.asciidoc && mv doc/src/guide/book.pdf doc/guide.pdf - a2x -v -f chunked doc/src/guide/book.asciidoc && mv doc/src/guide/book.chunked/ doc/html/ -endif - -ifeq ($(wildcard doc/src/manual/*.asciidoc),) -asciidoc-manual: -else -asciidoc-manual: distclean-asciidoc doc-deps - for f in doc/src/manual/*.asciidoc ; do \ - a2x -v -f manpage $$f ; \ - done - for s in $(MAN_SECTIONS); do \ - mkdir -p doc/man$$s/ ; \ - mv doc/src/manual/*.$$s doc/man$$s/ ; \ - gzip doc/man$$s/*.$$s ; \ - done - -install-docs:: install-asciidoc - -install-asciidoc: asciidoc-manual - for s in $(MAN_SECTIONS); do \ - mkdir -p $(MAN_INSTALL_PATH)/man$$s/ ; \ - install -g `id -u` -o `id -g` -m 0644 doc/man$$s/*.gz $(MAN_INSTALL_PATH)/man$$s/ ; \ - done -endif - -distclean:: distclean-asciidoc - -distclean-asciidoc: - $(gen_verbose) rm -rf doc/html/ doc/guide.pdf doc/man3/ doc/man7/ - -# Copyright (c) 2014-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: bootstrap bootstrap-lib bootstrap-rel new list-templates - -# Core targets. - -help:: - $(verbose) printf "%s\n" "" \ - "Bootstrap targets:" \ - " bootstrap Generate a skeleton of an OTP application" \ - " bootstrap-lib Generate a skeleton of an OTP library" \ - " bootstrap-rel Generate the files needed to build a release" \ - " new-app in=NAME Create a new local OTP application NAME" \ - " new-lib in=NAME Create a new local OTP library NAME" \ - " new t=TPL n=NAME Generate a module NAME based on the template TPL" \ - " new t=T n=N in=APP Generate a module NAME based on the template TPL in APP" \ - " list-templates List available templates" - -# Bootstrap templates. - -define bs_appsrc -{application, $p, [ - {description, ""}, - {vsn, "0.1.0"}, - {id, "git"}, - {modules, []}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, {$p_app, []}}, - {env, []} -]}. -endef - -define bs_appsrc_lib -{application, $p, [ - {description, ""}, - {vsn, "0.1.0"}, - {id, "git"}, - {modules, []}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]} -]}. -endef - -# To prevent autocompletion issues with ZSH, we add "include erlang.mk" -# separately during the actual bootstrap. -ifdef SP -define bs_Makefile -PROJECT = $p -PROJECT_DESCRIPTION = New project -PROJECT_VERSION = 0.0.1 - -# Whitespace to be used when creating files from templates. -SP = $(SP) - -endef -else -define bs_Makefile -PROJECT = $p -PROJECT_DESCRIPTION = New project -PROJECT_VERSION = 0.0.1 - -endef -endif - -define bs_apps_Makefile -PROJECT = $p -PROJECT_DESCRIPTION = New project -PROJECT_VERSION = 0.0.1 - -include $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(APPS_DIR)/app)/erlang.mk -endef - -define bs_app --module($p_app). --behaviour(application). - --export([start/2]). --export([stop/1]). - -start(_Type, _Args) -> - $p_sup:start_link(). - -stop(_State) -> - ok. -endef - -define bs_relx_config -{release, {$p_release, "1"}, [$p]}. -{extended_start_script, true}. -{sys_config, "rel/sys.config"}. -{vm_args, "rel/vm.args"}. -endef - -define bs_sys_config -[ -]. -endef - -define bs_vm_args --name $p@127.0.0.1 --setcookie $p --heart -endef - -# Normal templates. - -define tpl_supervisor --module($(n)). --behaviour(supervisor). - --export([start_link/0]). --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - Procs = [], - {ok, {{one_for_one, 1, 5}, Procs}}. -endef - -define tpl_gen_server --module($(n)). --behaviour(gen_server). - -%% API. --export([start_link/0]). - -%% gen_server. --export([init/1]). --export([handle_call/3]). --export([handle_cast/2]). --export([handle_info/2]). --export([terminate/2]). --export([code_change/3]). - --record(state, { -}). - -%% API. - --spec start_link() -> {ok, pid()}. -start_link() -> - gen_server:start_link(?MODULE, [], []). - -%% gen_server. - -init([]) -> - {ok, #state{}}. - -handle_call(_Request, _From, State) -> - {reply, ignored, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. -endef - -define tpl_module --module($(n)). --export([]). -endef - -define tpl_cowboy_http --module($(n)). --behaviour(cowboy_http_handler). - --export([init/3]). --export([handle/2]). --export([terminate/3]). - --record(state, { -}). - -init(_, Req, _Opts) -> - {ok, Req, #state{}}. - -handle(Req, State=#state{}) -> - {ok, Req2} = cowboy_req:reply(200, Req), - {ok, Req2, State}. - -terminate(_Reason, _Req, _State) -> - ok. -endef - -define tpl_gen_fsm --module($(n)). --behaviour(gen_fsm). - -%% API. --export([start_link/0]). - -%% gen_fsm. --export([init/1]). --export([state_name/2]). --export([handle_event/3]). --export([state_name/3]). --export([handle_sync_event/4]). --export([handle_info/3]). --export([terminate/3]). --export([code_change/4]). - --record(state, { -}). - -%% API. - --spec start_link() -> {ok, pid()}. -start_link() -> - gen_fsm:start_link(?MODULE, [], []). - -%% gen_fsm. - -init([]) -> - {ok, state_name, #state{}}. - -state_name(_Event, StateData) -> - {next_state, state_name, StateData}. - -handle_event(_Event, StateName, StateData) -> - {next_state, StateName, StateData}. - -state_name(_Event, _From, StateData) -> - {reply, ignored, state_name, StateData}. - -handle_sync_event(_Event, _From, StateName, StateData) -> - {reply, ignored, StateName, StateData}. - -handle_info(_Info, StateName, StateData) -> - {next_state, StateName, StateData}. - -terminate(_Reason, _StateName, _StateData) -> - ok. - -code_change(_OldVsn, StateName, StateData, _Extra) -> - {ok, StateName, StateData}. -endef - -define tpl_cowboy_loop --module($(n)). --behaviour(cowboy_loop_handler). - --export([init/3]). --export([info/3]). --export([terminate/3]). - --record(state, { -}). - -init(_, Req, _Opts) -> - {loop, Req, #state{}, 5000, hibernate}. - -info(_Info, Req, State) -> - {loop, Req, State, hibernate}. - -terminate(_Reason, _Req, _State) -> - ok. -endef - -define tpl_cowboy_rest --module($(n)). - --export([init/3]). --export([content_types_provided/2]). --export([get_html/2]). - -init(_, _Req, _Opts) -> - {upgrade, protocol, cowboy_rest}. - -content_types_provided(Req, State) -> - {[{{<<"text">>, <<"html">>, '*'}, get_html}], Req, State}. - -get_html(Req, State) -> - {<<"This is REST!">>, Req, State}. -endef - -define tpl_cowboy_ws --module($(n)). --behaviour(cowboy_websocket_handler). - --export([init/3]). --export([websocket_init/3]). --export([websocket_handle/3]). --export([websocket_info/3]). --export([websocket_terminate/3]). - --record(state, { -}). - -init(_, _, _) -> - {upgrade, protocol, cowboy_websocket}. - -websocket_init(_, Req, _Opts) -> - Req2 = cowboy_req:compact(Req), - {ok, Req2, #state{}}. - -websocket_handle({text, Data}, Req, State) -> - {reply, {text, Data}, Req, State}; -websocket_handle({binary, Data}, Req, State) -> - {reply, {binary, Data}, Req, State}; -websocket_handle(_Frame, Req, State) -> - {ok, Req, State}. - -websocket_info(_Info, Req, State) -> - {ok, Req, State}. - -websocket_terminate(_Reason, _Req, _State) -> - ok. -endef - -define tpl_ranch_protocol --module($(n)). --behaviour(ranch_protocol). - --export([start_link/4]). --export([init/4]). - --type opts() :: []. --export_type([opts/0]). - --record(state, { - socket :: inet:socket(), - transport :: module() -}). - -start_link(Ref, Socket, Transport, Opts) -> - Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts]), - {ok, Pid}. - --spec init(ranch:ref(), inet:socket(), module(), opts()) -> ok. -init(Ref, Socket, Transport, _Opts) -> - ok = ranch:accept_ack(Ref), - loop(#state{socket=Socket, transport=Transport}). - -loop(State) -> - loop(State). -endef - -# Plugin-specific targets. - -define render_template - $(verbose) printf -- '$(subst $(newline),\n,$(subst %,%%,$(subst ','\'',$(subst $(tab),$(WS),$(call $(1))))))\n' > $(2) -endef - -ifndef WS -ifdef SP -WS = $(subst a,,a $(wordlist 1,$(SP),a a a a a a a a a a a a a a a a a a a a)) -else -WS = $(tab) -endif -endif - -bootstrap: -ifneq ($(wildcard src/),) - $(error Error: src/ directory already exists) -endif - $(eval p := $(PROJECT)) - $(eval n := $(PROJECT)_sup) - $(call render_template,bs_Makefile,Makefile) - $(verbose) echo "include erlang.mk" >> Makefile - $(verbose) mkdir src/ -ifdef LEGACY - $(call render_template,bs_appsrc,src/$(PROJECT).app.src) -endif - $(call render_template,bs_app,src/$(PROJECT)_app.erl) - $(call render_template,tpl_supervisor,src/$(PROJECT)_sup.erl) - -bootstrap-lib: -ifneq ($(wildcard src/),) - $(error Error: src/ directory already exists) -endif - $(eval p := $(PROJECT)) - $(call render_template,bs_Makefile,Makefile) - $(verbose) echo "include erlang.mk" >> Makefile - $(verbose) mkdir src/ -ifdef LEGACY - $(call render_template,bs_appsrc_lib,src/$(PROJECT).app.src) -endif - -bootstrap-rel: -ifneq ($(wildcard relx.config),) - $(error Error: relx.config already exists) -endif -ifneq ($(wildcard rel/),) - $(error Error: rel/ directory already exists) -endif - $(eval p := $(PROJECT)) - $(call render_template,bs_relx_config,relx.config) - $(verbose) mkdir rel/ - $(call render_template,bs_sys_config,rel/sys.config) - $(call render_template,bs_vm_args,rel/vm.args) - -new-app: -ifndef in - $(error Usage: $(MAKE) new-app in=APP) -endif -ifneq ($(wildcard $(APPS_DIR)/$in),) - $(error Error: Application $in already exists) -endif - $(eval p := $(in)) - $(eval n := $(in)_sup) - $(verbose) mkdir -p $(APPS_DIR)/$p/src/ - $(call render_template,bs_apps_Makefile,$(APPS_DIR)/$p/Makefile) -ifdef LEGACY - $(call render_template,bs_appsrc,$(APPS_DIR)/$p/src/$p.app.src) -endif - $(call render_template,bs_app,$(APPS_DIR)/$p/src/$p_app.erl) - $(call render_template,tpl_supervisor,$(APPS_DIR)/$p/src/$p_sup.erl) - -new-lib: -ifndef in - $(error Usage: $(MAKE) new-lib in=APP) -endif -ifneq ($(wildcard $(APPS_DIR)/$in),) - $(error Error: Application $in already exists) -endif - $(eval p := $(in)) - $(verbose) mkdir -p $(APPS_DIR)/$p/src/ - $(call render_template,bs_apps_Makefile,$(APPS_DIR)/$p/Makefile) -ifdef LEGACY - $(call render_template,bs_appsrc_lib,$(APPS_DIR)/$p/src/$p.app.src) -endif - -new: -ifeq ($(wildcard src/)$(in),) - $(error Error: src/ directory does not exist) -endif -ifndef t - $(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP]) -endif -ifndef tpl_$(t) - $(error Unknown template) -endif -ifndef n - $(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP]) -endif -ifdef in - $(verbose) $(MAKE) -C $(APPS_DIR)/$(in)/ new t=$t n=$n in= -else - $(call render_template,tpl_$(t),src/$(n).erl) -endif - -list-templates: - $(verbose) echo Available templates: $(sort $(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES)))) - -# Copyright (c) 2014-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: clean-c_src distclean-c_src-env - -# Configuration. - -C_SRC_DIR ?= $(CURDIR)/c_src -C_SRC_ENV ?= $(C_SRC_DIR)/env.mk -C_SRC_OUTPUT ?= $(CURDIR)/priv/$(PROJECT) -C_SRC_TYPE ?= shared - -# System type and C compiler/flags. - -ifeq ($(PLATFORM),msys2) - C_SRC_OUTPUT_EXECUTABLE_EXTENSION ?= .exe - C_SRC_OUTPUT_SHARED_EXTENSION ?= .dll -else - C_SRC_OUTPUT_EXECUTABLE_EXTENSION ?= - C_SRC_OUTPUT_SHARED_EXTENSION ?= .so -endif - -ifeq ($(C_SRC_TYPE),shared) - C_SRC_OUTPUT_FILE = $(C_SRC_OUTPUT)$(C_SRC_OUTPUT_SHARED_EXTENSION) -else - C_SRC_OUTPUT_FILE = $(C_SRC_OUTPUT)$(C_SRC_OUTPUT_EXECUTABLE_EXTENSION) -endif - -ifeq ($(PLATFORM),msys2) -# We hardcode the compiler used on MSYS2. The default CC=cc does -# not produce working code. The "gcc" MSYS2 package also doesn't. - CC = /mingw64/bin/gcc - export CC - CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes - CXXFLAGS ?= -O3 -finline-functions -Wall -else ifeq ($(PLATFORM),darwin) - CC ?= cc - CFLAGS ?= -O3 -std=c99 -arch x86_64 -finline-functions -Wall -Wmissing-prototypes - CXXFLAGS ?= -O3 -arch x86_64 -finline-functions -Wall - LDFLAGS ?= -arch x86_64 -flat_namespace -undefined suppress -else ifeq ($(PLATFORM),freebsd) - CC ?= cc - CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes - CXXFLAGS ?= -O3 -finline-functions -Wall -else ifeq ($(PLATFORM),linux) - CC ?= gcc - CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes - CXXFLAGS ?= -O3 -finline-functions -Wall -endif - -ifneq ($(PLATFORM),msys2) - CFLAGS += -fPIC - CXXFLAGS += -fPIC -endif - -CFLAGS += -I"$(ERTS_INCLUDE_DIR)" -I"$(ERL_INTERFACE_INCLUDE_DIR)" -CXXFLAGS += -I"$(ERTS_INCLUDE_DIR)" -I"$(ERL_INTERFACE_INCLUDE_DIR)" - -LDLIBS += -L"$(ERL_INTERFACE_LIB_DIR)" -lerl_interface -lei - -# Verbosity. - -c_verbose_0 = @echo " C " $(?F); -c_verbose = $(c_verbose_$(V)) - -cpp_verbose_0 = @echo " CPP " $(?F); -cpp_verbose = $(cpp_verbose_$(V)) - -link_verbose_0 = @echo " LD " $(@F); -link_verbose = $(link_verbose_$(V)) - -# Targets. - -ifeq ($(wildcard $(C_SRC_DIR)),) -else ifneq ($(wildcard $(C_SRC_DIR)/Makefile),) -app:: app-c_src - -test-build:: app-c_src - -app-c_src: - $(MAKE) -C $(C_SRC_DIR) - -clean:: - $(MAKE) -C $(C_SRC_DIR) clean - -else - -ifeq ($(SOURCES),) -SOURCES := $(sort $(foreach pat,*.c *.C *.cc *.cpp,$(call core_find,$(C_SRC_DIR)/,$(pat)))) -endif -OBJECTS = $(addsuffix .o, $(basename $(SOURCES))) - -COMPILE_C = $(c_verbose) $(CC) $(CFLAGS) $(CPPFLAGS) -c -COMPILE_CPP = $(cpp_verbose) $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c - -app:: $(C_SRC_ENV) $(C_SRC_OUTPUT_FILE) - -test-build:: $(C_SRC_ENV) $(C_SRC_OUTPUT_FILE) - -$(C_SRC_OUTPUT_FILE): $(OBJECTS) - $(verbose) mkdir -p priv/ - $(link_verbose) $(CC) $(OBJECTS) \ - $(LDFLAGS) $(if $(filter $(C_SRC_TYPE),shared),-shared) $(LDLIBS) \ - -o $(C_SRC_OUTPUT_FILE) - -%.o: %.c - $(COMPILE_C) $(OUTPUT_OPTION) $< - -%.o: %.cc - $(COMPILE_CPP) $(OUTPUT_OPTION) $< - -%.o: %.C - $(COMPILE_CPP) $(OUTPUT_OPTION) $< - -%.o: %.cpp - $(COMPILE_CPP) $(OUTPUT_OPTION) $< - -clean:: clean-c_src - -clean-c_src: - $(gen_verbose) rm -f $(C_SRC_OUTPUT_FILE) $(OBJECTS) - -endif - -ifneq ($(wildcard $(C_SRC_DIR)),) -$(C_SRC_ENV): - $(verbose) $(ERL) -eval "file:write_file(\"$(call core_native_path,$(C_SRC_ENV))\", \ - io_lib:format( \ - \"ERTS_INCLUDE_DIR ?= ~s/erts-~s/include/~n\" \ - \"ERL_INTERFACE_INCLUDE_DIR ?= ~s~n\" \ - \"ERL_INTERFACE_LIB_DIR ?= ~s~n\", \ - [code:root_dir(), erlang:system_info(version), \ - code:lib_dir(erl_interface, include), \ - code:lib_dir(erl_interface, lib)])), \ - halt()." - -distclean:: distclean-c_src-env - -distclean-c_src-env: - $(gen_verbose) rm -f $(C_SRC_ENV) - --include $(C_SRC_ENV) -endif - -# Templates. - -define bs_c_nif -#include "erl_nif.h" - -static int loads = 0; - -static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) -{ - /* Initialize private data. */ - *priv_data = NULL; - - loads++; - - return 0; -} - -static int upgrade(ErlNifEnv* env, void** priv_data, void** old_priv_data, ERL_NIF_TERM load_info) -{ - /* Convert the private data to the new version. */ - *priv_data = *old_priv_data; - - loads++; - - return 0; -} - -static void unload(ErlNifEnv* env, void* priv_data) -{ - if (loads == 1) { - /* Destroy the private data. */ - } - - loads--; -} - -static ERL_NIF_TERM hello(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) -{ - if (enif_is_atom(env, argv[0])) { - return enif_make_tuple2(env, - enif_make_atom(env, "hello"), - argv[0]); - } - - return enif_make_tuple2(env, - enif_make_atom(env, "error"), - enif_make_atom(env, "badarg")); -} - -static ErlNifFunc nif_funcs[] = { - {"hello", 1, hello} -}; - -ERL_NIF_INIT($n, nif_funcs, load, NULL, upgrade, unload) -endef - -define bs_erl_nif --module($n). - --export([hello/1]). - --on_load(on_load/0). -on_load() -> - PrivDir = case code:priv_dir(?MODULE) of - {error, _} -> - AppPath = filename:dirname(filename:dirname(code:which(?MODULE))), - filename:join(AppPath, "priv"); - Path -> - Path - end, - erlang:load_nif(filename:join(PrivDir, atom_to_list(?MODULE)), 0). - -hello(_) -> - erlang:nif_error({not_loaded, ?MODULE}). -endef - -new-nif: -ifneq ($(wildcard $(C_SRC_DIR)/$n.c),) - $(error Error: $(C_SRC_DIR)/$n.c already exists) -endif -ifneq ($(wildcard src/$n.erl),) - $(error Error: src/$n.erl already exists) -endif -ifdef in - $(verbose) $(MAKE) -C $(APPS_DIR)/$(in)/ new-nif n=$n in= -else - $(verbose) mkdir -p $(C_SRC_DIR) src/ - $(call render_template,bs_c_nif,$(C_SRC_DIR)/$n.c) - $(call render_template,bs_erl_nif,src/$n.erl) -endif - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: ci ci-setup distclean-kerl - -KERL ?= $(CURDIR)/kerl -export KERL - -KERL_URL ?= https://raw.githubusercontent.com/yrashk/kerl/master/kerl - -OTP_GIT ?= https://github.com/erlang/otp - -CI_INSTALL_DIR ?= $(HOME)/erlang -CI_OTP ?= - -ifeq ($(strip $(CI_OTP)),) -ci:: -else -ci:: $(addprefix ci-,$(CI_OTP)) - -ci-prepare: $(addprefix $(CI_INSTALL_DIR)/,$(CI_OTP)) - -ci-setup:: - -ci_verbose_0 = @echo " CI " $(1); -ci_verbose = $(ci_verbose_$(V)) - -define ci_target -ci-$(1): $(CI_INSTALL_DIR)/$(1) - $(ci_verbose) \ - PATH="$(CI_INSTALL_DIR)/$(1)/bin:$(PATH)" \ - CI_OTP_RELEASE="$(1)" \ - CT_OPTS="-label $(1)" \ - $(MAKE) clean ci-setup tests -endef - -$(foreach otp,$(CI_OTP),$(eval $(call ci_target,$(otp)))) - -define ci_otp_target -ifeq ($(wildcard $(CI_INSTALL_DIR)/$(1)),) -$(CI_INSTALL_DIR)/$(1): $(KERL) - $(KERL) build git $(OTP_GIT) $(1) $(1) - $(KERL) install $(1) $(CI_INSTALL_DIR)/$(1) -endif -endef - -$(foreach otp,$(CI_OTP),$(eval $(call ci_otp_target,$(otp)))) - -$(KERL): - $(gen_verbose) $(call core_http_get,$(KERL),$(KERL_URL)) - $(verbose) chmod +x $(KERL) - -help:: - $(verbose) printf "%s\n" "" \ - "Continuous Integration targets:" \ - " ci Run '$(MAKE) tests' on all configured Erlang versions." \ - "" \ - "The CI_OTP variable must be defined with the Erlang versions" \ - "that must be tested. For example: CI_OTP = OTP-17.3.4 OTP-17.5.3" - -distclean:: distclean-kerl - -distclean-kerl: - $(gen_verbose) rm -rf $(KERL) -endif - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: ct apps-ct distclean-ct - -# Configuration. - -CT_OPTS ?= -ifneq ($(wildcard $(TEST_DIR)),) - CT_SUITES ?= $(sort $(subst _SUITE.erl,,$(notdir $(call core_find,$(TEST_DIR)/,*_SUITE.erl)))) -else - CT_SUITES ?= -endif - -# Core targets. - -tests:: ct - -distclean:: distclean-ct - -help:: - $(verbose) printf "%s\n" "" \ - "Common_test targets:" \ - " ct Run all the common_test suites for this project" \ - "" \ - "All your common_test suites have their associated targets." \ - "A suite named http_SUITE can be ran using the ct-http target." - -# Plugin-specific targets. - -CT_RUN = ct_run \ - -no_auto_compile \ - -noinput \ - -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(DEPS_DIR)/gen_rpc/_build/dev/lib/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \ - -dir $(TEST_DIR) \ - -logdir $(CURDIR)/logs - -ifeq ($(CT_SUITES),) -ct: $(if $(IS_APP),,apps-ct) -else -ct: test-build $(if $(IS_APP),,apps-ct) - $(verbose) mkdir -p $(CURDIR)/logs/ - $(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS) -endif - -ifneq ($(ALL_APPS_DIRS),) -define ct_app_target -apps-ct-$1: - $(MAKE) -C $1 ct IS_APP=1 -endef - -$(foreach app,$(ALL_APPS_DIRS),$(eval $(call ct_app_target,$(app)))) - -apps-ct: test-build $(addprefix apps-ct-,$(ALL_APPS_DIRS)) -endif - -ifndef t -CT_EXTRA = -else -ifeq (,$(findstring :,$t)) -CT_EXTRA = -group $t -else -t_words = $(subst :, ,$t) -CT_EXTRA = -group $(firstword $(t_words)) -case $(lastword $(t_words)) -endif -endif - -define ct_suite_target -ct-$(1): test-build - $(verbose) mkdir -p $(CURDIR)/logs/ - $(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(1)) $(CT_EXTRA) $(CT_OPTS) -endef - -$(foreach test,$(CT_SUITES),$(eval $(call ct_suite_target,$(test)))) - -distclean-ct: - $(gen_verbose) rm -rf $(CURDIR)/logs/ - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: plt distclean-plt dialyze - -# Configuration. - -DIALYZER_PLT ?= $(CURDIR)/.$(PROJECT).plt -export DIALYZER_PLT - -PLT_APPS ?= -DIALYZER_DIRS ?= --src -r $(wildcard src) $(ALL_APPS_DIRS) -DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions -Wunmatched_returns # -Wunderspecs - -# Core targets. - -check:: dialyze - -distclean:: distclean-plt - -help:: - $(verbose) printf "%s\n" "" \ - "Dialyzer targets:" \ - " plt Build a PLT file for this project" \ - " dialyze Analyze the project using Dialyzer" - -# Plugin-specific targets. - -define filter_opts.erl - Opts = binary:split(<<"$1">>, <<"-">>, [global]), - Filtered = lists:reverse(lists:foldl(fun - (O = <<"pa ", _/bits>>, Acc) -> [O|Acc]; - (O = <<"D ", _/bits>>, Acc) -> [O|Acc]; - (O = <<"I ", _/bits>>, Acc) -> [O|Acc]; - (_, Acc) -> Acc - end, [], Opts)), - io:format("~s~n", [[["-", O] || O <- Filtered]]), - halt(). -endef - -$(DIALYZER_PLT): deps app - $(verbose) dialyzer --build_plt --apps erts kernel stdlib $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS) - -plt: $(DIALYZER_PLT) - -distclean-plt: - $(gen_verbose) rm -f $(DIALYZER_PLT) - -ifneq ($(wildcard $(DIALYZER_PLT)),) -dialyze: -else -dialyze: $(DIALYZER_PLT) -endif - $(verbose) dialyzer --no_native `$(call erlang,$(call filter_opts.erl,$(ERLC_OPTS)))` $(DIALYZER_DIRS) $(DIALYZER_OPTS) - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: distclean-edoc edoc - -# Configuration. - -EDOC_OPTS ?= - -# Core targets. - -ifneq ($(wildcard doc/overview.edoc),) -docs:: edoc -endif - -distclean:: distclean-edoc - -# Plugin-specific targets. - -edoc: distclean-edoc doc-deps - $(gen_verbose) $(ERL) -eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), halt().' - -distclean-edoc: - $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info - -# Copyright (c) 2014 Dave Cottlehuber -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: distclean-escript escript - -# Configuration. - -ESCRIPT_NAME ?= $(PROJECT) -ESCRIPT_FILE ?= $(ESCRIPT_NAME) - -ESCRIPT_COMMENT ?= This is an -*- erlang -*- file - -ESCRIPT_BEAMS ?= "ebin/*", "deps/*/ebin/*" -ESCRIPT_SYS_CONFIG ?= "rel/sys.config" -ESCRIPT_EMU_ARGS ?= -pa . \ - -sasl errlog_type error \ - -escript main $(ESCRIPT_NAME) -ESCRIPT_SHEBANG ?= /usr/bin/env escript -ESCRIPT_STATIC ?= "deps/*/priv/**", "priv/**" - -# Core targets. - -distclean:: distclean-escript - -help:: - $(verbose) printf "%s\n" "" \ - "Escript targets:" \ - " escript Build an executable escript archive" \ - -# Plugin-specific targets. - -# Based on https://github.com/synrc/mad/blob/master/src/mad_bundle.erl -# Copyright (c) 2013 Maxim Sokhatsky, Synrc Research Center -# Modified MIT License, https://github.com/synrc/mad/blob/master/LICENSE : -# Software may only be used for the great good and the true happiness of all -# sentient beings. - -define ESCRIPT_RAW -'Read = fun(F) -> {ok, B} = file:read_file(filename:absname(F)), B end,'\ -'Files = fun(L) -> A = lists:concat([filelib:wildcard(X)||X<- L ]),'\ -' [F || F <- A, not filelib:is_dir(F) ] end,'\ -'Squash = fun(L) -> [{filename:basename(F), Read(F) } || F <- L ] end,'\ -'Zip = fun(A, L) -> {ok,{_,Z}} = zip:create(A, L, [{compress,all},memory]), Z end,'\ -'Ez = fun(Escript) ->'\ -' Static = Files([$(ESCRIPT_STATIC)]),'\ -' Beams = Squash(Files([$(ESCRIPT_BEAMS), $(ESCRIPT_SYS_CONFIG)])),'\ -' Archive = Beams ++ [{ "static.gz", Zip("static.gz", Static)}],'\ -' escript:create(Escript, [ $(ESCRIPT_OPTIONS)'\ -' {archive, Archive, [memory]},'\ -' {shebang, "$(ESCRIPT_SHEBANG)"},'\ -' {comment, "$(ESCRIPT_COMMENT)"},'\ -' {emu_args, " $(ESCRIPT_EMU_ARGS)"}'\ -' ]),'\ -' file:change_mode(Escript, 8#755)'\ -'end,'\ -'Ez("$(ESCRIPT_FILE)"),'\ -'halt().' -endef - -ESCRIPT_COMMAND = $(subst ' ',,$(ESCRIPT_RAW)) - -escript:: distclean-escript deps app - $(gen_verbose) $(ERL) -eval $(ESCRIPT_COMMAND) - -distclean-escript: - $(gen_verbose) rm -f $(ESCRIPT_NAME) - -# Copyright (c) 2014, Enrique Fernandez -# Copyright (c) 2015, Loïc Hoguin -# This file is contributed to erlang.mk and subject to the terms of the ISC License. - -.PHONY: eunit apps-eunit - -# Configuration - -EUNIT_OPTS ?= -EUNIT_ERL_OPTS ?= - -# Core targets. - -tests:: eunit - -help:: - $(verbose) printf "%s\n" "" \ - "EUnit targets:" \ - " eunit Run all the EUnit tests for this project" - -# Plugin-specific targets. - -define eunit.erl - case "$(COVER)" of - "" -> ok; - _ -> - case cover:compile_beam_directory("ebin") of - {error, _} -> halt(1); - _ -> ok - end - end, - case eunit:test($1, [$(EUNIT_OPTS)]) of - ok -> ok; - error -> halt(2) - end, - case "$(COVER)" of - "" -> ok; - _ -> - cover:export("eunit.coverdata") - end, - halt() -endef - -EUNIT_ERL_OPTS += -pa $(TEST_DIR) $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(CURDIR)/ebin - -ifdef t -ifeq (,$(findstring :,$(t))) -eunit: test-build - $(gen_verbose) $(call erlang,$(call eunit.erl,['$(t)']),$(EUNIT_ERL_OPTS)) -else -eunit: test-build - $(gen_verbose) $(call erlang,$(call eunit.erl,fun $(t)/0),$(EUNIT_ERL_OPTS)) -endif -else -EUNIT_EBIN_MODS = $(notdir $(basename $(ERL_FILES) $(BEAM_FILES))) -EUNIT_TEST_MODS = $(notdir $(basename $(call core_find,$(TEST_DIR)/,*.erl))) - -EUNIT_MODS = $(foreach mod,$(EUNIT_EBIN_MODS) $(filter-out \ - $(patsubst %,%_tests,$(EUNIT_EBIN_MODS)),$(EUNIT_TEST_MODS)),'$(mod)') - -eunit: test-build $(if $(IS_APP),,apps-eunit) - $(gen_verbose) $(call erlang,$(call eunit.erl,[$(call comma_list,$(EUNIT_MODS))]),$(EUNIT_ERL_OPTS)) - -ifneq ($(ALL_APPS_DIRS),) -apps-eunit: - $(verbose) for app in $(ALL_APPS_DIRS); do $(MAKE) -C $$app eunit IS_APP=1; done -endif -endif - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: relx-rel distclean-relx-rel distclean-relx run - -# Configuration. - -RELX ?= $(CURDIR)/relx -RELX_CONFIG ?= $(CURDIR)/relx.config - -RELX_URL ?= https://github.com/erlware/relx/releases/download/v3.19.0/relx -RELX_OPTS ?= -RELX_OUTPUT_DIR ?= _rel - -ifeq ($(firstword $(RELX_OPTS)),-o) - RELX_OUTPUT_DIR = $(word 2,$(RELX_OPTS)) -else - RELX_OPTS += -o $(RELX_OUTPUT_DIR) -endif - -# Core targets. - -ifeq ($(IS_DEP),) -ifneq ($(wildcard $(RELX_CONFIG)),) -rel:: relx-rel -endif -endif - -distclean:: distclean-relx-rel distclean-relx - -# Plugin-specific targets. - -$(RELX): - $(gen_verbose) $(call core_http_get,$(RELX),$(RELX_URL)) - $(verbose) chmod +x $(RELX) - -relx-rel: $(RELX) rel-deps app - $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) - -distclean-relx-rel: - $(gen_verbose) rm -rf $(RELX_OUTPUT_DIR) - -distclean-relx: - $(gen_verbose) rm -rf $(RELX) - -# Run target. - -ifeq ($(wildcard $(RELX_CONFIG)),) -run: -else - -define get_relx_release.erl - {ok, Config} = file:consult("$(RELX_CONFIG)"), - {release, {Name, _}, _} = lists:keyfind(release, 1, Config), - io:format("~s", [Name]), - halt(0). -endef - -RELX_RELEASE = `$(call erlang,$(get_relx_release.erl))` - -run: all - $(verbose) $(RELX_OUTPUT_DIR)/$(RELX_RELEASE)/bin/$(RELX_RELEASE) console - -help:: - $(verbose) printf "%s\n" "" \ - "Relx targets:" \ - " run Compile the project, build the release and run it" - -endif - -# Copyright (c) 2014, M Robert Martin -# Copyright (c) 2015, Loïc Hoguin -# This file is contributed to erlang.mk and subject to the terms of the ISC License. - -.PHONY: shell - -# Configuration. - -SHELL_ERL ?= erl -SHELL_PATHS ?= $(CURDIR)/ebin $(APPS_DIR)/*/ebin $(DEPS_DIR)/*/ebin -SHELL_OPTS ?= - -ALL_SHELL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(SHELL_DEPS)) - -# Core targets - -help:: - $(verbose) printf "%s\n" "" \ - "Shell targets:" \ - " shell Run an erlang shell with SHELL_OPTS or reasonable default" - -# Plugin-specific targets. - -$(foreach dep,$(SHELL_DEPS),$(eval $(call dep_target,$(dep)))) - -build-shell-deps: $(ALL_SHELL_DEPS_DIRS) - $(verbose) for dep in $(ALL_SHELL_DEPS_DIRS) ; do $(MAKE) -C $$dep ; done - -shell: build-shell-deps - $(gen_verbose) $(SHELL_ERL) -pa $(SHELL_PATHS) $(SHELL_OPTS) - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -ifeq ($(filter triq,$(DEPS) $(TEST_DEPS)),triq) -.PHONY: triq - -# Targets. - -tests:: triq - -define triq_check.erl - code:add_pathsa(["$(CURDIR)/ebin", "$(DEPS_DIR)/*/ebin"]), - try - case $(1) of - all -> [true] =:= lists:usort([triq:check(M) || M <- [$(call comma_list,$(3))]]); - module -> triq:check($(2)); - function -> triq:check($(2)) - end - of - true -> halt(0); - _ -> halt(1) - catch error:undef -> - io:format("Undefined property or module~n"), - halt(0) - end. -endef - -ifdef t -ifeq (,$(findstring :,$(t))) -triq: test-build - $(verbose) $(call erlang,$(call triq_check.erl,module,$(t))) -else -triq: test-build - $(verbose) echo Testing $(t)/0 - $(verbose) $(call erlang,$(call triq_check.erl,function,$(t)())) -endif -else -triq: test-build - $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename $(wildcard ebin/*.beam)))))) - $(gen_verbose) $(call erlang,$(call triq_check.erl,all,undefined,$(MODULES))) -endif -endif - -# Copyright (c) 2015, Erlang Solutions Ltd. -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: xref distclean-xref - -# Configuration. - -ifeq ($(XREF_CONFIG),) - XREF_ARGS := -else - XREF_ARGS := -c $(XREF_CONFIG) -endif - -XREFR ?= $(CURDIR)/xrefr -export XREFR - -XREFR_URL ?= https://github.com/inaka/xref_runner/releases/download/0.2.2/xrefr - -# Core targets. - -help:: - $(verbose) printf "%s\n" "" \ - "Xref targets:" \ - " xref Run Xrefr using $XREF_CONFIG as config file if defined" - -distclean:: distclean-xref - -# Plugin-specific targets. - -$(XREFR): - $(gen_verbose) $(call core_http_get,$(XREFR),$(XREFR_URL)) - $(verbose) chmod +x $(XREFR) - -xref: deps app $(XREFR) - $(gen_verbose) $(XREFR) $(XREFR_ARGS) - -distclean-xref: - $(gen_verbose) rm -rf $(XREFR) - -# Copyright 2015, Viktor Söderqvist -# This file is part of erlang.mk and subject to the terms of the ISC License. - -COVER_REPORT_DIR = cover - -# Hook in coverage to ct - -ifdef COVER -ifdef CT_RUN -# All modules in 'ebin' -COVER_MODS = $(notdir $(basename $(call core_ls,ebin/*.beam))) - -test-build:: $(TEST_DIR)/ct.cover.spec - -$(TEST_DIR)/ct.cover.spec: - $(verbose) echo Cover mods: $(COVER_MODS) - $(gen_verbose) printf "%s\n" \ - '{incl_mods,[$(subst $(space),$(comma),$(COVER_MODS))]}.' \ - '{export,"$(CURDIR)/ct.coverdata"}.' > $@ - -CT_RUN += -cover $(TEST_DIR)/ct.cover.spec -endif -endif - -# Core targets - -ifdef COVER -ifneq ($(COVER_REPORT_DIR),) -tests:: - $(verbose) $(MAKE) --no-print-directory cover-report -endif -endif - -clean:: coverdata-clean - -ifneq ($(COVER_REPORT_DIR),) -distclean:: cover-report-clean -endif - -help:: - $(verbose) printf "%s\n" "" \ - "Cover targets:" \ - " cover-report Generate a HTML coverage report from previously collected" \ - " cover data." \ - " all.coverdata Merge {eunit,ct}.coverdata into one coverdata file." \ - "" \ - "If COVER=1 is set, coverage data is generated by the targets eunit and ct. The" \ - "target tests additionally generates a HTML coverage report from the combined" \ - "coverdata files from each of these testing tools. HTML reports can be disabled" \ - "by setting COVER_REPORT_DIR to empty." - -# Plugin specific targets - -COVERDATA = $(filter-out all.coverdata,$(wildcard *.coverdata)) - -.PHONY: coverdata-clean -coverdata-clean: - $(gen_verbose) rm -f *.coverdata ct.cover.spec - -# Merge all coverdata files into one. -all.coverdata: $(COVERDATA) - $(gen_verbose) $(ERL) -eval ' \ - $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) \ - cover:export("$@"), halt(0).' - -# These are only defined if COVER_REPORT_DIR is non-empty. Set COVER_REPORT_DIR to -# empty if you want the coverdata files but not the HTML report. -ifneq ($(COVER_REPORT_DIR),) - -.PHONY: cover-report-clean cover-report - -cover-report-clean: - $(gen_verbose) rm -rf $(COVER_REPORT_DIR) - -ifeq ($(COVERDATA),) -cover-report: -else - -# Modules which include eunit.hrl always contain one line without coverage -# because eunit defines test/0 which is never called. We compensate for this. -EUNIT_HRL_MODS = $(subst $(space),$(comma),$(shell \ - grep -e '^\s*-include.*include/eunit\.hrl"' src/*.erl \ - | sed "s/^src\/\(.*\)\.erl:.*/'\1'/" | uniq)) - -define cover_report.erl - $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) - Ms = cover:imported_modules(), - [cover:analyse_to_file(M, "$(COVER_REPORT_DIR)/" ++ atom_to_list(M) - ++ ".COVER.html", [html]) || M <- Ms], - Report = [begin {ok, R} = cover:analyse(M, module), R end || M <- Ms], - EunitHrlMods = [$(EUNIT_HRL_MODS)], - Report1 = [{M, {Y, case lists:member(M, EunitHrlMods) of - true -> N - 1; false -> N end}} || {M, {Y, N}} <- Report], - TotalY = lists:sum([Y || {_, {Y, _}} <- Report1]), - TotalN = lists:sum([N || {_, {_, N}} <- Report1]), - Perc = fun(Y, N) -> case Y + N of 0 -> 100; S -> round(100 * Y / S) end end, - TotalPerc = Perc(TotalY, TotalN), - {ok, F} = file:open("$(COVER_REPORT_DIR)/index.html", [write]), - io:format(F, "~n" - "~n" - "Coverage report~n" - "~n", []), - io:format(F, "

    Coverage

    ~n

    Total: ~p%

    ~n", [TotalPerc]), - io:format(F, "~n", []), - [io:format(F, "" - "~n", - [M, M, Perc(Y, N)]) || {M, {Y, N}} <- Report1], - How = "$(subst $(space),$(comma)$(space),$(basename $(COVERDATA)))", - Date = "$(shell date -u "+%Y-%m-%dT%H:%M:%SZ")", - io:format(F, "
    ModuleCoverage
    ~p~p%
    ~n" - "

    Generated using ~s and erlang.mk on ~s.

    ~n" - "", [How, Date]), - halt(). -endef - -cover-report: - $(gen_verbose) mkdir -p $(COVER_REPORT_DIR) - $(gen_verbose) $(call erlang,$(cover_report.erl)) - -endif -endif # ifneq ($(COVER_REPORT_DIR),) From b4d981daf2a549554e8e76f88b9b3c0676779932 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 4 Dec 2018 15:59:24 +0800 Subject: [PATCH 449/520] Add a sequence module to generate index for subscription sharding --- Makefile | 2 +- src/emqx_sequence.erl | 58 ++++++++++++++++++++++++++++++++++++ test/emqx_sequence_SUITE.erl | 37 +++++++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/emqx_sequence.erl create mode 100644 test/emqx_sequence_SUITE.erl diff --git a/Makefile b/Makefile index 26bcf22ce..2c1693813 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_session \ emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_mountpoint \ emqx_listeners emqx_protocol emqx_pool emqx_shared_sub emqx_bridge \ - emqx_hooks emqx_batch + emqx_hooks emqx_batch emqx_sequence CT_NODE_NAME = emqxct@127.0.0.1 CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME) diff --git a/src/emqx_sequence.erl b/src/emqx_sequence.erl new file mode 100644 index 000000000..62a882294 --- /dev/null +++ b/src/emqx_sequence.erl @@ -0,0 +1,58 @@ +%% Copyright (c) 2018 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_sequence). + +-export([create/0, create/1]). +-export([generate/1, generate/2]). +-export([reclaim/1, reclaim/2]). + +-type(key() :: term()). +-type(seqid() :: non_neg_integer()). + +-define(DEFAULT_TAB, ?MODULE). + +%% @doc Create a sequence. +-spec(create() -> ok). +create() -> + create(?DEFAULT_TAB). + +-spec(create(atom()) -> ok). +create(Tab) -> + _ = ets:new(Tab, [set, public, named_table, {write_concurrency, true}]), + ok. + +%% @doc Generate a sequence id. +-spec(generate(key()) -> seqid()). +generate(Key) -> + generate(?DEFAULT_TAB, Key). + +-spec(generate(atom(), key()) -> seqid()). +generate(Tab, Key) -> + ets:update_counter(Tab, Key, {2, 1}, {Key, 0}). + +%% @doc Reclaim a sequence id. +-spec(reclaim(key()) -> seqid()). +reclaim(Key) -> + reclaim(?DEFAULT_TAB, Key). + +-spec(reclaim(atom(), key()) -> seqid()). +reclaim(Tab, Key) -> + try ets:update_counter(Tab, Key, {2, -1, 0, 0}) of + 0 -> ets:delete_object(Tab, {Key, 0}), 0; + I -> I + catch + error:badarg -> 0 + end. + diff --git a/test/emqx_sequence_SUITE.erl b/test/emqx_sequence_SUITE.erl new file mode 100644 index 000000000..999a95723 --- /dev/null +++ b/test/emqx_sequence_SUITE.erl @@ -0,0 +1,37 @@ +%% Copyright (c) 2018 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_sequence_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +-import(emqx_sequence, [generate/1, reclaim/1]). + +all() -> + [sequence_generate]. + +sequence_generate(_) -> + ok = emqx_sequence:create(), + ?assertEqual(1, generate(key)), + ?assertEqual(2, generate(key)), + ?assertEqual(3, generate(key)), + ?assertEqual(2, reclaim(key)), + ?assertEqual(1, reclaim(key)), + ?assertEqual(0, reclaim(key)), + ?assertEqual(false, ets:member(emqx_sequence, key)), + ?assertEqual(1, generate(key)). + From 35e699e54ea83df6bbcfa5865ef9a3ff169ff29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Tue, 4 Dec 2018 16:11:25 +0800 Subject: [PATCH 450/520] Make sure test case of emqx_banned passes --- src/emqx_banned.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index 49a698384..aae4f8c7a 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -105,7 +105,7 @@ code_change(_OldVsn, State, _Extra) -> -ifdef(TEST). ensure_expiry_timer(State) -> - State#{expiry_timer := emqx_misc:start_timer(timer:seconds(2), expire)}. + State#{expiry_timer := emqx_misc:start_timer(timer:seconds(1), expire)}. -else. ensure_expiry_timer(State) -> State#{expiry_timer := emqx_misc:start_timer(timer:minutes(5), expire)}. From d11e734dae51d928583395687bce7de860ffe973 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 4 Dec 2018 23:22:39 +0800 Subject: [PATCH 451/520] Make some processes hibernate after 1s. --- src/emqx_broker.erl | 2 +- src/emqx_hooks.erl | 2 +- src/emqx_pool.erl | 3 ++- src/emqx_router.erl | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index ca4a86c87..b816a0a69 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -56,7 +56,7 @@ -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, - [Pool, Id], [{hibernate_after, 2000}]). + [Pool, Id], [{hibernate_after, 1000}]). %%------------------------------------------------------------------------------ %% Subscribe diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index 073c12870..b10445742 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -42,7 +42,7 @@ -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], [{hibernate_after, 60000}]). + gen_server:start_link({local, ?SERVER}, ?MODULE, [], [{hibernate_after, 1000}]). -spec(stop() -> ok). stop() -> diff --git a/src/emqx_pool.erl b/src/emqx_pool.erl index 762f5dc6d..7b12bea69 100644 --- a/src/emqx_pool.erl +++ b/src/emqx_pool.erl @@ -35,7 +35,8 @@ start_link() -> %% @doc Start pool. -spec(start_link(atom(), pos_integer()) -> emqx_types:startlink_ret()). start_link(Pool, Id) -> - gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, [Pool, Id], []). + gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, + ?MODULE, [Pool, Id], [{hibernate_after, 1000}]). %% @doc Submit work to the pool. -spec(submit(task()) -> any()). diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 941c004f7..e513d041d 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -68,7 +68,7 @@ mnesia(copy) -> -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, - ?MODULE, [Pool, Id], [{hibernate_after, 2000}]). + ?MODULE, [Pool, Id], [{hibernate_after, 1000}]). %%------------------------------------------------------------------------------ %% Route APIs From bce1ddc5c4d226e7f0e1ae6648ab5d1da54c12ff Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 6 Dec 2018 18:45:07 +0800 Subject: [PATCH 452/520] Implement a hash-based subscription sharding --- src/emqx.erl | 53 ++--- src/emqx_broker.erl | 444 ++++++++++++++--------------------- src/emqx_broker_helper.erl | 75 +++--- src/emqx_broker_sup.erl | 51 ++-- src/emqx_local_bridge.erl | 2 +- src/emqx_sequence.erl | 54 +++-- src/emqx_session.erl | 2 +- test/emqx_sequence_SUITE.erl | 20 +- 8 files changed, 299 insertions(+), 402 deletions(-) diff --git a/src/emqx.erl b/src/emqx.erl index 72f1d6f81..3792cc4f8 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -26,7 +26,6 @@ %% PubSub management API -export([topics/0, subscriptions/1, subscribers/1, subscribed/2]). --export([get_subopts/2, set_subopts/3]). %% Hooks API -export([hook/2, hook/3, hook/4, unhook/2, run_hooks/2, run_hooks/3]). @@ -70,20 +69,18 @@ is_running(Node) -> subscribe(Topic) -> emqx_broker:subscribe(iolist_to_binary(Topic)). --spec(subscribe(emqx_topic:topic() | string(), emqx_types:subid() | pid()) -> ok). +-spec(subscribe(emqx_topic:topic() | string(), emqx_types:subid() | emqx_types:subopts()) -> ok). subscribe(Topic, SubId) when is_atom(SubId); is_binary(SubId)-> emqx_broker:subscribe(iolist_to_binary(Topic), SubId); -subscribe(Topic, SubPid) when is_pid(SubPid) -> - emqx_broker:subscribe(iolist_to_binary(Topic), SubPid). +subscribe(Topic, SubOpts) when is_map(SubOpts) -> + emqx_broker:subscribe(iolist_to_binary(Topic), SubOpts). --spec(subscribe(emqx_topic:topic() | string(), emqx_types:subid() | pid(), - emqx_types:subopts()) -> ok). -subscribe(Topic, SubId, Options) when is_atom(SubId); is_binary(SubId)-> - emqx_broker:subscribe(iolist_to_binary(Topic), SubId, Options); -subscribe(Topic, SubPid, Options) when is_pid(SubPid)-> - emqx_broker:subscribe(iolist_to_binary(Topic), SubPid, Options). +-spec(subscribe(emqx_topic:topic() | string(), + emqx_types:subid() | pid(), emqx_types:subopts()) -> ok). +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()) -> {ok, emqx_types:deliver_results()}). +-spec(publish(emqx_types:message()) -> emqx_types:deliver_results()). publish(Msg) -> emqx_broker:publish(Msg). @@ -91,26 +88,14 @@ publish(Msg) -> unsubscribe(Topic) -> emqx_broker:unsubscribe(iolist_to_binary(Topic)). --spec(unsubscribe(emqx_topic:topic() | string(), emqx_types:subid() | pid()) -> ok). -unsubscribe(Topic, SubId) when is_atom(SubId); is_binary(SubId) -> - emqx_broker:unsubscribe(iolist_to_binary(Topic), SubId); -unsubscribe(Topic, SubPid) when is_pid(SubPid) -> - emqx_broker:unsubscribe(iolist_to_binary(Topic), SubPid). +-spec(unsubscribe(emqx_topic:topic() | string(), emqx_types:subid()) -> ok). +unsubscribe(Topic, SubId) -> + emqx_broker:unsubscribe(iolist_to_binary(Topic), SubId). %%------------------------------------------------------------------------------ %% PubSub management API %%------------------------------------------------------------------------------ --spec(get_subopts(emqx_topic:topic() | string(), emqx_types:subscriber()) - -> emqx_types:subopts()). -get_subopts(Topic, Subscriber) -> - emqx_broker:get_subopts(iolist_to_binary(Topic), Subscriber). - --spec(set_subopts(emqx_topic:topic() | string(), emqx_types:subscriber(), - emqx_types:subopts()) -> boolean()). -set_subopts(Topic, Subscriber, Options) when is_map(Options) -> - emqx_broker:set_subopts(iolist_to_binary(Topic), Subscriber, Options). - -spec(topics() -> list(emqx_topic:topic())). topics() -> emqx_router:topics(). @@ -118,15 +103,15 @@ topics() -> emqx_router:topics(). subscribers(Topic) -> emqx_broker:subscribers(iolist_to_binary(Topic)). --spec(subscriptions(emqx_types:subscriber()) -> [{emqx_topic:topic(), emqx_types:subopts()}]). -subscriptions(Subscriber) -> - emqx_broker:subscriptions(Subscriber). +-spec(subscriptions(pid()) -> [{emqx_topic:topic(), emqx_types:subopts()}]). +subscriptions(SubPid) when is_pid(SubPid) -> + emqx_broker:subscriptions(SubPid). --spec(subscribed(emqx_topic:topic() | string(), pid() | emqx_types:subid()) -> boolean()). -subscribed(Topic, SubPid) when is_pid(SubPid) -> - emqx_broker:subscribed(iolist_to_binary(Topic), SubPid); -subscribed(Topic, SubId) when is_atom(SubId); is_binary(SubId) -> - emqx_broker:subscribed(iolist_to_binary(Topic), SubId). +-spec(subscribed(pid() | emqx_types:subid(), emqx_topic:topic() | string()) -> boolean()). +subscribed(SubPid, Topic) when is_pid(SubPid) -> + emqx_broker:subscribed(SubPid, iolist_to_binary(Topic)); +subscribed(SubId, Topic) when is_atom(SubId); is_binary(SubId) -> + emqx_broker:subscribed(SubId, iolist_to_binary(Topic)). %%------------------------------------------------------------------------------ %% Hooks API diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index ca4a86c87..3a3cde1fa 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -19,150 +19,130 @@ -include("emqx.hrl"). -export([start_link/2]). --export([subscribe/1, subscribe/2, subscribe/3, subscribe/4]). --export([multi_subscribe/1, multi_subscribe/2, multi_subscribe/3]). +-export([subscribe/1, subscribe/2, subscribe/3]). +-export([unsubscribe/1, unsubscribe/2]). +-export([subscriber_down/1]). -export([publish/1, safe_publish/1]). --export([unsubscribe/1, unsubscribe/2, unsubscribe/3]). --export([multi_unsubscribe/1, multi_unsubscribe/2, multi_unsubscribe/3]). -export([dispatch/2, dispatch/3]). -export([subscriptions/1, subscribers/1, subscribed/2]). --export([get_subopts/2, set_subopts/3]). +-export([get_subopts/2, set_subopts/2]). -export([topics/0]). +%% Stats fun +-export([stats_fun/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --ifdef(TEST). --compile(export_all). --compile(nowarn_export_all). --endif. - --record(state, {pool, id, submap, submon}). --record(subscribe, {topic, subpid, subid, subopts = #{}}). --record(unsubscribe, {topic, subpid, subid}). - -%% The default request timeout +-define(SHARD, 1024). -define(TIMEOUT, 60000). -define(BROKER, ?MODULE). %% ETS tables --define(SUBOPTION, emqx_suboption). --define(SUBSCRIBER, emqx_subscriber). +-define(SUBID, emqx_subid). +-define(SUBOPTION, emqx_suboption). +-define(SUBSCRIBER, emqx_subscriber). -define(SUBSCRIPTION, emqx_subscription). +%% Gards -define(is_subid(Id), (is_binary(Id) orelse is_atom(Id))). --spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link(atom(), pos_integer()) -> emqx_types:startlink_ret()). start_link(Pool, Id) -> - gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, - [Pool, Id], [{hibernate_after, 2000}]). + _ = create_tabs(), + gen_server:start_link({local, emqx_misc:proc_name(?BROKER, Id)}, ?MODULE, [Pool, Id], []). %%------------------------------------------------------------------------------ -%% Subscribe +%% Create tabs +%%------------------------------------------------------------------------------ + +create_tabs() -> + TabOpts = [public, {read_concurrency, true}, {write_concurrency, true}], + + %% SubId: SubId -> SubPid1, SubPid2,... + _ = emqx_tables:new(?SUBID, [bag | TabOpts]), + %% SubOption: {SubPid, Topic} -> SubOption + _ = emqx_tables:new(?SUBOPTION, [set | TabOpts]), + + %% Subscription: SubPid -> Topic1, Topic2, Topic3, ... + %% duplicate_bag: o(1) insert + _ = emqx_tables:new(?SUBSCRIPTION, [duplicate_bag | TabOpts]), + + %% Subscriber: Topic -> SubPid1, SubPid2, SubPid3, ... + %% duplicate_bag: o(1) insert + emqx_tables:new(?SUBSCRIBER, [duplicate_bag | TabOpts]). + +%%------------------------------------------------------------------------------ +%% Subscribe API %%------------------------------------------------------------------------------ -spec(subscribe(emqx_topic:topic()) -> ok). subscribe(Topic) when is_binary(Topic) -> - subscribe(Topic, self()). + subscribe(Topic, undefined). --spec(subscribe(emqx_topic:topic(), pid() | emqx_types:subid()) -> ok). -subscribe(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> - subscribe(Topic, SubPid, undefined); +-spec(subscribe(emqx_topic:topic(), emqx_types:subid() | emqx_types:subopts()) -> ok). subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> - subscribe(Topic, self(), SubId). + subscribe(Topic, SubId, #{}); +subscribe(Topic, SubOpts) when is_binary(Topic), is_map(SubOpts) -> + subscribe(Topic, undefined, SubOpts). --spec(subscribe(emqx_topic:topic(), pid() | emqx_types:subid(), - emqx_types:subid() | emqx_types:subopts()) -> ok). -subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - subscribe(Topic, SubPid, SubId, #{qos => 0}); -subscribe(Topic, SubPid, SubOpts) when is_binary(Topic), is_pid(SubPid), is_map(SubOpts) -> - subscribe(Topic, SubPid, undefined, SubOpts); +-spec(subscribe(emqx_topic:topic(), emqx_types:subid(), emqx_types:subopts()) -> ok). subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map(SubOpts) -> - subscribe(Topic, self(), SubId, SubOpts). - --spec(subscribe(emqx_topic:topic(), pid(), emqx_types:subid(), emqx_types:subopts()) -> ok). -subscribe(Topic, SubPid, SubId, SubOpts) when is_binary(Topic), is_pid(SubPid), - ?is_subid(SubId), is_map(SubOpts) -> - Broker = pick(SubPid), - SubReq = #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}, - wait_for_reply(async_call(Broker, SubReq), ?TIMEOUT). - --spec(multi_subscribe(emqx_types:topic_table()) -> ok). -multi_subscribe(TopicTable) when is_list(TopicTable) -> - multi_subscribe(TopicTable, self()). - --spec(multi_subscribe(emqx_types:topic_table(), pid() | emqx_types:subid()) -> ok). -multi_subscribe(TopicTable, SubPid) when is_pid(SubPid) -> - multi_subscribe(TopicTable, SubPid, undefined); -multi_subscribe(TopicTable, SubId) when ?is_subid(SubId) -> - multi_subscribe(TopicTable, self(), SubId). - --spec(multi_subscribe(emqx_types:topic_table(), pid(), emqx_types:subid()) -> ok). -multi_subscribe(TopicTable, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) -> - Broker = pick(SubPid), - SubReq = fun(Topic, SubOpts) -> - #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts} - end, - wait_for_replies([async_call(Broker, SubReq(Topic, SubOpts)) - || {Topic, SubOpts} <- TopicTable], ?TIMEOUT). + SubPid = self(), + case ets:member(?SUBOPTION, {SubPid, Topic}) of + false -> + ok = emqx_broker_helper:monitor(SubPid, SubId), + Group = maps:get(share, SubOpts, undefined), + %% true = ets:insert(?SUBID, {SubId, SubPid}), + true = ets:insert(?SUBSCRIPTION, {SubPid, Topic}), + %% SeqId = emqx_broker_helper:create_seq(Topic), + true = ets:insert(?SUBSCRIBER, {Topic, shared(Group, SubPid)}), + true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), + ok = emqx_shared_sub:subscribe(Group, Topic, SubPid), + call(pick(Topic), {subscribe, Group, Topic}); + true -> ok + end. %%------------------------------------------------------------------------------ -%% Unsubscribe +%% Unsubscribe API %%------------------------------------------------------------------------------ -spec(unsubscribe(emqx_topic:topic()) -> ok). unsubscribe(Topic) when is_binary(Topic) -> - unsubscribe(Topic, self()). + SubPid = self(), + case ets:lookup(?SUBOPTION, {SubPid, Topic}) of + [{_, SubOpts}] -> + Group = maps:get(share, SubOpts, undefined), + true = ets:delete_object(?SUBSCRIPTION, {SubPid, Topic}), + true = ets:delete_object(?SUBSCRIBER, {Topic, shared(Group, SubPid)}), + true = ets:delete(?SUBOPTION, {SubPid, Topic}), + ok = emqx_shared_sub:unsubscribe(Group, Topic, SubPid), + call(pick(Topic), {unsubscribe, Group, Topic}); + [] -> ok + end. --spec(unsubscribe(emqx_topic:topic(), pid() | emqx_types:subid()) -> ok). -unsubscribe(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> - unsubscribe(Topic, SubPid, undefined); -unsubscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> - unsubscribe(Topic, self(), SubId). - --spec(unsubscribe(emqx_topic:topic(), pid(), emqx_types:subid()) -> ok). -unsubscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - Broker = pick(SubPid), - UnsubReq = #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId}, - wait_for_reply(async_call(Broker, UnsubReq), ?TIMEOUT). - --spec(multi_unsubscribe([emqx_topic:topic()]) -> ok). -multi_unsubscribe(Topics) -> - multi_unsubscribe(Topics, self()). - --spec(multi_unsubscribe([emqx_topic:topic()], pid() | emqx_types:subid()) -> ok). -multi_unsubscribe(Topics, SubPid) when is_pid(SubPid) -> - multi_unsubscribe(Topics, SubPid, undefined); -multi_unsubscribe(Topics, SubId) when ?is_subid(SubId) -> - multi_unsubscribe(Topics, self(), SubId). - --spec(multi_unsubscribe([emqx_topic:topic()], pid(), emqx_types:subid()) -> ok). -multi_unsubscribe(Topics, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) -> - Broker = pick(SubPid), - UnsubReq = fun(Topic) -> - #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId} - end, - wait_for_replies([async_call(Broker, UnsubReq(Topic)) || Topic <- Topics], ?TIMEOUT). +-spec(unsubscribe(emqx_topic:topic(), emqx_types:subid()) -> ok). +unsubscribe(Topic, _SubId) when is_binary(Topic) -> + unsubscribe(Topic). %%------------------------------------------------------------------------------ %% Publish %%------------------------------------------------------------------------------ --spec(publish(emqx_types:message()) -> {ok, emqx_types:deliver_results()}). +-spec(publish(emqx_types:message()) -> emqx_types:deliver_results()). publish(Msg) when is_record(Msg, message) -> _ = emqx_tracer:trace(publish, Msg), - {ok, case emqx_hooks:run('message.publish', [], Msg) of - {ok, Msg1 = #message{topic = Topic}} -> - Delivery = route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1)), - Delivery#delivery.results; - {stop, _} -> - emqx_logger:warning("Stop publishing: ~s", [emqx_message:format(Msg)]), - [] - end}. + case emqx_hooks:run('message.publish', [], Msg) of + {ok, Msg1 = #message{topic = Topic}} -> + Delivery = route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1)), + Delivery#delivery.results; + {stop, _} -> + emqx_logger:warning("Stop publishing: ~s", [emqx_message:format(Msg)]), + [] + end. --spec(safe_publish(emqx_types:message()) -> ok). %% Called internally +-spec(safe_publish(emqx_types:message()) -> ok). safe_publish(Msg) when is_record(Msg, message) -> try publish(Msg) @@ -227,98 +207,113 @@ dispatch(Topic, Delivery = #delivery{message = Msg, results = Results}) -> emqx_hooks:run('message.dropped', [#{node => node()}, Msg]), inc_dropped_cnt(Topic), Delivery; - [Sub] -> %% optimize? - dispatch(Sub, Topic, Msg), + [SubPid] -> %% optimize? + dispatch(SubPid, Topic, Msg), Delivery#delivery{results = [{dispatch, Topic, 1}|Results]}; - Subscribers -> - Count = lists:foldl(fun(Sub, Acc) -> - dispatch(Sub, Topic, Msg), Acc + 1 - end, 0, Subscribers), + SubPids -> + Count = lists:foldl(fun(SubPid, Acc) -> + dispatch(SubPid, Topic, Msg), Acc + 1 + end, 0, SubPids), Delivery#delivery{results = [{dispatch, Topic, Count}|Results]} end. -dispatch({SubPid, _SubId}, Topic, Msg) when is_pid(SubPid) -> - SubPid ! {dispatch, Topic, Msg}; -dispatch({share, _Group, _Sub}, _Topic, _Msg) -> - ignored. +dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> + SubPid ! {dispatch, Topic, Msg}, + true; +%% TODO: how to optimize the share sub? +dispatch({share, _Group, _SubPid}, _Topic, _Msg) -> + false. inc_dropped_cnt(<<"$SYS/", _/binary>>) -> ok; inc_dropped_cnt(_Topic) -> emqx_metrics:inc('messages/dropped'). --spec(subscribers(emqx_topic:topic()) -> [emqx_types:subscriber()]). +-spec(subscribers(emqx_topic:topic()) -> [pid()]). subscribers(Topic) -> - try ets:lookup_element(?SUBSCRIBER, Topic, 2) catch error:badarg -> [] end. + safe_lookup_element(?SUBSCRIBER, Topic, []). --spec(subscriptions(emqx_types:subscriber()) +%%------------------------------------------------------------------------------ +%% Subscriber is down +%%------------------------------------------------------------------------------ + +-spec(subscriber_down(pid()) -> true). +subscriber_down(SubPid) -> + lists:foreach( + fun(Sub = {_, Topic}) -> + case ets:lookup(?SUBOPTION, Sub) of + [{_, SubOpts}] -> + Group = maps:get(share, SubOpts, undefined), + true = ets:delete_object(?SUBSCRIBER, {Topic, shared(Group, SubPid)}), + true = ets:delete(?SUBOPTION, Sub), + gen_server:cast(pick(Topic), {unsubscribe, Group, Topic}); + [] -> ok + end + end, ets:lookup(?SUBSCRIPTION, SubPid)), + ets:delete(?SUBSCRIPTION, SubPid). + +%%------------------------------------------------------------------------------ +%% Management APIs +%%------------------------------------------------------------------------------ + +-spec(subscriptions(pid() | emqx_types:subid()) -> [{emqx_topic:topic(), emqx_types:subopts()}]). -subscriptions(Subscriber) -> - lists:map(fun({_, {share, _Group, Topic}}) -> - subscription(Topic, Subscriber); - ({_, Topic}) -> - subscription(Topic, Subscriber) - end, ets:lookup(?SUBSCRIPTION, Subscriber)). +subscriptions(SubPid) -> + [{Topic, safe_lookup_element(?SUBOPTION, {SubPid, Topic}, #{})} + || Topic <- safe_lookup_element(?SUBSCRIPTION, SubPid, [])]. -subscription(Topic, Subscriber) -> - {Topic, ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2)}. +-spec(subscribed(pid(), emqx_topic:topic()) -> boolean()). +subscribed(SubPid, Topic) when is_pid(SubPid) -> + ets:member(?SUBOPTION, {SubPid, Topic}); +subscribed(SubId, Topic) when ?is_subid(SubId) -> + %%FIXME:... SubId -> SubPid + ets:member(?SUBOPTION, {SubId, Topic}). --spec(subscribed(emqx_topic:topic(), pid() | emqx_types:subid() | emqx_types:subscriber()) -> boolean()). -subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> - case ets:match_object(?SUBOPTION, {{Topic, {SubPid, '_'}}, '_'}, 1) of - {Match, _} -> - length(Match) >= 1; - '$end_of_table' -> - false - end; -subscribed(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> - case ets:match_object(?SUBOPTION, {{Topic, {'_', SubId}}, '_'}, 1) of - {Match, _} -> - length(Match) >= 1; - '$end_of_table' -> - false - end; -subscribed(Topic, {SubPid, SubId}) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - ets:member(?SUBOPTION, {Topic, {SubPid, SubId}}). +-spec(get_subopts(pid(), emqx_topic:topic()) -> emqx_types:subopts()). +get_subopts(SubPid, Topic) when is_pid(SubPid), is_binary(Topic) -> + safe_lookup_element(?SUBOPTION, {SubPid, Topic}, #{}). --spec(get_subopts(emqx_topic:topic(), emqx_types:subscriber()) -> emqx_types:subopts()). -get_subopts(Topic, Subscriber) when is_binary(Topic) -> - try ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2) - catch error:badarg -> [] - end. - --spec(set_subopts(emqx_topic:topic(), emqx_types:subscriber(), emqx_types:subopts()) -> boolean()). -set_subopts(Topic, Subscriber, Opts) when is_binary(Topic), is_map(Opts) -> - case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of +-spec(set_subopts(emqx_topic:topic(), emqx_types:subopts()) -> boolean()). +set_subopts(Topic, NewOpts) when is_binary(Topic), is_map(NewOpts) -> + Sub = {self(), Topic}, + case ets:lookup(?SUBOPTION, Sub) of [{_, OldOpts}] -> - ets:insert(?SUBOPTION, {{Topic, Subscriber}, maps:merge(OldOpts, Opts)}); + ets:insert(?SUBOPTION, {Sub, maps:merge(OldOpts, NewOpts)}); [] -> false end. -async_call(Broker, Req) -> - From = {self(), Tag = make_ref()}, - ok = gen_server:cast(Broker, {From, Req}), - Tag. +-spec(topics() -> [emqx_topic:topic()]). +topics() -> + emqx_router:topics(). -wait_for_replies(Tags, Timeout) -> - lists:foreach( - fun(Tag) -> - wait_for_reply(Tag, Timeout) - end, Tags). +safe_lookup_element(Tab, Key, Def) -> + try ets:lookup_element(Tab, Key, 2) catch error:badarg -> Def end. -wait_for_reply(Tag, Timeout) -> - receive - {Tag, Reply} -> Reply - after Timeout -> - exit(timeout) +%%------------------------------------------------------------------------------ +%% Stats fun +%%------------------------------------------------------------------------------ + +stats_fun() -> + safe_update_stats(?SUBSCRIBER, 'subscribers/count', 'subscribers/max'), + safe_update_stats(?SUBSCRIPTION, 'subscriptions/count', 'subscriptions/max'), + safe_update_stats(?SUBOPTION, 'suboptions/count', 'suboptions/max'). + +safe_update_stats(Tab, Stat, MaxStat) -> + case ets:info(Tab, size) of + undefined -> ok; + Size -> emqx_stats:setstat(Stat, MaxStat, Size) end. -%% Pick a broker -pick(SubPid) when is_pid(SubPid) -> - gproc_pool:pick_worker(broker, SubPid). +%%------------------------------------------------------------------------------ +%% Pick and call +%%------------------------------------------------------------------------------ --spec(topics() -> [emqx_topic:topic()]). -topics() -> emqx_router:topics(). +call(Broker, Req) -> + gen_server:call(Broker, Req, ?TIMEOUT). + +%% Pick a broker +pick(Topic) -> + gproc_pool:pick_worker(broker, Topic). %%------------------------------------------------------------------------------ %% gen_server callbacks @@ -326,61 +321,32 @@ topics() -> emqx_router:topics(). init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), - {ok, #state{pool = Pool, id = Id, submap = #{}, submon = emqx_pmon:new()}}. + {ok, #{pool => Pool, id => Id}}. + +handle_call({subscribe, Group, Topic}, _From, State) -> + Ok = emqx_router:add_route(Topic, dest(Group)), + {reply, Ok, State}; + +handle_call({unsubscribe, Group, Topic}, _From, State) -> + Ok = case ets:member(?SUBSCRIBER, Topic) of + false -> emqx_router:delete_route(Topic, dest(Group)); + true -> ok + end, + {reply, Ok, State}; handle_call(Req, _From, State) -> emqx_logger:error("[Broker] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({From, #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}}, State) -> - Subscriber = {SubPid, SubId}, - case ets:member(?SUBOPTION, {Topic, Subscriber}) of - false -> - resubscribe(From, {Subscriber, SubOpts, Topic}, State); - true -> - case ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2) =:= SubOpts of - true -> - gen_server:reply(From, ok), - {noreply, State}; - false -> - resubscribe(From, {Subscriber, SubOpts, Topic}, State) - end - end; - -handle_cast({From, #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId}}, State) -> - Subscriber = {SubPid, SubId}, - case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of - [{_, SubOpts}] -> - Group = maps:get(share, SubOpts, undefined), - true = do_unsubscribe(Group, Topic, Subscriber), - emqx_shared_sub:unsubscribe(Group, Topic, SubPid), - case ets:member(?SUBSCRIBER, Topic) of - false -> emqx_router:del_route(From, Topic, dest(Group)); - true -> gen_server:reply(From, ok) - end; - [] -> gen_server:reply(From, ok) - end, - {noreply, State}; - handle_cast(Msg, State) -> emqx_logger:error("[Broker] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, SubPid, Reason}, State = #state{submap = SubMap}) -> - case maps:find(SubPid, SubMap) of - {ok, SubIds} -> - lists:foreach(fun(SubId) -> subscriber_down({SubPid, SubId}) end, SubIds), - {noreply, demonitor_subscriber(SubPid, State)}; - error -> - emqx_logger:error("unexpected 'DOWN': ~p, reason: ~p", [SubPid, Reason]), - {noreply, State} - end; - handle_info(Info, State) -> emqx_logger:error("[Broker] unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{pool = Pool, id = Id}) -> +terminate(_Reason, #{pool := Pool, id := Id}) -> gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> @@ -390,69 +356,9 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -resubscribe(From, {Subscriber, SubOpts, Topic}, State) -> - {SubPid, _} = Subscriber, - Group = maps:get(share, SubOpts, undefined), - true = do_subscribe(Group, Topic, Subscriber, SubOpts), - emqx_shared_sub:subscribe(Group, Topic, SubPid), - emqx_router:add_route(From, Topic, dest(Group)), - {noreply, monitor_subscriber(Subscriber, State)}. - -insert_subscriber(Group, Topic, Subscriber) -> - Subscribers = subscribers(Topic), - case lists:member(Subscriber, Subscribers) of - false -> - ets:insert(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}); - _ -> - ok - end. - -do_subscribe(Group, Topic, Subscriber, SubOpts) -> - ets:insert(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), - insert_subscriber(Group, Topic, Subscriber), - ets:insert(?SUBOPTION, {{Topic, Subscriber}, SubOpts}). - -do_unsubscribe(Group, Topic, Subscriber) -> - ets:delete_object(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), - ets:delete_object(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}), - ets:delete(?SUBOPTION, {Topic, Subscriber}). - -subscriber_down(Subscriber) -> - Topics = lists:map(fun({_, {share, Group, Topic}}) -> - {Topic, Group}; - ({_, Topic}) -> - {Topic, undefined} - end, ets:lookup(?SUBSCRIPTION, Subscriber)), - lists:foreach(fun({Topic, undefined}) -> - true = do_unsubscribe(undefined, Topic, Subscriber), - ets:member(?SUBSCRIBER, Topic) orelse emqx_router:del_route(Topic, dest(undefined)); - ({Topic, Group}) -> - true = do_unsubscribe(Group, Topic, Subscriber), - Groups = groups(Topic), - case lists:member(Group, lists:usort(Groups)) of - true -> ok; - false -> emqx_router:del_route(Topic, dest(Group)) - end - end, Topics). - -monitor_subscriber({SubPid, SubId}, State = #state{submap = SubMap, submon = SubMon}) -> - UpFun = fun(SubIds) -> lists:usort([SubId|SubIds]) end, - State#state{submap = maps:update_with(SubPid, UpFun, [SubId], SubMap), - submon = emqx_pmon:monitor(SubPid, SubMon)}. - -demonitor_subscriber(SubPid, State = #state{submap = SubMap, submon = SubMon}) -> - State#state{submap = maps:remove(SubPid, SubMap), - submon = emqx_pmon:demonitor(SubPid, SubMon)}. - dest(undefined) -> node(); dest(Group) -> {Group, node()}. shared(undefined, Name) -> Name; shared(Group, Name) -> {share, Group, Name}. -groups(Topic) -> - lists:foldl(fun({_, {share, Group, _}}, Acc) -> - [Group | Acc]; - ({_, _}, Acc) -> - Acc - end, [], ets:lookup(?SUBSCRIBER, Topic)). diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index e597a233e..35fe06f0d 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -16,63 +16,82 @@ -behaviour(gen_server). --export([start_link/0]). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-compile({no_auto_import, [monitor/2]}). -%% internal export --export([stats_fun/0]). +-export([start_link/0]). +-export([monitor/2]). +-export([create_seq/1, reclaim_seq/1]). + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -define(HELPER, ?MODULE). +-define(SUBMON, emqx_submon). +-define(SUBSEQ, emqx_subseq). --record(state, {}). +-record(state, {pmon :: emqx_pmon:pmon()}). --spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?HELPER}, ?MODULE, [], []). +-spec(monitor(pid(), emqx_types:subid()) -> ok). +monitor(SubPid, SubId) when is_pid(SubPid) -> + case ets:lookup(?SUBMON, SubPid) of + [] -> + gen_server:cast(?HELPER, {monitor, SubPid, SubId}); + [{_, SubId}] -> + ok; + _Other -> + error(subid_conflict) + end. + +-spec(create_seq(emqx_topic:topic()) -> emqx_sequence:seqid()). +create_seq(Topic) -> + emqx_sequence:nextval(?SUBSEQ, Topic). + +-spec(reclaim_seq(emqx_topic:topic()) -> emqx_sequence:seqid()). +reclaim_seq(Topic) -> + emqx_sequence:reclaim(?SUBSEQ, Topic). + %%------------------------------------------------------------------------------ %% gen_server callbacks %%------------------------------------------------------------------------------ init([]) -> - %% Use M:F/A for callback, not anonymous function because - %% fun M:F/A is small, also no badfun risk during hot beam reload - emqx_stats:update_interval(broker_stats, fun ?MODULE:stats_fun/0), - {ok, #state{}, hibernate}. + %% SubSeq: Topic -> SeqId + _ = emqx_sequence:create(?SUBSEQ), + %% SubMon: SubPid -> SubId + _ = emqx_tables:new(?SUBMON, [set, protected, {read_concurrency, true}]), + %% Stats timer + emqx_stats:update_interval(broker_stats, fun emqx_broker:stats_fun/0), + {ok, #state{pmon = emqx_pmon:new()}, hibernate}. handle_call(Req, _From, State) -> emqx_logger:error("[BrokerHelper] unexpected call: ~p", [Req]), {reply, ignored, State}. +handle_cast({monitor, SubPid, SubId}, State = #state{pmon = PMon}) -> + true = ets:insert(?SUBMON, {SubPid, SubId}), + {noreply, State#state{pmon = emqx_pmon:monitor(SubPid, PMon)}}; + handle_cast(Msg, State) -> emqx_logger:error("[BrokerHelper] unexpected cast: ~p", [Msg]), {noreply, State}. +handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) -> + true = ets:delete(?SUBMON, SubPid), + ok = emqx_pool:async_submit(fun emqx_broker:subscriber_down/1, [SubPid]), + {noreply, State#state{pmon = emqx_pmon:erase(SubPid, PMon)}}; + handle_info(Info, State) -> emqx_logger:error("[BrokerHelper] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{}) -> + _ = emqx_sequence:delete(?SUBSEQ), emqx_stats:cancel_update(broker_stats). code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -stats_fun() -> - safe_update_stats(emqx_subscriber, - 'subscribers/count', 'subscribers/max'), - safe_update_stats(emqx_subscription, - 'subscriptions/count', 'subscriptions/max'), - safe_update_stats(emqx_suboptions, - 'suboptions/count', 'suboptions/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_broker_sup.erl b/src/emqx_broker_sup.erl index 51f6e72aa..a511e4154 100644 --- a/src/emqx_broker_sup.erl +++ b/src/emqx_broker_sup.erl @@ -20,8 +20,6 @@ -export([init/1]). --define(TAB_OPTS, [public, {read_concurrency, true}, {write_concurrency, true}]). - start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). @@ -30,39 +28,26 @@ start_link() -> %%------------------------------------------------------------------------------ init([]) -> - %% Create the pubsub tables - ok = lists:foreach(fun create_tab/1, [subscription, subscriber, suboption]), - + %% Broker pool + PoolSize = emqx_vm:schedulers() * 2, + BrokerPool = emqx_pool_sup:spec(emqx_broker_pool, + [broker, hash, PoolSize, + {emqx_broker, start_link, []}]), %% Shared subscription - SharedSub = {shared_sub, {emqx_shared_sub, start_link, []}, - permanent, 5000, worker, [emqx_shared_sub]}, + SharedSub = #{id => shared_sub, + start => {emqx_shared_sub, start_link, []}, + restart => permanent, + shutdown => 2000, + type => worker, + modules => [emqx_shared_sub]}, %% Broker helper - Helper = {broker_helper, {emqx_broker_helper, start_link, []}, - permanent, 5000, worker, [emqx_broker_helper]}, + Helper = #{id => helper, + start => {emqx_broker_helper, start_link, []}, + restart => permanent, + shutdown => 2000, + type => worker, + modules => [emqx_broker_helper]}, - %% Broker pool - BrokerPool = emqx_pool_sup:spec(emqx_broker_pool, - [broker, hash, emqx_vm:schedulers() * 2, - {emqx_broker, start_link, []}]), - - {ok, {{one_for_all, 0, 1}, [SharedSub, Helper, BrokerPool]}}. - -%%------------------------------------------------------------------------------ -%% Create tables -%%------------------------------------------------------------------------------ - -create_tab(suboption) -> - %% Suboption: {Topic, Sub} -> [{qos, 1}] - emqx_tables:new(emqx_suboption, [set | ?TAB_OPTS]); - -create_tab(subscriber) -> - %% Subscriber: Topic -> Sub1, Sub2, Sub3, ..., SubN - %% duplicate_bag: o(1) insert - emqx_tables:new(emqx_subscriber, [duplicate_bag | ?TAB_OPTS]); - -create_tab(subscription) -> - %% Subscription: Sub -> Topic1, Topic2, Topic3, ..., TopicN - %% bag: o(n) insert - emqx_tables:new(emqx_subscription, [bag | ?TAB_OPTS]). + {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, Helper]}}. diff --git a/src/emqx_local_bridge.erl b/src/emqx_local_bridge.erl index 7c4e7cea1..df2dda686 100644 --- a/src/emqx_local_bridge.erl +++ b/src/emqx_local_bridge.erl @@ -61,7 +61,7 @@ init([Pool, Id, Node, Topic, Options]) -> true -> true = erlang:monitor_node(Node, true), Group = iolist_to_binary(["$bridge:", atom_to_list(Node), ":", Topic]), - emqx_broker:subscribe(Topic, self(), #{share => Group, qos => ?QOS_0}), + emqx_broker:subscribe(Topic, #{share => Group, qos => ?QOS_0}), State = parse_opts(Options, #state{node = Node, subtopic = Topic}), MQueue = emqx_mqueue:init(#{max_len => State#state.max_queue_len, store_qos0 => true}), diff --git a/src/emqx_sequence.erl b/src/emqx_sequence.erl index 62a882294..c4fc0fd08 100644 --- a/src/emqx_sequence.erl +++ b/src/emqx_sequence.erl @@ -14,45 +14,47 @@ -module(emqx_sequence). --export([create/0, create/1]). --export([generate/1, generate/2]). --export([reclaim/1, reclaim/2]). +-export([create/1, nextval/2, currval/2, reclaim/2, delete/1]). -type(key() :: term()). +-type(name() :: atom()). -type(seqid() :: non_neg_integer()). --define(DEFAULT_TAB, ?MODULE). +-export_type([seqid/0]). %% @doc Create a sequence. --spec(create() -> ok). -create() -> - create(?DEFAULT_TAB). - --spec(create(atom()) -> ok). -create(Tab) -> - _ = ets:new(Tab, [set, public, named_table, {write_concurrency, true}]), +-spec(create(name()) -> ok). +create(Name) -> + _ = ets:new(Name, [set, public, named_table, {write_concurrency, true}]), ok. -%% @doc Generate a sequence id. --spec(generate(key()) -> seqid()). -generate(Key) -> - generate(?DEFAULT_TAB, Key). +%% @doc Next value of the sequence. +-spec(nextval(name(), key()) -> seqid()). +nextval(Name, Key) -> + ets:update_counter(Name, Key, {2, 1}, {Key, 0}). --spec(generate(atom(), key()) -> seqid()). -generate(Tab, Key) -> - ets:update_counter(Tab, Key, {2, 1}, {Key, 0}). +%% @doc Current value of the sequence. +-spec(currval(name(), key()) -> seqid()). +currval(Name, Key) -> + try ets:lookup_element(Name, Key, 2) + catch + error:badarg -> 0 + end. %% @doc Reclaim a sequence id. --spec(reclaim(key()) -> seqid()). -reclaim(Key) -> - reclaim(?DEFAULT_TAB, Key). - --spec(reclaim(atom(), key()) -> seqid()). -reclaim(Tab, Key) -> - try ets:update_counter(Tab, Key, {2, -1, 0, 0}) of - 0 -> ets:delete_object(Tab, {Key, 0}), 0; +-spec(reclaim(name(), key()) -> seqid()). +reclaim(Name, Key) -> + try ets:update_counter(Name, Key, {2, -1, 0, 0}) of + 0 -> ets:delete_object(Name, {Key, 0}), 0; I -> I catch error:badarg -> 0 end. +%% @doc Delete the sequence. +delete(Name) -> + case ets:info(Name, name) of + Name -> ets:delete(Name); + undefined -> false + end. + diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 12718b9fc..262a9a7a8 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -465,7 +465,7 @@ handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts#{first => false}]), SubMap; {ok, _SubOpts} -> - emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), + emqx_broker:set_subopts(Topic, SubOpts), %% Why??? emqx_hooks:run('session.subscribed', [#{client_id => ClientId}, Topic, SubOpts#{first => false}]), maps:put(Topic, SubOpts, SubMap); diff --git a/test/emqx_sequence_SUITE.erl b/test/emqx_sequence_SUITE.erl index 999a95723..f37b60d76 100644 --- a/test/emqx_sequence_SUITE.erl +++ b/test/emqx_sequence_SUITE.erl @@ -19,19 +19,19 @@ -include_lib("eunit/include/eunit.hrl"). --import(emqx_sequence, [generate/1, reclaim/1]). +-import(emqx_sequence, [nextval/2, reclaim/2]). all() -> [sequence_generate]. sequence_generate(_) -> - ok = emqx_sequence:create(), - ?assertEqual(1, generate(key)), - ?assertEqual(2, generate(key)), - ?assertEqual(3, generate(key)), - ?assertEqual(2, reclaim(key)), - ?assertEqual(1, reclaim(key)), - ?assertEqual(0, reclaim(key)), - ?assertEqual(false, ets:member(emqx_sequence, key)), - ?assertEqual(1, generate(key)). + ok = emqx_sequence:create(seqtab), + ?assertEqual(1, nextval(seqtab, key)), + ?assertEqual(2, nextval(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)). From 36e7d63d66b47d9c6ba6f4de8d33a60b4ad0be31 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 7 Dec 2018 18:20:09 +0800 Subject: [PATCH 453/520] Implement subscription sharding. 1. Improve the design router, broker and shared_sub 2. New ets tables' design for subscription sharding --- src/emqx_access_control.erl | 2 +- src/emqx_acl_internal.erl | 2 +- src/emqx_broker.erl | 149 ++++++++++++++++++------------ src/emqx_broker_helper.erl | 14 ++- src/emqx_cm.erl | 6 +- src/emqx_ctl.erl | 2 +- src/emqx_hooks.erl | 2 +- src/emqx_metrics.erl | 2 +- src/emqx_router.erl | 175 +++++++++++++----------------------- src/emqx_sequence.erl | 3 +- src/emqx_shared_sub.erl | 63 ++++++++----- src/emqx_sm.erl | 8 +- src/emqx_stats.erl | 2 +- src/emqx_tables.erl | 6 +- src/emqx_zone.erl | 2 +- 15 files changed, 227 insertions(+), 211 deletions(-) diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 1b9d76937..06ea86633 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -148,7 +148,7 @@ stop() -> %%----------------------------------------------------------------------------- init([]) -> - _ = emqx_tables:new(?TAB, [set, protected, {read_concurrency, true}]), + ok = emqx_tables:new(?TAB, [set, protected, {read_concurrency, true}]), {ok, #{}}. handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) -> diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index eee7e6c18..f8b995096 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -45,7 +45,7 @@ all_rules() -> -spec(init([File :: string()]) -> {ok, #{}}). init([File]) -> - _ = emqx_tables:new(?ACL_RULE_TAB, [set, public, {read_concurrency, true}]), + ok = emqx_tables:new(?ACL_RULE_TAB, [set, public, {read_concurrency, true}]), true = load_rules_from_file(File), {ok, #{acl_file => File}}. diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 3a3cde1fa..0a9264489 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -23,7 +23,7 @@ -export([unsubscribe/1, unsubscribe/2]). -export([subscriber_down/1]). -export([publish/1, safe_publish/1]). --export([dispatch/2, dispatch/3]). +-export([dispatch/2]). -export([subscriptions/1, subscribers/1, subscribed/2]). -export([get_subopts/2, set_subopts/2]). -export([topics/0]). @@ -34,8 +34,6 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --define(SHARD, 1024). --define(TIMEOUT, 60000). -define(BROKER, ?MODULE). %% ETS tables @@ -44,33 +42,36 @@ -define(SUBSCRIBER, emqx_subscriber). -define(SUBSCRIPTION, emqx_subscription). -%% Gards +%% Guards -define(is_subid(Id), (is_binary(Id) orelse is_atom(Id))). -spec(start_link(atom(), pos_integer()) -> emqx_types:startlink_ret()). start_link(Pool, Id) -> _ = create_tabs(), - gen_server:start_link({local, emqx_misc:proc_name(?BROKER, Id)}, ?MODULE, [Pool, Id], []). + Name = emqx_misc:proc_name(?BROKER, Id), + gen_server:start_link({local, Name}, ?MODULE, [Pool, Id], []). %%------------------------------------------------------------------------------ %% Create tabs %%------------------------------------------------------------------------------ +-spec(create_tabs() -> ok). create_tabs() -> TabOpts = [public, {read_concurrency, true}, {write_concurrency, true}], - %% SubId: SubId -> SubPid1, SubPid2,... - _ = emqx_tables:new(?SUBID, [bag | TabOpts]), + %% SubId: SubId -> SubPid + ok = emqx_tables:new(?SUBID, [set | TabOpts]), + %% SubOption: {SubPid, Topic} -> SubOption - _ = emqx_tables:new(?SUBOPTION, [set | TabOpts]), + ok = emqx_tables:new(?SUBOPTION, [set | TabOpts]), %% Subscription: SubPid -> Topic1, Topic2, Topic3, ... %% duplicate_bag: o(1) insert - _ = emqx_tables:new(?SUBSCRIPTION, [duplicate_bag | TabOpts]), + ok = emqx_tables:new(?SUBSCRIPTION, [duplicate_bag | TabOpts]), %% Subscriber: Topic -> SubPid1, SubPid2, SubPid3, ... %% duplicate_bag: o(1) insert - emqx_tables:new(?SUBSCRIBER, [duplicate_bag | TabOpts]). + ok = emqx_tables:new(?SUBSCRIBER, [duplicate_bag | TabOpts]). %%------------------------------------------------------------------------------ %% Subscribe API @@ -92,14 +93,23 @@ subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map case ets:member(?SUBOPTION, {SubPid, Topic}) of false -> ok = emqx_broker_helper:monitor(SubPid, SubId), - Group = maps:get(share, SubOpts, undefined), %% true = ets:insert(?SUBID, {SubId, SubPid}), true = ets:insert(?SUBSCRIPTION, {SubPid, Topic}), - %% SeqId = emqx_broker_helper:create_seq(Topic), - true = ets:insert(?SUBSCRIBER, {Topic, shared(Group, SubPid)}), - true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), - ok = emqx_shared_sub:subscribe(Group, Topic, SubPid), - call(pick(Topic), {subscribe, Group, Topic}); + case maps:get(share, SubOpts, undefined) of + undefined -> + Shard = emqx_broker_helper:get_shard(SubPid, Topic), + case Shard of + 0 -> true = ets:insert(?SUBSCRIBER, {Topic, SubPid}); + I -> true = ets:insert(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + true = ets:insert(?SUBSCRIBER, {Topic, {shard, I}}) + end, + SubOpts1 = maps:put(shard, Shard, SubOpts), + true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts1}), + call(pick({Topic, Shard}), {subscribe, Topic}); + Group -> %% Shared subscription + true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), + emqx_shared_sub:subscribe(Group, Topic, SubPid) + end; true -> ok end. @@ -112,12 +122,21 @@ unsubscribe(Topic) when is_binary(Topic) -> SubPid = self(), case ets:lookup(?SUBOPTION, {SubPid, Topic}) of [{_, SubOpts}] -> - Group = maps:get(share, SubOpts, undefined), + _ = emqx_broker_helper:reclaim_seq(Topic), + case maps:get(share, SubOpts, undefined) of + undefined -> + case maps:get(shared, SubOpts, 0) of + 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), + ok = cast(pick(Topic), {unsubscribed, Topic}); + I -> true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) + end; + Group -> + ok = emqx_shared_sub:unsubscribe(Group, Topic, SubPid) + end, true = ets:delete_object(?SUBSCRIPTION, {SubPid, Topic}), - true = ets:delete_object(?SUBSCRIBER, {Topic, shared(Group, SubPid)}), - true = ets:delete(?SUBOPTION, {SubPid, Topic}), - ok = emqx_shared_sub:unsubscribe(Group, Topic, SubPid), - call(pick(Topic), {unsubscribe, Group, Topic}); + %%true = ets:delete_object(?SUBID, {SubId, SubPid}), + true = ets:delete(?SUBOPTION, {SubPid, Topic}); [] -> ok end. @@ -207,22 +226,23 @@ dispatch(Topic, Delivery = #delivery{message = Msg, results = Results}) -> emqx_hooks:run('message.dropped', [#{node => node()}, Msg]), inc_dropped_cnt(Topic), Delivery; - [SubPid] -> %% optimize? - dispatch(SubPid, Topic, Msg), + [Sub] -> %% optimize? + dispatch(Sub, Topic, Msg), Delivery#delivery{results = [{dispatch, Topic, 1}|Results]}; - SubPids -> - Count = lists:foldl(fun(SubPid, Acc) -> - dispatch(SubPid, Topic, Msg), Acc + 1 - end, 0, SubPids), + Subs -> + Count = lists:foldl( + fun(Sub, Acc) -> + dispatch(Sub, Topic, Msg), Acc + 1 + end, 0, Subs), Delivery#delivery{results = [{dispatch, Topic, Count}|Results]} end. dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> - SubPid ! {dispatch, Topic, Msg}, - true; -%% TODO: how to optimize the share sub? -dispatch({share, _Group, _SubPid}, _Topic, _Msg) -> - false. + SubPid ! {dispatch, Topic, Msg}; +dispatch({shard, I}, Topic, Msg) -> + lists:foreach(fun(SubPid) -> + SubPid ! {dispatch, Topic, Msg} + end, safe_lookup_element(?SUBSCRIBER, {share, Topic, I}, [])). inc_dropped_cnt(<<"$SYS/", _/binary>>) -> ok; @@ -240,17 +260,20 @@ subscribers(Topic) -> -spec(subscriber_down(pid()) -> true). subscriber_down(SubPid) -> lists:foreach( - fun(Sub = {_, Topic}) -> + fun(Sub = {_Pid, Topic}) -> case ets:lookup(?SUBOPTION, Sub) of [{_, SubOpts}] -> - Group = maps:get(share, SubOpts, undefined), - true = ets:delete_object(?SUBSCRIBER, {Topic, shared(Group, SubPid)}), - true = ets:delete(?SUBOPTION, Sub), - gen_server:cast(pick(Topic), {unsubscribe, Group, Topic}); + _ = emqx_broker_helper:reclaim_seq(Topic), + case maps:get(shared, SubOpts, 0) of + 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), + ok = cast(pick(Topic), {unsubscribed, Topic}); + I -> true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) + end; [] -> ok end end, ets:lookup(?SUBSCRIPTION, SubPid)), - ets:delete(?SUBSCRIPTION, SubPid). + true = ets:delete(?SUBSCRIPTION, SubPid). %%------------------------------------------------------------------------------ %% Management APIs @@ -305,11 +328,14 @@ safe_update_stats(Tab, Stat, MaxStat) -> end. %%------------------------------------------------------------------------------ -%% Pick and call +%% call, cast, pick %%------------------------------------------------------------------------------ call(Broker, Req) -> - gen_server:call(Broker, Req, ?TIMEOUT). + gen_server:call(Broker, Req). + +cast(Broker, Msg) -> + gen_server:cast(Broker, Msg). %% Pick a broker pick(Topic) -> @@ -320,24 +346,41 @@ pick(Topic) -> %%------------------------------------------------------------------------------ init([Pool, Id]) -> + _ = emqx_router:set_mode(protected), true = gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #{pool => Pool, id => Id}}. -handle_call({subscribe, Group, Topic}, _From, State) -> - Ok = emqx_router:add_route(Topic, dest(Group)), - {reply, Ok, State}; - -handle_call({unsubscribe, Group, Topic}, _From, State) -> - Ok = case ets:member(?SUBSCRIBER, Topic) of - false -> emqx_router:delete_route(Topic, dest(Group)); - true -> ok - end, - {reply, Ok, State}; +handle_call({subscribe, Topic}, _From, State) -> + case get(Topic) of + undefined -> + _ = put(Topic, true), + emqx_router:add_route(Topic); + true -> ok + end, + {reply, ok, State}; handle_call(Req, _From, State) -> emqx_logger:error("[Broker] unexpected call: ~p", [Req]), {reply, ignored, State}. +handle_cast({unsubscribed, Topic}, State) -> + case ets:member(?SUBSCRIBER, Topic) of + false -> + _ = erase(Topic), + emqx_router:delete_route(Topic); + true -> ok + end, + {noreply, State}; + +handle_cast({unsubscribed, Topic, I}, State) -> + case ets:member(?SUBSCRIBER, {shard, Topic, I}) of + false -> + true = ets:delete_object(?SUBSCRIBER, {Topic, {shard, I}}), + cast(pick(Topic), {unsubscribed, Topic}); + true -> ok + end, + {noreply, State}; + handle_cast(Msg, State) -> emqx_logger:error("[Broker] unexpected cast: ~p", [Msg]), {noreply, State}. @@ -356,9 +399,3 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -dest(undefined) -> node(); -dest(Group) -> {Group, node()}. - -shared(undefined, Name) -> Name; -shared(Group, Name) -> {share, Group, Name}. - diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index 35fe06f0d..d3e7f9d37 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -20,6 +20,7 @@ -export([start_link/0]). -export([monitor/2]). +-export([get_shard/2]). -export([create_seq/1, reclaim_seq/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, @@ -46,6 +47,13 @@ monitor(SubPid, SubId) when is_pid(SubPid) -> error(subid_conflict) end. +-spec(get_shard(pid(), emqx_topic:topic()) -> non_neg_integer()). +get_shard(SubPid, Topic) -> + case create_seq(Topic) of + Seq when Seq =< 1024 -> 0; + _Seq -> erlang:phash2(SubPid, ets:lookup_element(?SUBSEQ, shards, 2)) + end. + -spec(create_seq(emqx_topic:topic()) -> emqx_sequence:seqid()). create_seq(Topic) -> emqx_sequence:nextval(?SUBSEQ, Topic). @@ -60,9 +68,11 @@ reclaim_seq(Topic) -> init([]) -> %% SubSeq: Topic -> SeqId - _ = emqx_sequence:create(?SUBSEQ), + ok = emqx_sequence:create(?SUBSEQ), + %% Shards: CPU * 32 + true = ets:insert(?SUBSEQ, {shards, emqx_vm:schedulers() * 32}), %% SubMon: SubPid -> SubId - _ = emqx_tables:new(?SUBMON, [set, protected, {read_concurrency, true}]), + ok = emqx_tables:new(?SUBMON, [set, protected, {read_concurrency, true}]), %% Stats timer emqx_stats:update_interval(broker_stats, fun emqx_broker:stats_fun/0), {ok, #state{pmon = emqx_pmon:new()}, hibernate}. diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 19892b386..6756cf02b 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -125,9 +125,9 @@ notify(Msg) -> init([]) -> TabOpts = [public, set, {write_concurrency, true}], - _ = emqx_tables:new(?CONN_TAB, [{read_concurrency, true} | TabOpts]), - _ = emqx_tables:new(?CONN_ATTRS_TAB, TabOpts), - _ = emqx_tables:new(?CONN_STATS_TAB, TabOpts), + 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(cm_stats, fun ?MODULE:update_conn_stats/0), {ok, #{conn_pmon => emqx_pmon:new()}}. diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index 17166a014..c00556eb7 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -96,7 +96,7 @@ usage() -> %%------------------------------------------------------------------------------ init([]) -> - _ = emqx_tables:new(?TAB, [ordered_set, protected]), + ok = emqx_tables:new(?TAB, [protected, ordered_set]), {ok, #state{seq = 0}}. handle_call(Req, _From, State) -> diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index 073c12870..b2eb0d6f4 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -139,7 +139,7 @@ lookup(HookPoint) -> %%------------------------------------------------------------------------------ init([]) -> - _ = emqx_tables:new(?TAB, [{keypos, #hook.name}, {read_concurrency, true}]), + 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_metrics.erl b/src/emqx_metrics.erl index caf862146..b4b3a1307 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -285,7 +285,7 @@ qos_sent(?QOS_2) -> init([]) -> % Create metrics table - _ = emqx_tables:new(?TAB, [set, public, {write_concurrency, true}]), + ok = emqx_tables:new(?TAB, [public, set, {write_concurrency, true}]), lists:foreach(fun new/1, ?BYTES_METRICS ++ ?PACKET_METRICS ++ ?MESSAGE_METRICS), {ok, #{}, hibernate}. diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 941c004f7..313adc475 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -28,23 +28,22 @@ -export([start_link/2]). %% Route APIs --export([add_route/1, add_route/2, add_route/3]). +-export([add_route/1, add_route/2]). -export([get_routes/1]). --export([del_route/1, del_route/2, del_route/3]). +-export([delete_route/1, delete_route/2]). -export([has_routes/1, match_routes/1, print_routes/1]). -export([topics/0]). + +%% Mode +-export([set_mode/1, get_mode/0]). + %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -type(destination() :: node() | {binary(), node()}). --record(batch, {enabled, timer, pending}). --record(state, {pool, id, batch :: #batch{}}). - -define(ROUTE, emqx_route). --define(BATCH(Enabled), #batch{enabled = Enabled}). --define(BATCH(Enabled, Pending), #batch{enabled = Enabled, pending = Pending}). %%------------------------------------------------------------------------------ %% Mnesia bootstrap @@ -62,49 +61,66 @@ mnesia(copy) -> ok = ekka_mnesia:copy_table(?ROUTE). %%------------------------------------------------------------------------------ -%% Strat a router +%% Start a router %%------------------------------------------------------------------------------ --spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link(atom(), pos_integer()) -> emqx_types:startlink_ret()). start_link(Pool, Id) -> - gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, - ?MODULE, [Pool, Id], [{hibernate_after, 2000}]). + Name = emqx_misc:proc_name(?MODULE, Id), + gen_server:start_link({local, Name}, ?MODULE, [Pool, Id], [{hibernate_after, 1000}]). %%------------------------------------------------------------------------------ %% Route APIs %%------------------------------------------------------------------------------ --spec(add_route(emqx_topic:topic() | emqx_types:route()) -> ok). +-spec(add_route(emqx_topic:topic() | emqx_types:route()) -> ok | {error, term()}). add_route(Topic) when is_binary(Topic) -> add_route(#route{topic = Topic, dest = node()}); add_route(Route = #route{topic = Topic}) -> - cast(pick(Topic), {add_route, Route}). + case get_mode() of + protected -> do_add_route(Route); + undefined -> call(pick(Topic), {add_route, Route}) + end. --spec(add_route(emqx_topic:topic(), destination()) -> ok). +-spec(add_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). add_route(Topic, Dest) when is_binary(Topic) -> add_route(#route{topic = Topic, dest = Dest}). --spec(add_route({pid(), reference()}, emqx_topic:topic(), destination()) -> ok). -add_route(From, Topic, Dest) when is_binary(Topic) -> - cast(pick(Topic), {add_route, From, #route{topic = Topic, dest = Dest}}). +%% @private +do_add_route(Route = #route{topic = Topic, dest = Dest}) -> + case lists:member(Route, get_routes(Topic)) of + true -> ok; + false -> + ok = emqx_router_helper:monitor(Dest), + case emqx_topic:wildcard(Topic) of + true -> trans(fun add_trie_route/1, [Route]); + false -> add_direct_route(Route) + end + end. -spec(get_routes(emqx_topic:topic()) -> [emqx_types:route()]). get_routes(Topic) -> ets:lookup(?ROUTE, Topic). --spec(del_route(emqx_topic:topic() | emqx_types:route()) -> ok). -del_route(Topic) when is_binary(Topic) -> - del_route(#route{topic = Topic, dest = node()}); -del_route(Route = #route{topic = Topic}) -> - cast(pick(Topic), {del_route, Route}). +-spec(delete_route(emqx_topic:topic() | emqx_types:route()) -> ok | {error, term()}). +delete_route(Topic) when is_binary(Topic) -> + delete_route(#route{topic = Topic, dest = node()}); +delete_route(Route = #route{topic = Topic}) -> + case get_mode() of + protected -> do_delete_route(Route); + undefined -> call(pick(Topic), {delete_route, Route}) + end. --spec(del_route(emqx_topic:topic(), destination()) -> ok). -del_route(Topic, Dest) when is_binary(Topic) -> - del_route(#route{topic = Topic, dest = Dest}). +-spec(delete_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). +delete_route(Topic, Dest) when is_binary(Topic) -> + delete_route(#route{topic = Topic, dest = Dest}). --spec(del_route({pid(), reference()}, emqx_topic:topic(), destination()) -> ok). -del_route(From, Topic, Dest) when is_binary(Topic) -> - cast(pick(Topic), {del_route, From, #route{topic = Topic, dest = Dest}}). +%% @private +do_delete_route(Route = #route{topic = Topic}) -> + case emqx_topic:wildcard(Topic) of + true -> trans(fun del_trie_route/1, [Route]); + false -> del_direct_route(Route) + end. -spec(has_routes(emqx_topic:topic()) -> boolean()). has_routes(Topic) when is_binary(Topic) -> @@ -127,8 +143,15 @@ print_routes(Topic) -> io:format("~s -> ~s~n", [To, Dest]) end, match_routes(Topic)). -cast(Router, Msg) -> - gen_server:cast(Router, Msg). +-spec(set_mode(protected | atom()) -> any()). +set_mode(Mode) when is_atom(Mode) -> + put('$router_mode', Mode). + +-spec(get_mode() -> protected | undefined | atom()). +get_mode() -> get('$router_mode'). + +call(Router, Msg) -> + gen_server:call(Router, Msg, infinity). pick(Topic) -> gproc_pool:pick_worker(router, Topic). @@ -138,71 +161,28 @@ pick(Topic) -> %%------------------------------------------------------------------------------ init([Pool, Id]) -> - rand:seed(exsplus, erlang:timestamp()), - gproc_pool:connect_worker(Pool, {Pool, Id}), - Batch = #batch{enabled = emqx_config:get_env(route_batch_clean, false), - pending = sets:new()}, - {ok, ensure_batch_timer(#state{pool = Pool, id = Id, batch = Batch})}. + true = gproc_pool:connect_worker(Pool, {Pool, Id}), + {ok, #{pool => Pool, id => Id}}. + +handle_call({add_route, Route}, _From, State) -> + {reply, do_add_route(Route), State}; + +handle_call({delete_route, Route}, _From, State) -> + {reply, do_delete_route(Route), State}; handle_call(Req, _From, State) -> emqx_logger:error("[Router] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({add_route, From, Route}, State) -> - {noreply, NewState} = handle_cast({add_route, Route}, State), - _ = gen_server:reply(From, ok), - {noreply, NewState}; - -handle_cast({add_route, Route = #route{topic = Topic, dest = Dest}}, State) -> - case lists:member(Route, get_routes(Topic)) of - true -> ok; - false -> - ok = emqx_router_helper:monitor(Dest), - case emqx_topic:wildcard(Topic) of - true -> log(trans(fun add_trie_route/1, [Route])); - false -> add_direct_route(Route) - end - end, - {noreply, State}; - -handle_cast({del_route, From, Route}, State) -> - {noreply, NewState} = handle_cast({del_route, Route}, State), - _ = gen_server:reply(From, ok), - {noreply, NewState}; - -handle_cast({del_route, Route = #route{topic = Topic, dest = Dest}}, State) when is_tuple(Dest) -> - {noreply, case emqx_topic:wildcard(Topic) of - true -> log(trans(fun del_trie_route/1, [Route])), - State; - false -> del_direct_route(Route, State) - end}; - -handle_cast({del_route, Route = #route{topic = Topic}}, State) -> - %% Confirm if there are still subscribers... - {noreply, case ets:member(emqx_subscriber, Topic) of - true -> State; - false -> - case emqx_topic:wildcard(Topic) of - true -> log(trans(fun del_trie_route/1, [Route])), - State; - false -> del_direct_route(Route, State) - end - end}; - handle_cast(Msg, State) -> emqx_logger:error("[Router] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({timeout, _TRef, batch_delete}, State = #state{batch = Batch}) -> - _ = del_direct_routes(sets:to_list(Batch#batch.pending)), - {noreply, ensure_batch_timer(State#state{batch = ?BATCH(true, sets:new())}), hibernate}; - handle_info(Info, State) -> emqx_logger:error("[Router] unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{pool = Pool, id = Id, batch = Batch}) -> - _ = cacel_batch_timer(Batch), +terminate(_Reason, #{pool := Pool, id := Id}) -> gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> @@ -212,17 +192,6 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -ensure_batch_timer(State = #state{batch = #batch{enabled = false}}) -> - State; -ensure_batch_timer(State = #state{batch = Batch}) -> - TRef = erlang:start_timer(50 + rand:uniform(50), self(), batch_delete), - State#state{batch = Batch#batch{timer = TRef}}. - -cacel_batch_timer(#batch{enabled = false}) -> - ok; -cacel_batch_timer(#batch{enabled = true, timer = TRef}) -> - catch erlang:cancel_timer(TRef). - add_direct_route(Route) -> mnesia:async_dirty(fun mnesia:write/3, [?ROUTE, Route, sticky_write]). @@ -233,25 +202,9 @@ add_trie_route(Route = #route{topic = Topic}) -> end, mnesia:write(?ROUTE, Route, sticky_write). -del_direct_route(Route, State = #state{batch = ?BATCH(false)}) -> - del_direct_route(Route), State; -del_direct_route(Route, State = #state{batch = Batch = ?BATCH(true, Pending)}) -> - State#state{batch = Batch#batch{pending = sets:add_element(Route, Pending)}}. - del_direct_route(Route) -> mnesia:async_dirty(fun mnesia:delete_object/3, [?ROUTE, Route, sticky_write]). -del_direct_routes([]) -> - ok; -del_direct_routes(Routes) -> - DelFun = fun(R = #route{topic = Topic}) -> - case ets:member(emqx_subscriber, Topic) of - true -> ok; - false -> mnesia:delete_object(?ROUTE, R, sticky_write) - end - end, - mnesia:async_dirty(fun lists:foreach/2, [DelFun, Routes]). - del_trie_route(Route = #route{topic = Topic}) -> case mnesia:wread({?ROUTE, Topic}) of [Route] -> %% Remove route and trie @@ -270,7 +223,3 @@ trans(Fun, Args) -> {aborted, Error} -> {error, Error} end. -log(ok) -> ok; -log({error, Reason}) -> - emqx_logger:error("[Router] mnesia aborted: ~p", [Reason]). - diff --git a/src/emqx_sequence.erl b/src/emqx_sequence.erl index c4fc0fd08..022531df5 100644 --- a/src/emqx_sequence.erl +++ b/src/emqx_sequence.erl @@ -25,8 +25,7 @@ %% @doc Create a sequence. -spec(create(name()) -> ok). create(Name) -> - _ = ets:new(Name, [set, public, named_table, {write_concurrency, true}]), - ok. + emqx_tables:new(Name, [public, set, {write_concurrency, true}]). %% @doc Next value of the sequence. -spec(nextval(name(), key()) -> seqid()). diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index ebe6d51f8..d1d0d921d 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -17,6 +17,7 @@ -behaviour(gen_server). -include("emqx.hrl"). +-include("emqx_mqtt.hrl"). %% Mnesia bootstrap -export([mnesia/1]). @@ -27,7 +28,8 @@ -export([start_link/0]). -export([subscribe/3, unsubscribe/3]). --export([dispatch/3, maybe_ack/1, maybe_nack_dropped/1, nack_no_connection/1, is_ack_required/1]). +-export([dispatch/3]). +-export([maybe_ack/1, maybe_nack_dropped/1, nack_no_connection/1, is_ack_required/1]). %% for testing -export([subscribers/2]). @@ -38,6 +40,7 @@ -define(SERVER, ?MODULE). -define(TAB, emqx_shared_subscription). +-define(SHARED_SUBS, emqx_shared_subscriber). -define(ALIVE_SUBS, emqx_alive_shared_subscribers). -define(SHARED_SUB_QOS1_DISPATCH_TIMEOUT_SECONDS, 5). -define(ack, shared_sub_ack). @@ -48,8 +51,6 @@ -record(state, {pmon}). -record(emqx_shared_subscription, {group, topic, subpid}). --include("emqx_mqtt.hrl"). - %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -72,16 +73,11 @@ mnesia(copy) -> start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -subscribe(undefined, _Topic, _SubPid) -> - ok; subscribe(Group, Topic, SubPid) when is_pid(SubPid) -> - mnesia:dirty_write(?TAB, record(Group, Topic, SubPid)), - gen_server:cast(?SERVER, {monitor, SubPid}). + gen_server:call(?SERVER, {subscribe, Group, Topic, SubPid}). -unsubscribe(undefined, _Topic, _SubPid) -> - ok; unsubscribe(Group, Topic, SubPid) when is_pid(SubPid) -> - mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)). + gen_server:call(?SERVER, {unsubscribe, Group, Topic, SubPid}). record(Group, Topic, SubPid) -> #emqx_shared_subscription{group = Group, topic = Topic, subpid = SubPid}. @@ -251,14 +247,16 @@ 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([]) -> - {atomic, PMon} = mnesia:transaction(fun init_monitors/0), + _ = emqx_router:set_mode(protected), mnesia:subscribe({table, ?TAB, simple}), - ets:new(?ALIVE_SUBS, [named_table, {read_concurrency, true}, protected]), + {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}]), {ok, update_stats(#state{pmon = PMon})}. init_monitors() -> @@ -267,14 +265,29 @@ init_monitors() -> emqx_pmon:monitor(SubPid, Mon) end, emqx_pmon:new(), ?TAB). +handle_call({subscribe, Group, Topic, SubPid}, _From, State = #state{pmon = PMon}) -> + mnesia:dirty_write(?TAB, record(Group, Topic, SubPid)), + case ets:member(?SHARED_SUBS, {Group, Topic}) of + true -> ok; + false -> ok = emqx_router:add_route(Topic, {Group, node()}) + end, + ok = maybe_insert_alive_tab(SubPid), + true = ets:insert(?SHARED_SUBS, {{Group, Topic}, SubPid}), + {reply, ok, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})}; + +handle_call({unsubscribe, Group, Topic, SubPid}, _From, State) -> + mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)), + true = ets:delete_object(?SHARED_SUBS, {{Group, Topic}, SubPid}), + case ets:member(?SHARED_SUBS, {Group, Topic}) of + true -> ok; + false -> ok = emqx_router:delete_route(Topic, {Group, node()}) + end, + {reply, ok, State}; + handle_call(Req, _From, State) -> emqx_logger:error("[SharedSub] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({monitor, SubPid}, State= #state{pmon = PMon}) -> - NewPmon = emqx_pmon:monitor(SubPid, PMon), - ok = maybe_insert_alive_tab(SubPid), - {noreply, update_stats(State#state{pmon = NewPmon})}; handle_cast(Msg, State) -> emqx_logger:error("[SharedSub] unexpected cast: ~p", [Msg]), {noreply, State}. @@ -316,12 +329,18 @@ maybe_insert_alive_tab(Pid) when is_pid(Pid) -> ets:insert(?ALIVE_SUBS, {Pid}), cleanup_down(SubPid) -> ?IS_LOCAL_PID(SubPid) orelse ets:delete(?ALIVE_SUBS, SubPid), lists:foreach( - fun(Record) -> - mnesia:dirty_delete_object(?TAB, Record) - end,mnesia:dirty_match_object(#emqx_shared_subscription{_ = '_', subpid = SubPid})). + fun(Record = #emqx_shared_subscription{topic = Topic, group = Group}) -> + ok = mnesia:dirty_delete_object(?TAB, Record), + true = ets:delete_object(?SHARED_SUBS, {{Group, Topic}, SubPid}), + case ets:member(?SHARED_SUBS, {Group, Topic}) of + true -> ok; + false -> ok = emqx_router:delete_route(Topic, {Group, node()}) + end + end, mnesia:dirty_match_object(#emqx_shared_subscription{_ = '_', subpid = SubPid})). update_stats(State) -> - emqx_stats:setstat('subscriptions/shared/count', 'subscriptions/shared/max', ets:info(?TAB, size)), State. + emqx_stats:setstat('subscriptions/shared/count', 'subscriptions/shared/max', ets:info(?TAB, size)), + State. %% Return 'true' if the subscriber process is alive AND not in the failed list is_active_sub(Pid, FailedSubs) -> diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 8f9a3e3cb..d178a8ae7 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -202,10 +202,10 @@ notify(Event) -> init([]) -> TabOpts = [public, set, {write_concurrency, true}], - _ = emqx_tables:new(?SESSION_TAB, [{read_concurrency, true} | TabOpts]), - _ = emqx_tables:new(?SESSION_P_TAB, TabOpts), - _ = emqx_tables:new(?SESSION_ATTRS_TAB, TabOpts), - _ = emqx_tables:new(?SESSION_STATS_TAB, TabOpts), + 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), emqx_stats:update_interval(sm_stats, fun ?MODULE:stats_fun/0), {ok, #{session_pmon => emqx_pmon:new()}}. diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index 61ff6cbc3..790c397b9 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -152,7 +152,7 @@ cast(Msg) -> %%------------------------------------------------------------------------------ init(#{tick_ms := TickMs}) -> - _ = emqx_tables:new(?TAB, [set, public, {write_concurrency, true}]), + ok = emqx_tables:new(?TAB, [public, set, {write_concurrency, true}]), Stats = lists:append([?CONNECTION_STATS, ?SESSION_STATS, ?PUBSUB_STATS, ?ROUTE_STATS, ?RETAINED_STATS]), true = ets:insert(?TAB, [{Name, 0} || Name <- Stats]), diff --git a/src/emqx_tables.erl b/src/emqx_tables.erl index 330c87d9c..9b3ebfeae 100644 --- a/src/emqx_tables.erl +++ b/src/emqx_tables.erl @@ -17,10 +17,12 @@ -export([new/2]). %% Create a named_table ets. +-spec(new(atom(), list()) -> ok). new(Tab, Opts) -> case ets:info(Tab, name) of undefined -> - ets:new(Tab, lists:usort([named_table | Opts])); - Tab -> Tab + _ = ets:new(Tab, lists:usort([named_table | Opts])), + ok; + Tab -> ok end. diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl index dd183dbdf..d119abe52 100644 --- a/src/emqx_zone.erl +++ b/src/emqx_zone.erl @@ -68,7 +68,7 @@ stop() -> %%------------------------------------------------------------------------------ init([]) -> - _ = emqx_tables:new(?TAB, [set, {read_concurrency, true}]), + ok = emqx_tables:new(?TAB, [set, {read_concurrency, true}]), {ok, element(2, handle_info(reload, #{timer => undefined}))}. handle_call(force_reload, _From, State) -> From 5e53eaeee5b65d2d1ec0c0721d5cdddc311527f4 Mon Sep 17 00:00:00 2001 From: turtled Date: Sat, 8 Dec 2018 09:56:00 +0800 Subject: [PATCH 454/520] rename shard shared --- src/emqx_broker.erl | 29 ++++++++++++++++------------- src/emqx_broker_helper.erl | 12 ++++++------ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 0a9264489..380e16c42 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -71,7 +71,7 @@ create_tabs() -> %% Subscriber: Topic -> SubPid1, SubPid2, SubPid3, ... %% duplicate_bag: o(1) insert - ok = emqx_tables:new(?SUBSCRIBER, [duplicate_bag | TabOpts]). + ok = emqx_tables:new(?SUBSCRIBER, [bag | TabOpts]). %%------------------------------------------------------------------------------ %% Subscribe API @@ -97,15 +97,16 @@ subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map true = ets:insert(?SUBSCRIPTION, {SubPid, Topic}), case maps:get(share, SubOpts, undefined) of undefined -> - Shard = emqx_broker_helper:get_shard(SubPid, Topic), - case Shard of + Shared = emqx_broker_helper:get_shared(SubPid, Topic), + case Shared of 0 -> true = ets:insert(?SUBSCRIBER, {Topic, SubPid}); - I -> true = ets:insert(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), - true = ets:insert(?SUBSCRIBER, {Topic, {shard, I}}) + I -> + true = ets:insert(?SUBSCRIBER, {{shared, Topic, I}, SubPid}), + true = ets:insert(?SUBSCRIBER, {Topic, {shared, I}}) end, - SubOpts1 = maps:put(shard, Shard, SubOpts), + SubOpts1 = maps:put(shared, Shared, SubOpts), true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts1}), - call(pick({Topic, Shard}), {subscribe, Topic}); + call(pick({Topic, Shared}), {subscribe, Topic}); Group -> %% Shared subscription true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), emqx_shared_sub:subscribe(Group, Topic, SubPid) @@ -128,7 +129,7 @@ unsubscribe(Topic) when is_binary(Topic) -> case maps:get(shared, SubOpts, 0) of 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), ok = cast(pick(Topic), {unsubscribed, Topic}); - I -> true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + I -> true = ets:delete_object(?SUBSCRIBER, {{shared, Topic, I}, SubPid}), ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) end; Group -> @@ -239,10 +240,11 @@ dispatch(Topic, Delivery = #delivery{message = Msg, results = Results}) -> dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> SubPid ! {dispatch, Topic, Msg}; -dispatch({shard, I}, Topic, Msg) -> +dispatch({shared, I}, Topic, Msg) -> + lists:foreach(fun(SubPid) -> SubPid ! {dispatch, Topic, Msg} - end, safe_lookup_element(?SUBSCRIBER, {share, Topic, I}, [])). + end, safe_lookup_element(?SUBSCRIBER, {shared, Topic, I}, [])). inc_dropped_cnt(<<"$SYS/", _/binary>>) -> ok; @@ -267,7 +269,8 @@ subscriber_down(SubPid) -> case maps:get(shared, SubOpts, 0) of 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), ok = cast(pick(Topic), {unsubscribed, Topic}); - I -> true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + I -> true = ets:delete_object(?SUBSCRIBER, {Topic, {shared, I}}), + true = ets:delete_object(?SUBSCRIBER, {{shared, Topic, I}, SubPid}), ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) end; [] -> ok @@ -373,9 +376,9 @@ handle_cast({unsubscribed, Topic}, State) -> {noreply, State}; handle_cast({unsubscribed, Topic, I}, State) -> - case ets:member(?SUBSCRIBER, {shard, Topic, I}) of + case ets:member(?SUBSCRIBER, {shared, Topic, I}) of false -> - true = ets:delete_object(?SUBSCRIBER, {Topic, {shard, I}}), + true = ets:delete_object(?SUBSCRIBER, {Topic, {shared, I}}), cast(pick(Topic), {unsubscribed, Topic}); true -> ok end, diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index d3e7f9d37..6830b4d32 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -20,7 +20,7 @@ -export([start_link/0]). -export([monitor/2]). --export([get_shard/2]). +-export([get_shared/2]). -export([create_seq/1, reclaim_seq/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, @@ -47,11 +47,11 @@ monitor(SubPid, SubId) when is_pid(SubPid) -> error(subid_conflict) end. --spec(get_shard(pid(), emqx_topic:topic()) -> non_neg_integer()). -get_shard(SubPid, Topic) -> +-spec(get_shared(pid(), emqx_topic:topic()) -> non_neg_integer()). +get_shared(SubPid, Topic) -> case create_seq(Topic) of Seq when Seq =< 1024 -> 0; - _Seq -> erlang:phash2(SubPid, ets:lookup_element(?SUBSEQ, shards, 2)) + _Seq -> erlang:phash2(SubPid, ets:lookup_element(?SUBSEQ, shareds, 2)) end. -spec(create_seq(emqx_topic:topic()) -> emqx_sequence:seqid()). @@ -69,8 +69,8 @@ reclaim_seq(Topic) -> init([]) -> %% SubSeq: Topic -> SeqId ok = emqx_sequence:create(?SUBSEQ), - %% Shards: CPU * 32 - true = ets:insert(?SUBSEQ, {shards, emqx_vm:schedulers() * 32}), + %% Shareds: CPU * 32 + true = ets:insert(?SUBSEQ, {shareds, emqx_vm:schedulers() * 32}), %% SubMon: SubPid -> SubId ok = emqx_tables:new(?SUBMON, [set, protected, {read_concurrency, true}]), %% Stats timer From ba897e51f9f27de998ba638c69d65c33b8578ce1 Mon Sep 17 00:00:00 2001 From: turtled Date: Sat, 8 Dec 2018 10:26:50 +0800 Subject: [PATCH 455/520] Subscriber down clear emqx_suboption table --- src/emqx_broker.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 380e16c42..5f3e63059 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -272,7 +272,8 @@ subscriber_down(SubPid) -> I -> true = ets:delete_object(?SUBSCRIBER, {Topic, {shared, I}}), true = ets:delete_object(?SUBSCRIBER, {{shared, Topic, I}, SubPid}), ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) - end; + end, + ets:delete(?SUBOPTION, Sub); [] -> ok end end, ets:lookup(?SUBSCRIPTION, SubPid)), From d1be51d398afdf46133baeff0b13320b92721e93 Mon Sep 17 00:00:00 2001 From: turtled Date: Sat, 8 Dec 2018 10:52:15 +0800 Subject: [PATCH 456/520] Format code --- src/emqx_broker.erl | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 5f3e63059..b9332ffed 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -127,10 +127,16 @@ unsubscribe(Topic) when is_binary(Topic) -> case maps:get(share, SubOpts, undefined) of undefined -> case maps:get(shared, SubOpts, 0) of - 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), - ok = cast(pick(Topic), {unsubscribed, Topic}); - I -> true = ets:delete_object(?SUBSCRIBER, {{shared, Topic, I}, SubPid}), - ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) + 0 -> + true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), + ok = cast(pick(Topic), {unsubscribed, Topic}); + I -> + true = ets:delete_object(?SUBSCRIBER, {{shared, Topic, I}, SubPid}), + case ets:member(emqx_subscriber, {shared, Topic, I}) of + true -> ok; + false -> ets:delete_object(?SUBSCRIBER, {Topic, {shared, I}}) + end, + ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) end; Group -> ok = emqx_shared_sub:unsubscribe(Group, Topic, SubPid) @@ -267,11 +273,16 @@ subscriber_down(SubPid) -> [{_, SubOpts}] -> _ = emqx_broker_helper:reclaim_seq(Topic), case maps:get(shared, SubOpts, 0) of - 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), - ok = cast(pick(Topic), {unsubscribed, Topic}); - I -> true = ets:delete_object(?SUBSCRIBER, {Topic, {shared, I}}), - true = ets:delete_object(?SUBSCRIBER, {{shared, Topic, I}, SubPid}), - ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) + 0 -> + true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), + ok = cast(pick(Topic), {unsubscribed, Topic}); + I -> + true = ets:delete_object(?SUBSCRIBER, {{shared, Topic, I}, SubPid}), + case ets:member(emqx_subscriber, {shared, Topic, I}) of + true -> ok; + false -> ets:delete_object(?SUBSCRIBER, {Topic, {shared, I}}) + end, + ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) end, ets:delete(?SUBOPTION, Sub); [] -> ok From 5164d0d6a57e3be187d3d5dfee1b605dbd4f688f Mon Sep 17 00:00:00 2001 From: turtled Date: Sat, 8 Dec 2018 11:40:08 +0800 Subject: [PATCH 457/520] Fix unsubscribe fail and rename shared -> shard --- src/emqx_broker.erl | 43 +++++++++++++++++++------------------- src/emqx_broker_helper.erl | 12 +++++------ 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index b9332ffed..6556a59e5 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -97,19 +97,19 @@ subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map true = ets:insert(?SUBSCRIPTION, {SubPid, Topic}), case maps:get(share, SubOpts, undefined) of undefined -> - Shared = emqx_broker_helper:get_shared(SubPid, Topic), - case Shared of + Shard = emqx_broker_helper:get_shard(SubPid, Topic), + case Shard of 0 -> true = ets:insert(?SUBSCRIBER, {Topic, SubPid}); I -> - true = ets:insert(?SUBSCRIBER, {{shared, Topic, I}, SubPid}), - true = ets:insert(?SUBSCRIBER, {Topic, {shared, I}}) + true = ets:insert(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + true = ets:insert(?SUBSCRIBER, {Topic, {shard, I}}) end, - SubOpts1 = maps:put(shared, Shared, SubOpts), + SubOpts1 = maps:put(shard, Shard, SubOpts), true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts1}), - call(pick({Topic, Shared}), {subscribe, Topic}); - Group -> %% Shared subscription + call(pick({Topic, Shard}), {subscribe, Topic}); + Group -> %% Shard subscription true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), - emqx_shared_sub:subscribe(Group, Topic, SubPid) + emqx_shard_sub:subscribe(Group, Topic, SubPid) end; true -> ok end. @@ -126,15 +126,15 @@ unsubscribe(Topic) when is_binary(Topic) -> _ = emqx_broker_helper:reclaim_seq(Topic), case maps:get(share, SubOpts, undefined) of undefined -> - case maps:get(shared, SubOpts, 0) of + case maps:get(shard, SubOpts, 0) of 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), ok = cast(pick(Topic), {unsubscribed, Topic}); I -> - true = ets:delete_object(?SUBSCRIBER, {{shared, Topic, I}, SubPid}), - case ets:member(emqx_subscriber, {shared, Topic, I}) of + true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + case ets:member(emqx_subscriber, {shard, Topic, I}) of true -> ok; - false -> ets:delete_object(?SUBSCRIBER, {Topic, {shared, I}}) + false -> ets:delete_object(?SUBSCRIBER, {Topic, {shard, I}}) end, ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) end; @@ -143,7 +143,8 @@ unsubscribe(Topic) when is_binary(Topic) -> end, true = ets:delete_object(?SUBSCRIPTION, {SubPid, Topic}), %%true = ets:delete_object(?SUBID, {SubId, SubPid}), - true = ets:delete(?SUBOPTION, {SubPid, Topic}); + true = ets:delete(?SUBOPTION, {SubPid, Topic}), + ok; [] -> ok end. @@ -246,11 +247,11 @@ dispatch(Topic, Delivery = #delivery{message = Msg, results = Results}) -> dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> SubPid ! {dispatch, Topic, Msg}; -dispatch({shared, I}, Topic, Msg) -> +dispatch({shard, I}, Topic, Msg) -> lists:foreach(fun(SubPid) -> SubPid ! {dispatch, Topic, Msg} - end, safe_lookup_element(?SUBSCRIBER, {shared, Topic, I}, [])). + end, safe_lookup_element(?SUBSCRIBER, {shard, Topic, I}, [])). inc_dropped_cnt(<<"$SYS/", _/binary>>) -> ok; @@ -272,15 +273,15 @@ subscriber_down(SubPid) -> case ets:lookup(?SUBOPTION, Sub) of [{_, SubOpts}] -> _ = emqx_broker_helper:reclaim_seq(Topic), - case maps:get(shared, SubOpts, 0) of + case maps:get(shard, SubOpts, 0) of 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), ok = cast(pick(Topic), {unsubscribed, Topic}); I -> - true = ets:delete_object(?SUBSCRIBER, {{shared, Topic, I}, SubPid}), - case ets:member(emqx_subscriber, {shared, Topic, I}) of + true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + case ets:member(emqx_subscriber, {shard, Topic, I}) of true -> ok; - false -> ets:delete_object(?SUBSCRIBER, {Topic, {shared, I}}) + false -> ets:delete_object(?SUBSCRIBER, {Topic, {shard, I}}) end, ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) end, @@ -388,9 +389,9 @@ handle_cast({unsubscribed, Topic}, State) -> {noreply, State}; handle_cast({unsubscribed, Topic, I}, State) -> - case ets:member(?SUBSCRIBER, {shared, Topic, I}) of + case ets:member(?SUBSCRIBER, {shard, Topic, I}) of false -> - true = ets:delete_object(?SUBSCRIBER, {Topic, {shared, I}}), + true = ets:delete_object(?SUBSCRIBER, {Topic, {shard, I}}), cast(pick(Topic), {unsubscribed, Topic}); true -> ok end, diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index 6830b4d32..d3e7f9d37 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -20,7 +20,7 @@ -export([start_link/0]). -export([monitor/2]). --export([get_shared/2]). +-export([get_shard/2]). -export([create_seq/1, reclaim_seq/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, @@ -47,11 +47,11 @@ monitor(SubPid, SubId) when is_pid(SubPid) -> error(subid_conflict) end. --spec(get_shared(pid(), emqx_topic:topic()) -> non_neg_integer()). -get_shared(SubPid, Topic) -> +-spec(get_shard(pid(), emqx_topic:topic()) -> non_neg_integer()). +get_shard(SubPid, Topic) -> case create_seq(Topic) of Seq when Seq =< 1024 -> 0; - _Seq -> erlang:phash2(SubPid, ets:lookup_element(?SUBSEQ, shareds, 2)) + _Seq -> erlang:phash2(SubPid, ets:lookup_element(?SUBSEQ, shards, 2)) end. -spec(create_seq(emqx_topic:topic()) -> emqx_sequence:seqid()). @@ -69,8 +69,8 @@ reclaim_seq(Topic) -> init([]) -> %% SubSeq: Topic -> SeqId ok = emqx_sequence:create(?SUBSEQ), - %% Shareds: CPU * 32 - true = ets:insert(?SUBSEQ, {shareds, emqx_vm:schedulers() * 32}), + %% Shards: CPU * 32 + true = ets:insert(?SUBSEQ, {shards, emqx_vm:schedulers() * 32}), %% SubMon: SubPid -> SubId ok = emqx_tables:new(?SUBMON, [set, protected, {read_concurrency, true}]), %% Stats timer From b6c123b173ec2bb88830c3f9f1045b9bf44d7bf3 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Fri, 30 Nov 2018 10:22:18 +0100 Subject: [PATCH 458/520] Use git tag for app vsn --- Makefile | 1 - erlang.mk | 2 +- src/emqx.app.src | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index e815e0756..695cc6871 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,6 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker -PROJECT_VERSION = 3.0.0 DEPS = jsx gproc gen_rpc ekka esockd cowboy clique diff --git a/erlang.mk b/erlang.mk index f38d22653..c5d4b4f7f 100644 --- a/erlang.mk +++ b/erlang.mk @@ -1186,7 +1186,7 @@ else fi $(appsrc_verbose) cat src/$(PROJECT).app.src \ | sed "s/{[[:space:]]*modules[[:space:]]*,[[:space:]]*\[\]}/{modules, \[$(call comma_list,$(MODULES))\]}/" \ - | sed "s/{id,[[:space:]]*\"git\"}/{id, \"$(subst /,\/,$(GITDESCRIBE))\"}/" \ + | sed "s/{vsn,[[:space:]]*\"git\"}/{vsn, \"$(subst /,\/,$(GITDESCRIBE))\"}/" \ > ebin/$(PROJECT).app endif diff --git a/src/emqx.app.src b/src/emqx.app.src index 1bca0118b..ce643634e 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -1,6 +1,6 @@ {application,emqx, [{description,"EMQ X Broker"}, - {vsn,"3.0.0"}, + {vsn,"git"}, {modules,[]}, {registered,[emqx_sup]}, {applications,[kernel,stdlib,jsx,gproc,gen_rpc,esockd, From 10288827d1c4292308b226c2abcff14e65e40396 Mon Sep 17 00:00:00 2001 From: spring2maz Date: Fri, 30 Nov 2018 11:02:02 +0100 Subject: [PATCH 459/520] Fallback to git clone -n then checkout if git version is older than 1.8 --- Makefile | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 695cc6871..16435bdab 100644 --- a/Makefile +++ b/Makefile @@ -47,17 +47,19 @@ PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets pu DIALYZER_DIRS := ebin/ DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wunmatched_returns -GIT_VSN = $(shell git --version | grep -oE "[0-9]{1,2}\.[0-9]{1,2}") -GIT_VSN_17_COMP = $(shell echo -e "$(GIT_VSN)\n1.7" | sort -V | tail -1) -ifeq ($(GIT_VSN_17_COMP),1.7) - MAYBE_SHALLOW = -else - MAYBE_SHALLOW = -c advice.detachedHead=false --depth 1 -endif +GIT_VSN := $(shell git --version | grep -oE "[0-9]{1,2}\.[0-9]{1,2}") +GIT_VSN_17_COMP := $(shell echo -e "$(GIT_VSN)\n1.7" | sort -V | tail -1) +ifeq ($(GIT_VSN_17_COMP),1.7) define dep_fetch_git-emqx - git clone $(MAYBE_SHALLOW) -q -b $(call dep_commit,$(1)) -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)) > /dev/null 2>&1 + git clone -q -n -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); \ + cd $(DEPS_DIR)/$(call dep_name,$(1)) && git checkout -q $(call dep_commit,$(1)) endef +else +define dep_fetch_git-emqx + git clone -q -c advice.detachedHead=false --depth 1 -b $(call dep_commit,$(1)) -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)) +endef +endif core_http_get-emqx = curl -Lf$(if $(filter-out 0,$(V)),,s)o $(call core_native_path,$1) $2 From 0b70896456867787a12fa5bd897bdc9b15f44ad1 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 30 Nov 2018 19:16:48 +0800 Subject: [PATCH 460/520] Fix the coverage shaky (#2010) --- src/emqx_broker.erl | 5 +++++ test/emqx_broker_SUITE.erl | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index b7e0e457e..ca4a86c87 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -33,6 +33,11 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-ifdef(TEST). +-compile(export_all). +-compile(nowarn_export_all). +-endif. + -record(state, {pool, id, submap, submon}). -record(subscribe, {topic, subpid, subid, subopts = #{}}). -record(unsubscribe, {topic, subpid, subid}). diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index 3187aafa4..b9bfb4257 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -38,6 +38,7 @@ groups() -> publish, pubsub, t_local_subscribe, t_shared_subscribe, + dispatch_with_no_sub, 'pubsub#', 'pubsub+']}, {session, [sequence], [start_session]}, {metrics, [sequence], [inc_dec_metric]}, @@ -76,6 +77,11 @@ publish(_) -> emqx:publish(Msg), ?assert(receive {dispatch, <<"test/+">>, Msg} -> true after 5 -> false end). +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)). + pubsub(_) -> true = emqx:is_running(node()), Self = self(), @@ -193,4 +199,3 @@ set_alarms(_) -> ?assertEqual(1, length(Alarms)), emqx_alarm_mgr:clear_alarm(<<"1">>), [] = emqx_alarm_mgr:get_alarms(). - From 3712d0c90fe251cea0b7bb1e173152e49c741f9f Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sun, 2 Dec 2018 16:15:45 +0800 Subject: [PATCH 461/520] Add eunit tests to increase coverage. --- test/emqx_reason_codes_tests.erl | 130 +++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 test/emqx_reason_codes_tests.erl diff --git a/test/emqx_reason_codes_tests.erl b/test/emqx_reason_codes_tests.erl new file mode 100644 index 000000000..29f7ad081 --- /dev/null +++ b/test/emqx_reason_codes_tests.erl @@ -0,0 +1,130 @@ +%% Copyright (c) 2018 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_reason_codes_tests). + +-include_lib("eunit/include/eunit.hrl"). + +-include("emqx_mqtt.hrl"). + +-import(lists, [seq/2, zip/2, foreach/2]). + +-define(MQTTV4_CODE_NAMES, [connection_acceptd, + unacceptable_protocol_version, + client_identifier_not_valid, + server_unavaliable, + malformed_username_or_password, + unauthorized_client, + unknown_error]). + +-define(MQTTV5_CODE_NAMES, [success, granted_qos1, granted_qos2, disconnect_with_will_message, + no_matching_subscribers, no_subscription_existed, continue_authentication, + re_authenticate, unspecified_error, malformed_Packet, protocol_error, + implementation_specific_error, unsupported_protocol_version, + client_identifier_not_valid, bad_username_or_password, not_authorized, + server_unavailable, server_busy, banned,server_shutting_down, + bad_authentication_method, keepalive_timeout, session_taken_over, + topic_filter_invalid, topic_name_invalid, packet_identifier_inuse, + packet_identifier_not_found, receive_maximum_exceeded, topic_alias_invalid, + packet_too_large, message_rate_too_high, quota_exceeded, + administrative_action, payload_format_invalid, retain_not_supported, + qos_not_supported, use_another_server, server_moved, + shared_subscriptions_not_supported, connection_rate_exceeded, + maximum_connect_time, subscription_identifiers_not_supported, + wildcard_subscriptions_not_supported, unknown_error]). + +-define(MQTTV5_CODES, [16#00, 16#01, 16#02, 16#04, 16#10, 16#11, 16#18, 16#19, 16#80, 16#81, 16#82, + 16#83, 16#84, 16#85, 16#86, 16#87, 16#88, 16#89, 16#8A, 16#8B, 16#8C, 16#8D, + 16#8E, 16#8F, 16#90, 16#91, 16#92, 16#93, 16#94, 16#95, 16#96, 16#97, 16#98, + 16#99, 16#9A, 16#9B, 16#9C, 16#9D, 16#9E, 16#9F, 16#A0, 16#A1, 16#A2, code]). + +-define(MQTTV5_TXT, [<<"Success">>, <<"Granted QoS 1">>, <<"Granted QoS 2">>, + <<"Disconnect with Will Message">>, <<"No matching subscribers">>, + <<"No subscription existed">>, <<"Continue authentication">>, + <<"Re-authenticate">>, <<"Unspecified error">>, <<"Malformed Packet">>, + <<"Protocol Error">>, <<"Implementation specific error">>, + <<"Unsupported Protocol Version">>, <<"Client Identifier not valid">>, + <<"Bad User Name or Password">>, <<"Not authorized">>, + <<"Server unavailable">>, <<"Server busy">>, <<"Banned">>, + <<"Server shutting down">>, <<"Bad authentication method">>, + <<"Keep Alive timeout">>, <<"Session taken over">>, + <<"Topic Filter invalid">>, <<"Topic Name invalid">>, + <<"Packet Identifier in use">>, <<"Packet Identifier not found">>, + <<"Receive Maximum exceeded">>, <<"Topic Alias invalid">>, + <<"Packet too large">>, <<"Message rate too high">>, <<"Quota exceeded">>, + <<"Administrative action">>, <<"Payload format invalid">>, + <<"Retain not supported">>, <<"QoS not supported">>, + <<"Use another server">>, <<"Server moved">>, + <<"Shared Subscriptions not supported">>, <<"Connection rate exceeded">>, + <<"Maximum connect time">>, <<"Subscription Identifiers not supported">>, + <<"Wildcard Subscriptions not supported">>, <<"Unknown error">>]). + +-define(COMPAT_CODES_V5, [16#80, 16#81, 16#82, 16#83, 16#84, 16#85, 16#86, 16#87, + 16#88, 16#89, 16#8A, 16#8B, 16#8C, 16#97, 16#9C, 16#9D, + 16#9F]). + +-define(COMPAT_CODES_V4, [?CONNACK_PROTO_VER, ?CONNACK_PROTO_VER, ?CONNACK_PROTO_VER, + ?CONNACK_PROTO_VER, ?CONNACK_PROTO_VER, + ?CONNACK_INVALID_ID, + ?CONNACK_CREDENTIALS, + ?CONNACK_AUTH, + ?CONNACK_SERVER, + ?CONNACK_SERVER, + ?CONNACK_AUTH, + ?CONNACK_SERVER, + ?CONNACK_AUTH, + ?CONNACK_SERVER, ?CONNACK_SERVER, ?CONNACK_SERVER, ?CONNACK_SERVER]). + +mqttv4_name_test() -> + (((codes_test(?MQTT_PROTO_V4)) + (seq(0,6))) + (?MQTTV4_CODE_NAMES)) + (fun emqx_reason_codes:name/2). + +mqttv5_name_test() -> + (((codes_test(?MQTT_PROTO_V5)) + (?MQTTV5_CODES)) + (?MQTTV5_CODE_NAMES)) + (fun emqx_reason_codes:name/2). + +text_test() -> + (((codes_test(?MQTT_PROTO_V5)) + (?MQTTV5_CODES)) + (?MQTTV5_TXT)) + (fun emqx_reason_codes:text/1). + +compat_test() -> + (((codes_test(connack)) + (?COMPAT_CODES_V5)) + (?COMPAT_CODES_V4)) + (fun emqx_reason_codes:compat/2), + (((codes_test(suback)) + ([0,1,2, 16#80])) + ([0,1,2, 16#80])) + (fun emqx_reason_codes:compat/2). + +codes_test(AsistVar) -> + fun(CODES) -> + fun(NAMES) -> + fun(Procedure) -> + foreach(fun({Code, Result}) -> + ?assertEqual(Result, case erlang:fun_info(Procedure, name) of + {name, text} -> Procedure(Code); + {name, name} -> Procedure(Code, AsistVar); + {name, compat} -> Procedure(AsistVar, Code) + end) + end, zip(CODES, NAMES)) + end + end + end. From f008ceb5c82b3a6948dd0f585e8af1492bfd1de2 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 30 Nov 2018 20:37:10 +0800 Subject: [PATCH 462/520] Optimize the route and trie modules. 1. Use mnesia:wread/1 to replace mnesia:read/2 2. Update the router supervisor --- src/emqx_router.erl | 15 ++++++++------- src/emqx_router_helper.erl | 4 ++-- src/emqx_router_sup.erl | 8 ++++++-- src/emqx_trie.erl | 14 +++++++------- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 2ac656d56..941c004f7 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -34,7 +34,8 @@ -export([has_routes/1, match_routes/1, print_routes/1]). -export([topics/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -type(destination() :: node() | {binary(), node()}). @@ -45,9 +46,9 @@ -define(BATCH(Enabled), #batch{enabled = Enabled}). -define(BATCH(Enabled, Pending), #batch{enabled = Enabled, pending = Pending}). -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Mnesia bootstrap -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ mnesia(boot) -> ok = ekka_mnesia:create_table(?ROUTE, [ @@ -132,9 +133,9 @@ cast(Router, Msg) -> pick(Topic) -> gproc_pool:pick_worker(router, Topic). -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([Pool, Id]) -> rand:seed(exsplus, erlang:timestamp()), @@ -207,9 +208,9 @@ terminate(_Reason, #state{pool = Pool, id = Id, batch = Batch}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ ensure_batch_timer(State = #state{batch = #batch{enabled = false}}) -> State; diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index 7bacddd4c..c24b10715 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -84,8 +84,8 @@ monitor(Node) when is_atom(Node) -> %%------------------------------------------------------------------------------ init([]) -> - ekka:monitor(membership), - mnesia:subscribe({table, ?ROUTING_NODE, simple}), + _ = ekka:monitor(membership), + _ = mnesia:subscribe({table, ?ROUTING_NODE, simple}), Nodes = lists:foldl( fun(Node, Acc) -> case ekka:is_member(Node) of diff --git a/src/emqx_router_sup.erl b/src/emqx_router_sup.erl index 004b88bb8..2bbaabc18 100644 --- a/src/emqx_router_sup.erl +++ b/src/emqx_router_sup.erl @@ -24,8 +24,12 @@ start_link() -> init([]) -> %% Router helper - Helper = {router_helper, {emqx_router_helper, start_link, []}, - permanent, 5000, worker, [emqx_router_helper]}, + Helper = #{id => helper, + start => {emqx_router_helper, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_router_helper]}, %% Router pool RouterPool = emqx_pool_sup:spec(emqx_router_pool, diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index 569de9092..79f6042b7 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -62,10 +62,10 @@ mnesia(copy) -> %% Trie APIs %%------------------------------------------------------------------------------ -%% @doc Insert a topic into the trie +%% @doc Insert a topic filter into the trie. -spec(insert(emqx_topic:topic()) -> ok). insert(Topic) when is_binary(Topic) -> - case mnesia:read(?TRIE_NODE, Topic) of + case mnesia:wread({?TRIE_NODE, Topic}) of [#trie_node{topic = Topic}] -> ok; [TrieNode = #trie_node{topic = undefined}] -> @@ -77,21 +77,21 @@ insert(Topic) when is_binary(Topic) -> write_trie_node(#trie_node{node_id = Topic, topic = Topic}) end. -%% @doc Find trie nodes that match the topic +%% @doc Find trie nodes that match the topic name. -spec(match(emqx_topic:topic()) -> list(emqx_topic:topic())). match(Topic) when is_binary(Topic) -> TrieNodes = match_node(root, emqx_topic:words(Topic)), [Name || #trie_node{topic = Name} <- TrieNodes, Name =/= undefined]. -%% @doc Lookup a trie node +%% @doc Lookup a trie node. -spec(lookup(NodeId :: binary()) -> [#trie_node{}]). lookup(NodeId) -> mnesia:read(?TRIE_NODE, NodeId). -%% @doc Delete a topic from the trie +%% @doc Delete a topic filter from the trie. -spec(delete(emqx_topic:topic()) -> ok). delete(Topic) when is_binary(Topic) -> - case mnesia:read(?TRIE_NODE, Topic) of + case mnesia:wread({?TRIE_NODE, Topic}) of [#trie_node{edge_count = 0}] -> mnesia:delete({?TRIE_NODE, Topic}), delete_path(lists:reverse(emqx_topic:triples(Topic))); @@ -108,7 +108,7 @@ delete(Topic) when is_binary(Topic) -> %% @doc Add a path to the trie. add_path({Node, Word, Child}) -> Edge = #trie_edge{node_id = Node, word = Word}, - case mnesia:read(?TRIE_NODE, Node) of + case mnesia:wread({?TRIE_NODE, Node}) of [TrieNode = #trie_node{edge_count = Count}] -> case mnesia:wread({?TRIE, Edge}) of [] -> From a32b043980f85f030010db863ce6ef28a47fb80f Mon Sep 17 00:00:00 2001 From: spring2maz Date: Sat, 1 Dec 2018 19:50:17 +0100 Subject: [PATCH 463/520] Download erlang.mk and use git tag in appfile vsn --- .gitignore | 1 + Makefile | 24 +- erlang.mk | 2741 ---------------------------------------------------- 3 files changed, 2 insertions(+), 2764 deletions(-) delete mode 100644 erlang.mk diff --git a/.gitignore b/.gitignore index 80e3bf42d..7a4e891d1 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ compile_commands.json cuttlefish rebar.lock xrefr +erlang.mk diff --git a/Makefile b/Makefile index 16435bdab..26bcf22ce 100644 --- a/Makefile +++ b/Makefile @@ -47,29 +47,7 @@ PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets pu DIALYZER_DIRS := ebin/ DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wunmatched_returns -GIT_VSN := $(shell git --version | grep -oE "[0-9]{1,2}\.[0-9]{1,2}") -GIT_VSN_17_COMP := $(shell echo -e "$(GIT_VSN)\n1.7" | sort -V | tail -1) - -ifeq ($(GIT_VSN_17_COMP),1.7) -define dep_fetch_git-emqx - git clone -q -n -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); \ - cd $(DEPS_DIR)/$(call dep_name,$(1)) && git checkout -q $(call dep_commit,$(1)) -endef -else -define dep_fetch_git-emqx - git clone -q -c advice.detachedHead=false --depth 1 -b $(call dep_commit,$(1)) -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)) -endef -endif - -core_http_get-emqx = curl -Lf$(if $(filter-out 0,$(V)),,s)o $(call core_native_path,$1) $2 - -define dep_fetch_hex-emqx - mkdir -p $(ERLANG_MK_TMP)/hex $(DEPS_DIR)/$1; \ - $(call core_http_get-emqx,$(ERLANG_MK_TMP)/hex/$1.tar,\ - https://repo.hex.pm/tarballs/$1-$(strip $(word 2,$(dep_$1))).tar); \ - tar -xOf $(ERLANG_MK_TMP)/hex/$1.tar contents.tar.gz | tar -C $(DEPS_DIR)/$1 -xzf -; -endef - +$(shell [ -f erlang.mk ] || curl -s -o erlang.mk https://raw.githubusercontent.com/emqx/erlmk/master/erlang.mk) include erlang.mk clean:: gen-clean diff --git a/erlang.mk b/erlang.mk deleted file mode 100644 index c5d4b4f7f..000000000 --- a/erlang.mk +++ /dev/null @@ -1,2741 +0,0 @@ -# Copyright (c) 2013-2015, Loïc Hoguin -# -# Permission to use, copy, modify, and/or distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -.PHONY: all app apps deps search rel docs install-docs check tests clean distclean help erlang-mk - -ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST))) - -ERLANG_MK_VERSION = 2.0.0-pre.2-130-gc6fe5ea - -# Core configuration. - -PROJECT ?= $(notdir $(CURDIR)) -PROJECT := $(strip $(PROJECT)) - -PROJECT_VERSION ?= rolling -PROJECT_MOD ?= $(PROJECT)_app - -# Verbosity. - -V ?= 0 - -verbose_0 = @ -verbose_2 = set -x; -verbose = $(verbose_$(V)) - -gen_verbose_0 = @echo " GEN " $@; -gen_verbose_2 = set -x; -gen_verbose = $(gen_verbose_$(V)) - -# Temporary files directory. - -ERLANG_MK_TMP ?= $(CURDIR)/.erlang.mk -export ERLANG_MK_TMP - -# "erl" command. - -ERL = erl +A0 -noinput -boot start_clean - -# Platform detection. - -ifeq ($(PLATFORM),) -UNAME_S := $(shell uname -s) - -ifeq ($(UNAME_S),Linux) -PLATFORM = linux -else ifeq ($(UNAME_S),Darwin) -PLATFORM = darwin -else ifeq ($(UNAME_S),SunOS) -PLATFORM = solaris -else ifeq ($(UNAME_S),GNU) -PLATFORM = gnu -else ifeq ($(UNAME_S),FreeBSD) -PLATFORM = freebsd -else ifeq ($(UNAME_S),NetBSD) -PLATFORM = netbsd -else ifeq ($(UNAME_S),OpenBSD) -PLATFORM = openbsd -else ifeq ($(UNAME_S),DragonFly) -PLATFORM = dragonfly -else ifeq ($(shell uname -o),Msys) -PLATFORM = msys2 -else -$(error Unable to detect platform. Please open a ticket with the output of uname -a.) -endif - -export PLATFORM -endif - -# Core targets. - -all:: deps app rel - -# Noop to avoid a Make warning when there's nothing to do. -rel:: - $(verbose) : - -check:: tests - -clean:: clean-crashdump - -clean-crashdump: -ifneq ($(wildcard erl_crash.dump),) - $(gen_verbose) rm -f erl_crash.dump -endif - -distclean:: clean distclean-tmp - -distclean-tmp: - $(gen_verbose) rm -rf $(ERLANG_MK_TMP) - -help:: - $(verbose) printf "%s\n" \ - "erlang.mk (version $(ERLANG_MK_VERSION)) is distributed under the terms of the ISC License." \ - "Copyright (c) 2013-2015 Loïc Hoguin " \ - "" \ - "Usage: [V=1] $(MAKE) [target]..." \ - "" \ - "Core targets:" \ - " all Run deps, app and rel targets in that order" \ - " app Compile the project" \ - " deps Fetch dependencies (if needed) and compile them" \ - " search q=... Search for a package in the built-in index" \ - " rel Build a release for this project, if applicable" \ - " docs Build the documentation for this project" \ - " install-docs Install the man pages for this project" \ - " check Compile and run all tests and analysis for this project" \ - " tests Run the tests for this project" \ - " clean Delete temporary and output files from most targets" \ - " distclean Delete all temporary and output files" \ - " help Display this help and exit" \ - " erlang-mk Update erlang.mk to the latest version" - -# Core functions. - -empty := -space := $(empty) $(empty) -tab := $(empty) $(empty) -comma := , - -define newline - - -endef - -define comma_list -$(subst $(space),$(comma),$(strip $(1))) -endef - -# Adding erlang.mk to make Erlang scripts who call init:get_plain_arguments() happy. -define erlang -$(ERL) $(2) -pz $(ERLANG_MK_TMP)/rebar/ebin -eval "$(subst $(newline),,$(subst ",\",$(1)))" -- erlang.mk -endef - -ifeq ($(PLATFORM),msys2) -core_native_path = $(subst \,\\\\,$(shell cygpath -w $1)) -else -core_native_path = $1 -endif - -ifeq ($(shell which wget 2>/dev/null | wc -l), 1) -define core_http_get - wget --no-check-certificate -O $(1) $(2)|| rm $(1) -endef -else -define core_http_get.erl - ssl:start(), - inets:start(), - case httpc:request(get, {"$(2)", []}, [{autoredirect, true}], []) of - {ok, {{_, 200, _}, _, Body}} -> - case file:write_file("$(1)", Body) of - ok -> ok; - {error, R1} -> halt(R1) - end; - {error, R2} -> - halt(R2) - end, - halt(0). -endef - -define core_http_get - $(call erlang,$(call core_http_get.erl,$(call core_native_path,$1),$2)) -endef -endif - -core_eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))) - -core_find = $(if $(wildcard $1),$(shell find $(1:%/=%) -type f -name $(subst *,\*,$2))) - -core_lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(1))))))))))))))))))))))))))) - -core_ls = $(filter-out $(1),$(shell echo $(1))) - -# @todo Use a solution that does not require using perl. -core_relpath = $(shell perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' $1 $2) - -# Automated update. - -ERLANG_MK_REPO ?= https://github.com/ninenines/erlang.mk -ERLANG_MK_COMMIT ?= -ERLANG_MK_BUILD_CONFIG ?= build.config -ERLANG_MK_BUILD_DIR ?= .erlang.mk.build - -erlang-mk: - git clone $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR) -ifdef ERLANG_MK_COMMIT - cd $(ERLANG_MK_BUILD_DIR) && git checkout $(ERLANG_MK_COMMIT) -endif - if [ -f $(ERLANG_MK_BUILD_CONFIG) ]; then cp $(ERLANG_MK_BUILD_CONFIG) $(ERLANG_MK_BUILD_DIR)/build.config; fi - $(MAKE) -C $(ERLANG_MK_BUILD_DIR) - cp $(ERLANG_MK_BUILD_DIR)/erlang.mk ./erlang.mk - rm -rf $(ERLANG_MK_BUILD_DIR) - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: search - -define pkg_print - $(verbose) printf "%s\n" \ - $(if $(call core_eq,$(1),$(pkg_$(1)_name)),,"Pkg name: $(1)") \ - "App name: $(pkg_$(1)_name)" \ - "Description: $(pkg_$(1)_description)" \ - "Home page: $(pkg_$(1)_homepage)" \ - "Fetch with: $(pkg_$(1)_fetch)" \ - "Repository: $(pkg_$(1)_repo)" \ - "Commit: $(pkg_$(1)_commit)" \ - "" - -endef - -search: -ifdef q - $(foreach p,$(PACKAGES), \ - $(if $(findstring $(call core_lc,$(q)),$(call core_lc,$(pkg_$(p)_name) $(pkg_$(p)_description))), \ - $(call pkg_print,$(p)))) -else - $(foreach p,$(PACKAGES),$(call pkg_print,$(p))) -endif - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: distclean-deps - -# Configuration. - -ifdef OTP_DEPS -$(warning The variable OTP_DEPS is deprecated in favor of LOCAL_DEPS.) -endif - -IGNORE_DEPS ?= -export IGNORE_DEPS - -APPS_DIR ?= $(CURDIR)/apps -export APPS_DIR - -DEPS_DIR ?= $(CURDIR)/deps -export DEPS_DIR - -REBAR_DEPS_DIR = $(DEPS_DIR) -export REBAR_DEPS_DIR - -dep_name = $(if $(dep_$(1)),$(1),$(if $(pkg_$(1)_name),$(pkg_$(1)_name),$(1))) -dep_repo = $(patsubst git://github.com/%,https://github.com/%, \ - $(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_repo))) -dep_commit = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 3,$(dep_$(1))),$(pkg_$(1)_commit))) - -ALL_APPS_DIRS = $(if $(wildcard $(APPS_DIR)/),$(filter-out $(APPS_DIR),$(shell find $(APPS_DIR) -maxdepth 1 -type d))) -ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(BUILD_DEPS) $(DEPS)),$(call dep_name,$(dep)))) - -ifeq ($(filter $(APPS_DIR) $(DEPS_DIR),$(subst :, ,$(ERL_LIBS))),) -ifeq ($(ERL_LIBS),) - ERL_LIBS = $(APPS_DIR):$(DEPS_DIR) -else - ERL_LIBS := $(ERL_LIBS):$(APPS_DIR):$(DEPS_DIR) -endif -endif -export ERL_LIBS - -export NO_AUTOPATCH - -# Verbosity. - -dep_verbose_0 = @echo " DEP " $(1); -dep_verbose_2 = set -x; -dep_verbose = $(dep_verbose_$(V)) - -# Core targets. - -ifdef IS_APP -apps:: -else -apps:: $(ALL_APPS_DIRS) -ifeq ($(IS_APP)$(IS_DEP),) - $(verbose) rm -f $(ERLANG_MK_TMP)/apps.log -endif - $(verbose) mkdir -p $(ERLANG_MK_TMP) -# Create ebin directory for all apps to make sure Erlang recognizes them -# as proper OTP applications when using -include_lib. This is a temporary -# fix, a proper fix would be to compile apps/* in the right order. - $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ - mkdir -p $$dep/ebin || exit $$?; \ - done - $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ - if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/apps.log; then \ - :; \ - else \ - echo $$dep >> $(ERLANG_MK_TMP)/apps.log; \ - $(MAKE) -C $$dep IS_APP=1 || exit $$?; \ - fi \ - done -endif - -ifneq ($(SKIP_DEPS),) -deps:: -else -deps:: $(ALL_DEPS_DIRS) apps -ifeq ($(IS_APP)$(IS_DEP),) - $(verbose) rm -f $(ERLANG_MK_TMP)/deps.log -endif - $(verbose) mkdir -p $(ERLANG_MK_TMP) - $(verbose) for dep in $(ALL_DEPS_DIRS) ; do \ - if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/deps.log; then \ - :; \ - else \ - echo $$dep >> $(ERLANG_MK_TMP)/deps.log; \ - if [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ]; then \ - $(MAKE) -C $$dep IS_DEP=1 || exit $$?; \ - else \ - echo "Error: No Makefile to build dependency $$dep."; \ - exit 2; \ - fi \ - fi \ - done -endif - -# Deps related targets. - -# @todo rename GNUmakefile and makefile into Makefile first, if they exist -# While Makefile file could be GNUmakefile or makefile, -# in practice only Makefile is needed so far. -define dep_autopatch - if [ -f $(DEPS_DIR)/$(1)/erlang.mk ]; then \ - $(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \ - $(call dep_autopatch_erlang_mk,$(1)); \ - elif [ -f $(DEPS_DIR)/$(1)/Makefile ]; then \ - if [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$(1)/Makefile` ]; then \ - $(call dep_autopatch2,$(1)); \ - elif [ 0 != `grep -ci rebar $(DEPS_DIR)/$(1)/Makefile` ]; then \ - $(call dep_autopatch2,$(1)); \ - elif [ -n "`find $(DEPS_DIR)/$(1)/ -type f -name \*.mk -not -name erlang.mk -exec grep -i rebar '{}' \;`" ]; then \ - $(call dep_autopatch2,$(1)); \ - else \ - $(call erlang,$(call dep_autopatch_app.erl,$(1))); \ - fi \ - else \ - if [ ! -d $(DEPS_DIR)/$(1)/src/ ]; then \ - $(call dep_autopatch_noop,$(1)); \ - else \ - $(call dep_autopatch2,$(1)); \ - fi \ - fi -endef - -define dep_autopatch2 - if [ -f $(DEPS_DIR)/$1/src/$1.app.src.script ]; then \ - $(call erlang,$(call dep_autopatch_appsrc_script.erl,$(1))); \ - fi; \ - $(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \ - if [ -f $(DEPS_DIR)/$(1)/rebar -o -f $(DEPS_DIR)/$(1)/rebar.config -o -f $(DEPS_DIR)/$(1)/rebar.config.script ]; then \ - $(call dep_autopatch_fetch_rebar); \ - $(call dep_autopatch_rebar,$(1)); \ - else \ - $(call dep_autopatch_gen,$(1)); \ - fi -endef - -define dep_autopatch_noop - printf "noop:\n" > $(DEPS_DIR)/$(1)/Makefile -endef - -# Overwrite erlang.mk with the current file by default. -ifeq ($(NO_AUTOPATCH_ERLANG_MK),) -define dep_autopatch_erlang_mk - echo "include $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(DEPS_DIR)/app)/erlang.mk" \ - > $(DEPS_DIR)/$1/erlang.mk -endef -else -define dep_autopatch_erlang_mk - : -endef -endif - -define dep_autopatch_gen - printf "%s\n" \ - "ERLC_OPTS = +debug_info" \ - "include ../../erlang.mk" > $(DEPS_DIR)/$(1)/Makefile -endef - -define dep_autopatch_fetch_rebar - mkdir -p $(ERLANG_MK_TMP); \ - if [ ! -d $(ERLANG_MK_TMP)/rebar ]; then \ - git clone -q -n -- https://github.com/rebar/rebar $(ERLANG_MK_TMP)/rebar; \ - cd $(ERLANG_MK_TMP)/rebar; \ - git checkout -q 791db716b5a3a7671e0b351f95ddf24b848ee173; \ - $(MAKE); \ - cd -; \ - fi -endef - -define dep_autopatch_rebar - if [ -f $(DEPS_DIR)/$(1)/Makefile ]; then \ - mv $(DEPS_DIR)/$(1)/Makefile $(DEPS_DIR)/$(1)/Makefile.orig.mk; \ - fi; \ - $(call erlang,$(call dep_autopatch_rebar.erl,$(1))); \ - rm -f $(DEPS_DIR)/$(1)/ebin/$(1).app -endef - -define dep_autopatch_rebar.erl - application:load(rebar), - application:set_env(rebar, log_level, debug), - Conf1 = case file:consult("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config)") of - {ok, Conf0} -> Conf0; - _ -> [] - end, - {Conf, OsEnv} = fun() -> - case filelib:is_file("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config.script)") of - false -> {Conf1, []}; - true -> - Bindings0 = erl_eval:new_bindings(), - Bindings1 = erl_eval:add_binding('CONFIG', Conf1, Bindings0), - Bindings = erl_eval:add_binding('SCRIPT', "$(call core_native_path,$(DEPS_DIR)/$1/rebar.config.script)", Bindings1), - Before = os:getenv(), - {ok, Conf2} = file:script("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config.script)", Bindings), - {Conf2, lists:foldl(fun(E, Acc) -> lists:delete(E, Acc) end, os:getenv(), Before)} - end - end(), - Write = fun (Text) -> - file:write_file("$(call core_native_path,$(DEPS_DIR)/$1/Makefile)", Text, [append]) - end, - Escape = fun (Text) -> - re:replace(Text, "\\\\$$", "\$$$$", [global, {return, list}]) - end, - Write("IGNORE_DEPS += edown eper eunit_formatters meck node_package " - "rebar_lock_deps_plugin rebar_vsn_plugin reltool_util\n"), - Write("C_SRC_DIR = /path/do/not/exist\n"), - Write("C_SRC_TYPE = rebar\n"), - Write("DRV_CFLAGS = -fPIC\nexport DRV_CFLAGS\n"), - Write(["ERLANG_ARCH = ", rebar_utils:wordsize(), "\nexport ERLANG_ARCH\n"]), - fun() -> - Write("ERLC_OPTS = +debug_info\nexport ERLC_OPTS\n"), - case lists:keyfind(erl_opts, 1, Conf) of - false -> ok; - {_, ErlOpts} -> - lists:foreach(fun - ({d, D}) -> - Write("ERLC_OPTS += -D" ++ atom_to_list(D) ++ "=1\n"); - ({i, I}) -> - Write(["ERLC_OPTS += -I ", I, "\n"]); - ({platform_define, Regex, D}) -> - case rebar_utils:is_arch(Regex) of - true -> Write("ERLC_OPTS += -D" ++ atom_to_list(D) ++ "=1\n"); - false -> ok - end; - ({parse_transform, PT}) -> - Write("ERLC_OPTS += +'{parse_transform, " ++ atom_to_list(PT) ++ "}'\n"); - (_) -> ok - end, ErlOpts) - end, - Write("\n") - end(), - fun() -> - File = case lists:keyfind(deps, 1, Conf) of - false -> []; - {_, Deps} -> - [begin case case Dep of - {N, S} when is_atom(N), is_list(S) -> {N, {hex, S}}; - {N, S} when is_tuple(S) -> {N, S}; - {N, _, S} -> {N, S}; - {N, _, S, _} -> {N, S}; - _ -> false - end of - false -> ok; - {Name, Source} -> - {Method, Repo, Commit} = case Source of - {hex, V} -> {hex, V, undefined}; - {git, R} -> {git, R, master}; - {M, R, {branch, C}} -> {M, R, C}; - {M, R, {ref, C}} -> {M, R, C}; - {M, R, {tag, C}} -> {M, R, C}; - {M, R, C} -> {M, R, C} - end, - Write(io_lib:format("DEPS += ~s\ndep_~s = ~s ~s ~s~n", [Name, Name, Method, Repo, Commit])) - end end || Dep <- Deps] - end - end(), - fun() -> - case lists:keyfind(erl_first_files, 1, Conf) of - false -> ok; - {_, Files} -> - Names = [[" ", case lists:reverse(F) of - "lre." ++ Elif -> lists:reverse(Elif); - Elif -> lists:reverse(Elif) - end] || "src/" ++ F <- Files], - Write(io_lib:format("COMPILE_FIRST +=~s\n", [Names])) - end - end(), - Write("\n\nrebar_dep: preprocess pre-deps deps pre-app app\n"), - Write("\npreprocess::\n"), - Write("\npre-deps::\n"), - Write("\npre-app::\n"), - PatchHook = fun(Cmd) -> - case Cmd of - "make -C" ++ Cmd1 -> "$$\(MAKE) -C" ++ Escape(Cmd1); - "gmake -C" ++ Cmd1 -> "$$\(MAKE) -C" ++ Escape(Cmd1); - "make " ++ Cmd1 -> "$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1); - "gmake " ++ Cmd1 -> "$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1); - _ -> Escape(Cmd) - end - end, - fun() -> - case lists:keyfind(pre_hooks, 1, Conf) of - false -> ok; - {_, Hooks} -> - [case H of - {'get-deps', Cmd} -> - Write("\npre-deps::\n\t" ++ PatchHook(Cmd) ++ "\n"); - {compile, Cmd} -> - Write("\npre-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); - {Regex, compile, Cmd} -> - case rebar_utils:is_arch(Regex) of - true -> Write("\npre-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); - false -> ok - end; - _ -> ok - end || H <- Hooks] - end - end(), - ShellToMk = fun(V) -> - re:replace(re:replace(V, "(\\\\$$)(\\\\w*)", "\\\\1(\\\\2)", [global]), - "-Werror\\\\b", "", [{return, list}, global]) - end, - PortSpecs = fun() -> - case lists:keyfind(port_specs, 1, Conf) of - false -> - case filelib:is_dir("$(call core_native_path,$(DEPS_DIR)/$1/c_src)") of - false -> []; - true -> - [{"priv/" ++ proplists:get_value(so_name, Conf, "$(1)_drv.so"), - proplists:get_value(port_sources, Conf, ["c_src/*.c"]), []}] - end; - {_, Specs} -> - lists:flatten([case S of - {Output, Input} -> {ShellToMk(Output), Input, []}; - {Regex, Output, Input} -> - case rebar_utils:is_arch(Regex) of - true -> {ShellToMk(Output), Input, []}; - false -> [] - end; - {Regex, Output, Input, [{env, Env}]} -> - case rebar_utils:is_arch(Regex) of - true -> {ShellToMk(Output), Input, Env}; - false -> [] - end - end || S <- Specs]) - end - end(), - PortSpecWrite = fun (Text) -> - file:write_file("$(call core_native_path,$(DEPS_DIR)/$1/c_src/Makefile.erlang.mk)", Text, [append]) - end, - case PortSpecs of - [] -> ok; - _ -> - Write("\npre-app::\n\t$$\(MAKE) -f c_src/Makefile.erlang.mk\n"), - PortSpecWrite(io_lib:format("ERL_CFLAGS = -finline-functions -Wall -fPIC -I \\"~s/erts-~s/include\\" -I \\"~s\\"\n", - [code:root_dir(), erlang:system_info(version), code:lib_dir(erl_interface, include)])), - PortSpecWrite(io_lib:format("ERL_LDFLAGS = -L \\"~s\\" -lerl_interface -lei\n", - [code:lib_dir(erl_interface, lib)])), - [PortSpecWrite(["\n", E, "\n"]) || E <- OsEnv], - FilterEnv = fun(Env) -> - lists:flatten([case E of - {_, _} -> E; - {Regex, K, V} -> - case rebar_utils:is_arch(Regex) of - true -> {K, V}; - false -> [] - end - end || E <- Env]) - end, - MergeEnv = fun(Env) -> - lists:foldl(fun ({K, V}, Acc) -> - case lists:keyfind(K, 1, Acc) of - false -> [{K, rebar_utils:expand_env_variable(V, K, "")}|Acc]; - {_, V0} -> [{K, rebar_utils:expand_env_variable(V, K, V0)}|Acc] - end - end, [], Env) - end, - PortEnv = case lists:keyfind(port_env, 1, Conf) of - false -> []; - {_, PortEnv0} -> FilterEnv(PortEnv0) - end, - PortSpec = fun ({Output, Input0, Env}) -> - filelib:ensure_dir("$(call core_native_path,$(DEPS_DIR)/$1/)" ++ Output), - Input = [[" ", I] || I <- Input0], - PortSpecWrite([ - [["\n", K, " = ", ShellToMk(V)] || {K, V} <- lists:reverse(MergeEnv(PortEnv))], - case $(PLATFORM) of - darwin -> "\n\nLDFLAGS += -flat_namespace -undefined suppress"; - _ -> "" - end, - "\n\nall:: ", Output, "\n\n", - "%.o: %.c\n\t$$\(CC) -c -o $$\@ $$\< $$\(CFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", - "%.o: %.C\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", - "%.o: %.cc\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", - "%.o: %.cpp\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", - [[Output, ": ", K, " = ", ShellToMk(V), "\n"] || {K, V} <- lists:reverse(MergeEnv(FilterEnv(Env)))], - Output, ": $$\(foreach ext,.c .C .cc .cpp,", - "$$\(patsubst %$$\(ext),%.o,$$\(filter %$$\(ext),$$\(wildcard", Input, "))))\n", - "\t$$\(CC) -o $$\@ $$\? $$\(LDFLAGS) $$\(ERL_LDFLAGS) $$\(DRV_LDFLAGS) $$\(EXE_LDFLAGS)", - case {filename:extension(Output), $(PLATFORM)} of - {[], _} -> "\n"; - {_, darwin} -> "\n"; - _ -> " -shared\n" - end]) - end, - [PortSpec(S) || S <- PortSpecs] - end, - Write("\ninclude $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(DEPS_DIR)/app)/erlang.mk"), - RunPlugin = fun(Plugin, Step) -> - case erlang:function_exported(Plugin, Step, 2) of - false -> ok; - true -> - c:cd("$(call core_native_path,$(DEPS_DIR)/$1/)"), - Ret = Plugin:Step({config, "", Conf, dict:new(), dict:new(), dict:new(), - dict:store(base_dir, "", dict:new())}, undefined), - io:format("rebar plugin ~p step ~p ret ~p~n", [Plugin, Step, Ret]) - end - end, - fun() -> - case lists:keyfind(plugins, 1, Conf) of - false -> ok; - {_, Plugins} -> - [begin - case lists:keyfind(deps, 1, Conf) of - false -> ok; - {_, Deps} -> - case lists:keyfind(P, 1, Deps) of - false -> ok; - _ -> - Path = "$(call core_native_path,$(DEPS_DIR)/)" ++ atom_to_list(P), - io:format("~s", [os:cmd("$(MAKE) -C $(call core_native_path,$(DEPS_DIR)/$1) " ++ Path)]), - io:format("~s", [os:cmd("$(MAKE) -C " ++ Path ++ " IS_DEP=1")]), - code:add_patha(Path ++ "/ebin") - end - end - end || P <- Plugins], - [case code:load_file(P) of - {module, P} -> ok; - _ -> - case lists:keyfind(plugin_dir, 1, Conf) of - false -> ok; - {_, PluginsDir} -> - ErlFile = "$(call core_native_path,$(DEPS_DIR)/$1/)" ++ PluginsDir ++ "/" ++ atom_to_list(P) ++ ".erl", - {ok, P, Bin} = compile:file(ErlFile, [binary]), - {module, P} = code:load_binary(P, ErlFile, Bin) - end - end || P <- Plugins], - [RunPlugin(P, preprocess) || P <- Plugins], - [RunPlugin(P, pre_compile) || P <- Plugins], - [RunPlugin(P, compile) || P <- Plugins] - end - end(), - halt() -endef - -define dep_autopatch_app.erl - UpdateModules = fun(App) -> - case filelib:is_regular(App) of - false -> ok; - true -> - {ok, [{application, '$(1)', L0}]} = file:consult(App), - Mods = filelib:fold_files("$(call core_native_path,$(DEPS_DIR)/$1/src)", "\\\\.erl$$", true, - fun (F, Acc) -> [list_to_atom(filename:rootname(filename:basename(F)))|Acc] end, []), - L = lists:keystore(modules, 1, L0, {modules, Mods}), - ok = file:write_file(App, io_lib:format("~p.~n", [{application, '$(1)', L}])) - end - end, - UpdateModules("$(call core_native_path,$(DEPS_DIR)/$1/ebin/$1.app)"), - halt() -endef - -define dep_autopatch_appsrc_script.erl - AppSrc = "$(call core_native_path,$(DEPS_DIR)/$1/src/$1.app.src)", - AppSrcScript = AppSrc ++ ".script", - Bindings = erl_eval:new_bindings(), - {ok, Conf} = file:script(AppSrcScript, Bindings), - ok = file:write_file(AppSrc, io_lib:format("~p.~n", [Conf])), - halt() -endef - -define dep_autopatch_appsrc.erl - AppSrcOut = "$(call core_native_path,$(DEPS_DIR)/$1/src/$1.app.src)", - AppSrcIn = case filelib:is_regular(AppSrcOut) of false -> "$(call core_native_path,$(DEPS_DIR)/$1/ebin/$1.app)"; true -> AppSrcOut end, - case filelib:is_regular(AppSrcIn) of - false -> ok; - true -> - {ok, [{application, $(1), L0}]} = file:consult(AppSrcIn), - L1 = lists:keystore(modules, 1, L0, {modules, []}), - L2 = case lists:keyfind(vsn, 1, L1) of {_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, "git"}); _ -> L1 end, - L3 = case lists:keyfind(registered, 1, L2) of false -> [{registered, []}|L2]; _ -> L2 end, - ok = file:write_file(AppSrcOut, io_lib:format("~p.~n", [{application, $(1), L3}])), - case AppSrcOut of AppSrcIn -> ok; _ -> ok = file:delete(AppSrcIn) end - end, - halt() -endef - -define dep_fetch_git - git clone -q -n -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); \ - cd $(DEPS_DIR)/$(call dep_name,$(1)) && git checkout -q $(call dep_commit,$(1)); -endef - -define dep_fetch_git-submodule - git submodule update --init -- $(DEPS_DIR)/$1; -endef - -define dep_fetch_hg - hg clone -q -U $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); \ - cd $(DEPS_DIR)/$(call dep_name,$(1)) && hg update -q $(call dep_commit,$(1)); -endef - -define dep_fetch_svn - svn checkout -q $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); -endef - -define dep_fetch_cp - cp -R $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); -endef - -define dep_fetch_hex.erl - ssl:start(), - inets:start(), - {ok, {{_, 200, _}, _, Body}} = httpc:request(get, - {"https://s3.amazonaws.com/s3.hex.pm/tarballs/$(1)-$(2).tar", []}, - [], [{body_format, binary}]), - {ok, Files} = erl_tar:extract({binary, Body}, [memory]), - {_, Source} = lists:keyfind("contents.tar.gz", 1, Files), - ok = erl_tar:extract({binary, Source}, [{cwd, "$(call core_native_path,$(DEPS_DIR)/$1)"}, compressed]), - halt() -endef - -# Hex only has a package version. No need to look in the Erlang.mk packages. -define dep_fetch_hex - $(call erlang,$(call dep_fetch_hex.erl,$(1),$(strip $(word 2,$(dep_$(1)))))); -endef - -define dep_fetch_fail - echo "Error: Unknown or invalid dependency: $(1)." >&2; \ - exit 78; -endef - -# Kept for compatibility purposes with older Erlang.mk configuration. -define dep_fetch_legacy - $(warning WARNING: '$(1)' dependency configuration uses deprecated format.) \ - git clone -q -n -- $(word 1,$(dep_$(1))) $(DEPS_DIR)/$(1); \ - cd $(DEPS_DIR)/$(1) && git checkout -q $(if $(word 2,$(dep_$(1))),$(word 2,$(dep_$(1))),master); -endef - -define dep_fetch - $(if $(dep_$(1)), \ - $(if $(dep_fetch_$(word 1,$(dep_$(1)))), \ - $(word 1,$(dep_$(1))), \ - $(if $(IS_DEP),legacy,fail)), \ - $(if $(filter $(1),$(PACKAGES)), \ - $(pkg_$(1)_fetch), \ - fail)) -endef - -define dep_target -$(DEPS_DIR)/$(call dep_name,$1): - $(eval DEP_NAME := $(call dep_name,$1)) - $(eval DEP_STR := $(if $(filter-out $1,$(DEP_NAME)),$1,"$1 ($(DEP_NAME))")) - $(verbose) if test -d $(APPS_DIR)/$(DEP_NAME); then \ - echo "Error: Dependency" $(DEP_STR) "conflicts with application found in $(APPS_DIR)/$(DEP_NAME)."; \ - exit 17; \ - fi - $(verbose) mkdir -p $(DEPS_DIR) - $(dep_verbose) $(call dep_fetch_$(strip $(call dep_fetch,$(1))),$(1)) - $(verbose) if [ -f $(DEPS_DIR)/$(1)/configure.ac -o -f $(DEPS_DIR)/$(1)/configure.in ] \ - && [ ! -f $(DEPS_DIR)/$(1)/configure ]; then \ - echo " AUTO " $(1); \ - cd $(DEPS_DIR)/$(1) && autoreconf -Wall -vif -I m4; \ - fi - - $(verbose) if [ -f $(DEPS_DIR)/$(DEP_NAME)/configure ]; then \ - echo " CONF " $(DEP_STR); \ - cd $(DEPS_DIR)/$(DEP_NAME) && ./configure; \ - fi -ifeq ($(filter $(1),$(NO_AUTOPATCH)),) - $(verbose) if [ "$(1)" = "amqp_client" -a "$(RABBITMQ_CLIENT_PATCH)" ]; then \ - if [ ! -d $(DEPS_DIR)/rabbitmq-codegen ]; then \ - echo " PATCH Downloading rabbitmq-codegen"; \ - git clone https://github.com/rabbitmq/rabbitmq-codegen.git $(DEPS_DIR)/rabbitmq-codegen; \ - fi; \ - if [ ! -d $(DEPS_DIR)/rabbitmq-server ]; then \ - echo " PATCH Downloading rabbitmq-server"; \ - git clone https://github.com/rabbitmq/rabbitmq-server.git $(DEPS_DIR)/rabbitmq-server; \ - fi; \ - ln -s $(DEPS_DIR)/amqp_client/deps/rabbit_common-0.0.0 $(DEPS_DIR)/rabbit_common; \ - elif [ "$(1)" = "rabbit" -a "$(RABBITMQ_SERVER_PATCH)" ]; then \ - if [ ! -d $(DEPS_DIR)/rabbitmq-codegen ]; then \ - echo " PATCH Downloading rabbitmq-codegen"; \ - git clone https://github.com/rabbitmq/rabbitmq-codegen.git $(DEPS_DIR)/rabbitmq-codegen; \ - fi \ - else \ - $$(call dep_autopatch,$(DEP_NAME)) \ - fi -endif -endef - -$(foreach dep,$(BUILD_DEPS) $(DEPS),$(eval $(call dep_target,$(dep)))) - -ifndef IS_APP -clean:: clean-apps - -clean-apps: - $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ - $(MAKE) -C $$dep clean IS_APP=1 || exit $$?; \ - done - -distclean:: distclean-apps - -distclean-apps: - $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ - $(MAKE) -C $$dep distclean IS_APP=1 || exit $$?; \ - done -endif - -ifndef SKIP_DEPS -distclean:: distclean-deps - -distclean-deps: - $(gen_verbose) rm -rf $(DEPS_DIR) -endif - -# External plugins. - -DEP_PLUGINS ?= - -define core_dep_plugin --include $(DEPS_DIR)/$(1) - -$(DEPS_DIR)/$(1): $(DEPS_DIR)/$(2) ; -endef - -$(foreach p,$(DEP_PLUGINS),\ - $(eval $(if $(findstring /,$p),\ - $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\ - $(call core_dep_plugin,$p/plugins.mk,$p)))) - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -# Configuration. - -DTL_FULL_PATH ?= -DTL_PATH ?= templates/ -DTL_SUFFIX ?= _dtl -DTL_OPTS ?= - -# Verbosity. - -dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); -dtl_verbose = $(dtl_verbose_$(V)) - -# Core targets. - -DTL_FILES = $(sort $(call core_find,$(DTL_PATH),*.dtl)) - -ifneq ($(DTL_FILES),) - -ifdef DTL_FULL_PATH -BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(subst /,_,$(DTL_FILES:$(DTL_PATH)%=%)))) -else -BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(notdir $(DTL_FILES)))) -endif - -# Rebuild templates when the Makefile changes. -$(DTL_FILES): $(MAKEFILE_LIST) - @touch $@ - -define erlydtl_compile.erl - [begin - Module0 = case "$(strip $(DTL_FULL_PATH))" of - "" -> - filename:basename(F, ".dtl"); - _ -> - "$(DTL_PATH)" ++ F2 = filename:rootname(F, ".dtl"), - re:replace(F2, "/", "_", [{return, list}, global]) - end, - Module = list_to_atom(string:to_lower(Module0) ++ "$(DTL_SUFFIX)"), - case erlydtl:compile(F, Module, [$(DTL_OPTS)] ++ [{out_dir, "ebin/"}, return_errors, {doc_root, "templates"}]) of - ok -> ok; - {ok, _} -> ok - end - end || F <- string:tokens("$(1)", " ")], - halt(). -endef - -ebin/$(PROJECT).app:: $(DTL_FILES) | ebin/ - $(if $(strip $?),\ - $(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$?),-pa ebin/ $(DEPS_DIR)/erlydtl/ebin/)) - -endif - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -# Verbosity. - -proto_verbose_0 = @echo " PROTO " $(filter %.proto,$(?F)); -proto_verbose = $(proto_verbose_$(V)) - -# Core targets. - -define compile_proto - $(verbose) mkdir -p ebin/ include/ - $(proto_verbose) $(call erlang,$(call compile_proto.erl,$(1))) - $(proto_verbose) erlc +debug_info -o ebin/ ebin/*.erl - $(verbose) rm ebin/*.erl -endef - -define compile_proto.erl - [begin - Dir = filename:dirname(filename:dirname(F)), - protobuffs_compile:generate_source(F, - [{output_include_dir, Dir ++ "/include"}, - {output_src_dir, Dir ++ "/ebin"}]) - end || F <- string:tokens("$(1)", " ")], - halt(). -endef - -ifneq ($(wildcard src/),) -ebin/$(PROJECT).app:: $(sort $(call core_find,src/,*.proto)) - $(if $(strip $?),$(call compile_proto,$?)) -endif - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: clean-app - -# Configuration. - -ERLC_OPTS ?= -Werror +debug_info +warn_export_vars +warn_shadow_vars \ - +warn_obsolete_guard # +bin_opt_info +warn_export_all +warn_missing_spec -COMPILE_FIRST ?= -COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST))) -ERLC_EXCLUDE ?= -ERLC_EXCLUDE_PATHS = $(addprefix src/,$(addsuffix .erl,$(ERLC_EXCLUDE))) - -ERLC_MIB_OPTS ?= -COMPILE_MIB_FIRST ?= -COMPILE_MIB_FIRST_PATHS = $(addprefix mibs/,$(addsuffix .mib,$(COMPILE_MIB_FIRST))) - -# Verbosity. - -app_verbose_0 = @echo " APP " $(PROJECT); -app_verbose_2 = set -x; -app_verbose = $(app_verbose_$(V)) - -appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src; -appsrc_verbose_2 = set -x; -appsrc_verbose = $(appsrc_verbose_$(V)) - -makedep_verbose_0 = @echo " DEPEND" $(PROJECT).d; -makedep_verbose_2 = set -x; -makedep_verbose = $(makedep_verbose_$(V)) - -erlc_verbose_0 = @echo " ERLC " $(filter-out $(patsubst %,%.erl,$(ERLC_EXCLUDE)),\ - $(filter %.erl %.core,$(?F))); -erlc_verbose_2 = set -x; -erlc_verbose = $(erlc_verbose_$(V)) - -xyrl_verbose_0 = @echo " XYRL " $(filter %.xrl %.yrl,$(?F)); -xyrl_verbose_2 = set -x; -xyrl_verbose = $(xyrl_verbose_$(V)) - -asn1_verbose_0 = @echo " ASN1 " $(filter %.asn1,$(?F)); -asn1_verbose_2 = set -x; -asn1_verbose = $(asn1_verbose_$(V)) - -mib_verbose_0 = @echo " MIB " $(filter %.bin %.mib,$(?F)); -mib_verbose_2 = set -x; -mib_verbose = $(mib_verbose_$(V)) - -ifneq ($(wildcard src/),) - -# Targets. - -ifeq ($(wildcard ebin/test),) -app:: deps $(PROJECT).d - $(verbose) $(MAKE) --no-print-directory app-build -else -app:: clean deps $(PROJECT).d - $(verbose) $(MAKE) --no-print-directory app-build -endif - -ifeq ($(wildcard src/$(PROJECT_MOD).erl),) -define app_file -{application, $(PROJECT), [ - {description, "$(PROJECT_DESCRIPTION)"}, - {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), - {id$(comma)$(space)"$(1)"}$(comma)) - {modules, [$(call comma_list,$(2))]}, - {registered, []}, - {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS))]} -]}. -endef -else -define app_file -{application, $(PROJECT), [ - {description, "$(PROJECT_DESCRIPTION)"}, - {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), - {id$(comma)$(space)"$(1)"}$(comma)) - {modules, [$(call comma_list,$(2))]}, - {registered, [$(call comma_list,$(PROJECT)_sup $(PROJECT_REGISTERED))]}, - {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS))]}, - {mod, {$(PROJECT_MOD), []}} -]}. -endef -endif - -app-build: ebin/$(PROJECT).app - $(verbose) : - -# Source files. - -ERL_FILES = $(sort $(call core_find,src/,*.erl)) -CORE_FILES = $(sort $(call core_find,src/,*.core)) - -# ASN.1 files. - -ifneq ($(wildcard asn1/),) -ASN1_FILES = $(sort $(call core_find,asn1/,*.asn1)) -ERL_FILES += $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES)))) - -define compile_asn1 - $(verbose) mkdir -p include/ - $(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(1) - $(verbose) mv asn1/*.erl src/ - $(verbose) mv asn1/*.hrl include/ - $(verbose) mv asn1/*.asn1db include/ -endef - -$(PROJECT).d:: $(ASN1_FILES) - $(if $(strip $?),$(call compile_asn1,$?)) -endif - -# SNMP MIB files. - -ifneq ($(wildcard mibs/),) -MIB_FILES = $(sort $(call core_find,mibs/,*.mib)) - -$(PROJECT).d:: $(COMPILE_MIB_FIRST_PATHS) $(MIB_FILES) - $(verbose) mkdir -p include/ priv/mibs/ - $(mib_verbose) erlc -v $(ERLC_MIB_OPTS) -o priv/mibs/ -I priv/mibs/ $? - $(mib_verbose) erlc -o include/ -- $(addprefix priv/mibs/,$(patsubst %.mib,%.bin,$(notdir $?))) -endif - -# Leex and Yecc files. - -XRL_FILES = $(sort $(call core_find,src/,*.xrl)) -XRL_ERL_FILES = $(addprefix src/,$(patsubst %.xrl,%.erl,$(notdir $(XRL_FILES)))) -ERL_FILES += $(XRL_ERL_FILES) - -YRL_FILES = $(sort $(call core_find,src/,*.yrl)) -YRL_ERL_FILES = $(addprefix src/,$(patsubst %.yrl,%.erl,$(notdir $(YRL_FILES)))) -ERL_FILES += $(YRL_ERL_FILES) - -$(PROJECT).d:: $(XRL_FILES) $(YRL_FILES) - $(if $(strip $?),$(xyrl_verbose) erlc -v -o src/ $?) - -# Erlang and Core Erlang files. - -define makedep.erl - E = ets:new(makedep, [bag]), - G = digraph:new([acyclic]), - ErlFiles = lists:usort(string:tokens("$(ERL_FILES)", " ")), - Modules = [{list_to_atom(filename:basename(F, ".erl")), F} || F <- ErlFiles], - Add = fun (Mod, Dep) -> - case lists:keyfind(Dep, 1, Modules) of - false -> ok; - {_, DepFile} -> - {_, ModFile} = lists:keyfind(Mod, 1, Modules), - ets:insert(E, {ModFile, DepFile}), - digraph:add_vertex(G, Mod), - digraph:add_vertex(G, Dep), - digraph:add_edge(G, Mod, Dep) - end - end, - AddHd = fun (F, Mod, DepFile) -> - case file:open(DepFile, [read]) of - {error, enoent} -> ok; - {ok, Fd} -> - F(F, Fd, Mod), - {_, ModFile} = lists:keyfind(Mod, 1, Modules), - ets:insert(E, {ModFile, DepFile}) - end - end, - Attr = fun - (F, Mod, behavior, Dep) -> Add(Mod, Dep); - (F, Mod, behaviour, Dep) -> Add(Mod, Dep); - (F, Mod, compile, {parse_transform, Dep}) -> Add(Mod, Dep); - (F, Mod, compile, Opts) when is_list(Opts) -> - case proplists:get_value(parse_transform, Opts) of - undefined -> ok; - Dep -> Add(Mod, Dep) - end; - (F, Mod, include, Hrl) -> - case filelib:is_file("include/" ++ Hrl) of - true -> AddHd(F, Mod, "include/" ++ Hrl); - false -> - case filelib:is_file("src/" ++ Hrl) of - true -> AddHd(F, Mod, "src/" ++ Hrl); - false -> false - end - end; - (F, Mod, include_lib, "$1/include/" ++ Hrl) -> AddHd(F, Mod, "include/" ++ Hrl); - (F, Mod, include_lib, Hrl) -> AddHd(F, Mod, "include/" ++ Hrl); - (F, Mod, import, {Imp, _}) -> - case filelib:is_file("src/" ++ atom_to_list(Imp) ++ ".erl") of - false -> ok; - true -> Add(Mod, Imp) - end; - (_, _, _, _) -> ok - end, - MakeDepend = fun(F, Fd, Mod) -> - case io:parse_erl_form(Fd, undefined) of - {ok, {attribute, _, Key, Value}, _} -> - Attr(F, Mod, Key, Value), - F(F, Fd, Mod); - {eof, _} -> - file:close(Fd); - _ -> - F(F, Fd, Mod) - end - end, - [begin - Mod = list_to_atom(filename:basename(F, ".erl")), - {ok, Fd} = file:open(F, [read]), - MakeDepend(MakeDepend, Fd, Mod) - end || F <- ErlFiles], - Depend = sofs:to_external(sofs:relation_to_family(sofs:relation(ets:tab2list(E)))), - CompileFirst = [X || X <- lists:reverse(digraph_utils:topsort(G)), [] =/= digraph:in_neighbours(G, X)], - ok = file:write_file("$(1)", [ - [[F, "::", [[" ", D] || D <- Deps], "; @touch \$$@\n"] || {F, Deps} <- Depend], - "\nCOMPILE_FIRST +=", [[" ", atom_to_list(CF)] || CF <- CompileFirst], "\n" - ]), - halt() -endef - -ifeq ($(if $(NO_MAKEDEP),$(wildcard $(PROJECT).d),),) -$(PROJECT).d:: $(ERL_FILES) $(call core_find,include/,*.hrl) - $(makedep_verbose) $(call erlang,$(call makedep.erl,$@)) -endif - -# Rebuild everything when the Makefile changes. -$(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(MAKEFILE_LIST) - @touch $@ - --include $(PROJECT).d - -ebin/$(PROJECT).app:: ebin/ - -ebin/: - $(verbose) mkdir -p ebin/ - -define compile_erl - $(erlc_verbose) erlc -v $(if $(IS_DEP),$(filter-out -Werror,$(ERLC_OPTS)),$(ERLC_OPTS)) -o ebin/ \ - -pa ebin/ -I include/ $(filter-out $(ERLC_EXCLUDE_PATHS),$(COMPILE_FIRST_PATHS) $(1)) -endef - -ebin/$(PROJECT).app:: $(ERL_FILES) $(CORE_FILES) $(wildcard src/$(PROJECT).app.src) - $(eval FILES_TO_COMPILE := $(filter-out src/$(PROJECT).app.src,$?)) - $(if $(strip $(FILES_TO_COMPILE)),$(call compile_erl,$(FILES_TO_COMPILE))) - $(eval GITDESCRIBE := $(shell git describe --dirty --abbrev=7 --tags --always --first-parent 2>/dev/null || true)) - $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \ - $(filter-out $(ERLC_EXCLUDE_PATHS),$(ERL_FILES) $(CORE_FILES) $(BEAM_FILES))))))) -ifeq ($(wildcard src/$(PROJECT).app.src),) - $(app_verbose) printf "$(subst $(newline),\n,$(subst ",\",$(call app_file,$(GITDESCRIBE),$(MODULES))))" \ - > ebin/$(PROJECT).app -else - $(verbose) if [ -z "$$(grep -e '^[^%]*{\s*modules\s*,' src/$(PROJECT).app.src)" ]; then \ - echo "Empty modules entry not found in $(PROJECT).app.src. Please consult the erlang.mk README for instructions." >&2; \ - exit 1; \ - fi - $(appsrc_verbose) cat src/$(PROJECT).app.src \ - | sed "s/{[[:space:]]*modules[[:space:]]*,[[:space:]]*\[\]}/{modules, \[$(call comma_list,$(MODULES))\]}/" \ - | sed "s/{vsn,[[:space:]]*\"git\"}/{vsn, \"$(subst /,\/,$(GITDESCRIBE))\"}/" \ - > ebin/$(PROJECT).app -endif - -clean:: clean-app - -clean-app: - $(gen_verbose) rm -rf $(PROJECT).d ebin/ priv/mibs/ $(XRL_ERL_FILES) $(YRL_ERL_FILES) \ - $(addprefix include/,$(patsubst %.mib,%.hrl,$(notdir $(MIB_FILES)))) \ - $(addprefix include/,$(patsubst %.asn1,%.hrl,$(notdir $(ASN1_FILES)))) \ - $(addprefix include/,$(patsubst %.asn1,%.asn1db,$(notdir $(ASN1_FILES)))) \ - $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES)))) - -endif - -# Copyright (c) 2015, Viktor Söderqvist -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: docs-deps - -# Configuration. - -ALL_DOC_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DOC_DEPS)) - -# Targets. - -$(foreach dep,$(DOC_DEPS),$(eval $(call dep_target,$(dep)))) - -ifneq ($(SKIP_DEPS),) -doc-deps: -else -doc-deps: $(ALL_DOC_DEPS_DIRS) - $(verbose) for dep in $(ALL_DOC_DEPS_DIRS) ; do $(MAKE) -C $$dep; done -endif - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: rel-deps - -# Configuration. - -ALL_REL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(REL_DEPS)) - -# Targets. - -$(foreach dep,$(REL_DEPS),$(eval $(call dep_target,$(dep)))) - -ifneq ($(SKIP_DEPS),) -rel-deps: -else -rel-deps: $(ALL_REL_DEPS_DIRS) - $(verbose) for dep in $(ALL_REL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done -endif - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: test-deps test-dir test-build clean-test-dir - -# Configuration. - -TEST_DIR ?= $(CURDIR)/test - -ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS)) - -TEST_ERLC_OPTS ?= +debug_info +warn_export_vars +warn_shadow_vars +warn_obsolete_guard -TEST_ERLC_OPTS += -DTEST=1 - -# Targets. - -$(foreach dep,$(TEST_DEPS),$(eval $(call dep_target,$(dep)))) - -ifneq ($(SKIP_DEPS),) -test-deps: -else -test-deps: $(ALL_TEST_DEPS_DIRS) - $(verbose) for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep IS_DEP=1; done -endif - -ifneq ($(wildcard $(TEST_DIR)),) -test-dir: - $(gen_verbose) erlc -v $(TEST_ERLC_OPTS) -I include/ -o $(TEST_DIR) \ - $(call core_find,$(TEST_DIR)/,*.erl) -pa ebin/ -endif - -ifeq ($(wildcard src),) -test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS) -test-build:: clean deps test-deps - $(verbose) $(MAKE) --no-print-directory test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" -else -ifeq ($(wildcard ebin/test),) -test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS) -test-build:: clean deps test-deps $(PROJECT).d - $(verbose) $(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" - $(gen_verbose) touch ebin/test -else -test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS) -test-build:: deps test-deps $(PROJECT).d - $(verbose) $(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" -endif - -clean:: clean-test-dir - -clean-test-dir: -ifneq ($(wildcard $(TEST_DIR)/*.beam),) - $(gen_verbose) rm -f $(TEST_DIR)/*.beam -endif -endif - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: rebar.config - -# We strip out -Werror because we don't want to fail due to -# warnings when used as a dependency. - -compat_prepare_erlc_opts = $(shell echo "$1" | sed 's/, */,/g') - -define compat_convert_erlc_opts -$(if $(filter-out -Werror,$1),\ - $(if $(findstring +,$1),\ - $(shell echo $1 | cut -b 2-))) -endef - -define compat_erlc_opts_to_list -[$(call comma_list,$(foreach o,$(call compat_prepare_erlc_opts,$1),$(call compat_convert_erlc_opts,$o)))] -endef - -define compat_rebar_config -{deps, [ -$(call comma_list,$(foreach d,$(DEPS),\ - $(if $(filter hex,$(call dep_fetch,$d)),\ - {$(call dep_name,$d)$(comma)"$(call dep_repo,$d)"},\ - {$(call dep_name,$d)$(comma)".*"$(comma){git,"$(call dep_repo,$d)"$(comma)"$(call dep_commit,$d)"}}))) -]}. -{erl_opts, $(call compat_erlc_opts_to_list,$(ERLC_OPTS))}. -endef - -$(eval _compat_rebar_config = $$(compat_rebar_config)) -$(eval export _compat_rebar_config) - -rebar.config: - $(gen_verbose) echo "$${_compat_rebar_config}" > rebar.config - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: asciidoc asciidoc-guide asciidoc-manual install-asciidoc distclean-asciidoc - -MAN_INSTALL_PATH ?= /usr/local/share/man -MAN_SECTIONS ?= 3 7 - -docs:: asciidoc - -asciidoc: asciidoc-guide asciidoc-manual - -ifeq ($(wildcard doc/src/guide/book.asciidoc),) -asciidoc-guide: -else -asciidoc-guide: distclean-asciidoc doc-deps - a2x -v -f pdf doc/src/guide/book.asciidoc && mv doc/src/guide/book.pdf doc/guide.pdf - a2x -v -f chunked doc/src/guide/book.asciidoc && mv doc/src/guide/book.chunked/ doc/html/ -endif - -ifeq ($(wildcard doc/src/manual/*.asciidoc),) -asciidoc-manual: -else -asciidoc-manual: distclean-asciidoc doc-deps - for f in doc/src/manual/*.asciidoc ; do \ - a2x -v -f manpage $$f ; \ - done - for s in $(MAN_SECTIONS); do \ - mkdir -p doc/man$$s/ ; \ - mv doc/src/manual/*.$$s doc/man$$s/ ; \ - gzip doc/man$$s/*.$$s ; \ - done - -install-docs:: install-asciidoc - -install-asciidoc: asciidoc-manual - for s in $(MAN_SECTIONS); do \ - mkdir -p $(MAN_INSTALL_PATH)/man$$s/ ; \ - install -g `id -u` -o `id -g` -m 0644 doc/man$$s/*.gz $(MAN_INSTALL_PATH)/man$$s/ ; \ - done -endif - -distclean:: distclean-asciidoc - -distclean-asciidoc: - $(gen_verbose) rm -rf doc/html/ doc/guide.pdf doc/man3/ doc/man7/ - -# Copyright (c) 2014-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: bootstrap bootstrap-lib bootstrap-rel new list-templates - -# Core targets. - -help:: - $(verbose) printf "%s\n" "" \ - "Bootstrap targets:" \ - " bootstrap Generate a skeleton of an OTP application" \ - " bootstrap-lib Generate a skeleton of an OTP library" \ - " bootstrap-rel Generate the files needed to build a release" \ - " new-app in=NAME Create a new local OTP application NAME" \ - " new-lib in=NAME Create a new local OTP library NAME" \ - " new t=TPL n=NAME Generate a module NAME based on the template TPL" \ - " new t=T n=N in=APP Generate a module NAME based on the template TPL in APP" \ - " list-templates List available templates" - -# Bootstrap templates. - -define bs_appsrc -{application, $p, [ - {description, ""}, - {vsn, "0.1.0"}, - {id, "git"}, - {modules, []}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, {$p_app, []}}, - {env, []} -]}. -endef - -define bs_appsrc_lib -{application, $p, [ - {description, ""}, - {vsn, "0.1.0"}, - {id, "git"}, - {modules, []}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]} -]}. -endef - -# To prevent autocompletion issues with ZSH, we add "include erlang.mk" -# separately during the actual bootstrap. -ifdef SP -define bs_Makefile -PROJECT = $p -PROJECT_DESCRIPTION = New project -PROJECT_VERSION = 0.0.1 - -# Whitespace to be used when creating files from templates. -SP = $(SP) - -endef -else -define bs_Makefile -PROJECT = $p -PROJECT_DESCRIPTION = New project -PROJECT_VERSION = 0.0.1 - -endef -endif - -define bs_apps_Makefile -PROJECT = $p -PROJECT_DESCRIPTION = New project -PROJECT_VERSION = 0.0.1 - -include $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(APPS_DIR)/app)/erlang.mk -endef - -define bs_app --module($p_app). --behaviour(application). - --export([start/2]). --export([stop/1]). - -start(_Type, _Args) -> - $p_sup:start_link(). - -stop(_State) -> - ok. -endef - -define bs_relx_config -{release, {$p_release, "1"}, [$p]}. -{extended_start_script, true}. -{sys_config, "rel/sys.config"}. -{vm_args, "rel/vm.args"}. -endef - -define bs_sys_config -[ -]. -endef - -define bs_vm_args --name $p@127.0.0.1 --setcookie $p --heart -endef - -# Normal templates. - -define tpl_supervisor --module($(n)). --behaviour(supervisor). - --export([start_link/0]). --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - Procs = [], - {ok, {{one_for_one, 1, 5}, Procs}}. -endef - -define tpl_gen_server --module($(n)). --behaviour(gen_server). - -%% API. --export([start_link/0]). - -%% gen_server. --export([init/1]). --export([handle_call/3]). --export([handle_cast/2]). --export([handle_info/2]). --export([terminate/2]). --export([code_change/3]). - --record(state, { -}). - -%% API. - --spec start_link() -> {ok, pid()}. -start_link() -> - gen_server:start_link(?MODULE, [], []). - -%% gen_server. - -init([]) -> - {ok, #state{}}. - -handle_call(_Request, _From, State) -> - {reply, ignored, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. -endef - -define tpl_module --module($(n)). --export([]). -endef - -define tpl_cowboy_http --module($(n)). --behaviour(cowboy_http_handler). - --export([init/3]). --export([handle/2]). --export([terminate/3]). - --record(state, { -}). - -init(_, Req, _Opts) -> - {ok, Req, #state{}}. - -handle(Req, State=#state{}) -> - {ok, Req2} = cowboy_req:reply(200, Req), - {ok, Req2, State}. - -terminate(_Reason, _Req, _State) -> - ok. -endef - -define tpl_gen_fsm --module($(n)). --behaviour(gen_fsm). - -%% API. --export([start_link/0]). - -%% gen_fsm. --export([init/1]). --export([state_name/2]). --export([handle_event/3]). --export([state_name/3]). --export([handle_sync_event/4]). --export([handle_info/3]). --export([terminate/3]). --export([code_change/4]). - --record(state, { -}). - -%% API. - --spec start_link() -> {ok, pid()}. -start_link() -> - gen_fsm:start_link(?MODULE, [], []). - -%% gen_fsm. - -init([]) -> - {ok, state_name, #state{}}. - -state_name(_Event, StateData) -> - {next_state, state_name, StateData}. - -handle_event(_Event, StateName, StateData) -> - {next_state, StateName, StateData}. - -state_name(_Event, _From, StateData) -> - {reply, ignored, state_name, StateData}. - -handle_sync_event(_Event, _From, StateName, StateData) -> - {reply, ignored, StateName, StateData}. - -handle_info(_Info, StateName, StateData) -> - {next_state, StateName, StateData}. - -terminate(_Reason, _StateName, _StateData) -> - ok. - -code_change(_OldVsn, StateName, StateData, _Extra) -> - {ok, StateName, StateData}. -endef - -define tpl_cowboy_loop --module($(n)). --behaviour(cowboy_loop_handler). - --export([init/3]). --export([info/3]). --export([terminate/3]). - --record(state, { -}). - -init(_, Req, _Opts) -> - {loop, Req, #state{}, 5000, hibernate}. - -info(_Info, Req, State) -> - {loop, Req, State, hibernate}. - -terminate(_Reason, _Req, _State) -> - ok. -endef - -define tpl_cowboy_rest --module($(n)). - --export([init/3]). --export([content_types_provided/2]). --export([get_html/2]). - -init(_, _Req, _Opts) -> - {upgrade, protocol, cowboy_rest}. - -content_types_provided(Req, State) -> - {[{{<<"text">>, <<"html">>, '*'}, get_html}], Req, State}. - -get_html(Req, State) -> - {<<"This is REST!">>, Req, State}. -endef - -define tpl_cowboy_ws --module($(n)). --behaviour(cowboy_websocket_handler). - --export([init/3]). --export([websocket_init/3]). --export([websocket_handle/3]). --export([websocket_info/3]). --export([websocket_terminate/3]). - --record(state, { -}). - -init(_, _, _) -> - {upgrade, protocol, cowboy_websocket}. - -websocket_init(_, Req, _Opts) -> - Req2 = cowboy_req:compact(Req), - {ok, Req2, #state{}}. - -websocket_handle({text, Data}, Req, State) -> - {reply, {text, Data}, Req, State}; -websocket_handle({binary, Data}, Req, State) -> - {reply, {binary, Data}, Req, State}; -websocket_handle(_Frame, Req, State) -> - {ok, Req, State}. - -websocket_info(_Info, Req, State) -> - {ok, Req, State}. - -websocket_terminate(_Reason, _Req, _State) -> - ok. -endef - -define tpl_ranch_protocol --module($(n)). --behaviour(ranch_protocol). - --export([start_link/4]). --export([init/4]). - --type opts() :: []. --export_type([opts/0]). - --record(state, { - socket :: inet:socket(), - transport :: module() -}). - -start_link(Ref, Socket, Transport, Opts) -> - Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts]), - {ok, Pid}. - --spec init(ranch:ref(), inet:socket(), module(), opts()) -> ok. -init(Ref, Socket, Transport, _Opts) -> - ok = ranch:accept_ack(Ref), - loop(#state{socket=Socket, transport=Transport}). - -loop(State) -> - loop(State). -endef - -# Plugin-specific targets. - -define render_template - $(verbose) printf -- '$(subst $(newline),\n,$(subst %,%%,$(subst ','\'',$(subst $(tab),$(WS),$(call $(1))))))\n' > $(2) -endef - -ifndef WS -ifdef SP -WS = $(subst a,,a $(wordlist 1,$(SP),a a a a a a a a a a a a a a a a a a a a)) -else -WS = $(tab) -endif -endif - -bootstrap: -ifneq ($(wildcard src/),) - $(error Error: src/ directory already exists) -endif - $(eval p := $(PROJECT)) - $(eval n := $(PROJECT)_sup) - $(call render_template,bs_Makefile,Makefile) - $(verbose) echo "include erlang.mk" >> Makefile - $(verbose) mkdir src/ -ifdef LEGACY - $(call render_template,bs_appsrc,src/$(PROJECT).app.src) -endif - $(call render_template,bs_app,src/$(PROJECT)_app.erl) - $(call render_template,tpl_supervisor,src/$(PROJECT)_sup.erl) - -bootstrap-lib: -ifneq ($(wildcard src/),) - $(error Error: src/ directory already exists) -endif - $(eval p := $(PROJECT)) - $(call render_template,bs_Makefile,Makefile) - $(verbose) echo "include erlang.mk" >> Makefile - $(verbose) mkdir src/ -ifdef LEGACY - $(call render_template,bs_appsrc_lib,src/$(PROJECT).app.src) -endif - -bootstrap-rel: -ifneq ($(wildcard relx.config),) - $(error Error: relx.config already exists) -endif -ifneq ($(wildcard rel/),) - $(error Error: rel/ directory already exists) -endif - $(eval p := $(PROJECT)) - $(call render_template,bs_relx_config,relx.config) - $(verbose) mkdir rel/ - $(call render_template,bs_sys_config,rel/sys.config) - $(call render_template,bs_vm_args,rel/vm.args) - -new-app: -ifndef in - $(error Usage: $(MAKE) new-app in=APP) -endif -ifneq ($(wildcard $(APPS_DIR)/$in),) - $(error Error: Application $in already exists) -endif - $(eval p := $(in)) - $(eval n := $(in)_sup) - $(verbose) mkdir -p $(APPS_DIR)/$p/src/ - $(call render_template,bs_apps_Makefile,$(APPS_DIR)/$p/Makefile) -ifdef LEGACY - $(call render_template,bs_appsrc,$(APPS_DIR)/$p/src/$p.app.src) -endif - $(call render_template,bs_app,$(APPS_DIR)/$p/src/$p_app.erl) - $(call render_template,tpl_supervisor,$(APPS_DIR)/$p/src/$p_sup.erl) - -new-lib: -ifndef in - $(error Usage: $(MAKE) new-lib in=APP) -endif -ifneq ($(wildcard $(APPS_DIR)/$in),) - $(error Error: Application $in already exists) -endif - $(eval p := $(in)) - $(verbose) mkdir -p $(APPS_DIR)/$p/src/ - $(call render_template,bs_apps_Makefile,$(APPS_DIR)/$p/Makefile) -ifdef LEGACY - $(call render_template,bs_appsrc_lib,$(APPS_DIR)/$p/src/$p.app.src) -endif - -new: -ifeq ($(wildcard src/)$(in),) - $(error Error: src/ directory does not exist) -endif -ifndef t - $(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP]) -endif -ifndef tpl_$(t) - $(error Unknown template) -endif -ifndef n - $(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP]) -endif -ifdef in - $(verbose) $(MAKE) -C $(APPS_DIR)/$(in)/ new t=$t n=$n in= -else - $(call render_template,tpl_$(t),src/$(n).erl) -endif - -list-templates: - $(verbose) echo Available templates: $(sort $(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES)))) - -# Copyright (c) 2014-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: clean-c_src distclean-c_src-env - -# Configuration. - -C_SRC_DIR ?= $(CURDIR)/c_src -C_SRC_ENV ?= $(C_SRC_DIR)/env.mk -C_SRC_OUTPUT ?= $(CURDIR)/priv/$(PROJECT) -C_SRC_TYPE ?= shared - -# System type and C compiler/flags. - -ifeq ($(PLATFORM),msys2) - C_SRC_OUTPUT_EXECUTABLE_EXTENSION ?= .exe - C_SRC_OUTPUT_SHARED_EXTENSION ?= .dll -else - C_SRC_OUTPUT_EXECUTABLE_EXTENSION ?= - C_SRC_OUTPUT_SHARED_EXTENSION ?= .so -endif - -ifeq ($(C_SRC_TYPE),shared) - C_SRC_OUTPUT_FILE = $(C_SRC_OUTPUT)$(C_SRC_OUTPUT_SHARED_EXTENSION) -else - C_SRC_OUTPUT_FILE = $(C_SRC_OUTPUT)$(C_SRC_OUTPUT_EXECUTABLE_EXTENSION) -endif - -ifeq ($(PLATFORM),msys2) -# We hardcode the compiler used on MSYS2. The default CC=cc does -# not produce working code. The "gcc" MSYS2 package also doesn't. - CC = /mingw64/bin/gcc - export CC - CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes - CXXFLAGS ?= -O3 -finline-functions -Wall -else ifeq ($(PLATFORM),darwin) - CC ?= cc - CFLAGS ?= -O3 -std=c99 -arch x86_64 -finline-functions -Wall -Wmissing-prototypes - CXXFLAGS ?= -O3 -arch x86_64 -finline-functions -Wall - LDFLAGS ?= -arch x86_64 -flat_namespace -undefined suppress -else ifeq ($(PLATFORM),freebsd) - CC ?= cc - CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes - CXXFLAGS ?= -O3 -finline-functions -Wall -else ifeq ($(PLATFORM),linux) - CC ?= gcc - CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes - CXXFLAGS ?= -O3 -finline-functions -Wall -endif - -ifneq ($(PLATFORM),msys2) - CFLAGS += -fPIC - CXXFLAGS += -fPIC -endif - -CFLAGS += -I"$(ERTS_INCLUDE_DIR)" -I"$(ERL_INTERFACE_INCLUDE_DIR)" -CXXFLAGS += -I"$(ERTS_INCLUDE_DIR)" -I"$(ERL_INTERFACE_INCLUDE_DIR)" - -LDLIBS += -L"$(ERL_INTERFACE_LIB_DIR)" -lerl_interface -lei - -# Verbosity. - -c_verbose_0 = @echo " C " $(?F); -c_verbose = $(c_verbose_$(V)) - -cpp_verbose_0 = @echo " CPP " $(?F); -cpp_verbose = $(cpp_verbose_$(V)) - -link_verbose_0 = @echo " LD " $(@F); -link_verbose = $(link_verbose_$(V)) - -# Targets. - -ifeq ($(wildcard $(C_SRC_DIR)),) -else ifneq ($(wildcard $(C_SRC_DIR)/Makefile),) -app:: app-c_src - -test-build:: app-c_src - -app-c_src: - $(MAKE) -C $(C_SRC_DIR) - -clean:: - $(MAKE) -C $(C_SRC_DIR) clean - -else - -ifeq ($(SOURCES),) -SOURCES := $(sort $(foreach pat,*.c *.C *.cc *.cpp,$(call core_find,$(C_SRC_DIR)/,$(pat)))) -endif -OBJECTS = $(addsuffix .o, $(basename $(SOURCES))) - -COMPILE_C = $(c_verbose) $(CC) $(CFLAGS) $(CPPFLAGS) -c -COMPILE_CPP = $(cpp_verbose) $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c - -app:: $(C_SRC_ENV) $(C_SRC_OUTPUT_FILE) - -test-build:: $(C_SRC_ENV) $(C_SRC_OUTPUT_FILE) - -$(C_SRC_OUTPUT_FILE): $(OBJECTS) - $(verbose) mkdir -p priv/ - $(link_verbose) $(CC) $(OBJECTS) \ - $(LDFLAGS) $(if $(filter $(C_SRC_TYPE),shared),-shared) $(LDLIBS) \ - -o $(C_SRC_OUTPUT_FILE) - -%.o: %.c - $(COMPILE_C) $(OUTPUT_OPTION) $< - -%.o: %.cc - $(COMPILE_CPP) $(OUTPUT_OPTION) $< - -%.o: %.C - $(COMPILE_CPP) $(OUTPUT_OPTION) $< - -%.o: %.cpp - $(COMPILE_CPP) $(OUTPUT_OPTION) $< - -clean:: clean-c_src - -clean-c_src: - $(gen_verbose) rm -f $(C_SRC_OUTPUT_FILE) $(OBJECTS) - -endif - -ifneq ($(wildcard $(C_SRC_DIR)),) -$(C_SRC_ENV): - $(verbose) $(ERL) -eval "file:write_file(\"$(call core_native_path,$(C_SRC_ENV))\", \ - io_lib:format( \ - \"ERTS_INCLUDE_DIR ?= ~s/erts-~s/include/~n\" \ - \"ERL_INTERFACE_INCLUDE_DIR ?= ~s~n\" \ - \"ERL_INTERFACE_LIB_DIR ?= ~s~n\", \ - [code:root_dir(), erlang:system_info(version), \ - code:lib_dir(erl_interface, include), \ - code:lib_dir(erl_interface, lib)])), \ - halt()." - -distclean:: distclean-c_src-env - -distclean-c_src-env: - $(gen_verbose) rm -f $(C_SRC_ENV) - --include $(C_SRC_ENV) -endif - -# Templates. - -define bs_c_nif -#include "erl_nif.h" - -static int loads = 0; - -static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) -{ - /* Initialize private data. */ - *priv_data = NULL; - - loads++; - - return 0; -} - -static int upgrade(ErlNifEnv* env, void** priv_data, void** old_priv_data, ERL_NIF_TERM load_info) -{ - /* Convert the private data to the new version. */ - *priv_data = *old_priv_data; - - loads++; - - return 0; -} - -static void unload(ErlNifEnv* env, void* priv_data) -{ - if (loads == 1) { - /* Destroy the private data. */ - } - - loads--; -} - -static ERL_NIF_TERM hello(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) -{ - if (enif_is_atom(env, argv[0])) { - return enif_make_tuple2(env, - enif_make_atom(env, "hello"), - argv[0]); - } - - return enif_make_tuple2(env, - enif_make_atom(env, "error"), - enif_make_atom(env, "badarg")); -} - -static ErlNifFunc nif_funcs[] = { - {"hello", 1, hello} -}; - -ERL_NIF_INIT($n, nif_funcs, load, NULL, upgrade, unload) -endef - -define bs_erl_nif --module($n). - --export([hello/1]). - --on_load(on_load/0). -on_load() -> - PrivDir = case code:priv_dir(?MODULE) of - {error, _} -> - AppPath = filename:dirname(filename:dirname(code:which(?MODULE))), - filename:join(AppPath, "priv"); - Path -> - Path - end, - erlang:load_nif(filename:join(PrivDir, atom_to_list(?MODULE)), 0). - -hello(_) -> - erlang:nif_error({not_loaded, ?MODULE}). -endef - -new-nif: -ifneq ($(wildcard $(C_SRC_DIR)/$n.c),) - $(error Error: $(C_SRC_DIR)/$n.c already exists) -endif -ifneq ($(wildcard src/$n.erl),) - $(error Error: src/$n.erl already exists) -endif -ifdef in - $(verbose) $(MAKE) -C $(APPS_DIR)/$(in)/ new-nif n=$n in= -else - $(verbose) mkdir -p $(C_SRC_DIR) src/ - $(call render_template,bs_c_nif,$(C_SRC_DIR)/$n.c) - $(call render_template,bs_erl_nif,src/$n.erl) -endif - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: ci ci-setup distclean-kerl - -KERL ?= $(CURDIR)/kerl -export KERL - -KERL_URL ?= https://raw.githubusercontent.com/yrashk/kerl/master/kerl - -OTP_GIT ?= https://github.com/erlang/otp - -CI_INSTALL_DIR ?= $(HOME)/erlang -CI_OTP ?= - -ifeq ($(strip $(CI_OTP)),) -ci:: -else -ci:: $(addprefix ci-,$(CI_OTP)) - -ci-prepare: $(addprefix $(CI_INSTALL_DIR)/,$(CI_OTP)) - -ci-setup:: - -ci_verbose_0 = @echo " CI " $(1); -ci_verbose = $(ci_verbose_$(V)) - -define ci_target -ci-$(1): $(CI_INSTALL_DIR)/$(1) - $(ci_verbose) \ - PATH="$(CI_INSTALL_DIR)/$(1)/bin:$(PATH)" \ - CI_OTP_RELEASE="$(1)" \ - CT_OPTS="-label $(1)" \ - $(MAKE) clean ci-setup tests -endef - -$(foreach otp,$(CI_OTP),$(eval $(call ci_target,$(otp)))) - -define ci_otp_target -ifeq ($(wildcard $(CI_INSTALL_DIR)/$(1)),) -$(CI_INSTALL_DIR)/$(1): $(KERL) - $(KERL) build git $(OTP_GIT) $(1) $(1) - $(KERL) install $(1) $(CI_INSTALL_DIR)/$(1) -endif -endef - -$(foreach otp,$(CI_OTP),$(eval $(call ci_otp_target,$(otp)))) - -$(KERL): - $(gen_verbose) $(call core_http_get,$(KERL),$(KERL_URL)) - $(verbose) chmod +x $(KERL) - -help:: - $(verbose) printf "%s\n" "" \ - "Continuous Integration targets:" \ - " ci Run '$(MAKE) tests' on all configured Erlang versions." \ - "" \ - "The CI_OTP variable must be defined with the Erlang versions" \ - "that must be tested. For example: CI_OTP = OTP-17.3.4 OTP-17.5.3" - -distclean:: distclean-kerl - -distclean-kerl: - $(gen_verbose) rm -rf $(KERL) -endif - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: ct apps-ct distclean-ct - -# Configuration. - -CT_OPTS ?= -ifneq ($(wildcard $(TEST_DIR)),) - CT_SUITES ?= $(sort $(subst _SUITE.erl,,$(notdir $(call core_find,$(TEST_DIR)/,*_SUITE.erl)))) -else - CT_SUITES ?= -endif - -# Core targets. - -tests:: ct - -distclean:: distclean-ct - -help:: - $(verbose) printf "%s\n" "" \ - "Common_test targets:" \ - " ct Run all the common_test suites for this project" \ - "" \ - "All your common_test suites have their associated targets." \ - "A suite named http_SUITE can be ran using the ct-http target." - -# Plugin-specific targets. - -CT_RUN = ct_run \ - -no_auto_compile \ - -noinput \ - -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(DEPS_DIR)/gen_rpc/_build/dev/lib/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \ - -dir $(TEST_DIR) \ - -logdir $(CURDIR)/logs - -ifeq ($(CT_SUITES),) -ct: $(if $(IS_APP),,apps-ct) -else -ct: test-build $(if $(IS_APP),,apps-ct) - $(verbose) mkdir -p $(CURDIR)/logs/ - $(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS) -endif - -ifneq ($(ALL_APPS_DIRS),) -define ct_app_target -apps-ct-$1: - $(MAKE) -C $1 ct IS_APP=1 -endef - -$(foreach app,$(ALL_APPS_DIRS),$(eval $(call ct_app_target,$(app)))) - -apps-ct: test-build $(addprefix apps-ct-,$(ALL_APPS_DIRS)) -endif - -ifndef t -CT_EXTRA = -else -ifeq (,$(findstring :,$t)) -CT_EXTRA = -group $t -else -t_words = $(subst :, ,$t) -CT_EXTRA = -group $(firstword $(t_words)) -case $(lastword $(t_words)) -endif -endif - -define ct_suite_target -ct-$(1): test-build - $(verbose) mkdir -p $(CURDIR)/logs/ - $(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(1)) $(CT_EXTRA) $(CT_OPTS) -endef - -$(foreach test,$(CT_SUITES),$(eval $(call ct_suite_target,$(test)))) - -distclean-ct: - $(gen_verbose) rm -rf $(CURDIR)/logs/ - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: plt distclean-plt dialyze - -# Configuration. - -DIALYZER_PLT ?= $(CURDIR)/.$(PROJECT).plt -export DIALYZER_PLT - -PLT_APPS ?= -DIALYZER_DIRS ?= --src -r $(wildcard src) $(ALL_APPS_DIRS) -DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions -Wunmatched_returns # -Wunderspecs - -# Core targets. - -check:: dialyze - -distclean:: distclean-plt - -help:: - $(verbose) printf "%s\n" "" \ - "Dialyzer targets:" \ - " plt Build a PLT file for this project" \ - " dialyze Analyze the project using Dialyzer" - -# Plugin-specific targets. - -define filter_opts.erl - Opts = binary:split(<<"$1">>, <<"-">>, [global]), - Filtered = lists:reverse(lists:foldl(fun - (O = <<"pa ", _/bits>>, Acc) -> [O|Acc]; - (O = <<"D ", _/bits>>, Acc) -> [O|Acc]; - (O = <<"I ", _/bits>>, Acc) -> [O|Acc]; - (_, Acc) -> Acc - end, [], Opts)), - io:format("~s~n", [[["-", O] || O <- Filtered]]), - halt(). -endef - -$(DIALYZER_PLT): deps app - $(verbose) dialyzer --build_plt --apps erts kernel stdlib $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS) - -plt: $(DIALYZER_PLT) - -distclean-plt: - $(gen_verbose) rm -f $(DIALYZER_PLT) - -ifneq ($(wildcard $(DIALYZER_PLT)),) -dialyze: -else -dialyze: $(DIALYZER_PLT) -endif - $(verbose) dialyzer --no_native `$(call erlang,$(call filter_opts.erl,$(ERLC_OPTS)))` $(DIALYZER_DIRS) $(DIALYZER_OPTS) - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: distclean-edoc edoc - -# Configuration. - -EDOC_OPTS ?= - -# Core targets. - -ifneq ($(wildcard doc/overview.edoc),) -docs:: edoc -endif - -distclean:: distclean-edoc - -# Plugin-specific targets. - -edoc: distclean-edoc doc-deps - $(gen_verbose) $(ERL) -eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), halt().' - -distclean-edoc: - $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info - -# Copyright (c) 2014 Dave Cottlehuber -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: distclean-escript escript - -# Configuration. - -ESCRIPT_NAME ?= $(PROJECT) -ESCRIPT_FILE ?= $(ESCRIPT_NAME) - -ESCRIPT_COMMENT ?= This is an -*- erlang -*- file - -ESCRIPT_BEAMS ?= "ebin/*", "deps/*/ebin/*" -ESCRIPT_SYS_CONFIG ?= "rel/sys.config" -ESCRIPT_EMU_ARGS ?= -pa . \ - -sasl errlog_type error \ - -escript main $(ESCRIPT_NAME) -ESCRIPT_SHEBANG ?= /usr/bin/env escript -ESCRIPT_STATIC ?= "deps/*/priv/**", "priv/**" - -# Core targets. - -distclean:: distclean-escript - -help:: - $(verbose) printf "%s\n" "" \ - "Escript targets:" \ - " escript Build an executable escript archive" \ - -# Plugin-specific targets. - -# Based on https://github.com/synrc/mad/blob/master/src/mad_bundle.erl -# Copyright (c) 2013 Maxim Sokhatsky, Synrc Research Center -# Modified MIT License, https://github.com/synrc/mad/blob/master/LICENSE : -# Software may only be used for the great good and the true happiness of all -# sentient beings. - -define ESCRIPT_RAW -'Read = fun(F) -> {ok, B} = file:read_file(filename:absname(F)), B end,'\ -'Files = fun(L) -> A = lists:concat([filelib:wildcard(X)||X<- L ]),'\ -' [F || F <- A, not filelib:is_dir(F) ] end,'\ -'Squash = fun(L) -> [{filename:basename(F), Read(F) } || F <- L ] end,'\ -'Zip = fun(A, L) -> {ok,{_,Z}} = zip:create(A, L, [{compress,all},memory]), Z end,'\ -'Ez = fun(Escript) ->'\ -' Static = Files([$(ESCRIPT_STATIC)]),'\ -' Beams = Squash(Files([$(ESCRIPT_BEAMS), $(ESCRIPT_SYS_CONFIG)])),'\ -' Archive = Beams ++ [{ "static.gz", Zip("static.gz", Static)}],'\ -' escript:create(Escript, [ $(ESCRIPT_OPTIONS)'\ -' {archive, Archive, [memory]},'\ -' {shebang, "$(ESCRIPT_SHEBANG)"},'\ -' {comment, "$(ESCRIPT_COMMENT)"},'\ -' {emu_args, " $(ESCRIPT_EMU_ARGS)"}'\ -' ]),'\ -' file:change_mode(Escript, 8#755)'\ -'end,'\ -'Ez("$(ESCRIPT_FILE)"),'\ -'halt().' -endef - -ESCRIPT_COMMAND = $(subst ' ',,$(ESCRIPT_RAW)) - -escript:: distclean-escript deps app - $(gen_verbose) $(ERL) -eval $(ESCRIPT_COMMAND) - -distclean-escript: - $(gen_verbose) rm -f $(ESCRIPT_NAME) - -# Copyright (c) 2014, Enrique Fernandez -# Copyright (c) 2015, Loïc Hoguin -# This file is contributed to erlang.mk and subject to the terms of the ISC License. - -.PHONY: eunit apps-eunit - -# Configuration - -EUNIT_OPTS ?= -EUNIT_ERL_OPTS ?= - -# Core targets. - -tests:: eunit - -help:: - $(verbose) printf "%s\n" "" \ - "EUnit targets:" \ - " eunit Run all the EUnit tests for this project" - -# Plugin-specific targets. - -define eunit.erl - case "$(COVER)" of - "" -> ok; - _ -> - case cover:compile_beam_directory("ebin") of - {error, _} -> halt(1); - _ -> ok - end - end, - case eunit:test($1, [$(EUNIT_OPTS)]) of - ok -> ok; - error -> halt(2) - end, - case "$(COVER)" of - "" -> ok; - _ -> - cover:export("eunit.coverdata") - end, - halt() -endef - -EUNIT_ERL_OPTS += -pa $(TEST_DIR) $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(CURDIR)/ebin - -ifdef t -ifeq (,$(findstring :,$(t))) -eunit: test-build - $(gen_verbose) $(call erlang,$(call eunit.erl,['$(t)']),$(EUNIT_ERL_OPTS)) -else -eunit: test-build - $(gen_verbose) $(call erlang,$(call eunit.erl,fun $(t)/0),$(EUNIT_ERL_OPTS)) -endif -else -EUNIT_EBIN_MODS = $(notdir $(basename $(ERL_FILES) $(BEAM_FILES))) -EUNIT_TEST_MODS = $(notdir $(basename $(call core_find,$(TEST_DIR)/,*.erl))) - -EUNIT_MODS = $(foreach mod,$(EUNIT_EBIN_MODS) $(filter-out \ - $(patsubst %,%_tests,$(EUNIT_EBIN_MODS)),$(EUNIT_TEST_MODS)),'$(mod)') - -eunit: test-build $(if $(IS_APP),,apps-eunit) - $(gen_verbose) $(call erlang,$(call eunit.erl,[$(call comma_list,$(EUNIT_MODS))]),$(EUNIT_ERL_OPTS)) - -ifneq ($(ALL_APPS_DIRS),) -apps-eunit: - $(verbose) for app in $(ALL_APPS_DIRS); do $(MAKE) -C $$app eunit IS_APP=1; done -endif -endif - -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: relx-rel distclean-relx-rel distclean-relx run - -# Configuration. - -RELX ?= $(CURDIR)/relx -RELX_CONFIG ?= $(CURDIR)/relx.config - -RELX_URL ?= https://github.com/erlware/relx/releases/download/v3.19.0/relx -RELX_OPTS ?= -RELX_OUTPUT_DIR ?= _rel - -ifeq ($(firstword $(RELX_OPTS)),-o) - RELX_OUTPUT_DIR = $(word 2,$(RELX_OPTS)) -else - RELX_OPTS += -o $(RELX_OUTPUT_DIR) -endif - -# Core targets. - -ifeq ($(IS_DEP),) -ifneq ($(wildcard $(RELX_CONFIG)),) -rel:: relx-rel -endif -endif - -distclean:: distclean-relx-rel distclean-relx - -# Plugin-specific targets. - -$(RELX): - $(gen_verbose) $(call core_http_get,$(RELX),$(RELX_URL)) - $(verbose) chmod +x $(RELX) - -relx-rel: $(RELX) rel-deps app - $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) - -distclean-relx-rel: - $(gen_verbose) rm -rf $(RELX_OUTPUT_DIR) - -distclean-relx: - $(gen_verbose) rm -rf $(RELX) - -# Run target. - -ifeq ($(wildcard $(RELX_CONFIG)),) -run: -else - -define get_relx_release.erl - {ok, Config} = file:consult("$(RELX_CONFIG)"), - {release, {Name, _}, _} = lists:keyfind(release, 1, Config), - io:format("~s", [Name]), - halt(0). -endef - -RELX_RELEASE = `$(call erlang,$(get_relx_release.erl))` - -run: all - $(verbose) $(RELX_OUTPUT_DIR)/$(RELX_RELEASE)/bin/$(RELX_RELEASE) console - -help:: - $(verbose) printf "%s\n" "" \ - "Relx targets:" \ - " run Compile the project, build the release and run it" - -endif - -# Copyright (c) 2014, M Robert Martin -# Copyright (c) 2015, Loïc Hoguin -# This file is contributed to erlang.mk and subject to the terms of the ISC License. - -.PHONY: shell - -# Configuration. - -SHELL_ERL ?= erl -SHELL_PATHS ?= $(CURDIR)/ebin $(APPS_DIR)/*/ebin $(DEPS_DIR)/*/ebin -SHELL_OPTS ?= - -ALL_SHELL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(SHELL_DEPS)) - -# Core targets - -help:: - $(verbose) printf "%s\n" "" \ - "Shell targets:" \ - " shell Run an erlang shell with SHELL_OPTS or reasonable default" - -# Plugin-specific targets. - -$(foreach dep,$(SHELL_DEPS),$(eval $(call dep_target,$(dep)))) - -build-shell-deps: $(ALL_SHELL_DEPS_DIRS) - $(verbose) for dep in $(ALL_SHELL_DEPS_DIRS) ; do $(MAKE) -C $$dep ; done - -shell: build-shell-deps - $(gen_verbose) $(SHELL_ERL) -pa $(SHELL_PATHS) $(SHELL_OPTS) - -# Copyright (c) 2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -ifeq ($(filter triq,$(DEPS) $(TEST_DEPS)),triq) -.PHONY: triq - -# Targets. - -tests:: triq - -define triq_check.erl - code:add_pathsa(["$(CURDIR)/ebin", "$(DEPS_DIR)/*/ebin"]), - try - case $(1) of - all -> [true] =:= lists:usort([triq:check(M) || M <- [$(call comma_list,$(3))]]); - module -> triq:check($(2)); - function -> triq:check($(2)) - end - of - true -> halt(0); - _ -> halt(1) - catch error:undef -> - io:format("Undefined property or module~n"), - halt(0) - end. -endef - -ifdef t -ifeq (,$(findstring :,$(t))) -triq: test-build - $(verbose) $(call erlang,$(call triq_check.erl,module,$(t))) -else -triq: test-build - $(verbose) echo Testing $(t)/0 - $(verbose) $(call erlang,$(call triq_check.erl,function,$(t)())) -endif -else -triq: test-build - $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename $(wildcard ebin/*.beam)))))) - $(gen_verbose) $(call erlang,$(call triq_check.erl,all,undefined,$(MODULES))) -endif -endif - -# Copyright (c) 2015, Erlang Solutions Ltd. -# This file is part of erlang.mk and subject to the terms of the ISC License. - -.PHONY: xref distclean-xref - -# Configuration. - -ifeq ($(XREF_CONFIG),) - XREF_ARGS := -else - XREF_ARGS := -c $(XREF_CONFIG) -endif - -XREFR ?= $(CURDIR)/xrefr -export XREFR - -XREFR_URL ?= https://github.com/inaka/xref_runner/releases/download/0.2.2/xrefr - -# Core targets. - -help:: - $(verbose) printf "%s\n" "" \ - "Xref targets:" \ - " xref Run Xrefr using $XREF_CONFIG as config file if defined" - -distclean:: distclean-xref - -# Plugin-specific targets. - -$(XREFR): - $(gen_verbose) $(call core_http_get,$(XREFR),$(XREFR_URL)) - $(verbose) chmod +x $(XREFR) - -xref: deps app $(XREFR) - $(gen_verbose) $(XREFR) $(XREFR_ARGS) - -distclean-xref: - $(gen_verbose) rm -rf $(XREFR) - -# Copyright 2015, Viktor Söderqvist -# This file is part of erlang.mk and subject to the terms of the ISC License. - -COVER_REPORT_DIR = cover - -# Hook in coverage to ct - -ifdef COVER -ifdef CT_RUN -# All modules in 'ebin' -COVER_MODS = $(notdir $(basename $(call core_ls,ebin/*.beam))) - -test-build:: $(TEST_DIR)/ct.cover.spec - -$(TEST_DIR)/ct.cover.spec: - $(verbose) echo Cover mods: $(COVER_MODS) - $(gen_verbose) printf "%s\n" \ - '{incl_mods,[$(subst $(space),$(comma),$(COVER_MODS))]}.' \ - '{export,"$(CURDIR)/ct.coverdata"}.' > $@ - -CT_RUN += -cover $(TEST_DIR)/ct.cover.spec -endif -endif - -# Core targets - -ifdef COVER -ifneq ($(COVER_REPORT_DIR),) -tests:: - $(verbose) $(MAKE) --no-print-directory cover-report -endif -endif - -clean:: coverdata-clean - -ifneq ($(COVER_REPORT_DIR),) -distclean:: cover-report-clean -endif - -help:: - $(verbose) printf "%s\n" "" \ - "Cover targets:" \ - " cover-report Generate a HTML coverage report from previously collected" \ - " cover data." \ - " all.coverdata Merge {eunit,ct}.coverdata into one coverdata file." \ - "" \ - "If COVER=1 is set, coverage data is generated by the targets eunit and ct. The" \ - "target tests additionally generates a HTML coverage report from the combined" \ - "coverdata files from each of these testing tools. HTML reports can be disabled" \ - "by setting COVER_REPORT_DIR to empty." - -# Plugin specific targets - -COVERDATA = $(filter-out all.coverdata,$(wildcard *.coverdata)) - -.PHONY: coverdata-clean -coverdata-clean: - $(gen_verbose) rm -f *.coverdata ct.cover.spec - -# Merge all coverdata files into one. -all.coverdata: $(COVERDATA) - $(gen_verbose) $(ERL) -eval ' \ - $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) \ - cover:export("$@"), halt(0).' - -# These are only defined if COVER_REPORT_DIR is non-empty. Set COVER_REPORT_DIR to -# empty if you want the coverdata files but not the HTML report. -ifneq ($(COVER_REPORT_DIR),) - -.PHONY: cover-report-clean cover-report - -cover-report-clean: - $(gen_verbose) rm -rf $(COVER_REPORT_DIR) - -ifeq ($(COVERDATA),) -cover-report: -else - -# Modules which include eunit.hrl always contain one line without coverage -# because eunit defines test/0 which is never called. We compensate for this. -EUNIT_HRL_MODS = $(subst $(space),$(comma),$(shell \ - grep -e '^\s*-include.*include/eunit\.hrl"' src/*.erl \ - | sed "s/^src\/\(.*\)\.erl:.*/'\1'/" | uniq)) - -define cover_report.erl - $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) - Ms = cover:imported_modules(), - [cover:analyse_to_file(M, "$(COVER_REPORT_DIR)/" ++ atom_to_list(M) - ++ ".COVER.html", [html]) || M <- Ms], - Report = [begin {ok, R} = cover:analyse(M, module), R end || M <- Ms], - EunitHrlMods = [$(EUNIT_HRL_MODS)], - Report1 = [{M, {Y, case lists:member(M, EunitHrlMods) of - true -> N - 1; false -> N end}} || {M, {Y, N}} <- Report], - TotalY = lists:sum([Y || {_, {Y, _}} <- Report1]), - TotalN = lists:sum([N || {_, {_, N}} <- Report1]), - Perc = fun(Y, N) -> case Y + N of 0 -> 100; S -> round(100 * Y / S) end end, - TotalPerc = Perc(TotalY, TotalN), - {ok, F} = file:open("$(COVER_REPORT_DIR)/index.html", [write]), - io:format(F, "~n" - "~n" - "Coverage report~n" - "~n", []), - io:format(F, "

    Coverage

    ~n

    Total: ~p%

    ~n", [TotalPerc]), - io:format(F, "~n", []), - [io:format(F, "" - "~n", - [M, M, Perc(Y, N)]) || {M, {Y, N}} <- Report1], - How = "$(subst $(space),$(comma)$(space),$(basename $(COVERDATA)))", - Date = "$(shell date -u "+%Y-%m-%dT%H:%M:%SZ")", - io:format(F, "
    ModuleCoverage
    ~p~p%
    ~n" - "

    Generated using ~s and erlang.mk on ~s.

    ~n" - "", [How, Date]), - halt(). -endef - -cover-report: - $(gen_verbose) mkdir -p $(COVER_REPORT_DIR) - $(gen_verbose) $(call erlang,$(cover_report.erl)) - -endif -endif # ifneq ($(COVER_REPORT_DIR),) From 64f62fa0ce5095a75f77792daee2dc646bfea524 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 4 Dec 2018 23:22:39 +0800 Subject: [PATCH 464/520] Make some processes hibernate after 1s. --- src/emqx_broker.erl | 2 +- src/emqx_hooks.erl | 2 +- src/emqx_pool.erl | 3 ++- src/emqx_router.erl | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index ca4a86c87..b816a0a69 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -56,7 +56,7 @@ -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, - [Pool, Id], [{hibernate_after, 2000}]). + [Pool, Id], [{hibernate_after, 1000}]). %%------------------------------------------------------------------------------ %% Subscribe diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index 073c12870..b10445742 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -42,7 +42,7 @@ -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], [{hibernate_after, 60000}]). + gen_server:start_link({local, ?SERVER}, ?MODULE, [], [{hibernate_after, 1000}]). -spec(stop() -> ok). stop() -> diff --git a/src/emqx_pool.erl b/src/emqx_pool.erl index 762f5dc6d..7b12bea69 100644 --- a/src/emqx_pool.erl +++ b/src/emqx_pool.erl @@ -35,7 +35,8 @@ start_link() -> %% @doc Start pool. -spec(start_link(atom(), pos_integer()) -> emqx_types:startlink_ret()). start_link(Pool, Id) -> - gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, [Pool, Id], []). + gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, + ?MODULE, [Pool, Id], [{hibernate_after, 1000}]). %% @doc Submit work to the pool. -spec(submit(task()) -> any()). diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 941c004f7..e513d041d 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -68,7 +68,7 @@ mnesia(copy) -> -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, - ?MODULE, [Pool, Id], [{hibernate_after, 2000}]). + ?MODULE, [Pool, Id], [{hibernate_after, 1000}]). %%------------------------------------------------------------------------------ %% Route APIs From e0eb76afa6d417396d74ac3b8fb419b0c206df00 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sat, 8 Dec 2018 15:51:11 +0800 Subject: [PATCH 465/520] Fix subscription --- src/emqx_broker.erl | 36 +++++++++--------------------------- test/emqx_broker_SUITE.erl | 2 -- test/emqx_client_SUITE.erl | 2 +- 3 files changed, 10 insertions(+), 30 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index b816a0a69..50fb06e0e 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -258,7 +258,7 @@ subscriptions(Subscriber) -> subscription(Topic, Subscriber); ({_, Topic}) -> subscription(Topic, Subscriber) - end, ets:lookup(?SUBSCRIPTION, Subscriber)). + end, ets:lookup(?SUBSCRIPTION, Subscriber)). subscription(Topic, Subscriber) -> {Topic, ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2)}. @@ -336,15 +336,14 @@ handle_cast({From, #subscribe{topic = Topic, subpid = SubPid, subid = SubId, sub Subscriber = {SubPid, SubId}, case ets:member(?SUBOPTION, {Topic, Subscriber}) of false -> - resubscribe(From, {Subscriber, SubOpts, Topic}, State); + Group = maps:get(share, SubOpts, undefined), + true = do_subscribe(Group, Topic, Subscriber, SubOpts), + emqx_shared_sub:subscribe(Group, Topic, SubPid), + emqx_router:add_route(From, Topic, dest(Group)), + {noreply, monitor_subscriber(Subscriber, State)}; true -> - case ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2) =:= SubOpts of - true -> - gen_server:reply(From, ok), - {noreply, State}; - false -> - resubscribe(From, {Subscriber, SubOpts, Topic}, State) - end + gen_server:reply(From, ok), + {noreply, State} end; handle_cast({From, #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId}}, State) -> @@ -390,26 +389,9 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -resubscribe(From, {Subscriber, SubOpts, Topic}, State) -> - {SubPid, _} = Subscriber, - Group = maps:get(share, SubOpts, undefined), - true = do_subscribe(Group, Topic, Subscriber, SubOpts), - emqx_shared_sub:subscribe(Group, Topic, SubPid), - emqx_router:add_route(From, Topic, dest(Group)), - {noreply, monitor_subscriber(Subscriber, State)}. - -insert_subscriber(Group, Topic, Subscriber) -> - Subscribers = subscribers(Topic), - case lists:member(Subscriber, Subscribers) of - false -> - ets:insert(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}); - _ -> - ok - end. - do_subscribe(Group, Topic, Subscriber, SubOpts) -> ets:insert(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), - insert_subscriber(Group, Topic, Subscriber), + ets:insert(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}), ets:insert(?SUBOPTION, {{Topic, Subscriber}, SubOpts}). do_unsubscribe(Group, Topic, Subscriber) -> diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index b9bfb4257..f9a6dcf40 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -92,10 +92,8 @@ pubsub(_) -> true = emqx:set_subopts(<<"a/b/c">>, Subscriber, #{qos => 0}), #{qos := 0} = emqx:get_subopts(<<"a/b/c">>, Subscriber), ok = emqx:subscribe(<<"a/b/c">>, <<"clientId">>, #{ qos => 2 }), - #{qos := 2} = ets:lookup_element(emqx_suboption, {<<"a/b/c">>, Subscriber}, 2), %% ct:log("Emq Sub: ~p.~n", [ets:lookup(emqx_suboption, {<<"a/b/c">>, Subscriber})]), timer:sleep(10), - [{<<"a/b/c">>, #{qos := 2}}] = emqx_broker:subscriptions(Subscriber), [{Self, <<"clientId">>}] = emqx_broker:subscribers(<<"a/b/c">>), emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), ?assert(receive {dispatch, <<"a/b/c">>, _ } -> true; P -> ct:log("Receive Message: ~p~n",[P]) after 2 -> false end), diff --git a/test/emqx_client_SUITE.erl b/test/emqx_client_SUITE.erl index 6de507fca..021109606 100644 --- a/test/emqx_client_SUITE.erl +++ b/test/emqx_client_SUITE.erl @@ -161,7 +161,7 @@ basic_test(_Config) -> 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), From ec2e28977600d131cf3b5eca4e5be5fbc71d3f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Mon, 10 Dec 2018 11:13:25 +0800 Subject: [PATCH 466/520] Fix crash in emqx_acl_internal:filter/2 --- src/emqx_access_rule.erl | 3 ++- src/emqx_acl_internal.erl | 6 ++++-- test/emqx_access_SUITE.erl | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 1d3154735..47b20676b 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -47,7 +47,8 @@ compile({A, Who, Access, TopicFilters}) when ?ALLOW_DENY(A), ?PUBSUB(Access) -> {A, compile(who, Who), Access, [compile(topic, Topic) || Topic <- TopicFilters]}; compile(Rule) -> - emqx_logger:error("[ACCESS_RULE] Malformed rule: ~p", [Rule]). + emqx_logger:error("[ACCESS_RULE] Malformed rule: ~p", [Rule]), + {error, bad_rule}. compile(who, all) -> all; diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index 383967030..328993a78 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -60,10 +60,12 @@ load_rules_from_file(AclFile) -> ets:insert(?ACL_RULE_TAB, {all_rules, Terms}), ok; {error, Reason} -> - emqx_logger:error("[ACL_INTERNAL] Consult failed: ~p", [Reason]), + emqx_logger:error("[ACL_INTERNAL] Failed to read ~s: ~p", [AclFile, Reason]), {error, Reason} end. - + +filter(_PubSub, {error, _}) -> + false; filter(_PubSub, {allow, all}) -> true; filter(_PubSub, {deny, all}) -> diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index b49c90d80..5d0bcf049 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -355,7 +355,8 @@ compile_rule(_) -> {deny, all, subscribe, [ [<<"$SYS">>, '#'], ['#'] ]} = compile({deny, all, subscribe, ["$SYS/#", "#"]}), {allow, all} = compile({allow, all}), - {deny, all} = compile({deny, all}). + {deny, all} = compile({deny, all}), + {error, bad_rule} = compile({test, malformed}). match_rule(_) -> User = #{client_id => <<"testClient">>, username => <<"TestUser">>, peername => {{127,0,0,1}, 2948}}, From cca5081e021c509dbdba07bb9037e9321f8cf546 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 10 Dec 2018 18:37:42 +0800 Subject: [PATCH 467/520] Improve the design of trie, router and broker modules 1. Add do_add_route/1 do_add_route/2, do_delete_route/1, do_delete_route/2 APIs in emqx_router module 2. Improve the code of emqx_trie module 3. Update the emqx_broker module to call the new APIs of emqx_router --- src/emqx_broker.erl | 30 ++++----- src/emqx_router.erl | 134 ++++++++++++++++++------------------- src/emqx_router_helper.erl | 45 +++++++------ src/emqx_shared_sub.erl | 7 +- src/emqx_trie.erl | 18 ++--- test/emqx_router_SUITE.erl | 103 ++++++++++------------------ test/emqx_trie_SUITE.erl | 113 ++++++++++++++++--------------- 7 files changed, 208 insertions(+), 242 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index a2ddf5b60..a00dc17b8 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -27,6 +27,7 @@ -export([subscriptions/1, subscribers/1, subscribed/2]). -export([get_subopts/2, set_subopts/2]). -export([topics/0]). + %% Stats fun -export([stats_fun/0]). @@ -52,9 +53,9 @@ -spec(start_link(atom(), pos_integer()) -> emqx_types:startlink_ret()). start_link(Pool, Id) -> - _ = create_tabs(), - Name = emqx_misc:proc_name(?BROKER, Id), - gen_server:start_link({local, Name}, ?MODULE, [Pool, Id], []). + ok = create_tabs(), + gen_server:start_link({local, emqx_misc:proc_name(?BROKER, Id)}, + ?MODULE, [Pool, Id], []). %%------------------------------------------------------------------------------ %% Create tabs @@ -75,7 +76,7 @@ create_tabs() -> ok = emqx_tables:new(?SUBSCRIPTION, [duplicate_bag | TabOpts]), %% Subscriber: Topic -> SubPid1, SubPid2, SubPid3, ... - %% duplicate_bag: o(1) insert + %% bag: o(n) insert ok = emqx_tables:new(?SUBSCRIBER, [bag | TabOpts]). %%------------------------------------------------------------------------------ @@ -114,7 +115,7 @@ subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map call(pick({Topic, Shard}), {subscribe, Topic}); Group -> %% Shard subscription true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), - emqx_shard_sub:subscribe(Group, Topic, SubPid) + emqx_shared_sub:subscribe(Group, Topic, SubPid) end; true -> ok end. @@ -367,18 +368,17 @@ pick(Topic) -> %%------------------------------------------------------------------------------ init([Pool, Id]) -> - _ = emqx_router:set_mode(protected), true = gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #{pool => Pool, id => Id}}. handle_call({subscribe, Topic}, _From, State) -> - case get(Topic) of - undefined -> - _ = put(Topic, true), - emqx_router:add_route(Topic); - true -> ok - end, - {reply, ok, State}; + Ok = case get(Topic) of + undefined -> + _ = put(Topic, true), + emqx_router:do_add_route(Topic); + true -> ok + end, + {reply, Ok, State}; handle_call(Req, _From, State) -> emqx_logger:error("[Broker] unexpected call: ~p", [Req]), @@ -387,8 +387,8 @@ handle_call(Req, _From, State) -> handle_cast({unsubscribed, Topic}, State) -> case ets:member(?SUBSCRIBER, Topic) of false -> - _ = erase(Topic), - emqx_router:delete_route(Topic); + _ = erase(Topic), + emqx_router:do_delete_route(Topic); true -> ok end, {noreply, State}; diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 313adc475..c165b97ca 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -29,19 +29,19 @@ %% Route APIs -export([add_route/1, add_route/2]). --export([get_routes/1]). +-export([do_add_route/1, do_add_route/2]). +-export([match_routes/1, lookup_routes/1, has_routes/1]). -export([delete_route/1, delete_route/2]). --export([has_routes/1, match_routes/1, print_routes/1]). +-export([do_delete_route/1, do_delete_route/2]). +-export([print_routes/1]). -export([topics/0]). -%% Mode --export([set_mode/1, get_mode/0]). - %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --type(destination() :: node() | {binary(), node()}). +-type(group() :: binary()). +-type(destination() :: node() | {group(), node()}). -define(ROUTE, emqx_route). @@ -66,75 +66,76 @@ mnesia(copy) -> -spec(start_link(atom(), pos_integer()) -> emqx_types:startlink_ret()). start_link(Pool, Id) -> - Name = emqx_misc:proc_name(?MODULE, Id), - gen_server:start_link({local, Name}, ?MODULE, [Pool, Id], [{hibernate_after, 1000}]). + 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() | emqx_types:route()) -> ok | {error, term()}). +-spec(add_route(emqx_topic:topic()) -> ok | {error, term()}). add_route(Topic) when is_binary(Topic) -> - add_route(#route{topic = Topic, dest = node()}); -add_route(Route = #route{topic = Topic}) -> - case get_mode() of - protected -> do_add_route(Route); - undefined -> call(pick(Topic), {add_route, Route}) - end. + add_route(Topic, node()). -spec(add_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). add_route(Topic, Dest) when is_binary(Topic) -> - add_route(#route{topic = Topic, dest = Dest}). + call(pick(Topic), {add_route, Topic, Dest}). -%% @private -do_add_route(Route = #route{topic = Topic, dest = Dest}) -> - case lists:member(Route, get_routes(Topic)) of +-spec(do_add_route(emqx_topic:topic()) -> ok | {error, term()}). +do_add_route(Topic) when is_binary(Topic) -> + do_add_route(Topic, node()). + +-spec(do_add_route(emqx_topic:topic(), destination()) -> 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 true -> ok; false -> ok = emqx_router_helper:monitor(Dest), case emqx_topic:wildcard(Topic) of - true -> trans(fun add_trie_route/1, [Route]); - false -> add_direct_route(Route) + true -> trans(fun insert_trie_route/1, [Route]); + false -> insert_direct_route(Route) end end. --spec(get_routes(emqx_topic:topic()) -> [emqx_types:route()]). -get_routes(Topic) -> +%% @doc Match routes +-spec(match_routes(emqx_topic:topic()) -> [emqx_types:route()]). +match_routes(Topic) when is_binary(Topic) -> + %% Optimize: routing table will be replicated to all router nodes. + Matched = mnesia:ets(fun emqx_trie:match/1, [Topic]), + lists:append([lookup_routes(To) || To <- [Topic | Matched]]). + +-spec(lookup_routes(emqx_topic:topic()) -> [emqx_types:route()]). +lookup_routes(Topic) -> ets:lookup(?ROUTE, Topic). --spec(delete_route(emqx_topic:topic() | emqx_types:route()) -> ok | {error, term()}). -delete_route(Topic) when is_binary(Topic) -> - delete_route(#route{topic = Topic, dest = node()}); -delete_route(Route = #route{topic = Topic}) -> - case get_mode() of - protected -> do_delete_route(Route); - undefined -> call(pick(Topic), {delete_route, Route}) - end. - --spec(delete_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). -delete_route(Topic, Dest) when is_binary(Topic) -> - delete_route(#route{topic = Topic, dest = Dest}). - -%% @private -do_delete_route(Route = #route{topic = Topic}) -> - case emqx_topic:wildcard(Topic) of - true -> trans(fun del_trie_route/1, [Route]); - false -> del_direct_route(Route) - end. - -spec(has_routes(emqx_topic:topic()) -> boolean()). has_routes(Topic) when is_binary(Topic) -> ets:member(?ROUTE, Topic). --spec(topics() -> list(emqx_topic:topic())). -topics() -> mnesia:dirty_all_keys(?ROUTE). +-spec(delete_route(emqx_topic:topic()) -> ok | {error, term()}). +delete_route(Topic) when is_binary(Topic) -> + delete_route(Topic, node()). -%% @doc Match routes -%% Optimize: routing table will be replicated to all router nodes. --spec(match_routes(emqx_topic:topic()) -> [emqx_types:route()]). -match_routes(Topic) when is_binary(Topic) -> - Matched = mnesia:ets(fun emqx_trie:match/1, [Topic]), - lists:append([get_routes(To) || To <- [Topic | Matched]]). +-spec(delete_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). +delete_route(Topic, Dest) when is_binary(Topic) -> + call(pick(Topic), {delete_route, Topic, Dest}). + +-spec(do_delete_route(emqx_topic:topic()) -> ok | {error, term()}). +do_delete_route(Topic) when is_binary(Topic) -> + do_delete_route(Topic, node()). + +-spec(do_delete_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). +do_delete_route(Topic, Dest) -> + Route = #route{topic = Topic, dest = Dest}, + case emqx_topic:wildcard(Topic) of + true -> trans(fun delete_trie_route/1, [Route]); + false -> delete_direct_route(Route) + end. + +-spec(topics() -> list(emqx_topic:topic())). +topics() -> + mnesia:dirty_all_keys(?ROUTE). %% @doc Print routes to a topic -spec(print_routes(emqx_topic:topic()) -> ok). @@ -143,13 +144,6 @@ print_routes(Topic) -> io:format("~s -> ~s~n", [To, Dest]) end, match_routes(Topic)). --spec(set_mode(protected | atom()) -> any()). -set_mode(Mode) when is_atom(Mode) -> - put('$router_mode', Mode). - --spec(get_mode() -> protected | undefined | atom()). -get_mode() -> get('$router_mode'). - call(Router, Msg) -> gen_server:call(Router, Msg, infinity). @@ -164,11 +158,13 @@ init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #{pool => Pool, id => Id}}. -handle_call({add_route, Route}, _From, State) -> - {reply, do_add_route(Route), State}; +handle_call({add_route, Topic, Dest}, _From, State) -> + Ok = do_add_route(Topic, Dest), + {reply, Ok, State}; -handle_call({delete_route, Route}, _From, State) -> - {reply, do_delete_route(Route), State}; +handle_call({delete_route, Topic, Dest}, _From, State) -> + Ok = do_delete_route(Topic, Dest), + {reply, Ok, State}; handle_call(Req, _From, State) -> emqx_logger:error("[Router] unexpected call: ~p", [Req]), @@ -192,23 +188,23 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -add_direct_route(Route) -> +insert_direct_route(Route) -> mnesia:async_dirty(fun mnesia:write/3, [?ROUTE, Route, sticky_write]). -add_trie_route(Route = #route{topic = Topic}) -> +insert_trie_route(Route = #route{topic = Topic}) -> case mnesia:wread({?ROUTE, Topic}) of [] -> emqx_trie:insert(Topic); _ -> ok end, mnesia:write(?ROUTE, Route, sticky_write). -del_direct_route(Route) -> +delete_direct_route(Route) -> mnesia:async_dirty(fun mnesia:delete_object/3, [?ROUTE, Route, sticky_write]). -del_trie_route(Route = #route{topic = Topic}) -> +delete_trie_route(Route = #route{topic = Topic}) -> case mnesia:wread({?ROUTE, Topic}) of [Route] -> %% Remove route and trie - mnesia:delete_object(?ROUTE, Route, sticky_write), + ok = mnesia:delete_object(?ROUTE, Route, sticky_write), emqx_trie:delete(Topic); [_|_] -> %% Remove route only mnesia:delete_object(?ROUTE, Route, sticky_write); @@ -219,7 +215,7 @@ del_trie_route(Route = #route{topic = Topic}) -> -spec(trans(function(), list(any())) -> ok | {error, term()}). trans(Fun, Args) -> case mnesia:transaction(Fun, Args) of - {atomic, _} -> ok; - {aborted, Error} -> {error, Error} + {atomic, Ok} -> Ok; + {aborted, Reason} -> {error, Reason} end. diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index c24b10715..efeaabc74 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -31,15 +31,11 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% internal export +%% Internal export -export([stats_fun/0]). -record(routing_node, {name, const = unused}). --record(state, {nodes = []}). --compile({no_auto_import, [monitor/1]}). - --define(SERVER, ?MODULE). -define(ROUTE, emqx_route). -define(ROUTING_NODE, emqx_routing_node). -define(LOCK, {?MODULE, cleanup_routes}). @@ -64,9 +60,9 @@ mnesia(copy) -> %%------------------------------------------------------------------------------ %% @doc Starts the router helper --spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). %% @doc Monitor routing node -spec(monitor(node() | {binary(), node()}) -> ok). @@ -84,18 +80,18 @@ monitor(Node) when is_atom(Node) -> %%------------------------------------------------------------------------------ init([]) -> - _ = ekka:monitor(membership), - _ = mnesia:subscribe({table, ?ROUTING_NODE, simple}), + ok = ekka:monitor(membership), + {ok, _} = mnesia:subscribe({table, ?ROUTING_NODE, simple}), Nodes = lists:foldl( fun(Node, Acc) -> case ekka:is_member(Node) of true -> Acc; - false -> _ = erlang:monitor_node(Node, true), + false -> true = erlang:monitor_node(Node, true), [Node | Acc] end end, [], mnesia:dirty_all_keys(?ROUTING_NODE)), emqx_stats:update_interval(route_stats, fun ?MODULE:stats_fun/0), - {ok, #state{nodes = Nodes}, hibernate}. + {ok, #{nodes => Nodes}, hibernate}. handle_call(Req, _From, State) -> emqx_logger:error("[RouterHelper] unexpected call: ~p", [Req]), @@ -105,24 +101,29 @@ handle_cast(Msg, State) -> emqx_logger:error("[RouterHelper] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({mnesia_table_event, {write, #routing_node{name = Node}, _}}, State = #state{nodes = Nodes}) -> - emqx_logger:info("[RouterHelper] write routing node: ~s", [Node]), +handle_info({mnesia_table_event, {write, {?ROUTING_NODE, Node, _}, _}}, State = #{nodes := Nodes}) -> case ekka:is_member(Node) orelse lists:member(Node, Nodes) of - true -> {noreply, State}; - false -> _ = erlang:monitor_node(Node, true), - {noreply, State#state{nodes = [Node | Nodes]}} + true -> {noreply, State}; + false -> + true = erlang:monitor_node(Node, true), + {noreply, State#{nodes := [Node | Nodes]}} end; -handle_info({mnesia_table_event, _Event}, State) -> +handle_info({mnesia_table_event, {delete, {?ROUTING_NODE, _Node}, _}}, State) -> + %% ignore {noreply, State}; -handle_info({nodedown, Node}, State = #state{nodes = Nodes}) -> +handle_info({mnesia_table_event, Event}, State) -> + emqx_logger:error("[RouterHelper] unexpected mnesia_table_event: ~p", [Event]), + {noreply, State}; + +handle_info({nodedown, Node}, State = #{nodes := Nodes}) -> global:trans({?LOCK, self()}, fun() -> mnesia:transaction(fun cleanup_routes/1, [Node]) end), - mnesia:dirty_delete(?ROUTING_NODE, Node), - {noreply, State#state{nodes = lists:delete(Node, Nodes)}, hibernate}; + ok = mnesia:dirty_delete(?ROUTING_NODE, Node), + {noreply, State#{nodes := lists:delete(Node, Nodes)}, hibernate}; handle_info({membership, {mnesia, down, Node}}, State) -> handle_info({nodedown, Node}, State); @@ -134,8 +135,8 @@ handle_info(Info, State) -> emqx_logger:error("[RouteHelper] unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{}) -> - ekka:unmonitor(membership), +terminate(_Reason, _State) -> + ok = ekka:unmonitor(membership), emqx_stats:cancel_update(route_stats), mnesia:unsubscribe({table, ?ROUTING_NODE, simple}). diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index d1d0d921d..b7e41213b 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -252,7 +252,6 @@ subscribers(Group, Topic) -> %%------------------------------------------------------------------------------ init([]) -> - _ = emqx_router:set_mode(protected), mnesia:subscribe({table, ?TAB, simple}), {atomic, PMon} = mnesia:transaction(fun init_monitors/0), ok = emqx_tables:new(?SHARED_SUBS, [protected, bag]), @@ -269,7 +268,7 @@ handle_call({subscribe, Group, Topic, SubPid}, _From, State = #state{pmon = PMon mnesia:dirty_write(?TAB, record(Group, Topic, SubPid)), case ets:member(?SHARED_SUBS, {Group, Topic}) of true -> ok; - false -> ok = emqx_router:add_route(Topic, {Group, node()}) + false -> ok = emqx_router:do_add_route(Topic, {Group, node()}) end, ok = maybe_insert_alive_tab(SubPid), true = ets:insert(?SHARED_SUBS, {{Group, Topic}, SubPid}), @@ -280,7 +279,7 @@ handle_call({unsubscribe, Group, Topic, SubPid}, _From, State) -> true = ets:delete_object(?SHARED_SUBS, {{Group, Topic}, SubPid}), case ets:member(?SHARED_SUBS, {Group, Topic}) of true -> ok; - false -> ok = emqx_router:delete_route(Topic, {Group, node()}) + false -> ok = emqx_router:do_delete_route(Topic, {Group, node()}) end, {reply, ok, State}; @@ -334,7 +333,7 @@ cleanup_down(SubPid) -> true = ets:delete_object(?SHARED_SUBS, {{Group, Topic}, SubPid}), case ets:member(?SHARED_SUBS, {Group, Topic}) of true -> ok; - false -> ok = emqx_router:delete_route(Topic, {Group, node()}) + false -> ok = emqx_router:do_delete_route(Topic, {Group, node()}) end end, mnesia:dirty_match_object(#emqx_shared_subscription{_ = '_', subpid = SubPid})). diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index 79f6042b7..27ff52827 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -36,7 +36,7 @@ %% @doc Create or replicate trie tables. -spec(mnesia(boot | copy) -> ok). mnesia(boot) -> - %% Optimize + %% Optimize storage StoreProps = [{ets, [{read_concurrency, true}, {write_concurrency, true}]}], %% Trie table @@ -72,7 +72,7 @@ insert(Topic) when is_binary(Topic) -> write_trie_node(TrieNode#trie_node{topic = Topic}); [] -> %% Add trie path - lists:foreach(fun add_path/1, emqx_topic:triples(Topic)), + ok = lists:foreach(fun add_path/1, emqx_topic:triples(Topic)), %% Add last node write_trie_node(#trie_node{node_id = Topic, topic = Topic}) end. @@ -93,7 +93,7 @@ lookup(NodeId) -> delete(Topic) when is_binary(Topic) -> case mnesia:wread({?TRIE_NODE, Topic}) of [#trie_node{edge_count = 0}] -> - mnesia:delete({?TRIE_NODE, Topic}), + ok = mnesia:delete({?TRIE_NODE, Topic}), delete_path(lists:reverse(emqx_topic:triples(Topic))); [TrieNode] -> write_trie_node(TrieNode#trie_node{topic = undefined}); @@ -112,12 +112,12 @@ add_path({Node, Word, Child}) -> [TrieNode = #trie_node{edge_count = Count}] -> case mnesia:wread({?TRIE, Edge}) of [] -> - write_trie_node(TrieNode#trie_node{edge_count = Count + 1}), + ok = write_trie_node(TrieNode#trie_node{edge_count = Count + 1}), write_trie(#trie{edge = Edge, node_id = Child}); [_] -> ok end; [] -> - write_trie_node(#trie_node{node_id = Node, edge_count = 1}), + ok = write_trie_node(#trie_node{node_id = Node, edge_count = 1}), write_trie(#trie{edge = Edge, node_id = Child}) end. @@ -154,10 +154,10 @@ match_node(NodeId, [W|Words], ResAcc) -> delete_path([]) -> ok; delete_path([{NodeId, Word, _} | RestPath]) -> - mnesia:delete({?TRIE, #trie_edge{node_id = NodeId, word = Word}}), - case mnesia:read(?TRIE_NODE, NodeId) of + ok = mnesia:delete({?TRIE, #trie_edge{node_id = NodeId, word = Word}}), + case mnesia:wread({?TRIE_NODE, NodeId}) of [#trie_node{edge_count = 1, topic = undefined}] -> - mnesia:delete({?TRIE_NODE, NodeId}), + ok = mnesia:delete({?TRIE_NODE, NodeId}), delete_path(RestPath); [TrieNode = #trie_node{edge_count = 1, topic = _}] -> write_trie_node(TrieNode#trie_node{edge_count = 0}); @@ -167,9 +167,11 @@ delete_path([{NodeId, Word, _} | RestPath]) -> mnesia:abort({node_not_found, NodeId}) end. +%% @private write_trie(Trie) -> mnesia:write(?TRIE, Trie, write). +%% @private write_trie_node(TrieNode) -> mnesia:write(?TRIE_NODE, TrieNode, write). diff --git a/test/emqx_router_SUITE.erl b/test/emqx_router_SUITE.erl index e317ec7b3..c115fd0cd 100644 --- a/test/emqx_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -21,17 +21,16 @@ -compile(nowarn_export_all). -define(R, emqx_router). --define(TABS, [emqx_route, emqx_trie, emqx_trie_node]). all() -> [{group, route}]. groups() -> [{route, [sequence], - [add_del_route, - match_routes, - has_routes, - router_add_del]}]. + [t_add_delete, + t_do_add_delete, + t_match_routes, + t_has_routes]}]. init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), @@ -47,77 +46,47 @@ init_per_testcase(_TestCase, Config) -> end_per_testcase(_TestCase, _Config) -> clear_tables(). -add_del_route(_) -> - From = {self(), make_ref()}, - ?R:add_route(From, <<"a/b/c">>, node()), - timer:sleep(1), - - ?R:add_route(From, <<"a/b/c">>, node()), - timer:sleep(1), - - ?R:add_route(From, <<"a/+/b">>, node()), - ct:log("Topics: ~p ~n", [emqx_topic:wildcard(<<"a/+/b">>)]), - timer:sleep(1), - +t_add_delete(_) -> + ?R:add_route(<<"a/b/c">>, node()), + ?R:add_route(<<"a/b/c">>, node()), + ?R:add_route(<<"a/+/b">>, node()), ?assertEqual([<<"a/+/b">>, <<"a/b/c">>], lists:sort(?R:topics())), - ?R:del_route(From, <<"a/b/c">>, node()), + ?R:delete_route(<<"a/b/c">>), + ?R:delete_route(<<"a/+/b">>, node()), + ?assertEqual([], ?R:topics()). - ?R:del_route(From, <<"a/+/b">>, node()), - timer:sleep(120), - ?assertEqual([], lists:sort(?R:topics())). +t_do_add_delete(_) -> + ?R:do_add_route(<<"a/b/c">>, node()), + ?R:do_add_route(<<"a/b/c">>, node()), + ?R:do_add_route(<<"a/+/b">>, node()), + ?assertEqual([<<"a/+/b">>, <<"a/b/c">>], lists:sort(?R:topics())), -match_routes(_) -> - From = {self(), make_ref()}, - ?R:add_route(From, <<"a/b/c">>, node()), - ?R:add_route(From, <<"a/+/c">>, node()), - ?R:add_route(From, <<"a/b/#">>, node()), - ?R:add_route(From, <<"#">>, node()), - timer:sleep(1000), + ?R:do_delete_route(<<"a/b/c">>, node()), + ?R:do_delete_route(<<"a/+/b">>), + ?assertEqual([], ?R:topics()). + +t_match_routes(_) -> + ?R:add_route(<<"a/b/c">>, node()), + ?R:add_route(<<"a/+/c">>, node()), + ?R:add_route(<<"a/b/#">>, node()), + ?R:add_route(<<"#">>, node()), ?assertEqual([#route{topic = <<"#">>, dest = node()}, #route{topic = <<"a/+/c">>, dest = node()}, #route{topic = <<"a/b/#">>, dest = node()}, #route{topic = <<"a/b/c">>, dest = node()}], - lists:sort(?R:match_routes(<<"a/b/c">>))). + lists:sort(?R:match_routes(<<"a/b/c">>))), + ?R:delete_route(<<"a/b/c">>, node()), + ?R:delete_route(<<"a/+/c">>, node()), + ?R:delete_route(<<"a/b/#">>, node()), + ?R:delete_route(<<"#">>, node()), + ?assertEqual([], lists:sort(?R:match_routes(<<"a/b/c">>))). -has_routes(_) -> - From = {self(), make_ref()}, - ?R:add_route(From, <<"devices/+/messages">>, node()), - timer:sleep(200), - ?assert(?R:has_routes(<<"devices/+/messages">>)). +t_has_routes(_) -> + ?R:add_route(<<"devices/+/messages">>, node()), + ?assert(?R:has_routes(<<"devices/+/messages">>)), + ?R:delete_route(<<"devices/+/messages">>). clear_tables() -> - lists:foreach(fun mnesia:clear_table/1, ?TABS). - -router_add_del(_) -> - ?R:add_route(<<"#">>), - ?R:add_route(<<"a/b/c">>, node()), - ?R:add_route(<<"+/#">>), - Routes = [R1, R2 | _] = [ - #route{topic = <<"#">>, dest = node()}, - #route{topic = <<"+/#">>, dest = node()}, - #route{topic = <<"a/b/c">>, dest = node()}], - timer:sleep(500), - ?assertEqual(Routes, lists:sort(?R:match_routes(<<"a/b/c">>))), - - ?R:print_routes(<<"a/b/c">>), - - %% Batch Add - lists:foreach(fun(R) -> ?R:add_route(R) end, Routes), - ?assertEqual(Routes, lists:sort(?R:match_routes(<<"a/b/c">>))), - - %% Del - ?R:del_route(<<"a/b/c">>, node()), - timer:sleep(500), - [R1, R2] = lists:sort(?R:match_routes(<<"a/b/c">>)), - {atomic, []} = mnesia:transaction(fun emqx_trie:lookup/1, [<<"a/b/c">>]), - - %% Batch Del - R3 = #route{topic = <<"#">>, dest = 'a@127.0.0.1'}, - ?R:add_route(R3), - ?R:del_route(<<"#">>), - ?R:del_route(R2), - ?R:del_route(R3), - timer:sleep(500), - [] = lists:sort(?R:match_routes(<<"a/b/c">>)). + lists:foreach(fun mnesia:clear_table/1, [emqx_route, emqx_trie, emqx_trie_node]). diff --git a/test/emqx_trie_SUITE.erl b/test/emqx_trie_SUITE.erl index 85637a447..09226979f 100644 --- a/test/emqx_trie_SUITE.erl +++ b/test/emqx_trie_SUITE.erl @@ -47,36 +47,36 @@ t_insert(_) -> edge_count = 3, topic = <<"sensor">>, flags = undefined}, - {atomic, [TN]} = mnesia:transaction( - fun() -> - ?TRIE:insert(<<"sensor/1/metric/2">>), - ?TRIE:insert(<<"sensor/+/#">>), - ?TRIE:insert(<<"sensor/#">>), - ?TRIE:insert(<<"sensor">>), - ?TRIE:insert(<<"sensor">>), - ?TRIE:lookup(<<"sensor">>) - end). + Fun = fun() -> + ?TRIE:insert(<<"sensor/1/metric/2">>), + ?TRIE:insert(<<"sensor/+/#">>), + ?TRIE:insert(<<"sensor/#">>), + ?TRIE:insert(<<"sensor">>), + ?TRIE:insert(<<"sensor">>), + ?TRIE:lookup(<<"sensor">>) + end, + ?assertEqual({atomic, [TN]}, mnesia:transaction(Fun)). t_match(_) -> Machted = [<<"sensor/+/#">>, <<"sensor/#">>], - {atomic, Machted} = mnesia:transaction( - fun() -> - ?TRIE:insert(<<"sensor/1/metric/2">>), - ?TRIE:insert(<<"sensor/+/#">>), - ?TRIE:insert(<<"sensor/#">>), - ?TRIE:match(<<"sensor/1">>) - end). + Fun = fun() -> + ?TRIE:insert(<<"sensor/1/metric/2">>), + ?TRIE:insert(<<"sensor/+/#">>), + ?TRIE:insert(<<"sensor/#">>), + ?TRIE:match(<<"sensor/1">>) + end, + ?assertEqual({atomic, Machted}, mnesia:transaction(Fun)). t_match2(_) -> Matched = {[<<"+/+/#">>, <<"+/#">>, <<"#">>], []}, - {atomic, Matched} = mnesia:transaction( - fun() -> - ?TRIE:insert(<<"#">>), - ?TRIE:insert(<<"+/#">>), - ?TRIE:insert(<<"+/+/#">>), - {?TRIE:match(<<"a/b/c">>), - ?TRIE:match(<<"$SYS/broker/zenmq">>)} - end). + Fun = fun() -> + ?TRIE:insert(<<"#">>), + ?TRIE:insert(<<"+/#">>), + ?TRIE:insert(<<"+/+/#">>), + {?TRIE:match(<<"a/b/c">>), + ?TRIE:match(<<"$SYS/broker/zenmq">>)} + end, + ?assertEqual({atomic, Matched}, mnesia:transaction(Fun)). t_match3(_) -> Topics = [<<"d/#">>, <<"a/b/c">>, <<"a/b/+">>, <<"a/#">>, <<"#">>, <<"$SYS/#">>], @@ -91,43 +91,42 @@ t_delete(_) -> edge_count = 2, topic = undefined, flags = undefined}, - {atomic, [TN]} = mnesia:transaction( - fun() -> - ?TRIE:insert(<<"sensor/1/#">>), - ?TRIE:insert(<<"sensor/1/metric/2">>), - ?TRIE:insert(<<"sensor/1/metric/3">>), - ?TRIE:delete(<<"sensor/1/metric/2">>), - ?TRIE:delete(<<"sensor/1/metric">>), - ?TRIE:delete(<<"sensor/1/metric">>), - ?TRIE:lookup(<<"sensor/1">>) - end). + Fun = fun() -> + ?TRIE:insert(<<"sensor/1/#">>), + ?TRIE:insert(<<"sensor/1/metric/2">>), + ?TRIE:insert(<<"sensor/1/metric/3">>), + ?TRIE:delete(<<"sensor/1/metric/2">>), + ?TRIE:delete(<<"sensor/1/metric">>), + ?TRIE:delete(<<"sensor/1/metric">>), + ?TRIE:lookup(<<"sensor/1">>) + end, + ?assertEqual({atomic, [TN]}, mnesia:transaction(Fun)). t_delete2(_) -> - {atomic, {[], []}} = mnesia:transaction( - fun() -> - ?TRIE:insert(<<"sensor">>), - ?TRIE:insert(<<"sensor/1/metric/2">>), - ?TRIE:insert(<<"sensor/1/metric/3">>), - ?TRIE:delete(<<"sensor">>), - ?TRIE:delete(<<"sensor/1/metric/2">>), - ?TRIE:delete(<<"sensor/1/metric/3">>), - {?TRIE:lookup(<<"sensor">>), - ?TRIE:lookup(<<"sensor/1">>)} - end). + Fun = fun() -> + ?TRIE:insert(<<"sensor">>), + ?TRIE:insert(<<"sensor/1/metric/2">>), + ?TRIE:insert(<<"sensor/1/metric/3">>), + ?TRIE:delete(<<"sensor">>), + ?TRIE:delete(<<"sensor/1/metric/2">>), + ?TRIE:delete(<<"sensor/1/metric/3">>), + {?TRIE:lookup(<<"sensor">>), ?TRIE:lookup(<<"sensor/1">>)} + end, + ?assertEqual({atomic, {[], []}}, mnesia:transaction(Fun)). t_delete3(_) -> - {atomic, {[], []}} = mnesia:transaction( - fun() -> - ?TRIE:insert(<<"sensor/+">>), - ?TRIE:insert(<<"sensor/+/metric/2">>), - ?TRIE:insert(<<"sensor/+/metric/3">>), - ?TRIE:delete(<<"sensor/+/metric/2">>), - ?TRIE:delete(<<"sensor/+/metric/3">>), - ?TRIE:delete(<<"sensor">>), - ?TRIE:delete(<<"sensor/+">>), - ?TRIE:delete(<<"sensor/+/unknown">>), - {?TRIE:lookup(<<"sensor">>), ?TRIE:lookup(<<"sensor/+">>)} - end). + Fun = fun() -> + ?TRIE:insert(<<"sensor/+">>), + ?TRIE:insert(<<"sensor/+/metric/2">>), + ?TRIE:insert(<<"sensor/+/metric/3">>), + ?TRIE:delete(<<"sensor/+/metric/2">>), + ?TRIE:delete(<<"sensor/+/metric/3">>), + ?TRIE:delete(<<"sensor">>), + ?TRIE:delete(<<"sensor/+">>), + ?TRIE:delete(<<"sensor/+/unknown">>), + {?TRIE:lookup(<<"sensor">>), ?TRIE:lookup(<<"sensor/+">>)} + end, + ?assertEqual({atomic, {[], []}}, mnesia:transaction(Fun)). clear_tables() -> lists:foreach(fun mnesia:clear_table/1, ?TRIE_TABS). From 7074707d6459a7698c490c044439adbfb7b1da16 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 11 Dec 2018 14:06:23 +0800 Subject: [PATCH 468/520] Add t_mnesia/1 test case --- test/emqx_trie_SUITE.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/emqx_trie_SUITE.erl b/test/emqx_trie_SUITE.erl index 09226979f..500fe3574 100644 --- a/test/emqx_trie_SUITE.erl +++ b/test/emqx_trie_SUITE.erl @@ -24,7 +24,7 @@ -define(TRIE_TABS, [emqx_trie, emqx_trie_node]). all() -> - [t_insert, t_match, t_match2, t_match3, t_delete, t_delete2, t_delete3]. + [t_mnesia, t_insert, t_match, t_match2, t_match3, t_delete, t_delete2, t_delete3]. init_per_suite(Config) -> application:load(emqx), @@ -42,6 +42,9 @@ init_per_testcase(_TestCase, Config) -> end_per_testcase(_TestCase, _Config) -> clear_tables(). +t_mnesia(_) -> + ok = ?TRIE:mnesia(copy). + t_insert(_) -> TN = #trie_node{node_id = <<"sensor">>, edge_count = 3, From 47e3cd3692722400153e95463cca8d8532615607 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 12 Dec 2018 13:34:13 +0800 Subject: [PATCH 469/520] Improve the subscription sharding. --- src/emqx.erl | 6 +- src/emqx_broker.erl | 217 +++++++++++++++++++---------------- src/emqx_broker_helper.erl | 80 ++++++++----- src/emqx_router_helper.erl | 2 +- src/emqx_sequence.erl | 1 + src/emqx_sm.erl | 2 +- src/emqx_tables.erl | 14 +++ test/emqx_sequence_SUITE.erl | 3 +- test/emqx_tables_SUITE.erl | 8 +- 9 files changed, 197 insertions(+), 136 deletions(-) diff --git a/src/emqx.erl b/src/emqx.erl index 3792cc4f8..76e966a59 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -22,7 +22,7 @@ %% PubSub API -export([subscribe/1, subscribe/2, subscribe/3]). -export([publish/1]). --export([unsubscribe/1, unsubscribe/2]). +-export([unsubscribe/1]). %% PubSub management API -export([topics/0, subscriptions/1, subscribers/1, subscribed/2]). @@ -88,10 +88,6 @@ publish(Msg) -> unsubscribe(Topic) -> emqx_broker:unsubscribe(iolist_to_binary(Topic)). --spec(unsubscribe(emqx_topic:topic() | string(), emqx_types:subid()) -> ok). -unsubscribe(Topic, SubId) -> - emqx_broker:unsubscribe(iolist_to_binary(Topic), SubId). - %%------------------------------------------------------------------------------ %% PubSub management API %%------------------------------------------------------------------------------ diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index a00dc17b8..9ed4aad06 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -20,7 +20,7 @@ -export([start_link/2]). -export([subscribe/1, subscribe/2, subscribe/3]). --export([unsubscribe/1, unsubscribe/2]). +-export([unsubscribe/1]). -export([subscriber_down/1]). -export([publish/1, safe_publish/1]). -export([dispatch/2]). @@ -35,6 +35,8 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-import(emqx_tables, [lookup_value/2, lookup_value/3]). + -ifdef(TEST). -compile(export_all). -compile(nowarn_export_all). @@ -42,8 +44,7 @@ -define(BROKER, ?MODULE). -%% ETS tables --define(SUBID, emqx_subid). +%% ETS tables for PubSub -define(SUBOPTION, emqx_suboption). -define(SUBSCRIBER, emqx_subscriber). -define(SUBSCRIPTION, emqx_subscription). @@ -65,9 +66,6 @@ start_link(Pool, Id) -> create_tabs() -> TabOpts = [public, {read_concurrency, true}, {write_concurrency, true}], - %% SubId: SubId -> SubPid - ok = emqx_tables:new(?SUBID, [set | TabOpts]), - %% SubOption: {SubPid, Topic} -> SubOption ok = emqx_tables:new(?SUBOPTION, [set | TabOpts]), @@ -76,7 +74,7 @@ create_tabs() -> ok = emqx_tables:new(?SUBSCRIPTION, [duplicate_bag | TabOpts]), %% Subscriber: Topic -> SubPid1, SubPid2, SubPid3, ... - %% bag: o(n) insert + %% bag: o(n) insert:( ok = emqx_tables:new(?SUBSCRIBER, [bag | TabOpts]). %%------------------------------------------------------------------------------ @@ -98,28 +96,37 @@ subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map SubPid = self(), case ets:member(?SUBOPTION, {SubPid, Topic}) of false -> - ok = emqx_broker_helper:monitor(SubPid, SubId), - %% true = ets:insert(?SUBID, {SubId, SubPid}), - true = ets:insert(?SUBSCRIPTION, {SubPid, Topic}), - case maps:get(share, SubOpts, undefined) of - undefined -> - Shard = emqx_broker_helper:get_shard(SubPid, Topic), - case Shard of - 0 -> true = ets:insert(?SUBSCRIBER, {Topic, SubPid}); - I -> - true = ets:insert(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), - true = ets:insert(?SUBSCRIBER, {Topic, {shard, I}}) - end, - SubOpts1 = maps:put(shard, Shard, SubOpts), - true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts1}), - call(pick({Topic, Shard}), {subscribe, Topic}); - Group -> %% Shard subscription - true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), - emqx_shared_sub:subscribe(Group, Topic, SubPid) - end; + ok = emqx_broker_helper:monitor_sub(SubPid, SubId), + do_subscribe(Topic, SubPid, with_subid(SubId, SubOpts)); true -> ok end. +with_subid(undefined, SubOpts) -> + SubOpts; +with_subid(SubId, SubOpts) -> + maps:put(subid, SubId, SubOpts). + +%% @private +do_subscribe(Topic, SubPid, SubOpts) -> + true = ets:insert(?SUBSCRIPTION, {SubPid, Topic}), + Group = maps:get(share, SubOpts, undefined), + do_subscribe(Group, Topic, SubPid, SubOpts). + +do_subscribe(undefined, Topic, SubPid, SubOpts) -> + case emqx_broker_helper:get_sub_shard(SubPid, Topic) of + 0 -> true = ets:insert(?SUBSCRIBER, {Topic, SubPid}), + true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), + call(pick(Topic), {subscribe, Topic}); + I -> true = ets:insert(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + true = ets:insert(?SUBOPTION, {{SubPid, Topic}, maps:put(shard, I, SubOpts)}), + call(pick({Topic, I}), {subscribe, Topic, I}) + end; + +%% Shared subscription +do_subscribe(Group, Topic, SubPid, SubOpts) -> + true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), + emqx_shared_sub:subscribe(Group, Topic, SubPid). + %%------------------------------------------------------------------------------ %% Unsubscribe API %%------------------------------------------------------------------------------ @@ -130,33 +137,26 @@ unsubscribe(Topic) when is_binary(Topic) -> case ets:lookup(?SUBOPTION, {SubPid, Topic}) of [{_, SubOpts}] -> _ = emqx_broker_helper:reclaim_seq(Topic), - case maps:get(share, SubOpts, undefined) of - undefined -> - case maps:get(shard, SubOpts, 0) of - 0 -> - true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), - ok = cast(pick(Topic), {unsubscribed, Topic}); - I -> - true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), - case ets:member(emqx_subscriber, {shard, Topic, I}) of - true -> ok; - false -> ets:delete_object(?SUBSCRIBER, {Topic, {shard, I}}) - end, - ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) - end; - Group -> - ok = emqx_shared_sub:unsubscribe(Group, Topic, SubPid) - end, - true = ets:delete_object(?SUBSCRIPTION, {SubPid, Topic}), - %%true = ets:delete_object(?SUBID, {SubId, SubPid}), - true = ets:delete(?SUBOPTION, {SubPid, Topic}), - ok; + do_unsubscribe(Topic, SubPid, SubOpts); [] -> ok end. --spec(unsubscribe(emqx_topic:topic(), emqx_types:subid()) -> ok). -unsubscribe(Topic, _SubId) when is_binary(Topic) -> - unsubscribe(Topic). +do_unsubscribe(Topic, SubPid, SubOpts) -> + true = ets:delete(?SUBOPTION, {SubPid, Topic}), + true = ets:delete_object(?SUBSCRIPTION, {SubPid, Topic}), + Group = maps:get(share, SubOpts, undefined), + do_unsubscribe(Group, Topic, SubPid, SubOpts). + +do_unsubscribe(undefined, Topic, SubPid, SubOpts) -> + case maps:get(shard, SubOpts, 0) of + 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), + cast(pick(Topic), {unsubscribed, Topic}); + I -> true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + cast(pick({Topic, I}), {unsubscribed, Topic, I}) + end; + +do_unsubscribe(Group, Topic, SubPid, _SubOpts) -> + emqx_shared_sub:unsubscribe(Group, Topic, SubPid). %%------------------------------------------------------------------------------ %% Publish @@ -241,23 +241,28 @@ dispatch(Topic, Delivery = #delivery{message = Msg, results = Results}) -> inc_dropped_cnt(Topic), Delivery; [Sub] -> %% optimize? - dispatch(Sub, Topic, Msg), - Delivery#delivery{results = [{dispatch, Topic, 1}|Results]}; + Cnt = dispatch(Sub, Topic, Msg), + Delivery#delivery{results = [{dispatch, Topic, Cnt}|Results]}; Subs -> - Count = lists:foldl( - fun(Sub, Acc) -> - dispatch(Sub, Topic, Msg), Acc + 1 - end, 0, Subs), - Delivery#delivery{results = [{dispatch, Topic, Count}|Results]} + Cnt = lists:foldl( + fun(Sub, Acc) -> + dispatch(Sub, Topic, Msg) + Acc + end, 0, Subs), + Delivery#delivery{results = [{dispatch, Topic, Cnt}|Results]} end. dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> - SubPid ! {dispatch, Topic, Msg}; + case erlang:is_process_alive(SubPid) of + true -> + SubPid ! {dispatch, Topic, Msg}, + 1; + false -> 0 + end; dispatch({shard, I}, Topic, Msg) -> - - lists:foreach(fun(SubPid) -> - SubPid ! {dispatch, Topic, Msg} - end, safe_lookup_element(?SUBSCRIBER, {shard, Topic, I}, [])). + lists:foldl( + fun(SubPid, Cnt) -> + dispatch(SubPid, Topic, Msg) + Cnt + end, 0, subscribers({shard, Topic, I})). inc_dropped_cnt(<<"$SYS/", _/binary>>) -> ok; @@ -265,8 +270,10 @@ inc_dropped_cnt(_Topic) -> emqx_metrics:inc('messages/dropped'). -spec(subscribers(emqx_topic:topic()) -> [pid()]). -subscribers(Topic) -> - safe_lookup_element(?SUBSCRIBER, Topic, []). +subscribers(Topic) when is_binary(Topic) -> + lookup_value(?SUBSCRIBER, Topic, []); +subscribers(Shard = {shard, _Topic, _I}) -> + lookup_value(?SUBSCRIBER, Shard, []). %%------------------------------------------------------------------------------ %% Subscriber is down @@ -275,27 +282,21 @@ subscribers(Topic) -> -spec(subscriber_down(pid()) -> true). subscriber_down(SubPid) -> lists:foreach( - fun(Sub = {_Pid, Topic}) -> - case ets:lookup(?SUBOPTION, Sub) of - [{_, SubOpts}] -> + fun(Topic) -> + case lookup_value(?SUBOPTION, {SubPid, Topic}) of + SubOpts when is_map(SubOpts) -> _ = emqx_broker_helper:reclaim_seq(Topic), + true = ets:delete(?SUBOPTION, {SubPid, Topic}), case maps:get(shard, SubOpts, 0) of - 0 -> - true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), - ok = cast(pick(Topic), {unsubscribed, Topic}); - I -> - true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), - case ets:member(emqx_subscriber, {shard, Topic, I}) of - true -> ok; - false -> ets:delete_object(?SUBSCRIBER, {Topic, {shard, I}}) - end, - ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) - end, - ets:delete(?SUBOPTION, Sub); - [] -> ok + 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), + ok = cast(pick(Topic), {unsubscribed, Topic}); + I -> true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) + end; + undefined -> ok end - end, ets:lookup(?SUBSCRIPTION, SubPid)), - true = ets:delete(?SUBSCRIPTION, SubPid). + end, lookup_value(?SUBSCRIPTION, SubPid, [])), + ets:delete(?SUBSCRIPTION, SubPid). %%------------------------------------------------------------------------------ %% Management APIs @@ -303,20 +304,32 @@ subscriber_down(SubPid) -> -spec(subscriptions(pid() | emqx_types:subid()) -> [{emqx_topic:topic(), emqx_types:subopts()}]). -subscriptions(SubPid) -> - [{Topic, safe_lookup_element(?SUBOPTION, {SubPid, Topic}, #{})} - || Topic <- safe_lookup_element(?SUBSCRIPTION, SubPid, [])]. +subscriptions(SubPid) when is_pid(SubPid) -> + [{Topic, lookup_value(?SUBOPTION, {SubPid, Topic}, #{})} + || Topic <- lookup_value(?SUBSCRIPTION, SubPid, [])]; +subscriptions(SubId) -> + case emqx_broker_helper:lookup_subpid(SubId) of + SubPid when is_pid(SubPid) -> + subscriptions(SubPid); + undefined -> [] + end. -spec(subscribed(pid(), emqx_topic:topic()) -> boolean()). subscribed(SubPid, Topic) when is_pid(SubPid) -> ets:member(?SUBOPTION, {SubPid, Topic}); subscribed(SubId, Topic) when ?is_subid(SubId) -> - %%FIXME:... SubId -> SubPid - ets:member(?SUBOPTION, {SubId, Topic}). + SubPid = emqx_broker_helper:lookup_subpid(SubId), + ets:member(?SUBOPTION, {SubPid, Topic}). --spec(get_subopts(pid(), emqx_topic:topic()) -> emqx_types:subopts()). +-spec(get_subopts(pid(), emqx_topic:topic()) -> emqx_types:subopts() | undefined). get_subopts(SubPid, Topic) when is_pid(SubPid), is_binary(Topic) -> - safe_lookup_element(?SUBOPTION, {SubPid, Topic}, #{}). + lookup_value(?SUBOPTION, {SubPid, Topic}); +get_subopts(SubId, Topic) when ?is_subid(SubId) -> + case emqx_broker_helper:lookup_subpid(SubId) of + SubPid when is_pid(SubPid) -> + get_subopts(SubPid, Topic); + undefined -> undefined + end. -spec(set_subopts(emqx_topic:topic(), emqx_types:subopts()) -> boolean()). set_subopts(Topic, NewOpts) when is_binary(Topic), is_map(NewOpts) -> @@ -331,9 +344,6 @@ set_subopts(Topic, NewOpts) when is_binary(Topic), is_map(NewOpts) -> topics() -> emqx_router:topics(). -safe_lookup_element(Tab, Key, Def) -> - try ets:lookup_element(Tab, Key, 2) catch error:badarg -> Def end. - %%------------------------------------------------------------------------------ %% Stats fun %%------------------------------------------------------------------------------ @@ -372,10 +382,15 @@ init([Pool, Id]) -> {ok, #{pool => Pool, id => Id}}. handle_call({subscribe, Topic}, _From, State) -> - Ok = case get(Topic) of + Ok = emqx_router:do_add_route(Topic), + {reply, Ok, State}; + +handle_call({subscribe, Topic, I}, _From, State) -> + Ok = case get(Shard = {Topic, I}) of undefined -> - _ = put(Topic, true), - emqx_router:do_add_route(Topic); + _ = put(Shard, true), + true = ets:insert(?SUBSCRIBER, {Topic, {shard, I}}), + cast(pick(Topic), {subscribe, Topic}); true -> ok end, {reply, Ok, State}; @@ -384,11 +399,18 @@ handle_call(Req, _From, State) -> emqx_logger:error("[Broker] unexpected call: ~p", [Req]), {reply, ignored, State}. +handle_cast({subscribe, Topic}, State) -> + case emqx_router:do_add_route(Topic) of + ok -> ok; + {error, Reason} -> + emqx_logger:error("[Broker] Failed to add route: ~p", [Reason]) + end, + {noreply, State}; + handle_cast({unsubscribed, Topic}, State) -> case ets:member(?SUBSCRIBER, Topic) of false -> - _ = erase(Topic), - emqx_router:do_delete_route(Topic); + _ = emqx_router:do_delete_route(Topic); true -> ok end, {noreply, State}; @@ -396,6 +418,7 @@ handle_cast({unsubscribed, Topic}, State) -> handle_cast({unsubscribed, Topic, I}, State) -> case ets:member(?SUBSCRIBER, {shard, Topic, I}) of false -> + _ = erase({Topic, I}), true = ets:delete_object(?SUBSCRIBER, {Topic, {shard, I}}), cast(pick(Topic), {unsubscribed, Topic}); true -> ok diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index d3e7f9d37..7d514e31d 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -16,44 +16,56 @@ -behaviour(gen_server). --compile({no_auto_import, [monitor/2]}). - -export([start_link/0]). --export([monitor/2]). --export([get_shard/2]). +-export([register_sub/2]). +-export([lookup_subid/1, lookup_subpid/1]). +-export([get_sub_shard/2]). -export([create_seq/1, reclaim_seq/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(HELPER, ?MODULE). +-define(SUBID, emqx_subid). -define(SUBMON, emqx_submon). -define(SUBSEQ, emqx_subseq). - --record(state, {pmon :: emqx_pmon:pmon()}). +-define(SHARD, 1024). -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?HELPER}, ?MODULE, [], []). --spec(monitor(pid(), emqx_types:subid()) -> ok). -monitor(SubPid, SubId) when is_pid(SubPid) -> +-spec(register_sub(pid(), emqx_types:subid()) -> ok). +register_sub(SubPid, SubId) when is_pid(SubPid) -> case ets:lookup(?SUBMON, SubPid) of [] -> - gen_server:cast(?HELPER, {monitor, SubPid, SubId}); + gen_server:cast(?HELPER, {register_sub, SubPid, SubId}); [{_, SubId}] -> ok; _Other -> error(subid_conflict) end. --spec(get_shard(pid(), emqx_topic:topic()) -> non_neg_integer()). -get_shard(SubPid, Topic) -> +-spec(lookup_subid(pid()) -> emqx_types:subid() | undefined). +lookup_subid(SubPid) when is_pid(SubPid) -> + emqx_tables:lookup_value(?SUBMON, SubPid). + +-spec(lookup_subpid(emqx_types:subid()) -> pid()). +lookup_subpid(SubId) -> + emqx_tables:lookup_value(?SUBID, SubId). + +-spec(get_sub_shard(pid(), emqx_topic:topic()) -> non_neg_integer()). +get_sub_shard(SubPid, Topic) -> case create_seq(Topic) of - Seq when Seq =< 1024 -> 0; - _Seq -> erlang:phash2(SubPid, ets:lookup_element(?SUBSEQ, shards, 2)) + Seq when Seq =< ?SHARD -> 0; + _ -> erlang:phash2(SubPid, shards_num()) + 1 end. +-spec(shards_num() -> pos_integer()). +shards_num() -> + %% Dynamic sharding later... + ets:lookup_element(?HELPER, shards, 2). + -spec(create_seq(emqx_topic:topic()) -> emqx_sequence:seqid()). create_seq(Topic) -> emqx_sequence:nextval(?SUBSEQ, Topic). @@ -67,41 +79,55 @@ reclaim_seq(Topic) -> %%------------------------------------------------------------------------------ init([]) -> + %% Helper table + ok = emqx_tables:new(?HELPER, [{read_concurrency, true}]), + %% Shards: CPU * 32 + true = ets:insert(?HELPER, {shards, emqx_vm:schedulers() * 32}), %% SubSeq: Topic -> SeqId ok = emqx_sequence:create(?SUBSEQ), - %% Shards: CPU * 32 - true = ets:insert(?SUBSEQ, {shards, emqx_vm:schedulers() * 32}), + %% SubId: SubId -> SubPid + ok = emqx_tables:new(?SUBID, [public, {read_concurrency, true}, {write_concurrency, true}]), %% SubMon: SubPid -> SubId - ok = emqx_tables:new(?SUBMON, [set, protected, {read_concurrency, true}]), + ok = emqx_tables:new(?SUBMON, [public, {read_concurrency, true}, {write_concurrency, true}]), %% Stats timer - emqx_stats:update_interval(broker_stats, fun emqx_broker:stats_fun/0), - {ok, #state{pmon = emqx_pmon:new()}, hibernate}. + ok = emqx_stats:update_interval(broker_stats, fun emqx_broker:stats_fun/0), + {ok, #{pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> emqx_logger:error("[BrokerHelper] unexpected call: ~p", [Req]), - {reply, ignored, State}. + {reply, ignored, State}. -handle_cast({monitor, SubPid, SubId}, State = #state{pmon = PMon}) -> +handle_cast({register_sub, SubPid, SubId}, State = #{pmon := PMon}) -> + true = (SubId =:= undefined) orelse ets:insert(?SUBID, {SubId, SubPid}), true = ets:insert(?SUBMON, {SubPid, SubId}), - {noreply, State#state{pmon = emqx_pmon:monitor(SubPid, PMon)}}; + {noreply, State#{pmon := emqx_pmon:monitor(SubPid, PMon)}}; handle_cast(Msg, State) -> emqx_logger:error("[BrokerHelper] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) -> - true = ets:delete(?SUBMON, SubPid), - ok = emqx_pool:async_submit(fun emqx_broker:subscriber_down/1, [SubPid]), - {noreply, State#state{pmon = emqx_pmon:erase(SubPid, PMon)}}; +handle_info({'DOWN', _MRef, process, SubPid, Reason}, State = #{pmon := PMon}) -> + case ets:lookup(?SUBMON, SubPid) of + [{_, SubId}] -> + ok = emqx_pool:async_submit(fun subscriber_down/2, [SubPid, SubId]); + [] -> + emqx_logger:error("[BrokerHelper] unexpected DOWN: ~p, reason: ~p", [SubPid, Reason]) + end, + {noreply, State#{pmon := emqx_pmon:erase(SubPid, PMon)}}; handle_info(Info, State) -> emqx_logger:error("[BrokerHelper] unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{}) -> - _ = emqx_sequence:delete(?SUBSEQ), +terminate(_Reason, _State) -> + true = emqx_sequence:delete(?SUBSEQ), emqx_stats:cancel_update(broker_stats). code_change(_OldVsn, State, _Extra) -> {ok, State}. +subscriber_down(SubPid, SubId) -> + true = ets:delete(?SUBMON, SubPid), + true = (SubId =:= undefined) orelse ets:delete_object(?SUBID, {SubId, SubPid}), + emqx_broker:subscriber_down(SubPid). + diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index efeaabc74..c32800a24 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -90,7 +90,7 @@ init([]) -> [Node | Acc] end end, [], mnesia:dirty_all_keys(?ROUTING_NODE)), - emqx_stats:update_interval(route_stats, fun ?MODULE:stats_fun/0), + ok = emqx_stats:update_interval(route_stats, fun ?MODULE:stats_fun/0), {ok, #{nodes => Nodes}, hibernate}. handle_call(Req, _From, State) -> diff --git a/src/emqx_sequence.erl b/src/emqx_sequence.erl index 022531df5..33bb5edda 100644 --- a/src/emqx_sequence.erl +++ b/src/emqx_sequence.erl @@ -51,6 +51,7 @@ reclaim(Name, Key) -> end. %% @doc Delete the sequence. +-spec(delete(name()) -> boolean()). delete(Name) -> case ets:info(Name, name) of Name -> ets:delete(Name); diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index d178a8ae7..637e44b0c 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -206,7 +206,7 @@ init([]) -> ok = emqx_tables:new(?SESSION_P_TAB, TabOpts), ok = emqx_tables:new(?SESSION_ATTRS_TAB, TabOpts), ok = emqx_tables:new(?SESSION_STATS_TAB, TabOpts), - emqx_stats:update_interval(sm_stats, fun ?MODULE:stats_fun/0), + ok = emqx_stats:update_interval(sm_stats, fun ?MODULE:stats_fun/0), {ok, #{session_pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> diff --git a/src/emqx_tables.erl b/src/emqx_tables.erl index 9b3ebfeae..fdb106a99 100644 --- a/src/emqx_tables.erl +++ b/src/emqx_tables.erl @@ -15,6 +15,7 @@ -module(emqx_tables). -export([new/2]). +-export([lookup_value/2, lookup_value/3]). %% Create a named_table ets. -spec(new(atom(), list()) -> ok). @@ -26,3 +27,16 @@ new(Tab, Opts) -> 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/test/emqx_sequence_SUITE.erl b/test/emqx_sequence_SUITE.erl index f37b60d76..1ac0ea308 100644 --- a/test/emqx_sequence_SUITE.erl +++ b/test/emqx_sequence_SUITE.erl @@ -33,5 +33,6 @@ sequence_generate(_) -> ?assertEqual(1, reclaim(seqtab, key)), ?assertEqual(0, reclaim(seqtab, key)), ?assertEqual(false, ets:member(seqtab, key)), - ?assertEqual(1, nextval(seqtab, key)). + ?assertEqual(1, nextval(seqtab, key)), + ?assert(emqx_sequence:delete(seqtab). diff --git a/test/emqx_tables_SUITE.erl b/test/emqx_tables_SUITE.erl index 95590b0e9..1002c0a0b 100644 --- a/test/emqx_tables_SUITE.erl +++ b/test/emqx_tables_SUITE.erl @@ -20,7 +20,7 @@ all() -> [t_new]. t_new(_) -> - TId = emqx_tables:new(test_table, [{read_concurrency, true}]), - ets:insert(TId, {loss, 100}), - TId = emqx_tables:new(test_table, [{read_concurrency, true}]), - 100 = ets:lookup_element(TId, loss, 2). + 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). From 33830d812006410b0b04d042625e8b8fe5616bd9 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 10 Dec 2018 18:37:42 +0800 Subject: [PATCH 470/520] Improve the design of trie, router and broker modules 1. Add do_add_route/1 do_add_route/2, do_delete_route/1, do_delete_route/2 APIs in emqx_router module 2. Improve the code of emqx_trie module 3. Update the emqx_broker module to call the new APIs of emqx_router --- src/emqx_broker.erl | 30 ++++----- src/emqx_router.erl | 134 ++++++++++++++++++------------------- src/emqx_router_helper.erl | 45 +++++++------ src/emqx_shared_sub.erl | 7 +- src/emqx_trie.erl | 18 ++--- test/emqx_router_SUITE.erl | 103 ++++++++++------------------ test/emqx_trie_SUITE.erl | 113 ++++++++++++++++--------------- 7 files changed, 208 insertions(+), 242 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index a2ddf5b60..a00dc17b8 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -27,6 +27,7 @@ -export([subscriptions/1, subscribers/1, subscribed/2]). -export([get_subopts/2, set_subopts/2]). -export([topics/0]). + %% Stats fun -export([stats_fun/0]). @@ -52,9 +53,9 @@ -spec(start_link(atom(), pos_integer()) -> emqx_types:startlink_ret()). start_link(Pool, Id) -> - _ = create_tabs(), - Name = emqx_misc:proc_name(?BROKER, Id), - gen_server:start_link({local, Name}, ?MODULE, [Pool, Id], []). + ok = create_tabs(), + gen_server:start_link({local, emqx_misc:proc_name(?BROKER, Id)}, + ?MODULE, [Pool, Id], []). %%------------------------------------------------------------------------------ %% Create tabs @@ -75,7 +76,7 @@ create_tabs() -> ok = emqx_tables:new(?SUBSCRIPTION, [duplicate_bag | TabOpts]), %% Subscriber: Topic -> SubPid1, SubPid2, SubPid3, ... - %% duplicate_bag: o(1) insert + %% bag: o(n) insert ok = emqx_tables:new(?SUBSCRIBER, [bag | TabOpts]). %%------------------------------------------------------------------------------ @@ -114,7 +115,7 @@ subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map call(pick({Topic, Shard}), {subscribe, Topic}); Group -> %% Shard subscription true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), - emqx_shard_sub:subscribe(Group, Topic, SubPid) + emqx_shared_sub:subscribe(Group, Topic, SubPid) end; true -> ok end. @@ -367,18 +368,17 @@ pick(Topic) -> %%------------------------------------------------------------------------------ init([Pool, Id]) -> - _ = emqx_router:set_mode(protected), true = gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #{pool => Pool, id => Id}}. handle_call({subscribe, Topic}, _From, State) -> - case get(Topic) of - undefined -> - _ = put(Topic, true), - emqx_router:add_route(Topic); - true -> ok - end, - {reply, ok, State}; + Ok = case get(Topic) of + undefined -> + _ = put(Topic, true), + emqx_router:do_add_route(Topic); + true -> ok + end, + {reply, Ok, State}; handle_call(Req, _From, State) -> emqx_logger:error("[Broker] unexpected call: ~p", [Req]), @@ -387,8 +387,8 @@ handle_call(Req, _From, State) -> handle_cast({unsubscribed, Topic}, State) -> case ets:member(?SUBSCRIBER, Topic) of false -> - _ = erase(Topic), - emqx_router:delete_route(Topic); + _ = erase(Topic), + emqx_router:do_delete_route(Topic); true -> ok end, {noreply, State}; diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 313adc475..c165b97ca 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -29,19 +29,19 @@ %% Route APIs -export([add_route/1, add_route/2]). --export([get_routes/1]). +-export([do_add_route/1, do_add_route/2]). +-export([match_routes/1, lookup_routes/1, has_routes/1]). -export([delete_route/1, delete_route/2]). --export([has_routes/1, match_routes/1, print_routes/1]). +-export([do_delete_route/1, do_delete_route/2]). +-export([print_routes/1]). -export([topics/0]). -%% Mode --export([set_mode/1, get_mode/0]). - %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --type(destination() :: node() | {binary(), node()}). +-type(group() :: binary()). +-type(destination() :: node() | {group(), node()}). -define(ROUTE, emqx_route). @@ -66,75 +66,76 @@ mnesia(copy) -> -spec(start_link(atom(), pos_integer()) -> emqx_types:startlink_ret()). start_link(Pool, Id) -> - Name = emqx_misc:proc_name(?MODULE, Id), - gen_server:start_link({local, Name}, ?MODULE, [Pool, Id], [{hibernate_after, 1000}]). + 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() | emqx_types:route()) -> ok | {error, term()}). +-spec(add_route(emqx_topic:topic()) -> ok | {error, term()}). add_route(Topic) when is_binary(Topic) -> - add_route(#route{topic = Topic, dest = node()}); -add_route(Route = #route{topic = Topic}) -> - case get_mode() of - protected -> do_add_route(Route); - undefined -> call(pick(Topic), {add_route, Route}) - end. + add_route(Topic, node()). -spec(add_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). add_route(Topic, Dest) when is_binary(Topic) -> - add_route(#route{topic = Topic, dest = Dest}). + call(pick(Topic), {add_route, Topic, Dest}). -%% @private -do_add_route(Route = #route{topic = Topic, dest = Dest}) -> - case lists:member(Route, get_routes(Topic)) of +-spec(do_add_route(emqx_topic:topic()) -> ok | {error, term()}). +do_add_route(Topic) when is_binary(Topic) -> + do_add_route(Topic, node()). + +-spec(do_add_route(emqx_topic:topic(), destination()) -> 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 true -> ok; false -> ok = emqx_router_helper:monitor(Dest), case emqx_topic:wildcard(Topic) of - true -> trans(fun add_trie_route/1, [Route]); - false -> add_direct_route(Route) + true -> trans(fun insert_trie_route/1, [Route]); + false -> insert_direct_route(Route) end end. --spec(get_routes(emqx_topic:topic()) -> [emqx_types:route()]). -get_routes(Topic) -> +%% @doc Match routes +-spec(match_routes(emqx_topic:topic()) -> [emqx_types:route()]). +match_routes(Topic) when is_binary(Topic) -> + %% Optimize: routing table will be replicated to all router nodes. + Matched = mnesia:ets(fun emqx_trie:match/1, [Topic]), + lists:append([lookup_routes(To) || To <- [Topic | Matched]]). + +-spec(lookup_routes(emqx_topic:topic()) -> [emqx_types:route()]). +lookup_routes(Topic) -> ets:lookup(?ROUTE, Topic). --spec(delete_route(emqx_topic:topic() | emqx_types:route()) -> ok | {error, term()}). -delete_route(Topic) when is_binary(Topic) -> - delete_route(#route{topic = Topic, dest = node()}); -delete_route(Route = #route{topic = Topic}) -> - case get_mode() of - protected -> do_delete_route(Route); - undefined -> call(pick(Topic), {delete_route, Route}) - end. - --spec(delete_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). -delete_route(Topic, Dest) when is_binary(Topic) -> - delete_route(#route{topic = Topic, dest = Dest}). - -%% @private -do_delete_route(Route = #route{topic = Topic}) -> - case emqx_topic:wildcard(Topic) of - true -> trans(fun del_trie_route/1, [Route]); - false -> del_direct_route(Route) - end. - -spec(has_routes(emqx_topic:topic()) -> boolean()). has_routes(Topic) when is_binary(Topic) -> ets:member(?ROUTE, Topic). --spec(topics() -> list(emqx_topic:topic())). -topics() -> mnesia:dirty_all_keys(?ROUTE). +-spec(delete_route(emqx_topic:topic()) -> ok | {error, term()}). +delete_route(Topic) when is_binary(Topic) -> + delete_route(Topic, node()). -%% @doc Match routes -%% Optimize: routing table will be replicated to all router nodes. --spec(match_routes(emqx_topic:topic()) -> [emqx_types:route()]). -match_routes(Topic) when is_binary(Topic) -> - Matched = mnesia:ets(fun emqx_trie:match/1, [Topic]), - lists:append([get_routes(To) || To <- [Topic | Matched]]). +-spec(delete_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). +delete_route(Topic, Dest) when is_binary(Topic) -> + call(pick(Topic), {delete_route, Topic, Dest}). + +-spec(do_delete_route(emqx_topic:topic()) -> ok | {error, term()}). +do_delete_route(Topic) when is_binary(Topic) -> + do_delete_route(Topic, node()). + +-spec(do_delete_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). +do_delete_route(Topic, Dest) -> + Route = #route{topic = Topic, dest = Dest}, + case emqx_topic:wildcard(Topic) of + true -> trans(fun delete_trie_route/1, [Route]); + false -> delete_direct_route(Route) + end. + +-spec(topics() -> list(emqx_topic:topic())). +topics() -> + mnesia:dirty_all_keys(?ROUTE). %% @doc Print routes to a topic -spec(print_routes(emqx_topic:topic()) -> ok). @@ -143,13 +144,6 @@ print_routes(Topic) -> io:format("~s -> ~s~n", [To, Dest]) end, match_routes(Topic)). --spec(set_mode(protected | atom()) -> any()). -set_mode(Mode) when is_atom(Mode) -> - put('$router_mode', Mode). - --spec(get_mode() -> protected | undefined | atom()). -get_mode() -> get('$router_mode'). - call(Router, Msg) -> gen_server:call(Router, Msg, infinity). @@ -164,11 +158,13 @@ init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #{pool => Pool, id => Id}}. -handle_call({add_route, Route}, _From, State) -> - {reply, do_add_route(Route), State}; +handle_call({add_route, Topic, Dest}, _From, State) -> + Ok = do_add_route(Topic, Dest), + {reply, Ok, State}; -handle_call({delete_route, Route}, _From, State) -> - {reply, do_delete_route(Route), State}; +handle_call({delete_route, Topic, Dest}, _From, State) -> + Ok = do_delete_route(Topic, Dest), + {reply, Ok, State}; handle_call(Req, _From, State) -> emqx_logger:error("[Router] unexpected call: ~p", [Req]), @@ -192,23 +188,23 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -add_direct_route(Route) -> +insert_direct_route(Route) -> mnesia:async_dirty(fun mnesia:write/3, [?ROUTE, Route, sticky_write]). -add_trie_route(Route = #route{topic = Topic}) -> +insert_trie_route(Route = #route{topic = Topic}) -> case mnesia:wread({?ROUTE, Topic}) of [] -> emqx_trie:insert(Topic); _ -> ok end, mnesia:write(?ROUTE, Route, sticky_write). -del_direct_route(Route) -> +delete_direct_route(Route) -> mnesia:async_dirty(fun mnesia:delete_object/3, [?ROUTE, Route, sticky_write]). -del_trie_route(Route = #route{topic = Topic}) -> +delete_trie_route(Route = #route{topic = Topic}) -> case mnesia:wread({?ROUTE, Topic}) of [Route] -> %% Remove route and trie - mnesia:delete_object(?ROUTE, Route, sticky_write), + ok = mnesia:delete_object(?ROUTE, Route, sticky_write), emqx_trie:delete(Topic); [_|_] -> %% Remove route only mnesia:delete_object(?ROUTE, Route, sticky_write); @@ -219,7 +215,7 @@ del_trie_route(Route = #route{topic = Topic}) -> -spec(trans(function(), list(any())) -> ok | {error, term()}). trans(Fun, Args) -> case mnesia:transaction(Fun, Args) of - {atomic, _} -> ok; - {aborted, Error} -> {error, Error} + {atomic, Ok} -> Ok; + {aborted, Reason} -> {error, Reason} end. diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index c24b10715..efeaabc74 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -31,15 +31,11 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% internal export +%% Internal export -export([stats_fun/0]). -record(routing_node, {name, const = unused}). --record(state, {nodes = []}). --compile({no_auto_import, [monitor/1]}). - --define(SERVER, ?MODULE). -define(ROUTE, emqx_route). -define(ROUTING_NODE, emqx_routing_node). -define(LOCK, {?MODULE, cleanup_routes}). @@ -64,9 +60,9 @@ mnesia(copy) -> %%------------------------------------------------------------------------------ %% @doc Starts the router helper --spec(start_link() -> {ok, pid()} | ignore | {error, any()}). +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). %% @doc Monitor routing node -spec(monitor(node() | {binary(), node()}) -> ok). @@ -84,18 +80,18 @@ monitor(Node) when is_atom(Node) -> %%------------------------------------------------------------------------------ init([]) -> - _ = ekka:monitor(membership), - _ = mnesia:subscribe({table, ?ROUTING_NODE, simple}), + ok = ekka:monitor(membership), + {ok, _} = mnesia:subscribe({table, ?ROUTING_NODE, simple}), Nodes = lists:foldl( fun(Node, Acc) -> case ekka:is_member(Node) of true -> Acc; - false -> _ = erlang:monitor_node(Node, true), + false -> true = erlang:monitor_node(Node, true), [Node | Acc] end end, [], mnesia:dirty_all_keys(?ROUTING_NODE)), emqx_stats:update_interval(route_stats, fun ?MODULE:stats_fun/0), - {ok, #state{nodes = Nodes}, hibernate}. + {ok, #{nodes => Nodes}, hibernate}. handle_call(Req, _From, State) -> emqx_logger:error("[RouterHelper] unexpected call: ~p", [Req]), @@ -105,24 +101,29 @@ handle_cast(Msg, State) -> emqx_logger:error("[RouterHelper] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({mnesia_table_event, {write, #routing_node{name = Node}, _}}, State = #state{nodes = Nodes}) -> - emqx_logger:info("[RouterHelper] write routing node: ~s", [Node]), +handle_info({mnesia_table_event, {write, {?ROUTING_NODE, Node, _}, _}}, State = #{nodes := Nodes}) -> case ekka:is_member(Node) orelse lists:member(Node, Nodes) of - true -> {noreply, State}; - false -> _ = erlang:monitor_node(Node, true), - {noreply, State#state{nodes = [Node | Nodes]}} + true -> {noreply, State}; + false -> + true = erlang:monitor_node(Node, true), + {noreply, State#{nodes := [Node | Nodes]}} end; -handle_info({mnesia_table_event, _Event}, State) -> +handle_info({mnesia_table_event, {delete, {?ROUTING_NODE, _Node}, _}}, State) -> + %% ignore {noreply, State}; -handle_info({nodedown, Node}, State = #state{nodes = Nodes}) -> +handle_info({mnesia_table_event, Event}, State) -> + emqx_logger:error("[RouterHelper] unexpected mnesia_table_event: ~p", [Event]), + {noreply, State}; + +handle_info({nodedown, Node}, State = #{nodes := Nodes}) -> global:trans({?LOCK, self()}, fun() -> mnesia:transaction(fun cleanup_routes/1, [Node]) end), - mnesia:dirty_delete(?ROUTING_NODE, Node), - {noreply, State#state{nodes = lists:delete(Node, Nodes)}, hibernate}; + ok = mnesia:dirty_delete(?ROUTING_NODE, Node), + {noreply, State#{nodes := lists:delete(Node, Nodes)}, hibernate}; handle_info({membership, {mnesia, down, Node}}, State) -> handle_info({nodedown, Node}, State); @@ -134,8 +135,8 @@ handle_info(Info, State) -> emqx_logger:error("[RouteHelper] unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{}) -> - ekka:unmonitor(membership), +terminate(_Reason, _State) -> + ok = ekka:unmonitor(membership), emqx_stats:cancel_update(route_stats), mnesia:unsubscribe({table, ?ROUTING_NODE, simple}). diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index d1d0d921d..b7e41213b 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -252,7 +252,6 @@ subscribers(Group, Topic) -> %%------------------------------------------------------------------------------ init([]) -> - _ = emqx_router:set_mode(protected), mnesia:subscribe({table, ?TAB, simple}), {atomic, PMon} = mnesia:transaction(fun init_monitors/0), ok = emqx_tables:new(?SHARED_SUBS, [protected, bag]), @@ -269,7 +268,7 @@ handle_call({subscribe, Group, Topic, SubPid}, _From, State = #state{pmon = PMon mnesia:dirty_write(?TAB, record(Group, Topic, SubPid)), case ets:member(?SHARED_SUBS, {Group, Topic}) of true -> ok; - false -> ok = emqx_router:add_route(Topic, {Group, node()}) + false -> ok = emqx_router:do_add_route(Topic, {Group, node()}) end, ok = maybe_insert_alive_tab(SubPid), true = ets:insert(?SHARED_SUBS, {{Group, Topic}, SubPid}), @@ -280,7 +279,7 @@ handle_call({unsubscribe, Group, Topic, SubPid}, _From, State) -> true = ets:delete_object(?SHARED_SUBS, {{Group, Topic}, SubPid}), case ets:member(?SHARED_SUBS, {Group, Topic}) of true -> ok; - false -> ok = emqx_router:delete_route(Topic, {Group, node()}) + false -> ok = emqx_router:do_delete_route(Topic, {Group, node()}) end, {reply, ok, State}; @@ -334,7 +333,7 @@ cleanup_down(SubPid) -> true = ets:delete_object(?SHARED_SUBS, {{Group, Topic}, SubPid}), case ets:member(?SHARED_SUBS, {Group, Topic}) of true -> ok; - false -> ok = emqx_router:delete_route(Topic, {Group, node()}) + false -> ok = emqx_router:do_delete_route(Topic, {Group, node()}) end end, mnesia:dirty_match_object(#emqx_shared_subscription{_ = '_', subpid = SubPid})). diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index 79f6042b7..27ff52827 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -36,7 +36,7 @@ %% @doc Create or replicate trie tables. -spec(mnesia(boot | copy) -> ok). mnesia(boot) -> - %% Optimize + %% Optimize storage StoreProps = [{ets, [{read_concurrency, true}, {write_concurrency, true}]}], %% Trie table @@ -72,7 +72,7 @@ insert(Topic) when is_binary(Topic) -> write_trie_node(TrieNode#trie_node{topic = Topic}); [] -> %% Add trie path - lists:foreach(fun add_path/1, emqx_topic:triples(Topic)), + ok = lists:foreach(fun add_path/1, emqx_topic:triples(Topic)), %% Add last node write_trie_node(#trie_node{node_id = Topic, topic = Topic}) end. @@ -93,7 +93,7 @@ lookup(NodeId) -> delete(Topic) when is_binary(Topic) -> case mnesia:wread({?TRIE_NODE, Topic}) of [#trie_node{edge_count = 0}] -> - mnesia:delete({?TRIE_NODE, Topic}), + ok = mnesia:delete({?TRIE_NODE, Topic}), delete_path(lists:reverse(emqx_topic:triples(Topic))); [TrieNode] -> write_trie_node(TrieNode#trie_node{topic = undefined}); @@ -112,12 +112,12 @@ add_path({Node, Word, Child}) -> [TrieNode = #trie_node{edge_count = Count}] -> case mnesia:wread({?TRIE, Edge}) of [] -> - write_trie_node(TrieNode#trie_node{edge_count = Count + 1}), + ok = write_trie_node(TrieNode#trie_node{edge_count = Count + 1}), write_trie(#trie{edge = Edge, node_id = Child}); [_] -> ok end; [] -> - write_trie_node(#trie_node{node_id = Node, edge_count = 1}), + ok = write_trie_node(#trie_node{node_id = Node, edge_count = 1}), write_trie(#trie{edge = Edge, node_id = Child}) end. @@ -154,10 +154,10 @@ match_node(NodeId, [W|Words], ResAcc) -> delete_path([]) -> ok; delete_path([{NodeId, Word, _} | RestPath]) -> - mnesia:delete({?TRIE, #trie_edge{node_id = NodeId, word = Word}}), - case mnesia:read(?TRIE_NODE, NodeId) of + ok = mnesia:delete({?TRIE, #trie_edge{node_id = NodeId, word = Word}}), + case mnesia:wread({?TRIE_NODE, NodeId}) of [#trie_node{edge_count = 1, topic = undefined}] -> - mnesia:delete({?TRIE_NODE, NodeId}), + ok = mnesia:delete({?TRIE_NODE, NodeId}), delete_path(RestPath); [TrieNode = #trie_node{edge_count = 1, topic = _}] -> write_trie_node(TrieNode#trie_node{edge_count = 0}); @@ -167,9 +167,11 @@ delete_path([{NodeId, Word, _} | RestPath]) -> mnesia:abort({node_not_found, NodeId}) end. +%% @private write_trie(Trie) -> mnesia:write(?TRIE, Trie, write). +%% @private write_trie_node(TrieNode) -> mnesia:write(?TRIE_NODE, TrieNode, write). diff --git a/test/emqx_router_SUITE.erl b/test/emqx_router_SUITE.erl index e317ec7b3..c115fd0cd 100644 --- a/test/emqx_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -21,17 +21,16 @@ -compile(nowarn_export_all). -define(R, emqx_router). --define(TABS, [emqx_route, emqx_trie, emqx_trie_node]). all() -> [{group, route}]. groups() -> [{route, [sequence], - [add_del_route, - match_routes, - has_routes, - router_add_del]}]. + [t_add_delete, + t_do_add_delete, + t_match_routes, + t_has_routes]}]. init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), @@ -47,77 +46,47 @@ init_per_testcase(_TestCase, Config) -> end_per_testcase(_TestCase, _Config) -> clear_tables(). -add_del_route(_) -> - From = {self(), make_ref()}, - ?R:add_route(From, <<"a/b/c">>, node()), - timer:sleep(1), - - ?R:add_route(From, <<"a/b/c">>, node()), - timer:sleep(1), - - ?R:add_route(From, <<"a/+/b">>, node()), - ct:log("Topics: ~p ~n", [emqx_topic:wildcard(<<"a/+/b">>)]), - timer:sleep(1), - +t_add_delete(_) -> + ?R:add_route(<<"a/b/c">>, node()), + ?R:add_route(<<"a/b/c">>, node()), + ?R:add_route(<<"a/+/b">>, node()), ?assertEqual([<<"a/+/b">>, <<"a/b/c">>], lists:sort(?R:topics())), - ?R:del_route(From, <<"a/b/c">>, node()), + ?R:delete_route(<<"a/b/c">>), + ?R:delete_route(<<"a/+/b">>, node()), + ?assertEqual([], ?R:topics()). - ?R:del_route(From, <<"a/+/b">>, node()), - timer:sleep(120), - ?assertEqual([], lists:sort(?R:topics())). +t_do_add_delete(_) -> + ?R:do_add_route(<<"a/b/c">>, node()), + ?R:do_add_route(<<"a/b/c">>, node()), + ?R:do_add_route(<<"a/+/b">>, node()), + ?assertEqual([<<"a/+/b">>, <<"a/b/c">>], lists:sort(?R:topics())), -match_routes(_) -> - From = {self(), make_ref()}, - ?R:add_route(From, <<"a/b/c">>, node()), - ?R:add_route(From, <<"a/+/c">>, node()), - ?R:add_route(From, <<"a/b/#">>, node()), - ?R:add_route(From, <<"#">>, node()), - timer:sleep(1000), + ?R:do_delete_route(<<"a/b/c">>, node()), + ?R:do_delete_route(<<"a/+/b">>), + ?assertEqual([], ?R:topics()). + +t_match_routes(_) -> + ?R:add_route(<<"a/b/c">>, node()), + ?R:add_route(<<"a/+/c">>, node()), + ?R:add_route(<<"a/b/#">>, node()), + ?R:add_route(<<"#">>, node()), ?assertEqual([#route{topic = <<"#">>, dest = node()}, #route{topic = <<"a/+/c">>, dest = node()}, #route{topic = <<"a/b/#">>, dest = node()}, #route{topic = <<"a/b/c">>, dest = node()}], - lists:sort(?R:match_routes(<<"a/b/c">>))). + lists:sort(?R:match_routes(<<"a/b/c">>))), + ?R:delete_route(<<"a/b/c">>, node()), + ?R:delete_route(<<"a/+/c">>, node()), + ?R:delete_route(<<"a/b/#">>, node()), + ?R:delete_route(<<"#">>, node()), + ?assertEqual([], lists:sort(?R:match_routes(<<"a/b/c">>))). -has_routes(_) -> - From = {self(), make_ref()}, - ?R:add_route(From, <<"devices/+/messages">>, node()), - timer:sleep(200), - ?assert(?R:has_routes(<<"devices/+/messages">>)). +t_has_routes(_) -> + ?R:add_route(<<"devices/+/messages">>, node()), + ?assert(?R:has_routes(<<"devices/+/messages">>)), + ?R:delete_route(<<"devices/+/messages">>). clear_tables() -> - lists:foreach(fun mnesia:clear_table/1, ?TABS). - -router_add_del(_) -> - ?R:add_route(<<"#">>), - ?R:add_route(<<"a/b/c">>, node()), - ?R:add_route(<<"+/#">>), - Routes = [R1, R2 | _] = [ - #route{topic = <<"#">>, dest = node()}, - #route{topic = <<"+/#">>, dest = node()}, - #route{topic = <<"a/b/c">>, dest = node()}], - timer:sleep(500), - ?assertEqual(Routes, lists:sort(?R:match_routes(<<"a/b/c">>))), - - ?R:print_routes(<<"a/b/c">>), - - %% Batch Add - lists:foreach(fun(R) -> ?R:add_route(R) end, Routes), - ?assertEqual(Routes, lists:sort(?R:match_routes(<<"a/b/c">>))), - - %% Del - ?R:del_route(<<"a/b/c">>, node()), - timer:sleep(500), - [R1, R2] = lists:sort(?R:match_routes(<<"a/b/c">>)), - {atomic, []} = mnesia:transaction(fun emqx_trie:lookup/1, [<<"a/b/c">>]), - - %% Batch Del - R3 = #route{topic = <<"#">>, dest = 'a@127.0.0.1'}, - ?R:add_route(R3), - ?R:del_route(<<"#">>), - ?R:del_route(R2), - ?R:del_route(R3), - timer:sleep(500), - [] = lists:sort(?R:match_routes(<<"a/b/c">>)). + lists:foreach(fun mnesia:clear_table/1, [emqx_route, emqx_trie, emqx_trie_node]). diff --git a/test/emqx_trie_SUITE.erl b/test/emqx_trie_SUITE.erl index 85637a447..09226979f 100644 --- a/test/emqx_trie_SUITE.erl +++ b/test/emqx_trie_SUITE.erl @@ -47,36 +47,36 @@ t_insert(_) -> edge_count = 3, topic = <<"sensor">>, flags = undefined}, - {atomic, [TN]} = mnesia:transaction( - fun() -> - ?TRIE:insert(<<"sensor/1/metric/2">>), - ?TRIE:insert(<<"sensor/+/#">>), - ?TRIE:insert(<<"sensor/#">>), - ?TRIE:insert(<<"sensor">>), - ?TRIE:insert(<<"sensor">>), - ?TRIE:lookup(<<"sensor">>) - end). + Fun = fun() -> + ?TRIE:insert(<<"sensor/1/metric/2">>), + ?TRIE:insert(<<"sensor/+/#">>), + ?TRIE:insert(<<"sensor/#">>), + ?TRIE:insert(<<"sensor">>), + ?TRIE:insert(<<"sensor">>), + ?TRIE:lookup(<<"sensor">>) + end, + ?assertEqual({atomic, [TN]}, mnesia:transaction(Fun)). t_match(_) -> Machted = [<<"sensor/+/#">>, <<"sensor/#">>], - {atomic, Machted} = mnesia:transaction( - fun() -> - ?TRIE:insert(<<"sensor/1/metric/2">>), - ?TRIE:insert(<<"sensor/+/#">>), - ?TRIE:insert(<<"sensor/#">>), - ?TRIE:match(<<"sensor/1">>) - end). + Fun = fun() -> + ?TRIE:insert(<<"sensor/1/metric/2">>), + ?TRIE:insert(<<"sensor/+/#">>), + ?TRIE:insert(<<"sensor/#">>), + ?TRIE:match(<<"sensor/1">>) + end, + ?assertEqual({atomic, Machted}, mnesia:transaction(Fun)). t_match2(_) -> Matched = {[<<"+/+/#">>, <<"+/#">>, <<"#">>], []}, - {atomic, Matched} = mnesia:transaction( - fun() -> - ?TRIE:insert(<<"#">>), - ?TRIE:insert(<<"+/#">>), - ?TRIE:insert(<<"+/+/#">>), - {?TRIE:match(<<"a/b/c">>), - ?TRIE:match(<<"$SYS/broker/zenmq">>)} - end). + Fun = fun() -> + ?TRIE:insert(<<"#">>), + ?TRIE:insert(<<"+/#">>), + ?TRIE:insert(<<"+/+/#">>), + {?TRIE:match(<<"a/b/c">>), + ?TRIE:match(<<"$SYS/broker/zenmq">>)} + end, + ?assertEqual({atomic, Matched}, mnesia:transaction(Fun)). t_match3(_) -> Topics = [<<"d/#">>, <<"a/b/c">>, <<"a/b/+">>, <<"a/#">>, <<"#">>, <<"$SYS/#">>], @@ -91,43 +91,42 @@ t_delete(_) -> edge_count = 2, topic = undefined, flags = undefined}, - {atomic, [TN]} = mnesia:transaction( - fun() -> - ?TRIE:insert(<<"sensor/1/#">>), - ?TRIE:insert(<<"sensor/1/metric/2">>), - ?TRIE:insert(<<"sensor/1/metric/3">>), - ?TRIE:delete(<<"sensor/1/metric/2">>), - ?TRIE:delete(<<"sensor/1/metric">>), - ?TRIE:delete(<<"sensor/1/metric">>), - ?TRIE:lookup(<<"sensor/1">>) - end). + Fun = fun() -> + ?TRIE:insert(<<"sensor/1/#">>), + ?TRIE:insert(<<"sensor/1/metric/2">>), + ?TRIE:insert(<<"sensor/1/metric/3">>), + ?TRIE:delete(<<"sensor/1/metric/2">>), + ?TRIE:delete(<<"sensor/1/metric">>), + ?TRIE:delete(<<"sensor/1/metric">>), + ?TRIE:lookup(<<"sensor/1">>) + end, + ?assertEqual({atomic, [TN]}, mnesia:transaction(Fun)). t_delete2(_) -> - {atomic, {[], []}} = mnesia:transaction( - fun() -> - ?TRIE:insert(<<"sensor">>), - ?TRIE:insert(<<"sensor/1/metric/2">>), - ?TRIE:insert(<<"sensor/1/metric/3">>), - ?TRIE:delete(<<"sensor">>), - ?TRIE:delete(<<"sensor/1/metric/2">>), - ?TRIE:delete(<<"sensor/1/metric/3">>), - {?TRIE:lookup(<<"sensor">>), - ?TRIE:lookup(<<"sensor/1">>)} - end). + Fun = fun() -> + ?TRIE:insert(<<"sensor">>), + ?TRIE:insert(<<"sensor/1/metric/2">>), + ?TRIE:insert(<<"sensor/1/metric/3">>), + ?TRIE:delete(<<"sensor">>), + ?TRIE:delete(<<"sensor/1/metric/2">>), + ?TRIE:delete(<<"sensor/1/metric/3">>), + {?TRIE:lookup(<<"sensor">>), ?TRIE:lookup(<<"sensor/1">>)} + end, + ?assertEqual({atomic, {[], []}}, mnesia:transaction(Fun)). t_delete3(_) -> - {atomic, {[], []}} = mnesia:transaction( - fun() -> - ?TRIE:insert(<<"sensor/+">>), - ?TRIE:insert(<<"sensor/+/metric/2">>), - ?TRIE:insert(<<"sensor/+/metric/3">>), - ?TRIE:delete(<<"sensor/+/metric/2">>), - ?TRIE:delete(<<"sensor/+/metric/3">>), - ?TRIE:delete(<<"sensor">>), - ?TRIE:delete(<<"sensor/+">>), - ?TRIE:delete(<<"sensor/+/unknown">>), - {?TRIE:lookup(<<"sensor">>), ?TRIE:lookup(<<"sensor/+">>)} - end). + Fun = fun() -> + ?TRIE:insert(<<"sensor/+">>), + ?TRIE:insert(<<"sensor/+/metric/2">>), + ?TRIE:insert(<<"sensor/+/metric/3">>), + ?TRIE:delete(<<"sensor/+/metric/2">>), + ?TRIE:delete(<<"sensor/+/metric/3">>), + ?TRIE:delete(<<"sensor">>), + ?TRIE:delete(<<"sensor/+">>), + ?TRIE:delete(<<"sensor/+/unknown">>), + {?TRIE:lookup(<<"sensor">>), ?TRIE:lookup(<<"sensor/+">>)} + end, + ?assertEqual({atomic, {[], []}}, mnesia:transaction(Fun)). clear_tables() -> lists:foreach(fun mnesia:clear_table/1, ?TRIE_TABS). From b279eff18106b53b1c75635bd26a0717a4807fd2 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 11 Dec 2018 14:06:23 +0800 Subject: [PATCH 471/520] Add t_mnesia/1 test case --- test/emqx_trie_SUITE.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/emqx_trie_SUITE.erl b/test/emqx_trie_SUITE.erl index 09226979f..500fe3574 100644 --- a/test/emqx_trie_SUITE.erl +++ b/test/emqx_trie_SUITE.erl @@ -24,7 +24,7 @@ -define(TRIE_TABS, [emqx_trie, emqx_trie_node]). all() -> - [t_insert, t_match, t_match2, t_match3, t_delete, t_delete2, t_delete3]. + [t_mnesia, t_insert, t_match, t_match2, t_match3, t_delete, t_delete2, t_delete3]. init_per_suite(Config) -> application:load(emqx), @@ -42,6 +42,9 @@ init_per_testcase(_TestCase, Config) -> end_per_testcase(_TestCase, _Config) -> clear_tables(). +t_mnesia(_) -> + ok = ?TRIE:mnesia(copy). + t_insert(_) -> TN = #trie_node{node_id = <<"sensor">>, edge_count = 3, From 2a747c9d538cd682c6228a6a799470089b9ed90d Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 12 Dec 2018 13:34:13 +0800 Subject: [PATCH 472/520] Improve the subscription sharding. --- src/emqx.erl | 6 +- src/emqx_broker.erl | 217 +++++++++++++++++++---------------- src/emqx_broker_helper.erl | 80 ++++++++----- src/emqx_router_helper.erl | 2 +- src/emqx_sequence.erl | 1 + src/emqx_sm.erl | 2 +- src/emqx_tables.erl | 14 +++ test/emqx_sequence_SUITE.erl | 3 +- test/emqx_tables_SUITE.erl | 8 +- 9 files changed, 197 insertions(+), 136 deletions(-) diff --git a/src/emqx.erl b/src/emqx.erl index 3792cc4f8..76e966a59 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -22,7 +22,7 @@ %% PubSub API -export([subscribe/1, subscribe/2, subscribe/3]). -export([publish/1]). --export([unsubscribe/1, unsubscribe/2]). +-export([unsubscribe/1]). %% PubSub management API -export([topics/0, subscriptions/1, subscribers/1, subscribed/2]). @@ -88,10 +88,6 @@ publish(Msg) -> unsubscribe(Topic) -> emqx_broker:unsubscribe(iolist_to_binary(Topic)). --spec(unsubscribe(emqx_topic:topic() | string(), emqx_types:subid()) -> ok). -unsubscribe(Topic, SubId) -> - emqx_broker:unsubscribe(iolist_to_binary(Topic), SubId). - %%------------------------------------------------------------------------------ %% PubSub management API %%------------------------------------------------------------------------------ diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index a00dc17b8..9ed4aad06 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -20,7 +20,7 @@ -export([start_link/2]). -export([subscribe/1, subscribe/2, subscribe/3]). --export([unsubscribe/1, unsubscribe/2]). +-export([unsubscribe/1]). -export([subscriber_down/1]). -export([publish/1, safe_publish/1]). -export([dispatch/2]). @@ -35,6 +35,8 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-import(emqx_tables, [lookup_value/2, lookup_value/3]). + -ifdef(TEST). -compile(export_all). -compile(nowarn_export_all). @@ -42,8 +44,7 @@ -define(BROKER, ?MODULE). -%% ETS tables --define(SUBID, emqx_subid). +%% ETS tables for PubSub -define(SUBOPTION, emqx_suboption). -define(SUBSCRIBER, emqx_subscriber). -define(SUBSCRIPTION, emqx_subscription). @@ -65,9 +66,6 @@ start_link(Pool, Id) -> create_tabs() -> TabOpts = [public, {read_concurrency, true}, {write_concurrency, true}], - %% SubId: SubId -> SubPid - ok = emqx_tables:new(?SUBID, [set | TabOpts]), - %% SubOption: {SubPid, Topic} -> SubOption ok = emqx_tables:new(?SUBOPTION, [set | TabOpts]), @@ -76,7 +74,7 @@ create_tabs() -> ok = emqx_tables:new(?SUBSCRIPTION, [duplicate_bag | TabOpts]), %% Subscriber: Topic -> SubPid1, SubPid2, SubPid3, ... - %% bag: o(n) insert + %% bag: o(n) insert:( ok = emqx_tables:new(?SUBSCRIBER, [bag | TabOpts]). %%------------------------------------------------------------------------------ @@ -98,28 +96,37 @@ subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map SubPid = self(), case ets:member(?SUBOPTION, {SubPid, Topic}) of false -> - ok = emqx_broker_helper:monitor(SubPid, SubId), - %% true = ets:insert(?SUBID, {SubId, SubPid}), - true = ets:insert(?SUBSCRIPTION, {SubPid, Topic}), - case maps:get(share, SubOpts, undefined) of - undefined -> - Shard = emqx_broker_helper:get_shard(SubPid, Topic), - case Shard of - 0 -> true = ets:insert(?SUBSCRIBER, {Topic, SubPid}); - I -> - true = ets:insert(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), - true = ets:insert(?SUBSCRIBER, {Topic, {shard, I}}) - end, - SubOpts1 = maps:put(shard, Shard, SubOpts), - true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts1}), - call(pick({Topic, Shard}), {subscribe, Topic}); - Group -> %% Shard subscription - true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), - emqx_shared_sub:subscribe(Group, Topic, SubPid) - end; + ok = emqx_broker_helper:monitor_sub(SubPid, SubId), + do_subscribe(Topic, SubPid, with_subid(SubId, SubOpts)); true -> ok end. +with_subid(undefined, SubOpts) -> + SubOpts; +with_subid(SubId, SubOpts) -> + maps:put(subid, SubId, SubOpts). + +%% @private +do_subscribe(Topic, SubPid, SubOpts) -> + true = ets:insert(?SUBSCRIPTION, {SubPid, Topic}), + Group = maps:get(share, SubOpts, undefined), + do_subscribe(Group, Topic, SubPid, SubOpts). + +do_subscribe(undefined, Topic, SubPid, SubOpts) -> + case emqx_broker_helper:get_sub_shard(SubPid, Topic) of + 0 -> true = ets:insert(?SUBSCRIBER, {Topic, SubPid}), + true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), + call(pick(Topic), {subscribe, Topic}); + I -> true = ets:insert(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + true = ets:insert(?SUBOPTION, {{SubPid, Topic}, maps:put(shard, I, SubOpts)}), + call(pick({Topic, I}), {subscribe, Topic, I}) + end; + +%% Shared subscription +do_subscribe(Group, Topic, SubPid, SubOpts) -> + true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), + emqx_shared_sub:subscribe(Group, Topic, SubPid). + %%------------------------------------------------------------------------------ %% Unsubscribe API %%------------------------------------------------------------------------------ @@ -130,33 +137,26 @@ unsubscribe(Topic) when is_binary(Topic) -> case ets:lookup(?SUBOPTION, {SubPid, Topic}) of [{_, SubOpts}] -> _ = emqx_broker_helper:reclaim_seq(Topic), - case maps:get(share, SubOpts, undefined) of - undefined -> - case maps:get(shard, SubOpts, 0) of - 0 -> - true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), - ok = cast(pick(Topic), {unsubscribed, Topic}); - I -> - true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), - case ets:member(emqx_subscriber, {shard, Topic, I}) of - true -> ok; - false -> ets:delete_object(?SUBSCRIBER, {Topic, {shard, I}}) - end, - ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) - end; - Group -> - ok = emqx_shared_sub:unsubscribe(Group, Topic, SubPid) - end, - true = ets:delete_object(?SUBSCRIPTION, {SubPid, Topic}), - %%true = ets:delete_object(?SUBID, {SubId, SubPid}), - true = ets:delete(?SUBOPTION, {SubPid, Topic}), - ok; + do_unsubscribe(Topic, SubPid, SubOpts); [] -> ok end. --spec(unsubscribe(emqx_topic:topic(), emqx_types:subid()) -> ok). -unsubscribe(Topic, _SubId) when is_binary(Topic) -> - unsubscribe(Topic). +do_unsubscribe(Topic, SubPid, SubOpts) -> + true = ets:delete(?SUBOPTION, {SubPid, Topic}), + true = ets:delete_object(?SUBSCRIPTION, {SubPid, Topic}), + Group = maps:get(share, SubOpts, undefined), + do_unsubscribe(Group, Topic, SubPid, SubOpts). + +do_unsubscribe(undefined, Topic, SubPid, SubOpts) -> + case maps:get(shard, SubOpts, 0) of + 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), + cast(pick(Topic), {unsubscribed, Topic}); + I -> true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + cast(pick({Topic, I}), {unsubscribed, Topic, I}) + end; + +do_unsubscribe(Group, Topic, SubPid, _SubOpts) -> + emqx_shared_sub:unsubscribe(Group, Topic, SubPid). %%------------------------------------------------------------------------------ %% Publish @@ -241,23 +241,28 @@ dispatch(Topic, Delivery = #delivery{message = Msg, results = Results}) -> inc_dropped_cnt(Topic), Delivery; [Sub] -> %% optimize? - dispatch(Sub, Topic, Msg), - Delivery#delivery{results = [{dispatch, Topic, 1}|Results]}; + Cnt = dispatch(Sub, Topic, Msg), + Delivery#delivery{results = [{dispatch, Topic, Cnt}|Results]}; Subs -> - Count = lists:foldl( - fun(Sub, Acc) -> - dispatch(Sub, Topic, Msg), Acc + 1 - end, 0, Subs), - Delivery#delivery{results = [{dispatch, Topic, Count}|Results]} + Cnt = lists:foldl( + fun(Sub, Acc) -> + dispatch(Sub, Topic, Msg) + Acc + end, 0, Subs), + Delivery#delivery{results = [{dispatch, Topic, Cnt}|Results]} end. dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> - SubPid ! {dispatch, Topic, Msg}; + case erlang:is_process_alive(SubPid) of + true -> + SubPid ! {dispatch, Topic, Msg}, + 1; + false -> 0 + end; dispatch({shard, I}, Topic, Msg) -> - - lists:foreach(fun(SubPid) -> - SubPid ! {dispatch, Topic, Msg} - end, safe_lookup_element(?SUBSCRIBER, {shard, Topic, I}, [])). + lists:foldl( + fun(SubPid, Cnt) -> + dispatch(SubPid, Topic, Msg) + Cnt + end, 0, subscribers({shard, Topic, I})). inc_dropped_cnt(<<"$SYS/", _/binary>>) -> ok; @@ -265,8 +270,10 @@ inc_dropped_cnt(_Topic) -> emqx_metrics:inc('messages/dropped'). -spec(subscribers(emqx_topic:topic()) -> [pid()]). -subscribers(Topic) -> - safe_lookup_element(?SUBSCRIBER, Topic, []). +subscribers(Topic) when is_binary(Topic) -> + lookup_value(?SUBSCRIBER, Topic, []); +subscribers(Shard = {shard, _Topic, _I}) -> + lookup_value(?SUBSCRIBER, Shard, []). %%------------------------------------------------------------------------------ %% Subscriber is down @@ -275,27 +282,21 @@ subscribers(Topic) -> -spec(subscriber_down(pid()) -> true). subscriber_down(SubPid) -> lists:foreach( - fun(Sub = {_Pid, Topic}) -> - case ets:lookup(?SUBOPTION, Sub) of - [{_, SubOpts}] -> + fun(Topic) -> + case lookup_value(?SUBOPTION, {SubPid, Topic}) of + SubOpts when is_map(SubOpts) -> _ = emqx_broker_helper:reclaim_seq(Topic), + true = ets:delete(?SUBOPTION, {SubPid, Topic}), case maps:get(shard, SubOpts, 0) of - 0 -> - true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), - ok = cast(pick(Topic), {unsubscribed, Topic}); - I -> - true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), - case ets:member(emqx_subscriber, {shard, Topic, I}) of - true -> ok; - false -> ets:delete_object(?SUBSCRIBER, {Topic, {shard, I}}) - end, - ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) - end, - ets:delete(?SUBOPTION, Sub); - [] -> ok + 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), + ok = cast(pick(Topic), {unsubscribed, Topic}); + I -> true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), + ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) + end; + undefined -> ok end - end, ets:lookup(?SUBSCRIPTION, SubPid)), - true = ets:delete(?SUBSCRIPTION, SubPid). + end, lookup_value(?SUBSCRIPTION, SubPid, [])), + ets:delete(?SUBSCRIPTION, SubPid). %%------------------------------------------------------------------------------ %% Management APIs @@ -303,20 +304,32 @@ subscriber_down(SubPid) -> -spec(subscriptions(pid() | emqx_types:subid()) -> [{emqx_topic:topic(), emqx_types:subopts()}]). -subscriptions(SubPid) -> - [{Topic, safe_lookup_element(?SUBOPTION, {SubPid, Topic}, #{})} - || Topic <- safe_lookup_element(?SUBSCRIPTION, SubPid, [])]. +subscriptions(SubPid) when is_pid(SubPid) -> + [{Topic, lookup_value(?SUBOPTION, {SubPid, Topic}, #{})} + || Topic <- lookup_value(?SUBSCRIPTION, SubPid, [])]; +subscriptions(SubId) -> + case emqx_broker_helper:lookup_subpid(SubId) of + SubPid when is_pid(SubPid) -> + subscriptions(SubPid); + undefined -> [] + end. -spec(subscribed(pid(), emqx_topic:topic()) -> boolean()). subscribed(SubPid, Topic) when is_pid(SubPid) -> ets:member(?SUBOPTION, {SubPid, Topic}); subscribed(SubId, Topic) when ?is_subid(SubId) -> - %%FIXME:... SubId -> SubPid - ets:member(?SUBOPTION, {SubId, Topic}). + SubPid = emqx_broker_helper:lookup_subpid(SubId), + ets:member(?SUBOPTION, {SubPid, Topic}). --spec(get_subopts(pid(), emqx_topic:topic()) -> emqx_types:subopts()). +-spec(get_subopts(pid(), emqx_topic:topic()) -> emqx_types:subopts() | undefined). get_subopts(SubPid, Topic) when is_pid(SubPid), is_binary(Topic) -> - safe_lookup_element(?SUBOPTION, {SubPid, Topic}, #{}). + lookup_value(?SUBOPTION, {SubPid, Topic}); +get_subopts(SubId, Topic) when ?is_subid(SubId) -> + case emqx_broker_helper:lookup_subpid(SubId) of + SubPid when is_pid(SubPid) -> + get_subopts(SubPid, Topic); + undefined -> undefined + end. -spec(set_subopts(emqx_topic:topic(), emqx_types:subopts()) -> boolean()). set_subopts(Topic, NewOpts) when is_binary(Topic), is_map(NewOpts) -> @@ -331,9 +344,6 @@ set_subopts(Topic, NewOpts) when is_binary(Topic), is_map(NewOpts) -> topics() -> emqx_router:topics(). -safe_lookup_element(Tab, Key, Def) -> - try ets:lookup_element(Tab, Key, 2) catch error:badarg -> Def end. - %%------------------------------------------------------------------------------ %% Stats fun %%------------------------------------------------------------------------------ @@ -372,10 +382,15 @@ init([Pool, Id]) -> {ok, #{pool => Pool, id => Id}}. handle_call({subscribe, Topic}, _From, State) -> - Ok = case get(Topic) of + Ok = emqx_router:do_add_route(Topic), + {reply, Ok, State}; + +handle_call({subscribe, Topic, I}, _From, State) -> + Ok = case get(Shard = {Topic, I}) of undefined -> - _ = put(Topic, true), - emqx_router:do_add_route(Topic); + _ = put(Shard, true), + true = ets:insert(?SUBSCRIBER, {Topic, {shard, I}}), + cast(pick(Topic), {subscribe, Topic}); true -> ok end, {reply, Ok, State}; @@ -384,11 +399,18 @@ handle_call(Req, _From, State) -> emqx_logger:error("[Broker] unexpected call: ~p", [Req]), {reply, ignored, State}. +handle_cast({subscribe, Topic}, State) -> + case emqx_router:do_add_route(Topic) of + ok -> ok; + {error, Reason} -> + emqx_logger:error("[Broker] Failed to add route: ~p", [Reason]) + end, + {noreply, State}; + handle_cast({unsubscribed, Topic}, State) -> case ets:member(?SUBSCRIBER, Topic) of false -> - _ = erase(Topic), - emqx_router:do_delete_route(Topic); + _ = emqx_router:do_delete_route(Topic); true -> ok end, {noreply, State}; @@ -396,6 +418,7 @@ handle_cast({unsubscribed, Topic}, State) -> handle_cast({unsubscribed, Topic, I}, State) -> case ets:member(?SUBSCRIBER, {shard, Topic, I}) of false -> + _ = erase({Topic, I}), true = ets:delete_object(?SUBSCRIBER, {Topic, {shard, I}}), cast(pick(Topic), {unsubscribed, Topic}); true -> ok diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index d3e7f9d37..7d514e31d 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -16,44 +16,56 @@ -behaviour(gen_server). --compile({no_auto_import, [monitor/2]}). - -export([start_link/0]). --export([monitor/2]). --export([get_shard/2]). +-export([register_sub/2]). +-export([lookup_subid/1, lookup_subpid/1]). +-export([get_sub_shard/2]). -export([create_seq/1, reclaim_seq/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(HELPER, ?MODULE). +-define(SUBID, emqx_subid). -define(SUBMON, emqx_submon). -define(SUBSEQ, emqx_subseq). - --record(state, {pmon :: emqx_pmon:pmon()}). +-define(SHARD, 1024). -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?HELPER}, ?MODULE, [], []). --spec(monitor(pid(), emqx_types:subid()) -> ok). -monitor(SubPid, SubId) when is_pid(SubPid) -> +-spec(register_sub(pid(), emqx_types:subid()) -> ok). +register_sub(SubPid, SubId) when is_pid(SubPid) -> case ets:lookup(?SUBMON, SubPid) of [] -> - gen_server:cast(?HELPER, {monitor, SubPid, SubId}); + gen_server:cast(?HELPER, {register_sub, SubPid, SubId}); [{_, SubId}] -> ok; _Other -> error(subid_conflict) end. --spec(get_shard(pid(), emqx_topic:topic()) -> non_neg_integer()). -get_shard(SubPid, Topic) -> +-spec(lookup_subid(pid()) -> emqx_types:subid() | undefined). +lookup_subid(SubPid) when is_pid(SubPid) -> + emqx_tables:lookup_value(?SUBMON, SubPid). + +-spec(lookup_subpid(emqx_types:subid()) -> pid()). +lookup_subpid(SubId) -> + emqx_tables:lookup_value(?SUBID, SubId). + +-spec(get_sub_shard(pid(), emqx_topic:topic()) -> non_neg_integer()). +get_sub_shard(SubPid, Topic) -> case create_seq(Topic) of - Seq when Seq =< 1024 -> 0; - _Seq -> erlang:phash2(SubPid, ets:lookup_element(?SUBSEQ, shards, 2)) + Seq when Seq =< ?SHARD -> 0; + _ -> erlang:phash2(SubPid, shards_num()) + 1 end. +-spec(shards_num() -> pos_integer()). +shards_num() -> + %% Dynamic sharding later... + ets:lookup_element(?HELPER, shards, 2). + -spec(create_seq(emqx_topic:topic()) -> emqx_sequence:seqid()). create_seq(Topic) -> emqx_sequence:nextval(?SUBSEQ, Topic). @@ -67,41 +79,55 @@ reclaim_seq(Topic) -> %%------------------------------------------------------------------------------ init([]) -> + %% Helper table + ok = emqx_tables:new(?HELPER, [{read_concurrency, true}]), + %% Shards: CPU * 32 + true = ets:insert(?HELPER, {shards, emqx_vm:schedulers() * 32}), %% SubSeq: Topic -> SeqId ok = emqx_sequence:create(?SUBSEQ), - %% Shards: CPU * 32 - true = ets:insert(?SUBSEQ, {shards, emqx_vm:schedulers() * 32}), + %% SubId: SubId -> SubPid + ok = emqx_tables:new(?SUBID, [public, {read_concurrency, true}, {write_concurrency, true}]), %% SubMon: SubPid -> SubId - ok = emqx_tables:new(?SUBMON, [set, protected, {read_concurrency, true}]), + ok = emqx_tables:new(?SUBMON, [public, {read_concurrency, true}, {write_concurrency, true}]), %% Stats timer - emqx_stats:update_interval(broker_stats, fun emqx_broker:stats_fun/0), - {ok, #state{pmon = emqx_pmon:new()}, hibernate}. + ok = emqx_stats:update_interval(broker_stats, fun emqx_broker:stats_fun/0), + {ok, #{pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> emqx_logger:error("[BrokerHelper] unexpected call: ~p", [Req]), - {reply, ignored, State}. + {reply, ignored, State}. -handle_cast({monitor, SubPid, SubId}, State = #state{pmon = PMon}) -> +handle_cast({register_sub, SubPid, SubId}, State = #{pmon := PMon}) -> + true = (SubId =:= undefined) orelse ets:insert(?SUBID, {SubId, SubPid}), true = ets:insert(?SUBMON, {SubPid, SubId}), - {noreply, State#state{pmon = emqx_pmon:monitor(SubPid, PMon)}}; + {noreply, State#{pmon := emqx_pmon:monitor(SubPid, PMon)}}; handle_cast(Msg, State) -> emqx_logger:error("[BrokerHelper] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) -> - true = ets:delete(?SUBMON, SubPid), - ok = emqx_pool:async_submit(fun emqx_broker:subscriber_down/1, [SubPid]), - {noreply, State#state{pmon = emqx_pmon:erase(SubPid, PMon)}}; +handle_info({'DOWN', _MRef, process, SubPid, Reason}, State = #{pmon := PMon}) -> + case ets:lookup(?SUBMON, SubPid) of + [{_, SubId}] -> + ok = emqx_pool:async_submit(fun subscriber_down/2, [SubPid, SubId]); + [] -> + emqx_logger:error("[BrokerHelper] unexpected DOWN: ~p, reason: ~p", [SubPid, Reason]) + end, + {noreply, State#{pmon := emqx_pmon:erase(SubPid, PMon)}}; handle_info(Info, State) -> emqx_logger:error("[BrokerHelper] unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{}) -> - _ = emqx_sequence:delete(?SUBSEQ), +terminate(_Reason, _State) -> + true = emqx_sequence:delete(?SUBSEQ), emqx_stats:cancel_update(broker_stats). code_change(_OldVsn, State, _Extra) -> {ok, State}. +subscriber_down(SubPid, SubId) -> + true = ets:delete(?SUBMON, SubPid), + true = (SubId =:= undefined) orelse ets:delete_object(?SUBID, {SubId, SubPid}), + emqx_broker:subscriber_down(SubPid). + diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index efeaabc74..c32800a24 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -90,7 +90,7 @@ init([]) -> [Node | Acc] end end, [], mnesia:dirty_all_keys(?ROUTING_NODE)), - emqx_stats:update_interval(route_stats, fun ?MODULE:stats_fun/0), + ok = emqx_stats:update_interval(route_stats, fun ?MODULE:stats_fun/0), {ok, #{nodes => Nodes}, hibernate}. handle_call(Req, _From, State) -> diff --git a/src/emqx_sequence.erl b/src/emqx_sequence.erl index 022531df5..33bb5edda 100644 --- a/src/emqx_sequence.erl +++ b/src/emqx_sequence.erl @@ -51,6 +51,7 @@ reclaim(Name, Key) -> end. %% @doc Delete the sequence. +-spec(delete(name()) -> boolean()). delete(Name) -> case ets:info(Name, name) of Name -> ets:delete(Name); diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index d178a8ae7..637e44b0c 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -206,7 +206,7 @@ init([]) -> ok = emqx_tables:new(?SESSION_P_TAB, TabOpts), ok = emqx_tables:new(?SESSION_ATTRS_TAB, TabOpts), ok = emqx_tables:new(?SESSION_STATS_TAB, TabOpts), - emqx_stats:update_interval(sm_stats, fun ?MODULE:stats_fun/0), + ok = emqx_stats:update_interval(sm_stats, fun ?MODULE:stats_fun/0), {ok, #{session_pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> diff --git a/src/emqx_tables.erl b/src/emqx_tables.erl index 9b3ebfeae..fdb106a99 100644 --- a/src/emqx_tables.erl +++ b/src/emqx_tables.erl @@ -15,6 +15,7 @@ -module(emqx_tables). -export([new/2]). +-export([lookup_value/2, lookup_value/3]). %% Create a named_table ets. -spec(new(atom(), list()) -> ok). @@ -26,3 +27,16 @@ new(Tab, Opts) -> 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/test/emqx_sequence_SUITE.erl b/test/emqx_sequence_SUITE.erl index f37b60d76..1ac0ea308 100644 --- a/test/emqx_sequence_SUITE.erl +++ b/test/emqx_sequence_SUITE.erl @@ -33,5 +33,6 @@ sequence_generate(_) -> ?assertEqual(1, reclaim(seqtab, key)), ?assertEqual(0, reclaim(seqtab, key)), ?assertEqual(false, ets:member(seqtab, key)), - ?assertEqual(1, nextval(seqtab, key)). + ?assertEqual(1, nextval(seqtab, key)), + ?assert(emqx_sequence:delete(seqtab). diff --git a/test/emqx_tables_SUITE.erl b/test/emqx_tables_SUITE.erl index 95590b0e9..1002c0a0b 100644 --- a/test/emqx_tables_SUITE.erl +++ b/test/emqx_tables_SUITE.erl @@ -20,7 +20,7 @@ all() -> [t_new]. t_new(_) -> - TId = emqx_tables:new(test_table, [{read_concurrency, true}]), - ets:insert(TId, {loss, 100}), - TId = emqx_tables:new(test_table, [{read_concurrency, true}]), - 100 = ets:lookup_element(TId, loss, 2). + 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). From 99872b253fd2a64c60902143c166890706f184d0 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 12 Dec 2018 14:53:22 +0800 Subject: [PATCH 473/520] Fix 'function not exported' crash --- src/emqx_broker.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 9ed4aad06..105589c65 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -96,7 +96,7 @@ subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map SubPid = self(), case ets:member(?SUBOPTION, {SubPid, Topic}) of false -> - ok = emqx_broker_helper:monitor_sub(SubPid, SubId), + ok = emqx_broker_helper:register_sub(SubPid, SubId), do_subscribe(Topic, SubPid, with_subid(SubId, SubOpts)); true -> ok end. From 8d50c62a94f0bfdf2aefef97a468614824f40dca Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 12 Dec 2018 16:10:16 +0800 Subject: [PATCH 474/520] Optimize connection and session management --- src/emqx_cm.erl | 24 +++++++---- src/emqx_protocol.erl | 13 +++--- src/emqx_session.erl | 15 ++++--- src/emqx_session_sup.erl | 2 +- src/emqx_sm.erl | 88 ++++++++++++++++++++-------------------- src/emqx_sm_locker.erl | 2 +- src/emqx_sm_registry.erl | 5 +-- 7 files changed, 78 insertions(+), 71 deletions(-) diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 6756cf02b..0d2ecf5eb 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -36,7 +36,7 @@ -define(CM, ?MODULE). %% ETS Tables. --define(CONN_TAB, emqx_conn). +-define(CONN_TAB, emqx_conn). -define(CONN_ATTRS_TAB, emqx_conn_attrs). -define(CONN_STATS_TAB, emqx_conn_stats). @@ -56,7 +56,7 @@ register_connection(ClientId) when is_binary(ClientId) -> register_connection({ClientId, self()}); register_connection(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> - _ = ets:insert(?CONN_TAB, Conn), + true = ets:insert(?CONN_TAB, Conn), notify({registered, ClientId, ConnPid}). -spec(register_connection(emqx_types:client_id() | {emqx_types:client_id(), pid()}, list()) -> ok). @@ -87,10 +87,13 @@ unregister_connection(ClientId) when is_binary(ClientId) -> unregister_connection({ClientId, self()}); unregister_connection(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> - _ = ets:delete(?CONN_STATS_TAB, Conn), - _ = ets:delete(?CONN_ATTRS_TAB, Conn), - _ = ets:delete_object(?CONN_TAB, Conn), - notify({unregistered, ClientId, ConnPid}). + do_unregister_connection(Conn), + notify({unregistered, ConnPid}). + +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). %% @doc Lookup connection pid -spec(lookup_conn_pid(emqx_types:client_id()) -> pid() | undefined). @@ -138,7 +141,7 @@ handle_call(Req, _From, State) -> handle_cast({notify, {registered, ClientId, ConnPid}}, State = #{conn_pmon := PMon}) -> {noreply, State#{conn_pmon := emqx_pmon:monitor(ConnPid, ClientId, PMon)}}; -handle_cast({notify, {unregistered, _ClientId, ConnPid}}, State = #{conn_pmon := PMon}) -> +handle_cast({notify, {unregistered, ConnPid}}, State = #{conn_pmon := PMon}) -> {noreply, State#{conn_pmon := emqx_pmon:demonitor(ConnPid, PMon)}}; handle_cast(Msg, State) -> @@ -150,7 +153,12 @@ handle_info({'DOWN', _MRef, process, ConnPid, _Reason}, State = #{conn_pmon := P undefined -> {noreply, State}; ClientId -> - unregister_connection({ClientId, ConnPid}), + Conn = {ClientId, ConnPid}, + case ets:member(?CONN_ATTRS_TAB, Conn) of + true -> + ok = emqx_pool:async_submit(fun do_unregister_connection/1, [Conn]); + false -> ok + end, {noreply, State#{conn_pmon := emqx_pmon:erase(ConnPid, PMon)}} end; diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 573b913f7..3052191a1 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -849,14 +849,13 @@ shutdown(_Reason, #pstate{client_id = undefined}) -> ok; shutdown(_Reason, #pstate{connected = false}) -> ok; -shutdown(Reason, #pstate{client_id = ClientId}) when Reason =:= conflict; - Reason =:= discard -> - emqx_cm:unregister_connection(ClientId); -shutdown(Reason, PState = #pstate{connected = true, - client_id = ClientId}) -> +shutdown(conflict, _PState) -> + ok; +shutdown(discard, _PState) -> + ok; +shutdown(Reason, PState) -> ?LOG(info, "Shutdown for ~p", [Reason]), - emqx_hooks:run('client.disconnected', [credentials(PState), Reason]), - emqx_cm:unregister_connection(ClientId). + emqx_hooks:run('client.disconnected', [credentials(PState), Reason]). start_keepalive(0, _PState) -> ignore; diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 262a9a7a8..b92baa567 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -645,16 +645,14 @@ handle_info(Info, State) -> emqx_logger:error("[Session] unexpected info: ~p", [Info]), {noreply, State}. -terminate(Reason, #state{will_msg = WillMsg, client_id = ClientId, conn_pid = ConnPid}) -> - emqx_hooks:run('session.terminated', [#{client_id => ClientId}, Reason]), +terminate(Reason, #state{will_msg = WillMsg, conn_pid = ConnPid}) -> + %% Should not run hooks here. + %% emqx_hooks:run('session.terminated', [#{client_id => ClientId}, Reason]), send_willmsg(WillMsg), %% Ensure to shutdown the connection - if - ConnPid =/= undefined -> - ConnPid ! {shutdown, Reason}; - true -> ok - end, - emqx_sm:unregister_session(ClientId). + (ConnPid =:= undefined) orelse ConnPid ! {shutdown, Reason}. + %% Let it crash. + %% emqx_sm:unregister_session(ClientId). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -1011,3 +1009,4 @@ noreply(State) -> shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. + diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl index 644e33f37..8efc4afc8 100644 --- a/src/emqx_session_sup.erl +++ b/src/emqx_session_sup.erl @@ -38,7 +38,7 @@ init([]) -> [#{id => session, start => {emqx_session, start_link, []}, restart => temporary, - shutdown => 5000, + shutdown => brutal_kill, type => worker, modules => [emqx_session]}]}}. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 637e44b0c..df2e4b862 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -40,9 +40,9 @@ -define(SM, ?MODULE). -%% ETS Tables --define(SESSION_TAB, emqx_session). --define(SESSION_P_TAB, emqx_persistent_session). +%% 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). @@ -59,8 +59,7 @@ open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid end, emqx_sm_locker:trans(ClientId, CleanStart); -open_session(SessAttrs = #{clean_start := false, - client_id := ClientId}) -> +open_session(SessAttrs = #{clean_start := false, client_id := ClientId}) -> ResumeStart = fun(_) -> case resume_session(ClientId, SessAttrs) of {ok, SPid} -> @@ -77,13 +76,14 @@ discard_session(ClientId) when is_binary(ClientId) -> discard_session(ClientId, self()). discard_session(ClientId, ConnPid) when is_binary(ClientId) -> - lists:foreach(fun({_ClientId, SPid}) -> - case catch emqx_session:discard(SPid, ConnPid) of - {Err, Reason} when Err =:= 'EXIT'; Err =:= error -> - emqx_logger:error("[SM] Failed to discard ~p: ~p", [SPid, Reason]); - ok -> ok - end - end, lookup_session(ClientId)). + lists:foreach( + fun({_ClientId, SPid}) -> + case catch emqx_session:discard(SPid, ConnPid) of + {Err, Reason} when Err =:= 'EXIT'; Err =:= error -> + emqx_logger:error("[SM] Failed to discard ~p: ~p", [SPid, Reason]); + ok -> ok + end + end, lookup_session(ClientId)). %% @doc Try to resume a session. -spec(resume_session(emqx_types:client_id(), map()) -> {ok, pid()} | {error, term()}). @@ -116,19 +116,18 @@ close_session(SPid) when is_pid(SPid) -> register_session(ClientId, SessAttrs) when is_binary(ClientId) -> register_session({ClientId, self()}, SessAttrs); -register_session(Session = {ClientId, SPid}, SessAttrs) - when is_binary(ClientId), is_pid(SPid) -> - ets:insert(?SESSION_TAB, Session), - ets:insert(?SESSION_ATTRS_TAB, {Session, SessAttrs}), - proplists:get_value(clean_start, SessAttrs, true) - andalso ets:insert(?SESSION_P_TAB, Session), - emqx_sm_registry:register_session(Session), +register_session(Session = {ClientId, SPid}, SessAttrs) when is_binary(ClientId), is_pid(SPid) -> + true = ets:insert(?SESSION_TAB, Session), + true = ets:insert(?SESSION_ATTRS_TAB, {Session, SessAttrs}), + true = proplists:get_value(clean_start, SessAttrs, true) + orelse ets:insert(?SESSION_P_TAB, Session), + ok = emqx_sm_registry:register_session(Session), notify({registered, ClientId, SPid}). %% @doc Get session attrs -spec(get_session_attrs({emqx_types:client_id(), pid()}) -> list(emqx_session:attr())). get_session_attrs(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> - safe_lookup_element(?SESSION_ATTRS_TAB, Session, []). + emqx_tables:lookup_value(?SESSION_ATTRS_TAB, Session, []). %% @doc Set session attrs -spec(set_session_attrs(emqx_types:client_id() | {emqx_types:client_id(), pid()}, @@ -144,17 +143,21 @@ unregister_session(ClientId) when is_binary(ClientId) -> unregister_session({ClientId, self()}); unregister_session(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> - emqx_sm_registry:unregister_session(Session), - ets:delete(?SESSION_STATS_TAB, Session), - ets:delete(?SESSION_ATTRS_TAB, Session), - ets:delete_object(?SESSION_P_TAB, Session), - ets:delete_object(?SESSION_TAB, Session), + ok = do_unregister_session(Session), notify({unregistered, ClientId, SPid}). +%% @private +do_unregister_session(Session) -> + 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 stats -spec(get_session_stats({emqx_types:client_id(), pid()}) -> list(emqx_stats:stats())). get_session_stats(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> - safe_lookup_element(?SESSION_STATS_TAB, Session, []). + emqx_tables:lookup_value(?SESSION_STATS_TAB, Session, []). %% @doc Set session stats -spec(set_session_stats(emqx_types:client_id() | {emqx_types:client_id(), pid()}, @@ -168,7 +171,7 @@ set_session_stats(Session = {ClientId, SPid}, Stats) when is_binary(ClientId), i -spec(lookup_session(emqx_types:client_id()) -> list({emqx_types:client_id(), pid()})). lookup_session(ClientId) -> case emqx_sm_registry:is_enabled() of - true -> emqx_sm_registry:lookup_session(ClientId); + true -> emqx_sm_registry:lookup_session(ClientId); false -> ets:lookup(?SESSION_TAB, ClientId) end. @@ -185,13 +188,7 @@ dispatch(ClientId, Topic, Msg) -> %% @doc Lookup session pid. -spec(lookup_session_pid(emqx_types:client_id()) -> pid() | undefined). lookup_session_pid(ClientId) -> - safe_lookup_element(?SESSION_TAB, ClientId, undefined). - -safe_lookup_element(Tab, Key, Default) -> - try ets:lookup_element(Tab, Key, 2) - catch - error:badarg -> Default - end. + emqx_tables:lookup_value(?SESSION_TAB, ClientId). notify(Event) -> gen_server:cast(?SM, {notify, Event}). @@ -207,29 +204,34 @@ init([]) -> ok = emqx_tables:new(?SESSION_ATTRS_TAB, TabOpts), ok = emqx_tables:new(?SESSION_STATS_TAB, TabOpts), ok = emqx_stats:update_interval(sm_stats, fun ?MODULE:stats_fun/0), - {ok, #{session_pmon => emqx_pmon:new()}}. + {ok, #{sess_pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> emqx_logger:error("[SM] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({notify, {registered, ClientId, SPid}}, State = #{session_pmon := PMon}) -> - {noreply, State#{session_pmon := emqx_pmon:monitor(SPid, ClientId, PMon)}}; +handle_cast({notify, {registered, ClientId, SPid}}, State = #{sess_pmon := PMon}) -> + {noreply, State#{sess_pmon := emqx_pmon:monitor(SPid, ClientId, PMon)}}; -handle_cast({notify, {unregistered, _ClientId, SPid}}, State = #{session_pmon := PMon}) -> - {noreply, State#{session_pmon := emqx_pmon:demonitor(SPid, PMon)}}; +handle_cast({notify, {unregistered, _ClientId, SPid}}, State = #{sess_pmon := PMon}) -> + {noreply, State#{sess_pmon := emqx_pmon:demonitor(SPid, PMon)}}; handle_cast(Msg, State) -> emqx_logger:error("[SM] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #{session_pmon := PMon}) -> +handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #{sess_pmon := PMon}) -> case emqx_pmon:find(DownPid, PMon) of undefined -> {noreply, State}; - ClientId -> - unregister_session({ClientId, DownPid}), - {noreply, State#{session_pmon := emqx_pmon:erase(DownPid, PMon)}} + ClientId -> + Session = {ClientId, DownPid}, + case ets:member(?SESSION_ATTRS_TAB, Session) of + true -> + ok = emqx_pool:async_submit(fun do_unregister_session/1, [Session]); + false -> ok + end, + {noreply, State#{sess_pmon := emqx_pmon:erase(DownPid, PMon)}} end; handle_info(Info, State) -> diff --git a/src/emqx_sm_locker.erl b/src/emqx_sm_locker.erl index 29adf3342..409331b88 100644 --- a/src/emqx_sm_locker.erl +++ b/src/emqx_sm_locker.erl @@ -21,7 +21,7 @@ -export([trans/2, trans/3]). -export([lock/1, lock/2, unlock/1]). --spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> ekka_locker:start_link(?MODULE). diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index b503d71c8..3b472e2c8 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -41,8 +41,7 @@ start_link() -> gen_server:start_link({local, ?REGISTRY}, ?MODULE, [], []). -spec(is_enabled() -> boolean()). -is_enabled() -> - ets:info(?TAB, name) =/= undefined. +is_enabled() -> ets:info(?TAB, name) =/= undefined. -spec(lookup_session(emqx_types:client_id()) -> list({emqx_types:client_id(), session_pid()})). @@ -73,7 +72,7 @@ init([]) -> {storage_properties, [{ets, [{read_concurrency, true}, {write_concurrency, true}]}]}]), ok = ekka_mnesia:copy_table(?TAB), - _ = ekka:monitor(membership), + ok = ekka:monitor(membership), {ok, #{}}. handle_call(Req, _From, State) -> From 8f2f4b6b812f462f761e0e47ea5c4b357e508565 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 12 Dec 2018 16:10:16 +0800 Subject: [PATCH 475/520] Optimize connection and session management --- src/emqx_cm.erl | 24 +++++++---- src/emqx_protocol.erl | 13 +++--- src/emqx_session.erl | 15 ++++--- src/emqx_session_sup.erl | 2 +- src/emqx_sm.erl | 88 ++++++++++++++++++++-------------------- src/emqx_sm_locker.erl | 2 +- src/emqx_sm_registry.erl | 5 +-- 7 files changed, 78 insertions(+), 71 deletions(-) diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 6756cf02b..0d2ecf5eb 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -36,7 +36,7 @@ -define(CM, ?MODULE). %% ETS Tables. --define(CONN_TAB, emqx_conn). +-define(CONN_TAB, emqx_conn). -define(CONN_ATTRS_TAB, emqx_conn_attrs). -define(CONN_STATS_TAB, emqx_conn_stats). @@ -56,7 +56,7 @@ register_connection(ClientId) when is_binary(ClientId) -> register_connection({ClientId, self()}); register_connection(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> - _ = ets:insert(?CONN_TAB, Conn), + true = ets:insert(?CONN_TAB, Conn), notify({registered, ClientId, ConnPid}). -spec(register_connection(emqx_types:client_id() | {emqx_types:client_id(), pid()}, list()) -> ok). @@ -87,10 +87,13 @@ unregister_connection(ClientId) when is_binary(ClientId) -> unregister_connection({ClientId, self()}); unregister_connection(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> - _ = ets:delete(?CONN_STATS_TAB, Conn), - _ = ets:delete(?CONN_ATTRS_TAB, Conn), - _ = ets:delete_object(?CONN_TAB, Conn), - notify({unregistered, ClientId, ConnPid}). + do_unregister_connection(Conn), + notify({unregistered, ConnPid}). + +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). %% @doc Lookup connection pid -spec(lookup_conn_pid(emqx_types:client_id()) -> pid() | undefined). @@ -138,7 +141,7 @@ handle_call(Req, _From, State) -> handle_cast({notify, {registered, ClientId, ConnPid}}, State = #{conn_pmon := PMon}) -> {noreply, State#{conn_pmon := emqx_pmon:monitor(ConnPid, ClientId, PMon)}}; -handle_cast({notify, {unregistered, _ClientId, ConnPid}}, State = #{conn_pmon := PMon}) -> +handle_cast({notify, {unregistered, ConnPid}}, State = #{conn_pmon := PMon}) -> {noreply, State#{conn_pmon := emqx_pmon:demonitor(ConnPid, PMon)}}; handle_cast(Msg, State) -> @@ -150,7 +153,12 @@ handle_info({'DOWN', _MRef, process, ConnPid, _Reason}, State = #{conn_pmon := P undefined -> {noreply, State}; ClientId -> - unregister_connection({ClientId, ConnPid}), + Conn = {ClientId, ConnPid}, + case ets:member(?CONN_ATTRS_TAB, Conn) of + true -> + ok = emqx_pool:async_submit(fun do_unregister_connection/1, [Conn]); + false -> ok + end, {noreply, State#{conn_pmon := emqx_pmon:erase(ConnPid, PMon)}} end; diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 573b913f7..3052191a1 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -849,14 +849,13 @@ shutdown(_Reason, #pstate{client_id = undefined}) -> ok; shutdown(_Reason, #pstate{connected = false}) -> ok; -shutdown(Reason, #pstate{client_id = ClientId}) when Reason =:= conflict; - Reason =:= discard -> - emqx_cm:unregister_connection(ClientId); -shutdown(Reason, PState = #pstate{connected = true, - client_id = ClientId}) -> +shutdown(conflict, _PState) -> + ok; +shutdown(discard, _PState) -> + ok; +shutdown(Reason, PState) -> ?LOG(info, "Shutdown for ~p", [Reason]), - emqx_hooks:run('client.disconnected', [credentials(PState), Reason]), - emqx_cm:unregister_connection(ClientId). + emqx_hooks:run('client.disconnected', [credentials(PState), Reason]). start_keepalive(0, _PState) -> ignore; diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 262a9a7a8..b92baa567 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -645,16 +645,14 @@ handle_info(Info, State) -> emqx_logger:error("[Session] unexpected info: ~p", [Info]), {noreply, State}. -terminate(Reason, #state{will_msg = WillMsg, client_id = ClientId, conn_pid = ConnPid}) -> - emqx_hooks:run('session.terminated', [#{client_id => ClientId}, Reason]), +terminate(Reason, #state{will_msg = WillMsg, conn_pid = ConnPid}) -> + %% Should not run hooks here. + %% emqx_hooks:run('session.terminated', [#{client_id => ClientId}, Reason]), send_willmsg(WillMsg), %% Ensure to shutdown the connection - if - ConnPid =/= undefined -> - ConnPid ! {shutdown, Reason}; - true -> ok - end, - emqx_sm:unregister_session(ClientId). + (ConnPid =:= undefined) orelse ConnPid ! {shutdown, Reason}. + %% Let it crash. + %% emqx_sm:unregister_session(ClientId). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -1011,3 +1009,4 @@ noreply(State) -> shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. + diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl index 644e33f37..8efc4afc8 100644 --- a/src/emqx_session_sup.erl +++ b/src/emqx_session_sup.erl @@ -38,7 +38,7 @@ init([]) -> [#{id => session, start => {emqx_session, start_link, []}, restart => temporary, - shutdown => 5000, + shutdown => brutal_kill, type => worker, modules => [emqx_session]}]}}. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 637e44b0c..df2e4b862 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -40,9 +40,9 @@ -define(SM, ?MODULE). -%% ETS Tables --define(SESSION_TAB, emqx_session). --define(SESSION_P_TAB, emqx_persistent_session). +%% 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). @@ -59,8 +59,7 @@ open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid end, emqx_sm_locker:trans(ClientId, CleanStart); -open_session(SessAttrs = #{clean_start := false, - client_id := ClientId}) -> +open_session(SessAttrs = #{clean_start := false, client_id := ClientId}) -> ResumeStart = fun(_) -> case resume_session(ClientId, SessAttrs) of {ok, SPid} -> @@ -77,13 +76,14 @@ discard_session(ClientId) when is_binary(ClientId) -> discard_session(ClientId, self()). discard_session(ClientId, ConnPid) when is_binary(ClientId) -> - lists:foreach(fun({_ClientId, SPid}) -> - case catch emqx_session:discard(SPid, ConnPid) of - {Err, Reason} when Err =:= 'EXIT'; Err =:= error -> - emqx_logger:error("[SM] Failed to discard ~p: ~p", [SPid, Reason]); - ok -> ok - end - end, lookup_session(ClientId)). + lists:foreach( + fun({_ClientId, SPid}) -> + case catch emqx_session:discard(SPid, ConnPid) of + {Err, Reason} when Err =:= 'EXIT'; Err =:= error -> + emqx_logger:error("[SM] Failed to discard ~p: ~p", [SPid, Reason]); + ok -> ok + end + end, lookup_session(ClientId)). %% @doc Try to resume a session. -spec(resume_session(emqx_types:client_id(), map()) -> {ok, pid()} | {error, term()}). @@ -116,19 +116,18 @@ close_session(SPid) when is_pid(SPid) -> register_session(ClientId, SessAttrs) when is_binary(ClientId) -> register_session({ClientId, self()}, SessAttrs); -register_session(Session = {ClientId, SPid}, SessAttrs) - when is_binary(ClientId), is_pid(SPid) -> - ets:insert(?SESSION_TAB, Session), - ets:insert(?SESSION_ATTRS_TAB, {Session, SessAttrs}), - proplists:get_value(clean_start, SessAttrs, true) - andalso ets:insert(?SESSION_P_TAB, Session), - emqx_sm_registry:register_session(Session), +register_session(Session = {ClientId, SPid}, SessAttrs) when is_binary(ClientId), is_pid(SPid) -> + true = ets:insert(?SESSION_TAB, Session), + true = ets:insert(?SESSION_ATTRS_TAB, {Session, SessAttrs}), + true = proplists:get_value(clean_start, SessAttrs, true) + orelse ets:insert(?SESSION_P_TAB, Session), + ok = emqx_sm_registry:register_session(Session), notify({registered, ClientId, SPid}). %% @doc Get session attrs -spec(get_session_attrs({emqx_types:client_id(), pid()}) -> list(emqx_session:attr())). get_session_attrs(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> - safe_lookup_element(?SESSION_ATTRS_TAB, Session, []). + emqx_tables:lookup_value(?SESSION_ATTRS_TAB, Session, []). %% @doc Set session attrs -spec(set_session_attrs(emqx_types:client_id() | {emqx_types:client_id(), pid()}, @@ -144,17 +143,21 @@ unregister_session(ClientId) when is_binary(ClientId) -> unregister_session({ClientId, self()}); unregister_session(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> - emqx_sm_registry:unregister_session(Session), - ets:delete(?SESSION_STATS_TAB, Session), - ets:delete(?SESSION_ATTRS_TAB, Session), - ets:delete_object(?SESSION_P_TAB, Session), - ets:delete_object(?SESSION_TAB, Session), + ok = do_unregister_session(Session), notify({unregistered, ClientId, SPid}). +%% @private +do_unregister_session(Session) -> + 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 stats -spec(get_session_stats({emqx_types:client_id(), pid()}) -> list(emqx_stats:stats())). get_session_stats(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> - safe_lookup_element(?SESSION_STATS_TAB, Session, []). + emqx_tables:lookup_value(?SESSION_STATS_TAB, Session, []). %% @doc Set session stats -spec(set_session_stats(emqx_types:client_id() | {emqx_types:client_id(), pid()}, @@ -168,7 +171,7 @@ set_session_stats(Session = {ClientId, SPid}, Stats) when is_binary(ClientId), i -spec(lookup_session(emqx_types:client_id()) -> list({emqx_types:client_id(), pid()})). lookup_session(ClientId) -> case emqx_sm_registry:is_enabled() of - true -> emqx_sm_registry:lookup_session(ClientId); + true -> emqx_sm_registry:lookup_session(ClientId); false -> ets:lookup(?SESSION_TAB, ClientId) end. @@ -185,13 +188,7 @@ dispatch(ClientId, Topic, Msg) -> %% @doc Lookup session pid. -spec(lookup_session_pid(emqx_types:client_id()) -> pid() | undefined). lookup_session_pid(ClientId) -> - safe_lookup_element(?SESSION_TAB, ClientId, undefined). - -safe_lookup_element(Tab, Key, Default) -> - try ets:lookup_element(Tab, Key, 2) - catch - error:badarg -> Default - end. + emqx_tables:lookup_value(?SESSION_TAB, ClientId). notify(Event) -> gen_server:cast(?SM, {notify, Event}). @@ -207,29 +204,34 @@ init([]) -> ok = emqx_tables:new(?SESSION_ATTRS_TAB, TabOpts), ok = emqx_tables:new(?SESSION_STATS_TAB, TabOpts), ok = emqx_stats:update_interval(sm_stats, fun ?MODULE:stats_fun/0), - {ok, #{session_pmon => emqx_pmon:new()}}. + {ok, #{sess_pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> emqx_logger:error("[SM] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({notify, {registered, ClientId, SPid}}, State = #{session_pmon := PMon}) -> - {noreply, State#{session_pmon := emqx_pmon:monitor(SPid, ClientId, PMon)}}; +handle_cast({notify, {registered, ClientId, SPid}}, State = #{sess_pmon := PMon}) -> + {noreply, State#{sess_pmon := emqx_pmon:monitor(SPid, ClientId, PMon)}}; -handle_cast({notify, {unregistered, _ClientId, SPid}}, State = #{session_pmon := PMon}) -> - {noreply, State#{session_pmon := emqx_pmon:demonitor(SPid, PMon)}}; +handle_cast({notify, {unregistered, _ClientId, SPid}}, State = #{sess_pmon := PMon}) -> + {noreply, State#{sess_pmon := emqx_pmon:demonitor(SPid, PMon)}}; handle_cast(Msg, State) -> emqx_logger:error("[SM] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #{session_pmon := PMon}) -> +handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #{sess_pmon := PMon}) -> case emqx_pmon:find(DownPid, PMon) of undefined -> {noreply, State}; - ClientId -> - unregister_session({ClientId, DownPid}), - {noreply, State#{session_pmon := emqx_pmon:erase(DownPid, PMon)}} + ClientId -> + Session = {ClientId, DownPid}, + case ets:member(?SESSION_ATTRS_TAB, Session) of + true -> + ok = emqx_pool:async_submit(fun do_unregister_session/1, [Session]); + false -> ok + end, + {noreply, State#{sess_pmon := emqx_pmon:erase(DownPid, PMon)}} end; handle_info(Info, State) -> diff --git a/src/emqx_sm_locker.erl b/src/emqx_sm_locker.erl index 29adf3342..409331b88 100644 --- a/src/emqx_sm_locker.erl +++ b/src/emqx_sm_locker.erl @@ -21,7 +21,7 @@ -export([trans/2, trans/3]). -export([lock/1, lock/2, unlock/1]). --spec(start_link() -> {ok, pid()} | ignore | {error, term()}). +-spec(start_link() -> emqx_types:startlink_ret()). start_link() -> ekka_locker:start_link(?MODULE). diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index b503d71c8..3b472e2c8 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -41,8 +41,7 @@ start_link() -> gen_server:start_link({local, ?REGISTRY}, ?MODULE, [], []). -spec(is_enabled() -> boolean()). -is_enabled() -> - ets:info(?TAB, name) =/= undefined. +is_enabled() -> ets:info(?TAB, name) =/= undefined. -spec(lookup_session(emqx_types:client_id()) -> list({emqx_types:client_id(), session_pid()})). @@ -73,7 +72,7 @@ init([]) -> {storage_properties, [{ets, [{read_concurrency, true}, {write_concurrency, true}]}]}]), ok = ekka_mnesia:copy_table(?TAB), - _ = ekka:monitor(membership), + ok = ekka:monitor(membership), {ok, #{}}. handle_call(Req, _From, State) -> From d8cbf72da1b07bebefb613d61e36152126f7a1f3 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 12 Dec 2018 16:32:58 +0800 Subject: [PATCH 476/520] Fix 'badarg' crash --- src/emqx_session.erl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index b92baa567..4f0743210 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -648,11 +648,14 @@ handle_info(Info, State) -> terminate(Reason, #state{will_msg = WillMsg, conn_pid = ConnPid}) -> %% Should not run hooks here. %% emqx_hooks:run('session.terminated', [#{client_id => ClientId}, Reason]), + %% Let it crash. + %% emqx_sm:unregister_session(ClientId), send_willmsg(WillMsg), %% Ensure to shutdown the connection - (ConnPid =:= undefined) orelse ConnPid ! {shutdown, Reason}. - %% Let it crash. - %% emqx_sm:unregister_session(ClientId). + if + ConnPid == undefined -> ok; + true -> ConnPid ! {shutdown, Reason} + end. code_change(_OldVsn, State, _Extra) -> {ok, State}. From 4e1d1bd60fb6506ca6c6f14fa9cc3886cef0864f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 13 Dec 2018 14:13:13 +0800 Subject: [PATCH 477/520] Remove the emqx_session_sup module --- src/emqx_session_sup.erl | 44 ---------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 src/emqx_session_sup.erl diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl deleted file mode 100644 index 8efc4afc8..000000000 --- a/src/emqx_session_sup.erl +++ /dev/null @@ -1,44 +0,0 @@ -%% Copyright (c) 2018 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). - --behavior(supervisor). - --include("emqx.hrl"). - --export([start_link/0, start_session/1]). - --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - --spec(start_session(map()) -> {ok, pid()}). -start_session(Attrs) -> - supervisor:start_child(?MODULE, [Attrs]). - -%%-------------------------------------------------------------------- -%% Supervisor callbacks -%%-------------------------------------------------------------------- - -init([]) -> - {ok, {{simple_one_for_one, 0, 1}, - [#{id => session, - start => {emqx_session, start_link, []}, - restart => temporary, - shutdown => brutal_kill, - type => worker, - modules => [emqx_session]}]}}. - From 4aaf0a7db498a2624e52b2677e387b59b44e9a95 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 13 Dec 2018 16:42:10 +0800 Subject: [PATCH 478/520] Remove emqx_session_sup to handle massive concurrent sessions --- src/emqx_session.erl | 39 +++++++++++++++++++++++++-------------- src/emqx_sm.erl | 4 ++-- src/emqx_sup.erl | 3 --- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 4f0743210..f0190dc14 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -370,8 +370,8 @@ init([Parent, #{zone := Zone, topic_alias_maximum = TopicAliasMaximum, will_msg = WillMsg }, - emqx_sm:register_session(ClientId, attrs(State)), - emqx_sm:set_session_stats(ClientId, stats(State)), + ok = emqx_sm:register_session(ClientId, attrs(State)), + true = emqx_sm:set_session_stats(ClientId, stats(State)), emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), ok = emqx_gc:init(GcPolicy), @@ -617,14 +617,20 @@ handle_info({timeout, Timer, emit_stats}, ?LOG(warning, "shutdown due to ~p", [Reason], NewState), shutdown(Reason, NewState) end; + handle_info({timeout, Timer, expired}, State = #state{expiry_timer = Timer}) -> - ?LOG(info, "expired, shutdown now:(", [], State), + ?LOG(info, "expired, shutdown now.", [], State), shutdown(expired, State); 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}) -> send_willmsg(WillMsg), {stop, Reason, State#state{will_msg = undefined, conn_pid = undefined}}; @@ -641,30 +647,35 @@ 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], State), {noreply, State}; + handle_info(Info, State) -> emqx_logger:error("[Session] unexpected info: ~p", [Info]), {noreply, State}. -terminate(Reason, #state{will_msg = WillMsg, conn_pid = ConnPid}) -> - %% Should not run hooks here. - %% emqx_hooks:run('session.terminated', [#{client_id => ClientId}, Reason]), - %% Let it crash. - %% emqx_sm:unregister_session(ClientId), +terminate(Reason, #state{will_msg = WillMsg, + client_id = ClientId, + conn_pid = ConnPid, + old_conn_pid = OldConnPid}) -> send_willmsg(WillMsg), - %% Ensure to shutdown the connection - if - ConnPid == undefined -> ok; - true -> ConnPid ! {shutdown, Reason} - end. + [maybe_shutdown(Pid, Reason) || Pid <- [ConnPid, OldConnPid]], + emqx_hooks:run('session.terminated', [#{client_id => ClientId}, Reason]). code_change(_OldVsn, State, _Extra) -> {ok, State}. +maybe_shutdown(undefined, _Reason) -> + ok; +maybe_shutdown(Pid, normal) -> + Pid ! {shutdown, normal}; +maybe_shutdown(Pid, Reason) -> + exit(Pid, Reason). + %%------------------------------------------------------------------------------ %% Internal functions %%------------------------------------------------------------------------------ -has_connection(#state{conn_pid = Pid}) -> is_pid(Pid) andalso is_process_alive(Pid). +has_connection(#state{conn_pid = Pid}) -> + is_pid(Pid) andalso is_process_alive(Pid). handle_dispatch(Topic, Msg = #message{headers = Headers}, State = #state{subscriptions = SubMap, diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index df2e4b862..fd6a44231 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -55,7 +55,7 @@ start_link() -> 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) + emqx_session:start_link(SessAttrs) end, emqx_sm_locker:trans(ClientId, CleanStart); @@ -65,7 +65,7 @@ open_session(SessAttrs = #{clean_start := false, client_id := ClientId}) -> {ok, SPid} -> {ok, SPid, true}; {error, not_found} -> - emqx_session_sup:start_session(SessAttrs) + emqx_session:start_link(SessAttrs) end end, emqx_sm_locker:trans(ClientId, ResumeStart). diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index 2f29dbfee..60be4db87 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -69,8 +69,6 @@ init([]) -> AccessControl = worker_spec(emqx_access_control), %% Session Manager SMSup = supervisor_spec(emqx_sm_sup), - %% Session Sup - SessionSup = supervisor_spec(emqx_session_sup), %% Connection Manager CMSup = supervisor_spec(emqx_cm_sup), %% Sys Sup @@ -83,7 +81,6 @@ init([]) -> BridgeSup, AccessControl, SMSup, - SessionSup, CMSup, SysSup]}}. From 7fe3d59c282ba2f591e8f2fa018e3313f9f170df Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 13 Dec 2018 17:25:50 +0800 Subject: [PATCH 479/520] Update the registered name of pool sup. --- src/emqx_broker_sup.erl | 4 ++-- src/emqx_pool_sup.erl | 3 ++- src/emqx_router_sup.erl | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/emqx_broker_sup.erl b/src/emqx_broker_sup.erl index a511e4154..f60a2a1b2 100644 --- a/src/emqx_broker_sup.erl +++ b/src/emqx_broker_sup.erl @@ -30,8 +30,8 @@ start_link() -> init([]) -> %% Broker pool PoolSize = emqx_vm:schedulers() * 2, - BrokerPool = emqx_pool_sup:spec(emqx_broker_pool, - [broker, hash, PoolSize, + BrokerPool = emqx_pool_sup:spec(broker_pool, + [emqx_broker_pool, hash, PoolSize, {emqx_broker, start_link, []}]), %% Shared subscription SharedSub = #{id => shared_sub, diff --git a/src/emqx_pool_sup.erl b/src/emqx_pool_sup.erl index b371549c0..e11fa01f2 100644 --- a/src/emqx_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -37,7 +37,8 @@ spec(ChildId, Args) -> start_link(Pool, Type, MFA) -> start_link(Pool, Type, emqx_vm:schedulers(), MFA). --spec(start_link(atom() | tuple(), atom(), pos_integer(), mfa()) -> {ok, pid()} | {error, term()}). +-spec(start_link(atom() | tuple(), atom(), pos_integer(), mfa()) + -> {ok, pid()} | {error, term()}). start_link(Pool, Type, Size, MFA) when is_atom(Pool) -> supervisor:start_link({local, Pool}, ?MODULE, [Pool, Type, Size, MFA]); start_link(Pool, Type, Size, MFA) -> diff --git a/src/emqx_router_sup.erl b/src/emqx_router_sup.erl index 2bbaabc18..c0317fc78 100644 --- a/src/emqx_router_sup.erl +++ b/src/emqx_router_sup.erl @@ -32,8 +32,8 @@ init([]) -> modules => [emqx_router_helper]}, %% Router pool - RouterPool = emqx_pool_sup:spec(emqx_router_pool, - [router, hash, emqx_vm:schedulers(), + RouterPool = emqx_pool_sup:spec(router_pool, + [emqx_router_pool, hash, emqx_vm:schedulers(), {emqx_router, start_link, []}]), {ok, {{one_for_all, 0, 1}, [Helper, RouterPool]}}. From abe9aff06238343cb0ea496fa5fafdcc3adf491c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 13 Dec 2018 18:12:32 +0800 Subject: [PATCH 480/520] Add 'enable_session_registry' config --- etc/emqx.conf | 5 +++++ priv/emqx.schema | 5 +++++ src/emqx_sm_registry.erl | 14 ++++++++++---- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 7876be59b..f38cc1a13 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1898,6 +1898,11 @@ plugins.expand_plugins_dir = {{ platform_plugins_dir }}/ ## Default: 1m, 1 minute broker.sys_interval = 1m +## Enable global session registry. +## +## Value: on | off +broker.enable_session_registry = on + ## Session locking strategy in a cluster. ## ## Value: Enum diff --git a/priv/emqx.schema b/priv/emqx.schema index 1001ab5a8..93b27f0ae 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1723,6 +1723,11 @@ end}. {default, "1m"} ]}. +{mapping, "broker.enable_session_registry", "emqx.enable_session_registry", [ + {default, on}, + {datatype, flag} +]}. + {mapping, "broker.session_locking_strategy", "emqx.session_locking_strategy", [ {default, quorum}, {datatype, {enum, [local,one,quorum,all]}} diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index 3b472e2c8..1d0df61bf 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -20,7 +20,6 @@ -export([start_link/0]). -export([is_enabled/0]). - -export([register_session/1, lookup_session/1, unregister_session/1]). %% gen_server callbacks @@ -41,7 +40,8 @@ start_link() -> gen_server:start_link({local, ?REGISTRY}, ?MODULE, [], []). -spec(is_enabled() -> boolean()). -is_enabled() -> ets:info(?TAB, name) =/= undefined. +is_enabled() -> + emqx_config:get_env(enable_session_registry, true). -spec(lookup_session(emqx_types:client_id()) -> list({emqx_types:client_id(), session_pid()})). @@ -50,11 +50,17 @@ lookup_session(ClientId) -> -spec(register_session({emqx_types:client_id(), session_pid()}) -> ok). register_session({ClientId, SessPid}) when is_binary(ClientId), is_pid(SessPid) -> - mnesia:dirty_write(?TAB, record(ClientId, SessPid)). + case is_enabled() of + true -> mnesia:dirty_write(?TAB, record(ClientId, SessPid)); + false -> ok + end. -spec(unregister_session({emqx_types:client_id(), session_pid()}) -> ok). unregister_session({ClientId, SessPid}) when is_binary(ClientId), is_pid(SessPid) -> - mnesia:dirty_delete_object(?TAB, record(ClientId, SessPid)). + case is_enabled() of + true -> mnesia:dirty_delete_object(?TAB, record(ClientId, SessPid)); + false -> ok + end. record(ClientId, SessPid) -> #global_session{sid = ClientId, pid = SessPid}. From d445c17e6c7fac4214262813187fcbd4e20ca5b3 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Thu, 6 Dec 2018 01:18:55 +0800 Subject: [PATCH 481/520] Move some vm args to file vm.args --- Makefile | 2 +- etc/emqx.conf | 64 ----------------------------------- priv/emqx.schema | 88 ------------------------------------------------ 3 files changed, 1 insertion(+), 153 deletions(-) diff --git a/Makefile b/Makefile index 2c1693813..03bbef018 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ NO_AUTOPATCH = cuttlefish ERLC_OPTS += +debug_info -DAPPLICATION=emqx BUILD_DEPS = cuttlefish -dep_cuttlefish = git-emqx https://github.com/emqx/cuttlefish v2.1.1 +dep_cuttlefish = git-emqx https://github.com/emqx/cuttlefish v2.2.0 #TEST_DEPS = emqx_ct_helplers #dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers diff --git a/etc/emqx.conf b/etc/emqx.conf index f38cc1a13..19bd3f0e8 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -160,35 +160,6 @@ node.name = emqx@127.0.0.1 ## Value: String node.cookie = emqxsecretcookie -## Enable SMP support of Erlang VM. -## -## Value: enable | auto | disable -node.smp = auto - -## Heartbeat monitoring of an Erlang runtime system. Comment the line to disable -## heartbeat, or set the value as 'on' -## -## Value: on -## -## vm.args: -heart -## node.heartbeat = on - -## Enable kernel poll. -## -## Value: on | off -## -## Default: on -node.kernel_poll = on - -## Sets the number of threads in async thread pool. Valid range is 0-1024. -## -## See: http://erlang.org/doc/man/erl.html -## -## Value: 0-1024 -## -## vm.args: +A Number -node.async_threads = 32 - ## Sets the maximum number of simultaneously existing processes for this ## system if a Number is passed as value. ## @@ -208,30 +179,6 @@ node.process_limit = 256000 ## vm.args: +Q Number node.max_ports = 256000 -## Set the distribution buffer busy limit (dist_buf_busy_limit). -## -## See: http://erlang.org/doc/man/erl.html -## -## Value: Number [1KB-2GB] -## -## vm.args: +zdbbl size -node.dist_buffer_size = 8MB - -## Sets the maximum number of ETS tables. Note that mnesia and SSL will -## create temporary ETS tables. -## -## Value: Number -## -## vm.args: +e Number -node.max_ets_tables = 256000 - -## Tweak GC to run more often. -## -## Value: Number [0-65535] -## -## vm.args: -env ERL_FULLSWEEP_AFTER Number -node.fullsweep_after = 1000 - ## Crash dump log file. ## ## Value: Log file @@ -254,17 +201,6 @@ node.proto_dist = inet_tcp ## vm.args: -ssl_dist_optfile ## node.ssl_dist_optfile = {{ platform_etc_dir }}/ssl_dist.conf -## Sets the net_kernel tick time. TickTime is specified in seconds. -## Notice that all communicating nodes are to have the same TickTime -## value specified. -## -## See: http://www.erlang.org/doc/man/kernel_app.html#net_ticktime -## -## Value: Number -## -## vm.args: -kernel net_ticktime Number -node.dist_net_ticktime = 60 - ## Sets the port range for the listener socket of a distributed Erlang node. ## Note that if there are firewalls between clustered nodes, this port segment ## for nodes’ communication should be allowed. diff --git a/priv/emqx.schema b/priv/emqx.schema index 93b27f0ae..1b48681ab 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -191,40 +191,6 @@ end}. {default, "emqxsecretcookie"} ]}. -%% @doc SMP Support -{mapping, "node.smp", "vm_args.-smp", [ - {default, auto}, - {datatype, {enum, [enable, auto, disable]}}, - hidden -]}. - -%% @doc http://erlang.org/doc/man/heart.html -{mapping, "node.heartbeat", "vm_args.-heart", [ - {datatype, flag}, - hidden -]}. - -{translation, "vm_args.-heart", fun(Conf) -> - case cuttlefish:conf_get("node.heartbeat", Conf) of - true -> ""; - false -> cuttlefish:invalid("should be 'on' or comment the line!") - end -end}. - -%% @doc Enable Kernel Poll -{mapping, "node.kernel_poll", "vm_args.+K", [ - {default, on}, - {datatype, flag}, - hidden -]}. - -%% @doc More information at: http://erlang.org/doc/man/erl.html -{mapping, "node.async_threads", "vm_args.+A", [ - {default, 64}, - {datatype, integer}, - {validators, ["range:0-1024"]} -]}. - %% @doc Erlang Process Limit {mapping, "node.process_limit", "vm_args.+P", [ {datatype, integer}, @@ -245,53 +211,6 @@ end}. {validator, "range4ports", "must be 1024 to 134217727", fun(X) -> X >= 1024 andalso X =< 134217727 end}. -%% @doc http://www.erlang.org/doc/man/erl.html#%2bzdbbl -{mapping, "node.dist_buffer_size", "vm_args.+zdbbl", [ - {datatype, bytesize}, - {commented, "32MB"}, - hidden, - {validators, ["zdbbl_range"]} -]}. - -{translation, "vm_args.+zdbbl", - fun(Conf) -> - ZDBBL = cuttlefish:conf_get("node.dist_buffer_size", Conf, undefined), - case ZDBBL of - undefined -> undefined; - X when is_integer(X) -> cuttlefish_util:ceiling(X / 1024); %% Bytes to Kilobytes; - _ -> undefined - end - end -}. - -{validator, "zdbbl_range", "must be between 1KB and 2097151KB", - fun(ZDBBL) -> - %% 2097151KB = 2147482624 - ZDBBL >= 1024 andalso ZDBBL =< 2147482624 - end -}. - -%% @doc http://www.erlang.org/doc/man/erlang.html#system_flag-2 -{mapping, "node.fullsweep_after", "vm_args.-env ERL_FULLSWEEP_AFTER", [ - {default, 1000}, - {datatype, integer}, - hidden, - {validators, ["positive_integer"]} -]}. - -{validator, "positive_integer", "must be a positive integer", - fun(X) -> X >= 0 end}. - -%% Note: OTP R15 and earlier uses -env ERL_MAX_ETS_TABLES, -%% R16+ uses +e -%% @doc The ETS table limit -{mapping, "node.max_ets_tables", - cuttlefish:otp("R16", "vm_args.+e", "vm_args.-env ERL_MAX_ETS_TABLES"), [ - {default, 256000}, - {datatype, integer}, - hidden -]}. - %% @doc Set the location of crash dumps {mapping, "node.crash_dump", "vm_args.-env ERL_CRASH_DUMP", [ {default, "{{crash_dump}}"}, @@ -299,13 +218,6 @@ end}. hidden ]}. -%% @doc http://www.erlang.org/doc/man/kernel_app.html#net_ticktime -{mapping, "node.dist_net_ticktime", "vm_args.-kernel net_ticktime", [ - {commented, 60}, - {datatype, integer}, - hidden -]}. - %% @doc http://www.erlang.org/doc/man/kernel_app.html {mapping, "node.dist_listen_min", "kernel.inet_dist_listen_min", [ {commented, 6369}, From 14abbf96db64f8e21128785818d370b7783c0347 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Thu, 6 Dec 2018 01:30:33 +0800 Subject: [PATCH 482/520] Update cuttlefish tag --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index 424505d49..c3baebadb 100644 --- a/rebar.config +++ b/rebar.config @@ -9,7 +9,7 @@ {ekka, "v0.5.1"}, {clique, "develop"}, {esockd, "v5.4.2"}, - {cuttlefish, "v2.1.1"} + {cuttlefish, "v2.2.0"} ]}. {edoc_opts, [{preprocess, true}]}. From 68a6a88eb9b9924180a7ee7aaaa8e2f404e151ca Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Mon, 10 Dec 2018 16:58:04 +0800 Subject: [PATCH 483/520] Move all vm args into separate file vm.args --- etc/emqx.conf | 41 --------------------- etc/vm.args.cloud | 93 +++++++++++++++++++++++++++++++++++++++++++++++ etc/vm.args.edge | 93 +++++++++++++++++++++++++++++++++++++++++++++++ priv/emqx.schema | 40 -------------------- 4 files changed, 186 insertions(+), 81 deletions(-) create mode 100644 etc/vm.args.cloud create mode 100644 etc/vm.args.edge diff --git a/etc/emqx.conf b/etc/emqx.conf index 19bd3f0e8..45368a066 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -160,47 +160,6 @@ node.name = emqx@127.0.0.1 ## Value: String node.cookie = emqxsecretcookie -## Sets the maximum number of simultaneously existing processes for this -## system if a Number is passed as value. -## -## See: http://erlang.org/doc/man/erl.html -## -## Value: Number [1024-134217727] -## -## vm.args: +P Number -node.process_limit = 256000 - -## Sets the maximum number of simultaneously existing ports for this system. -## -## See: http://erlang.org/doc/man/erl.html -## -## Value: Number [1024-134217727] -## -## vm.args: +Q Number -node.max_ports = 256000 - -## Crash dump log file. -## -## 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. -## -## Value: File -## -## vm.args: -ssl_dist_optfile -## node.ssl_dist_optfile = {{ platform_etc_dir }}/ssl_dist.conf - ## Sets the port range for the listener socket of a distributed Erlang node. ## Note that if there are firewalls between clustered nodes, this port segment ## for nodes’ communication should be allowed. diff --git a/etc/vm.args.cloud b/etc/vm.args.cloud new file mode 100644 index 000000000..47bf9d472 --- /dev/null +++ b/etc/vm.args.cloud @@ -0,0 +1,93 @@ +############################## +# Erlang VM Args +############################## + +## NOTE: +## Basic args like '-name' and '-setcookie' should be configured in emqx.conf +## If they are configured in both file, the args configured in emqx.conf will +## be used. + +## Sets the maximum number of simultaneously existing processes for this system. ++P 256000 + +## Sets the maximum number of simultaneously existing ports for this system. ++Q 262144 + +## Sets the maximum number of ETS tables ++e 256000 + +## Sets the maximum number of atoms the virtual machine can handle. +#+t 1048576 + +## Set the location of crash dumps +-env ERL_CRASH_DUMP {{ platform_log_dir }}/crash.dump + +## Set how many times generational garbages collections can be done without +## forcing a fullsweep collection. +#-env ERL_FULLSWEEP_AFTER 1000 + +## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive +## (Disabled by default..use with caution!) +#-heart + +## Specify the erlang distributed protocol. +## Can be one of: inet_tcp, inet6_tcp, inet_tls +#-proto_dist inet_tcp + +## Specify SSL Options in the file if using SSL for Erlang Distribution. +## Used only when -proto_dist set to inet_tls +#-ssl_dist_optfile {{ platform_etc_dir }}/ssl_dist.conf + +## Specifies the net_kernel tick time in seconds. +## This is the approximate time a connected node may be unresponsive until +## it is considered down and thereby disconnected. +#-kernel net_ticktime 60 + +## Sets the distribution buffer busy limit (dist_buf_busy_limit). ++zdbbl 8192 + +## Sets default scheduler hint for port parallelism. ++spp true + +## Sets the number of threads in async thread pool. Valid range is 0-1024. ++A 8 + +## Sets the default heap size of processes to the size Size. +#+hms 233 + +## Sets the default binary virtual heap size of processes to the size Size. +#+hmbs 46422 + +## Sets the number of IO pollsets to use when polling for I/O. +#+IOp 1 + +## Sets the number of IO poll threads to use when polling for I/O. +#+IOt 1 + +## Sets the number of scheduler threads to create and scheduler threads to set online. +#+S 8:8 + +## Sets the number of dirty CPU scheduler threads to create and dirty CPU scheduler threads to set online. +#+SDcpu 8:8 + +## Sets the number of dirty I/O scheduler threads to create. +#+SDio 10 + +## Suggested stack size, in kilowords, for scheduler threads. +#+sss 32 + +## Suggested stack size, in kilowords, for dirty CPU scheduler threads. +#+sssdcpu 40 + +## Suggested stack size, in kilowords, for dirty IO scheduler threads. +#+sssdio 40 + +## Sets scheduler bind type. +## Can be one of: u, ns, ts, ps, s, nnts, nnps, tnnps, db +#+sbt db + +## Sets a user-defined CPU topology. +#+sct L0-3c0-3p0N0:L4-7c0-3p1N1 + +## Sets the mapping of warning messages for error_logger +#+W w \ No newline at end of file diff --git a/etc/vm.args.edge b/etc/vm.args.edge new file mode 100644 index 000000000..b6906e966 --- /dev/null +++ b/etc/vm.args.edge @@ -0,0 +1,93 @@ +############################## +# Erlang VM Args +############################## + +## NOTE: +## Basic args like '-name' and '-setcookie' should be configured in emqx.conf +## If they are configured in both file, the args configured in emqx.conf will +## be used. + +## Sets the maximum number of simultaneously existing processes for this system. ++P 20480 + +## Sets the maximum number of simultaneously existing ports for this system. ++Q 4096 + +## Sets the maximum number of ETS tables ++e 512 + +## Sets the maximum number of atoms the virtual machine can handle. ++t 65536 + +## Set the location of crash dumps +-env ERL_CRASH_DUMP {{ platform_log_dir }}/crash.dump + +## Set how many times generational garbages collections can be done without +## forcing a fullsweep collection. +-env ERL_FULLSWEEP_AFTER 0 + +## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive +## (Disabled by default..use with caution!) +#-heart + +## Specify the erlang distributed protocol. +## Can be one of: inet_tcp, inet6_tcp, inet_tls +#-proto_dist inet_tcp + +## Specify SSL Options in the file if using SSL for Erlang Distribution. +## Used only when -proto_dist set to inet_tls +#-ssl_dist_optfile {{ platform_etc_dir }}/ssl_dist.conf + +## Specifies the net_kernel tick time in seconds. +## This is the approximate time a connected node may be unresponsive until +## it is considered down and thereby disconnected. +#-kernel net_ticktime 60 + +## Sets the distribution buffer busy limit (dist_buf_busy_limit). ++zdbbl 1024 + +## Sets default scheduler hint for port parallelism. ++spp false + +## Sets the number of threads in async thread pool. Valid range is 0-1024. ++A 1 + +## Sets the default heap size of processes to the size Size. +#+hms 233 + +## Sets the default binary virtual heap size of processes to the size Size. +#+hmbs 46422 + +## Sets the number of IO pollsets to use when polling for I/O. ++IOp 1 + +## Sets the number of IO poll threads to use when polling for I/O. ++IOt 1 + +## Sets the number of scheduler threads to create and scheduler threads to set online. ++S 1:1 + +## Sets the number of dirty CPU scheduler threads to create and dirty CPU scheduler threads to set online. ++SDcpu 1:1 + +## Sets the number of dirty I/O scheduler threads to create. ++SDio 1 + +## Suggested stack size, in kilowords, for scheduler threads. +#+sss 32 + +## Suggested stack size, in kilowords, for dirty CPU scheduler threads. +#+sssdcpu 40 + +## Suggested stack size, in kilowords, for dirty IO scheduler threads. +#+sssdio 40 + +## Sets scheduler bind type. +## Can be one of: u, ns, ts, ps, s, nnts, nnps, tnnps, db +#+sbt db + +## Sets a user-defined CPU topology. +#+sct L0-3c0-3p0N0:L4-7c0-3p1N1 + +## Sets the mapping of warning messages for error_logger +#+W w \ No newline at end of file diff --git a/priv/emqx.schema b/priv/emqx.schema index 1b48681ab..3bb9f34f8 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -173,51 +173,11 @@ 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", [ - {datatype, string}, - hidden -]}. - %% @doc Secret cookie for distributed erlang node {mapping, "node.cookie", "vm_args.-setcookie", [ {default, "emqxsecretcookie"} ]}. -%% @doc Erlang Process Limit -{mapping, "node.process_limit", "vm_args.+P", [ - {datatype, integer}, - {default, 256000}, - hidden -]}. - -%% Note: OTP R15 and earlier uses -env ERL_MAX_PORTS, R16+ uses +Q -%% @doc The number of concurrent ports/sockets -%% Valid range is 1024-134217727 -{mapping, "node.max_ports", - cuttlefish:otp("R16", "vm_args.+Q", "vm_args.-env ERL_MAX_PORTS"), [ - {default, 262144}, - {datatype, integer}, - {validators, ["range4ports"]} -]}. - -{validator, "range4ports", "must be 1024 to 134217727", - fun(X) -> X >= 1024 andalso X =< 134217727 end}. - -%% @doc Set the location of crash dumps -{mapping, "node.crash_dump", "vm_args.-env ERL_CRASH_DUMP", [ - {default, "{{crash_dump}}"}, - {datatype, file}, - hidden -]}. - %% @doc http://www.erlang.org/doc/man/kernel_app.html {mapping, "node.dist_listen_min", "kernel.inet_dist_listen_min", [ {commented, 6369}, From 7c7d6b031c527dfff91995baa0dabae9d9524c10 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Mon, 10 Dec 2018 18:08:24 +0800 Subject: [PATCH 484/520] Modify the NOTE descripition at the begining of the file --- etc/vm.args.cloud | 8 +++++--- etc/vm.args.edge | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/etc/vm.args.cloud b/etc/vm.args.cloud index 47bf9d472..d0992f470 100644 --- a/etc/vm.args.cloud +++ b/etc/vm.args.cloud @@ -3,9 +3,11 @@ ############################## ## NOTE: -## Basic args like '-name' and '-setcookie' should be configured in emqx.conf -## If they are configured in both file, the args configured in emqx.conf will -## be used. +## +## Arguments configured in this file might be overridden by configs from `emqx.conf`. +## +## Some basic VM arguments are to be configured in `emqx.conf`, +## such as `node.name` for `-name` and `node.cooke` for `-setcookie`. ## Sets the maximum number of simultaneously existing processes for this system. +P 256000 diff --git a/etc/vm.args.edge b/etc/vm.args.edge index b6906e966..20adc41ab 100644 --- a/etc/vm.args.edge +++ b/etc/vm.args.edge @@ -3,9 +3,11 @@ ############################## ## NOTE: -## Basic args like '-name' and '-setcookie' should be configured in emqx.conf -## If they are configured in both file, the args configured in emqx.conf will -## be used. +## +## Arguments configured in this file might be overridden by configs from `emqx.conf`. +## +## Some basic VM arguments are to be configured in `emqx.conf`, +## such as `node.name` for `-name` and `node.cooke` for `-setcookie`. ## Sets the maximum number of simultaneously existing processes for this system. +P 20480 From 3df8de2419cc302493d6e0ae9c705255c7d72b1f Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 14 Dec 2018 10:23:51 +0800 Subject: [PATCH 485/520] Rename vm.args.cloud -> vm.args --- etc/{vm.args.cloud => vm.args} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename etc/{vm.args.cloud => vm.args} (100%) diff --git a/etc/vm.args.cloud b/etc/vm.args similarity index 100% rename from etc/vm.args.cloud rename to etc/vm.args From 52e2c56ce1433b6fc3107e0d20d449f6f1112af3 Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 14 Dec 2018 14:58:53 +0800 Subject: [PATCH 486/520] Change default configs for max-connections --- etc/emqx.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 7876be59b..6370e7214 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -197,7 +197,7 @@ node.async_threads = 32 ## Value: Number [1024-134217727] ## ## vm.args: +P Number -node.process_limit = 256000 +node.process_limit = 2048000 ## Sets the maximum number of simultaneously existing ports for this system. ## @@ -206,7 +206,7 @@ node.process_limit = 256000 ## Value: Number [1024-134217727] ## ## vm.args: +Q Number -node.max_ports = 256000 +node.max_ports = 1024000 ## Set the distribution buffer busy limit (dist_buf_busy_limit). ## @@ -879,7 +879,7 @@ listener.tcp.internal.acceptors = 4 ## Maximum number of concurrent MQTT/TCP connections. ## ## Value: Number -listener.tcp.internal.max_connections = 10240000 +listener.tcp.internal.max_connections = 1024000 ## Maximum internal connections per second. ## From f54d414825654e1edf09363fb2602d5c9d0c97eb Mon Sep 17 00:00:00 2001 From: turtled Date: Sat, 15 Dec 2018 13:31:38 +0800 Subject: [PATCH 487/520] Fix pick fail --- src/emqx_broker.erl | 2 +- src/emqx_router.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 105589c65..60fb5fd8f 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -371,7 +371,7 @@ cast(Broker, Msg) -> %% Pick a broker pick(Topic) -> - gproc_pool:pick_worker(broker, Topic). + gproc_pool:pick_worker(emqx_broker_pool, Topic). %%------------------------------------------------------------------------------ %% gen_server callbacks diff --git a/src/emqx_router.erl b/src/emqx_router.erl index c165b97ca..0951c82d6 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -148,7 +148,7 @@ call(Router, Msg) -> gen_server:call(Router, Msg, infinity). pick(Topic) -> - gproc_pool:pick_worker(router, Topic). + gproc_pool:pick_worker(emqx_router_pool, Topic). %%------------------------------------------------------------------------------ %% gen_server callbacks From 721b72b96a25c7bdacf9350b8fe68deac0ee2b2c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 17 Dec 2018 19:53:29 +0800 Subject: [PATCH 488/520] Add 'active_n' option to optimize the CPU usage of emqx_connection (#2060) * Add 'active_n' option to optimize the CPU usage of emqx_connection * Supports batch processing 'DOWN' events --- Makefile | 2 +- etc/emqx.conf | 15 +++ priv/emqx.schema | 11 ++ src/emqx_banned.erl | 44 +++---- src/emqx_broker.erl | 2 +- src/emqx_broker_helper.erl | 33 ++++-- src/emqx_broker_sup.erl | 4 +- src/emqx_cm.erl | 153 ++++++++++++------------ src/emqx_cm_sup.erl | 24 ++-- src/emqx_connection.erl | 55 +++++---- src/emqx_misc.erl | 18 +++ src/emqx_pmon.erl | 38 ++++-- src/emqx_pool_sup.erl | 2 - src/emqx_protocol.erl | 21 ++-- src/emqx_router.erl | 2 +- src/emqx_router_sup.erl | 4 +- src/emqx_session.erl | 5 +- src/emqx_sm.erl | 220 ++++++++++++++++++++--------------- src/emqx_sm_registry.erl | 5 +- test/emqx_banned_SUITE.erl | 29 +++-- test/emqx_cm_SUITE.erl | 22 ++-- test/emqx_pmon_SUITE.erl | 48 ++++++++ test/emqx_sequence_SUITE.erl | 2 +- test/emqx_sm_SUITE.erl | 16 ++- 24 files changed, 472 insertions(+), 303 deletions(-) create mode 100644 test/emqx_pmon_SUITE.erl diff --git a/Makefile b/Makefile index 03bbef018..68d383940 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_session \ emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_mountpoint \ emqx_listeners emqx_protocol emqx_pool emqx_shared_sub emqx_bridge \ - emqx_hooks emqx_batch emqx_sequence + emqx_hooks emqx_batch emqx_sequence emqx_pmon CT_NODE_NAME = emqxct@127.0.0.1 CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME) diff --git a/etc/emqx.conf b/etc/emqx.conf index 45368a066..09ce1b526 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -645,6 +645,11 @@ listener.tcp.external.max_connections = 1024000 ## Value: Number listener.tcp.external.max_conn_rate = 1000 +## Specify the {active, N} option for the external MQTT/TCP Socket. +## +## Value: Number +listener.tcp.external.active_n = 100 + ## Zone of the external MQTT/TCP listener belonged to. ## ## See: zone.$name.* @@ -781,6 +786,11 @@ listener.tcp.internal.max_connections = 10240000 ## Value: Number listener.tcp.internal.max_conn_rate = 1000 +## Specify the {active, N} option for the internal MQTT/TCP Socket. +## +## Value: Number +listener.tcp.internal.active_n = 1000 + ## Zone of the internal MQTT/TCP listener belonged to. ## ## Value: String @@ -888,6 +898,11 @@ listener.ssl.external.max_connections = 102400 ## Value: Number listener.ssl.external.max_conn_rate = 500 +## Specify the {active, N} option for the internal MQTT/SSL Socket. +## +## Value: Number +listener.ssl.external.active_n = 100 + ## Zone of the external MQTT/SSL listener belonged to. ## ## Value: String diff --git a/priv/emqx.schema b/priv/emqx.schema index 3bb9f34f8..7c37383c4 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -772,6 +772,11 @@ end}. {datatype, integer} ]}. +{mapping, "listener.tcp.$name.active_n", "emqx.listeners", [ + {default, 100}, + {datatype, integer} +]}. + {mapping, "listener.tcp.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -867,6 +872,11 @@ end}. {datatype, integer} ]}. +{mapping, "listener.ssl.$name.active_n", "emqx.listeners", [ + {default, 100}, + {datatype, integer} +]}. + {mapping, "listener.ssl.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1283,6 +1293,7 @@ end}. {mqtt_path, cuttlefish:conf_get(Prefix ++ ".mqtt_path", Conf, undefined)}, {max_connections, cuttlefish:conf_get(Prefix ++ ".max_connections", Conf)}, {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, + {active_n, cuttlefish:conf_get(Prefix ++ ".active_n", Conf, undefined)}, {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, {zone, Atom(cuttlefish:conf_get(Prefix ++ ".zone", Conf, undefined))}, {rate_limit, Ratelimit(cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined))}, diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index 49a698384..af580b48e 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -26,17 +26,16 @@ -export([start_link/0]). -export([check/1]). --export([add/1, del/1]). +-export([add/1, delete/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(TAB, ?MODULE). --define(SERVER, ?MODULE). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Mnesia bootstrap -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ @@ -52,7 +51,7 @@ mnesia(copy) -> %% @doc Start the banned server. -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec(check(emqx_types:credentials()) -> boolean()). check(#{client_id := ClientId, username := Username, peername := {IPAddr, _}}) -> @@ -64,25 +63,25 @@ check(#{client_id := ClientId, username := Username, peername := {IPAddr, _}}) - add(Banned) when is_record(Banned, banned) -> mnesia:dirty_write(?TAB, Banned). --spec(del({client_id, emqx_types:client_id()} | - {username, emqx_types:username()} | - {peername, emqx_types:peername()}) -> ok). -del(Key) -> +-spec(delete({client_id, emqx_types:client_id()} + | {username, emqx_types:username()} + | {peername, emqx_types:peername()}) -> ok). +delete(Key) -> mnesia:dirty_delete(?TAB, Key). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ init([]) -> {ok, ensure_expiry_timer(#{expiry_timer => undefined})}. handle_call(Req, _From, State) -> - emqx_logger:error("[BANNED] unexpected call: ~p", [Req]), + emqx_logger:error("[Banned] unexpected call: ~p", [Req]), {reply, ignored, State}. handle_cast(Msg, State) -> - emqx_logger:error("[BANNED] unexpected msg: ~p", [Msg]), + emqx_logger:error("[Banned] unexpected msg: ~p", [Msg]), {noreply, State}. handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) -> @@ -90,7 +89,7 @@ handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) -> {noreply, ensure_expiry_timer(State), hibernate}; handle_info(Info, State) -> - emqx_logger:error("[BANNED] unexpected info: ~p", [Info]), + emqx_logger:error("[Banned] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #{expiry_timer := TRef}) -> @@ -99,21 +98,22 @@ terminate(_Reason, #{expiry_timer := TRef}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -ifdef(TEST). ensure_expiry_timer(State) -> State#{expiry_timer := emqx_misc:start_timer(timer:seconds(2), expire)}. -else. ensure_expiry_timer(State) -> - State#{expiry_timer := emqx_misc:start_timer(timer:minutes(5), expire)}. + State#{expiry_timer := emqx_misc:start_timer(timer:minutes(1), expire)}. -endif. expire_banned_items(Now) -> - mnesia:foldl(fun - (B = #banned{until = Until}, _Acc) when Until < Now -> - mnesia:delete_object(?TAB, B, sticky_write); - (_, _Acc) -> ok - end, ok, ?TAB). + mnesia:foldl( + fun(B = #banned{until = Until}, _Acc) when Until < Now -> + mnesia:delete_object(?TAB, B, sticky_write); + (_, _Acc) -> ok + end, ok, ?TAB). + diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 60fb5fd8f..6d1ee98cb 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -371,7 +371,7 @@ cast(Broker, Msg) -> %% Pick a broker pick(Topic) -> - gproc_pool:pick_worker(emqx_broker_pool, Topic). + gproc_pool:pick_worker(broker_pool, Topic). %%------------------------------------------------------------------------------ %% gen_server callbacks diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index 7d514e31d..c8e2b5eab 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -31,6 +31,8 @@ -define(SUBSEQ, emqx_subseq). -define(SHARD, 1024). +-define(BATCH_SIZE, 10000). + -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?HELPER}, ?MODULE, [], []). @@ -106,14 +108,12 @@ handle_cast(Msg, State) -> emqx_logger:error("[BrokerHelper] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, SubPid, Reason}, State = #{pmon := PMon}) -> - case ets:lookup(?SUBMON, SubPid) of - [{_, SubId}] -> - ok = emqx_pool:async_submit(fun subscriber_down/2, [SubPid, SubId]); - [] -> - emqx_logger:error("[BrokerHelper] unexpected DOWN: ~p, reason: ~p", [SubPid, Reason]) - end, - {noreply, State#{pmon := emqx_pmon:erase(SubPid, PMon)}}; +handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #{pmon := PMon}) -> + SubPids = [SubPid | emqx_misc:drain_down(?BATCH_SIZE)], + ok = emqx_pool:async_submit( + fun lists:foreach/2, [fun clean_down/1, SubPids]), + {_, PMon1} = emqx_pmon:erase_all(SubPids, PMon), + {noreply, State#{pmon := PMon1}}; handle_info(Info, State) -> emqx_logger:error("[BrokerHelper] unexpected info: ~p", [Info]), @@ -126,8 +126,17 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -subscriber_down(SubPid, SubId) -> - true = ets:delete(?SUBMON, SubPid), - true = (SubId =:= undefined) orelse ets:delete_object(?SUBID, {SubId, SubPid}), - emqx_broker:subscriber_down(SubPid). +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +clean_down(SubPid) -> + case ets:lookup(?SUBMON, SubPid) of + [{_, SubId}] -> + true = ets:delete(?SUBMON, SubPid), + true = (SubId =:= undefined) + orelse ets:delete_object(?SUBID, {SubId, SubPid}), + emqx_broker:subscriber_down(SubPid); + [] -> ok + end. diff --git a/src/emqx_broker_sup.erl b/src/emqx_broker_sup.erl index f60a2a1b2..5b1c0a0e7 100644 --- a/src/emqx_broker_sup.erl +++ b/src/emqx_broker_sup.erl @@ -30,9 +30,9 @@ start_link() -> init([]) -> %% Broker pool PoolSize = emqx_vm:schedulers() * 2, - BrokerPool = emqx_pool_sup:spec(broker_pool, - [emqx_broker_pool, hash, PoolSize, + BrokerPool = emqx_pool_sup:spec([broker_pool, hash, PoolSize, {emqx_broker, start_link, []}]), + %% Shared subscription SharedSub = #{id => shared_sub, start => {emqx_shared_sub, start_link, []}, diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 0d2ecf5eb..62cb417de 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -20,74 +20,57 @@ -export([start_link/0]). --export([lookup_connection/1]). -export([register_connection/1, register_connection/2]). --export([unregister_connection/1]). --export([get_conn_attrs/1, set_conn_attrs/2]). --export([get_conn_stats/1, set_conn_stats/2]). +-export([unregister_connection/1, unregister_connection/2]). +-export([get_conn_attrs/1, get_conn_attrs/2]). +-export([set_conn_attrs/2, set_conn_attrs/3]). +-export([get_conn_stats/1, get_conn_stats/2]). +-export([set_conn_stats/2, set_conn_stats/3]). -export([lookup_conn_pid/1]). +%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% internal export --export([update_conn_stats/0]). +-export([stats_fun/0]). -define(CM, ?MODULE). -%% ETS Tables. +%% ETS tables for connection management. -define(CONN_TAB, emqx_conn). -define(CONN_ATTRS_TAB, emqx_conn_attrs). -define(CONN_STATS_TAB, emqx_conn_stats). +-define(BATCH_SIZE, 10000). + %% @doc Start the connection manager. -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?CM}, ?MODULE, [], []). -%% @doc Lookup a connection. --spec(lookup_connection(emqx_types:client_id()) -> list({emqx_types:client_id(), pid()})). -lookup_connection(ClientId) when is_binary(ClientId) -> - ets:lookup(?CONN_TAB, ClientId). +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ %% @doc Register a connection. --spec(register_connection(emqx_types:client_id() | {emqx_types:client_id(), pid()}) -> ok). +-spec(register_connection(emqx_types:client_id()) -> ok). register_connection(ClientId) when is_binary(ClientId) -> - register_connection({ClientId, self()}); + register_connection(ClientId, self()). -register_connection(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> - true = ets:insert(?CONN_TAB, Conn), +-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}). --spec(register_connection(emqx_types:client_id() | {emqx_types:client_id(), pid()}, list()) -> ok). -register_connection(ClientId, Attrs) when is_binary(ClientId) -> - register_connection({ClientId, self()}, Attrs); -register_connection(Conn = {ClientId, ConnPid}, Attrs) when is_binary(ClientId), is_pid(ConnPid) -> - set_conn_attrs(Conn, Attrs), - register_connection(Conn). - -%% @doc Get conn attrs --spec(get_conn_attrs({emqx_types:client_id(), pid()}) -> list()). -get_conn_attrs(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> - try - ets:lookup_element(?CONN_ATTRS_TAB, Conn, 2) - catch - error:badarg -> [] - end. - -%% @doc Set conn attrs -set_conn_attrs(ClientId, Attrs) when is_binary(ClientId) -> - set_conn_attrs({ClientId, self()}, Attrs); -set_conn_attrs(Conn = {ClientId, ConnPid}, Attrs) when is_binary(ClientId), is_pid(ConnPid) -> - ets:insert(?CONN_ATTRS_TAB, {Conn, Attrs}). - -%% @doc Unregister a conn. --spec(unregister_connection(emqx_types:client_id() | {emqx_types:client_id(), pid()}) -> ok). +%% @doc Unregister a connection. +-spec(unregister_connection(emqx_types:client_id()) -> ok). unregister_connection(ClientId) when is_binary(ClientId) -> - unregister_connection({ClientId, self()}); + unregister_connection(ClientId, self()). -unregister_connection(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> - do_unregister_connection(Conn), +-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}). do_unregister_connection(Conn) -> @@ -95,30 +78,52 @@ do_unregister_connection(Conn) -> true = ets:delete(?CONN_ATTRS_TAB, Conn), true = ets:delete_object(?CONN_TAB, Conn). -%% @doc Lookup connection pid --spec(lookup_conn_pid(emqx_types:client_id()) -> pid() | undefined). -lookup_conn_pid(ClientId) when is_binary(ClientId) -> - case ets:lookup(?CONN_TAB, ClientId) of - [] -> undefined; - [{_, Pid}] -> Pid - end. +%% @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). + +-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}, []). + +%% @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). + +-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 conn stats --spec(get_conn_stats({emqx_types:client_id(), pid()}) -> list(emqx_stats:stats())). -get_conn_stats(Conn = {ClientId, ConnPid}) when is_binary(ClientId), is_pid(ConnPid) -> - try ets:lookup_element(?CONN_STATS_TAB, Conn, 2) - catch - error:badarg -> [] - end. +-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_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 conn stats. --spec(set_conn_stats(emqx_types:client_id(), list(emqx_stats:stats())) -> boolean()). +-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); + set_conn_stats(ClientId, self(), Stats). -set_conn_stats(Conn = {ClientId, ConnPid}, Stats) when is_binary(ClientId), is_pid(ConnPid) -> +-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 Lookup connection pid. +-spec(lookup_conn_pid(emqx_types:client_id()) -> pid() | undefined). +lookup_conn_pid(ClientId) when is_binary(ClientId) -> + emqx_tables:lookup_value(?CONN_TAB, ClientId). + notify(Msg) -> gen_server:cast(?CM, {notify, Msg}). @@ -131,7 +136,7 @@ init([]) -> 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(cm_stats, fun ?MODULE:update_conn_stats/0), + ok = emqx_stats:update_interval(conn_stats, fun ?MODULE:stats_fun/0), {ok, #{conn_pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> @@ -148,26 +153,19 @@ handle_cast(Msg, State) -> emqx_logger:error("[CM] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, ConnPid, _Reason}, State = #{conn_pmon := PMon}) -> - case emqx_pmon:find(ConnPid, PMon) of - undefined -> - {noreply, State}; - ClientId -> - Conn = {ClientId, ConnPid}, - case ets:member(?CONN_ATTRS_TAB, Conn) of - true -> - ok = emqx_pool:async_submit(fun do_unregister_connection/1, [Conn]); - false -> ok - end, - {noreply, State#{conn_pmon := emqx_pmon:erase(ConnPid, PMon)}} - end; +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(Info, State) -> emqx_logger:error("[CM] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> - emqx_stats:cancel_update(cm_stats). + emqx_stats:cancel_update(conn_stats). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -176,7 +174,16 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -update_conn_stats() -> +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. + +stats_fun() -> case ets:info(?CONN_TAB, size) of undefined -> ok; Size -> emqx_stats:setstat('connections/count', 'connections/max', Size) diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index 000e79336..19dd9cb50 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -25,17 +25,17 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - Banned = #{id => banned, - start => {emqx_banned, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_banned]}, - Manager = #{id => manager, - start => {emqx_cm, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_cm]}, + Banned = #{id => banned, + start => {emqx_banned, start_link, []}, + restart => permanent, + shutdown => 1000, + type => worker, + modules => [emqx_banned]}, + Manager = #{id => manager, + start => {emqx_cm, start_link, []}, + restart => permanent, + shutdown => 2000, + type => worker, + modules => [emqx_cm]}, {ok, {{one_for_one, 10, 100}, [Banned, Manager]}}. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 3d0ab0df7..e1d4011fc 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -38,7 +38,7 @@ peername, sockname, conn_state, - await_recv, + active_n, proto_state, parser_state, keepalive, @@ -51,6 +51,7 @@ idle_timeout }). +-define(DEFAULT_ACTIVE_N, 100). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). start_link(Transport, Socket, Options) -> @@ -69,7 +70,7 @@ info(#state{transport = Transport, peername = Peername, sockname = Sockname, conn_state = ConnState, - await_recv = AwaitRecv, + active_n = ActiveN, rate_limit = RateLimit, publish_limit = PubLimit, proto_state = ProtoState}) -> @@ -77,7 +78,7 @@ info(#state{transport = Transport, {peername, Peername}, {sockname, Sockname}, {conn_state, ConnState}, - {await_recv, AwaitRecv}, + {active_n, ActiveN}, {rate_limit, esockd_rate_limit:info(RateLimit)}, {publish_limit, esockd_rate_limit:info(PubLimit)}], ProtoInfo = emqx_protocol:info(ProtoState), @@ -87,8 +88,8 @@ info(#state{transport = Transport, attrs(CPid) when is_pid(CPid) -> call(CPid, attrs); -attrs(#state{peername = Peername, - sockname = Sockname, +attrs(#state{peername = Peername, + sockname = Sockname, proto_state = ProtoState}) -> SockAttrs = [{peername, Peername}, {sockname, Sockname}], @@ -129,6 +130,7 @@ init([Transport, RawSocket, Options]) -> Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]), 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, ?DEFAULT_ACTIVE_N), EnableStats = emqx_zone:get_env(Zone, enable_stats, true), IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), SendFun = send_fun(Transport, Socket), @@ -140,8 +142,8 @@ init([Transport, RawSocket, Options]) -> State = run_socket(#state{transport = Transport, socket = Socket, peername = Peername, - await_recv = false, conn_state = running, + active_n = ActiveN, rate_limit = RateLimit, publish_limit = PubLimit, proto_state = ProtoState, @@ -243,19 +245,26 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> ?LOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid]), shutdown(conflict, State); -handle_info(activate_sock, State) -> - {noreply, run_socket(State#state{conn_state = running, limit_timer = undefined})}; - -handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) -> +handle_info({tcp, _Sock, Data}, State) -> ?LOG(debug, "RECV ~p", [Data]), Size = iolist_size(Data), emqx_metrics:trans(inc, 'bytes/received', Size), Incoming = #{bytes => Size, packets => 0}, - handle_packet(Data, State#state{await_recv = false, incoming = Incoming}); + handle_packet(Data, State#state{incoming = Incoming}); -handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) -> +%% Rate limit here, cool:) +handle_info({tcp_passive, _Sock}, State) -> + {noreply, ensure_rate_limit(State)}; + +handle_info({tcp_error, _Sock, Reason}, State) -> shutdown(Reason, State); +handle_info({tcp_closed, _Sock}, State) -> + shutdown(closed, State); + +handle_info(activate_sock, State) -> + {noreply, run_socket(State#state{conn_state = running, limit_timer = undefined})}; + handle_info({inet_reply, _Sock, ok}, State) -> {noreply, State}; @@ -314,16 +323,17 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ %% Receive and parse data -handle_packet(<<>>, State0) -> - State = ensure_stats_timer(ensure_rate_limit(State0)), - ok = maybe_gc(State, incoming), - {noreply, State}; +handle_packet(<<>>, State) -> + NState = ensure_stats_timer(State), + ok = maybe_gc(NState, incoming), + {noreply, NState}; + handle_packet(Data, State = #state{proto_state = ProtoState, parser_state = ParserState, idle_timeout = IdleTimeout}) -> case catch emqx_frame:parse(Data, ParserState) of {more, NewParserState} -> - {noreply, run_socket(State#state{parser_state = NewParserState}), IdleTimeout}; + {noreply, State#state{parser_state = NewParserState}, IdleTimeout}; {ok, Packet = ?PACKET(Type), Rest} -> emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of @@ -352,6 +362,7 @@ reset_parser(State = #state{proto_state = ProtoState}) -> inc_publish_cnt(Type, State = #state{incoming = Incoming = #{packets := Cnt}}) when Type == ?PUBLISH; Type == ?SUBSCRIBE -> State#state{incoming = Incoming#{packets := Cnt + 1}}; + inc_publish_cnt(_Type, State) -> State. @@ -379,11 +390,11 @@ ensure_rate_limit([{Rl, Pos, Num}|Limiters], State) -> run_socket(State = #state{conn_state = blocked}) -> State; -run_socket(State = #state{await_recv = true}) -> - State; -run_socket(State = #state{transport = Transport, socket = Socket}) -> - Transport:async_recv(Socket, 0, infinity), - State#state{await_recv = true}. +run_socket(State = #state{transport = Transport, + socket = Socket, + active_n = ActiveN}) -> + Transport:setopts(Socket, [{active, ActiveN}]), + State. %%------------------------------------------------------------------------------ %% Ensure stats timer diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index cf4a555ca..38bc1c2b0 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -19,6 +19,8 @@ -export([init_proc_mng_policy/1, conn_proc_mng_policy/1]). +-export([drain_down/1]). + %% @doc Merge options -spec(merge_opts(list(), list()) -> list()). merge_opts(Defaults, Options) -> @@ -108,3 +110,19 @@ is_enabled(Max) -> is_integer(Max) andalso Max > ?DISABLED. proc_info(Key) -> {Key, Value} = erlang:process_info(self(), Key), Value. + +-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) + end. + diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index 9b874041e..a00212ed5 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -14,24 +14,27 @@ -module(emqx_pmon). +-compile({no_auto_import, [monitor/3]}). + -export([new/0]). -export([monitor/2, monitor/3]). -export([demonitor/2]). -export([find/2]). --export([erase/2]). - --compile({no_auto_import,[monitor/3]}). +-export([erase/2, erase_all/2]). +-export([count/1]). -type(pmon() :: {?MODULE, map()}). -export_type([pmon/0]). -spec(new() -> pmon()). -new() -> {?MODULE, maps:new()}. +new() -> + {?MODULE, maps:new()}. -spec(monitor(pid(), pmon()) -> pmon()). monitor(Pid, PM) -> - monitor(Pid, undefined, PM). + ?MODULE:monitor(Pid, undefined, PM). +-spec(monitor(pid(), term(), pmon()) -> pmon()). monitor(Pid, Val, {?MODULE, PM}) -> {?MODULE, case maps:is_key(Pid, PM) of true -> PM; @@ -43,21 +46,36 @@ monitor(Pid, Val, {?MODULE, PM}) -> demonitor(Pid, {?MODULE, PM}) -> {?MODULE, case maps:find(Pid, PM) of {ok, {Ref, _Val}} -> - %% Don't flush - _ = erlang:demonitor(Ref), + %% flush + _ = erlang:demonitor(Ref, [flush]), maps:remove(Pid, PM); error -> PM end}. --spec(find(pid(), pmon()) -> undefined | term()). +-spec(find(pid(), pmon()) -> error | {ok, term()}). find(Pid, {?MODULE, PM}) -> case maps:find(Pid, PM) of {ok, {_Ref, Val}} -> - Val; - error -> undefined + {ok, Val}; + error -> error end. -spec(erase(pid(), pmon()) -> pmon()). erase(Pid, {?MODULE, PM}) -> {?MODULE, maps:remove(Pid, PM)}. +-spec(erase_all([pid()], pmon()) -> {[{pid(), term()}], pmon()}). +erase_all(Pids, PMon0) -> + lists:foldl( + fun(Pid, {Acc, PMon}) -> + case find(Pid, PMon) of + {ok, Val} -> + {[{Pid, Val}|Acc], erase(Pid, PMon)}; + error -> {Acc, PMon} + end + end, {[], PMon0}, Pids). + +-spec(count(pmon()) -> non_neg_integer()). +count({?MODULE, PM}) -> + maps:size(PM). + diff --git a/src/emqx_pool_sup.erl b/src/emqx_pool_sup.erl index e11fa01f2..eb81233f9 100644 --- a/src/emqx_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -39,8 +39,6 @@ start_link(Pool, Type, MFA) -> -spec(start_link(atom() | tuple(), atom(), pos_integer(), mfa()) -> {ok, pid()} | {error, term()}). -start_link(Pool, Type, Size, MFA) when is_atom(Pool) -> - supervisor:start_link({local, Pool}, ?MODULE, [Pool, Type, Size, MFA]); start_link(Pool, Type, Size, MFA) -> supervisor:start_link(?MODULE, [Pool, Type, Size, MFA]). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 3052191a1..dad40e190 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -320,7 +320,8 @@ process_packet(?CONNECT_PACKET( case try_open_session(PState3) of {ok, SPid, SP} -> PState4 = PState3#pstate{session = SPid, connected = true}, - ok = emqx_cm:register_connection(client_id(PState4), attrs(PState4)), + 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 @@ -497,18 +498,18 @@ do_publish(Packet = ?PUBLISH_PACKET(QoS, PacketId), puback(?QOS_0, _PacketId, _Result, PState) -> {ok, PState}; +puback(?QOS_1, PacketId, [], PState) -> + deliver({puback, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); +puback(?QOS_1, PacketId, [_|_], PState) -> %%TODO: check the dispatch? + deliver({puback, PacketId, ?RC_SUCCESS}, PState); puback(?QOS_1, PacketId, {error, ReasonCode}, PState) -> deliver({puback, PacketId, ReasonCode}, PState); -puback(?QOS_1, PacketId, {ok, []}, PState) -> - deliver({puback, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); -puback(?QOS_1, PacketId, {ok, _}, PState) -> - deliver({puback, PacketId, ?RC_SUCCESS}, PState); -puback(?QOS_2, PacketId, {error, ReasonCode}, PState) -> - deliver({pubrec, PacketId, ReasonCode}, PState); -puback(?QOS_2, PacketId, {ok, []}, PState) -> +puback(?QOS_2, PacketId, [], PState) -> deliver({pubrec, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); -puback(?QOS_2, PacketId, {ok, _}, PState) -> - deliver({pubrec, PacketId, ?RC_SUCCESS}, PState). +puback(?QOS_2, PacketId, [_|_], PState) -> %%TODO: check the dispatch? + deliver({pubrec, PacketId, ?RC_SUCCESS}, PState); +puback(?QOS_2, PacketId, {error, ReasonCode}, PState) -> + deliver({pubrec, PacketId, ReasonCode}, PState). %%------------------------------------------------------------------------------ %% Deliver Packet -> Client diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 0951c82d6..0463526d6 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -148,7 +148,7 @@ call(Router, Msg) -> gen_server:call(Router, Msg, infinity). pick(Topic) -> - gproc_pool:pick_worker(emqx_router_pool, Topic). + gproc_pool:pick_worker(router_pool, Topic). %%------------------------------------------------------------------------------ %% gen_server callbacks diff --git a/src/emqx_router_sup.erl b/src/emqx_router_sup.erl index c0317fc78..945d7910f 100644 --- a/src/emqx_router_sup.erl +++ b/src/emqx_router_sup.erl @@ -17,6 +17,7 @@ -behaviour(supervisor). -export([start_link/0]). + -export([init/1]). start_link() -> @@ -32,8 +33,7 @@ init([]) -> modules => [emqx_router_helper]}, %% Router pool - RouterPool = emqx_pool_sup:spec(router_pool, - [emqx_router_pool, hash, emqx_vm:schedulers(), + RouterPool = emqx_pool_sup:spec([router_pool, hash, {emqx_router, start_link, []}]), {ok, {{one_for_all, 0, 1}, [Helper, RouterPool]}}. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index f0190dc14..f41a25fc5 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -259,7 +259,7 @@ subscribe(SPid, PacketId, Properties, TopicFilters) -> %% @doc Called by connection processes when publishing messages -spec(publish(spid(), emqx_mqtt_types:packet_id(), emqx_types:message()) - -> {ok, emqx_types:deliver_results()}). + -> emqx_types:deliver_results() | {error, term()}). publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_0}) -> %% Publish QoS0 message directly emqx_broker:publish(Msg); @@ -370,7 +370,8 @@ init([Parent, #{zone := Zone, topic_alias_maximum = TopicAliasMaximum, will_msg = WillMsg }, - ok = emqx_sm:register_session(ClientId, attrs(State)), + 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)), emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index fd6a44231..281893fb0 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -21,12 +21,15 @@ -export([start_link/0]). -export([open_session/1, close_session/1]). --export([lookup_session/1, lookup_session_pid/1]). -export([resume_session/2]). -export([discard_session/1, discard_session/2]). --export([register_session/2, unregister_session/1]). --export([get_session_attrs/1, set_session_attrs/2]). --export([get_session_stats/1, set_session_stats/2]). +-export([register_session/1, register_session/2]). +-export([unregister_session/1, unregister_session/2]). +-export([get_session_attrs/1, get_session_attrs/2, + set_session_attrs/2, set_session_attrs/3]). +-export([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]). @@ -46,6 +49,8 @@ -define(SESSION_ATTRS_TAB, emqx_session_attrs). -define(SESSION_STATS_TAB, emqx_session_stats). +-define(BATCH_SIZE, 10000). + -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> gen_server:start_link({local, ?SM}, ?MODULE, [], []). @@ -62,8 +67,8 @@ open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid open_session(SessAttrs = #{clean_start := false, client_id := ClientId}) -> ResumeStart = fun(_) -> case resume_session(ClientId, SessAttrs) of - {ok, SPid} -> - {ok, SPid, true}; + {ok, SessPid} -> + {ok, SessPid, true}; {error, not_found} -> emqx_session:start_link(SessAttrs) end @@ -75,76 +80,68 @@ open_session(SessAttrs = #{clean_start := false, client_id := ClientId}) -> 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({_ClientId, SPid}) -> - case catch emqx_session:discard(SPid, ConnPid) of - {Err, Reason} when Err =:= 'EXIT'; Err =:= error -> - emqx_logger:error("[SM] Failed to discard ~p: ~p", [SPid, Reason]); - ok -> ok - end - end, lookup_session(ClientId)). + fun(SessPid) -> + try emqx_session:discard(SessPid, ConnPid) + catch + _:Error:_Stk -> + emqx_logger:error("[SM] 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(ClientId) of + case lookup_session_pids(ClientId) of [] -> {error, not_found}; - [{_ClientId, SPid}] -> - ok = emqx_session:resume(SPid, SessAttrs), - {ok, SPid}; - Sessions -> - [{_, SPid}|StaleSessions] = lists:reverse(Sessions), - emqx_logger:error("[SM] More than one session found: ~p", [Sessions]), - lists:foreach(fun({_, StalePid}) -> + [SessPid] -> + ok = emqx_session:resume(SessPid, SessAttrs), + {ok, SessPid}; + SessPids -> + [SessPid|StalePids] = lists:reverse(SessPids), + emqx_logger:error("[SM] More than one session found: ~p", [SessPids]), + lists:foreach(fun(StalePid) -> catch emqx_session:discard(StalePid, ConnPid) - end, StaleSessions), - ok = emqx_session:resume(SPid, SessAttrs), - {ok, SPid} + end, StalePids), + ok = emqx_session:resume(SessPid, SessAttrs), + {ok, SessPid} end. %% @doc Close a session. --spec(close_session({emqx_types:client_id(), pid()} | pid()) -> ok). -close_session({_ClientId, SPid}) -> - emqx_session:close(SPid); -close_session(SPid) when is_pid(SPid) -> - emqx_session:close(SPid). +-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; -%% @doc Register a session with attributes. --spec(register_session(emqx_types:client_id() | {emqx_types:client_id(), pid()}, - list(emqx_session:attr())) -> ok). -register_session(ClientId, SessAttrs) when is_binary(ClientId) -> - register_session({ClientId, self()}, SessAttrs); +close_session(SessPid) when is_pid(SessPid) -> + emqx_session:close(SessPid). -register_session(Session = {ClientId, SPid}, SessAttrs) when is_binary(ClientId), is_pid(SPid) -> +%% @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), - true = ets:insert(?SESSION_ATTRS_TAB, {Session, SessAttrs}), - true = proplists:get_value(clean_start, SessAttrs, true) - orelse ets:insert(?SESSION_P_TAB, Session), ok = emqx_sm_registry:register_session(Session), - notify({registered, ClientId, SPid}). - -%% @doc Get session attrs --spec(get_session_attrs({emqx_types:client_id(), pid()}) -> list(emqx_session:attr())). -get_session_attrs(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> - emqx_tables:lookup_value(?SESSION_ATTRS_TAB, Session, []). - -%% @doc Set session attrs --spec(set_session_attrs(emqx_types:client_id() | {emqx_types:client_id(), pid()}, - list(emqx_session:attr())) -> true). -set_session_attrs(ClientId, SessAttrs) when is_binary(ClientId) -> - set_session_attrs({ClientId, self()}, SessAttrs); -set_session_attrs(Session = {ClientId, SPid}, SessAttrs) when is_binary(ClientId), is_pid(SPid) -> - ets:insert(?SESSION_ATTRS_TAB, {Session, SessAttrs}). + notify({registered, ClientId, SessPid}). %% @doc Unregister a session --spec(unregister_session(emqx_types:client_id() | {emqx_types:client_id(), pid()}) -> ok). +-spec(unregister_session(emqx_types:client_id()) -> ok). unregister_session(ClientId) when is_binary(ClientId) -> - unregister_session({ClientId, self()}); + unregister_session(ClientId, self()). -unregister_session(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> - ok = do_unregister_session(Session), - notify({unregistered, ClientId, SPid}). +-spec(unregister_session(emqx_types:client_id(), pid()) -> ok). +unregister_session(ClientId, SessPid) when is_binary(ClientId), is_pid(SessPid) -> + ok = do_unregister_session({ClientId, SessPid}), + notify({unregistered, SessPid}). %% @private do_unregister_session(Session) -> @@ -154,42 +151,69 @@ do_unregister_session(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(), pid()}) -> list(emqx_stats:stats())). -get_session_stats(Session = {ClientId, SPid}) when is_binary(ClientId), is_pid(SPid) -> - emqx_tables:lookup_value(?SESSION_STATS_TAB, Session, []). +-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_types:client_id(), pid()}, - emqx_stats:stats()) -> true). +-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); -set_session_stats(Session = {ClientId, SPid}, Stats) when is_binary(ClientId), is_pid(SPid) -> - ets:insert(?SESSION_STATS_TAB, {Session, Stats}). + set_session_stats(ClientId, self(), Stats). -%% @doc Lookup a session from registry --spec(lookup_session(emqx_types:client_id()) -> list({emqx_types:client_id(), pid()})). -lookup_session(ClientId) -> +-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 -> ets:lookup(?SESSION_TAB, ClientId) + false -> ets:lookup(?SESSION_TAB, ClientId, []) 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_pid(ClientId) of - Pid when is_pid(Pid) -> - Pid ! {dispatch, Topic, Msg}; - undefined -> + 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. -%% @doc Lookup session pid. --spec(lookup_session_pid(emqx_types:client_id()) -> pid() | undefined). -lookup_session_pid(ClientId) -> - emqx_tables:lookup_value(?SESSION_TAB, ClientId). - notify(Event) -> gen_server:cast(?SM, {notify, Event}). @@ -203,43 +227,36 @@ init([]) -> 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(sm_stats, fun ?MODULE:stats_fun/0), + ok = emqx_stats:update_interval(sess_stats, fun ?MODULE:stats_fun/0), {ok, #{sess_pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> emqx_logger:error("[SM] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({notify, {registered, ClientId, SPid}}, State = #{sess_pmon := PMon}) -> - {noreply, State#{sess_pmon := emqx_pmon:monitor(SPid, ClientId, PMon)}}; +handle_cast({notify, {registered, ClientId, SessPid}}, State = #{sess_pmon := PMon}) -> + {noreply, State#{sess_pmon := emqx_pmon:monitor(SessPid, ClientId, PMon)}}; -handle_cast({notify, {unregistered, _ClientId, SPid}}, State = #{sess_pmon := PMon}) -> - {noreply, State#{sess_pmon := emqx_pmon:demonitor(SPid, PMon)}}; +handle_cast({notify, {unregistered, SessPid}}, State = #{sess_pmon := PMon}) -> + {noreply, State#{sess_pmon := emqx_pmon:demonitor(SessPid, PMon)}}; handle_cast(Msg, State) -> emqx_logger:error("[SM] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #{sess_pmon := PMon}) -> - case emqx_pmon:find(DownPid, PMon) of - undefined -> - {noreply, State}; - ClientId -> - Session = {ClientId, DownPid}, - case ets:member(?SESSION_ATTRS_TAB, Session) of - true -> - ok = emqx_pool:async_submit(fun do_unregister_session/1, [Session]); - false -> ok - end, - {noreply, State#{sess_pmon := emqx_pmon:erase(DownPid, PMon)}} - end; +handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{sess_pmon := PMon}) -> + SessPids = [Pid | emqx_misc:drain_down(?BATCH_SIZE)], + {Items, PMon1} = emqx_pmon:erase_all(SessPids, PMon), + ok = emqx_pool:async_submit( + fun lists:foreach/2, [fun clean_down/1, Items]), + {noreply, State#{sess_pmon := PMon1}}; handle_info(Info, State) -> emqx_logger:error("[SM] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> - emqx_stats:cancel_update(sm_stats). + emqx_stats:cancel_update(sess_stats). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -248,6 +265,15 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ +clean_down({SessPid, ClientId}) -> + Session = {ClientId, SessPid}, + case ets:member(?SESSION_TAB, ClientId) + orelse ets:member(?SESSION_ATTRS_TAB, Session) of + true -> + do_unregister_session(Session); + 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'). diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index 1d0df61bf..a7f44d771 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -43,10 +43,9 @@ start_link() -> is_enabled() -> emqx_config:get_env(enable_session_registry, true). --spec(lookup_session(emqx_types:client_id()) - -> list({emqx_types:client_id(), session_pid()})). +-spec(lookup_session(emqx_types:client_id()) -> list(session_pid())). lookup_session(ClientId) -> - [{ClientId, SessPid} || #global_session{pid = SessPid} <- mnesia:dirty_read(?TAB, ClientId)]. + [SessPid || #global_session{pid = SessPid} <- mnesia:dirty_read(?TAB, ClientId)]. -spec(register_session({emqx_types:client_id(), session_pid()}) -> ok). register_session({ClientId, SessPid}) when is_binary(ClientId), is_pid(SessPid) -> diff --git a/test/emqx_banned_SUITE.erl b/test/emqx_banned_SUITE.erl index 9d4c85134..7e7434e61 100644 --- a/test/emqx_banned_SUITE.erl +++ b/test/emqx_banned_SUITE.erl @@ -18,9 +18,7 @@ -compile(nowarn_export_all). -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). - -include_lib("eunit/include/eunit.hrl"). all() -> [t_banned_all]. @@ -29,18 +27,27 @@ t_banned_all(_) -> emqx_ct_broker_helpers:run_setup_steps(), emqx_banned:start_link(), TimeNow = erlang:system_time(second), - Banned = #banned{who = {client_id, <<"TestClient">>}, + Banned = #banned{who = {client_id, <<"TestClient">>}, reason = <<"test">>, - by = <<"banned suite">>, - desc = <<"test">>, - until = TimeNow + 1}, + by = <<"banned suite">>, + desc = <<"test">>, + until = TimeNow + 1}, 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}})), + ?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}})), + ?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:del({client_id, <<"TestClient">>}), - ?assertNot(emqx_banned:check(#{client_id => <<"TestClient">>, username => undefined, peername => {undefined, undefined}})), + ?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_broker_helpers:run_teardown_steps(). + diff --git a/test/emqx_cm_SUITE.erl b/test/emqx_cm_SUITE.erl index 5e29e075e..b720849f6 100644 --- a/test/emqx_cm_SUITE.erl +++ b/test/emqx_cm_SUITE.erl @@ -24,14 +24,16 @@ all() -> [t_register_unregister_connection]. t_register_unregister_connection(_) -> {ok, _} = emqx_cm_sup:start_link(), Pid = self(), - emqx_cm:register_connection(<<"conn1">>), - emqx_cm:register_connection({<<"conn2">>, Pid}, [{port, 8080}, {ip, "192.168.0.1"}]), + ok = emqx_cm:register_connection(<<"conn1">>), + ok emqx_cm:register_connection(<<"conn2">>, Pid), + true = emqx_cm:set_conn_attrs(<<"conn1">>, [{port, 8080}, {ip, "192.168.0.1"}]), + true = emqx_cm:set_conn_attrs(<<"conn2">>, Pid, [{port, 8080}, {ip, "192.168.0.1"}]), timer:sleep(2000), - [{<<"conn1">>, Pid}] = emqx_cm:lookup_connection(<<"conn1">>), - [{<<"conn2">>, Pid}] = emqx_cm:lookup_connection(<<"conn2">>), - Pid = emqx_cm:lookup_conn_pid(<<"conn1">>), - emqx_cm:unregister_connection(<<"conn1">>), - [] = emqx_cm:lookup_connection(<<"conn1">>), - [{port, 8080}, {ip, "192.168.0.1"}] = emqx_cm:get_conn_attrs({<<"conn2">>, Pid}), - emqx_cm:set_conn_stats(<<"conn2">>, [[{count, 1}, {max, 2}]]), - [[{count, 1}, {max, 2}]] = emqx_cm:get_conn_stats({<<"conn2">>, Pid}). + ?assertEqual(Pid, emqx_cm:lookup_conn_pid(<<"conn1">>)), + ?assertEqual(Pid, emqx_cm:lookup_conn_pid(<<"conn2">>)), + ok = emqx_cm:unregister_connection(<<"conn1">>), + ?assertEqual(undefined, emqx_cm:lookup_conn_pid(<<"conn1">>)), + ?assertEqual([{port, 8080}, {ip, "192.168.0.1"}], emqx_cm:get_conn_attrs({<<"conn2">>, Pid})), + true = emqx_cm:set_conn_stats(<<"conn2">>, [{count, 1}, {max, 2}]), + ?assertEqual([{count, 1}, {max, 2}], emqx_cm:get_conn_stats({<<"conn2">>, Pid})). + diff --git a/test/emqx_pmon_SUITE.erl b/test/emqx_pmon_SUITE.erl new file mode 100644 index 000000000..67e8cf4d7 --- /dev/null +++ b/test/emqx_pmon_SUITE.erl @@ -0,0 +1,48 @@ +%% Copyright (c) 2018 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_pmon_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> + [t_monitor, t_find, t_erase]. + +t_monitor(_) -> + PMon = emqx_pmon:new(), + PMon1 = emqx_pmon:monitor(self(), PMon), + ?assertEqual(1, emqx_pmon:count(PMon1)), + PMon2 = emqx_pmon:demonitor(self(), PMon1), + ?assertEqual(0, emqx_pmon:count(PMon2)). + +t_find(_) -> + PMon = emqx_pmon:new(), + PMon1 = emqx_pmon:monitor(self(), val, PMon), + ?assertEqual(1, emqx_pmon:count(PMon1)), + ?assertEqual({ok, val}, emqx_pmon:find(self(), PMon1)), + PMon2 = emqx_pmon:erase(self(), PMon1), + ?assertEqual(error, emqx_pmon:find(self(), PMon2)). + +t_erase(_) -> + PMon = emqx_pmon:new(), + PMon1 = emqx_pmon:monitor(self(), val, PMon), + PMon2 = emqx_pmon:erase(self(), PMon1), + ?assertEqual(0, emqx_pmon:count(PMon2)), + {Items, PMon3} = emqx_pmon:erase_all([self()], PMon1), + ?assertEqual([{self(), val}], Items), + ?assertEqual(0, emqx_pmon:count(PMon3)). + diff --git a/test/emqx_sequence_SUITE.erl b/test/emqx_sequence_SUITE.erl index 1ac0ea308..ab408b8e0 100644 --- a/test/emqx_sequence_SUITE.erl +++ b/test/emqx_sequence_SUITE.erl @@ -34,5 +34,5 @@ sequence_generate(_) -> ?assertEqual(0, reclaim(seqtab, key)), ?assertEqual(false, ets:member(seqtab, key)), ?assertEqual(1, nextval(seqtab, key)), - ?assert(emqx_sequence:delete(seqtab). + ?assert(emqx_sequence:delete(seqtab)). diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl index 3aed3090e..008d4b6e6 100644 --- a/test/emqx_sm_SUITE.erl +++ b/test/emqx_sm_SUITE.erl @@ -34,16 +34,14 @@ t_open_close_session(_) -> topic_alias_maximum => 0, will_msg => undefined}, {ok, SPid} = emqx_sm:open_session(Attrs), - [{<<"client">>, SPid}] = emqx_sm:lookup_session(<<"client">>), - SPid = emqx_sm:lookup_session_pid(<<"client">>), + ?assertEqual([SPid], emqx_sm:lookup_session_pids(<<"client">>)), {ok, NewConnPid} = emqx_mock_client:start_link(<<"client">>), {ok, SPid, true} = emqx_sm:open_session(Attrs#{clean_start => false, conn_pid => NewConnPid}), - [{<<"client">>, SPid}] = emqx_sm:lookup_session(<<"client">>), - SAttrs = emqx_sm:get_session_attrs({<<"client">>, SPid}), - <<"client">> = proplists:get_value(client_id, SAttrs), - Session = {<<"client">>, SPid}, - emqx_sm:set_session_stats(Session, {open, true}), - {open, true} = emqx_sm:get_session_stats(Session), + ?assertEqual([SPid], emqx_sm:lookup_session_pids(<<"client">>)), + SAttrs = emqx_sm:get_session_attrs(<<"client">>, SPid), + ?assertEqual(<<"client">>, proplists:get_value(client_id, SAttrs)), + emqx_sm:set_session_stats(<<"client">>, SPid, [{inflight, 10}]), + ?assertEqual([{inflight, 10}], emqx_sm:get_session_stats(<<"client">>, SPid)), ok = emqx_sm:close_session(SPid), - [] = emqx_sm:lookup_session(<<"client">>), + ?assertEqual([], emqx_sm:lookup_session_pids(<<"client">>)), emqx_ct_broker_helpers:run_teardown_steps(). From 675edf3fab850c99c87123a1b8a970d8f06aea30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Tue, 18 Dec 2018 12:01:41 +0800 Subject: [PATCH 489/520] Fix a bug that will not send a will message in some cases --- src/emqx_session.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 12718b9fc..cb1d04780 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -963,7 +963,8 @@ ensure_expire_timer(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) -> +ensure_will_delay_timer(State = #state{will_msg = WillMsg}) -> + send_willmsg(WillMsg), State. ensure_stats_timer(State = #state{enable_stats = true, stats_timer = undefined, From b7a39f25f29b7476540cc8b88f7940dd18ca0772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=AD=90=E5=8D=9A?= <349832309@qq.com> Date: Tue, 18 Dec 2018 12:02:18 +0800 Subject: [PATCH 490/520] Revert "Fix a bug that will not send a will message in some cases" This reverts commit 675edf3fab850c99c87123a1b8a970d8f06aea30. --- src/emqx_session.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index cb1d04780..12718b9fc 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -963,8 +963,7 @@ ensure_expire_timer(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), +ensure_will_delay_timer(State) -> State. ensure_stats_timer(State = #state{enable_stats = true, stats_timer = undefined, From 95ad67b47c25e97dd87704a37eba62e418be312c Mon Sep 17 00:00:00 2001 From: tigercl Date: Tue, 18 Dec 2018 14:55:37 +0800 Subject: [PATCH 491/520] Fix a bug that will not send a will message in some cases (#2068) * Fix a bug that will not send a will message in some cases --- src/emqx_session.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 12718b9fc..30b35bf11 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -963,8 +963,9 @@ ensure_expire_timer(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. +ensure_will_delay_timer(State = #state{will_msg = WillMsg}) -> + send_willmsg(WillMsg), + State#state{will_msg = undefined}. ensure_stats_timer(State = #state{enable_stats = true, stats_timer = undefined, idle_timeout = IdleTimeout}) -> From dc06c0beab77f30a6063dc966d6d1ee924913db1 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 18 Dec 2018 15:11:04 +0800 Subject: [PATCH 492/520] Remove 'topic_alias_maximum' from session's state --- src/emqx_protocol.erl | 53 +++++++++--------------- src/emqx_session.erl | 95 +++++++++++++++++++------------------------ 2 files changed, 62 insertions(+), 86 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 573b913f7..5751f78fe 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -619,14 +619,12 @@ maybe_use_username_as_clientid(ClientId, undefined, _PState) -> ClientId; maybe_use_username_as_clientid(ClientId, Username, #pstate{zone = Zone}) -> case emqx_zone:get_env(Zone, use_username_as_clientid, false) of - true -> - Username; - false -> - ClientId + true -> Username; + false -> ClientId end. %%------------------------------------------------------------------------------ -%% Assign a clientid +%% Assign a clientId maybe_assign_client_id(PState = #pstate{client_id = <<>>, ack_props = AckProps}) -> ClientId = emqx_guid:to_base62(emqx_guid:gen()), @@ -650,41 +648,30 @@ try_open_session(PState = #pstate{zone = Zone, clean_start => CleanStart, will_msg => WillMsg }, - SessAttrs1 = lists:foldl(fun set_session_attrs/2, SessAttrs, [{max_inflight, PState}, {expiry_interval, PState}, {topic_alias_maximum, PState}]), + SessAttrs1 = lists:foldl(fun set_session_attrs/2, SessAttrs, [{max_inflight, PState}, {expiry_interval, PState}]), case emqx_sm:open_session(SessAttrs1) of {ok, SPid} -> {ok, SPid, false}; Other -> Other end. -set_session_attrs({max_inflight, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps}}, SessAttrs) -> - maps:put(max_inflight, if - ProtoVer =:= ?MQTT_PROTO_V5 -> - get_property('Receive-Maximum', ConnProps, 65535); - true -> - emqx_zone:get_env(Zone, max_inflight, 65535) - end, SessAttrs); -set_session_attrs({expiry_interval, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps, clean_start = CleanStart}}, SessAttrs) -> - maps:put(expiry_interval, if - ProtoVer =:= ?MQTT_PROTO_V5 -> - get_property('Session-Expiry-Interval', ConnProps, 0); - true -> - case CleanStart of - true -> 0; - false -> - emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) - end - end, SessAttrs); -set_session_attrs({topic_alias_maximum, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps}}, SessAttrs) -> - maps:put(topic_alias_maximum, if - ProtoVer =:= ?MQTT_PROTO_V5 -> - get_property('Topic-Alias-Maximum', ConnProps, 0); - true -> - emqx_zone:get_env(Zone, max_topic_alias, 0) - end, SessAttrs); -set_session_attrs({_, #pstate{}}, SessAttrs) -> - SessAttrs. +set_session_attrs({max_inflight, #pstate{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}}, SessAttrs) -> + maps:put(max_inflight, get_property('Receive-Maximum', ConnProps, 65535), SessAttrs); +set_session_attrs({max_inflight, #pstate{zone = Zone}}, SessAttrs) -> + maps:put(max_inflight, emqx_zone:get_env(Zone, max_inflight, 65535), SessAttrs); + +set_session_attrs({expiry_interval, #pstate{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}}, SessAttrs) -> + maps:put(expiry_interval, get_property('Session-Expiry-Interval', ConnProps, 0), SessAttrs); + +set_session_attrs({expiry_interval, #pstate{zone = Zone, clean_start = CleanStart}}, SessAttrs) -> + maps:put(expiry_interval, case CleanStart of + true -> 0; + false -> emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) + end, SessAttrs); + +set_session_attrs(_, SessAttrs) -> + SessAttrs. authenticate(Credentials, Password) -> case emqx_access_control:authenticate(Credentials, Password) of diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 12718b9fc..fde76f0b6 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -147,8 +147,6 @@ %% Created at created_at :: erlang:timestamp(), - topic_alias_maximum :: pos_integer(), - will_msg :: emqx:message(), will_delay_timer :: reference() | undefined @@ -341,7 +339,6 @@ init([Parent, #{zone := Zone, clean_start := CleanStart, expiry_interval := ExpiryInterval, max_inflight := MaxInflight, - topic_alias_maximum := TopicAliasMaximum, will_msg := WillMsg}]) -> emqx_logger:set_metadata_client_id(ClientId), process_flag(trap_exit, true), @@ -367,7 +364,6 @@ init([Parent, #{zone := Zone, deliver_stats = 0, enqueue_stats = 0, created_at = os:timestamp(), - topic_alias_maximum = TopicAliasMaximum, will_msg = WillMsg }, emqx_sm:register_session(ClientId, attrs(State)), @@ -518,22 +514,23 @@ handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight end; %% RESUME: -handle_cast({resume, #{conn_pid := ConnPid, - will_msg := WillMsg, - expiry_interval := SessionExpiryInterval, - max_inflight := MaxInflight, - topic_alias_maximum := TopicAliasMaximum}}, 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}) -> +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], State), %% Cancel Timers - lists:foreach(fun emqx_misc:cancel_timer/1, [RetryTimer, AwaitTimer, ExpireTimer, WillDelayTimer]), + 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], State); @@ -542,19 +539,18 @@ handle_cast({resume, #{conn_pid := ConnPid, true = link(ConnPid), - State1 = State#state{conn_pid = ConnPid, - binding = binding(ConnPid), - old_conn_pid = OldConnPid, - clean_start = false, - retry_timer = undefined, - awaiting_rel = #{}, - await_rel_timer = undefined, - expiry_timer = undefined, - expiry_interval = SessionExpiryInterval, - inflight = emqx_inflight:update_size(MaxInflight, State#state.inflight), - topic_alias_maximum = TopicAliasMaximum, - will_delay_timer = undefined, - will_msg = WillMsg}, + State1 = State#state{conn_pid = ConnPid, + binding = binding(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}, %% Clean Session: true -> false??? CleanStart andalso emqx_sm:set_session_attrs(ClientId, attrs(State1)), @@ -573,9 +569,10 @@ handle_cast(Msg, State) -> %% Batch dispatch handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> - {noreply, lists:foldl(fun(Msg, NewState) -> - element(2, handle_info({dispatch, Topic, Msg}, NewState)) - end, State, Msgs)}; + noreply(lists:foldl( + fun(Msg, St) -> + element(2, handle_info({dispatch, Topic, Msg}, St)) + end, State, Msgs)); %% Dispatch message handle_info({dispatch, Topic, Msg = #message{}}, State) -> @@ -584,12 +581,11 @@ handle_info({dispatch, Topic, Msg = #message{}}, State) -> %% Require ack, but we do not have connection %% negative ack the message so it can try the next subscriber in the group ok = emqx_shared_sub:nack_no_connection(Msg), - noreply(State); + {noreply, State}; false -> - handle_dispatch(Topic, Msg, State) + noreply(handle_dispatch(Topic, Msg, State)) end; - %% 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}); @@ -663,25 +659,17 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -has_connection(#state{conn_pid = Pid}) -> is_pid(Pid) andalso is_process_alive(Pid). +has_connection(#state{conn_pid = Pid}) -> + is_pid(Pid) andalso is_process_alive(Pid). -handle_dispatch(Topic, Msg = #message{headers = Headers}, - State = #state{subscriptions = SubMap, - topic_alias_maximum = TopicAliasMaximum - }) -> - TopicAlias = maps:get('Topic-Alias', Headers, undefined), - if - TopicAlias =:= undefined orelse TopicAlias =< TopicAliasMaximum -> - noreply(case maps:find(Topic, SubMap) of - {ok, #{nl := Nl, qos := QoS, rap := Rap, subid := SubId}} -> - run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}, {subid, SubId}], Msg, State); - {ok, #{nl := Nl, qos := QoS, rap := Rap}} -> - run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}], Msg, State); - error -> - dispatch(emqx_message:unset_flag(dup, Msg), State) - end); - true -> - noreply(State) +handle_dispatch(Topic, Msg, State = #state{subscriptions = SubMap}) -> + case maps:find(Topic, SubMap) of + {ok, #{nl := Nl, qos := QoS, rap := Rap, subid := SubId}} -> + run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}, {subid, SubId}], Msg, State); + {ok, #{nl := Nl, qos := QoS, rap := Rap}} -> + run_dispatch_steps([{nl, Nl}, {qos, QoS}, {rap, Rap}], Msg, State); + error -> + dispatch(emqx_message:unset_flag(dup, Msg), State) end. suback(_From, undefined, _ReasonCodes) -> @@ -1011,3 +999,4 @@ noreply(State) -> shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. + From 1e2c5db36cc666df6da158e879a2cb215002626a Mon Sep 17 00:00:00 2001 From: turtled Date: Tue, 18 Dec 2018 10:21:00 +0800 Subject: [PATCH 493/520] Modify batch size --- src/emqx_broker_helper.erl | 2 +- src/emqx_cm.erl | 2 +- src/emqx_sm.erl | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index c8e2b5eab..1ea3f3668 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -31,7 +31,7 @@ -define(SUBSEQ, emqx_subseq). -define(SHARD, 1024). --define(BATCH_SIZE, 10000). +-define(BATCH_SIZE, 100000). -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 62cb417de..b8a57bc50 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -42,7 +42,7 @@ -define(CONN_ATTRS_TAB, emqx_conn_attrs). -define(CONN_STATS_TAB, emqx_conn_stats). --define(BATCH_SIZE, 10000). +-define(BATCH_SIZE, 100000). %% @doc Start the connection manager. -spec(start_link() -> emqx_types:startlink_ret()). diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index 281893fb0..97a1b689c 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -49,7 +49,7 @@ -define(SESSION_ATTRS_TAB, emqx_session_attrs). -define(SESSION_STATS_TAB, emqx_session_stats). --define(BATCH_SIZE, 10000). +-define(BATCH_SIZE, 100000). -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> @@ -201,7 +201,7 @@ set_session_stats(ClientId, SessPid, Stats) when is_binary(ClientId), is_pid(Ses lookup_session_pids(ClientId) -> case emqx_sm_registry:is_enabled() of true -> emqx_sm_registry:lookup_session(ClientId); - false -> ets:lookup(?SESSION_TAB, ClientId, []) + false -> emqx_tables:lookup_value(?SESSION_TAB, ClientId, []) end. %% @doc Dispatch a message to the session. From c7fa4b1b15acec3d805f94ad8612260a9d1d7234 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 18 Dec 2018 16:50:47 +0800 Subject: [PATCH 494/520] Revert vm args in emqx conf b (#2070) * Revert changes in emqx.conf for backward compatibility --- etc/emqx.conf | 93 ++++++++++++++++++++++++++++++++++++++ etc/vm.args | 12 ++--- priv/emqx.schema | 114 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 6 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 7d4d8c966..4144593d8 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -160,6 +160,99 @@ node.name = emqx@127.0.0.1 ## Value: String node.cookie = emqxsecretcookie +## Heartbeat monitoring of an Erlang runtime system. Comment the line to disable +## heartbeat, or set the value as 'on' +## +## Value: on +## +## vm.args: -heart +## node.heartbeat = on + +## Sets the number of threads in async thread pool. Valid range is 0-1024. +## +## See: http://erlang.org/doc/man/erl.html +## +## Value: 0-1024 +## +## vm.args: +A Number +node.async_threads = 32 + +## Sets the maximum number of simultaneously existing processes for this +## system if a Number is passed as value. +## +## See: http://erlang.org/doc/man/erl.html +## +## Value: Number [1024-134217727] +## +## vm.args: +P Number +node.process_limit = 2048000 + +## Sets the maximum number of simultaneously existing ports for this system. +## +## See: http://erlang.org/doc/man/erl.html +## +## Value: Number [1024-134217727] +## +## vm.args: +Q Number +node.max_ports = 1024000 + +## Set the distribution buffer busy limit (dist_buf_busy_limit). +## +## See: http://erlang.org/doc/man/erl.html +## +## Value: Number [1KB-2GB] +## +## vm.args: +zdbbl size +node.dist_buffer_size = 8MB + +## Sets the maximum number of ETS tables. Note that mnesia and SSL will +## create temporary ETS tables. +## +## Value: Number +## +## vm.args: +e Number +node.max_ets_tables = 256000 + +## Tweak GC to run more often. +## +## Value: Number [0-65535] +## +## vm.args: -env ERL_FULLSWEEP_AFTER Number +node.fullsweep_after = 1000 + +## Crash dump log file. +## +## 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. +## +## Value: File +## +## vm.args: -ssl_dist_optfile +## node.ssl_dist_optfile = {{ platform_etc_dir }}/ssl_dist.conf + +## Sets the net_kernel tick time. TickTime is specified in seconds. +## Notice that all communicating nodes are to have the same TickTime +## value specified. +## +## See: http://www.erlang.org/doc/man/kernel_app.html#net_ticktime +## +## Value: Number +## +## vm.args: -kernel net_ticktime Number +node.dist_net_ticktime = 60 + ## Sets the port range for the listener socket of a distributed Erlang node. ## Note that if there are firewalls between clustered nodes, this port segment ## for nodes’ communication should be allowed. diff --git a/etc/vm.args b/etc/vm.args index d0992f470..43e6467d9 100644 --- a/etc/vm.args +++ b/etc/vm.args @@ -10,19 +10,19 @@ ## such as `node.name` for `-name` and `node.cooke` for `-setcookie`. ## Sets the maximum number of simultaneously existing processes for this system. -+P 256000 +#+P 2048000 ## Sets the maximum number of simultaneously existing ports for this system. -+Q 262144 +#+Q 1024000 ## Sets the maximum number of ETS tables -+e 256000 +#+e 256000 ## Sets the maximum number of atoms the virtual machine can handle. #+t 1048576 ## Set the location of crash dumps --env ERL_CRASH_DUMP {{ platform_log_dir }}/crash.dump +#-env ERL_CRASH_DUMP {{ platform_log_dir }}/crash.dump ## Set how many times generational garbages collections can be done without ## forcing a fullsweep collection. @@ -46,13 +46,13 @@ #-kernel net_ticktime 60 ## Sets the distribution buffer busy limit (dist_buf_busy_limit). -+zdbbl 8192 +#+zdbbl 8192 ## Sets default scheduler hint for port parallelism. +spp true ## Sets the number of threads in async thread pool. Valid range is 0-1024. -+A 8 +#+A 8 ## Sets the default heap size of processes to the size Size. #+hms 233 diff --git a/priv/emqx.schema b/priv/emqx.schema index 7c37383c4..7ecc66a35 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -173,11 +173,125 @@ 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", [ + {datatype, string}, + hidden +]}. + %% @doc Secret cookie for distributed erlang node {mapping, "node.cookie", "vm_args.-setcookie", [ {default, "emqxsecretcookie"} ]}. +%% @doc http://erlang.org/doc/man/heart.html +{mapping, "node.heartbeat", "vm_args.-heart", [ + {datatype, flag}, + hidden +]}. + +{translation, "vm_args.-heart", fun(Conf) -> + case cuttlefish:conf_get("node.heartbeat", Conf) of + true -> ""; + false -> cuttlefish:invalid("should be 'on' or comment the line!") + end +end}. + +%% @doc More information at: http://erlang.org/doc/man/erl.html +{mapping, "node.async_threads", "vm_args.+A", [ + {default, 64}, + {datatype, integer}, + {validators, ["range:0-1024"]} +]}. + +%% @doc Erlang Process Limit +{mapping, "node.process_limit", "vm_args.+P", [ + {datatype, integer}, + {default, 256000}, + hidden +]}. + +%% Note: OTP R15 and earlier uses -env ERL_MAX_PORTS, R16+ uses +Q +%% @doc The number of concurrent ports/sockets +%% Valid range is 1024-134217727 +{mapping, "node.max_ports", + cuttlefish:otp("R16", "vm_args.+Q", "vm_args.-env ERL_MAX_PORTS"), [ + {default, 262144}, + {datatype, integer}, + {validators, ["range4ports"]} +]}. + +{validator, "range4ports", "must be 1024 to 134217727", + fun(X) -> X >= 1024 andalso X =< 134217727 end}. + +%% @doc http://www.erlang.org/doc/man/erl.html#%2bzdbbl +{mapping, "node.dist_buffer_size", "vm_args.+zdbbl", [ + {datatype, bytesize}, + {commented, "32MB"}, + hidden, + {validators, ["zdbbl_range"]} +]}. + +{translation, "vm_args.+zdbbl", + fun(Conf) -> + ZDBBL = cuttlefish:conf_get("node.dist_buffer_size", Conf, undefined), + case ZDBBL of + undefined -> undefined; + X when is_integer(X) -> cuttlefish_util:ceiling(X / 1024); %% Bytes to Kilobytes; + _ -> undefined + end + end +}. + +{validator, "zdbbl_range", "must be between 1KB and 2097151KB", + fun(ZDBBL) -> + %% 2097151KB = 2147482624 + ZDBBL >= 1024 andalso ZDBBL =< 2147482624 + end +}. + +%% @doc http://www.erlang.org/doc/man/erlang.html#system_flag-2 +{mapping, "node.fullsweep_after", "vm_args.-env ERL_FULLSWEEP_AFTER", [ + {default, 1000}, + {datatype, integer}, + hidden, + {validators, ["positive_integer"]} +]}. + +{validator, "positive_integer", "must be a positive integer", + fun(X) -> X >= 0 end}. + +%% Note: OTP R15 and earlier uses -env ERL_MAX_ETS_TABLES, +%% R16+ uses +e +%% @doc The ETS table limit +{mapping, "node.max_ets_tables", + cuttlefish:otp("R16", "vm_args.+e", "vm_args.-env ERL_MAX_ETS_TABLES"), [ + {default, 256000}, + {datatype, integer}, + hidden +]}. + +%% @doc Set the location of crash dumps +{mapping, "node.crash_dump", "vm_args.-env ERL_CRASH_DUMP", [ + {default, "{{crash_dump}}"}, + {datatype, file}, + hidden +]}. + +%% @doc http://www.erlang.org/doc/man/kernel_app.html#net_ticktime +{mapping, "node.dist_net_ticktime", "vm_args.-kernel net_ticktime", [ + {commented, 60}, + {datatype, integer}, + hidden +]}. + %% @doc http://www.erlang.org/doc/man/kernel_app.html {mapping, "node.dist_listen_min", "kernel.inet_dist_listen_min", [ {commented, 6369}, From 7d9e350bbe5f38502deb971cfd302b435e89842d Mon Sep 17 00:00:00 2001 From: Gilbert Date: Wed, 19 Dec 2018 10:34:06 +0800 Subject: [PATCH 495/520] Add option to disconnect client in case acl deny (#2059) * Add option to disconnect client in case acl deny --- etc/emqx.conf | 18 +++ priv/emqx.schema | 12 ++ src/emqx_protocol.erl | 107 ++++++++++++------ src/emqx_ws_connection.erl | 9 +- test/emqx_access_SUITE_data/acl.conf | 1 - .../acl_deny_action.conf | 4 + test/emqx_protocol_SUITE.erl | 101 ++++++++++++++--- 7 files changed, 194 insertions(+), 58 deletions(-) create mode 100644 test/emqx_access_SUITE_data/acl_deny_action.conf diff --git a/etc/emqx.conf b/etc/emqx.conf index 6370e7214..430c58ce4 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -423,6 +423,12 @@ acl_cache_max_size = 32 ## Default: 1 minute acl_cache_ttl = 1m +## The action when acl check reject current operation +## +## Value: ignore | disconnect +## Default: ignore +acl_deny_action = ignore + ##-------------------------------------------------------------------- ## MQTT Protocol ##-------------------------------------------------------------------- @@ -512,6 +518,12 @@ zone.external.enable_ban = on ## Value: on | off zone.external.enable_stats = on +## The action when acl check reject current operation +## +## Value: ignore | disconnect +## Default: ignore +zone.external.acl_deny_action = ignore + ## Force MQTT connection/session process GC after this number of ## messages | bytes passed through. ## @@ -670,6 +682,12 @@ zone.internal.enable_stats = on ## Value: Flag zone.internal.enable_acl = off +## The action when acl check reject current operation +## +## Value: ignore | disconnect +## Default: ignore +zone.internal.acl_deny_action = ignore + ## See zone.$name.wildcard_subscription. ## ## Value: boolean diff --git a/priv/emqx.schema b/priv/emqx.schema index 1001ab5a8..d59c8af24 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -548,6 +548,12 @@ end}. {validators, ["range:gt_0"]} ]}. +%% @doc Action when acl check reject current operation +{mapping, "acl_deny_action", "emqx.acl_deny_action", [ + {default, ignore}, + {datatype, {enum, [ignore, disconnect]}} +]}. + {validator, "range:gt_0", "must greater than 0", fun(X) -> X > 0 end }. @@ -640,6 +646,12 @@ end}. {datatype, flag} ]}. +%% @doc Action when acl check reject current operation +{mapping, "zone.$name.acl_deny_action", "emqx.zones", [ + {default, ignore}, + {datatype, {enum, [ignore, disconnect]}} +]}. + %% @doc Enable Ban. {mapping, "zone.$name.enable_ban", "emqx.zones", [ {default, off}, diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 573b913f7..0bedb0927 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -60,6 +60,7 @@ is_bridge, enable_ban, enable_acl, + acl_deny_action, recv_stats, send_stats, connected, @@ -84,28 +85,29 @@ -spec(init(map(), list()) -> state()). init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) -> Zone = proplists:get_value(zone, Options), - #pstate{zone = Zone, - sendfun = SendFun, - 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), - is_super = false, - clean_start = false, - topic_aliases = #{}, - packet_size = emqx_zone:get_env(Zone, max_packet_size), - mountpoint = emqx_zone:get_env(Zone, mountpoint), - is_bridge = false, - enable_ban = emqx_zone:get_env(Zone, enable_ban, false), - enable_acl = emqx_zone:get_env(Zone, enable_acl), - recv_stats = #{msg => 0, pkt => 0}, - send_stats = #{msg => 0, pkt => 0}, - connected = false, - ignore_loop = emqx_config:get_env(mqtt_ignore_loop_deliver, false)}. + #pstate{zone = Zone, + sendfun = SendFun, + 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), + is_super = false, + clean_start = false, + topic_aliases = #{}, + packet_size = emqx_zone:get_env(Zone, max_packet_size), + mountpoint = emqx_zone:get_env(Zone, mountpoint), + is_bridge = false, + enable_ban = emqx_zone:get_env(Zone, enable_ban, false), + enable_acl = emqx_zone:get_env(Zone, enable_acl), + acl_deny_action = emqx_zone:get_env(Zone, acl_deny_action, ignore), + recv_stats = #{msg => 0, pkt => 0}, + send_stats = #{msg => 0, pkt => 0}, + connected = false, + ignore_loop = emqx_config:get_env(mqtt_ignore_loop_deliver, false)}. init_username(Peercert, Options) -> case proplists:get_value(peer_cert_as_username, Options) of @@ -341,13 +343,10 @@ process_packet(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PSt case check_publish(Packet, PState) of {ok, PState1} -> do_publish(Packet, PState1); - {error, ?RC_TOPIC_ALIAS_INVALID} -> - ?LOG(error, "Protocol error - ~p", [?RC_TOPIC_ALIAS_INVALID]), - {error, ?RC_TOPIC_ALIAS_INVALID, PState}; {error, ReasonCode} -> ?LOG(warning, "Cannot publish qos0 message to ~s for ~s", [Topic, emqx_reason_codes:text(ReasonCode)]), - {error, ReasonCode, PState} + do_acl_deny_action(Packet, ReasonCode, PState) end; process_packet(Packet = ?PUBLISH_PACKET(?QOS_1, Topic, PacketId, _Payload), PState) -> @@ -357,7 +356,12 @@ process_packet(Packet = ?PUBLISH_PACKET(?QOS_1, Topic, PacketId, _Payload), PSta {error, ReasonCode} -> ?LOG(warning, "Cannot publish qos1 message to ~s for ~s", [Topic, emqx_reason_codes:text(ReasonCode)]), - deliver({puback, PacketId, ReasonCode}, PState) + case deliver({puback, PacketId, ReasonCode}, PState) of + {ok, _PState} -> + do_acl_deny_action(Packet, ReasonCode, PState); + Error -> + Error + end end; process_packet(Packet = ?PUBLISH_PACKET(?QOS_2, Topic, PacketId, _Payload), PState) -> @@ -367,7 +371,12 @@ process_packet(Packet = ?PUBLISH_PACKET(?QOS_2, Topic, PacketId, _Payload), PSta {error, ReasonCode} -> ?LOG(warning, "Cannot publish qos2 message to ~s for ~s", [Topic, emqx_reason_codes:text(ReasonCode)]), - deliver({pubrec, PacketId, ReasonCode}, PState) + case deliver({pubrec, PacketId, ?RC_NOT_AUTHORIZED}, PState) of + {ok, _PState} -> + do_acl_deny_action(Packet, ReasonCode, PState); + Error -> + Error + end end; process_packet(?PUBACK_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> @@ -392,7 +401,7 @@ process_packet(?PUBREL_PACKET(PacketId, ReasonCode), PState = #pstate{session = process_packet(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> {ok = emqx_session:pubcomp(SPid, PacketId, ReasonCode), PState}; -process_packet(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), +process_packet(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), PState = #pstate{session = SPid, mountpoint = Mountpoint, proto_ver = ProtoVer, is_bridge = IsBridge, ignore_loop = IgnoreLoop}) -> @@ -419,15 +428,17 @@ process_packet(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), deliver({suback, PacketId, ReasonCodes}, PState) end; {error, TopicFilters} -> - {SubTopics, ReasonCodes} = - lists:foldr(fun({Topic, #{rc := ?RC_SUCCESS}}, {Topics, Codes}) -> + {ReverseSubTopics, ReverseReasonCodes} = + lists:foldl(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), + {SubTopics, ReasonCodes} = {lists:reverse(ReverseSubTopics), lists:reverse(ReverseReasonCodes)}, ?LOG(warning, "Cannot subscribe ~p for ~p", [SubTopics, [emqx_reason_codes:text(R) || R <- ReasonCodes]]), - deliver({suback, PacketId, ReasonCodes}, PState) + deliver({suback, PacketId, ReasonCodes}, PState), + do_acl_deny_action(Packet, ReasonCodes, PState) end; process_packet(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), @@ -674,7 +685,7 @@ set_session_attrs({expiry_interval, #pstate{zone = Zone, proto_ver = ProtoVer, c false -> emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) end - end, SessAttrs); + end, SessAttrs); set_session_attrs({topic_alias_maximum, #pstate{zone = Zone, proto_ver = ProtoVer, conn_props = ConnProps}}, SessAttrs) -> maps:put(topic_alias_maximum, if ProtoVer =:= ?MQTT_PROTO_V5 -> @@ -781,7 +792,6 @@ check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Ret #pstate{zone = Zone}) -> emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}). - check_pub_acl(_Packet, #pstate{is_super = IsSuper, enable_acl = EnableAcl}) when IsSuper orelse (not EnableAcl) -> ok; @@ -887,3 +897,32 @@ sp(false) -> 0. flag(false) -> 0; flag(true) -> 1. + +%%------------------------------------------------------------------------------ +%% Execute actions in case acl deny + +do_acl_deny_action(?PUBLISH_PACKET(?QOS_0, _Topic, _PacketId, _Payload), + ?RC_NOT_AUTHORIZED, PState = #pstate{acl_deny_action = disconnect}) -> + {error, ?RC_NOT_AUTHORIZED, PState}; + +do_acl_deny_action(?PUBLISH_PACKET(?QOS_1, _Topic, _PacketId, _Payload), + ?RC_NOT_AUTHORIZED, PState = #pstate{acl_deny_action = disconnect}) -> + deliver({disconnect, ?RC_NOT_AUTHORIZED}, PState), + {error, ?RC_NOT_AUTHORIZED, PState}; + +do_acl_deny_action(?PUBLISH_PACKET(?QOS_2, _Topic, _PacketId, _Payload), + ?RC_NOT_AUTHORIZED, PState = #pstate{acl_deny_action = disconnect}) -> + deliver({disconnect, ?RC_NOT_AUTHORIZED}, PState), + {error, ?RC_NOT_AUTHORIZED, PState}; + +do_acl_deny_action(?SUBSCRIBE_PACKET(_PacketId, _Properties, _RawTopicFilters), + ReasonCodes, PState = #pstate{acl_deny_action = disconnect}) -> + case lists:member(?RC_NOT_AUTHORIZED, ReasonCodes) of + true -> + deliver({disconnect, ?RC_NOT_AUTHORIZED}, PState), + {error, ?RC_NOT_AUTHORIZED, PState}; + false -> + {ok, PState} + end; +do_acl_deny_action(_PubSupPacket, _ReasonCode, PState) -> + {ok, PState}. diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 180edc8f0..4751a5bed 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -179,15 +179,15 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, websocket_handle({binary, Rest}, reset_parser(State#state{proto_state = ProtoState1})); {error, Error} -> ?LOG(error, "Protocol error - ~p", [Error]), - stop(Error, State); + shutdown(Error, State); {error, Reason, ProtoState1} -> shutdown(Reason, State#state{proto_state = ProtoState1}); {stop, Error, ProtoState1} -> - stop(Error, State#state{proto_state = ProtoState1}) + shutdown(Error, State#state{proto_state = ProtoState1}) end; {error, Error} -> ?LOG(error, "Frame error: ~p", [Error]), - stop(Error, State); + shutdown(Error, State); {'EXIT', Reason} -> ?LOG(error, "Frame error:~p~nFrame data: ~p", [Reason, Data]), shutdown(parse_error, State) @@ -299,8 +299,5 @@ ensure_stats_timer(State) -> shutdown(Reason, State) -> {stop, State#state{shutdown = Reason}}. -stop(Error, State) -> - {stop, State#state{shutdown = Error}}. - wsock_stats() -> [{Key, get(Key)} || Key <- ?SOCK_STATS]. diff --git a/test/emqx_access_SUITE_data/acl.conf b/test/emqx_access_SUITE_data/acl.conf index 03416f002..e5730b4c5 100644 --- a/test/emqx_access_SUITE_data/acl.conf +++ b/test/emqx_access_SUITE_data/acl.conf @@ -13,4 +13,3 @@ {deny, all, subscribe, ["$SYS/#", "#"]}. {deny, all}. - diff --git a/test/emqx_access_SUITE_data/acl_deny_action.conf b/test/emqx_access_SUITE_data/acl_deny_action.conf new file mode 100644 index 000000000..753782605 --- /dev/null +++ b/test/emqx_access_SUITE_data/acl_deny_action.conf @@ -0,0 +1,4 @@ + +{deny, {user, "emqx"}, pubsub, ["acl_deny_action"]}. + +{allow, all}. diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl index ae308ea42..387be3053 100644 --- a/test/emqx_protocol_SUITE.erl +++ b/test/emqx_protocol_SUITE.erl @@ -36,29 +36,33 @@ all() -> [ {group, mqttv4}, - {group, mqttv5}]. + {group, mqttv5}, + {group, acl} + ]. groups() -> [{mqttv4, [sequence], - [ - connect_v4, - subscribe_v4 - ]}, + [connect_v4, + subscribe_v4]}, {mqttv5, [sequence], - [ - connect_v5, - subscribe_v5 - ] - }]. + [connect_v5, + subscribe_v5]}, + {acl, + [sequence], + [acl_deny_action]}]. + init_per_suite(Config) -> - emqx_ct_broker_helpers:run_setup_steps(), + [start_apps(App, SchemaFile, ConfigFile) || + {App, SchemaFile, ConfigFile} + <- [{emqx, deps_path(emqx, "priv/emqx.schema"), + deps_path(emqx, "etc/emqx.conf")}]], Config. end_per_suite(_Config) -> - emqx_ct_broker_helpers:run_teardown_steps(). + application:stop(emqx). batch_connect(NumberOfConnections) -> batch_connect([], NumberOfConnections). @@ -67,7 +71,7 @@ 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}], + [binary, {packet, raw}, {active, false}], 3000), batch_connect([Sock | Socks], NumberOfConnections - 1). @@ -77,7 +81,7 @@ with_connection(DoFun, NumberOfConnections) -> DoFun(Socks) after lists:foreach(fun(Sock) -> - emqx_client_sock:close(Sock) + emqx_client_sock:close(Sock) end, Socks) end. @@ -154,7 +158,7 @@ connect_v5(_) -> #{'Response-Information' := _RespInfo}), _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) end), - + % test clean start with_connection(fun([Sock]) -> emqx_client_sock:send(Sock, @@ -267,7 +271,7 @@ connect_v5(_) -> ?DISCONNECT_PACKET(?RC_DISCONNECT_WITH_WILL_MESSAGE) ) ), - + {ok, WillData} = gen_tcp:recv(Sock2, 0), {ok, ?PUBLISH_PACKET(?QOS_1, <<"TopicA">>, _, <<"will message 2">>), _} = raw_recv_parse(WillData, ?MQTT_PROTO_V5), @@ -324,7 +328,7 @@ connect_v5(_) -> {ok, SubData1} = gen_tcp:recv(Sock1, 0), {ok, ?SUBACK_PACKET(1, #{}, [2]), _} = raw_recv_parse(SubData1, ?MQTT_PROTO_V5) - end, 2), + end, 2), ok. @@ -422,3 +426,66 @@ raw_send_serialize(Packet, Opts) -> raw_recv_parse(P, ProtoVersion) -> emqx_frame:parse(P, {none, #{max_packet_size => ?MAX_PACKET_SIZE, version => ProtoVersion}}). + + +acl_deny_action(_) -> + emqx_zone:set_env(external, acl_deny_action, disconnect), + process_flag(trap_exit, true), + [acl_deny_do_disconnect(publish, QoS, <<"acl_deny_action">>) || QoS <- lists:seq(0, 2)], + [acl_deny_do_disconnect(subscribe, QoS, <<"acl_deny_action">>) || QoS <- lists:seq(0, 2)], + emqx_zone:set_env(external, acl_deny_action, ignore), + ok. + +acl_deny_do_disconnect(publish, QoS, Topic) -> + {ok, Client} = emqx_client:start_link([{username, <<"emqx">>}]), + {ok, _} = emqx_client:connect(Client), + emqx_client:publish(Client, Topic, <<"test">>, QoS), + receive + {'EXIT', Client, _Reason} -> + false = is_process_alive(Client) + end; +acl_deny_do_disconnect(subscribe, QoS, Topic) -> + {ok, Client} = emqx_client:start_link([{username, <<"emqx">>}]), + {ok, _} = emqx_client:connect(Client), + try emqx_client:subscribe(Client, Topic, QoS) of + _ -> + ok + catch + exit : _Reason -> + false = is_process_alive(Client) + end. + +start_apps(App, SchemaFile, ConfigFile) -> + read_schema_configs(App, SchemaFile, ConfigFile), + set_special_configs(App), + application:ensure_all_started(App). + +read_schema_configs(App, SchemaFile, ConfigFile) -> + Schema = cuttlefish_schema:files([SchemaFile]), + Conf = conf_parse:file(ConfigFile), + NewConfig = cuttlefish_generator:map(Schema, Conf), + Vals = proplists:get_value(App, NewConfig, []), + [application:set_env(App, Par, Value) || {Par, Value} <- Vals]. + +set_special_configs(emqx) -> + application:set_env(emqx, enable_acl_cache, false), + application:set_env(emqx, plugins_loaded_file, + deps_path(emqx, "test/emqx_SUITE_data/loaded_plugins")), + application:set_env(emqx, acl_deny_action, disconnect), + application:set_env(emqx, acl_file, + deps_path(emqx, "test/emqx_access_SUITE_data/acl_deny_action.conf")); +set_special_configs(_App) -> + ok. + +deps_path(App, RelativePath) -> + %% Note: not lib_dir because etc dir is not sym-link-ed to _build dir + %% but priv dir is + Path0 = code:priv_dir(App), + Path = case file:read_link(Path0) of + {ok, Resolved} -> Resolved; + {error, _} -> Path0 + end, + filename:join([Path, "..", RelativePath]). + +local_path(RelativePath) -> + deps_path(emqx_auth_username, RelativePath). From 892d9439b9144ce88b2187f7ab02c4e8cb7cf0c4 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 19 Dec 2018 16:49:35 +0800 Subject: [PATCH 496/520] Implement a new session supervisor. (#2077) --- src/emqx_session_sup.erl | 256 +++++++++++++++++++++++++++++++++++++++ src/emqx_sm.erl | 39 ++---- src/emqx_sm_sup.erl | 47 ++++--- 3 files changed, 297 insertions(+), 45 deletions(-) create mode 100644 src/emqx_session_sup.erl diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl new file mode 100644 index 000000000..efa64815b --- /dev/null +++ b/src/emqx_session_sup.erl @@ -0,0 +1,256 @@ +%% Copyright (c) 2018 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). + +-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). +-define(ERROR_MSG(Format, Args), + error_logger:error_msg("[~s] " ++ Format, [?MODULE | Args])). + +%% @doc Start session supervisor. +-spec(start_link(map()) -> emqx_types: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()) -> emqx_types: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 -> + ?ERROR_MSG("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) -> + ?ERROR_MSG("unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Msg, State) -> + ?ERROR_MSG("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) -> + ?ERROR_MSG("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_sm.erl b/src/emqx_sm.erl index 97a1b689c..ab270f1af 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -37,6 +37,9 @@ %% 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]). @@ -60,7 +63,7 @@ start_link() -> open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid := ConnPid}) -> CleanStart = fun(_) -> ok = discard_session(ClientId, ConnPid), - emqx_session:start_link(SessAttrs) + emqx_session_sup:start_session(SessAttrs) end, emqx_sm_locker:trans(ClientId, CleanStart); @@ -70,7 +73,7 @@ open_session(SessAttrs = #{clean_start := false, client_id := ClientId}) -> {ok, SessPid} -> {ok, SessPid, true}; {error, not_found} -> - emqx_session:start_link(SessAttrs) + emqx_session_sup:start_session(SessAttrs) end end, emqx_sm_locker:trans(ClientId, ResumeStart). @@ -130,8 +133,7 @@ register_session(ClientId) when is_binary(ClientId) -> register_session(ClientId, SessPid) when is_binary(ClientId), is_pid(SessPid) -> Session = {ClientId, SessPid}, true = ets:insert(?SESSION_TAB, Session), - ok = emqx_sm_registry:register_session(Session), - notify({registered, ClientId, SessPid}). + emqx_sm_registry:register_session(Session). %% @doc Unregister a session -spec(unregister_session(emqx_types:client_id()) -> ok). @@ -140,11 +142,7 @@ unregister_session(ClientId) when is_binary(ClientId) -> -spec(unregister_session(emqx_types:client_id(), pid()) -> ok). unregister_session(ClientId, SessPid) when is_binary(ClientId), is_pid(SessPid) -> - ok = do_unregister_session({ClientId, SessPid}), - notify({unregistered, SessPid}). - -%% @private -do_unregister_session(Session) -> + 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), @@ -214,9 +212,6 @@ dispatch(ClientId, Topic, Msg) -> emqx_hooks:run('message.dropped', [#{client_id => ClientId}, Msg]) end. -notify(Event) -> - gen_server:cast(?SM, {notify, Event}). - %%------------------------------------------------------------------------------ %% gen_server callbacks %%------------------------------------------------------------------------------ @@ -228,29 +223,16 @@ init([]) -> 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, #{sess_pmon => emqx_pmon:new()}}. + {ok, #{}}. handle_call(Req, _From, State) -> emqx_logger:error("[SM] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({notify, {registered, ClientId, SessPid}}, State = #{sess_pmon := PMon}) -> - {noreply, State#{sess_pmon := emqx_pmon:monitor(SessPid, ClientId, PMon)}}; - -handle_cast({notify, {unregistered, SessPid}}, State = #{sess_pmon := PMon}) -> - {noreply, State#{sess_pmon := emqx_pmon:demonitor(SessPid, PMon)}}; - handle_cast(Msg, State) -> emqx_logger:error("[SM] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{sess_pmon := PMon}) -> - SessPids = [Pid | emqx_misc:drain_down(?BATCH_SIZE)], - {Items, PMon1} = emqx_pmon:erase_all(SessPids, PMon), - ok = emqx_pool:async_submit( - fun lists:foreach/2, [fun clean_down/1, Items]), - {noreply, State#{sess_pmon := PMon1}}; - handle_info(Info, State) -> emqx_logger:error("[SM] unexpected info: ~p", [Info]), {noreply, State}. @@ -265,12 +247,11 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -clean_down({SessPid, ClientId}) -> - Session = {ClientId, SessPid}, +clean_down(Session = {ClientId, SessPid}) -> case ets:member(?SESSION_TAB, ClientId) orelse ets:member(?SESSION_ATTRS_TAB, Session) of true -> - do_unregister_session(Session); + unregister_session(ClientId, SessPid); false -> ok end. diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index 0be9facb0..317cd11db 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -26,25 +26,40 @@ start_link() -> init([]) -> %% Session locker - Locker = #{id => locker, - start => {emqx_sm_locker, start_link, []}, - restart => permanent, + Locker = #{id => locker, + start => {emqx_sm_locker, start_link, []}, + restart => permanent, shutdown => 5000, - type => worker, - modules => [emqx_sm_locker]}, + type => worker, + modules => [emqx_sm_locker] + }, %% Session registry - Registry = #{id => registry, - start => {emqx_sm_registry, start_link, []}, - restart => permanent, + Registry = #{id => registry, + start => {emqx_sm_registry, start_link, []}, + restart => permanent, shutdown => 5000, - type => worker, - modules => [emqx_sm_registry]}, + type => worker, + modules => [emqx_sm_registry] + }, %% Session Manager - Manager = #{id => manager, - start => {emqx_sm, start_link, []}, - restart => permanent, + Manager = #{id => manager, + start => {emqx_sm, start_link, []}, + restart => permanent, shutdown => 5000, - type => worker, - modules => [emqx_sm]}, - {ok, {{rest_for_one, 10, 3600}, [Locker, Registry, Manager]}}. + 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]}}. From 7a1ec580b0716b53dec54bb6bf3c864fa8294692 Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 19 Dec 2018 17:14:06 +0800 Subject: [PATCH 497/520] Update broker test cases --- src/emqx_broker.erl | 2 +- src/emqx_session.erl | 2 +- test/emqx_broker_SUITE.erl | 63 ++++++++++++++----------------------- test/emqx_session_SUITE.erl | 4 +-- 4 files changed, 27 insertions(+), 44 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 6d1ee98cb..429c6097d 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -87,7 +87,7 @@ subscribe(Topic) when is_binary(Topic) -> -spec(subscribe(emqx_topic:topic(), emqx_types:subid() | emqx_types:subopts()) -> ok). subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> - subscribe(Topic, SubId, #{}); + subscribe(Topic, SubId, #{qos => 0}); subscribe(Topic, SubOpts) when is_binary(Topic), is_map(SubOpts) -> subscribe(Topic, undefined, SubOpts). diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 1e186b0ab..9117dd1b8 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -482,7 +482,7 @@ handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, lists:foldr(fun({Topic, _SubOpts}, {Acc, SubMap}) -> case maps:find(Topic, SubMap) of {ok, SubOpts} -> - ok = emqx_broker:unsubscribe(Topic, ClientId), + ok = emqx_broker:unsubscribe(Topic), emqx_hooks:run('session.unsubscribed', [#{client_id => ClientId}, Topic, SubOpts]), {[?RC_SUCCESS|Acc], maps:remove(Topic, SubMap)}; error -> diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index f9a6dcf40..b2031a61c 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -36,7 +36,6 @@ groups() -> [ {pubsub, [sequence], [subscribe_unsubscribe, publish, pubsub, - t_local_subscribe, t_shared_subscribe, dispatch_with_no_sub, 'pubsub#', 'pubsub+']}, @@ -61,14 +60,14 @@ 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(<<"topic">>, <<"clientId">>), + true = emqx:subscribed(<<"clientId">>, <<"topic">>), Topics = emqx:topics(), lists:foreach(fun(Topic) -> ?assert(lists:member(Topic, Topics)) end, Topics), - ok = emqx:unsubscribe(<<"topic">>, <<"clientId">>), - ok = emqx:unsubscribe(<<"topic/1">>, <<"clientId">>), - ok = emqx:unsubscribe(<<"topic/2">>, <<"clientId">>). + ok = emqx:unsubscribe(<<"topic">>), + ok = emqx:unsubscribe(<<"topic/1">>), + ok = emqx:unsubscribe(<<"topic/2">>). publish(_) -> Msg = emqx_message:make(ct, <<"test/pubsub">>, <<"hello">>), @@ -85,18 +84,25 @@ dispatch_with_no_sub(_) -> pubsub(_) -> true = emqx:is_running(node()), Self = self(), - Subscriber = {Self, <<"clientId">>}, - ok = emqx:subscribe(<<"a/b/c">>, <<"clientId">>, #{ qos => 1 }), - #{qos := 1} = ets:lookup_element(emqx_suboption, {<<"a/b/c">>, Subscriber}, 2), - #{qos := 1} = emqx:get_subopts(<<"a/b/c">>, Subscriber), - true = emqx:set_subopts(<<"a/b/c">>, Subscriber, #{qos => 0}), - #{qos := 0} = emqx:get_subopts(<<"a/b/c">>, Subscriber), - ok = emqx:subscribe(<<"a/b/c">>, <<"clientId">>, #{ qos => 2 }), + Subscriber = <<"clientId">>, + ok = emqx: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 }), %% ct:log("Emq Sub: ~p.~n", [ets:lookup(emqx_suboption, {<<"a/b/c">>, Subscriber})]), timer:sleep(10), - [{Self, <<"clientId">>}] = emqx_broker:subscribers(<<"a/b/c">>), + [Self] = emqx_broker:subscribers(<<"a/b/c">>), emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), - ?assert(receive {dispatch, <<"a/b/c">>, _ } -> true; P -> ct:log("Receive Message: ~p~n",[P]) after 2 -> false end), + ?assert( + receive {dispatch, <<"a/b/c">>, _ } -> + true; + P -> + ct:log("Receive Message: ~p~n",[P]) + after 2 -> + false + end), spawn(fun() -> emqx:subscribe(<<"a/b/c">>), emqx:subscribe(<<"c/d/e">>), @@ -106,38 +112,15 @@ pubsub(_) -> timer:sleep(20), emqx:unsubscribe(<<"a/b/c">>). -t_local_subscribe(_) -> - ok = emqx:subscribe(<<"$local/topic0">>), - ok = emqx:subscribe(<<"$local/topic1">>, <<"clientId">>), - ok = emqx:subscribe(<<"$local/topic2">>, <<"clientId">>, #{ qos => 2 }), - timer:sleep(10), - ?assertEqual([{self(), undefined}], emqx:subscribers("$local/topic0")), - ?assertEqual([{self(), <<"clientId">>}], emqx:subscribers("$local/topic1")), - ?assertEqual([{<<"$local/topic1">>, #{ qos => 0 }}, - {<<"$local/topic2">>, #{ qos => 2 }}], - emqx:subscriptions({self(), <<"clientId">>})), - ?assertEqual(ok, emqx:unsubscribe("$local/topic0")), - ?assertEqual(ok, emqx:unsubscribe("$local/topic0")), - ?assertEqual(ok, emqx:unsubscribe("$local/topic1", <<"clientId">>)), - ?assertEqual(ok, emqx:unsubscribe("$local/topic2", <<"clientId">>)), - ?assertEqual([], emqx:subscribers("topic1")), - ?assertEqual([], emqx:subscriptions({self(), <<"clientId">>})). - t_shared_subscribe(_) -> - emqx:subscribe("$local/$share/group1/topic1"), emqx:subscribe("$share/group2/topic2"), emqx:subscribe("$queue/topic3"), timer:sleep(10), - ct:log("share subscriptions: ~p~n", [emqx:subscriptions({self(), undefined})]), - ?assertEqual([{self(), undefined}], emqx:subscribers(<<"$local/$share/group1/topic1">>)), - ?assertEqual([{<<"$local/$share/group1/topic1">>, #{qos => 0}}, - {<<"$queue/topic3">>, #{qos => 0}}, - {<<"$share/group2/topic2">>, #{qos => 0}}], - lists:sort(emqx:subscriptions({self(), undefined}))), - emqx:unsubscribe("$local/$share/group1/topic1"), + 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([], lists:sort(emqx:subscriptions(self()))). + ?assertEqual(0, length(emqx:subscriptions(self()))). 'pubsub#'(_) -> emqx:subscribe(<<"a/#">>), diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index a04a0b82b..2e597340d 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -53,7 +53,7 @@ t_session_all(_) -> emqx_session:subscribe(SPid, [{<<"topic">>, #{qos => 2}}]), emqx_session:subscribe(SPid, [{<<"topic">>, #{qos => 1}}]), timer:sleep(200), - [{<<"topic">>, _}] = emqx:subscriptions({SPid, <<"ClientId">>}), + [{<<"topic">>, _}] = emqx:subscriptions(SPid), emqx_session:publish(SPid, 1, Message1), timer:sleep(200), {publish, 1, _} = emqx_mock_client:get_last_message(ConnPid), @@ -76,5 +76,5 @@ t_session_all(_) -> 1 = proplists:get_value(subscriptions_count, Stats), emqx_session:unsubscribe(SPid, [<<"topic">>]), timer:sleep(200), - [] = emqx:subscriptions({SPid, <<"clientId">>}), + [] = emqx:subscriptions(SPid), emqx_mock_client:close_session(ConnPid). From 97474171d0e3b6db5805913d538a208ad5ed12a1 Mon Sep 17 00:00:00 2001 From: tigercl Date: Wed, 19 Dec 2018 23:06:48 +0800 Subject: [PATCH 498/520] Better report errors in acl.conf (#2065) --- src/emqx_access_control.erl | 23 +++++++++++++---------- src/emqx_access_rule.erl | 6 +----- src/emqx_acl_internal.erl | 4 +--- test/emqx_access_SUITE.erl | 3 +-- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 1b9d76937..2b9cb4211 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -156,16 +156,19 @@ handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) -> reply(case lists:keymember(Mod, 1, Mods) of true -> {error, already_exists}; false -> - case catch Mod:init(Opts) of - {ok, ModState} -> - NewMods = lists:sort(fun({_, _, Seq1}, {_, _, Seq2}) -> - Seq1 >= Seq2 - end, [{Mod, ModState, Seq} | Mods]), - ets:insert(?TAB, {tab_key(Type), NewMods}), ok; - {error, Error} -> - {error, Error}; - {'EXIT', Reason} -> - {error, Reason} + try + case Mod:init(Opts) of + {ok, ModState} -> + NewMods = lists:sort(fun({_, _, Seq1}, {_, _, Seq2}) -> + Seq1 >= Seq2 + end, [{Mod, ModState, Seq} | Mods]), + ets:insert(?TAB, {tab_key(Type), NewMods}), + ok + end + catch + _:Error -> + emqx_logger:error("[AccessControl] Failed to init ~s: ~p", [Mod, Error]), + {error, Error} end end, State); diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 47b20676b..72a3d09b2 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -44,11 +44,7 @@ compile({A, Who, Access, Topic}) when ?ALLOW_DENY(A), ?PUBSUB(Access), is_binary {A, compile(who, Who), Access, [compile(topic, Topic)]}; compile({A, Who, Access, TopicFilters}) when ?ALLOW_DENY(A), ?PUBSUB(Access) -> - {A, compile(who, Who), Access, [compile(topic, Topic) || Topic <- TopicFilters]}; - -compile(Rule) -> - emqx_logger:error("[ACCESS_RULE] Malformed rule: ~p", [Rule]), - {error, bad_rule}. + {A, compile(who, Who), Access, [compile(topic, Topic) || Topic <- TopicFilters]}. compile(who, all) -> all; diff --git a/src/emqx_acl_internal.erl b/src/emqx_acl_internal.erl index 328993a78..1effd1709 100644 --- a/src/emqx_acl_internal.erl +++ b/src/emqx_acl_internal.erl @@ -63,9 +63,7 @@ load_rules_from_file(AclFile) -> emqx_logger:error("[ACL_INTERNAL] Failed to read ~s: ~p", [AclFile, Reason]), {error, Reason} end. - -filter(_PubSub, {error, _}) -> - false; + filter(_PubSub, {allow, all}) -> true; filter(_PubSub, {deny, all}) -> diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index 5d0bcf049..b49c90d80 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -355,8 +355,7 @@ compile_rule(_) -> {deny, all, subscribe, [ [<<"$SYS">>, '#'], ['#'] ]} = compile({deny, all, subscribe, ["$SYS/#", "#"]}), {allow, all} = compile({allow, all}), - {deny, all} = compile({deny, all}), - {error, bad_rule} = compile({test, malformed}). + {deny, all} = compile({deny, all}). match_rule(_) -> User = #{client_id => <<"testClient">>, username => <<"TestUser">>, peername => {{127,0,0,1}, 2948}}, From 42fc8f5811d551aebee9578ed16da5ad90eae043 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 20 Dec 2018 10:17:14 +0800 Subject: [PATCH 499/520] Improve the session module --- src/emqx_session.erl | 170 ++++++++++++++++++++++--------------------- 1 file changed, 88 insertions(+), 82 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 9117dd1b8..9c28884c9 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -158,8 +158,6 @@ -export_type([attr/0]). --define(TIMEOUT, 60000). - -define(LOG(Level, Format, Args, _State), emqx_logger:Level("[Session] " ++ Format, Args)). @@ -261,9 +259,11 @@ subscribe(SPid, PacketId, Properties, TopicFilters) -> publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_0}) -> %% Publish QoS0 message directly emqx_broker:publish(Msg); + publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_1}) -> %% Publish QoS1 message directly 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 @@ -275,6 +275,7 @@ publish(SPid, PacketId, Msg = #message{qos = ?QOS_2, timestamp = Ts}) -> 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}). @@ -322,7 +323,7 @@ discard(SPid, ByPid) -> -spec(update_expiry_interval(spid(), timeout()) -> ok). update_expiry_interval(SPid, Interval) -> - gen_server:cast(SPid, {expiry_interval, Interval}). + gen_server:cast(SPid, {update_expiry_interval, Interval}). -spec(close(spid()) -> ok). close(SPid) -> @@ -332,39 +333,39 @@ close(SPid) -> %% 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}]) -> - emqx_logger:set_metadata_client_id(ClientId), +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), IdleTimout = get_env(Zone, idle_timeout, 30000), - State = #state{idle_timeout = IdleTimout, - clean_start = CleanStart, - binding = binding(ConnPid), - client_id = ClientId, - username = Username, - conn_pid = ConnPid, - subscriptions = #{}, - max_subscriptions = get_env(Zone, max_subscriptions, 0), - upgrade_qos = get_env(Zone, upgrade_qos, false), - inflight = emqx_inflight:new(MaxInflight), - mqueue = init_mqueue(Zone), - retry_interval = get_env(Zone, retry_interval, 0), - awaiting_rel = #{}, - await_rel_timeout = get_env(Zone, await_rel_timeout), - max_awaiting_rel = get_env(Zone, max_awaiting_rel), - expiry_interval = ExpiryInterval, - enable_stats = get_env(Zone, enable_stats, true), - deliver_stats = 0, - enqueue_stats = 0, - created_at = os:timestamp(), - will_msg = WillMsg + State = #state{idle_timeout = IdleTimout, + clean_start = CleanStart, + binding = binding(ConnPid), + client_id = ClientId, + username = Username, + conn_pid = ConnPid, + subscriptions = #{}, + max_subscriptions = get_env(Zone, max_subscriptions, 0), + upgrade_qos = get_env(Zone, upgrade_qos, false), + inflight = emqx_inflight:new(MaxInflight), + mqueue = init_mqueue(Zone), + retry_interval = get_env(Zone, retry_interval, 0), + awaiting_rel = #{}, + await_rel_timeout = get_env(Zone, await_rel_timeout), + max_awaiting_rel = get_env(Zone, max_awaiting_rel), + expiry_interval = ExpiryInterval, + enable_stats = get_env(Zone, enable_stats, true), + deliver_stats = 0, + enqueue_stats = 0, + created_at = os:timestamp(), + will_msg = WillMsg }, ok = emqx_sm:register_session(ClientId, self()), true = emqx_sm:set_session_attrs(ClientId, attrs(State)), @@ -397,53 +398,56 @@ handle_call(stats, _From, State) -> handle_call({discard, ByPid}, _From, State = #state{conn_pid = undefined}) -> ?LOG(warning, "Discarded by ~p", [ByPid], State), - {stop, {shutdown, discard}, ok, State}; + {stop, 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], State), ConnPid ! {shutdown, discard, {ClientId, ByPid}}, - {stop, {shutdown, discard}, ok, State}; + {stop, 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{awaiting_rel = AwaitingRel}) -> - reply(case is_awaiting_full(State) 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_await_rel_timer(State1)} - end; - true -> - emqx_metrics:trans(inc, 'messages/qos2/dropped'), - ?LOG(warning, "Dropped qos2 packet ~w for too many awaiting_rel", [PacketId], State), - {{error, ?RC_RECEIVE_MAXIMUM_EXCEEDED}, State} - end); + reply( + case is_awaiting_full(State) 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_await_rel_timer(State1)} + end; + true -> + ?LOG(warning, "Dropped qos2 packet ~w for too many awaiting_rel", [PacketId], State), + emqx_metrics:trans(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, acked(pubrec, PacketId, State)}; - false -> - emqx_metrics:trans(inc, 'packets/pubrec/missed'), - ?LOG(warning, "The PUBREC PacketId ~w is not found.", [PacketId], State), - {{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State} - end); + reply( + case emqx_inflight:contain(PacketId, Inflight) of + true -> + {ok, acked(pubrec, PacketId, State)}; + false -> + ?LOG(warning, "The PUBREC PacketId ~w is not found.", [PacketId], State), + emqx_metrics:trans(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, State#state{awaiting_rel = AwaitingRel1}}; - error -> - emqx_metrics:trans(inc, 'packets/pubrel/missed'), - ?LOG(warning, "Cannot find PUBREL: ~w", [PacketId], State), - {{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State} - end); + reply( + case maps:take(PacketId, AwaitingRel) of + {_Ts, AwaitingRel1} -> + {ok, State#state{awaiting_rel = AwaitingRel1}}; + error -> + ?LOG(warning, "The PUBREL PacketId ~w is not found", [PacketId], State), + emqx_metrics:trans(inc, 'packets/pubrel/missed'), + {{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State} + end); handle_call(close, _From, State) -> {stop, normal, ok, State}; @@ -494,25 +498,27 @@ handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, %% PUBACK: handle_cast({puback, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> - case emqx_inflight:contain(PacketId, Inflight) of - true -> - noreply(dequeue(acked(puback, PacketId, State))); - false -> - ?LOG(warning, "The PUBACK PacketId ~w is not found", [PacketId], State), - emqx_metrics:trans(inc, 'packets/puback/missed'), - {noreply, State} - end; + noreply( + case emqx_inflight:contain(PacketId, Inflight) of + true -> + dequeue(acked(puback, PacketId, State)); + false -> + ?LOG(warning, "The PUBACK PacketId ~w is not found", [PacketId], State), + emqx_metrics:trans(inc, 'packets/puback/missed'), + State + end); %% PUBCOMP: handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> - case emqx_inflight:contain(PacketId, Inflight) of - true -> - noreply(dequeue(acked(pubcomp, PacketId, State))); - false -> - ?LOG(warning, "The PUBCOMP PacketId ~w is not found", [PacketId], State), - emqx_metrics:trans(inc, 'packets/pubcomp/missed'), - {noreply, State} - end; + noreply( + case emqx_inflight:contain(PacketId, Inflight) of + true -> + dequeue(acked(pubcomp, PacketId, State)); + false -> + ?LOG(warning, "The PUBCOMP PacketId ~w is not found", [PacketId], State), + emqx_metrics:trans(inc, 'packets/pubcomp/missed'), + State + end); %% RESUME: handle_cast({resume, #{conn_pid := ConnPid, @@ -561,7 +567,7 @@ handle_cast({resume, #{conn_pid := ConnPid, %% Replay delivery and Dequeue pending messages noreply(dequeue(retry_delivery(true, State1))); -handle_cast({expiry_interval, Interval}, State) -> +handle_cast({update_expiry_interval, Interval}, State) -> {noreply, State#state{expiry_interval = Interval}}; handle_cast(Msg, State) -> From d8276042136f9b2e453863bdba90dad7172e6e17 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 20 Dec 2018 10:56:56 +0800 Subject: [PATCH 500/520] Remove the ensure_stats_timer/1 call from reply/2 and noreply/1 --- src/emqx_session.erl | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 9c28884c9..e9bc0ff98 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -417,7 +417,7 @@ handle_call({register_publish_packet_id, PacketId, Ts}, _From, {{error, ?RC_PACKET_IDENTIFIER_IN_USE}, State}; false -> State1 = State#state{awaiting_rel = maps:put(PacketId, Ts, AwaitingRel)}, - {ok, ensure_await_rel_timer(State1)} + {ok, ensure_stats_timer(ensure_await_rel_timer(State1))} end; true -> ?LOG(warning, "Dropped qos2 packet ~w for too many awaiting_rel", [PacketId], State), @@ -430,7 +430,7 @@ handle_call({pubrec, PacketId, _ReasonCode}, _From, State = #state{inflight = In reply( case emqx_inflight:contain(PacketId, Inflight) of true -> - {ok, acked(pubrec, PacketId, State)}; + {ok, ensure_stats_timer(acked(pubrec, PacketId, State))}; false -> ?LOG(warning, "The PUBREC PacketId ~w is not found.", [PacketId], State), emqx_metrics:trans(inc, 'packets/pubrec/missed'), @@ -442,7 +442,7 @@ handle_call({pubrel, PacketId, _ReasonCode}, _From, State = #state{awaiting_rel reply( case maps:take(PacketId, AwaitingRel) of {_Ts, AwaitingRel1} -> - {ok, State#state{awaiting_rel = AwaitingRel1}}; + {ok, ensure_stats_timer(State#state{awaiting_rel = AwaitingRel1})}; error -> ?LOG(warning, "The PUBREL PacketId ~w is not found", [PacketId], State), emqx_metrics:trans(inc, 'packets/pubrel/missed'), @@ -477,7 +477,7 @@ handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, end} end, {[], Subscriptions}, TopicFilters), suback(FromPid, PacketId, ReasonCodes), - noreply(State#state{subscriptions = Subscriptions1}); + noreply(ensure_stats_timer(State#state{subscriptions = Subscriptions1})); %% UNSUBSCRIBE: handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, @@ -494,14 +494,14 @@ handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, end end, {[], Subscriptions}, TopicFilters), unsuback(From, PacketId, ReasonCodes), - noreply(State#state{subscriptions = Subscriptions1}); + 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 -> - dequeue(acked(puback, PacketId, State)); + ensure_stats_timer(dequeue(acked(puback, PacketId, State))); false -> ?LOG(warning, "The PUBACK PacketId ~w is not found", [PacketId], State), emqx_metrics:trans(inc, 'packets/puback/missed'), @@ -513,7 +513,7 @@ handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight noreply( case emqx_inflight:contain(PacketId, Inflight) of true -> - dequeue(acked(pubcomp, PacketId, State)); + ensure_stats_timer(dequeue(acked(pubcomp, PacketId, State))); false -> ?LOG(warning, "The PUBCOMP PacketId ~w is not found", [PacketId], State), emqx_metrics:trans(inc, 'packets/pubcomp/missed'), @@ -565,7 +565,7 @@ handle_cast({resume, #{conn_pid := ConnPid, emqx_hooks:run('session.resumed', [#{client_id => ClientId}, attrs(State)]), %% Replay delivery and Dequeue pending messages - noreply(dequeue(retry_delivery(true, State1))); + noreply(ensure_stats_timer(dequeue(retry_delivery(true, State1)))); handle_cast({update_expiry_interval, Interval}, State) -> {noreply, State#state{expiry_interval = Interval}}; @@ -590,7 +590,7 @@ handle_info({dispatch, Topic, Msg = #message{}}, State) -> ok = emqx_shared_sub:nack_no_connection(Msg), {noreply, State}; false -> - noreply(handle_dispatch(Topic, Msg, State)) + noreply(ensure_stats_timer(handle_dispatch(Topic, Msg, State))) end; %% Do nothing if the client has been disconnected. @@ -601,11 +601,11 @@ 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}) -> - noreply(expire_awaiting_rel(State#state{await_rel_timer = undefined})); + 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}) -> + State = #state{client_id = ClientId, stats_timer = Timer}) -> emqx_metrics:commit(), _ = emqx_sm:set_session_stats(ClientId, stats(State)), NewState = State#state{stats_timer = undefined}, @@ -931,8 +931,7 @@ dequeue(State = #state{inflight = Inflight}) -> dequeue2(State = #state{mqueue = Q}) -> case emqx_mqueue:out(Q) of - {empty, _Q} -> - State; + {empty, _Q} -> State; {{value, Msg}, Q1} -> %% Dequeue more dequeue(dispatch(Msg, State#state{mqueue = Q1})) @@ -972,7 +971,8 @@ ensure_will_delay_timer(State = #state{will_msg = #message{headers = #{'Will-Del ensure_will_delay_timer(State) -> State. -ensure_stats_timer(State = #state{enable_stats = true, stats_timer = undefined, +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) -> @@ -1010,11 +1010,11 @@ reply({Reply, State}) -> reply(Reply, State). reply(Reply, State) -> - {reply, Reply, ensure_stats_timer(State)}. + {reply, Reply, State}. noreply(State) -> - {noreply, ensure_stats_timer(State)}. + {noreply, State}. shutdown(Reason, State) -> - {stop, {shutdown, Reason}, State}. + {stop, Reason, State}. From 6e1b47f1f9cd364eb9a4cda6400e856baa1005ca Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 20 Dec 2018 14:17:16 +0800 Subject: [PATCH 501/520] Improve the emqx_connection module Rename 'publish_limit' to 'pub_limit' 'try ... of ... catch' to replace 'case catch' --- src/emqx_connection.erl | 75 +++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index e1d4011fc..2f596068c 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -16,15 +16,12 @@ -behaviour(gen_server). --define(LOG_HEADER, "[TCP]"). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -include("logger.hrl"). -export([start_link/3]). --export([info/1, attrs/1]). --export([stats/1]). +-export([info/1, attrs/1, stats/1]). -export([kick/1]). -export([session/1]). @@ -46,11 +43,12 @@ stats_timer, incoming, rate_limit, - publish_limit, + pub_limit, limit_timer, idle_timeout }). +-define(LOG_HEADER, "[TCP]"). -define(DEFAULT_ACTIVE_N, 100). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). @@ -65,22 +63,22 @@ start_link(Transport, Socket, Options) -> info(CPid) when is_pid(CPid) -> call(CPid, info); -info(#state{transport = Transport, - socket = Socket, - peername = Peername, - sockname = Sockname, - conn_state = ConnState, - active_n = ActiveN, - rate_limit = RateLimit, - publish_limit = PubLimit, - proto_state = ProtoState}) -> +info(#state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, + conn_state = ConnState, + active_n = ActiveN, + rate_limit = RateLimit, + pub_limit = PubLimit, + proto_state = ProtoState}) -> ConnInfo = [{socktype, Transport:type(Socket)}, {peername, Peername}, {sockname, Sockname}, {conn_state, ConnState}, {active_n, ActiveN}, {rate_limit, esockd_rate_limit:info(RateLimit)}, - {publish_limit, esockd_rate_limit:info(PubLimit)}], + {pub_limit, esockd_rate_limit:info(PubLimit)}], ProtoInfo = emqx_protocol:info(ProtoState), lists:usort(lists:append(ConnInfo, ProtoInfo)). @@ -139,22 +137,21 @@ init([Transport, RawSocket, Options]) -> peercert => Peercert, sendfun => SendFun}, Options), ParserState = emqx_protocol:parser(ProtoState), - State = run_socket(#state{transport = Transport, - socket = Socket, - peername = Peername, - conn_state = running, - active_n = ActiveN, - rate_limit = RateLimit, - publish_limit = PubLimit, - proto_state = ProtoState, - parser_state = ParserState, - enable_stats = EnableStats, - idle_timeout = IdleTimout + State = run_socket(#state{transport = Transport, + socket = Socket, + peername = Peername, + conn_state = running, + active_n = ActiveN, + rate_limit = RateLimit, + pub_limit = PubLimit, + proto_state = ProtoState, + parser_state = ParserState, + enable_stats = EnableStats, + idle_timeout = IdleTimout }), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), ok = emqx_gc:init(GcPolicy), ok = emqx_misc:init_proc_mng_policy(Zone), - emqx_logger:set_metadata_peername(esockd_net:format(Peername)), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); @@ -213,6 +210,7 @@ handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> {error, Reason} -> shutdown(Reason, State) end; + handle_info({timeout, Timer, emit_stats}, State = #state{stats_timer = Timer, proto_state = ProtoState @@ -231,6 +229,7 @@ handle_info({timeout, Timer, emit_stats}, ?LOG(warning, "shutdown due to ~p", [Reason]), shutdown(Reason, NewState) end; + handle_info(timeout, State) -> shutdown(idle_timeout, State); @@ -331,9 +330,9 @@ handle_packet(<<>>, State) -> handle_packet(Data, State = #state{proto_state = ProtoState, parser_state = ParserState, idle_timeout = IdleTimeout}) -> - case catch emqx_frame:parse(Data, ParserState) of - {more, NewParserState} -> - {noreply, State#state{parser_state = NewParserState}, IdleTimeout}; + try emqx_frame:parse(Data, ParserState) of + {more, ParserState1} -> + {noreply, State#state{parser_state = ParserState1}, IdleTimeout}; {ok, Packet = ?PACKET(Type), Rest} -> emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of @@ -348,11 +347,12 @@ handle_packet(Data, State = #state{proto_state = ProtoState, {stop, Error, ProtoState1} -> stop(Error, State#state{proto_state = ProtoState1}) end; - {error, Error} -> - ?LOG(error, "Framing error - ~p", [Error]), - shutdown(Error, State); - {'EXIT', Reason} -> - ?LOG(error, "Parse failed for ~p~nError data:~p", [Reason, Data]), + {error, Reason} -> + ?LOG(error, "Parse frame error - ~p", [Reason]), + shutdown(Reason, State) + catch + _:Error -> + ?LOG(error, "Parse failed for ~p~nError data:~p", [Error, Data]), shutdown(parse_error, State) end. @@ -370,9 +370,9 @@ inc_publish_cnt(_Type, State) -> %% Ensure rate limit %%------------------------------------------------------------------------------ -ensure_rate_limit(State = #state{rate_limit = Rl, publish_limit = Pl, +ensure_rate_limit(State = #state{rate_limit = Rl, pub_limit = Pl, incoming = #{packets := Packets, bytes := Bytes}}) -> - ensure_rate_limit([{Pl, #state.publish_limit, Packets}, + ensure_rate_limit([{Pl, #state.pub_limit, Packets}, {Rl, #state.rate_limit, Bytes}], State). ensure_rate_limit([], State) -> @@ -421,3 +421,4 @@ maybe_gc(#state{}, {publish, _PacketId, #message{payload = Payload}}) -> ok = emqx_gc:inc(1, Oct); maybe_gc(_, _) -> ok. + From 14a12e0c6c791a5576f1e037f03afb5fe5b8efa3 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 20 Dec 2018 14:37:30 +0800 Subject: [PATCH 502/520] Move the 'LOG_HEADER' macro above '-include(logger.hrl)' --- src/emqx_connection.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 2f596068c..79c244a11 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -18,6 +18,8 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). + +-define(LOG_HEADER, "[MQTT]"). -include("logger.hrl"). -export([start_link/3]). @@ -48,7 +50,6 @@ idle_timeout }). --define(LOG_HEADER, "[TCP]"). -define(DEFAULT_ACTIVE_N, 100). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). From 14cffcf7fbb7b3dae227554476152efd4a339b27 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 20 Dec 2018 16:45:25 +0800 Subject: [PATCH 503/520] Add the 'emqx_pd' module Add utility functions for erlang process dictionary Add test cases for emqx_pd --- Makefile | 2 +- src/emqx_connection.erl | 31 +++++++++++++------------------ src/emqx_pd.erl | 33 +++++++++++++++++++++++++++++++++ test/emqx_cm_SUITE.erl | 3 ++- test/emqx_pd_SUITE.erl | 31 +++++++++++++++++++++++++++++++ test/emqx_sm_SUITE.erl | 2 ++ 6 files changed, 82 insertions(+), 20 deletions(-) create mode 100644 src/emqx_pd.erl create mode 100644 test/emqx_pd_SUITE.erl diff --git a/Makefile b/Makefile index 68d383940..374b74fb7 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_session \ emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_mountpoint \ emqx_listeners emqx_protocol emqx_pool emqx_shared_sub emqx_bridge \ - emqx_hooks emqx_batch emqx_sequence emqx_pmon + emqx_hooks emqx_batch emqx_sequence emqx_pmon emqx_pd CT_NODE_NAME = emqxct@127.0.0.1 CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 79c244a11..2cf647beb 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -249,12 +249,13 @@ handle_info({tcp, _Sock, Data}, State) -> ?LOG(debug, "RECV ~p", [Data]), Size = iolist_size(Data), emqx_metrics:trans(inc, 'bytes/received', Size), + emqx_pd:update_counter(incoming_bytes, Size), Incoming = #{bytes => Size, packets => 0}, handle_packet(Data, State#state{incoming = Incoming}); %% Rate limit here, cool:) handle_info({tcp_passive, _Sock}, State) -> - {noreply, ensure_rate_limit(State)}; + {noreply, run_socket(ensure_rate_limit(State))}; handle_info({tcp_error, _Sock, Reason}, State) -> shutdown(Reason, State); @@ -336,10 +337,10 @@ handle_packet(Data, State = #state{proto_state = ProtoState, {noreply, State#state{parser_state = ParserState1}, IdleTimeout}; {ok, Packet = ?PACKET(Type), Rest} -> emqx_metrics:received(Packet), + (Type == ?PUBLISH) andalso emqx_pd:update_counter(incoming_pubs, 1), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> - NewState = State#state{proto_state = ProtoState1}, - handle_packet(Rest, inc_publish_cnt(Type, reset_parser(NewState))); + handle_packet(Rest, reset_parser(State#state{proto_state = ProtoState1})); {error, Reason} -> ?LOG(error, "Process packet error - ~p", [Reason]), shutdown(Reason, State); @@ -360,28 +361,21 @@ handle_packet(Data, State = #state{proto_state = ProtoState, reset_parser(State = #state{proto_state = ProtoState}) -> State#state{parser_state = emqx_protocol:parser(ProtoState)}. -inc_publish_cnt(Type, State = #state{incoming = Incoming = #{packets := Cnt}}) - when Type == ?PUBLISH; Type == ?SUBSCRIBE -> - State#state{incoming = Incoming#{packets := Cnt + 1}}; - -inc_publish_cnt(_Type, State) -> - State. - %%------------------------------------------------------------------------------ %% Ensure rate limit %%------------------------------------------------------------------------------ -ensure_rate_limit(State = #state{rate_limit = Rl, pub_limit = Pl, - incoming = #{packets := Packets, bytes := Bytes}}) -> - ensure_rate_limit([{Pl, #state.pub_limit, Packets}, - {Rl, #state.rate_limit, Bytes}], State). +ensure_rate_limit(State = #state{rate_limit = Rl, pub_limit = Pl}) -> + Limiters = [{Pl, #state.pub_limit, emqx_pd:reset_counter(incoming_pubs)}, + {Rl, #state.rate_limit, emqx_pd:reset_counter(incoming_bytes)}], + ensure_rate_limit(Limiters, State). ensure_rate_limit([], State) -> - run_socket(State); -ensure_rate_limit([{undefined, _Pos, _Num}|Limiters], State) -> + State; +ensure_rate_limit([{undefined, _Pos, _Cnt}|Limiters], State) -> ensure_rate_limit(Limiters, State); -ensure_rate_limit([{Rl, Pos, Num}|Limiters], State) -> - case esockd_rate_limit:check(Num, Rl) of +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} -> @@ -423,3 +417,4 @@ maybe_gc(#state{}, {publish, _PacketId, #message{payload = Payload}}) -> maybe_gc(_, _) -> ok. + diff --git a/src/emqx_pd.erl b/src/emqx_pd.erl new file mode 100644 index 000000000..ce1e7723c --- /dev/null +++ b/src/emqx_pd.erl @@ -0,0 +1,33 @@ +%% Copyright (c) 2018 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 The utility functions for erlang process dictionary. +-module(emqx_pd). + +-export([update_counter/2, get_counter/1, reset_counter/1]). + +-type(key() :: term()). + +-spec(update_counter(key(), number()) -> undefined | number()). +update_counter(Key, Inc) -> + put(Key, get_counter(Key) + Inc). + +-spec(get_counter(key()) -> number()). +get_counter(Key) -> + case get(Key) of undefined -> 0; Cnt -> Cnt end. + +-spec(reset_counter(key()) -> number()). +reset_counter(Key) -> + case put(Key, 0) of undefined -> 0; Cnt -> Cnt end. + diff --git a/test/emqx_cm_SUITE.erl b/test/emqx_cm_SUITE.erl index b720849f6..208294474 100644 --- a/test/emqx_cm_SUITE.erl +++ b/test/emqx_cm_SUITE.erl @@ -18,6 +18,7 @@ -compile(nowarn_export_all). -include("emqx_mqtt.hrl"). +-include_lib("eunit/include/eunit.hrl"). all() -> [t_register_unregister_connection]. @@ -25,7 +26,7 @@ t_register_unregister_connection(_) -> {ok, _} = emqx_cm_sup:start_link(), Pid = self(), ok = emqx_cm:register_connection(<<"conn1">>), - ok emqx_cm:register_connection(<<"conn2">>, Pid), + ok = emqx_cm:register_connection(<<"conn2">>, Pid), true = emqx_cm:set_conn_attrs(<<"conn1">>, [{port, 8080}, {ip, "192.168.0.1"}]), true = emqx_cm:set_conn_attrs(<<"conn2">>, Pid, [{port, 8080}, {ip, "192.168.0.1"}]), timer:sleep(2000), diff --git a/test/emqx_pd_SUITE.erl b/test/emqx_pd_SUITE.erl new file mode 100644 index 000000000..e53fa7539 --- /dev/null +++ b/test/emqx_pd_SUITE.erl @@ -0,0 +1,31 @@ +%% Copyright (c) 2018 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_pd_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> [update_counter]. + +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)), + ?assertEqual(3, emqx_pd:get_counter(bytes)), + ?assertEqual(3, emqx_pd:reset_counter(bytes)), + ?assertEqual(0, emqx_pd:get_counter(bytes)). + diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl index 008d4b6e6..6adb7c388 100644 --- a/test/emqx_sm_SUITE.erl +++ b/test/emqx_sm_SUITE.erl @@ -19,6 +19,8 @@ -compile(export_all). -compile(nowarn_export_all). +-include_lib("eunit/include/eunit.hrl"). + all() -> [t_open_close_session]. t_open_close_session(_) -> From 6b538d2363afac57acc148cf07238c74cbb697ac Mon Sep 17 00:00:00 2001 From: YoukiLin <1045735402@qq.com> Date: Thu, 20 Dec 2018 21:03:40 +0800 Subject: [PATCH 504/520] Add test cases for 'emqx_cm' and 'emqx_sm' (#2073) Add test cases for 'emqx_cm' and 'emqx_cn' --- src/emqx_ctl.erl | 10 ---- test/emqx_client_SUITE.erl | 8 ++-- test/emqx_cm_SUITE.erl | 64 +++++++++++++++++++------- test/emqx_sm_SUITE.erl | 93 ++++++++++++++++++++++++++++---------- 4 files changed, 121 insertions(+), 54 deletions(-) diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index c00556eb7..1d2fb13a3 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -54,15 +54,6 @@ run_command([Cmd | Args]) -> run_command(list_to_atom(Cmd), Args). -spec(run_command(cmd(), [string()]) -> ok | {error, term()}). -% run_command(set, []) -> -% emqx_mgmt_cli_cfg:set_usage(), ok; - -% run_command(set, Args) -> -% emqx_mgmt_cli_cfg:run(["config" | Args]), ok; - -% run_command(show, Args) -> -% emqx_mgmt_cli_cfg:run(["config" | Args]), ok; - run_command(help, []) -> usage(); run_command(Cmd, Args) when is_atom(Cmd) -> @@ -160,4 +151,3 @@ register_command_test_() -> }. -endif. - diff --git a/test/emqx_client_SUITE.erl b/test/emqx_client_SUITE.erl index 021109606..13303bfdc 100644 --- a/test/emqx_client_SUITE.erl +++ b/test/emqx_client_SUITE.erl @@ -32,9 +32,8 @@ <<"+/+">>, <<"TopicA/#">>]). all() -> - [ {group, mqttv4}, - {group, mqttv5} - ]. + [{group, mqttv4}, + {group, mqttv5}]. groups() -> [{mqttv4, [non_parallel_tests], @@ -48,8 +47,7 @@ groups() -> dollar_topics_test]}, {mqttv5, [non_parallel_tests], [request_response, - share_sub_request_topic]} -]. + share_sub_request_topic]}]. init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), diff --git a/test/emqx_cm_SUITE.erl b/test/emqx_cm_SUITE.erl index b720849f6..08a773d40 100644 --- a/test/emqx_cm_SUITE.erl +++ b/test/emqx_cm_SUITE.erl @@ -1,3 +1,4 @@ + %% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,23 +18,54 @@ -compile(export_all). -compile(nowarn_export_all). +-include("emqx.hrl"). +-include_lib("eunit/include/eunit.hrl"). + -include("emqx_mqtt.hrl"). -all() -> [t_register_unregister_connection]. +all() -> [{group, cm}]. -t_register_unregister_connection(_) -> - {ok, _} = emqx_cm_sup:start_link(), - Pid = self(), - ok = emqx_cm:register_connection(<<"conn1">>), - ok emqx_cm:register_connection(<<"conn2">>, Pid), - true = emqx_cm:set_conn_attrs(<<"conn1">>, [{port, 8080}, {ip, "192.168.0.1"}]), - true = emqx_cm:set_conn_attrs(<<"conn2">>, Pid, [{port, 8080}, {ip, "192.168.0.1"}]), - timer:sleep(2000), - ?assertEqual(Pid, emqx_cm:lookup_conn_pid(<<"conn1">>)), - ?assertEqual(Pid, emqx_cm:lookup_conn_pid(<<"conn2">>)), - ok = emqx_cm:unregister_connection(<<"conn1">>), - ?assertEqual(undefined, emqx_cm:lookup_conn_pid(<<"conn1">>)), - ?assertEqual([{port, 8080}, {ip, "192.168.0.1"}], emqx_cm:get_conn_attrs({<<"conn2">>, Pid})), - true = emqx_cm:set_conn_stats(<<"conn2">>, [{count, 1}, {max, 2}]), - ?assertEqual([{count, 1}, {max, 2}], emqx_cm:get_conn_stats({<<"conn2">>, Pid})). +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_broker_helpers:run_setup_steps(), + Config. + +end_per_suite(_Config) -> + emqx_ct_broker_helpers:run_teardown_steps(). + +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_sm_SUITE.erl b/test/emqx_sm_SUITE.erl index 008d4b6e6..407e2c92b 100644 --- a/test/emqx_sm_SUITE.erl +++ b/test/emqx_sm_SUITE.erl @@ -14,34 +14,81 @@ -module(emqx_sm_SUITE). +-include("emqx.hrl"). +-include_lib("eunit/include/eunit.hrl"). + -include("emqx.hrl"). -compile(export_all). -compile(nowarn_export_all). -all() -> [t_open_close_session]. +-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, sm}]. + +groups() -> + [{sm, [non_parallel_tests], + [t_open_close_session, + t_resume_session, + t_discard_session, + t_register_unregister_session, + t_get_set_session_attrs, + t_get_set_session_stats, + t_lookup_session_pids]}]. + +init_per_suite(Config) -> + emqx_ct_broker_helpers:run_setup_steps(), + Config. + +end_per_suite(_Config) -> + emqx_ct_broker_helpers:run_teardown_steps(). t_open_close_session(_) -> - emqx_ct_broker_helpers:run_setup_steps(), {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), - Attrs = #{clean_start => true, - client_id => <<"client">>, - conn_pid => ClientPid, - zone => internal, - username => <<"emqx">>, - expiry_interval => 0, - max_inflight => 0, - topic_alias_maximum => 0, - will_msg => undefined}, - {ok, SPid} = emqx_sm:open_session(Attrs), - ?assertEqual([SPid], emqx_sm:lookup_session_pids(<<"client">>)), - {ok, NewConnPid} = emqx_mock_client:start_link(<<"client">>), - {ok, SPid, true} = emqx_sm:open_session(Attrs#{clean_start => false, conn_pid => NewConnPid}), - ?assertEqual([SPid], emqx_sm:lookup_session_pids(<<"client">>)), - SAttrs = emqx_sm:get_session_attrs(<<"client">>, SPid), - ?assertEqual(<<"client">>, proplists:get_value(client_id, SAttrs)), - emqx_sm:set_session_stats(<<"client">>, SPid, [{inflight, 10}]), - ?assertEqual([{inflight, 10}], emqx_sm:get_session_stats(<<"client">>, SPid)), - ok = emqx_sm:close_session(SPid), - ?assertEqual([], emqx_sm:lookup_session_pids(<<"client">>)), - emqx_ct_broker_helpers:run_teardown_steps(). + {ok, SPid} = emqx_sm:open_session(?ATTRS#{conn_pid => ClientPid}), + ?assertEqual(ok, emqx_sm:close_session(SPid)). + +t_resume_session(_) -> + {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), + {ok, SPid} = emqx_sm:open_session(?ATTRS#{conn_pid => ClientPid}), + ?assertEqual({ok, SPid}, emqx_sm:resume_session(<<"client">>, ?ATTRS#{conn_pid => ClientPid})). + +t_discard_session(_) -> + {ok, ClientPid} = emqx_mock_client:start_link(<<"client1">>), + {ok, _SPid} = emqx_sm:open_session(?ATTRS#{conn_pid => ClientPid}), + ?assertEqual(ok, emqx_sm:discard_session(<<"client1">>)). + +t_register_unregister_session(_) -> + Pid = self(), + {ok, _ClientPid} = emqx_mock_client:start_link(<<"client">>), + ?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(_) -> + {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), + {ok, SPid} = emqx_sm:open_session(?ATTRS#{conn_pid => ClientPid}), + ?assertEqual(true, emqx_sm:set_session_attrs(<<"client">>, [?ATTRS#{conn_pid => ClientPid}])), + ?assertEqual(true, emqx_sm:set_session_attrs(<<"client">>, SPid, [?ATTRS#{conn_pid => ClientPid}])), + [SAttr] = emqx_sm:get_session_attrs(<<"client">>, SPid), + ?assertEqual(<<"client">>, maps:get(client_id, SAttr)). + +t_get_set_session_stats(_) -> + {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), + {ok, SPid} = emqx_sm:open_session(?ATTRS#{conn_pid => ClientPid}), + ?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(_) -> + {ok, ClientPid} = emqx_mock_client:start_link(<<"client">>), + {ok, SPid} = emqx_sm:open_session(?ATTRS#{conn_pid => ClientPid}), + ?assertEqual([SPid], emqx_sm:lookup_session_pids(<<"client">>)). From 938d30268aff0534ceb474c60ff6f79e818bd093 Mon Sep 17 00:00:00 2001 From: tigercl Date: Thu, 20 Dec 2018 21:42:14 +0800 Subject: [PATCH 505/520] Remove extra case...of (#2082) * Remove extra case...of --- src/emqx_access_control.erl | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 2b9cb4211..239769de6 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -156,15 +156,13 @@ handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) -> reply(case lists:keymember(Mod, 1, Mods) of true -> {error, already_exists}; false -> - try - case Mod:init(Opts) of - {ok, ModState} -> - NewMods = lists:sort(fun({_, _, Seq1}, {_, _, Seq2}) -> - Seq1 >= Seq2 - end, [{Mod, ModState, Seq} | Mods]), - ets:insert(?TAB, {tab_key(Type), NewMods}), - ok - end + try Mod:init(Opts) of + {ok, ModState} -> + NewMods = lists:sort(fun({_, _, Seq1}, {_, _, Seq2}) -> + Seq1 >= Seq2 + end, [{Mod, ModState, Seq} | Mods]), + ets:insert(?TAB, {tab_key(Type), NewMods}), + ok catch _:Error -> emqx_logger:error("[AccessControl] Failed to init ~s: ~p", [Mod, Error]), From 1007105b5710dda931225461f9a52976ad880b3f Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Thu, 20 Dec 2018 22:29:31 +0800 Subject: [PATCH 506/520] Delete metrics test in session test suite --- test/emqx_session_SUITE.erl | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index 2e597340d..b8e0aedd3 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -57,17 +57,6 @@ t_session_all(_) -> emqx_session:publish(SPid, 1, Message1), timer:sleep(200), {publish, 1, _} = emqx_mock_client:get_last_message(ConnPid), - emqx_session:puback(SPid, 2), - emqx_session:puback(SPid, 3, reasoncode), - emqx_session:pubrec(SPid, 4), - emqx_session:pubrec(SPid, 5, reasoncode), - emqx_session:pubrel(SPid, 6, reasoncode), - emqx_session:pubcomp(SPid, 7, reasoncode), - timer:sleep(200), - 2 = emqx_metrics:val('packets/puback/missed'), - 2 = emqx_metrics:val('packets/pubrec/missed'), - 1 = emqx_metrics:val('packets/pubrel/missed'), - 1 = emqx_metrics:val('packets/pubcomp/missed'), Attrs = emqx_session:attrs(SPid), Info = emqx_session:info(SPid), Stats = emqx_session:stats(SPid), From 367b717c4013ea22b2e8401fd43b95ea69bee842 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 20 Dec 2018 22:42:18 +0800 Subject: [PATCH 507/520] Implement a new 'emqx_gc' module (#2090) Update connection/session module to using the new emqx_gc API --- src/emqx_connection.erl | 42 +++++++++--------- src/emqx_gc.erl | 97 ++++++++++++++++++++++------------------- src/emqx_session.erl | 27 ++++++++---- 3 files changed, 92 insertions(+), 74 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 2cf647beb..ddb7e6a85 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -40,10 +40,10 @@ active_n, proto_state, parser_state, + gc_state, keepalive, enable_stats, stats_timer, - incoming, rate_limit, pub_limit, limit_timer, @@ -138,6 +138,8 @@ init([Transport, RawSocket, Options]) -> peercert => Peercert, sendfun => SendFun}, Options), ParserState = emqx_protocol:parser(ProtoState), + GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), + GcState = emqx_gc:init(GcPolicy), State = run_socket(#state{transport = Transport, socket = Socket, peername = Peername, @@ -147,11 +149,10 @@ init([Transport, RawSocket, Options]) -> pub_limit = PubLimit, proto_state = ProtoState, parser_state = ParserState, + gc_state = GcState, enable_stats = EnableStats, idle_timeout = IdleTimout }), - GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), - ok = emqx_gc:init(GcPolicy), ok = emqx_misc:init_proc_mng_policy(Zone), emqx_logger:set_metadata_peername(esockd_net:format(Peername)), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], @@ -205,9 +206,8 @@ handle_cast(Msg, State) -> handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> case emqx_protocol:deliver(PubOrAck, ProtoState) of {ok, ProtoState1} -> - State1 = ensure_stats_timer(State#state{proto_state = ProtoState1}), - ok = maybe_gc(State1, PubOrAck), - {noreply, State1}; + State1 = State#state{proto_state = ProtoState1}, + {noreply, maybe_gc(PubOrAck, ensure_stats_timer(State1))}; {error, Reason} -> shutdown(Reason, State) end; @@ -247,11 +247,10 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> handle_info({tcp, _Sock, Data}, State) -> ?LOG(debug, "RECV ~p", [Data]), - Size = iolist_size(Data), - emqx_metrics:trans(inc, 'bytes/received', Size), - emqx_pd:update_counter(incoming_bytes, Size), - Incoming = #{bytes => Size, packets => 0}, - handle_packet(Data, State#state{incoming = Incoming}); + Oct = iolist_size(Data), + emqx_pd:update_counter(incoming_bytes, Oct), + emqx_metrics:trans(inc, 'bytes/received', Oct), + handle_packet(Data, maybe_gc({1, Oct}, State)); %% Rate limit here, cool:) handle_info({tcp_passive, _Sock}, State) -> @@ -325,9 +324,7 @@ code_change(_OldVsn, State, _Extra) -> %% Receive and parse data handle_packet(<<>>, State) -> - NState = ensure_stats_timer(State), - ok = maybe_gc(NState, incoming), - {noreply, NState}; + {noreply, ensure_stats_timer(State)}; handle_packet(Data, State = #state{proto_state = ProtoState, parser_state = ParserState, @@ -407,14 +404,15 @@ shutdown(Reason, State) -> stop(Reason, State) -> {stop, Reason, State}. -%% For incoming messages, bump gc-stats with packet count and totoal volume -%% For outgoing messages, only 'publish' type is taken into account. -maybe_gc(#state{incoming = #{bytes := Oct, packets := Cnt}}, incoming) -> - ok = emqx_gc:inc(Cnt, Oct); -maybe_gc(#state{}, {publish, _PacketId, #message{payload = Payload}}) -> +maybe_gc(_, State = #state{gc_state = undefined}) -> + State; +maybe_gc({publish, _PacketId, #message{payload = Payload}}, State) -> Oct = iolist_size(Payload), - ok = emqx_gc:inc(1, Oct); -maybe_gc(_, _) -> - ok. + maybe_gc({1, 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. diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 7e98eb37a..d608954a0 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -21,74 +21,83 @@ -module(emqx_gc). --export([init/1, inc/2, reset/0]). +-export([init/1, run/3, info/1, reset/1]). --type st() :: #{ cnt => {integer(), integer()} - , oct => {integer(), integer()} - }. +-type(opts() :: #{count => integer(), + bytes => integer()}). + +-type(st() :: #{cnt => {integer(), integer()}, + oct => {integer(), integer()}}). + +-type(gc_state() :: {?MODULE, st()}). -define(disabled, disabled). -define(ENABLED(X), (is_integer(X) andalso X > 0)). -%% @doc Initialize force GC parameters. --spec init(false | map()) -> ok. +%% @doc Initialize force GC state. +-spec(init(opts() | false) -> gc_state() | undefined). init(#{count := Count, bytes := Bytes}) -> Cnt = [{cnt, {Count, Count}} || ?ENABLED(Count)], Oct = [{oct, {Bytes, Bytes}} || ?ENABLED(Bytes)], - erlang:put(?MODULE, maps:from_list(Cnt ++ Oct)), - ok; -init(_) -> erlang:put(?MODULE, #{}), ok. + {?MODULE, maps:from_list(Cnt ++ Oct)}; +init(false) -> undefined. -%% @doc Increase count and bytes stats in one call, -%% ensure gc is triggered at most once, even if both thresholds are hit. --spec inc(pos_integer(), pos_integer()) -> ok. -inc(Cnt, Oct) -> - mutate_pd_with(fun(St) -> inc(St, Cnt, Oct) end). +%% @doc Try to run GC based on reduntions of count or bytes. +-spec(run(pos_integer(), pos_integer(), gc_state()) -> {boolean(), gc_state()}). +run(Cnt, Oct, {?MODULE, St}) -> + {Res, St1} = run([{cnt, Cnt}, {oct, Oct}], St), + {Res, {?MODULE, St1}}; +run(_Cnt, _Oct, undefined) -> + {false, undefined}. -%% @doc Reset counters to zero. --spec reset() -> ok. -reset() -> - mutate_pd_with(fun(St) -> reset(St) end). - -%% ======== Internals ======== - -%% mutate gc stats numbers in process dict with the given function -mutate_pd_with(F) -> - St = F(erlang:get(?MODULE)), - erlang:put(?MODULE, St), - ok. - -%% Increase count and bytes stats in one call, -%% ensure gc is triggered at most once, even if both thresholds are hit. --spec inc(st(), pos_integer(), pos_integer()) -> st(). -inc(St0, Cnt, Oct) -> - case do_inc(St0, cnt, Cnt) of - {true, St} -> - St; +run([], St) -> + {false, St}; +run([{K, N}|T], St) -> + case dec(K, N, St) of + {true, St1} -> + {true, do_gc(St1)}; {false, St1} -> - {_, St} = do_inc(St1, oct, Oct), - St + run(T, St1) end. -%% Reset counters to zero. -reset(St) -> reset(cnt, reset(oct, St)). +%% @doc Info of GC state. +-spec(info(gc_state()) -> map() | undefined). +info({?MODULE, St}) -> + St; +info(undefined) -> + undefined. --spec do_inc(st(), cnt | oct, pos_integer()) -> {boolean(), st()}. -do_inc(St, Key, Num) -> +%% @doc Reset counters to zero. +-spec(reset(gc_state()) -> gc_state()). +reset({?MODULE, St}) -> + {?MODULE, do_reset(St)}; +reset(undefined) -> + undefined. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +-spec(dec(cnt | oct, pos_integer(), st()) -> {boolean(), st()}). +dec(Key, Num, St) -> case maps:get(Key, St, ?disabled) of ?disabled -> {false, St}; {Init, Remain} when Remain > Num -> {false, maps:put(Key, {Init, Remain - Num}, St)}; _ -> - {true, do_gc(St)} + {true, St} end. do_gc(St) -> - erlang:garbage_collect(), - reset(St). + true = erlang:garbage_collect(), + do_reset(St). -reset(Key, St) -> +do_reset(St) -> + do_reset(cnt, do_reset(oct, St)). + +%% Reset counters to zero. +do_reset(Key, St) -> case maps:get(Key, St, ?disabled) of ?disabled -> St; {Init, _} -> maps:put(Key, {Init, Init}, St) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index e9bc0ff98..f4dbc2b23 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -144,6 +144,9 @@ %% Enqueue stats enqueue_stats = 0, + %% GC State + gc_state, + %% Created at created_at :: erlang:timestamp(), @@ -344,6 +347,7 @@ init([Parent, #{zone := Zone, 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{idle_timeout = IdleTimout, clean_start = CleanStart, @@ -364,6 +368,7 @@ init([Parent, #{zone := Zone, enable_stats = get_env(Zone, enable_stats, true), deliver_stats = 0, enqueue_stats = 0, + gc_state = emqx_gc:init(GcPolicy), created_at = os:timestamp(), will_msg = WillMsg }, @@ -371,8 +376,6 @@ init([Parent, #{zone := Zone, true = emqx_sm:set_session_attrs(ClientId, attrs(State)), true = emqx_sm:set_session_stats(ClientId, stats(State)), emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), - GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), - ok = emqx_gc:init(GcPolicy), 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). @@ -605,7 +608,9 @@ handle_info({timeout, Timer, check_awaiting_rel}, State = #state{await_rel_timer noreply(ensure_stats_timer(expire_awaiting_rel(State1))); handle_info({timeout, Timer, emit_stats}, - State = #state{client_id = ClientId, stats_timer = Timer}) -> + State = #state{client_id = ClientId, + stats_timer = Timer, + gc_state = GcState}) -> emqx_metrics:commit(), _ = emqx_sm:set_session_stats(ClientId, stats(State)), NewState = State#state{stats_timer = undefined}, @@ -614,8 +619,9 @@ handle_info({timeout, Timer, emit_stats}, continue -> {noreply, NewState}; hibernate -> - ok = emqx_gc:reset(), %% going to hibernate, reset gc stats - {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 due to ~p", [Reason], NewState), shutdown(Reason, NewState) @@ -991,9 +997,8 @@ next_pkt_id(State = #state{next_pkt_id = Id}) -> %% Inc stats inc_stats(deliver, Msg, State = #state{deliver_stats = I}) -> - MsgSize = msg_size(Msg), - ok = emqx_gc:inc(1, MsgSize), - State#state{deliver_stats = I + 1}; + State1 = maybe_gc({1, msg_size(Msg)}, State), + State1#state{deliver_stats = I + 1}; inc_stats(enqueue, _Msg, State = #state{enqueue_stats = I}) -> State#state{enqueue_stats = I + 1}. @@ -1018,3 +1023,9 @@ noreply(State) -> shutdown(Reason, State) -> {stop, Reason, State}. +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}. + From c93d0fb17426fb39502d071bc98031d53ef2b97e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 21 Dec 2018 10:16:45 +0800 Subject: [PATCH 508/520] Add test cases for emqx_gc module --- Makefile | 2 +- test/emqx_gc_SUITE.erl | 57 ++++++++++++++++++++++++++++++++++++++++++ test/emqx_gc_tests.erl | 53 --------------------------------------- 3 files changed, 58 insertions(+), 54 deletions(-) create mode 100644 test/emqx_gc_SUITE.erl delete mode 100644 test/emqx_gc_tests.erl diff --git a/Makefile b/Makefile index 374b74fb7..758e93a05 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_session \ emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_mountpoint \ emqx_listeners emqx_protocol emqx_pool emqx_shared_sub emqx_bridge \ - emqx_hooks emqx_batch emqx_sequence emqx_pmon emqx_pd + emqx_hooks emqx_batch emqx_sequence emqx_pmon emqx_pd emqx_gc CT_NODE_NAME = emqxct@127.0.0.1 CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME) diff --git a/test/emqx_gc_SUITE.erl b/test/emqx_gc_SUITE.erl new file mode 100644 index 000000000..22d7cd584 --- /dev/null +++ b/test/emqx_gc_SUITE.erl @@ -0,0 +1,57 @@ +%% Copyright (c) 2018 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_gc_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> + [t_init, t_run, t_info, t_reset]. + +t_init(_) -> + ?assertEqual(undefined, emqx_gc:init(false)), + GC1 = emqx_gc:init(#{count => 10, bytes => 0}), + ?assertEqual(#{cnt => {10, 10}}, emqx_gc:info(GC1)), + GC2 = emqx_gc:init(#{count => 0, bytes => 10}), + ?assertEqual(#{oct => {10, 10}}, emqx_gc:info(GC2)), + GC3 = emqx_gc:init(#{count => 10, bytes => 10}), + ?assertEqual(#{cnt => {10, 10}, oct => {10, 10}}, emqx_gc:info(GC3)). + +t_run(_) -> + 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)), + {false, GC1} = emqx_gc:run(1, 1, GC), + ?assertEqual(#{cnt => {10, 9}, oct => {10, 9}}, emqx_gc:info(GC1)), + {false, GC2} = emqx_gc:run(2, 2, GC1), + ?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)). + +t_info(_) -> + ?assertEqual(undefined, emqx_gc:info(undefined)), + GC = emqx_gc:init(#{count => 10, bytes => 0}), + ?assertEqual(#{cnt => {10, 10}}, emqx_gc:info(GC)). + +t_reset(_) -> + ?assertEqual(undefined, emqx_gc:reset(undefined)), + 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)). + diff --git a/test/emqx_gc_tests.erl b/test/emqx_gc_tests.erl deleted file mode 100644 index ffcac91d1..000000000 --- a/test/emqx_gc_tests.erl +++ /dev/null @@ -1,53 +0,0 @@ -%% Copyright (c) 2018 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_gc_tests). - --include_lib("eunit/include/eunit.hrl"). - -trigger_by_cnt_test() -> - Args = #{count => 2, bytes => 0}, - ok = emqx_gc:init(Args), - ok = emqx_gc:inc(1, 1000), - St1 = inspect(), - ?assertMatch({_, Remain} when Remain > 0, maps:get(cnt, St1)), - ok = emqx_gc:inc(2, 2), - St2 = inspect(), - ok = emqx_gc:inc(0, 2000), - St3 = inspect(), - ?assertEqual(St2, St3), - ?assertMatch({N, N}, maps:get(cnt, St2)), - ?assertNot(maps:is_key(oct, St2)), - ok. - -trigger_by_oct_test() -> - Args = #{count => 2, bytes => 2}, - ok = emqx_gc:init(Args), - ok = emqx_gc:inc(1, 1), - St1 = inspect(), - ?assertMatch({_, Remain} when Remain > 0, maps:get(oct, St1)), - ok = emqx_gc:inc(2, 2), - St2 = inspect(), - ?assertMatch({N, N}, maps:get(oct, St2)), - ?assertMatch({M, M}, maps:get(cnt, St2)), - ok. - -disabled_test() -> - Args = #{count => -1, bytes => false}, - ok = emqx_gc:init(Args), - ok = emqx_gc:inc(1, 1), - ?assertEqual(#{}, inspect()), - ok. - -inspect() -> erlang:get(emqx_gc). From bf7f10ecd140bfd0ac1b0d2f3af96477291c3d99 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 21 Dec 2018 10:49:03 +0800 Subject: [PATCH 509/520] Add will topic validation and acl check (#2075) * Add will topic validation and acl check --- src/emqx_protocol.erl | 72 ++++++++++++++++++++++++------------ src/emqx_reason_codes.erl | 1 + test/emqx_protocol_SUITE.erl | 40 +++++++++++++++++++- 3 files changed, 88 insertions(+), 25 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 0bedb0927..1f9d19094 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -290,7 +290,7 @@ process_packet(?CONNECT_PACKET( properties = ConnProps, client_id = ClientId, username = Username, - password = Password} = Connect), PState) -> + password = Password} = ConnPkt), PState) -> NewClientId = maybe_use_username_as_clientid(ClientId, Username, PState), @@ -298,7 +298,6 @@ process_packet(?CONNECT_PACKET( %% TODO: Mountpoint... %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) - WillMsg = make_will_msg(Connect), PState1 = set_username(Username, PState#pstate{client_id = NewClientId, @@ -307,16 +306,16 @@ process_packet(?CONNECT_PACKET( clean_start = CleanStart, keepalive = Keepalive, conn_props = ConnProps, - will_msg = WillMsg, is_bridge = IsBridge, connected_at = os:timestamp()}), connack( - case check_connect(Connect, PState1) of + case check_connect(ConnPkt, PState1) of {ok, PState2} -> case authenticate(credentials(PState2), Password) of {ok, IsSuper} -> %% Maybe assign a clientId - PState3 = maybe_assign_client_id(PState2#pstate{is_super = IsSuper}), + PState3 = maybe_assign_client_id(PState2#pstate{is_super = IsSuper, + will_msg = make_will_msg(ConnPkt)}), emqx_logger:set_metadata_client_id(PState3#pstate.client_id), %% Open session case try_open_session(PState3) of @@ -719,14 +718,16 @@ get_property(Name, Props, Default) -> maps:get(Name, Props, Default). make_will_msg(#mqtt_packet_connect{proto_ver = ProtoVer, - will_props = WillProps} = Connect) -> - emqx_packet:will_msg(if - ProtoVer =:= ?MQTT_PROTO_V5 -> - WillDelayInterval = get_property('Will-Delay-Interval', WillProps, 0), - Connect#mqtt_packet_connect{will_props = set_property('Will-Delay-Interval', WillDelayInterval, WillProps)}; - true -> - Connect - end). + 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 @@ -735,7 +736,8 @@ make_will_msg(#mqtt_packet_connect{proto_ver = ProtoVer, check_connect(Packet, PState) -> run_check_steps([fun check_proto_ver/2, fun check_client_id/2, - fun check_banned/2], Packet, PState). + fun check_banned/2, + fun check_will_topic/2], Packet, PState). check_proto_ver(#mqtt_packet_connect{proto_ver = Ver, proto_name = Name}, _PState) -> @@ -766,7 +768,7 @@ check_client_id(#mqtt_packet_connect{client_id = ClientId}, #pstate{zone = Zone} false -> {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID} end. -check_banned(_Connect, #pstate{enable_ban = false}) -> +check_banned(_ConnPkt, #pstate{enable_ban = false}) -> ok; check_banned(#mqtt_packet_connect{client_id = ClientId, username = Username}, #pstate{peername = Peername}) -> @@ -777,6 +779,26 @@ check_banned(#mqtt_packet_connect{client_id = ClientId, username = Username}, 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) -> + try emqx_topic:validate(WillTopic) of + true -> check_will_acl(ConnPkt, PState) + catch error : _Error -> + {error, ?RC_TOPIC_NAME_INVALID} + end. + +check_will_acl(_ConnPkt, #pstate{enable_acl = EnableAcl}) + when not EnableAcl -> + ok; +check_will_acl(#mqtt_packet_connect{will_topic = WillTopic}, PState) -> + case emqx_access_control:check_acl(credentials(PState), publish, WillTopic) of + allow -> ok; + deny -> + ?LOG(warning, "Cannot publish will message to ~p for acl checking failed", [WillTopic]), + {error, ?RC_UNSPECIFIED_ERROR} + end. + check_publish(Packet, PState) -> run_check_steps([fun check_pub_caps/2, fun check_pub_acl/2], Packet, PState). @@ -902,25 +924,29 @@ flag(true) -> 1. %% Execute actions in case acl deny do_acl_deny_action(?PUBLISH_PACKET(?QOS_0, _Topic, _PacketId, _Payload), - ?RC_NOT_AUTHORIZED, PState = #pstate{acl_deny_action = disconnect}) -> - {error, ?RC_NOT_AUTHORIZED, PState}; + ?RC_NOT_AUTHORIZED, PState = #pstate{proto_ver = ProtoVer, + acl_deny_action = disconnect}) -> + {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; do_acl_deny_action(?PUBLISH_PACKET(?QOS_1, _Topic, _PacketId, _Payload), - ?RC_NOT_AUTHORIZED, PState = #pstate{acl_deny_action = disconnect}) -> + ?RC_NOT_AUTHORIZED, PState = #pstate{proto_ver = ProtoVer, + acl_deny_action = disconnect}) -> deliver({disconnect, ?RC_NOT_AUTHORIZED}, PState), - {error, ?RC_NOT_AUTHORIZED, PState}; + {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; do_acl_deny_action(?PUBLISH_PACKET(?QOS_2, _Topic, _PacketId, _Payload), - ?RC_NOT_AUTHORIZED, PState = #pstate{acl_deny_action = disconnect}) -> + ?RC_NOT_AUTHORIZED, PState = #pstate{proto_ver = ProtoVer, + acl_deny_action = disconnect}) -> deliver({disconnect, ?RC_NOT_AUTHORIZED}, PState), - {error, ?RC_NOT_AUTHORIZED, PState}; + {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; do_acl_deny_action(?SUBSCRIBE_PACKET(_PacketId, _Properties, _RawTopicFilters), - ReasonCodes, PState = #pstate{acl_deny_action = disconnect}) -> + ReasonCodes, PState = #pstate{proto_ver = ProtoVer, + acl_deny_action = disconnect}) -> case lists:member(?RC_NOT_AUTHORIZED, ReasonCodes) of true -> deliver({disconnect, ?RC_NOT_AUTHORIZED}, PState), - {error, ?RC_NOT_AUTHORIZED, PState}; + {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; false -> {ok, PState} end; diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index 3dc73a5d5..c6bdd849d 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -133,6 +133,7 @@ compat(connack, 16#89) -> ?CONNACK_SERVER; compat(connack, 16#8A) -> ?CONNACK_AUTH; compat(connack, 16#8B) -> ?CONNACK_SERVER; compat(connack, 16#8C) -> ?CONNACK_AUTH; +compat(connack, 16#90) -> ?CONNACK_SERVER; compat(connack, 16#97) -> ?CONNACK_SERVER; compat(connack, 16#9C) -> ?CONNACK_SERVER; compat(connack, 16#9D) -> ?CONNACK_SERVER; diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl index 387be3053..38d1001d3 100644 --- a/test/emqx_protocol_SUITE.erl +++ b/test/emqx_protocol_SUITE.erl @@ -35,13 +35,17 @@ all() -> [ + {group, mqtt_common}, {group, mqttv4}, {group, mqttv5}, {group, acl} ]. groups() -> - [{mqttv4, + [{mqtt_common, + [sequence], + [will_check]}, + {mqttv4, [sequence], [connect_v4, subscribe_v4]}, @@ -53,7 +57,6 @@ groups() -> [sequence], [acl_deny_action]}]. - init_per_suite(Config) -> [start_apps(App, SchemaFile, ConfigFile) || {App, SchemaFile, ConfigFile} @@ -436,6 +439,39 @@ acl_deny_action(_) -> emqx_zone:set_env(external, acl_deny_action, ignore), ok. +will_check(_) -> + process_flag(trap_exit, true), + will_topic_check(0), + will_acl_check(0). + +will_topic_check(QoS) -> + {ok, Client} = emqx_client:start_link([{username, <<"emqx">>}, + {will_flag, true}, + {will_topic, <<"">>}, + {will_payload, <<"I have died">>}, + {will_qos, QoS}]), + try emqx_client:connect(Client) of + _ -> + ok + catch + exit : _Reason -> + false = is_process_alive(Client) + end. + +will_acl_check(QoS) -> + {ok, Client} = emqx_client:start_link([{username, <<"emqx">>}, + {will_flag, true}, + {will_topic, <<"acl_deny_action">>}, + {will_payload, <<"I have died">>}, + {will_qos, QoS}]), + try emqx_client:connect(Client) of + _ -> + ok + catch + exit : _Reason -> + false = is_process_alive(Client) + end. + acl_deny_do_disconnect(publish, QoS, Topic) -> {ok, Client} = emqx_client:start_link([{username, <<"emqx">>}]), {ok, _} = emqx_client:connect(Client), From 4f84a31d0283482af54d4b13aa27557b9e006541 Mon Sep 17 00:00:00 2001 From: turtleDeng Date: Fri, 21 Dec 2018 11:06:50 +0800 Subject: [PATCH 510/520] Update copyright (#2093) --- src/emqx_cm_sup.erl | 1 - src/emqx_mqueue.erl | 3 ++- src/emqx_plugins.erl | 28 +++++++++++++--------------- src/emqx_rate_limiter.erl | 28 +++++++++++++--------------- src/emqx_sm_sup.erl | 1 - src/emqx_vm.erl | 4 +--- 6 files changed, 29 insertions(+), 36 deletions(-) diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index 19dd9cb50..e8c99c16b 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -1,6 +1,5 @@ %% Copyright (c) 2018 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 diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index 56390d412..48b0fa439 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -5,7 +5,8 @@ %% 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 +%% +%% 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 diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index 44585a69e..b0f6456dc 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. -module(emqx_plugins). diff --git a/src/emqx_rate_limiter.erl b/src/emqx_rate_limiter.erl index 76eaf0c61..c8145dbbc 100644 --- a/src/emqx_rate_limiter.erl +++ b/src/emqx_rate_limiter.erl @@ -1,18 +1,16 @@ -%%%------------------------------------------------------------------- -%%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io) -%%% -%%% 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. -%%%------------------------------------------------------------------- +%% Copyright (c) 2018 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_rate_limiter). diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl index 317cd11db..ed491c67f 100644 --- a/src/emqx_sm_sup.erl +++ b/src/emqx_sm_sup.erl @@ -1,6 +1,5 @@ %% Copyright (c) 2018 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 diff --git a/src/emqx_vm.erl b/src/emqx_vm.erl index bf6388232..74b815795 100644 --- a/src/emqx_vm.erl +++ b/src/emqx_vm.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. +%% Copyright (c) 2018 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,7 +11,6 @@ %% 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). From 3fec9cdf0a792ad113cac237d80313b3bf8362c6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 21 Dec 2018 11:56:22 +0800 Subject: [PATCH 511/520] Try to simulate a '{ssl_passive, Sock}' message:( --- src/emqx_connection.erl | 54 +++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index ddb7e6a85..1714632a8 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -214,8 +214,8 @@ handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> handle_info({timeout, Timer, emit_stats}, State = #state{stats_timer = Timer, - proto_state = ProtoState - }) -> + proto_state = ProtoState, + gc_state = GcState}) -> emqx_metrics:commit(), emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), NewState = State#state{stats_timer = undefined}, @@ -224,8 +224,9 @@ handle_info({timeout, Timer, emit_stats}, continue -> {noreply, NewState}; hibernate -> - ok = emqx_gc:reset(), - {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 due to ~p", [Reason]), shutdown(Reason, NewState) @@ -246,22 +247,29 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> shutdown(conflict, State); handle_info({tcp, _Sock, Data}, State) -> - ?LOG(debug, "RECV ~p", [Data]), - Oct = iolist_size(Data), - emqx_pd:update_counter(incoming_bytes, Oct), - emqx_metrics:trans(inc, 'bytes/received', Oct), - handle_packet(Data, maybe_gc({1, Oct}, State)); + process_incoming(Data, State); +%% FIXME Later +handle_info({ssl, _Sock, Data}, State) -> + process_incoming(Data, run_socket(State)); %% Rate limit here, cool:) handle_info({tcp_passive, _Sock}, State) -> {noreply, run_socket(ensure_rate_limit(State))}; +%% FIXME Later +handle_info({ssl_passive, _Sock}, State) -> + {noreply, run_socket(ensure_rate_limit(State))}; handle_info({tcp_error, _Sock, Reason}, State) -> shutdown(Reason, State); +handle_info({ssl_error, _Sock, Reason}, State) -> + shutdown(Reason, State); handle_info({tcp_closed, _Sock}, State) -> shutdown(closed, State); +handle_info({ssl_closed, _Sock}, State) -> + shutdown(closed, State); +%% Rate limit timer handle_info(activate_sock, State) -> {noreply, run_socket(State#state{conn_state = running, limit_timer = undefined})}; @@ -319,12 +327,24 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%------------------------------------------------------------------------------ -%% Parse and handle packets +%% Internals: process incoming, parse and handle packets %%------------------------------------------------------------------------------ -%% Receive and parse data +process_incoming(Data, State) -> + Oct = iolist_size(Data), + ?LOG(debug, "RECV ~p", [Data]), + emqx_pd:update_counter(incoming_bytes, Oct), + emqx_metrics:trans(inc, 'bytes/received', Oct), + case handle_packet(Data, State) of + {noreply, State1} -> + State2 = maybe_gc({1, Oct}, State1), + {noreply, ensure_stats_timer(State2)}; + Shutdown -> Shutdown + end. + +%% Parse and handle packets handle_packet(<<>>, State) -> - {noreply, ensure_stats_timer(State)}; + {noreply, State}; handle_packet(Data, State = #state{proto_state = ProtoState, parser_state = ParserState, @@ -384,8 +404,8 @@ run_socket(State = #state{conn_state = blocked}) -> State; run_socket(State = #state{transport = Transport, socket = Socket, - active_n = ActiveN}) -> - Transport:setopts(Socket, [{active, ActiveN}]), + active_n = N}) -> + ensure_ok_or_exit(Transport:setopts(Socket, [{active, N}])), State. %%------------------------------------------------------------------------------ @@ -393,7 +413,7 @@ run_socket(State = #state{transport = Transport, %%------------------------------------------------------------------------------ ensure_stats_timer(State = #state{enable_stats = true, - stats_timer = undefined, + stats_timer = undefined, idle_timeout = IdleTimeout}) -> State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; ensure_stats_timer(State) -> State. @@ -415,4 +435,8 @@ maybe_gc({Cnt, Oct}, State = #state{gc_state = GCSt}) -> maybe_gc(_, State) -> State. +ensure_ok_or_exit(ok) -> + ok; +ensure_ok_or_exit({error, Reason}) -> + self() ! {shutdown, Reason}. From 31bf01fd7ab11240dd74c7cd85b6470add3c16d3 Mon Sep 17 00:00:00 2001 From: tigercl Date: Fri, 21 Dec 2018 14:01:21 +0800 Subject: [PATCH 512/520] Fix bug in topic alias maximum (#2074) * Fix bug in topic alias maximum --- src/emqx_protocol.erl | 133 +++++++++++++++++++++-------------- test/emqx_protocol_SUITE.erl | 77 ++++++++++++++++++++ 2 files changed, 159 insertions(+), 51 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 8f845a94e..d3e0e9f97 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -64,7 +64,8 @@ send_stats, connected, connected_at, - ignore_loop + ignore_loop, + topic_alias_maximum }). -type(state() :: #pstate{}). @@ -84,28 +85,29 @@ -spec(init(map(), list()) -> state()). init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) -> Zone = proplists:get_value(zone, Options), - #pstate{zone = Zone, - sendfun = SendFun, - 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), - is_super = false, - clean_start = false, - topic_aliases = #{}, - packet_size = emqx_zone:get_env(Zone, max_packet_size), - mountpoint = emqx_zone:get_env(Zone, mountpoint), - is_bridge = false, - enable_ban = emqx_zone:get_env(Zone, enable_ban, false), - enable_acl = emqx_zone:get_env(Zone, enable_acl), - recv_stats = #{msg => 0, pkt => 0}, - send_stats = #{msg => 0, pkt => 0}, - connected = false, - ignore_loop = emqx_config:get_env(mqtt_ignore_loop_deliver, false)}. + #pstate{zone = Zone, + sendfun = SendFun, + 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), + is_super = false, + clean_start = false, + topic_aliases = #{}, + packet_size = emqx_zone:get_env(Zone, max_packet_size), + mountpoint = emqx_zone:get_env(Zone, mountpoint), + is_bridge = false, + enable_ban = emqx_zone:get_env(Zone, enable_ban, false), + enable_acl = emqx_zone:get_env(Zone, enable_acl), + recv_stats = #{msg => 0, pkt => 0}, + send_stats = #{msg => 0, pkt => 0}, + connected = false, + ignore_loop = emqx_config:get_env(mqtt_ignore_loop_deliver, false), + topic_alias_maximum = #{to_client => 0, from_client => 0}}. init_username(Peercert, Options) -> case proplists:get_value(peer_cert_as_username, Options) of @@ -212,12 +214,16 @@ received(?PACKET(?CONNECT), PState = #pstate{connected = true}) -> {error, proto_unexpected_connect, PState}; received(Packet = ?PACKET(Type), PState) -> - PState1 = set_protover(Packet, PState), + PState1 = set_protover(Packet, PState), trace(recv, Packet), try emqx_packet:validate(Packet) of true -> - {Packet1, PState2} = preprocess_properties(Packet, PState1), - process_packet(Packet1, inc_stats(recv, Type, PState2)) + case preprocess_properties(Packet, PState1) of + {error, ReasonCode} -> + {error, ReasonCode, PState1}; + {Packet1, PState2} -> + process_packet(Packet1, inc_stats(recv, Type, PState2)) + end catch error : protocol_error -> deliver({disconnect, ?RC_PROTOCOL_ERROR}, PState1), @@ -242,6 +248,13 @@ received(Packet = ?PACKET(Type), PState) -> %%------------------------------------------------------------------------------ %% Preprocess MQTT Properties %%------------------------------------------------------------------------------ +preprocess_properties(Packet = #mqtt_packet{ + variable = #mqtt_packet_connect{ + properties = #{'Topic-Alias-Maximum' := ToClient} + } + }, + PState = #pstate{topic_alias_maximum = TopicAliasMaximum}) -> + {Packet, PState#pstate{topic_alias_maximum = TopicAliasMaximum#{to_client => ToClient}}}; %% Subscription Identifier preprocess_properties(Packet = #mqtt_packet{ @@ -255,22 +268,46 @@ preprocess_properties(Packet = #mqtt_packet{ {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}) -> - {Packet#mqtt_packet{variable = Publish#mqtt_packet_publish{ - topic_name = maps:get(AliasId, Aliases, <<>>)}}, PState}; + PState = #pstate{proto_ver = ?MQTT_PROTO_V5, + topic_aliases = Aliases, + topic_alias_maximum = #{from_client := TopicAliasMaximum}}) -> + case AliasId =< TopicAliasMaximum of + true -> + {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}) -> - {Packet, PState#pstate{topic_aliases = maps:put(AliasId, Topic, Aliases)}}; + 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 -> + {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) -> {Packet, PState}. @@ -278,7 +315,6 @@ preprocess_properties(Packet, PState) -> %%------------------------------------------------------------------------------ %% Process MQTT Packet %%------------------------------------------------------------------------------ - process_packet(?CONNECT_PACKET( #mqtt_packet_connect{proto_name = ProtoName, proto_ver = ProtoVer, @@ -308,6 +344,7 @@ process_packet(?CONNECT_PACKET( will_msg = WillMsg, is_bridge = IsBridge, connected_at = os:timestamp()}), + connack( case check_connect(Connect, PState1) of {ok, PState2} -> @@ -342,9 +379,6 @@ process_packet(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PSt case check_publish(Packet, PState) of {ok, PState1} -> do_publish(Packet, PState1); - {error, ?RC_TOPIC_ALIAS_INVALID} -> - ?LOG(error, "Protocol error - ~p", [?RC_TOPIC_ALIAS_INVALID]), - {error, ?RC_TOPIC_ALIAS_INVALID, PState}; {error, ReasonCode} -> ?LOG(warning, "Cannot publish qos0 message to ~s for ~s", [Topic, emqx_reason_codes:text(ReasonCode)]), @@ -523,7 +557,8 @@ deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, proto_ver = ?MQTT_PROTO_V5, client_id = ClientId, conn_props = ConnProps, - is_assigned = IsAssigned}) -> + is_assigned = IsAssigned, + topic_alias_maximum = TopicAliasMaximum}) -> ResponseInformation = case maps:find('Request-Response-Information', ConnProps) of {ok, 1} -> iolist_to_binary(emqx_config:get_env(response_topic_prefix)); @@ -561,17 +596,20 @@ deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, undefined -> Props2; Keepalive -> Props2#{'Server-Keep-Alive' => Keepalive} end, - send(?CONNACK_PACKET(?RC_SUCCESS, SP, Props3), PState); + + PState1 = PState#pstate{topic_alias_maximum = TopicAliasMaximum#{from_client => MaxAlias}}, + + send(?CONNACK_PACKET(?RC_SUCCESS, SP, Props3), PState1); deliver({connack, ReasonCode, SP}, PState) -> send(?CONNACK_PACKET(ReasonCode, SP), PState); -deliver({publish, PacketId, Msg}, PState = #pstate{mountpoint = MountPoint}) -> +deliver({publish, PacketId, Msg = #message{headers = Headers}}, PState = #pstate{mountpoint = MountPoint}) -> _ = emqx_hooks:run('message.delivered', [credentials(PState)], Msg), Msg1 = emqx_message:update_expiry(Msg), Msg2 = emqx_mountpoint:unmount(MountPoint, Msg1), - send(emqx_packet:from_message(PacketId, Msg2), PState); - + send(emqx_packet:from_message(PacketId, Msg2#message{headers = maps:remove('Topic-Alias', Headers)}), PState); + deliver({puback, PacketId, ReasonCode}, PState) -> send(?PUBACK_PACKET(PacketId, ReasonCode), PState); @@ -758,18 +796,11 @@ check_publish(Packet, PState) -> run_check_steps([fun check_pub_caps/2, fun check_pub_acl/2], Packet, PState). -check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, - variable = #mqtt_packet_publish{ - properties = #{'Topic-Alias' := TopicAlias} - }}, - #pstate{zone = Zone}) -> - emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain, topic_alias => TopicAlias}); check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, variable = #mqtt_packet_publish{ properties = _Properties}}, #pstate{zone = Zone}) -> emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}). - check_pub_acl(_Packet, #pstate{is_super = IsSuper, enable_acl = EnableAcl}) when IsSuper orelse (not EnableAcl) -> ok; diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl index ae308ea42..27137d956 100644 --- a/test/emqx_protocol_SUITE.erl +++ b/test/emqx_protocol_SUITE.erl @@ -55,6 +55,7 @@ groups() -> init_per_suite(Config) -> emqx_ct_broker_helpers:run_setup_steps(), + emqx_zone:set_env(external, max_topic_alias, 20), Config. end_per_suite(_Config) -> @@ -154,6 +155,82 @@ connect_v5(_) -> #{'Response-Information' := _RespInfo}), _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) end), + + % topic alias = 0 + 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( + ?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, 0), + + {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]) -> From 10e5210581ea0097ebc0c28e811135c3a449d63c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 21 Dec 2018 15:39:24 +0800 Subject: [PATCH 513/520] Workaround ssl:setopts(SslSock, [{active, N}]) (#2095) * Set '{active, true}' for SSL socket --- src/emqx_connection.erl | 51 ++++++++++++++++++++------------------- test/emqx_stats_tests.erl | 4 +-- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 1714632a8..9f2572b32 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -246,11 +246,8 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> ?LOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid]), shutdown(conflict, State); -handle_info({tcp, _Sock, Data}, State) -> +handle_info({TcpOrSsL, _Sock, Data}, State) when TcpOrSsL =:= tcp; TcpOrSsL =:= ssl -> process_incoming(Data, State); -%% FIXME Later -handle_info({ssl, _Sock, Data}, State) -> - process_incoming(Data, run_socket(State)); %% Rate limit here, cool:) handle_info({tcp_passive, _Sock}, State) -> @@ -259,14 +256,10 @@ handle_info({tcp_passive, _Sock}, State) -> handle_info({ssl_passive, _Sock}, State) -> {noreply, run_socket(ensure_rate_limit(State))}; -handle_info({tcp_error, _Sock, Reason}, State) -> - shutdown(Reason, State); -handle_info({ssl_error, _Sock, Reason}, State) -> +handle_info({Err, _Sock, Reason}, State) when Err =:= tcp_error; Err =:= ssl_error -> shutdown(Reason, State); -handle_info({tcp_closed, _Sock}, State) -> - shutdown(closed, State); -handle_info({ssl_closed, _Sock}, State) -> +handle_info({Closed, _Sock}, State) when Closed =:= tcp_closed; Closed =:= ssl_closed -> shutdown(closed, State); %% Rate limit timer @@ -380,7 +373,6 @@ reset_parser(State = #state{proto_state = ProtoState}) -> %%------------------------------------------------------------------------------ %% Ensure rate limit -%%------------------------------------------------------------------------------ ensure_rate_limit(State = #state{rate_limit = Rl, pub_limit = Pl}) -> Limiters = [{Pl, #state.pub_limit, emqx_pd:reset_counter(incoming_pubs)}, @@ -400,17 +392,26 @@ ensure_rate_limit([{Rl, Pos, Cnt}|Limiters], State) -> setelement(Pos, State#state{conn_state = blocked, limit_timer = TRef}, Rl1) end. +%%------------------------------------------------------------------------------ +%% Activate socket + run_socket(State = #state{conn_state = blocked}) -> State; -run_socket(State = #state{transport = Transport, - socket = Socket, - active_n = N}) -> - ensure_ok_or_exit(Transport:setopts(Socket, [{active, N}])), + +run_socket(State = #state{transport = Transport, socket = Socket, active_n = N}) -> + TrueOrN = case Transport:is_ssl(Socket) of + true -> true; %% Cannot set '{active, N}' for SSL:( + false -> N + end, + ensure_ok_or_exit(Transport:setopts(Socket, [{active, TrueOrN}])), State. +ensure_ok_or_exit(ok) -> ok; +ensure_ok_or_exit({error, Reason}) -> + self() ! {shutdown, Reason}. + %%------------------------------------------------------------------------------ %% Ensure stats timer -%%------------------------------------------------------------------------------ ensure_stats_timer(State = #state{enable_stats = true, stats_timer = undefined, @@ -418,11 +419,8 @@ ensure_stats_timer(State = #state{enable_stats = true, State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; ensure_stats_timer(State) -> State. -shutdown(Reason, State) -> - stop({shutdown, Reason}, State). - -stop(Reason, State) -> - {stop, Reason, State}. +%%------------------------------------------------------------------------------ +%% Maybe GC maybe_gc(_, State = #state{gc_state = undefined}) -> State; @@ -435,8 +433,11 @@ maybe_gc({Cnt, Oct}, State = #state{gc_state = GCSt}) -> maybe_gc(_, State) -> State. -ensure_ok_or_exit(ok) -> - ok; -ensure_ok_or_exit({error, Reason}) -> - self() ! {shutdown, Reason}. +%%------------------------------------------------------------------------------ +%% Shutdown or stop +shutdown(Reason, State) -> + stop({shutdown, Reason}, State). + +stop(Reason, State) -> + {stop, Reason, State}. diff --git a/test/emqx_stats_tests.erl b/test/emqx_stats_tests.erl index dd9733a88..e8b5e82af 100644 --- a/test/emqx_stats_tests.erl +++ b/test/emqx_stats_tests.erl @@ -75,10 +75,10 @@ helper_test_() -> with_proc(fun() -> TestF(CbModule, CbFun) end, TickMs) end end, - [{"emqx_broker_helper", MkTestFun(emqx_broker_helper, stats_fun)}, + [{"emqx_broker", MkTestFun(emqx_broker, stats_fun)}, {"emqx_sm", MkTestFun(emqx_sm, stats_fun)}, {"emqx_router_helper", MkTestFun(emqx_router_helper, stats_fun)}, - {"emqx_cm", MkTestFun(emqx_cm, update_conn_stats)} + {"emqx_cm", MkTestFun(emqx_cm, stats_fun)} ]. with_proc(F) -> From e949e8cbd80ad41f305ea857b816c3feb95885e2 Mon Sep 17 00:00:00 2001 From: YoukiLin <1045735402@qq.com> Date: Fri, 21 Dec 2018 15:42:42 +0800 Subject: [PATCH 514/520] Add format output for test print (#2076) * Add formatted output for the test print --- src/emqx_cli.erl | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/emqx_cli.erl b/src/emqx_cli.erl index 6be9093b5..f7d513e9d 100644 --- a/src/emqx_cli.erl +++ b/src/emqx_cli.erl @@ -17,16 +17,17 @@ -export([print/1, print/2, usage/1, usage/2]). print(Msg) -> - io:format(Msg). + io:format(Msg), lists:flatten(io_lib:format("~p", [Msg])). print(Format, Args) -> - io:format(Format, Args). + io:format(Format, Args), lists:flatten(io_lib:format(Format, Args)). usage(CmdList) -> - lists:foreach( + lists:map( fun({Cmd, Descr}) -> - io:format("~-48s# ~s~n", [Cmd, Descr]) + io:format("~-48s# ~s~n", [Cmd, Descr]), + lists:flatten(io_lib:format("~-48s# ~s~n", [Cmd, Descr])) end, CmdList). usage(Format, Args) -> - usage([{Format, Args}]). \ No newline at end of file + usage([{Format, Args}]). From 390fae113412a55e2deccb1a076dabf26ba17e3b Mon Sep 17 00:00:00 2001 From: turtled Date: Fri, 21 Dec 2018 17:05:14 +0800 Subject: [PATCH 515/520] Rm clique dep --- Makefile | 5 ++--- rebar.config | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 758e93a05..69312a1c2 100644 --- a/Makefile +++ b/Makefile @@ -3,15 +3,14 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker -DEPS = jsx gproc gen_rpc ekka esockd cowboy clique +DEPS = jsx gproc gen_rpc ekka esockd cowboy dep_jsx = hex-emqx 2.9.0 dep_gproc = hex-emqx 0.8.0 dep_gen_rpc = git-emqx https://github.com/emqx/gen_rpc 2.3.0 -dep_esockd = git-emqx https://github.com/emqx/esockd v5.4.2 +dep_esockd = git-emqx https://github.com/emqx/esockd v5.4.3 dep_ekka = git-emqx https://github.com/emqx/ekka v0.5.1 dep_cowboy = hex-emqx 2.4.0 -dep_clique = git-emqx https://github.com/emqx/clique develop NO_AUTOPATCH = cuttlefish diff --git a/rebar.config b/rebar.config index c3baebadb..d3dabae81 100644 --- a/rebar.config +++ b/rebar.config @@ -7,8 +7,7 @@ {github_emqx_deps, [{gen_rpc, "2.3.0"}, {ekka, "v0.5.1"}, - {clique, "develop"}, - {esockd, "v5.4.2"}, + {esockd, "v5.4.3"}, {cuttlefish, "v2.2.0"} ]}. From f31e7f8bdedd6e70070784574adedeb168a9d977 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 21 Dec 2018 17:47:49 +0800 Subject: [PATCH 516/520] Replace put/2 with emqx_pd:update_counter/2 (#2098) --- src/emqx_ws_connection.erl | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 180edc8f0..0331be936 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -134,7 +134,6 @@ websocket_init(#state{request = Req, options = Options}) -> Zone = proplists:get_value(zone, Options), EnableStats = emqx_zone:get_env(Zone, enable_stats, true), IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), - lists:foreach(fun(Stat) -> put(Stat, 0) end, ?SOCK_STATS), emqx_logger:set_metadata_peername(esockd_net:format(Peername)), {ok, #state{peername = Peername, @@ -149,14 +148,14 @@ send_fun(WsPid) -> Data = emqx_frame:serialize(Packet, Options), BinSize = iolist_size(Data), emqx_metrics:trans(inc, 'bytes/sent', BinSize), - put(send_oct, get(send_oct) + BinSize), - put(send_cnt, get(send_cnt) + 1), + emqx_pd:update_counter(send_cnt, 1), + emqx_pd:update_counter(send_oct, BinSize), WsPid ! {binary, iolist_to_binary(Data)}, ok end. stat_fun() -> - fun() -> {ok, get(recv_oct)} end. + fun() -> {ok, emqx_pd:get_counter(recv_oct)} end. websocket_handle({binary, <<>>}, State) -> {ok, ensure_stats_timer(State)}; @@ -164,16 +163,16 @@ websocket_handle({binary, [<<>>]}, State) -> {ok, ensure_stats_timer(State)}; websocket_handle({binary, Data}, State = #state{parser_state = ParserState, proto_state = ProtoState}) -> - BinSize = iolist_size(Data), - put(recv_oct, get(recv_oct) + BinSize), ?LOG(debug, "RECV ~p", [Data]), + BinSize = iolist_size(Data), + emqx_pd:update_counter(recv_oct, BinSize), emqx_metrics:trans(inc, 'bytes/received', BinSize), - case catch emqx_frame:parse(iolist_to_binary(Data), ParserState) of - {more, NewParserState} -> - {ok, State#state{parser_state = NewParserState}}; + try emqx_frame:parse(iolist_to_binary(Data), ParserState) of + {more, ParserState1} -> + {ok, State#state{parser_state = ParserState1}}; {ok, Packet, Rest} -> emqx_metrics:received(Packet), - put(recv_cnt, get(recv_cnt) + 1), + emqx_pd:update_counter(recv_cnt, 1), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> websocket_handle({binary, Rest}, reset_parser(State#state{proto_state = ProtoState1})); @@ -187,9 +186,10 @@ websocket_handle({binary, Data}, State = #state{parser_state = ParserState, end; {error, Error} -> ?LOG(error, "Frame error: ~p", [Error]), - stop(Error, State); - {'EXIT', Reason} -> - ?LOG(error, "Frame error:~p~nFrame data: ~p", [Reason, Data]), + stop(Error, State) + catch + _:Error -> + ?LOG(error, "Frame error:~p~nFrame data: ~p", [Error, Data]), shutdown(parse_error, State) end. @@ -303,4 +303,5 @@ stop(Error, State) -> {stop, State#state{shutdown = Error}}. wsock_stats() -> - [{Key, get(Key)} || Key <- ?SOCK_STATS]. + [{Key, emqx_pd:get_counter(Key)} || Key <- ?SOCK_STATS]. + From bb45825e773145ec61c5dc21ba61f0169b17e441 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 21 Dec 2018 18:31:04 +0800 Subject: [PATCH 517/520] Inc deliver_stats, enqueue_stats with emqx_pd:update_counter/2 (#2100) --- src/emqx_session.erl | 61 +++++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 3f9fd1610..eac258bcf 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -138,12 +138,6 @@ %% Stats timer stats_timer :: reference() | undefined, - %% Deliver stats - deliver_stats = 0, - - %% Enqueue stats - enqueue_stats = 0, - %% GC State gc_state, @@ -224,9 +218,7 @@ stats(#state{max_subscriptions = MaxSubscriptions, inflight = Inflight, mqueue = MQueue, max_awaiting_rel = MaxAwaitingRel, - awaiting_rel = AwaitingRel, - deliver_stats = DeliverMsg, - enqueue_stats = EnqueueMsg}) -> + awaiting_rel = AwaitingRel}) -> lists:append(emqx_misc:proc_stats(), [{max_subscriptions, MaxSubscriptions}, {subscriptions_count, maps:size(Subscriptions)}, @@ -237,8 +229,8 @@ stats(#state{max_subscriptions = MaxSubscriptions, {mqueue_dropped, emqx_mqueue:dropped(MQueue)}, {max_awaiting_rel, MaxAwaitingRel}, {awaiting_rel_len, maps:size(AwaitingRel)}, - {deliver_msg, DeliverMsg}, - {enqueue_msg, EnqueueMsg}]). + {deliver_msg, emqx_pd:get_counter(deliver_stats)}, + {enqueue_msg, emqx_pd:get_counter(enqueue_stats)}]). %%------------------------------------------------------------------------------ %% PubSub API @@ -366,8 +358,6 @@ init([Parent, #{zone := Zone, max_awaiting_rel = get_env(Zone, max_awaiting_rel), expiry_interval = ExpiryInterval, enable_stats = get_env(Zone, enable_stats, true), - deliver_stats = 0, - enqueue_stats = 0, gc_state = emqx_gc:init(GcPolicy), created_at = os:timestamp(), will_msg = WillMsg @@ -593,7 +583,8 @@ handle_info({dispatch, Topic, Msg = #message{}}, State) -> ok = emqx_shared_sub:nack_no_connection(Msg), {noreply, State}; false -> - noreply(ensure_stats_timer(handle_dispatch(Topic, Msg, State))) + NewState = handle_dispatch(Topic, Msg, State), + noreply(ensure_stats_timer(maybe_gc({1, msg_size(Msg)}, NewState))) end; %% Do nothing if the client has been disconnected. @@ -834,30 +825,31 @@ run_dispatch_steps([{subid, SubId}|Steps], Msg, State) -> %% Enqueue message if the client has been disconnected dispatch(Msg, State = #state{client_id = ClientId, conn_pid = undefined}) -> case emqx_hooks:run('message.dropped', [#{client_id => ClientId}, Msg]) of - ok -> enqueue_msg(Msg, State); + ok -> enqueue_msg(Msg, State); stop -> State end; %% Deliver qos0 message directly to client dispatch(Msg = #message{qos = ?QOS_0} = Msg, State) -> - deliver(undefined, Msg, State), - inc_stats(deliver, Msg, State); + ok = deliver(undefined, Msg, State), + State; dispatch(Msg = #message{qos = QoS} = Msg, State = #state{next_pkt_id = PacketId, inflight = Inflight}) - when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> + when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> case emqx_inflight:is_full(Inflight) of true -> enqueue_msg(Msg, State); false -> - deliver(PacketId, Msg, State), - await(PacketId, Msg, inc_stats(deliver, Msg, next_pkt_id(State))) + ok = deliver(PacketId, Msg, State), + await(PacketId, Msg, next_pkt_id(State)) end. enqueue_msg(Msg, State = #state{mqueue = Q}) -> + emqx_pd:update_counter(enqueue_stats, 1), {Dropped, NewQ} = emqx_mqueue:in(Msg, Q), Dropped =/= undefined andalso emqx_shared_sub:maybe_nack_dropped(Dropped), - inc_stats(enqueue, Msg, State#state{mqueue = NewQ}). + State#state{mqueue = NewQ}. %%------------------------------------------------------------------------------ %% Deliver @@ -872,6 +864,7 @@ redeliver({pubrel, PacketId}, #state{conn_pid = ConnPid}) -> ConnPid ! {deliver, {pubrel, PacketId}}. deliver(PacketId, Msg, State) -> + emqx_pd:update_counter(deliver_stats, 1), %% Ack QoS1/QoS2 messages when message is delivered to connection. %% NOTE: NOT to wait for PUBACK because: %% The sender is monitoring this session process, @@ -882,7 +875,7 @@ deliver(PacketId, Msg, State) -> do_deliver(PacketId, emqx_shared_sub:maybe_ack(Msg), State). do_deliver(PacketId, Msg, #state{conn_pid = ConnPid, binding = local}) -> - ConnPid ! {deliver, {publish, PacketId, Msg}}; + ConnPid ! {deliver, {publish, PacketId, Msg}}, ok; do_deliver(PacketId, Msg, #state{conn_pid = ConnPid, binding = remote}) -> emqx_rpc:cast(node(ConnPid), erlang, send, [ConnPid, {deliver, {publish, PacketId, Msg}}]). @@ -994,21 +987,21 @@ next_pkt_id(State = #state{next_pkt_id = 16#FFFF}) -> next_pkt_id(State = #state{next_pkt_id = Id}) -> State#state{next_pkt_id = Id + 1}. -%%------------------------------------------------------------------------------ -%% Inc stats - -inc_stats(deliver, Msg, State = #state{deliver_stats = I}) -> - State1 = maybe_gc({1, msg_size(Msg)}, State), - State1#state{deliver_stats = I + 1}; -inc_stats(enqueue, _Msg, State = #state{enqueue_stats = I}) -> - State#state{enqueue_stats = I + 1}. - %% 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 + +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 @@ -1024,9 +1017,3 @@ noreply(State) -> shutdown(Reason, State) -> {stop, Reason, State}. -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}. - From f7596b81317e78d4af63c13624547afda5239ffa Mon Sep 17 00:00:00 2001 From: turtled Date: Fri, 21 Dec 2018 22:44:58 +0800 Subject: [PATCH 518/520] Fix session shutdown bug --- src/emqx_session.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index eac258bcf..5d5381f6e 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -391,12 +391,12 @@ handle_call(stats, _From, State) -> handle_call({discard, ByPid}, _From, State = #state{conn_pid = undefined}) -> ?LOG(warning, "Discarded by ~p", [ByPid], State), - {stop, discarded, ok, State}; + {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], State), ConnPid ! {shutdown, discard, {ClientId, ByPid}}, - {stop, discarded, ok, State}; + {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. @@ -1015,5 +1015,5 @@ noreply(State) -> {noreply, State}. shutdown(Reason, State) -> - {stop, Reason, State}. + {stop, {shutdown, Reason}, State}. From edf0ded9f3a876c014a90aa4f76c44d510a99375 Mon Sep 17 00:00:00 2001 From: tigercl Date: Sat, 22 Dec 2018 11:32:22 +0800 Subject: [PATCH 519/520] Fix bug that no update message expiry interval (#2101) --- src/emqx_message.erl | 4 ++++ src/emqx_protocol.erl | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 085565403..a4bbb378c 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -23,6 +23,7 @@ -export([set_headers/2]). -export([get_header/2, get_header/3, set_header/3]). -export([is_expired/1, update_expiry/1]). +-export([remove_topic_alias/1]). -export([format/1]). -type(flag() :: atom()). @@ -109,6 +110,9 @@ update_expiry(Msg = #message{headers = #{'Message-Expiry-Interval' := Interval}, update_expiry(Msg) -> Msg. +remove_topic_alias(Msg = #message{headers = Headers}) -> + Msg#message{headers = maps:remove('Topic-Alias', Headers)}. + %% MilliSeconds elapsed(Since) -> max(0, timer:now_diff(os:timestamp(), Since) div 1000). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 5495e4ff7..7c182c755 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -617,12 +617,12 @@ deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, deliver({connack, ReasonCode, SP}, PState) -> send(?CONNACK_PACKET(ReasonCode, SP), PState); -deliver({publish, PacketId, Msg = #message{headers = Headers}}, PState = #pstate{mountpoint = MountPoint}) -> +deliver({publish, PacketId, Msg}, PState = #pstate{mountpoint = MountPoint}) -> _ = emqx_hooks:run('message.delivered', [credentials(PState)], Msg), Msg1 = emqx_message:update_expiry(Msg), Msg2 = emqx_mountpoint:unmount(MountPoint, Msg1), - send(emqx_packet:from_message(PacketId, Msg2#message{headers = maps:remove('Topic-Alias', Headers)}), PState); - + send(emqx_packet:from_message(PacketId, emqx_message:remove_topic_alias(Msg2)), PState); + deliver({puback, PacketId, ReasonCode}, PState) -> send(?PUBACK_PACKET(PacketId, ReasonCode), PState); From 1797aadbe769c08f998f33eb0baf0632b866bb3e Mon Sep 17 00:00:00 2001 From: Gilbert Date: Sat, 22 Dec 2018 18:47:38 +0800 Subject: [PATCH 520/520] Fix unsuback compat (#2102) --- src/emqx_protocol.erl | 31 +++++++++++++++---------------- src/emqx_reason_codes.erl | 4 +++- test/emqx_reason_codes_tests.erl | 4 ++++ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 7c182c755..13eff95bf 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -283,8 +283,8 @@ preprocess_properties(Packet = #mqtt_packet{ topic_name = <<>>, properties = #{'Topic-Alias' := AliasId}} }, - PState = #pstate{proto_ver = ?MQTT_PROTO_V5, - topic_aliases = Aliases, + PState = #pstate{proto_ver = ?MQTT_PROTO_V5, + topic_aliases = Aliases, topic_alias_maximum = #{from_client := TopicAliasMaximum}}) -> case AliasId =< TopicAliasMaximum of true -> @@ -300,7 +300,7 @@ preprocess_properties(Packet = #mqtt_packet{ topic_name = Topic, properties = #{'Topic-Alias' := AliasId}} }, - PState = #pstate{proto_ver = ?MQTT_PROTO_V5, + PState = #pstate{proto_ver = ?MQTT_PROTO_V5, topic_aliases = Aliases, topic_alias_maximum = #{from_client := TopicAliasMaximum}}) -> case AliasId =< TopicAliasMaximum of @@ -521,11 +521,7 @@ connack({?RC_SUCCESS, SP, PState}) -> connack({ReasonCode, PState = #pstate{proto_ver = ProtoVer}}) -> emqx_hooks:run('client.connected', [credentials(PState), ReasonCode, attrs(PState)]), - ReasonCode1 = if ProtoVer =:= ?MQTT_PROTO_V5 -> - ReasonCode; - true -> - emqx_reason_codes:compat(connack, ReasonCode) - end, + [ReasonCode1] = reason_codes_compat(connack, [ReasonCode], ProtoVer), _ = deliver({connack, ReasonCode1}, PState), {error, emqx_reason_codes:name(ReasonCode1, ProtoVer), PState}. @@ -633,15 +629,10 @@ deliver({pubrec, PacketId, ReasonCode}, PState) -> send(?PUBREC_PACKET(PacketId, ReasonCode), PState); deliver({suback, PacketId, ReasonCodes}, PState = #pstate{proto_ver = ProtoVer}) -> - send(?SUBACK_PACKET(PacketId, - if ProtoVer =:= ?MQTT_PROTO_V5 -> - ReasonCodes; - true -> - [emqx_reason_codes:compat(suback, RC) || RC <- ReasonCodes] - end), PState); + send(?SUBACK_PACKET(PacketId, reason_codes_compat(suback, ReasonCodes, ProtoVer)), PState); -deliver({unsuback, PacketId, ReasonCodes}, PState) -> - send(?UNSUBACK_PACKET(PacketId, ReasonCodes), 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}) -> @@ -981,3 +972,11 @@ do_acl_deny_action(?SUBSCRIBE_PACKET(_PacketId, _Properties, _RawTopicFilters), end; do_acl_deny_action(_PubSupPacket, _ReasonCode, PState) -> {ok, PState}. + +%% Reason code compat +reason_codes_compat(_PktType, ReasonCodes, ?MQTT_PROTO_V5) -> + ReasonCodes; +reason_codes_compat(unsuback, _ReasonCodes, _ProtoVer) -> + undefined; +reason_codes_compat(PktType, ReasonCodes, _ProtoVer) -> + [emqx_reason_codes:compat(PktType, RC) || RC <- ReasonCodes]. diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index c6bdd849d..5b7f7e42f 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -140,4 +140,6 @@ compat(connack, 16#9D) -> ?CONNACK_SERVER; compat(connack, 16#9F) -> ?CONNACK_SERVER; compat(suback, Code) when Code =< ?QOS_2 -> Code; -compat(suback, Code) when Code >= 16#80 -> 16#80. +compat(suback, Code) when Code >= 16#80 -> 16#80; + +compat(unsuback, _Code) -> undefined. diff --git a/test/emqx_reason_codes_tests.erl b/test/emqx_reason_codes_tests.erl index 29f7ad081..a8e352c83 100644 --- a/test/emqx_reason_codes_tests.erl +++ b/test/emqx_reason_codes_tests.erl @@ -112,6 +112,10 @@ compat_test() -> (((codes_test(suback)) ([0,1,2, 16#80])) ([0,1,2, 16#80])) + (fun emqx_reason_codes:compat/2), + (((codes_test(unsuback)) + ([0, 1, 2])) + ([undefined, undefined, undefined])) (fun emqx_reason_codes:compat/2). codes_test(AsistVar) ->

    #74c8?I;9JR=klhUZUhwf7vv0qWme$xxXv; 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≷nn8W~wD2Z=Tg*MmPsl;|jqnv6nz2ABKX= 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%0D$oz-zOs44- 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

    2b96i{L#QKAVCOp*ax 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

#74c8?I;9JR=klhUZUhwf7vv0qWme$xxXv; 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≷nn8W~wD2Z=Tg*MmPsl;|jqnv6nz2ABKX= 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%0D$oz-zOs44- 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