Merge branch 'master' into EMQX-871-872

This commit is contained in:
x1001100011 2021-07-31 08:26:06 -07:00
commit 25f03ed87a
636 changed files with 34879 additions and 33996 deletions

View File

@ -39,7 +39,8 @@ emqx_test(){
unzip -q "${PACKAGE_PATH}/${packagename}"
export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 \
EMQX_MQTT__MAX_TOPIC_ALIAS=10
sed -i '/emqx_telemetry/d' "${PACKAGE_PATH}"/emqx/data/loaded_plugins
[[ $(arch) == *arm* || $(arch) == aarch64 ]] && export EMQX_ZONES__DEFAULT__LISTENERS__MQTT_QUIC__ENABLED=false
# sed -i '/emqx_telemetry/d' "${PACKAGE_PATH}"/emqx/data/loaded_plugins
echo "running ${packagename} start"
if ! "${PACKAGE_PATH}"/emqx/bin/emqx start; then
@ -48,7 +49,7 @@ emqx_test(){
exit 1
fi
IDLE_TIME=0
while ! curl http://localhost:8081/status >/dev/null 2>&1; do
while ! curl http://localhost:8081/api/v5/status >/dev/null 2>&1; do
if [ $IDLE_TIME -gt 10 ]
then
echo "emqx running error"
@ -113,17 +114,31 @@ emqx_test(){
}
running_test(){
export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 \
EMQX_MQTT__MAX_TOPIC_ALIAS=10
sed -i '/emqx_telemetry/d' /var/lib/emqx/loaded_plugins
# sed -i '/emqx_telemetry/d' /var/lib/emqx/loaded_plugins
emqx_env_vars=$(dirname "$(readlink "$(command -v emqx)")")/../releases/emqx_vars
if ! emqx start; then
if [ -f "$emqx_env_vars" ];
then
tee -a "$emqx_env_vars" <<EOF
export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60
export EMQX_MQTT__MAX_TOPIC_ALIAS=10
EOF
## for ARM, due to CI env issue, skip start of quic listener for the moment
[[ $(arch) == *arm* || $(arch) == aarch64 ]] && tee -a "$emqx_env_vars" <<EOF
export EMQX_ZONES__DEFAULT__LISTENERS__MQTT_QUIC__ENABLED=false
EOF
else
echo "Error: cannot locate emqx_vars"
exit 1
fi
if ! su - emqx -c "emqx start"; then
cat /var/log/emqx/erlang.log.1 || true
cat /var/log/emqx/emqx.log.1 || true
exit 1
fi
IDLE_TIME=0
while ! curl http://localhost:8081/status >/dev/null 2>&1; do
while ! curl http://localhost:8081/api/v5/status >/dev/null 2>&1; do
if [ $IDLE_TIME -gt 10 ]
then
echo "emqx running error"
@ -138,14 +153,13 @@ running_test(){
if [ "$(sed -n '/^ID=/p' /etc/os-release | sed -r 's/ID=(.*)/\1/g' | sed 's/"//g')" = ubuntu ] \
|| [ "$(sed -n '/^ID=/p' /etc/os-release | sed -r 's/ID=(.*)/\1/g' | sed 's/"//g')" = debian ] ;then
if ! service emqx start; then
cat /var/log/emqx/erlang.log.1 || true
cat /var/log/emqx/emqx.log.1 || true
exit 1
fi
IDLE_TIME=0
while ! curl http://localhost:8081/status >/dev/null 2>&1; do
while ! curl http://localhost:8081/api/v5/status >/dev/null 2>&1; do
if [ $IDLE_TIME -gt 10 ]
then
echo "emqx service error"

View File

@ -1,7 +1,8 @@
EMQX_NAME=emqx
EMQX_CLUSTER__DISCOVERY=static
EMQX_CLUSTER__STATIC__SEEDS="emqx@node1.emqx.io, emqx@node2.emqx.io"
EMQX_LISTENER__TCP__EXTERNAL__PROXY_PROTOCOL=on
EMQX_LISTENER__WS__EXTERNAL__PROXY_PROTOCOL=on
EMQX_LOG__LEVEL=debug
EMQX_LOADED_PLUGINS=emqx_sn
EMQX_CLUSTER__DISCOVERY_STRATEGY=static
EMQX_CLUSTER__STATIC__SEEDS="[emqx@node1.emqx.io, emqx@node2.emqx.io]"
EMQX_ZONES__DEFAULT__LISTENERS__MQTT_TCP__PROXY_PROTOCOL=true
EMQX_ZONES__DEFAULT__LISTENERS__MQTT_WS__PROXY_PROTOCOL=true
EMQX_LOG__CONSOLE_HANDLER__ENABLE=true
EMQX_LOG__CONSOLE_HANDLER__LEVEL=debug
EMQX_LOG__PRIMARY_LEVEL=debug

View File

@ -10,5 +10,4 @@ EMQX_AUTH__PGSQL__PASSWORD=public
EMQX_AUTH__PGSQL__DATABASE=mqtt
EMQX_AUTH__REDIS__SERVER=redis_server:6379
EMQX_AUTH__REDIS__PASSWORD=public
CUTTLEFISH_ENV_OVERRIDE_PREFIX=EMQX_
HOCON_ENV_OVERRIDE_PREFIX=EMQX_

View File

@ -33,13 +33,6 @@ services:
- conf.cluster.env
environment:
- "EMQX_HOST=node1.emqx.io"
command:
- /bin/sh
- -c
- |
sed -i "s 127.0.0.1 $$(ip route show |grep "link" |awk '{print $$1}') g" /opt/emqx/etc/acl.conf
sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins
/opt/emqx/bin/emqx foreground
healthcheck:
test: ["CMD", "/opt/emqx/bin/emqx_ctl", "status"]
interval: 5s
@ -57,13 +50,6 @@ services:
- conf.cluster.env
environment:
- "EMQX_HOST=node2.emqx.io"
command:
- /bin/sh
- -c
- |
sed -i "s 127.0.0.1 $$(ip route show |grep "link" |awk '{print $$1}') g" /opt/emqx/etc/acl.conf
sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins
/opt/emqx/bin/emqx foreground
healthcheck:
test: ["CMD", "/opt/emqx/bin/emqx", "ping"]
interval: 5s

View File

@ -0,0 +1,81 @@
version: "3"
services:
mongo1:
hostname: mongo1
container_name: mongo1
image: mongo:${MONGO_TAG}
environment:
MONGO_INITDB_DATABASE: mqtt
networks:
- emqx_bridge
expose:
- 27017
ports:
- 27011:27017
restart: always
command:
--ipv6
--bind_ip_all
--replSet rs0
mongo2:
hostname: mongo2
container_name: mongo2
image: mongo:${MONGO_TAG}
environment:
MONGO_INITDB_DATABASE: mqtt
networks:
- emqx_bridge
expose:
- 27017
ports:
- 27012:27017
restart: always
command:
--ipv6
--bind_ip_all
--replSet rs0
mongo3:
hostname: mongo3
container_name: mongo3
image: mongo:${MONGO_TAG}
environment:
MONGO_INITDB_DATABASE: mqtt
networks:
- emqx_bridge
expose:
- 27017
ports:
- 27013:27017
restart: always
command:
--ipv6
--bind_ip_all
--replSet rs0
mongo_client:
image: mongo:${MONGO_TAG}
container_name: mongo_client
networks:
- emqx_bridge
depends_on:
- mongo1
- mongo2
- mongo3
command:
- /bin/bash
- -c
- |
while ! mongo --host mongo1 --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do
sleep 1
done
while ! mongo --host mongo2 --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do
sleep 1
done
while ! mongo --host mongo3 --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do
sleep 1
done
mongo --host mongo1 --eval "rs.initiate( { _id : 'rs0', members: [ { _id : 0, host : 'mongo1:27017' }, { _id : 1, host : 'mongo2:27017' }, { _id : 2, host : 'mongo3:27017' } ] })" --quiet
mongo --host mongo1 --eval "rs.status()" --quiet

View File

@ -0,0 +1,98 @@
version: "3"
services:
mongo1:
hostname: mongo1
container_name: mongo1
image: mongo:${MONGO_TAG}
environment:
MONGO_INITDB_DATABASE: mqtt
networks:
- emqx_bridge
expose:
- 27017
ports:
- 27011:27017
restart: always
volumes:
- ../../apps/emqx/etc/certs/cert.pem:/etc/certs/cert.pem
- ../../apps/emqx/etc/certs/key.pem:/etc/certs/key.pem
command:
- /bin/bash
- -c
- |
cat /etc/certs/key.pem /etc/certs/cert.pem > /etc/certs/mongodb.pem
mongod --ipv6 --bind_ip_all --tlsMode requireTLS --tlsCertificateKeyFile /etc/certs/mongodb.pem --replSet rs0
mongo2:
hostname: mongo2
container_name: mongo2
image: mongo:${MONGO_TAG}
environment:
MONGO_INITDB_DATABASE: mqtt
networks:
- emqx_bridge
expose:
- 27017
ports:
- 27012:27017
restart: always
volumes:
- ../../apps/emqx/etc/certs/cert.pem:/etc/certs/cert.pem
- ../../apps/emqx/etc/certs/key.pem:/etc/certs/key.pem
command:
- /bin/bash
- -c
- |
cat /etc/certs/key.pem /etc/certs/cert.pem > /etc/certs/mongodb.pem
mongod --ipv6 --bind_ip_all --tlsMode requireTLS --tlsCertificateKeyFile /etc/certs/mongodb.pem --replSet rs0
mongo3:
hostname: mongo3
container_name: mongo3
image: mongo:${MONGO_TAG}
environment:
MONGO_INITDB_DATABASE: mqtt
networks:
- emqx_bridge
expose:
- 27017
ports:
- 27013:27017
restart: always
volumes:
- ../../apps/emqx/etc/certs/cert.pem:/etc/certs/cert.pem
- ../../apps/emqx/etc/certs/key.pem:/etc/certs/key.pem
command:
- /bin/bash
- -c
- |
cat /etc/certs/key.pem /etc/certs/cert.pem > /etc/certs/mongodb.pem
mongod --ipv6 --bind_ip_all --tlsMode requireTLS --tlsCertificateKeyFile /etc/certs/mongodb.pem --replSet rs0
mongo_client:
image: mongo:${MONGO_TAG}
container_name: mongo_client
networks:
- emqx_bridge
depends_on:
- mongo1
- mongo2
- mongo3
volumes:
- ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/cacert.pem
command:
- /bin/bash
- -c
- |
while ! mongo --host mongo1 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do
sleep 1
done
while ! mongo --host mongo2 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do
sleep 1
done
while ! mongo --host mongo3 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do
sleep 1
done
mongo --host mongo1 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval "rs.initiate( { _id : 'rs0', members: [ { _id : 0, host : 'mongo1:27017' }, { _id : 1, host : 'mongo2:27017' }, { _id : 2, host : 'mongo3:27017' } ] })" --quiet
mongo --host mongo1 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval "rs.status()" --quiet

View File

@ -9,6 +9,8 @@ services:
MONGO_INITDB_DATABASE: mqtt
networks:
- emqx_bridge
ports:
- "27017:27017"
command:
--ipv6
--bind_ip_all

View File

@ -0,0 +1,23 @@
version: '3.9'
services:
mongo_server:
container_name: mongo
image: mongo:${MONGO_TAG}
restart: always
environment:
MONGO_INITDB_DATABASE: mqtt
volumes:
- ../../apps/emqx/etc/certs/cert.pem:/etc/certs/cert.pem
- ../../apps/emqx/etc/certs/key.pem:/etc/certs/key.pem
networks:
- emqx_bridge
ports:
- "27017:27017"
command:
- /bin/bash
- -c
- |
cat /etc/certs/key.pem /etc/certs/cert.pem > /etc/certs/mongodb.pem
mongod --ipv6 --bind_ip_all --sslMode requireSSL --sslPEMKeyFile /etc/certs/mongodb.pem

View File

@ -1,18 +0,0 @@
version: '3.9'
services:
mongo_server:
container_name: mongo
image: mongo:${MONGO_TAG}
restart: always
environment:
MONGO_INITDB_DATABASE: mqtt
volumes:
- ../../apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/mongodb.pem/:/etc/certs/mongodb.pem
networks:
- emqx_bridge
command:
--ipv6
--bind_ip_all
--sslMode requireSSL
--sslPEMKeyFile /etc/certs/mongodb.pem

View File

@ -11,9 +11,11 @@ services:
MYSQL_USER: ssluser
MYSQL_PASSWORD: public
volumes:
- ../../apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/ca.pem:/etc/certs/ca-cert.pem
- ../../apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/server-cert.pem:/etc/certs/server-cert.pem
- ../../apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/server-key.pem:/etc/certs/server-key.pem
- ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca-cert.pem
- ../../apps/emqx/etc/certs/cert.pem:/etc/certs/server-cert.pem
- ../../apps/emqx/etc/certs/key.pem:/etc/certs/server-key.pem
ports:
- "3306:3306"
networks:
- emqx_bridge
command:

View File

@ -5,7 +5,9 @@ services:
container_name: redis
image: redis:${REDIS_TAG}
volumes:
- ../../apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs:/tls
- ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca.crt
- ../../apps/emqx/etc/certs/cert.pem:/etc/certs/redis.crt
- ../../apps/emqx/etc/certs/key.pem:/etc/certs/redis.key
- ./redis/:/data/conf
command: bash -c "/bin/bash /data/conf/redis.sh --node cluster --tls-enabled && tail -f /var/log/redis-server.log"
networks:

View File

@ -5,7 +5,9 @@ services:
container_name: redis
image: redis:${REDIS_TAG}
volumes:
- ../../apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs:/tls
- ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca.crt
- ../../apps/emqx/etc/certs/cert.pem:/etc/certs/redis.crt
- ../../apps/emqx/etc/certs/key.pem:/etc/certs/redis.key
- ./redis/:/data/conf
command: bash -c "/bin/bash /data/conf/redis.sh --node sentinel --tls-enabled && tail -f /var/log/redis-server.log"
networks:

View File

@ -5,15 +5,17 @@ services:
container_name: redis
image: redis:${REDIS_TAG}
volumes:
- ../../apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs:/tls
- ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca.crt
- ../../apps/emqx/etc/certs/cert.pem:/etc/certs/redis.crt
- ../../apps/emqx/etc/certs/key.pem:/etc/certs/redis.key
command:
- redis-server
- "--bind 0.0.0.0 ::"
- --requirepass public
- --tls-port 6380
- --tls-cert-file /tls/redis.crt
- --tls-key-file /tls/redis.key
- --tls-ca-cert-file /tls/ca.crt
- --tls-cert-file /etc/certs/redis.crt
- --tls-key-file /etc/certs/redis.key
- --tls-ca-cert-file /etc/certs/ca.crt
restart: always
networks:
- emqx_bridge

View File

@ -2,9 +2,9 @@ ARG BUILD_FROM=postgres:11
FROM ${BUILD_FROM}
ARG POSTGRES_USER=postgres
COPY --chown=$POSTGRES_USER .ci/docker-compose-file/pgsql/pg_hba.conf /var/lib/postgresql/pg_hba.conf
COPY --chown=$POSTGRES_USER apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/server-key.pem /var/lib/postgresql/server.key
COPY --chown=$POSTGRES_USER apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/server-cert.pem /var/lib/postgresql/server.crt
COPY --chown=$POSTGRES_USER apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/ca.pem /var/lib/postgresql/root.crt
COPY --chown=$POSTGRES_USER apps/emqx/etc/certs/key.pem /var/lib/postgresql/server.key
COPY --chown=$POSTGRES_USER apps/emqx/etc/certs/cert.pem /var/lib/postgresql/server.crt
COPY --chown=$POSTGRES_USER apps/emqx/etc/certs/cacert.pem /var/lib/postgresql/root.crt
RUN chmod 600 /var/lib/postgresql/pg_hba.conf
RUN chmod 600 /var/lib/postgresql/server.key
RUN chmod 600 /var/lib/postgresql/server.crt

View File

@ -1,9 +1,9 @@
daemonize yes
bind 0.0.0.0 ::
logfile /var/log/redis-server.log
tls-cert-file /tls/redis.crt
tls-key-file /tls/redis.key
tls-ca-cert-file /tls/ca.crt
tls-cert-file /etc/certs/redis.crt
tls-key-file /etc/certs/redis.key
tls-ca-cert-file /etc/certs/ca.crt
tls-replication yes
tls-cluster yes
protected-mode no

View File

@ -91,7 +91,7 @@ do
fi
if [ "${node}" = "cluster" ] ; then
if $tls ; then
yes "yes" | redis-cli --cluster create "$LOCAL_IP:8000" "$LOCAL_IP:8001" "$LOCAL_IP:8002" --pass public --no-auth-warning --tls true --cacert /tls/ca.crt --cert /tls/redis.crt --key /tls/redis.key;
yes "yes" | redis-cli --cluster create "$LOCAL_IP:8000" "$LOCAL_IP:8001" "$LOCAL_IP:8002" --pass public --no-auth-warning --tls true --cacert /etc/certs/ca.crt --cert /etc/certs/redis.crt --key /etc/certs/redis.key;
else
yes "yes" | redis-cli --cluster create "$LOCAL_IP:7000" "$LOCAL_IP:7001" "$LOCAL_IP:7002" --pass public --no-auth-warning;
fi
@ -107,9 +107,9 @@ EOF
cat >>/_sentinel.conf<<EOF
tls-port 26380
tls-replication yes
tls-cert-file /tls/redis.crt
tls-key-file /tls/redis.key
tls-ca-cert-file /tls/ca.crt
tls-cert-file /etc/certs/redis.crt
tls-key-file /etc/certs/redis.key
tls-ca-cert-file /etc/certs/ca.crt
sentinel monitor mymaster $LOCAL_IP 8000 1
EOF
else

View File

@ -131,6 +131,13 @@
[shell bench]
???publish complete
??SH-PROMPT:
!sleep 5
?SH-PROMPT
!curl --user admin:public --silent --show-error http://localhost:8081/api/v4/rules | jq --raw-output ".data[0].metrics[] | select(.node==\"emqx@127.0.0.1\").matched"
?300
?SH-PROMPT
!curl http://127.0.0.1:8080/counter
???{"data":300,"code":0}
?SH-PROMPT

View File

@ -3,7 +3,6 @@ name: Bug Report
about: Create a report to help us improve
title: ''
labels: Support
assignees: tigercl
---

View File

@ -3,7 +3,6 @@ name: Feature Request
about: Suggest an idea for this project
title: ''
labels: Feature
assignees: tigercl
---

View File

@ -3,7 +3,6 @@ name: Support Needed
about: Asking a question about usages, docs or anything you're insterested in
title: ''
labels: Support
assignees: tigercl
---

View File

@ -1,15 +0,0 @@
<!-- Please describe the current behavior and link to a relevant issue. -->
Fixes <issue-number>
**If your build fails** due to your commit message not passing the build checks, please review the guidelines here: https://github.com/emqx/emqx/blob/master/CONTRIBUTING.md.
## PR Checklist
Please convert it to a draft if any of the following conditions are not met. Reviewers may skip over until all the items are checked:
- [ ] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)
- [ ] In case of non-backward compatible changes, reviewer should check this item as a write-off, and add details in **Backward Compatibility** section
## Backward Compatibility
## More information

View File

@ -42,6 +42,7 @@ jobs:
if: endsWith(github.repository, 'emqx')
run: |
make -C source deps-all
rm source/rebar.lock
zip -ryq source.zip source/* source/.[^.]*
- name: get_all_deps
if: endsWith(github.repository, 'enterprise')
@ -63,6 +64,7 @@ jobs:
if: endsWith(github.repository, 'emqx')
strategy:
fail-fast: false
matrix:
profile: ${{fromJSON(needs.prepare.outputs.profiles)}}
exclude:
@ -131,6 +133,7 @@ jobs:
needs: prepare
strategy:
fail-fast: false
matrix:
profile: ${{fromJSON(needs.prepare.outputs.profiles)}}
erl_otp:
@ -179,11 +182,11 @@ jobs:
cd source
pkg_name=$(basename _packages/${{ matrix.profile }}/${{ matrix.profile }}-*.zip)
unzip -q _packages/${{ matrix.profile }}/$pkg_name
gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins
# gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins
./emqx/bin/emqx start || cat emqx/log/erlang.log.1
ready='no'
for i in {1..10}; do
if curl -fs 127.0.0.1:18083 > /dev/null; then
if curl -fs 127.0.0.1:8081/api/v5/status > /dev/null; then
ready='yes'
break
fi
@ -210,6 +213,7 @@ jobs:
needs: prepare
strategy:
fail-fast: false
matrix:
profile: ${{fromJSON(needs.prepare.outputs.profiles)}}
arch:
@ -336,19 +340,13 @@ jobs:
needs: prepare
strategy:
fail-fast: false
matrix:
profile: ${{fromJSON(needs.prepare.outputs.profiles)}}
arch:
- [amd64, x86_64]
- [arm64v8, aarch64]
- [arm32v7, arm]
- [i386, i386]
- [s390x, s390x]
exclude:
- profile: emqx-ee
arch: [i386, i386]
- profile: emqx-ee
arch: [s390x, s390x]
steps:
- uses: actions/download-artifact@v2

View File

@ -108,11 +108,11 @@ jobs:
run: |
pkg_name=$(basename _packages/${EMQX_NAME}/emqx-*.zip)
unzip -q _packages/${EMQX_NAME}/$pkg_name
gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins
# gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins
./emqx/bin/emqx start || cat emqx/log/erlang.log.1
ready='no'
for i in {1..10}; do
if curl -fs 127.0.0.1:18083 > /dev/null; then
if curl -fs 127.0.0.1:8081/api/v5/status > /dev/null; then
ready='yes'
break
fi

View File

@ -36,10 +36,9 @@ jobs:
timeout-minutes: 5
run: |
set -e -u -x
echo "CUTTLEFISH_ENV_OVERRIDE_PREFIX=EMQX_" >> .ci/docker-compose-file/conf.cluster.env
echo "HOCON_ENV_OVERRIDE_PREFIX=EMQX_" >> .ci/docker-compose-file/conf.cluster.env
echo "EMQX_ZONE__EXTERNAL__RETRY_INTERVAL=2s" >> .ci/docker-compose-file/conf.cluster.env
echo "EMQX_MQTT__MAX_TOPIC_ALIAS=10" >> .ci/docker-compose-file/conf.cluster.env
echo "EMQX_ZONES__DEFAULT__MQTT__RETRY_INTERVAL=2s" >> .ci/docker-compose-file/conf.cluster.env
echo "EMQX_ZONES__DEFAULT__MQTT__MAX_TOPIC_ALIAS=10" >> .ci/docker-compose-file/conf.cluster.env
docker-compose \
-f .ci/docker-compose-file/docker-compose-emqx-cluster.yaml \
-f .ci/docker-compose-file/docker-compose-python.yaml \
@ -48,13 +47,13 @@ jobs:
echo "['$(date -u +"%Y-%m-%dT%H:%M:%SZ")']:waiting emqx";
sleep 5;
done
- name: verify EMQX_LOADED_PLUGINS override working
run: |
expected="{emqx_sn, true}."
output=$(docker exec -i node1.emqx.io bash -c "cat data/loaded_plugins" | tail -n1)
if [ "$expected" != "$output" ]; then
exit 1
fi
# - name: verify EMQX_LOADED_PLUGINS override working
# run: |
# expected="{emqx_sn, true}."
# output=$(docker exec -i node1.emqx.io bash -c "cat data/loaded_plugins" | tail -n1)
# if [ "$expected" != "$output" ]; then
# exit 1
# fi
- name: make paho tests
run: |
if ! docker exec -i python /scripts/pytest.sh; then
@ -118,8 +117,8 @@ jobs:
--set image.pullPolicy=Never \
--set emqxAclConfig="" \
--set image.pullPolicy=Never \
--set emqxConfig.EMQX_ZONE__EXTERNAL__RETRY_INTERVAL=2s \
--set emqxConfig.EMQX_MQTT__MAX_TOPIC_ALIAS=10 \
--set emqxConfig.EMQX_ZONES__DEFAULT__MQTT__RETRY_INTERVAL=2s \
--set emqxConfig.EMQX_ZONES__DEFAULT__MQTT__MAX_TOPIC_ALIAS=10 \
deploy/charts/emqx \
--debug
@ -131,11 +130,27 @@ jobs:
echo "waiting emqx started";
sleep 10;
done
- name: get pods log
- name: get emqx-0 pods log
if: failure()
env:
KUBECONFIG: "/etc/rancher/k3s/k3s.yaml"
run: kubectl describe pods emqx-0
run: |
kubectl describe pods emqx-0
kubectl logs emqx-0
- name: get emqx-1 pods log
if: failure()
env:
KUBECONFIG: "/etc/rancher/k3s/k3s.yaml"
run: |
kubectl describe pods emqx-1
kubectl logs emqx-1
- name: get emqx-2 pods log
if: failure()
env:
KUBECONFIG: "/etc/rancher/k3s/k3s.yaml"
run: |
kubectl describe pods emqx-2
kubectl logs emqx-2
- uses: actions/checkout@v2
with:
repository: emqx/paho.mqtt.testing

4
.gitignore vendored
View File

@ -45,6 +45,8 @@ emqx_dialyzer_*_plt
*/emqx_dashboard/priv/www
dist.zip
scripts/git-token
etc/*.seg
apps/*/etc/*.all
_upgrade_base/
TAGS
erlang_ls.config
.els_cache/

View File

@ -1,5 +1,5 @@
$(shell $(CURDIR)/scripts/git-hooks-init.sh)
REBAR_VERSION = 3.14.3-emqx-8
REBAR_VERSION = 3.16.1-emqx-1
REBAR = $(CURDIR)/rebar3
BUILD = $(CURDIR)/build
SCRIPTS = $(CURDIR)/scripts
@ -73,7 +73,8 @@ coveralls: $(REBAR)
@ENABLE_COVER_COMPILE=1 $(REBAR) as test coveralls send
.PHONY: $(REL_PROFILES)
$(REL_PROFILES:%=%): $(REBAR) get-dashboard
$(REL_PROFILES:%=%): $(REBAR) get-dashboard conf-segs
@$(REBAR) as $(@) do compile,release
## Not calling rebar3 clean because
@ -91,6 +92,7 @@ $(PROFILES:%=clean-%):
.PHONY: clean-all
clean-all:
@rm -f rebar.lock
@rm -rf _build
.PHONY: deps-all
@ -111,7 +113,7 @@ xref: $(REBAR)
dialyzer: $(REBAR)
@$(REBAR) as check dialyzer
COMMON_DEPS := $(REBAR) get-dashboard $(CONF_SEGS)
COMMON_DEPS := $(REBAR) get-dashboard conf-segs
## rel target is to create release package without relup
.PHONY: $(REL_PROFILES:%=%-rel) $(PKG_PROFILES:%=%-rel)
@ -152,3 +154,6 @@ quickrun:
./_build/$(PROFILE)/rel/emqx/bin/emqx console
include docker.mk
conf-segs:
@scripts/merge-config.escript

5
NOTICE Normal file
View File

@ -0,0 +1,5 @@
EMQ X, highly scalable, highly available distributed MQTT messaging platform for IoT.
Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
This product contains code developed at EMQ Technologies Co., Ltd.
Visit https://www.emqx.come to learn more.

View File

@ -9,7 +9,7 @@
[![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow)](https://askemq.com)
[![YouTube](https://img.shields.io/badge/Subscribe-EMQ%20中文-FF0000?logo=youtube)](https://www.youtube.com/channel/UCir_r04HIsLjf2qqyZ4A8Cg)
[![最棒的物联网 MQTT 开源团队期待您的加入](https://www.emqx.io/static/img/github_readme_cn_bg.png)](https://careers.emqx.cn/)
[![最棒的物联网 MQTT 开源团队期待您的加入](https://static.emqx.net/images/github_readme_cn_bg.png)](https://careers.emqx.cn/)
[English](./README.md) | 简体中文 | [日本語](./README-JP.md) | [русский](./README-RU.md)
@ -18,7 +18,7 @@
从 3.0 版本开始,*EMQ X* 完整支持 MQTT V5.0 协议规范,向下兼容 MQTT V3.1 和 V3.1.1,并支持 MQTT-SN、CoAP、LwM2M、WebSocket 和 STOMP 等通信协议。EMQ X 3.0 单集群可支持千万级别的 MQTT 并发连接。
- 新功能的完整列表,请参阅 [EMQ X Release Notes](https://github.com/emqx/emqx/releases)。
- 获取更多信息,请访问 [EMQ X 官网](https://www.emqx.cn/)。
- 获取更多信息,请访问 [EMQ X 官网](https://www.emqx.io/zh)。
## 安装
@ -34,7 +34,7 @@ docker run -d --name emqx -p 1883:1883 -p 8081:8081 -p 8083:8083 -p 8883:8883 -p
#### 二进制软件包安装
需从 [EMQ X 下载](https://www.emqx.cn/downloads) 页面获取相应操作系统的二进制软件包。
需从 [EMQ X 下载](https://www.emqx.com/zh/downloads) 页面获取相应操作系统的二进制软件包。
- [单节点安装文档](https://docs.emqx.cn/broker/latest/getting-started/install.html)
- [集群配置文档](https://docs.emqx.cn/broker/latest/advanced/cluster.html)
@ -133,7 +133,7 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_authz make dialyzer
- [Facebook](https://www.facebook.com/emqxmqtt)
- [Reddit](https://www.reddit.com/r/emqx/)
- [Weibo](https://weibo.com/emqtt)
- [Blog](https://www.emqx.cn/blog)
- [Blog](https://www.emqx.com/zh/blog)
欢迎你将任何 bug、问题和功能请求提交到 [emqx/emqx](https://github.com/emqx/emqx/issues)。
@ -145,7 +145,7 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_authz make dialyzer
[MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html)
[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf)
[MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf)
## 开源许可

View File

@ -8,7 +8,7 @@
[![Twitter](https://img.shields.io/badge/Twitter-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech)
[![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q)
[![The best IoT MQTT open source team looks forward to your joining](https://www.emqx.io/static/img/github_readme_en_bg.png)](https://www.emqx.io/careers)
[![The best IoT MQTT open source team looks forward to your joining](https://static.emqx.net/images/github_readme_en_bg.png)](https://www.emqx.com/en/careers)
[English](./README.md) | [简体中文](./README-CN.md) | 日本語 | [русский](./README-RU.md)
@ -35,7 +35,7 @@ docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8883:8883 -p 8084:8084 -p
#### バイナリパッケージによるインストール
それぞれのOSに対応したバイナリソフトウェアパッケージは、[EMQ Xのダウンロード](https://www.emqx.io/downloads)ページから取得できます。
それぞれのOSに対応したバイナリソフトウェアパッケージは、[EMQ Xのダウンロード](https://www.emqx.com/en/downloads)ページから取得できます。
- [シングルノードインストール](https://docs.emqx.io/broker/latest/en/getting-started/installation.html)
- [マルチノードインストール](https://docs.emqx.io/broker/latest/en/advanced/cluster.html)
@ -125,7 +125,7 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_authz make dialyzer
[MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html)
[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf)
[MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf)
## License

View File

@ -9,7 +9,7 @@
[![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow?logo=github)](https://github.com/emqx/emqx/discussions)
[![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q)
[![The best IoT MQTT open source team looks forward to your joining](https://www.emqx.io/static/img/github_readme_en_bg.png)](https://www.emqx.io/careers)
[![The best IoT MQTT open source team looks forward to your joining](https://static.emqx.net/images/github_readme_en_bg.png)](https://www.emqx.com/en/careers)
[English](./README.md) | [简体中文](./README-CN.md) | [日本語](./README-JP.md) | русский
@ -18,7 +18,7 @@
Начиная с релиза 3.0, брокер *EMQ X* полностью поддерживает протокол MQTT версии 5.0, и обратно совместим с версиями 3.1 и 3.1.1, а также протоколами MQTT-SN, CoAP, LwM2M, WebSocket и STOMP. Начиная с релиза 3.0, брокер *EMQ X* может масштабироваться до более чем 10 миллионов одновременных MQTT соединений на один кластер.
- Полный список возможностей доступен по ссылке: [EMQ X Release Notes](https://github.com/emqx/emqx/releases).
- Более подробная информация доступна на нашем сайте: [EMQ X homepage](https://www.emqx.io).
- Более подробная информация доступна на нашем сайте: [EMQ X homepage](https://www.emqx.io/).
## Установка
@ -34,7 +34,7 @@ docker run -d --name emqx -p 1883:1883 -p 8081:8081 -p 8083:8083 -p 8883:8883 -p
#### Установка бинарного пакета
Сборки для различных операционных систем: [Загрузить EMQ X](https://www.emqx.io/downloads).
Сборки для различных операционных систем: [Загрузить EMQ X](https://www.emqx.com/en/downloads).
- [Установка на одном сервере](https://docs.emqx.io/en/broker/latest/getting-started/install.html)
- [Установка на кластере](https://docs.emqx.io/en/broker/latest/advanced/cluster.html)
@ -135,7 +135,7 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_authz make dialyzer
[MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html)
[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf)
[MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf)
## Лицензия

View File

@ -8,7 +8,7 @@
[![Twitter](https://img.shields.io/badge/Follow-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech)
[![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q)
[![The best IoT MQTT open source team looks forward to your joining](https://www.emqx.io/static/img/github_readme_en_bg.png)](https://www.emqx.io/careers)
[![The best IoT MQTT open source team looks forward to your joining](https://static.emqx.net/images/github_readme_en_bg.png)](https://www.emqx.com/en/careers)
English | [简体中文](./README-CN.md) | [日本語](./README-JP.md) | [русский](./README-RU.md)
@ -17,7 +17,7 @@ English | [简体中文](./README-CN.md) | [日本語](./README-JP.md) | [рус
Starting from 3.0 release, *EMQ X* broker fully supports MQTT V5.0 protocol specifications and backward compatible with MQTT V3.1 and V3.1.1, as well as other communication protocols such as MQTT-SN, CoAP, LwM2M, WebSocket and STOMP. The 3.0 release of the *EMQ X* broker can scaled to 10+ million concurrent MQTT connections on one cluster.
- For full list of new features, please read [EMQ X Release Notes](https://github.com/emqx/emqx/releases).
- For more information, please visit [EMQ X homepage](https://www.emqx.io).
- For more information, please visit [EMQ X homepage](https://www.emqx.io/).
## Installation
@ -33,7 +33,7 @@ docker run -d --name emqx -p 1883:1883 -p 8081:8081 -p 8083:8083 -p 8883:8883 -p
#### Installing via Binary Package
Get the binary package of the corresponding OS from [EMQ X Download](https://www.emqx.io/downloads) page.
Get the binary package of the corresponding OS from [EMQ X Download](https://www.emqx.com/en/downloads) page.
- [Single Node Install](https://docs.emqx.io/en/broker/latest/getting-started/install.html)
- [Multi Node Install](https://docs.emqx.io/en/broker/latest/advanced/cluster.html)
@ -134,7 +134,7 @@ You can read the mqtt protocol via the following links:
[MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html)
[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf)
[MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf)
## License

5
apps/emqx/NOTICE Normal file
View File

@ -0,0 +1,5 @@
EMQ X, highly scalable, highly available distributed MQTT messaging platform for IoT.
Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
This product contains code developed at EMQ Technologies Co., Ltd.
Visit https://www.emqx.come to learn more.

View File

@ -1,26 +0,0 @@
%%--------------------------------------------------------------------
%% [ACL](https://docs.emqx.io/broker/v3/en/config.html)
%%
%% -type(who() :: all | binary() |
%% {ipaddr, esockd_access:cidr()} |
%% {client, binary()} |
%% {user, binary()}).
%%
%% -type(access() :: subscribe | publish | pubsub).
%%
%% -type(topic() :: binary()).
%%
%% -type(rule() :: {allow, all} |
%% {allow, who(), access(), list(topic())} |
%% {deny, all} |
%% {deny, who(), access(), list(topic())}).
%%--------------------------------------------------------------------
{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}.
{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}.
{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.
{allow, all}.

View File

@ -1,14 +0,0 @@
%%--------------------------------------------------------------------
%% For paho interoperability test cases
%%--------------------------------------------------------------------
{deny, {client, "myclientid"}, subscribe, ["test/nosubscribe"]}.
{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}.
{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}.
{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.
{allow, all}.

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,10 @@
-define(Otherwise, true).
-define(COMMON_SHARD, emqx_common_shard).
-define(SHARED_SUB_SHARD, emqx_shared_sub_shard).
-define(MOD_DELAYED_SHARD, emqx_delayed_shard).
%%--------------------------------------------------------------------
%% Banner
%%--------------------------------------------------------------------
@ -31,12 +35,6 @@
-define(ERTS_MINIMUM_REQUIRED, "10.0").
%%--------------------------------------------------------------------
%% Configs
%%--------------------------------------------------------------------
-define(NO_PRIORITY_TABLE, none).
%%--------------------------------------------------------------------
%% Topics' prefix: $SYS | $queue | $share
%%--------------------------------------------------------------------
@ -86,6 +84,9 @@
-define(ROUTE_SHARD, route_shard).
-define(RULE_ENGINE_SHARD, emqx_rule_engine_shard).
-record(route, {
topic :: binary(),
dest :: node() | {binary(), node()}
@ -101,8 +102,7 @@
descr :: string(),
vendor :: string() | undefined,
active = false :: boolean(),
info = #{} :: map(),
type :: atom()
info = #{} :: map()
}).
%%--------------------------------------------------------------------
@ -134,4 +134,3 @@
}).
-endif.

View File

@ -29,7 +29,7 @@
-ifndef(EMQX_ENTERPRISE).
-define(EMQX_RELEASE, {opensource, "5.0-pre"}).
-define(EMQX_RELEASE, {opensource, "5.0-alpha.3"}).
-else.

View File

@ -12,12 +12,11 @@
[ {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}
, {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}
, {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}}
, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.0"}}}
, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.2"}}}
, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}}
, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.4"}}}
, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}}
, {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} %% todo delete when plugins use hocon
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.0"}}}
, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}}
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.10.5"}}}
, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}
, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}}
]}.
@ -30,7 +29,7 @@
[ meck
, {bbmustache,"1.10.0"}
, {emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers", {branch,"hocon"}}}
, {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.3.1"}}}
, {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.2"}}}
]},
{extra_src_dirs, [{"test",[recursive]}]}
]}

View File

@ -1,11 +1,30 @@
IsCentos6 = fun() ->
case file:read_file("/etc/centos-release") of
{ok, <<"CentOS release 6", _/binary >>} ->
true;
_ ->
false
end
end,
IsWin32 = fun() ->
win32 =:= element(1, os:type())
end,
IsQuicSupp = fun() ->
not (IsCentos6() orelse IsWin32() orelse
false =/= os:getenv("BUILD_WITHOUT_QUIC")
)
end,
Bcrypt = {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {branch, "0.6.0"}}},
AddBcrypt = fun(C) ->
Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {branch, "main"}}},
ExtraDeps = fun(C) ->
{deps, Deps0} = lists:keyfind(deps, 1, C),
Deps = [Bcrypt | Deps0],
Deps = Deps0 ++ [Bcrypt || not IsWin32()] ++
[ Quicer || IsQuicSupp()],
lists:keystore(deps, 1, C, {deps, Deps})
end,
case os:type() of
{win32, _} -> CONFIG;
_ -> AddBcrypt(CONFIG)
end.
ExtraDeps(CONFIG).

View File

@ -4,7 +4,7 @@
{vsn, "5.0.0"}, % strict semver, bump manually!
{modules, []},
{registered, []},
{applications, [kernel,stdlib,gproc,gen_rpc,esockd,cowboy,sasl,os_mon]},
{applications, [kernel,stdlib,gproc,gen_rpc,esockd,cowboy,sasl,os_mon,jiffy]},
{mod, {emqx_app,[]}},
{env, []},
{licenses, ["Apache-2.0"]},

View File

@ -1,111 +0,0 @@
%% -*- mode: erlang -*-
{VSN,
[
{"4.3.4",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}
]},
{"4.3.3",
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}
]},
{"4.3.2",
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]}
]},
{"4.3.1",
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_frame,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_congestion,brutal_purge,soft_purge,[]},
{load_module,emqx_node_dump,brutal_purge,soft_purge,[]},
{load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]},
{"4.3.0",
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_logger_jsonfmt,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_congestion,brutal_purge,soft_purge,[]},
{load_module,emqx_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_frame,brutal_purge,soft_purge,[]},
{load_module,emqx_trie,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_node_dump,brutal_purge,soft_purge,[]},
{load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]},
{load_module,emqx_metrics,brutal_purge,soft_purge,[]},
{apply,{emqx_metrics,upgrade_retained_delayed_counter_type,[]}},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]},
{<<".*">>,[]}],
[
{"4.3.4",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}
]},
{"4.3.3",
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}
]},
{"4.3.2",
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]}
]},
{"4.3.1",
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_frame,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_congestion,brutal_purge,soft_purge,[]},
{load_module,emqx_node_dump,brutal_purge,soft_purge,[]},
{load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]},
{"4.3.0",
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_logger_jsonfmt,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_congestion,brutal_purge,soft_purge,[]},
{load_module,emqx_frame,brutal_purge,soft_purge,[]},
{load_module,emqx_trie,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_node_dump,brutal_purge,soft_purge,[]},
{load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]},
{load_module,emqx_metrics,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]},
{<<".*">>,[]}]}.

View File

@ -29,10 +29,6 @@
, stop/0
]).
-export([ get_env/1
, get_env/2
]).
%% PubSub API
-export([ subscribe/1
, subscribe/2
@ -126,15 +122,6 @@ is_running(Node) ->
Pid when is_pid(Pid) -> true
end.
%% @doc Get environment
-spec(get_env(Key :: atom()) -> maybe(term())).
get_env(Key) ->
get_env(Key, undefined).
-spec(get_env(Key :: atom(), Default :: term()) -> term()).
get_env(Key, Default) ->
application:get_env(?APP, Key, Default).
%%--------------------------------------------------------------------
%% PubSub API
%%--------------------------------------------------------------------
@ -227,7 +214,6 @@ shutdown() ->
shutdown(Reason) ->
?LOG(critical, "emqx shutdown for ~s", [Reason]),
_ = emqx_alarm_handler:unload(),
_ = emqx_plugins:unload(),
lists:foreach(fun application:stop/1
, lists:reverse(default_started_applications())
).
@ -235,13 +221,8 @@ shutdown(Reason) ->
reboot() ->
lists:foreach(fun application:start/1 , default_started_applications()).
-ifdef(EMQX_ENTERPRISE).
default_started_applications() ->
[gproc, esockd, ranch, cowboy, ekka, emqx].
-else.
default_started_applications() ->
[gproc, esockd, ranch, cowboy, ekka, emqx, emqx_modules].
-endif.
[gproc, esockd, ranch, cowboy, ekka, quicer, emqx] ++ emqx_feature().
%%--------------------------------------------------------------------
%% Internal functions
@ -253,3 +234,9 @@ reload_config(ConfFile) ->
[application:set_env(App, Par, Val) || {Par, Val} <- Vals]
end, Conf).
-ifndef(EMQX_DEP_APPS).
emqx_feature() -> [].
-else.
emqx_feature() ->
?EMQX_DEP_APPS.
-endif.

View File

@ -18,66 +18,43 @@
-include("emqx.hrl").
-export([authenticate/1]).
-export([ check_acl/3
-export([ authenticate/1
, authorize/3
]).
-type(result() :: #{auth_result := emqx_types:auth_result(),
anonymous := boolean()
}).
%%--------------------------------------------------------------------
%% APIs
%%--------------------------------------------------------------------
-spec(authenticate(emqx_types:clientinfo()) -> {ok, result()} | {error, term()}).
authenticate(ClientInfo = #{zone := Zone}) ->
AuthResult = default_auth_result(Zone),
case emqx_zone:get_env(Zone, bypass_auth_plugins, false) of
true ->
return_auth_result(AuthResult);
false ->
return_auth_result(run_hooks('client.authenticate', [ClientInfo], AuthResult))
-spec(authenticate(emqx_types:clientinfo()) ->
ok | {ok, binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}).
authenticate(Credential) ->
run_hooks('client.authenticate', [Credential], ok).
%% @doc Check Authorization
-spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic())
-> allow | deny.
authorize(ClientInfo = #{zone := Zone}, PubSub, Topic) ->
case emqx_authz_cache:is_enabled(Zone) of
true -> check_authorization_cache(ClientInfo, PubSub, Topic);
false -> do_authorize(ClientInfo, PubSub, Topic)
end.
%% @doc Check ACL
-spec(check_acl(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic())
-> allow | deny).
check_acl(ClientInfo, PubSub, Topic) ->
case emqx_acl_cache:is_enabled() of
true -> check_acl_cache(ClientInfo, PubSub, Topic);
false -> do_check_acl(ClientInfo, PubSub, Topic)
end.
check_acl_cache(ClientInfo, PubSub, Topic) ->
case emqx_acl_cache:get_acl_cache(PubSub, Topic) of
check_authorization_cache(ClientInfo = #{zone := Zone}, PubSub, Topic) ->
case emqx_authz_cache:get_authz_cache(Zone, PubSub, Topic) of
not_found ->
AclResult = do_check_acl(ClientInfo, PubSub, Topic),
emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult),
AclResult;
AclResult -> AclResult
AuthzResult = do_authorize(ClientInfo, PubSub, Topic),
emqx_authz_cache:put_authz_cache(Zone, PubSub, Topic, AuthzResult),
AuthzResult;
AuthzResult -> AuthzResult
end.
do_check_acl(ClientInfo = #{zone := Zone}, PubSub, Topic) ->
Default = emqx_zone:get_env(Zone, acl_nomatch, deny),
case run_hooks('client.check_acl', [ClientInfo, PubSub, Topic], Default) of
do_authorize(ClientInfo, PubSub, Topic) ->
case run_hooks('client.authorize', [ClientInfo, PubSub, Topic], allow) of
allow -> allow;
_Other -> deny
end.
default_auth_result(Zone) ->
case emqx_zone:get_env(Zone, allow_anonymous, false) of
true -> #{auth_result => success, anonymous => true};
false -> #{auth_result => not_authorized, anonymous => false}
end.
-compile({inline, [run_hooks/3]}).
run_hooks(Name, Args, Acc) ->
ok = emqx_metrics:inc(Name), emqx_hooks:run_fold(Name, Args, Acc).
-compile({inline, [return_auth_result/1]}).
return_auth_result(Result = #{auth_result := success}) ->
{ok, Result};
return_auth_result(Result) ->
{error, maps:get(auth_result, Result, unknown_error)}.

View File

@ -17,6 +17,7 @@
-module(emqx_alarm).
-behaviour(gen_server).
-behaviour(emqx_config_handler).
-include("emqx.hrl").
-include("logger.hrl").
@ -29,10 +30,14 @@
-boot_mnesia({mnesia, [boot]}).
-copy_mnesia({mnesia, [copy]}).
-export([ start_link/1
-export([pre_config_update/2]).
-export([ start_link/0
, stop/0
]).
-export([format/1]).
%% API
-export([ activate/1
, activate/2
@ -75,21 +80,16 @@
}).
-record(state, {
actions :: [action()],
size_limit :: non_neg_integer(),
validity_period :: non_neg_integer(),
timer = undefined :: undefined | reference()
timer :: reference()
}).
-type action() :: log | publish | event.
-define(ACTIVATED_ALARM, emqx_activated_alarm).
-define(DEACTIVATED_ALARM, emqx_deactivated_alarm).
-rlog_shard({?COMMON_SHARD, ?ACTIVATED_ALARM}).
-rlog_shard({?COMMON_SHARD, ?DEACTIVATED_ALARM}).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
@ -120,8 +120,8 @@ mnesia(copy) ->
%% API
%%--------------------------------------------------------------------
start_link(Opts) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [Opts], []).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
stop() ->
gen_server:stop(?MODULE).
@ -153,27 +153,50 @@ get_alarms(activated) ->
get_alarms(deactivated) ->
gen_server:call(?MODULE, {get_alarms, deactivated}).
pre_config_update(#{<<"validity_period">> := Period0} = NewConf, OldConf) ->
?MODULE ! {update_timer, hocon_postprocess:duration(Period0)},
merge(OldConf, NewConf);
pre_config_update(NewConf, OldConf) ->
merge(OldConf, NewConf).
merge(undefined, New) -> New;
merge(Old, New) -> maps:merge(Old, New).
format(#activated_alarm{name = Name, message = Message, activate_at = At, details = Details}) ->
Now = erlang:system_time(microsecond),
#{
node => node(),
name => Name,
message => Message,
duration => Now - At,
details => Details
};
format(#deactivated_alarm{name = Name, message = Message, activate_at = At, details = Details,
deactivate_at = DAt}) ->
#{
node => node(),
name => Name,
message => Message,
duration => DAt - At,
details => Details
};
format(_) ->
{error, unknow_alarm}.
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([]) ->
Opts = [{actions, [log, publish]}],
init([Opts]);
init([Opts]) ->
deactivate_all_alarms(),
Actions = proplists:get_value(actions, Opts),
SizeLimit = proplists:get_value(size_limit, Opts),
ValidityPeriod = timer:seconds(proplists:get_value(validity_period, Opts)),
{ok, ensure_delete_timer(#state{actions = Actions,
size_limit = SizeLimit,
validity_period = ValidityPeriod})}.
emqx_config_handler:add_handler([alarm], ?MODULE),
{ok, #state{timer = ensure_timer(undefined, get_validity_period())}}.
%% suppress dialyzer warning due to dirty read/write race condition.
%% TODO: change from dirty_read/write to transactional.
%% TODO: handle mnesia write errors.
-dialyzer([{nowarn_function, [handle_call/3]}]).
handle_call({activate_alarm, Name, Details}, _From, State = #state{actions = Actions}) ->
handle_call({activate_alarm, Name, Details}, _From, State) ->
case mnesia:dirty_read(?ACTIVATED_ALARM, Name) of
[#activated_alarm{name = Name}] ->
{reply, {error, already_existed}, State};
@ -182,18 +205,17 @@ handle_call({activate_alarm, Name, Details}, _From, State = #state{actions = Act
details = Details,
message = normalize_message(Name, Details),
activate_at = erlang:system_time(microsecond)},
mnesia:dirty_write(?ACTIVATED_ALARM, Alarm),
do_actions(activate, Alarm, Actions),
ekka_mnesia:dirty_write(?ACTIVATED_ALARM, Alarm),
do_actions(activate, Alarm, emqx_config:get([alarm, actions])),
{reply, ok, State}
end;
handle_call({deactivate_alarm, Name, Details}, _From, State = #state{
actions = Actions, size_limit = SizeLimit}) ->
handle_call({deactivate_alarm, Name, Details}, _From, State) ->
case mnesia:dirty_read(?ACTIVATED_ALARM, Name) of
[] ->
{reply, {error, not_found}, State};
[Alarm] ->
deactivate_alarm(Details, SizeLimit, Actions, Alarm),
deactivate_alarm(Details, Alarm),
{reply, ok, State}
end;
@ -202,9 +224,14 @@ handle_call(delete_all_deactivated_alarms, _From, State) ->
{reply, ok, State};
handle_call({get_alarms, all}, _From, State) ->
Alarms = [normalize(Alarm) ||
{atomic, Alarms} =
ekka_mnesia:ro_transaction(
?COMMON_SHARD,
fun() ->
[normalize(Alarm) ||
Alarm <- ets:tab2list(?ACTIVATED_ALARM)
++ ets:tab2list(?DEACTIVATED_ALARM)],
++ ets:tab2list(?DEACTIVATED_ALARM)]
end),
{reply, Alarms, State};
handle_call({get_alarms, activated}, _From, State) ->
@ -223,11 +250,15 @@ handle_cast(Msg, State) ->
?LOG(error, "Unexpected msg: ~p", [Msg]),
{noreply, State}.
handle_info({timeout, TRef, delete_expired_deactivated_alarm},
State = #state{timer = TRef,
validity_period = ValidityPeriod}) ->
delete_expired_deactivated_alarms(erlang:system_time(microsecond) - ValidityPeriod * 1000),
{noreply, ensure_delete_timer(State)};
handle_info({timeout, _TRef, delete_expired_deactivated_alarm},
#state{timer = TRef} = State) ->
Period = get_validity_period(),
delete_expired_deactivated_alarms(erlang:system_time(microsecond) - Period * 1000),
{noreply, State#state{timer = ensure_timer(TRef, Period)}};
handle_info({update_timer, Period}, #state{timer = TRef} = State) ->
?LOG(warning, "update the 'validity_period' timer to ~p", [Period]),
{noreply, State#state{timer = ensure_timer(TRef, Period)}};
handle_info(Info, State) ->
?LOG(error, "Unexpected info: ~p", [Info]),
@ -243,16 +274,18 @@ code_change(_OldVsn, State, _Extra) ->
%% Internal functions
%%------------------------------------------------------------------------------
deactivate_alarm(Details, SizeLimit, Actions, #activated_alarm{
activate_at = ActivateAt, name = Name, details = Details0,
message = Msg0}) ->
case SizeLimit > 0 andalso
(mnesia:table_info(?DEACTIVATED_ALARM, size) >= SizeLimit) of
get_validity_period() ->
emqx_config:get([alarm, validity_period]).
deactivate_alarm(Details, #activated_alarm{activate_at = ActivateAt, name = Name,
details = Details0, message = Msg0}) ->
SizeLimit = emqx_config:get([alarm, size_limit]),
case SizeLimit > 0 andalso (mnesia:table_info(?DEACTIVATED_ALARM, size) >= SizeLimit) of
true ->
case mnesia:dirty_first(?DEACTIVATED_ALARM) of
'$end_of_table' -> ok;
ActivateAt2 ->
mnesia:dirty_delete(?DEACTIVATED_ALARM, ActivateAt2)
ekka_mnesia:dirty_delete(?DEACTIVATED_ALARM, ActivateAt2)
end;
false -> ok
end,
@ -261,9 +294,9 @@ deactivate_alarm(Details, SizeLimit, Actions, #activated_alarm{
DeActAlarm = make_deactivated_alarm(ActivateAt, Name, Details,
normalize_message(Name, Details),
erlang:system_time(microsecond)),
mnesia:dirty_write(?DEACTIVATED_ALARM, HistoryAlarm),
mnesia:dirty_delete(?ACTIVATED_ALARM, Name),
do_actions(deactivate, DeActAlarm, Actions).
ekka_mnesia:dirty_write(?DEACTIVATED_ALARM, HistoryAlarm),
ekka_mnesia:dirty_delete(?ACTIVATED_ALARM, Name),
do_actions(deactivate, DeActAlarm, emqx_config:get([alarm, actions])).
make_deactivated_alarm(ActivateAt, Name, Details, Message, DeActivateAt) ->
#deactivated_alarm{
@ -279,7 +312,7 @@ deactivate_all_alarms() ->
details = Details,
message = Message,
activate_at = ActivateAt}) ->
mnesia:dirty_write(?DEACTIVATED_ALARM,
ekka_mnesia:dirty_write(?DEACTIVATED_ALARM,
#deactivated_alarm{
activate_at = ActivateAt,
name = Name,
@ -291,7 +324,7 @@ deactivate_all_alarms() ->
%% Delete all records from the given table, ignore result.
clear_table(TableName) ->
case mnesia:clear_table(TableName) of
case ekka_mnesia:clear_table(TableName) of
{aborted, Reason} ->
?LOG(warning, "Faile to clear table ~p reason: ~p",
[TableName, Reason]);
@ -299,9 +332,12 @@ clear_table(TableName) ->
ok
end.
ensure_delete_timer(State = #state{validity_period = ValidityPeriod}) ->
TRef = emqx_misc:start_timer(ValidityPeriod, delete_expired_deactivated_alarm),
State#state{timer = TRef}.
ensure_timer(OldTRef, Period) ->
_ = case is_reference(OldTRef) of
true -> erlang:cancel_timer(OldTRef);
false -> ok
end,
emqx_misc:start_timer(Period, delete_expired_deactivated_alarm).
delete_expired_deactivated_alarms(Checkpoint) ->
delete_expired_deactivated_alarms(mnesia:dirty_first(?DEACTIVATED_ALARM), Checkpoint).
@ -311,7 +347,7 @@ delete_expired_deactivated_alarms('$end_of_table', _Checkpoint) ->
delete_expired_deactivated_alarms(ActivatedAt, Checkpoint) ->
case ActivatedAt =< Checkpoint of
true ->
mnesia:dirty_delete(?DEACTIVATED_ALARM, ActivatedAt),
ekka_mnesia:dirty_delete(?DEACTIVATED_ALARM, ActivatedAt),
NActivatedAt = mnesia:dirty_next(?DEACTIVATED_ALARM, ActivatedAt),
delete_expired_deactivated_alarms(NActivatedAt, Checkpoint);
false ->
@ -372,9 +408,9 @@ normalize_message(high_system_memory_usage, #{high_watermark := HighWatermark})
normalize_message(high_process_memory_usage, #{high_watermark := HighWatermark}) ->
list_to_binary(io_lib:format("Process memory usage is higher than ~p%", [HighWatermark]));
normalize_message(high_cpu_usage, #{usage := Usage}) ->
list_to_binary(io_lib:format("~p% cpu usage", [Usage]));
list_to_binary(io_lib:format("~s cpu usage", [Usage]));
normalize_message(too_many_processes, #{usage := Usage}) ->
list_to_binary(io_lib:format("~p% process usage", [Usage]));
list_to_binary(io_lib:format("~s process usage", [Usage]));
normalize_message(partition, #{occurred := Node}) ->
list_to_binary(io_lib:format("Partition occurs at node ~s", [Node]));
normalize_message(<<"resource", _/binary>>, #{type := Type, id := ID}) ->

View File

@ -57,11 +57,13 @@ init(_) ->
{ok, []}.
handle_event({set_alarm, {system_memory_high_watermark, []}}, State) ->
emqx_alarm:activate(high_system_memory_usage, #{high_watermark => emqx_os_mon:get_sysmem_high_watermark()}),
emqx_alarm:activate(high_system_memory_usage,
#{high_watermark => emqx_os_mon:get_sysmem_high_watermark()}),
{ok, State};
handle_event({set_alarm, {process_memory_high_watermark, Pid}}, State) ->
emqx_alarm:activate(high_process_memory_usage, #{pid => Pid,
emqx_alarm:activate(high_process_memory_usage,
#{pid => list_to_binary(pid_to_list(Pid)),
high_watermark => emqx_os_mon:get_procmem_high_watermark()}),
{ok, State};

View File

@ -19,16 +19,23 @@
-behaviour(application).
-export([ start/2
, prep_stop/1
, stop/1
, get_description/0
, get_release/0
, set_init_config_load_done/0
]).
-include("emqx.hrl").
-define(APP, emqx).
-define(EMQX_SHARDS, [route_shard]).
-define(EMQX_SHARDS, [ ?ROUTE_SHARD
, ?COMMON_SHARD
, ?SHARED_SUB_SHARD
, ?RULE_ENGINE_SHARD
, ?MOD_DELAYED_SHARD
]).
-include("emqx_release.hrl").
@ -37,13 +44,16 @@
%%--------------------------------------------------------------------
start(_Type, _Args) ->
set_backtrace_depth(),
ok = maybe_load_config(),
ok = set_backtrace_depth(),
print_otp_version_warning(),
print_banner(),
%% Load application first for ekka_mnesia scanner
_ = load_ce_modules(),
ekka:start(),
ok = ekka_rlog:wait_for_shards(?EMQX_SHARDS, infinity),
false == os:getenv("EMQX_NO_QUIC")
andalso application:ensure_all_started(quicer),
{ok, Sup} = emqx_sup:start_link(),
ok = start_autocluster(),
%% ok = emqx_plugins:init(),
@ -55,14 +65,31 @@ start(_Type, _Args) ->
print_vsn(),
{ok, Sup}.
-spec(stop(State :: term()) -> term()).
stop(_State) ->
prep_stop(_State) ->
ok = emqx_alarm_handler:unload(),
emqx_boot:is_enabled(listeners)
andalso emqx_listeners:stop().
stop(_State) -> ok.
%% @doc Call this function to make emqx boot without loading config,
%% in case we want to delegate the config load to a higher level app
%% which manages emqx app.
set_init_config_load_done() ->
application:set_env(emqx, init_config_load_done, true).
maybe_load_config() ->
case application:get_env(emqx, init_config_load_done, false) of
true ->
ok;
false ->
%% the app env 'config_files' should be set before emqx get started.
ConfFiles = application:get_env(emqx, config_files, []),
emqx_config:init_load(emqx_schema, ConfFiles)
end.
set_backtrace_depth() ->
Depth = application:get_env(?APP, backtrace_depth, 16),
Depth = emqx_config:get([node, backtrace_depth]),
_ = erlang:system_flag(backtrace_depth, Depth),
ok.

View File

@ -14,19 +14,19 @@
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_acl_cache).
-module(emqx_authz_cache).
-include("emqx.hrl").
-export([ list_acl_cache/0
, get_acl_cache/2
, put_acl_cache/3
, cleanup_acl_cache/0
, empty_acl_cache/0
, dump_acl_cache/0
, get_cache_max_size/0
, get_cache_ttl/0
, is_enabled/0
-export([ list_authz_cache/1
, get_authz_cache/3
, put_authz_cache/4
, cleanup_authz_cache/1
, empty_authz_cache/0
, dump_authz_cache/0
, get_cache_max_size/1
, get_cache_ttl/1
, is_enabled/1
, drain_cache/0
]).
@ -38,93 +38,95 @@
, get_oldest_key/0
]).
-type(acl_result() :: allow | deny).
-type(authz_result() :: allow | deny).
-type(system_time() :: integer()).
-type(cache_key() :: {emqx_types:pubsub(), emqx_types:topic()}).
-type(cache_val() :: {acl_result(), system_time()}).
-type(cache_val() :: {authz_result(), system_time()}).
-type(acl_cache_entry() :: {cache_key(), cache_val()}).
-type(authz_cache_entry() :: {cache_key(), cache_val()}).
%% Wrappers for key and value
cache_k(PubSub, Topic)-> {PubSub, Topic}.
cache_v(AclResult)-> {AclResult, time_now()}.
cache_v(AuthzResult)-> {AuthzResult, time_now()}.
drain_k() -> {?MODULE, drain_timestamp}.
-spec(is_enabled() -> boolean()).
is_enabled() ->
application:get_env(emqx, enable_acl_cache, true).
-spec(is_enabled(atom()) -> boolean()).
is_enabled(Zone) ->
emqx_config:get_zone_conf(Zone, [authorization, cache, enable]).
-spec(get_cache_max_size() -> integer()).
get_cache_max_size() ->
application:get_env(emqx, acl_cache_max_size, 32).
-spec(get_cache_max_size(atom()) -> integer()).
get_cache_max_size(Zone) ->
emqx_config:get_zone_conf(Zone, [authorization, cache, max_size]).
-spec(get_cache_ttl() -> integer()).
get_cache_ttl() ->
application:get_env(emqx, acl_cache_ttl, 60000).
-spec(get_cache_ttl(atom()) -> integer()).
get_cache_ttl(Zone) ->
emqx_config:get_zone_conf(Zone, [authorization, cache, ttl]).
-spec(list_acl_cache() -> [acl_cache_entry()]).
list_acl_cache() ->
cleanup_acl_cache(),
map_acl_cache(fun(Cache) -> Cache end).
-spec(list_authz_cache(atom()) -> [authz_cache_entry()]).
list_authz_cache(Zone) ->
cleanup_authz_cache(Zone),
map_authz_cache(fun(Cache) -> Cache end).
%% We'll cleanup the cache before replacing an expired acl.
-spec(get_acl_cache(emqx_types:pubsub(), emqx_topic:topic()) -> (acl_result() | not_found)).
get_acl_cache(PubSub, Topic) ->
%% We'll cleanup the cache before replacing an expired authz.
-spec get_authz_cache(atom(), emqx_types:pubsub(), emqx_topic:topic()) ->
authz_result() | not_found.
get_authz_cache(Zone, PubSub, Topic) ->
case erlang:get(cache_k(PubSub, Topic)) of
undefined -> not_found;
{AclResult, CachedAt} ->
if_expired(CachedAt,
{AuthzResult, CachedAt} ->
if_expired(get_cache_ttl(Zone), CachedAt,
fun(false) ->
AclResult;
AuthzResult;
(true) ->
cleanup_acl_cache(),
cleanup_authz_cache(Zone),
not_found
end)
end.
%% If the cache get full, and also the latest one
%% is expired, then delete all the cache entries
-spec(put_acl_cache(emqx_types:pubsub(), emqx_topic:topic(), acl_result()) -> ok).
put_acl_cache(PubSub, Topic, AclResult) ->
MaxSize = get_cache_max_size(), true = (MaxSize =/= 0),
-spec put_authz_cache(atom(), emqx_types:pubsub(), emqx_topic:topic(), authz_result())
-> ok.
put_authz_cache(Zone, PubSub, Topic, AuthzResult) ->
MaxSize = get_cache_max_size(Zone), true = (MaxSize =/= 0),
Size = get_cache_size(),
case Size < MaxSize of
true ->
add_acl(PubSub, Topic, AclResult);
add_authz(PubSub, Topic, AuthzResult);
false ->
NewestK = get_newest_key(),
{_AclResult, CachedAt} = erlang:get(NewestK),
if_expired(CachedAt,
{_AuthzResult, CachedAt} = erlang:get(NewestK),
if_expired(get_cache_ttl(Zone), CachedAt,
fun(true) ->
% all cache expired, cleanup first
empty_acl_cache(),
add_acl(PubSub, Topic, AclResult);
empty_authz_cache(),
add_authz(PubSub, Topic, AuthzResult);
(false) ->
% cache full, perform cache replacement
evict_acl_cache(),
add_acl(PubSub, Topic, AclResult)
evict_authz_cache(),
add_authz(PubSub, Topic, AuthzResult)
end)
end.
%% delete all the acl entries
-spec(empty_acl_cache() -> ok).
empty_acl_cache() ->
foreach_acl_cache(fun({CacheK, _CacheV}) -> erlang:erase(CacheK) end),
%% delete all the authz entries
-spec(empty_authz_cache() -> ok).
empty_authz_cache() ->
foreach_authz_cache(fun({CacheK, _CacheV}) -> erlang:erase(CacheK) end),
set_cache_size(0),
keys_queue_set(queue:new()).
%% delete the oldest acl entry
-spec(evict_acl_cache() -> ok).
evict_acl_cache() ->
%% delete the oldest authz entry
-spec(evict_authz_cache() -> ok).
evict_authz_cache() ->
OldestK = keys_queue_out(),
erlang:erase(OldestK),
decr_cache_size().
%% cleanup all the expired cache entries
-spec(cleanup_acl_cache() -> ok).
cleanup_acl_cache() ->
-spec(cleanup_authz_cache(atom()) -> ok).
cleanup_authz_cache(Zone) ->
keys_queue_set(
cleanup_acl(keys_queue_get())).
cleanup_authz(get_cache_ttl(Zone), keys_queue_get())).
get_oldest_key() ->
keys_queue_pick(queue_front()).
@ -132,22 +134,22 @@ get_newest_key() ->
keys_queue_pick(queue_rear()).
get_cache_size() ->
case erlang:get(acl_cache_size) of
case erlang:get(authz_cache_size) of
undefined -> 0;
Size -> Size
end.
dump_acl_cache() ->
map_acl_cache(fun(Cache) -> Cache end).
dump_authz_cache() ->
map_authz_cache(fun(Cache) -> Cache end).
map_acl_cache(Fun) ->
[Fun(R) || R = {{SubPub, _T}, _Acl} <- get(), SubPub =:= publish
map_authz_cache(Fun) ->
[Fun(R) || R = {{SubPub, _T}, _Authz} <- get(), SubPub =:= publish
orelse SubPub =:= subscribe].
foreach_acl_cache(Fun) ->
_ = map_acl_cache(Fun),
foreach_authz_cache(Fun) ->
_ = map_authz_cache(Fun),
ok.
%% All acl cache entries added before `drain_cache()` invocation will become expired
%% All authz cache entries added before `drain_cache()` invocation will become expired
drain_cache() ->
_ = persistent_term:put(drain_k(), time_now()),
ok.
@ -156,52 +158,52 @@ drain_cache() ->
%% Internal functions
%%--------------------------------------------------------------------
add_acl(PubSub, Topic, AclResult) ->
add_authz(PubSub, Topic, AuthzResult) ->
K = cache_k(PubSub, Topic),
V = cache_v(AclResult),
V = cache_v(AuthzResult),
case erlang:get(K) of
undefined -> add_new_acl(K, V);
{_AclResult, _CachedAt} ->
update_acl(K, V)
undefined -> add_new_authz(K, V);
{_AuthzResult, _CachedAt} ->
update_authz(K, V)
end.
add_new_acl(K, V) ->
add_new_authz(K, V) ->
erlang:put(K, V),
keys_queue_in(K),
incr_cache_size().
update_acl(K, V) ->
update_authz(K, V) ->
erlang:put(K, V),
keys_queue_update(K).
cleanup_acl(KeysQ) ->
cleanup_authz(TTL, KeysQ) ->
case queue:out(KeysQ) of
{{value, OldestK}, KeysQ2} ->
{_AclResult, CachedAt} = erlang:get(OldestK),
if_expired(CachedAt,
{_AuthzResult, CachedAt} = erlang:get(OldestK),
if_expired(TTL, CachedAt,
fun(false) -> KeysQ;
(true) ->
erlang:erase(OldestK),
decr_cache_size(),
cleanup_acl(KeysQ2)
cleanup_authz(TTL, KeysQ2)
end);
{empty, KeysQ} -> KeysQ
end.
incr_cache_size() ->
erlang:put(acl_cache_size, get_cache_size() + 1), ok.
erlang:put(authz_cache_size, get_cache_size() + 1), ok.
decr_cache_size() ->
Size = get_cache_size(),
case Size > 1 of
true ->
erlang:put(acl_cache_size, Size-1);
erlang:put(authz_cache_size, Size-1);
false ->
erlang:put(acl_cache_size, 0)
erlang:put(authz_cache_size, 0)
end,
ok.
set_cache_size(N) ->
erlang:put(acl_cache_size, N), ok.
erlang:put(authz_cache_size, N), ok.
%%% Ordered Keys Q %%%
keys_queue_in(Key) ->
@ -234,9 +236,9 @@ keys_queue_remove(Key, KeysQ) ->
end, KeysQ).
keys_queue_set(KeysQ) ->
erlang:put(acl_keys_q, KeysQ), ok.
erlang:put(authz_keys_q, KeysQ), ok.
keys_queue_get() ->
case erlang:get(acl_keys_q) of
case erlang:get(authz_keys_q) of
undefined -> queue:new();
KeysQ -> KeysQ
end.
@ -246,8 +248,7 @@ queue_rear() -> fun queue:get_r/1.
time_now() -> erlang:system_time(millisecond).
if_expired(CachedAt, Fun) ->
TTL = get_cache_ttl(),
if_expired(TTL, CachedAt, Fun) ->
Now = time_now(),
CurrentEvictTimestamp = persistent_term:get(drain_k(), 0),
case CachedAt =< CurrentEvictTimestamp orelse (CachedAt + TTL) =< Now of

View File

@ -51,6 +51,8 @@
-define(BANNED_TAB, ?MODULE).
-rlog_shard({?COMMON_SHARD, ?BANNED_TAB}).
%%--------------------------------------------------------------------
%% Mnesia bootstrap
%%--------------------------------------------------------------------
@ -96,19 +98,19 @@ create(#{who := Who,
reason := Reason,
at := At,
until := Until}) ->
mnesia:dirty_write(?BANNED_TAB, #banned{who = Who,
ekka_mnesia:dirty_write(?BANNED_TAB, #banned{who = Who,
by = By,
reason = Reason,
at = At,
until = Until});
create(Banned) when is_record(Banned, banned) ->
mnesia:dirty_write(?BANNED_TAB, Banned).
ekka_mnesia:dirty_write(?BANNED_TAB, Banned).
-spec(delete({clientid, emqx_types:clientid()}
| {username, emqx_types:username()}
| {peerhost, emqx_types:peerhost()}) -> ok).
delete(Who) ->
mnesia:dirty_delete(?BANNED_TAB, Who).
ekka_mnesia:dirty_delete(?BANNED_TAB, Who).
info(InfoKey) ->
mnesia:table_info(?BANNED_TAB, InfoKey).
@ -129,7 +131,7 @@ handle_cast(Msg, State) ->
{noreply, State}.
handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) ->
mnesia:async_dirty(fun expire_banned_items/1, [erlang:system_time(second)]),
ekka_mnesia:transaction(?COMMON_SHARD, fun expire_banned_items/1, [erlang:system_time(second)]),
{noreply, ensure_expiry_timer(State), hibernate};
handle_info(Info, State) ->
@ -160,4 +162,3 @@ expire_banned_items(Now) ->
mnesia:delete_object(?BANNED_TAB, B, sticky_write);
(_, _Acc) -> ok
end, ok, ?BANNED_TAB).

View File

@ -243,7 +243,7 @@ route(Routes, Delivery) ->
do_route({To, Node}, Delivery) when Node =:= node() ->
{Node, To, dispatch(To, Delivery)};
do_route({To, Node}, Delivery) when is_atom(Node) ->
{Node, To, forward(Node, To, Delivery, emqx:get_env(rpc_mode, async))};
{Node, To, forward(Node, To, Delivery, emqx_config:get([rpc, mode]))};
do_route({To, Group}, Delivery) when is_tuple(Group); is_binary(Group) ->
{share, To, emqx_shared_sub:dispatch(Group, To, Delivery)}.

View File

@ -31,6 +31,8 @@
-export([ info/1
, info/2
, get_mqtt_conf/2
, get_mqtt_conf/3
, set_conn_state/2
, get_session/1
, set_session/2
@ -63,7 +65,7 @@
, maybe_apply/2
]).
-export_type([channel/0]).
-export_type([channel/0, opts/0]).
-record(channel, {
%% MQTT ConnInfo
@ -98,7 +100,9 @@
-type(channel() :: #channel{}).
-type(conn_state() :: idle | connecting | connected | disconnected).
-type(opts() :: #{zone := atom(), listener := atom(), atom() => term()}).
-type(conn_state() :: idle | connecting | connected | reauthenticating | disconnected).
-type(reply() :: {outgoing, emqx_types:packet()}
| {outgoing, [emqx_types:packet()]}
@ -151,7 +155,9 @@ info(connected_at, #channel{conninfo = ConnInfo}) ->
info(clientinfo, #channel{clientinfo = ClientInfo}) ->
ClientInfo;
info(zone, #channel{clientinfo = ClientInfo}) ->
maps:get(zone, ClientInfo, undefined);
maps:get(zone, ClientInfo);
info(listener, #channel{clientinfo = ClientInfo}) ->
maps:get(listener, ClientInfo);
info(clientid, #channel{clientinfo = ClientInfo}) ->
maps:get(clientid, ClientInfo, undefined);
info(username, #channel{clientinfo = ClientInfo}) ->
@ -195,17 +201,20 @@ caps(#channel{clientinfo = #{zone := Zone}}) ->
%% Init the channel
%%--------------------------------------------------------------------
-spec(init(emqx_types:conninfo(), proplists:proplist()) -> channel()).
-spec(init(emqx_types:conninfo(), opts()) -> channel()).
init(ConnInfo = #{peername := {PeerHost, _Port},
sockname := {_Host, SockPort}}, Options) ->
Zone = proplists:get_value(zone, Options),
sockname := {_Host, SockPort}}, #{zone := Zone, listener := Listener}) ->
Peercert = maps:get(peercert, ConnInfo, undefined),
Protocol = maps:get(protocol, ConnInfo, mqtt),
MountPoint = emqx_zone:mountpoint(Zone),
QuotaPolicy = emqx_zone:quota_policy(Zone),
ClientInfo = setting_peercert_infos(
MountPoint = case get_mqtt_conf(Zone, mountpoint) of
<<>> -> undefined;
MP -> MP
end,
QuotaPolicy = emqx_config:get_listener_conf(Zone, Listener,[rate_limit, quota], []),
ClientInfo = set_peercert_infos(
Peercert,
#{zone => Zone,
listener => Listener,
protocol => Protocol,
peerhost => PeerHost,
sockport => SockPort,
@ -214,7 +223,7 @@ init(ConnInfo = #{peername := {PeerHost, _Port},
mountpoint => MountPoint,
is_bridge => false,
is_superuser => false
}, Options),
}, Zone, Listener),
{NClientInfo, NConnInfo} = take_ws_cookie(ClientInfo, ConnInfo),
#channel{conninfo = NConnInfo,
clientinfo = NClientInfo,
@ -222,7 +231,7 @@ init(ConnInfo = #{peername := {PeerHost, _Port},
outbound => #{}
},
auth_cache = #{},
quota = emqx_limiter:init(Zone, QuotaPolicy),
quota = emqx_limiter:init(Zone, quota_policy(QuotaPolicy)),
timers = #{},
conn_state = idle,
takeover = false,
@ -230,30 +239,32 @@ init(ConnInfo = #{peername := {PeerHost, _Port},
pendings = []
}.
setting_peercert_infos(NoSSL, ClientInfo, _Options)
quota_policy(RawPolicy) ->
[{Name, {list_to_integer(StrCount),
erlang:trunc(hocon_postprocess:duration(StrWind) / 1000)}}
|| {Name, [StrCount, StrWind]} <- maps:to_list(RawPolicy)].
set_peercert_infos(NoSSL, ClientInfo, _, _)
when NoSSL =:= nossl;
NoSSL =:= undefined ->
ClientInfo#{username => undefined};
setting_peercert_infos(Peercert, ClientInfo, Options) ->
set_peercert_infos(Peercert, ClientInfo, Zone, _Listener) ->
{DN, CN} = {esockd_peercert:subject(Peercert),
esockd_peercert:common_name(Peercert)},
Username = peer_cert_as(peer_cert_as_username, Options, Peercert, DN, CN),
ClientId = peer_cert_as(peer_cert_as_clientid, Options, Peercert, DN, CN),
ClientInfo#{username => Username, clientid => ClientId, dn => DN, cn => CN}.
-dialyzer([{nowarn_function, [peer_cert_as/5]}]).
% esockd_peercert:peercert is opaque
% https://github.com/emqx/esockd/blob/master/src/esockd_peercert.erl
peer_cert_as(Key, Options, Peercert, DN, CN) ->
case proplists:get_value(Key, Options) of
PeercetAs = fun(Key) ->
case get_mqtt_conf(Zone, Key) of
cn -> CN;
dn -> DN;
crt -> Peercert;
pem -> base64:encode(Peercert);
md5 -> emqx_passwd:hash(md5, Peercert);
pem when is_binary(Peercert) -> base64:encode(Peercert);
md5 when is_binary(Peercert) -> emqx_passwd:hash(md5, Peercert);
_ -> undefined
end.
end
end,
Username = PeercetAs(peer_cert_as_username),
ClientId = PeercetAs(peer_cert_as_clientid),
ClientInfo#{username => Username, clientid => ClientId, dn => DN, cn => CN}.
take_ws_cookie(ClientInfo, ConnInfo) ->
case maps:take(ws_cookie, ConnInfo) of
@ -272,65 +283,77 @@ take_ws_cookie(ClientInfo, ConnInfo) ->
| {ok, replies(), channel()}
| {shutdown, Reason :: term(), channel()}
| {shutdown, Reason :: term(), replies(), channel()}).
handle_in(?CONNECT_PACKET(), Channel = #channel{conn_state = connected}) ->
handle_in(?CONNECT_PACKET(), Channel = #channel{conn_state = ConnState})
when ConnState =:= connected orelse ConnState =:= reauthenticating ->
handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel);
handle_in(?CONNECT_PACKET(), Channel = #channel{conn_state = connecting}) ->
handle_out(connack, ?RC_PROTOCOL_ERROR, Channel);
handle_in(?CONNECT_PACKET(ConnPkt), Channel) ->
case pipeline([fun enrich_conninfo/2,
fun run_conn_hooks/2,
fun check_connect/2,
fun enrich_client/2,
fun set_log_meta/2,
fun check_banned/2,
fun auth_connect/2
fun check_banned/2
], ConnPkt, Channel#channel{conn_state = connecting}) of
{ok, NConnPkt, NChannel = #channel{clientinfo = ClientInfo}} ->
NChannel1 = NChannel#channel{
will_msg = emqx_packet:will_msg(NConnPkt),
alias_maximum = init_alias_maximum(NConnPkt, ClientInfo)
},
case enhanced_auth(?CONNECT_PACKET(NConnPkt), NChannel1) of
case authenticate(?CONNECT_PACKET(NConnPkt), NChannel1) of
{ok, Properties, NChannel2} ->
process_connect(Properties, ensure_connected(NChannel2));
{continue, Properties, NChannel2} ->
handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, Properties}, NChannel2);
{error, ReasonCode, NChannel2} ->
handle_out(connack, ReasonCode, NChannel2)
{error, ReasonCode} ->
handle_out(connack, ReasonCode, NChannel1)
end;
{error, ReasonCode, NChannel} ->
handle_out(connack, ReasonCode, NChannel)
end;
handle_in(Packet = ?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION, _Properties),
handle_in(Packet = ?AUTH_PACKET(ReasonCode, _Properties),
Channel = #channel{conn_state = ConnState}) ->
case enhanced_auth(Packet, Channel) of
try
case {ReasonCode, ConnState} of
{?RC_CONTINUE_AUTHENTICATION, connecting} -> ok;
{?RC_CONTINUE_AUTHENTICATION, reauthenticating} -> ok;
{?RC_RE_AUTHENTICATE, connected} -> ok;
_ -> error(protocol_error)
end,
case authenticate(Packet, Channel) of
{ok, NProperties, NChannel} ->
case ConnState of
connecting ->
process_connect(NProperties, ensure_connected(NChannel));
connected ->
handle_out(auth, {?RC_SUCCESS, NProperties}, NChannel);
_ ->
handle_out(auth, {?RC_SUCCESS, NProperties}, NChannel#channel{conn_state = connected})
end;
{continue, NProperties, NChannel} ->
handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, NProperties}, NChannel#channel{conn_state = reauthenticating});
{error, NReasonCode} ->
case ConnState of
connecting ->
handle_out(connack, NReasonCode, Channel);
_ ->
handle_out(disconnect, NReasonCode, Channel)
end
end
catch
_Class:_Reason ->
case ConnState of
connecting ->
handle_out(connack, ?RC_PROTOCOL_ERROR, Channel);
_ ->
handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel)
end;
{continue, NProperties, NChannel} ->
handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, NProperties}, NChannel);
{error, NReasonCode, NChannel} ->
handle_out(connack, NReasonCode, NChannel)
end
end;
handle_in(Packet = ?AUTH_PACKET(?RC_RE_AUTHENTICATE, _Properties),
Channel = #channel{conn_state = connected}) ->
case enhanced_auth(Packet, Channel) of
{ok, NProperties, NChannel} ->
handle_out(auth, {?RC_SUCCESS, NProperties}, NChannel);
{continue, NProperties, NChannel} ->
handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, NProperties}, NChannel);
{error, NReasonCode, NChannel} ->
handle_out(disconnect, NReasonCode, NChannel)
end;
handle_in(?PACKET(_), Channel = #channel{conn_state = ConnState}) when ConnState =/= connected ->
handle_in(?PACKET(_), Channel = #channel{conn_state = ConnState})
when ConnState =/= connected andalso ConnState =/= reauthenticating ->
handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel);
handle_in(Packet = ?PUBLISH_PACKET(_QoS), Channel) ->
@ -408,11 +431,12 @@ handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters),
ok ->
TopicFilters0 = parse_topic_filters(TopicFilters),
TopicFilters1 = put_subid_in_subopts(Properties, TopicFilters0),
TupleTopicFilters0 = check_sub_acls(TopicFilters1, Channel),
case emqx_zone:get_env(Zone, acl_deny_action, ignore) =:= disconnect andalso
lists:any(fun({_TopicFilter, ReasonCode}) ->
TupleTopicFilters0 = check_sub_authzs(TopicFilters1, Channel),
HasAuthzDeny = lists:any(fun({_TopicFilter, ReasonCode}) ->
ReasonCode =:= ?RC_NOT_AUTHORIZED
end, TupleTopicFilters0) of
end, TupleTopicFilters0),
DenyAction = emqx_config:get_zone_conf(Zone, [authorization, deny_action]),
case DenyAction =:= disconnect andalso HasAuthzDeny of
true -> handle_out(disconnect, ?RC_NOT_AUTHORIZED, Channel);
false ->
Replace = fun
@ -469,9 +493,11 @@ handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = connec
handle_in({frame_error, Reason}, Channel = #channel{conn_state = connecting}) ->
shutdown(Reason, ?CONNACK_PACKET(?RC_MALFORMED_PACKET), Channel);
handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = connected}) ->
handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = ConnState})
when ConnState =:= connected orelse ConnState =:= reauthenticating ->
handle_out(disconnect, {?RC_PACKET_TOO_LARGE, frame_too_large}, Channel);
handle_in({frame_error, Reason}, Channel = #channel{conn_state = connected}) ->
handle_in({frame_error, Reason}, Channel = #channel{conn_state = ConnState})
when ConnState =:= connected orelse ConnState =:= reauthenticating ->
handle_out(disconnect, {?RC_MALFORMED_PACKET, Reason}, Channel);
handle_in({frame_error, Reason}, Channel = #channel{conn_state = disconnected}) ->
@ -516,7 +542,7 @@ process_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId),
case pipeline([fun check_quota_exceeded/2,
fun process_alias/2,
fun check_pub_alias/2,
fun check_pub_acl/2,
fun check_pub_authz/2,
fun check_pub_caps/2
], Packet, Channel) of
{ok, NPacket, NChannel} ->
@ -525,7 +551,7 @@ process_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId),
{error, Rc = ?RC_NOT_AUTHORIZED, NChannel} ->
?LOG(warning, "Cannot publish message to ~s due to ~s.",
[Topic, emqx_reason_codes:text(Rc)]),
case emqx_zone:get_env(Zone, acl_deny_action, ignore) of
case emqx_config:get_zone_conf(Zone, [authorization, deny_action]) of
ignore ->
case QoS of
?QOS_0 -> {ok, NChannel};
@ -711,7 +737,7 @@ process_disconnect(ReasonCode, Properties, Channel) ->
maybe_update_expiry_interval(#{'Session-Expiry-Interval' := Interval},
Channel = #channel{conninfo = ConnInfo}) ->
Channel#channel{conninfo = ConnInfo#{expiry_interval => Interval}};
Channel#channel{conninfo = ConnInfo#{expiry_interval => timer:seconds(Interval)}};
maybe_update_expiry_interval(_Properties, Channel) -> Channel.
%%--------------------------------------------------------------------
@ -930,8 +956,9 @@ handle_call({takeover, 'end'}, Channel = #channel{session = Session,
AllPendings = lists:append(Delivers, Pendings),
disconnect_and_shutdown(takeovered, AllPendings, Channel);
handle_call(list_acl_cache, Channel) ->
{reply, emqx_acl_cache:list_acl_cache(), Channel};
handle_call(list_authz_cache, #channel{clientinfo = #{zone := Zone}}
= Channel) ->
{reply, emqx_authz_cache:list_authz_cache(Zone), Channel};
handle_call({quota, Policy}, Channel) ->
Zone = info(zone, Channel),
@ -967,9 +994,10 @@ handle_info({sock_closed, Reason}, Channel = #channel{conn_state = connecting})
shutdown(Reason, Channel);
handle_info({sock_closed, Reason}, Channel =
#channel{conn_state = connected,
clientinfo = ClientInfo = #{zone := Zone}}) ->
emqx_zone:enable_flapping_detect(Zone)
#channel{conn_state = ConnState,
clientinfo = ClientInfo = #{zone := Zone}})
when ConnState =:= connected orelse ConnState =:= reauthenticating ->
emqx_config:get_zone_conf(Zone, [flapping_detect, enable])
andalso emqx_flapping:detect(ClientInfo),
Channel1 = ensure_disconnected(Reason, mabye_publish_will_msg(Channel)),
case maybe_shutdown(Reason, Channel1) of
@ -981,8 +1009,8 @@ handle_info({sock_closed, Reason}, Channel = #channel{conn_state = disconnected}
?LOG(error, "Unexpected sock_closed: ~p", [Reason]),
{ok, Channel};
handle_info(clean_acl_cache, Channel) ->
ok = emqx_acl_cache:empty_acl_cache(),
handle_info(clean_authz_cache, Channel) ->
ok = emqx_authz_cache:empty_authz_cache(),
{ok, Channel};
handle_info(Info, Channel) ->
@ -1086,11 +1114,11 @@ clean_timer(Name, Channel = #channel{timers = Timers}) ->
interval(alive_timer, #channel{keepalive = KeepAlive}) ->
emqx_keepalive:info(interval, KeepAlive);
interval(retry_timer, #channel{session = Session}) ->
timer:seconds(emqx_session:info(retry_interval, Session));
emqx_session:info(retry_interval, Session);
interval(await_timer, #channel{session = Session}) ->
timer:seconds(emqx_session:info(await_rel_timeout, Session));
emqx_session:info(await_rel_timeout, Session);
interval(expire_timer, #channel{conninfo = ConnInfo}) ->
timer:seconds(maps:get(expiry_interval, ConnInfo));
maps:get(expiry_interval, ConnInfo);
interval(will_timer, #channel{will_msg = WillMsg}) ->
timer:seconds(will_delay_interval(WillMsg)).
@ -1146,17 +1174,16 @@ enrich_conninfo(ConnPkt = #mqtt_packet_connect{
{ok, Channel#channel{conninfo = NConnInfo}}.
%% If the Session Expiry Interval is absent the value 0 is used.
-compile({inline, [expiry_interval/2]}).
expiry_interval(_Zone, #mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V5,
expiry_interval(_, #mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V5,
properties = ConnProps}) ->
emqx_mqtt_props:get('Session-Expiry-Interval', ConnProps, 0);
timer:seconds(emqx_mqtt_props:get('Session-Expiry-Interval', ConnProps, 0));
expiry_interval(Zone, #mqtt_packet_connect{clean_start = false}) ->
emqx_zone:session_expiry_interval(Zone);
expiry_interval(_Zone, #mqtt_packet_connect{clean_start = true}) ->
get_mqtt_conf(Zone, session_expiry_interval);
expiry_interval(_, #mqtt_packet_connect{clean_start = true}) ->
0.
receive_maximum(Zone, ConnProps) ->
MaxInflightConfig = case emqx_zone:max_inflight(Zone) of
MaxInflightConfig = case get_mqtt_conf(Zone, max_inflight) of
0 -> ?RECEIVE_MAXIMUM_LIMIT;
N -> N
end,
@ -1205,8 +1232,9 @@ set_bridge_mode(_ConnPkt, _ClientInfo) -> ok.
maybe_username_as_clientid(_ConnPkt, ClientInfo = #{username := undefined}) ->
{ok, ClientInfo};
maybe_username_as_clientid(_ConnPkt, ClientInfo = #{zone := Zone, username := Username}) ->
case emqx_zone:use_username_as_clientid(Zone) of
maybe_username_as_clientid(_ConnPkt, ClientInfo = #{zone := Zone,
username := Username}) ->
case get_mqtt_conf(Zone, use_username_as_clientid) of
true -> {ok, ClientInfo#{clientid => Username}};
false -> ok
end.
@ -1234,82 +1262,67 @@ set_log_meta(_ConnPkt, #channel{clientinfo = #{clientid := ClientId}}) ->
%%--------------------------------------------------------------------
%% Check banned
check_banned(_ConnPkt, #channel{clientinfo = ClientInfo = #{zone := Zone}}) ->
case emqx_zone:enable_ban(Zone) andalso emqx_banned:check(ClientInfo) of
check_banned(_ConnPkt, #channel{clientinfo = ClientInfo}) ->
case emqx_banned:check(ClientInfo) of
true -> {error, ?RC_BANNED};
false -> ok
end.
%%--------------------------------------------------------------------
%% Auth Connect
%% Authenticate
auth_connect(#mqtt_packet_connect{password = Password},
authenticate(?CONNECT_PACKET(
#mqtt_packet_connect{
proto_ver = ?MQTT_PROTO_V5,
properties = #{'Authentication-Method' := AuthMethod} = Properties}),
#channel{clientinfo = ClientInfo,
auth_cache = AuthCache} = Channel) ->
AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined),
do_authenticate(ClientInfo#{auth_method => AuthMethod,
auth_data => AuthData,
auth_cache => AuthCache}, Channel);
authenticate(?CONNECT_PACKET(#mqtt_packet_connect{password = Password}),
#channel{clientinfo = ClientInfo} = Channel) ->
#{clientid := ClientId,
username := Username} = ClientInfo,
case emqx_access_control:authenticate(ClientInfo#{password => Password}) of
{ok, AuthResult} ->
is_anonymous(AuthResult) andalso
emqx_metrics:inc('client.auth.anonymous'),
NClientInfo = maps:merge(ClientInfo, AuthResult),
{ok, Channel#channel{clientinfo = NClientInfo}};
{error, Reason} ->
?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p",
[ClientId, Username, Reason]),
{error, emqx_reason_codes:connack_error(Reason)}
do_authenticate(ClientInfo#{password => Password}, Channel);
authenticate(?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properties),
#channel{clientinfo = ClientInfo,
conninfo = #{conn_props := ConnProps},
auth_cache = AuthCache} = Channel) ->
case emqx_mqtt_props:get('Authentication-Method', ConnProps, undefined) of
AuthMethod ->
AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined),
do_authenticate(ClientInfo#{auth_method => AuthMethod,
auth_data => AuthData,
auth_cache => AuthCache}, Channel);
_ ->
{error, ?RC_BAD_AUTHENTICATION_METHOD}
end.
is_anonymous(#{anonymous := true}) -> true;
is_anonymous(_AuthResult) -> false.
%%--------------------------------------------------------------------
%% Enhanced Authentication
enhanced_auth(?CONNECT_PACKET(#mqtt_packet_connect{
proto_ver = Protover,
properties = Properties
}), Channel) ->
case Protover of
?MQTT_PROTO_V5 ->
AuthMethod = emqx_mqtt_props:get('Authentication-Method', Properties, undefined),
AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined),
do_enhanced_auth(AuthMethod, AuthData, Channel);
_ ->
{ok, #{}, Channel}
do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) ->
Properties = #{'Authentication-Method' => AuthMethod},
case emqx_access_control:authenticate(Credential) of
ok ->
{ok, Properties, Channel#channel{auth_cache = #{}}};
{ok, AuthData} ->
{ok, Properties#{'Authentication-Data' => AuthData},
Channel#channel{auth_cache = #{}}};
{continue, AuthCache} ->
{continue, Properties, Channel#channel{auth_cache = AuthCache}};
{continue, AuthData, AuthCache} ->
{continue, Properties#{'Authentication-Data' => AuthData},
Channel#channel{auth_cache = AuthCache}};
{error, Reason} ->
{error, emqx_reason_codes:connack_error(Reason)}
end;
enhanced_auth(?AUTH_PACKET(_ReasonCode, Properties), Channel = #channel{conninfo = ConnInfo}) ->
AuthMethod = emqx_mqtt_props:get('Authentication-Method',
emqx_mqtt_props:get(conn_props, ConnInfo, #{}),
undefined
),
NAuthMethod = emqx_mqtt_props:get('Authentication-Method', Properties, undefined),
AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined),
case NAuthMethod =:= undefined orelse NAuthMethod =/= AuthMethod of
true ->
{error, emqx_reason_codes:connack_error(bad_authentication_method), Channel};
false ->
do_enhanced_auth(AuthMethod, AuthData, Channel)
end.
do_enhanced_auth(undefined, undefined, Channel) ->
do_authenticate(Credential, Channel) ->
case emqx_access_control:authenticate(Credential) of
ok ->
{ok, #{}, Channel};
do_enhanced_auth(undefined, _AuthData, Channel) ->
{error, emqx_reason_codes:connack_error(not_authorized), Channel};
do_enhanced_auth(_AuthMethod, undefined, Channel) ->
{error, emqx_reason_codes:connack_error(not_authorized), Channel};
do_enhanced_auth(AuthMethod, AuthData, Channel = #channel{auth_cache = Cache}) ->
case run_hooks('client.enhanced_authenticate', [AuthMethod, AuthData], Cache) of
{ok, NAuthData, NCache} ->
NProperties = #{'Authentication-Method' => AuthMethod,
'Authentication-Data' => NAuthData},
{ok, NProperties, Channel#channel{auth_cache = NCache}};
{continue, NAuthData, NCache} ->
NProperties = #{'Authentication-Method' => AuthMethod,
'Authentication-Data' => NAuthData},
{continue, NProperties, Channel#channel{auth_cache = NCache}};
_ ->
{error, emqx_reason_codes:connack_error(not_authorized), Channel}
{error, Reason} ->
{error, emqx_reason_codes:connack_error(Reason)}
end.
%%--------------------------------------------------------------------
@ -1401,12 +1414,12 @@ check_pub_alias(#mqtt_packet{
check_pub_alias(_Packet, _Channel) -> ok.
%%--------------------------------------------------------------------
%% Check Pub ACL
%% Check Pub Authorization
check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}},
check_pub_authz(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}},
#channel{clientinfo = ClientInfo}) ->
case is_acl_enabled(ClientInfo) andalso
emqx_access_control:check_acl(ClientInfo, publish, Topic) of
case is_authz_enabled(ClientInfo) andalso
emqx_access_control:authorize(ClientInfo, publish, Topic) of
false -> ok;
allow -> ok;
deny -> {error, ?RC_NOT_AUTHORIZED}
@ -1423,24 +1436,24 @@ check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS,
emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain, topic => Topic}).
%%--------------------------------------------------------------------
%% Check Sub ACL
%% Check Sub Authorization
check_sub_acls(TopicFilters, Channel) ->
check_sub_acls(TopicFilters, Channel, []).
check_sub_authzs(TopicFilters, Channel) ->
check_sub_authzs(TopicFilters, Channel, []).
check_sub_acls([ TopicFilter = {Topic, _} | More] , Channel, Acc) ->
case check_sub_acl(Topic, Channel) of
check_sub_authzs([ TopicFilter = {Topic, _} | More] , Channel, Acc) ->
case check_sub_authz(Topic, Channel) of
allow ->
check_sub_acls(More, Channel, [ {TopicFilter, 0} | Acc]);
check_sub_authzs(More, Channel, [ {TopicFilter, 0} | Acc]);
deny ->
check_sub_acls(More, Channel, [ {TopicFilter, ?RC_NOT_AUTHORIZED} | Acc])
check_sub_authzs(More, Channel, [ {TopicFilter, ?RC_NOT_AUTHORIZED} | Acc])
end;
check_sub_acls([], _Channel, Acc) ->
check_sub_authzs([], _Channel, Acc) ->
lists:reverse(Acc).
check_sub_acl(TopicFilter, #channel{clientinfo = ClientInfo}) ->
case is_acl_enabled(ClientInfo) andalso
emqx_access_control:check_acl(ClientInfo, subscribe, TopicFilter) of
check_sub_authz(TopicFilter, #channel{clientinfo = ClientInfo}) ->
case is_authz_enabled(ClientInfo) andalso
emqx_access_control:authorize(ClientInfo, subscribe, TopicFilter) of
false -> allow;
Result -> Result
end.
@ -1464,13 +1477,14 @@ put_subid_in_subopts(_Properties, TopicFilters) -> TopicFilters.
enrich_subopts(SubOpts, _Channel = ?IS_MQTT_V5) ->
SubOpts;
enrich_subopts(SubOpts, #channel{clientinfo = #{zone := Zone, is_bridge := IsBridge}}) ->
NL = flag(emqx_zone:ignore_loop_deliver(Zone)),
NL = flag(get_mqtt_conf(Zone, ignore_loop_deliver)),
SubOpts#{rap => flag(IsBridge), nl => NL}.
%%--------------------------------------------------------------------
%% Enrich ConnAck Caps
enrich_connack_caps(AckProps, ?IS_MQTT_V5 = #channel{clientinfo = #{zone := Zone}}) ->
enrich_connack_caps(AckProps, ?IS_MQTT_V5 = #channel{clientinfo = #{
zone := Zone}}) ->
#{max_packet_size := MaxPktSize,
max_qos_allowed := MaxQoS,
retain_available := Retain,
@ -1500,8 +1514,8 @@ enrich_connack_caps(AckProps, _Channel) -> AckProps.
%% Enrich server keepalive
enrich_server_keepalive(AckProps, #channel{clientinfo = #{zone := Zone}}) ->
case emqx_zone:server_keepalive(Zone) of
undefined -> AckProps;
case get_mqtt_conf(Zone, server_keepalive) of
disabled -> AckProps;
Keepalive -> AckProps#{'Server-Keep-Alive' => Keepalive}
end.
@ -1512,7 +1526,11 @@ enrich_response_information(AckProps, #channel{conninfo = #{conn_props := ConnPr
clientinfo = #{zone := Zone}}) ->
case emqx_mqtt_props:get('Request-Response-Information', ConnProps, 0) of
0 -> AckProps;
1 -> AckProps#{'Response-Information' => emqx_zone:response_information(Zone)}
1 -> AckProps#{'Response-Information' =>
case get_mqtt_conf(Zone, response_information, "") of
"" -> undefined;
RspInfo -> RspInfo
end}
end.
%%--------------------------------------------------------------------
@ -1544,7 +1562,7 @@ init_alias_maximum(#mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V5,
properties = Properties},
#{zone := Zone} = _ClientInfo) ->
#{outbound => emqx_mqtt_props:get('Topic-Alias-Maximum', Properties, 0),
inbound => emqx_mqtt_caps:get_caps(Zone, max_topic_alias, ?MAX_TOPIC_AlIAS)
inbound => maps:get(max_topic_alias, emqx_mqtt_caps:get_caps(Zone))
};
init_alias_maximum(_ConnPkt, _ClientInfo) -> undefined.
@ -1560,8 +1578,9 @@ ensure_keepalive(_AckProps, Channel = #channel{conninfo = ConnInfo}) ->
ensure_keepalive_timer(maps:get(keepalive, ConnInfo), Channel).
ensure_keepalive_timer(0, Channel) -> Channel;
ensure_keepalive_timer(disabled, Channel) -> Channel;
ensure_keepalive_timer(Interval, Channel = #channel{clientinfo = #{zone := Zone}}) ->
Backoff = emqx_zone:keepalive_backoff(Zone),
Backoff = get_mqtt_conf(Zone, keepalive_backoff),
Keepalive = emqx_keepalive:init(round(timer:seconds(Interval) * Backoff)),
ensure_timer(alive_timer, Channel#channel{keepalive = Keepalive}).
@ -1596,16 +1615,14 @@ maybe_shutdown(Reason, Channel = #channel{conninfo = ConnInfo}) ->
case maps:get(expiry_interval, ConnInfo) of
?UINT_MAX -> {ok, Channel};
I when I > 0 ->
{ok, ensure_timer(expire_timer, timer:seconds(I), Channel)};
{ok, ensure_timer(expire_timer, I, Channel)};
_ -> shutdown(Reason, Channel)
end.
%%--------------------------------------------------------------------
%% Is ACL enabled?
-compile({inline, [is_acl_enabled/1]}).
is_acl_enabled(#{zone := Zone, is_superuser := IsSuperuser}) ->
(not IsSuperuser) andalso emqx_zone:enable_acl(Zone).
%% Is Authorization enabled?
is_authz_enabled(#{zone := Zone, is_superuser := IsSuperuser}) ->
(not IsSuperuser) andalso emqx_config:get_zone_conf(Zone, [authorization, enable]).
%%--------------------------------------------------------------------
%% Parse Topic Filters
@ -1703,7 +1720,8 @@ shutdown(Reason, Reply, Packet, Channel) ->
{shutdown, Reason, Reply, Packet, Channel}.
disconnect_and_shutdown(Reason, Reply, Channel = ?IS_MQTT_V5
= #channel{conn_state = connected}) ->
= #channel{conn_state = ConnState})
when ConnState =:= connected orelse ConnState =:= reauthenticating ->
shutdown(Reason, Reply, ?DISCONNECT_PACKET(reason_code(Reason)), Channel);
disconnect_and_shutdown(Reason, Reply, Channel) ->
@ -1715,6 +1733,12 @@ sp(false) -> 0.
flag(true) -> 1;
flag(false) -> 0.
get_mqtt_conf(Zone, Key) ->
emqx_config:get_zone_conf(Zone, [mqtt, Key]).
get_mqtt_conf(Zone, Key, Default) ->
emqx_config:get_zone_conf(Zone, [mqtt, Key], Default).
%%--------------------------------------------------------------------
%% For CT tests
%%--------------------------------------------------------------------
@ -1722,4 +1746,3 @@ flag(false) -> 0.
set_field(Name, Value, Channel) ->
Pos = emqx_misc:index_of(Name, record_info(fields, channel)),
setelement(Pos+1, Channel, Value).

View File

@ -241,11 +241,31 @@ open_session(false, ClientInfo = #{clientid := ClientId}, ConnInfo) ->
emqx_cm_locker:trans(ClientId, ResumeStart).
create_session(ClientInfo, ConnInfo) ->
Session = emqx_session:init(ClientInfo, ConnInfo),
Options = get_session_confs(ClientInfo, ConnInfo),
Session = emqx_session:init(Options),
ok = emqx_metrics:inc('session.created'),
ok = emqx_hooks:run('session.created', [ClientInfo, emqx_session:info(Session)]),
Session.
get_session_confs(#{zone := Zone}, #{receive_maximum := MaxInflight}) ->
#{max_subscriptions => get_mqtt_conf(Zone, max_subscriptions),
upgrade_qos => get_mqtt_conf(Zone, upgrade_qos),
max_inflight => MaxInflight,
retry_interval => get_mqtt_conf(Zone, retry_interval),
await_rel_timeout => get_mqtt_conf(Zone, await_rel_timeout),
mqueue => mqueue_confs(Zone)
}.
mqueue_confs(Zone) ->
#{max_len => get_mqtt_conf(Zone, max_mqueue_len),
store_qos0 => get_mqtt_conf(Zone, mqueue_store_qos0),
priorities => get_mqtt_conf(Zone, mqueue_priorities),
default_priority => get_mqtt_conf(Zone, mqueue_default_priority)
}.
get_mqtt_conf(Zone, Key) ->
emqx_config:get_zone_conf(Zone, [mqtt, Key]).
%% @doc Try to takeover a session.
-spec(takeover_session(emqx_types:clientid())
-> {error, term()}

View File

@ -62,5 +62,5 @@ unlock(ClientId) ->
-spec(strategy() -> local | leader | quorum | all).
strategy() ->
emqx:get_env(session_locking_strategy, quorum).
emqx_config:get([broker, session_locking_strategy]).

View File

@ -48,7 +48,9 @@
-define(TAB, emqx_channel_registry).
-define(LOCK, {?MODULE, cleanup_down}).
-rlog_shard({?ROUTE_SHARD, ?TAB}).
-define(CM_SHARD, emqx_cm_shard).
-rlog_shard({?CM_SHARD, ?TAB}).
-record(channel, {chid, pid}).
@ -64,7 +66,7 @@ start_link() ->
%% @doc Is the global registry enabled?
-spec(is_enabled() -> boolean()).
is_enabled() ->
emqx:get_env(enable_session_registry, true).
emqx_config:get([broker, enable_session_registry]).
%% @doc Register a global channel.
-spec(register_channel(emqx_types:clientid()
@ -111,6 +113,7 @@ init([]) ->
{storage_properties, [{ets, [{read_concurrency, true},
{write_concurrency, true}]}]}]),
ok = ekka_mnesia:copy_table(?TAB, ram_copies),
ok = ekka_rlog:wait_for_shards([?CM_SHARD], infinity),
ok = ekka:monitor(membership),
{ok, #{}}.
@ -125,7 +128,7 @@ handle_cast(Msg, State) ->
handle_info({membership, {mnesia, down, Node}}, State) ->
global:trans({?LOCK, self()},
fun() ->
ekka_mnesia:transaction(?ROUTE_SHARD, fun cleanup_channels/1, [Node])
ekka_mnesia:transaction(?CM_SHARD, fun cleanup_channels/1, [Node])
end),
{noreply, State};

View File

@ -15,132 +15,271 @@
%%--------------------------------------------------------------------
-module(emqx_config).
-compile({no_auto_import, [get/0, get/1]}).
-compile({no_auto_import, [get/0, get/1, put/2]}).
-export([ get/0
, get/1
-export([ init_load/2
, read_override_conf/0
, check_config/2
, save_configs/4
, save_to_app_env/1
, save_to_config_map/2
, save_to_override_conf/1
]).
-export([get_root/1,
get_root_raw/1]).
-export([ get/1
, get/2
, find/1
, put/1
, put/2
]).
-export([ update_config/2
-export([ get_zone_conf/2
, get_zone_conf/3
, put_zone_conf/3
, find_zone_conf/2
]).
%% raw configs is the config that is now parsed and tranlated by hocon schema
-export([ get_raw/0
, get_raw/1
-export([ get_listener_conf/3
, get_listener_conf/4
, put_listener_conf/4
, find_listener_conf/3
]).
-export([ update/2
, update/3
, remove/1
, remove/2
]).
-export([ get_raw/1
, get_raw/2
, put_raw/1
, put_raw/2
]).
-export([ deep_get/2
, deep_get/3
, deep_put/3
, safe_atom_key_map/1
, unsafe_atom_key_map/1
]).
-define(CONF, fun(ROOT) -> {?MODULE, bin(ROOT)} end).
-define(RAW_CONF, fun(ROOT) -> {?MODULE, raw, bin(ROOT)} end).
-define(ZONE_CONF_PATH(ZONE, PATH), [zones, ZONE | PATH]).
-define(LISTENER_CONF_PATH(ZONE, LISTENER, PATH), [zones, ZONE, listeners, LISTENER | PATH]).
-define(CONF, ?MODULE).
-define(RAW_CONF, {?MODULE, raw}).
-export_type([update_request/0, raw_config/0, config_key/0, config_key_path/0]).
-export_type([update_request/0, raw_config/0, config/0]).
-type update_request() :: term().
-type raw_config() :: hocon:config() | undefined.
-type config_key() :: atom() | binary().
-type config_key_path() :: [config_key()].
%% raw_config() is the config that is NOT parsed and tranlated by hocon schema
-type raw_config() :: #{binary() => term()} | undefined.
%% config() is the config that is parsed and tranlated by hocon schema
-type config() :: #{atom() => term()} | undefined.
-type app_envs() :: [proplists:property()].
-spec get() -> map().
get() ->
persistent_term:get(?CONF, #{}).
%% @doc For the given path, get root value enclosed in a single-key map.
-spec get_root(emqx_map_lib:config_key_path()) -> map().
get_root([RootName | _]) ->
#{RootName => do_get(?CONF, [RootName], #{})}.
-spec get(config_key_path()) -> term().
get(KeyPath) ->
deep_get(KeyPath, get()).
%% @doc For the given path, get raw root value enclosed in a single-key map.
%% key is ensured to be binary.
get_root_raw([RootName | _]) ->
#{bin(RootName) => do_get(?RAW_CONF, [RootName], #{})}.
-spec get(config_key_path(), term()) -> term().
get(KeyPath, Default) ->
deep_get(KeyPath, get(), Default).
%% @doc Get a config value for the given path.
%% The path should at least include root config name.
-spec get(emqx_map_lib:config_key_path()) -> term().
get(KeyPath) -> do_get(?CONF, KeyPath).
-spec get(emqx_map_lib:config_key_path(), term()) -> term().
get(KeyPath, Default) -> do_get(?CONF, KeyPath, Default).
-spec find(emqx_map_lib:config_key_path()) ->
{ok, term()} | {not_found, emqx_map_lib:config_key_path(), term()}.
find(KeyPath) ->
emqx_map_lib:deep_find(KeyPath, get_root(KeyPath)).
-spec get_zone_conf(atom(), emqx_map_lib:config_key_path()) -> term().
get_zone_conf(Zone, KeyPath) ->
?MODULE:get(?ZONE_CONF_PATH(Zone, KeyPath)).
-spec get_zone_conf(atom(), emqx_map_lib:config_key_path(), term()) -> term().
get_zone_conf(Zone, KeyPath, Default) ->
?MODULE:get(?ZONE_CONF_PATH(Zone, KeyPath), Default).
-spec put_zone_conf(atom(), emqx_map_lib:config_key_path(), term()) -> ok.
put_zone_conf(Zone, KeyPath, Conf) ->
?MODULE:put(?ZONE_CONF_PATH(Zone, KeyPath), Conf).
-spec find_zone_conf(atom(), emqx_map_lib:config_key_path()) ->
{ok, term()} | {not_found, emqx_map_lib:config_key_path(), term()}.
find_zone_conf(Zone, KeyPath) ->
find(?ZONE_CONF_PATH(Zone, KeyPath)).
-spec get_listener_conf(atom(), atom(), emqx_map_lib:config_key_path()) -> term().
get_listener_conf(Zone, Listener, KeyPath) ->
?MODULE:get(?LISTENER_CONF_PATH(Zone, Listener, KeyPath)).
-spec get_listener_conf(atom(), atom(), emqx_map_lib:config_key_path(), term()) -> term().
get_listener_conf(Zone, Listener, KeyPath, Default) ->
?MODULE:get(?LISTENER_CONF_PATH(Zone, Listener, KeyPath), Default).
-spec put_listener_conf(atom(), atom(), emqx_map_lib:config_key_path(), term()) -> ok.
put_listener_conf(Zone, Listener, KeyPath, Conf) ->
?MODULE:put(?LISTENER_CONF_PATH(Zone, Listener, KeyPath), Conf).
-spec find_listener_conf(atom(), atom(), emqx_map_lib:config_key_path()) ->
{ok, term()} | {not_found, emqx_map_lib:config_key_path(), term()}.
find_listener_conf(Zone, Listener, KeyPath) ->
find(?LISTENER_CONF_PATH(Zone, Listener, KeyPath)).
-spec put(map()) -> ok.
put(Config) ->
persistent_term:put(?CONF, Config).
maps:fold(fun(RootName, RootValue, _) ->
?MODULE:put([RootName], RootValue)
end, [], Config).
-spec put(config_key_path(), term()) -> ok.
put(KeyPath, Config) ->
put(deep_put(KeyPath, get(), Config)).
-spec put(emqx_map_lib:config_key_path(), term()) -> ok.
put(KeyPath, Config) -> do_put(?CONF, KeyPath, Config).
-spec update_config(config_key_path(), update_request()) ->
-spec update(emqx_map_lib:config_key_path(), update_request()) ->
ok | {error, term()}.
update_config(ConfKeyPath, UpdateReq) ->
emqx_config_handler:update_config(ConfKeyPath, UpdateReq, get_raw()).
update(ConfKeyPath, UpdateReq) ->
update(emqx_schema, ConfKeyPath, UpdateReq).
-spec get_raw() -> map().
get_raw() ->
persistent_term:get(?RAW_CONF, #{}).
-spec update(module(), emqx_map_lib:config_key_path(), update_request()) ->
ok | {error, term()}.
update(SchemaModule, ConfKeyPath, UpdateReq) ->
emqx_config_handler:update_config(SchemaModule, ConfKeyPath, UpdateReq).
-spec get_raw(config_key_path()) -> term().
get_raw(KeyPath) ->
deep_get(KeyPath, get_raw()).
-spec remove(emqx_map_lib:config_key_path()) -> ok | {error, term()}.
remove(ConfKeyPath) ->
remove(emqx_schema, ConfKeyPath).
-spec get_raw(config_key_path(), term()) -> term().
get_raw(KeyPath, Default) ->
deep_get(KeyPath, get_raw(), Default).
remove(SchemaModule, ConfKeyPath) ->
emqx_config_handler:remove_config(SchemaModule, ConfKeyPath).
-spec get_raw(emqx_map_lib:config_key_path()) -> term().
get_raw(KeyPath) -> do_get(?RAW_CONF, KeyPath).
-spec get_raw(emqx_map_lib:config_key_path(), term()) -> term().
get_raw(KeyPath, Default) -> do_get(?RAW_CONF, KeyPath, Default).
-spec put_raw(map()) -> ok.
put_raw(Config) ->
persistent_term:put(?RAW_CONF, Config).
maps:fold(fun(RootName, RootV, _) ->
?MODULE:put_raw([RootName], RootV)
end, [], hocon_schema:get_value([], Config)).
-spec put_raw(config_key_path(), term()) -> ok.
put_raw(KeyPath, Config) ->
put_raw(deep_put(KeyPath, get_raw(), Config)).
-spec put_raw(emqx_map_lib:config_key_path(), term()) -> ok.
put_raw(KeyPath, Config) -> do_put(?RAW_CONF, KeyPath, Config).
%%-----------------------------------------------------------------
-dialyzer([{nowarn_function, [deep_get/2]}]).
-spec deep_get(config_key_path(), map()) -> term().
deep_get(ConfKeyPath, Map) ->
do_deep_get(ConfKeyPath, Map, fun(KeyPath, Data) ->
error({not_found, KeyPath, Data}) end).
%%============================================================================
%% Load/Update configs From/To files
%%============================================================================
-spec deep_get(config_key_path(), map(), term()) -> term().
deep_get(ConfKeyPath, Map, Default) ->
do_deep_get(ConfKeyPath, Map, fun(_, _) -> Default end).
-spec deep_put(config_key_path(), map(), term()) -> map().
deep_put([], Map, Config) when is_map(Map) ->
Config;
deep_put([Key | KeyPath], Map, Config) ->
SubMap = deep_put(KeyPath, maps:get(Key, Map, #{}), Config),
Map#{Key => SubMap}.
unsafe_atom_key_map(Map) ->
covert_keys_to_atom(Map, fun(K) -> binary_to_atom(K, utf8) end).
safe_atom_key_map(Map) ->
covert_keys_to_atom(Map, fun(K) -> binary_to_existing_atom(K, utf8) end).
%%---------------------------------------------------------------------------
-spec do_deep_get(config_key_path(), map(), fun((config_key(), term()) -> any())) -> term().
do_deep_get([], Map, _) ->
Map;
do_deep_get([Key | KeyPath], Map, OnNotFound) when is_map(Map) ->
case maps:find(Key, Map) of
{ok, SubMap} -> do_deep_get(KeyPath, SubMap, OnNotFound);
error -> OnNotFound(Key, Map)
%% @doc Initial load of the given config files.
%% NOTE: The order of the files is significant, configs from files orderd
%% in the rear of the list overrides prior values.
-spec init_load(module(), [string()] | binary() | hocon:config()) -> ok.
init_load(SchemaModule, Conf) when is_list(Conf) orelse is_binary(Conf) ->
ParseOptions = #{format => richmap},
Parser = case is_binary(Conf) of
true -> fun hocon:binary/2;
false -> fun hocon:files/2
end,
case Parser(Conf, ParseOptions) of
{ok, RawRichConf} ->
init_load(SchemaModule, RawRichConf);
{error, Reason} ->
logger:error(#{msg => failed_to_load_hocon_conf,
reason => Reason
}),
error(failed_to_load_hocon_conf)
end;
do_deep_get([Key | _KeyPath], Data, OnNotFound) ->
OnNotFound(Key, Data).
init_load(SchemaModule, RawRichConf) when is_map(RawRichConf) ->
%% check with richmap for line numbers in error reports (future enhancement)
Opts = #{return_plain => true,
nullable => true
},
%% this call throws exception in case of check failure
{_AppEnvs, CheckedConf} = hocon_schema:map_translate(SchemaModule, RawRichConf, Opts),
ok = save_to_config_map(emqx_map_lib:unsafe_atom_key_map(CheckedConf),
hocon_schema:richmap_to_map(RawRichConf)).
covert_keys_to_atom(BinKeyMap, Conv) when is_map(BinKeyMap) ->
maps:fold(
fun(K, V, Acc) when is_binary(K) ->
Acc#{Conv(K) => covert_keys_to_atom(V, Conv)};
(K, V, Acc) when is_atom(K) ->
%% richmap keys
Acc#{K => covert_keys_to_atom(V, Conv)}
end, #{}, BinKeyMap);
covert_keys_to_atom(ListV, Conv) when is_list(ListV) ->
[covert_keys_to_atom(V, Conv) || V <- ListV];
covert_keys_to_atom(Val, _) -> Val.
-spec check_config(module(), raw_config()) -> {AppEnvs, CheckedConf}
when AppEnvs :: app_envs(), CheckedConf :: config().
check_config(SchemaModule, RawConf) ->
Opts = #{return_plain => true,
nullable => true,
is_richmap => false
},
{AppEnvs, CheckedConf} =
hocon_schema:map_translate(SchemaModule, RawConf, Opts),
Conf = maps:with(maps:keys(RawConf), CheckedConf),
{AppEnvs, emqx_map_lib:unsafe_atom_key_map(Conf)}.
-spec read_override_conf() -> raw_config().
read_override_conf() ->
load_hocon_file(emqx_override_conf_name(), map).
-spec save_configs(app_envs(), config(), raw_config(), raw_config()) -> ok | {error, term()}.
save_configs(_AppEnvs, Conf, RawConf, OverrideConf) ->
%% We may need also support hot config update for the apps that use application envs.
%% If that is the case uncomment the following line to update the configs to app env
%save_to_app_env(AppEnvs),
save_to_config_map(Conf, RawConf),
%% TODO: merge RawConf to OverrideConf can be done here
save_to_override_conf(OverrideConf).
-spec save_to_app_env([tuple()]) -> ok.
save_to_app_env(AppEnvs) ->
lists:foreach(fun({AppName, Envs}) ->
[application:set_env(AppName, Par, Val) || {Par, Val} <- Envs]
end, AppEnvs).
-spec save_to_config_map(config(), raw_config()) -> ok.
save_to_config_map(Conf, RawConf) ->
?MODULE:put(Conf),
?MODULE:put_raw(RawConf).
-spec save_to_override_conf(raw_config()) -> ok | {error, term()}.
save_to_override_conf(RawConf) ->
FileName = emqx_override_conf_name(),
ok = filelib:ensure_dir(FileName),
case file:write_file(FileName, jsx:prettify(jsx:encode(RawConf))) of
ok -> ok;
{error, Reason} ->
logger:error("write to ~s failed, ~p", [FileName, Reason]),
{error, Reason}
end.
load_hocon_file(FileName, LoadType) ->
case filelib:is_regular(FileName) of
true ->
{ok, Raw0} = hocon:load(FileName, #{format => LoadType}),
Raw0;
false -> #{}
end.
emqx_override_conf_name() ->
filename:join([?MODULE:get([node, data_dir]), "emqx_override.conf"]).
bin(Bin) when is_binary(Bin) -> Bin;
bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8).
do_get(PtKey, KeyPath) ->
Ref = make_ref(),
Res = do_get(PtKey, KeyPath, Ref),
case Res =:= Ref of
true -> error({config_not_found, KeyPath});
false -> Res
end.
do_get(PtKey, [RootName], Default) ->
persistent_term:get(PtKey(RootName), Default);
do_get(PtKey, [RootName | KeyPath], Default) ->
RootV = persistent_term:get(PtKey(RootName), #{}),
emqx_map_lib:deep_get(KeyPath, RootV, Default).
do_put(PtKey, [RootName | KeyPath], DeepValue) ->
OldValue = do_get(PtKey, [RootName], #{}),
NewValue = emqx_map_lib:deep_put(KeyPath, OldValue, DeepValue),
persistent_term:put(PtKey(RootName), NewValue).

View File

@ -25,13 +25,10 @@
-export([ start_link/0
, add_handler/2
, update_config/3
, remove_config/2
, merge_to_old_config/2
]).
%% emqx_config_handler callbacks
-export([ handle_update_config/2
]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
@ -41,15 +38,21 @@
code_change/3]).
-define(MOD, {mod}).
-define(REMOVE_CONF, '$remove_config').
-type handler_name() :: module().
-type handlers() :: #{emqx_config:config_key() => handlers(), ?MOD => handler_name()}.
-optional_callbacks([handle_update_config/2]).
-optional_callbacks([ pre_config_update/2
, post_config_update/3
]).
-callback handle_update_config(emqx_config:update_request(), emqx_config:raw_config()) ->
-callback pre_config_update(emqx_config:update_request(), emqx_config:raw_config()) ->
emqx_config:update_request().
-callback post_config_update(emqx_config:update_request(), emqx_config:config(),
emqx_config:config()) -> any().
-type state() :: #{
handlers := handlers(),
atom() => term()
@ -58,11 +61,15 @@
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, {}, []).
-spec update_config(emqx_config:config_key_path(), emqx_config:update_request(),
emqx_config:raw_config()) ->
-spec update_config(module(), emqx_config:config_key_path(), emqx_config:update_request()) ->
ok | {error, term()}.
update_config(ConfKeyPath, UpdateReq, RawConfig) ->
gen_server:call(?MODULE, {update_config, ConfKeyPath, UpdateReq, RawConfig}).
update_config(SchemaModule, ConfKeyPath, UpdateReq) when UpdateReq =/= ?REMOVE_CONF ->
gen_server:call(?MODULE, {change_config, SchemaModule, ConfKeyPath, UpdateReq}).
-spec remove_config(module(), emqx_config:config_key_path()) ->
ok | {error, term()}.
remove_config(SchemaModule, ConfKeyPath) ->
gen_server:call(?MODULE, {change_config, SchemaModule, ConfKeyPath, ?REMOVE_CONF}).
-spec add_handler(emqx_config:config_key_path(), handler_name()) -> ok.
add_handler(ConfKeyPath, HandlerName) ->
@ -72,26 +79,28 @@ add_handler(ConfKeyPath, HandlerName) ->
-spec init(term()) -> {ok, state()}.
init(_) ->
{ok, RawConf} = hocon:load(emqx_conf_name(), #{format => richmap}),
{_MappedEnvs, Conf} = hocon_schema:map_translate(emqx_schema, RawConf, #{}),
ok = save_config_to_emqx(to_plainmap(Conf), to_plainmap(RawConf)),
{ok, #{handlers => #{?MOD => ?MODULE}}}.
handle_call({add_child, ConfKeyPath, HandlerName}, _From,
State = #{handlers := Handlers}) ->
{reply, ok, State#{handlers =>
emqx_config:deep_put(ConfKeyPath, Handlers, #{?MOD => HandlerName})}};
emqx_map_lib:deep_put(ConfKeyPath, Handlers, #{?MOD => HandlerName})}};
handle_call({update_config, ConfKeyPath, UpdateReq, RawConf}, _From,
handle_call({change_config, SchemaModule, ConfKeyPath, UpdateReq}, _From,
#{handlers := Handlers} = State) ->
try {RootKeys, Conf} = do_update_config(ConfKeyPath, Handlers, RawConf, UpdateReq),
{reply, save_configs(RootKeys, Conf), State}
catch
throw: Reason ->
{reply, {error, Reason}, State};
Error : Reason : ST ->
?LOG(error, "update config failed: ~p", [{Error, Reason, ST}]),
{reply, {error, Reason}, State}
end;
OldConf = emqx_config:get_root(ConfKeyPath),
OldRawConf = emqx_config:get_root_raw(ConfKeyPath),
Result = try
{NewRawConf, OverrideConf} = process_upadate_request(ConfKeyPath, OldRawConf,
Handlers, UpdateReq),
{AppEnvs, CheckedConf} = emqx_config:check_config(SchemaModule, NewRawConf),
_ = do_post_config_update(ConfKeyPath, Handlers, OldConf, CheckedConf, UpdateReq),
emqx_config:save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf)
catch Error:Reason:ST ->
?LOG(error, "change_config failed: ~p", [{Error, Reason, ST}]),
{error, Reason}
end,
{reply, Result, State};
handle_call(_Request, _From, State) ->
Reply = ok,
@ -109,104 +118,67 @@ terminate(_Reason, _State) ->
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
do_update_config([], Handlers, OldConf, UpdateReq) ->
call_handle_update_config(Handlers, OldConf, UpdateReq);
do_update_config([ConfKey | ConfKeyPath], Handlers, OldConf, UpdateReq) ->
process_upadate_request(ConfKeyPath, OldRawConf, _Handlers, ?REMOVE_CONF) ->
BinKeyPath = bin_path(ConfKeyPath),
NewRawConf = emqx_map_lib:deep_remove(BinKeyPath, OldRawConf),
OverrideConf = emqx_map_lib:deep_remove(BinKeyPath, emqx_config:read_override_conf()),
{NewRawConf, OverrideConf};
process_upadate_request(ConfKeyPath, OldRawConf, Handlers, UpdateReq) ->
NewRawConf = do_update_config(ConfKeyPath, Handlers, OldRawConf, UpdateReq),
OverrideConf = update_override_config(NewRawConf),
{NewRawConf, OverrideConf}.
do_update_config([], Handlers, OldRawConf, UpdateReq) ->
call_pre_config_update(Handlers, OldRawConf, UpdateReq);
do_update_config([ConfKey | ConfKeyPath], Handlers, OldRawConf, UpdateReq) ->
SubOldRawConf = get_sub_config(bin(ConfKey), OldRawConf),
SubHandlers = maps:get(ConfKey, Handlers, #{}),
NewUpdateReq = do_update_config(ConfKeyPath, SubHandlers, SubOldRawConf, UpdateReq),
call_pre_config_update(Handlers, OldRawConf, #{bin(ConfKey) => NewUpdateReq}).
do_post_config_update([], Handlers, OldConf, NewConf, UpdateReq) ->
call_post_config_update(Handlers, OldConf, NewConf, UpdateReq);
do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, UpdateReq) ->
SubOldConf = get_sub_config(ConfKey, OldConf),
case maps:find(ConfKey, Handlers) of
error -> throw({handler_not_found, ConfKey});
{ok, SubHandlers} ->
NewUpdateReq = do_update_config(ConfKeyPath, SubHandlers, SubOldConf, UpdateReq),
call_handle_update_config(Handlers, OldConf, #{bin(ConfKey) => NewUpdateReq})
end.
SubNewConf = get_sub_config(ConfKey, NewConf),
SubHandlers = maps:get(ConfKey, Handlers, #{}),
_ = do_post_config_update(ConfKeyPath, SubHandlers, SubOldConf, SubNewConf, UpdateReq),
call_post_config_update(Handlers, OldConf, NewConf, UpdateReq).
get_sub_config(_, undefined) ->
undefined;
get_sub_config(ConfKey, OldConf) when is_map(OldConf) ->
maps:get(bin(ConfKey), OldConf, undefined);
get_sub_config(_, OldConf) ->
OldConf.
get_sub_config(ConfKey, Conf) when is_map(Conf) ->
maps:get(ConfKey, Conf, undefined);
get_sub_config(_, _Conf) -> %% the Conf is a primitive
undefined.
call_handle_update_config(Handlers, OldConf, UpdateReq) ->
call_pre_config_update(Handlers, OldRawConf, UpdateReq) ->
HandlerName = maps:get(?MOD, Handlers, undefined),
case erlang:function_exported(HandlerName, handle_update_config, 2) of
true -> HandlerName:handle_update_config(UpdateReq, OldConf);
false -> UpdateReq %% the default behaviour is overwriting the old config
case erlang:function_exported(HandlerName, pre_config_update, 2) of
true -> HandlerName:pre_config_update(UpdateReq, OldRawConf);
false -> merge_to_old_config(UpdateReq, OldRawConf)
end.
%% callbacks for the top-level handler
handle_update_config(UpdateReq, OldConf) ->
FullRawConf = merge_to_old_config(UpdateReq, OldConf),
{maps:keys(UpdateReq), FullRawConf}.
%% default callback of config handlers
merge_to_old_config(UpdateReq, undefined) ->
merge_to_old_config(UpdateReq, #{});
merge_to_old_config(UpdateReq, RawConf) ->
maps:merge(RawConf, UpdateReq).
%%============================================================================
save_configs(RootKeys, RawConf) ->
{_MappedEnvs, Conf} = hocon_schema:map_translate(emqx_schema, to_richmap(RawConf), #{}),
%% We may need also support hot config update for the apps that use application envs.
%% If so uncomment the following line to update the configs to application env
%save_config_to_app_env(_MappedEnvs),
save_config_to_emqx(to_plainmap(Conf), RawConf),
save_config_to_disk(RootKeys, RawConf).
% save_config_to_app_env(MappedEnvs) ->
% lists:foreach(fun({AppName, Envs}) ->
% [application:set_env(AppName, Par, Val) || {Par, Val} <- Envs]
% end, MappedEnvs).
save_config_to_emqx(Conf, RawConf) ->
emqx_config:put(emqx_config:unsafe_atom_key_map(Conf)),
emqx_config:put_raw(RawConf).
save_config_to_disk(RootKeys, Conf) ->
FileName = emqx_override_conf_name(),
OldConf = read_old_config(FileName),
%% We don't save the overall config to file, but only the sub configs
%% under RootKeys
write_new_config(FileName,
maps:merge(OldConf, maps:with(RootKeys, Conf))).
write_new_config(FileName, Conf) ->
case file:write_file(FileName, jsx:prettify(jsx:encode(Conf))) of
ok -> ok;
{error, Reason} ->
logger:error("write to ~s failed, ~p", [FileName, Reason]),
{error, Reason}
call_post_config_update(Handlers, OldConf, NewConf, UpdateReq) ->
HandlerName = maps:get(?MOD, Handlers, undefined),
case erlang:function_exported(HandlerName, post_config_update, 3) of
true -> HandlerName:post_config_update(UpdateReq, NewConf, OldConf);
false -> ok
end.
read_old_config(FileName) ->
case file:read_file(FileName) of
{ok, Text} ->
try jsx:decode(Text, [{return_maps, true}]) of
Conf when is_map(Conf) -> Conf;
_ -> #{}
catch _Err : _Reason ->
#{}
end;
_ -> #{}
end.
%% The default callback of config handlers
%% the behaviour is overwriting the old config if:
%% 1. the old config is undefined
%% 2. either the old or the new config is not of map type
%% the behaviour is merging the new the config to the old config if they are maps.
merge_to_old_config(UpdateReq, RawConf) when is_map(UpdateReq), is_map(RawConf) ->
maps:merge(RawConf, UpdateReq);
merge_to_old_config(UpdateReq, _RawConf) ->
UpdateReq.
emqx_conf_name() ->
filename:join([etc_dir(), "emqx.conf"]).
update_override_config(RawConf) ->
OldConf = emqx_config:read_override_conf(),
maps:merge(OldConf, RawConf).
emqx_override_conf_name() ->
filename:join([emqx:get_env(data_dir), "emqx_override.conf"]).
bin_path(ConfKeyPath) -> [bin(Key) || Key <- ConfKeyPath].
etc_dir() ->
emqx:get_env(etc_dir).
to_richmap(Map) ->
{ok, RichMap} = hocon:binary(jsx:encode(Map), #{format => richmap}),
RichMap.
to_plainmap(RichMap) ->
hocon_schema:richmap_to_map(RichMap).
bin(A) when is_atom(A) -> list_to_binary(atom_to_list(A));
bin(B) when is_binary(B) -> B;
bin(S) when is_list(S) -> list_to_binary(S).
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin(B) when is_binary(B) -> B.

View File

@ -55,8 +55,8 @@ cancel_alarms(Socket, Transport, Channel) ->
end, ?ALL_ALARM_REASONS).
is_alarm_enabled(Channel) ->
emqx_zone:get_env(emqx_channel:info(zone, Channel),
conn_congestion_alarm_enabled, false).
Zone = emqx_channel:info(zone, Channel),
emqx_config:get_zone_conf(Zone, [conn_congestion, enable_alarm]).
alarm_congestion(Socket, Transport, Channel, Reason) ->
case has_alarm_sent(Reason) of
@ -68,8 +68,8 @@ alarm_congestion(Socket, Transport, Channel, Reason) ->
cancel_alarm_congestion(Socket, Transport, Channel, Reason) ->
Zone = emqx_channel:info(zone, Channel),
WontClearIn = emqx_zone:get_env(Zone, conn_congestion_min_alarm_sustain_duration,
?WONT_CLEAR_IN),
WontClearIn = emqx_config:get_zone_conf(Zone, [conn_congestion,
min_alarm_sustain_duration]),
case has_alarm_sent(Reason) andalso long_time_since_last_alarm(Reason, WontClearIn) of
true -> do_cancel_alarm_congestion(Socket, Transport, Channel, Reason);
false -> ok

View File

@ -83,8 +83,6 @@
sockname :: emqx_types:peername(),
%% Sock State
sockstate :: emqx_types:sockstate(),
%% The {active, N} option
active_n :: pos_integer(),
%% Limiter
limiter :: maybe(emqx_limiter:limiter()),
%% Limit Timer
@ -102,13 +100,17 @@
%% Idle Timeout
idle_timeout :: integer(),
%% Idle Timer
idle_timer :: maybe(reference())
idle_timer :: maybe(reference()),
%% Zone name
zone :: atom(),
%% Listener Name
listener :: atom()
}).
-type(state() :: #state{}).
-define(ACTIVE_N, 100).
-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]).
-define(INFO_KEYS, [socktype, peername, sockname, sockstate]).
-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]).
-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]).
@ -134,7 +136,7 @@
, system_code_change/4
]}).
-spec(start_link(esockd:transport(), esockd:socket(), proplists:proplist())
-spec(start_link(esockd:transport(), esockd:socket(), emqx_channel:opts())
-> {ok, pid()}).
start_link(Transport, Socket, Options) ->
Args = [self(), Transport, Socket, Options],
@ -165,8 +167,6 @@ info(sockname, #state{sockname = Sockname}) ->
Sockname;
info(sockstate, #state{sockstate = SockSt}) ->
SockSt;
info(active_n, #state{active_n = ActiveN}) ->
ActiveN;
info(stats_timer, #state{stats_timer = StatsTimer}) ->
StatsTimer;
info(limit_timer, #state{limit_timer = LimitTimer}) ->
@ -243,7 +243,7 @@ init(Parent, Transport, RawSocket, Options) ->
exit_on_sock_error(Reason)
end.
init_state(Transport, Socket, Options) ->
init_state(Transport, Socket, #{zone := Zone, listener := Listener} = Opts) ->
{ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]),
{ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]),
Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]),
@ -253,26 +253,29 @@ init_state(Transport, Socket, Options) ->
peercert => Peercert,
conn_mod => ?MODULE
},
Zone = proplists:get_value(zone, Options),
ActiveN = proplists:get_value(active_n, Options, ?ACTIVE_N),
PubLimit = emqx_zone:publish_limit(Zone),
BytesIn = proplists:get_value(rate_limit, Options),
RateLimit = emqx_zone:ratelimit(Zone),
Limiter = emqx_limiter:init(Zone, PubLimit, BytesIn, RateLimit),
FrameOpts = emqx_zone:mqtt_frame_options(Zone),
Limiter = emqx_limiter:init(Zone, undefined, undefined, []),
FrameOpts = #{
strict_mode => emqx_config:get_zone_conf(Zone, [mqtt, strict_mode]),
max_size => emqx_config:get_zone_conf(Zone, [mqtt, max_packet_size])
},
ParseState = emqx_frame:initial_parse_state(FrameOpts),
Serialize = emqx_frame:serialize_opts(),
Channel = emqx_channel:init(ConnInfo, Options),
GcState = emqx_zone:init_gc_state(Zone),
StatsTimer = emqx_zone:stats_timer(Zone),
IdleTimeout = emqx_zone:idle_timeout(Zone),
Channel = emqx_channel:init(ConnInfo, Opts),
GcState = case emqx_config:get_zone_conf(Zone, [force_gc]) of
#{enable := false} -> undefined;
GcPolicy -> emqx_gc:init(GcPolicy)
end,
StatsTimer = case emqx_config:get_zone_conf(Zone, [stats, enable]) of
true -> undefined;
false -> disabled
end,
IdleTimeout = emqx_channel:get_mqtt_conf(Zone, idle_timeout),
IdleTimer = start_timer(IdleTimeout, idle_timeout),
#state{transport = Transport,
socket = Socket,
peername = Peername,
sockname = Sockname,
sockstate = idle,
active_n = ActiveN,
limiter = Limiter,
parse_state = ParseState,
serialize = Serialize,
@ -280,7 +283,9 @@ init_state(Transport, Socket, Options) ->
gc_state = GcState,
stats_timer = StatsTimer,
idle_timeout = IdleTimeout,
idle_timer = IdleTimer
idle_timer = IdleTimer,
zone = Zone,
listener = Listener
}.
run_loop(Parent, State = #state{transport = Transport,
@ -288,8 +293,9 @@ run_loop(Parent, State = #state{transport = Transport,
peername = Peername,
channel = Channel}) ->
emqx_logger:set_metadata_peername(esockd:format(Peername)),
emqx_misc:tune_heap_size(emqx_zone:oom_policy(
emqx_channel:info(zone, Channel))),
ShutdownPolicy = emqx_config:get_zone_conf(emqx_channel:info(zone, Channel),
[force_shutdown]),
emqx_misc:tune_heap_size(ShutdownPolicy),
case activate_socket(State) of
{ok, NState} -> hibernate(Parent, NState);
{error, Reason} ->
@ -416,6 +422,13 @@ handle_msg({Inet, _Sock, Data}, State) when Inet == tcp; Inet == ssl ->
ok = emqx_metrics:inc('bytes.received', Oct),
parse_incoming(Data, State);
handle_msg({quic, Data, _Sock, _, _, _}, State) ->
?LOG(debug, "RECV ~0p", [Data]),
Oct = iolist_size(Data),
inc_counter(incoming_bytes, Oct),
ok = emqx_metrics:inc('bytes.received', Oct),
parse_incoming(Data, State);
handle_msg({incoming, Packet = ?CONNECT_PACKET(ConnPkt)},
State = #state{idle_timer = IdleTimer}) ->
ok = emqx_misc:cancel_timer(IdleTimer),
@ -440,7 +453,7 @@ handle_msg({Closed, _Sock}, State)
handle_info({sock_closed, Closed}, close_socket(State));
handle_msg({Passive, _Sock}, State)
when Passive == tcp_passive; Passive == ssl_passive ->
when Passive == tcp_passive; Passive == ssl_passive; Passive =:= quic_passive ->
%% In Stats
Pubs = emqx_pd:reset_counter(incoming_pubs),
Bytes = emqx_pd:reset_counter(incoming_bytes),
@ -451,14 +464,15 @@ handle_msg({Passive, _Sock}, State)
NState1 = check_oom(run_gc(InStats, NState)),
handle_info(activate_socket, NState1);
handle_msg(Deliver = {deliver, _Topic, _Msg},
#state{active_n = ActiveN} = State) ->
handle_msg(Deliver = {deliver, _Topic, _Msg}, #state{zone = Zone,
listener = Listener} = State) ->
ActiveN = get_active_n(Zone, Listener),
Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)],
with_channel(handle_deliver, [Delivers], State);
%% Something sent
handle_msg({inet_reply, _Sock, ok}, State = #state{active_n = ActiveN}) ->
case emqx_pd:get_counter(outgoing_pubs) > ActiveN of
handle_msg({inet_reply, _Sock, ok}, State = #state{zone = Zone, listener = Listener}) ->
case emqx_pd:get_counter(outgoing_pubs) > get_active_n(Zone, Listener) of
true ->
Pubs = emqx_pd:reset_counter(outgoing_pubs),
Bytes = emqx_pd:reset_counter(outgoing_bytes),
@ -731,6 +745,15 @@ handle_info({sock_error, Reason}, State) ->
end,
handle_info({sock_closed, Reason}, close_socket(State));
handle_info({quic, peer_send_shutdown, _Stream}, State) ->
handle_info({sock_closed, force}, close_socket(State));
handle_info({quic, closed, _Channel, ReasonFlag}, State) ->
handle_info({sock_closed, ReasonFlag}, State);
handle_info({quic, closed, _Stream}, State) ->
handle_info({sock_closed, force}, State);
handle_info(Info, State) ->
with_channel(handle_info, [Info], State).
@ -778,15 +801,14 @@ run_gc(Stats, State = #state{gc_state = GcSt}) ->
end.
check_oom(State = #state{channel = Channel}) ->
Zone = emqx_channel:info(zone, Channel),
OomPolicy = emqx_zone:oom_policy(Zone),
?tp(debug, check_oom, #{policy => OomPolicy}),
case ?ENABLED(OomPolicy) andalso emqx_misc:check_oom(OomPolicy) of
ShutdownPolicy = emqx_config:get_zone_conf(
emqx_channel:info(zone, Channel), [force_shutdown]),
?tp(debug, check_oom, #{policy => ShutdownPolicy}),
case emqx_misc:check_oom(ShutdownPolicy) of
{shutdown, Reason} ->
%% triggers terminate/2 callback immediately
erlang:exit({shutdown, Reason});
_Other ->
ok
_ -> ok
end,
State.
@ -798,10 +820,10 @@ activate_socket(State = #state{sockstate = closed}) ->
{ok, State};
activate_socket(State = #state{sockstate = blocked}) ->
{ok, State};
activate_socket(State = #state{transport = Transport,
socket = Socket,
active_n = N}) ->
case Transport:setopts(Socket, [{active, N}]) of
activate_socket(State = #state{transport = Transport, socket = Socket,
zone = Zone, listener = Listener}) ->
ActiveN = get_active_n(Zone, Listener),
case Transport:setopts(Socket, [{active, ActiveN}]) of
ok -> {ok, State#state{sockstate = running}};
Error -> Error
end.
@ -882,3 +904,9 @@ get_state(Pid) ->
State = sys:get_state(Pid),
maps:from_list(lists:zip(record_info(fields, state),
tl(tuple_to_list(State)))).
get_active_n(Zone, Listener) ->
case emqx_config:get([zones, Zone, listeners, Listener, type]) of
quic -> 100;
_ -> emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n])
end.

View File

@ -45,16 +45,16 @@
-define(FLAPPING_DURATION, 60000).
-define(FLAPPING_BANNED_INTERVAL, 300000).
-define(DEFAULT_DETECT_POLICY,
#{threshold => ?FLAPPING_THRESHOLD,
duration => ?FLAPPING_DURATION,
banned_interval => ?FLAPPING_BANNED_INTERVAL
#{max_count => ?FLAPPING_THRESHOLD,
window_time => ?FLAPPING_DURATION,
ban_time => ?FLAPPING_BANNED_INTERVAL
}).
-record(flapping, {
clientid :: emqx_types:clientid(),
peerhost :: emqx_types:peerhost(),
started_at :: pos_integer(),
detect_cnt :: pos_integer()
detect_cnt :: integer()
}).
-opaque(flapping() :: #flapping{}).
@ -69,33 +69,28 @@ stop() -> gen_server:stop(?MODULE).
%% @doc Detect flapping when a MQTT client disconnected.
-spec(detect(emqx_types:clientinfo()) -> boolean()).
detect(Client) -> detect(Client, get_policy()).
detect(#{clientid := ClientId, peerhost := PeerHost}, Policy = #{threshold := Threshold}) ->
try ets:update_counter(?FLAPPING_TAB, ClientId, {#flapping.detect_cnt, 1}) of
detect(#{clientid := ClientId, peerhost := PeerHost, zone := Zone}) ->
Policy = #{max_count := Threshold} = get_policy(Zone),
%% The initial flapping record sets the detect_cnt to 0.
InitVal = #flapping{
clientid = ClientId,
peerhost = PeerHost,
started_at = erlang:system_time(millisecond),
detect_cnt = 0
},
case ets:update_counter(?FLAPPING_TAB, ClientId, {#flapping.detect_cnt, 1}, InitVal) of
Cnt when Cnt < Threshold -> false;
_Cnt -> case ets:take(?FLAPPING_TAB, ClientId) of
_Cnt ->
case ets:take(?FLAPPING_TAB, ClientId) of
[Flapping] ->
ok = gen_server:cast(?MODULE, {detected, Flapping, Policy}),
true;
[] -> false
end
catch
error:badarg ->
%% Create a flapping record.
Flapping = #flapping{clientid = ClientId,
peerhost = PeerHost,
started_at = erlang:system_time(millisecond),
detect_cnt = 1
},
true = ets:insert(?FLAPPING_TAB, Flapping),
false
end.
-compile({inline, [get_policy/0, now_diff/1]}).
get_policy() ->
emqx:get_env(flapping_detect_policy, ?DEFAULT_DETECT_POLICY).
get_policy(Zone) ->
emqx_config:get_zone_conf(Zone, [flapping_detect]).
now_diff(TS) -> erlang:system_time(millisecond) - TS.
@ -105,11 +100,12 @@ now_diff(TS) -> erlang:system_time(millisecond) - TS.
init([]) ->
ok = emqx_tables:new(?FLAPPING_TAB, [public, set,
{keypos, 2},
{keypos, #flapping.clientid},
{read_concurrency, true},
{write_concurrency, true}
]),
{ok, ensure_timer(#{}), hibernate}.
start_timers(),
{ok, #{}, hibernate}.
handle_call(Req, _From, State) ->
?LOG(error, "Unexpected call: ~p", [Req]),
@ -119,17 +115,17 @@ handle_cast({detected, #flapping{clientid = ClientId,
peerhost = PeerHost,
started_at = StartedAt,
detect_cnt = DetectCnt},
#{duration := Duration, banned_interval := Interval}}, State) ->
case now_diff(StartedAt) < Duration of
#{window_time := WindTime, ban_time := Interval}}, State) ->
case now_diff(StartedAt) < WindTime of
true -> %% Flapping happened:(
?LOG(error, "Flapping detected: ~s(~s) disconnected ~w times in ~wms",
[ClientId, inet:ntoa(PeerHost), DetectCnt, Duration]),
[ClientId, inet:ntoa(PeerHost), DetectCnt, WindTime]),
Now = erlang:system_time(second),
Banned = #banned{who = {clientid, ClientId},
by = <<"flapping detector">>,
reason = <<"flapping is detected">>,
at = Now,
until = Now + Interval},
until = Now + (Interval div 1000)},
emqx_banned:create(Banned);
false ->
?LOG(warning, "~s(~s) disconnected ~w times in ~wms",
@ -141,11 +137,13 @@ handle_cast(Msg, State) ->
?LOG(error, "Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({timeout, TRef, expired_detecting}, State = #{expired_timer := TRef}) ->
Timestamp = erlang:system_time(millisecond) - maps:get(duration, get_policy()),
handle_info({timeout, _TRef, {garbage_collect, Zone}}, State) ->
Timestamp = erlang:system_time(millisecond)
- maps:get(window_time, get_policy(Zone)),
MatchSpec = [{{'_', '_', '_', '$1', '_'},[{'<', '$1', Timestamp}], [true]}],
ets:select_delete(?FLAPPING_TAB, MatchSpec),
{noreply, ensure_timer(State), hibernate};
start_timer(Zone),
{noreply, State, hibernate};
handle_info(Info, State) ->
?LOG(error, "Unexpected info: ~p", [Info]),
@ -157,7 +155,11 @@ terminate(_Reason, _State) ->
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
ensure_timer(State) ->
Timeout = maps:get(duration, get_policy()),
TRef = emqx_misc:start_timer(Timeout, expired_detecting),
State#{expired_timer => TRef}.
start_timer(Zone) ->
WindTime = maps:get(window_time, get_policy(Zone)),
emqx_misc:start_timer(WindTime, {garbage_collect, Zone}).
start_timers() ->
lists:foreach(fun({Zone, _ZoneConf}) ->
start_timer(Zone)
end, maps:to_list(emqx_config:get([zones], #{}))).

View File

@ -81,11 +81,7 @@ initial_parse_state() ->
-spec(initial_parse_state(options()) -> {none, options()}).
initial_parse_state(Options) when is_map(Options) ->
?none(merge_opts(Options)).
%% @pivate
merge_opts(Options) ->
maps:merge(?DEFAULT_OPTIONS, Options).
?none(maps:merge(?DEFAULT_OPTIONS, Options)).
%%--------------------------------------------------------------------
%% Parse MQTT Frame
@ -643,7 +639,7 @@ serialize_properties(Props) when is_map(Props) ->
Bin = << <<(serialize_property(Prop, Val))/binary>> || {Prop, Val} <- maps:to_list(Props) >>,
[serialize_variable_byte_integer(byte_size(Bin)), Bin].
serialize_property(_, undefined) ->
serialize_property(_, Disabled) when Disabled =:= disabled; Disabled =:= undefined ->
<<>>;
serialize_property('Payload-Format-Indicator', Val) ->
<<16#01, Val>>;

View File

@ -85,9 +85,9 @@ code_change(_OldVsn, State, _Extra) ->
%%--------------------------------------------------------------------
ensure_timer(State) ->
case emqx:get_env(global_gc_interval) of
case emqx_config:get([node, global_gc_interval]) of
undefined -> State;
Interval -> TRef = emqx_misc:start_timer(timer:seconds(Interval), run),
Interval -> TRef = emqx_misc:start_timer(Interval, run),
State#{timer := TRef}
end.

View File

@ -20,6 +20,7 @@
-include("logger.hrl").
-include("types.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-logger_header("[Hooks]").
@ -32,6 +33,8 @@
, add/3
, add/4
, put/2
, put/3
, put/4
, del/2
, run/2
, run_fold/3
@ -130,11 +133,25 @@ add(HookPoint, Action, Filter, Priority) when is_integer(Priority) ->
%% @doc Like add/2, it register a callback, discard 'already_exists' error.
-spec(put(hookpoint(), action() | #callback{}) -> ok).
put(HookPoint, Callback) ->
put(HookPoint, Callback) when is_record(Callback, callback) ->
case add(HookPoint, Callback) of
ok -> ok;
{error, already_exists} -> ok
end.
{error, already_exists} ->
gen_server:call(?SERVER, {put, HookPoint, Callback}, infinity)
end;
put(HookPoint, Action) when is_function(Action); is_tuple(Action) ->
?MODULE:put(HookPoint, #callback{action = Action, priority = 0}).
-spec(put(hookpoint(), action(), filter() | integer() | list()) -> ok).
put(HookPoint, Action, {_M, _F, _A} = Filter) ->
?MODULE:put(HookPoint, #callback{action = Action, filter = Filter, priority = 0});
put(HookPoint, Action, Priority) when is_integer(Priority) ->
?MODULE:put(HookPoint, #callback{action = Action, priority = Priority}).
-spec(put(hookpoint(), action(), filter(), integer()) -> ok).
put(HookPoint, Action, Filter, Priority) when is_integer(Priority) ->
?MODULE:put(HookPoint, #callback{action = Action, filter = Filter, priority = Priority}).
%% @doc Unregister a callback.
-spec(del(hookpoint(), action() | {module(), atom()}) -> ok).
@ -215,15 +232,20 @@ init([]) ->
ok = emqx_tables:new(?TAB, [{keypos, #hook.name}, {read_concurrency, true}]),
{ok, #{}}.
handle_call({add, HookPoint, Callback = #callback{action = Action}}, _From, State) ->
Reply = case lists:keymember(Action, #callback.action, Callbacks = lookup(HookPoint)) of
true ->
{error, already_exists};
false ->
insert_hook(HookPoint, add_callback(Callback, Callbacks))
handle_call({add, HookPoint, Callback = #callback{action = {M, F, _}}}, _From, State) ->
Reply = case lists:any(fun (#callback{action = {M0, F0, _}}) ->
M0 =:= M andalso F0 =:= F
end, Callbacks = lookup(HookPoint)) of
true -> {error, already_exists};
false -> insert_hook(HookPoint, add_callback(Callback, Callbacks))
end,
{reply, Reply, State};
handle_call({put, HookPoint, Callback = #callback{action = {M, F, _}}}, _From, State) ->
Callbacks = del_callback({M, F}, lookup(HookPoint)),
Reply = update_hook(HookPoint, add_callback(Callback, Callbacks)),
{reply, Reply, State};
handle_call(Req, _From, State) ->
?LOG(error, "Unexpected call: ~p", [Req]),
{reply, ignored, State}.
@ -257,6 +279,10 @@ code_change(_OldVsn, State, _Extra) ->
insert_hook(HookPoint, Callbacks) ->
ets:insert(?TAB, #hook{name = HookPoint, callbacks = Callbacks}), ok.
update_hook(HookPoint, Callbacks) ->
Ms = ets:fun2ms(fun ({hook, K, V}) when K =:= HookPoint -> {hook, K, Callbacks} end),
ets:select_replace(emqx_hooks, Ms),
ok.
add_callback(C, Callbacks) ->
add_callback(C, Callbacks, []).

View File

@ -27,14 +27,14 @@ start_link() ->
init([]) ->
{ok, {{one_for_one, 10, 100},
[child_spec(emqx_global_gc, worker),
child_spec(emqx_pool_sup, supervisor),
child_spec(emqx_hooks, worker),
child_spec(emqx_stats, worker),
child_spec(emqx_metrics, worker),
child_spec(emqx_ctl, worker),
child_spec(emqx_zone, worker),
child_spec(emqx_config_handler, worker)
%% always start emqx_config_handler first to load the emqx.conf to emqx_config
[ child_spec(emqx_config_handler, worker)
, child_spec(emqx_global_gc, worker)
, child_spec(emqx_pool_sup, supervisor)
, child_spec(emqx_hooks, worker)
, child_spec(emqx_stats, worker)
, child_spec(emqx_metrics, worker)
, child_spec(emqx_ctl, worker)
]}}.
child_spec(M, Type) ->

View File

@ -27,7 +27,7 @@
-record(limiter, {
%% Zone
zone :: emqx_zone:zone(),
zone :: atom(),
%% Checkers
checkers :: [checker()]
}).
@ -35,7 +35,7 @@
-type(checker() :: #{ name := name()
, capacity := non_neg_integer()
, interval := non_neg_integer()
, consumer := esockd_rate_limit:bucket() | emqx_zone:zone()
, consumer := esockd_rate_limit:bucket() | atom()
}).
-type(name() :: conn_bytes_in
@ -59,7 +59,7 @@
%% APIs
%%--------------------------------------------------------------------
-spec(init(emqx_zone:zone(),
-spec(init(atom(),
maybe(esockd_rate_limit:config()),
maybe(esockd_rate_limit:config()), policy())
-> maybe(limiter())).
@ -69,7 +69,7 @@ init(Zone, PubLimit, BytesIn, Specs) ->
Filtered = maps:filter(fun(_, V) -> V /= undefined end, Merged),
init(Zone, maps:to_list(Filtered)).
-spec(init(emqx_zone:zone(), policy()) -> maybe(limiter())).
-spec(init(atom(), policy()) -> maybe(limiter())).
init(_Zone, []) ->
undefined;
init(Zone, Specs) ->

View File

@ -20,97 +20,96 @@
-include("emqx_mqtt.hrl").
%% APIs
-export([ start/0
, ensure_all_started/0
-export([ list/0
, start/0
, restart/0
, stop/0
, is_running/1
]).
-export([ start_listener/1
, start_listener/3
, stop_listener/1
, stop_listener/3
, restart_listener/1
, restart_listener/3
]).
-export([ find_id_by_listen_on/1
, find_by_listen_on/1
, find_by_id/1
, identifier/1
, format_listen_on/1
]).
-spec(list() -> [{ListenerId :: atom(), ListenerConf :: map()}]).
list() ->
Zones = maps:to_list(emqx_config:get([zones], #{})),
lists:append([list(ZoneName, ZoneConf) || {ZoneName, ZoneConf} <- Zones]).
-type(listener() :: #{ name := binary()
, proto := esockd:proto()
, listen_on := esockd:listen_on()
, opts := [esockd:option()]
}).
list(ZoneName, ZoneConf) ->
Listeners = maps:to_list(maps:get(listeners, ZoneConf, #{})),
[
begin
ListenerId = listener_id(ZoneName, LName),
Running = is_running(ListenerId),
Conf = merge_zone_and_listener_confs(ZoneConf, LConf),
{ListenerId, maps:put(running, Running, Conf)}
end
|| {LName, LConf} <- Listeners].
%% @doc Find listener identifier by listen-on.
%% Return empty string (binary) if listener is not found in config.
-spec(find_id_by_listen_on(esockd:listen_on()) -> binary() | false).
find_id_by_listen_on(ListenOn) ->
case find_by_listen_on(ListenOn) of
false -> false;
L -> identifier(L)
-spec is_running(ListenerId :: atom()) -> boolean() | {error, no_found}.
is_running(ListenerId) ->
Zones = maps:to_list(emqx_config:get([zones], #{})),
Listeners = lists:append(
[
[{listener_id(ZoneName, LName),merge_zone_and_listener_confs(ZoneConf, LConf)}
|| {LName, LConf} <- maps:to_list(maps:get(listeners, ZoneConf, #{}))]
|| {ZoneName, ZoneConf} <- Zones]),
case proplists:get_value(ListenerId, Listeners, undefined) of
undefined ->
{error, no_found};
Conf ->
is_running(ListenerId, Conf)
end.
%% @doc Find listener by listen-on.
%% Return 'false' if not found.
-spec(find_by_listen_on(esockd:listen_on()) -> listener() | false).
find_by_listen_on(ListenOn) ->
find_by_listen_on(ListenOn, emqx:get_env(listeners, [])).
is_running(ListenerId, #{type := tcp, bind := ListenOn})->
try esockd:listener({ListenerId, ListenOn}) of
Pid when is_pid(Pid)->
true
catch _:_ ->
false
end;
%% @doc Find listener by identifier.
%% Return 'false' if not found.
-spec(find_by_id(string() | binary()) -> listener() | false).
find_by_id(Id) ->
find_by_id(iolist_to_binary(Id), emqx:get_env(listeners, [])).
is_running(ListenerId, #{type := ws})->
try
Info = ranch:info(ListenerId),
proplists:get_value(status, Info) =:= running
catch _:_ ->
false
end;
%% @doc Return the ID of the given listener.
-spec identifier(listener()) -> binary().
identifier(#{proto := Proto, name := Name}) ->
identifier(Proto, Name).
is_running(_ListenerId, #{type := quic})->
%% TODO: quic support
{error, no_found}.
%% @doc Start all listeners.
-spec(start() -> ok).
start() ->
lists:foreach(fun start_listener/1, emqx:get_env(listeners, [])).
foreach_listeners(fun start_listener/3).
%% @doc Ensure all configured listeners are started.
%% Raise exception if any of them failed to start.
-spec(ensure_all_started() -> ok).
ensure_all_started() ->
ensure_all_started(emqx:get_env(listeners, []), []).
-spec start_listener(atom()) -> ok | {error, term()}.
start_listener(ListenerId) ->
apply_on_listener(ListenerId, fun start_listener/3).
ensure_all_started([], []) -> ok;
ensure_all_started([], Failed) -> error(Failed);
ensure_all_started([L | Rest], Results) ->
#{proto := Proto, listen_on := ListenOn, opts := Options} = L,
NewResults =
case start_listener(Proto, ListenOn, Options) of
{ok, _Pid} ->
Results;
{error, {already_started, _Pid}} ->
Results;
{error, Reason} ->
[{identifier(L), Reason} | Results]
end,
ensure_all_started(Rest, NewResults).
%% @doc Format address:port for logging.
-spec(format_listen_on(esockd:listen_on()) -> [char()]).
format_listen_on(ListenOn) -> format(ListenOn).
-spec(start_listener(listener()) -> ok).
start_listener(#{proto := Proto, name := Name, listen_on := ListenOn, opts := Options}) ->
ID = identifier(Proto, Name),
case start_listener(Proto, ListenOn, Options) of
-spec start_listener(atom(), atom(), map()) -> ok | {error, term()}.
start_listener(ZoneName, ListenerName, #{type := Type, bind := Bind} = Conf) ->
case do_start_listener(ZoneName, ListenerName, Conf) of
{ok, {skipped, Reason}} when Reason =:= listener_disabled;
Reason =:= quic_app_missing ->
console_print("- Skip - starting ~s listener ~s on ~s ~n due to ~p",
[Type, listener_id(ZoneName, ListenerName), format(Bind), Reason]);
{ok, _} ->
console_print("Start ~s listener on ~s successfully.~n", [ID, format(ListenOn)]);
console_print("Start ~s listener ~s on ~s successfully.~n",
[Type, listener_id(ZoneName, ListenerName), format(Bind)]);
{error, {already_started, Pid}} ->
{error, {already_started, Pid}};
{error, Reason} ->
io:format(standard_error, "Failed to start mqtt listener ~s on ~s: ~0p~n",
[ID, format(ListenOn), Reason]),
io:format(standard_error, "Failed to start ~s listener ~s on ~s: ~0p~n",
[Type, listener_id(ZoneName, ListenerName), format(Bind), Reason]),
error(Reason)
end.
@ -122,124 +121,136 @@ console_print(_Fmt, _Args) -> ok.
-endif.
%% Start MQTT/TCP listener
-spec(start_listener(esockd:proto(), esockd:listen_on(), [esockd:option()])
-> {ok, pid()} | {error, term()}).
start_listener(tcp, ListenOn, Options) ->
start_mqtt_listener('mqtt:tcp', ListenOn, Options);
%% Start MQTT/TLS listener
start_listener(Proto, ListenOn, Options) when Proto == ssl; Proto == tls ->
start_mqtt_listener('mqtt:ssl', ListenOn, Options);
-spec(do_start_listener(atom(), atom(), map())
-> {ok, pid() | {skipped, atom()}} | {error, term()}).
do_start_listener(_ZoneName, _ListenerName, #{enabled := false}) ->
{ok, {skipped, listener_disabled}};
do_start_listener(ZoneName, ListenerName, #{type := tcp, bind := ListenOn} = Opts) ->
esockd:open(listener_id(ZoneName, ListenerName), ListenOn, merge_default(esockd_opts(Opts)),
{emqx_connection, start_link,
[#{zone => ZoneName, listener => ListenerName}]});
%% Start MQTT/WS listener
start_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws ->
start_http_listener(fun cowboy:start_clear/3, 'mqtt:ws', ListenOn,
ranch_opts(Options), ws_opts(Options));
do_start_listener(ZoneName, ListenerName, #{type := ws, bind := ListenOn} = Opts) ->
Id = listener_id(ZoneName, ListenerName),
RanchOpts = ranch_opts(ListenOn, Opts),
WsOpts = ws_opts(ZoneName, ListenerName, Opts),
case is_ssl(Opts) of
false ->
cowboy:start_clear(Id, RanchOpts, WsOpts);
true ->
cowboy:start_tls(Id, RanchOpts, WsOpts)
end;
%% Start MQTT/WSS listener
start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss ->
start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn,
ranch_opts(Options), ws_opts(Options)).
replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)].
drop_tls13_for_old_otp(Options) ->
case proplists:get_value(ssl_options, Options) of
undefined -> Options;
SslOpts ->
SslOpts1 = emqx_tls_lib:drop_tls13_for_old_otp(SslOpts),
replace(Options, ssl_options, SslOpts1)
%% Start MQTT/QUIC listener
do_start_listener(ZoneName, ListenerName, #{type := quic, bind := ListenOn} = Opts) ->
case [ A || {quicer, _, _} = A<-application:which_applications() ] of
[_] ->
%% @fixme unsure why we need reopen lib and reopen config.
quicer_nif:open_lib(),
quicer_nif:reg_open(),
DefAcceptors = erlang:system_info(schedulers_online) * 8,
ListenOpts = [ {cert, maps:get(certfile, Opts)}
, {key, maps:get(keyfile, Opts)}
, {alpn, ["mqtt"]}
, {conn_acceptors, maps:get(acceptors, Opts, DefAcceptors)}
, {idle_timeout_ms, emqx_config:get_zone_conf(ZoneName, [mqtt, idle_timeout])}
],
ConnectionOpts = #{conn_callback => emqx_quic_connection
, peer_unidi_stream_count => 1
, peer_bidi_stream_count => 10
, zone => ZoneName
, listener => ListenerName
},
StreamOpts = [],
quicer:start_listener(listener_id(ZoneName, ListenerName),
port(ListenOn), {ListenOpts, ConnectionOpts, StreamOpts});
[] ->
{ok, {skipped, quic_app_missing}}
end.
start_mqtt_listener(Name, ListenOn, Options0) ->
Options = drop_tls13_for_old_otp(Options0),
SockOpts = esockd:parse_opt(Options),
esockd:open(Name, ListenOn, merge_default(SockOpts),
{emqx_connection, start_link, [Options -- SockOpts]}).
esockd_opts(Opts0) ->
Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0),
Opts2 = case emqx_map_lib:deep_get([rate_limit, max_conn_rate], Opts0) of
infinity -> Opts1;
Rate -> Opts1#{max_conn_rate => Rate}
end,
Opts3 = Opts2#{access_rules => esockd_access_rules(maps:get(access_rules, Opts0, []))},
maps:to_list(case is_ssl(Opts0) of
false ->
Opts3#{tcp_options => tcp_opts(Opts0)};
true ->
Opts3#{ssl_options => ssl_opts(Opts0), tcp_options => tcp_opts(Opts0)}
end).
start_http_listener(Start, Name, ListenOn, RanchOpts, ProtoOpts) ->
Start(ws_name(Name, ListenOn), with_port(ListenOn, RanchOpts), ProtoOpts).
mqtt_path(Options) ->
proplists:get_value(mqtt_path, Options, "/mqtt").
ws_opts(Options) ->
WsPaths = [{mqtt_path(Options), emqx_ws_connection, Options}],
ws_opts(ZoneName, ListenerName, Opts) ->
WsPaths = [{maps:get(mqtt_path, Opts, "/mqtt"), emqx_ws_connection,
#{zone => ZoneName, listener => ListenerName}}],
Dispatch = cowboy_router:compile([{'_', WsPaths}]),
ProxyProto = proplists:get_value(proxy_protocol, Options, false),
ProxyProto = maps:get(proxy_protocol, Opts, false),
#{env => #{dispatch => Dispatch}, proxy_header => ProxyProto}.
ranch_opts(Options0) ->
Options = drop_tls13_for_old_otp(Options0),
NumAcceptors = proplists:get_value(acceptors, Options, 4),
MaxConnections = proplists:get_value(max_connections, Options, 1024),
TcpOptions = proplists:get_value(tcp_options, Options, []),
RanchOpts = #{num_acceptors => NumAcceptors,
ranch_opts(ListenOn, Opts) ->
NumAcceptors = maps:get(acceptors, Opts, 4),
MaxConnections = maps:get(max_connections, Opts, 1024),
SocketOpts = case is_ssl(Opts) of
true -> tcp_opts(Opts) ++ proplists:delete(handshake_timeout, ssl_opts(Opts));
false -> tcp_opts(Opts)
end,
#{num_acceptors => NumAcceptors,
max_connections => MaxConnections,
socket_opts => TcpOptions},
case proplists:get_value(ssl_options, Options) of
undefined -> RanchOpts;
SslOptions -> RanchOpts#{socket_opts => TcpOptions ++ SslOptions}
end.
handshake_timeout => maps:get(handshake_timeout, Opts, 15000),
socket_opts => ip_port(ListenOn) ++
%% cowboy don't allow us to set 'reuseaddr'
proplists:delete(reuseaddr, SocketOpts)}.
with_port(Port, Opts = #{socket_opts := SocketOption}) when is_integer(Port) ->
Opts#{socket_opts => [{port, Port}| SocketOption]};
with_port({Addr, Port}, Opts = #{socket_opts := SocketOption}) ->
Opts#{socket_opts => [{ip, Addr}, {port, Port}| SocketOption]}.
ip_port(Port) when is_integer(Port) ->
[{port, Port}];
ip_port({Addr, Port}) ->
[{ip, Addr}, {port, Port}].
port(Port) when is_integer(Port) -> Port;
port({_Addr, Port}) when is_integer(Port) -> Port.
esockd_access_rules(StrRules) ->
Access = fun(S) ->
[A, CIDR] = string:tokens(S, " "),
{list_to_atom(A), case CIDR of "all" -> all; _ -> CIDR end}
end,
[Access(R) || R <- StrRules].
%% @doc Restart all listeners
-spec(restart() -> ok).
restart() ->
lists:foreach(fun restart_listener/1, emqx:get_env(listeners, [])).
foreach_listeners(fun restart_listener/3).
-spec(restart_listener(listener() | string() | binary()) -> ok | {error, any()}).
restart_listener(#{proto := Proto, listen_on := ListenOn, opts := Options}) ->
restart_listener(Proto, ListenOn, Options);
restart_listener(Identifier) ->
case emqx_listeners:find_by_id(Identifier) of
false -> {error, {no_such_listener, Identifier}};
Listener -> restart_listener(Listener)
-spec(restart_listener(atom()) -> ok | {error, term()}).
restart_listener(ListenerId) ->
apply_on_listener(ListenerId, fun restart_listener/3).
-spec(restart_listener(atom(), atom(), map()) -> ok | {error, term()}).
restart_listener(ZoneName, ListenerName, Conf) ->
case stop_listener(ZoneName, ListenerName, Conf) of
ok -> start_listener(ZoneName, ListenerName, Conf);
Error -> Error
end.
-spec(restart_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) ->
ok | {error, any()}).
restart_listener(tcp, ListenOn, _Options) ->
esockd:reopen('mqtt:tcp', ListenOn);
restart_listener(Proto, ListenOn, _Options) when Proto == ssl; Proto == tls ->
esockd:reopen('mqtt:ssl', ListenOn);
restart_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws ->
_ = cowboy:stop_listener(ws_name('mqtt:ws', ListenOn)),
ok(start_listener(Proto, ListenOn, Options));
restart_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss ->
_ = cowboy:stop_listener(ws_name('mqtt:wss', ListenOn)),
ok(start_listener(Proto, ListenOn, Options));
restart_listener(Proto, ListenOn, _Opts) ->
esockd:reopen(Proto, ListenOn).
ok({ok, _}) -> ok;
ok(Other) -> Other.
%% @doc Stop all listeners.
-spec(stop() -> ok).
stop() ->
lists:foreach(fun stop_listener/1, emqx:get_env(listeners, [])).
foreach_listeners(fun stop_listener/3).
-spec(stop_listener(listener()) -> ok | {error, term()}).
stop_listener(#{proto := Proto, listen_on := ListenOn, opts := Opts}) ->
stop_listener(Proto, ListenOn, Opts).
-spec(stop_listener(atom()) -> ok | {error, term()}).
stop_listener(ListenerId) ->
apply_on_listener(ListenerId, fun stop_listener/3).
-spec(stop_listener(esockd:proto(), esockd:listen_on(), [esockd:option()])
-> ok | {error, term()}).
stop_listener(tcp, ListenOn, _Opts) ->
esockd:close('mqtt:tcp', ListenOn);
stop_listener(Proto, ListenOn, _Opts) when Proto == ssl; Proto == tls ->
esockd:close('mqtt:ssl', ListenOn);
stop_listener(Proto, ListenOn, _Opts) when Proto == http; Proto == ws ->
cowboy:stop_listener(ws_name('mqtt:ws', ListenOn));
stop_listener(Proto, ListenOn, _Opts) when Proto == https; Proto == wss ->
cowboy:stop_listener(ws_name('mqtt:wss', ListenOn));
stop_listener(Proto, ListenOn, _Opts) ->
esockd:close(Proto, ListenOn).
-spec(stop_listener(atom(), atom(), map()) -> ok | {error, term()}).
stop_listener(ZoneName, ListenerName, #{type := tcp, bind := ListenOn}) ->
esockd:close(listener_id(ZoneName, ListenerName), ListenOn);
stop_listener(ZoneName, ListenerName, #{type := ws}) ->
cowboy:stop_listener(listener_id(ZoneName, ListenerName));
stop_listener(ZoneName, ListenerName, #{type := quic}) ->
quicer:stop_listener(listener_id(ZoneName, ListenerName)).
merge_default(Options) ->
case lists:keytake(tcp_options, 1, Options) of
@ -256,23 +267,46 @@ format({Addr, Port}) when is_list(Addr) ->
format({Addr, Port}) when is_tuple(Addr) ->
io_lib:format("~s:~w", [inet:ntoa(Addr), Port]).
ws_name(Name, {_Addr, Port}) ->
ws_name(Name, Port);
ws_name(Name, Port) ->
list_to_atom(lists:concat([Name, ":", Port])).
listener_id(ZoneName, ListenerName) ->
list_to_atom(lists:append([atom_to_list(ZoneName), ":", atom_to_list(ListenerName)])).
identifier(Proto, Name) when is_atom(Proto) ->
identifier(atom_to_list(Proto), Name);
identifier(Proto, Name) ->
iolist_to_binary(["mqtt", ":", Proto, ":", Name]).
find_by_listen_on(_ListenOn, []) -> false;
find_by_listen_on(ListenOn, [#{listen_on := ListenOn} = L | _]) -> L;
find_by_listen_on(ListenOn, [_ | Rest]) -> find_by_listen_on(ListenOn, Rest).
find_by_id(_Id, []) -> false;
find_by_id(Id, [L | Rest]) ->
case identifier(L) =:= Id of
true -> L;
false -> find_by_id(Id, Rest)
decode_listener_id(Id) ->
case string:split(atom_to_list(Id), ":", leading) of
[Zone, Listen] -> {list_to_atom(Zone), list_to_atom(Listen)};
_ -> error({invalid_listener_id, Id})
end.
ssl_opts(Opts) ->
maps:to_list(
emqx_tls_lib:drop_tls13_for_old_otp(
maps:without([enable],
maps:get(ssl, Opts, #{})))).
tcp_opts(Opts) ->
maps:to_list(
maps:without([active_n],
maps:get(tcp, Opts, #{}))).
is_ssl(Opts) ->
emqx_map_lib:deep_get([ssl, enable], Opts, false).
foreach_listeners(Do) ->
lists:foreach(fun({ZoneName, ZoneConf}) ->
lists:foreach(fun({LName, LConf}) ->
Do(ZoneName, LName, merge_zone_and_listener_confs(ZoneConf, LConf))
end, maps:to_list(maps:get(listeners, ZoneConf, #{})))
end, maps:to_list(emqx_config:get([zones], #{}))).
%% merge the configs in zone and listeners in a manner that
%% all config entries in the listener are prior to the ones in the zone.
merge_zone_and_listener_confs(ZoneConf, ListenerConf) ->
ConfsInZonesOnly = [listeners, overall_max_connections],
BaseConf = maps:without(ConfsInZonesOnly, ZoneConf),
emqx_map_lib:deep_merge(BaseConf, ListenerConf).
apply_on_listener(ListenerId, Do) ->
{ZoneName, ListenerName} = decode_listener_id(ListenerId),
case emqx_config:find_listener_conf(ZoneName, ListenerName, []) of
{not_found, _, _} -> error({listener_config_not_found, ZoneName, ListenerName});
{ok, Conf} -> Do(ZoneName, ListenerName, Conf)
end.

View File

@ -0,0 +1,117 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_map_lib).
-export([ deep_get/2
, deep_get/3
, deep_find/2
, deep_put/3
, deep_remove/2
, deep_merge/2
, safe_atom_key_map/1
, unsafe_atom_key_map/1
]).
-export_type([config_key/0, config_key_path/0]).
-type config_key() :: atom() | binary().
-type config_key_path() :: [config_key()].
%%-----------------------------------------------------------------
-spec deep_get(config_key_path(), map()) -> term().
deep_get(ConfKeyPath, Map) ->
Ref = make_ref(),
Res = deep_get(ConfKeyPath, Map, Ref),
case Res =:= Ref of
true -> error({config_not_found, ConfKeyPath});
false -> Res
end.
-spec deep_get(config_key_path(), map(), term()) -> term().
deep_get(ConfKeyPath, Map, Default) ->
case deep_find(ConfKeyPath, Map) of
{not_found, _KeyPath, _Data} -> Default;
{ok, Data} -> Data
end.
-spec deep_find(config_key_path(), map()) ->
{ok, term()} | {not_found, config_key_path(), term()}.
deep_find([], Map) ->
{ok, Map};
deep_find([Key | KeyPath] = Path, Map) when is_map(Map) ->
case maps:find(Key, Map) of
{ok, SubMap} -> deep_find(KeyPath, SubMap);
error -> {not_found, Path, Map}
end;
deep_find(_KeyPath, Data) ->
{not_found, _KeyPath, Data}.
-spec deep_put(config_key_path(), map(), term()) -> map().
deep_put([], Map, Config) when is_map(Map) ->
Config;
deep_put([], _Map, Config) -> %% not map, replace it
Config;
deep_put([Key | KeyPath], Map, Config) ->
SubMap = deep_put(KeyPath, maps:get(Key, Map, #{}), Config),
Map#{Key => SubMap}.
-spec deep_remove(config_key_path(), map()) -> map().
deep_remove([], Map) ->
Map;
deep_remove([Key], Map) ->
maps:remove(Key, Map);
deep_remove([Key | KeyPath], Map) ->
case maps:find(Key, Map) of
{ok, SubMap} when is_map(SubMap) ->
Map#{Key => deep_remove(KeyPath, SubMap)};
{ok, _Val} -> Map;
error -> Map
end.
%% #{a => #{b => 3, c => 2}, d => 4}
%% = deep_merge(#{a => #{b => 1, c => 2}, d => 4}, #{a => #{b => 3}}).
-spec deep_merge(map(), map()) -> map().
deep_merge(BaseMap, NewMap) ->
NewKeys = maps:keys(NewMap) -- maps:keys(BaseMap),
MergedBase = maps:fold(fun(K, V, Acc) ->
case maps:find(K, NewMap) of
error ->
Acc#{K => V};
{ok, NewV} when is_map(V), is_map(NewV) ->
Acc#{K => deep_merge(V, NewV)};
{ok, NewV} ->
Acc#{K => NewV}
end
end, #{}, BaseMap),
maps:merge(MergedBase, maps:with(NewKeys, NewMap)).
unsafe_atom_key_map(Map) ->
covert_keys_to_atom(Map, fun(K) -> binary_to_atom(K, utf8) end).
safe_atom_key_map(Map) ->
covert_keys_to_atom(Map, fun(K) -> binary_to_existing_atom(K, utf8) end).
%%---------------------------------------------------------------------------
covert_keys_to_atom(BinKeyMap, Conv) when is_map(BinKeyMap) ->
maps:fold(
fun(K, V, Acc) when is_binary(K) ->
Acc#{Conv(K) => covert_keys_to_atom(V, Conv)};
(K, V, Acc) when is_atom(K) ->
%% richmap keys
Acc#{K => covert_keys_to_atom(V, Conv)}
end, #{}, BinKeyMap);
covert_keys_to_atom(ListV, Conv) when is_list(ListV) ->
[covert_keys_to_atom(V, Conv) || V <- ListV];
covert_keys_to_atom(Val, _) -> Val.

View File

@ -172,7 +172,7 @@
{counter, 'client.connected'},
{counter, 'client.authenticate'},
{counter, 'client.auth.anonymous'},
{counter, 'client.check_acl'},
{counter, 'client.authorize'},
{counter, 'client.subscribe'},
{counter, 'client.unsubscribe'},
{counter, 'client.disconnected'}
@ -563,7 +563,7 @@ reserved_idx('client.connected') -> 202;
reserved_idx('client.authenticate') -> 203;
reserved_idx('client.enhanced_authenticate') -> 204;
reserved_idx('client.auth.anonymous') -> 205;
reserved_idx('client.check_acl') -> 206;
reserved_idx('client.authorize') -> 206;
reserved_idx('client.subscribe') -> 207;
reserved_idx('client.unsubscribe') -> 208;
reserved_idx('client.disconnected') -> 209;

View File

@ -197,7 +197,8 @@ check_oom(Policy) ->
check_oom(self(), Policy).
-spec(check_oom(pid(), emqx_types:oom_policy()) -> ok | {shutdown, term()}).
check_oom(Pid, #{message_queue_len := MaxQLen,
check_oom(_Pid, #{enable := false}) -> ok;
check_oom(Pid, #{max_message_queue_len := MaxQLen,
max_heap_size := MaxHeapSize}) ->
case process_info(Pid, [message_queue_len, total_heap_size]) of
undefined -> ok;
@ -214,13 +215,26 @@ do_check_oom([{Val, Max, Reason}|Rest]) ->
false -> do_check_oom(Rest)
end.
tune_heap_size(#{max_heap_size := MaxHeapSize}) ->
%% If set to zero, the limit is disabled.
erlang:process_flag(max_heap_size, #{size => MaxHeapSize,
kill => false,
tune_heap_size(#{enable := false}) ->
ok;
%% If the max_heap_size is set to zero, the limit is disabled.
tune_heap_size(#{max_heap_size := MaxHeapSize}) when MaxHeapSize > 0 ->
MaxSize = case erlang:system_info(wordsize) of
8 -> % arch_64
(1 bsl 59) - 1;
4 -> % arch_32
(1 bsl 27) - 1
end,
OverflowedSize = case erlang:trunc(MaxHeapSize * 1.5) of
SZ when SZ > MaxSize -> MaxSize;
SZ -> SZ
end,
erlang:process_flag(max_heap_size, #{
size => OverflowedSize,
kill => true,
error_logger => true
});
tune_heap_size(undefined) -> ok.
}).
-spec(proc_name(atom(), pos_integer()) -> atom()).
proc_name(Mod, Id) ->

View File

@ -25,14 +25,8 @@
]).
-export([ get_caps/1
, get_caps/2
, get_caps/3
]).
-export([default_caps/0]).
-export([default/0]).
-export_type([caps/0]).
-type(caps() :: #{max_packet_size => integer(),
@ -46,7 +40,7 @@
shared_subscription => boolean()
}).
-define(UNLIMITED, 0).
-define(MAX_TOPIC_LEVELS, 65535).
-define(PUBCAP_KEYS, [max_topic_levels,
max_qos_allowed,
@ -62,7 +56,7 @@
-define(DEFAULT_CAPS, #{max_packet_size => ?MAX_PACKET_SIZE,
max_clientid_len => ?MAX_CLIENTID_LEN,
max_topic_alias => ?MAX_TOPIC_AlIAS,
max_topic_levels => ?UNLIMITED,
max_topic_levels => ?MAX_TOPIC_LEVELS,
max_qos_allowed => ?QOS_2,
retain_available => true,
wildcard_subscription => true,
@ -81,7 +75,7 @@ check_pub(Zone, Flags) when is_map(Flags) ->
Flags1#{topic_levels => emqx_topic:levels(Topic)};
error ->
Flags
end, get_caps(Zone, publish)).
end, maps:with(?PUBCAP_KEYS, get_caps(Zone))).
do_check_pub(#{topic_levels := Levels}, #{max_topic_levels := Limit})
when Limit > 0, Levels > Limit ->
@ -98,7 +92,7 @@ do_check_pub(_Flags, _Caps) -> ok.
emqx_types:subopts())
-> ok_or_error(emqx_types:reason_code())).
check_sub(Zone, Topic, SubOpts) ->
Caps = get_caps(Zone, subscribe),
Caps = maps:with(?SUBCAP_KEYS, get_caps(Zone)),
Flags = lists:foldl(
fun(max_topic_levels, Map) ->
Map#{topic_levels => emqx_topic:levels(Topic)};
@ -119,42 +113,7 @@ do_check_sub(#{is_shared := true}, #{shared_subscription := false}) ->
{error, ?RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED};
do_check_sub(_Flags, _Caps) -> ok.
default_caps() ->
?DEFAULT_CAPS.
get_caps(Zone, Cap, Def) ->
emqx_zone:get_env(Zone, Cap, Def).
get_caps(Zone, publish) ->
with_env(Zone, '$mqtt_pub_caps',
fun() ->
filter_caps(?PUBCAP_KEYS, get_caps(Zone))
end);
get_caps(Zone, subscribe) ->
with_env(Zone, '$mqtt_sub_caps',
fun() ->
filter_caps(?SUBCAP_KEYS, get_caps(Zone))
end).
get_caps(Zone) ->
with_env(Zone, '$mqtt_caps',
fun() ->
maps:map(fun(Cap, Def) ->
emqx_zone:get_env(Zone, Cap, Def)
end, ?DEFAULT_CAPS)
end).
filter_caps(Keys, Caps) ->
maps:filter(fun(Key, _Val) -> lists:member(Key, Keys) end, Caps).
-spec(default() -> caps()).
default() -> ?DEFAULT_CAPS.
with_env(Zone, Key, InitFun) ->
case emqx_zone:get_env(Zone, Key) of
undefined -> Caps = InitFun(),
ok = emqx_zone:set_env(Zone, Key, Caps),
Caps;
ZoneCaps -> ZoneCaps
end.
lists:foldl(fun({K, V}, Acc) ->
Acc#{K => emqx_config:get_zone_conf(Zone, [mqtt, K], V)}
end, #{}, maps:to_list(?DEFAULT_CAPS)).

View File

@ -67,6 +67,8 @@
, dropped/1
]).
-define(NO_PRIORITY_TABLE, disabled).
-export_type([mqueue/0, options/0]).
-type(topic() :: emqx_topic:topic()).

View File

@ -22,15 +22,9 @@
-logger_header("[OS_MON]").
-export([start_link/1]).
-export([start_link/0]).
-export([ get_cpu_check_interval/0
, set_cpu_check_interval/1
, get_cpu_high_watermark/0
, set_cpu_high_watermark/1
, get_cpu_low_watermark/0
, set_cpu_low_watermark/1
, get_mem_check_interval/0
-export([ get_mem_check_interval/0
, set_mem_check_interval/1
, get_sysmem_high_watermark/0
, set_sysmem_high_watermark/1
@ -51,119 +45,76 @@
-define(OS_MON, ?MODULE).
start_link(Opts) ->
gen_server:start_link({local, ?OS_MON}, ?MODULE, [Opts], []).
start_link() ->
gen_server:start_link({local, ?OS_MON}, ?MODULE, [], []).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
get_cpu_check_interval() ->
call(get_cpu_check_interval).
set_cpu_check_interval(Seconds) ->
call({set_cpu_check_interval, Seconds}).
get_cpu_high_watermark() ->
call(get_cpu_high_watermark).
set_cpu_high_watermark(Float) ->
call({set_cpu_high_watermark, Float}).
get_cpu_low_watermark() ->
call(get_cpu_low_watermark).
set_cpu_low_watermark(Float) ->
call({set_cpu_low_watermark, Float}).
get_mem_check_interval() ->
memsup:get_check_interval() div 1000.
memsup:get_check_interval().
set_mem_check_interval(Seconds) when Seconds < 60 ->
set_mem_check_interval(Seconds) when Seconds < 60000 ->
memsup:set_check_interval(1);
set_mem_check_interval(Seconds) ->
memsup:set_check_interval(Seconds div 60).
memsup:set_check_interval(Seconds div 60000).
get_sysmem_high_watermark() ->
memsup:get_sysmem_high_watermark().
set_sysmem_high_watermark(Float) ->
memsup:set_sysmem_high_watermark(Float / 100).
memsup:set_sysmem_high_watermark(Float).
get_procmem_high_watermark() ->
memsup:get_procmem_high_watermark().
set_procmem_high_watermark(Float) ->
memsup:set_procmem_high_watermark(Float / 100).
call(Req) ->
gen_server:call(?OS_MON, Req, infinity).
memsup:set_procmem_high_watermark(Float).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([Opts]) ->
set_mem_check_interval(proplists:get_value(mem_check_interval, Opts)),
set_sysmem_high_watermark(proplists:get_value(sysmem_high_watermark, Opts)),
set_procmem_high_watermark(proplists:get_value(procmem_high_watermark, Opts)),
{ok, ensure_check_timer(#{cpu_high_watermark => proplists:get_value(cpu_high_watermark, Opts),
cpu_low_watermark => proplists:get_value(cpu_low_watermark, Opts),
cpu_check_interval => proplists:get_value(cpu_check_interval, Opts),
timer => undefined})}.
handle_call(get_cpu_check_interval, _From, State) ->
{reply, maps:get(cpu_check_interval, State, undefined), State};
handle_call({set_cpu_check_interval, Seconds}, _From, State) ->
{reply, ok, State#{cpu_check_interval := Seconds}};
handle_call(get_cpu_high_watermark, _From, State) ->
{reply, maps:get(cpu_high_watermark, State, undefined), State};
handle_call({set_cpu_high_watermark, Float}, _From, State) ->
{reply, ok, State#{cpu_high_watermark := Float}};
handle_call(get_cpu_low_watermark, _From, State) ->
{reply, maps:get(cpu_low_watermark, State, undefined), State};
handle_call({set_cpu_low_watermark, Float}, _From, State) ->
{reply, ok, State#{cpu_low_watermark := Float}};
init([]) ->
Opts = emqx_config:get([sysmon, os]),
set_mem_check_interval(maps:get(mem_check_interval, Opts)),
set_sysmem_high_watermark(maps:get(sysmem_high_watermark, Opts)),
set_procmem_high_watermark(maps:get(procmem_high_watermark, Opts)),
_ = start_check_timer(),
{ok, #{}}.
handle_call(Req, _From, State) ->
?LOG(error, "Unexpected call: ~p", [Req]),
{reply, ignored, State}.
{reply, {error, {unexpected_call, Req}}, State}.
handle_cast(Msg, State) ->
?LOG(error, "Unexpected cast: ~p", [Msg]),
?LOG(error, "unexpected_cast_discarded: ~p", [Msg]),
{noreply, State}.
handle_info({timeout, Timer, check}, State = #{timer := Timer,
cpu_high_watermark := CPUHighWatermark,
cpu_low_watermark := CPULowWatermark}) ->
NState =
case emqx_vm:cpu_util() of %% TODO: should be improved?
0 ->
State#{timer := undefined};
handle_info({timeout, _Timer, check}, State) ->
CPUHighWatermark = emqx_config:get([sysmon, os, cpu_high_watermark]) * 100,
CPULowWatermark = emqx_config:get([sysmon, os, cpu_low_watermark]) * 100,
_ = case emqx_vm:cpu_util() of %% TODO: should be improved?
0 -> ok;
Busy when Busy >= CPUHighWatermark ->
emqx_alarm:activate(high_cpu_usage, #{usage => Busy,
emqx_alarm:activate(high_cpu_usage, #{usage => io_lib:format("~p%", [Busy]),
high_watermark => CPUHighWatermark,
low_watermark => CPULowWatermark}),
ensure_check_timer(State);
start_check_timer();
Busy when Busy =< CPULowWatermark ->
emqx_alarm:deactivate(high_cpu_usage),
ensure_check_timer(State);
start_check_timer();
_Busy ->
ensure_check_timer(State)
start_check_timer()
end,
{noreply, NState};
{noreply, State};
handle_info(Info, State) ->
?LOG(error, "unexpected info: ~p", [Info]),
?LOG(info, "unexpected_info_discarded: ~p", [Info]),
{noreply, State}.
terminate(_Reason, #{timer := Timer}) ->
emqx_misc:cancel_timer(Timer).
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
@ -172,8 +123,9 @@ code_change(_OldVsn, State, _Extra) ->
%% Internal functions
%%--------------------------------------------------------------------
ensure_check_timer(State = #{cpu_check_interval := Interval}) ->
start_check_timer() ->
Interval = emqx_config:get([sysmon, os, cpu_check_interval]),
case erlang:system_info(system_architecture) of
"x86_64-pc-linux-musl" -> State;
_ -> State#{timer := emqx_misc:start_timer(timer:seconds(Interval), check)}
"x86_64-pc-linux-musl" -> ok;
_ -> emqx_misc:start_timer(Interval, check)
end.

View File

@ -21,8 +21,6 @@
-logger_header("[Plugins]").
-export([init/0]).
-export([ load/0
, load/1
, unload/0
@ -30,8 +28,6 @@
, reload/1
, list/0
, find_plugin/1
, generate_configs/1
, apply_configs/1
]).
-export([funlog/2]).
@ -41,35 +37,14 @@
-compile(nowarn_export_all).
-endif.
-dialyzer({no_match, [ plugin_loaded/2
, plugin_unloaded/2
]}).
%%--------------------------------------------------------------------
%% APIs
%%--------------------------------------------------------------------
%% @doc Init plugins' config
-spec(init() -> ok).
init() ->
case emqx:get_env(plugins_etc_dir) of
undefined -> ok;
PluginsEtc ->
CfgFiles = [filename:join(PluginsEtc, File) ||
File <- filelib:wildcard("*.config", PluginsEtc)],
lists:foreach(fun init_config/1, CfgFiles)
end.
%% @doc Load all plugins when the broker started.
-spec(load() -> ok | ignore | {error, term()}).
load() ->
ok = load_ext_plugins(emqx:get_env(expand_plugins_dir)),
case emqx:get_env(plugins_loaded_file) of
undefined -> ignore; %% No plugins available
File ->
_ = ensure_file(File),
with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end)
end.
ok = load_ext_plugins(emqx_config:get([plugins, expand_plugins_dir], undefined)).
%% @doc Load a Plugin
-spec(load(atom()) -> ok | {error, term()}).
@ -82,17 +57,13 @@ load(PluginName) when is_atom(PluginName) ->
?LOG(notice, "Plugin ~s is already started", [PluginName]),
{error, already_started};
{_, false} ->
load_plugin(PluginName, true)
load_plugin(PluginName)
end.
%% @doc Unload all plugins before broker stopped.
-spec(unload() -> list() | {error, term()}).
-spec(unload() -> ok).
unload() ->
case emqx:get_env(plugins_loaded_file) of
undefined -> ignore;
File ->
with_loaded_file(File, fun stop_plugins/1)
end.
stop_plugins(list()).
%% @doc UnLoad a Plugin
-spec(unload(atom()) -> ok | {error, term()}).
@ -105,7 +76,7 @@ unload(PluginName) when is_atom(PluginName) ->
?LOG(error, "Plugin ~s is not started", [PluginName]),
{error, not_started};
{_, _} ->
unload_plugin(PluginName, true)
unload_plugin(PluginName)
end.
reload(PluginName) when is_atom(PluginName)->
@ -126,8 +97,8 @@ reload(PluginName) when is_atom(PluginName)->
-spec(list() -> [emqx_types:plugin()]).
list() ->
StartedApps = names(started_app),
lists:map(fun({Name, _, [Type| _]}) ->
Plugin = plugin(Name, Type),
lists:map(fun({Name, _, _}) ->
Plugin = plugin(Name),
case lists:member(Name, StartedApps) of
true -> Plugin#plugin{active = true};
false -> Plugin
@ -144,12 +115,6 @@ find_plugin(Name, Plugins) ->
%% Internal functions
%%--------------------------------------------------------------------
init_config(CfgFile) ->
{ok, [AppsEnv]} = file:consult(CfgFile),
lists:foreach(fun({App, Envs}) ->
[application:set_env(App, Par, Val) || {Par, Val} <- Envs]
end, AppsEnv).
%% load external plugins which are placed in etc/plugins dir
load_ext_plugins(undefined) -> ok;
load_ext_plugins(Dir) ->
@ -173,15 +138,15 @@ load_ext_plugin(PluginDir) ->
?LOG(alert, "plugin_app_file_not_found: ~s", [AppFile]),
error({plugin_app_file_not_found, AppFile})
end,
ok = load_plugin_app(AppName, Ebin),
try
ok = generate_configs(AppName, PluginDir)
catch
throw : {conf_file_not_found, ConfFile} ->
%% this is maybe a dependency of an external plugin
?LOG(debug, "config_load_error_ignored for app=~p, path=~s", [AppName, ConfFile]),
ok
end.
ok = load_plugin_app(AppName, Ebin).
% try
% ok = generate_configs(AppName, PluginDir)
% catch
% throw : {conf_file_not_found, ConfFile} ->
% %% this is maybe a dependency of an external plugin
% ?LOG(debug, "config_load_error_ignored for app=~p, path=~s", [AppName, ConfFile]),
% ok
% end.
load_plugin_app(AppName, Ebin) ->
_ = code:add_patha(Ebin),
@ -199,57 +164,24 @@ load_plugin_app(AppName, Ebin) ->
{error, {already_loaded, _}} -> ok
end.
ensure_file(File) ->
case filelib:is_file(File) of false -> write_loaded([]); true -> ok end.
with_loaded_file(File, SuccFun) ->
case read_loaded(File) of
{ok, Names0} ->
Names = filter_plugins(Names0),
SuccFun(Names);
{error, Error} ->
?LOG(alert, "Failed to read: ~p, error: ~p", [File, Error]),
{error, Error}
end.
filter_plugins(Names) ->
lists:filtermap(fun(Name1) when is_atom(Name1) -> {true, Name1};
({Name1, true}) -> {true, Name1};
({_Name1, false}) -> false
end, Names).
load_plugins(Names, Persistent) ->
Plugins = list(),
NotFound = Names -- names(Plugins),
case NotFound of
[] -> ok;
NotFound -> ?LOG(alert, "cannot_find_plugins: ~p", [NotFound])
end,
NeedToLoad = Names -- NotFound -- names(started_app),
lists:foreach(fun(Name) ->
Plugin = find_plugin(Name, Plugins),
load_plugin(Plugin#plugin.name, Persistent)
end, NeedToLoad).
%% Stop plugins
stop_plugins(Names) ->
_ = [stop_app(App) || App <- Names],
stop_plugins(Plugins) ->
_ = [stop_app(Plugin#plugin.name) || Plugin <- Plugins],
ok.
plugin(AppName, Type) ->
plugin(AppName) ->
case application:get_all_key(AppName) of
{ok, Attrs} ->
Descr = proplists:get_value(description, Attrs, ""),
#plugin{name = AppName, descr = Descr, type = plugin_type(Type)};
#plugin{name = AppName, descr = Descr};
undefined -> error({plugin_not_found, AppName})
end.
load_plugin(Name, Persistent) ->
load_plugin(Name) ->
try
ok = ?MODULE:generate_configs(Name),
case load_app(Name) of
ok ->
start_app(Name, fun(App) -> plugin_loaded(App, Persistent) end);
start_app(Name);
{error, Error0} ->
{error, Error0}
end
@ -268,22 +200,21 @@ load_app(App) ->
{error, Error}
end.
start_app(App, SuccFun) ->
start_app(App) ->
case application:ensure_all_started(App) of
{ok, Started} ->
?LOG(info, "Started plugins: ~p", [Started]),
?LOG(info, "Load plugin ~s successfully", [App]),
_ = SuccFun(App),
ok;
{error, {ErrApp, Reason}} ->
?LOG(error, "Load plugin ~s failed, cannot start plugin ~s for ~0p", [App, ErrApp, Reason]),
{error, {ErrApp, Reason}}
end.
unload_plugin(App, Persistent) ->
unload_plugin(App) ->
case stop_app(App) of
ok ->
_ = plugin_unloaded(App, Persistent), ok;
ok;
{error, Reason} ->
{error, Reason}
end.
@ -307,133 +238,5 @@ names(started_app) ->
names(Plugins) ->
[Name || #plugin{name = Name} <- Plugins].
plugin_loaded(_Name, false) ->
ok;
plugin_loaded(Name, true) ->
case read_loaded() of
{ok, Names} ->
case lists:member(Name, Names) of
false ->
%% write file if plugin is loaded
write_loaded(lists:append(Names, [{Name, true}]));
true ->
ignore
end;
{error, Error} ->
?LOG(error, "Cannot read loaded plugins: ~p", [Error])
end.
plugin_unloaded(_Name, false) ->
ok;
plugin_unloaded(Name, true) ->
case read_loaded() of
{ok, Names0} ->
Names = filter_plugins(Names0),
case lists:member(Name, Names) of
true ->
write_loaded(lists:delete(Name, Names));
false ->
?LOG(error, "Cannot find ~s in loaded_file", [Name])
end;
{error, Error} ->
?LOG(error, "Cannot read loaded_plugins: ~p", [Error])
end.
read_loaded() ->
case emqx:get_env(plugins_loaded_file) of
undefined -> {error, not_found};
File -> read_loaded(File)
end.
read_loaded(File) -> file:consult(File).
write_loaded(AppNames) ->
FilePath = emqx:get_env(plugins_loaded_file),
case file:write_file(FilePath, [io_lib:format("~p.~n", [Name]) || Name <- AppNames]) of
ok -> ok;
{error, Error} ->
?LOG(error, "Write File ~p Error: ~p", [FilePath, Error]),
{error, Error}
end.
plugin_type(auth) -> auth;
plugin_type(protocol) -> protocol;
plugin_type(backend) -> backend;
plugin_type(bridge) -> bridge;
plugin_type(_) -> feature.
funlog(Key, Value) ->
?LOG(info, "~s = ~p", [string:join(Key, "."), Value]).
generate_configs(App) ->
PluginConfDir = emqx:get_env(plugins_etc_dir),
PluginSchemaDir = code:priv_dir(App),
generate_configs(App, PluginConfDir, PluginSchemaDir).
generate_configs(App, PluginDir) ->
PluginConfDir = filename:join([PluginDir, "etc"]),
PluginSchemaDir = filename:join([PluginDir, "priv"]),
generate_configs(App, PluginConfDir, PluginSchemaDir).
generate_configs(App, PluginConfDir, PluginSchemaDir) ->
ConfigFile = filename:join([PluginConfDir, App]) ++ ".config",
case filelib:is_file(ConfigFile) of
true ->
{ok, [Configs]} = file:consult(ConfigFile),
apply_configs(Configs);
false ->
SchemaFile = filename:join([PluginSchemaDir, App]) ++ ".schema",
case filelib:is_file(SchemaFile) of
true ->
AppsEnv = do_generate_configs(App),
apply_configs(AppsEnv);
false ->
SchemaMod = lists:concat([App, "_schema"]),
ConfName = filename:join([PluginConfDir, App]) ++ ".conf",
SchemaFile1 = filename:join([code:lib_dir(App), "ebin", SchemaMod]) ++ ".beam",
do_generate_hocon_configs(App, ConfName, SchemaFile1)
end
end.
do_generate_configs(App) ->
Name1 = filename:join([emqx:get_env(plugins_etc_dir), App]) ++ ".conf",
Name2 = filename:join([code:lib_dir(App), "etc", App]) ++ ".conf",
ConfFile = case {filelib:is_file(Name1), filelib:is_file(Name2)} of
{true, _} -> Name1;
{false, true} -> Name2;
{false, false} -> error({config_not_found, [Name1, Name2]})
end,
SchemaFile = filename:join([code:priv_dir(App), App]) ++ ".schema",
case filelib:is_file(SchemaFile) of
true ->
Schema = cuttlefish_schema:files([SchemaFile]),
Conf = cuttlefish_conf:file(ConfFile),
cuttlefish_generator:map(Schema, Conf, undefined, fun ?MODULE:funlog/2);
false ->
error({schema_not_found, SchemaFile})
end.
do_generate_hocon_configs(App, ConfName, SchemaFile) ->
SchemaMod = lists:concat([App, "_schema"]),
case {filelib:is_file(ConfName), filelib:is_file(SchemaFile)} of
{true, true} ->
{ok, RawConfig} = hocon:load(ConfName, #{format => richmap}),
_ = hocon_schema:check(list_to_atom(SchemaMod), RawConfig, #{atom_key => true,
return_plain => true}),
ok;
% emqx_config:update_config([App], Config);
{true, false} ->
error({schema_not_found, [SchemaFile]});
{false, true} ->
error({config_not_found, [ConfName]});
{false, false} ->
error({conf_and_schema_not_found, [ConfName, SchemaFile]})
end.
apply_configs([]) ->
ok;
apply_configs([{App, Config} | More]) ->
lists:foreach(fun({Key, _}) -> application:unset_env(App, Key) end, application:get_all_env(App)),
lists:foreach(fun({Key, Val}) -> application:set_env(App, Key, Val) end, Config),
apply_configs(More).

View File

@ -1,5 +1,5 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@ -14,10 +14,11 @@
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_gen_mod).
-module(emqx_quic_connection).
-callback(load(Opts :: any()) -> ok | {error, term()}).
%% Callbacks
-export([ new_conn/2
]).
-callback(unload(State :: term()) -> term()).
-callback(description() -> any()).
new_conn(Conn, {_L, COpts, _S}) when is_map(COpts) ->
emqx_connection:start_link(emqx_quic_stream, Conn, COpts).

View File

@ -0,0 +1,92 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% MQTT/QUIC Stream
-module(emqx_quic_stream).
%% emqx transport Callbacks
-export([ type/1
, wait/1
, getstat/2
, fast_close/1
, ensure_ok_or_exit/2
, async_send/3
, setopts/2
, getopts/2
, peername/1
, sockname/1
, peercert/1
]).
wait(Conn) ->
quicer:accept_stream(Conn, []).
type(_) ->
quic.
peername(S) ->
quicer:peername(S).
sockname(S) ->
quicer:sockname(S).
peercert(_S) ->
nossl.
getstat(Socket, Stats) ->
case quicer:getstat(Socket, Stats) of
{error, _} -> {error, closed};
Res -> Res
end.
setopts(Socket, Opts) ->
lists:foreach(fun({Opt, V}) when is_atom(Opt) ->
quicer:setopt(Socket, Opt, V);
(Opt) when is_atom(Opt) ->
quicer:setopt(Socket, Opt, true)
end, Opts),
ok.
getopts(_Socket, _Opts) ->
%% @todo
{ok, [{high_watermark, 0},
{high_msgq_watermark, 0},
{sndbuf, 0},
{recbuf, 0},
{buffer,80000}]}.
fast_close(Stream) ->
%% Stream might be closed already.
_ = quicer:async_close_stream(Stream),
ok.
-spec(ensure_ok_or_exit(atom(), list(term())) -> term()).
ensure_ok_or_exit(Fun, Args = [Sock|_]) when is_atom(Fun), is_list(Args) ->
case erlang:apply(?MODULE, Fun, Args) of
{error, Reason} when Reason =:= enotconn; Reason =:= closed ->
fast_close(Sock),
exit(normal);
{error, Reason} ->
fast_close(Sock),
exit({shutdown, Reason});
Result -> Result
end.
async_send(Stream, Data, Options) when is_list(Data) ->
async_send(Stream, iolist_to_binary(Data), Options);
async_send(Stream, Data, _Options) when is_binary(Data) ->
{ok, _Len} = quicer:send(Stream, Data),
ok.

View File

@ -170,16 +170,11 @@ frame_error(frame_too_large) -> ?RC_PACKET_TOO_LARGE;
frame_error(_) -> ?RC_MALFORMED_PACKET.
connack_error(protocol_error) -> ?RC_PROTOCOL_ERROR;
connack_error(client_identifier_not_valid) -> ?RC_CLIENT_IDENTIFIER_NOT_VALID;
connack_error(bad_username_or_password) -> ?RC_BAD_USER_NAME_OR_PASSWORD;
connack_error(bad_clientid_or_password) -> ?RC_BAD_USER_NAME_OR_PASSWORD;
connack_error(username_or_password_undefined) -> ?RC_BAD_USER_NAME_OR_PASSWORD;
connack_error(password_error) -> ?RC_BAD_USER_NAME_OR_PASSWORD;
connack_error(not_authorized) -> ?RC_NOT_AUTHORIZED;
connack_error(server_unavailable) -> ?RC_SERVER_UNAVAILABLE;
connack_error(server_busy) -> ?RC_SERVER_BUSY;
connack_error(banned) -> ?RC_BANNED;
connack_error(bad_authentication_method) -> ?RC_BAD_AUTHENTICATION_METHOD;
%% TODO: ???
connack_error(_) -> ?RC_NOT_AUTHORIZED.
connack_error(_) -> ?RC_UNSPECIFIED_ERROR.

View File

@ -251,7 +251,7 @@ delete_trie_route(Route = #route{topic = Topic}) ->
%% @private
-spec(maybe_trans(function(), list(any())) -> ok | {error, term()}).
maybe_trans(Fun, Args) ->
case persistent_term:get(emqx_route_lock_type) of
case emqx_config:get([broker, perf, route_lock_type]) of
key ->
trans(Fun, Args);
global ->

View File

@ -33,11 +33,6 @@ init([]) ->
shutdown => 5000,
type => worker,
modules => [emqx_router_helper]},
ok = persistent_term:put(emqx_route_lock_type,
application:get_env(emqx, route_lock_type, key)
),
%% Router pool
RouterPool = emqx_pool_sup:spec([router_pool, hash,
{emqx_router, start_link, []}]),

View File

@ -53,11 +53,9 @@ cast(Key, Node, Mod, Fun, Args) ->
filter_result(?RPC:cast(rpc_node({Key, Node}), Mod, Fun, Args)).
rpc_node(Node) when is_atom(Node) ->
ClientNum = application:get_env(gen_rpc, tcp_client_num, ?DefaultClientNum),
{Node, rand:uniform(ClientNum)};
{Node, rand:uniform(max_client_num())};
rpc_node({Key, Node}) when is_atom(Node) ->
ClientNum = application:get_env(gen_rpc, tcp_client_num, ?DefaultClientNum),
{Node, erlang:phash2(Key, ClientNum) + 1}.
{Node, erlang:phash2(Key, max_client_num()) + 1}.
rpc_nodes(Nodes) ->
rpc_nodes(Nodes, []).
@ -72,3 +70,6 @@ filter_result({Error, Reason})
{badrpc, Reason};
filter_result(Delivery) ->
Delivery.
max_client_num() ->
emqx_config:get([rpc, tcp_client_num], ?DefaultClientNum).

File diff suppressed because it is too large Load Diff

View File

@ -55,7 +55,7 @@
-compile(nowarn_export_all).
-endif.
-export([init/2]).
-export([init/1]).
-export([ info/1
, info/2
@ -92,13 +92,11 @@
-export_type([session/0]).
-import(emqx_zone, [get_env/3]).
-record(session, {
%% Clients Subscriptions.
subscriptions :: map(),
%% Max subscriptions allowed
max_subscriptions :: non_neg_integer(),
max_subscriptions :: non_neg_integer() | infinity,
%% Upgrade QoS?
upgrade_qos :: boolean(),
%% Client <- Broker: QoS1/2 messages sent to the client but
@ -117,7 +115,7 @@
%% have not been completely acknowledged
awaiting_rel :: map(),
%% Maximum number of awaiting QoS2 messages allowed
max_awaiting_rel :: non_neg_integer(),
max_awaiting_rel :: non_neg_integer() | infinity,
%% Awaiting PUBREL Timeout (Unit: millsecond)
await_rel_timeout :: timeout(),
%% Created at
@ -153,34 +151,40 @@
-define(DEFAULT_BATCH_N, 1000).
-type options() :: #{ max_subscriptions => non_neg_integer()
, upgrade_qos => boolean()
, retry_interval => timeout()
, max_awaiting_rel => non_neg_integer() | infinity
, await_rel_timeout => timeout()
, max_inflight => integer()
, mqueue => emqx_mqueue:options()
}.
%%--------------------------------------------------------------------
%% Init a Session
%%--------------------------------------------------------------------
-spec(init(emqx_types:clientinfo(), emqx_types:conninfo()) -> session()).
init(#{zone := Zone}, #{receive_maximum := MaxInflight}) ->
#session{max_subscriptions = get_env(Zone, max_subscriptions, 0),
-spec(init(options()) -> session()).
init(Opts) ->
MaxInflight = maps:get(max_inflight, Opts, 1),
QueueOpts = maps:merge(
#{max_len => 1000,
store_qos0 => true
}, maps:get(mqueue, Opts, #{})),
#session{
max_subscriptions = maps:get(max_subscriptions, Opts, infinity),
subscriptions = #{},
upgrade_qos = get_env(Zone, upgrade_qos, false),
upgrade_qos = maps:get(upgrade_qos, Opts, false),
inflight = emqx_inflight:new(MaxInflight),
mqueue = init_mqueue(Zone),
mqueue = emqx_mqueue:init(QueueOpts),
next_pkt_id = 1,
retry_interval = timer:seconds(get_env(Zone, retry_interval, 0)),
retry_interval = maps:get(retry_interval, Opts, 30000),
awaiting_rel = #{},
max_awaiting_rel = get_env(Zone, max_awaiting_rel, 100),
await_rel_timeout = timer:seconds(get_env(Zone, await_rel_timeout, 300)),
max_awaiting_rel = maps:get(max_awaiting_rel, Opts, 100),
await_rel_timeout = maps:get(await_rel_timeout, Opts, 300000),
created_at = erlang:system_time(millisecond)
}.
%% @private init mq
init_mqueue(Zone) ->
emqx_mqueue:init(#{max_len => get_env(Zone, max_mqueue_len, 1000),
store_qos0 => get_env(Zone, mqueue_store_qos0, true),
priorities => get_env(Zone, mqueue_priorities, none),
default_priority => get_env(Zone, mqueue_default_priority, lowest)
}).
%%--------------------------------------------------------------------
%% Info, Stats
%%--------------------------------------------------------------------
@ -207,7 +211,7 @@ info(inflight_cnt, #session{inflight = Inflight}) ->
info(inflight_max, #session{inflight = Inflight}) ->
emqx_inflight:max_size(Inflight);
info(retry_interval, #session{retry_interval = Interval}) ->
Interval div 1000;
Interval;
info(mqueue, #session{mqueue = MQueue}) ->
MQueue;
info(mqueue_len, #session{mqueue = MQueue}) ->
@ -225,7 +229,7 @@ info(awaiting_rel_cnt, #session{awaiting_rel = AwaitingRel}) ->
info(awaiting_rel_max, #session{max_awaiting_rel = Max}) ->
Max;
info(await_rel_timeout, #session{await_rel_timeout = Timeout}) ->
Timeout div 1000;
Timeout;
info(created_at, #session{created_at = CreatedAt}) ->
CreatedAt.
@ -253,7 +257,7 @@ subscribe(ClientInfo = #{clientid := ClientId}, TopicFilter, SubOpts,
end.
-compile({inline, [is_subscriptions_full/1]}).
is_subscriptions_full(#session{max_subscriptions = 0}) ->
is_subscriptions_full(#session{max_subscriptions = infinity}) ->
false;
is_subscriptions_full(#session{subscriptions = Subs,
max_subscriptions = MaxLimit}) ->
@ -302,7 +306,7 @@ publish(_PacketId, Msg, Session) ->
{ok, emqx_broker:publish(Msg), Session}.
-compile({inline, [is_awaiting_full/1]}).
is_awaiting_full(#session{max_awaiting_rel = 0}) ->
is_awaiting_full(#session{max_awaiting_rel = infinity}) ->
false;
is_awaiting_full(#session{awaiting_rel = AwaitingRel,
max_awaiting_rel = MaxLimit}) ->
@ -696,4 +700,3 @@ age(Now, Ts) -> Now - Ts.
set_field(Name, Value, Session) ->
Pos = emqx_misc:index_of(Name, record_info(fields, session)),
setelement(Pos+1, Session, Value).

View File

@ -77,6 +77,8 @@
-define(NACK(Reason), {shared_sub_nack, Reason}).
-define(NO_ACK, no_ack).
-rlog_shard({?SHARED_SUB_SHARD, ?TAB}).
-record(state, {pmon}).
-record(emqx_shared_subscription, {group, topic, subpid}).
@ -135,11 +137,11 @@ dispatch(Group, Topic, Delivery = #delivery{message = Msg}, FailedSubs) ->
-spec(strategy() -> strategy()).
strategy() ->
emqx:get_env(shared_subscription_strategy, random).
emqx_config:get([broker, shared_subscription_strategy]).
-spec(ack_enabled() -> boolean()).
ack_enabled() ->
emqx:get_env(shared_dispatch_ack_enabled, false).
emqx_config:get([broker, shared_dispatch_ack_enabled]).
do_dispatch(SubPid, Topic, Msg, _Type) when SubPid =:= self() ->
%% Deadlock otherwise
@ -297,7 +299,7 @@ subscribers(Group, Topic) ->
init([]) ->
{ok, _} = mnesia:subscribe({table, ?TAB, simple}),
{atomic, PMon} = mnesia:transaction(fun init_monitors/0),
{atomic, PMon} = ekka_mnesia:transaction(?SHARED_SUB_SHARD, fun init_monitors/0),
ok = emqx_tables:new(?SHARED_SUBS, [protected, bag]),
ok = emqx_tables:new(?ALIVE_SUBS, [protected, set, {read_concurrency, true}]),
{ok, update_stats(#state{pmon = PMon})}.
@ -309,7 +311,7 @@ init_monitors() ->
end, emqx_pmon:new(), ?TAB).
handle_call({subscribe, Group, Topic, SubPid}, _From, State = #state{pmon = PMon}) ->
mnesia:dirty_write(?TAB, record(Group, Topic, SubPid)),
ekka_mnesia:dirty_write(?TAB, record(Group, Topic, SubPid)),
case ets:member(?SHARED_SUBS, {Group, Topic}) of
true -> ok;
false -> ok = emqx_router:do_add_route(Topic, {Group, node()})
@ -319,7 +321,7 @@ handle_call({subscribe, Group, Topic, SubPid}, _From, State = #state{pmon = PMon
{reply, ok, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})};
handle_call({unsubscribe, Group, Topic, SubPid}, _From, State) ->
mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)),
ekka_mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)),
true = ets:delete_object(?SHARED_SUBS, {{Group, Topic}, SubPid}),
delete_route_if_needed({Group, Topic}),
{reply, ok, State};
@ -373,7 +375,7 @@ cleanup_down(SubPid) ->
?IS_LOCAL_PID(SubPid) orelse ets:delete(?ALIVE_SUBS, SubPid),
lists:foreach(
fun(Record = #emqx_shared_subscription{topic = Topic, group = Group}) ->
ok = mnesia:dirty_delete_object(?TAB, Record),
ok = ekka_mnesia:dirty_delete_object(?TAB, Record),
true = ets:delete_object(?SHARED_SUBS, {{Group, Topic}, SubPid}),
delete_route_if_needed({Group, Topic})
end, mnesia:dirty_match_object(#emqx_shared_subscription{_ = '_', subpid = SubPid})).

View File

@ -32,8 +32,6 @@
, uptime/0
, datetime/0
, sysdescr/0
, sys_interval/0
, sys_heatbeat_interval/0
]).
-export([info/0]).
@ -104,15 +102,11 @@ datetime() ->
io_lib:format(
"~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])).
%% @doc Get sys interval
-spec(sys_interval() -> pos_integer()).
sys_interval() ->
emqx:get_env(broker_sys_interval, 60000).
emqx_config:get([broker, sys_msg_interval]).
%% @doc Get sys heatbeat interval
-spec(sys_heatbeat_interval() -> pos_integer()).
sys_heatbeat_interval() ->
emqx:get_env(broker_sys_heartbeat, 30000).
emqx_config:get([broker, sys_heartbeat_interval]).
%% @doc Get sys info
-spec(info() -> list(tuple())).

View File

@ -23,7 +23,7 @@
-logger_header("[SYSMON]").
-export([start_link/1]).
-export([start_link/0]).
%% compress unused warning
-export([procinfo/1]).
@ -37,25 +37,19 @@
, code_change/3
]).
-type(option() :: {long_gc, non_neg_integer()}
| {long_schedule, non_neg_integer()}
| {large_heap, non_neg_integer()}
| {busy_port, boolean()}
| {busy_dist_port, boolean()}).
-define(SYSMON, ?MODULE).
%% @doc Start the system monitor.
-spec(start_link(list(option())) -> startlink_ret()).
start_link(Opts) ->
gen_server:start_link({local, ?SYSMON}, ?MODULE, [Opts], []).
-spec(start_link() -> startlink_ret()).
start_link() ->
gen_server:start_link({local, ?SYSMON}, ?MODULE, [], []).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([Opts]) ->
_ = erlang:system_monitor(self(), parse_opt(Opts)),
init([]) ->
_ = erlang:system_monitor(self(), sysm_opts()),
emqx_logger:set_proc_metadata(#{sysmon => true}),
%% Monitor cluster partition event
@ -66,30 +60,28 @@ init([Opts]) ->
start_timer(State) ->
State#{timer := emqx_misc:start_timer(timer:seconds(2), reset)}.
parse_opt(Opts) ->
parse_opt(Opts, []).
parse_opt([], Acc) ->
sysm_opts() ->
sysm_opts(maps:to_list(emqx_config:get([sysmon, vm])), []).
sysm_opts([], Acc) ->
Acc;
parse_opt([{long_gc, 0}|Opts], Acc) ->
parse_opt(Opts, Acc);
parse_opt([{long_gc, Ms}|Opts], Acc) when is_integer(Ms) ->
parse_opt(Opts, [{long_gc, Ms}|Acc]);
parse_opt([{long_schedule, 0}|Opts], Acc) ->
parse_opt(Opts, Acc);
parse_opt([{long_schedule, Ms}|Opts], Acc) when is_integer(Ms) ->
parse_opt(Opts, [{long_schedule, Ms}|Acc]);
parse_opt([{large_heap, Size}|Opts], Acc) when is_integer(Size) ->
parse_opt(Opts, [{large_heap, Size}|Acc]);
parse_opt([{busy_port, true}|Opts], Acc) ->
parse_opt(Opts, [busy_port|Acc]);
parse_opt([{busy_port, false}|Opts], Acc) ->
parse_opt(Opts, Acc);
parse_opt([{busy_dist_port, true}|Opts], Acc) ->
parse_opt(Opts, [busy_dist_port|Acc]);
parse_opt([{busy_dist_port, false}|Opts], Acc) ->
parse_opt(Opts, Acc);
parse_opt([_Opt|Opts], Acc) ->
parse_opt(Opts, Acc).
sysm_opts([{_, disabled}|Opts], Acc) ->
sysm_opts(Opts, Acc);
sysm_opts([{long_gc, Ms}|Opts], Acc) when is_integer(Ms) ->
sysm_opts(Opts, [{long_gc, Ms}|Acc]);
sysm_opts([{long_schedule, Ms}|Opts], Acc) when is_integer(Ms) ->
sysm_opts(Opts, [{long_schedule, Ms}|Acc]);
sysm_opts([{large_heap, Size}|Opts], Acc) when is_integer(Size) ->
sysm_opts(Opts, [{large_heap, Size}|Acc]);
sysm_opts([{busy_port, true}|Opts], Acc) ->
sysm_opts(Opts, [busy_port|Acc]);
sysm_opts([{busy_port, false}|Opts], Acc) ->
sysm_opts(Opts, Acc);
sysm_opts([{busy_dist_port, true}|Opts], Acc) ->
sysm_opts(Opts, [busy_dist_port|Acc]);
sysm_opts([{busy_dist_port, false}|Opts], Acc) ->
sysm_opts(Opts, Acc);
sysm_opts([_Opt|Opts], Acc) ->
sysm_opts(Opts, Acc).
handle_call(Req, _From, State) ->
?LOG(error, "Unexpected call: ~p", [Req]),

View File

@ -27,10 +27,10 @@ start_link() ->
init([]) ->
Childs = [child_spec(emqx_sys),
child_spec(emqx_alarm, [config(alarm)]),
child_spec(emqx_sys_mon, [config(sysmon)]),
child_spec(emqx_os_mon, [config(os_mon)]),
child_spec(emqx_vm_mon, [config(vm_mon)])],
child_spec(emqx_alarm),
child_spec(emqx_sys_mon),
child_spec(emqx_os_mon),
child_spec(emqx_vm_mon)],
{ok, {{one_for_one, 10, 100}, Childs}}.
%%--------------------------------------------------------------------
@ -48,6 +48,3 @@ child_spec(Mod, Args) ->
type => worker,
modules => [Mod]
}.
config(Name) -> emqx:get_env(Name, []).

View File

@ -161,17 +161,16 @@ drop_tls13_for_old_otp(SslOpts) ->
, "TLS_AES_128_CCM_8_SHA256"
]).
drop_tls13(SslOpts0) ->
SslOpts1 = case proplists:get_value(versions, SslOpts0) of
undefined -> SslOpts0;
Vsns -> replace(SslOpts0, versions, Vsns -- ['tlsv1.3'])
SslOpts1 = case maps:find(versions, SslOpts0) of
error -> SslOpts0;
{ok, Vsns} -> SslOpts0#{versions => (Vsns -- ['tlsv1.3'])}
end,
case proplists:get_value(ciphers, SslOpts1) of
undefined -> SslOpts1;
Ciphers -> replace(SslOpts1, ciphers, Ciphers -- ?TLSV13_EXCLUSIVE_CIPHERS)
case maps:find(ciphers, SslOpts1) of
error -> SslOpts1;
{ok, Ciphers} ->
SslOpts1#{ciphers => Ciphers -- ?TLSV13_EXCLUSIVE_CIPHERS}
end.
replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)].
-if(?OTP_RELEASE > 22).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@ -181,13 +180,13 @@ drop_tls13_test() ->
?assert(lists:member('tlsv1.3', Versions)),
Ciphers = default_ciphers(),
?assert(has_tlsv13_cipher(Ciphers)),
Opts0 = [{versions, Versions}, {ciphers, Ciphers}, other, {bool, true}],
Opts0 = #{versions => Versions, ciphers => Ciphers, other => true},
Opts = drop_tls13(Opts0),
?assertNot(lists:member('tlsv1.3', proplists:get_value(versions, Opts))),
?assertNot(has_tlsv13_cipher(proplists:get_value(ciphers, Opts))).
?assertNot(lists:member('tlsv1.3', maps:get(versions, Opts, undefined))),
?assertNot(has_tlsv13_cipher(maps:get(ciphers, Opts, undefined))).
drop_tls13_no_versions_cipers_test() ->
Opts0 = [other, {bool, true}],
Opts0 = #{other => 0, bool => true},
Opts = drop_tls13(Opts0),
?_assertEqual(Opts0, Opts).

View File

@ -28,14 +28,14 @@
-export([ insert/1
, match/1
, delete/1
, put_compaction_flag/1
, put_default_compaction_flag/0
]).
-export([ empty/0
, lock_tables/0
]).
-export([is_compact/0, set_compact/1]).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
@ -50,21 +50,12 @@
, count = 0 :: non_neg_integer()
}).
-define(IS_COMPACT, true).
-rlog_shard({?ROUTE_SHARD, ?TRIE}).
%%--------------------------------------------------------------------
%% Mnesia bootstrap
%%--------------------------------------------------------------------
put_compaction_flag(Bool) when is_boolean(Bool) ->
_ = persistent_term:put({?MODULE, compaction}, Bool),
ok.
put_default_compaction_flag() ->
ok = put_compaction_flag(?IS_COMPACT).
%% @doc Create or replicate topics table.
-spec(mnesia(boot | copy) -> ok).
mnesia(boot) ->
@ -279,16 +270,10 @@ match_compact([Word | Words], Prefix, IsWildcard, Acc0) ->
lookup_topic(MlTopic).
is_compact() ->
case persistent_term:get({?MODULE, compaction}, undefined) of
undefined ->
Default = ?IS_COMPACT,
FromEnv = emqx:get_env(trie_compaction, Default),
_ = put_compaction_flag(FromEnv),
true = is_boolean(FromEnv),
FromEnv;
Value when is_boolean(Value) ->
Value
end.
emqx_config:get([broker, perf, trie_compaction], true).
set_compact(Bool) ->
emqx_config:put([broker, perf, trie_compaction], Bool).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@ -315,10 +300,11 @@ words(T) -> emqx_topic:words(T).
make_prefixes_t(Topic) -> make_prefixes(words(Topic)).
with_compact_flag(IsCmopact, F) ->
put_compaction_flag(IsCmopact),
with_compact_flag(IsCompact, F) ->
OldV = is_compact(),
set_compact(IsCompact),
try F()
after put_default_compaction_flag()
after set_compact(OldV)
end.
make_prefixes_test_() ->

View File

@ -94,14 +94,16 @@
-type(ver() :: ?MQTT_PROTO_V3
| ?MQTT_PROTO_V4
| ?MQTT_PROTO_V5
| non_neg_integer()).
| non_neg_integer()
| binary() % For lwm2m, mqtt-sn...
).
-type(qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2).
-type(qos_name() :: qos0 | at_most_once |
qos1 | at_least_once |
qos2 | exactly_once).
-type(zone() :: emqx_zone:zone()).
-type(zone() :: atom()).
-type(pubsub() :: publish | subscribe).
-type(topic() :: emqx_topic:topic()).
-type(subid() :: binary() | atom()).
@ -209,7 +211,8 @@
-type(infos() :: #{atom() => term()}).
-type(stats() :: [{atom(), term()}]).
-type(oom_policy() :: #{message_queue_len => non_neg_integer(),
max_heap_size => non_neg_integer()
-type(oom_policy() :: #{max_message_queue_len => non_neg_integer(),
max_heap_size => non_neg_integer(),
enable => boolean()
}).

View File

@ -21,15 +21,7 @@
-include("logger.hrl").
%% APIs
-export([start_link/1]).
-export([ get_check_interval/0
, set_check_interval/1
, get_process_high_watermark/0
, set_process_high_watermark/1
, get_process_low_watermark/0
, set_process_low_watermark/1
]).
-export([start_link/0]).
%% gen_server callbacks
-export([ init/1
@ -42,61 +34,19 @@
-define(VM_MON, ?MODULE).
start_link(Opts) ->
gen_server:start_link({local, ?VM_MON}, ?MODULE, [Opts], []).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
get_check_interval() ->
call(get_check_interval).
set_check_interval(Seconds) ->
call({set_check_interval, Seconds}).
get_process_high_watermark() ->
call(get_process_high_watermark).
set_process_high_watermark(Float) ->
call({set_process_high_watermark, Float}).
get_process_low_watermark() ->
call(get_process_low_watermark).
set_process_low_watermark(Float) ->
call({set_process_low_watermark, Float}).
call(Req) ->
gen_server:call(?VM_MON, Req, infinity).
start_link() ->
gen_server:start_link({local, ?VM_MON}, ?MODULE, [], []).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([Opts]) ->
{ok, ensure_check_timer(#{check_interval => proplists:get_value(check_interval, Opts),
process_high_watermark => proplists:get_value(process_high_watermark, Opts),
process_low_watermark => proplists:get_value(process_low_watermark, Opts),
timer => undefined})}.
handle_call(get_check_interval, _From, State) ->
{reply, maps:get(check_interval, State, undefined), State};
handle_call({set_check_interval, Seconds}, _From, State) ->
{reply, ok, State#{check_interval := Seconds}};
handle_call(get_process_high_watermark, _From, State) ->
{reply, maps:get(process_high_watermark, State, undefined), State};
handle_call({set_process_high_watermark, Float}, _From, State) ->
{reply, ok, State#{process_high_watermark := Float}};
handle_call(get_process_low_watermark, _From, State) ->
{reply, maps:get(process_low_watermark, State, undefined), State};
handle_call({set_process_low_watermark, Float}, _From, State) ->
{reply, ok, State#{process_low_watermark := Float}};
init([]) ->
start_check_timer(),
{ok, #{}}.
handle_call(Req, _From, State) ->
?LOG(error, "[VM_MON] Unexpected call: ~p", [Req]),
@ -106,14 +56,14 @@ handle_cast(Msg, State) ->
?LOG(error, "[VM_MON] Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({timeout, Timer, check},
State = #{timer := Timer,
process_high_watermark := ProcHighWatermark,
process_low_watermark := ProcLowWatermark}) ->
handle_info({timeout, _Timer, check}, State) ->
ProcHighWatermark = emqx_config:get([sysmon, vm, process_high_watermark]),
ProcLowWatermark = emqx_config:get([sysmon, vm, process_low_watermark]),
ProcessCount = erlang:system_info(process_count),
case ProcessCount / erlang:system_info(process_limit) * 100 of
case ProcessCount / erlang:system_info(process_limit) of
Percent when Percent >= ProcHighWatermark ->
emqx_alarm:activate(too_many_processes, #{usage => Percent,
emqx_alarm:activate(too_many_processes, #{
usage => io_lib:format("~p%", [Percent*100]),
high_watermark => ProcHighWatermark,
low_watermark => ProcLowWatermark});
Percent when Percent < ProcLowWatermark ->
@ -121,14 +71,15 @@ handle_info({timeout, Timer, check},
_Precent ->
ok
end,
{noreply, ensure_check_timer(State)};
start_check_timer(),
{noreply, State};
handle_info(Info, State) ->
?LOG(error, "[VM_MON] Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, #{timer := Timer}) ->
emqx_misc:cancel_timer(Timer).
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
@ -137,5 +88,6 @@ code_change(_OldVsn, State, _Extra) ->
%% Internal functions
%%--------------------------------------------------------------------
ensure_check_timer(State = #{check_interval := Interval}) ->
State#{timer := emqx_misc:start_timer(timer:seconds(Interval), check)}.
start_check_timer() ->
Interval = emqx_config:get([sysmon, vm, process_check_interval]),
emqx_misc:start_timer(Interval, check).

View File

@ -62,8 +62,6 @@
sockname :: emqx_types:peername(),
%% Sock state
sockstate :: emqx_types:sockstate(),
%% Simulate the active_n opt
active_n :: pos_integer(),
%% MQTT Piggyback
mqtt_piggyback :: single | multiple,
%% Limiter
@ -85,7 +83,11 @@
%% Idle Timeout
idle_timeout :: timeout(),
%% Idle Timer
idle_timer :: maybe(reference())
idle_timer :: maybe(reference()),
%% Zone name
zone :: atom(),
%% Listener Name
listener :: atom()
}).
-type(state() :: #state{}).
@ -93,7 +95,7 @@
-type(ws_cmd() :: {active, boolean()}|close).
-define(ACTIVE_N, 100).
-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]).
-define(INFO_KEYS, [socktype, peername, sockname, sockstate]).
-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]).
-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]).
@ -124,8 +126,6 @@ info(sockname, #state{sockname = Sockname}) ->
Sockname;
info(sockstate, #state{sockstate = SockSt}) ->
SockSt;
info(active_n, #state{active_n = ActiveN}) ->
ActiveN;
info(limiter, #state{limiter = Limiter}) ->
maybe_apply(fun emqx_limiter:info/1, Limiter);
info(channel, #state{channel = Channel}) ->
@ -174,21 +174,13 @@ call(WsPid, Req, Timeout) when is_pid(WsPid) ->
%% WebSocket callbacks
%%--------------------------------------------------------------------
init(Req, Opts) ->
init(Req, #{zone := Zone, listener := Listener} = Opts) ->
%% WS Transport Idle Timeout
IdleTimeout = proplists:get_value(idle_timeout, Opts, 7200000),
DeflateOptions = maps:from_list(proplists:get_value(deflate_options, Opts, [])),
MaxFrameSize = case proplists:get_value(max_frame_size, Opts, 0) of
0 -> infinity;
I -> I
end,
Compress = proplists:get_bool(compress, Opts),
WsOpts = #{compress => Compress,
deflate_opts => DeflateOptions,
max_frame_size => MaxFrameSize,
idle_timeout => IdleTimeout
WsOpts = #{compress => get_ws_opts(Zone, Listener, compress),
deflate_opts => get_ws_opts(Zone, Listener, deflate_opts),
max_frame_size => get_ws_opts(Zone, Listener, max_frame_size),
idle_timeout => get_ws_opts(Zone, Listener, idle_timeout)
},
case check_origin_header(Req, Opts) of
{error, Message} ->
?LOG(error, "Invalid Origin Header ~p~n", [Message]),
@ -196,18 +188,17 @@ init(Req, Opts) ->
ok -> parse_sec_websocket_protocol(Req, Opts, WsOpts)
end.
parse_sec_websocket_protocol(Req, Opts, WsOpts) ->
FailIfNoSubprotocol = proplists:get_value(fail_if_no_subprotocol, Opts),
parse_sec_websocket_protocol(Req, #{zone := Zone, listener := Listener} = Opts, WsOpts) ->
case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of
undefined ->
case FailIfNoSubprotocol of
case get_ws_opts(Zone, Listener, fail_if_no_subprotocol) of
true ->
{ok, cowboy_req:reply(400, Req), WsOpts};
false ->
{cowboy_websocket, Req, [Req, Opts], WsOpts}
end;
Subprotocols ->
SupportedSubprotocols = proplists:get_value(supported_subprotocols, Opts),
SupportedSubprotocols = get_ws_opts(Zone, Listener, supported_subprotocols),
NSupportedSubprotocols = [list_to_binary(Subprotocol)
|| Subprotocol <- SupportedSubprotocols],
case pick_subprotocol(Subprotocols, NSupportedSubprotocols) of
@ -231,31 +222,30 @@ pick_subprotocol([Subprotocol | Rest], SupportedSubprotocols) ->
pick_subprotocol(Rest, SupportedSubprotocols)
end.
parse_header_fun_origin(Req, Opts) ->
parse_header_fun_origin(Req, #{zone := Zone, listener := Listener}) ->
case cowboy_req:header(<<"origin">>, Req) of
undefined ->
case proplists:get_bool(allow_origin_absence, Opts) of
case get_ws_opts(Zone, Listener, allow_origin_absence) of
true -> ok;
false -> {error, origin_header_cannot_be_absent}
end;
Value ->
Origins = proplists:get_value(check_origins, Opts, []),
case lists:member(Value, Origins) of
case lists:member(Value, get_ws_opts(Zone, Listener, check_origins)) of
true -> ok;
false -> {origin_not_allowed, Value}
end
end.
check_origin_header(Req, Opts) ->
case proplists:get_bool(check_origin_enable, Opts) of
check_origin_header(Req, #{zone := Zone, listener := Listener} = Opts) ->
case get_ws_opts(Zone, Listener, check_origin_enable) of
true -> parse_header_fun_origin(Req, Opts);
false -> ok
end.
websocket_init([Req, Opts]) ->
websocket_init([Req, #{zone := Zone, listener := Listener} = Opts]) ->
{Peername, Peercert} =
case proplists:get_bool(proxy_protocol, Opts)
andalso maps:get(proxy_header, Req) of
case emqx_config:get_listener_conf(Zone, Listener, [proxy_protocol]) andalso
maps:get(proxy_header, Req) of
#{src_address := SrcAddr, src_port := SrcPort, ssl := SSL} ->
SourceName = {SrcAddr, SrcPort},
%% Notice: Only CN is available in Proxy Protocol V2 additional info
@ -288,28 +278,35 @@ websocket_init([Req, Opts]) ->
ws_cookie => WsCookie,
conn_mod => ?MODULE
},
Zone = proplists:get_value(zone, Opts),
PubLimit = emqx_zone:publish_limit(Zone),
BytesIn = proplists:get_value(rate_limit, Opts),
RateLimit = emqx_zone:ratelimit(Zone),
Limiter = emqx_limiter:init(Zone, PubLimit, BytesIn, RateLimit),
ActiveN = proplists:get_value(active_n, Opts, ?ACTIVE_N),
MQTTPiggyback = proplists:get_value(mqtt_piggyback, Opts, multiple),
FrameOpts = emqx_zone:mqtt_frame_options(Zone),
Limiter = emqx_limiter:init(Zone, undefined, undefined, []),
MQTTPiggyback = get_ws_opts(Zone, Listener, mqtt_piggyback),
FrameOpts = #{
strict_mode => emqx_config:get_zone_conf(Zone, [mqtt, strict_mode]),
max_size => emqx_config:get_zone_conf(Zone, [mqtt, max_packet_size])
},
ParseState = emqx_frame:initial_parse_state(FrameOpts),
Serialize = emqx_frame:serialize_opts(),
Channel = emqx_channel:init(ConnInfo, Opts),
GcState = emqx_zone:init_gc_state(Zone),
StatsTimer = emqx_zone:stats_timer(Zone),
GcState = case emqx_config:get_zone_conf(Zone, [force_gc]) of
#{enable := false} -> undefined;
GcPolicy -> emqx_gc:init(GcPolicy)
end,
StatsTimer = case emqx_config:get_zone_conf(Zone, [stats, enable]) of
true -> undefined;
false -> disabled
end,
%% MQTT Idle Timeout
IdleTimeout = emqx_zone:idle_timeout(Zone),
IdleTimeout = emqx_channel:get_mqtt_conf(Zone, idle_timeout),
IdleTimer = start_timer(IdleTimeout, idle_timeout),
emqx_misc:tune_heap_size(emqx_zone:oom_policy(Zone)),
case emqx_config:get_zone_conf(emqx_channel:info(zone, Channel),
[force_shutdown]) of
#{enable := false} -> ok;
ShutdownPolicy -> emqx_misc:tune_heap_size(ShutdownPolicy)
end,
emqx_logger:set_metadata_peername(esockd:format(Peername)),
{ok, #state{peername = Peername,
sockname = Sockname,
sockstate = running,
active_n = ActiveN,
mqtt_piggyback = MQTTPiggyback,
limiter = Limiter,
parse_state = ParseState,
@ -319,7 +316,9 @@ websocket_init([Req, Opts]) ->
postponed = [],
stats_timer = StatsTimer,
idle_timeout = IdleTimeout,
idle_timer = IdleTimer
idle_timer = IdleTimer,
zone = Zone,
listener = Listener
}, hibernate}.
websocket_handle({binary, Data}, State) when is_list(Data) ->
@ -372,7 +371,8 @@ websocket_info({check_gc, Stats}, State) ->
return(check_oom(run_gc(Stats, State)));
websocket_info(Deliver = {deliver, _Topic, _Msg},
State = #state{active_n = ActiveN}) ->
State = #state{zone = Zone, listener = Listener}) ->
ActiveN = emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]),
Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)],
with_channel(handle_deliver, [Delivers], State);
@ -521,11 +521,16 @@ run_gc(Stats, State = #state{gc_state = GcSt}) ->
end.
check_oom(State = #state{channel = Channel}) ->
OomPolicy = emqx_zone:oom_policy(emqx_channel:info(zone, Channel)),
case ?ENABLED(OomPolicy) andalso emqx_misc:check_oom(OomPolicy) of
ShutdownPolicy = emqx_config:get_zone_conf(
emqx_channel:info(zone, Channel), [force_shutdown]),
case ShutdownPolicy of
#{enable := false} -> State;
#{enable := true} ->
case emqx_misc:check_oom(ShutdownPolicy) of
Shutdown = {shutdown, _Reason} ->
postpone(Shutdown, State);
_Other -> State
end
end.
%%--------------------------------------------------------------------
@ -554,11 +559,12 @@ parse_incoming(Data, State = #state{parse_state = ParseState}) ->
%% Handle incoming packet
%%--------------------------------------------------------------------
handle_incoming(Packet, State = #state{active_n = ActiveN})
handle_incoming(Packet, State = #state{zone = Zone, listener = Listener})
when is_record(Packet, mqtt_packet) ->
?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]),
ok = inc_incoming_stats(Packet),
NState = case emqx_pd:get_counter(incoming_pubs) > ActiveN of
NState = case emqx_pd:get_counter(incoming_pubs) >
emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) of
true -> postpone({cast, rate_limit}, State);
false -> State
end,
@ -589,11 +595,13 @@ with_channel(Fun, Args, State = #state{channel = Channel}) ->
%% Handle outgoing packets
%%--------------------------------------------------------------------
handle_outgoing(Packets, State = #state{active_n = ActiveN, mqtt_piggyback = MQTTPiggyback}) ->
handle_outgoing(Packets, State = #state{mqtt_piggyback = MQTTPiggyback,
zone = Zone, listener = Listener}) ->
IoData = lists:map(serialize_and_inc_stats_fun(State), Packets),
Oct = iolist_size(IoData),
ok = inc_sent_stats(length(Packets), Oct),
NState = case emqx_pd:get_counter(outgoing_pubs) > ActiveN of
NState = case emqx_pd:get_counter(outgoing_pubs) >
emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) of
true ->
Stats = #{cnt => emqx_pd:reset_counter(outgoing_pubs),
oct => emqx_pd:reset_counter(outgoing_bytes)
@ -742,9 +750,10 @@ classify([Event|More], Packets, Cmds, Events) ->
trigger(Event) -> erlang:send(self(), Event).
get_peer(Req, Opts) ->
get_peer(Req, #{zone := Zone, listener := Listener}) ->
{PeerAddr, PeerPort} = cowboy_req:peer(Req),
AddrHeader = cowboy_req:header(proplists:get_value(proxy_address_header, Opts), Req, <<>>),
AddrHeader = cowboy_req:header(
get_ws_opts(Zone, Listener, proxy_address_header), Req, <<>>),
ClientAddr = case string:tokens(binary_to_list(AddrHeader), ", ") of
[] ->
undefined;
@ -757,7 +766,8 @@ get_peer(Req, Opts) ->
_ ->
PeerAddr
end,
PortHeader = cowboy_req:header(proplists:get_value(proxy_port_header, Opts), Req, <<>>),
PortHeader = cowboy_req:header(
get_ws_opts(Zone, Listener, proxy_port_header), Req, <<>>),
ClientPort = case string:tokens(binary_to_list(PortHeader), ", ") of
[] ->
undefined;
@ -778,3 +788,5 @@ set_field(Name, Value, State) ->
Pos = emqx_misc:index_of(Name, record_info(fields, state)),
setelement(Pos+1, State, Value).
get_ws_opts(Zone, Listener, Key) ->
emqx_config:get_listener_conf(Zone, Listener, [websocket, Key]).

View File

@ -1,298 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2018-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_zone).
-behaviour(gen_server).
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
-include("logger.hrl").
-include("types.hrl").
-logger_header("[Zone]").
-compile({inline,
[ idle_timeout/1
, publish_limit/1
, ratelimit/1
, mqtt_frame_options/1
, mqtt_strict_mode/1
, max_packet_size/1
, mountpoint/1
, use_username_as_clientid/1
, stats_timer/1
, enable_stats/1
, enable_acl/1
, enable_ban/1
, enable_flapping_detect/1
, ignore_loop_deliver/1
, server_keepalive/1
, keepalive_backoff/1
, max_inflight/1
, session_expiry_interval/1
, force_gc_policy/1
, force_shutdown_policy/1
, response_information/1
, quota_policy/1
, get_env/2
, get_env/3
]}).
%% APIs
-export([start_link/0, stop/0]).
%% Zone Option API
-export([ idle_timeout/1
%% XXX: Dedeprecated at v4.2
, publish_limit/1
, ratelimit/1
, mqtt_frame_options/1
, mqtt_strict_mode/1
, max_packet_size/1
, mountpoint/1
, use_username_as_clientid/1
, stats_timer/1
, enable_stats/1
, enable_acl/1
, enable_ban/1
, enable_flapping_detect/1
, ignore_loop_deliver/1
, server_keepalive/1
, keepalive_backoff/1
, max_inflight/1
, session_expiry_interval/1
, force_gc_policy/1
, force_shutdown_policy/1
, response_information/1
, quota_policy/1
]).
-export([ init_gc_state/1
, oom_policy/1
]).
%% Zone API
-export([ get_env/2
, get_env/3
, set_env/3
, unset_env/2
, unset_all_env/0
]).
-export([force_reload/0]).
%% gen_server callbacks
-export([ init/1
, handle_call/3
, handle_cast/2
, handle_info/2
, terminate/2
, code_change/3
]).
-import(emqx_misc, [maybe_apply/2]).
-export_type([zone/0]).
-type(zone() :: atom()).
-define(TAB, ?MODULE).
-define(SERVER, ?MODULE).
-define(DEFAULT_IDLE_TIMEOUT, 30000).
-define(KEY(Zone, Key), {?MODULE, Zone, Key}).
-spec(start_link() -> startlink_ret()).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-spec(stop() -> ok).
stop() ->
gen_server:stop(?SERVER).
-spec(init_gc_state(zone()) -> maybe(emqx_gc:gc_state())).
init_gc_state(Zone) ->
maybe_apply(fun emqx_gc:init/1, force_gc_policy(Zone)).
-spec(oom_policy(zone()) -> emqx_types:oom_policy()).
oom_policy(Zone) -> force_shutdown_policy(Zone).
%%--------------------------------------------------------------------
%% Zone Options API
%%--------------------------------------------------------------------
-spec(idle_timeout(zone()) -> pos_integer()).
idle_timeout(Zone) ->
get_env(Zone, idle_timeout, ?DEFAULT_IDLE_TIMEOUT).
-spec(publish_limit(zone()) -> maybe(esockd_rate_limit:config())).
publish_limit(Zone) ->
get_env(Zone, publish_limit).
-spec(ratelimit(zone()) -> [emqx_limiter:specs()]).
ratelimit(Zone) ->
get_env(Zone, ratelimit, []).
-spec(mqtt_frame_options(zone()) -> emqx_frame:options()).
mqtt_frame_options(Zone) ->
#{strict_mode => mqtt_strict_mode(Zone),
max_size => max_packet_size(Zone)
}.
-spec(mqtt_strict_mode(zone()) -> boolean()).
mqtt_strict_mode(Zone) ->
get_env(Zone, strict_mode, false).
-spec(max_packet_size(zone()) -> integer()).
max_packet_size(Zone) ->
get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE).
-spec(mountpoint(zone()) -> maybe(emqx_mountpoint:mountpoint())).
mountpoint(Zone) -> get_env(Zone, mountpoint).
-spec(use_username_as_clientid(zone()) -> boolean()).
use_username_as_clientid(Zone) ->
get_env(Zone, use_username_as_clientid, false).
-spec(stats_timer(zone()) -> undefined | disabled).
stats_timer(Zone) ->
case enable_stats(Zone) of true -> undefined; false -> disabled end.
-spec(enable_stats(zone()) -> boolean()).
enable_stats(Zone) ->
get_env(Zone, enable_stats, true).
-spec(enable_acl(zone()) -> boolean()).
enable_acl(Zone) ->
get_env(Zone, enable_acl, true).
-spec(enable_ban(zone()) -> boolean()).
enable_ban(Zone) ->
get_env(Zone, enable_ban, false).
-spec(enable_flapping_detect(zone()) -> boolean()).
enable_flapping_detect(Zone) ->
get_env(Zone, enable_flapping_detect, false).
-spec(ignore_loop_deliver(zone()) -> boolean()).
ignore_loop_deliver(Zone) ->
get_env(Zone, ignore_loop_deliver, false).
-spec(server_keepalive(zone()) -> maybe(pos_integer())).
server_keepalive(Zone) ->
get_env(Zone, server_keepalive).
-spec(keepalive_backoff(zone()) -> float()).
keepalive_backoff(Zone) ->
get_env(Zone, keepalive_backoff, 0.75).
-spec(max_inflight(zone()) -> 0..65535).
max_inflight(Zone) ->
get_env(Zone, max_inflight, 65535).
-spec(session_expiry_interval(zone()) -> non_neg_integer()).
session_expiry_interval(Zone) ->
get_env(Zone, session_expiry_interval, 0).
-spec(force_gc_policy(zone()) -> maybe(emqx_gc:opts())).
force_gc_policy(Zone) ->
get_env(Zone, force_gc_policy).
-spec(force_shutdown_policy(zone()) -> maybe(emqx_oom:opts())).
force_shutdown_policy(Zone) ->
get_env(Zone, force_shutdown_policy).
-spec(response_information(zone()) -> string()).
response_information(Zone) ->
get_env(Zone, response_information).
-spec(quota_policy(zone()) -> emqx_quota:policy()).
quota_policy(Zone) ->
get_env(Zone, quota, []).
%%--------------------------------------------------------------------
%% APIs
%%--------------------------------------------------------------------
-spec(get_env(maybe(zone()), atom()) -> maybe(term())).
get_env(undefined, Key) -> emqx:get_env(Key);
get_env(Zone, Key) ->
get_env(Zone, Key, undefined).
-spec(get_env(maybe(zone()), atom(), term()) -> maybe(term())).
get_env(undefined, Key, Def) ->
emqx:get_env(Key, Def);
get_env(Zone, Key, Def) ->
try persistent_term:get(?KEY(Zone, Key))
catch error:badarg ->
emqx:get_env(Key, Def)
end.
-spec(set_env(zone(), atom(), term()) -> ok).
set_env(Zone, Key, Val) ->
persistent_term:put(?KEY(Zone, Key), Val).
-spec(unset_env(zone(), atom()) -> boolean()).
unset_env(Zone, Key) ->
persistent_term:erase(?KEY(Zone, Key)).
-spec(unset_all_env() -> ok).
unset_all_env() ->
[unset_env(Zone, Key) || {?KEY(Zone, Key), _Val} <- persistent_term:get()],
ok.
-spec(force_reload() -> ok).
force_reload() ->
gen_server:call(?SERVER, force_reload).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([]) ->
_ = do_reload(),
{ok, #{}}.
handle_call(force_reload, _From, State) ->
_ = do_reload(),
{reply, ok, State};
handle_call(Req, _From, State) ->
?LOG(error, "Unexpected call: ~p", [Req]),
{reply, ignored, State}.
handle_cast(Msg, State) ->
?LOG(error, "Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
?LOG(error, "Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
do_reload() ->
[persistent_term:put(?KEY(Zone, Key), Val)
|| {Zone, Opts} <- emqx:get_env(zones, []), {Key, Val} <- Opts].

View File

@ -51,14 +51,6 @@ t_stop_start(_) ->
ok = emqx:shutdown(for_test),
false = emqx:is_running(node()).
t_get_env(_) ->
?assertEqual(undefined, emqx:get_env(undefined_key)),
?assertEqual(default_value, emqx:get_env(undefined_key, default_value)),
application:set_env(emqx, undefined_key, hello),
?assertEqual(hello, emqx:get_env(undefined_key)),
?assertEqual(hello, emqx:get_env(undefined_key, default_value)),
application:unset_env(emqx, undefined_key).
t_emqx_pubsub_api(_) ->
true = emqx:is_running(node()),
{ok, C} = emqtt:start_link([{host, "localhost"}, {clientid, "myclient"}]),

View File

@ -1,6 +1,6 @@
%%--------------------------------------------------------------------
%%
%% [ACL](https://github.com/emqtt/emqttd/wiki/ACL)
%% [Authorization](https://github.com/emqtt/emqttd/wiki/Authorization)
%%
%% -type who() :: all | binary() |
%% {ipaddr, esockd_access:cidr()} |

View File

@ -33,43 +33,20 @@ end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]).
t_authenticate(_) ->
emqx_zone:set_env(zone, allow_anonymous, false),
?assertMatch({error, _}, emqx_access_control:authenticate(clientinfo())),
emqx_zone:set_env(zone, allow_anonymous, true),
?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())).
?assertMatch(ok, emqx_access_control:authenticate(clientinfo())).
t_check_acl(_) ->
emqx_zone:set_env(zone, acl_nomatch, deny),
application:set_env(emqx, enable_acl_cache, false),
t_authorize(_) ->
Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>),
?assertEqual(deny, emqx_access_control:check_acl(clientinfo(), Publish, <<"t">>)),
emqx_zone:set_env(zone, acl_nomatch, allow),
application:set_env(emqx, enable_acl_cache, true),
Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>),
?assertEqual(allow, emqx_access_control:check_acl(clientinfo(), Publish, <<"t">>)).
t_bypass_auth_plugins(_) ->
ClientInfo = clientinfo(),
emqx_zone:set_env(bypass_zone, allow_anonymous, true),
emqx_zone:set_env(zone, allow_anonymous, false),
emqx_zone:set_env(bypass_zone, bypass_auth_plugins, true),
emqx:hook('client.authenticate',{?MODULE, auth_fun, []}),
?assertMatch({ok, _}, emqx_access_control:authenticate(ClientInfo#{zone => bypass_zone})),
?assertMatch({ok, _}, emqx_access_control:authenticate(ClientInfo)).
?assertEqual(allow, emqx_access_control:authorize(clientinfo(), Publish, <<"t">>)).
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
auth_fun(#{zone := bypass_zone}, AuthRes) ->
{stop, AuthRes#{auth_result => password_error}};
auth_fun(#{zone := _}, AuthRes) ->
{stop, AuthRes#{auth_result => success}}.
clientinfo() -> clientinfo(#{}).
clientinfo(InitProps) ->
maps:merge(#{zone => zone,
maps:merge(#{zone => default,
listener => mqtt_tcp,
protocol => mqtt,
peerhost => {127,0,0,1},
clientid => <<"clientid">>,
@ -79,3 +56,6 @@ clientinfo(InitProps) ->
peercert => undefined,
mountpoint => undefined
}, InitProps).
toggle_auth(Bool) when is_boolean(Bool) ->
emqx_config:put_zone_conf(default, [auth, enable], Bool).

View File

@ -27,27 +27,17 @@ all() -> emqx_ct:all(?MODULE).
init_per_testcase(t_size_limit, Config) ->
emqx_ct_helpers:boot_modules(all),
emqx_ct_helpers:start_apps([],
fun(emqx) ->
application:set_env(emqx, alarm, [{actions, [log,publish]},
{size_limit, 2},
{validity_period, 3600}]),
ok;
(_) ->
ok
end),
emqx_ct_helpers:start_apps([]),
emqx_config:update([alarm], #{
<<"size_limit">> => 2
}),
Config;
init_per_testcase(t_validity_period, Config) ->
emqx_ct_helpers:boot_modules(all),
emqx_ct_helpers:start_apps([],
fun(emqx) ->
application:set_env(emqx, alarm, [{actions, [log,publish]},
{size_limit, 1000},
{validity_period, 1}]),
ok;
(_) ->
ok
end),
emqx_ct_helpers:start_apps([]),
emqx_config:update([alarm], #{
<<"validity_period">> => <<"1s">>
}),
Config;
init_per_testcase(_, Config) ->
emqx_ct_helpers:boot_modules(all),
@ -89,7 +79,7 @@ t_size_limit(_) ->
ok = emqx_alarm:activate(b),
ok = emqx_alarm:deactivate(b),
?assertNotEqual({error, not_found}, get_alarm(a, emqx_alarm:get_alarms(deactivated))),
?assertNotEqual({error, not_found}, get_alarm(a, emqx_alarm:get_alarms(deactivated))),
?assertNotEqual({error, not_found}, get_alarm(b, emqx_alarm:get_alarms(deactivated))),
ok = emqx_alarm:activate(c),
ok = emqx_alarm:deactivate(c),
?assertNotEqual({error, not_found}, get_alarm(c, emqx_alarm:get_alarms(deactivated))),

View File

@ -14,7 +14,7 @@
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_acl_cache_SUITE).
-module(emqx_authz_cache_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
@ -26,6 +26,7 @@ all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) ->
emqx_ct_helpers:boot_modules(all),
emqx_ct_helpers:start_apps([]),
toggle_authz(true),
Config.
end_per_suite(_Config) ->
@ -35,7 +36,7 @@ end_per_suite(_Config) ->
%% Test cases
%%--------------------------------------------------------------------
t_clean_acl_cache(_) ->
t_clean_authz_cache(_) ->
{ok, Client} = emqtt:start_link([{clientid, <<"emqx_c">>}]),
{ok, _} = emqtt:connect(Client),
{ok, _, _} = emqtt:subscribe(Client, <<"t2">>, 0),
@ -48,15 +49,14 @@ t_clean_acl_cache(_) ->
lists:last(Pids);
_ -> {error, not_found}
end,
Caches = gen_server:call(ClientPid, list_acl_cache),
ct:log("acl caches: ~p", [Caches]),
Caches = gen_server:call(ClientPid, list_authz_cache),
ct:log("authz caches: ~p", [Caches]),
?assert(length(Caches) > 0),
erlang:send(ClientPid, clean_acl_cache),
?assertEqual(0, length(gen_server:call(ClientPid, list_acl_cache))),
erlang:send(ClientPid, clean_authz_cache),
?assertEqual(0, length(gen_server:call(ClientPid, list_authz_cache))),
emqtt:stop(Client).
t_drain_acl_cache(_) ->
t_drain_authz_cache(_) ->
{ok, Client} = emqtt:start_link([{clientid, <<"emqx_c">>}]),
{ok, _} = emqtt:connect(Client),
{ok, _, _} = emqtt:subscribe(Client, <<"t2">>, 0),
@ -69,80 +69,15 @@ t_drain_acl_cache(_) ->
lists:last(Pids);
_ -> {error, not_found}
end,
Caches = gen_server:call(ClientPid, list_acl_cache),
ct:log("acl caches: ~p", [Caches]),
Caches = gen_server:call(ClientPid, list_authz_cache),
ct:log("authz caches: ~p", [Caches]),
?assert(length(Caches) > 0),
emqx_acl_cache:drain_cache(),
?assertEqual(0, length(gen_server:call(ClientPid, list_acl_cache))),
emqx_authz_cache:drain_cache(),
?assertEqual(0, length(gen_server:call(ClientPid, list_authz_cache))),
ct:sleep(100),
{ok, _, _} = emqtt:subscribe(Client, <<"t2">>, 0),
?assert(length(gen_server:call(ClientPid, list_acl_cache)) > 0),
?assert(length(gen_server:call(ClientPid, list_authz_cache)) > 0),
emqtt:stop(Client).
% optimize??
t_reload_aclfile_and_cleanall(_Config) ->
RasieMsg = fun() -> Self = self(), #{puback => fun(Msg) -> Self ! {puback, Msg} end,
disconnected => fun(_) -> ok end,
publish => fun(_) -> ok end } end,
{ok, Client} = emqtt:start_link([{clientid, <<"emqx_c">>}, {proto_ver, v5},
{msg_handler, RasieMsg()}]),
{ok, _} = emqtt:connect(Client),
{ok, PktId} = emqtt:publish(Client, <<"t1">>, <<"{\"x\":1}">>, qos1),
%% Success publish to broker
receive
{puback, #{packet_id := PktId, reason_code := Rc}} ->
?assertEqual(16#10, Rc);
_ ->
?assert(false)
end,
%% Check acl cache list
[ClientPid] = emqx_cm:lookup_channels(<<"emqx_c">>),
?assert(length(gen_server:call(ClientPid, list_acl_cache)) > 0),
emqtt:stop(Client).
%% @private
testdir(DataPath) ->
Ls = filename:split(DataPath),
filename:join(lists:sublist(Ls, 1, length(Ls) - 1)).
% t_cache_k(_) ->
% error('TODO').
% t_cache_v(_) ->
% error('TODO').
% t_cleanup_acl_cache(_) ->
% error('TODO').
% t_get_oldest_key(_) ->
% error('TODO').
% t_get_newest_key(_) ->
% error('TODO').
% t_get_cache_max_size(_) ->
% error('TODO').
% t_get_cache_size(_) ->
% error('TODO').
% t_dump_acl_cache(_) ->
% error('TODO').
% t_empty_acl_cache(_) ->
% error('TODO').
% t_put_acl_cache(_) ->
% error('TODO').
% t_get_acl_cache(_) ->
% error('TODO').
% t_is_enabled(_) ->
% error('TODO').
toggle_authz(Bool) when is_boolean(Bool) ->
emqx_config:put_zone_conf(default, [authorization, enable], Bool).

View File

@ -14,20 +14,20 @@
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_acl_test_mod).
-module(emqx_authz_test_mod).
%% ACL callbacks
%% Authorization callbacks
-export([ init/1
, check_acl/2
, authorize/2
, description/0
]).
init(AclOpts) ->
{ok, AclOpts}.
init(AuthzOpts) ->
{ok, AuthzOpts}.
check_acl({_User, _PubSub, _Topic}, _State) ->
authorize({_User, _PubSub, _Topic}, _State) ->
allow.
description() ->
"Test ACL Mod".
"Test Authorization Mod".

View File

@ -42,19 +42,19 @@ end_per_suite(_Config) ->
%%--------------------------------------------------------------------
t_stats_fun(_) ->
?assertEqual(0, emqx_stats:getstat('subscribers.count')),
?assertEqual(0, emqx_stats:getstat('subscriptions.count')),
?assertEqual(0, emqx_stats:getstat('suboptions.count')),
Subscribers = emqx_stats:getstat('subscribers.count'),
Subscriptions = emqx_stats:getstat('subscriptions.count'),
Subopts = emqx_stats:getstat('suboptions.count'),
ok = emqx_broker:subscribe(<<"topic">>, <<"clientid">>),
ok = emqx_broker:subscribe(<<"topic2">>, <<"clientid">>),
emqx_broker:stats_fun(),
ct:sleep(10),
?assertEqual(2, emqx_stats:getstat('subscribers.count')),
?assertEqual(2, emqx_stats:getstat('subscribers.max')),
?assertEqual(2, emqx_stats:getstat('subscriptions.count')),
?assertEqual(2, emqx_stats:getstat('subscriptions.max')),
?assertEqual(2, emqx_stats:getstat('suboptions.count')),
?assertEqual(2, emqx_stats:getstat('suboptions.max')).
?assertEqual(Subscribers + 2, emqx_stats:getstat('subscribers.count')),
?assertEqual(Subscribers + 2, emqx_stats:getstat('subscribers.max')),
?assertEqual(Subscriptions + 2, emqx_stats:getstat('subscriptions.count')),
?assertEqual(Subscriptions + 2, emqx_stats:getstat('subscriptions.max')),
?assertEqual(Subopts + 2, emqx_stats:getstat('suboptions.count')),
?assertEqual(Subopts + 2, emqx_stats:getstat('suboptions.max')).
t_subscribed(_) ->
emqx_broker:subscribe(<<"topic">>),

View File

@ -24,7 +24,152 @@
-include_lib("eunit/include/eunit.hrl").
all() -> emqx_ct:all(?MODULE).
all() ->
emqx_ct:all(?MODULE).
mqtt_conf() ->
#{await_rel_timeout => 300000,
idle_timeout => 15000,
ignore_loop_deliver => false,
keepalive_backoff => 0.75,
max_awaiting_rel => 100,
max_clientid_len => 65535,
max_inflight => 32,
max_mqueue_len => 1000,
max_packet_size => 1048576,
max_qos_allowed => 2,
max_subscriptions => infinity,
max_topic_alias => 65535,
max_topic_levels => 65535,
mountpoint => <<>>,
mqueue_default_priority => lowest,
mqueue_priorities => #{},
mqueue_store_qos0 => true,
peer_cert_as_clientid => disabled,
peer_cert_as_username => disabled,
response_information => [],
retain_available => true,
retry_interval => 30000,
server_keepalive => disabled,
session_expiry_interval => 7200000,
shared_subscription => true,
strict_mode => false,
upgrade_qos => false,
use_username_as_clientid => false,
wildcard_subscription => true}.
listener_mqtt_tcp_conf() ->
#{acceptors => 16,
access_rules => ["allow all"],
bind => {{0,0,0,0},1883},
max_connections => 1024000,
proxy_protocol => false,
proxy_protocol_timeout => 3000,
rate_limit =>
#{conn_bytes_in =>
["100KB","10s"],
conn_messages_in =>
["100","10s"],
max_conn_rate => 1000,
quota =>
#{conn_messages_routing => infinity,
overall_messages_routing => infinity}},
tcp =>
#{active_n => 100,
backlog => 1024,
buffer => 4096,
high_watermark => 1048576,
send_timeout => 15000,
send_timeout_close =>
true},
type => tcp}.
listener_mqtt_ws_conf() ->
#{acceptors => 16,
access_rules => ["allow all"],
bind => {{0,0,0,0},8083},
max_connections => 1024000,
proxy_protocol => false,
proxy_protocol_timeout => 3000,
rate_limit =>
#{conn_bytes_in =>
["100KB","10s"],
conn_messages_in =>
["100","10s"],
max_conn_rate => 1000,
quota =>
#{conn_messages_routing => infinity,
overall_messages_routing => infinity}},
tcp =>
#{active_n => 100,
backlog => 1024,
buffer => 4096,
high_watermark => 1048576,
send_timeout => 15000,
send_timeout_close =>
true},
type => ws,
websocket =>
#{allow_origin_absence =>
true,
check_origin_enable =>
false,
check_origins => [],
compress => false,
deflate_opts =>
#{client_max_window_bits =>
15,
mem_level => 8,
server_max_window_bits =>
15},
fail_if_no_subprotocol =>
true,
idle_timeout => 86400000,
max_frame_size => infinity,
mqtt_path => "/mqtt",
mqtt_piggyback => multiple,
proxy_address_header =>
"x-forwarded-for",
proxy_port_header =>
"x-forwarded-port",
supported_subprotocols =>
["mqtt","mqtt-v3",
"mqtt-v3.1.1",
"mqtt-v5"]}}.
default_zone_conf() ->
#{zones =>
#{default =>
#{ authorization => #{
cache => #{enable => true,max_size => 32, ttl => 60000},
deny_action => ignore,
enable => false
},
auth => #{enable => false},
overall_max_connections => infinity,
stats => #{enable => true},
conn_congestion =>
#{enable_alarm => true, min_alarm_sustain_duration => 60000},
flapping_detect =>
#{ban_time => 300000,enable => false,
max_count => 15,window_time => 60000},
force_gc =>
#{bytes => 16777216,count => 16000,
enable => true},
force_shutdown =>
#{enable => true,
max_heap_size => 4194304,
max_message_queue_len => 1000},
mqtt => mqtt_conf(),
listeners =>
#{mqtt_tcp => listener_mqtt_tcp_conf(),
mqtt_ws => listener_mqtt_ws_conf()}
}
}
}.
set_default_zone_conf() ->
emqx_config:put(default_zone_conf()).
%%--------------------------------------------------------------------
%% CT Callbacks
@ -36,8 +181,8 @@ init_per_suite(Config) ->
%% Access Control Meck
ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_access_control, authenticate,
fun(_) -> {ok, #{auth_result => success}} end),
ok = meck:expect(emqx_access_control, check_acl, fun(_, _, _) -> allow end),
fun(_) -> ok end),
ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end),
%% Broker Meck
ok = meck:new(emqx_broker, [passthrough, no_history, no_link]),
%% Hooks Meck
@ -50,6 +195,9 @@ init_per_suite(Config) ->
ok = meck:new(emqx_metrics, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_metrics, inc, fun(_) -> ok end),
ok = meck:expect(emqx_metrics, inc, fun(_, _) -> ok end),
%% Ban
meck:new(emqx_banned, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_banned, check, fun(_ConnInfo) -> false end),
Config.
end_per_suite(_Config) ->
@ -58,15 +206,15 @@ end_per_suite(_Config) ->
emqx_session,
emqx_broker,
emqx_hooks,
emqx_cm
emqx_cm,
emqx_banned
]).
init_per_testcase(_TestCase, Config) ->
meck:new(emqx_zone, [passthrough, no_history, no_link]),
set_default_zone_conf(),
Config.
end_per_testcase(_TestCase, Config) ->
meck:unload([emqx_zone]),
Config.
%%--------------------------------------------------------------------
@ -83,7 +231,7 @@ t_chan_caps(_) ->
#{max_clientid_len := 65535,
max_qos_allowed := 2,
max_topic_alias := 65535,
max_topic_levels := 0,
max_topic_levels := 65535,
retain_available := true,
shared_subscription := true,
subscription_identifiers := true,
@ -120,35 +268,40 @@ t_handle_in_unexpected_packet(_) ->
{ok, [{outgoing, Packet}, {close, protocol_error}], Channel} =
emqx_channel:handle_in(?PUBLISH_PACKET(?QOS_0), Channel).
t_handle_in_connect_auth_failed(_) ->
ConnPkt = #mqtt_packet_connect{
proto_name = <<"MQTT">>,
proto_ver = ?MQTT_PROTO_V5,
is_bridge = false,
clean_start = true,
keepalive = 30,
properties = #{
'Authentication-Method' => <<"failed_auth_method">>,
'Authentication-Data' => <<"failed_auth_data">>
},
clientid = <<"clientid">>,
username = <<"username">>
},
{shutdown, not_authorized, ?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _} =
emqx_channel:handle_in(?CONNECT_PACKET(ConnPkt), channel(#{conn_state => idle})).
% t_handle_in_connect_auth_failed(_) ->
% ConnPkt = #mqtt_packet_connect{
% proto_name = <<"MQTT">>,
% proto_ver = ?MQTT_PROTO_V5,
% is_bridge = false,
% clean_start = true,
% keepalive = 30,
% properties = #{
% 'Authentication-Method' => <<"failed_auth_method">>,
% 'Authentication-Data' => <<"failed_auth_data">>
% },
% clientid = <<"clientid">>,
% username = <<"username">>
% },
% {shutdown, not_authorized, ?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _} =
% emqx_channel:handle_in(?CONNECT_PACKET(ConnPkt), channel(#{conn_state => idle})).
t_handle_in_continue_auth(_) ->
Properties = #{
'Authentication-Method' => <<"failed_auth_method">>,
'Authentication-Data' => <<"failed_auth_data">>
},
{shutdown, bad_authentication_method, ?CONNACK_PACKET(?RC_BAD_AUTHENTICATION_METHOD), _} =
emqx_channel:handle_in(?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION,Properties), channel()),
{shutdown, not_authorized, ?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _} =
Channel1 = channel(#{conn_state => connected}),
{ok, [{outgoing, ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR)}, {close, protocol_error}], Channel1} =
emqx_channel:handle_in(?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION, Properties), Channel1),
Channel2 = channel(#{conn_state => connecting}),
ConnInfo = emqx_channel:info(conninfo, Channel2),
Channel3 = emqx_channel:set_field(conninfo, ConnInfo#{conn_props => Properties}, Channel2),
{ok, [{event, connected}, {connack, ?CONNACK_PACKET(?RC_SUCCESS)}], _} =
emqx_channel:handle_in(
?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION,Properties),
channel(#{conninfo => #{proto_ver => ?MQTT_PROTO_V5, conn_props => Properties}})
).
?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION, Properties), Channel3).
t_handle_in_re_auth(_) ->
Properties = #{
@ -167,10 +320,14 @@ t_handle_in_re_auth(_) ->
?AUTH_PACKET(?RC_RE_AUTHENTICATE,Properties),
channel(#{conninfo => #{proto_ver => ?MQTT_PROTO_V5, conn_props => undefined}})
),
{ok, [{outgoing, ?DISCONNECT_PACKET(?RC_NOT_AUTHORIZED)}, {close, not_authorized}], _} =
Channel1 = channel(),
ConnInfo = emqx_channel:info(conninfo, Channel1),
Channel2 = emqx_channel:set_field(conninfo, ConnInfo#{conn_props => Properties}, Channel1),
{ok, ?AUTH_PACKET(?RC_SUCCESS), _} =
emqx_channel:handle_in(
?AUTH_PACKET(?RC_RE_AUTHENTICATE,Properties),
channel(#{conninfo => #{proto_ver => ?MQTT_PROTO_V5, conn_props => Properties}})
?AUTH_PACKET(?RC_RE_AUTHENTICATE,Properties), Channel2
).
t_handle_in_qos0_publish(_) ->
@ -241,7 +398,7 @@ t_bad_receive_maximum(_) ->
fun(true, _ClientInfo, _ConnInfo) ->
{ok, #{session => session(), present => false}}
end),
ok = meck:expect(emqx_zone, response_information, fun(_) -> test end),
emqx_config:put_zone_conf(default, [mqtt, response_information], test),
C1 = channel(#{conn_state => idle}),
{shutdown, protocol_error, _, _} =
emqx_channel:handle_in(
@ -254,8 +411,8 @@ t_override_client_receive_maximum(_) ->
fun(true, _ClientInfo, _ConnInfo) ->
{ok, #{session => session(), present => false}}
end),
ok = meck:expect(emqx_zone, response_information, fun(_) -> test end),
ok = meck:expect(emqx_zone, max_inflight, fun(_) -> 0 end),
emqx_config:put_zone_conf(default, [mqtt, response_information], test),
emqx_config:put_zone_conf(default, [mqtt, max_inflight], 0),
C1 = channel(#{conn_state => idle}),
ClientCapacity = 2,
{ok, [{event, connected}, _ConnAck], C2} =
@ -346,8 +503,8 @@ t_handle_in_disconnect(_) ->
t_handle_in_auth(_) ->
Channel = channel(#{conn_state => connected}),
Packet = ?DISCONNECT_PACKET(?RC_IMPLEMENTATION_SPECIFIC_ERROR),
{ok, [{outgoing, Packet}, {close, implementation_specific_error}], Channel} =
Packet = ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR),
{ok, [{outgoing, Packet}, {close, protocol_error}], Channel} =
emqx_channel:handle_in(?AUTH_PACKET(), Channel).
t_handle_in_frame_error(_) ->
@ -477,7 +634,7 @@ t_handle_deliver_nl(_) ->
Channel = channel(#{clientinfo => ClientInfo, session => Session}),
Msg = emqx_message:make(<<"clientid">>, ?QOS_1, <<"t1">>, <<"qos1">>),
NMsg = emqx_message:set_flag(nl, Msg),
{ok, Channel} = emqx_channel:handle_deliver([{deliver, <<"t1">>, NMsg}], Channel).
{ok, _} = emqx_channel:handle_deliver([{deliver, <<"t1">>, NMsg}], Channel).
%%--------------------------------------------------------------------
%% Test cases for handle_out
@ -506,7 +663,7 @@ t_handle_out_connack_response_information(_) ->
fun(true, _ClientInfo, _ConnInfo) ->
{ok, #{session => session(), present => false}}
end),
ok = meck:expect(emqx_zone, response_information, fun(_) -> test end),
emqx_config:put_zone_conf(default, [mqtt, response_information], test),
IdleChannel = channel(#{conn_state => idle}),
{ok, [{event, connected},
{connack, ?CONNACK_PACKET(?RC_SUCCESS, 0, #{'Response-Information' := test})}],
@ -520,7 +677,7 @@ t_handle_out_connack_not_response_information(_) ->
fun(true, _ClientInfo, _ConnInfo) ->
{ok, #{session => session(), present => false}}
end),
ok = meck:expect(emqx_zone, response_information, fun(_) -> test end),
emqx_config:put_zone_conf(default, [mqtt, response_information], test),
IdleChannel = channel(#{conn_state => idle}),
{ok, [{event, connected}, {connack, ?CONNACK_PACKET(?RC_SUCCESS, 0, AckProps)}], _} =
emqx_channel:handle_in(
@ -660,11 +817,8 @@ t_enrich_conninfo(_) ->
t_enrich_client(_) ->
{ok, _ConnPkt, _Chan} = emqx_channel:enrich_client(connpkt(), channel()).
t_check_banned(_) ->
ok = emqx_channel:check_banned(connpkt(), channel()).
t_auth_connect(_) ->
{ok, _Chan} = emqx_channel:auth_connect(connpkt(), channel()).
{ok, _, _Chan} = emqx_channel:authenticate(?CONNECT_PACKET(connpkt()), channel()).
t_process_alias(_) ->
Publish = #mqtt_packet_publish{topic_name = <<>>, properties = #{'Topic-Alias' => 1}},
@ -708,20 +862,20 @@ t_packing_alias(_) ->
#mqtt_packet{variable = #mqtt_packet_publish{topic_name = <<"z">>}},
channel())).
t_check_pub_acl(_) ->
ok = meck:expect(emqx_zone, enable_acl, fun(_) -> true end),
t_check_pub_authz(_) ->
emqx_config:put_zone_conf(default, [authorization, enable], true),
Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>),
ok = emqx_channel:check_pub_acl(Publish, channel()).
ok = emqx_channel:check_pub_authz(Publish, channel()).
t_check_pub_alias(_) ->
Publish = #mqtt_packet_publish{topic_name = <<>>, properties = #{'Topic-Alias' => 1}},
Channel = emqx_channel:set_field(alias_maximum, #{inbound => 10}, channel()),
ok = emqx_channel:check_pub_alias(#mqtt_packet{variable = Publish}, Channel).
t_check_sub_acls(_) ->
ok = meck:expect(emqx_zone, enable_acl, fun(_) -> true end),
t_check_sub_authzs(_) ->
emqx_config:put_zone_conf(default, [authorization, enable], true),
TopicFilter = {<<"t">>, ?DEFAULT_SUBOPTS},
[{TopicFilter, 0}] = emqx_channel:check_sub_acls([TopicFilter], channel()).
[{TopicFilter, 0}] = emqx_channel:check_sub_authzs([TopicFilter], channel()).
t_enrich_connack_caps(_) ->
ok = meck:new(emqx_mqtt_caps, [passthrough, no_history]),
@ -763,7 +917,7 @@ t_ws_cookie_init(_) ->
conn_mod => emqx_ws_connection,
ws_cookie => WsCookie
},
Channel = emqx_channel:init(ConnInfo, [{zone, zone}]),
Channel = emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_tcp}),
?assertMatch(#{ws_cookie := WsCookie}, emqx_channel:info(clientinfo, Channel)).
%%--------------------------------------------------------------------
@ -788,7 +942,7 @@ channel(InitFields) ->
maps:fold(fun(Field, Value, Channel) ->
emqx_channel:set_field(Field, Value, Channel)
end,
emqx_channel:init(ConnInfo, [{zone, zone}]),
emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_tcp}),
maps:merge(#{clientinfo => clientinfo(),
session => session(),
conn_state => connected
@ -796,7 +950,8 @@ channel(InitFields) ->
clientinfo() -> clientinfo(#{}).
clientinfo(InitProps) ->
maps:merge(#{zone => zone,
maps:merge(#{zone => default,
listener => mqtt_tcp,
protocol => mqtt,
peerhost => {127,0,0,1},
clientid => <<"clientid">>,
@ -828,7 +983,7 @@ session(InitFields) when is_map(InitFields) ->
maps:fold(fun(Field, Value, Session) ->
emqx_session:set_field(Field, Value, Session)
end,
emqx_session:init(#{zone => channel}, #{receive_maximum => 0}),
emqx_session:init(#{max_inflight => 0}),
InitFields).
%% conn: 5/s; overall: 10/s

View File

@ -78,17 +78,14 @@ groups() ->
init_per_suite(Config) ->
emqx_ct_helpers:boot_modules(all),
emqx_ct_helpers:start_apps([], fun set_special_confs/1),
emqx_ct_helpers:start_apps([]),
emqx_config:put_listener_conf(default, mqtt_ssl, [ssl, verify], verify_peer),
emqx_listeners:restart_listener('default:mqtt_ssl'),
Config.
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]).
set_special_confs(emqx) ->
emqx_ct_helpers:change_emqx_opts(ssl_twoway, [{peer_cert_as_username, cn}]);
set_special_confs(_) ->
ok.
%%--------------------------------------------------------------------
%% Test cases for MQTT v3
%%--------------------------------------------------------------------
@ -104,8 +101,7 @@ t_basic_v4(_Config) ->
t_basic([{proto_ver, v4}]).
t_cm(_) ->
IdleTimeout = emqx_zone:get_env(external, idle_timeout, 30000),
emqx_zone:set_env(external, idle_timeout, 1000),
emqx_config:put_zone_conf(default, [mqtt, idle_timeout], 1000),
ClientId = <<"myclient">>,
{ok, C} = emqtt:start_link([{clientid, ClientId}]),
{ok, _} = emqtt:connect(C),
@ -115,7 +111,7 @@ t_cm(_) ->
ct:sleep(1200),
Stats = emqx_cm:get_chan_stats(ClientId),
?assertEqual(1, proplists:get_value(subscriptions_cnt, Stats)),
emqx_zone:set_env(external, idle_timeout, IdleTimeout).
emqx_config:put_zone_conf(default, [mqtt, idle_timeout], 15000).
t_cm_registry(_) ->
Info = supervisor:which_children(emqx_cm_sup),
@ -273,15 +269,13 @@ t_basic(_Opts) ->
ok = emqtt:disconnect(C).
t_username_as_clientid(_) ->
emqx_zone:set_env(external, use_username_as_clientid, true),
emqx_config:put_zone_conf(default, [mqtt, use_username_as_clientid], true),
Username = <<"usera">>,
{ok, C} = emqtt:start_link([{username, Username}]),
{ok, _} = emqtt:connect(C),
#{clientinfo := #{clientid := Username}} = emqx_cm:get_chan_info(Username),
emqtt:disconnect(C).
t_certcn_as_clientid_default_config_tls(_) ->
tls_certcn_as_clientid(default).
@ -329,7 +323,7 @@ tls_certcn_as_clientid(TLSVsn) ->
tls_certcn_as_clientid(TLSVsn, RequiredTLSVsn) ->
CN = <<"Client">>,
emqx_zone:set_env(external, use_username_as_clientid, true),
emqx_config:put_zone_conf(default, [mqtt, peer_cert_as_clientid], cn),
SslConf = emqx_ct_helpers:client_ssl_twoway(TLSVsn),
{ok, Client} = emqtt:start_link([{port, 8883}, {ssl, true}, {ssl_opts, SslConf}]),
{ok, _} = emqtt:connect(Client),

View File

@ -89,7 +89,7 @@ t_open_session(_) ->
ok = meck:expect(emqx_connection, call, fun(_, _) -> ok end),
ok = meck:expect(emqx_connection, call, fun(_, _, _) -> ok end),
ClientInfo = #{zone => external,
ClientInfo = #{zone => default, listener => mqtt_tcp,
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1}},
@ -114,7 +114,7 @@ rand_client_id() ->
t_open_session_race_condition(_) ->
ClientId = rand_client_id(),
ClientInfo = #{zone => external,
ClientInfo = #{zone => default, listener => mqtt_tcp,
clientid => ClientId,
username => <<"username">>,
peerhost => {127,0,0,1}},

View File

@ -42,26 +42,26 @@ end_per_testcase(_TestCase, Config) ->
Config.
t_is_enabled(_) ->
application:set_env(emqx, enable_session_registry, false),
emqx_config:put([broker, enable_session_registry], false),
?assertEqual(false, emqx_cm_registry:is_enabled()),
application:set_env(emqx, enable_session_registry, true),
emqx_config:put([broker, enable_session_registry], true),
?assertEqual(true, emqx_cm_registry:is_enabled()).
t_register_unregister_channel(_) ->
ClientId = <<"clientid">>,
application:set_env(emqx, enable_session_registry, false),
emqx_config:put([broker, enable_session_registry], false),
emqx_cm_registry:register_channel(ClientId),
?assertEqual([], emqx_cm_registry:lookup_channels(ClientId)),
application:set_env(emqx, enable_session_registry, true),
emqx_config:put([broker, enable_session_registry], true),
emqx_cm_registry:register_channel(ClientId),
?assertEqual([self()], emqx_cm_registry:lookup_channels(ClientId)),
application:set_env(emqx, enable_session_registry, false),
emqx_config:put([broker, enable_session_registry], false),
emqx_cm_registry:unregister_channel(ClientId),
?assertEqual([self()], emqx_cm_registry:lookup_channels(ClientId)),
application:set_env(emqx, enable_session_registry, true),
emqx_config:put([broker, enable_session_registry], true),
emqx_cm_registry:unregister_channel(ClientId),
?assertEqual([], emqx_cm_registry:lookup_channels(ClientId)).

Some files were not shown because too many files have changed in this diff Show More