├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── erlang环境配置.md
├── etc
└── emqx_backend_mysql.conf
├── include
└── emqx_backend_mysql.hrl
├── priv
└── emqx_backend_mysql.schema
├── rebar.config
├── rebar.config.script
├── rebar3.crashdump
├── sql
├── mqtt_acked.sql
├── mqtt_client.sql
├── mqtt_msg.sql
├── mqtt_retain.sql
└── mqtt_sub.sql
├── src
├── emqx_backend_mysql.app
├── emqx_backend_mysql.app.src
├── emqx_backend_mysql.app.src.script
├── emqx_backend_mysql.erl
├── emqx_backend_mysql_actions.erl
├── emqx_backend_mysql_app.erl
├── emqx_backend_mysql_batcher.erl
├── emqx_backend_mysql_cli.erl
└── emqx_backend_mysql_sup.erl
└── test
└── emqx_backend_mysql_SUITE.erl
/.gitignore:
--------------------------------------------------------------------------------
1 | # eclipse ignore
2 | .settings/
3 | .project
4 | .classpath
5 |
6 | # idea ignore
7 | .idea/
8 | *.ipr
9 | *.iml
10 | *.iws
11 |
12 | # system ignore
13 | .DS_Store
14 | Thumbs.db
15 |
--------------------------------------------------------------------------------
/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 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | ## shallow clone for speed
2 |
3 | REBAR_GIT_CLONE_OPTIONS += --depth 1
4 | export REBAR_GIT_CLONE_OPTIONS
5 |
6 | REBAR = rebar3
7 | all: compile
8 |
9 | compile:
10 | $(REBAR) compile
11 |
12 | ct: compile
13 | $(REBAR) as test ct -v
14 |
15 | eunit: compile
16 | $(REBAR) as test eunit
17 |
18 | xref:
19 | $(REBAR) xref
20 |
21 | cover:
22 | $(REBAR) cover
23 |
24 | clean: distclean
25 |
26 | distclean:
27 | @rm -rf _build
28 | @rm -f data/app.*.config data/vm.*.args rebar.lock
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # buildrun-emqx-backend-mysql
2 | EMQX client connection and message save to MySQL
3 |
4 | emqx 客户端连接状态和消息持久化到MySQL 插件
5 |
6 | ### erlang 环境配置
7 |
8 | 若没配置erlang环境,请参考[erlang 环境配置](./erlang环境配置.md).
9 |
10 | ### 编译发布插件
11 |
12 | 1、clone emqx-rel 项目, 切换到改 tag v4.1.2
13 |
14 | ``` bash
15 | git clone https://github.com/emqx/emqx-rel.git
16 | cd emqx-rel
17 | git checkout v4.1.2
18 | make
19 | ```
20 |
21 | 正常构建成功后,运行 `_build/emqx/rel/emqx/bin/emqx console` 启动 emqx
22 |
23 | ### 添加自定义插件
24 |
25 | 2.rebar.config 添加依赖
26 |
27 | ```erl
28 | {deps,
29 | [ {buildrun_emqx_backend_mysql, {git, "https://github.com/goBuildRun/buildrun-emqx-backend-mysql.git", {branch, "master"}}}
30 | , ....
31 | ....
32 | ]
33 | }
34 |
35 | ```
36 |
37 | 3.rebar.config 中 relx 段落添加
38 |
39 | ```erl
40 | {relx,
41 | [...
42 | , ...
43 | , {release, {emqx, git_describe},
44 | [
45 | {buildrun_emqx_backend_mysql, load},
46 | ]
47 | }
48 | ]
49 | }
50 | ```
51 | 4.编译
52 |
53 | > make
54 |
55 | 打开 `localhost:18083` 的插件栏可以看到 `buildrun_emqx_backend_mysql` 插件。
56 |
57 | ### config配置
58 |
59 | File: etc/buildrun_emqx_backend_mysql.conf
60 |
61 | ```
62 | # mysql 服务器
63 | mysql.server = 127.0.0.1:3306
64 |
65 | # 连接池数量
66 | mysql.pool_size = 8
67 |
68 | # mysql 用户名
69 | mysql.username = buildrun
70 |
71 | # mysql密码
72 | mysql.password = buildrun
73 |
74 | # 数据库名
75 | mysql.database = mqtt
76 |
77 | # 超时时间(秒)
78 | mysql.query_timeout = 10s
79 |
80 | ```
81 |
82 | ### mqtt_client.sql, mqtt_msg.sql
83 |
84 | 到你的数据库中执行[mqtt_client.sql](./sql/mqtt_client.sql), [mqtt_msg.sql](./sql/mqtt_msg.sql)。
85 |
86 | ### 加载插件
87 |
88 | > ./bin/emqx_ctl plugins load buildrun_emqx_backend_mysql
89 |
90 | 或者编辑
91 |
92 | data/loaded_plugins
93 |
94 | > 添加 {buildrun_emqx_backend_mysql, true}.
95 |
96 | 设置插件自动启动
97 |
98 | 注意:这种方式适用emqx未启动之前
99 |
100 | 或者
101 |
102 | 可以在emqx启动后,在 dashboard 插件栏点击启用 `buildrun_emqx_backend_mysql` 即可启用成功
103 |
104 | ### 使用
105 |
106 | 此插件会把public发布的消息保存到mysql中,但并不是全部。也可以在业务规则中添加存储类型的资源。
107 |
108 | 需要在发布消息的参数中 retain 值设置为 true。 这样这条消息才会被保存在mysql中
109 |
110 | eg:
111 |
112 | ```json
113 | {
114 | "topic": "buildruntopic",
115 | "payload": "hello,Buidrun",
116 | "qos": 1,
117 | "retain": true,
118 | "client_id": "mqttjs_8b3b4182ae"
119 | }
120 | ```
121 |
122 | ### 构建镜像
123 |
124 | `emqx-rel` 项目自带构建镜像的功能
125 |
126 | ``` bash
127 | make emqx-docker-build
128 | ```
129 |
130 | ### 最后
131 |
132 | 有什么问题和功能需求都可以给我提issue,欢迎关注。
133 |
134 | ### License
135 |
136 | Apache License Version 2.0
137 |
--------------------------------------------------------------------------------
/erlang环境配置.md:
--------------------------------------------------------------------------------
1 | # erlang环境配置
2 |
3 | erlang 推荐使用 22 版本,经过测试用 23 版本编译出来运行有问题。各平台安装 erlang 详情请参考官方文档。 linux 系统可以使用 kerl 脚本安装,构建最新版本的 emqx 时注意编译的 gcc 版本不能用 10,编译通不过,推荐用 gcc-9
4 |
5 | 下载 kerl
6 |
7 | ``` bash
8 | curl -O https://raw.githubusercontent.com/kerl/kerl/master/kerl
9 | chmod +x kerl
10 | ```
11 |
12 | 安装 erlang
13 |
14 | ``` bash
15 | kerl update releases
16 | # 如果直接构建报错,修改 ~/.kerl/builds/22.3/otp_src_22.3/Makefile
17 | # 将 gcc 改为 gcc9
18 | kerl build 22.3 22.3
19 | kerl install 22.3 /opt/erlang/22.3
20 | # 启用环境
21 | source /opt/erlang/22.3/activate
22 | # 设置环境变量,这样不用每次手动启用
23 | echo 'source /opt/erlang/22.3/activate' >> ~/.bashrc
24 | ```
25 |
26 | 安装 rebar3
27 |
28 | ``` bash
29 | curl -OL https://s3.amazonaws.com/rebar3/rebar3
30 | ./rebar3 local install
31 | # 然后根据输出操作
32 | ```
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/etc/emqx_backend_mysql.conf:
--------------------------------------------------------------------------------
1 | ##====================================================================
2 | ## Configuration for EMQ X MySQL Backend
3 | ##====================================================================
4 |
5 | ## MySQL Server
6 | backend.mysql.pool1.server = 127.0.0.1:3306
7 |
8 | ## MySQL Pool Size
9 | backend.mysql.pool1.pool_size = 8
10 | ## MySQL Username
11 | backend.mysql.pool1.user = root
12 | ## MySQL Password
13 | backend.mysql.pool1.password = public
14 | ## MySQL Database
15 | backend.mysql.pool1.database = mqtt
16 |
17 | ## Client Connected Record
18 | backend.mysql.hook.client.connected.1 = {"action": {"function": "on_client_connected"}, "pool": "pool1"}
19 |
20 | ## Session Created Record
21 | backend.mysql.hook.client.connected.2 = {"action": {"function": "on_subscribe_lookup"}, "pool": "pool1"}
22 |
23 | ## Client DisConnected Record
24 | backend.mysql.hook.client.disconnected.1 = {"action": {"function": "on_client_disconnected"}, "pool": "pool1"}
25 |
26 | ## Lookup Unread Message QOS > 0
27 | backend.mysql.hook.session.subscribed.1 = {"topic": "#", "action": {"function": "on_message_fetch"}, "offline_opts": {"max_returned_count": 500, "time_range": "2h"}, "pool": "pool1"}
28 |
29 | ## Lookup Retain Message
30 | backend.mysql.hook.session.subscribed.2 = {"topic": "#", "action": {"function": "on_retain_lookup"}, "pool": "pool1"}
31 |
32 | ## Delete Acked Record
33 | backend.mysql.hook.session.unsubscribed.1= {"topic": "#", "action": {"sql": ["delete from mqtt_acked where clientid = ${clientid} and topic = ${topic}"]}, "pool": "pool1"}
34 |
35 | ## Store Publish Message QOS > 0
36 | backend.mysql.hook.message.publish.1 = {"topic": "#", "action": {"function": "on_message_publish"}, "pool": "pool1"}
37 |
38 | ## Store Retain Message
39 | backend.mysql.hook.message.publish.2 = {"topic": "#", "action": {"function": "on_message_retain"}, "pool": "pool1"}
40 |
41 | ## Delete Retain Message
42 | backend.mysql.hook.message.publish.3 = {"topic": "#", "action": {"function": "on_retain_delete"}, "pool": "pool1"}
43 |
44 | ## Store Ack
45 | backend.mysql.hook.message.acked.1 = {"topic": "#", "action": {"function": "on_message_acked"}, "pool": "pool1"}
46 |
47 | ## Max number of fetch offline messages
48 | ## max_returned_count = 500
49 |
50 | ## Time Range
51 | ## d - day
52 | ## h - hour
53 | ## m - minute
54 | ## s - second
55 | ## time_range = 2h
56 | ## backend.mysql.hook.session.subscribed.1 = {"topic": "#", "action": {"function": "on_message_fetch"}, "offline_opts": {"max_returned_count": 500, "time_range": "2h"}, "pool": "pool1"}
--------------------------------------------------------------------------------
/include/emqx_backend_mysql.hrl:
--------------------------------------------------------------------------------
1 | -define(APP, emqx_backend_mysql).
2 |
--------------------------------------------------------------------------------
/priv/emqx_backend_mysql.schema:
--------------------------------------------------------------------------------
1 | %%-*- mode: erlang -*-
2 | %% emqx_backend_mysql config mapping
3 |
4 | {mapping, "backend.mysql.$name.server", "emqx_backend_mysql.pools", [
5 | {default, {"127.0.0.1", 3306}},
6 | {datatype, [integer, ip, string]}
7 | ]}.
8 |
9 | {mapping, "backend.mysql.$name.pool_size", "emqx_backend_mysql.pools", [
10 | {default, 8},
11 | {datatype, integer}
12 | ]}.
13 |
14 | {mapping, "backend.mysql.$name.user", "emqx_backend_mysql.pools", [
15 | {default, ""},
16 | {datatype, string},
17 | hidden
18 | ]}.
19 |
20 | {mapping, "backend.mysql.$name.password", "emqx_backend_mysql.pools", [
21 | {default, ""},
22 | {datatype, string}
23 | ]}.
24 |
25 | {mapping, "backend.mysql.$name.database", "emqx_backend_mysql.pools", [
26 | {default, ""},
27 | {datatype, string}
28 | ]}.
29 |
30 | {translation, "emqx_backend_mysql.pools", fun(Conf) ->
31 | Vars = cuttlefish_variable:fuzzy_matches(["backend", "mysql", "$name", "server"], Conf),
32 | Key = fun(Name, Attr) -> string:join(["backend.mysql", Name, Attr], ".") end,
33 | lists:map(fun({_, Name}) ->
34 | {MyHost, MyPort} =
35 | case cuttlefish:conf_get(Key(Name, "server"), Conf) of
36 | {Ip, Port} -> {Ip, Port};
37 | S -> case string:tokens(S, ":") of
38 | [Domain] -> {Domain, 3306};
39 | [Domain, Port] -> {Domain, list_to_integer(Port)}
40 | end
41 | end,
42 | {list_to_atom(Name), [{host, MyHost}, {port, MyPort},
43 | {pool_size, cuttlefish:conf_get(Key(Name, "pool_size"), Conf)},
44 | {user, cuttlefish:conf_get(Key(Name, "user"), Conf)},
45 | {password, cuttlefish:conf_get(Key(Name, "password"), Conf)},
46 | {database, cuttlefish:conf_get(Key(Name, "database"), Conf)},
47 | {auto_reconnect, 1},
48 | {encoding, utf8},
49 | {keep_alive, true}]}
50 | end, Vars)
51 | end}.
52 |
53 | {mapping, "backend.mysql.hook.client.connected.$name", "emqx_backend_mysql.hooks", [
54 | {datatype, string}
55 | ]}.
56 |
57 | {mapping, "backend.mysql.hook.client.disconnected.$name", "emqx_backend_mysql.hooks", [
58 | {datatype, string}
59 | ]}.
60 |
61 | {mapping, "backend.mysql.hook.session.subscribed.$name", "emqx_backend_mysql.hooks", [
62 | {datatype, string}
63 | ]}.
64 |
65 | {mapping, "backend.mysql.hook.session.unsubscribed.$name", "emqx_backend_mysql.hooks", [
66 | {datatype, string}
67 | ]}.
68 |
69 | {mapping, "backend.mysql.hook.message.publish.$name", "emqx_backend_mysql.hooks", [
70 | {datatype, string}
71 | ]}.
72 |
73 | {mapping, "backend.mysql.hook.message.acked.$name", "emqx_backend_mysql.hooks", [
74 | {datatype, string}
75 | ]}.
76 |
77 | {mapping, "backend.mysql.hook.message.delivered.$name", "emqx_backend_mysql.hooks", [
78 | {datatype, string}
79 | ]}.
80 |
81 |
82 | {translation, "emqx_backend_mysql.hooks", fun(Conf) ->
83 | Hooks = cuttlefish_variable:filter_by_prefix("backend.mysql.hook", Conf),
84 | lists:map(
85 | fun({[_, _, _,Name1,Name2, _], Val}) ->
86 | {lists:concat([Name1,".",Name2]), list_to_binary(Val)}
87 | end, Hooks)
88 | end}.
89 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | {deps, []}.
2 |
3 | {erl_opts, [debug_info]}.
4 |
--------------------------------------------------------------------------------
/rebar.config.script:
--------------------------------------------------------------------------------
1 | %%-*- mode: erlang -*-
2 |
3 | DEPS = case lists:keyfind(deps, 1, CONFIG) of
4 | {_, Deps} -> Deps;
5 | _ -> []
6 | end,
7 |
8 | ComparingFun = fun
9 | _Fun([C1|R1], [C2|R2]) when is_list(C1), is_list(C2);
10 | is_integer(C1), is_integer(C2) -> C1 < C2 orelse _Fun(R1, R2);
11 | _Fun([C1|R1], [C2|R2]) when is_integer(C1), is_list(C2) -> _Fun(R1, R2);
12 | _Fun([C1|R1], [C2|R2]) when is_list(C1), is_integer(C2) -> true;
13 | _Fun(_, _) -> false
14 | end,
15 |
16 | SortFun = fun(T1, T2) ->
17 | C = fun(T) ->
18 | [case catch list_to_integer(E) of
19 | I when is_integer(I) -> I;
20 | _ -> E
21 | end || E <- re:split(string:sub_string(T, 2), "[.-]", [{return, list}])]
22 | end,
23 | ComparingFun(C(T1), C(T2))
24 | end,
25 |
26 | VTags = string:tokens(os:cmd("git tag -l \"v*\" --points-at $(git rev-parse $(git describe --abbrev=0 --tags))"), "\n"),
27 |
28 | Tags = case VTags of
29 | [] -> string:tokens(os:cmd("git tag -l \"e*\" --points-at $(git rev-parse $(git describe --abbrev=0 --tags))"), "\n");
30 | _ -> VTags
31 | end,
32 |
33 | LatestTag = lists:last(lists:sort(SortFun, Tags)),
34 |
35 | Branch = case os:getenv("GITHUB_RUN_ID") of
36 | false -> os:cmd("git branch | grep -e '^*' | cut -d' ' -f 2") -- "\n";
37 | _ -> re:replace(os:getenv("GITHUB_REF"), "^refs/heads/|^refs/tags/", "", [global, {return ,list}])
38 | end,
39 |
40 | GitDescribe = case re:run(Branch, "master|^dev/|^hotfix/", [{capture, none}]) of
41 | match -> {branch, Branch};
42 | _ -> {tag, LatestTag}
43 | end,
44 |
45 | UrlPrefix = "https://github.com/emqx/",
46 |
47 | EMQX_DEP = {emqx, {git, UrlPrefix ++ "emqx", GitDescribe}},
48 | EMQX_MGMT_DEP = {emqx_management, {git, UrlPrefix ++ "emqx-management", GitDescribe}},
49 |
50 | NewDeps = [EMQX_DEP, EMQX_MGMT_DEP | DEPS],
51 |
52 | CONFIG1 = lists:keystore(deps, 1, CONFIG, {deps, NewDeps}),
53 |
54 | CONFIG1.
55 |
--------------------------------------------------------------------------------
/rebar3.crashdump:
--------------------------------------------------------------------------------
1 | Error: {badmatch,{error,{1,file,undefined_script}}}
2 | [{rebar_config,consult_file_,1,
3 | [{file,"/private/tmp/rebar3-20191223-90221-8wanow/rebar3-3.13.0/src/rebar_config.erl"},
4 | {line,230}]},
5 | {rebar_app_discover,create_app_info,3,
6 | [{file,"/private/tmp/rebar3-20191223-90221-8wanow/rebar3-3.13.0/src/rebar_app_discover.erl"},
7 | {line,356}]},
8 | {rebar_app_discover,try_handle_app_src_file,5,
9 | [{file,"/private/tmp/rebar3-20191223-90221-8wanow/rebar3-3.13.0/src/rebar_app_discover.erl"},
10 | {line,457}]},
11 | {rebar_utils,filtermap,2,
12 | [{file,"/private/tmp/rebar3-20191223-90221-8wanow/rebar3-3.13.0/src/rebar_utils.erl"},
13 | {line,113}]},
14 | {rebar_app_discover,do,2,
15 | [{file,"/private/tmp/rebar3-20191223-90221-8wanow/rebar3-3.13.0/src/rebar_app_discover.erl"},
16 | {line,26}]},
17 | {rebar_prv_app_discovery,do,1,
18 | [{file,"/private/tmp/rebar3-20191223-90221-8wanow/rebar3-3.13.0/src/rebar_prv_app_discovery.erl"},
19 | {line,38}]},
20 | {rebar_core,do,2,
21 | [{file,"/private/tmp/rebar3-20191223-90221-8wanow/rebar3-3.13.0/src/rebar_core.erl"},
22 | {line,154}]},
23 | {rebar3,run_aux,2,
24 | [{file,"/private/tmp/rebar3-20191223-90221-8wanow/rebar3-3.13.0/src/rebar3.erl"},
25 | {line,182}]}]
26 |
27 |
--------------------------------------------------------------------------------
/sql/mqtt_acked.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS `mqtt_acked`;
2 | CREATE TABLE `mqtt_acked` (
3 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
4 | `clientid` varchar(200) DEFAULT NULL,
5 | `topic` varchar(200) DEFAULT NULL,
6 | `mid` int(200) DEFAULT NULL,
7 | `created` timestamp NULL DEFAULT NULL,
8 | PRIMARY KEY (`id`),
9 | UNIQUE KEY `mqtt_acked_key` (`clientid`,`topic`)
10 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
11 |
12 |
--------------------------------------------------------------------------------
/sql/mqtt_client.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS `mqtt_client`;
2 | CREATE TABLE `mqtt_client` (
3 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
4 | `clientid` varchar(64) DEFAULT NULL,
5 | `state` varchar(3) DEFAULT NULL,
6 | `node` varchar(100) DEFAULT NULL,
7 | `online_at` datetime DEFAULT NULL,
8 | `offline_at` datetime DEFAULT NULL,
9 | `created` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
10 | PRIMARY KEY (`id`),
11 | KEY `mqtt_client_idx` (`clientid`),
12 | UNIQUE KEY `mqtt_client_key` (`clientid`)
13 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
14 |
15 |
--------------------------------------------------------------------------------
/sql/mqtt_msg.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS `mqtt_msg`;
2 | CREATE TABLE `mqtt_msg` (
3 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
4 | `msgid` varchar(100) DEFAULT NULL,
5 | `topic` varchar(1024) DEFAULT NULL,
6 | `sender` varchar(1024) DEFAULT NULL,
7 | `node` varchar(60) DEFAULT NULL,
8 | `qos` int(11) DEFAULT '0',
9 | `retain` tinyint(2) DEFAULT NULL,
10 | `payload` blob,
11 | `arrived` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
12 | PRIMARY KEY (`id`)
13 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
14 |
15 |
--------------------------------------------------------------------------------
/sql/mqtt_retain.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS `mqtt_retain`;
2 | CREATE TABLE `mqtt_retain` (
3 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
4 | `topic` varchar(200) DEFAULT NULL,
5 | `msgid` varchar(60) DEFAULT NULL,
6 | `sender` varchar(100) DEFAULT NULL,
7 | `node` varchar(100) DEFAULT NULL,
8 | `qos` int(2) DEFAULT NULL,
9 | `payload` blob,
10 | `arrived` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
11 | PRIMARY KEY (`id`),
12 | UNIQUE KEY `mqtt_retain_key` (`topic`)
13 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
14 |
15 |
--------------------------------------------------------------------------------
/sql/mqtt_sub.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS `mqtt_sub`;
2 | CREATE TABLE `mqtt_sub` (
3 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
4 | `clientid` varchar(64) DEFAULT NULL,
5 | `topic` varchar(255) DEFAULT NULL,
6 | `qos` int(3) DEFAULT NULL,
7 | `created` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
8 | PRIMARY KEY (`id`),
9 | KEY `mqtt_sub_idx` (`clientid`,`topic`(255),`qos`),
10 | UNIQUE KEY `mqtt_sub_key` (`clientid`,`topic`)
11 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
12 |
13 |
--------------------------------------------------------------------------------
/src/emqx_backend_mysql.app:
--------------------------------------------------------------------------------
1 | {application,emqx_backend_mysql,
2 | [{description,"EMQ X MySQL Backend"},
3 | {vsn,"4.1.1"},
4 | {modules,[emqx_backend_mysql,emqx_backend_mysql_actions,
5 | emqx_backend_mysql_app,emqx_backend_mysql_batcher,
6 | emqx_backend_mysql_cli,emqx_backend_mysql_sup]},
7 | {registered,[emqx_backend_mysql_sup]},
8 | {applications,[kernel,stdlib,mysql,ecpool]},
9 | {mod,{emqx_backend_mysql_app,[]}}]}.
10 |
--------------------------------------------------------------------------------
/src/emqx_backend_mysql.app.src:
--------------------------------------------------------------------------------
1 | {application, emqx_backend_mysql,
2 | [{description, "EMQ X Backend Mysql"},
3 | {vsn, "git"},
4 | {modules, []},
5 | {registered, [emqx_backend_mysql_sup]},
6 | {applications, [kernel,stdlib]},
7 | {mod, {emqx_backend_mysql_app,[]}},
8 | {env, []},
9 | {licenses, ["Apache-2.0"]},
10 | {maintainers, ["EMQ X Team "]},
11 | {links, [{"Homepage", "https://emqx.io/"},
12 | {"Github", "https://github.com/emqx/emqx-plugin-template"}
13 | ]}
14 | ]}.
15 |
--------------------------------------------------------------------------------
/src/emqx_backend_mysql.app.src.script:
--------------------------------------------------------------------------------
1 | %%-*- mode: erlang -*-
2 | %% .app.src.script
3 |
4 | RemoveLeadingV =
5 | fun(Tag) ->
6 | case re:run(Tag, "^[v|e]?[0-9]\.[0-9]\.([0-9]|(rc|beta|alpha)\.[0-9])", [{capture, none}]) of
7 | nomatch ->
8 | re:replace(Tag, "/", "-", [{return ,list}]);
9 | _ ->
10 | %% if it is a version number prefixed by 'v' or 'e', then remove it
11 | re:replace(Tag, "[v|e]", "", [{return ,list}])
12 | end
13 | end,
14 |
15 | case os:getenv("EMQX_DEPS_DEFAULT_VSN") of
16 | false -> CONFIG; % env var not defined
17 | [] -> CONFIG; % env var set to empty string
18 | Tag ->
19 | [begin
20 | AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}),
21 | {application, App, AppConf0}
22 | end || Conf = {application, App, AppConf} <- CONFIG]
23 | end.
24 |
25 |
--------------------------------------------------------------------------------
/src/emqx_backend_mysql.erl:
--------------------------------------------------------------------------------
1 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
2 | "mqx_backend_mysql.erl",
3 | 1).
4 |
5 | -module(emqx_backend_mysql).
6 |
7 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/inclu"
8 | "de/emqx_backend_mysql.hrl",
9 | 1).
10 |
11 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
12 | "mqx_backend_mysql.erl",
13 | 10).
14 |
15 | -file("/emqx_rel/_checkouts/emqx/include/emqx.hrl", 1).
16 |
17 | -record(subscription, {topic, subid, subopts}).
18 |
19 | -record(message,
20 | {id :: binary(), qos = 0, from :: atom() | binary(),
21 | flags :: #{atom() => boolean()}, headers :: map(),
22 | topic :: binary(), payload :: binary(),
23 | timestamp :: integer()}).
24 |
25 | -record(delivery,
26 | {sender :: pid(), message :: #message{}}).
27 |
28 | -record(route,
29 | {topic :: binary(),
30 | dest :: node() | {binary(), node()}}).
31 |
32 | -type trie_node_id() :: binary() | atom().
33 |
34 | -record(trie_node,
35 | {node_id :: trie_node_id(),
36 | edge_count = 0 :: non_neg_integer(),
37 | topic :: binary() | undefined, flags :: [atom()]}).
38 |
39 | -record(trie_edge,
40 | {node_id :: trie_node_id(),
41 | word :: binary() | atom()}).
42 |
43 | -record(trie,
44 | {edge :: #trie_edge{}, node_id :: trie_node_id()}).
45 |
46 | -record(alarm,
47 | {id :: binary(),
48 | severity :: notice | warning | error | critical,
49 | title :: iolist(), summary :: iolist(),
50 | timestamp :: erlang:timestamp()}).
51 |
52 | -record(plugin,
53 | {name :: atom(), dir :: string(), descr :: string(),
54 | vendor :: string(), active = false :: boolean(),
55 | info :: map(), type :: atom()}).
56 |
57 | -record(command,
58 | {name :: atom(), action :: atom(),
59 | args = [] :: list(), opts = [] :: list(),
60 | usage :: string(), descr :: string()}).
61 |
62 | -record(banned,
63 | {who ::
64 | {clientid, binary()} | {username, binary()} |
65 | {ip_address, inet:ip_address()},
66 | by :: binary(), reason :: binary(), at :: integer(),
67 | until :: integer()}).
68 |
69 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
70 | "mqx_backend_mysql.erl",
71 | 12).
72 |
73 | -export([pool_name/1]).
74 |
75 | -export([register_metrics/0, load/0, unload/0]).
76 |
77 | -export([on_client_connected/3, on_subscribe_lookup/3,
78 | on_client_disconnected/4, on_message_fetch/4,
79 | on_retain_lookup/4, on_message_publish/2,
80 | on_message_store/2, on_message_retain/2,
81 | on_retain_delete/2, on_message_delivered/3,
82 | on_message_acked/3, run_mysql_sql/2, run_mysql_sql/3,
83 | run_mysql_sql/4]).
84 |
85 | pool_name(Pool) ->
86 | list_to_atom(lists:concat([emqx_backend_mysql, '_',
87 | Pool])).
88 |
89 | register_metrics() ->
90 | [emqx_metrics:new(MetricName)
91 | || MetricName
92 | <- ['backend.mysql.client_connected',
93 | 'backend.mysql.subscribe_lookup',
94 | 'backend.mysql.client_disconnected',
95 | 'backend.mysql.on_message_fetch',
96 | 'backend.mysql.on_retain_lookup',
97 | 'backend.mysql.on_message_publish',
98 | 'backend.mysql.on_message_store',
99 | 'backend.mysql.on_message_retain',
100 | 'backend.mysql.on_retain_delete',
101 | 'backend.mysql.on_message_acked',
102 | 'backend.mysql.run_mysql_sql.publish',
103 | 'backend.mysql.run_mysql_sql.acked_delivered',
104 | 'backend.mysql.run_mysql_sql.sub_unsub',
105 | 'backend.mysql.run_mysql_sql.connected',
106 | 'backend.mysql.run_mysql_sql.disconnected']].
107 |
108 | load() ->
109 | HookList =
110 | parse_hook(application:get_env(emqx_backend_mysql,
111 | hooks, [])),
112 | lists:foreach(fun ({Hook, Action, Pool, Filter,
113 | OfflineOpts}) ->
114 | case proplists:get_value(<<"function">>, Action) of
115 | undefined ->
116 | SqlList = [compile_sql(SQL)
117 | || SQL
118 | <- proplists:get_value(<<"sql">>,
119 | Action,
120 | [])],
121 | load_(Hook, run_mysql_sql, OfflineOpts,
122 | {Filter, Pool, SqlList});
123 | Fun ->
124 | load_(Hook, b2a(Fun), OfflineOpts,
125 | {Filter, Pool})
126 | end
127 | end,
128 | HookList),
129 | io:format("~s is loaded.~n", [emqx_backend_mysql]),
130 | ok.
131 |
132 | load_(Hook, Fun, OfflineOpts, Params) ->
133 | case Hook of
134 | 'client.connected' ->
135 | emqx:hook(Hook, fun emqx_backend_mysql:Fun/3, [Params]);
136 | 'client.disconnected' ->
137 | emqx:hook(Hook, fun emqx_backend_mysql:Fun/4, [Params]);
138 | 'session.subscribed' ->
139 | emqx:hook(Hook, fun emqx_backend_mysql:Fun/4,
140 | [erlang:append_element(Params, OfflineOpts)]);
141 | 'session.unsubscribed' ->
142 | emqx:hook(Hook, fun emqx_backend_mysql:Fun/4, [Params]);
143 | 'message.publish' ->
144 | emqx:hook(Hook, fun emqx_backend_mysql:Fun/2, [Params]);
145 | 'message.acked' ->
146 | emqx:hook(Hook, fun emqx_backend_mysql:Fun/3, [Params]);
147 | 'message.delivered' ->
148 | emqx:hook(Hook, fun emqx_backend_mysql:Fun/3, [Params])
149 | end.
150 |
151 | unload() ->
152 | HookList =
153 | parse_hook(application:get_env(emqx_backend_mysql,
154 | hooks, [])),
155 | lists:foreach(fun ({Hook, Action, _Pool, _Filter,
156 | _OfflineOpts}) ->
157 | case proplists:get_value(<<"function">>, Action) of
158 | undefined -> unload_(Hook, run_mysql_sql);
159 | Fun -> unload_(Hook, b2a(Fun))
160 | end
161 | end,
162 | HookList),
163 | io:format("~s is unloaded.~n", [emqx_backend_mysql]),
164 | ok.
165 |
166 | unload_(Hook, Fun) ->
167 | case Hook of
168 | 'client.connected' ->
169 | emqx:unhook(Hook, fun emqx_backend_mysql:Fun/3);
170 | 'client.disconnected' ->
171 | emqx:unhook(Hook, fun emqx_backend_mysql:Fun/4);
172 | 'session.subscribed' ->
173 | emqx:unhook(Hook, fun emqx_backend_mysql:Fun/4);
174 | 'session.unsubscribed' ->
175 | emqx:unhook(Hook, fun emqx_backend_mysql:Fun/4);
176 | 'message.publish' ->
177 | emqx:unhook(Hook, fun emqx_backend_mysql:Fun/2);
178 | 'message.acked' ->
179 | emqx:unhook(Hook, fun emqx_backend_mysql:Fun/3);
180 | 'message.delivered' ->
181 | emqx:unhook(Hook, fun emqx_backend_mysql:Fun/3)
182 | end.
183 |
184 | on_client_connected(#{clientid := ClientId}, _ConnInfo,
185 | {Filter, Pool}) ->
186 | with_filter(fun () ->
187 | emqx_metrics:inc('backend.mysql.client_connected'),
188 | emqx_backend_mysql_cli:client_connected(Pool,
189 | [{clientid,
190 | ClientId}])
191 | end,
192 | undefined, Filter).
193 |
194 | on_subscribe_lookup(#{clientid := ClientId}, _ConnInfo,
195 | {Filter, Pool}) ->
196 | with_filter(fun () ->
197 | emqx_metrics:inc('backend.mysql.subscribe_lookup'),
198 | case emqx_backend_mysql_cli:subscribe_lookup(Pool,
199 | [{clientid,
200 | ClientId}])
201 | of
202 | [] -> ok;
203 | TopicTable -> self() ! {subscribe, TopicTable}, ok
204 | end
205 | end,
206 | undefined, Filter).
207 |
208 | on_client_disconnected(#{clientid := ClientId}, _Reason,
209 | _ConnInfo, {Filter, Pool}) ->
210 | with_filter(fun () ->
211 | emqx_metrics:inc('backend.mysql.client_disconnected'),
212 | emqx_backend_mysql_cli:client_disconnected(Pool,
213 | [{clientid,
214 | ClientId}])
215 | end,
216 | undefined, Filter).
217 |
218 | on_message_fetch(#{clientid := ClientId}, Topic, Opts,
219 | {Filter, Pool, OfflineOpts}) ->
220 | with_filter(fun () ->
221 | emqx_metrics:inc('backend.mysql.on_message_fetch'),
222 | case maps:get(qos, Opts, 0) > 0 andalso
223 | maps:get(first, Opts, true)
224 | of
225 | true ->
226 | MsgList =
227 | emqx_backend_mysql_cli:message_fetch(Pool,
228 | [{clientid,
229 | ClientId},
230 | {topic,
231 | Topic}],
232 | OfflineOpts),
233 | [self() ! {deliver, Topic, Msg}
234 | || Msg <- MsgList];
235 | false -> ok
236 | end
237 | end,
238 | Topic, Filter).
239 |
240 | on_retain_lookup(_Client, Topic, _Opts,
241 | {Filter, Pool, _OfflineOpts}) ->
242 | with_filter(fun () ->
243 | emqx_metrics:inc('backend.mysql.on_retain_lookup'),
244 | MsgList = emqx_backend_mysql_cli:lookup_retain(Pool,
245 | [{topic,
246 | Topic}]),
247 | [self() ! {deliver, Topic, set_retain(true, Msg)}
248 | || Msg <- MsgList]
249 | end,
250 | Topic, Filter).
251 |
252 | set_retain(Value, Msg) ->
253 | Msg1 = emqx_message:set_flags(#{retained => Value},
254 | Msg),
255 | emqx_message:set_header(retained, Value, Msg1).
256 |
257 | on_message_publish(Msg = #message{flags =
258 | #{retain := true},
259 | payload = <<>>},
260 | _Rule) ->
261 | {ok, Msg};
262 | on_message_publish(Msg = #message{qos = Qos},
263 | {_Filter, _Pool})
264 | when Qos =:= 0 ->
265 | {ok, Msg};
266 | on_message_publish(Msg0 = #message{topic = Topic},
267 | {Filter, Pool}) ->
268 | with_filter(fun () ->
269 | emqx_metrics:inc('backend.mysql.on_message_publish'),
270 | Msg = emqx_backend_mysql_cli:message_publish(Pool,
271 | Msg0),
272 | {ok, Msg}
273 | end,
274 | Msg0, Topic, Filter).
275 |
276 | on_message_store(Msg = #message{flags =
277 | #{retain := true},
278 | payload = <<>>},
279 | _Rule) ->
280 | {ok, Msg};
281 | on_message_store(Msg0 = #message{topic = Topic},
282 | {Filter, Pool}) ->
283 | with_filter(fun () ->
284 | emqx_metrics:inc('backend.mysql.on_message_store'),
285 | Msg = emqx_backend_mysql_cli:message_store(Pool, Msg0),
286 | {ok, Msg}
287 | end,
288 | Msg0, Topic, Filter).
289 |
290 | on_message_retain(Msg = #message{flags =
291 | #{retain := false}},
292 | _Rule) ->
293 | {ok, Msg};
294 | on_message_retain(Msg = #message{flags =
295 | #{retain := true},
296 | payload = <<>>},
297 | _Rule) ->
298 | {ok, Msg};
299 | on_message_retain(Msg0 = #message{flags =
300 | #{retain := true},
301 | topic = Topic, headers = Headers0},
302 | {Filter, Pool}) ->
303 | Headers = case erlang:is_map(Headers0) of
304 | true -> Headers0;
305 | false -> #{}
306 | end,
307 | case maps:find(retained, Headers) of
308 | {ok, true} -> {ok, Msg0};
309 | _ ->
310 | with_filter(fun () ->
311 | emqx_metrics:inc('backend.mysql.on_message_retain'),
312 | Msg = emqx_backend_mysql_cli:message_retain(Pool,
313 | Msg0),
314 | {ok, Msg}
315 | end,
316 | Msg0, Topic, Filter)
317 | end;
318 | on_message_retain(Msg, _Rule) -> {ok, Msg}.
319 |
320 | on_retain_delete(Msg0 = #message{flags =
321 | #{retain := true},
322 | topic = Topic, payload = <<>>},
323 | {Filter, Pool}) ->
324 | with_filter(fun () ->
325 | emqx_metrics:inc('backend.mysql.on_retain_delete'),
326 | Msg = emqx_backend_mysql_cli:delete_retain(Pool, Msg0),
327 | {ok, Msg}
328 | end,
329 | Msg0, Topic, Filter);
330 | on_retain_delete(Msg, _Rule) -> {ok, Msg}.
331 |
332 | on_message_delivered(_Client, _Msg, _Rule) -> ok.
333 |
334 | on_message_acked(#{clientid := ClientId},
335 | #message{topic = Topic, headers = Headers},
336 | {Filter, Pool}) ->
337 | case maps:get(mysql_id, Headers, undefined) of
338 | undefined -> ok;
339 | Id ->
340 | with_filter(fun () ->
341 | emqx_metrics:inc('backend.mysql.on_message_acked'),
342 | emqx_backend_mysql_cli:message_acked(Pool,
343 | [{clientid,
344 | ClientId},
345 | {topic,
346 | Topic},
347 | {mysql_id,
348 | Id}])
349 | end,
350 | Topic, Filter)
351 | end.
352 |
353 | run_mysql_sql(Msg0 = #message{topic = Topic},
354 | {Filter, Pool, SqlList}) ->
355 | with_filter(fun () ->
356 | emqx_metrics:inc('backend.mysql.run_mysql_sql.publish'),
357 | Msg = emqx_backend_mysql_cli:run_mysql_sql(Pool, Msg0,
358 | SqlList),
359 | {ok, Msg}
360 | end,
361 | Msg0, Topic, Filter).
362 |
363 | run_mysql_sql(#{clientid := ClientId},
364 | #message{topic = Topic, id = MsgId},
365 | {Filter, Pool, SqlList}) ->
366 | with_filter(fun () ->
367 | emqx_metrics:inc('backend.mysql.run_mysql_sql.acked_delivered'),
368 | emqx_backend_mysql_cli:run_mysql_sql(Pool,
369 | [{clientid,
370 | ClientId},
371 | {topic, Topic},
372 | {msgid, MsgId}],
373 | SqlList)
374 | end,
375 | Topic, Filter);
376 | run_mysql_sql(#{clientid := ClientId}, _ConnAttrs,
377 | {Filter, Pool, SqlList}) ->
378 | with_filter(fun () ->
379 | emqx_metrics:inc('backend.mysql.run_mysql_sql.connected'),
380 | emqx_backend_mysql_cli:run_mysql_sql(Pool,
381 | [{clientid,
382 | ClientId}],
383 | SqlList)
384 | end,
385 | undefined, Filter);
386 | run_mysql_sql(_, _, _) -> ok.
387 |
388 | run_mysql_sql(#{clientid := ClientId}, Topic, Opts,
389 | {Filter, Pool, SqlList})
390 | when is_binary(Topic) ->
391 | with_filter(fun () ->
392 | emqx_metrics:inc('backend.mysql.run_mysql_sql.sub_unsub'),
393 | QoS = maps:get(qos, Opts, 0),
394 | emqx_backend_mysql_cli:run_mysql_sql(Pool,
395 | [{clientid,
396 | ClientId},
397 | {topic, Topic},
398 | {qos, QoS}],
399 | SqlList)
400 | end,
401 | Topic, Filter);
402 | run_mysql_sql(Client, Topic, Opts,
403 | {Filter, Pool, SqlList, _})
404 | when is_binary(Topic) ->
405 | run_mysql_sql(Client, Topic, Opts,
406 | {Filter, Pool, SqlList});
407 | run_mysql_sql(#{clientid := ClientId}, _Reason,
408 | _ConnInfo, {_Filter, Pool, SqlList}) ->
409 | emqx_metrics:inc('backend.mysql.run_mysql_sql.disconnected'),
410 | emqx_backend_mysql_cli:run_mysql_sql(Pool,
411 | [{clientid, ClientId}], SqlList);
412 | run_mysql_sql(_, _, _, _) -> ok.
413 |
414 | parse_hook(Hooks) -> parse_hook(Hooks, []).
415 |
416 | parse_hook([], Acc) -> Acc;
417 | parse_hook([{Hook, Item} | Hooks], Acc) ->
418 | Params = emqx_json:decode(Item),
419 | Action = proplists:get_value(<<"action">>, Params),
420 | Pool = proplists:get_value(<<"pool">>, Params),
421 | Filter = proplists:get_value(<<"topic">>, Params),
422 | OfflineOpts =
423 | parse_offline_opts(proplists:get_value(<<"offline_opts">>,
424 | Params, [])),
425 | parse_hook(Hooks,
426 | [{l2a(Hook), Action, pool_name(b2a(Pool)), Filter,
427 | OfflineOpts}
428 | | Acc]).
429 |
430 | compile_sql(SQL) ->
431 | case re:run(SQL, <<"\\$\\{[^}]+\\}">>,
432 | [global, {capture, all, binary}])
433 | of
434 | nomatch -> {SQL, []};
435 | {match, Vars} ->
436 | {re:replace(SQL, <<"\\$\\{[^}]+\\}">>, <<"?">>,
437 | [global, {return, binary}]),
438 | [Var || [Var] <- Vars]}
439 | end.
440 |
441 | with_filter(Fun, _, undefined) -> Fun(), ok;
442 | with_filter(Fun, Topic, Filter) ->
443 | case emqx_topic:match(Topic, Filter) of
444 | true -> Fun(), ok;
445 | false -> ok
446 | end.
447 |
448 | with_filter(Fun, _, _, undefined) -> Fun();
449 | with_filter(Fun, Msg, Topic, Filter) ->
450 | case emqx_topic:match(Topic, Filter) of
451 | true -> Fun();
452 | false -> {ok, Msg}
453 | end.
454 |
455 | l2a(L) -> erlang:list_to_atom(L).
456 |
457 | b2a(B) -> erlang:binary_to_atom(B, utf8).
458 |
459 | b2l(B) -> erlang:binary_to_list(B).
460 |
461 | parse_offline_opts(OfflineOpts) ->
462 | parse_offline_opts(OfflineOpts, []).
463 |
464 | parse_offline_opts([], Acc) -> Acc;
465 | parse_offline_opts([{<<"max_returned_count">>,
466 | MaxReturnedCount}
467 | | OfflineOpts],
468 | Acc)
469 | when is_integer(MaxReturnedCount) ->
470 | parse_offline_opts(OfflineOpts,
471 | [{max_returned_count, MaxReturnedCount} | Acc]);
472 | parse_offline_opts([{<<"time_range">>, TimeRange}
473 | | OfflineOpts],
474 | Acc)
475 | when is_binary(TimeRange) ->
476 | parse_offline_opts(OfflineOpts,
477 | [{time_range,
478 | cuttlefish_duration:parse(b2l(TimeRange), s)}
479 | | Acc]);
480 | parse_offline_opts([_ | OfflineOpts], Acc) ->
481 | parse_offline_opts(OfflineOpts, Acc).
482 |
483 |
484 |
--------------------------------------------------------------------------------
/src/emqx_backend_mysql_actions.erl:
--------------------------------------------------------------------------------
1 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
2 | "mqx_backend_mysql_actions.erl",
3 | 1).
4 |
5 | -module(emqx_backend_mysql_actions).
6 |
7 | -export(['$logger_header'/0]).
8 |
9 | -behaviour(ecpool_worker).
10 |
11 | -file("/emqx_rel/_checkouts/emqx/include/emqx.hrl", 1).
12 |
13 | -record(subscription, {topic, subid, subopts}).
14 |
15 | -record(message,
16 | {id :: binary(), qos = 0, from :: atom() | binary(),
17 | flags :: #{atom() => boolean()}, headers :: map(),
18 | topic :: binary(), payload :: binary(),
19 | timestamp :: integer()}).
20 |
21 | -record(delivery,
22 | {sender :: pid(), message :: #message{}}).
23 |
24 | -record(route,
25 | {topic :: binary(),
26 | dest :: node() | {binary(), node()}}).
27 |
28 | -type trie_node_id() :: binary() | atom().
29 |
30 | -record(trie_node,
31 | {node_id :: trie_node_id(),
32 | edge_count = 0 :: non_neg_integer(),
33 | topic :: binary() | undefined, flags :: [atom()]}).
34 |
35 | -record(trie_edge,
36 | {node_id :: trie_node_id(),
37 | word :: binary() | atom()}).
38 |
39 | -record(trie,
40 | {edge :: #trie_edge{}, node_id :: trie_node_id()}).
41 |
42 | -record(alarm,
43 | {id :: binary(),
44 | severity :: notice | warning | error | critical,
45 | title :: iolist(), summary :: iolist(),
46 | timestamp :: erlang:timestamp()}).
47 |
48 | -record(plugin,
49 | {name :: atom(), dir :: string(), descr :: string(),
50 | vendor :: string(), active = false :: boolean(),
51 | info :: map(), type :: atom()}).
52 |
53 | -record(command,
54 | {name :: atom(), action :: atom(),
55 | args = [] :: list(), opts = [] :: list(),
56 | usage :: string(), descr :: string()}).
57 |
58 | -record(banned,
59 | {who ::
60 | {clientid, binary()} | {username, binary()} |
61 | {ip_address, inet:ip_address()},
62 | by :: binary(), reason :: binary(), at :: integer(),
63 | until :: integer()}).
64 |
65 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
66 | "mqx_backend_mysql_actions.erl",
67 | 12).
68 |
69 | -file("/emqx_rel/_checkouts/emqx/include/logger.hrl",
70 | 1).
71 |
72 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
73 | "mqx_backend_mysql_actions.erl",
74 | 13).
75 |
76 | -export([connect/1, send_batch/2]).
77 |
78 | -import(emqx_rule_utils, [str/1, unsafe_atom_key/1]).
79 |
80 | -export([on_resource_create/2, on_resource_destroy/2,
81 | on_get_resource_status/2]).
82 |
83 | -export([on_action_create_data_to_mysql/2]).
84 |
85 | -export([on_action_create_offline_msg/2]).
86 |
87 | -export([on_action_create_lookup_sub/2]).
88 |
89 | -resource_type(#{create => on_resource_create,
90 | description =>
91 | #{en =>
92 | <<77, 121, 83, 81, 76, 32, 68, 97, 116, 97, 98, 97, 115, 101>>,
93 | zh =>
94 | <<77, 121, 83, 81, 76, 32, 230, 149, 176, 230, 141, 174, 229, 186, 147>>},
95 | destroy => on_resource_destroy, name => backend_mysql,
96 | params =>
97 | #{auto_reconnect =>
98 | #{default => true,
99 | description =>
100 | #{en =>
101 | <<73, 102, 32, 114, 101, 45, 116, 114,
102 | 121, 32, 119, 104, 101, 110, 32, 116,
103 | 104, 101, 32, 99, 111, 110, 110, 101,
104 | 99, 116, 105, 111, 110, 32, 108, 111,
105 | 115, 116>>,
106 | zh =>
107 | <<77, 121, 83, 81, 76, 32, 232, 191, 158,
108 | 230, 142, 165, 230, 150, 173, 229, 188,
109 | 128, 230, 151, 182, 230, 152, 175, 229,
110 | 144, 166, 233, 135, 141, 232, 191,
111 | 158>>},
112 | order => 9,
113 | title =>
114 | #{en =>
115 | <<69, 110, 97, 98, 108, 101, 32, 82, 101,
116 | 99, 111, 110, 110, 101, 99, 116>>,
117 | zh =>
118 | <<230, 152, 175, 229, 144, 166, 233, 135,
119 | 141, 232, 191, 158>>},
120 | type => boolean},
121 | batch_size =>
122 | #{default => 100,
123 | description =>
124 | #{en =>
125 | <<84, 104, 101, 32, 115, 105, 122, 101,
126 | 32, 111, 102, 32, 77, 121, 83, 81, 76,
127 | 32, 98, 97, 116, 99, 104, 32, 105, 110,
128 | 115, 101, 114, 116, 32, 83, 81, 76>>,
129 | zh =>
130 | <<77, 121, 83, 81, 76, 32, 230, 137, 185,
131 | 233, 135, 143, 229, 134, 153, 229, 133,
132 | 165, 229, 164, 167, 229, 176, 143>>},
133 | order => 7,
134 | title =>
135 | #{en =>
136 | <<66, 97, 116, 99, 104, 32, 83, 105, 122,
137 | 101>>,
138 | zh =>
139 | <<230, 137, 185, 233, 135, 143, 229, 134,
140 | 153, 229, 133, 165, 229, 164, 167, 229,
141 | 176, 143>>},
142 | type => number},
143 | batch_time =>
144 | #{default => 10,
145 | description =>
146 | #{en =>
147 | <<84, 105, 109, 101, 32, 111, 102, 32,
148 | 77, 121, 83, 81, 76, 32, 98, 97, 116,
149 | 99, 104, 32, 105, 110, 115, 101, 114,
150 | 116, 32, 83, 81, 76, 32, 105, 110, 116,
151 | 101, 114, 118, 97, 108, 40, 109, 105,
152 | 108, 108, 105, 115, 101, 99, 111, 110,
153 | 100, 41>>,
154 | zh =>
155 | <<77, 121, 83, 81, 76, 32, 230, 137, 185,
156 | 233, 135, 143, 229, 134, 153, 229, 133,
157 | 165, 233, 151, 180, 233, 154, 148, 40,
158 | 230, 175, 171, 231, 167, 146, 41>>},
159 | order => 8,
160 | title =>
161 | #{en =>
162 | <<66, 97, 116, 99, 104, 32, 84, 105, 109,
163 | 101>>,
164 | zh =>
165 | <<230, 137, 185, 233, 135, 143, 229, 134,
166 | 153, 229, 133, 165, 233, 151, 180, 233,
167 | 154, 148, 40, 230, 175, 171, 231, 167,
168 | 146, 41>>},
169 | type => number},
170 | database =>
171 | #{description =>
172 | #{en =>
173 | <<68, 97, 116, 97, 98, 97, 115, 101, 32,
174 | 110, 97, 109, 101, 32, 102, 111, 114,
175 | 32, 99, 111, 110, 110, 101, 99, 116,
176 | 105, 110, 103, 32, 116, 111, 32, 77,
177 | 121, 83, 81, 76>>,
178 | zh =>
179 | <<77, 121, 83, 81, 76, 32, 230, 149, 176,
180 | 230, 141, 174, 229, 186, 147, 229, 144,
181 | 141>>},
182 | order => 3, required => true,
183 | title =>
184 | #{en =>
185 | <<77, 121, 83, 81, 76, 32, 68, 97, 116,
186 | 97, 98, 97, 115, 101>>,
187 | zh =>
188 | <<77, 121, 83, 81, 76, 32, 230, 149, 176,
189 | 230, 141, 174, 229, 186, 147, 229, 144,
190 | 141>>},
191 | type => string},
192 | password =>
193 | #{default => <<>>,
194 | description =>
195 | #{en =>
196 | <<80, 97, 115, 115, 119, 111, 114, 100,
197 | 32, 102, 111, 114, 32, 99, 111, 110,
198 | 110, 101, 99, 116, 105, 110, 103, 32,
199 | 116, 111, 32, 77, 121, 83, 81, 76>>,
200 | zh =>
201 | <<77, 121, 83, 81, 76, 32, 229, 175, 134,
202 | 231, 160, 129>>},
203 | order => 6,
204 | title =>
205 | #{en =>
206 | <<77, 121, 83, 81, 76, 32, 80, 97, 115,
207 | 115, 119, 111, 114, 100>>,
208 | zh =>
209 | <<77, 121, 83, 81, 76, 32, 229, 175, 134,
210 | 231, 160, 129>>},
211 | type => string},
212 | pool_size =>
213 | #{default => 8,
214 | description =>
215 | #{en =>
216 | <<84, 104, 101, 32, 115, 105, 122, 101,
217 | 32, 111, 102, 32, 99, 111, 110, 110,
218 | 101, 99, 116, 105, 111, 110, 32, 112,
219 | 111, 111, 108, 32, 102, 111, 114, 32,
220 | 77, 121, 83, 81, 76>>,
221 | zh =>
222 | <<77, 121, 83, 81, 76, 32, 232, 191, 158,
223 | 230, 142, 165, 230, 177, 160, 229, 164,
224 | 167, 229, 176, 143>>},
225 | order => 4,
226 | title =>
227 | #{en =>
228 | <<80, 111, 111, 108, 32, 83, 105, 122,
229 | 101>>,
230 | zh =>
231 | <<232, 191, 158, 230, 142, 165, 230, 177,
232 | 160, 229, 164, 167, 229, 176, 143>>},
233 | type => number},
234 | server =>
235 | #{default =>
236 | <<49, 50, 55, 46, 48, 46, 48, 46, 49, 58, 51,
237 | 51, 48, 54>>,
238 | description =>
239 | #{en =>
240 | <<77, 121, 83, 81, 76, 32, 73, 80, 32,
241 | 97, 100, 100, 114, 101, 115, 115, 32,
242 | 111, 114, 32, 104, 111, 115, 116, 110,
243 | 97, 109, 101, 32, 97, 110, 100, 32,
244 | 112, 111, 114, 116>>,
245 | zh =>
246 | <<77, 121, 83, 81, 76, 32, 230, 156, 141,
247 | 229, 138, 161, 229, 153, 168, 229, 156,
248 | 176, 229, 157, 128>>},
249 | order => 1, required => true,
250 | title =>
251 | #{en =>
252 | <<77, 121, 83, 81, 76, 32, 83, 101, 114,
253 | 118, 101, 114>>,
254 | zh =>
255 | <<77, 121, 83, 81, 76, 32, 230, 156, 141,
256 | 229, 138, 161, 229, 153, 168>>},
257 | type => string},
258 | user =>
259 | #{description =>
260 | #{en =>
261 | <<85, 115, 101, 114, 110, 97, 109, 101,
262 | 32, 102, 111, 114, 32, 99, 111, 110,
263 | 110, 101, 99, 116, 105, 110, 103, 32,
264 | 116, 111, 32, 77, 121, 83, 81, 76>>,
265 | zh =>
266 | <<77, 121, 83, 81, 76, 32, 231, 148, 168,
267 | 230, 136, 183, 229, 144, 141>>},
268 | order => 5, required => true,
269 | title =>
270 | #{en =>
271 | <<77, 121, 83, 81, 76, 32, 85, 115, 101,
272 | 114, 32, 78, 97, 109, 101>>,
273 | zh =>
274 | <<77, 121, 83, 81, 76, 32, 231, 148, 168,
275 | 230, 136, 183, 229, 144, 141>>},
276 | type => string}},
277 | status => on_get_resource_status,
278 | title =>
279 | #{en => <<77, 121, 83, 81, 76>>,
280 | zh => <<77, 121, 83, 81, 76>>}}).
281 |
282 | -rule_action(#{create => on_action_create_data_to_mysql,
283 | description =>
284 | #{en =>
285 | <<68, 97, 116, 97, 32, 116, 111, 32, 77, 121, 83, 81,
286 | 76>>,
287 | zh =>
288 | <<228, 191, 157, 229, 173, 152, 230, 149, 176, 230,
289 | 141, 174, 229, 136, 176, 32, 77, 121, 83, 81, 76, 32,
290 | 230, 149, 176, 230, 141, 174, 229, 186, 147>>},
291 | for => '$any', name => data_to_mysql,
292 | params =>
293 | #{'$resource' =>
294 | #{description =>
295 | #{en =>
296 | <<66, 105, 110, 100, 32, 97, 32, 114, 101,
297 | 115, 111, 117, 114, 99, 101, 32, 116,
298 | 111, 32, 116, 104, 105, 115, 32, 97, 99,
299 | 116, 105, 111, 110>>,
300 | zh =>
301 | <<231, 187, 153, 229, 138, 168, 228, 189,
302 | 156, 231, 187, 145, 229, 174, 154, 228,
303 | 184, 128, 228, 184, 170, 232, 181, 132,
304 | 230, 186, 144>>},
305 | required => true,
306 | title =>
307 | #{en =>
308 | <<82, 101, 115, 111, 117, 114, 99, 101, 32,
309 | 73, 68>>,
310 | zh =>
311 | <<232, 181, 132, 230, 186, 144, 32, 73,
312 | 68>>},
313 | type => string},
314 | sql =>
315 | #{description =>
316 | #{en =>
317 | <<83, 81, 76, 32, 116, 101, 109, 112, 108,
318 | 97, 116, 101, 32, 119, 105, 116, 104, 32,
319 | 112, 108, 97, 99, 101, 104, 111, 108,
320 | 100, 101, 114, 115, 32, 102, 111, 114,
321 | 32, 105, 110, 115, 101, 114, 116, 105,
322 | 110, 103, 47, 117, 112, 100, 97, 116,
323 | 105, 110, 103, 32, 100, 97, 116, 97, 32,
324 | 116, 111, 32, 77, 121, 83, 81, 76>>,
325 | zh =>
326 | <<229, 140, 133, 229, 144, 171, 228, 186,
327 | 134, 229, 141, 160, 228, 189, 141, 231,
328 | 172, 166, 231, 154, 132, 32, 83, 81, 76,
329 | 32, 230, 168, 161, 230, 157, 191, 239,
330 | 188, 140, 231, 148, 168, 228, 187, 165,
331 | 230, 143, 146, 229, 133, 165, 230, 136,
332 | 150, 230, 155, 180, 230, 150, 176, 230,
333 | 149, 176, 230, 141, 174, 229, 136, 176,
334 | 32, 77, 121, 83, 81, 76, 32, 230, 149,
335 | 176, 230, 141, 174, 229, 186, 147>>},
336 | input => textarea, required => true,
337 | title =>
338 | #{en =>
339 | <<83, 81, 76, 32, 84, 101, 109, 112, 108,
340 | 97, 116, 101>>,
341 | zh =>
342 | <<83, 81, 76, 32, 230, 168, 161, 230, 157,
343 | 191>>},
344 | type => string}},
345 | title =>
346 | #{en =>
347 | <<68, 97, 116, 97, 32, 116, 111, 32, 77, 121, 83, 81,
348 | 76>>,
349 | zh =>
350 | <<228, 191, 157, 229, 173, 152, 230, 149, 176, 230,
351 | 141, 174, 229, 136, 176, 32, 77, 121, 83, 81, 76>>},
352 | types => [backend_mysql]}).
353 |
354 | -rule_action(#{create => on_action_create_offline_msg,
355 | description =>
356 | #{en =>
357 | <<79, 102, 102, 108, 105, 110, 101, 32, 77, 115, 103,
358 | 32, 116, 111, 32, 77, 121, 83, 81, 76>>,
359 | zh =>
360 | <<231, 166, 187, 231, 186, 191, 230, 182, 136, 230,
361 | 129, 175, 228, 191, 157, 229, 173, 152, 229, 136,
362 | 176, 32, 77, 121, 83, 81, 76>>},
363 | for => '$any', name => offline_msg_to_mysql,
364 | params =>
365 | #{'$resource' =>
366 | #{description =>
367 | #{en =>
368 | <<66, 105, 110, 100, 32, 97, 32, 114, 101,
369 | 115, 111, 117, 114, 99, 101, 32, 116,
370 | 111, 32, 116, 104, 105, 115, 32, 97, 99,
371 | 116, 105, 111, 110>>,
372 | zh =>
373 | <<231, 187, 153, 229, 138, 168, 228, 189,
374 | 156, 231, 187, 145, 229, 174, 154, 228,
375 | 184, 128, 228, 184, 170, 232, 181, 132,
376 | 230, 186, 144>>},
377 | required => true,
378 | title =>
379 | #{en =>
380 | <<82, 101, 115, 111, 117, 114, 99, 101, 32,
381 | 73, 68>>,
382 | zh =>
383 | <<232, 181, 132, 230, 186, 144, 32, 73,
384 | 68>>},
385 | type => string},
386 | max_returned_count =>
387 | #{default => 0,
388 | description =>
389 | #{en =>
390 | <<77, 97, 120, 32, 110, 117, 109, 98, 101,
391 | 114, 32, 111, 102, 32, 102, 101, 116, 99,
392 | 104, 32, 111, 102, 102, 108, 105, 110,
393 | 101, 32, 109, 101, 115, 115, 97, 103,
394 | 101, 115>>,
395 | zh =>
396 | <<232, 142, 183, 229, 143, 150, 231, 166,
397 | 187, 231, 186, 191, 230, 182, 136, 230,
398 | 129, 175, 231, 154, 132, 230, 156, 128,
399 | 229, 164, 167, 230, 157, 161, 230, 149,
400 | 176>>},
401 | required => false,
402 | title =>
403 | #{en =>
404 | <<77, 97, 120, 32, 82, 101, 116, 117, 114,
405 | 110, 101, 100, 32, 67, 111, 117, 110,
406 | 116>>,
407 | zh =>
408 | <<77, 97, 120, 32, 82, 101, 116, 117, 114,
409 | 110, 101, 100, 32, 67, 111, 117, 110,
410 | 116>>},
411 | type => number},
412 | time_range =>
413 | #{default => <<>>,
414 | description =>
415 | #{en =>
416 | <<84, 105, 109, 101, 32, 82, 97, 110, 103,
417 | 101, 32, 111, 102, 32, 102, 101, 116, 99,
418 | 104, 32, 111, 102, 102, 108, 105, 110,
419 | 101, 32, 109, 101, 115, 115, 97, 103,
420 | 101, 115>>,
421 | zh =>
422 | <<232, 142, 183, 229, 143, 150, 230, 156,
423 | 128, 232, 191, 145, 230, 151, 182, 233,
424 | 151, 180, 232, 140, 131, 229, 155, 180,
425 | 229, 134, 133, 231, 154, 132, 231, 166,
426 | 187, 231, 186, 191, 230, 182, 136, 230,
427 | 129, 175>>},
428 | required => false,
429 | title =>
430 | #{en =>
431 | <<84, 105, 109, 101, 32, 82, 97, 110, 103,
432 | 101>>,
433 | zh =>
434 | <<84, 105, 109, 101, 32, 82, 97, 110, 103,
435 | 101>>},
436 | type => string}},
437 | title =>
438 | #{en =>
439 | <<79, 102, 102, 108, 105, 110, 101, 32, 77, 115, 103,
440 | 32, 116, 111, 32, 77, 121, 83, 81, 76>>,
441 | zh =>
442 | <<231, 166, 187, 231, 186, 191, 230, 182, 136, 230,
443 | 129, 175, 228, 191, 157, 229, 173, 152, 229, 136,
444 | 176, 32, 77, 121, 83, 81, 76>>},
445 | types => [backend_mysql]}).
446 |
447 | -rule_action(#{create => on_action_create_lookup_sub,
448 | description =>
449 | #{en =>
450 | <<71, 101, 116, 32, 83, 117, 98, 115, 99, 114, 105,
451 | 112, 116, 105, 111, 110, 32, 76, 105, 115, 116, 32,
452 | 70, 114, 111, 109, 32, 77, 121, 83, 81, 76>>,
453 | zh =>
454 | <<228, 187, 142, 32, 77, 121, 83, 81, 76, 32, 228, 184,
455 | 173, 232, 142, 183, 229, 143, 150, 232, 174, 162,
456 | 233, 152, 133, 229, 136, 151, 232, 161, 168>>},
457 | for => '$any', name => lookup_sub_to_mysql,
458 | params =>
459 | #{'$resource' =>
460 | #{description =>
461 | #{en =>
462 | <<66, 105, 110, 100, 32, 97, 32, 114, 101,
463 | 115, 111, 117, 114, 99, 101, 32, 116,
464 | 111, 32, 116, 104, 105, 115, 32, 97, 99,
465 | 116, 105, 111, 110>>,
466 | zh =>
467 | <<231, 187, 153, 229, 138, 168, 228, 189,
468 | 156, 231, 187, 145, 229, 174, 154, 228,
469 | 184, 128, 228, 184, 170, 232, 181, 132,
470 | 230, 186, 144>>},
471 | required => true,
472 | title =>
473 | #{en =>
474 | <<82, 101, 115, 111, 117, 114, 99, 101, 32,
475 | 73, 68>>,
476 | zh =>
477 | <<232, 181, 132, 230, 186, 144, 32, 73,
478 | 68>>},
479 | type => string}},
480 | title =>
481 | #{en =>
482 | <<83, 117, 98, 115, 99, 114, 105, 112, 116, 105, 111,
483 | 110, 32, 76, 105, 115, 116, 32, 70, 114, 111, 109,
484 | 32, 77, 121, 83, 81, 76>>,
485 | zh =>
486 | <<228, 187, 142, 32, 77, 121, 83, 81, 76, 32, 228, 184,
487 | 173, 232, 142, 183, 229, 143, 150, 232, 174, 162,
488 | 233, 152, 133, 229, 136, 151, 232, 161, 168>>},
489 | types => [backend_mysql]}).
490 |
491 | on_resource_create(ResId,
492 | Config = #{<<"server">> := Server, <<"user">> := User,
493 | <<"database">> := DB}) ->
494 | begin
495 | logger:log(info, #{},
496 | #{report_cb =>
497 | fun (_) ->
498 | {'$logger_header'() ++
499 | "Initiating Resource ~p, ResId: ~p",
500 | [backend_mysql, ResId]}
501 | end,
502 | mfa =>
503 | {emqx_backend_mysql_actions, on_resource_create, 2},
504 | line => 229})
505 | end,
506 | {ok, _} = application:ensure_all_started(ecpool),
507 | {Host, Port} = host(Server),
508 | Options = [{host, str(Host)}, {port, Port},
509 | {user, str(User)},
510 | {password, str(maps:get(<<"password">>, Config, ""))},
511 | {database, str(DB)},
512 | {auto_reconnect,
513 | case maps:get(<<"auto_reconnect">>, Config, true) of
514 | true -> 15;
515 | false -> false
516 | end},
517 | {pool_size, maps:get(<<"pool_size">>, Config, 8)},
518 | {batch_size, maps:get(<<"batch_size">>, Config, 100)},
519 | {batch_time, maps:get(<<"batch_time">>, Config, 10)}],
520 | PoolName = pool_name(ResId),
521 | start_resource(ResId, PoolName, Options),
522 | #{<<"pool">> => PoolName}.
523 |
524 | start_resource(ResId, PoolName, Options) ->
525 | case ecpool:start_sup_pool(PoolName,
526 | emqx_backend_mysql_actions, Options)
527 | of
528 | {ok, _} ->
529 | begin
530 | logger:log(info, #{},
531 | #{report_cb =>
532 | fun (_) ->
533 | {'$logger_header'() ++
534 | "Initiated Resource ~p Successfully, "
535 | "ResId: ~p",
536 | [backend_mysql, ResId]}
537 | end,
538 | mfa => {emqx_backend_mysql_actions, start_resource, 3},
539 | line => 251})
540 | end;
541 | {error, {already_started, _Pid}} ->
542 | on_resource_destroy(ResId, #{<<"pool">> => PoolName}),
543 | start_resource(ResId, PoolName, Options);
544 | {error, Reason} ->
545 | begin
546 | logger:log(error, #{},
547 | #{report_cb =>
548 | fun (_) ->
549 | {'$logger_header'() ++
550 | "Initiate Resource ~p failed, ResId: "
551 | "~p, ~0p",
552 | [backend_mysql, ResId, Reason]}
553 | end,
554 | mfa => {emqx_backend_mysql_actions, start_resource, 3},
555 | line => 256})
556 | end,
557 | error({{backend_mysql, ResId}, create_failed})
558 | end.
559 |
560 | -spec on_get_resource_status(ResId :: binary(),
561 | Params :: map()) -> Status :: map().
562 |
563 | on_get_resource_status(_ResId,
564 | #{<<"pool">> := PoolName}) ->
565 | Status = [ecpool_worker:is_connected(Worker)
566 | || {_WorkerName, Worker} <- ecpool:workers(PoolName)],
567 | #{is_alive =>
568 | length(Status) > 0 andalso
569 | lists:all(fun (St) -> St =:= true end, Status)}.
570 |
571 | on_resource_destroy(ResId, #{<<"pool">> := PoolName}) ->
572 | begin
573 | logger:log(info, #{},
574 | #{report_cb =>
575 | fun (_) ->
576 | {'$logger_header'() ++
577 | "Destroying Resource ~p, ResId: ~p",
578 | [backend_mysql, ResId]}
579 | end,
580 | mfa =>
581 | {emqx_backend_mysql_actions, on_resource_destroy, 2},
582 | line => 267})
583 | end,
584 | case ecpool:stop_sup_pool(PoolName) of
585 | ok ->
586 | begin
587 | logger:log(info, #{},
588 | #{report_cb =>
589 | fun (_) ->
590 | {'$logger_header'() ++
591 | "Destroyed Resource ~p Successfully, "
592 | "ResId: ~p",
593 | [backend_mysql, ResId]}
594 | end,
595 | mfa =>
596 | {emqx_backend_mysql_actions, on_resource_destroy,
597 | 2},
598 | line => 270})
599 | end;
600 | {error, Reason} ->
601 | begin
602 | logger:log(error, #{},
603 | #{report_cb =>
604 | fun (_) ->
605 | {'$logger_header'() ++
606 | "Destroy Resource ~p failed, ResId: ~p, ~p",
607 | [backend_mysql, ResId, Reason]}
608 | end,
609 | mfa =>
610 | {emqx_backend_mysql_actions, on_resource_destroy,
611 | 2},
612 | line => 272})
613 | end,
614 | error({{backend_mysql, ResId}, destroy_failed})
615 | end.
616 |
617 | -spec on_action_create_data_to_mysql(ActionInstId ::
618 | binary(),
619 | #{}) -> fun((Msg :: map()) -> any()).
620 |
621 | on_action_create_data_to_mysql(ActionInstId,
622 | #{<<"pool">> := PoolName,
623 | <<"sql">> := SqlTemplate}) ->
624 | PrepareSqlKey = unsafe_atom_key(ActionInstId),
625 | begin
626 | logger:log(info, #{},
627 | #{report_cb =>
628 | fun (_) ->
629 | {'$logger_header'() ++
630 | "Initiating Action ~p, SqlTemplate: ~p, "
631 | "PrepareSqlKey: ~p",
632 | [on_action_create_data_to_mysql, SqlTemplate,
633 | PrepareSqlKey]}
634 | end,
635 | mfa =>
636 | {emqx_backend_mysql_actions,
637 | on_action_create_data_to_mysql, 2},
638 | line => 285})
639 | end,
640 | {PrepareStatement, GetPrepareParams} =
641 | emqx_rule_utils:preproc_sql(SqlTemplate, '?'),
642 | prepare_sql(PrepareSqlKey, PrepareStatement, PoolName),
643 | ecpool:add_reconnect_callback(PoolName,
644 | fun (Batch) ->
645 | prepare_sql_to_conn(PrepareSqlKey,
646 | PrepareStatement,
647 | emqx_backend_mysql_batcher:get_cbst(Batch))
648 | end),
649 | fun (Msg, _Envs) ->
650 | case mysql_query(PoolName, PrepareSqlKey,
651 | PrepareStatement, GetPrepareParams(Msg))
652 | of
653 | {error, Reason} -> error({data_to_mysql, Reason});
654 | _ -> ok
655 | end
656 | end.
657 |
658 | on_action_create_offline_msg(ActionInstId,
659 | #{<<"pool">> := PoolName,
660 | <<"time_range">> := TimeRange0,
661 | <<"max_returned_count">> :=
662 | MaxReturnedCount0}) ->
663 | InsertSql = <<"insert into mqtt_msg(msgid, sender, "
664 | "topic, qos, retain, payload, arrived) "
665 | "values (?, ?, ?, ?, ?, ?, FROM_UNIXTIME(?) "
666 | ")">>,
667 | DeleteSql = <<"delete from mqtt_msg where msgid = ? "
668 | "and topic = ?">>,
669 | SelectSql = <<"select id, topic, msgid, sender, qos, "
670 | "payload, retain, UNIX_TIMESTAMP(arrived) "
671 | "from mqtt_msg where topic = ? order "
672 | "by id DESC">>,
673 | InsertKey = unsafe_atom_key(<<"insert_",
674 | ActionInstId/binary>>),
675 | DeleteKey = unsafe_atom_key(<<"delete_",
676 | ActionInstId/binary>>),
677 | SelectKey = unsafe_atom_key(<<"select_",
678 | ActionInstId/binary>>),
679 | TimeRange = case to_undefined(TimeRange0) of
680 | undefined -> undefined;
681 | TimeRange0 -> cuttlefish_duration:parse(TimeRange0, s)
682 | end,
683 | MaxReturnedCount = to_undefined(MaxReturnedCount0),
684 | {SelectSql1, Params} = case {TimeRange,
685 | MaxReturnedCount}
686 | of
687 | {undefined, undefined} -> {SelectSql, []};
688 | {TimeRange, undefined} ->
689 | {<= FROM_UNIXTIME(?) ">>,
691 | [TimeRange div 1000]};
692 | {undefined, MaxReturnedCount} ->
693 | {<>,
694 | [MaxReturnedCount]};
695 | {TimeRange, MaxReturnedCount} ->
696 | {<= FROM_UNIXTIME(?) limit ? ">>,
698 | [TimeRange div 1000, MaxReturnedCount]}
699 | end,
700 | prepare_sql(InsertKey, InsertSql, PoolName),
701 | prepare_sql(DeleteKey, DeleteSql, PoolName),
702 | prepare_sql(SelectKey, SelectSql1, PoolName),
703 | ecpool:add_reconnect_callback(PoolName,
704 | fun (Conn) ->
705 | prepare_sql_to_conn(InsertKey,
706 | InsertSql, Conn),
707 | prepare_sql_to_conn(SelectKey,
708 | SelectSql1, Conn),
709 | prepare_sql_to_conn(DeleteKey,
710 | DeleteSql, Conn)
711 | end),
712 | fun (Msg = #{event := Event, topic := Topic}, _Envs) ->
713 | case Event of
714 | 'message.publish' ->
715 | #{id := MsgId, qos := Qos, flags := Flags,
716 | payload := Payload, publish_received_at := Ts,
717 | clientid := From} =
718 | Msg,
719 | Retain = maps:get(retain, Flags, true),
720 | insert_message(PoolName, InsertKey, InsertSql,
721 | [MsgId, From, Topic, Qos, i(Retain), Payload,
722 | Ts div 1000]);
723 | 'session.subscribed' ->
724 | MsgList = lookup_message(PoolName, SelectKey, SelectSql,
725 | [Topic] ++ Params),
726 | [self() ! {deliver, Topic, M} || M <- MsgList];
727 | 'message.acked' ->
728 | #{id := MsgId} = Msg,
729 | delete_message(PoolName, DeleteKey, DeleteSql,
730 | [MsgId, Topic])
731 | end
732 | end.
733 |
734 | on_action_create_lookup_sub(ActionInstId,
735 | #{<<"pool">> := PoolName}) ->
736 | SelectPrepare =
737 | <<"select topic, qos from mqtt_sub where "
738 | "clientid = ?">>,
739 | InsertPrepare =
740 | <<"insert into mqtt_sub(clientid, topic, "
741 | "qos) values(?, ?, ?)">>,
742 | SelectKey = unsafe_atom_key(<<"select_",
743 | ActionInstId/binary>>),
744 | InsertKey = unsafe_atom_key(<<"insert_",
745 | ActionInstId/binary>>),
746 | prepare_sql(SelectKey, SelectPrepare, PoolName),
747 | prepare_sql(InsertKey, InsertPrepare, PoolName),
748 | ecpool:add_reconnect_callback(PoolName,
749 | fun (Batch) ->
750 | prepare_sql_to_conn(SelectKey,
751 | SelectPrepare,
752 | emqx_backend_mysql_batcher:get_cbst(Batch)),
753 | prepare_sql_to_conn(InsertKey,
754 | InsertPrepare,
755 | emqx_backend_mysql_batcher:get_cbst(Batch))
756 | end),
757 | fun (Msg = #{event := Event, clientid := ClientId},
758 | _Envs) ->
759 | case Event of
760 | 'client.connected' ->
761 | case lookup_subscribe(PoolName, SelectKey,
762 | SelectPrepare, [ClientId])
763 | of
764 | [] -> ok;
765 | TopicTable -> self() ! {subscribe, TopicTable}
766 | end;
767 | 'session.subscribed' ->
768 | #{topic := Topic, qos := QoS} = Msg,
769 | insert_subscribe(PoolName, InsertKey, InsertPrepare,
770 | [ClientId, Topic, QoS])
771 | end
772 | end.
773 |
774 | prepare_sql(PrepareSqlKey, PrepareStatement,
775 | PoolName) ->
776 | [begin
777 | {ok, Batch} = ecpool_worker:client(Worker),
778 | prepare_sql_to_conn(PrepareSqlKey, PrepareStatement,
779 | emqx_backend_mysql_batcher:get_cbst(Batch)),
780 | case binary:split(PrepareStatement, <<"values ">>) of
781 | [Insert, Params] ->
782 | emqx_backend_mysql_batcher:set_value(Batch,
783 | {insert_sql, PrepareSqlKey},
784 | {binary_to_list(Insert),
785 | binary_to_list(Params)});
786 | _ -> ok
787 | end
788 | end
789 | || {_WorkerName, Worker} <- ecpool:workers(PoolName)].
790 |
791 | prepare_sql_to_conn(PrepareSqlKey, PrepareStatement,
792 | Conn) ->
793 | case mysql:prepare(Conn, PrepareSqlKey,
794 | PrepareStatement)
795 | of
796 | {ok, Name} ->
797 | begin
798 | logger:log(info, #{},
799 | #{report_cb =>
800 | fun (_) ->
801 | {'$logger_header'() ++
802 | "Prepare Statement: ~p, PreparedSQL Key: ~p",
803 | [PrepareStatement, Name]}
804 | end,
805 | mfa =>
806 | {emqx_backend_mysql_actions, prepare_sql_to_conn,
807 | 3},
808 | line => 398})
809 | end;
810 | {error, Reason} ->
811 | begin
812 | logger:log(error, #{},
813 | #{report_cb =>
814 | fun (_) ->
815 | {'$logger_header'() ++
816 | "Prepare Statement: ~p, ~0p",
817 | [PrepareStatement, Reason]}
818 | end,
819 | mfa =>
820 | {emqx_backend_mysql_actions, prepare_sql_to_conn,
821 | 3},
822 | line => 400})
823 | end,
824 | error(prepare_sql_failed)
825 | end.
826 |
827 | connect(Options) ->
828 | InitFun = fun () ->
829 | {ok, Conn} = mysql:start_link(Options),
830 | put(backslash_escapes_enabled,
831 | gen_server:call(Conn, backslash_escapes_enabled)),
832 | {ok, Conn}
833 | end,
834 | Handler = fun (Conn, Batch) ->
835 | {emqx_backend_mysql_actions:send_batch(Conn, Batch),
836 | Conn}
837 | end,
838 | emqx_backend_mysql_batcher:start_link(InitFun, Handler,
839 | #{batch_size =>
840 | proplists:get_value(batch_size,
841 | Options),
842 | batch_time =>
843 | proplists:get_value(batch_time,
844 | Options)}).
845 |
846 | mysql_query(Pool, Ref, PrepareStatement, Params) ->
847 | begin
848 | logger:log(debug, #{},
849 | #{report_cb =>
850 | fun (_) ->
851 | {'$logger_header'() ++
852 | "Send to mysql, pool: ~p, prepared_key: "
853 | "~p, ~0p",
854 | [Pool, Ref, Params]}
855 | end,
856 | mfa => {emqx_backend_mysql_actions, mysql_query, 4},
857 | line => 420})
858 | end,
859 | ecpool:with_client(Pool,
860 | fun (Conn) ->
861 | Fun = fun () ->
862 | get({insert_sql, Ref}) =/=
863 | undefined
864 | end,
865 | case emqx_backend_mysql_batcher:try_batch(Conn,
866 | {Ref,
867 | Params},
868 | Fun)
869 | of
870 | {error, disconnected} -> exit(Conn, restart);
871 | {error, not_prepared} ->
872 | prepare_sql_to_conn(Ref, PrepareStatement,
873 | emqx_backend_mysql_batcher:get_cbst(Conn)),
874 | emqx_backend_mysql_batcher:try_batch(Conn,
875 | {Ref,
876 | Params},
877 | Fun);
878 | Res -> Res
879 | end
880 | end).
881 |
882 | mysql_query_not_batch(Pool, Ref, PrepareStatement,
883 | Params) ->
884 | begin
885 | logger:log(debug, #{},
886 | #{report_cb =>
887 | fun (_) ->
888 | {'$logger_header'() ++
889 | "Send to mysql, pool: ~p, prepared_key: "
890 | "~p, ~0p",
891 | [Pool, Ref, Params]}
892 | end,
893 | mfa =>
894 | {emqx_backend_mysql_actions, mysql_query_not_batch, 4},
895 | line => 434})
896 | end,
897 | ecpool:with_client(Pool,
898 | fun (Conn) ->
899 | MConn =
900 | emqx_backend_mysql_batcher:get_cbst(Conn),
901 | case mysql:execute(MConn, Ref, Params) of
902 | {error, disconnected} -> exit(Conn, restart);
903 | {error, not_prepared} ->
904 | prepare_sql_to_conn(Ref, PrepareStatement,
905 | MConn),
906 | mysql:execute(MConn, Ref, Params);
907 | Res -> Res
908 | end
909 | end).
910 |
911 | send_batch(Conn, {Ref, Params}) ->
912 | mysql:execute(Conn, Ref, Params);
913 | send_batch(Conn, Batch) -> send_inserts(Conn, Batch).
914 |
915 | send_inserts(Conn, Msgs) ->
916 | Msgs1 = maps:to_list(lists:foldl(fun ({_, K, V}, Acc) ->
917 | L = maps:get(K, Acc, []),
918 | Acc#{K => [V | L]}
919 | end,
920 | #{}, Msgs)),
921 | lists:map(fun ({Ref, Msgs2}) ->
922 | SQL = insert_batch_sql(Ref, Msgs),
923 | case mysql:query(Conn, SQL, lists:flatten(Msgs2)) of
924 | {error, Reason} -> error(insert_error, Reason);
925 | _ -> lists:map(fun (_) -> ok end, Msgs)
926 | end
927 | end,
928 | Msgs1).
929 |
930 | insert_batch_sql(Ref, Msgs) ->
931 | {InsertSQL, ValuesSQL} = get({insert_sql, Ref}),
932 | [InsertSQL, "values ", values(Msgs, ValuesSQL)].
933 |
934 | values(Msgs, ValuesSQL) -> values(Msgs, ValuesSQL, []).
935 |
936 | values([], _, Acc) -> Acc;
937 | values([_ | Rest], ValuesSQL, Acc) ->
938 | NewAcc = case Rest == [] of
939 | true -> Acc ++ ValuesSQL;
940 | false -> Acc ++ ValuesSQL ++ ", "
941 | end,
942 | values(Rest, ValuesSQL, NewAcc).
943 |
944 | pool_name(ResId) ->
945 | list_to_atom("backend_mysql:" ++ str(ResId)).
946 |
947 | host(Server) when is_binary(Server) ->
948 | case string:split(Server, ":") of
949 | [Host, Port] -> {Host, binary_to_integer(Port)};
950 | [Host] -> {Host, 3306}
951 | end.
952 |
953 | insert_message(Pool, PrepareSqlKey, PrepareStatement,
954 | PrepareParams) ->
955 | case mysql_query_not_batch(Pool, PrepareSqlKey,
956 | PrepareStatement, PrepareParams)
957 | of
958 | {error, Error} ->
959 | begin
960 | logger:log(error, #{},
961 | #{report_cb =>
962 | fun (_) ->
963 | {'$logger_header'() ++
964 | "Failed to store message: ~p",
965 | [Error]}
966 | end,
967 | mfa => {emqx_backend_mysql_actions, insert_message, 4},
968 | line => 492})
969 | end;
970 | _ -> ok
971 | end.
972 |
973 | lookup_message(Pool, PrepareSqlKey, PrepareStatement,
974 | PrepareParams) ->
975 | case mysql_query_not_batch(Pool, PrepareSqlKey,
976 | PrepareStatement, PrepareParams)
977 | of
978 | {ok, _Cols, []} -> [];
979 | {ok, Cols, Rows} ->
980 | [begin
981 | Msg =
982 | emqx_backend_mysql_cli:record_to_msg(lists:zip(Cols,
983 | Row)),
984 | Msg#message{id = emqx_guid:from_hexstr(Msg#message.id)}
985 | end
986 | || Row <- lists:reverse(Rows)];
987 | {error, Error} ->
988 | begin
989 | logger:log(error, #{},
990 | #{report_cb =>
991 | fun (_) ->
992 | {'$logger_header'() ++
993 | "Failed to lookup message error: ~p",
994 | [Error]}
995 | end,
996 | mfa => {emqx_backend_mysql_actions, lookup_message, 4},
997 | line => 503})
998 | end,
999 | []
1000 | end.
1001 |
1002 | delete_message(Pool, PrepareSqlKey, PrepareStatement,
1003 | PrepareParams) ->
1004 | case mysql_query_not_batch(Pool, PrepareSqlKey,
1005 | PrepareStatement, PrepareParams)
1006 | of
1007 | {error, Error} ->
1008 | begin
1009 | logger:log(error, #{},
1010 | #{report_cb =>
1011 | fun (_) ->
1012 | {'$logger_header'() ++
1013 | "Failed to delete message: ~p",
1014 | [Error]}
1015 | end,
1016 | mfa => {emqx_backend_mysql_actions, delete_message, 4},
1017 | line => 508})
1018 | end;
1019 | _ -> ok
1020 | end.
1021 |
1022 | insert_subscribe(Pool, PrepareSqlKey, PrepareStatement,
1023 | PrepareParams) ->
1024 | case mysql_query_not_batch(Pool, PrepareSqlKey,
1025 | PrepareStatement, PrepareParams)
1026 | of
1027 | {error, Error} ->
1028 | begin
1029 | logger:log(error, #{},
1030 | #{report_cb =>
1031 | fun (_) ->
1032 | {'$logger_header'() ++
1033 | "Failed to store subscribe: ~p",
1034 | [Error]}
1035 | end,
1036 | mfa =>
1037 | {emqx_backend_mysql_actions, insert_subscribe, 4},
1038 | line => 514})
1039 | end;
1040 | _ -> ok
1041 | end.
1042 |
1043 | lookup_subscribe(Pool, PrepareSqlKey, PrepareStatement,
1044 | PrepareParams) ->
1045 | case mysql_query_not_batch(Pool, PrepareSqlKey,
1046 | PrepareStatement, PrepareParams)
1047 | of
1048 | {ok, _Cols, []} -> [];
1049 | {error, Error} ->
1050 | begin
1051 | logger:log(error, #{},
1052 | #{report_cb =>
1053 | fun (_) ->
1054 | {'$logger_header'() ++
1055 | "Lookup subscription error: ~p",
1056 | [Error]}
1057 | end,
1058 | mfa =>
1059 | {emqx_backend_mysql_actions, lookup_subscribe, 4},
1060 | line => 521})
1061 | end,
1062 | [];
1063 | {ok, _Cols, Rows} ->
1064 | [{Topic, #{qos => Qos}} || [Topic, Qos] <- Rows]
1065 | end.
1066 |
1067 | i(true) -> 1;
1068 | i(false) -> 0.
1069 |
1070 | to_undefined(<<>>) -> undefined;
1071 | to_undefined(0) -> undefined;
1072 | to_undefined(V) -> V.
1073 |
1074 | '$logger_header'() -> "[RuleEngine MySql] ".
1075 |
1076 |
1077 |
--------------------------------------------------------------------------------
/src/emqx_backend_mysql_app.erl:
--------------------------------------------------------------------------------
1 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
2 | "mqx_backend_mysql_app.erl",
3 | 1).
4 |
5 | -module(emqx_backend_mysql_app).
6 |
7 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/inclu"
8 | "de/emqx_backend_mysql.hrl",
9 | 1).
10 |
11 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
12 | "mqx_backend_mysql_app.erl",
13 | 10).
14 |
15 | -behaviour(application).
16 |
17 | -emqx_plugin(backend).
18 |
19 | -export([start/2, stop/1]).
20 |
21 | start(_Type, _Args) ->
22 | Pools = application:get_env(emqx_backend_mysql, pools,
23 | []),
24 | {ok, Sup} = emqx_backend_mysql_sup:start_link(Pools),
25 | emqx_backend_mysql:register_metrics(),
26 | emqx_backend_mysql:load(),
27 | {ok, Sup}.
28 |
29 | stop(_State) -> emqx_backend_mysql:unload().
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/emqx_backend_mysql_batcher.erl:
--------------------------------------------------------------------------------
1 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
2 | "mqx_backend_mysql_batcher.erl",
3 | 1).
4 |
5 | -module(emqx_backend_mysql_batcher).
6 |
7 | -behaviour(gen_statem).
8 |
9 | -export([start_link/3, call/2, call/3, get_cbst/1,
10 | try_batch/3, set_value/3]).
11 |
12 | -export([callback_mode/0, init/1]).
13 |
14 | -export([do/3]).
15 |
16 | callback_mode() -> [state_functions, state_enter].
17 |
18 | start_link(InitFun, CallHandler, Opts) ->
19 | gen_statem:start_link(emqx_backend_mysql_batcher,
20 | {InitFun, CallHandler, Opts}, []).
21 |
22 | call(Pid, Data) -> gen_statem:call(Pid, {call, Data}).
23 |
24 | call(Pid, Data, Opts) ->
25 | gen_statem:call(Pid, {call, Data, Opts}).
26 |
27 | try_batch(Pid, Data, Fun) ->
28 | gen_statem:call(Pid, {try_batch, Data, Fun}).
29 |
30 | set_value(Pid, Key, Value) ->
31 | gen_statem:cast(Pid, {set_value, Key, Value}).
32 |
33 | get_cbst(Pid) -> gen_statem:call(Pid, get_cbst).
34 |
35 | init({InitFun, CallHandler, Opts}) ->
36 | {ok, CbSt} = InitFun(),
37 | BatchSize = maps:get(batch_size, Opts, 100),
38 | BatchTime = maps:get(batch_time, Opts, 10),
39 | St = #{batch_size => BatchSize, batch_time => BatchTime,
40 | handler => CallHandler, cb_st => CbSt, acc => [],
41 | acc_left => BatchSize},
42 | {ok, do, St}.
43 |
44 | do(enter, _, #{batch_time := T}) ->
45 | Action = {timeout, T, flush},
46 | {keep_state_and_data, [Action]};
47 | do({call, From}, {call, Data, flush}, St) ->
48 | {repeat_state, flush(From, Data, St)};
49 | do({call, From}, {call, Data},
50 | #{acc := Acc, acc_left := Left} = St0) ->
51 | St = St0#{acc := [{From, Data} | Acc],
52 | acc_left := Left - 1},
53 | case Left =< 1 of
54 | true -> {repeat_state, flush(St)};
55 | false -> {repeat_state, St}
56 | end;
57 | do({call, From}, {try_batch, {Ref, Data}, Fun}, St) ->
58 | case Fun() of
59 | true -> do({call, From}, {call, {i, Ref, Data}}, St);
60 | false ->
61 | do({call, From}, {call, {Ref, Data}, flush}, St)
62 | end;
63 | do(cast, {set_value, Key, Value}, St) ->
64 | put(Key, Value), {repeat_state, St};
65 | do({call, From}, get_cbst, #{cb_st := CbSt}) ->
66 | {keep_state_and_data, [{reply, From, CbSt}]};
67 | do(info, {timeout, _Ref, flush}, St) ->
68 | {repeat_state, flush(St)};
69 | do(timeout, flush, St) -> {repeat_state, flush(St)}.
70 |
71 | flush(#{acc := []} = St) -> St;
72 | flush(#{acc := Acc, batch_size := Size, cb_st := CbSt,
73 | handler := Handler} =
74 | St) ->
75 | {Froms, Batch} = lists:unzip(lists:reverse(Acc)),
76 | {Results, NewCbSt} = Handler(CbSt, Batch),
77 | lists:foreach(fun ({From, Result}) ->
78 | gen_statem:reply(From, Result)
79 | end,
80 | lists:zip(Froms, Results)),
81 | St#{cb_st := NewCbSt, acc_left := Size, acc := []}.
82 |
83 | flush(From, Data,
84 | #{cb_st := CbSt, handler := Handler} = St) ->
85 | {Result, NewCbSt} = Handler(CbSt, Data),
86 | gen_statem:reply(From, Result),
87 | St#{cb_st := NewCbSt}.
88 |
89 |
90 |
--------------------------------------------------------------------------------
/src/emqx_backend_mysql_cli.erl:
--------------------------------------------------------------------------------
1 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
2 | "mqx_backend_mysql_cli.erl",
3 | 1).
4 |
5 | -module(emqx_backend_mysql_cli).
6 |
7 | -file("/emqx_rel/_checkouts/emqx/include/emqx.hrl", 1).
8 |
9 | -record(subscription, {topic, subid, subopts}).
10 |
11 | -record(message,
12 | {id :: binary(), qos = 0, from :: atom() | binary(),
13 | flags :: #{atom() => boolean()}, headers :: map(),
14 | topic :: binary(), payload :: binary(),
15 | timestamp :: integer()}).
16 |
17 | -record(delivery,
18 | {sender :: pid(), message :: #message{}}).
19 |
20 | -record(route,
21 | {topic :: binary(),
22 | dest :: node() | {binary(), node()}}).
23 |
24 | -type trie_node_id() :: binary() | atom().
25 |
26 | -record(trie_node,
27 | {node_id :: trie_node_id(),
28 | edge_count = 0 :: non_neg_integer(),
29 | topic :: binary() | undefined, flags :: [atom()]}).
30 |
31 | -record(trie_edge,
32 | {node_id :: trie_node_id(),
33 | word :: binary() | atom()}).
34 |
35 | -record(trie,
36 | {edge :: #trie_edge{}, node_id :: trie_node_id()}).
37 |
38 | -record(alarm,
39 | {id :: binary(),
40 | severity :: notice | warning | error | critical,
41 | title :: iolist(), summary :: iolist(),
42 | timestamp :: erlang:timestamp()}).
43 |
44 | -record(plugin,
45 | {name :: atom(), dir :: string(), descr :: string(),
46 | vendor :: string(), active = false :: boolean(),
47 | info :: map(), type :: atom()}).
48 |
49 | -record(command,
50 | {name :: atom(), action :: atom(),
51 | args = [] :: list(), opts = [] :: list(),
52 | usage :: string(), descr :: string()}).
53 |
54 | -record(banned,
55 | {who ::
56 | {clientid, binary()} | {username, binary()} |
57 | {ip_address, inet:ip_address()},
58 | by :: binary(), reason :: binary(), at :: integer(),
59 | until :: integer()}).
60 |
61 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
62 | "mqx_backend_mysql_cli.erl",
63 | 10).
64 |
65 | -export([client_connected/2, subscribe_lookup/2,
66 | client_disconnected/2, message_fetch/3, lookup_retain/2,
67 | message_publish/2, message_store/2, message_retain/2,
68 | delete_retain/2, message_acked/2, run_mysql_sql/3]).
69 |
70 | -export([record_to_msg/1]).
71 |
72 | -export([connect/1, send_batch/2]).
73 |
74 | client_connected(Pool, Msg) ->
75 | mysql_execute(Pool, client_connected_query,
76 | [proplists:get_value(clientid, Msg), 1,
77 | atom_to_list(node()), 1, atom_to_list(node())]),
78 | ok.
79 |
80 | subscribe_lookup(Pool, Msg) ->
81 | case mysql_execute(Pool, subscribe_lookup_query,
82 | feed_var([<<"${clientid}">>], Msg))
83 | of
84 | {ok, _Cols, []} -> [];
85 | {error, Error} ->
86 | logger:error("Lookup retain error: ~p", [Error]), [];
87 | {ok, _Cols, Rows} ->
88 | [{Topic, #{qos => Qos}} || [Topic, Qos] <- Rows]
89 | end.
90 |
91 | client_disconnected(Pool, Msg) ->
92 | mysql_execute(Pool, client_disconnected_query,
93 | [0, proplists:get_value(clientid, Msg)]),
94 | ok.
95 |
96 | message_fetch(Pool, Msg, Opts) ->
97 | Topic = proplists:get_value(topic, Msg),
98 | ClientId = proplists:get_value(clientid, Msg),
99 | case mysql_execute(Pool, message_fetch_query,
100 | feed_var([<<"${topic}">>, <<"${clientid}">>], Msg))
101 | of
102 | {ok, [_], []} ->
103 | MsgId = case mysql_execute(Pool, message_lastid_query,
104 | [Topic])
105 | of
106 | {ok, [_], []} -> 0;
107 | {ok, [_], [[MId]]} -> MId;
108 | {error, Error2} ->
109 | logger:error("Lookup msg error: ~p", [Error2]), 0
110 | end,
111 | mysql_execute(Pool, insert_acked_query,
112 | [ClientId, Topic, MsgId]),
113 | [];
114 | {ok, [_], [[AckId]]} ->
115 | Sql = <<"select id, topic, msgid, sender, qos, "
116 | "payload, retain, UNIX_TIMESTAMP(arrived) "
117 | "as arrived from mqtt_msg where id > "
118 | "? and topic = ? ">>,
119 | {Sql1, Params1} = case proplists:get_value(time_range,
120 | Opts)
121 | of
122 | undefined ->
123 | {<<" order by id DESC ">>, [AckId, Topic]};
124 | TimeRange ->
125 | Time = erlang:system_time(seconds) -
126 | TimeRange,
127 | {<<" and arrived >= FROM_UNIXTIME(?) order "
128 | "by id DESC ">>,
129 | [AckId, Topic, Time]}
130 | end,
131 | {Sql2, Params2} = case
132 | proplists:get_value(max_returned_count, Opts)
133 | of
134 | undefined -> {<<"">>, Params1};
135 | MaxReturnedCount ->
136 | {<<" limit ?">>,
137 | Params1 ++ [MaxReturnedCount]}
138 | end,
139 | case mysql_query(Pool,
140 | <>, Params2)
141 | of
142 | {ok, _Cols, []} -> [];
143 | {ok, Cols, Rows} ->
144 | [begin
145 | M = record_to_msg(lists:zip(Cols, Row)),
146 | M#message{id = emqx_guid:from_base62(M#message.id)}
147 | end
148 | || Row <- lists:reverse(Rows)];
149 | {error, Error1} ->
150 | logger:error("Lookup msg error: ~p", [Error1]), []
151 | end;
152 | {error, Error} ->
153 | logger:error("Lookup msg error: ~p", [Error]), []
154 | end.
155 |
156 | lookup_retain(Pool, Msg0) ->
157 | case mysql_execute(Pool, lookup_retain_query,
158 | [proplists:get_value(topic, Msg0)])
159 | of
160 | {ok, Cols, [Row | _]} ->
161 | M = record_to_msg(lists:zip(Cols, Row)),
162 | [M#message{id = emqx_guid:from_base62(M#message.id)}];
163 | {ok, _Cols, []} -> [];
164 | {error, Error} ->
165 | logger:error("Lookup retain error: ~p", [Error]), []
166 | end.
167 |
168 | message_publish(Pool, Msg) ->
169 | ParamsKey = [<<"${msgid}">>, <<"${clientid}">>,
170 | <<"${topic}">>, <<"${qos}">>, <<"${retain}">>,
171 | <<"${payload}">>, <<"${timestamp}">>],
172 | case mysql_insert(Pool, message_publish_query,
173 | feed_var(ParamsKey, Msg))
174 | of
175 | {ok, Id} -> emqx_message:set_header(mysql_id, Id, Msg);
176 | {error, Error} ->
177 | logger:error("Failed to store message: ~p", [Error]),
178 | Msg
179 | end.
180 |
181 | message_store(Pool, Msg) ->
182 | case mysql_insert_batch(Pool, Msg) of
183 | {error, Error} ->
184 | logger:error("Failed to store message: ~p", [Error]),
185 | Msg;
186 | _ -> Msg
187 | end.
188 |
189 | message_retain(Pool, Msg) ->
190 | ParamsKey = [<<"${topic}">>, <<"${msgid}">>,
191 | <<"${clientid}">>, <<"${qos}">>, <<"${payload}">>,
192 | <<"${timestamp}">>, <<"${msgid}">>, <<"${clientid}">>,
193 | <<"${qos}">>, <<"${payload}">>, <<"${timestamp}">>],
194 | case mysql_execute(Pool, message_retain_query,
195 | feed_var(ParamsKey, Msg))
196 | of
197 | ok -> Msg;
198 | {error, Error} ->
199 | logger:error("Failed to retain message: ~p", [Error]),
200 | Msg
201 | end.
202 |
203 | delete_retain(Pool, Msg) ->
204 | case mysql_execute(Pool, delete_retain_query,
205 | feed_var([<<"${topic}">>], Msg))
206 | of
207 | ok -> Msg;
208 | {error, Error} ->
209 | logger:error("Failed to delete retain: ~p", [Error]),
210 | Msg
211 | end.
212 |
213 | message_acked(Pool, Msg) ->
214 | ParamsKey = [<<"${clientid}">>, <<"${topic}">>,
215 | <<"${mysql_id}">>, <<"${mysql_id}">>],
216 | case mysql_execute(Pool, message_acked_query,
217 | feed_var(ParamsKey, Msg))
218 | of
219 | ok -> ok;
220 | {error, Error} ->
221 | logger:error("Failed to ack message: ~p", [Error])
222 | end.
223 |
224 | run_mysql_sql(Pool, Msg, SqlList) ->
225 | lists:foreach(fun ({Sql, ParamsKey}) ->
226 | case mysql_query(Pool, Sql, feed_var(ParamsKey, Msg))
227 | of
228 | ok -> ok;
229 | {error, Error} ->
230 | logger:error("Sql:~p~n, params:~p, error: ~p",
231 | [Sql, ParamsKey, Error])
232 | end
233 | end,
234 | SqlList),
235 | Msg.
236 |
237 | feed_var(Params, Msg) -> feed_var(Params, Msg, []).
238 |
239 | feed_var([], _Msg, Acc) -> lists:reverse(Acc);
240 | feed_var([<<"${topic}">> | Params],
241 | Msg = #message{topic = Topic}, Acc) ->
242 | feed_var(Params, Msg, [Topic | Acc]);
243 | feed_var([<<"${topic}">> | Params], Vals, Acc)
244 | when is_list(Vals) ->
245 | feed_var(Params, Vals,
246 | [proplists:get_value(topic, Vals, null) | Acc]);
247 | feed_var([<<"${msgid}">> | Params],
248 | Msg = #message{id = MsgId}, Acc) ->
249 | feed_var(Params, Msg,
250 | [emqx_guid:to_base62(MsgId) | Acc]);
251 | feed_var([<<"${msgid}">> | Params], Vals, Acc)
252 | when is_list(Vals) ->
253 | feed_var(Params, Vals,
254 | [emqx_guid:to_base62(proplists:get_value(msgid, Vals,
255 | null))
256 | | Acc]);
257 | feed_var([<<"${mysql_id}">> | Params], Vals, Acc)
258 | when is_list(Vals) ->
259 | feed_var(Params, Vals,
260 | [proplists:get_value(mysql_id, Vals, null) | Acc]);
261 | feed_var([<<"${clientid}">> | Params],
262 | Msg = #message{from = ClientId}, Acc)
263 | when is_atom(ClientId) ->
264 | feed_var(Params, Msg,
265 | [atom_to_binary(ClientId, utf8) | Acc]);
266 | feed_var([<<"${clientid}">> | Params],
267 | Msg = #message{from = ClientId}, Acc)
268 | when is_binary(ClientId) ->
269 | feed_var(Params, Msg, [ClientId | Acc]);
270 | feed_var([<<"${clientid}">> | Params], Vals, Acc)
271 | when is_list(Vals) ->
272 | feed_var(Params, Vals,
273 | [proplists:get_value(clientid, Vals, null) | Acc]);
274 | feed_var([<<"${qos}">> | Params],
275 | Msg = #message{qos = Qos}, Acc) ->
276 | feed_var(Params, Msg, [Qos | Acc]);
277 | feed_var([<<"${qos}">> | Params], Vals, Acc)
278 | when is_list(Vals) ->
279 | feed_var(Params, Vals,
280 | [proplists:get_value(qos, Vals, null) | Acc]);
281 | feed_var([<<"${retain}">> | Params],
282 | Msg = #message{flags = #{retain := Retain}}, Acc) ->
283 | feed_var(Params, Msg, [i(Retain) | Acc]);
284 | feed_var([<<"${payload}">> | Params],
285 | Msg = #message{payload = Payload}, Acc) ->
286 | feed_var(Params, Msg, [Payload | Acc]);
287 | feed_var([<<"${timestamp}">> | Params],
288 | Msg = #message{timestamp = Ts}, Acc) ->
289 | feed_var(Params, Msg, [round(Ts / 1000) | Acc]);
290 | feed_var([<<"${timestamp_str}">> | Params],
291 | Msg = #message{timestamp = Ts}, Acc) ->
292 | {{Y, M, D}, {H, Mm, S}} =
293 | calendar:system_time_to_local_time(Ts, millisecond),
294 | TsStr =
295 | iolist_to_binary(io_lib:format("~w-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w",
296 | [Y, M, D, H, Mm, S])),
297 | feed_var(Params, Msg, [TsStr | Acc]);
298 | feed_var([_ | Params], Msg, Acc) ->
299 | feed_var(Params, Msg, [null | Acc]).
300 |
301 | i(true) -> 1;
302 | i(false) -> 0.
303 |
304 | b(0) -> false;
305 | b(1) -> true.
306 |
307 | record_to_msg(Record) ->
308 | record_to_msg(Record, #message{headers = #{}}).
309 |
310 | record_to_msg([], Msg) -> Msg;
311 | record_to_msg([{<<"id">>, Id} | Record], Msg) ->
312 | record_to_msg(Record,
313 | emqx_message:set_header(mysql_id, Id, Msg));
314 | record_to_msg([{<<"msgid">>, MsgId} | Record], Msg) ->
315 | record_to_msg(Record, Msg#message{id = MsgId});
316 | record_to_msg([{<<"topic">>, Topic} | Record], Msg) ->
317 | record_to_msg(Record, Msg#message{topic = Topic});
318 | record_to_msg([{<<"sender">>, Sender} | Record], Msg) ->
319 | record_to_msg(Record, Msg#message{from = Sender});
320 | record_to_msg([{<<"qos">>, Qos} | Record], Msg) ->
321 | record_to_msg(Record, Msg#message{qos = Qos});
322 | record_to_msg([{<<"retain">>, R} | Record], Msg) ->
323 | record_to_msg(Record,
324 | Msg#message{flags = #{retain => b(R)}});
325 | record_to_msg([{<<"payload">>, Payload} | Record],
326 | Msg) ->
327 | record_to_msg(Record, Msg#message{payload = Payload});
328 | record_to_msg([{<<"arrived">>, Arrived} | Record],
329 | Msg) ->
330 | record_to_msg(Record, Msg#message{timestamp = Arrived});
331 | record_to_msg([_ | Record], Msg) ->
332 | record_to_msg(Record, Msg).
333 |
334 | connect(Options) ->
335 | Prepares = [{client_connected_query,
336 | <<"insert into mqtt_client(clientid, state, "
337 | "node, online_at, offline_at) values(?, "
338 | "?, ?, now(), null) on duplicate key "
339 | "update state = ?, node = ?, online_at "
340 | "= now(), offline_at = null">>},
341 | {subscribe_lookup_query,
342 | <<"select topic, qos from mqtt_sub where "
343 | "clientid = ?">>},
344 | {client_disconnected_query,
345 | <<"update mqtt_client set state = ?, offline_at "
346 | "= now() where clientid = ?">>},
347 | {message_fetch_query,
348 | <<"select mid from mqtt_acked where topic "
349 | "= ? and clientid = ?">>},
350 | {message_lastid_query,
351 | <<"select id from mqtt_msg where topic "
352 | "= ? order by id DESC limit 1">>},
353 | {insert_acked_query,
354 | <<"insert into mqtt_acked(clientid, topic, "
355 | "mid) values(?, ?, ?)">>},
356 | {lookup_retain_query,
357 | <<"select topic, msgid, sender, qos, payload, "
358 | "UNIX_TIMESTAMP(arrived) as arrived from "
359 | "mqtt_retain where topic = ?">>},
360 | {message_publish_query,
361 | <<"insert into mqtt_msg(msgid, sender, "
362 | "topic, qos, retain, payload, arrived) "
363 | "values (?, ?, ?, ?, ?, ?, FROM_UNIXTIME(?))">>},
364 | {message_retain_query,
365 | <<"insert into mqtt_retain(topic, msgid, "
366 | "sender, qos, payload, arrived) values "
367 | "(?, ?, ?, ?, ?, FROM_UNIXTIME(?))on "
368 | "duplicate key update msgid = ?, sender "
369 | "= ?, qos = ?, payload = ?, arrived = "
370 | "FROM_UNIXTIME(?)">>},
371 | {delete_retain_query,
372 | <<"delete from mqtt_retain where topic "
373 | "= ?">>},
374 | {message_acked_query,
375 | <<"insert into mqtt_acked(clientid, topic, "
376 | "mid) values(?, ?, ?) on duplicate key "
377 | "update mid = ?">>}],
378 | InitFun = fun () ->
379 | {ok, Conn} = mysql:start_link([{prepare, Prepares}
380 | | Options]),
381 | put(backslash_escapes_enabled,
382 | gen_server:call(Conn, backslash_escapes_enabled)),
383 | {ok, Conn}
384 | end,
385 | Handler = fun (Conn, Batch) ->
386 | {emqx_backend_mysql_cli:send_batch(Conn, Batch), Conn}
387 | end,
388 | emqx_backend_mysql_batcher:start_link(InitFun, Handler,
389 | #{}).
390 |
391 | mysql_insert(Pool, Ref, Params) ->
392 | logger:debug("Pool:~p, Statement: ~p, Params:~p",
393 | [Pool, Ref, Params]),
394 | ecpool:with_client(Pool,
395 | fun (C) ->
396 | emqx_backend_mysql_batcher:call(C,
397 | {insert_returning_id,
398 | Ref, Params},
399 | flush)
400 | end).
401 |
402 | mysql_query(Pool, Sql, Params) ->
403 | logger:debug("Pool:~p, SQL: ~p, Params:~p",
404 | [Pool, Sql, Params]),
405 | ecpool:with_client(Pool,
406 | fun (C) ->
407 | emqx_backend_mysql_batcher:call(C,
408 | {query, Sql,
409 | Params},
410 | flush)
411 | end).
412 |
413 | mysql_execute(Pool, Ref, Params) ->
414 | logger:debug("Pool:~p, Statement: ~p, Params:~p",
415 | [Pool, Ref, Params]),
416 | ecpool:with_client(Pool,
417 | fun (C) ->
418 | emqx_backend_mysql_batcher:call(C,
419 | {execute, Ref,
420 | Params},
421 | flush)
422 | end).
423 |
424 | mysql_insert_batch(Pool, Msg) ->
425 | logger:debug("Pool:~p, batch insert Msg: ~p",
426 | [Pool, Msg]),
427 | ecpool:with_client(Pool,
428 | fun (C) -> emqx_backend_mysql_batcher:call(C, Msg) end).
429 |
430 | send_batch(Conn, {What, Sql, Params}) ->
431 | send_one(What, Conn, Sql, Params);
432 | send_batch(Conn, Batch) -> send_inserts(Conn, Batch).
433 |
434 | send_one(insert_returning_id, Conn, Sql, Params) ->
435 | case mysql:execute(Conn, Sql, Params) of
436 | ok -> {ok, mysql:insert_id(Conn)};
437 | {error, Error} -> {error, Error}
438 | end;
439 | send_one(query, Conn, Sql, Params) ->
440 | mysql:query(Conn, Sql, Params);
441 | send_one(execute, Conn, Sql, Params) ->
442 | mysql:execute(Conn, Sql, Params).
443 |
444 | send_inserts(_Conn, []) -> [];
445 | send_inserts(Conn, Msgs) ->
446 | SQL = insert_msgs_sql(Msgs),
447 | mysql:query(Conn, SQL),
448 | lists:map(fun (_) -> ok end, Msgs).
449 |
450 | insert_msgs_sql(Msgs) ->
451 | ParamsKey = [<<"${msgid}">>, <<"${clientid}">>,
452 | <<"${topic}">>, <<"${qos}">>, <<"${retain}">>,
453 | <<"${payload}">>, <<"${timestamp_str}">>],
454 | Values = lists:map(fun (Msg) ->
455 | lists:map(fun quote/1, feed_var(ParamsKey, Msg))
456 | end,
457 | Msgs),
458 | ["insert into mqtt_msg(msgid, sender, "
459 | "topic, qos, retain, payload, arrived) ",
460 | "values ", values(Values)].
461 |
462 | values([]) -> [];
463 | values([Columns | Rest]) ->
464 | ["(", infix(Columns, ", "), ")",
465 | case Rest of
466 | [] -> "";
467 | _ -> ", "
468 | end
469 | | values(Rest)].
470 |
471 | infix([X], _) -> [X];
472 | infix([H | T], In) -> [H, In | infix(T, In)].
473 |
474 | quote(V) when is_integer(V) -> integer_to_list(V);
475 | quote(V) when is_atom(V) -> atom_to_binary(V, utf8);
476 | quote(V) when is_list(V) ->
477 | quote(unicode:characters_to_binary(V));
478 | quote(V) when is_binary(V) -> quote_str(V).
479 |
480 | quote_str(Bin0) when is_binary(Bin0) ->
481 | Bin = case get(backslash_escapes_enabled) of
482 | true ->
483 | binary:replace(Bin0, <<"\\">>, <<"\\\\">>, [global]);
484 | false -> Bin0
485 | end,
486 | Escaped = binary:replace(Bin, <<"'">>, <<"''">>,
487 | [global]),
488 | [$', Escaped, $'].
489 |
490 |
491 |
--------------------------------------------------------------------------------
/src/emqx_backend_mysql_sup.erl:
--------------------------------------------------------------------------------
1 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
2 | "mqx_backend_mysql_sup.erl",
3 | 1).
4 |
5 | -module(emqx_backend_mysql_sup).
6 |
7 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/inclu"
8 | "de/emqx_backend_mysql.hrl",
9 | 1).
10 |
11 | -file("/emqx_rel/_checkouts/emqx_backend_mysql/src/e"
12 | "mqx_backend_mysql_sup.erl",
13 | 10).
14 |
15 | -behaviour(supervisor).
16 |
17 | -export([start_link/1]).
18 |
19 | -export([init/1]).
20 |
21 | start_link(Pools) ->
22 | supervisor:start_link({local, emqx_backend_mysql_sup},
23 | emqx_backend_mysql_sup, [Pools]).
24 |
25 | init([Pools]) ->
26 | {ok,
27 | {{one_for_one, 10, 100},
28 | [pool_spec(Pool, Env) || {Pool, Env} <- Pools]}}.
29 |
30 | pool_spec(Pool, Env) ->
31 | ecpool:pool_spec({emqx_backend_mysql, Pool},
32 | emqx_backend_mysql:pool_name(Pool),
33 | emqx_backend_mysql_cli, Env).
34 |
35 |
36 |
--------------------------------------------------------------------------------
/test/emqx_backend_mysql_SUITE.erl:
--------------------------------------------------------------------------------
1 | %%--------------------------------------------------------------------
2 | %% Copyright (c) 2020 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 |
17 | -module(emqx_backend_mysql_SUITE).
18 |
19 | -compile(export_all).
20 |
21 | all() -> [].
22 |
23 | groups() -> [].
24 |
--------------------------------------------------------------------------------