├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.adoc ├── bin └── emqttb ├── config └── vm.args.src ├── doc └── src │ ├── autorate_descr.texi │ ├── emqttb.texi │ ├── intro.texi │ └── schema.texi ├── emqttb-dashboard.json ├── include └── emqttb.hrl ├── rebar.config ├── rebar.config.script ├── scripts ├── docgen.escript └── rename-package.sh ├── src ├── behaviors │ ├── emqttb_behavior_conn.erl │ ├── emqttb_behavior_pub.erl │ └── emqttb_behavior_sub.erl ├── conf │ ├── emqttb_conf.erl │ └── emqttb_conf_model.erl ├── emqttb.app.src ├── emqttb.erl ├── framework │ ├── emqttb_app.erl │ ├── emqttb_autorate.erl │ ├── emqttb_autorate_sup.erl │ ├── emqttb_group.erl │ ├── emqttb_group_sup.erl │ ├── emqttb_internal.hrl │ ├── emqttb_logger.erl │ ├── emqttb_misc_sup.erl │ ├── emqttb_scenario.erl │ ├── emqttb_scenarios_sup.erl │ ├── emqttb_sup.erl │ └── emqttb_worker.erl ├── metrics │ ├── emqttb_grafana.erl │ ├── emqttb_metrics.erl │ └── emqttb_pushgw.erl ├── restapi │ ├── emqttb_http.erl │ ├── emqttb_http_healthcheck.erl │ ├── emqttb_http_metrics.erl │ ├── emqttb_http_scenario_conf.erl │ ├── emqttb_http_sighup.erl │ └── emqttb_http_stage.erl └── scenarios │ ├── emqttb_scenario_conn.erl │ ├── emqttb_scenario_persistent_session.erl │ ├── emqttb_scenario_pub.erl │ ├── emqttb_scenario_pubsub_fwd.erl │ ├── emqttb_scenario_sub.erl │ └── emqttb_scenario_sub_flapping.erl └── test ├── emqtt_gauge_SUITE.erl ├── emqttb_dummy_behavior.erl ├── emqttb_worker_SUITE.erl └── escript_SUITE.erl /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | otp: 14 | - "25.3.2-2" 15 | os: 16 | - ubuntu24.04 17 | container: 18 | image: ghcr.io/emqx/emqx-builder/5.3-6:1.15.7-${{ matrix.otp }}-${{ matrix.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 22 | with: 23 | fetch-depth: 0 24 | - name: Install additional packages 25 | run: | 26 | apt-get update 27 | apt-get install -y texinfo install-info 28 | - name: Compile and run tests 29 | env: 30 | BUILD_WITHOUT_QUIC: "true" 31 | run: | 32 | git config --global --add safe.directory $(pwd) 33 | make all docs 34 | - name: Create release package 35 | shell: bash 36 | run: | 37 | make release 38 | - if: failure() 39 | run: cat rebar3.crashdump 40 | - run: ./_build/default/bin/emqttb 41 | - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 42 | with: 43 | name: packages 44 | path: ./*.tar.gz 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | workflow_dispatch: 8 | inputs: 9 | ref: 10 | type: string 11 | required: true 12 | dryrun: 13 | type: boolean 14 | required: true 15 | default: false 16 | 17 | jobs: 18 | # Build documentation once, since it requires a lot of 3rd party 19 | # tooling, then reuse it as an artifact: 20 | docs: 21 | runs-on: ubuntu-latest 22 | 23 | container: 24 | image: ghcr.io/emqx/emqx-builder/5.3-6:1.15.7-26.2.3-1-ubuntu24.04 25 | 26 | steps: 27 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 28 | with: 29 | ref: ${{ github.event.inputs.ref }} 30 | fetch-depth: 0 31 | 32 | - shell: bash 33 | name: Install additional packages 34 | run: | 35 | apt-get update 36 | apt-get install -y texinfo install-info 37 | 38 | - name: Build 39 | shell: bash 40 | run: | 41 | export BUILD_WITHOUT_QUIC=1 42 | git config --global --add safe.directory $(pwd) 43 | make release 44 | tar czf docs.tar.gz doc/html doc/info 45 | 46 | - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 47 | with: 48 | name: docs 49 | path: docs.tar.gz 50 | 51 | # Do normal builds: 52 | linux: 53 | runs-on: ubuntu-latest 54 | needs: docs 55 | strategy: 56 | fail-fast: false 57 | matrix: 58 | otp: 59 | - "26.2.3-1" 60 | os: 61 | - ubuntu24.04 62 | - ubuntu22.04 63 | - ubuntu20.04 64 | - debian12 65 | - debian11 66 | - debian10 67 | - el9 68 | - el8 69 | - el7 70 | - amzn2 71 | - amzn2023 72 | env: 73 | EMQX_BUILDER: ghcr.io/emqx/emqx-builder/5.3-6:1.15.7-${{ matrix.otp }}-${{ matrix.os }} 74 | 75 | steps: 76 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 77 | with: 78 | ref: ${{ github.event.inputs.ref }} 79 | fetch-depth: 0 80 | 81 | - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 82 | with: 83 | name: docs 84 | path: . 85 | 86 | - name: Build 87 | shell: bash 88 | run: | 89 | tar xf docs.tar.gz 90 | docker run -t --rm -v $(pwd):/emqttb -w /emqttb -e CAN_BUILD_DOCS=false -e REBAR=/usr/local/bin/rebar3 $EMQX_BUILDER bash -c 'git config --global --add safe.directory /emqttb && make release' 91 | - name: Test 92 | shell: bash 93 | run: | 94 | mkdir test-package 95 | cp *.tar.gz test-package/ 96 | pushd test-package 97 | tar xfz emqttb*.tar.gz 98 | docker network create emqttb 99 | docker run -d --name emqx --network emqttb emqx/emqx:latest 100 | docker run -t --rm -v $(pwd):/emqttb -w /emqttb --network emqttb $EMQX_BUILDER bash -c 'bin/emqttb --loiter 5s @pub -t test @g --host emqx' 101 | popd 102 | - if: failure() 103 | run: cat rebar3.crashdump 104 | - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 105 | if: success() 106 | with: 107 | name: emqttb-${{ matrix.os }}-${{ matrix.otp }} 108 | path: ./emqttb*.tar.gz 109 | 110 | mac: 111 | strategy: 112 | fail-fast: false 113 | matrix: 114 | macos: 115 | - macos-13 116 | - macos-14 117 | otp: 118 | - "26" 119 | 120 | runs-on: ${{ matrix.macos }} 121 | needs: docs 122 | 123 | steps: 124 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 125 | with: 126 | ref: ${{ github.event.inputs.ref }} 127 | fetch-depth: 0 128 | - name: prepare 129 | run: | 130 | brew install coreutils erlang@${{ matrix.otp }} 131 | brew link --force erlang@${{ matrix.otp }} 132 | 133 | - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 134 | with: 135 | name: docs 136 | path: . 137 | 138 | - name: build 139 | shell: bash 140 | run: | 141 | tar xf docs.tar.gz 142 | export CAN_BUILD_DOCS=false 143 | export BUILD_WITHOUT_QUIC=1 144 | make release 145 | 146 | - name: Test 147 | shell: bash 148 | run: | 149 | brew install emqx 150 | emqx start 151 | mkdir test-package 152 | cp *.tar.gz test-package/ 153 | pushd test-package 154 | tar xfz emqttb*.tar.gz 155 | bin/emqttb --loiter 5s @pub -t test @g --host localhost 156 | popd 157 | 158 | - if: failure() 159 | run: cat rebar3.crashdump 160 | - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 161 | if: success() 162 | with: 163 | name: emqttb-${{ matrix.macos }}-${{ matrix.otp }} 164 | path: ./emqttb-*.tar.gz 165 | 166 | release: 167 | runs-on: ubuntu-latest 168 | needs: 169 | - linux 170 | - mac 171 | if: startsWith(github.ref, 'refs/tags/') && !inputs.dryrun 172 | 173 | steps: 174 | - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 175 | with: 176 | pattern: "emqttb-*" 177 | path: packages 178 | merge-multiple: true 179 | - name: Create Release 180 | uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87 # v2.0.5 181 | with: 182 | name: EMQTT bench daemon ${{ github.ref_name }} Released 183 | body: EMQTT bench daemon ${{ github.ref_name }} Released 184 | files: packages/* 185 | draft: false 186 | prerelease: false 187 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | *.iml 18 | rebar3.crashdump 19 | *~ 20 | docs/ 21 | TAGS 22 | /rebar.lock 23 | /.emqttb.repeat 24 | /*.tar.gz 25 | /emqttb 26 | /libquicer_nif.so 27 | doc/lee 28 | doc/info 29 | doc/html 30 | perf.data -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2022, EMQ Technologies Co., Ltd. 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR ?= $(CURDIR)/rebar3 2 | REBAR_URL ?= https://s3.amazonaws.com/rebar3/rebar3 3 | TEXINFO := doc/src/emqttb.texi doc/lee/cli_params.texi doc/lee/os_env.texi doc/lee/value.texi 4 | 5 | .PHONY: all 6 | all: $(REBAR) 7 | $(REBAR) do compile, dialyzer, xref, eunit, ct 8 | 9 | .PHONY: compile 10 | compile: $(REBAR) 11 | $(REBAR) compile 12 | 13 | .PHONY: dialyzer 14 | dialyzer: $(REBAR) 15 | $(REBAR) do compile, dialyzer 16 | 17 | .PHONY: test 18 | test: $(REBAR) 19 | $(REBAR) do compile, eunit, ct 20 | 21 | .PHONY: release 22 | release: compile docs 23 | @$(REBAR) as emqttb tar 24 | @$(CURDIR)/scripts/rename-package.sh 25 | 26 | .PHONY: docs 27 | docs: doc/info/emqttb.info doc/html/index.html 28 | 29 | doc/info/emqttb.info: $(TEXINFO) 30 | texi2any -I doc/lee --info -o $@ $< 31 | install-info $@ doc/info/dir 32 | 33 | doc/html/index.html: $(TEXINFO) 34 | texi2any -I doc/lee --html -c INFO_JS_DIR=js -c HTML_MATH=mathjax -o doc/html/ $< 35 | 36 | $(TEXINFO): scripts/docgen.escript compile 37 | scripts/docgen.escript doc/lee 38 | 39 | .PHONY: clean 40 | clean: distclean 41 | 42 | .PHONY: distclean 43 | distclean: 44 | @rm -rf _build erl_crash.dump rebar3.crashdump rebar.lock emqttb doc/lee doc/html doc/info 45 | 46 | $(REBAR): 47 | @curl -skfL "$(REBAR_URL)" -o $@ 48 | @chmod +x $@ 49 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | :!sectids: 2 | = EMQTT bench daemon 3 | 4 | A scriptable load generator for MQTT 5 | 6 | == Quick start 7 | 8 | Start 100 clients connecting to localhost with 10ms pause in between. 9 | Clients publish a 1kb-size message every 10ms to topic `t/%clientid%`. 10 | Run steady traffic for 30s and then exit: 11 | 12 | [source,bash] 13 | ---- 14 | emqttb --loiter 30s \ 15 | @pub --topic 't/%n' --conninterval 10ms --pubinterval 10ms --num-clients 100 --size 1kb \ 16 | @g --host 127.0.0.1 17 | ---- 18 | 19 | Same with shortened arguments: 20 | 21 | [source,bash] 22 | ---- 23 | emqttb -L 30s @pub -t 't/%n' -I 10ms -i 10ms -N 100 -s 1kb @g -h 127.0.0.1 24 | ---- 25 | 26 | Start 10 clients that subscribe to a wildcard topic and run steady traffic for 30s: 27 | 28 | [source,bash] 29 | ---- 30 | emqttb --loiter 30s \ 31 | @sub --topic 't/#' --conninterval 10ms --num-clients 10 \ 32 | @g --host 127.0.0.1 33 | ---- 34 | 35 | Same with shortened arguments: 36 | 37 | [source,bash] 38 | ---- 39 | emqttb -L 30s @sub -t 't/#' -I 10ms -N 10 @g -h 127.0.0.1 40 | ---- 41 | 42 | Combine the above scenarios in a single command: 43 | 44 | [source,bash] 45 | ---- 46 | emqttb -L 30s @pub -t 't/%n' -I 10ms -i 10ms -N 100 -s 1kb \ 47 | @sub -t 't/#' -I 10ms -N 10 @g -h 127.0.0.1 48 | ---- 49 | 50 | == Understanding EMQTTB CLI 51 | 52 | EMQTTB executable accepts named CLI arguments (such as `--foo` or `-f` positional arguments and *actions*. 53 | Actions are special CLI arguments that start with `@` character (e.g. `@my-action`). 54 | Actions correspond to different load-generation scenarios, such as `@pub` and `@sub`, client group configurations and autorates. 55 | 56 | Each CLI action defines its own *scope* of named and positional CLI arguments. 57 | Positional arguments are always specified after named arguments within the scope. 58 | There is also a *global* scope of arguments that don't belong to any action. Global arguments are specified before the very first action. 59 | 60 | Example: 61 | 62 | [source,bash] 63 | ---- 64 | emqttb --foo --bar 1 positional1 positional2 @action1 --foo 1 positional1 @action2 ... 65 | |___________| |_____________________| |_________________| 66 | |regular args positional args | action1 scope 67 | |___________________________________| 68 | global scope 69 | ---- 70 | 71 | Additional features: 72 | 73 | - Boolean flag arguments can be set to false by adding `no-` prefix, for example `--no-pushgw` 74 | - Short boolean flags can be set to false using `+` sigil instead of `-` 75 | 76 | == Logs 77 | 78 | emqttb tries to keep the standard output clean, so all the logs generated by the workers and MQTT clients are forwarded to `/tmp/emqttb.log` file. 79 | Most errors and messages are found there. 80 | 81 | == Core concepts 82 | 83 | * *Worker* is a process that corresponds to a single MQTT client 84 | 85 | * *Behavior* is a callback module containing functions that workers run in a loop 86 | 87 | * *Group* is a supervised colletion of workers running the same behavior and sharing the same configuration. 88 | Group manager controls the number of workers, restarts failed workers and implements ramp up/down logic. 89 | 90 | * *Scenario* is a callback module that creates several worker groups and manupulates group configuration using autorate. 91 | 92 | * *Autorate* a process that adjusts group parameters (such as number of workers in the group, or worker configuration) based on constants or dynamic parameters, e.g. available RAM or CPU load, observed latency and so on. 93 | 94 | == Client group configuration 95 | 96 | Load generation scenarios (such as `@pub` and `@sub`) don't explicitly specify how the clients should connect to the MQTT broker. 97 | These settings are delegated to clinet group configuration action (`@g`). 98 | 99 | Each group configuration has an id. EMQTTB always creates a default group with id=`default`. 100 | All scenarios use it by default. 101 | 102 | Scenarios use group configuration for each client group they create. 103 | For example, `@pub` scenario creates only one group for publishers which is specified by `--group` or `-g` CLI argument. 104 | Usually this scenario is invoked with implicit `default` group configuration: 105 | 106 | [source,bash] 107 | ---- 108 | emqttb @pub -t foo @g -h localhost 109 | ---- 110 | 111 | It is equivalent to the following command: 112 | 113 | [source,bash] 114 | ---- 115 | emqttb @pub -t foo --group default @g --group default -h localhost 116 | ---- 117 | 118 | Sometimes it is necessary to use different group configurations for different scenarios. 119 | For example, imagine we need to test pub/sub scenario where publishers use websocket connections and subscribers use MQTT, or we want to test bridging between MQTT brokers that have different hostnames. 120 | It can be achieved like this: 121 | 122 | [source,bash] 123 | ---- 124 | emqttb @pub -t 'foo/%n' -g wss \ 125 | @sub -t 'foo/#' -g mqtt \ 126 | @g -g wss -h localhost --transport ws --ssl \ 127 | @g -g mqtt -h localhost --transport sock 128 | ---- 129 | 130 | == Metrics 131 | 132 | EMQTTB can export metrics to Prometheus using pushgateway or scraping REST endpoint. 133 | Scraping endpoint is enabled automatically when the script is started with `--restapi` global flag. 134 | Pushgateway should be enabled explicitly: 135 | 136 | [source,bash] 137 | ---- 138 | emqttb --pushgw --pushgw-url http://localhost:9091 139 | ---- 140 | 141 | There is a ready-to-use grafana dashboard for emqttb: 142 | https://github.com/ieQu1/grafana-dashboards/blob/master/grafana/dashboards/emqttb-dashboard.json[emqttb-dashboard.json]. 143 | Also there is a fully ready docker images that include Grafana and Postgres with all the necessary schemas and dashboards for emqttb and emqx performance analysis: 144 | https://github.com/ieQu1?tab=packages&repo_name=grafana-dashboards[github.com/ieQu1/grafana-dashboards]. 145 | 146 | Additionally, EMQTTB can add annotations to grafana dashboards when scenarios start, advance to a next stage or finish. 147 | This requires a Grafana API key with `Editor` role. 148 | Once the key is obtained, it can be used like this: 149 | 150 | [source,bash] 151 | ---- 152 | export EMQTTB_METRICS__GRAFANA__API_KEY="Bearer eyJrIjoiNmhSdTFnWGJlaE9tZXQ2YXI4WlEyUGNSMXFMb1oyUXkiLCJuIjoiZW1xdHRiIiwiaWQiOjF9" 153 | export EMQTTB_METRICS__GRAFANA__URL="http://localhost:3000" 154 | emqttb --grafana ... 155 | ---- 156 | 157 | == Build and run locally 158 | 159 | Requirements: 160 | 161 | * https://www.erlang.org/[Erlang/OTP 24+] 162 | * https://rebar3.org/[rebar3] 163 | * make 164 | * cmake (for quicer) 165 | 166 | If you want to build HTML documentation and manpages, it's necessary to install the following tools: 167 | 168 | * asciidoctor (lee dependency) 169 | * xsltproc 170 | * docbook-xsl 171 | * java (jre is enough) 172 | 173 | This can be omitted by running `export CAN_BUILD_DOCS=false`. 174 | 175 | [source,bash] 176 | ---- 177 | git clone git@github.com:emqx/emqttb.git 178 | cd emqttb 179 | make release 180 | ---- 181 | 182 | === Building on macOS 183 | 184 | Assuming you already have https://brew.sh/[Homebrew] installed. 185 | 186 | [source,bash] 187 | ---- 188 | brew install erlang rebar3 cmake java asciidoctor xsltproc docbook-xsl 189 | git clone git@github.com:emqx/emqttb.git 190 | cd emqttb 191 | export MANPAGE_STYLESHEET=$(brew --prefix docbook-xsl)/docbook-xsl/manpages/docbook.xsl 192 | make release 193 | ---- 194 | 195 | == Getting more help 196 | 197 | It is possible to get information about CLI actions by running `emqttb --help `. 198 | For example, the following command will show manual page about `@pub` scenario: 199 | 200 | [source,bash] 201 | ---- 202 | emqttb --help pub 203 | ---- 204 | 205 | emqttb also serves the documentation in HTML format directly from the REST endpoint (`emqttb --restapi`). 206 | It is available at the following URL: http://localhost:8017/doc/index.html 207 | -------------------------------------------------------------------------------- /bin/emqttb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | ## constants from relx template 5 | RUNNER_ROOT_DIR="{{ runner_root_dir }}" 6 | RUNNER_ESCRIPT_DIR="{{ runner_escript_dir }}" 7 | ERTS_VSN="{{ erts_vsn }}" 8 | 9 | ERTS_PATH="${RUNNER_ROOT_DIR}/erts-${ERTS_VSN}/bin" 10 | 11 | export INFOPATH="${INFOPATH}:${RUNNER_ROOT_DIR}/doc/info" 12 | 13 | ulimit -n $(ulimit -Hn) 14 | 15 | help() { 16 | info emqttb "${@}" 17 | } 18 | 19 | if [ $# -eq 2 ] && [ $1 = "--help" ]; then 20 | help "${2}" 21 | elif [ $# -eq 1 ] && [ $1 = "--help" ]; then 22 | help 23 | elif [ $# -eq 0 ]; then 24 | help 25 | else 26 | exec ${ERTS_PATH}/escript ${RUNNER_ESCRIPT_DIR}/emqttb "$@" 27 | fi 28 | -------------------------------------------------------------------------------- /config/vm.args.src: -------------------------------------------------------------------------------- 1 | -sname emqttb 2 | +Q134217727 3 | -a16 4 | 5 | -setcookie emqttb_cookie 6 | 7 | +K true 8 | +A30 9 | -------------------------------------------------------------------------------- /doc/src/autorate_descr.texi: -------------------------------------------------------------------------------- 1 | When the loadgen creates too much traffic, the system may get overloaded. 2 | In this case, the test usually has to be restarted all over again with different parameters. 3 | This can be very expensive in man-hours and computing resources. 4 | 5 | In order to prevent that, emqttb can tune some parameters (such as message publishing interval) 6 | automatically using 7 | @url{https://controlguru.com/integral-reset-windup-jacketing-logic-and-the-velocity-pi-form/,PI controller}. 8 | 9 | The following formula is used for the error function: 10 | 11 | @math{e=(a_{SP} - a_{PV}) a_{coeff}} 12 | 13 | @node Autoscale 14 | @section Autoscale 15 | 16 | A special autorate controlling the rate of spawning new clients is implicitly created for each client group. 17 | Its name usually follows the pattern @code{%scenario%/conn_interval}. 18 | 19 | By default, the number of pending (unacked) connections is used as the process variable. 20 | Number of pending connections is a metric that responds very fast to target overload, so it makes a reasonable default. 21 | 22 | For example the following command can automatically adjust the rate of connections: 23 | 24 | @example 25 | ./emqttb --pushgw @@conn -I 10ms -N 5_000 \ 26 | @@a -a conn/conninterval -V 1000 --setpoint 10 27 | @end example 28 | 29 | @node SCRAM 30 | @section SCRAM 31 | 32 | Normally, autorate adjusts the control variable gradually. 33 | However, sometimes the system under test becomes overloaded suddenly, and in this case slowly decreasing the pressure may not be efficient enough. 34 | To combat this situation, @code{emqttb} has "SCRAM" mechanism, that immediately resets the control variable to a @ref{value/autorate/_/scram/override,configured safe value}. 35 | This happens when the value of process variable exceeds a @ref{value/autorate/_/scram/threshold,certain threshold}. 36 | 37 | SCRAM mode remains in effect until the number of pending connections becomes less than @math{\frac{t h}{100}} 38 | where @math{t} is threshold, and @math{h} is @ref{value/autorate/_/scram/hysteresis,hystersis}. 39 | 40 | @node Autorate List 41 | @section List of autorate variables 42 | @include autorate.texi 43 | 44 | @node Metrics List 45 | @section List of metrics 46 | @include metric.texi 47 | -------------------------------------------------------------------------------- /doc/src/emqttb.texi: -------------------------------------------------------------------------------- 1 | \input texinfo @c -*-texinfo-*- 2 | @c %**start of header 3 | @setfilename emqttb.info 4 | @settitle EMQTTB 5 | @c %**end of header 6 | 7 | @dircategory EMQTTB: an MQTT load generator. 8 | @direntry 9 | * EMQTTB: (emqttb). 10 | @end direntry 11 | 12 | @copying 13 | A scriptable autotuning MQTT load generator. 14 | 15 | Copyright @copyright{} 2022-2025 EMQ Technologies Co., Ltd. All Rights Reserved. 16 | 17 | @end copying 18 | 19 | @include schema.texi 20 | 21 | @c The document itself 22 | 23 | @titlepage 24 | @title EMQTTB 25 | @subtitle A scriptable autotuning MQTT load generator 26 | @page 27 | @vskip 0pt plus 1filll 28 | @insertcopying 29 | @end titlepage 30 | 31 | @c Output the table of the contents at the beginning. 32 | @contents 33 | 34 | @ifnottex 35 | @node Top 36 | @top EMQTTB 37 | 38 | @insertcopying 39 | @end ifnottex 40 | 41 | @node Introduction 42 | @chapter Introduction 43 | @include intro.texi 44 | 45 | @node Invokation 46 | @chapter Invokation 47 | EMQTTB executable accepts named CLI arguments (such as @option{--foo} or -f positional arguments and actions. Actions are special CLI arguments that start with @@ character (e.g. @option{@@pub}). Actions correspond to different load-generation scenarios, such as @option{@@pub} and @option{@@sub}, client group configurations (@option{@@g}) and autorates (@option{@@g}). 48 | 49 | Each CLI action defines its own scope of named and positional CLI arguments. Positional arguments are always specified after named arguments within the scope. There is also a global scope of arguments that don’t belong to any action. Global arguments are specified before the very first action. 50 | 51 | Example: 52 | 53 | @example 54 | emqttb --foo --bar 1 positional1 positional2 @@action1 --foo 1 positional1 @@action2 ... 55 | |___________| |_____________________| |_________________| 56 | |regular args positional args | action1 scope 57 | |___________________________________| 58 | global scope 59 | @end example 60 | 61 | Flag negation: 62 | 63 | Boolean flag arguments can be set to false by adding @code{no-} prefix, for example @option{--no-pushgw}. 64 | 65 | Short boolean flags can be set to false using @code{+} sigil instead of @code{-}. 66 | 67 | @node CLI 68 | @section CLI Arguments 69 | @lowersections 70 | @include cli_param.texi 71 | @raisesections 72 | 73 | @node OS Environment Variables 74 | @section OS Environment Variables 75 | @lowersections 76 | @include os_env.texi 77 | @raisesections 78 | 79 | @node Autorate 80 | @chapter Autorate 81 | @include autorate_descr.texi 82 | 83 | @node All Values 84 | @chapter All Configurable Values 85 | @include value.texi 86 | 87 | @node Index 88 | @unnumbered Index 89 | 90 | @syncodeindex vr cp 91 | @syncodeindex fn cp 92 | @printindex cp 93 | 94 | @bye 95 | -------------------------------------------------------------------------------- /doc/src/intro.texi: -------------------------------------------------------------------------------- 1 | @node Concepts 2 | @section Concepts: Worker, Group, Scenario... 3 | 4 | @b{Worker} is a process that corresponds to a single MQTT client. 5 | 6 | @b{Behavior} is a callback module containing functions that workers run in a loop. 7 | 8 | @b{Group} is a supervised colletion of workers running the same behavior and sharing the same configuration. Group manager controls the number of workers, restarts failed workers and implements ramp up/down logic. 9 | 10 | @b{Scenario} is a callback module that creates several worker groups and manupulates group configuration using autorate. 11 | 12 | @b{Autorate} a process that adjusts group parameters (such as number of workers in the group, or worker configuration) based on constants or dynamic parameters, e.g. available RAM or CPU load, observed latency and so on. 13 | 14 | @node Topic Patterns 15 | @section Topic Patterns 16 | 17 | @code{emqttb} supports pattern substitution in the topic names. 18 | 19 | @table @samp 20 | @item %n 21 | replaced with the worker ID (integer) 22 | @item %g 23 | replaced with the group ID 24 | @item %h 25 | replaced with the hostname 26 | @end table 27 | 28 | 29 | @node Verify Message Sequence 30 | @section Message Sequence Verification 31 | 32 | @code{emqttb} has builtin tools for detecting message loss and repetition. 33 | 34 | @quotation Warning 35 | Publishers should insert metadata into the payloads in order for this feature to work. 36 | @end quotation 37 | 38 | @quotation Warning 39 | This feature can use a lot of RAM to store the sequence numbers for each triple of sender client id, receiver client id, and MQTT topic. 40 | @end quotation 41 | 42 | Errors about missing messages and warnings about duplicate messages are printed to the emqttb log. 43 | 44 | @heading Prometheus metrics 45 | 46 | @table @code 47 | @item emqttb_repeats_number 48 | number of times when the sequence number of the message goes backwards 49 | @item emqttb_gaps_number 50 | number of times when the sequence number of the message skips the messages (a gap) 51 | @item emqttb_repeat_size 52 | rolling average; size of the repeated sequence 53 | @item emqttb_gap_size 54 | rolling average; size of the gap 55 | @end table 56 | -------------------------------------------------------------------------------- /doc/src/schema.texi: -------------------------------------------------------------------------------- 1 | @c Put long definitions here 2 | 3 | @macro doc-clientid 4 | Pattern used to generate ClientID. 5 | 6 | The following substitutions are supported: 7 | 8 | @table @samp 9 | @item %n 10 | replaced with the worker ID (integer) 11 | @item %g 12 | replaced with the group ID 13 | @item %h 14 | replaced with the hostname 15 | @end table 16 | 17 | @end macro 18 | 19 | @macro doc-ifaddr 20 | Bind a specific local IP address to the connection. 21 | If multiple IP addresses are given, workers choose local address using round-robin algorithm. 22 | 23 | @quotation Warning 24 | Setting a local address for a client TCP connection explicitly has a nasty side effect: 25 | when you do this @code{gen_tpc} calls @code{bind} on this address to get a free ephemeral port. 26 | But the OS doesn't know that in advance that we won't be listening on the port, so it reserves the local port number for the connection. 27 | However, when we connect to multiple EMQX brokers, we do want to reuse local ports. 28 | So don't use this option when the number of local addresses is less than the number of remote addresses. 29 | @end quotation 30 | 31 | @end macro 32 | 33 | @macro doc-interval 34 | Supported units: 35 | 36 | @table @samp 37 | @item us 38 | microseconds 39 | @item ms 40 | milliseconds 41 | @item s 42 | seconds 43 | @item min 44 | minutes 45 | @item h 46 | hours 47 | @end table 48 | 49 | If unit is not specified then @samp{ms} is assumed. 50 | 51 | @end macro 52 | 53 | @macro doc-scenario-sub 54 | This scenario starts @code{-N} workers, which subscribe to a specified topic. 55 | The only mandatory parameter is @code{--topic}, which supports pattern substitutions. 56 | 57 | @heading Client groups 58 | @code{sub} 59 | 60 | @end macro 61 | 62 | @macro doc-scenario-conn 63 | This scenario starts @code{-N} workers, which connect to the broker and then simply linger around. 64 | 65 | @heading Client groups 66 | @code{conn} 67 | 68 | @end macro 69 | 70 | @macro doc-scenario-pub 71 | This scenario starts @code{-N} workers, which publish messages to the specified topic at period @code{--pubinterval}. 72 | The only mandatory parameter is @code{--topic}, which supports pattern substitutions. 73 | 74 | @heading Client groups 75 | @code{pub} 76 | 77 | @heading Basic usage example 78 | @example 79 | emqttb @@pub -t foo/%n -N 100 -i 10ms -s 1kb 80 | @end example 81 | 82 | 83 | In this example the loadgen connects to the default broker @url{mqtt://localhost:1883}, 84 | starts 100 publishers which send messages to topic with the suffix of the worker id every 10 milliseconds. 85 | Size of the messages is 1kb. 86 | 87 | @heading Changing client settings 88 | @example 89 | emqttb @@pub -t foo/%n @@g --ssl --transport ws -h 127.0.0.1 90 | @end example 91 | 92 | In this example settings of the default client group has been changed: TLS encryption is enabled, and WebSocket transport is used. 93 | Also the hostname of the broker is specified explicitly. 94 | 95 | @example 96 | emqttb @@pub -t foo/%n -q 1 -g pub @@g -g pub --ssl --transport ws -h 127.0.0.1 97 | @end example 98 | 99 | The above example is similar to the previous one, except QoS of the messages is set to 1, 100 | and a dedicated client configuration with id `pub` is used for the publishers. 101 | It's useful for running multiple scenarios (e.g. @code{@@pub} and @code{@@sub}) in parallel, when they must use different settings. 102 | For example, it can be used for testing MQTT bridge. 103 | 104 | @heading Tuning publishing rate automatically 105 | 106 | By default, @code{@@pub} scenario keeps @code{pubinterval} constant. 107 | However, in some situations it should be tuned dynamically: 108 | suppose one wants to measure what publishing rate the broker can sustain while keeping publish latency under @code{--publatency}. 109 | 110 | This is also useful for preventing system overload. 111 | Generating too much load can bring the system down, and the test has to be started all over again with different parameters. 112 | Sometimes finding the correct rate takes many attempts, wasting human and machine time. 113 | Dynamic tuning of the publishing rate for keeping the latency constant can help in this situation. 114 | 115 | By default the maximum speed of rate adjustment is set to 0, effectively locking the @code{pubinterval} at a constant value. 116 | To enable automatic tuning, the autorate speed @code{-V} must be set to a non-zero value, also it makes sense to set 117 | the minimum (@code{-m}) and maximum (@code{-M}) values of the autorate: 118 | 119 | @example 120 | emqttb @@pub -t foo -i 1s -q 1 --publatency 50ms @@a -V 10 -m 0 -M 10000 121 | @end example 122 | 123 | Once automatic adjustment of the publishing interval is enabled, @code{-i} parameter sets the starting value of the publish interval, 124 | rather than the constant value. So the above example reads like this: 125 | 126 | Publish messages to topic @code{foo} with QoS 1, starting at the publishing interval of 1000 milliseconds, dynamically adjusting it 127 | so to keep the publishing latency around 50 milliseconds. The publishing interval is kept between 0 and 10 seconds, 128 | and the maximum rate of its change is 10 milliseconds per second. 129 | 130 | @end macro 131 | 132 | @macro doc-scenario-pubsub-fwd 133 | First all subscribers connect and subscribe to the brokers, then the publishers start to connect and publish. 134 | The default is to use full forwarding of messages between the nodes: 135 | that is, each publisher client publishes to a topic subscribed by a single client, and both clients reside on distinct nodes. 136 | 137 | Full forwarding of messages is the default and can be set by full_forwarding. 138 | 139 | @heading Client Groups 140 | @itemize 141 | @item 142 | @code{pubsub_forward.pub} 143 | @item 144 | @code{pubsub_forward.sub} 145 | @end itemize 146 | 147 | @heading Basic Usage 148 | 149 | @example 150 | ./emqttb --restapi @@pubsub_fwd --publatency 10ms --num-clients 400 -i 70ms \ 151 | @@g -h 172.25.0.2:1883,172.25.0.3:1883,172.25.0.4:1883 152 | @end example 153 | 154 | In this example the loadgen connects to a list of brokers in a round-robin in the declared order. 155 | First all the subscribers, then the publishers, 156 | with the difference that publishers will shift the given host list by one position to ensure each publisher and subscriber pair will reside on different hosts, 157 | thus forcing all messages to be forwarded. 158 | 159 | @end macro 160 | 161 | @macro doc-scenario-persistent-session 162 | This scenario starts @code{-N} workers, which connect to the broker and then simply linger around. 163 | 164 | This scenario measures throughput of MQTT broker in presence of persistent sessions. 165 | It is split in two stages that repeat in a loop: 166 | 167 | @enumerate 168 | @item 169 | @samp{consume} stage where subscribers (re)connect to the broker with `clean_session=false` and ingest saved messages 170 | @item 171 | @samp{publish} stage where subscribers disconnect, and another group of clients publishes messages to the topics 172 | @end enumerate 173 | 174 | This separation helps to measure throughput of writing and reading messages independently. 175 | 176 | Publish stage runs for a @ref{value/scenarios/persistent_session/_/pub/pub_time,set period of time}. 177 | It's possible to adjust publishing rate via autorate. 178 | 179 | Consume stages runs until the subscribers ingest all published messages, 180 | or until @ref{value/scenarios/persistent_session/_/max_stuck_time,timeout}. 181 | Please note that throughput measurement is not reliable when the consume stage is aborted due to timeout. 182 | 183 | @heading Client Groups 184 | @itemize 185 | @item 186 | @code{persistent_session.pub} 187 | @item 188 | @code{persistent_session.sub} 189 | @end itemize 190 | 191 | @end macro 192 | 193 | @macro doc-scenario-sub-flapping 194 | FIXME 195 | 196 | @end macro 197 | -------------------------------------------------------------------------------- /include/emqttb.hrl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -ifndef(EMQX_BENCHD_HRL). 17 | -define(EMQX_BENCHD_HRL, true). 18 | 19 | -define(APP, emqttb). 20 | 21 | -define(CONF_STORE, lee_conf_store). 22 | -define(MODEL_STORE, lee_model_store). 23 | 24 | -include_lib("lee/include/lee.hrl"). 25 | 26 | -define(MYCONF, ?lee_persistent_term_storage(?CONF_STORE)). 27 | -define(MYMODEL, persistent_term:get(?MODEL_STORE)). 28 | 29 | -define(CFG(Key), lee:get( ?MYMODEL 30 | , ?MYCONF 31 | , Key 32 | )). 33 | 34 | -define(CFG_LIST(Key), lee:list( ?MYMODEL 35 | , ?MYCONF 36 | , Key 37 | )). 38 | 39 | -define(SK(SCENARIO), scenarios, SCENARIO, {}). 40 | 41 | -endif. 42 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang -*- 2 | {erl_opts, [debug_info]}. 3 | {validate_app_modules, true}. 4 | 5 | {deps, 6 | [ {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.14.4"}}} 7 | , {gproc, "0.9.1"} 8 | , {lee, {git, "https://github.com/k32/lee", {tag, "0.5.1"}}} 9 | , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe", {tag, "1.0.10"}}} 10 | , {prometheus, {git, "https://github.com/deadtrickster/prometheus.erl", {tag, "v4.8.2"}}} 11 | , hackney 12 | , {cowboy, "2.9.0"} 13 | , {jsone, "1.7.0"} 14 | 15 | , {system_monitor, {git, "https://github.com/ieQu1/system_monitor.git", {tag, "3.0.6"}}} 16 | ]}. 17 | 18 | {escript_name, emqttb}. 19 | {escript_main_app, emqttb}. 20 | {escript_include_apps, [kernel, emqttb, system_monitor]}. 21 | {escript_emu_args, "%%! -smp true +K true +a16 +P16000000 +Q134217727 +Muacnl 0 +hms 64 -env ERL_MAX_PORTS 16000000 -env ERTS_MAX_PORTS 16000000 +h 50\n"}. 22 | {escript_shebang, "#!/usr/bin/env escript\n"}. 23 | {provider_hooks, [{post, [{compile, escriptize}]}]}. 24 | {post_hooks, [{"(linux|darwin|solaris|freebsd|netbsd|openbsd)", 25 | escriptize, 26 | "bash -c ' " 27 | " for nifso in ${REBAR_DEPS_DIR}/quicer/priv/libquicer_nif.so " 28 | " ${REBAR_CHECKOUTS_OUT_DIR}/quicer/priv/libquicer_nif.so; " 29 | " do [ -f $nifso ] && cp $nifso ${REBAR_BUILD_DIR}/bin/; " 30 | " done; " 31 | " rm ./emqttb ./libquicer_nif.so; " 32 | " ln -s \"${REBAR_BUILD_DIR}/rel/emqttb/bin/emqttb\" ./emqttb; " 33 | " ln -s \"${REBAR_BUILD_DIR}/bin/libquicer_nif.so\" ./libquicer_nif.so; " 34 | " ' " 35 | }, 36 | {"win32", 37 | escriptize, 38 | "robocopy \"%REBAR_BUILD_DIR%/bin/\" ./ emqttb* " 39 | "/njs /njh /nfl /ndl & exit /b 0"} % silence things 40 | ]}. 41 | 42 | {ct_readable, false}. 43 | 44 | {xref_checks,[undefined_function_calls,undefined_functions,locals_not_used, 45 | deprecated_function_calls, 46 | deprecated_functions]}. 47 | 48 | {dialyzer, [ 49 | {plt_apps, all_apps}, 50 | {extra_plt_apps, [lee, typerefl, ssl, stdlib]}, 51 | {statistics, true} 52 | ]}. 53 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | %% -*- mode:erlang -*- 2 | IsCentos6 = 3 | fun() -> 4 | case file:read_file("/etc/centos-release") of 5 | {ok, <<"CentOS release 6", _/binary >>} -> 6 | true; 7 | _ -> 8 | false 9 | end 10 | end, 11 | 12 | IsWin32 = 13 | fun() -> 14 | win32 =:= element(1, os:type()) 15 | end, 16 | 17 | IsDarwin = 18 | fun() -> 19 | {unix, darwin} =:= os:type() 20 | end, 21 | 22 | CanBuildDocs = 23 | fun() -> 24 | "true" =:= os:getenv("CAN_BUILD_DOCS", "true") 25 | end, 26 | 27 | Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.111"}}}, 28 | 29 | IsQuicSupp = not (IsCentos6() orelse IsWin32() orelse IsDarwin() orelse 30 | false =/= os:getenv("BUILD_WITHOUT_QUIC") 31 | ), 32 | 33 | CopyDocsSteps = 34 | case CanBuildDocs() of 35 | true -> 36 | [ {mkdir, "doc"} 37 | , {copy, "doc/html/", "doc/html"} 38 | , {copy, "doc/info/", "doc/info"} 39 | ]; 40 | false -> 41 | [] 42 | end, 43 | 44 | Profiles = 45 | {profiles, 46 | [ {escript, []} 47 | , {test, 48 | [{deps, [ {proper, "1.3.0"} 49 | ]}]} 50 | , {emqttb, 51 | [{relx, [ {release, {emqttb, git}, [ kernel 52 | , stdlib 53 | , syntax_tools 54 | , xmerl 55 | , mnesia 56 | , emqtt 57 | , gun 58 | , cowlib 59 | , lee 60 | , system_monitor 61 | | [ quicer || IsQuicSupp ] 62 | ] } 63 | , {overlay_vars_values, [ {runner_root_dir, "$(cd $(dirname $(readlink $0 || echo $0))/..; pwd -P)"} 64 | , {runner_escript_dir, "$RUNNER_ROOT_DIR/escript"} 65 | ]} 66 | , {overlay, [ {mkdir, "bin"} 67 | , {mkdir, "escript"} 68 | , {copy, "_build/emqttb/bin", "escript"} 69 | , {copy, "bin/emqttb","bin/emqttb"} 70 | , {template,"bin/emqttb","bin/emqttb"} 71 | | CopyDocsSteps 72 | ]} 73 | , {dev_mode, false} 74 | , {include_src, false} 75 | , {include_erts, true} 76 | , {extended_start_script, false} 77 | , {generate_start_script, false} 78 | , {sys_config, false} 79 | , {vm_args, false} 80 | ]} 81 | ]} 82 | ]}, 83 | 84 | ExtraDeps = 85 | fun(C) -> 86 | {deps, Deps0} = lists:keyfind(deps, 1, C), 87 | Deps = Deps0 ++ 88 | [ Quicer || IsQuicSupp ], 89 | lists:keystore(deps, 1, C, {deps, Deps}) 90 | end, 91 | 92 | NewConfig = 93 | [ {escript_incl_apps, 94 | [emqttb, gproc | 95 | [ quicer || IsQuicSupp ] 96 | ]} 97 | , Profiles 98 | | ExtraDeps(CONFIG)], 99 | %io:format("New Config: ~p~n", [NewConfig]), 100 | NewConfig. 101 | -------------------------------------------------------------------------------- /scripts/docgen.escript: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -pa _build/default/lib/lee/ebin -pa _build/default/lib/typerefl/ebin -pa _build/default/lib/emqttb/ebin 3 | %% -*- erlang -*- 4 | 5 | -mode(compile). 6 | 7 | -include_lib("lee/include/lee.hrl"). 8 | 9 | main(OutDir) -> 10 | ExtractorConfig = #{ output_dir => OutDir 11 | , extension => ".texi" 12 | , formatter => fun texinfo/3 13 | , metatypes => [cli_param, value, os_env, autorate, metric] 14 | }, 15 | ok = emqttb_conf:load_model(), 16 | [_|_] = lee_doc:make_docs(emqttb_conf:model(), ExtractorConfig). 17 | 18 | texinfo(Options, FD, L) when is_list(L) -> 19 | [texinfo(Options, FD, I) || I <- L], 20 | ok; 21 | texinfo(Options, FD, Doclet) -> 22 | P = fun(L) -> io:put_chars(FD, L) end, 23 | case Doclet of 24 | %% Autorate 25 | #doclet{mt = autorate, tag = container, data = Children} -> 26 | P(["@table @code\n"]), 27 | texinfo(Options, FD, Children), 28 | P(["@end table\n"]); 29 | #doclet{mt = autorate, tag = autorate, key = Key, data = Title} -> 30 | P(["@anchor{", lee_doc:texi_key([autorate | Key]), "}\n"]), 31 | P(["@item ", Title, $\n]); 32 | %% Metric 33 | #doclet{mt = metric, tag = container, data = Children} -> 34 | P(["@itemize\n"]), 35 | texinfo(Options, FD, Children), 36 | P(["@end itemize\n"]); 37 | #doclet{mt = metric, tag = metric, key = Key} -> 38 | P(["@item\n@anchor{", lee_doc:texi_key([metric | Key]), "}\n"]), 39 | P(["@verbatim\n", lee_lib:term_to_string(Key), "\n@end verbatim\n"]); 40 | #doclet{mt = metric, tag = type, data = Data} -> 41 | P(["@b{Type}: ", Data, "\n\n"]); 42 | #doclet{mt = metric, tag = labels, data = Data} -> 43 | P("@b{Prometheus labels}: "), 44 | P(lists:join(", ", Data)), 45 | P("\n\n"); 46 | #doclet{mt = metric, tag = prometheus_id, data = Data} -> 47 | P(["@b{Prometheus name}: @code{", Data, "}\n\n"]); 48 | _ -> 49 | lee_doc:texinfo(Options, FD, Doclet) 50 | end. 51 | -------------------------------------------------------------------------------- /scripts/rename-package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." 6 | 7 | UNAME="$(uname -s)" 8 | 9 | case "$UNAME" in 10 | Darwin) 11 | DIST='macos' 12 | VERSION_ID=$(sw_vers -productVersion | cut -d '.' -f 1) 13 | SYSTEM="${DIST}${VERSION_ID}" 14 | ;; 15 | Linux) 16 | DIST="$(sed -n '/^ID=/p' /etc/os-release | sed -r 's/ID=(.*)/\1/g' | sed 's/"//g')" 17 | VERSION_ID="$(sed -n '/^VERSION_ID=/p' /etc/os-release | sed -r 's/VERSION_ID=(.*)/\1/g' | sed 's/"//g')" 18 | SYSTEM="$(echo "${DIST}${VERSION_ID}" | sed -r 's/([a-zA-Z]*)-.*/\1/g')" 19 | ;; 20 | CYGWIN*|MSYS*|MINGW*) 21 | SYSTEM="windows" 22 | ;; 23 | esac 24 | 25 | ARCH="$(uname -m)" 26 | case "$ARCH" in 27 | x86_64) 28 | ARCH='amd64' 29 | ;; 30 | aarch64) 31 | ARCH='arm64' 32 | ;; 33 | arm*) 34 | ARCH=arm 35 | ;; 36 | esac 37 | 38 | BASE=$(find ./_build/emqttb/rel/emqttb -name "*.tar.gz" | tail -1) 39 | VSN="$(echo "$BASE" | sed -E -e 's|.+emqttb-(.+)\.tar\.gz|\1|')" 40 | QUIC=$(find ./_build/emqttb/rel/emqttb -name "quicer-*" | grep -q quicer && echo '-quic' || echo '') 41 | cp "$BASE" "./emqttb-${VSN}-${SYSTEM}-${ARCH}${QUIC}.tar.gz" 42 | -------------------------------------------------------------------------------- /src/behaviors/emqttb_behavior_conn.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_behavior_conn). 17 | 18 | -behavior(emqttb_worker). 19 | 20 | %% API: 21 | -export([model/1]). 22 | 23 | %% behavior callbacks: 24 | -export([init_per_group/2, init/1, handle_message/3, terminate/2]). 25 | 26 | -export_type([prototype/0, config/0]). 27 | 28 | %%================================================================================ 29 | %% Type declarations 30 | %%================================================================================ 31 | 32 | -type config() :: #{ expiry => non_neg_integer() 33 | , clean_start => boolean() 34 | , metrics := lee:model_key() 35 | }. 36 | 37 | -type prototype() :: {?MODULE, config()}. 38 | 39 | %%================================================================================ 40 | %% API 41 | %%================================================================================ 42 | 43 | model(GroupId) -> 44 | #{ conn_latency => 45 | emqttb_metrics:opstat(GroupId, connect) 46 | }. 47 | 48 | %%================================================================================ 49 | %% behavior callbacks 50 | %%================================================================================ 51 | 52 | init_per_group(_Group, Opts = #{metrics := MetricsKey}) -> 53 | Defaults = #{ expiry => 0 54 | , clean_start => true 55 | }, 56 | Config = maps:merge(Defaults, Opts), 57 | Config#{ conn_opstat => emqttb_metrics:opstat_from_model(MetricsKey ++ [conn_latency]) 58 | }. 59 | 60 | init(#{clean_start := CleanStart, expiry := Expiry, conn_opstat := ConnOpstat}) -> 61 | Props = case Expiry of 62 | undefined -> #{}; 63 | _ -> #{'Session-Expiry-Interval' => Expiry} 64 | end, 65 | {ok, Conn} = emqttb_worker:connect(ConnOpstat, Props, [{clean_start, CleanStart}], [], []), 66 | Conn. 67 | 68 | handle_message(_, Conn, _) -> 69 | {ok, Conn}. 70 | 71 | terminate(_Shared, Conn) -> 72 | emqtt:disconnect(Conn), 73 | emqtt:stop(Conn). 74 | 75 | %%================================================================================ 76 | %% Internal functions 77 | %%================================================================================ 78 | -------------------------------------------------------------------------------- /src/behaviors/emqttb_behavior_pub.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2025 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_behavior_pub). 17 | 18 | -behavior(emqttb_worker). 19 | 20 | %% API 21 | -export([parse_metadata/1, model/1]). 22 | 23 | %% behavior callbacks: 24 | -export([init_per_group/2, init/1, handle_message/3, terminate/2]). 25 | 26 | -export_type([prototype/0, config/0]). 27 | 28 | -import(emqttb_worker, [send_after/2, send_after_rand/2, repeat/2, 29 | my_group/0, my_id/0, my_clientid/0, my_cfg/1, connect/2]). 30 | 31 | -include("../framework/emqttb_internal.hrl"). 32 | 33 | %%================================================================================ 34 | %% Type declarations 35 | %%================================================================================ 36 | 37 | -type config() :: #{ topic := binary() 38 | , pubinterval := lee:model_key() 39 | , msg_size := non_neg_integer() 40 | , metrics := lee:model_key() 41 | , qos := emqttb:qos() 42 | , retain => boolean() 43 | , metadata => boolean() 44 | , host_shift => integer() 45 | , random => boolean() 46 | , host_selection => random | round_robin 47 | , clean_start => boolean() 48 | , expiry => non_neg_integer() | undefined 49 | }. 50 | 51 | -type prototype() :: {?MODULE, config()}. 52 | 53 | %%================================================================================ 54 | %% API 55 | %%================================================================================ 56 | 57 | -spec model(atom()) -> lee:lee_module(). 58 | model(Group) -> 59 | #{ conn_latency => 60 | emqttb_metrics:opstat(Group, connect) 61 | , pub_latency => 62 | emqttb_metrics:opstat(Group, publish) 63 | , n_published => 64 | {[metric], 65 | #{ oneliner => "Total number of messages published by the group" 66 | , id => {emqttb_published_messages, Group} 67 | , labels => [group] 68 | , metric_type => counter 69 | }} 70 | }. 71 | 72 | -spec parse_metadata(Msg) -> {ID, SeqNo, TS} 73 | when Msg :: binary(), 74 | ID :: integer(), 75 | SeqNo :: non_neg_integer(), 76 | TS :: integer(). 77 | parse_metadata(<>) -> 78 | {ID, SeqNo, TS}. 79 | 80 | %%================================================================================ 81 | %% behavior callbacks 82 | %%================================================================================ 83 | 84 | init_per_group(Group, 85 | #{ topic := Topic 86 | , pubinterval := PubInterval 87 | , msg_size := MsgSize 88 | , qos := QoS 89 | , metrics := MetricsKey 90 | } = Conf) when is_binary(Topic), 91 | is_integer(MsgSize) -> 92 | AddMetadata = maps:get(metadata, Conf, false), 93 | PubRate = emqttb_autorate:get_counter(emqttb_autorate:from_model(PubInterval)), 94 | MetadataSize = case AddMetadata of 95 | true -> (32 + 64 + 64) div 8; 96 | false -> 0 97 | end, 98 | HostShift = maps:get(host_shift, Conf, 0), 99 | HostSelection = maps:get(host_selection, Conf, random), 100 | Retain = maps:get(retain, Conf, false), 101 | Size = max(0, MsgSize - MetadataSize), 102 | #{ topic => Topic 103 | , message => message(Size) 104 | , size => Size 105 | , pub_opts => [{qos, QoS}, {retain, Retain}] 106 | , pubinterval => PubRate 107 | , metadata => AddMetadata 108 | , host_shift => HostShift 109 | , host_selection => HostSelection 110 | , random => maps:get(random, Conf, false) 111 | , expiry => maps:get(expiry, Conf, undefined) 112 | , clean_start => maps:get(clean_start, Conf, true) 113 | , pub_opstat => emqttb_metrics:opstat_from_model(MetricsKey ++ [pub_latency]) 114 | , conn_opstat => emqttb_metrics:opstat_from_model(MetricsKey ++ [conn_latency]) 115 | , pub_counter => emqttb_metrics:from_model(MetricsKey ++ [n_published]) 116 | }. 117 | 118 | init(PubOpts = #{pubinterval := I, conn_opstat := ConnOpstat, 119 | expiry := Expiry, clean_start := CleanStart}) -> 120 | {SleepTime, N} = emqttb:get_duration_and_repeats(I), 121 | send_after_rand(SleepTime, {publish, N}), 122 | HostShift = maps:get(host_shift, PubOpts, 0), 123 | HostSelection = maps:get(host_selection, PubOpts, random), 124 | Props = case Expiry of 125 | undefined -> #{}; 126 | _ -> #{'Session-Expiry-Interval' => Expiry} 127 | end, 128 | {ok, Conn} = emqttb_worker:connect(ConnOpstat, Props#{host_shift => HostShift, host_selection => HostSelection}, 129 | [{clean_start, CleanStart}], [], []), 130 | Conn. 131 | 132 | handle_message(Shared, Conn, {publish, N1}) -> 133 | #{ topic := TP, pubinterval := I, message := Msg0, pub_opts := PubOpts 134 | , pub_counter := PubCounter 135 | , pub_opstat := PubOpstat 136 | , metadata := AddMetadata 137 | , random := Random 138 | , size := Size 139 | } = Shared, 140 | {SleepTime, N2} = emqttb:get_duration_and_repeats(I), 141 | send_after(SleepTime, {publish, N2}), 142 | Msg = if AddMetadata andalso Random -> [message_metadata(), rand:bytes(Size)]; 143 | Random -> rand:bytes(Size); 144 | AddMetadata -> [message_metadata(), Msg0]; 145 | true -> Msg0 146 | end, 147 | T = emqttb_worker:format_topic(TP), 148 | repeat(N1, fun() -> 149 | emqttb_metrics:call_with_counter(PubOpstat, emqtt, publish, [Conn, T, Msg, PubOpts]), 150 | emqttb_metrics:counter_inc(PubCounter, 1) 151 | end), 152 | {ok, Conn}; 153 | handle_message(_, Conn, _) -> 154 | {ok, Conn}. 155 | 156 | terminate(_Shared, Conn) -> 157 | emqtt:disconnect(Conn). 158 | 159 | %%================================================================================ 160 | %% Internal functions 161 | %%================================================================================ 162 | 163 | message(Size) -> 164 | list_to_binary([$A || _ <- lists:seq(1, Size)]). 165 | 166 | message_metadata() -> 167 | SeqNo = msg_seqno(), 168 | ID = erlang:phash2({node(), self()}), 169 | TS = os:system_time(microsecond), 170 | <>. 171 | 172 | msg_seqno() -> 173 | case get(emqttb_behavior_pub_seqno) of 174 | undefined -> N = 0; 175 | N -> ok 176 | end, 177 | put(emqttb_behavior_pub_seqno, N + 1), 178 | N. 179 | -------------------------------------------------------------------------------- /src/behaviors/emqttb_behavior_sub.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2025 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_behavior_sub). 17 | 18 | -behavior(emqttb_worker). 19 | 20 | %% API: 21 | -export([model/1]). 22 | 23 | %% behavior callbacks: 24 | -export([init_per_group/2, init/1, handle_message/3, terminate/2]). 25 | 26 | -export_type([prototype/0, config/0]). 27 | 28 | %%================================================================================ 29 | %% Type declarations 30 | %%================================================================================ 31 | 32 | -type config() :: #{ topic := binary() 33 | , qos := 0..2 34 | , metrics := lee:model_key() 35 | , clean_start => boolean() 36 | , expiry => non_neg_integer() | undefined 37 | , host_shift => integer() 38 | , host_selection => _ 39 | , parse_metadata => boolean() 40 | , verify_sequence => boolean() 41 | }. 42 | 43 | -type prototype() :: {?MODULE, config()}. 44 | 45 | -type sequence() :: {_From :: binary(), _To :: binary(), _Topic :: binary()}. 46 | 47 | -define(seq_tab, emqttb_behavior_sub_seq_tab). 48 | 49 | %%================================================================================ 50 | %% API 51 | %%================================================================================ 52 | 53 | -spec model(atom()) -> lee:lee_module(). 54 | model(GroupId) -> 55 | #{ n_received => 56 | {[metric], 57 | #{ oneliner => "Total number of received messages" 58 | , id => {emqttb_received_messages, GroupId} 59 | , metric_type => counter 60 | , labels => [group] 61 | }} 62 | , conn_latency => 63 | emqttb_metrics:opstat(GroupId, 'connect') 64 | , sub_latency => 65 | emqttb_metrics:opstat(GroupId, 'subscribe') 66 | 67 | , number_of_gaps => 68 | {[metric], 69 | #{ oneliner => "Number of gaps in the sequence numbers" 70 | , metric_type => counter 71 | , id => {emqttb_gaps_number, GroupId} 72 | , labels => [group] 73 | }} 74 | , gap_size => 75 | {[metric], 76 | #{ oneliner => "Average size of the gap in the sequence numbers" 77 | , metric_type => rolling_average 78 | , id => {emqttb_gap_size, GroupId} 79 | , labels => [group] 80 | }} 81 | 82 | , number_of_repeats => 83 | {[metric], 84 | #{ oneliner => "Number of repeats of the sequence numbers" 85 | , metric_type => counter 86 | , id => {emqttb_repeats_number, GroupId} 87 | , labels => [group] 88 | }} 89 | , repeat_size => 90 | {[metric], 91 | #{ oneliner => "Average size of the repeated sequence of seqence numbers" 92 | , metric_type => rolling_average 93 | , id => {emqttb_repeat_size, GroupId} 94 | , labels => [group] 95 | }} 96 | , n_streams => 97 | {[metric], 98 | #{ oneliner => "Number of sequences" 99 | , metric_type => gauge 100 | , id => {emqttb_n_sequences, GroupId} 101 | , labels => [group] 102 | }} 103 | 104 | , e2e_latency => 105 | {[metric], 106 | #{ oneliner => "End-to-end latency" 107 | , id => {emqttb_e2e_latency, GroupId} 108 | , metric_type => rolling_average 109 | , labels => [group] 110 | , unit => "microsecond" 111 | }} 112 | }. 113 | 114 | %%================================================================================ 115 | %% behavior callbacks 116 | %%================================================================================ 117 | 118 | init_per_group(_Group, 119 | #{ topic := Topic 120 | , qos := _QoS 121 | , metrics := MetricsModelKey 122 | } = Opts) when is_binary(Topic) -> 123 | ParseMetadata = maps:get(parse_metadata, Opts, false) orelse 124 | maps:get(verify_sequence, Opts, false), 125 | Defaults = #{ expiry => 0 126 | , clean_start => true 127 | , host_shift => 0 128 | , host_selection => random 129 | , parse_metadata => ParseMetadata 130 | , verify_sequence => false 131 | }, 132 | NStreams = emqttb_metrics:from_model(MetricsModelKey ++ [n_streams]), 133 | emqttb_metrics:gauge_set(NStreams, 0), 134 | Conf = maps:merge(Defaults, Opts), 135 | ensure_sequence_table(), 136 | Conf#{ conn_opstat => emqttb_metrics:opstat_from_model(MetricsModelKey ++ [conn_latency]) 137 | , sub_opstat => emqttb_metrics:opstat_from_model(MetricsModelKey ++ [sub_latency]) 138 | , e2e_latency => emqttb_metrics:from_model(MetricsModelKey ++ [e2e_latency]) 139 | , sub_counter => emqttb_metrics:from_model(MetricsModelKey ++ [n_received]) 140 | , number_of_gaps => emqttb_metrics:from_model(MetricsModelKey ++ [number_of_gaps]) 141 | , gap_size => emqttb_metrics:from_model(MetricsModelKey ++ [gap_size]) 142 | , number_of_repeats => emqttb_metrics:from_model(MetricsModelKey ++ [number_of_repeats]) 143 | , repeat_size => emqttb_metrics:from_model(MetricsModelKey ++ [repeat_size]) 144 | , n_streams => NStreams 145 | }. 146 | 147 | init(SubOpts0 = #{ topic := T 148 | , qos := QoS 149 | , expiry := Expiry 150 | , clean_start := CleanStart 151 | , conn_opstat := ConnOpstat 152 | , sub_opstat := SubOpstat 153 | }) -> 154 | SubOpts = maps:with([host_shift, host_selection], SubOpts0), 155 | Props = case Expiry of 156 | undefined -> SubOpts#{}; 157 | _ -> SubOpts#{'Session-Expiry-Interval' => Expiry} 158 | end, 159 | {ok, Conn} = emqttb_worker:connect(ConnOpstat, Props, [{clean_start, CleanStart}], [], []), 160 | emqttb_metrics:call_with_counter(SubOpstat, emqtt, subscribe, [Conn, emqttb_worker:format_topic(T), QoS]), 161 | Conn. 162 | 163 | handle_message(#{ parse_metadata := ParseMetadata, verify_sequence := VerifySequence, 164 | sub_counter := SubCnt, e2e_latency := E2ELatency 165 | } = Conf, 166 | Conn, 167 | {publish, #{client_pid := Pid, payload := Payload, topic := Topic}} 168 | ) when Pid =:= Conn -> 169 | emqttb_metrics:counter_inc(SubCnt, 1), 170 | case ParseMetadata of 171 | true -> 172 | {Id, SeqNo, TS} = emqttb_behavior_pub:parse_metadata(Payload), 173 | Dt = os:system_time(microsecond) - TS, 174 | emqttb_metrics:rolling_average_observe(E2ELatency, Dt), 175 | case VerifySequence of 176 | true -> 177 | verify_sequence(Conf, Id, Topic, SeqNo); 178 | false -> 179 | ok 180 | end; 181 | false -> 182 | ok 183 | end, 184 | {ok, Conn}; 185 | handle_message(_, Conn, _) -> 186 | {ok, Conn}. 187 | 188 | terminate(_Shared, Conn) -> 189 | emqtt:disconnect(Conn), 190 | emqtt:stop(Conn). 191 | 192 | %%================================================================================ 193 | %% Internal functions 194 | %%================================================================================ 195 | 196 | verify_sequence(#{ number_of_gaps := NGaps, gap_size := GapSize, number_of_repeats := NRepeats 197 | , repeat_size := RepeatSize, n_streams := NStreams}, 198 | From, Topic, SeqNo) -> 199 | Key = {From, emqttb_worker:my_id(), Topic}, 200 | case ets:lookup(?seq_tab, Key) of 201 | [] -> 202 | emqttb_metrics:counter_inc(NStreams, 1), 203 | ok; 204 | [{_, OldSeqNo}] when SeqNo =:= OldSeqNo + 1 -> 205 | ok; 206 | [{_, OldSeqNo}] when SeqNo > OldSeqNo -> 207 | logger:warning("Gap detected: ~p ~p; ~p", [OldSeqNo, SeqNo, Key]), 208 | emqttb_metrics:counter_inc(NGaps, 1), 209 | emqttb_metrics:rolling_average_observe(GapSize, SeqNo - OldSeqNo - 1); 210 | [{_, OldSeqNo}] -> 211 | logger:info("Repeat detected: ~p ~p; ~p", [OldSeqNo, SeqNo, Key]), 212 | emqttb_metrics:counter_inc(NRepeats, 1), 213 | emqttb_metrics:rolling_average_observe(RepeatSize, OldSeqNo - SeqNo + 1) 214 | end, 215 | ets:insert(?seq_tab, {Key, SeqNo}). 216 | 217 | ensure_sequence_table() -> 218 | catch ets:new(?seq_tab, 219 | [ named_table 220 | , set 221 | , public 222 | , {write_concurrency, true} 223 | , {read_concurrency, true} 224 | , {heir, whereis(emqttb_metrics), ?seq_tab} 225 | ]), 226 | ok. 227 | -------------------------------------------------------------------------------- /src/conf/emqttb_conf.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023, 2025 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_conf). 17 | 18 | %% API: 19 | -export([load_model/0, model/0, load_conf/0, get/1, list_keys/1, reload/0, patch/1, string2patch/1]). 20 | -export([compile_model/1]). 21 | 22 | -export_type([]). 23 | 24 | -include("emqttb.hrl"). 25 | -include_lib("typerefl/include/types.hrl"). 26 | 27 | 28 | %%================================================================================ 29 | %% Type declarations 30 | %%================================================================================ 31 | 32 | %%================================================================================ 33 | %% API funcions 34 | %%================================================================================ 35 | 36 | model() -> 37 | ?MYMODEL. 38 | 39 | load_model() -> 40 | case compile_model(emqttb_conf_model:model()) of 41 | {ok, Model} -> 42 | persistent_term:put(?MODEL_STORE, Model), 43 | ok; 44 | {error, Errors} -> 45 | logger:critical("Configuration model is invalid!"), 46 | [logger:critical(E) || E <- Errors], 47 | emqttb:setfail("invalid configuration model"), 48 | emqttb:terminate() 49 | end. 50 | 51 | load_conf() -> 52 | Storage = lee_storage:new(lee_persistent_term_storage, ?CONF_STORE), 53 | case lee:init_config(?MYMODEL, Storage) of 54 | {ok, _Data, _Warnings} -> 55 | maybe_load_repeat(), 56 | maybe_load_conf_file(), 57 | maybe_dump_conf(), 58 | ok; 59 | {error, Errors, Warnings} -> 60 | [logger:critical(E) || E <- Errors], 61 | [logger:warning(E) || E <- Warnings], 62 | emqttb:setfail("invalid configuration"), 63 | emqttb:terminate() 64 | end. 65 | 66 | compile_model(Model) -> 67 | MTs = metamodel(), 68 | lee_model:compile(MTs, [Model]). 69 | 70 | reload() -> 71 | logger:notice("Reloading configuration"), 72 | case lee:init_config(?MYMODEL, ?MYCONF) of 73 | {ok, _, _} -> true; 74 | _ -> false 75 | end. 76 | 77 | patch(Patch) -> 78 | logger:debug("Patching configuration: ~p", [Patch]), 79 | case lee:patch(?MYMODEL, ?MYCONF, Patch) of 80 | {ok, _, _} -> true; 81 | _ -> false 82 | end. 83 | 84 | -spec get(lee:key()) -> term(). 85 | get(Key) -> 86 | ?CFG(Key). 87 | 88 | -spec list_keys(lee:model_key()) -> [lee:key()]. 89 | list_keys(Key) -> 90 | ?CFG_LIST(Key). 91 | 92 | %%================================================================================ 93 | %% Internal functions 94 | %%================================================================================ 95 | 96 | maybe_dump_conf() -> 97 | case {?CFG([convenience, conf_dump]), ?CFG([convenience, again])} of 98 | {Filename, false} when is_list(Filename) -> 99 | Dump = lee_storage:dump(?MYCONF), 100 | {ok, FD} = file:open(Filename, [write]), 101 | [ok = io:format(FD, "~p.~n", [I]) || I <- Dump], 102 | ok = file:close(FD); 103 | _ -> 104 | ok 105 | end. 106 | 107 | maybe_load_repeat() -> 108 | case {?CFG([convenience, conf_dump]), ?CFG([convenience, again])} of 109 | {Filename, true} when is_list(Filename) -> 110 | case file:consult(Filename) of 111 | {ok, Patch} -> 112 | lee:patch(?MYMODEL, ?MYCONF, Patch); 113 | _ -> 114 | ok 115 | end; 116 | _ -> 117 | ok 118 | end. 119 | 120 | maybe_load_conf_file() -> 121 | case ?CFG([convenience, conf_file]) of 122 | undefined -> 123 | ok; 124 | File -> 125 | {ok, _, _} = lee_config_file:read_to(?MYMODEL, ?MYCONF, File) 126 | end. 127 | 128 | 129 | metamodel() -> 130 | [ lee:base_metamodel() 131 | , lee_metatype:create(lee_os_env, #{prefix => "EMQTTB_", priority => 0}) 132 | , lee_metatype:create(lee_app_env) 133 | , lee_metatype:create(lee_logger) 134 | , lee_metatype:create(lee_cli, 135 | #{ cli_opts => fun cli_args_getter/0 136 | , priority => 10 137 | }) 138 | , lee_metatype:create(lee_config_file, 139 | #{ tag => system_wide_conf 140 | , priority => -110 141 | , file => "/etc/emqttb/emqttb.conf" 142 | }) 143 | , lee_metatype:create(emqttb_scenario) 144 | , lee_metatype:create(emqttb_metrics) 145 | , lee_metatype:create(emqttb_autorate) 146 | ]. 147 | 148 | cli_args_getter() -> 149 | application:get_env(emqttb, cli_args, []). 150 | 151 | string2patch(Str) -> 152 | %% Lazy attempt to parse string to a list of command line arguments 153 | {match, L0} = re:run(Str, "'(.+)'|([^ ]+) *", [global, {capture, all_but_first, list}]), 154 | L = lists:map(fun lists:append/1, L0), 155 | {ok, Patch} = lee_cli:read(?MYMODEL, L), 156 | Patch. 157 | 158 | %% maybe_show_help_and_exit() -> 159 | %% ?CFG([help]) 160 | %% andalso open_port({spawn, "man -l docs/EMQTT\\ bench\\ daemon.man"}, [nouse_stdio, out]), 161 | %% emqttb:setfail(), 162 | %% emqttb:terminate(). 163 | -------------------------------------------------------------------------------- /src/conf/emqttb_conf_model.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023, 2025 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_conf_model). 17 | 18 | %% API: 19 | -export([model/0]). 20 | 21 | -export_type([object_type/0]). 22 | 23 | -include("emqttb.hrl"). 24 | -include_lib("typerefl/include/types.hrl"). 25 | 26 | %%================================================================================ 27 | %% Type declarations 28 | %%================================================================================ 29 | 30 | -type object_type() :: autorate | metric. 31 | -reflect_type([object_type/0]). 32 | 33 | %%================================================================================ 34 | %% API funcions 35 | %%================================================================================ 36 | 37 | model() -> 38 | #{ cluster => 39 | #{ node_name => 40 | {[value, os_env], 41 | #{ oneliner => "Node name" 42 | , doc => "Note: erlang distribution is disabled when node name is @code{undefined}.\n" 43 | , type => atom() 44 | , default => undefined 45 | }} 46 | } 47 | , interval => 48 | {[value, cli_param], 49 | #{ oneliner => "Default interval between events" 50 | , doc => "@doc-interval" 51 | , type => emqttb:duration_us() 52 | , default_str => "10ms" 53 | , cli_operand => "max-rate" 54 | , cli_short => $R 55 | }} 56 | , n_clients => 57 | {[value, cli_param], 58 | #{ oneliner => "Maximum number of clients used by default by all groups" 59 | , type => emqttb:n_clients() 60 | , default => 1000 61 | , cli_operand => "max-clients" 62 | , cli_short => $N 63 | }} 64 | , inet => 65 | #{ reuseaddr => 66 | {[value, os_env], 67 | #{ oneliner => "Enable SO_REUSEADDR option for the TCP sockets" 68 | , type => boolean() 69 | , default => true 70 | }} 71 | } 72 | , restapi => 73 | #{ enabled => 74 | {[value, os_env, cli_param], 75 | #{ oneliner => "Enable REST API" 76 | , doc => "@option{--restapi} CLI argument enables REST API 77 | (available at @url{http://127.0.0.0:8017} by default), 78 | and it also means that the script keeps running after completing the scenarios. 79 | " 80 | , type => boolean() 81 | , default => false 82 | , cli_operand => "restapi" 83 | }} 84 | , listen_port => 85 | {[value, os_env, cli_param], 86 | #{ oneliner => "REST API listening interface/port" 87 | , type => typerefl:listen_port_ip4() 88 | , default_str => "0.0.0.0:8017" 89 | , cli_operand => "rest-listen" 90 | }} 91 | , tls => 92 | {[value, os_env], 93 | #{ oneliner => "Enable TLS for REST listener" 94 | , type => boolean() 95 | , default => false 96 | }} 97 | } 98 | , logging => 99 | #{ level => 100 | {[value, os_env, cli_param, logger_level], 101 | #{ oneliner => "Global log level" 102 | , type => lee_logger:level() 103 | , default => notice 104 | , cli_operand => "log-level" 105 | }} 106 | , default_handler_level => 107 | {[value, os_env, logger_level], 108 | #{ oneliner => "Log level for the default handler" 109 | , type => lee_logger:level() 110 | , default_ref => [logging, level] 111 | , logger_handler => default 112 | }} 113 | , client_handler_level => 114 | {[value, os_env, logger_level], 115 | #{ oneliner => "Log level for the MQTT clients and workers" 116 | , type => lee_logger:level() 117 | , logger_handler => client 118 | , default_ref => [logging, level] 119 | }} 120 | , directory => 121 | {[value, os_env], 122 | #{ oneliner => "Directory for the logs" 123 | , type => string() 124 | , default => "/tmp/" 125 | }} 126 | } 127 | , metrics => 128 | #{ pushgateway => 129 | #{ url => 130 | {[value, cli_param, os_env], 131 | #{ oneliner => "URL of pushgateway server" 132 | , type => string() 133 | , default => "http://localhost:9091" 134 | , cli_operand => "pushgw-url" 135 | }} 136 | , enabled => 137 | {[value, cli_param, os_env], 138 | #{ oneliner => "Enable sending metrics to pushgateway" 139 | , type => boolean() 140 | , default => false 141 | , cli_operand => "pushgw" 142 | }} 143 | , interval => 144 | {[value, cli_param, os_env], 145 | #{ oneliner => "Push interval (ms)" 146 | , type => emqttb:duration_ms() 147 | , default_str => "1s" 148 | , cli_operand => "pushgw-interval" 149 | }} 150 | } 151 | , grafana => 152 | #{ url => 153 | {[value, cli_param, os_env], 154 | #{ oneliner => "URL of Grafana server" 155 | , type => string() 156 | , default => "http://localhost:3000" 157 | , cli_operand => "grafana-url" 158 | }} 159 | , enabled => 160 | {[value, cli_param, os_env], 161 | #{ oneliner => "Add annotations to Grafana" 162 | , type => boolean() 163 | , default => false 164 | , cli_operand => "grafana" 165 | }} 166 | , login => 167 | {[value, cli_param, os_env], 168 | #{ oneliner => "Grafana login" 169 | , type => union(false, string()) 170 | , default => false 171 | , cli_operand => "grafana-login" 172 | }} 173 | , password => 174 | {[value, os_env], 175 | #{ oneliner => "Grafana password" 176 | , type => string() 177 | , default => "" 178 | }} 179 | , api_key => 180 | {[value, os_env], 181 | #{ oneliner => "Grafana API key" 182 | , type => union(false, string()) 183 | , default => false 184 | }} 185 | , motto => 186 | {[value, os_env, cli_param], 187 | #{ oneliner => "Add Grafana annotation at the end of the run" 188 | , type => string() 189 | , default => "" 190 | , cli_operand => "motto" 191 | }} 192 | , motto_tags => 193 | {[value, os_env, cli_param], 194 | #{ oneliner => "Custom tags added to the Grafana annotation span" 195 | , type => emqttb_grafana:tags() 196 | , default => [] 197 | , cli_operand => "motto-tags" 198 | }} 199 | } 200 | } 201 | , convenience => 202 | #{ again => 203 | {[value, cli_param], 204 | #{ oneliner => "Repeat the last execution" 205 | , doc => "Note: it tries best to restore the previous environment, 206 | so it only makes sense to use this option alone, as 207 | it overrides other options. 208 | " 209 | , type => boolean() 210 | , default => false 211 | , cli_operand => "again" 212 | }} 213 | , conf_dump => 214 | {[value, os_env, cli_param], 215 | #{ oneliner => "Name of the repeat file or `undefined`" 216 | , doc => "If set to a string value, emqttb will dump its configuration 217 | to a \"repeat\" file that can be used to quickly repeat the last run. 218 | 219 | Note: only the successful runs of the script are saved. 220 | " 221 | , type => union([undefined, string()]) 222 | , default => ".emqttb.repeat" 223 | , cli_operand => "conf-dump-file" 224 | }} 225 | , conf_file => 226 | {[value, cli_param, undocumented], %% Currently scuffed 227 | #{ oneliner => "Read configuration from a file" 228 | , type => union(string(), undefined) 229 | , default => undefined 230 | , cli_operand => "conf" 231 | }} 232 | , loiter => 233 | {[value, os_env, cli_param], 234 | #{ oneliner => "Default loiter time for the scenarios (sec)" 235 | , type => emqttb:wait_time() 236 | , default => infinity 237 | , cli_operand => "loiter" 238 | , cli_short => $L 239 | }} 240 | , keep_running => 241 | {[value, os_env, cli_param], 242 | #{ oneliner => "Keep the process running after completing all the scenarios" 243 | , doc => "By default, when started without REST API, emqttb script terminates 244 | after completing all the scenarios, which is useful for scripting. 245 | However, when running with REST API, such behavior is undesirable. 246 | So when REST is enabled, the default behavior is different: the 247 | process keeps running waiting for commands. 248 | 249 | This flag can be used to explicitly override this behavior. 250 | " 251 | , type => boolean() 252 | , default_ref => [restapi, enabled] 253 | , cli_operand => "keep-running" 254 | }} 255 | } 256 | , scenarios => emqttb_scenario:model() 257 | , actions => 258 | #{ ls => 259 | {[map, cli_action], 260 | #{ cli_operand => "ls" 261 | , key_elements => [] 262 | , oneliner => "List objects and exit" 263 | }, 264 | #{ what => 265 | {[value, cli_positional], 266 | #{ oneliner => "Type of objects to list" 267 | , type => object_type() 268 | , cli_arg_position => 1 269 | }} 270 | }} 271 | } 272 | , groups => 273 | {[map, cli_action, default_instance], 274 | #{ oneliner => "Client configuration" 275 | , doc => "Client configuration is kept separate from the scenario config. 276 | This is done so scenarios could share client configuration. 277 | " 278 | , cli_operand => "g" 279 | , key_elements => [[id]] 280 | }, 281 | emqttb_worker:model()} 282 | , autorate => 283 | {[map, cli_action], 284 | #{ oneliner => "Autorate configuration" 285 | , doc => "@xref{Autorate}\n" 286 | , cli_operand => "a" 287 | , key_elements => [[id]] 288 | }, 289 | emqttb_autorate:model()} 290 | }. 291 | 292 | %%================================================================================ 293 | %% Internal functions 294 | %%================================================================================ 295 | -------------------------------------------------------------------------------- /src/emqttb.app.src: -------------------------------------------------------------------------------- 1 | {application, emqttb, 2 | [{description, "Load generator for MQTT protocol"}, 3 | {vsn, "git"}, 4 | {registered, []}, 5 | {mod, {emqttb_app, []}}, 6 | {applications, 7 | [kernel, 8 | stdlib, 9 | gproc, 10 | cowboy, 11 | hackney, 12 | prometheus, 13 | jsone, 14 | lee, 15 | snabbkaffe, 16 | xmerl, 17 | gun, 18 | emqtt 19 | ]}, 20 | {env,[]}, 21 | {modules, []}, 22 | 23 | {licenses, ["Apache 2.0"]}, 24 | {links, []} 25 | ]}. 26 | -------------------------------------------------------------------------------- /src/emqttb.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023, 2025 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb). 17 | 18 | %% API: 19 | -export([main/1, terminate/0, setfail/1, get_duration_and_repeats/1, duration_to_sleep/1]). 20 | 21 | %% behavior callbacks: 22 | -export([]). 23 | 24 | %% internal exports: 25 | -export([n_clients/0, parse_hosts/1, 26 | parse_addresses/1, parse_duration_us/1, parse_duration_ms/1, parse_duration_s/1, 27 | parse_byte_size/1, wait_time/0]). 28 | 29 | -export_type([n_clients/0, autorate/0]). 30 | 31 | -include("emqttb.hrl"). 32 | -include_lib("typerefl/include/types.hrl"). 33 | 34 | %%================================================================================ 35 | %% Type declarations 36 | %%================================================================================ 37 | 38 | -type duration_us() :: integer(). 39 | -typerefl_from_string({duration_us/0, ?MODULE, parse_duration_us}). 40 | 41 | -type duration_ms() :: integer(). 42 | -typerefl_from_string({duration_ms/0, ?MODULE, parse_duration_ms}). 43 | 44 | -type duration_s() :: integer(). 45 | -typerefl_from_string({duration_s/0, ?MODULE, parse_duration_s}). 46 | 47 | -type scenario() :: atom(). 48 | 49 | -type stage() :: atom(). 50 | 51 | -type group() :: atom(). 52 | 53 | -type n_clients() :: non_neg_integer(). 54 | 55 | -type autorate() :: atom(). 56 | 57 | -type transport() :: sock | ws | quic. 58 | 59 | -type proto_ver() :: v3 | v4 | v5. 60 | 61 | -type qos() :: 0..2. 62 | 63 | -type ssl_verify() :: verify_peer | verify_none. 64 | 65 | -type net_port() :: 1..65535. 66 | 67 | -type byte_size() :: non_neg_integer(). 68 | -typerefl_from_string({byte_size/0, ?MODULE, parse_byte_size}). 69 | 70 | -type hosts() :: [{string(), net_port()} | string()]. 71 | -typerefl_from_string({hosts/0, ?MODULE, parse_hosts}). 72 | 73 | -type host_selection() :: round_robin | random. 74 | 75 | -type ifaddr_list() :: [inet:ip_address()]. 76 | -typerefl_from_string({ifaddr_list/0, ?MODULE, parse_addresses}). 77 | 78 | -type n_cycles() :: non_neg_integer() | undefined. 79 | 80 | -reflect_type([scenario/0, stage/0, group/0, transport/0, proto_ver/0, qos/0, 81 | net_port/0, hosts/0, ifaddr_list/0, ssl_verify/0, host_selection/0, 82 | duration_ms/0, duration_us/0, duration_s/0, byte_size/0, n_cycles/0]). 83 | 84 | %%================================================================================ 85 | %% API funcions 86 | %%================================================================================ 87 | 88 | %% Escript entrypoint 89 | -spec main([string()]) -> no_return(). 90 | main(Args) -> 91 | application:set_env(emqttb, cli_args, Args), 92 | application:set_env(emqttb, start_time, os:system_time(millisecond)), 93 | {ok, _} = application:ensure_all_started(?APP, permanent), 94 | %% Wait for completion of the scenarios: 95 | MRef = monitor(process, whereis(emqttb_scenarios_sup)), 96 | ?CFG([convenience, keep_running]) orelse 97 | emqttb_scenarios_sup:enable_autostop(), 98 | receive 99 | {'DOWN', MRef, _, _, Reason} -> 100 | Reason =/= shutdown andalso setfail(Reason), 101 | terminate() 102 | end. 103 | 104 | setfail(Reason) -> 105 | application:set_env(emqttb, fail_reason, Reason), 106 | application:set_env(emqttb, is_fail, true). 107 | 108 | %%================================================================================ 109 | %% Internal exports 110 | %%================================================================================ 111 | 112 | wait_time() -> 113 | union(duration_ms(), infinity). 114 | 115 | n_clients() -> 116 | typerefl:range(0, erlang:system_info(process_limit) - 100). 117 | 118 | -spec duration_to_sleep(duration_us()) -> {non_neg_integer(), 1..1000}. 119 | duration_to_sleep(0) -> 120 | {1, 1_000}; 121 | duration_to_sleep(DurationUs) when DurationUs >= 1_000 -> 122 | {DurationUs div 1_000, 1}; 123 | duration_to_sleep(DurationUs) -> 124 | {1, 1_000 div DurationUs}. 125 | 126 | -spec get_duration_and_repeats(counters:counters_ref() | emqttb:duration_us()) -> 127 | {non_neg_integer(), 1..1000}. 128 | get_duration_and_repeats(I) when is_integer(I) -> 129 | duration_to_sleep(I); 130 | get_duration_and_repeats(CRef) -> 131 | get_duration_and_repeats(counters:get(CRef, 1)). 132 | 133 | %%================================================================================ 134 | %% Internal functions 135 | %%================================================================================ 136 | 137 | -spec terminate() -> no_return(). 138 | terminate() -> 139 | annotate_run(), 140 | case application:get_env(emqttb, is_fail, false) of 141 | false -> 142 | timer:sleep(100), %% Ugly: give logger time to flush events... 143 | halt(0); 144 | true -> 145 | Reason = application:get_env(emqttb, fail_reason, ""), 146 | logger:critical("Run unsuccessful due to ~p", [Reason]), 147 | timer:sleep(100), %% Ugly: give logger time to flush events... 148 | halt(1) 149 | end. 150 | 151 | annotate_run() -> 152 | Motto = ?CFG([metrics, grafana, motto]), 153 | Tags = ?CFG([metrics, grafana, motto_tags]), 154 | emqttb_grafana:annotate_range(Motto, Tags, application:get_env(emqttb, start_time, 0), os:system_time(millisecond)). 155 | 156 | parse_addresses(Str) -> 157 | L = [inet:parse_address(I) || I <- string:tokens(Str, ", ")], 158 | case lists:keyfind(error, 1, L) of 159 | false -> 160 | {ok, [I || {ok, I} <- L]}; 161 | _ -> 162 | error 163 | end. 164 | 165 | parse_duration_us(Str) -> 166 | {Int, Unit0} = string:to_integer(Str), 167 | Unit = string:trim(Unit0), 168 | case Unit of 169 | "" -> {ok, Int * 1_000}; 170 | "us" -> {ok, Int}; 171 | "μs" -> {ok, Int}; 172 | "ms" -> {ok, Int * 1_000}; 173 | "s" -> {ok, Int * 1_000_000}; 174 | "min" -> {ok, Int * 60_000_000}; 175 | "h" -> {ok, Int * 3600_000_000}; 176 | _ -> error 177 | end. 178 | 179 | parse_duration_ms(Str) -> 180 | {Int, Unit0} = string:to_integer(Str), 181 | Unit = string:trim(Unit0), 182 | case Unit of 183 | "" -> {ok, Int}; 184 | "ms" -> {ok, Int}; 185 | "s" -> {ok, Int * 1_000}; 186 | "min" -> {ok, Int * 60_000}; 187 | "h" -> {ok, Int * 3600_000}; 188 | _ -> error 189 | end. 190 | 191 | parse_duration_s(Str) -> 192 | {Int, Unit0} = string:to_integer(Str), 193 | Unit = string:trim(Unit0), 194 | case Unit of 195 | "" -> {ok, Int}; 196 | "s" -> {ok, Int}; 197 | "min" -> {ok, Int * 60}; 198 | "h" -> {ok, Int * 3600}; 199 | _ -> error 200 | end. 201 | 202 | parse_byte_size(Str) -> 203 | {Int, Unit0} = string:to_integer(Str), 204 | Unit = string:trim(Unit0), 205 | case string:to_lower(Unit) of 206 | "" -> {ok, Int}; 207 | "kb" -> {ok, Int * 1024}; 208 | "mb" -> {ok, Int * 1024 * 1024} 209 | end. 210 | 211 | parse_hosts(Str) -> 212 | try 213 | {ok, [parse_host(I) || I <- string:tokens(Str, ", ")]} 214 | catch 215 | _:_ -> 216 | error 217 | end. 218 | 219 | parse_host(Str) -> 220 | case string:tokens(Str, ":") of 221 | [Host] -> 222 | Host; 223 | [Host, Port] -> 224 | {Host, list_to_integer(Port)} 225 | end. 226 | -------------------------------------------------------------------------------- /src/framework/emqttb_app.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc emqttb public API 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(emqttb_app). 7 | 8 | -behaviour(application). 9 | 10 | -export([start/2, stop/1]). 11 | 12 | -include_lib("typerefl/include/types.hrl"). 13 | -include("emqttb.hrl"). 14 | 15 | start(_StartType, _StartArgs) -> 16 | emqttb_conf:load_model(), 17 | Sup = emqttb_sup:start_link(), 18 | emqttb_conf:load_conf(), 19 | maybe_perform_special_action(), 20 | emqttb_autorate:create_autorates(), 21 | emqttb_scenario:run_scenarios(), 22 | CLIArgs = application:get_env(?APP, cli_args, []), 23 | emqttb_grafana:annotate(["Start emqttb " | lists:join($ , CLIArgs)]), 24 | post_init(), 25 | Sup. 26 | 27 | stop(_State) -> 28 | emqttb_grafana:annotate("Stop emqttb"), 29 | ok. 30 | 31 | %% internal functions 32 | 33 | %% Start misc. processes that depend on configuration 34 | post_init() -> 35 | ?CFG([restapi, enabled]) andalso 36 | emqttb_misc_sup:start_worker( emqttb_http 37 | , {emqttb_http, start_link, []} 38 | ), 39 | ?CFG([metrics, pushgateway, enabled]) andalso 40 | emqttb_misc_sup:start_worker( emqttb_pushgw 41 | , {emqttb_pushgw, start_link, []} 42 | ), 43 | emqttb_logger:setup(), 44 | maybe_start_distr(), 45 | ok. 46 | 47 | maybe_start_distr() -> 48 | case ?CFG([cluster, node_name]) of 49 | undefined -> 50 | ok; 51 | Name -> 52 | os:cmd("epmd -daemon"), 53 | Opts = #{dist_listen => true}, 54 | net_kernel:start(Name, Opts) 55 | end. 56 | 57 | maybe_perform_special_action() -> 58 | case ?CFG_LIST([actions, ls, {}]) of 59 | [] -> 60 | ok; 61 | [Key] -> 62 | case ?CFG(Key ++ [what]) of 63 | metric -> 64 | Objs = emqttb_metrics:ls(?MYMODEL); 65 | autorate -> 66 | {Objs, _} = lists:unzip(emqttb_autorate:ls(?MYMODEL)) 67 | end, 68 | lists:foreach(fun(K) -> io:format("~p~n", [K]) end, Objs), 69 | emqttb:terminate() 70 | end. 71 | -------------------------------------------------------------------------------- /src/framework/emqttb_autorate_sup.erl: -------------------------------------------------------------------------------- 1 | -module(emqttb_autorate_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | -export([start_link/0, ensure/1, list/0]). 6 | 7 | -export([init/1]). 8 | 9 | -include("emqttb.hrl"). 10 | 11 | -define(SERVER, ?MODULE). 12 | 13 | start_link() -> 14 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 15 | 16 | list() -> 17 | [Id || {Id, _Child, _Type, _Modules} <- supervisor:which_children(?SERVER)]. 18 | 19 | -spec ensure(emqttb_autorate:config()) -> {ok, pid()}. 20 | ensure(Options = #{id := Id}) -> 21 | Result = supervisor:start_child(?SERVER, 22 | #{ id => Id 23 | , type => worker 24 | , restart => temporary 25 | , start => {emqttb_autorate, start_link, [Options]} 26 | , shutdown => timer:seconds(1) 27 | }), 28 | case Result of 29 | {ok, _} = Ok -> 30 | Ok; 31 | {error, {already_started, Pid}} -> 32 | {ok, Pid}; 33 | {error, already_present} -> 34 | supervisor:restart_child(?SERVER, Id) 35 | end. 36 | 37 | init([]) -> 38 | SupFlags = #{ strategy => one_for_one 39 | , intensity => 1 40 | , period => 10 41 | }, 42 | ChildSpecs = [], 43 | {ok, {SupFlags, ChildSpecs}}. 44 | 45 | %% internal functions 46 | -------------------------------------------------------------------------------- /src/framework/emqttb_group.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023, 2025 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_group). 17 | 18 | -behavior(gen_server). 19 | 20 | %% API: 21 | -export([ensure/1, stop/1, set_target/2, set_target/3, set_target_async/3, broadcast/2, report_dead_id/2, report_live_id/2, info/0]). 22 | 23 | %% gen_server callbacks: 24 | -export([init/1, handle_call/3, handle_cast/2, terminate/2, handle_info/2]). 25 | 26 | %% Internal exports 27 | -export([start_link/1]). 28 | 29 | -export_type([group_config/0]). 30 | 31 | -include_lib("snabbkaffe/include/trace.hrl"). 32 | -include("emqttb_internal.hrl"). 33 | 34 | %%================================================================================ 35 | %% Type declarations 36 | %%================================================================================ 37 | 38 | -type prototype() :: emqttb_behavior_pub:prototype() 39 | | emqttb_behavior_sub:prototype() 40 | | emqttb_behavior_conn:prototype(). 41 | 42 | -type group_config() :: 43 | #{ id := atom() 44 | , client_config := atom() 45 | , behavior := prototype() 46 | , conn_interval := atom() 47 | , parent => pid() 48 | , start_n => integer() 49 | }. 50 | 51 | -define(id(ID), {n, l, {emqttb_group, ID}}). 52 | -define(via(ID), {via, gproc, ?id(ID)}). 53 | 54 | %%================================================================================ 55 | %% API funcions 56 | %%================================================================================ 57 | 58 | -spec ensure(group_config()) -> ok. 59 | ensure(Conf) -> 60 | emqttb_group_sup:ensure(Conf#{parent => self()}). 61 | 62 | -spec stop(atom()) -> ok. 63 | stop(ID) -> 64 | logger:info("Stopping group ~p", [ID]), 65 | emqttb_group_sup:stop(ID). 66 | 67 | -spec set_target(emqttb:group(), NClients) -> {ok, NClients} | {error, new_target} 68 | when NClients :: emqttb:n_clients(). 69 | set_target(Group, NClients) -> 70 | set_target(Group, NClients, undefined). 71 | 72 | %% @doc Autoscale the group to the target number of workers. Returns 73 | %% value when the target or a ratelimit has been reached, or error 74 | %% when the new target has been set. 75 | %% 76 | %% The group will try to maintain the last target number of workers 77 | %% even after set_target returns. 78 | %% 79 | %% Note: this implementation has been optimized for scaling up very 80 | %% fast, not scaling down. Scaling down is rather memory-expensive. 81 | %% 82 | %% Order of workers' removal during ramping down is not specified. 83 | -spec set_target(emqttb:group(), NClients, emqttb:duration_us() | undefined) -> 84 | {ok, NClients} | {error, new_target} 85 | when NClients :: emqttb:n_clients(). 86 | set_target(Id, Target, Interval) -> 87 | gen_server:call(?via(Id), {set_target, Target, Interval}, infinity). 88 | 89 | %% @doc Async version of `set_target' 90 | -spec set_target_async(emqttb:group(), emqttb:n_clients(), emqttb:duration_us()) -> ok. 91 | set_target_async(Id, Target, Interval) -> 92 | gen_server:cast(?via(Id), {set_target, Target, Interval}). 93 | 94 | %% @doc Send a message to all members of the group 95 | -spec broadcast(emqttb:group(), _Message) -> ok. 96 | broadcast(Group, Message) -> 97 | ?tp(emqttb_group_broadcast, #{group => Group, message => Message}), 98 | fold_workers( 99 | fun(Pid, _) -> 100 | Pid ! Message, 101 | [] 102 | end, 103 | [], 104 | Group). 105 | 106 | %% @doc Add an expired ID to the pool 107 | -spec report_dead_id(emqttb:group(), integer()) -> true. 108 | report_dead_id(Group, Id) -> 109 | ets:delete(live_id_pool(Group), Id), 110 | ets:insert(dead_id_pool(Group), {Id, []}). 111 | 112 | -spec report_live_id(emqttb:group(), integer()) -> true. 113 | report_live_id(Group, Id) -> 114 | ets:insert(live_id_pool(Group), {Id, self()}). 115 | 116 | info() -> 117 | [#{ '$id' => Id 118 | , conf_root => persistent_term:get(?GROUP_CONF_ID(Pid)) 119 | , behavior => persistent_term:get(?GROUP_BEHAVIOR(Pid)) 120 | , shared_state => persistent_term:get(?GROUP_BEHAVIOR_SHARED_STATE(Pid)) 121 | , n_workers => emqttb_metrics:get_counter(?GROUP_N_WORKERS(Id)) 122 | } 123 | || {Id, Pid} <- emqttb_group_sup:list()]. 124 | 125 | %%================================================================================ 126 | %% behavior callbacks 127 | %%================================================================================ 128 | 129 | %% Currently running scaling operation: 130 | -record(r, 131 | { direction :: up | down 132 | , on_complete :: fun((_Result) -> _) 133 | }). 134 | 135 | -record(s, 136 | { id :: atom() 137 | , behavior :: module() 138 | , conf_prefix :: lee:key() 139 | , scaling :: #r{} | undefined 140 | , target :: non_neg_integer() | undefined 141 | , interval :: counters:counters_ref() 142 | , autorate :: atom() 143 | , scale_timer :: reference() | undefined 144 | , tick_timer :: reference() 145 | , next_id = 0 :: non_neg_integer() 146 | , parent_ref :: reference() | undefined 147 | }). 148 | 149 | -define(TICK_TIME, 1000). 150 | 151 | init([Conf]) -> 152 | process_flag(trap_exit, true), 153 | #{ id := ID 154 | , client_config := ConfID 155 | , behavior := {Behavior, BehSettings} 156 | , conn_interval := ConnIntervalAutorateId 157 | } = Conf, 158 | ?tp(info, "Starting worker group", 159 | #{ id => ID 160 | , group_conf => ConfID 161 | }), 162 | emqttb_metrics:new_counter(?GROUP_N_WORKERS(ID), 163 | [ {help, <<"Number of workers in the group">>} 164 | , {labels, [group]} 165 | ]), 166 | ets:new(dead_id_pool(ID), [ordered_set, public, named_table]), 167 | ets:new(live_id_pool(ID), [ordered_set, public, named_table]), 168 | StartN = maps:get(start_n, Conf, 0), 169 | persistent_term:put(?GROUP_LEADER_TO_GROUP_ID(self()), ID), 170 | persistent_term:put(?GROUP_BEHAVIOR(self()), Behavior), 171 | persistent_term:put(?GROUP_CONF_ID(self()), ConfID), 172 | BehSharedState = emqttb_worker:init_per_group(Behavior, ID, BehSettings), 173 | persistent_term:put(?GROUP_BEHAVIOR_SHARED_STATE(self()), BehSharedState), 174 | Autorate = emqttb_autorate:get_counter(ConnIntervalAutorateId), 175 | S = #s{ id = ID 176 | , behavior = Behavior 177 | , conf_prefix = [groups, ConfID] 178 | , tick_timer = set_tick_timer() 179 | , parent_ref = maybe_monitor_parent(Conf) 180 | , interval = Autorate 181 | , autorate = ConnIntervalAutorateId 182 | , next_id = StartN 183 | }, 184 | {ok, S}. 185 | 186 | handle_call({set_target, Target, Interval}, From, S) -> 187 | OnComplete = fun(Result) -> gen_server:reply(From, Result) end, 188 | {noreply, do_set_target(Target, Interval, OnComplete, S)}; 189 | handle_call(_, _, S) -> 190 | {reply, {error, unknown_call}, S}. 191 | 192 | handle_cast({set_target, Target, Interval}, S) -> 193 | OnComplete = fun(_Result) -> ok end, 194 | {noreply, do_set_target(Target, Interval, OnComplete, S)}; 195 | handle_cast(_, S) -> 196 | {noreply, S}. 197 | 198 | handle_info(tick, S) -> 199 | {noreply, do_tick(S#s{tick_timer = set_tick_timer()})}; 200 | handle_info(do_scale, S)-> 201 | {noreply, do_scale(S#s{scale_timer = undefined})}; 202 | handle_info({'DOWN', MRef, _, _, _}, S = #s{parent_ref = MRef}) -> 203 | {stop, normal, S}; 204 | handle_info(_, S) -> 205 | {noreply, S}. 206 | 207 | terminate(_Reason, #s{id = Id}) -> 208 | stop_group_workers(Id), 209 | ?tp(info, "Stopped worker group", #{id => Id}), 210 | persistent_term:erase(?GROUP_LEADER_TO_GROUP_ID(self())), 211 | persistent_term:erase(?GROUP_BEHAVIOR(self())), 212 | persistent_term:erase(?GROUP_CONF_ID(self())), 213 | persistent_term:erase(?GROUP_BEHAVIOR_SHARED_STATE(self())), 214 | ok. 215 | 216 | %%================================================================================ 217 | %% Internal exports 218 | %%================================================================================ 219 | 220 | -spec start_link(group_config()) -> {ok, pid()}. 221 | start_link(Conf = #{id := ID}) -> 222 | gen_server:start_link(?via(ID), ?MODULE, [Conf], []). 223 | 224 | %%================================================================================ 225 | %% Internal functions 226 | %%================================================================================ 227 | 228 | stop_group_workers(Id) -> 229 | %% Terminate workers in batches: 230 | {_, Group} = fold_workers(fun(Pid, {100, MRefs}) -> 231 | wait_group_stop(MRefs), 232 | {1, [stop_worker_async(Pid)]}; 233 | (Pid, {N, MRefs}) -> 234 | {N + 1, [stop_worker_async(Pid) | MRefs]} 235 | end, 236 | {0, []}, 237 | Id), 238 | wait_group_stop(Group). 239 | 240 | stop_worker_async(Pid) -> 241 | MRef = monitor(process, Pid), 242 | exit(Pid, shutdown), 243 | MRef. 244 | 245 | wait_group_stop([]) -> 246 | ok; 247 | wait_group_stop([MRef|Rest]) -> 248 | receive 249 | {'DOWN', MRef, _, _, _} -> 250 | wait_group_stop(Rest) 251 | end. 252 | 253 | do_set_target(Target, InitInterval, OnComplete, S = #s{ scaling = Scaling 254 | , autorate = Autorate 255 | }) -> 256 | N = n_clients(S), 257 | Direction = if Target > N -> up; 258 | Target =:= N -> stay; 259 | true -> down 260 | end, 261 | maybe_cancel_previous(Scaling), 262 | emqttb_autorate:activate(Autorate), 263 | case Direction of 264 | stay -> 265 | OnComplete({ok, N}), 266 | S#s{scaling = undefined, target = Target}; 267 | _ -> 268 | case InitInterval of 269 | _ when is_integer(InitInterval) -> 270 | emqttb_autorate:reset(Autorate, InitInterval); 271 | undefined -> 272 | ok 273 | end, 274 | start_scale(S, Direction, Target, OnComplete) 275 | end. 276 | 277 | start_scale(S0, Direction, Target, OnComplete) -> 278 | Scaling = #r{ direction = Direction 279 | , on_complete = OnComplete 280 | }, 281 | S = S0#s{scaling = Scaling, target = Target}, 282 | logger:info("Group ~p is scaling ~p...", [S0#s.id, Direction]), 283 | set_scale_timer(0, S). 284 | 285 | do_tick(S = #s{id = Id, target = Target}) -> 286 | N = n_clients(S), 287 | if N < Target -> %% Some clients died on us? 288 | logger:info("[~p]: ~p -> ~p", [Id, N, Target]), 289 | do_scale(S); 290 | true -> 291 | S 292 | end. 293 | 294 | do_scale(S0) -> 295 | #s{ target = Target 296 | , interval = IntervalCnt 297 | , id = ID 298 | } = S0, 299 | N = n_clients(S0), 300 | {SleepTime, NRepeats0} = emqttb:get_duration_and_repeats(IntervalCnt), 301 | NRepeats = min(abs(Target - N), NRepeats0), 302 | logger:debug("Scaling ~p; ~p -> ~p. Sleep time: ~p. Repeats: ~p", [ID, N, Target, SleepTime, NRepeats]), 303 | S = maybe_notify(N, S0), 304 | if N < Target -> 305 | S1 = set_scale_timer(SleepTime, S), 306 | scale_up(NRepeats, S1); 307 | N > Target -> 308 | S1 = set_scale_timer(SleepTime, S), 309 | scale_down(NRepeats, S1); 310 | true -> 311 | S 312 | end. 313 | 314 | scale_up(0, S) -> 315 | S; 316 | scale_up(NRepeats, S = #s{behavior = Behavior, id = Group}) -> 317 | case ets:first(dead_id_pool(Group)) of 318 | '$end_of_table' -> 319 | WorkerId = S#s.next_id, 320 | NextId = WorkerId + 1; 321 | WorkerId when is_integer(WorkerId) -> 322 | ets:delete(dead_id_pool(Group), WorkerId), 323 | NextId = S#s.next_id 324 | end, 325 | _Pid = emqttb_worker:start(Behavior, self(), WorkerId), 326 | ?tp(start_worker, #{group => Group, pid => _Pid}), 327 | scale_up(NRepeats - 1, S#s{next_id = NextId}). 328 | 329 | scale_down(0, S) -> 330 | S; 331 | scale_down(NRepeats, S = #s{id = Group}) -> 332 | case ets:first(live_id_pool(Group)) of 333 | '$end_of_table' -> 334 | S; 335 | Id -> 336 | case ets:lookup(live_id_pool(Group), Id) of 337 | [] -> 338 | %% Race: died? 339 | scale_down(NRepeats, S); 340 | [{Id, Pid}] -> 341 | MRef = monitor(process, Pid), 342 | exit(Pid, shutdown), 343 | receive 344 | {'DOWN', MRef, process, Pid, _R} -> 345 | ok 346 | end, 347 | scale_down(NRepeats - 1, S) 348 | end 349 | end. 350 | 351 | set_tick_timer() -> 352 | erlang:send_after(?TICK_TIME, self(), tick). 353 | 354 | set_scale_timer(SleepTime, S = #s{scale_timer = undefined}) -> 355 | S#s{scale_timer = erlang:send_after(SleepTime, self(), do_scale)}; 356 | set_scale_timer(_SleepTime, S) -> 357 | S. 358 | 359 | maybe_notify(_, S = #s{scaling = undefined}) -> 360 | S; 361 | maybe_notify(N, S) -> 362 | #s{ scaling = #r{direction = Direction, on_complete = OnComplete} 363 | , target = Target 364 | , id = Id 365 | } = S, 366 | if Direction =:= up andalso N >= Target; 367 | Direction =:= down andalso N =< Target -> 368 | logger:info("[~p]: Reached the target number of clients", [Id]), 369 | OnComplete({ok, N}), 370 | S#s{scaling = undefined}; 371 | true -> 372 | S 373 | end. 374 | 375 | maybe_cancel_previous(undefined) -> 376 | ok; 377 | maybe_cancel_previous(#r{on_complete = OnComplete}) -> 378 | OnComplete({error, new_target}). 379 | 380 | n_clients(#s{id = Id}) -> 381 | emqttb_metrics:get_counter(?GROUP_N_WORKERS(Id)). 382 | 383 | -spec fold_workers( fun((pid(), Acc) -> Acc) 384 | , Acc 385 | , pid() | atom() 386 | ) -> Acc. 387 | fold_workers(Fun, Acc, GroupId) when is_atom(GroupId) -> 388 | GL = gproc:where(?id(GroupId)), 389 | is_pid(GL) orelse error({group_is_not_alive, GroupId}), 390 | fold_workers(Fun, Acc, GL); 391 | fold_workers(Fun0, Acc, GL) -> 392 | PIDs = erlang:processes(), 393 | Fun = fun(Pid, Acc0) -> 394 | case process_info(Pid, [group_leader, initial_call]) of 395 | [{group_leader, GL}, {initial_call, {emqttb_worker, entrypoint, _}}] -> 396 | Fun0(Pid, Acc0); 397 | _ -> 398 | Acc0 399 | end 400 | end, 401 | lists:foldl(Fun, Acc, PIDs). 402 | 403 | maybe_monitor_parent(#{parent := Pid}) -> 404 | monitor(process, Pid); 405 | maybe_monitor_parent(_) -> 406 | undefined. 407 | 408 | live_id_pool(Group) -> 409 | list_to_atom(atom_to_list(Group) ++ "_live"). 410 | 411 | dead_id_pool(Group) -> 412 | Group. 413 | -------------------------------------------------------------------------------- /src/framework/emqttb_group_sup.erl: -------------------------------------------------------------------------------- 1 | -module(emqttb_group_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | -export([start_link/0, ensure/1, stop/1, list/0]). 6 | 7 | -export([init/1]). 8 | 9 | -include("emqttb.hrl"). 10 | 11 | -define(SERVER, ?MODULE). 12 | 13 | start_link() -> 14 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 15 | 16 | list() -> 17 | [{Id, Pid} || {Id, Pid, _Type, _Modules} <- supervisor:which_children(?SERVER), 18 | is_pid(Pid)]. 19 | 20 | -spec ensure(emqttb_group:group_config()) -> ok. 21 | ensure(Options = #{id := Id}) -> 22 | Result = supervisor:start_child(?SERVER, 23 | #{ id => Id 24 | , type => worker 25 | , restart => temporary 26 | , start => {emqttb_group, start_link, [Options]} 27 | , shutdown => timer:seconds(300) 28 | }), 29 | case Result of 30 | {ok, _} -> 31 | ok; 32 | {error, {already_started, _}} -> 33 | ok; 34 | {error, already_present} -> 35 | supervisor:restart_child(?SERVER, Id) 36 | end. 37 | 38 | -spec stop(atom()) -> ok. 39 | stop(ID) -> 40 | supervisor:terminate_child(?SERVER, ID). 41 | 42 | init([]) -> 43 | SupFlags = #{ strategy => one_for_one 44 | , intensity => 0 45 | , period => 1 46 | }, 47 | ChildSpecs = [], 48 | {ok, {SupFlags, ChildSpecs}}. 49 | 50 | %% internal functions 51 | -------------------------------------------------------------------------------- /src/framework/emqttb_internal.hrl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -ifndef(EMQTTB_INTERNAL_HRL). 17 | -define(EMQTTB_INTERNAL_HRL, true). 18 | 19 | -include("emqttb.hrl"). 20 | 21 | %% Persistent term keys: 22 | -define(GROUP_LEADER_TO_GROUP_ID(GL), {emqttb_group_leader_to_group_id, GL}). 23 | -define(GROUP_BEHAVIOR(GL), {emqttb_group_behavior, GL}). 24 | -define(GROUP_BEHAVIOR_SHARED_STATE(GL), {emqttb_group_behavior_settings, GL}). 25 | 26 | -define(GROUP_CONF_ID(GL), {emqttb_group_client_conf_id, GL}). 27 | 28 | %% Metrics: 29 | -define(GROUP_N_WORKERS(GRP), {emqttb_group_n_workers, GRP}). 30 | -define(GROUP_OP_TIME(GRP, OP), {emqttb_group_op_time, {GRP, OP}}). 31 | -define(GROUP_N_PENDING(GRP, OP), {emqttb_group_n_pending, {GRP, OP}}). 32 | -define(AUTORATE_RATE(ID), {emqttb_autorate_rate, ID}). 33 | -define(AUTORATE_CONTROL, emqttb_autorate_control). %% Prometheus only 34 | %% Publisher 35 | -define(CNT_PUB_MESSAGES(GRP), {emqttb_published_messages, GRP}). 36 | -define(AVG_PUB_TIME, publish). 37 | %% Subscriber 38 | -define(CNT_SUB_MESSAGES(GRP), {emqttb_received_messages, GRP}). 39 | -define(AVG_SUB_TIME, subscribe). 40 | 41 | -endif. 42 | -------------------------------------------------------------------------------- /src/framework/emqttb_logger.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_logger). 17 | 18 | %% API: 19 | -export([setup/0]). 20 | 21 | %% internal exports: 22 | -export([filter_client/2, filter_default/2]). 23 | -export([check_config/1, format/2]). 24 | 25 | -include("emqttb.hrl"). 26 | 27 | %%================================================================================ 28 | %% API funcions 29 | %%================================================================================ 30 | 31 | setup() -> 32 | %% ok = logger:set_handler_config(default, formatter, {?MODULE, []}), 33 | LogDir = ?CFG([logging, directory]), 34 | logger:add_handler(client, logger_std_h, #{config => #{file => filename:join(LogDir, "emqttb.log")}}), 35 | logger:add_handler_filter(client, client_filter, {fun ?MODULE:filter_client/2, []}), 36 | logger:add_handler_filter(default, client_filter, {fun ?MODULE:filter_default/2, []}), 37 | ok. 38 | 39 | %%================================================================================ 40 | %% Internal exports 41 | %%================================================================================ 42 | 43 | filter_client(Event, _) -> 44 | case is_client_event(Event) of 45 | true -> 46 | Event; 47 | false -> 48 | stop 49 | end. 50 | 51 | filter_default(Event, _) -> 52 | case is_client_event(Event) of 53 | true -> 54 | stop; 55 | false -> 56 | Event 57 | end. 58 | 59 | %% Debug 60 | check_config(_) -> 61 | ok. 62 | 63 | %% Debug 64 | format(LogEvent, _) -> 65 | io_lib:format("~p~n", [LogEvent]). 66 | 67 | %%================================================================================ 68 | %% Internal functions 69 | %%================================================================================ 70 | 71 | -compile({inline, [is_client_event/1]}). 72 | 73 | is_client_event(#{meta := #{domain := [group|_]}}) -> 74 | true; 75 | is_client_event(#{msg := {report, #{clientid := _}}}) -> 76 | true; 77 | is_client_event(#{msg := {report, #{clietntid := _}}}) -> 78 | true; 79 | is_client_event(#{msg := {report, #{modules := [emqtt]}}}) -> 80 | true; 81 | is_client_event(#{msg := {report, #{report := [[{initial_call,{emqtt,_,_}}|_]|_]}}}) -> 82 | true; 83 | is_client_event(_) -> 84 | false. 85 | -------------------------------------------------------------------------------- /src/framework/emqttb_misc_sup.erl: -------------------------------------------------------------------------------- 1 | -module(emqttb_misc_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | -export([start_link/0, start_worker/2]). 6 | 7 | -export([init/1]). 8 | 9 | -include("emqttb.hrl"). 10 | 11 | -define(SERVER, ?MODULE). 12 | 13 | start_link() -> 14 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 15 | 16 | start_worker(Id, Start) -> 17 | {ok, _} = supervisor:start_child(?SERVER, 18 | #{ id => Id 19 | , type => worker 20 | , start => Start 21 | , shutdown => timer:seconds(1) 22 | }). 23 | 24 | init([]) -> 25 | SupFlags = #{ strategy => one_for_one 26 | , intensity => 10 27 | , period => 1 28 | }, 29 | ChildSpecs = [], 30 | {ok, {SupFlags, ChildSpecs}}. 31 | 32 | %% internal functions 33 | -------------------------------------------------------------------------------- /src/framework/emqttb_scenario.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023, 2025 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_scenario). 17 | 18 | -behavior(lee_metatype). 19 | -behavior(gen_server). 20 | 21 | %% API: 22 | -export([set_stage/1, set_stage/2, stage/1, complete/1, loiter/0, 23 | model/0, list_enabled_scenarios/0, run/1, stop/1, 24 | my_scenario/0, my_conf_key/1, my_conf/1, module/1, name/1, 25 | info/0]). 26 | 27 | %% lee_metatype callbacks: 28 | -export([names/1, read_patch/2]). 29 | 30 | %% gen_server callbacks: 31 | -export([init/1, handle_call/3, handle_cast/2, handle_continue/2, terminate/2]). 32 | 33 | %% internal exports: 34 | -export([start_link/1, run_scenarios/0]). 35 | 36 | -export_type([result/0]). 37 | 38 | -include_lib("typerefl/include/types.hrl"). 39 | -include("emqttb.hrl"). 40 | 41 | %%================================================================================ 42 | %% Type declarations 43 | %%================================================================================ 44 | 45 | -type result() :: {ok | error, _} | ok. 46 | 47 | -define(SCENARIO_GROUP_LEADER(GL), {emqttb_scenario_group_leader, GL}). 48 | 49 | %%================================================================================ 50 | %% Behavior declaration 51 | %%================================================================================ 52 | 53 | %% Model fragment for the scenario: 54 | -callback model() -> lee:lee_module(). 55 | 56 | %% Callback that creates the default configuration for the scenario: 57 | -callback initial_config() -> lee:patch(). 58 | 59 | %% This callback executes the scenario: 60 | -callback run() -> ok. 61 | 62 | %% -callback name() -> atom(). 63 | 64 | %%================================================================================ 65 | %% API functions 66 | %%================================================================================ 67 | 68 | -spec run(emqttb:scenario()) -> ok. 69 | run(Name) -> 70 | emqttb_scenarios_sup:run(Name). 71 | 72 | -spec stop(module()) -> ok. 73 | stop(Module) -> 74 | emqttb_scenarios_sup:stop_child(Module). 75 | 76 | %% Get name of the scenario that spawned our process 77 | -spec my_scenario() -> module(). 78 | my_scenario() -> 79 | persistent_term:get(?SCENARIO_GROUP_LEADER(group_leader())). 80 | 81 | -spec my_conf_key(lee:key()) -> lee:key(). 82 | my_conf_key(Key) -> 83 | [?SK(my_scenario()) | Key]. 84 | 85 | %% Get configuartion parameter of the scenario 86 | -spec my_conf(lee:key()) -> term(). 87 | my_conf(Key) -> 88 | ?CFG(my_conf_key(Key)). 89 | 90 | -spec stage(emqttb:scenario()) -> emqttb:stage(). 91 | stage(Scenario) -> 92 | persistent_term:get({emqttb_stage, Scenario}, undefined). 93 | 94 | -spec set_stage(emqttb:stage()) -> ok. 95 | set_stage(Stage) -> 96 | Scenario = my_scenario(), 97 | Txt = lists:flatten(io_lib:format("~p entered stage ~p", [Scenario, Stage])), 98 | logger:notice(asciiart:visible($#, Txt, [])), 99 | emqttb_grafana:annotate(Txt, [Scenario]), 100 | persistent_term:put({emqttb_stage, Scenario}, Stage). 101 | 102 | -spec set_stage(emqttb:stage(), result()) -> ok. 103 | set_stage(Stage, ok) -> 104 | set_stage(Stage); 105 | set_stage(Stage, {ok, Result}) -> 106 | Scenario = my_scenario(), 107 | logger:notice(asciiart:visible( $#, "~p completed stage ~p~nResult: ~p" 108 | , [Scenario, stage(Scenario), Result] 109 | )), 110 | set_stage(Stage); 111 | set_stage(Stage, {error, Err}) -> 112 | Scenario = my_scenario(), 113 | logger:notice(asciiart:visible( $!, "Stage ~p of ~p failed~nError: ~p" 114 | , [stage(Scenario), Scenario, Err] 115 | )), 116 | set_stage(Stage). 117 | 118 | -spec loiter() -> ok. 119 | loiter() -> 120 | T = my_conf([loiter]), 121 | receive after T -> ok end. 122 | 123 | -spec complete(result()) -> no_return(). 124 | complete(PrevStageResult) -> 125 | Scenario = my_scenario(), 126 | set_stage(complete, PrevStageResult), 127 | case PrevStageResult of 128 | ok -> ok; 129 | {ok, _} -> ok; 130 | {error, _} -> emqttb:setfail(Scenario) 131 | end. 132 | 133 | -spec model() -> lee:lee_module(). 134 | model() -> 135 | application:load(?APP), 136 | Scenarios = all_scenario_modules(), 137 | Model = [{name(M), make_model(M)} || M <- Scenarios], 138 | maps:from_list(Model). 139 | 140 | list_enabled_scenarios() -> 141 | [I || [?SK(I)] <- ?CFG_LIST([?SK({})])]. 142 | 143 | %% @doc Convert name to module 144 | -spec module(emqttb:scenario()) -> module(). 145 | module(Id) -> 146 | list_to_existing_atom("emqttb_scenario_" ++ atom_to_list(Id)). 147 | 148 | %% @doc Convert module to scenario name 149 | -spec name(module()) -> emqttb:scenario(). 150 | name(Module) -> 151 | _ = Module:module_info(), 152 | "emqttb_scenario_" ++ Name = atom_to_list(Module), 153 | list_to_atom(Name). 154 | 155 | -spec info() -> [map()]. 156 | info() -> 157 | lists:map( 158 | fun(Module) -> 159 | Name = name(Module), 160 | #{ id => Name 161 | , enabled => lists:member(Name, list_enabled_scenarios()) 162 | } 163 | end, 164 | all_scenario_modules()). 165 | 166 | %%================================================================================ 167 | %% Behavior callbacks 168 | %%================================================================================ 169 | 170 | names(_) -> 171 | [scenario]. 172 | 173 | read_patch(scenario, _Model) -> 174 | %% Populate configuration with the default data: 175 | Patch = lists:flatmap( 176 | fun(Module) -> 177 | Module:initial_config() 178 | end, 179 | all_scenario_modules()), 180 | {ok, 999999, Patch}. 181 | 182 | 183 | %%================================================================================ 184 | %% External exports 185 | %%================================================================================ 186 | 187 | -spec start_link(module()) -> {ok, pid()}. 188 | start_link(Module) -> 189 | gen_server:start_link({local, Module}, ?MODULE, [Module], []). 190 | 191 | -spec run_scenarios() -> ok. 192 | run_scenarios() -> 193 | lists:foreach( 194 | fun([scenarios, Name]) -> 195 | case lee:list(?MYCONF, [?SK(Name)]) of 196 | [] -> ok; 197 | [_] -> emqttb_scenario:run(Name) 198 | end 199 | end, 200 | lee_model:get_metatype_index(scenario, ?MYMODEL)). 201 | 202 | %%================================================================================ 203 | %% gen_server callbacks 204 | %%================================================================================ 205 | 206 | -record(s, 207 | { module :: module() 208 | }). 209 | 210 | init([Module]) -> 211 | process_flag(trap_exit, true), 212 | Name = name(Module), 213 | logger:notice("Starting scenario ~p", [Name]), 214 | group_leader(self(), self()), 215 | persistent_term:put(?SCENARIO_GROUP_LEADER(group_leader()), Name), 216 | {ok, #s{module = Module}, {continue, start}}. 217 | 218 | handle_continue(start, S = #s{module = Module}) -> 219 | Module:run(), 220 | {stop, normal, S}. 221 | 222 | handle_call(_, _, S) -> 223 | {reply, {error, unknown_call}, S}. 224 | 225 | handle_cast(_, S) -> 226 | {noreply, S}. 227 | 228 | terminate(_, _State) -> 229 | persistent_term:erase(?SCENARIO_GROUP_LEADER(group_leader())). 230 | 231 | %%================================================================================ 232 | %% Internal functions 233 | %%================================================================================ 234 | 235 | make_model(Module) -> 236 | Name = name(Module), 237 | {[map, cli_action, scenario], 238 | #{ cli_operand => atom_to_list(Name) 239 | , key_elements => [] 240 | , oneliner => lists:concat(["Run scenario ", Name]) 241 | , doc => doc_macro(Module) 242 | }, 243 | maps:merge( 244 | #{ loiter => 245 | {[value, cli_param], 246 | #{ oneliner => "Keep running scenario stages for this period of time (sec)" 247 | , type => emqttb:wait_time() 248 | , default_ref => [convenience, loiter] 249 | , cli_operand => "loiter" 250 | }} 251 | }, 252 | Module:model())}. 253 | 254 | -spec all_scenario_modules() -> [module()]. 255 | all_scenario_modules() -> 256 | {ok, Modules} = application:get_key(?APP, modules), 257 | [M || M <- Modules, 258 | {behavior, Behaviors} <- proplists:get_value( attributes 259 | , M:module_info() 260 | ), 261 | ?MODULE <- Behaviors]. 262 | 263 | doc_macro(Module) -> 264 | <<"emqttb_scenario_", Rest/binary>> = atom_to_binary(Module), 265 | Name = binary:replace(Rest, <<"_">>, <<"-">>, [global]), 266 | <<"@doc-scenario-", Name/binary>>. 267 | -------------------------------------------------------------------------------- /src/framework/emqttb_scenarios_sup.erl: -------------------------------------------------------------------------------- 1 | -module(emqttb_scenarios_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | -export([start_link/0, enable_autostop/0, run/1, stop_child/1]). 6 | 7 | -export([init/1]). 8 | 9 | -export([start_retainer/0]). 10 | 11 | -include("emqttb.hrl"). 12 | 13 | -define(SERVER, ?MODULE). 14 | 15 | start_link() -> 16 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 17 | 18 | -spec run(emqttb:scenario()) -> ok. 19 | run(Name) -> 20 | Spec = #{ id => Name 21 | , start => {emqttb_scenario, start_link, [emqttb_scenario:module(Name)]} 22 | , type => worker 23 | , restart => transient 24 | , significant => true 25 | , shutdown => brutal_kill 26 | }, 27 | Result = supervisor:start_child(?SERVER, Spec), 28 | case Result of 29 | {ok, _} -> 30 | ok; 31 | {error, {already_started, _}} -> 32 | %% Let it run... 33 | ok; 34 | {error, already_present} -> 35 | supervisor:restart_child(?SERVER, Name), 36 | ok 37 | end. 38 | 39 | stop_child(Module) -> 40 | Id = Module:name(), 41 | supervisor:terminate_child(?SERVER, Id), 42 | supervisor:delete_child(?SERVER, Id). 43 | 44 | %% This supervisor is configured to stop when all of its significant 45 | %% children terminate. Initially it starts a dummy retainer process 46 | %% that keeps it alive even after when all scenarios complete. 47 | %% 48 | %% To enable autostop this retainer child is terminated. 49 | enable_autostop() -> 50 | logger:debug("Enabling autostop"), 51 | exit(whereis(emqttb_scenarios_sup_retainer), shutdown). 52 | 53 | init([]) -> 54 | SupFlags = #{ strategy => one_for_all 55 | , intensity => 0 56 | , period => 1 57 | , auto_shutdown => all_significant 58 | }, 59 | ChildSpecs = [retainer()], 60 | {ok, {SupFlags, ChildSpecs}}. 61 | 62 | %% internal functions 63 | 64 | retainer() -> 65 | #{ id => '$retainer' 66 | , type => worker 67 | , restart => transient 68 | , significant => true 69 | , start => {?MODULE, start_retainer, []} 70 | , shutdown => brutal_kill 71 | }. 72 | 73 | %% This child does nothing but sits there and prevents the supervisor 74 | %% from stopping. 75 | -dialyzer({nowarn_function, start_retainer/0}). 76 | start_retainer() -> 77 | {ok, spawn_link(fun() -> 78 | register(emqttb_scenarios_sup_retainer, self()), 79 | receive after infinity -> ok end 80 | end)}. 81 | -------------------------------------------------------------------------------- /src/framework/emqttb_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%%%% @doc emqttb top level supervisor. %% @end 3 | %%%------------------------------------------------------------------- 4 | 5 | -module(emqttb_sup). 6 | 7 | -behaviour(supervisor). 8 | 9 | -export([start_link/0]). 10 | 11 | -export([init/1]). 12 | 13 | -include("emqttb.hrl"). 14 | 15 | -define(SERVER, ?MODULE). 16 | 17 | start_link() -> 18 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 19 | 20 | init([]) -> 21 | SupFlags = #{ strategy => one_for_one 22 | , intensity => 0 23 | , period => 1 24 | , auto_shutdown => any_significant 25 | }, 26 | ChildSpecs = [ metrics() 27 | , scenarios_sup() 28 | , sup(emqttb_autorate_sup) 29 | , sup(emqttb_group_sup) 30 | , sup(emqttb_misc_sup) %% Allows restarts 31 | ], 32 | {ok, {SupFlags, ChildSpecs}}. 33 | 34 | %% internal functions 35 | 36 | metrics() -> 37 | #{ id => emqttb_metrics 38 | , start => {emqttb_metrics, start_link, []} 39 | , type => worker 40 | , shutdown => 1000 41 | }. 42 | 43 | scenarios_sup() -> 44 | #{ id => emqttb_scenarios_sup 45 | , start => {emqttb_scenarios_sup, start_link, []} 46 | , type => supervisor 47 | , shutdown => infinity 48 | , significant => true 49 | , restart => transient 50 | }. 51 | 52 | sup(Module) -> 53 | #{ id => Module 54 | , start => {Module, start_link, []} 55 | , type => supervisor 56 | , shutdown => infinity 57 | }. 58 | 59 | %% cluster_discovery() -> 60 | %% %% Low-tech cluster discovery with a single point of failure: the 61 | %% %% "bootstrap" node. If the bootstrap node fails, the cluster won't 62 | %% %% start. But we're not aiming for five nines here. 63 | %% logger:notice("Connecting to the cluster...", []), 64 | %% cluster_discovery_loop(), 65 | %% timer:sleep(1000), 66 | %% logger:notice("Joined the cluster", []). 67 | 68 | %% cluster_discovery_loop() -> 69 | %% BootstrapNode = ?CFG(bootstrap_node), 70 | %% case net_adm:ping(BootstrapNode) of 71 | %% pong -> 72 | %% ok; 73 | %% _ -> 74 | %% timer:sleep(1000), 75 | %% cluster_discovery_loop() 76 | %% end. 77 | -------------------------------------------------------------------------------- /src/metrics/emqttb_grafana.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_grafana). 17 | 18 | %% API: 19 | -export([annotate/2, annotate/1, annotate_range/4]). 20 | 21 | -export([parse_comma_separated/1]). 22 | 23 | -include("emqttb.hrl"). 24 | -include_lib("typerefl/include/types.hrl"). 25 | 26 | %%================================================================================ 27 | %% Type declarations 28 | %%================================================================================ 29 | 30 | -type tags() :: [string()]. 31 | -typerefl_from_string({tags/0, ?MODULE, parse_comma_separated}). 32 | 33 | -reflect_type([tags/0]). 34 | 35 | %%================================================================================ 36 | %% API functions 37 | %%================================================================================ 38 | 39 | -spec annotate(iolist()) -> ok. 40 | annotate(Text) -> 41 | annotate(Text, []). 42 | 43 | -spec annotate_range(iolist(), [string()], integer(), integer()) -> ok. 44 | annotate_range(Text, Tags, Time, TimeEnd) -> 45 | Range = #{time => Time, 'timeEnd' => TimeEnd}, 46 | annotate(Text, Tags, Range). 47 | 48 | -spec annotate(iolist(), [atom() | string()]) -> ok. 49 | annotate(Text, Tags0) -> 50 | annotate(Text, Tags0, #{}). 51 | 52 | -spec annotate(iolist(), [atom() | string()], map()) -> ok. 53 | annotate(Text, Tags0, Data) -> 54 | case ?CFG([metrics, grafana, enabled]) of 55 | true -> 56 | Tags = lists:map(fun ensure_string/1, Tags0), 57 | spawn(fun() -> do_annotate(Text, Tags, Data) end), 58 | ok; 59 | false -> 60 | ok 61 | end. 62 | 63 | %%================================================================================ 64 | %% Internal functions 65 | %%================================================================================ 66 | 67 | do_annotate(Text, Tags, Data0) -> 68 | Url = ?CFG([metrics, grafana, url]) ++ "/api/annotations", 69 | AuthToken = case ?CFG([metrics, grafana, api_key]) of 70 | false -> []; 71 | Token -> [{<<"Authorization">>, Token}] 72 | end, 73 | Headers = [{<<"Content-Type">>, <<"application/json">>} | AuthToken], 74 | Options = case AuthToken =:= [] andalso ?CFG([metrics, grafana, login]) of 75 | false -> []; 76 | Login -> [{basic_auth, {Login, ?CFG([metrics, grafana, password])}}] 77 | end, 78 | Data = Data0#{ text => iolist_to_binary(Text) 79 | , tags => [<<"emqttb">>, <<"mqtt">> | Tags] 80 | }, 81 | {ok, Code, _RespHeaders, ClientRef} = hackney:post(Url, Headers, jsone:encode(Data), Options), 82 | case Code of 83 | 200 -> 84 | hackney:skip_body(ClientRef); 85 | _ -> 86 | {ok, Body} = hackney:body(ClientRef), 87 | logger:warning("Grafana response code ~p: ~s", [Code, Body]) 88 | end. 89 | 90 | ensure_string(Atom) when is_atom(Atom) -> 91 | atom_to_binary(Atom); 92 | ensure_string(Str) -> 93 | list_to_binary(Str). 94 | 95 | parse_comma_separated(Str) -> 96 | {ok, string:tokens(Str, ",")}. 97 | -------------------------------------------------------------------------------- /src/metrics/emqttb_pushgw.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_pushgw). 17 | 18 | %% API: 19 | -export([start_link/0]). 20 | 21 | %% behavior callbacks: 22 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2]). 23 | 24 | -include("emqttb.hrl"). 25 | 26 | %%================================================================================ 27 | %% API funcions 28 | %%================================================================================ 29 | 30 | start_link() -> 31 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 32 | 33 | %%================================================================================ 34 | %% behavior callbacks 35 | %%================================================================================ 36 | 37 | init([]) -> 38 | start_timer(), 39 | {ok, []}. 40 | 41 | handle_call(_Msg, _From, State) -> 42 | {reply, {error, unknown_call}, State}. 43 | 44 | handle_cast(_Msg, State) -> 45 | {noreply, State}. 46 | 47 | handle_info(collect, State) -> 48 | start_timer(), 49 | do_collect(), 50 | {noreply, State}; 51 | handle_info(_Msg, State) -> 52 | {noreply, State}. 53 | 54 | %%================================================================================ 55 | %% Internal functions 56 | %%================================================================================ 57 | 58 | start_timer() -> 59 | erlang:send_after(?CFG([metrics, pushgateway, interval]), self(), collect). 60 | 61 | do_collect() -> 62 | Uri = ?CFG([metrics, pushgateway, url]), 63 | [Name, Ip] = string:tokens(atom_to_list(node()), "@"), 64 | Url = lists:concat([Uri, "/metrics/job/", Name, "/instance/", Name, "~", Ip]), 65 | Data = prometheus_text_format:format(), 66 | Headers = [], 67 | Options = [], 68 | {ok, _Code, _RespHeaders, ClientRef} = hackney:post(Url, Headers, Data, Options), 69 | hackney:skip_body(ClientRef). 70 | -------------------------------------------------------------------------------- /src/restapi/emqttb_http.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_http). 17 | 18 | %% API: 19 | -export([start_link/0]). 20 | 21 | -export_type([rest_options/0]). 22 | 23 | -include("emqttb.hrl"). 24 | 25 | -import(lee_doc, [sect/3, p/1, li/2, href/2]). 26 | 27 | %%================================================================================ 28 | %% Type declarations 29 | %%================================================================================ 30 | 31 | -type rest_options() :: map(). 32 | 33 | %%================================================================================ 34 | %% API funcions 35 | %%================================================================================ 36 | 37 | -include_lib("kernel/include/logger.hrl"). 38 | 39 | -spec start_link() -> {ok, pid()} | {error, _}. 40 | start_link() -> 41 | {IP, Port} = ?CFG([restapi, listen_port]), 42 | logger:info("Starting REST API at ~p:~p", [IP, Port]), 43 | TransportOpts = #{ port => Port 44 | , ip => IP 45 | }, 46 | Env = #{dispatch => dispatch()}, 47 | ProtocolOpts = #{env => Env}, 48 | StartFun = case ?CFG([restapi, tls]) of 49 | true -> 50 | ?LOG_INFO("Starting HTTPS listener with parameters ~p", [ProtocolOpts]), 51 | fun cowboy:start_tls/3; 52 | false -> 53 | ?LOG_INFO("Starting HTTP listener with parameters ~p", [ProtocolOpts]), 54 | fun cowboy:start_clear/3 55 | end, 56 | StartFun(rest_api, maps:to_list(TransportOpts), ProtocolOpts). 57 | 58 | %%================================================================================ 59 | %% Internal functions 60 | %%================================================================================ 61 | 62 | dispatch() -> 63 | cowboy_router:compile([{'_', routes()}]). 64 | 65 | routes() -> 66 | RootDir = os:getenv("ROOTDIR"), 67 | [ {"/healthcheck", emqttb_http_healthcheck, []} 68 | , {"/metrics", emqttb_http_metrics, []} 69 | , {"/scenario/:scenario/stage", emqttb_http_stage, []} 70 | , {"/conf/reload", emqttb_http_sighup, []} 71 | , {"/doc/[...]", cowboy_static, {dir, filename:join(RootDir, "doc/html")}} 72 | ]. 73 | -------------------------------------------------------------------------------- /src/restapi/emqttb_http_healthcheck.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_http_healthcheck). 17 | 18 | -export([ init/2 19 | , init/3 20 | , descr/0 21 | , handle_request/2 22 | , content_types_provided/2 23 | ]). 24 | 25 | descr() -> 26 | "Healthcheck endpoint. Just returns 200 all the time.". 27 | 28 | init(Req, Opts) -> 29 | {cowboy_rest, Req, Opts}. 30 | 31 | init(_Transport, _Req, []) -> 32 | {upgrade, protocol, cowboy_rest}. 33 | 34 | content_types_provided(Req, State) -> 35 | {[{<<"text/plain">>, handle_request}], Req, State}. 36 | 37 | handle_request(Req, State) -> 38 | {<<"">>, Req, State}. 39 | -------------------------------------------------------------------------------- /src/restapi/emqttb_http_metrics.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_http_metrics). 17 | 18 | -export([ init/2 19 | , allowed_methods/2 20 | , content_types_provided/2 21 | , text/2 22 | , descr/0 23 | ]). 24 | 25 | descr() -> 26 | "Prometheus metrics endpoiont". 27 | 28 | init(Req, State) -> 29 | {cowboy_rest, Req, State}. 30 | 31 | allowed_methods(Req, State) -> 32 | {[<<"GET">>], Req, State}. 33 | 34 | content_types_provided(Req, State) -> 35 | { [{<<"text/plain">>, text}] 36 | , Req 37 | , State 38 | }. 39 | 40 | text(Req, State) -> 41 | Body = prometheus_text_format:format(), 42 | {Body, Req, State}. 43 | 44 | %%%_* Emacs ==================================================================== 45 | %%% Local Variables: 46 | %%% allout-layout: t 47 | %%% erlang-indent-level: 2 48 | %%% End: 49 | -------------------------------------------------------------------------------- /src/restapi/emqttb_http_scenario_conf.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_http_scenario_conf). 17 | 18 | -export([ init/2 19 | , init/3 20 | , descr/0 21 | , handle_request/2 22 | , content_types_provided/2 23 | , allowed_methods/2 24 | , resource_exists/2 25 | ]). 26 | 27 | descr() -> 28 | "Get or set a configuration parameter for a scenario while it's running.". 29 | 30 | init(Req, Opts) -> 31 | {cowboy_rest, Req, Opts}. 32 | 33 | init(_Transport, _Req, []) -> 34 | {upgrade, protocol, cowboy_rest}. 35 | 36 | allowed_methods(Req , State) -> 37 | {[<<"GET">>, <<"PUT">>], Req, State}. 38 | 39 | resource_exists(Req = #{bindings := #{scenario := Scenario, key := _Key}}, State) -> 40 | Enabled = [atom_to_binary(I:name()) || I <- emqttb_scenario:list_enabled_scenarios()], 41 | Exists = lists:member(Scenario, Enabled), 42 | {Exists, Req, State}. 43 | 44 | content_types_provided(Req, State) -> 45 | {[{<<"text/plain">>, handle_request}], Req, State}. 46 | 47 | handle_request(Req = #{bindings := #{scenario := Scenario}}, State) -> 48 | Stage = atom_to_binary(emqttb_scenario:stage(binary_to_atom(Scenario))), 49 | {Stage, Req, State}. 50 | -------------------------------------------------------------------------------- /src/restapi/emqttb_http_sighup.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_http_sighup). 17 | 18 | -export([ init/2 19 | , init/3 20 | , descr/0 21 | , handle_request/2 22 | , allowed_methods/2 23 | , content_types_accepted/2 24 | ]). 25 | 26 | descr() -> 27 | "Reload configuration in the runtime.". 28 | 29 | init(Req, Opts) -> 30 | {cowboy_rest, Req, Opts}. 31 | 32 | init(_Transport, _Req, []) -> 33 | {upgrade, protocol, cowboy_rest}. 34 | 35 | allowed_methods(Req , State) -> 36 | {[<<"POST">>], Req, State}. 37 | 38 | content_types_accepted(Req, State) -> 39 | {[{'*', handle_request}], Req, State}. 40 | 41 | handle_request(Req, State) -> 42 | {emqttb_conf:reload(), Req, State}. 43 | -------------------------------------------------------------------------------- /src/restapi/emqttb_http_stage.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_http_stage). 17 | 18 | -export([ init/2 19 | , init/3 20 | , descr/0 21 | , handle_request/2 22 | , content_types_provided/2 23 | , resource_exists/2 24 | ]). 25 | 26 | descr() -> 27 | "Returns the currently running stage of a scenario.". 28 | 29 | init(Req, Opts) -> 30 | {cowboy_rest, Req, Opts}. 31 | 32 | init(_Transport, _Req, []) -> 33 | {upgrade, protocol, cowboy_rest}. 34 | 35 | resource_exists(Req = #{bindings := #{scenario := Scenario}}, State) -> 36 | Enabled = [atom_to_binary(I:name()) || I <- emqttb_scenario:list_enabled_scenarios()], 37 | Exists = lists:member(Scenario, Enabled), 38 | {Exists, Req, State}. 39 | 40 | content_types_provided(Req, State) -> 41 | {[{<<"text/plain">>, handle_request}], Req, State}. 42 | 43 | handle_request(Req = #{bindings := #{scenario := Scenario}}, State) -> 44 | Stage = atom_to_binary(emqttb_scenario:stage(binary_to_atom(Scenario))), 45 | {Stage, Req, State}. 46 | -------------------------------------------------------------------------------- /src/scenarios/emqttb_scenario_conn.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %%Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_scenario_conn). 17 | 18 | -behavior(emqttb_scenario). 19 | 20 | 21 | %% behavior callbacks: 22 | -export([ model/0 23 | , run/0 24 | , initial_config/0 25 | ]). 26 | 27 | %% internal exports: 28 | -export([]). 29 | 30 | -export_type([]). 31 | 32 | -include("emqttb.hrl"). 33 | -include_lib("typerefl/include/types.hrl"). 34 | 35 | -import(emqttb_scenario, [complete/1, loiter/0, my_conf/1, my_conf_key/1, set_stage/2, set_stage/1]). 36 | 37 | %%================================================================================ 38 | %% Type declarations 39 | %%================================================================================ 40 | 41 | -define(GROUP, conn). 42 | 43 | %%================================================================================ 44 | %% behavior callbacks 45 | %%================================================================================ 46 | 47 | model() -> 48 | #{ conninterval => 49 | {[value, cli_param, autorate], 50 | #{ oneliner => "Client connection interval" 51 | , type => emqttb:duration_us() 52 | , default_ref => [interval] 53 | , cli_operand => "conninterval" 54 | , cli_short => $I 55 | , autorate_id => 'conn/conninterval' 56 | }} 57 | , n_clients => 58 | {[value, cli_param], 59 | #{ oneliner => "Number of clients" 60 | , type => emqttb:n_clients() 61 | , default_ref => [n_clients] 62 | , cli_operand => "num-clients" 63 | , cli_short => $N 64 | }} 65 | , group => 66 | {[value, cli_param], 67 | #{ oneliner => "ID of the client group" 68 | , type => atom() 69 | , default => default 70 | , cli_operand => "group" 71 | , cli_short => $g 72 | }} 73 | , expiry => 74 | {[value, cli_param], 75 | #{ oneliner => "Set 'Session-Expiry' for persistent sessions (seconds)" 76 | , type => union(non_neg_integer(), undefined) 77 | , default => undefined 78 | , cli_operand => "expiry" 79 | , cli_short => $x 80 | }} 81 | , clean_start => 82 | {[value, cli_param], 83 | #{ oneliner => "Clean start" 84 | , type => boolean() 85 | , default => true 86 | , cli_operand => "clean-start" 87 | , cli_short => $C 88 | }} 89 | %% Metrics: 90 | , metrics => 91 | emqttb_behavior_conn:model('conn/conn') 92 | }. 93 | 94 | initial_config() -> 95 | emqttb_conf:string2patch("@a -a conn/conninterval --pvar '[scenarios,conn,{},metrics,conn_latency,pending]' --olp"). 96 | 97 | run() -> 98 | GroupId = ?GROUP, 99 | Opts = #{ expiry => my_conf([expiry]) 100 | , clean_start => my_conf([clean_start]) 101 | , metrics => my_conf_key([metrics]) 102 | }, 103 | emqttb_group:ensure(#{ id => GroupId 104 | , client_config => my_conf([group]) 105 | , behavior => {emqttb_behavior_conn, Opts} 106 | , conn_interval => emqttb_autorate:from_model(my_conf_key([conninterval])) 107 | }), 108 | Interval = my_conf([conninterval]), 109 | set_stage(ramp_up), 110 | N = my_conf([n_clients]), 111 | {ok, _} = emqttb_group:set_target(GroupId, N, Interval), 112 | set_stage(run_traffic), 113 | loiter(), 114 | complete(ok). 115 | 116 | %%================================================================================ 117 | %% Internal exports 118 | %%================================================================================ 119 | 120 | %%================================================================================ 121 | %% Internal functions 122 | %%================================================================================ 123 | -------------------------------------------------------------------------------- /src/scenarios/emqttb_scenario_persistent_session.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2025 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_scenario_persistent_session). 17 | 18 | -behavior(emqttb_scenario). 19 | 20 | %% The scenario alternates between `subscribe_phase' and 21 | %% `publish_phase' 22 | %% 23 | %% Publish stage ends after a set time interval 24 | %% 25 | %% Subscribe stage after the number of consumed messages per second 26 | %% drops to 0 27 | %% 28 | %% KPIs: 29 | %% - number of messages published during publish phase 30 | %% - time to consume consume the messages 31 | %% - subscription rate 32 | 33 | %% behavior callbacks: 34 | -export([ model/0 35 | , run/0 36 | , initial_config/0 37 | ]). 38 | 39 | %% internal exports: 40 | -export([]). 41 | 42 | -export_type([]). 43 | 44 | -include("emqttb.hrl"). 45 | -include("../framework/emqttb_internal.hrl"). 46 | -include_lib("typerefl/include/types.hrl"). 47 | 48 | -import(emqttb_scenario, [complete/1, loiter/0, my_conf/1, my_conf_key/1, set_stage/2, set_stage/1]). 49 | 50 | %%================================================================================ 51 | %% Type declarations 52 | %%================================================================================ 53 | 54 | -define(PUB_GROUP, 'persistent_session.pub'). 55 | -define(SUB_GROUP, 'persistent_session.sub'). 56 | 57 | -define(PUB_THROUGHPUT, emqttb_pers_sess_pub_throughput). 58 | -define(SUB_THROUGHPUT, emqttb_pers_sess_sub_throughput). 59 | -define(N_STUCK, emqttb_pers_sess_n_stuck). 60 | 61 | -define(CHECK_INTERVAL_MS, 10). 62 | 63 | -record(s, 64 | { produced = 0 65 | , consumed = 0 66 | , to_consume = 0 67 | , pubinterval :: non_neg_integer() | undefined 68 | }). 69 | 70 | %%================================================================================ 71 | %% behavior callbacks 72 | %%================================================================================ 73 | 74 | model() -> 75 | #{ pub => 76 | #{ qos => 77 | {[value, cli_param], 78 | #{ onliner => "QoS of published messages" 79 | , doc => "@quotation warning 80 | Changing QoS to values other then 2 is likely to cause consume stage to hang, 81 | since it has to consume the exact number of messages as previously produced. 82 | @end quotation 83 | " 84 | , type => emqttb:qos() 85 | , default => 2 86 | , cli_operand => "pub-qos" 87 | }} 88 | , msg_size => 89 | {[value, cli_param], 90 | #{ oneliner => "Size of the published message in bytes" 91 | , type => non_neg_integer() 92 | , cli_operand => "size" 93 | , cli_short => $s 94 | , default => 256 95 | }} 96 | , pubinterval => 97 | {[value, cli_param, autorate], 98 | #{ oneliner => "Message publishing interval (microsecond)" 99 | , type => emqttb:duration_us() 100 | , default_ref => [interval] 101 | , cli_operand => "pubinterval" 102 | , cli_short => $i 103 | , autorate_id => 'persistent_session/pubinterval' 104 | }} 105 | , n => 106 | {[value, cli_param], 107 | #{ oneliner => "Number of publishers" 108 | , type => emqttb:n_clients() 109 | , default => 10 110 | , cli_operand => "num-publishers" 111 | , cli_short => $P 112 | }} 113 | , topic_suffix => 114 | {[value, cli_param], 115 | #{ oneliner => "Suffix of the topic to publish to" 116 | , type => binary() 117 | , default => <<"%h/%n">> 118 | , cli_operand => "topic" 119 | , cli_short => $t 120 | }} 121 | , pub_time => 122 | {[value, cli_param], 123 | #{ oneliner => "Duration of publish stage" 124 | , type => emqttb:duration_ms() 125 | , default_str => "1s" 126 | , cli_operand => "pubtime" 127 | , cli_short => $T 128 | }} 129 | , metrics => 130 | emqttb_behavior_pub:model('persistent_session/pub') 131 | } 132 | , sub => 133 | #{ qos => 134 | {[value, cli_param], 135 | #{ oneliner => "Subscription QoS" 136 | , doc => "@quotation warning 137 | Changing QoS to values other then 2 is likely to cause consume stage to hang, 138 | since it has to consume the exact number of messages as previously produced. 139 | @end quotation 140 | " 141 | , type => emqttb:qos() 142 | , default => 2 143 | , cli_operand => "sub-qos" 144 | }} 145 | , n => 146 | {[value, cli_param], 147 | #{ oneliner => "Number of subscribers" 148 | , type => emqttb:n_clients() 149 | , default => 10 150 | , cli_operand => "num-subscribers" 151 | , cli_short => $S 152 | }} 153 | , expiry => 154 | {[value, cli_param], 155 | #{ oneliner => "Session expiry interval" 156 | , type => range(0, 16#FFFFFFFF) 157 | , default => 16#FFFFFFFF 158 | , cli_operand => "expiry" 159 | }} 160 | , metrics => 161 | emqttb_behavior_sub:model('persistent_session/sub') 162 | } 163 | , conninterval => 164 | {[value, cli_param, autorate], 165 | #{ oneliner => "Client connection interval" 166 | , type => emqttb:duration_us() 167 | , cli_operand => "conninterval" 168 | , cli_short => $I 169 | , default_str => "10ms" 170 | , autorate_id => 'persistent_session/conninterval' 171 | }} 172 | , group => 173 | {[value, cli_param], 174 | #{ oneliner => "ID of the client group" 175 | , type => atom() 176 | , default => default 177 | , cli_operand => "group" 178 | , cli_short => $g 179 | }} 180 | , n_cycles => 181 | {[value, cli_param], 182 | #{ oneliner => "How many times to repeat publish/consume cycle" 183 | , type => union(non_neg_integer(), inifinity) 184 | , default => 10 185 | , cli_operand => "cycles" 186 | , cli_short => $C 187 | }} 188 | , max_stuck_time => 189 | {[value, cli_param], 190 | #{ oneliner => "How long the consume stage can get stuck without any progress" 191 | , type => emqttb:duration_ms() 192 | , default_str => "10s" 193 | , cli_operand => "max-stuck-time" 194 | }} 195 | , verify_sequence => 196 | {[value, cli_param], 197 | #{ oneliner => "Run message sequence number analysis to check for gaps and unexpected repeats" 198 | , type => boolean() 199 | , default => false 200 | , cli_operand => "verify-sequence" 201 | }} 202 | }. 203 | 204 | initial_config() -> 205 | emqttb_conf:string2patch("@a -a persistent_session/pubinterval --pvar '[scenarios,persistent_session,{},pub,metrics,pub_latency,pending]'") ++ 206 | emqttb_conf:string2patch("@a -a persistent_session/conninterval --pvar '[scenarios,persistent_session,{},pub,metrics,conn_latency,pending]' --olp"). 207 | 208 | run() -> 209 | prometheus_summary:declare([ {name, ?PUB_THROUGHPUT} 210 | , {help, <<"Write throughput for the persistent session">>} 211 | ]), 212 | prometheus_summary:declare([ {name, ?SUB_THROUGHPUT} 213 | , {help, <<"Read throughput for the persistent session">>} 214 | ]), 215 | prometheus_counter:declare([ {name, ?N_STUCK} 216 | , {help, <<"Number of times the consumer got stuck">>} 217 | ]), 218 | NProd = try emqttb_metrics:get_counter(?CNT_PUB_MESSAGES(?PUB_GROUP)) 219 | catch _:_ -> 0 220 | end, 221 | NCons = try emqttb_metrics:get_counter(?CNT_SUB_MESSAGES(?SUB_GROUP)) 222 | catch _:_ -> 0 223 | end, 224 | S = #s{produced = NProd, consumed = NCons}, 225 | do_run(S, 0). 226 | 227 | %%================================================================================ 228 | %% Internal functions 229 | %%================================================================================ 230 | 231 | do_run(S0, N) -> 232 | case N < my_conf([n_cycles]) of 233 | true -> 234 | set_stage(consume), 235 | S1 = consume_stage(N, S0), 236 | set_stage(publish), 237 | S = publish_stage(S1), 238 | do_run(S, N + 1); 239 | false -> 240 | complete(ok) 241 | end. 242 | 243 | consume_stage(Cycle, S) -> 244 | TopicPrefix = topic_prefix(), 245 | SubOpts = #{ topic => <> 246 | , qos => my_conf([sub, qos]) 247 | , expiry => my_conf([sub, expiry]) 248 | , clean_start => Cycle =:= 0 249 | , metrics => my_conf_key([sub, metrics]) 250 | , verify_sequence => my_conf([verify_sequence]) 251 | }, 252 | emqttb_group:ensure(#{ id => ?SUB_GROUP 253 | , client_config => my_conf([group]) 254 | , behavior => {emqttb_behavior_sub, SubOpts} 255 | , conn_interval => emqttb_autorate:from_model(my_conf_key([conninterval])) 256 | }), 257 | N = my_conf([sub, n]), 258 | {ok, N} = emqttb_group:set_target(?SUB_GROUP, N), 259 | wait_consume_all(N, S), 260 | emqttb_group:stop(?SUB_GROUP), 261 | S#s{ to_consume = 0 262 | , consumed = total_consumed_messages() 263 | }. 264 | 265 | publish_stage(S = #s{produced = NPub0, pubinterval = PubInterval}) -> 266 | TopicPrefix = topic_prefix(), 267 | TopicSuffix = my_conf([pub, topic_suffix]), 268 | PubOpts = #{ topic => <> 269 | , pubinterval => my_conf_key([pub, pubinterval]) 270 | , msg_size => my_conf([pub, msg_size]) 271 | , qos => my_conf([pub, qos]) 272 | , metrics => my_conf_key([pub, metrics]) 273 | , metadata => true 274 | , random => true 275 | }, 276 | emqttb_group:ensure(#{ id => ?PUB_GROUP 277 | , client_config => my_conf([group]) 278 | , behavior => {emqttb_behavior_pub, PubOpts} 279 | , conn_interval => emqttb_autorate:from_model(my_conf_key([conninterval])) 280 | }), 281 | {ok, _} = emqttb_group:set_target(?PUB_GROUP, my_conf([pub, n])), 282 | PubTime = my_conf([pub, pub_time]), 283 | timer:sleep(PubTime), 284 | PubIntervalCref = emqttb_autorate:get_counter('persistent_session/pubinterval'), 285 | PubInterval2 = counters:get(PubIntervalCref, 1), 286 | emqttb_group:stop(?PUB_GROUP), 287 | NPub = emqttb_metrics:get_counter(emqttb_metrics:from_model(my_conf_key([pub, metrics, n_published]))), 288 | %% TODO: it doesn't take ramp up/down into account: 289 | prometheus_summary:observe(?PUB_THROUGHPUT, (NPub - NPub0) * timer:seconds(1) div PubTime), 290 | S#s{ produced = NPub 291 | , to_consume = NPub - NPub0 292 | , pubinterval = PubInterval2 293 | }. 294 | 295 | wait_consume_all(Nsubs, #s{to_consume = Nmsgs, consumed = Consumed}) -> 296 | LastConsumedMessages = total_consumed_messages(), 297 | do_consume(Consumed + Nsubs * Nmsgs, LastConsumedMessages, max_checks_without_progress()). 298 | 299 | do_consume(_, _, 0) -> 300 | %% We got stuck without progress for too long, just return the 301 | %% result. It will be unreliable for measuring throughput. 302 | prometheus_counter:inc(?N_STUCK), 303 | total_consumed_messages(); 304 | do_consume(Target, LastConsumedMessages, NChecksWithoutProgress) -> 305 | timer:sleep(?CHECK_INTERVAL_MS), 306 | N = total_consumed_messages(), 307 | logger:debug("Consumed ~p/~p", [N, Target]), 308 | if N >= Target -> 309 | %% Target reached. Consider all messages consumed and return: 310 | N; 311 | N =:= LastConsumedMessages -> 312 | %% Got stuck without progress: 313 | do_consume(Target, N, NChecksWithoutProgress - 1); 314 | true -> 315 | %% We didn't consume all the messages, but we've made some progress: 316 | do_consume(Target, N, max_checks_without_progress()) 317 | end. 318 | 319 | topic_prefix() -> 320 | <<"pers_session/">>. 321 | 322 | total_consumed_messages() -> 323 | CntrId = emqttb_metrics:from_model(my_conf_key([sub, metrics, n_received])), 324 | emqttb_metrics:get_counter(CntrId). 325 | 326 | max_checks_without_progress() -> 327 | my_conf([max_stuck_time]) div ?CHECK_INTERVAL_MS. 328 | -------------------------------------------------------------------------------- /src/scenarios/emqttb_scenario_pub.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %%Copyright (c) 2022-2025 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_scenario_pub). 17 | 18 | -behavior(emqttb_scenario). 19 | 20 | 21 | %% behavior callbacks: 22 | -export([ model/0 23 | , run/0 24 | , initial_config/0 25 | ]). 26 | 27 | %% internal exports: 28 | -export([]). 29 | 30 | -export_type([]). 31 | 32 | -include("emqttb.hrl"). 33 | -include_lib("typerefl/include/types.hrl"). 34 | 35 | -import(emqttb_scenario, [complete/1, loiter/0, my_conf_key/1, my_conf/1, set_stage/2, set_stage/1]). 36 | 37 | %%================================================================================ 38 | %% Type declarations 39 | %%================================================================================ 40 | 41 | -define(GROUP, 'pub'). 42 | 43 | %%================================================================================ 44 | %% behavior callbacks 45 | %%================================================================================ 46 | 47 | model() -> 48 | #{ topic => 49 | {[value, cli_param], 50 | #{ onliner => "Publish to topic" 51 | , doc => "@xref{Topic Patterns}\n" 52 | , type => binary() 53 | , cli_operand => "topic" 54 | , cli_short => $t 55 | }} 56 | , qos => 57 | {[value, cli_param], 58 | #{ oneliner => "QoS of the published messages" 59 | , type => emqttb:qos() 60 | , default => 0 61 | , cli_operand => "qos" 62 | , cli_short => $q 63 | }} 64 | , retain => 65 | {[value, cli_param], 66 | #{ oneliner => "Retain published messages" 67 | , type => boolean() 68 | , default => false 69 | , cli_operand => "retain" 70 | }} 71 | , msg_size => 72 | {[value, cli_param], 73 | #{ oneliner => "Size of the published message in bytes" 74 | , type => emqttb:byte_size() 75 | , cli_operand => "size" 76 | , cli_short => $s 77 | , default => 256 78 | }} 79 | , random => 80 | {[value, cli_param], 81 | #{ oneliner => "Randomize message contents" 82 | , type => boolean() 83 | , cli_operand => "random" 84 | , default => false 85 | }} 86 | , conninterval => 87 | {[value, cli_param, autorate], 88 | #{ oneliner => "Client connection interval (microsecond)" 89 | , type => emqttb:duration_us() 90 | , default_ref => [interval] 91 | , cli_operand => "conninterval" 92 | , cli_short => $I 93 | , autorate_id => 'pub/conninterval' 94 | }} 95 | , pubinterval => 96 | {[value, cli_param, autorate], 97 | #{ oneliner => "Message publishing interval (microsecond)" 98 | , type => emqttb:duration_us() 99 | , default_ref => [interval] 100 | , cli_operand => "pubinterval" 101 | , cli_short => $i 102 | , autorate_id => 'pub/pubinterval' 103 | }} 104 | , n_clients => 105 | {[value, cli_param], 106 | #{ oneliner => "Number of clients" 107 | , type => emqttb:n_clients() 108 | , default_ref => [n_clients] 109 | , cli_operand => "num-clients" 110 | , cli_short => $N 111 | }} 112 | , group => 113 | {[value, cli_param, pointer], 114 | #{ oneliner => "ID of the client group" 115 | , type => atom() 116 | , default => default 117 | , cli_operand => "group" 118 | , cli_short => $g 119 | , target_node => [groups] 120 | }} 121 | , metadata => 122 | {[value, cli_param], 123 | #{ oneliner => "Add metadata to the messages" 124 | , type => boolean() 125 | , default => false 126 | , cli_operand => "metadata" 127 | }} 128 | , start_n => 129 | {[value, cli_param], 130 | #{ oneliner => "Initial worker number for this bench (used for multi-loadgen test alignment)" 131 | , type => integer() 132 | , default => 0 133 | , cli_operand => "start-n" 134 | }} 135 | , expiry => 136 | {[value, cli_param], 137 | #{ type => union(non_neg_integer(), undefined) 138 | , default => undefined 139 | , cli_operand => "expiry" 140 | , cli_short => $x 141 | }} 142 | , clean_start => 143 | {[value, cli_param], 144 | #{ type => boolean() 145 | , default => true 146 | , cli_operand => "clean-start" 147 | , cli_short => $c 148 | }} 149 | , metrics => 150 | emqttb_behavior_pub:model('pub/pub') 151 | }. 152 | 153 | initial_config() -> 154 | emqttb_conf:string2patch("@a -a pub/pubinterval --pvar '[scenarios,pub,{},metrics,pub_latency,pending]'") ++ 155 | emqttb_conf:string2patch("@a -a pub/conninterval --pvar '[scenarios,pub,{},metrics,conn_latency,pending]' --olp"). 156 | 157 | run() -> 158 | PubOpts = #{ topic => my_conf([topic]) 159 | , pubinterval => my_conf_key([pubinterval]) 160 | , msg_size => my_conf([msg_size]) 161 | , qos => my_conf([qos]) 162 | , retain => my_conf([retain]) 163 | , metadata => my_conf([metadata]) 164 | , metrics => my_conf_key([metrics]) 165 | }, 166 | emqttb_group:ensure(#{ id => ?GROUP 167 | , client_config => my_conf([group]) 168 | , behavior => {emqttb_behavior_pub, PubOpts} 169 | , start_n => my_conf([start_n]) 170 | , conn_interval => emqttb_autorate:from_model(my_conf_key([conninterval])) 171 | }), 172 | Interval = my_conf([conninterval]), 173 | set_stage(ramp_up), 174 | N = my_conf([n_clients]), 175 | {ok, _} = emqttb_group:set_target(?GROUP, N, Interval), 176 | set_stage(run_traffic), 177 | loiter(), 178 | complete(ok). 179 | 180 | %%================================================================================ 181 | %% Internal exports 182 | %%================================================================================ 183 | 184 | %%================================================================================ 185 | %% Internal functions 186 | %%================================================================================ 187 | -------------------------------------------------------------------------------- /src/scenarios/emqttb_scenario_pubsub_fwd.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_scenario_pubsub_fwd). 17 | 18 | -behavior(emqttb_scenario). 19 | 20 | %% First all subscribers connect and subscribe to the brokers, then 21 | %% the publishers start to connect and publish. The default is to use 22 | %% full forwarding of messages between the nodes: that is, each 23 | %% publisher client publishes to a topic subscribed by a single 24 | %% client, and both clients reside on distinct nodes. 25 | 26 | %% behavior callbacks: 27 | -export([ model/0 28 | , run/0 29 | , initial_config/0 30 | ]). 31 | 32 | %% internal exports: 33 | -export([]). 34 | 35 | -export_type([]). 36 | 37 | -include("emqttb.hrl"). 38 | -include("../framework/emqttb_internal.hrl"). 39 | -include_lib("typerefl/include/types.hrl"). 40 | 41 | -import(emqttb_scenario, [complete/1, loiter/0, my_conf/1, my_conf_key/1, set_stage/2, set_stage/1]). 42 | 43 | %%================================================================================ 44 | %% Type declarations 45 | %%================================================================================ 46 | 47 | -define(PUB_GROUP, 'pubsub_fwd.pub'). 48 | -define(SUB_GROUP, 'pubsub_fwd.sub'). 49 | 50 | %%================================================================================ 51 | %% behavior callbacks 52 | %%================================================================================ 53 | 54 | model() -> 55 | #{ pub => 56 | #{ qos => 57 | {[value, cli_param], 58 | #{ oneliner => "QoS of the published messages" 59 | , type => emqttb:qos() 60 | , default => 1 61 | , cli_operand => "pub-qos" 62 | }} 63 | , msg_size => 64 | {[value, cli_param], 65 | #{ oneliner => "Size of the published message in bytes" 66 | , type => non_neg_integer() 67 | , cli_operand => "size" 68 | , cli_short => $s 69 | , default => 256 70 | }} 71 | , pubinterval => 72 | {[value, cli_param, autorate], 73 | #{ oneliner => "Message publishing interval (microsecond)" 74 | , type => emqttb:duration_us() 75 | , default_ref => [interval] 76 | , cli_operand => "pubinterval" 77 | , cli_short => $i 78 | , autorate_id => 'pubsub_fwd/pubinterval' 79 | }} 80 | %% Metrics: 81 | , n_published => 82 | {[metric], 83 | #{ oneliner => "Total number of published messages" 84 | , id => {emqttb_published_messages, pubsub_fwd} 85 | , metric_type => counter 86 | , labels => [scenario] 87 | }} 88 | , metrics => 89 | emqttb_behavior_pub:model('pubsub_fwd/pub') 90 | } 91 | , sub => 92 | #{ qos => 93 | {[value, cli_param], 94 | #{ oneliner => "Subscription QoS" 95 | , type => emqttb:qos() 96 | , default => 1 97 | , cli_operand => "sub-qos" 98 | }} 99 | , metrics => 100 | emqttb_behavior_sub:model('pubsub_fwd/sub') 101 | } 102 | , conninterval => 103 | {[value, cli_param, autorate], 104 | #{ oneliner => "Client connection interval" 105 | , type => emqttb:duration_us() 106 | , cli_operand => "conninterval" 107 | , cli_short => $I 108 | , default_str => "10ms" 109 | , autorate_id => 'pubsub_fwd/conninterval' 110 | }} 111 | , group => 112 | {[value, cli_param], 113 | #{ oneliner => "ID of the client group" 114 | , type => atom() 115 | , default => default 116 | , cli_operand => "group" 117 | , cli_short => $g 118 | }} 119 | , num_clients => 120 | {[value, cli_param], 121 | #{ oneliner => "Total number of connections (pub + sub)" 122 | , type => emqttb:n_clients() 123 | , default => 100 124 | , cli_operand => "num-clients" 125 | , cli_short => $n 126 | }} 127 | , full_forwarding => 128 | {[value, cli_param], 129 | #{ oneliner => "Whether all messages should be forwarded between nodes" 130 | , type => boolean() 131 | , default => true 132 | , cli_operand => "full-forwarding" 133 | }} 134 | , start_n => 135 | {[value, cli_param], 136 | #{ oneliner => "Initial worker number for this bench (used for multi-loadgen test alignment)" 137 | , type => integer() 138 | , default => 0 139 | , cli_operand => "start-n" 140 | }} 141 | , random_hosts => 142 | {[value, cli_param], 143 | #{ oneliner => "Whether to use random hosts rather than 1-shifted round-robin" 144 | , type => boolean() 145 | , default => false 146 | , cli_operand => "random-hosts" 147 | }} 148 | }. 149 | 150 | initial_config() -> 151 | emqttb_conf:string2patch("@a -a pubsub_fwd/pubinterval --pvar '[scenarios,pubsub_fwd,{},pub,metrics,pub_latency,pending]'") ++ 152 | emqttb_conf:string2patch("@a -a pubsub_fwd/conninterval --pvar '[scenarios,pubsub_fwd,{},pub,metrics,conn_latency,pending]' --olp"). 153 | 154 | run() -> 155 | set_stage(subscribe), 156 | subscribe_stage(), 157 | set_stage(publish), 158 | publish_stage(), 159 | loiter(), 160 | complete(ok). 161 | 162 | %%================================================================================ 163 | %% Internal functions 164 | %%================================================================================ 165 | 166 | subscribe_stage() -> 167 | TopicPrefix = topic_prefix(), 168 | RandomHosts = my_conf([random_hosts]), 169 | HostSelection = case RandomHosts of 170 | true -> random; 171 | false -> round_robin 172 | end, 173 | SubOpts = #{ topic => <> 174 | , qos => my_conf([sub, qos]) 175 | , expiry => undefined 176 | , clean_start => true 177 | , host_shift => 0 178 | , host_selection => HostSelection 179 | , parse_metadata => true 180 | , metrics => my_conf_key([sub, metrics]) 181 | }, 182 | emqttb_group:ensure(#{ id => ?SUB_GROUP 183 | , client_config => my_conf([group]) 184 | , behavior => {emqttb_behavior_sub, SubOpts} 185 | , start_n => my_conf([start_n]) 186 | , conn_interval => emqttb_autorate:from_model(my_conf_key([conninterval])) 187 | }), 188 | N = my_conf([num_clients]) div 2, 189 | {ok, _} = emqttb_group:set_target(?SUB_GROUP, N), 190 | ok. 191 | 192 | publish_stage() -> 193 | TopicPrefix = topic_prefix(), 194 | RandomHosts = my_conf([random_hosts]), 195 | HostSelection = case RandomHosts of 196 | true -> random; 197 | false -> round_robin 198 | end, 199 | HostShift = case my_conf([full_forwarding]) of 200 | true -> 1; 201 | false -> 0 202 | end, 203 | PubOpts = #{ topic => <> 204 | , pubinterval => my_conf_key([pub, pubinterval]) 205 | , msg_size => my_conf([pub, msg_size]) 206 | , qos => my_conf([pub, qos]) 207 | , metadata => true 208 | , host_shift => HostShift 209 | , host_selection => HostSelection 210 | , metrics => my_conf_key([pub, metrics]) 211 | }, 212 | emqttb_group:ensure(#{ id => ?PUB_GROUP 213 | , client_config => my_conf([group]) 214 | , behavior => {emqttb_behavior_pub, PubOpts} 215 | , start_n => my_conf([start_n]) 216 | , conn_interval => emqttb_autorate:from_model(my_conf_key([conninterval])) 217 | }), 218 | N = my_conf([num_clients]) div 2, 219 | {ok, _} = emqttb_group:set_target(?PUB_GROUP, N), 220 | ok. 221 | 222 | topic_prefix() -> 223 | <<"pubsub_fwd/">>. 224 | -------------------------------------------------------------------------------- /src/scenarios/emqttb_scenario_sub.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %%Copyright (c) 2022-2025 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_scenario_sub). 17 | 18 | -behavior(emqttb_scenario). 19 | 20 | %% behavior callbacks: 21 | -export([ model/0 22 | , initial_config/0 23 | , run/0 24 | ]). 25 | 26 | %% internal exports: 27 | -export([]). 28 | 29 | -export_type([]). 30 | 31 | -include("emqttb.hrl"). 32 | -include_lib("typerefl/include/types.hrl"). 33 | 34 | -import(emqttb_scenario, [complete/1, loiter/0, my_conf/1, my_conf_key/1, set_stage/2, set_stage/1]). 35 | 36 | %%================================================================================ 37 | %% Type declarations 38 | %%================================================================================ 39 | 40 | -define(GROUP, sub). 41 | 42 | %%================================================================================ 43 | %% behavior callbacks 44 | %%================================================================================ 45 | 46 | model() -> 47 | #{ topic => 48 | {[value, cli_param], 49 | #{ oneliner => "Topic that the clients shall subscribe" 50 | , doc => "@xref{Topic Patterns}\n" 51 | , type => binary() 52 | , cli_operand => "topic" 53 | , cli_short => $t 54 | }} 55 | , conninterval => 56 | {[value, cli_param, autorate], 57 | #{ oneliner => "Client connection interval" 58 | , type => emqttb:duration_us() 59 | , default_ref => [interval] 60 | , cli_operand => "conninterval" 61 | , cli_short => $I 62 | , autorate_id => 'sub/conninterval' 63 | }} 64 | , n_clients => 65 | {[value, cli_param], 66 | #{ oneliner => "Number of clients" 67 | , type => emqttb:n_clients() 68 | , default_ref => [n_clients] 69 | , cli_operand => "num-clients" 70 | , cli_short => $N 71 | }} 72 | , group => 73 | {[value, cli_param], 74 | #{ oneliner => "ID of the client group" 75 | , type => atom() 76 | , default => default 77 | , cli_operand => "group" 78 | , cli_short => $g 79 | }} 80 | , expiry => 81 | {[value, cli_param], 82 | #{ oneliner => "Set 'Session-Expiry' for persistent sessions (seconds)" 83 | , doc => "See @url{https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901048,Session Expiry Interval}.\n" 84 | , type => union(non_neg_integer(), undefined) 85 | , default => undefined 86 | , cli_operand => "expiry" 87 | , cli_short => $x 88 | }} 89 | , qos => 90 | {[value, cli_param], 91 | #{ oneliner => "QoS of the subscription" 92 | , type => emqttb:qos() 93 | , default => 0 94 | , cli_operand => "qos" 95 | , cli_short => $q 96 | }} 97 | , parse_metadata => 98 | {[value, cli_param], 99 | #{ oneliner => "Extract metadata from message payloads" 100 | , doc => "Subscribers will report end-to-end latency when this option is enabled. 101 | 102 | @quotation Warning 103 | Publishers should insert metadata into the payloads. 104 | For example, when using @ref{value/scenarios/pub,pub scenario} it's necessary to enable @ref{value/scenarios/pub/_/metadata,metadata} generation. 105 | @end quotation 106 | 107 | @quotation Warning 108 | In order to measure latency accurately, the scenario should ensure that publishers reside on the same emqttb host with the subscribers. 109 | Otherwise clock skew between different load generator instances will introduce a systematic error. 110 | @end quotation 111 | 112 | " 113 | , type => boolean() 114 | , default => false 115 | , cli_operand => "parse-metadata" 116 | }} 117 | , verify_sequence => 118 | {[value, cli_param], 119 | #{ oneliner => "Verify sequence of messages" 120 | , doc => "@xref{Verify Message Sequence}. Implies @ref{value/scenarios/sub/_/parse_metadata,parse_metadata}.\n" 121 | , type => boolean() 122 | , default => false 123 | , cli_operand => "verify-sequence" 124 | }} 125 | , clean_start => 126 | {[value, cli_param], 127 | #{ oneliner => "Clean Start" 128 | , doc => "Note: in order to disable clean start (and make the session persistent) this flag should be set to @code{false} 129 | (for example, @code{emqttb @@sub +c ...} via CLI). 130 | " 131 | , type => boolean() 132 | , default => true 133 | , cli_operand => "clean-start" 134 | , cli_short => $c 135 | }} 136 | , metrics => 137 | emqttb_behavior_sub:model('sub/sub') 138 | }. 139 | 140 | initial_config() -> 141 | emqttb_conf:string2patch("@a -a sub/conninterval --pvar '[scenarios,sub,{},metrics,conn_latency,pending]' --olp"). 142 | 143 | run() -> 144 | SubOpts = #{ topic => my_conf([topic]) 145 | , qos => my_conf([qos]) 146 | , expiry => my_conf([expiry]) 147 | , parse_metadata => my_conf([parse_metadata]) 148 | , verify_sequence => my_conf([verify_sequence]) 149 | , clean_start => my_conf([clean_start]) 150 | , metrics => my_conf_key([metrics]) 151 | }, 152 | emqttb_group:ensure(#{ id => ?GROUP 153 | , client_config => my_conf([group]) 154 | , behavior => {emqttb_behavior_sub, SubOpts} 155 | , conn_interval => emqttb_autorate:from_model(my_conf_key([conninterval])) 156 | }), 157 | set_stage(ramp_up), 158 | N = my_conf([n_clients]), 159 | {ok, _} = emqttb_group:set_target(?GROUP, N), 160 | set_stage(run_traffic), 161 | loiter(), 162 | complete(ok). 163 | 164 | %%================================================================================ 165 | %% Internal exports 166 | %%================================================================================ 167 | 168 | %%================================================================================ 169 | %% Internal functions 170 | %%================================================================================ 171 | -------------------------------------------------------------------------------- /src/scenarios/emqttb_scenario_sub_flapping.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_scenario_sub_flapping). 17 | 18 | -behavior(emqttb_scenario). 19 | 20 | %% behavior callbacks: 21 | -export([ model/0 22 | , initial_config/0 23 | , run/0 24 | ]). 25 | 26 | %% internal exports: 27 | -export([]). 28 | 29 | -export_type([]). 30 | 31 | -include("emqttb.hrl"). 32 | -include_lib("typerefl/include/types.hrl"). 33 | 34 | -import(emqttb_scenario, [complete/1, loiter/0, my_conf/1, my_conf_key/1, set_stage/2, set_stage/1]). 35 | 36 | %%================================================================================ 37 | %% Type declarations 38 | %%================================================================================ 39 | 40 | -define(GROUP, sub). 41 | 42 | %%================================================================================ 43 | %% behavior callbacks 44 | %%================================================================================ 45 | 46 | model() -> 47 | #{ topic => 48 | {[value, cli_param], 49 | #{ oneliner => "Topic that the clients shall subscribe" 50 | , type => binary() 51 | , cli_operand => "topic" 52 | , cli_short => $t 53 | }} 54 | , conninterval => 55 | {[value, cli_param, autorate], 56 | #{ oneliner => "Client connection interval" 57 | , type => emqttb:duration_us() 58 | , default_ref => [interval] 59 | , cli_operand => "conninterval" 60 | , cli_short => $I 61 | , autorate_id => 'sub_flapping/conninterval' 62 | }} 63 | , n_clients => 64 | {[value, cli_param], 65 | #{ oneliner => "Number of clients" 66 | , type => emqttb:n_clients() 67 | , default_ref => [n_clients] 68 | , cli_operand => "num-clients" 69 | , cli_short => $N 70 | }} 71 | , group => 72 | {[value, cli_param], 73 | #{ oneliner => "ID of the client group" 74 | , type => atom() 75 | , default => default 76 | , cli_operand => "group" 77 | , cli_short => $g 78 | }} 79 | , expiry => 80 | {[value, cli_param], 81 | #{ oneliner => "Set 'Session-Expiry' for persistent sessions (seconds)" 82 | , type => union(non_neg_integer(), undefined) 83 | , default => undefined 84 | , cli_operand => "expiry" 85 | , cli_short => $x 86 | }} 87 | , qos => 88 | {[value, cli_param], 89 | #{ oneliner => "QoS of the subscription" 90 | , type => emqttb:qos() 91 | , default => 0 92 | , cli_operand => "qos" 93 | , cli_short => $q 94 | }} 95 | , clean_start => 96 | {[value, cli_param], 97 | #{ type => boolean() 98 | , default => true 99 | , cli_operand => "clean-start" 100 | , cli_short => $c 101 | }} 102 | , n_cycles => 103 | {[value, cli_param], 104 | #{ type => emqttb:n_cycles() 105 | , default => 10 106 | , cli_operand => "cycles" 107 | , cli_short => $C 108 | }} 109 | , metrics => 110 | emqttb_behavior_sub:model('sub_flapping/sub') 111 | }. 112 | 113 | initial_config() -> 114 | emqttb_conf:string2patch("@a -a sub_flapping/conninterval --pvar '[scenarios,sub_flapping,{},metrics,conn_latency,pending]' --olp"). 115 | 116 | run() -> 117 | SubOpts = #{ topic => my_conf([topic]) 118 | , qos => my_conf([qos]) 119 | , expiry => my_conf([expiry]) 120 | , clean_start => my_conf([clean_start]) 121 | , metrics => my_conf_key([metrics]) 122 | }, 123 | emqttb_group:ensure(#{ id => ?GROUP 124 | , client_config => my_conf([group]) 125 | , behavior => {emqttb_behavior_sub, SubOpts} 126 | , conn_interval => emqttb_autorate:from_model(my_conf_key([conninterval])) 127 | }), 128 | cycle(0, my_conf([n_cycles])). 129 | 130 | cycle(Cycle, Max) when Cycle >= Max -> 131 | complete(ok); 132 | cycle(Cycle, Max) -> 133 | set_stage(ramp_up), 134 | N = my_conf([n_clients]), 135 | {ok, _} = emqttb_group:set_target(?GROUP, N), 136 | set_stage(ramp_down), 137 | {ok, _} = emqttb_group:set_target(?GROUP, 0, undefined), 138 | cycle(Cycle + 1, Max). 139 | 140 | %%================================================================================ 141 | %% Internal exports 142 | %%================================================================================ 143 | 144 | %%================================================================================ 145 | %% Internal functions 146 | %%================================================================================ 147 | -------------------------------------------------------------------------------- /test/emqtt_gauge_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqtt_gauge_SUITE). 17 | 18 | 19 | -compile(nowarn_export_all). 20 | -compile(export_all). 21 | 22 | -include_lib("snabbkaffe/include/snabbkaffe.hrl"). 23 | -include_lib("stdlib/include/assert.hrl"). 24 | 25 | suite() -> 26 | [{timetrap, {seconds, 30}}]. 27 | 28 | all() -> 29 | snabbkaffe:mk_all(?MODULE). 30 | 31 | init_per_testcase(_, Config) -> 32 | snabbkaffe:fix_ct_logging(), 33 | {ok, _} = application:ensure_all_started(emqttb), 34 | Config. 35 | 36 | end_per_testcase(_, _Config) -> 37 | application:stop(emqttb), 38 | snabbkaffe:stop(), 39 | ok. 40 | 41 | t_rolling_average(Config) -> 42 | N = 10, 43 | G = emqttb_metrics:new_rolling_average({foo, []}, [{help, <<"">>}]), 44 | ?assertEqual(0, emqttb_metrics:get_rolling_average(G)), 45 | [emqttb_metrics:rolling_average_observe(G, 10) || _ <- lists:seq(1, N)], 46 | ?assertEqual(10, emqttb_metrics:get_rolling_average(G)), 47 | timer:sleep(1000), 48 | ?assertEqual(0, emqttb_metrics:get_rolling_average(G, 500)), 49 | %% Check value with a larger window: 50 | ?assertEqual(10, emqttb_metrics:get_rolling_average(G, 5000)), 51 | %% Add more samples: 52 | [emqttb_metrics:rolling_average_observe(G, 20) || _ <- lists:seq(1, N)], 53 | ?assertEqual(20, emqttb_metrics:get_rolling_average(G, 500)), 54 | %% Large window must produce the average of 20 and 10: 55 | ?assertEqual(15, emqttb_metrics:get_rolling_average(G, 5000)). 56 | -------------------------------------------------------------------------------- /test/emqttb_dummy_behavior.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_dummy_behavior). 17 | 18 | -behavior(emqttb_worker). 19 | 20 | %% behavior callbacks: 21 | -export([init_per_group/2, init/1, handle_message/3, terminate/2]). 22 | 23 | -include_lib("snabbkaffe/include/trace.hrl"). 24 | 25 | -import(emqttb_worker, [my_group/0, my_id/0, my_clientid/0, my_cfg/1, connect/2]). 26 | 27 | %%================================================================================ 28 | %% behavior callbacks 29 | %%================================================================================ 30 | 31 | init_per_group(Group, Cfg) -> 32 | Cfg#{group => Group}. 33 | 34 | init(Shared) -> 35 | State = 0, 36 | ?tp(emqttb_dummy, #{ group => my_group() 37 | , id => my_id() 38 | , state => State 39 | , callback => ?FUNCTION_NAME 40 | }), 41 | case maps:get(?FUNCTION_NAME, Shared, ok) of 42 | error -> 43 | error(deliberate); 44 | _ -> 45 | State 46 | end. 47 | 48 | handle_message(Shared, State0, Msg) -> 49 | State = State0 + 1, 50 | ?tp(emqttb_dummy, #{ group => my_group() 51 | , id => my_id() 52 | , msg => Msg 53 | , state => State 54 | , callback => ?FUNCTION_NAME 55 | }), 56 | case maps:get(?FUNCTION_NAME, Shared, ok) of 57 | ok -> 58 | {ok, State}; 59 | error -> 60 | error(deliberate); 61 | invalid_return -> 62 | State 63 | end. 64 | 65 | terminate(Shared, State0) -> 66 | State = State0 + 1, 67 | ?tp(emqttb_dummy, #{ group => my_group() 68 | , id => my_id() 69 | , state => State 70 | , callback => ?FUNCTION_NAME 71 | }), 72 | case maps:get(?FUNCTION_NAME, Shared, ok) of 73 | error -> 74 | error(deliberate); 75 | _ -> 76 | State 77 | end. 78 | -------------------------------------------------------------------------------- /test/emqttb_worker_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqttb_worker_SUITE). 17 | 18 | 19 | -compile(nowarn_export_all). 20 | -compile(export_all). 21 | 22 | -include_lib("snabbkaffe/include/snabbkaffe.hrl"). 23 | -include_lib("stdlib/include/assert.hrl"). 24 | 25 | suite() -> 26 | [{timetrap, {seconds, 30}}]. 27 | 28 | all() -> 29 | snabbkaffe:mk_all(?MODULE). 30 | 31 | init_per_testcase(_, Config) -> 32 | snabbkaffe:fix_ct_logging(), 33 | {ok, _} = application:ensure_all_started(emqttb), 34 | Config. 35 | 36 | end_per_testcase(_, _Config) -> 37 | application:stop(emqttb), 38 | snabbkaffe:stop(), 39 | ok. 40 | 41 | t_group(_Config) -> 42 | NClients = 10, 43 | Group = test_group, 44 | ?check_trace( 45 | #{timeout => 1000}, 46 | begin 47 | {ok, Pid} = emqttb_group:start_link(#{ id => Group 48 | , client_config => #{} 49 | , behavior => {emqttb_dummy_behavior, #{}} 50 | , conn_interval => 'conn/conninterval' 51 | }), 52 | {ok, NActual} = emqttb_group:set_target(Group, NClients, 1), 53 | ?assert(NActual >= NClients), 54 | emqttb_group:broadcast(Group, message1), 55 | emqttb_group:broadcast(Group, message2), 56 | unlink(Pid), 57 | exit(Pid, shutdown) 58 | end, 59 | [ fun ?MODULE:worker_counter_spec/1 60 | , fun ?MODULE:state_continuity_spec/1 61 | , fun ?MODULE:cb_terminate_spec/1 62 | , {"Check that broadcast reaches all the clients", 63 | fun(Trace) -> 64 | [?assert( 65 | ?strict_causality( #{?snk_kind := emqttb_group_broadcast, message := _Msg} 66 | , #{?snk_kind := emqttb_dummy, id := Id, msg := _Msg} when Id =:= N 67 | , Trace 68 | )) 69 | || N <- lists:seq(0, NClients - 1)], 70 | true 71 | end} 72 | ]). 73 | 74 | t_error_in_init(_Config) -> 75 | error_scenario(#{init => error}). 76 | 77 | t_error_in_handle_msg(_Config) -> 78 | error_scenario(#{handle_message => error}). 79 | 80 | t_invalid_return(_Config) -> 81 | error_scenario(#{handle_message => invalid_return}). 82 | 83 | t_error_in_terminate(_Config) -> 84 | error_scenario(#{terminate => error}). 85 | 86 | error_scenario(Config) -> 87 | NClients = 1, 88 | Group = test_group, 89 | ?check_trace( 90 | #{timeout => 10, timetrap => 1000}, 91 | begin 92 | {ok, Pid} = emqttb_group:start_link(#{ id => Group 93 | , client_config => #{} 94 | , behavior => {emqttb_dummy_behavior, Config} 95 | , conn_interval => 'conn/conninterval' 96 | }), 97 | emqttb_group:set_target_async(Group, NClients, 1), 98 | %% Wait until the first worker start: 99 | ?block_until(#{?snk_kind := emqttb_dummy}), 100 | %% Before triggering crashes, make sure the group won't try to 101 | %% resurrect the workers: 102 | emqttb_group:set_target_async(Group, 0, 0), 103 | emqttb_group:broadcast(Group, heyhey), 104 | ?tp("Stopping test group", #{}), 105 | unlink(Pid), 106 | exit(Pid, shutdown) 107 | end, 108 | [fun ?MODULE:cb_terminate_spec/1 || not maps:is_key(init, Config)] ++ 109 | [fun ?MODULE:worker_counter_spec/1]). 110 | 111 | %% Trace specifications: 112 | 113 | %% Ensure that worker counter is always decremented 114 | worker_counter_spec(Trace) -> 115 | ?assert( 116 | ?strict_causality( #{?snk_kind := emqttb_worker_start, gl := _GL, number := _N} 117 | , #{?snk_kind := emqttb_worker_terminate, gl := _GL, number := _N} 118 | , Trace 119 | )). 120 | 121 | %% Ensure that the worker state is passed through callbacks 122 | state_continuity_spec(Trace) -> 123 | T0 = [{{Grp, Id}, S} || #{ ?snk_kind := emqttb_dummy 124 | , group := Grp 125 | , id := Id 126 | , state := S 127 | } <- Trace], 128 | T = lists:keysort(1, T0), %% Stable sort by group and ID, so it won't rearrange states 129 | ?assert(snabbkaffe:strictly_increasing(T)). 130 | 131 | %% Ensure that for every `init' there is a call of `terminate' callback 132 | cb_terminate_spec(Trace) -> 133 | ?assert( 134 | ?strict_causality( #{?snk_kind := emqttb_dummy, group := _G, id := _N, callback := init} 135 | , #{?snk_kind := emqttb_dummy, group := _G, id := _N, callback := terminate} 136 | , Trace 137 | )). 138 | -------------------------------------------------------------------------------- /test/escript_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(escript_SUITE). 17 | 18 | 19 | -compile(nowarn_export_all). 20 | -compile(export_all). 21 | 22 | -include_lib("snabbkaffe/include/ct_boilerplate.hrl"). 23 | 24 | suite() -> 25 | [{timetrap, {seconds, 30}}]. 26 | 27 | t_no_args(Config) when is_list(Config) -> 28 | ?assertMatch(0, run("")). 29 | 30 | t_pub(Config) when is_list(Config) -> 31 | ?assertMatch(0, run("--loiter 0 @pub -t foo -I 1000 -N 0")). 32 | 33 | t_sub(Config) when is_list(Config) -> 34 | ?assertMatch(0, run("--loiter 0 @sub -t foo -N 0")). 35 | 36 | t_conn(Config) when is_list(Config) -> 37 | ?assertMatch(0, run("--loiter 0 @conn -N 0")). 38 | 39 | t_pubsub_fwd(Config) when is_list(Config) -> 40 | ?assertMatch(0, run("--loiter 0 @pubsub_fwd -n 0")). 41 | 42 | t_sub_flapping(Config) when is_list(Config) -> 43 | ?assertMatch(0, run("--loiter 0 @sub_flapping -t foo --cycles 1 -N 0")). 44 | 45 | t_persistent_session(Config) when is_list(Config) -> 46 | ?assertMatch(0, run("@persistent_session --cycles 1 --pubtime 1ms -P 0 -S 0")). 47 | 48 | t_set_group_config(Config) when is_list(Config) -> 49 | ?assertMatch(0, run("@g -p 9090")), 50 | ?assertMatch(0, run("@g -g my_group -p 9090")), 51 | ?assertMatch(1, run("@g -g my_group -p foo")). 52 | 53 | run(CMD) -> 54 | RootDir = string:trim(os:cmd("git rev-parse --show-toplevel")), 55 | Path = filename:join(RootDir, "_build/default/bin/emqttb"), 56 | Port = open_port({spawn, Path ++ " " ++ CMD}, [nouse_stdio, exit_status]), 57 | receive 58 | {Port, {exit_status, E}} -> E 59 | end. 60 | --------------------------------------------------------------------------------