From 4dedf82c528bf0b441f02af6e1e1fe22806cc941 Mon Sep 17 00:00:00 2001 From: pingww Date: Tue, 8 Mar 2022 12:04:07 +0800 Subject: [PATCH] init --- .gitignore | 3 + README.md | 75 ++- assembly.xml | 37 ++ bin/mqtt.sh | 93 +++ bin/runserver.sh | 93 +++ conf/connect.conf | 18 + conf/logback.xml | 76 +++ conf/service.conf | 22 + conf/spring.xml | 25 + mqtt-common/pom.xml | 50 ++ .../mqtt/common/facade/AuthManager.java | 33 ++ .../mqtt/common/facade/LmqOffsetStore.java | 45 ++ .../mqtt/common/facade/LmqQueueStore.java | 92 +++ .../common/facade/MetaPersistManager.java | 44 ++ .../common/hook/AbstractUpstreamHook.java | 70 +++ .../rocketmq/mqtt/common/hook/Hook.java | 36 ++ .../rocketmq/mqtt/common/hook/HookResult.java | 109 ++++ .../mqtt/common/hook/UpstreamHook.java | 37 ++ .../mqtt/common/hook/UpstreamHookEnum.java | 25 + .../mqtt/common/hook/UpstreamHookManager.java | 45 ++ .../rocketmq/mqtt/common/model/Constants.java | 46 ++ .../rocketmq/mqtt/common/model/Message.java | 217 +++++++ .../mqtt/common/model/MessageEvent.java | 79 +++ .../common/model/MqttMessageUpContext.java | 59 ++ .../rocketmq/mqtt/common/model/MqttTopic.java | 47 ++ .../mqtt/common/model/PullResult.java | 65 +++ .../rocketmq/mqtt/common/model/Queue.java | 116 ++++ .../mqtt/common/model/QueueOffset.java | 72 +++ .../rocketmq/mqtt/common/model/Remark.java | 33 ++ .../rocketmq/mqtt/common/model/RpcCode.java | 28 + .../rocketmq/mqtt/common/model/RpcHeader.java | 25 + .../mqtt/common/model/StoreResult.java | 41 ++ .../mqtt/common/model/Subscription.java | 114 ++++ .../rocketmq/mqtt/common/model/Trie.java | 240 ++++++++ .../mqtt/common/model/TrieException.java | 41 ++ .../mqtt/common/model/TrieMethod.java | 33 ++ .../mqtt/common/util/HmacSHA1Util.java | 45 ++ .../rocketmq/mqtt/common/util/HostInfo.java | 56 ++ .../mqtt/common/util/MessageUtil.java | 119 ++++ .../mqtt/common/util/NamespaceUtil.java | 70 +++ .../rocketmq/mqtt/common/util/StatUtil.java | 472 +++++++++++++++ .../rocketmq/mqtt/common/util/TopicUtils.java | 195 +++++++ .../rocketmq/mqtt/common/test/TestTrie.java | 37 ++ mqtt-cs/pom.xml | 71 +++ .../mqtt/cs/channel/ChannelCloseFrom.java | 33 ++ .../mqtt/cs/channel/ChannelException.java | 41 ++ .../rocketmq/mqtt/cs/channel/ChannelInfo.java | 254 +++++++++ .../mqtt/cs/channel/ChannelManager.java | 64 +++ .../mqtt/cs/channel/ConnectHandler.java | 65 +++ .../cs/channel/DefaultChannelManager.java | 151 +++++ .../rocketmq/mqtt/cs/config/ConnectConf.java | 184 ++++++ .../mqtt/cs/config/ConnectConfListener.java | 73 +++ .../mqtt/cs/hook/UpstreamHookManagerImpl.java | 74 +++ .../protocol/mqtt/MqttPacketDispatcher.java | 179 ++++++ .../cs/protocol/mqtt/MqttPacketHandler.java | 37 ++ .../mqtt/handler/MqttConnectHandler.java | 111 ++++ .../mqtt/handler/MqttDisconnectHandler.java | 45 ++ .../mqtt/handler/MqttPingHandler.java | 57 ++ .../mqtt/handler/MqttPubAckHandler.java | 58 ++ .../mqtt/handler/MqttPubCompHandler.java | 67 +++ .../mqtt/handler/MqttPubRecHandler.java | 60 ++ .../mqtt/handler/MqttPubRelHandler.java | 51 ++ .../mqtt/handler/MqttPublishHandler.java | 114 ++++ .../mqtt/handler/MqttSubscribeHandler.java | 130 +++++ .../mqtt/handler/MqttUnSubscribeHandler.java | 91 +++ .../cs/protocol/rpc/RpcPacketDispatcher.java | 86 +++ .../protocol/ws/WebSocketServerHandler.java | 109 ++++ .../mqtt/cs/protocol/ws/WebsocketEncoder.java | 38 ++ .../rocketmq/mqtt/cs/session/QueueFresh.java | 65 +++ .../rocketmq/mqtt/cs/session/Session.java | 469 +++++++++++++++ .../mqtt/cs/session/infly/InFlyCache.java | 191 +++++++ .../mqtt/cs/session/infly/MqttMsgId.java | 93 +++ .../mqtt/cs/session/infly/PushAction.java | 187 ++++++ .../mqtt/cs/session/infly/RetryDriver.java | 327 +++++++++++ .../cs/session/loop/PullResultStatus.java | 26 + .../mqtt/cs/session/loop/QueueCache.java | 318 +++++++++++ .../mqtt/cs/session/loop/SessionLoop.java | 100 ++++ .../mqtt/cs/session/loop/SessionLoopImpl.java | 536 ++++++++++++++++++ .../mqtt/cs/session/match/MatchAction.java | 161 ++++++ .../session/notify/MessageNotifyAction.java | 88 +++ .../rocketmq/mqtt/cs/starter/MqttServer.java | 132 +++++ .../rocketmq/mqtt/cs/starter/RpcServer.java | 65 +++ .../rocketmq/mqtt/cs/starter/Startup.java | 36 ++ .../cs/test/TestDefaultChannelManager.java | 60 ++ .../rocketmq/mqtt/cs/test/TestInFlyCache.java | 49 ++ .../mqtt/cs/test/TestMatchAction.java | 69 +++ .../mqtt/cs/test/TestMessageNotifyAction.java | 87 +++ .../rocketmq/mqtt/cs/test/TestMqttMsgId.java | 38 ++ .../rocketmq/mqtt/cs/test/TestPushAction.java | 96 ++++ .../rocketmq/mqtt/cs/test/TestQueueCache.java | 112 ++++ .../mqtt/cs/test/TestRetryDriver.java | 91 +++ .../rocketmq/mqtt/cs/test/TestSession.java | 69 +++ .../mqtt/cs/test/TestSessionLoopImpl.java | 157 +++++ mqtt-ds/pom.xml | 63 ++ .../mqtt/ds/auth/AuthManagerSample.java | 96 ++++ .../rocketmq/mqtt/ds/config/ServiceConf.java | 139 +++++ .../mqtt/ds/config/ServiceConfListener.java | 73 +++ .../mqtt/ds/meta/FirstTopicManager.java | 162 ++++++ .../ds/meta/MetaPersistManagerSample.java | 128 +++++ .../mqtt/ds/meta/TopicNotExistException.java | 41 ++ .../mqtt/ds/meta/WildcardManager.java | 129 +++++ .../apache/rocketmq/mqtt/ds/mq/MqAdmin.java | 53 ++ .../rocketmq/mqtt/ds/mq/MqConsumer.java | 84 +++ .../apache/rocketmq/mqtt/ds/mq/MqFactory.java | 114 ++++ .../rocketmq/mqtt/ds/mq/MqProducer.java | 62 ++ .../rocketmq/mqtt/ds/mq/MqPullConsumer.java | 66 +++ .../mqtt/ds/notify/NotifyManager.java | 286 ++++++++++ .../mqtt/ds/notify/NotifyRetryManager.java | 97 ++++ .../mqtt/ds/store/LmqOffsetStoreManager.java | 155 +++++ .../mqtt/ds/store/LmqQueueStoreManager.java | 427 ++++++++++++++ .../mqtt/ds/upstream/UpstreamProcessor.java | 30 + .../ds/upstream/UpstreamProcessorManager.java | 79 +++ .../ds/upstream/processor/BaseProcessor.java | 37 ++ .../upstream/processor/ConnectProcessor.java | 46 ++ .../processor/DisconnectProcessor.java | 37 ++ .../upstream/processor/PublishProcessor.java | 77 +++ .../processor/SubscribeProcessor.java | 56 ++ .../processor/UnSubscribeProcessor.java | 53 ++ .../mqtt/ds/test/TestFirstTopicManager.java | 96 ++++ .../ds/test/TestLmqQueueStoreManager.java | 115 ++++ .../mqtt/ds/test/TestNotifyManager.java | 90 +++ .../mqtt/ds/test/TestWildcardManager.java | 58 ++ mqtt-example/pom.xml | 30 + .../rocketmq/mqtt/example/MqttConsumer.java | 98 ++++ .../rocketmq/mqtt/example/MqttProducer.java | 109 ++++ .../mqtt/example/RocketMQConsumer.java | 68 +++ .../mqtt/example/RocketMQProducer.java | 110 ++++ pom.xml | 189 ++++++ 128 files changed, 12695 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 assembly.xml create mode 100644 bin/mqtt.sh create mode 100644 bin/runserver.sh create mode 100644 conf/connect.conf create mode 100644 conf/logback.xml create mode 100644 conf/service.conf create mode 100644 conf/spring.xml create mode 100644 mqtt-common/pom.xml create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/AuthManager.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/LmqOffsetStore.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/LmqQueueStore.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/MetaPersistManager.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/AbstractUpstreamHook.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/Hook.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/HookResult.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/UpstreamHook.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/UpstreamHookEnum.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/UpstreamHookManager.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Constants.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Message.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/MessageEvent.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/MqttMessageUpContext.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/MqttTopic.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/PullResult.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Queue.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/QueueOffset.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Remark.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/RpcCode.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/RpcHeader.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/StoreResult.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Subscription.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Trie.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/TrieException.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/TrieMethod.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/HmacSHA1Util.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/HostInfo.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/MessageUtil.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/NamespaceUtil.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/StatUtil.java create mode 100644 mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/TopicUtils.java create mode 100644 mqtt-common/src/test/java/org/apache/rocketmq/mqtt/common/test/TestTrie.java create mode 100644 mqtt-cs/pom.xml create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelCloseFrom.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelException.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelInfo.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelManager.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ConnectHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/DefaultChannelManager.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/config/ConnectConf.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/config/ConnectConfListener.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/hook/UpstreamHookManagerImpl.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/MqttPacketDispatcher.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/MqttPacketHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttConnectHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttDisconnectHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPingHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubAckHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubCompHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubRecHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubRelHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPublishHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttSubscribeHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttUnSubscribeHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/rpc/RpcPacketDispatcher.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/ws/WebSocketServerHandler.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/ws/WebsocketEncoder.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/QueueFresh.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/Session.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/InFlyCache.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/MqttMsgId.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/PushAction.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/RetryDriver.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/PullResultStatus.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/QueueCache.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/SessionLoop.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/SessionLoopImpl.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/match/MatchAction.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/notify/MessageNotifyAction.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/MqttServer.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/RpcServer.java create mode 100644 mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/Startup.java create mode 100644 mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestDefaultChannelManager.java create mode 100644 mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestInFlyCache.java create mode 100644 mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestMatchAction.java create mode 100644 mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestMessageNotifyAction.java create mode 100644 mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestMqttMsgId.java create mode 100644 mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestPushAction.java create mode 100644 mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestQueueCache.java create mode 100644 mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestRetryDriver.java create mode 100644 mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestSession.java create mode 100644 mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestSessionLoopImpl.java create mode 100644 mqtt-ds/pom.xml create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/auth/AuthManagerSample.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/config/ServiceConf.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/config/ServiceConfListener.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/FirstTopicManager.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/MetaPersistManagerSample.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/TopicNotExistException.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/WildcardManager.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqAdmin.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqConsumer.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqFactory.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqProducer.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqPullConsumer.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/notify/NotifyManager.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/notify/NotifyRetryManager.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/store/LmqOffsetStoreManager.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/store/LmqQueueStoreManager.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/UpstreamProcessor.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/UpstreamProcessorManager.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/BaseProcessor.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/ConnectProcessor.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/DisconnectProcessor.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/PublishProcessor.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/SubscribeProcessor.java create mode 100644 mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/UnSubscribeProcessor.java create mode 100644 mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestFirstTopicManager.java create mode 100644 mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestLmqQueueStoreManager.java create mode 100644 mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestNotifyManager.java create mode 100644 mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestWildcardManager.java create mode 100644 mqtt-example/pom.xml create mode 100644 mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/MqttConsumer.java create mode 100644 mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/MqttProducer.java create mode 100644 mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/RocketMQConsumer.java create mode 100644 mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/RocketMQProducer.java create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..7466b121eb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +*.iml +target/ diff --git a/README.md b/README.md index f2609dea90d..c2676d65b44 100644 --- a/README.md +++ b/README.md @@ -1 +1,74 @@ -# rocketmq-mqtt +## Apache RocketMQ MQTT +A new MQTT protocol architecture model, based on which RocketMQ can better support messages from terminals such as IoT devices and Mobile APP. Based on the RocketMQ message unified storage engine, it supports both MQTT terminal and server message sending and receiving. + +## Architecture +The relevant architecture design is introduced in [RIP-33](https://docs.google.com/document/d/1AD1GkV9mqE_YFA97uVem4SmB8ZJSXiJZvzt7-K6Jons/edit#) + + +## Get Started + +### Prerequisites +The queue model of MQTT needs to be based on the lightweight queue feature ([RIP-28](https://github.com/apache/rocketmq/pull/3694)) of RocketMQ. RocketMQ has only supported this feature since version 4.9.3. Please ensure that the installed version of RocketMQ already supports this feature. + +1. Clone +```shell +git clone https://github.com/apache/rocketmq-mqtt +``` +2. Build the package +```shell +cd rocketmq-mqtt +mvn clean package -DskipTests=true assembly:assembly +``` +3. Config +```shell +cp -r target/rocketmq-mqtt ~ +cd ~/rocketmq-mqtt +cd conf +``` +Some important configuration items in the **service.conf** configuration file + +**Config Key** | **Instruction** +----- | ---- +username | used for auth +secretKey | used for auth +NAMESRV_ADDR | specify namesrv address +eventNotifyRetryTopic | notify event retry topic +clientRetryTopic | client retry topic + +4. CreateTopic + + create all first-level topics, including **eventNotifyRetryTopic** and **clientRetryTopic** in the configuration file above. +```shell +sh mqadmin updatetopic -c {cluster} -t {topic} -n {namesrv} +``` +5. Initialize Meta +- Configure Gateway Node List +```shell +sh mqadmin updateKvConfig -s LMQ -k LMQ_CONNECT_NODES -v {ip1,ip2} -n {namesrv} +``` +- Configure the first-level topic list +```shell +sh mqadmin updateKvConfig -s LMQ -k ALL_FIRST_TOPICS -v {topic1,topic2} -n {namesrv} +``` +- Configure a list of wildcard characters under each first-level topic +```shell +sh mqadmin updateKvConfig -s LMQ -k {topic} -v {topic/+} -n {namesrv} +``` +6. Start Process +```shell +cd bin +sh mqtt.sh start +``` +### Example +The mqtt-example module has written basic usage example code, which can be used for reference + +## Protocol Version +The currently supported protocol version is [MQTT 3.1.1](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.pdf), but the will and retain features are not supported yet + +## Authentication +At present, an implementation based on the HmacSHA1 signature algorithm is provided by default, Refer to **AuthManagerSample**. Users can customize other implementations to meet the needs of businesses to flexibly verify resources and identities. +## Meta Persistence +At present, meta data storage and management is simply implemented through the kvconfig mechanism of namesrv by default, Refer to **MetaPersistManagerSample**. Users can customize other implementations. + +## License +[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) Copyright (C) Apache Software Foundation. diff --git a/assembly.xml b/assembly.xml new file mode 100644 index 00000000000..19ffe3de0e4 --- /dev/null +++ b/assembly.xml @@ -0,0 +1,37 @@ + + + + dir + tar.gz + + + + + bin/* + conf/** + + + **/src/** + **/target/** + **/.*/** + + + + + + + org.apache.rocketmq:mqtt-cs + org.apache.rocketmq:mqtt-ds + + + lib + false + + + lib + + + + + + diff --git a/bin/mqtt.sh b/bin/mqtt.sh new file mode 100644 index 00000000000..d0bbbb43f00 --- /dev/null +++ b/bin/mqtt.sh @@ -0,0 +1,93 @@ +#!/bin/sh + +# +# /* +# * Licensed to the Apache Software Foundation (ASF) under one or more +# * contributor license agreements. See the NOTICE file distributed with +# * this work for additional information regarding copyright ownership. +# * The ASF licenses this file to You under the Apache License, Version 2.0 +# * (the "License"); you may not use this file except in compliance with +# * the License. You may obtain a copy of the License at +# * +# * http://www.apache.org/licenses/LICENSE-2.0 +# * +# * Unless required by applicable law or agreed to in writing, software +# * distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. +# */ +# + +if [ -z "$ROCKETMQ_MQTT_HOME" ]; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ]; do + ls=$(ls -ld "$PRG") + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' >/dev/null; then + PRG="$link" + else + PRG="$(dirname "$PRG")/$link" + fi + done + + saveddir=$(pwd) + + ROCKETMQ_MQTT_HOME=$(dirname "$PRG")/.. + + # make it fully qualified + ROCKETMQ_MQTT_HOME=$(cd "$ROCKETMQ_MQTT_HOME" && pwd) + + cd "$saveddir" +fi + +export ROCKETMQ_MQTT_HOME + +BASEDIR=$HOME +mkdir -p $BASEDIR/logs + +mainClass="org.apache.rocketmq.mqtt.cs.starter.Startup" + +function startup() { + pid=`ps aux|grep $mainClass|grep -v grep |awk '{print $2}'` + if [ ! -z "$pid" ]; then + echo "java is runing..." + exit 1 + fi + nohup sh ${ROCKETMQ_MQTT_HOME}/bin/runserver.sh $mainClass $@ >$BASEDIR/logs/start_out.log 2>&1 & +} + +function stop() { + pid=`ps aux|grep $mainClass|grep -v grep |awk '{print $2}'` + if [ -z "$pid" ]; then + echo "no java to kill" + fi + printf 'stop...' + kill $pid + sleep 3 + pid=`ps aux|grep $mainClass|grep -v grep |awk '{print $2}'` + + if [ ! -z $pid ]; then + kill -9 $pid + fi +} + +case "$1" in +start) + startup $@ + ;; +stop) + stop + ;; +restart) + stop + startup + ;; +*) + printf "Usage: sh $0 %s {start|stop|restart}\n" + exit 1 + ;; +esac diff --git a/bin/runserver.sh b/bin/runserver.sh new file mode 100644 index 00000000000..de9541c7462 --- /dev/null +++ b/bin/runserver.sh @@ -0,0 +1,93 @@ +#!/bin/sh + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#=========================================================================================== +# Java Environment Setting +#=========================================================================================== +error_exit () +{ + echo "ERROR: $1 !!" + exit 1 +} + +[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java +[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java +[ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!" + +export JAVA_HOME +export JAVA="$JAVA_HOME/bin/java" +export BASE_DIR=$(dirname $0)/.. +export CLASSPATH=.:${BASE_DIR}/conf:${CLASSPATH} + +#=========================================================================================== +# JVM Configuration +#=========================================================================================== +# The RAMDisk initializing size in MB on Darwin OS for gc-log +DIR_SIZE_IN_MB=600 + +choose_gc_log_directory() +{ + case "`uname`" in + Darwin) + if [ ! -d "/Volumes/RAMDisk" ]; then + # create ram disk on Darwin systems as gc-log directory + DEV=`hdiutil attach -nomount ram://$((2 * 1024 * DIR_SIZE_IN_MB))` > /dev/null + diskutil eraseVolume HFS+ RAMDisk ${DEV} > /dev/null + echo "Create RAMDisk /Volumes/RAMDisk for gc logging on Darwin OS." + fi + GC_LOG_DIR="/Volumes/RAMDisk" + ;; + *) + # check if /dev/shm exists on other systems + if [ -d "/dev/shm" ]; then + GC_LOG_DIR="/dev/shm" + else + GC_LOG_DIR=${BASE_DIR} + fi + ;; + esac +} + +choose_gc_options() +{ + # Example of JAVA_MAJOR_VERSION value : '1', '9', '10', '11', ... + # '1' means releases befor Java 9 + JAVA_MAJOR_VERSION=$("$JAVA" -version 2>&1 | sed -r -n 's/.* version "([0-9]*).*$/\1/p') + if [ -z "$JAVA_MAJOR_VERSION" ] || [ "$JAVA_MAJOR_VERSION" -lt "9" ] ; then + JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" + JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC" + JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps" + JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m" + else + JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" + JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0" + JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M" + fi +} + +choose_gc_log_directory +choose_gc_options +JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow" +JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages" +JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib:${JAVA_HOME}/lib/ext" +#JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n" +JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}" +JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}" + +JAVA_OPT="${JAVA_OPT} -Dlogback.configurationFile=${BASE_DIR}/conf/logback.xml" + +$JAVA ${JAVA_OPT} $@ \ No newline at end of file diff --git a/conf/connect.conf b/conf/connect.conf new file mode 100644 index 00000000000..35f34b71e2c --- /dev/null +++ b/conf/connect.conf @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +mqttPort=1883 + diff --git a/conf/logback.xml b/conf/logback.xml new file mode 100644 index 00000000000..b43d14729f3 --- /dev/null +++ b/conf/logback.xml @@ -0,0 +1,76 @@ + + + + ${user.home}/logs/mqtt.log + true + + ${user.home}/logs/mqtt.%d{yyyy-MM-dd}.log + 10 + + + %d{yyy-MM-dd HH:mm:ss:SSS,GMT+8} %p [%logger{0}] %m%n + UTF-8 + + + + + + + + ${user.home}/logs/rmq.log + true + + ${user.home}/logs/rmq.%d{yyyy-MM-dd}.log + 10 + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p [%logger{0}] %m%n + UTF-8 + + + + + + + + + ${user.home}/logs/stat.log + true + + ${user.home}/logs/stat.%d{yyyy-MM-dd}.log + 10 + 1GB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %m%n + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/service.conf b/conf/service.conf new file mode 100644 index 00000000000..2be8cdc6bc0 --- /dev/null +++ b/conf/service.conf @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +username= +secretKey= + +NAMESRV_ADDR= +eventNotifyRetryTopic= +clientRetryTopic= diff --git a/conf/spring.xml b/conf/spring.xml new file mode 100644 index 00000000000..2f415261c46 --- /dev/null +++ b/conf/spring.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/mqtt-common/pom.xml b/mqtt-common/pom.xml new file mode 100644 index 00000000000..f5c75b4084f --- /dev/null +++ b/mqtt-common/pom.xml @@ -0,0 +1,50 @@ + + + + rocketmq-mqtt + org.apache.rocketmq + 1.0.0-SNAPSHOT + + 4.0.0 + + mqtt-common + + + + junit + junit + test + + + org.mockito + mockito-core + test + + + org.apache.commons + commons-lang3 + + + io.netty + netty-all + + + org.apache.rocketmq + rocketmq-client + + + com.alibaba + fastjson + + + org.slf4j + slf4j-api + + + commons-codec + commons-codec + + + \ No newline at end of file diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/AuthManager.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/AuthManager.java new file mode 100644 index 00000000000..16f620becaa --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/AuthManager.java @@ -0,0 +1,33 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.facade; + +import io.netty.handler.codec.mqtt.MqttMessage; +import org.apache.rocketmq.mqtt.common.hook.HookResult; + +public interface AuthManager { + /** + * MQTT packet authentication + * + * @param message + * @return + */ + HookResult doAuth(MqttMessage message); +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/LmqOffsetStore.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/LmqOffsetStore.java new file mode 100644 index 00000000000..b3182dea74f --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/LmqOffsetStore.java @@ -0,0 +1,45 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.facade; + +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.QueueOffset; +import org.apache.rocketmq.mqtt.common.model.Subscription; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public interface LmqOffsetStore { + /** + * save lmq offset + * @param clientId + * @param offsetMap + */ + void save(String clientId, Map> offsetMap); + + /** + * get lmq offset + * @param clientId + * @param subscription + * @return + */ + CompletableFuture> getOffset(String clientId, Subscription subscription); +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/LmqQueueStore.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/LmqQueueStore.java new file mode 100644 index 00000000000..3e71ac7d472 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/LmqQueueStore.java @@ -0,0 +1,92 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.facade; + +import org.apache.rocketmq.mqtt.common.model.*; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public interface LmqQueueStore { + String LMQ_PREFIX = "%LMQ%"; + String PROPERTY_INNER_MULTI_DISPATCH = "INNER_MULTI_DISPATCH"; + String PROPERTY_INNER_MULTI_QUEUE_OFFSET = "INNER_MULTI_QUEUE_OFFSET"; + String MULTI_DISPATCH_QUEUE_SPLITTER = ","; + + /** + * put message and atomic dispatch to multiple queues + * + * @param queues + * @param message + * @return + */ + CompletableFuture putMessage(Set queues, Message message); + + /** + * pull messages + * + * @param firstTopic + * @param queue + * @param queueOffset + * @param count + * @return + */ + CompletableFuture pullMessage(String firstTopic, Queue queue, QueueOffset queueOffset, long count); + + /** + * pull last messages + * + * @param firstTopic + * @param queue + * @param count + * @return + */ + CompletableFuture pullLastMessages(String firstTopic, Queue queue, long count); + + /** + * query maxId of Queue + * + * @param queue + * @return + */ + CompletableFuture queryQueueMaxOffset(Queue queue); + + /** + * get readable brokers of the topic + * + * @param firstTopic + * @return + */ + Set getReadableBrokers(String firstTopic); + + /** + * retry topic of one mqtt client + * + * @return + */ + String getClientRetryTopic(); + + /** + * p2p topic of one mqtt client + * + * @return + */ + String getClientP2pTopic(); +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/MetaPersistManager.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/MetaPersistManager.java new file mode 100644 index 00000000000..805a3c84014 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/facade/MetaPersistManager.java @@ -0,0 +1,44 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.facade; + +import java.util.Set; + +public interface MetaPersistManager { + + /** + * get wildcards of the first topic + * @param firstTopic + * @return + */ + Set getWildcards(String firstTopic); + + /** + * get all first topics + * @return + */ + Set getAllFirstTopics(); + + /** + * get all connect nodes + * @return + */ + Set getConnectNodeSet(); +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/AbstractUpstreamHook.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/AbstractUpstreamHook.java new file mode 100644 index 00000000000..2ea972a7eca --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/AbstractUpstreamHook.java @@ -0,0 +1,70 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.hook; + +import io.netty.handler.codec.mqtt.MqttMessage; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; + +public abstract class AbstractUpstreamHook implements UpstreamHook { + public static Logger logger = LoggerFactory.getLogger(AbstractUpstreamHook.class); + public UpstreamHook nextUpstreamHook; + + @Override + public void setNextHook(Hook hook) { + this.nextUpstreamHook = (UpstreamHook) hook; + } + + @Override + public Hook getNextHook() { + return this.nextUpstreamHook; + } + + @Override + public CompletableFuture doHook(MqttMessageUpContext context, MqttMessage msg) { + try { + CompletableFuture result = processMqttMessage(context,msg); + if (nextUpstreamHook == null) { + return result; + } + return result.thenCompose(hookResult -> { + if (!hookResult.isSuccess()) { + CompletableFuture nextHookResult = new CompletableFuture<>(); + nextHookResult.complete(hookResult); + return nextHookResult; + } + return nextUpstreamHook.doHook(context, msg); + }); + } catch (Throwable t) { + logger.error("",t); + CompletableFuture result = new CompletableFuture<>(); + result.completeExceptionally(t); + return result; + } + } + + public abstract void register(); + + public abstract CompletableFuture processMqttMessage(MqttMessageUpContext context, MqttMessage message); + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/Hook.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/Hook.java new file mode 100644 index 00000000000..2d6824cec41 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/Hook.java @@ -0,0 +1,36 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.hook; + +public interface Hook { + /** + * set next hook + * + * @param Hook + */ + void setNextHook(Hook Hook); + + /** + * get next hook + * + * @return + */ + Hook getNextHook(); +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/HookResult.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/HookResult.java new file mode 100644 index 00000000000..3502cdb2fba --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/HookResult.java @@ -0,0 +1,109 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.hook; + +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +public class HookResult { + public static final int SUCCESS = 200; + public static final int FAIL = -200; + private int code; + private int subCode; + private String remark; + private byte[] data; + + public HookResult(int code, int subCode, String remark, byte[] data) { + this.code = code; + this.subCode = subCode; + this.remark = remark; + this.data = data; + } + + public HookResult(int code, String remark, byte[] data) { + this.code = code; + this.remark = remark; + this.data = data; + } + + public static CompletableFuture newHookResult(int code, String remark, byte[] data) { + CompletableFuture result = new CompletableFuture<>(); + result.complete(new HookResult(code, remark, data)); + return result; + } + + public static CompletableFuture newHookResult(int code, int subCode, String remark, byte[] data) { + CompletableFuture result = new CompletableFuture<>(); + result.complete(new HookResult(code, subCode, remark, data)); + return result; + } + + public boolean isSuccess() { + return SUCCESS == code; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public int getSubCode() { + return subCode; + } + + public void setSubCode(int subCode) { + this.subCode = subCode; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HookResult that = (HookResult) o; + return code == that.code && subCode == that.subCode && Objects.equals(remark, that.remark) && Arrays.equals(data, that.data); + } + + @Override + public int hashCode() { + int result = Objects.hash(code, subCode, remark); + result = 31 * result + Arrays.hashCode(data); + return result; + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/UpstreamHook.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/UpstreamHook.java new file mode 100644 index 00000000000..34b57b0ae2a --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/UpstreamHook.java @@ -0,0 +1,37 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.hook; + +import io.netty.handler.codec.mqtt.MqttMessage; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; + +import java.util.concurrent.CompletableFuture; + +public interface UpstreamHook extends Hook{ + + /** + * do hook in upstream + * @param context + * @param msg + * @return + */ + CompletableFuture doHook(MqttMessageUpContext context, MqttMessage msg); + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/UpstreamHookEnum.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/UpstreamHookEnum.java new file mode 100644 index 00000000000..0d49d1a2bea --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/UpstreamHookEnum.java @@ -0,0 +1,25 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.hook; + +public enum UpstreamHookEnum { + AUTH, + UPSTREAM_PROCESS +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/UpstreamHookManager.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/UpstreamHookManager.java new file mode 100644 index 00000000000..d38fcc7a2f9 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/hook/UpstreamHookManager.java @@ -0,0 +1,45 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.hook; + +import io.netty.handler.codec.mqtt.MqttMessage; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; + +import java.util.concurrent.CompletableFuture; + +public interface UpstreamHookManager { + /** + * add a hook + * + * @param index + * @param upstreamHook + */ + void addHook(int index, UpstreamHook upstreamHook); + + /** + * do hook in upstream + * + * @param context + * @param msg + * @return + */ + CompletableFuture doUpstreamHook(MqttMessageUpContext context, MqttMessage msg); + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Constants.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Constants.java new file mode 100644 index 00000000000..b2503a3bbcc --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Constants.java @@ -0,0 +1,46 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + +public class Constants { + public static final String NAMESPACE_SPLITER = "%"; + public static final String MQTT_TOPIC_DELIMITER = "/"; + + public static final String ADDFLAG = "+"; + public static final String JINFLAG = "#"; + + public static final String P2P = "/p2p/"; + public static final String RETRY = "/retry/"; + + public static String PROPERTY_NAMESPACE = "namespace"; + public static final String PROPERTY_ORIGIN_MQTT_TOPIC = "originMqttTopic"; + public static final String PROPERTY_MQTT_QOS = "qoslevel"; + public static final String PROPERTY_MQTT_CLEAN_SESSION = "cleansessionflag"; + public static final String PROPERTY_MQTT_CLIENT = "clientId"; + public static final String PROPERTY_MQTT_RETRY_TIMES = "retryTimes"; + public static final String PROPERTY_MQTT_EXT_DATA = "extData"; + + + public static final String PROPERTY_MQTT_MSG_EVENT_RETRY_NODE = "retryNode"; + public static final String PROPERTY_MQTT_MSG_EVENT_RETRY_TIME = "retryTime"; + + public static final String MQTT_TAG = "MQTT_COMMON"; + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Message.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Message.java new file mode 100644 index 00000000000..d15a7fcfcbd --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Message.java @@ -0,0 +1,217 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + + +public class Message { + private String msgId; + private String firstTopic; + private String originTopic; + private long offset; + private long nextOffset; + private int retry; + private byte[] payload; + private long bornTimestamp; + private long storeTimestamp; + private int ack = -1; + private Map userProperties = new HashMap<>(); + + public static String propertyFirstTopic = "firstTopic"; + public static String propertyOriginTopic = "originTopic"; + public static String propertyOffset = "offset"; + public static String propertyNextOffset = "nextOffset"; + public static String propertyMsgId = "msgId"; + public static String propertyRetry = "retry"; + public static String propertyBornTime = "bornTime"; + public static String propertyStoreTime = "storeTime"; + public static String propertyUserProperties = "extData"; + + public static String extPropertyMqttRealTopic = "mqttRealTopic"; + public static String extPropertyQoS = "qoslevel"; + public static String extPropertyCleanSessionFlag = "cleansessionflag"; + public static String extPropertyNamespaceId = "namespace"; + public static String extPropertyClientId = "clientId"; + + + public Message copy() { + Message message = new Message(); + message.setMsgId(msgId); + message.setFirstTopic(this.firstTopic); + message.setOriginTopic(this.getOriginTopic()); + message.setOffset(this.getOffset()); + message.setNextOffset(this.getNextOffset()); + message.setRetry(this.getRetry()); + message.setPayload(this.getPayload()); + message.setBornTimestamp(this.bornTimestamp); + message.setStoreTimestamp(this.storeTimestamp); + message.getUserProperties().putAll(this.userProperties); + return message; + } + + public Integer qos() { + if (getUserProperties() == null) { + return null; + } + if (!getUserProperties().containsKey(extPropertyQoS)) { + return null; + } + return Integer.parseInt(getUserProperties().get(extPropertyQoS)); + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public String getFirstTopic() { + return firstTopic; + } + + public void setFirstTopic(String firstTopic) { + this.firstTopic = firstTopic; + } + + public String getOriginTopic() { + return originTopic; + } + + public void setOriginTopic(String originTopic) { + this.originTopic = originTopic; + } + + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public int getRetry() { + return retry; + } + + public void setRetry(int retry) { + this.retry = retry; + } + + public byte[] getPayload() { + return payload; + } + + public void setPayload(byte[] payload) { + this.payload = payload; + } + + public long getNextOffset() { + return nextOffset; + } + + public void setNextOffset(long nextOffset) { + this.nextOffset = nextOffset; + } + + public long getBornTimestamp() { + return bornTimestamp; + } + + public void setBornTimestamp(long bornTimestamp) { + this.bornTimestamp = bornTimestamp; + } + + public long getStoreTimestamp() { + return storeTimestamp; + } + + public void setStoreTimestamp(long storeTimestamp) { + this.storeTimestamp = storeTimestamp; + } + + public int getAck() { + return ack; + } + + public void setAck(int ack) { + this.ack = ack; + } + + public Map getUserProperties() { + return userProperties; + } + + public void putUserProperty(String key, String value) { + if (StringUtils.isBlank(key) || StringUtils.isBlank(value)) { + return; + } + userProperties.put(key, value); + } + + public String getUserProperty(String key) { + if (StringUtils.isBlank(key)) { + return null; + } + return userProperties.get(key); + } + + public void clearUserProperty(String key) { + if (StringUtils.isBlank(key)) { + return; + } + if (userProperties == null) { + return; + } + userProperties.remove(key); + } + + public boolean isP2P() { + if (TopicUtils.isP2P(TopicUtils.decode(firstTopic).getSecondTopic())) { + return true; + } + return false; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Message message = (Message)o; + return offset == message.offset; + } + + @Override + public int hashCode() { + return Objects.hash(offset); + } + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/MessageEvent.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/MessageEvent.java new file mode 100644 index 00000000000..05c3f33bcfe --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/MessageEvent.java @@ -0,0 +1,79 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + +import java.util.Objects; + + +public class MessageEvent { + private String pubTopic; + private String namespace; + private long queueId; + private String brokerName; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MessageEvent that = (MessageEvent) o; + return Objects.equals(pubTopic, that.pubTopic) && Objects.equals(namespace, that.namespace) && Objects.equals(queueId, that.queueId) && Objects.equals(brokerName, that.brokerName); + } + + @Override + public int hashCode() { + return Objects.hash(pubTopic, namespace, queueId, brokerName); + } + + public String getPubTopic() { + return pubTopic; + } + + public void setPubTopic(String pubTopic) { + this.pubTopic = pubTopic; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public long getQueueId() { + return queueId; + } + + public void setQueueId(long queueId) { + this.queueId = queueId; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/MqttMessageUpContext.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/MqttMessageUpContext.java new file mode 100644 index 00000000000..234c7559625 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/MqttMessageUpContext.java @@ -0,0 +1,59 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + +public class MqttMessageUpContext { + private String namespace; + private String clientId; + private String channelId; + private String node; + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public String getNode() { + return node; + } + + public void setNode(String node) { + this.node = node; + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/MqttTopic.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/MqttTopic.java new file mode 100644 index 00000000000..894a03bbcbc --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/MqttTopic.java @@ -0,0 +1,47 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + + +public class MqttTopic { + private String firstTopic; + private String secondTopic; + + public MqttTopic(String firstTopic, String secondTopic) { + this.firstTopic = firstTopic; + this.secondTopic = secondTopic; + } + + public String getFirstTopic() { + return firstTopic; + } + + public void setFirstTopic(String firstTopic) { + this.firstTopic = firstTopic; + } + + public String getSecondTopic() { + return secondTopic; + } + + public void setSecondTopic(String secondTopic) { + this.secondTopic = secondTopic; + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/PullResult.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/PullResult.java new file mode 100644 index 00000000000..d208c90e5ed --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/PullResult.java @@ -0,0 +1,65 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + + +import java.util.List; + + +public class PullResult { + public static final int PULL_SUCCESS = 301; + public static final int PULL_OFFSET_MOVED = 302; + private int code = PULL_SUCCESS; + private String remark; + private List messageList; + private QueueOffset nextQueueOffset; + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public List getMessageList() { + return messageList; + } + + public void setMessageList(List messageList) { + this.messageList = messageList; + } + + public QueueOffset getNextQueueOffset() { + return nextQueueOffset; + } + + public void setNextQueueOffset(QueueOffset nextQueueOffset) { + this.nextQueueOffset = nextQueueOffset; + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Queue.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Queue.java new file mode 100644 index 00000000000..e286827817a --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Queue.java @@ -0,0 +1,116 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; + + +public class Queue { + protected long queueId; + protected String queueName; + protected String brokerName; + + public Queue() { + } + + public Queue(long queueId, String queueName, String brokerName) { + this.queueId = queueId; + this.queueName = queueName; + this.brokerName = brokerName; + } + + public boolean isLmq() { + return StringUtils.isNotBlank(brokerName); + } + + public String toFirstTopic() { + return TopicUtils.decode(queueName).getFirstTopic(); + } + + public boolean isRetry() { + return TopicUtils.isRetryTopic(queueName); + } + + public boolean isP2p() { + return TopicUtils.isP2pTopic(queueName); + } + + public long getQueueId() { + return queueId; + } + + public void setQueueId(long queueId) { + this.queueId = queueId; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public String getQueueName() { + return queueName; + } + + public void setQueueName(String queueName) { + this.queueName = queueName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Queue queue = (Queue) o; + + if (queueId != queue.queueId) { + return false; + } + if (queueName != null ? !queueName.equals(queue.queueName) : queue.queueName != null) { + return false; + } + return brokerName != null ? brokerName.equals(queue.brokerName) : queue.brokerName == null; + } + + @Override + public int hashCode() { + int result = (int) (queueId ^ (queueId >>> 32)); + result = 31 * result + (queueName != null ? queueName.hashCode() : 0); + result = 31 * result + (brokerName != null ? brokerName.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Queue{" + + "queueId=" + queueId + + ", queueName='" + queueName + '\'' + + ", brokerName='" + brokerName + '\'' + + '}'; + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/QueueOffset.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/QueueOffset.java new file mode 100644 index 00000000000..710bcccd66a --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/QueueOffset.java @@ -0,0 +1,72 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + +import java.util.Objects; + + +public class QueueOffset { + private volatile long offset = Long.MAX_VALUE; + private volatile byte initializingStatus = -1; + + public QueueOffset() { + } + + public QueueOffset(long offset) { + this.offset = offset; + } + + public boolean isInitialized() { + return offset != Long.MAX_VALUE || initializingStatus == 1; + } + + public boolean isInitializing() { + return initializingStatus == 0; + } + + public void setInitialized() { + initializingStatus = 1; + } + + public void setInitializing() { + initializingStatus = 0; + } + + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + QueueOffset that = (QueueOffset) o; + return offset == that.offset && initializingStatus == that.initializingStatus; + } + + @Override + public int hashCode() { + return Objects.hash(offset, initializingStatus); + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Remark.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Remark.java new file mode 100644 index 00000000000..8ae133190c1 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Remark.java @@ -0,0 +1,33 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + + +public class Remark { + + public static final String SUCCESS = "success"; + public static final String FAIL = "fail"; + public static final String CLIENT_ID_CONFLICT = "clientIdConflict"; + public static final String INVALID_PARAM = "Invalid Param"; + public static final String AUTH_FAILED = "Auth Failed"; + public static final String OVERFLOW = "overflow"; + public static final String EXCEPTION = "exception"; + public static final String EXPIRED = "expire"; +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/RpcCode.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/RpcCode.java new file mode 100644 index 00000000000..0a43185b7cd --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/RpcCode.java @@ -0,0 +1,28 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + +public class RpcCode { + public static final int SUCCESS = 1; + public static final int FAIL = -1; + + public static final int CMD_NOTIFY_MQTT_MESSAGE = 201; + public static final int CMD_CLOSE_CHANNEL = 203; +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/RpcHeader.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/RpcHeader.java new file mode 100644 index 00000000000..2b38b833de2 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/RpcHeader.java @@ -0,0 +1,25 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + +public class RpcHeader { + public static final String MQTT_CHANNEL_ID = "channelId"; + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/StoreResult.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/StoreResult.java new file mode 100644 index 00000000000..23a8d22920d --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/StoreResult.java @@ -0,0 +1,41 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + +public class StoreResult { + private Queue queue; + private String msgId; + + public Queue getQueue() { + return queue; + } + + public void setQueue(Queue queue) { + this.queue = queue; + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Subscription.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Subscription.java new file mode 100644 index 00000000000..52788afd542 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Subscription.java @@ -0,0 +1,114 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + +import org.apache.rocketmq.mqtt.common.util.TopicUtils; + + +public class Subscription { + private String topicFilter; + private int qos; + + public Subscription() { + } + + public Subscription(String topicFilter) { + this.topicFilter = topicFilter; + } + + public Subscription(String topicFilter, int qos) { + this.topicFilter = topicFilter; + this.qos = qos; + } + + public boolean isWildCard() { + return topicFilter != null && + (topicFilter.contains(Constants.JINFLAG) || topicFilter.contains(Constants.ADDFLAG)); + } + + public String toFirstTopic() { + return TopicUtils.decode(topicFilter).getFirstTopic(); + } + + public String toQueueName() { + return topicFilter; + } + + public static Subscription newP2pSubscription(String clientId) { + Subscription p2pSubscription = new Subscription(); + p2pSubscription.setTopicFilter(TopicUtils.getP2pTopic(clientId)); + p2pSubscription.setQos(1); + return p2pSubscription; + } + + public static Subscription newRetrySubscription(String clientId) { + Subscription retrySubscription = new Subscription(); + retrySubscription.setTopicFilter(TopicUtils.getRetryTopic(clientId)); + retrySubscription.setQos(1); + return retrySubscription; + } + + public boolean isRetry() { + return TopicUtils.isRetryTopic(topicFilter); + } + + public boolean isP2p() { + return TopicUtils.isP2pTopic(topicFilter); + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + + Subscription that = (Subscription)o; + + return topicFilter != null ? topicFilter.equals(that.topicFilter) : that.topicFilter == null; + } + + @Override + public int hashCode() { + return topicFilter != null ? topicFilter.hashCode() : 0; + } + + public String getTopicFilter() { + return topicFilter; + } + + public void setTopicFilter(String topicFilter) { + this.topicFilter = topicFilter; + } + + public int getQos() { + return qos; + } + + public void setQos(int qos) { + this.qos = qos; + } + + @Override + public String toString() { + return "Subscription{" + + "topicFilter='" + topicFilter + '\'' + + ", qos=" + qos + + '}'; + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Trie.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Trie.java new file mode 100644 index 00000000000..10ffde9aed5 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Trie.java @@ -0,0 +1,240 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + + +import org.apache.rocketmq.mqtt.common.util.TopicUtils; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + + +public class Trie { + + private TrieNode rootNode = new TrieNode(null); + + public synchronized V addNode(String key, V nodeValue, K nodeKey) { + try { + String[] keyArray = key.split(Constants.MQTT_TOPIC_DELIMITER); + TrieNode currentNode = rootNode; + int level = 0; + while (level < keyArray.length) { + TrieNode trieNode = currentNode.children.get(keyArray[level]); + if (trieNode == null) { + trieNode = new TrieNode(currentNode); + TrieNode oldNode = currentNode.children.putIfAbsent(keyArray[level], trieNode); + if (oldNode != null) { + trieNode = oldNode; + } + } + level++; + currentNode = trieNode; + } + V old = currentNode.valueSet.put(nodeKey, nodeValue); + return old; + } catch (Throwable e) { + throw new TrieException(e); + } + } + + /** + * @param key + * @param valueKey + * @return null if can not find the key and valueKey or return the value + */ + public synchronized V deleteNode(String key, K valueKey) { + try { + String[] keyArray = key.split(Constants.MQTT_TOPIC_DELIMITER); + TrieNode currentNode = rootNode; + int level = 0; + while (level < keyArray.length) { + TrieNode trieNode = currentNode.children.get(keyArray[level]); + if (trieNode == null) { + break; + } + level++; + currentNode = trieNode; + } + V oldValue = currentNode.valueSet.remove(valueKey); + //clean the empty node + while (currentNode.children.isEmpty() && currentNode.valueSet.isEmpty()) { + if (currentNode.parentNode != null) { + currentNode.parentNode.children.remove(keyArray[--level]); + currentNode = currentNode.parentNode; + } else { + break; + } + } + return oldValue; + } catch (Throwable e) { + throw new TrieException(e); + } + } + + public long countSubRecords() { + return countLevelRecords(rootNode); + } + + private long countLevelRecords(TrieNode currentNode) { + if (currentNode == null) { + return 0; + } + if (currentNode.children.isEmpty()) { + return currentNode.valueSet.size(); + } + long childrenCount = 0; + for (Map.Entry> entry : currentNode.children.entrySet()) { + childrenCount += countLevelRecords(entry.getValue()); + } + return childrenCount + currentNode.valueSet.size(); + } + + public Map getNode(String key) { + try { + String[] keyArray = key.split(Constants.MQTT_TOPIC_DELIMITER); + Map result = findValueSet(rootNode, keyArray, 0, keyArray.length, false); + return result; + } catch (Throwable e) { + throw new TrieException(e); + } + } + + public void traverseAll(TrieMethod method) { + StringBuilder builder = new StringBuilder(128); + traverse(rootNode, method, builder); + } + + public Set getNodePath(String key) { + try { + String[] keyArray = key.split(Constants.MQTT_TOPIC_DELIMITER); + StringBuilder builder = new StringBuilder(key.length()); + Set result = findValuePath(rootNode, keyArray, 0, keyArray.length, builder, false); + return result; + } catch (Throwable e) { + throw new TrieException(e); + } + } + + private Set findValuePath(TrieNode currentNode, String[] topicArray, int level, int maxLevel, + StringBuilder builder, boolean isJinFlag) { + Set result = new HashSet<>(); + if (level < maxLevel && !currentNode.children.isEmpty()) { + //first match the precise + TrieNode trieNode = currentNode.children.get(topicArray[level]); + if (trieNode != null) { + int start = builder.length(); + builder.append(topicArray[level]).append(Constants.MQTT_TOPIC_DELIMITER); + result.addAll(findValuePath(trieNode, topicArray, level + 1, maxLevel, builder, false)); + builder.delete(start, builder.length()); + } + //match the # + TrieNode jinMatch = currentNode.children.get(Constants.JINFLAG); + if (jinMatch != null) { + int start = builder.length(); + builder.append(Constants.JINFLAG).append(Constants.MQTT_TOPIC_DELIMITER); + result.addAll(findValuePath(jinMatch, topicArray, level + 1, maxLevel, builder, true)); + builder.delete(start, builder.length()); + } + //match the + + TrieNode jiaMatch = currentNode.children.get(Constants.ADDFLAG); + if (jiaMatch != null) { + int start = builder.length(); + builder.append(Constants.ADDFLAG).append(Constants.MQTT_TOPIC_DELIMITER); + result.addAll(findValuePath(jiaMatch, topicArray, level + 1, maxLevel, builder, false)); + builder.delete(start, builder.length()); + } + } else { + //match the # + TrieNode jinMatch = currentNode.children.get(Constants.JINFLAG); + if (jinMatch != null) { + int start = builder.length(); + builder.append(Constants.JINFLAG).append(Constants.MQTT_TOPIC_DELIMITER); + result.addAll(findValuePath(jinMatch, topicArray, level + 1, maxLevel, builder, true)); + builder.delete(start, builder.length()); + } + boolean jin = (level == maxLevel || isJinFlag) && !currentNode.valueSet.isEmpty() && builder.length() > 0; + if (jin) { + result.add(TopicUtils.normalizeTopic(builder.toString().substring(0, builder.length() - 1))); + } + } + return result; + } + + private void traverse(TrieNode currentNode, TrieMethod method, StringBuilder builder) { + for (Map.Entry> entry : currentNode.children.entrySet()) { + int start = builder.length(); + builder.append(entry.getKey()).append(Constants.MQTT_TOPIC_DELIMITER); + traverse(entry.getValue(), method, builder); + builder.delete(start, builder.length()); + } + Iterator> iterator = currentNode.valueSet.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + try { + method.doMethod(builder.toString(), entry.getKey()); + } catch (Throwable e) { + } + } + } + + private Map findValueSet(TrieNode currentNode, String[] topicArray, int level, int maxLevel, + boolean isJinFlag) { + Map result = new HashMap<>(16); + if (level < maxLevel && !currentNode.children.isEmpty()) { + //first match the precise + TrieNode trieNode = currentNode.children.get(topicArray[level]); + if (trieNode != null) { + result.putAll(findValueSet(trieNode, topicArray, level + 1, maxLevel, false)); + } + //match the # + TrieNode jinMatch = currentNode.children.get(Constants.JINFLAG); + if (jinMatch != null) { + result.putAll(findValueSet(jinMatch, topicArray, level + 1, maxLevel, true)); + } + //match the + + TrieNode jiaMatch = currentNode.children.get(Constants.ADDFLAG); + if (jiaMatch != null) { + result.putAll(findValueSet(jiaMatch, topicArray, level + 1, maxLevel, false)); + } + return result; + } else { + //match the # + TrieNode jinMatch = currentNode.children.get(Constants.JINFLAG); + if (jinMatch != null) { + result.putAll(findValueSet(jinMatch, topicArray, level + 1, maxLevel, true)); + } + if (level == maxLevel || isJinFlag) { + result.putAll(currentNode.valueSet); + } + return result; + } + } + + class TrieNode { + public TrieNode parentNode; + public Map> children = new ConcurrentHashMap<>(); + public Map valueSet = new ConcurrentHashMap<>(); + + public TrieNode(TrieNode parentNode) { + this.parentNode = parentNode; + } + } + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/TrieException.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/TrieException.java new file mode 100644 index 00000000000..d11b692f537 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/TrieException.java @@ -0,0 +1,41 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + +public class TrieException extends RuntimeException{ + public TrieException() { + } + + public TrieException(String message) { + super(message); + } + + public TrieException(String message, Throwable cause) { + super(message, cause); + } + + public TrieException(Throwable cause) { + super(cause); + } + + public TrieException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/TrieMethod.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/TrieMethod.java new file mode 100644 index 00000000000..a8e74b23d19 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/TrieMethod.java @@ -0,0 +1,33 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.model; + + +public interface TrieMethod { + + /** + * doMethod + * + * @param path + * @param nodeKey + */ + void doMethod(String path, K nodeKey); + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/HmacSHA1Util.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/HmacSHA1Util.java new file mode 100644 index 00000000000..6c948307f3a --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/HmacSHA1Util.java @@ -0,0 +1,45 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.util; + +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +public class HmacSHA1Util { + private static Charset charset = Charset.forName("UTF-8"); + private static String algorithm = "HmacSHA1"; + + public static String macSignature(String text, String secretKey) throws InvalidKeyException, NoSuchAlgorithmException { + Mac mac = Mac.getInstance(algorithm); + mac.init(new SecretKeySpec(secretKey.getBytes(charset), algorithm)); + byte[] bytes = mac.doFinal(text.getBytes(charset)); + return new String(Base64.encodeBase64(bytes), charset); + } + + public static boolean validateSign(String text, byte[] input, String secretKey) throws NoSuchAlgorithmException, InvalidKeyException { + String sign = macSignature(text, secretKey); + return sign.equals(new String(input, charset)); + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/HostInfo.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/HostInfo.java new file mode 100644 index 00000000000..044ca89f920 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/HostInfo.java @@ -0,0 +1,56 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.util; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class HostInfo { + private final String HOST_NAME; + private final String HOST_ADDRESS; + + private static final HostInfo INSTALL = new HostInfo(); + + public static HostInfo getInstall() { + return INSTALL; + } + + private HostInfo() { + String hostName; + String hostAddress; + try { + InetAddress localhost = InetAddress.getLocalHost(); + hostName = localhost.getHostName(); + hostAddress = localhost.getHostAddress(); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + HOST_NAME = hostName; + HOST_ADDRESS = hostAddress; + } + + public final String getName() { + return HOST_NAME; + } + + public final String getAddress() { + return HOST_ADDRESS; + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/MessageUtil.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/MessageUtil.java new file mode 100644 index 00000000000..8f4755d1430 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/MessageUtil.java @@ -0,0 +1,119 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.util; + +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.handler.codec.mqtt.*; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.mqtt.common.model.Message; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + + +public class MessageUtil { + public static final ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); + + public static MqttPublishMessage toMqttMessage(String topicName, byte[] body, int qos, int mqttId) { + ByteBuf payload = ALLOCATOR.buffer(); + payload.writeBytes(body); + MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, + MqttQoS.valueOf(qos), + false, 0); + MqttPublishVariableHeader mqttPublishVariableHeader = new MqttPublishVariableHeader(topicName, mqttId); + MqttPublishMessage mqttPublishMessage = new MqttPublishMessage(mqttFixedHeader, mqttPublishVariableHeader, + payload); + return mqttPublishMessage; + } + + public static Message toMessage(MqttPublishMessage mqttMessage) { + Message message = new Message(); + message.setFirstTopic(TopicUtils.decode(mqttMessage.variableHeader().topicName()).getFirstTopic()); + message.setOriginTopic(mqttMessage.variableHeader().topicName()); + message.putUserProperty(Message.extPropertyQoS, String.valueOf(mqttMessage.fixedHeader().qosLevel().value())); + int readableBytes = mqttMessage.payload().readableBytes(); + byte[] body = new byte[readableBytes]; + mqttMessage.payload().readBytes(body); + message.setPayload(body); + return message; + } + + + public static byte[] encode(List messageList) { + if (messageList == null || messageList.isEmpty()) { + return null; + } + List mqMessages = new ArrayList<>(); + for (Message message : messageList) { + org.apache.rocketmq.common.message.Message mqMessage = new org.apache.rocketmq.common.message.Message(); + mqMessage.setBody(message.getPayload()); + mqMessage.putUserProperty(Message.propertyFirstTopic, message.getFirstTopic()); + if (message.getOriginTopic() != null) { + mqMessage.putUserProperty(Message.propertyOriginTopic, message.getOriginTopic()); + } + if (message.getMsgId() != null) { + mqMessage.putUserProperty(Message.propertyMsgId, message.getMsgId()); + } + mqMessage.putUserProperty(Message.propertyOffset, String.valueOf(message.getOffset())); + mqMessage.putUserProperty(Message.propertyNextOffset, String.valueOf(message.getNextOffset())); + mqMessage.putUserProperty(Message.propertyRetry, String.valueOf(message.getRetry())); + mqMessage.putUserProperty(Message.propertyBornTime, String.valueOf(message.getBornTimestamp())); + mqMessage.putUserProperty(Message.propertyStoreTime, String.valueOf(message.getStoreTimestamp())); + mqMessage.putUserProperty(Message.propertyUserProperties, + JSONObject.toJSONString(message.getUserProperties())); + mqMessages.add(mqMessage); + } + return MessageDecoder.encodeMessages(mqMessages); + } + + public static List decode(ByteBuffer byteBuffer) throws Exception { + List mqMessages = MessageDecoder.decodeMessages(byteBuffer); + if (mqMessages == null) { + return null; + } + List messageList = new ArrayList<>(); + for (org.apache.rocketmq.common.message.Message mqMessage : mqMessages) { + Message message = new Message(); + message.setFirstTopic(mqMessage.getUserProperty(Message.propertyFirstTopic)); + message.setOriginTopic(mqMessage.getUserProperty(Message.propertyOriginTopic)); + message.setPayload(mqMessage.getBody()); + message.setMsgId(mqMessage.getUserProperty(Message.propertyMsgId)); + message.setOffset(Long.parseLong(mqMessage.getUserProperty(Message.propertyOffset))); + message.setNextOffset(Long.parseLong(mqMessage.getUserProperty(Message.propertyNextOffset))); + message.setStoreTimestamp(Long.parseLong(mqMessage.getUserProperty(Message.propertyStoreTime))); + message.setBornTimestamp(Long.parseLong(mqMessage.getUserProperty(Message.propertyBornTime))); + message.setRetry(Integer.parseInt(mqMessage.getUserProperty(Message.propertyRetry))); + String ext = mqMessage.getUserProperty(Message.propertyUserProperties); + if (ext != null) { + message.getUserProperties().putAll( + com.alibaba.fastjson.JSONObject.parseObject(ext, new TypeReference>() {})); + } + messageList.add(message); + } + return messageList; + } + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/NamespaceUtil.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/NamespaceUtil.java new file mode 100644 index 00000000000..093629fa5f8 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/NamespaceUtil.java @@ -0,0 +1,70 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.util; + +import org.apache.commons.lang3.StringUtils; + +public class NamespaceUtil { + public static final String NAMESPACE_SPLITER = "%"; + private static int RESOURCE_LENGTH = 2; + public static final String MQ_DEFAULT_NAMESPACE_NAME = "DEFAULT_INSTANCE"; + + public NamespaceUtil() { + } + + public static String encodeToNamespaceResource(String namespace, String resource) { + return resource != null && namespace != null ? StringUtils.join(new String[]{namespace, "%", resource}) : resource; + } + + public static String decodeOriginResource(String resource) { + if (resource != null && resource.contains("%")) { + int firstIndex = resource.indexOf("%"); + return resource.substring(firstIndex + 1); + } else { + return resource; + } + } + + public static String decodeMqttNamespaceIdFromKey(String key) { + return decodeMqttNamespaceIdFromClientId(key); + } + + public static String decodeMqttNamespaceIdFromClientId(String clientId) { + if (clientId != null && clientId.contains("%")) { + String mqttNamespaceId = clientId.split("%")[0]; + return mqttNamespaceId; + } else { + return null; + } + } + + public static String decodeStoreNamespaceIdFromTopic(String topic) { + if (topic != null && topic.contains("%")) { + String storeNamespaceId = topic.split("%")[0]; + return storeNamespaceId; + } else { + return null; + } + } + + public static String decodeNamespaceId(String resource) { + return resource != null && resource.contains("%") ? resource.split("%")[0] : null; + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/StatUtil.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/StatUtil.java new file mode 100644 index 00000000000..cd6ecd84037 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/StatUtil.java @@ -0,0 +1,472 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Generated; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import static java.math.BigDecimal.ROUND_HALF_UP; + +public class StatUtil { + private static Logger sysLogger = LoggerFactory.getLogger(StatUtil.class); + private static Logger logger = LoggerFactory.getLogger("StatLogger"); + private static final int MAX_KEY_NUM = Integer.parseInt(System.getProperty("stat.util.key.max.num", "10000")); + private static volatile ConcurrentMap invokeCache = new ConcurrentHashMap<>(64); + private static volatile ConcurrentMap> secondInvokeCache = new ConcurrentHashMap<>( + 64); + + private static int STAT_WINDOW_SECONDS = Integer.parseInt(System.getProperty("stat.win.seconds", "60")); + private static String SPLITTER = "|"; + private static ScheduledExecutorService daemon = Executors.newSingleThreadScheduledExecutor(); + + static class Invoke { + AtomicLong totalPv = new AtomicLong(); + AtomicLong failPv = new AtomicLong(); + AtomicLong sumRt = new AtomicLong(); + AtomicLong maxRt = new AtomicLong(); + AtomicLong minRt = new AtomicLong(); + AtomicInteger topSecondPv = new AtomicInteger(); + AtomicInteger secondPv = new AtomicInteger(); + AtomicLong second = new AtomicLong(System.currentTimeMillis() / 1000L); + } + + static class SecondInvoke implements Comparable { + AtomicLong total = new AtomicLong(); + AtomicLong fail = new AtomicLong(); + AtomicLong sumRt = new AtomicLong(); + AtomicLong maxRt = new AtomicLong(); + AtomicLong minRt = new AtomicLong(); + Long second = nowSecond(); + + @Override + public int compareTo(SecondInvoke o) { + return o.second.compareTo(second); + } + } + + static { + daemon.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + printInvokeStat(); + printSecondInvokeStat(); + } catch (Exception e) { + logger.error("", e); + } + } + }, STAT_WINDOW_SECONDS, STAT_WINDOW_SECONDS, TimeUnit.SECONDS); + } + + private static void printInvokeStat() { + Map tmp = invokeCache; + invokeCache = new ConcurrentHashMap<>(64); + for (Map.Entry entry : tmp.entrySet()) { + String key = entry.getKey(); + Invoke invoke = entry.getValue(); + logger.warn("{}", + buildLog(key, invoke.topSecondPv.get(), invoke.totalPv.get(), invoke.failPv.get(), invoke.minRt.get(), + invoke.maxRt.get(), invoke.sumRt.get())); + } + } + + private static void printSecondInvokeStat() { + for (Map.Entry> entry : secondInvokeCache.entrySet()) { + String key = entry.getKey(); + Map secondInvokeMap = entry.getValue(); + long totalPv = 0L; + long failPv = 0L; + long topSecondPv = 0L; + long sumRt = 0L; + long maxRt = 0L; + long minRt = 0L; + + for (Map.Entry invokeEntry : secondInvokeMap.entrySet()) { + long second = invokeEntry.getKey(); + SecondInvoke secondInvoke = invokeEntry.getValue(); + if (nowSecond() - second >= STAT_WINDOW_SECONDS) { + secondInvokeMap.remove(second); + continue; + } + long secondPv = secondInvoke.total.get(); + totalPv += secondPv; + failPv += secondInvoke.fail.get(); + sumRt += secondInvoke.sumRt.get(); + if (maxRt < secondInvoke.maxRt.get()) { + maxRt = secondInvoke.maxRt.get(); + } + if (minRt > secondInvoke.minRt.get()) { + minRt = secondInvoke.minRt.get(); + } + if (topSecondPv < secondPv) { + topSecondPv = secondPv; + } + } + if (secondInvokeMap.isEmpty()) { + secondInvokeCache.remove(key); + continue; + } + logger.warn("{}", buildLog(key, topSecondPv, totalPv, failPv, minRt, maxRt, sumRt)); + } + } + + private static String buildLog(String key, long topSecondPv, long totalPv, long failPv, long minRt, long maxRt, + long sumRt) { + StringBuilder sb = new StringBuilder(); + sb.append(SPLITTER); + sb.append(key); + sb.append(SPLITTER); + sb.append(topSecondPv); + sb.append(SPLITTER); + int tps = new BigDecimal(totalPv).divide(new BigDecimal(STAT_WINDOW_SECONDS), + ROUND_HALF_UP).intValue(); + sb.append(tps); + sb.append(SPLITTER); + sb.append(totalPv); + sb.append(SPLITTER); + sb.append(failPv); + sb.append(SPLITTER); + sb.append(minRt); + sb.append(SPLITTER); + long avg = new BigDecimal(sumRt).divide(new BigDecimal(totalPv), + ROUND_HALF_UP).longValue(); + sb.append(avg); + sb.append(SPLITTER); + sb.append(maxRt); + return sb.toString(); + } + + public static String buildKey(String... keys) { + if (keys == null || keys.length <= 0) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (String key : keys) { + sb.append(key); + sb.append(","); + } + sb.deleteCharAt(sb.length() - 1); + return sb.toString(); + } + + public static void addInvoke(String key, long rt) { + addInvoke(key, rt, true); + } + + private static Invoke getAndSetInvoke(String key) { + Invoke invoke = invokeCache.get(key); + if (invoke == null) { + invokeCache.putIfAbsent(key, new Invoke()); + } + return invokeCache.get(key); + } + + public static void addInvoke(String key, int num, long rt, boolean success) { + if (invokeCache.size() > MAX_KEY_NUM || secondInvokeCache.size() > MAX_KEY_NUM) { + return; + } + Invoke invoke = getAndSetInvoke(key); + if (invoke == null) { + return; + } + + invoke.totalPv.getAndAdd(num); + if (!success) { + invoke.failPv.getAndAdd(num); + } + long now = nowSecond(); + AtomicLong oldSecond = invoke.second; + if (oldSecond.get() == now) { + invoke.secondPv.getAndAdd(num); + } else { + if (oldSecond.compareAndSet(oldSecond.get(), now)) { + if (invoke.secondPv.get() > invoke.topSecondPv.get()) { + invoke.topSecondPv.set(invoke.secondPv.get()); + } + invoke.secondPv.set(num); + } else { + invoke.secondPv.getAndAdd(num); + } + } + + invoke.sumRt.addAndGet(rt); + if (invoke.maxRt.get() < rt) { + invoke.maxRt.set(rt); + } + if (invoke.minRt.get() > rt) { + invoke.minRt.set(rt); + } + } + + public static void addInvoke(String key, long rt, boolean success) { + if (invokeCache.size() > MAX_KEY_NUM || secondInvokeCache.size() > MAX_KEY_NUM) { + return; + } + Invoke invoke = getAndSetInvoke(key); + if (invoke == null) { + return; + } + + invoke.totalPv.getAndIncrement(); + if (!success) { + invoke.failPv.getAndIncrement(); + } + long now = nowSecond(); + AtomicLong oldSecond = invoke.second; + if (oldSecond.get() == now) { + invoke.secondPv.getAndIncrement(); + } else { + if (oldSecond.compareAndSet(oldSecond.get(), now)) { + if (invoke.secondPv.get() > invoke.topSecondPv.get()) { + invoke.topSecondPv.set(invoke.secondPv.get()); + } + invoke.secondPv.set(1); + } else { + invoke.secondPv.getAndIncrement(); + } + } + + invoke.sumRt.addAndGet(rt); + if (invoke.maxRt.get() < rt) { + invoke.maxRt.set(rt); + } + if (invoke.minRt.get() > rt) { + invoke.minRt.set(rt); + } + } + + public static SecondInvoke getAndSetSecondInvoke(String key) { + if (!secondInvokeCache.containsKey(key)) { + secondInvokeCache.putIfAbsent(key, new ConcurrentHashMap<>(STAT_WINDOW_SECONDS)); + } + Map secondInvokeMap = secondInvokeCache.get(key); + if (secondInvokeMap == null) { + return null; + } + long second = nowSecond(); + if (!secondInvokeMap.containsKey(second)) { + secondInvokeMap.putIfAbsent(second, new SecondInvoke()); + } + return secondInvokeMap.get(second); + } + + public static void addSecondInvoke(String key, long rt) { + addSecondInvoke(key, rt, true); + } + + public static void addSecondInvoke(String key, long rt, boolean success) { + if (invokeCache.size() > MAX_KEY_NUM || secondInvokeCache.size() > MAX_KEY_NUM) { + return; + } + SecondInvoke secondInvoke = getAndSetSecondInvoke(key); + if (secondInvoke == null) { + return; + } + secondInvoke.total.addAndGet(1); + if (!success) { + secondInvoke.fail.addAndGet(1); + } + secondInvoke.sumRt.addAndGet(rt); + if (secondInvoke.maxRt.get() < rt) { + secondInvoke.maxRt.set(rt); + } + if (secondInvoke.minRt.get() > rt) { + secondInvoke.minRt.set(rt); + } + } + + public static void addPv(String key, long totalPv) { + addPv(key, totalPv, true); + } + + public static void addPv(String key, long totalPv, boolean success) { + if (invokeCache.size() > MAX_KEY_NUM || secondInvokeCache.size() > MAX_KEY_NUM) { + return; + } + if (totalPv <= 0) { + return; + } + Invoke invoke = getAndSetInvoke(key); + if (invoke == null) { + return; + } + invoke.totalPv.addAndGet(totalPv); + if (!success) { + invoke.failPv.addAndGet(totalPv); + } + long now = nowSecond(); + AtomicLong oldSecond = invoke.second; + if (oldSecond.get() == now) { + invoke.secondPv.addAndGet((int)totalPv); + } else { + if (oldSecond.compareAndSet(oldSecond.get(), now)) { + if (invoke.secondPv.get() > invoke.topSecondPv.get()) { + invoke.topSecondPv.set(invoke.secondPv.get()); + } + invoke.secondPv.set((int)totalPv); + } else { + invoke.secondPv.addAndGet((int)totalPv); + } + } + } + + public static void addSecondPv(String key, long totalPv) { + addSecondPv(key, totalPv, true); + } + + public static void addSecondPv(String key, long totalPv, boolean success) { + if (invokeCache.size() > MAX_KEY_NUM || secondInvokeCache.size() > MAX_KEY_NUM) { + return; + } + if (totalPv <= 0) { + return; + } + SecondInvoke secondInvoke = getAndSetSecondInvoke(key); + if (secondInvoke == null) { + return; + } + secondInvoke.total.addAndGet(totalPv); + if (!success) { + secondInvoke.fail.addAndGet(totalPv); + } + } + + public static boolean isOverFlow(String key, int tps) { + return nowTps(key) >= tps; + } + + public static int nowTps(String key) { + Map secondInvokeMap = secondInvokeCache.get(key); + if (secondInvokeMap != null) { + SecondInvoke secondInvoke = secondInvokeMap.get(nowSecond()); + if (secondInvoke != null) { + return (int)secondInvoke.total.get(); + } + } + Invoke invoke = invokeCache.get(key); + if (invoke == null) { + return 0; + } + AtomicLong oldSecond = invoke.second; + if (oldSecond.get() == nowSecond()) { + return invoke.secondPv.get(); + } + return 0; + } + + public static int totalPvInWindow(String key, int windowSeconds) { + List list = secondInvokeList(key, windowSeconds); + long totalPv = 0; + for (int i = 0; i < windowSeconds && i < list.size(); i++) { + totalPv += list.get(i).total.get(); + } + return (int)totalPv; + } + + public static int failPvInWindow(String key, int windowSeconds) { + List list = secondInvokeList(key, windowSeconds); + long failPv = 0; + for (int i = 0; i < windowSeconds && i < list.size(); i++) { + failPv += list.get(i).fail.get(); + } + return (int)failPv; + } + + public static int topTpsInWindow(String key, int windowSeconds) { + List list = secondInvokeList(key, windowSeconds); + long topTps = 0; + for (int i = 0; i < windowSeconds && i < list.size(); i++) { + long secondPv = list.get(i).total.get(); + if (topTps < secondPv) { + topTps = secondPv; + } + } + return (int)topTps; + } + + public static int avgRtInWindow(String key, int windowSeconds) { + List list = secondInvokeList(key, windowSeconds); + long sumRt = 0; + long totalPv = 0; + for (int i = 0; i < windowSeconds && i < list.size(); i++) { + sumRt += list.get(i).sumRt.get(); + totalPv += list.get(i).total.get(); + } + if (totalPv <= 0) { + return 0; + } + long avg = new BigDecimal(sumRt).divide(new BigDecimal(totalPv), + ROUND_HALF_UP).longValue(); + return (int)avg; + } + + public static int maxRtInWindow(String key, int windowSeconds) { + List list = secondInvokeList(key, windowSeconds); + long maxRt = 0; + long totalPv = 0; + for (int i = 0; i < windowSeconds && i < list.size(); i++) { + if (maxRt < list.get(i).maxRt.get()) { + maxRt = list.get(i).maxRt.get(); + } + } + return (int)maxRt; + } + + public static int minRtInWindow(String key, int windowSeconds) { + List list = secondInvokeList(key, windowSeconds); + long minRt = 0; + long totalPv = 0; + for (int i = 0; i < windowSeconds && i < list.size(); i++) { + if (minRt < list.get(i).minRt.get()) { + minRt = list.get(i).minRt.get(); + } + } + return (int)minRt; + } + + private static List secondInvokeList(String key, int windowSeconds) { + if (windowSeconds > STAT_WINDOW_SECONDS || windowSeconds <= 0) { + throw new IllegalArgumentException("windowSeconds Must Not be great than " + STAT_WINDOW_SECONDS); + } + Map secondInvokeMap = secondInvokeCache.get(key); + if (secondInvokeMap == null || secondInvokeMap.isEmpty()) { + return new ArrayList<>(); + } + List list = new ArrayList<>(); + list.addAll(secondInvokeMap.values()); + Collections.sort(list); + return list; + } + + private static long nowSecond() { + return System.currentTimeMillis() / 1000L; + } + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/TopicUtils.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/TopicUtils.java new file mode 100644 index 00000000000..3622ee54697 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/TopicUtils.java @@ -0,0 +1,195 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.util; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.MqttTopic; + + +public class TopicUtils { + + /** + * t1/t2/t3/ + * + * @param topic + * @return + */ + public static String normalizeTopic(String topic) { + if (topic == null) { + return null; + } + if (!topic.contains(Constants.MQTT_TOPIC_DELIMITER)) { + return topic; + } + if (!topic.endsWith(Constants.MQTT_TOPIC_DELIMITER)) { + return topic + Constants.MQTT_TOPIC_DELIMITER; + } + return topic; + } + + /** + * /t2/t3/t4/ + * + * @param secondtopic + * @return + */ + public static String normalizeSecondTopic(String secondtopic) { + if (secondtopic == null || secondtopic.isEmpty()) { + return null; + } + if (!secondtopic.startsWith(Constants.MQTT_TOPIC_DELIMITER)) { + secondtopic = Constants.MQTT_TOPIC_DELIMITER + secondtopic; + } + if (!secondtopic.endsWith(Constants.MQTT_TOPIC_DELIMITER)) { + return secondtopic + Constants.MQTT_TOPIC_DELIMITER; + } + return secondtopic; + } + + public static boolean isP2P(String secondTopic) { + return secondTopic != null && secondTopic.startsWith(Constants.P2P); + } + + public static String getClientIdFromP2pTopic(String p2pTopic) { + String tmp = p2pTopic.substring(Constants.P2P.length()); + return tmp.substring(0, tmp.length() - 1); + } + + public static String getClientIdFromRetryTopic(String retryTopic) { + String tmp = retryTopic.substring(Constants.RETRY.length()); + return tmp.substring(0, tmp.length() - 1); + } + + public static String getP2pTopic(String clientId) { + return normalizeTopic(Constants.P2P + clientId + Constants.MQTT_TOPIC_DELIMITER); + } + + public static String getRetryTopic(String clientId) { + return normalizeTopic(Constants.RETRY + clientId + Constants.MQTT_TOPIC_DELIMITER); + } + + public static boolean isRetryTopic(String topic) { + return topic != null && topic.startsWith(Constants.RETRY); + } + + public static boolean isP2pTopic(String topic) { + return topic != null && topic.startsWith(Constants.P2P); + } + + public static String getP2Peer(MqttTopic mqttTopic, String namespace) { + if (!isP2P(mqttTopic.getSecondTopic())) { + return null; + } + if (mqttTopic.getSecondTopic() == null || mqttTopic.getFirstTopic() == null) { + return null; + } + if (mqttTopic.getFirstTopic().contains(Constants.NAMESPACE_SPLITER) && StringUtils.isNotBlank(namespace)) { + return StringUtils.join(namespace, Constants.NAMESPACE_SPLITER, mqttTopic.getSecondTopic().split(Constants.MQTT_TOPIC_DELIMITER)[2]); + } + return mqttTopic.getSecondTopic().split(Constants.MQTT_TOPIC_DELIMITER)[2]; + } + + public static String encode(String topic, String secondTopic) { + if (secondTopic != null && secondTopic.length() > 1) { + return topic + secondTopic; + } + return topic; + } + + public static MqttTopic decode(String topics) { + if (topics.startsWith(Constants.MQTT_TOPIC_DELIMITER)) { + topics = topics.substring(1); + } + String topic; + String secondTopic = null; + int index = topics.indexOf(Constants.MQTT_TOPIC_DELIMITER, 1); + if (index > 0) { + topic = topics.substring(0, index); + secondTopic = topics.substring(index); + } else { + topic = topics; + } + return new MqttTopic(topic, secondTopic); + } + + public static boolean isWildCard(String topicFilter) { + return topicFilter != null && + (topicFilter.contains(Constants.JINFLAG) || topicFilter.contains(Constants.ADDFLAG)); + } + + public static boolean isMatch(String topic, String topicFilter) { + if (topic.equals(topicFilter)) { + return true; + } + if (!isWildCard(topicFilter)) { + return false; + } + + String[] subscribeTopics = topicFilter.split(Constants.MQTT_TOPIC_DELIMITER); + String[] messageTopics = topic.split(Constants.MQTT_TOPIC_DELIMITER); + int targetTopicLength = messageTopics.length; + int sourceTopicLength = subscribeTopics.length; + int minTopicLength = Math.min(targetTopicLength, sourceTopicLength); + + for (int i = 0; i < minTopicLength; i++) { + String sourceTopic = subscribeTopics[i]; + + if (!Constants.JINFLAG.equals(sourceTopic) && + !Constants.ADDFLAG.equals(sourceTopic)) { + if (!sourceTopic.equals(messageTopics[i])) { + return false; + } + } + //多级 + if (Constants.JINFLAG.equals(sourceTopic)) { + return true; + } + boolean last = i == minTopicLength - 1 && + (sourceTopicLength == targetTopicLength || + (sourceTopicLength == targetTopicLength + 1 && + Constants.JINFLAG.equals(subscribeTopics[sourceTopicLength - 1]) + ) + ); + if (last) { + return true; + } + } + + return false; + } + + public static String wrapLmq(String firstTopic, String secondTopic) { + if (StringUtils.isBlank(secondTopic)) { + return firstTopic; + } + return firstTopic + normalizeSecondTopic(secondTopic); + } + + public static String wrapP2pLmq(String clientId) { + return normalizeTopic(Constants.P2P + clientId); + } + + public static void main(String[] args) { + String topic = "/t/t1/t2"; + String topicFilter = "/t/t1/t2"; + System.out.println(TopicUtils.isMatch(topic, topicFilter)); + } +} diff --git a/mqtt-common/src/test/java/org/apache/rocketmq/mqtt/common/test/TestTrie.java b/mqtt-common/src/test/java/org/apache/rocketmq/mqtt/common/test/TestTrie.java new file mode 100644 index 00000000000..3b12714b13f --- /dev/null +++ b/mqtt-common/src/test/java/org/apache/rocketmq/mqtt/common/test/TestTrie.java @@ -0,0 +1,37 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.common.test; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.mqtt.common.model.Trie; +import org.junit.Assert; +import org.junit.Test; + +public class TestTrie { + + @Test + public void test() { + Trie trie = new Trie<>(); + trie.addNode("test", "test", "test"); + Assert.assertTrue(trie.getNodePath("test").contains("test")); + trie.deleteNode("test", "test"); + Assert.assertTrue(CollectionUtils.isEmpty(trie.getNodePath("test"))); + } +} diff --git a/mqtt-cs/pom.xml b/mqtt-cs/pom.xml new file mode 100644 index 00000000000..582771e86c1 --- /dev/null +++ b/mqtt-cs/pom.xml @@ -0,0 +1,71 @@ + + + + rocketmq-mqtt + org.apache.rocketmq + 1.0.0-SNAPSHOT + + 4.0.0 + + mqtt-cs + + + + org.apache.rocketmq + mqtt-common + + + org.apache.rocketmq + rocketmq-client + + + io.netty + netty-all + + + org.slf4j + slf4j-api + + + org.springframework + spring-core + + + org.springframework + spring-context + + + org.springframework + spring-beans + + + org.apache.commons + commons-lang3 + + + com.alibaba + fastjson + + + com.github.ben-manes.caffeine + caffeine + + + com.google.code.findbugs + jsr305 + + + junit + junit + test + + + org.mockito + mockito-core + test + + + + \ No newline at end of file diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelCloseFrom.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelCloseFrom.java new file mode 100644 index 00000000000..700658fc36a --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelCloseFrom.java @@ -0,0 +1,33 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.channel; + + +public enum ChannelCloseFrom { + /** + * + */ + CLIENT, + + /** + * + */ + SERVER +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelException.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelException.java new file mode 100644 index 00000000000..dbdbbfd4763 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelException.java @@ -0,0 +1,41 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.channel; + +public class ChannelException extends RuntimeException{ + public ChannelException() { + } + + public ChannelException(String message) { + super(message); + } + + public ChannelException(String message, Throwable cause) { + super(message, cause); + } + + public ChannelException(Throwable cause) { + super(cause); + } + + public ChannelException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelInfo.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelInfo.java new file mode 100644 index 00000000000..695d6c628b1 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelInfo.java @@ -0,0 +1,254 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.channel; + +import com.alibaba.fastjson.JSON; +import io.netty.channel.Channel; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class ChannelInfo { + private static final String CHANNEL_ID_KEY = "0"; + private static final String CHANNEL_CLIENT_ID_KEY = "1"; + private static final String CHANNEL_TIMESTAMP_KEY = "2"; + private static final String CHANNEL_KEEPLIVE_KEY = "3"; + private static final String CHANNEL_OWNER_KEY = "4"; + private static final String CHANNEL_NAMESPACE_KEY = "5"; + private static final String CHANNEL_EXT_CHANGE_KEY = "6"; + private static final String CHANNEL_STORE_NAMESPACE_KEY = "7"; + private static final String CHANNEL_CLEAN_SESSION_KEY = "8"; + private static final String CHANNEL_SUB_NUM_KEY = "9"; + private static final String CHANNEL_FUTRUE_KEY = "10"; + private static final String CHANNEL_LIFE_CYCLE = "11"; + private static final String CHANNEL_LAST_ACTIVE_TIMESTAMP_KEY = "12"; + private static final String CHANNEL_IS_FLUSHING = "13"; + private static final String CHANNEL_REMOTE_IP = "14"; + private static final String CHANNEL_TUNNEL_ID = "15"; + + public static final String FUTURE_CONNECT = "connect"; + public static final String FUTURE_SUBSCRIBE = "subscribe"; + + public static final AttributeKey> CHANNEL_INFO_ATTRIBUTE_KEY = AttributeKey.valueOf("I"); + + public static final AttributeKey> CHANNEL_EXTDATA_ATTRIBUTE_KEY = AttributeKey + .valueOf("E"); + + public static final AttributeKey CHANNEL_GA_ATTRIBUTE_KEY = AttributeKey.valueOf("GA"); + + + public static Map getExtData(Channel channel) { + Attribute> extAttribute = channel.attr(CHANNEL_EXTDATA_ATTRIBUTE_KEY); + if (extAttribute.get() == null) { + extAttribute.setIfAbsent(new ConcurrentHashMap<>()); + } + return extAttribute.get(); + } + + public static String encodeExtData(Channel channel) { + Map extData = getExtData(channel); + return JSON.toJSONString(extData); + } + + public static boolean updateExtData(Channel channel, String extDataStr) { + if (StringUtils.isBlank(extDataStr)) { + return false; + } + updateExtData(channel, JSON.parseObject(extDataStr, Map.class)); + return true; + } + + private static void updateExtData(Channel channel, Map extData) { + Map currentExt = getExtData(channel); + currentExt.putAll(extData); + setExtDataChange(channel, true); + } + + public static void setExtDataChange(Channel channel, boolean flag) { + getInfo(channel).put(CHANNEL_EXT_CHANGE_KEY, flag); + } + + public static boolean checkExtDataChange(Channel channel) { + if (!getInfo(channel).containsKey(CHANNEL_EXT_CHANGE_KEY)) { + getInfo(channel).put(CHANNEL_EXT_CHANGE_KEY, false); + } + Object obj = getInfo(channel).get(CHANNEL_EXT_CHANGE_KEY); + if (obj == null) { + return false; + } + return (boolean)obj; + } + + public static String getId(Channel channel) { + Map info = getInfo(channel); + if (info.containsKey(CHANNEL_ID_KEY)) { + return (String)info.get(CHANNEL_ID_KEY); + } + String channelIdStr = UUID.randomUUID().toString().replaceAll("-", "").toLowerCase(); + info.put(CHANNEL_ID_KEY, channelIdStr); + return channelIdStr; + } + + public static Boolean getCleanSessionFlag(Channel channel) { + if (!getInfo(channel).containsKey(CHANNEL_CLEAN_SESSION_KEY)) { + getInfo(channel).put(CHANNEL_CLEAN_SESSION_KEY, true); + } + Object obj = getInfo(channel).get(CHANNEL_CLEAN_SESSION_KEY); + if (obj == null) { + return true; + } + return (Boolean)obj; + } + + public static void setCleanSessionFlag(Channel channel, Boolean cleanSessionFalg) { + getInfo(channel).put(CHANNEL_CLEAN_SESSION_KEY, cleanSessionFalg); + } + + public static String getClientId(Channel channel) { + return (String)getInfo(channel).get(CHANNEL_CLIENT_ID_KEY); + } + + public static long getChannelLifeCycle(Channel channel) { + Long expireTime = (Long) getInfo(channel).get(CHANNEL_LIFE_CYCLE); + if (expireTime == null) { + return Long.MAX_VALUE; + } + return expireTime; + } + + public static void setChannelLifeCycle(Channel channel, Long expireTime) { + getInfo(channel).put(CHANNEL_LIFE_CYCLE, expireTime == null ? Long.MAX_VALUE : expireTime); + } + + public static void setFuture(Channel channel, String futureKey, CompletableFuture future) { + getInfo(channel).put(CHANNEL_FUTRUE_KEY + futureKey, future); + } + + public static CompletableFuture getFuture(Channel channel, String futureKey) { + Object future = getInfo(channel).get(CHANNEL_FUTRUE_KEY + futureKey); + if (future != null) { + return (CompletableFuture)future; + } + return null; + } + + public static void removeFuture(Channel channel, String futureKey) { + getInfo(channel).remove(CHANNEL_FUTRUE_KEY + futureKey); + } + + public static void setClientId(Channel channel, String clientId) { + getInfo(channel).put(CHANNEL_CLIENT_ID_KEY, clientId); + } + + public static void touch(Channel channel) { + getInfo(channel).put(CHANNEL_TIMESTAMP_KEY, System.currentTimeMillis()); + } + + public static long getLastTouch(Channel channel) { + Object t = getInfo(channel).get(CHANNEL_TIMESTAMP_KEY); + return t != null ? (long)t : 0; + } + + public static void lastActive(Channel channel, long timeStamp) { + getInfo(channel).put(CHANNEL_LAST_ACTIVE_TIMESTAMP_KEY, timeStamp); + } + + public static long getLastActive(Channel channel) { + Object t = getInfo(channel).get(CHANNEL_LAST_ACTIVE_TIMESTAMP_KEY); + return t != null ? (long)t : 0; + } + + public static void setRemoteIP(Channel channel, String ip) { + getInfo(channel).put(CHANNEL_REMOTE_IP, ip); + } + + public static String getRemoteIP(Channel channel) { + Object t = getInfo(channel).get(CHANNEL_REMOTE_IP); + return t == null ? "" : (String) t; + } + + public static void setKeepLive(Channel channel, int seconds) { + getInfo(channel).put(CHANNEL_KEEPLIVE_KEY, seconds); + } + + public static Integer getKeepLive(Channel channel) { + return (Integer)getInfo(channel).get(CHANNEL_KEEPLIVE_KEY); + } + + public static boolean isExpired(Channel channel) { + Long timestamp = (Long)getInfo(channel).get(CHANNEL_TIMESTAMP_KEY); + if (timestamp == null) { + return true; + } + Integer keepLiveT = getKeepLive(channel); + if (keepLiveT == null) { + return true; + } + return System.currentTimeMillis() - timestamp > keepLiveT * 1000L * 1.5; + } + + public static void setOwner(Channel channel, String owner) { + getInfo(channel).put(CHANNEL_OWNER_KEY, owner); + } + + public static String getOwner(Channel channel) { + return (String)getInfo(channel).get(CHANNEL_OWNER_KEY); + } + + public static void setNamespace(Channel channel, String namespace) { + getInfo(channel).put(CHANNEL_NAMESPACE_KEY, namespace); + } + + public static String getNamespace(Channel channel) { + return (String)getInfo(channel).get(CHANNEL_NAMESPACE_KEY); + } + + /** + * clear channelInfo except the channelId、namespace + * + * @param channel + */ + public static void clear(Channel channel) { + String channelId = getId(channel); + String namespace = getNamespace(channel); + Map newInfoAttribute = new ConcurrentHashMap<>(8); + newInfoAttribute.put(CHANNEL_ID_KEY, channelId); + if (namespace != null) { + newInfoAttribute.put(CHANNEL_NAMESPACE_KEY, namespace); + } + channel.attr(CHANNEL_INFO_ATTRIBUTE_KEY).set(newInfoAttribute); + channel.attr(CHANNEL_EXTDATA_ATTRIBUTE_KEY).set(null); + } + + public static Map getInfo(Channel channel) { + Attribute> infoAttribute = channel.attr(CHANNEL_INFO_ATTRIBUTE_KEY); + if (infoAttribute.get() == null) { + infoAttribute.setIfAbsent(new ConcurrentHashMap<>(8)); + } + return infoAttribute.get(); + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelManager.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelManager.java new file mode 100644 index 00000000000..fb1e2091400 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ChannelManager.java @@ -0,0 +1,64 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.channel; + +import io.netty.channel.Channel; + + +public interface ChannelManager { + + /** + * addChannel + * + * @param channel + */ + void addChannel(Channel channel); + + /** + * closeConnect + * + * @param channel + * @param from + * @param reason + */ + void closeConnect(Channel channel, ChannelCloseFrom from, String reason); + + /** + * closeConnect + * @param channelId + * @param reason + */ + void closeConnect(String channelId, String reason); + + /** + * get channel by Id + * @param channelId + * @return + */ + Channel getChannelById(String channelId); + + /** + * totalConn + * + * @return + */ + int totalConn(); + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ConnectHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ConnectHandler.java new file mode 100644 index 00000000000..9ee80714b09 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/ConnectHandler.java @@ -0,0 +1,65 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.channel; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.List; + + +@ChannelHandler.Sharable +@Component +public class ConnectHandler extends ChannelInboundHandlerAdapter { + private static Logger logger = LoggerFactory.getLogger(ConnectHandler.class); + + @Resource + private ChannelManager channelManager; + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + ctx.fireChannelActive(); + channelManager.addChannel(ctx.channel()); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + channelManager.closeConnect(ctx.channel(), ChannelCloseFrom.CLIENT, "be closed"); + } + + public final List simpleExceptions = Arrays.asList("Connection reset by peer"); + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (cause.getMessage() != null && simpleExceptions.contains(cause.getMessage())) { + } else { + logger.error("exceptionCaught {}", ctx.channel(), cause); + } + channelManager.closeConnect(ctx.channel(), ChannelCloseFrom.SERVER, cause.getMessage()); + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/DefaultChannelManager.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/DefaultChannelManager.java new file mode 100644 index 00000000000..fcbd449ed48 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/DefaultChannelManager.java @@ -0,0 +1,151 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.channel; + +import io.netty.channel.Channel; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timeout; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.infly.RetryDriver; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@Component +public class DefaultChannelManager implements ChannelManager { + private static Logger logger = LoggerFactory.getLogger(DefaultChannelManager.class); + private Map channelMap = new ConcurrentHashMap<>(1024); + private HashedWheelTimer hashedWheelTimer; + private static int MinBlankChannelSeconds = 10; + private ScheduledThreadPoolExecutor scheduler; + + @Resource + private ConnectConf connectConf; + + @Resource + private SessionLoop sessionLoop; + + @Resource + private RetryDriver retryDriver; + + + @PostConstruct + public void init() { + sessionLoop.setChannelManager(this); + hashedWheelTimer = new HashedWheelTimer(1, TimeUnit.SECONDS); + hashedWheelTimer.start(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + for(Channel channel : channelMap.values()) { + closeConnect(channel, ChannelCloseFrom.SERVER, "ServerShutdown"); + } + })); + } + + @Override + public void addChannel(Channel channel) { + if (channelMap.size() > connectConf.getMaxConn()) { + closeConnect(channel, ChannelCloseFrom.SERVER, "overflow"); + logger.error("channel is too many {}", channelMap.size()); + return; + } + ChannelInfo.touch(channel); + channelMap.put(ChannelInfo.getId(channel), channel); + hashedWheelTimer.newTimeout(timeout -> doPing(timeout, channel), MinBlankChannelSeconds, TimeUnit.SECONDS); + } + + private void doPing(Timeout timeout, Channel channel) { + try { + if (StringUtils.isBlank(ChannelInfo.getClientId(channel))) { + //close + closeConnect(channel, ChannelCloseFrom.SERVER, "No CONNECT"); + return; + } + long channelLifeCycle = ChannelInfo.getChannelLifeCycle(channel); + if (System.currentTimeMillis() > channelLifeCycle) { + closeConnect(channel, ChannelCloseFrom.SERVER, "Channel Auth Expire"); + return; + } + if (ChannelInfo.isExpired(channel)) { + closeConnect(channel, ChannelCloseFrom.SERVER, "No Heart"); + } else { + int keepAliveTimeSeconds = ChannelInfo.getKeepLive(channel); + hashedWheelTimer.newTimeout(timeout.task(), (long)Math.ceil(keepAliveTimeSeconds * 1.5 + 1), + TimeUnit.SECONDS); + } + } catch (Exception e) { + logger.error("", e); + } + } + + @Override + public void closeConnect(Channel channel, ChannelCloseFrom from, String reason) { + String clientId = ChannelInfo.getClientId(channel); + String channelId = ChannelInfo.getId(channel); + if (clientId == null) { + channelMap.remove(channelId); + sessionLoop.unloadSession(clientId, channelId); + if (channel.isActive()) { + channel.close(); + } + return; + } + + //session maybe null + Session session = sessionLoop.unloadSession(clientId, channelId); + retryDriver.unloadSession(session); + channelMap.remove(channelId); + + ChannelInfo.clear(channel); + + if (channel.isActive()) { + channel.close(); + } + } + + @Override + public void closeConnect(String channelId, String reason) { + Channel channel = channelMap.get(channelId); + if (channel == null) { + return; + } + closeConnect(channel, ChannelCloseFrom.SERVER, reason); + } + + @Override + public Channel getChannelById(String channelId) { + return channelMap.get(channelId); + } + + @Override + public int totalConn() { + return channelMap.size(); + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/config/ConnectConf.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/config/ConnectConf.java new file mode 100644 index 00000000000..153fbaaa3e0 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/config/ConnectConf.java @@ -0,0 +1,184 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.config; + +import org.apache.rocketmq.common.MixAll; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +@Component +public class ConnectConf { + private static final String CONF_FILE_NAME = "connect.conf"; + private File confFile; + private int nettySelectThreadNum = 1; + private int nettyWorkerThreadNum = Runtime.getRuntime().availableProcessors() * 2; + private int mqttPort = 1883; + private int mqttWsPort = 8888; + private int maxPacketSizeInByte = 64 * 1024; + private int highWater = 256 * 1024; + private int lowWater = 16 * 1024; + private int maxConn = 10 * 10000; + private boolean order; + private int maxRetryTime = 15; + private int sizeOfNotRollWhenAckSlow = 32; + private int queueCacheSize = 128; + private int pullBatchSize = 32; + private int rpcListenPort = 7001; + private int retryIntervalSeconds = 3; + + public ConnectConf() throws IOException { + ClassPathResource classPathResource = new ClassPathResource(CONF_FILE_NAME); + InputStream in = classPathResource.getInputStream(); + Properties properties = new Properties(); + properties.load(in); + in.close(); + MixAll.properties2Object(properties, this); + this.confFile = new File(classPathResource.getURL().getFile()); + } + + public File getConfFile() { + return confFile; + } + + public int getNettySelectThreadNum() { + return nettySelectThreadNum; + } + + public void setNettySelectThreadNum(int nettySelectThreadNum) { + this.nettySelectThreadNum = nettySelectThreadNum; + } + + public int getNettyWorkerThreadNum() { + return nettyWorkerThreadNum; + } + + public void setNettyWorkerThreadNum(int nettyWorkerThreadNum) { + this.nettyWorkerThreadNum = nettyWorkerThreadNum; + } + + public int getMqttPort() { + return mqttPort; + } + + public void setMqttPort(int mqttPort) { + this.mqttPort = mqttPort; + } + + public int getMqttWsPort() { + return mqttWsPort; + } + + public void setMqttWsPort(int mqttWsPort) { + this.mqttWsPort = mqttWsPort; + } + + public int getMaxPacketSizeInByte() { + return maxPacketSizeInByte; + } + + public void setMaxPacketSizeInByte(int maxPacketSizeInByte) { + this.maxPacketSizeInByte = maxPacketSizeInByte; + } + + public int getHighWater() { + return highWater; + } + + public void setHighWater(int highWater) { + this.highWater = highWater; + } + + public int getLowWater() { + return lowWater; + } + + public void setLowWater(int lowWater) { + this.lowWater = lowWater; + } + + public int getMaxConn() { + return maxConn; + } + + public void setMaxConn(int maxConn) { + this.maxConn = maxConn; + } + + public boolean isOrder() { + return order; + } + + public void setOrder(boolean order) { + this.order = order; + } + + public int getMaxRetryTime() { + return maxRetryTime; + } + + public void setMaxRetryTime(int maxRetryTime) { + this.maxRetryTime = maxRetryTime; + } + + public int getSizeOfNotRollWhenAckSlow() { + return sizeOfNotRollWhenAckSlow; + } + + public void setSizeOfNotRollWhenAckSlow(int sizeOfNotRollWhenAckSlow) { + this.sizeOfNotRollWhenAckSlow = sizeOfNotRollWhenAckSlow; + } + + public int getPullBatchSize() { + return pullBatchSize; + } + + public void setPullBatchSize(int pullBatchSize) { + this.pullBatchSize = pullBatchSize; + } + + public int getQueueCacheSize() { + return queueCacheSize; + } + + public void setQueueCacheSize(int queueCacheSize) { + this.queueCacheSize = queueCacheSize; + } + + public int getRpcListenPort() { + return rpcListenPort; + } + + public void setRpcListenPort(int rpcListenPort) { + this.rpcListenPort = rpcListenPort; + } + + public int getRetryIntervalSeconds() { + return retryIntervalSeconds; + } + + public void setRetryIntervalSeconds(int retryIntervalSeconds) { + this.retryIntervalSeconds = retryIntervalSeconds; + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/config/ConnectConfListener.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/config/ConnectConfListener.java new file mode 100644 index 00000000000..6a8694b3b3f --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/config/ConnectConfListener.java @@ -0,0 +1,73 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.config; + +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Properties; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + + +@Component +public class ConnectConfListener { + private static Logger logger = LoggerFactory.getLogger(ConnectConfListener.class); + + @Resource + private ConnectConf connectConf; + + private File confFile; + private ScheduledThreadPoolExecutor scheduler; + private AtomicLong gmt = new AtomicLong(); + + @PostConstruct + public void start() { + confFile = connectConf.getConfFile(); + gmt.set(confFile.lastModified()); + scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("ConnectConfListener")); + scheduler.scheduleWithFixedDelay(() -> { + try { + if (gmt.get() == confFile.lastModified()) { + return; + } + gmt.set(confFile.lastModified()); + InputStream in = new FileInputStream(confFile.getAbsoluteFile()); + Properties properties = new Properties(); + properties.load(in); + in.close(); + MixAll.properties2Object(properties, connectConf); + logger.warn("UpdateConf:{}", confFile.getAbsolutePath()); + } catch (Exception e) { + logger.error("", e); + } + }, 3, 3, TimeUnit.SECONDS); + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/hook/UpstreamHookManagerImpl.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/hook/UpstreamHookManagerImpl.java new file mode 100644 index 00000000000..763b572998c --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/hook/UpstreamHookManagerImpl.java @@ -0,0 +1,74 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.hook; + +import io.netty.handler.codec.mqtt.MqttMessage; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.hook.UpstreamHook; +import org.apache.rocketmq.mqtt.common.hook.UpstreamHookEnum; +import org.apache.rocketmq.mqtt.common.hook.UpstreamHookManager; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +@Component +public class UpstreamHookManagerImpl implements UpstreamHookManager { + + private UpstreamHook[] upstreamHookList = new UpstreamHook[UpstreamHookEnum.values().length]; + private AtomicBoolean isAssembled = new AtomicBoolean(false); + + @Override + public void addHook(int index, UpstreamHook upstreamHook) { + if (isAssembled.get()) { + throw new IllegalArgumentException("Hook Was Assembled"); + } + synchronized (upstreamHookList) { + upstreamHookList[index] = upstreamHook; + } + } + + @Override + public CompletableFuture doUpstreamHook(MqttMessageUpContext context, MqttMessage msg) { + assembleNextHook(); + CompletableFuture hookResult = new CompletableFuture<>(); + if (upstreamHookList.length <= 0) { + hookResult.complete(new HookResult(HookResult.SUCCESS, -1, null, null)); + return hookResult; + } + return upstreamHookList[0].doHook(context, msg); + } + + private void assembleNextHook() { + if (isAssembled.compareAndSet(false, true)) { + synchronized (upstreamHookList) { + for (int i = 0; i < upstreamHookList.length - 1; i++) { + UpstreamHook upstreamHook = upstreamHookList[i]; + if (upstreamHook.getNextHook() != null) { + continue; + } + upstreamHook.setNextHook(upstreamHookList[i + 1]); + } + } + } + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/MqttPacketDispatcher.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/MqttPacketDispatcher.java new file mode 100644 index 00000000000..0cb302c3366 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/MqttPacketDispatcher.java @@ -0,0 +1,179 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.mqtt; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.mqtt.*; +import io.netty.util.ReferenceCountUtil; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.hook.UpstreamHookManager; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; +import org.apache.rocketmq.mqtt.common.util.HostInfo; +import org.apache.rocketmq.mqtt.cs.channel.ChannelException; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.protocol.mqtt.handler.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.CompletableFuture; + + +@ChannelHandler.Sharable +@Component +public class MqttPacketDispatcher extends SimpleChannelInboundHandler { + private static Logger logger = LoggerFactory.getLogger(MqttPacketDispatcher.class); + + @Resource + private MqttConnectHandler mqttConnectHandler; + + @Resource + private MqttDisconnectHandler mqttDisconnectHandler; + + @Resource + private MqttPublishHandler mqttPublishHandler; + + @Resource + private MqttSubscribeHandler mqttSubscribeHandler; + + @Resource + private MqttPubAckHandler mqttPubAckHandler; + + @Resource + private MqttPingHandler mqttPingHandler; + + @Resource + private MqttUnSubscribeHandler mqttUnSubscribeHandler; + + @Resource + private MqttPubRelHandler mqttPubRelHandler; + + @Resource + private MqttPubRecHandler mqttPubRecHandler; + + @Resource + private MqttPubCompHandler mqttPubCompHandler; + + @Resource + private UpstreamHookManager upstreamHookManager; + + @Resource + private ChannelManager channelManager; + + @Override + protected void channelRead0(ChannelHandlerContext ctx, MqttMessage msg) throws Exception { + if (!ctx.channel().isActive()) { + return; + } + if (!msg.decoderResult().isSuccess()) { + throw new RuntimeException(ChannelInfo.getClientId(ctx.channel()) + "," + msg.decoderResult()); + } + ChannelInfo.touch(ctx.channel()); + CompletableFuture upstreamHookResult; + try { + if (msg instanceof MqttPublishMessage) { + ((MqttPublishMessage) msg).retain(); + } + upstreamHookResult = upstreamHookManager.doUpstreamHook(buildMqttMessageUpContext(ctx), msg); + if (upstreamHookResult == null) { + _channelRead0(ctx, msg, null); + return; + } + } catch (Throwable t) { + logger.error("", t); + if (msg instanceof MqttPublishMessage) { + ReferenceCountUtil.release(msg); + } + throw new ChannelException(t.getMessage()); + } + upstreamHookResult.whenComplete((hookResult, throwable) -> { + if (msg instanceof MqttPublishMessage) { + ReferenceCountUtil.release(msg); + } + if (throwable != null) { + logger.error("", throwable); + ctx.fireExceptionCaught(new ChannelException(throwable.getMessage())); + return; + } + if (hookResult == null) { + ctx.fireExceptionCaught(new ChannelException("UpstreamHook Result Unknown")); + return; + } + try { + _channelRead0(ctx, msg, hookResult); + } catch (Throwable t) { + logger.error("", t); + ctx.fireExceptionCaught(new ChannelException(t.getMessage())); + } + }); + } + + private void _channelRead0(ChannelHandlerContext ctx, MqttMessage msg, HookResult upstreamHookResult) { + switch (msg.fixedHeader().messageType()) { + case CONNECT: + mqttConnectHandler.doHandler(ctx, (MqttConnectMessage) msg, upstreamHookResult); + break; + case PUBLISH: + mqttPublishHandler.doHandler(ctx, (MqttPublishMessage) msg, upstreamHookResult); + break; + case SUBSCRIBE: + mqttSubscribeHandler.doHandler(ctx, (MqttSubscribeMessage) msg, upstreamHookResult); + break; + case PUBACK: + mqttPubAckHandler.doHandler(ctx, (MqttPubAckMessage) msg, upstreamHookResult); + break; + case PINGREQ: + mqttPingHandler.doHandler(ctx, msg, upstreamHookResult); + break; + case UNSUBSCRIBE: + mqttUnSubscribeHandler.doHandler(ctx, (MqttUnsubscribeMessage) msg, upstreamHookResult); + break; + case PUBREL: + mqttPubRelHandler.doHandler(ctx, msg, upstreamHookResult); + break; + case PUBREC: + mqttPubRecHandler.doHandler(ctx, msg, upstreamHookResult); + break; + case PUBCOMP: + mqttPubCompHandler.doHandler(ctx, msg, upstreamHookResult); + break; + case DISCONNECT: + mqttDisconnectHandler.doHandler(ctx, msg, upstreamHookResult); + break; + default: + } + } + + public MqttMessageUpContext buildMqttMessageUpContext(ChannelHandlerContext ctx) { + MqttMessageUpContext context = new MqttMessageUpContext(); + Channel channel = ctx.channel(); + context.setClientId(ChannelInfo.getClientId(channel)); + context.setChannelId(ChannelInfo.getId(channel)); + context.setNode(HostInfo.getInstall().getAddress()); + context.setNamespace(ChannelInfo.getNamespace(channel)); + return context; + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/MqttPacketHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/MqttPacketHandler.java new file mode 100644 index 00000000000..73094f34b15 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/MqttPacketHandler.java @@ -0,0 +1,37 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.mqtt; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.MqttMessage; +import org.apache.rocketmq.mqtt.common.hook.HookResult; + + +public interface MqttPacketHandler { + + /** + * doHandler + * + * @param ctx + * @param mqttMessage + */ + void doHandler(ChannelHandlerContext ctx, T mqttMessage, HookResult upstreamHookResult); + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttConnectHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttConnectHandler.java new file mode 100644 index 00000000000..9fbcbac898e --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttConnectHandler.java @@ -0,0 +1,111 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.mqtt.handler; + + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.*; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.cs.channel.ChannelCloseFrom; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.protocol.mqtt.MqttPacketHandler; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + + +@Component +public class MqttConnectHandler implements MqttPacketHandler { + private static Logger logger = LoggerFactory.getLogger(MqttConnectHandler.class); + + @Resource + private ChannelManager channelManager; + + @Resource + private SessionLoop sessionLoop; + + @Resource + private ConnectConf connectConf; + + private ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("check_connect_future")); + + @Override + public void doHandler(ChannelHandlerContext ctx, MqttConnectMessage connectMessage, HookResult upstreamHookResult) { + MqttConnectVariableHeader variableHeader = connectMessage.variableHeader(); + Channel channel = ctx.channel(); + ChannelInfo.setKeepLive(channel, variableHeader.keepAliveTimeSeconds()); + ChannelInfo.setClientId(channel, connectMessage.payload().clientIdentifier()); + ChannelInfo.setCleanSessionFlag(channel, variableHeader.isCleanSession()); + CompletableFuture future = new CompletableFuture<>(); + ChannelInfo.setFuture(channel, ChannelInfo.FUTURE_CONNECT, future); + scheduler.schedule(() -> { + if (!future.isDone()) { + future.complete(null); + } + }, 1, TimeUnit.SECONDS); + String remark = upstreamHookResult.getRemark(); + if (!upstreamHookResult.isSuccess()) { + byte connAckCode = (byte) upstreamHookResult.getSubCode(); + MqttConnectReturnCode mqttConnectReturnCode = MqttConnectReturnCode.valueOf(connAckCode); + if (mqttConnectReturnCode == null) { + channelManager.closeConnect(channel, ChannelCloseFrom.SERVER, remark); + return; + } + channel.writeAndFlush(getMqttConnAckMessage(mqttConnectReturnCode)); + channelManager.closeConnect(channel, ChannelCloseFrom.SERVER, remark); + return; + } + try { + MqttConnAckMessage mqttConnAckMessage = getMqttConnAckMessage(MqttConnectReturnCode.CONNECTION_ACCEPTED); + future.thenAccept(aVoid -> { + if (!channel.isActive()) { + return; + } + ChannelInfo.removeFuture(channel, ChannelInfo.FUTURE_CONNECT); + channel.writeAndFlush(mqttConnAckMessage); + }); + sessionLoop.loadSession(ChannelInfo.getClientId(channel), channel); + } catch (Exception e) { + logger.error("Connect:{}", connectMessage.payload().clientIdentifier(), e); + channelManager.closeConnect(channel, ChannelCloseFrom.SERVER, "ConnectException"); + } + } + + private MqttConnAckMessage getMqttConnAckMessage(MqttConnectReturnCode returnCode) { + MqttConnAckVariableHeader mqttConnAckVariableHeader = + new MqttConnAckVariableHeader(returnCode, false); + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0); + MqttConnAckMessage mqttConnAckMessage = + new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader); + return mqttConnAckMessage; + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttDisconnectHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttDisconnectHandler.java new file mode 100644 index 00000000000..1c563c08afd --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttDisconnectHandler.java @@ -0,0 +1,45 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.mqtt.handler; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.MqttMessage; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.cs.channel.ChannelCloseFrom; +import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.protocol.mqtt.MqttPacketHandler; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + + + +@Component +public class MqttDisconnectHandler implements MqttPacketHandler { + + @Resource + private ChannelManager channelManager; + + @Override + public void doHandler(ChannelHandlerContext ctx, MqttMessage mqttMessage, HookResult upstreamHookResult) { + channelManager.closeConnect(ctx.channel(), ChannelCloseFrom.CLIENT, "disconnect"); + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPingHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPingHandler.java new file mode 100644 index 00000000000..d68ff79707a --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPingHandler.java @@ -0,0 +1,57 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.mqtt.handler; + + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.MqttFixedHeader; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttMessageType; +import io.netty.handler.codec.mqtt.MqttQoS; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.protocol.mqtt.MqttPacketHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + + + +@Component +public class MqttPingHandler implements MqttPacketHandler { + private static Logger logger = LoggerFactory.getLogger(MqttPingHandler.class); + + @Resource + private ChannelManager channelManager; + + @Override + public void doHandler(ChannelHandlerContext ctx, MqttMessage mqttMessage, HookResult upstreamHookResult) { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0); + Channel channel = ctx.channel(); + ChannelInfo.touch(channel); + MqttMessage pingMessage = new MqttMessage(mqttFixedHeader); + channel.writeAndFlush(pingMessage); + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubAckHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubAckHandler.java new file mode 100644 index 00000000000..ba2cbd4734f --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubAckHandler.java @@ -0,0 +1,58 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.mqtt.handler; + + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.MqttPubAckMessage; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.protocol.mqtt.MqttPacketHandler; +import org.apache.rocketmq.mqtt.cs.session.infly.PushAction; +import org.apache.rocketmq.mqtt.cs.session.infly.RetryDriver; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + + + +@Component +public class MqttPubAckHandler implements MqttPacketHandler { + private static Logger logger = LoggerFactory.getLogger(MqttPubAckHandler.class); + + @Resource + private PushAction pushAction; + + @Resource + private RetryDriver retryDriver; + + @Resource + private SessionLoop sessionLoop; + + @Override + public void doHandler(ChannelHandlerContext ctx, MqttPubAckMessage mqttMessage, HookResult upstreamHookResult) { + int messageId = mqttMessage.variableHeader().messageId(); + retryDriver.unMountPublish(messageId, ChannelInfo.getId(ctx.channel())); + pushAction.rollNextByAck(sessionLoop.getSession(ChannelInfo.getId(ctx.channel())), messageId); + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubCompHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubCompHandler.java new file mode 100644 index 00000000000..17913a614cd --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubCompHandler.java @@ -0,0 +1,67 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.mqtt.handler; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.protocol.mqtt.MqttPacketHandler; +import org.apache.rocketmq.mqtt.cs.session.infly.InFlyCache; +import org.apache.rocketmq.mqtt.cs.session.infly.MqttMsgId; +import org.apache.rocketmq.mqtt.cs.session.infly.PushAction; +import org.apache.rocketmq.mqtt.cs.session.infly.RetryDriver; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + + +@Component +public class MqttPubCompHandler implements MqttPacketHandler { + + @Resource + private RetryDriver retryDriver; + + @Resource + private InFlyCache inFlyCache; + + @Resource + private MqttMsgId mqttMsgId; + + @Resource + private PushAction pushAction; + + @Resource + private SessionLoop sessionLoop; + + @Override + public void doHandler(ChannelHandlerContext ctx, MqttMessage mqttMessage, HookResult upstreamHookResult) { + MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) mqttMessage.variableHeader(); + String channelId = ChannelInfo.getId(ctx.channel()); + + retryDriver.unMountPubRel(variableHeader.messageId(), ChannelInfo.getId(ctx.channel())); + + //The Packet Identifier becomes available for reuse once the Sender has received the PUBCOMP Packet. + pushAction.rollNextByAck(sessionLoop.getSession(channelId), variableHeader.messageId()); + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubRecHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubRecHandler.java new file mode 100644 index 00000000000..cfe0c0d2383 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubRecHandler.java @@ -0,0 +1,60 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.mqtt.handler; + + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.*; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.protocol.mqtt.MqttPacketHandler; +import org.apache.rocketmq.mqtt.cs.session.infly.InFlyCache; +import org.apache.rocketmq.mqtt.cs.session.infly.RetryDriver; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + + +@Component +public class MqttPubRecHandler implements MqttPacketHandler { + + @Resource + private RetryDriver retryDriver; + + @Resource + private InFlyCache inFlyCache; + + @Resource + private SessionLoop sessionLoop; + + @Override + public void doHandler(ChannelHandlerContext ctx, MqttMessage mqttMessage, HookResult upstreamHookResult) { + MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) mqttMessage.variableHeader(); + String channelId = ChannelInfo.getId(ctx.channel()); + retryDriver.unMountPublish(variableHeader.messageId(), channelId); + retryDriver.mountPubRel(variableHeader.messageId(), channelId); + + MqttFixedHeader pubRelMqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBREL, false, + MqttQoS.AT_LEAST_ONCE, false, 0); + MqttMessage pubRelMqttMessage = new MqttMessage(pubRelMqttFixedHeader, variableHeader); + ctx.channel().writeAndFlush(pubRelMqttMessage); + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubRelHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubRelHandler.java new file mode 100644 index 00000000000..11db6122ffc --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPubRelHandler.java @@ -0,0 +1,51 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.mqtt.handler; + + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.*; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.protocol.mqtt.MqttPacketHandler; +import org.apache.rocketmq.mqtt.cs.session.infly.InFlyCache; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + + +@Component +public class MqttPubRelHandler implements MqttPacketHandler { + + @Resource + private InFlyCache inFlyCache; + + @Override + public void doHandler(ChannelHandlerContext ctx, MqttMessage mqttMessage, HookResult upstreamHookResult) { + final MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) mqttMessage.variableHeader(); + String channelId = ChannelInfo.getId(ctx.channel()); + inFlyCache.remove(InFlyCache.CacheType.PUB, channelId, variableHeader.messageId()); + + MqttFixedHeader pubcompFixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, + false, 0); + MqttMessage pubcomMqttMessage = new MqttMessage(pubcompFixedHeader, variableHeader); + ctx.channel().writeAndFlush(pubcomMqttMessage); + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPublishHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPublishHandler.java new file mode 100644 index 00000000000..8825637e7eb --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttPublishHandler.java @@ -0,0 +1,114 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.mqtt.handler; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.*; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.cs.channel.ChannelCloseFrom; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.protocol.mqtt.MqttPacketHandler; +import org.apache.rocketmq.mqtt.cs.session.infly.InFlyCache; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +@Component +public class MqttPublishHandler implements MqttPacketHandler { + private static Logger logger = LoggerFactory.getLogger(MqttPublishHandler.class); + + @Resource + private InFlyCache inFlyCache; + + @Resource + private ChannelManager channelManager; + + @Resource + private SessionLoop sessionLoop; + + @Resource + private ConnectConf connectConf; + + + @Override + public void doHandler(ChannelHandlerContext ctx, + MqttPublishMessage mqttMessage, + HookResult upstreamHookResult) { + final MqttPublishVariableHeader variableHeader = mqttMessage.variableHeader(); + Channel channel = ctx.channel(); + String channelId = ChannelInfo.getId(channel); + final boolean isQos2Message = isQos2Message(mqttMessage); + if (isQos2Message) { + if (inFlyCache.contains(InFlyCache.CacheType.PUB, channelId, variableHeader.messageId())) { + doResponse(ctx, mqttMessage); + return; + } + } + String remark = upstreamHookResult.getRemark(); + if (!upstreamHookResult.isSuccess()) { + channelManager.closeConnect(channel, ChannelCloseFrom.SERVER, remark); + return; + } + doResponse(ctx, mqttMessage); + if (isQos2Message) { + inFlyCache.put(InFlyCache.CacheType.PUB, channelId, variableHeader.messageId()); + } + } + + private boolean isQos2Message(MqttPublishMessage mqttPublishMessage) { + return MqttQoS.EXACTLY_ONCE.equals(mqttPublishMessage.fixedHeader().qosLevel()); + } + + private void doResponse(ChannelHandlerContext ctx, MqttPublishMessage mqttMessage) { + MqttFixedHeader fixedHeader = mqttMessage.fixedHeader(); + MqttPublishVariableHeader variableHeader = mqttMessage.variableHeader(); + switch (fixedHeader.qosLevel()) { + case AT_MOST_ONCE: + break; + case AT_LEAST_ONCE: + MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, + MqttQoS.AT_MOST_ONCE, + false, 0); + MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader + .from(variableHeader.messageId()); + MqttPubAckMessage pubackMessage = new MqttPubAckMessage(mqttFixedHeader, mqttMessageIdVariableHeader); + ctx.channel().writeAndFlush(pubackMessage); + break; + case EXACTLY_ONCE: + MqttFixedHeader pubrecMqttHeader = new MqttFixedHeader(MqttMessageType.PUBREC, false, + MqttQoS.AT_MOST_ONCE, + false, 0); + MqttMessageIdVariableHeader pubrecMessageIdVariableHeader = MqttMessageIdVariableHeader + .from(variableHeader.messageId()); + MqttMessage pubrecMqttMessage = new MqttMessage(pubrecMqttHeader, pubrecMessageIdVariableHeader); + ctx.channel().writeAndFlush(pubrecMqttMessage); + break; + default: + throw new IllegalArgumentException("unknown qos:" + fixedHeader.qosLevel()); + } + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttSubscribeHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttSubscribeHandler.java new file mode 100644 index 00000000000..3435ff3d6be --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttSubscribeHandler.java @@ -0,0 +1,130 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.mqtt.handler; + + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.*; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.cs.channel.ChannelCloseFrom; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.protocol.mqtt.MqttPacketHandler; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader.from; +import static io.netty.handler.codec.mqtt.MqttMessageType.SUBACK; +import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE; + + + +@Component +public class MqttSubscribeHandler implements MqttPacketHandler { + private static Logger logger = LoggerFactory.getLogger(MqttSubscribeHandler.class); + + + @Resource + private SessionLoop sessionLoop; + + @Resource + private ChannelManager channelManager; + + @Resource + private ConnectConf connectConf; + + private ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("check_connect_future")); + + + @Override + public void doHandler(ChannelHandlerContext ctx, MqttSubscribeMessage mqttMessage, HookResult upstreamHookResult) { + String clientId = ChannelInfo.getClientId(ctx.channel()); + Channel channel = ctx.channel(); + CompletableFuture future = new CompletableFuture<>(); + ChannelInfo.setFuture(channel, ChannelInfo.FUTURE_SUBSCRIBE, future); + scheduler.schedule(() -> { + if(!future.isDone()){ + future.complete(null); + } + },1,TimeUnit.SECONDS); + String remark = upstreamHookResult.getRemark(); + if(!upstreamHookResult.isSuccess()){ + channelManager.closeConnect(channel, ChannelCloseFrom.SERVER, remark); + return; + } + try { + MqttSubscribePayload payload = mqttMessage.payload(); + List mqttTopicSubscriptions = payload.topicSubscriptions(); + if (mqttTopicSubscriptions != null && !mqttTopicSubscriptions.isEmpty()) { + Set subscriptions = new HashSet<>(mqttTopicSubscriptions.size()); + for (MqttTopicSubscription mqttTopicSubscription : mqttTopicSubscriptions) { + Subscription subscription = new Subscription(); + subscription.setQos(mqttTopicSubscription.qualityOfService().value()); + subscription.setTopicFilter(TopicUtils.normalizeTopic(mqttTopicSubscription.topicName())); + subscriptions.add(subscription); + } + sessionLoop.addSubscription(ChannelInfo.getId(ctx.channel()), subscriptions); + } + future.thenAccept(aVoid -> { + if (!channel.isActive()) { + return; + } + ChannelInfo.removeFuture(channel, ChannelInfo.FUTURE_SUBSCRIBE); + channel.writeAndFlush(getResponse(mqttMessage)); + }); + } catch (Exception e) { + logger.error("Subscribe:{}", clientId, e); + channelManager.closeConnect(channel, ChannelCloseFrom.SERVER, "SubscribeException"); + } + } + + + private MqttSubAckMessage getResponse(MqttSubscribeMessage mqttSubscribeMessage) { + MqttSubscribePayload payload = mqttSubscribeMessage.payload(); + List mqttTopicSubscriptions = payload.topicSubscriptions(); + // AT_MOST_ONCE + int[] qoss = new int[mqttTopicSubscriptions.size()]; + int i = 0; + for (MqttTopicSubscription sub : mqttTopicSubscriptions) { + qoss[i++] = sub.qualityOfService().value(); + } + MqttFixedHeader fixedHeader = new MqttFixedHeader(SUBACK, false, AT_MOST_ONCE, false, 0); + MqttMessageIdVariableHeader variableHeader = from(mqttSubscribeMessage.variableHeader().messageId()); + MqttSubAckMessage mqttSubAckMessage = new MqttSubAckMessage(fixedHeader, variableHeader, + new MqttSubAckPayload(qoss)); + return mqttSubAckMessage; + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttUnSubscribeHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttUnSubscribeHandler.java new file mode 100644 index 00000000000..4cc32b177b6 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/mqtt/handler/MqttUnSubscribeHandler.java @@ -0,0 +1,91 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.mqtt.handler; + + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.*; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.cs.channel.ChannelCloseFrom; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.protocol.mqtt.MqttPacketHandler; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.HashSet; +import java.util.Set; + +import static io.netty.handler.codec.mqtt.MqttMessageType.UNSUBACK; +import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE; + + +@Component +public class MqttUnSubscribeHandler implements MqttPacketHandler { + private static Logger logger = LoggerFactory.getLogger(MqttUnSubscribeHandler.class); + + + @Resource + private SessionLoop sessionLoop; + + @Resource + private ChannelManager channelManager; + + @Override + public void doHandler(ChannelHandlerContext ctx, MqttUnsubscribeMessage mqttMessage, HookResult upstreamHookResult) { + long start = System.currentTimeMillis(); + String clientId = ChannelInfo.getClientId(ctx.channel()); + Channel channel = ctx.channel(); + String remark = upstreamHookResult.getRemark(); + if (!upstreamHookResult.isSuccess()) { + channelManager.closeConnect(channel, ChannelCloseFrom.SERVER, remark); + return; + } + try { + MqttUnsubscribePayload payload = mqttMessage.payload(); + if (payload.topics() != null && !payload.topics().isEmpty()) { + Set subscriptions = new HashSet<>(); + for (String topic : payload.topics()) { + subscriptions.add(new Subscription(TopicUtils.normalizeTopic(topic))); + } + sessionLoop.removeSubscription(ChannelInfo.getId(ctx.channel()), subscriptions); + } + channel.writeAndFlush(getResponse(mqttMessage)); + } catch (Exception e) { + logger.error("UnSubscribe:{}", clientId, e); + channelManager.closeConnect(channel, ChannelCloseFrom.SERVER, "UnSubscribeException"); + } + } + + private MqttUnsubAckMessage getResponse(MqttUnsubscribeMessage mqttUnsubscribeMessage) { + MqttFixedHeader fixedHeader = new MqttFixedHeader(UNSUBACK, false, AT_MOST_ONCE, false, 0); + MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader + .from(mqttUnsubscribeMessage.variableHeader().messageId()); + MqttUnsubAckMessage mqttUnsubAckMessage = new MqttUnsubAckMessage(fixedHeader, variableHeader); + return mqttUnsubAckMessage; + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/rpc/RpcPacketDispatcher.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/rpc/RpcPacketDispatcher.java new file mode 100644 index 00000000000..bc0c316770f --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/rpc/RpcPacketDispatcher.java @@ -0,0 +1,86 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.rpc; + + +import com.alibaba.fastjson.JSONObject; +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.mqtt.common.model.MessageEvent; +import org.apache.rocketmq.mqtt.common.model.RpcCode; +import org.apache.rocketmq.mqtt.common.model.RpcHeader; +import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.session.notify.MessageNotifyAction; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.nio.charset.StandardCharsets; +import java.util.List; + + +@Component +public class RpcPacketDispatcher implements NettyRequestProcessor { + private static Logger logger = LoggerFactory.getLogger(RpcPacketDispatcher.class); + + @Resource + private MessageNotifyAction messageNotifyAction; + + @Resource + private ChannelManager channelManager; + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { + RemotingCommand response = RemotingCommand.createResponseCommand(RpcCode.SUCCESS, null); + response.setOpaque(request.getOpaque()); + int code = request.getCode(); + try { + if (RpcCode.CMD_NOTIFY_MQTT_MESSAGE == code) { + doNotify(request); + } else if (RpcCode.CMD_CLOSE_CHANNEL == code) { + closeChannel(request); + } + } catch (Throwable t) { + logger.error("", t); + response.setCode(RpcCode.FAIL); + } + return response; + } + + @Override + public boolean rejectRequest() { + return false; + } + + private void doNotify(RemotingCommand request) { + String payload = new String(request.getBody(), StandardCharsets.UTF_8); + List events = JSONObject.parseArray(payload, MessageEvent.class); + messageNotifyAction.notify(events); + } + + private void closeChannel(RemotingCommand request) { + String channelId = request.getExtFields() != null ? + request.getExtFields().get(RpcHeader.MQTT_CHANNEL_ID) : null; + channelManager.closeConnect(channelId, request.getRemark()); + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/ws/WebSocketServerHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/ws/WebSocketServerHandler.java new file mode 100644 index 00000000000..12e8a1aaba3 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/ws/WebSocketServerHandler.java @@ -0,0 +1,109 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ +package org.apache.rocketmq.mqtt.cs.protocol.ws; + +import io.netty.channel.*; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.websocketx.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static io.netty.handler.codec.http.HttpUtil.isKeepAlive; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + + +@ChannelHandler.Sharable +@Component +public class WebSocketServerHandler extends SimpleChannelInboundHandler { + private static Logger sysLogger = LoggerFactory.getLogger(WebSocketServerHandler.class); + + private WebSocketServerHandshaker handshaker; + + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } + + private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { + if (!req.decoderResult().isSuccess()) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST)); + return; + } + String upgrade = req.headers().get("Upgrade"); + if (upgrade == null || (!"websocket".equals(upgrade.toLowerCase()))) { + return; + } + WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:8888/mqtt", + "*", + false); + handshaker = wsFactory.newHandshaker(req); + if (handshaker == null) { + WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); + } else { + handshaker.handshake(ctx.channel(), req); + } + } + + private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { + if (frame instanceof CloseWebSocketFrame) { + handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); + return; + } + if (frame instanceof PingWebSocketFrame) { + ctx.channel().write(new PongWebSocketFrame(frame.content().retain())); + return; + } + if (frame instanceof PongWebSocketFrame) { + return; + } + if (frame instanceof BinaryWebSocketFrame) { + throw new UnsupportedOperationException( + String.format("%s frame types not supported", frame.getClass().getName())); + } + String request = ((TextWebSocketFrame) frame).text(); + ctx.channel().write( + new TextWebSocketFrame(request + " , welcome netty websocket:" + new java.util.Date().toString())); + } + + public static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { + ChannelFuture f = ctx.channel().writeAndFlush(res); + if (!isKeepAlive(req) || res.status().code() != 200) { + f.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof FullHttpRequest) { + handleHttpRequest(ctx, (FullHttpRequest) msg); + } else if (msg instanceof WebSocketFrame) { + if (msg instanceof BinaryWebSocketFrame) { + ((WebSocketFrame) msg).retain(); + ctx.fireChannelRead(((WebSocketFrame) msg).content()); + } else { + handleWebSocketFrame(ctx, (WebSocketFrame) msg); + } + } + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/ws/WebsocketEncoder.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/ws/WebsocketEncoder.java new file mode 100644 index 00000000000..dab63060231 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/ws/WebsocketEncoder.java @@ -0,0 +1,38 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.protocol.ws; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; + +import java.util.List; + +public class WebsocketEncoder extends MessageToMessageEncoder{ + + @Override + protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception { + msg.retain(); + BinaryWebSocketFrame binaryWebSocketFrame=new BinaryWebSocketFrame(msg); + out.add(binaryWebSocketFrame); + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/QueueFresh.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/QueueFresh.java new file mode 100644 index 00000000000..194dd4e92fb --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/QueueFresh.java @@ -0,0 +1,65 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.session; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.HashSet; +import java.util.Set; + +@Component +public class QueueFresh { + + @Resource + private LmqQueueStore lmqQueueStore; + + public Set freshQueue(Session session, Subscription subscription) { + Set queues = new HashSet<>(); + Set brokers; + if (subscription.isP2p()) { + String findTopic = lmqQueueStore.getClientP2pTopic(); + if(StringUtils.isBlank(findTopic)){ + findTopic = lmqQueueStore.getClientRetryTopic(); + } + brokers = lmqQueueStore.getReadableBrokers(findTopic); + } else if (subscription.isRetry()) { + brokers = lmqQueueStore.getReadableBrokers(lmqQueueStore.getClientRetryTopic()); + } else { + brokers = lmqQueueStore.getReadableBrokers(subscription.toFirstTopic()); + } + if (brokers == null || brokers.isEmpty()) { + return queues; + } + for (String broker : brokers) { + Queue moreQueue = new Queue(); + moreQueue.setQueueName(subscription.toQueueName()); + moreQueue.setBrokerName(broker); + queues.add(moreQueue); + } + session.freshQueue(subscription, queues); + return queues; + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/Session.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/Session.java new file mode 100644 index 00000000000..f3538b67e3e --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/Session.java @@ -0,0 +1,469 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.session; + + +import io.netty.channel.Channel; +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.QueueOffset; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; + +public class Session { + private static Logger logger = LoggerFactory.getLogger(Session.class); + private final long startTime = System.currentTimeMillis(); + private Channel channel; + private volatile boolean destroyed = false; + private volatile int loadStatus = -1; + private volatile int pullSize; + private String clientId; + private String channelId; + private AtomicBoolean needPersistOffset = new AtomicBoolean(false); + private ConcurrentMap> offsetMap = new ConcurrentHashMap<>(16); + private Map subscriptions = new ConcurrentHashMap<>(); + private ConcurrentMap>> sendingMessages = new ConcurrentHashMap<>(16); + private ConcurrentMap loadStatusMap = new ConcurrentHashMap<>(); + + public Session() { + } + + public ConcurrentMap getLoadStatusMap() { + return loadStatusMap; + } + + public long getStartTime() { + return startTime; + } + + public Channel getChannel() { + return channel; + } + + public void setChannel(Channel channel) { + this.channel = channel; + } + + public boolean isClean() { + return Boolean.TRUE.equals(ChannelInfo.getCleanSessionFlag(channel)); + } + + public boolean isLoaded() { + return this.loadStatus == 1; + } + + public void setLoaded() { + this.loadStatus = 1; + } + + public void setLoading() { + this.loadStatus = 0; + } + + public boolean isLoading() { + return loadStatus == 0; + } + + public void resetLoad() { + this.loadStatus = -1; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public boolean isDestroyed() { + return destroyed; + } + + public int getPullSize() { + return pullSize; + } + + public void setPullSize(int pullSize) { + this.pullSize = pullSize; + } + + public void destroy() { + this.destroyed = true; + this.offsetMap.clear(); + this.sendingMessages.clear(); + this.subscriptions.clear(); + } + + public Map> offsetMapSnapshot() { + Map> tmp = new HashMap<>(8); + for (String queueName : offsetMap.keySet()) { + Subscription subscription = subscriptions.get(queueName); + if (subscription == null) { + continue; + } + Map queueMap = new HashMap<>(8); + tmp.put(subscription, queueMap); + for (Map.Entry entry : offsetMap.get(queueName).entrySet()) { + queueMap.put(entry.getKey(), entry.getValue()); + } + } + return tmp; + } + + public Set subscriptionSnapshot() { + Set tmp = new HashSet<>(); + tmp.addAll(subscriptions.values()); + return tmp; + } + + public void removeSubscription(Subscription subscription) { + if (subscription == null) { + throw new RuntimeException("subscription is null"); + } + offsetMap.remove(subscription.toQueueName()); + sendingMessages.remove(subscription); + subscriptions.remove(subscription.getTopicFilter()); + } + + public void freshQueue(Subscription subscription, Set queues) { + if (subscription == null) { + throw new RuntimeException("subscription is null"); + } + if (queues == null) { + logger.warn("queues is null when freshQueue,{},{}", getClientId(), subscription); + return; + } + if (!subscriptions.containsKey(subscription.getTopicFilter())) { + return; + } + + String queueName = subscription.toQueueName(); + if (!offsetMap.containsKey(queueName)) { + offsetMap.putIfAbsent(queueName, new ConcurrentHashMap<>(16)); + } + for (Queue memQueue : offsetMap.get(queueName).keySet()) { + if (!queues.contains(memQueue)) { + offsetMap.get(queueName).remove(memQueue); + } + } + // init queueOffset + for (Queue nowQueue : queues) { + if (!offsetMap.get(queueName).containsKey(nowQueue)) { + QueueOffset queueOffset = new QueueOffset(); + //if no offset use init offset + offsetMap.get(queueName).put(nowQueue, queueOffset); + this.markPersistOffsetFlag(true); + } + } + + if (!sendingMessages.containsKey(subscription)) { + sendingMessages.putIfAbsent(subscription, new ConcurrentHashMap<>(16)); + } + for (Queue memQueue : sendingMessages.get(subscription).keySet()) { + if (!queues.contains(memQueue)) { + sendingMessages.get(subscription).remove(memQueue); + } + } + if (queues.isEmpty()) { + logger.warn("queues is empty when freshQueue,{},{}", getClientId(), subscription); + } + } + + public void addOffset(String queueName, Map map) { + if (queueName == null) { + throw new RuntimeException("queueName is null"); + } + + if (!offsetMap.containsKey(queueName)) { + offsetMap.putIfAbsent(queueName, new ConcurrentHashMap<>(16)); + } + offsetMap.get(queueName).putAll(map); + } + + public void addOffset(Map> offsetMapParam) { + if (offsetMapParam != null && !offsetMapParam.isEmpty()) { + for (String queueName : offsetMapParam.keySet()) { + if (!subscriptions.containsKey(queueName)) { + continue; + } + addOffset(queueName, offsetMapParam.get(queueName)); + } + } + } + + public void addSubscription(Set subscriptionsParam) { + if (CollectionUtils.isEmpty(subscriptionsParam)) { + return; + } + for (Subscription subscription : subscriptionsParam) { + addSubscription(subscription); + } + } + + public void addSubscription(Subscription subscriptionParam) { + if (subscriptionParam != null) { + subscriptions.put(subscriptionParam.getTopicFilter(), subscriptionParam); + } + } + + public QueueOffset getQueueOffset(Subscription subscription, Queue queue) { + if (subscription == null) { + throw new RuntimeException("subscription is null"); + } + if (queue == null) { + throw new RuntimeException("queue is null"); + } + String queueName = subscription.toQueueName(); + Map queueQueueOffsetMap = offsetMap.get(queueName); + if (queueQueueOffsetMap != null) { + return queueQueueOffsetMap.get(queue); + } + return null; + } + + public Map getQueueOffset(Subscription subscription) { + if (subscription == null) { + throw new RuntimeException("subscription is null"); + } + String queueName = subscription.toQueueName(); + return offsetMap.get(queueName); + } + + public boolean addSendingMessages(Subscription subscription, Queue queue, List messages) { + if (subscription == null) { + throw new RuntimeException("subscription is null"); + } + if (queue == null) { + throw new RuntimeException("queue is null"); + } + if (messages == null || messages.isEmpty()) { + return false; + } + if (!subscriptions.containsKey(subscription.getTopicFilter())) { + return false; + } + if (!sendingMessages.containsKey(subscription)) { + sendingMessages.putIfAbsent(subscription, new ConcurrentHashMap<>(16)); + } + if (!sendingMessages.get(subscription).containsKey(queue)) { + sendingMessages.get(subscription).putIfAbsent(queue, new LinkedHashSet<>(8)); + } + String queueName = subscription.toQueueName(); + Map queueOffsetMap = offsetMap.get(queueName); + if (queueOffsetMap == null || !queueOffsetMap.containsKey(queue)) { + logger.warn("not found queueOffset,{},{},{}", getClientId(), subscription, queue); + return false; + } + boolean add = false; + QueueOffset queueOffset; + queueOffset = queueOffsetMap.get(queue); + Iterator iterator = messages.iterator(); + while (iterator.hasNext()) { + Message message = iterator.next(); + if (message.getOffset() < queueOffset.getOffset() && queueOffset.getOffset() != Long.MAX_VALUE) { + continue; + } + synchronized (this) { + if (sendingMessages.get(subscription).get(queue).add(message.copy())) { + add = true; + } + } + } + return add; + } + + public Message rollNext(Subscription subscription, Queue pendingQueue, long pendingDownSeqId) { + if (subscription == null) { + throw new RuntimeException("subscription is null"); + } + if (pendingQueue == null) { + throw new RuntimeException("queue is null"); + } + Map> queueSendingMsgs = sendingMessages.get(subscription); + if (queueSendingMsgs == null || queueSendingMsgs.isEmpty()) { + return null; + } + LinkedHashSet messages = queueSendingMsgs.get(pendingQueue); + if (messages == null) { + return null; + } + Message message; + Message nextMessage = null; + synchronized (this) { + if (messages.isEmpty()) { + return null; + } + message = messages.iterator().next(); + if (message.getOffset() != pendingDownSeqId) { + return null; + } + messages.remove(message); + updateQueueOffset(subscription, pendingQueue, message); + this.markPersistOffsetFlag(true); + if (!messages.isEmpty()) { + nextMessage = messages.iterator().next(); + } + } + return nextMessage; + } + + public boolean sendingMessageIsEmpty(Subscription subscription, Queue queue) { + if (subscription == null) { + throw new RuntimeException("subscription is null"); + } + if (queue == null) { + throw new RuntimeException("queue is null"); + } + Map> queueSendingMsgs = sendingMessages.get(subscription); + if (queueSendingMsgs == null || queueSendingMsgs.isEmpty()) { + return true; + } + LinkedHashSet messages = queueSendingMsgs.get(queue); + if (messages == null) { + return true; + } + synchronized (this) { + return messages.size() <= 0; + } + } + + public Message nextSendMessageByOrder(Subscription subscription, Queue queue) { + if (subscription == null) { + throw new RuntimeException("subscription is null"); + } + if (queue == null) { + throw new RuntimeException("queue is null"); + } + Map> tmp = sendingMessages.get(subscription); + if (tmp != null && !tmp.isEmpty()) { + LinkedHashSet messages = tmp.get(queue); + if (messages == null) { + return null; + } + synchronized (this) { + return messages.isEmpty() ? null : messages.iterator().next(); + } + } + return null; + } + + public List pendMessageList(Subscription subscription, Queue queue) { + if (subscription == null) { + throw new RuntimeException("subscription is null"); + } + if (queue == null) { + throw new RuntimeException("queue is null"); + } + List list = new ArrayList<>(); + Map> tmp = sendingMessages.get(subscription); + if (tmp != null && !tmp.isEmpty()) { + LinkedHashSet messages = tmp.get(queue); + if (messages == null) { + return null; + } + synchronized (this) { + if (!messages.isEmpty()) { + for (Message message : messages) { + if (message.getAck() == -1) { + list.add(message); + } + } + } + } + } + return list; + } + + public void ack(Subscription subscription, Queue pendingQueue, long pendingDownSeqId) { + if (subscription == null) { + throw new RuntimeException("subscription is null"); + } + if (pendingQueue == null) { + throw new RuntimeException("queue is null"); + } + Map> queueSendingMsgs = sendingMessages.get(subscription); + if (queueSendingMsgs == null || queueSendingMsgs.isEmpty()) { + return; + } + LinkedHashSet messages = queueSendingMsgs.get(pendingQueue); + if (messages == null) { + return; + } + synchronized (this) { + if (messages.isEmpty()) { + return; + } + boolean flag = true; + Iterator iterator = messages.iterator(); + while (iterator.hasNext()) { + Message message = iterator.next(); + if (message.getOffset() == pendingDownSeqId) { + message.setAck(1); + } + if (message.getAck() != 1) { + flag = false; + } + if (flag) { + updateQueueOffset(subscription, pendingQueue, message); + this.markPersistOffsetFlag(true); + iterator.remove(); + } + } + } + } + + private void updateQueueOffset(Subscription subscription, Queue queue, Message message) { + String queueName = subscription.toQueueName(); + Map queueOffsetMap = offsetMap.get(queueName); + if (queueOffsetMap == null || !queueOffsetMap.containsKey(queue)) { + logger.warn("failed update queue offset,not found queueOffset,{},{},{}", getClientId(), subscription, + queue); + return; + } + QueueOffset queueOffset = queueOffsetMap.get(queue); + queueOffset.setOffset(message.getOffset() + 1); + } + + public boolean markPersistOffsetFlag(boolean flag) { + return this.needPersistOffset.compareAndSet(!flag, flag); + } + + public boolean getPersistOffsetFlag() { + return needPersistOffset.get(); + } +} + diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/InFlyCache.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/InFlyCache.java new file mode 100644 index 00000000000..93e21fb8ad9 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/InFlyCache.java @@ -0,0 +1,191 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.session.infly; + + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + + +@Component +public class InFlyCache { + + @Resource + private MqttMsgId mqttMsgId; + + private ConcurrentMap> pubCache = new ConcurrentHashMap<>(128); + private PendingDownCache pendingDownCache = new PendingDownCache(); + + public void cleanResource(String clientId,String channelId) { + pubCache.remove(channelId); + pendingDownCache.clear(clientId, channelId); + } + + public enum CacheType { + PUB, + } + + private ConcurrentMap> whichCache(CacheType cacheType) { + switch (cacheType) { + case PUB: + return pubCache; + default: + throw new RuntimeException("invalid cache type"); + } + } + + public void put(CacheType cacheType, String channelId, int mqttMsgId) { + ConcurrentMap> cache = whichCache(cacheType); + if (!cache.containsKey(channelId)) { + cache.putIfAbsent(channelId, new HashSet<>()); + } + Set idCache = cache.get(channelId); + if (idCache == null) { + return; + } + synchronized (idCache) { + cache.get(channelId).add(mqttMsgId); + } + } + + public boolean contains(CacheType cacheType, String channelId, int mqttMsgId) { + ConcurrentMap> cache = whichCache(cacheType); + Set idCache = cache.get(channelId); + if (idCache == null) { + return false; + } + synchronized (idCache) { + return idCache.contains(mqttMsgId); + } + } + + public void remove(CacheType cacheType, String channelId, int mqttMsgId) { + ConcurrentMap> cache = whichCache(cacheType); + Set idCache = cache.get(channelId); + if (idCache == null) { + return; + } + synchronized (idCache) { + idCache.remove(mqttMsgId); + if (idCache.isEmpty()) { + cache.remove(channelId); + } + } + } + + public PendingDownCache getPendingDownCache() { + return pendingDownCache; + } + + public class PendingDownCache { + private ConcurrentMap> cache = new ConcurrentHashMap<>(128); + + public PendingDown put(String channelId, int mqttMsgId, Subscription subscription, Queue queue, + Message message) { + PendingDown pendingDown = new PendingDown(); + pendingDown.setSubscription(subscription); + pendingDown.setQueue(queue); + pendingDown.setSeqId(message.getOffset()); + if (!cache.containsKey(channelId)) { + cache.putIfAbsent(channelId, new ConcurrentHashMap<>(16)); + } + cache.get(channelId).put(mqttMsgId, pendingDown); + return pendingDown; + } + + public Map all(String channelId) { + if (StringUtils.isBlank(channelId)) { + return null; + } + return cache.get(channelId); + } + + public PendingDown remove(String channelId, int mqttMsgId) { + Map map = cache.get(channelId); + if (map != null) { + return map.remove(mqttMsgId); + + } + return null; + } + + public PendingDown get(String channelId, int mqttMsgId) { + Map map = cache.get(channelId); + if (map != null) { + return map.get(mqttMsgId); + + } + return null; + } + + public void clear(String clientId, String channelId) { + Map pendingDownMap = cache.remove(channelId); + if (clientId != null && pendingDownMap != null) { + pendingDownMap.forEach((mqttId, pendingDown) -> mqttMsgId.releaseId(mqttId, clientId)); + } + } + } + + public class PendingDown { + private Subscription subscription; + private Queue queue; + private long seqId; + private long t = System.currentTimeMillis(); + + public Subscription getSubscription() { + return subscription; + } + + public void setSubscription(Subscription subscription) { + this.subscription = subscription; + } + + public Queue getQueue() { + return queue; + } + + public void setQueue(Queue queue) { + this.queue = queue; + } + + public long getSeqId() { + return seqId; + } + + public void setSeqId(long seqId) { + this.seqId = seqId; + } + + public long getT() { + return t; + } + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/MqttMsgId.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/MqttMsgId.java new file mode 100644 index 00000000000..e4838b196a0 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/MqttMsgId.java @@ -0,0 +1,93 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.session.infly; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class MqttMsgId { + + private static final int MAX_MSG_ID = 65535; + private static final int MIN_MSG_ID = 1; + + private static final int ID_POOL_SIZE = 8192; + private static final List ID_POOL = new ArrayList<>(ID_POOL_SIZE); + + @PostConstruct + public void init() { + for (int i = 0; i < ID_POOL_SIZE; i++) { + ID_POOL.add(new MsgIdEntry()); + } + } + + class MsgIdEntry { + private int nextMsgId = MIN_MSG_ID - 1; + private Map inUseMsgIds = new ConcurrentHashMap<>(); + } + + private MsgIdEntry hashMsgID(String clientId) { + int hashCode = clientId.hashCode(); + if (hashCode < 0) { + hashCode *= -1; + } + return ID_POOL.get(hashCode % ID_POOL_SIZE); + } + + public int nextId(String clientId) { + MsgIdEntry msgIdEntry = hashMsgID(clientId); + synchronized (msgIdEntry) { + int startingMessageId = msgIdEntry.nextMsgId; + int loopCount = 0; + int maxLoopCount = 2; + do { + msgIdEntry.nextMsgId++; + if (msgIdEntry.nextMsgId > MAX_MSG_ID) { + msgIdEntry.nextMsgId = MIN_MSG_ID; + } + if (msgIdEntry.nextMsgId == startingMessageId) { + loopCount++; + if (loopCount >= maxLoopCount) { + msgIdEntry.nextMsgId++; + break; + } + } + } while (msgIdEntry.inUseMsgIds.containsKey(new Integer(msgIdEntry.nextMsgId))); + Integer id = new Integer(msgIdEntry.nextMsgId); + msgIdEntry.inUseMsgIds.put(id, id); + return msgIdEntry.nextMsgId; + } + } + + public void releaseId(int msgId, String clientId) { + if (StringUtils.isBlank(clientId)) { + return; + } + MsgIdEntry msgIdEntry = hashMsgID(clientId); + msgIdEntry.inUseMsgIds.remove(msgId); + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/PushAction.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/PushAction.java new file mode 100644 index 00000000000..fb3776a343e --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/PushAction.java @@ -0,0 +1,187 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.session.infly; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.common.util.MessageUtil; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + + +@Component +public class PushAction { + private static Logger logger = LoggerFactory.getLogger(PushAction.class); + + @Resource + private MqttMsgId mqttMsgId; + + @Resource + private RetryDriver retryDriver; + + @Resource + private InFlyCache inFlyCache; + + @Resource + private ConnectConf connectConf; + + + public void messageArrive(Session session, Subscription subscription, Queue queue) { + if (session == null) { + return; + } + if (!connectConf.isOrder()) { + List list = session.pendMessageList(subscription, queue); + if (list != null && !list.isEmpty()) { + for (Message message : list) { + message.setAck(0); + push(message, subscription, session, queue); + } + } + return; + } + if (retryDriver.needRetryBefore(subscription, queue, session)) { + return; + } + Message message = session.nextSendMessageByOrder(subscription, queue); + if (message != null) { + push(message, subscription, session, queue); + } + } + + public void push(Message message, Subscription subscription, Session session, Queue queue) { + String clientId = session.getClientId(); + int mqttId = mqttMsgId.nextId(clientId); + inFlyCache.getPendingDownCache().put(session.getChannelId(), mqttId, subscription, queue, message); + try { + if (session.isClean()) { + if (message.getStoreTimestamp() > 0 && + message.getStoreTimestamp() < session.getStartTime()) { + logger.warn("old msg:{},{},{},{}", session.getClientId(), message.getMsgId(), + message.getStoreTimestamp(), session.getStartTime()); + rollNext(session, mqttId); + return; + } + } + } catch (Exception e) { + logger.error("", e); + } + int qos = subscription.getQos(); + if (subscription.isP2p() && message.qos() != null) { + qos = message.qos(); + } + if (qos == 0) { + write(session, message, mqttId, 0, subscription); + rollNextByAck(session, mqttId); + } else { + retryDriver.mountPublish(mqttId, message, subscription.getQos(), ChannelInfo.getId(session.getChannel()), subscription); + write(session, message, mqttId, qos, subscription); + } + } + + public void write(Session session, Message message, int mqttId, int qos, Subscription subscription) { + Channel channel = session.getChannel(); + String owner = ChannelInfo.getOwner(channel); + String clientId = session.getClientId(); + String topicName = message.getOriginTopic(); + String mqttRealTopic = message.getUserProperty(Message.extPropertyMqttRealTopic); + if (StringUtils.isNotBlank(mqttRealTopic)) { + topicName = mqttRealTopic; + } + if (StringUtils.isBlank(topicName)) { + topicName = message.getFirstTopic(); + } + boolean isP2P = TopicUtils.isP2P(TopicUtils.decode(topicName).getSecondTopic()); + if (!channel.isWritable()) { + logger.error("UnWritable:{}", clientId); + return; + } + Object data = MessageUtil.toMqttMessage(topicName, message.getPayload(), qos, mqttId); + ChannelFuture writeFuture = session.getChannel().writeAndFlush(data); + int bodySize = message.getPayload() != null ? message.getPayload().length : 0; + writeFuture.addListener((ChannelFutureListener) future -> { + if (subscription.isRetry()) { + message.setRetry(message.getRetry() + 1); + logger.warn("retryPush:{},{},{}", session.getClientId(), message.getMsgId(), message.getRetry()); + } + }); + } + + public void rollNextByAck(Session session, int mqttId) { + InFlyCache.PendingDown pendingDown = inFlyCache.getPendingDownCache().get(session.getChannelId(), mqttId); + if (pendingDown == null) { + return; + } + rollNext(session, mqttId); + } + + public void rollNext(Session session, int mqttId) { + if (session == null || session.isDestroyed()) { + return; + } + mqttMsgId.releaseId(mqttId, session.getClientId()); + InFlyCache.PendingDown pendingDown = inFlyCache.getPendingDownCache().remove(session.getChannelId(), mqttId); + if (pendingDown == null) { + return; + } + _rollNext(session, pendingDown); + } + + public void rollNextNoWaitRetry(Session session, int mqttId) { + if (session == null || session.isDestroyed()) { + return; + } + InFlyCache.PendingDown pendingDown = inFlyCache.getPendingDownCache().get(session.getChannelId(), mqttId); + if (pendingDown == null) { + return; + } + _rollNext(session, pendingDown); + } + + public void _rollNext(Session session, InFlyCache.PendingDown pendingDown) { + Subscription subscription = pendingDown.getSubscription(); + Queue pendingQueue = pendingDown.getQueue(); + long pendingDownSeqId = pendingDown.getSeqId(); + + if (!connectConf.isOrder()) { + session.ack(subscription, pendingQueue, pendingDownSeqId); + return; + } + + Message nextSendOne = session.rollNext(subscription, pendingQueue, pendingDownSeqId); + if (nextSendOne != null) { + push(nextSendOne, subscription, session, pendingQueue); + } + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/RetryDriver.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/RetryDriver.java new file mode 100644 index 00000000000..d4c3ae622a7 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/RetryDriver.java @@ -0,0 +1,327 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.session.infly; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalListener; +import io.netty.handler.codec.mqtt.*; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.StoreResult; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.session.QueueFresh; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + + +@Component +public class RetryDriver { + private static Logger logger = LoggerFactory.getLogger(RetryDriver.class); + + @Resource + private InFlyCache inFlyCache; + + @Resource + private MqttMsgId mqttMsgId; + + @Resource + private SessionLoop sessionLoop; + + @Resource + private PushAction pushAction; + + @Resource + private ChannelManager channelManager; + + @Resource + private ConnectConf connectConf; + + @Resource + private LmqQueueStore lmqQueueStore; + + @Resource + private QueueFresh queueFresh; + + private Cache retryCache; + private final int MAX_CACHE = 50000; + private Map> sessionNoWaitRetryMsgMap = new ConcurrentHashMap<>(16); + private ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(2, + new ThreadFactoryImpl("retry_msg_thread_")); + + @PostConstruct + public void init() { + retryCache = Caffeine.newBuilder().maximumSize(MAX_CACHE).removalListener((RemovalListener) (key, value, cause) -> { + if (value == null || key == null) { + return; + } + if (cause.wasEvicted()) { + saveRetryQueue(key, value); + } + }).build(); + + scheduler.scheduleWithFixedDelay(() -> doRetryCache(), 3, connectConf.getRetryIntervalSeconds(), TimeUnit.SECONDS); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + Map map = retryCache.asMap(); + if (map == null) { + return; + } + for (Map.Entry entry : map.entrySet()) { + saveRetryQueue(entry.getKey(), entry.getValue()); + } + })); + } + + public void unloadSession(Session session) { + if (session == null) { + return; + } + Map map = sessionNoWaitRetryMsgMap.remove(session.getChannelId()); + if (map == null || map.isEmpty()) { + return; + } + for (Map.Entry entry : map.entrySet()) { + String cacheKey = buildKey(entry.getKey(), session.getChannelId()); + retryCache.invalidate(cacheKey); + RetryMessage retryMessage = entry.getValue(); + saveRetryQueue(cacheKey, retryMessage); + } + } + + private void saveRetryQueue(String key, RetryMessage retryMessage) { + Message message = retryMessage.message.copy(); + message.setFirstTopic(lmqQueueStore.getClientRetryTopic()); + Session session = retryMessage.session; + int mqttMsgId = retryMessage.mqttMsgId; + String clientId = session.getClientId(); + if (message.getRetry() >= connectConf.getMaxRetryTime()) { + pushAction.rollNext(session, retryMessage.mqttMsgId); + return; + } + String retryQueue = Subscription.newRetrySubscription(clientId).toQueueName(); + CompletableFuture result = lmqQueueStore.putMessage(new HashSet<>(Arrays.asList(retryQueue)), message); + result.whenComplete((storeResult, throwable) -> { + if (throwable != null) { + retryCache.put(key, retryMessage); + return; + } + long queueId = storeResult.getQueue().getQueueId(); + String brokerName = storeResult.getQueue().getBrokerName(); + pushAction.rollNext(session, mqttMsgId); + scheduler.schedule(() -> { + Subscription subscription = Subscription.newRetrySubscription(clientId); + List sessionList = sessionLoop.getSessionList(clientId); + if (sessionList != null) { + for (Session eachSession : sessionList) { + Set set = queueFresh.freshQueue(eachSession, subscription); + if (set == null || set.isEmpty()) { + continue; + } + for (Queue queue : set) { + if (Objects.equals(queue.getBrokerName(), brokerName)) { + sessionLoop.notifyPullMessage(eachSession, subscription, queue); + } + } + } + } + }, 3, TimeUnit.SECONDS); + }); + } + + private void doRetryCache() { + try { + for (Map.Entry entry : retryCache.asMap().entrySet()) { + try { + RetryMessage retryMessage = entry.getValue(); + Message message = retryMessage.message; + Session session = retryMessage.session; + int mqttMsgId = retryMessage.mqttMsgId; + if (System.currentTimeMillis() - retryMessage.timestamp < 3000) { + continue; + } + if (MqttMessageType.PUBLISH.equals(retryMessage.mqttMessageType)) { + if (session == null || session.isDestroyed()) { + cleanRetryMessage(mqttMsgId, session.getChannelId()); + continue; + } + if (retryMessage.mountTimeout()) { + saveRetryQueue(entry.getKey(), retryMessage); + cleanRetryMessage(mqttMsgId, session.getChannelId()); + continue; + } + pushAction.write(session, message, mqttMsgId, retryMessage.qos, retryMessage.subscription); + retryMessage.timestamp = System.currentTimeMillis(); + retryMessage.localRetryTime++; + } else if (MqttMessageType.PUBREL.equals(retryMessage.mqttMessageType)) { + if (session == null || session.isDestroyed() || retryMessage.mountRelTimeout()) { + retryCache.invalidate(entry.getKey()); + logger.error("failed to retry pub rel more times,{},{}", session.getClientId(), mqttMsgId); + pushAction.rollNextByAck(session, mqttMsgId); + continue; + } + MqttFixedHeader pubRelMqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBREL, false, + MqttQoS.valueOf(retryMessage.qos), false, 0); + MqttMessage pubRelMqttMessage = new MqttMessage(pubRelMqttFixedHeader, + MqttMessageIdVariableHeader.from(mqttMsgId)); + session.getChannel().writeAndFlush(pubRelMqttMessage); + retryMessage.localRetryTime++; + retryMessage.timestamp = System.currentTimeMillis(); + logger.warn("retryPubRel:{},{}", session.getClientId(), mqttMsgId); + } else { + logger.error("error retry message type:{}", retryMessage.mqttMessageType); + } + } catch (Exception e) { + logger.error("", e); + } + } + } catch (Exception e) { + logger.error("", e); + } + } + + public void mountPublish(int mqttMsgId, Message message, int qos, String channelId, Subscription subscription) { + Session session = sessionLoop.getSession(channelId); + if (session == null) { + return; + } + + RetryMessage retryMessage = new RetryMessage(session, message, qos, mqttMsgId, MqttMessageType.PUBLISH, subscription); + retryCache.put(buildKey(mqttMsgId, channelId), retryMessage); + Map noWaitRetryMsgMap = sessionNoWaitRetryMsgMap.get(channelId); + if (noWaitRetryMsgMap == null) { + noWaitRetryMsgMap = new ConcurrentHashMap<>(2); + sessionNoWaitRetryMsgMap.putIfAbsent(channelId, noWaitRetryMsgMap); + } + + if (!subscription.isRetry() && + noWaitRetryMsgMap.size() < connectConf.getSizeOfNotRollWhenAckSlow()) { + noWaitRetryMsgMap.put(mqttMsgId, retryMessage); + pushAction.rollNextNoWaitRetry(session, mqttMsgId); + } + } + + private RetryMessage cleanRetryMessage(int mqttMsgId, String channelId) { + Map retryMsgMap = sessionNoWaitRetryMsgMap.get(channelId); + if (retryMsgMap != null) { + retryMsgMap.remove(mqttMsgId); + } + String key = buildKey(mqttMsgId, channelId); + return unMount(key); + } + + public void mountPubRel(int mqttMsgId, String channelId) { + Session session = sessionLoop.getSession(channelId); + if (session == null) { + return; + } + RetryMessage retryMessage = new RetryMessage(session, null, MqttQoS.AT_LEAST_ONCE.value(), mqttMsgId, + MqttMessageType.PUBREL, null); + retryCache.put(buildKey(mqttMsgId, channelId), retryMessage); + } + + public RetryMessage unMountPublish(int mqttMsgId, String channelId) { + RetryMessage retryMessage = cleanRetryMessage(mqttMsgId, channelId); + return retryMessage; + } + + public RetryMessage unMountPubRel(int mqttMsgId, String channelId) { + String key = buildKey(mqttMsgId, channelId); + return unMount(key); + } + + private RetryMessage unMount(String key) { + RetryMessage message = retryCache.getIfPresent(key); + if (message != null) { + retryCache.invalidate(key); + } + return message; + } + + public boolean needRetryBefore(Subscription subscription, Queue queue, Session session) { + Map pendingDowns = inFlyCache.getPendingDownCache().all( + session.getChannelId()); + if (pendingDowns == null || pendingDowns.isEmpty()) { + return false; + } + InFlyCache.PendingDown pendingDown = null; + for (Map.Entry entry : pendingDowns.entrySet()) { + InFlyCache.PendingDown each = entry.getValue(); + if (each.getSubscription().equals(subscription) && each.getQueue().equals(queue)) { + pendingDown = each; + break; + } + } + return pendingDown != null; + } + + private String buildKey(int mqttMsgId, String channelId) { + StringBuilder sb = new StringBuilder(); + sb.append(String.valueOf(mqttMsgId)); + sb.append("_"); + sb.append(channelId); + return sb.toString(); + } + + public class RetryMessage { + private Session session; + private Message message; + private Subscription subscription; + private int qos; + private int mqttMsgId; + private MqttMessageType mqttMessageType; + private int localRetryTime = 0; + private static final int MAX_LOCAL_RETRY = 1; + private long timestamp = System.currentTimeMillis(); + + public RetryMessage(Session session, Message message, int qos, int mqttMsgId, MqttMessageType mqttMessageType, Subscription subscription) { + this.session = session; + this.message = message; + this.qos = qos; + this.mqttMsgId = mqttMsgId; + this.mqttMessageType = mqttMessageType; + this.subscription = subscription; + } + + private boolean mountTimeout() { + return localRetryTime >= MAX_LOCAL_RETRY; + } + + private boolean mountRelTimeout() { + return localRetryTime >= 3; + } + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/PullResultStatus.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/PullResultStatus.java new file mode 100644 index 00000000000..b97f5cbb7af --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/PullResultStatus.java @@ -0,0 +1,26 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.session.loop; + + +public enum PullResultStatus { + DONE, + LATER +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/QueueCache.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/QueueCache.java new file mode 100644 index 00000000000..ebb3385e289 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/QueueCache.java @@ -0,0 +1,318 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.session.loop; + + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.*; +import org.apache.rocketmq.mqtt.common.util.StatUtil; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import static org.apache.rocketmq.mqtt.cs.session.loop.PullResultStatus.DONE; +import static org.apache.rocketmq.mqtt.cs.session.loop.PullResultStatus.LATER; + + +@Component +public class QueueCache { + private static Logger logger = LoggerFactory.getLogger(QueueCache.class); + + @Resource + private ConnectConf connectConf; + + @Resource + private LmqQueueStore lmqQueueStore; + + private ScheduledThreadPoolExecutor loadCacheService = new ScheduledThreadPoolExecutor(1, + new ThreadFactoryImpl("loadCache_")); + + private AtomicLong rid = new AtomicLong(); + private Map loadEvent = new ConcurrentHashMap<>(); + private Map loadStatus = new ConcurrentHashMap<>(); + + private Cache cache = Caffeine.newBuilder() + .expireAfterAccess(10, TimeUnit.MINUTES) + .maximumSize(1_000) + .build(); + + + @PostConstruct + public void init() { + loadCacheService.scheduleWithFixedDelay(() -> { + for (Map.Entry entry : loadEvent.entrySet()) { + Queue queue = entry.getKey(); + QueueEvent event = entry.getValue(); + if (Boolean.TRUE.equals(loadStatus.get(queue))) { + continue; + } + CacheEntry cacheEntry = cache.getIfPresent(queue); + if (cacheEntry == null) { + cacheEntry = new CacheEntry(); + cache.put(queue, cacheEntry); + } + if (CollectionUtils.isEmpty(cacheEntry.messageList)) { + loadCache(true, queue.toFirstTopic(), queue, null, 1, event); + continue; + } + QueueOffset queueOffset = new QueueOffset(); + Message lastMessage = cacheEntry.messageList.get(cacheEntry.messageList.size() - 1); + queueOffset.setOffset(lastMessage.getOffset() + 1); + loadCache(false, queue.toFirstTopic(), queue, queueOffset, connectConf.getPullBatchSize(), event); + } + }, 10, 10, TimeUnit.MILLISECONDS); + } + + public void refreshCache(Pair pair) { + Queue queue = pair.getLeft(); + if (queue == null) { + return; + } + if (queue.isP2p() || queue.isRetry()) { + return; + } + addLoadEvent(queue, pair.getRight()); + } + + class QueueEvent { + long id; + Session session; + + public QueueEvent(long id, Session session) { + this.id = id; + this.session = session; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + QueueEvent that = (QueueEvent) o; + return id == that.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + } + + private void addLoadEvent(Queue queue, Session session) { + loadEvent.put(queue, new QueueEvent(rid.incrementAndGet(), session)); + CacheEntry cacheEntry = cache.getIfPresent(queue); + if (cacheEntry == null) { + cacheEntry = new CacheEntry(); + cache.put(queue, cacheEntry); + } + } + + private void callbackResult(CompletableFuture pullResult, CompletableFuture callBackResult) { + pullResult.whenComplete((pullResult1, throwable) -> { + if (throwable != null) { + callBackResult.completeExceptionally(throwable); + } else { + callBackResult.complete(pullResult1); + } + }); + } + + private String toFirstTopic(Subscription subscription) { + String firstTopic = subscription.toFirstTopic(); + if (subscription.isRetry()) { + firstTopic = lmqQueueStore.getClientRetryTopic(); + } + if (subscription.isP2p()) { + if (StringUtils.isNotBlank(lmqQueueStore.getClientP2pTopic())) { + firstTopic = lmqQueueStore.getClientP2pTopic(); + } else { + firstTopic = lmqQueueStore.getClientRetryTopic(); + } + } + return firstTopic; + } + + public PullResultStatus pullMessage(Session session, Subscription subscription, Queue queue, + QueueOffset queueOffset, int count, + CompletableFuture callBackResult) { + if (subscription.isP2p() || subscription.isRetry()) { + StatUtil.addPv("NotPullCache", 1); + CompletableFuture pullResult = lmqQueueStore.pullMessage(toFirstTopic(subscription), queue, queueOffset, count); + callbackResult(pullResult, callBackResult); + return DONE; + } + CacheEntry cacheEntry = cache.getIfPresent(queue); + if (cacheEntry == null) { + StatUtil.addPv("NoPullCache", 1); + CompletableFuture pullResult = lmqQueueStore.pullMessage(toFirstTopic(subscription), queue, queueOffset, count); + callbackResult(pullResult, callBackResult); + return DONE; + } + if (cacheEntry.loading.get()) { + if (System.currentTimeMillis() - cacheEntry.startLoadingT > 1000) { + StatUtil.addPv("LoadPullCacheTimeout", 1); + CompletableFuture pullResult = lmqQueueStore.pullMessage(toFirstTopic(subscription), queue, queueOffset, count); + callbackResult(pullResult, callBackResult); + return DONE; + } + return LATER; + } + + List cacheMsgList = cacheEntry.messageList; + if (cacheMsgList.isEmpty()) { + if (loadEvent.get(queue) != null) { + StatUtil.addPv("EmptyPullCacheLATER", 1); + return LATER; + } + StatUtil.addPv("EmptyPullCache", 1); + CompletableFuture pullResult = lmqQueueStore.pullMessage(toFirstTopic(subscription), queue, queueOffset, count); + callbackResult(pullResult, callBackResult); + return DONE; + } + + if (queueOffset.getOffset() < cacheMsgList.get(0).getOffset()) { + StatUtil.addPv("OutPullCache", 1); + CompletableFuture pullResult = lmqQueueStore.pullMessage(toFirstTopic(subscription), queue, queueOffset, count); + callbackResult(pullResult, callBackResult); + return DONE; + } + List resultMsgs = new ArrayList<>(); + synchronized (cacheEntry) { + for (Message message : cacheMsgList) { + if (message.getOffset() >= queueOffset.getOffset()) { + resultMsgs.add(message); + } + if (resultMsgs.size() >= count) { + break; + } + } + } + if (resultMsgs.isEmpty()) { + if (loadEvent.get(queue) != null) { + StatUtil.addPv("PullCacheLATER", 1); + return LATER; + } + StatUtil.addPv("OutPullCache2", 1); + CompletableFuture pullResult = lmqQueueStore.pullMessage(toFirstTopic(subscription), queue, queueOffset, count); + callbackResult(pullResult, callBackResult); + return DONE; + } + PullResult pullResult = new PullResult(); + pullResult.setMessageList(resultMsgs); + callBackResult.complete(pullResult); + StatUtil.addPv("PullFromCache", 1); + if (loadEvent.get(queue) != null) { + return LATER; + } + return DONE; + } + + private void loadCache(boolean isFirst, String firstTopic, Queue queue, QueueOffset queueOffset, int count, + QueueEvent event) { + loadStatus.put(queue, true); + CacheEntry cacheEntry = cache.getIfPresent(queue); + if (cacheEntry == null) { + cacheEntry = new CacheEntry(); + cache.put(queue, cacheEntry); + } + cacheEntry.startLoad(); + CacheEntry finalCacheEntry = cacheEntry; + CompletableFuture result; + if (isFirst) { + result = lmqQueueStore.pullLastMessages(firstTopic, queue, count); + } else { + result = lmqQueueStore.pullMessage(firstTopic, queue, queueOffset, count); + } + result.whenComplete((pullResult, throwable) -> { + if (throwable != null) { + logger.error("", throwable); + loadEvent.remove(queue, event); + loadStatus.remove(queue); + finalCacheEntry.endLoad(); + addLoadEvent(queue, event.session); + return; + } + try { + if (pullResult != null && !CollectionUtils.isEmpty(pullResult.getMessageList())) { + synchronized (finalCacheEntry) { + finalCacheEntry.messageList.addAll(pullResult.getMessageList()); + if (isFirst) { + Collections.sort(finalCacheEntry.messageList, Comparator.comparingLong(Message::getOffset)); + } + int overNum = finalCacheEntry.messageList.size() - connectConf.getQueueCacheSize(); + for (int i = 0; i < overNum; i++) { + finalCacheEntry.messageList.remove(0); + } + } + if (pullResult.getMessageList().size() >= count && !isFirst) { + addLoadEvent(queue, event.session); + return; + } + } + } catch (Exception e) { + logger.error("loadCache failed {}", queue.getQueueName(), e); + addLoadEvent(queue, event.session); + } finally { + loadEvent.remove(queue, event); + loadStatus.remove(queue); + finalCacheEntry.endLoad(); + } + }); + } + + class CacheEntry { + private AtomicBoolean loading = new AtomicBoolean(false); + private List messageList = new ArrayList<>(); + private volatile long startLoadingT = System.currentTimeMillis(); + + private void startLoad() { + if (loading.compareAndSet(false, true)) { + startLoadingT = System.currentTimeMillis(); + } + } + + private void endLoad() { + loading.compareAndSet(true, false); + } + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/SessionLoop.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/SessionLoop.java new file mode 100644 index 00000000000..ddcab255312 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/SessionLoop.java @@ -0,0 +1,100 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.session.loop; + + +import io.netty.channel.Channel; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.session.Session; + +import java.util.List; +import java.util.Set; + + +public interface SessionLoop { + + /** + * set ChannelManager + * + * @param channelManager + */ + void setChannelManager(ChannelManager channelManager); + + /** + * load one mqtt session + * + * @param clientId + * @param channel + */ + void loadSession(String clientId, Channel channel); + + /** + * unload one mqtt session + * + * @param clientId + * @param channelId + * @return + */ + Session unloadSession(String clientId, String channelId); + + /** + * get the session by channelId + * + * @param channelId + * @return + */ + Session getSession(String channelId); + + /** + * get session list by clientId + * + * @param clientId + * @return + */ + List getSessionList(String clientId); + + /** + * add subscription + * + * @param channelId + * @param subscriptions + */ + void addSubscription(String channelId, Set subscriptions); + + /** + * remove subscription + * + * @param channelId + * @param subscriptions + */ + void removeSubscription(String channelId, Set subscriptions); + + /** + * notify to pull message from queue + * + * @param session + * @param subscription + * @param queue + */ + void notifyPullMessage(Session session, Subscription subscription, Queue queue); + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/SessionLoopImpl.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/SessionLoopImpl.java new file mode 100644 index 00000000000..00a938a6e09 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/SessionLoopImpl.java @@ -0,0 +1,536 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.session.loop; + +import com.alibaba.fastjson.JSONObject; +import io.netty.channel.Channel; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.mqtt.common.facade.LmqOffsetStore; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.model.PullResult; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.QueueOffset; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.session.QueueFresh; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.infly.InFlyCache; +import org.apache.rocketmq.mqtt.cs.session.infly.PushAction; +import org.apache.rocketmq.mqtt.cs.session.match.MatchAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + + +@Component +public class SessionLoopImpl implements SessionLoop { + private static Logger logger = LoggerFactory.getLogger(SessionLoopImpl.class); + + @Resource + private PushAction pushAction; + + @Resource + private MatchAction matchAction; + + @Resource + private ConnectConf connectConf; + + @Resource + private InFlyCache inFlyCache; + + @Resource + private QueueCache queueCache; + + @Resource + private LmqQueueStore lmqQueueStore; + + @Resource + private LmqOffsetStore lmqOffsetStore; + + @Resource + private QueueFresh queueFresh; + + private ChannelManager channelManager; + private ScheduledThreadPoolExecutor pullService; + private ScheduledThreadPoolExecutor scheduler; + private ScheduledThreadPoolExecutor persistOffsetScheduler; + + /** + * channelId->session + */ + private Map sessionMap = new ConcurrentHashMap<>(1024); + private Map> clientMap = new ConcurrentHashMap<>(1024); + private Map pullEventMap = new ConcurrentHashMap<>(1024); + private Map pullStatus = new ConcurrentHashMap<>(1024); + + private AtomicLong rid = new AtomicLong(); + private long pullIntervalMillis = 10; + + @PostConstruct + public void init() { + pullService = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("pull_message_thread_")); + scheduler = new ScheduledThreadPoolExecutor(2, new ThreadFactoryImpl("loop_scheduler_")); + persistOffsetScheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("persistOffset_scheduler_")); + persistOffsetScheduler.scheduleWithFixedDelay(() -> persistAllOffset(true), 5000, 5000, TimeUnit.MILLISECONDS); + pullService.scheduleWithFixedDelay(() -> pullLoop(), pullIntervalMillis, pullIntervalMillis, TimeUnit.MILLISECONDS); + } + + private void pullLoop() { + try { + for (Map.Entry entry : pullEventMap.entrySet()) { + PullEvent pullEvent = entry.getValue(); + Session session = pullEvent.session; + if (!session.getChannel().isActive()) { + pullStatus.remove(eventQueueKey(session, pullEvent.queue)); + pullEventMap.remove(entry.getKey()); + continue; + } + if (Boolean.TRUE.equals(pullStatus.get(eventQueueKey(session, pullEvent.queue)))) { + continue; + } + if (!session.getChannel().isWritable()) { + continue; + } + doPull(pullEvent); + } + } catch (Exception e) { + logger.error("", e); + } + } + + @Override + public void setChannelManager(ChannelManager channelManager) { + this.channelManager = channelManager; + } + + @Override + public void loadSession(String clientId, Channel channel) { + if (StringUtils.isBlank(clientId)) { + return; + } + if (!channel.isActive()) { + return; + } + String channelId = ChannelInfo.getId(channel); + if (sessionMap.containsKey(channelId)) { + return; + } + Session session = new Session(); + session.setClientId(clientId); + session.setChannelId(channelId); + session.setChannel(channel); + addSubscriptionAndInit(session, + new HashSet<>( + Arrays.asList(Subscription.newP2pSubscription(clientId), Subscription.newRetrySubscription(clientId))), + ChannelInfo.getFuture(channel, ChannelInfo.FUTURE_CONNECT)); + synchronized (this) { + sessionMap.put(channelId, session); + if (!clientMap.containsKey(clientId)) { + clientMap.putIfAbsent(clientId, new ConcurrentHashMap<>(2)); + } + clientMap.get(clientId).put(channelId, session); + } + if (!channel.isActive()) { + unloadSession(clientId, channelId); + return; + } + if (!session.isClean()) { + notifyPullMessage(session, null, null); + } + } + + @Override + public Session unloadSession(String clientId, String channelId) { + Session session = null; + try { + synchronized (this) { + session = sessionMap.remove(channelId); + if (clientId == null) { + if (session != null) { + clientId = session.getClientId(); + } + } + if (clientId != null && clientMap.containsKey(clientId)) { + clientMap.get(clientId).remove(channelId); + if (clientMap.get(clientId).isEmpty()) { + clientMap.remove(clientId); + } + } + } + inFlyCache.cleanResource(clientId, channelId); + if (session != null) { + matchAction.removeSubscription(session, session.subscriptionSnapshot()); + persistOffset(session); + } + } catch (Exception e) { + logger.error("unloadSession fail:{},{}", clientId, channelId, e); + } finally { + if (session != null) { + session.destroy(); + } + } + return session; + } + + @Override + public Session getSession(String channelId) { + return sessionMap.get(channelId); + } + + @Override + public List getSessionList(String clientId) { + List list = new ArrayList<>(); + Map sessions = clientMap.get(clientId); + if (sessions != null && !sessions.isEmpty()) { + for (Session session : sessions.values()) { + if (!session.isDestroyed()) { + list.add(session); + } else { + logger.error("the session was destroyed,{}", clientId); + sessions.remove(session.getChannelId()); + } + } + } + return list; + } + + @Override + public void addSubscription(String channelId, Set subscriptions) { + if (subscriptions == null || subscriptions.isEmpty()) { + return; + } + Session session = getSession(channelId); + if (session == null) { + return; + } + addSubscriptionAndInit(session, subscriptions, + ChannelInfo.getFuture(session.getChannel(), ChannelInfo.FUTURE_SUBSCRIBE)); + matchAction.addSubscription(session, subscriptions); + + if (!session.isClean()) { + for (Subscription subscription : subscriptions) { + notifyPullMessage(session, subscription, null); + } + } + } + + @Override + public void removeSubscription(String channelId, Set subscriptions) { + if (subscriptions == null || subscriptions.isEmpty()) { + return; + } + Session session = getSession(channelId); + if (session == null) { + return; + } + for (Subscription subscription : subscriptions) { + session.removeSubscription(subscription); + } + matchAction.removeSubscription(session, subscriptions); + } + + private void addSubscriptionAndInit(Session session, Set subscriptions, + CompletableFuture future) { + if (session == null) { + return; + } + if (subscriptions == null) { + return; + } + session.addSubscription(subscriptions); + AtomicInteger result = new AtomicInteger(subscriptions.size()); + for (Subscription subscription : subscriptions) { + queueFresh.freshQueue(session, subscription); + Map queueOffsets = session.getQueueOffset(subscription); + if (queueOffsets != null) { + for (Map.Entry entry : queueOffsets.entrySet()) { + initOffset(session, subscription, entry.getKey(), entry.getValue(), future, result); + } + } + } + } + + private void futureDone(CompletableFuture future, AtomicInteger result) { + if (future == null) { + return; + } + if (result == null) { + return; + } + if (result.decrementAndGet() <= 0) { + future.complete(null); + } + } + + private void initOffset(Session session, Subscription subscription, Queue queue, QueueOffset queueOffset, + CompletableFuture future, AtomicInteger result) { + if (queueOffset.isInitialized()) { + futureDone(future, result); + return; + } + if (queueOffset.isInitializing()) { + return; + } + queueOffset.setInitializing(); + CompletableFuture queryResult = lmqQueueStore.queryQueueMaxOffset(queue); + queryResult.whenComplete((maxOffset, throwable) -> { + if (throwable != null) { + logger.error("queryQueueMaxId onException {}", queue.getQueueName(), throwable); + QueueOffset _queueOffset = session.getQueueOffset(subscription, queue); + if (_queueOffset != null) { + if (!_queueOffset.isInitialized()) { + _queueOffset.setOffset(Long.MAX_VALUE); + } + _queueOffset.setInitialized(); + } + futureDone(future, result); + return; + } + QueueOffset _queueOffset = session.getQueueOffset(subscription, queue); + if (_queueOffset != null) { + if (!_queueOffset.isInitialized()) { + _queueOffset.setOffset(maxOffset); + } + _queueOffset.setInitialized(); + } + futureDone(future, result); + }); + } + + @Override + public void notifyPullMessage(Session session, Subscription subscription, Queue queue) { + if (session == null || session.isDestroyed()) { + return; + } + if (queue != null) { + if (subscription == null) { + throw new RuntimeException( + "invalid notifyPullMessage, subscription is null, but queue is not null," + session.getClientId()); + } + queueFresh.freshQueue(session, subscription); + pullMessage(session, subscription, queue); + return; + } + for (Subscription each : session.subscriptionSnapshot()) { + if (subscription != null && !each.equals(subscription)) { + continue; + } + queueFresh.freshQueue(session, each); + Map queueOffsets = session.getQueueOffset(each); + if (queueOffsets != null) { + for (Map.Entry entry : queueOffsets.entrySet()) { + pullMessage(session, each, entry.getKey()); + } + } + } + } + + private String eventQueueKey(Session session, Queue queue) { + StringBuilder sb = new StringBuilder(); + sb.append(ChannelInfo.getId(session.getChannel())); + sb.append("-"); + sb.append(queue.getQueueId()); + sb.append("-"); + sb.append(queue.getQueueName()); + sb.append("-"); + sb.append(queue.getBrokerName()); + return sb.toString(); + } + + private boolean needLoadPersistedOffset(Session session, Subscription subscription, Queue queue) { + if (session.isClean()) { + return false; + } + Integer status = session.getLoadStatusMap().get(subscription); + if (status != null && status == 1) { + return false; + } + if (status != null && status == 0) { + return true; + } + session.getLoadStatusMap().put(subscription, 0); + CompletableFuture> result = lmqOffsetStore.getOffset(session.getClientId(), subscription); + result.whenComplete((offsetMap, throwable) -> { + if (throwable != null) { + // retry + scheduler.schedule(() -> { + session.getLoadStatusMap().put(subscription, -1); + pullMessage(session, subscription, queue); + }, 3, TimeUnit.SECONDS); + return; + } + session.addOffset(subscription.toQueueName(), offsetMap); + session.getLoadStatusMap().put(subscription, 1); + pullMessage(session, subscription, queue); + }); + return true; + } + + private void pullMessage(Session session, Subscription subscription, Queue queue) { + if (queue == null) { + return; + } + if (session == null || session.isDestroyed()) { + return; + } + if (needLoadPersistedOffset(session, subscription, queue)) { + return; + } + if (!session.sendingMessageIsEmpty(subscription, queue)) { + scheduler.schedule(() -> pullMessage(session, subscription, queue), pullIntervalMillis, TimeUnit.MILLISECONDS); + } else { + PullEvent pullEvent = new PullEvent(session, subscription, queue); + pullEventMap.put(eventQueueKey(session, queue), pullEvent); + } + } + + private void doPull(PullEvent pullEvent) { + Session session = pullEvent.session; + Subscription subscription = pullEvent.subscription; + Queue queue = pullEvent.queue; + QueueOffset queueOffset = session.getQueueOffset(subscription, queue); + if (session.isDestroyed() || queueOffset == null) { + clearPullStatus(session, queue, pullEvent); + return; + } + + if (!queueOffset.isInitialized()) { + initOffset(session, subscription, queue, queueOffset, null, null); + scheduler.schedule(() -> pullMessage(session, subscription, queue), pullIntervalMillis, TimeUnit.MILLISECONDS); + return; + } + + pullStatus.put(eventQueueKey(session, queue), true); + int count = session.getPullSize() > 0 ? session.getPullSize() : connectConf.getPullBatchSize(); + CompletableFuture result = new CompletableFuture<>(); + queueCache.pullMessage(session, subscription, queue, queueOffset, count, result); + result.whenComplete((pullResult, throwable) -> { + if (throwable != null) { + clearPullStatus(session, queue, pullEvent); + logger.error("{}", session.getClientId(), throwable); + if (session.isDestroyed()) { + return; + } + scheduler.schedule(() -> pullMessage(session, subscription, queue), 1, TimeUnit.SECONDS); + return; + } + try { + if (session.isDestroyed()) { + return; + } + if (PullResult.PULL_SUCCESS == pullResult.getCode()) { + if (pullResult.getMessageList() != null && + pullResult.getMessageList().size() >= count) { + scheduler.schedule(() -> pullMessage(session, subscription, queue), pullIntervalMillis, TimeUnit.MILLISECONDS); + } + boolean add = session.addSendingMessages(subscription, queue, pullResult.getMessageList()); + if (add) { + pushAction.messageArrive(session, subscription, queue); + } + } else if (PullResult.PULL_OFFSET_MOVED == pullResult.getCode()) { + queueOffset.setOffset(pullResult.getNextQueueOffset().getOffset()); + queueOffset.setOffset(pullResult.getNextQueueOffset().getOffset()); + session.markPersistOffsetFlag(true); + pullMessage(session, subscription, queue); + } else { + logger.error("response:{},{}", session.getClientId(), JSONObject.toJSONString(pullResult)); + } + } finally { + clearPullStatus(session, queue, pullEvent); + } + }); + + PullResultStatus pullResultStatus = queueCache.pullMessage(session, subscription, queue, queueOffset, count, result); + if (PullResultStatus.LATER.equals(pullResultStatus)) { + clearPullStatus(session, queue, pullEvent); + scheduler.schedule(() -> pullMessage(session, subscription, queue), pullIntervalMillis, TimeUnit.MILLISECONDS); + } + } + + private void clearPullStatus(Session session, Queue queue, PullEvent pullEvent) { + pullEventMap.remove(eventQueueKey(session, queue), pullEvent); + pullStatus.remove(eventQueueKey(session, queue)); + } + + private void persistAllOffset(boolean needSleep) { + try { + for (Session session : sessionMap.values()) { + if (persistOffset(session) && needSleep) { + Thread.sleep(5L); + } + } + } catch (Exception e) { + logger.error("", e); + } + } + + private boolean persistOffset(Session session) { + try { + if (!session.getPersistOffsetFlag()) { + return false; + } + lmqOffsetStore.save(session.getClientId(), session.offsetMapSnapshot()); + } catch (Exception e) { + logger.error("{}", session.getClientId(), e); + } + return true; + } + + class PullEvent { + private Session session; + private Subscription subscription; + private Queue queue; + private long id = rid.getAndIncrement(); + + public PullEvent(Session session, Subscription subscription, Queue queue) { + this.session = session; + this.subscription = subscription; + this.queue = queue; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PullEvent pullEvent = (PullEvent) o; + + return id == pullEvent.id; + } + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/match/MatchAction.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/match/MatchAction.java new file mode 100644 index 00000000000..471185d8f7d --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/match/MatchAction.java @@ -0,0 +1,161 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.session.match; + + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.rocketmq.mqtt.common.model.MqttTopic; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.common.model.Trie; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + + +@Component +public class MatchAction { + private static Logger logger = LoggerFactory.getLogger(MatchAction.class); + + @Resource + private SessionLoop sessionLoop; + + private Trie trie = new Trie<>(); + private ConcurrentMap> topicCache = new ConcurrentHashMap<>(16); + + + public Set> matchClients(String topic, String namespace) { + Set> result = new HashSet<>(); + MqttTopic mqttTopic = TopicUtils.decode(topic); + String secondTopic = TopicUtils.normalizeSecondTopic(mqttTopic.getSecondTopic()); + if (TopicUtils.isRetryTopic(topic)) { + String clientId = TopicUtils.getClientIdFromRetryTopic(topic); + List sessions = sessionLoop.getSessionList(clientId); + for (Session session : sessions) { + result.add(Pair.of(session, Subscription.newRetrySubscription(clientId))); + } + } else if (TopicUtils.isP2P(secondTopic)) { + String clientId = TopicUtils.getP2Peer(mqttTopic, namespace); + List sessions = sessionLoop.getSessionList(clientId); + for (Session session : sessions) { + result.add(Pair.of(session, Subscription.newP2pSubscription(clientId))); + } + } else if(TopicUtils.isP2pTopic(topic)){ + // may be produced by rmq + String clientId = TopicUtils.getClientIdFromP2pTopic(topic); + List sessions = sessionLoop.getSessionList(clientId); + for (Session session : sessions) { + result.add(Pair.of(session, Subscription.newP2pSubscription(clientId))); + } + } else { + Set channelIdSet = new HashSet<>(); + synchronized (topicCache) { + Set precises = topicCache.get(topic); + if (precises != null && !precises.isEmpty()) { + channelIdSet.addAll(precises); + } + } + Map map = trie.getNode(topic); + if (map != null && !map.isEmpty()) { + channelIdSet.addAll(map.keySet()); + } + + for (String channelId : channelIdSet) { + Session session = sessionLoop.getSession(channelId); + if (session == null) { + continue; + } + Set tmp = session.subscriptionSnapshot(); + if (tmp != null && !tmp.isEmpty()) { + for (Subscription subscription : tmp) { + if (TopicUtils.isMatch(topic, subscription.getTopicFilter())) { + result.add(Pair.of(session, subscription)); + } + } + } + } + } + return result; + } + + public void addSubscription(Session session, Set subscriptions) { + String channelId = session.getChannelId(); + if (channelId == null || subscriptions == null || subscriptions.isEmpty()) { + return; + } + for (Subscription subscription : subscriptions) { + if (subscription.isRetry() || subscription.isP2p()) { + continue; + } + String topicFilter = subscription.getTopicFilter(); + boolean isWildCard = TopicUtils.isWildCard(topicFilter); + if (isWildCard) { + trie.addNode(topicFilter, subscription.getQos(), channelId); + continue; + } + + synchronized (topicCache) { + if (!topicCache.containsKey(topicFilter)) { + topicCache.putIfAbsent(topicFilter, new HashSet<>()); + } + topicCache.get(topicFilter).add(channelId); + } + } + } + + public void removeSubscription(Session session, Set subscriptions) { + String channelId = session.getChannelId(); + if (channelId == null || subscriptions == null || subscriptions.isEmpty()) { + return; + } + for (Subscription subscription : subscriptions) { + if (subscription.isRetry() || subscription.isP2p()) { + continue; + } + String topicFilter = subscription.getTopicFilter(); + boolean isWildCard = TopicUtils.isWildCard(topicFilter); + if (isWildCard) { + trie.deleteNode(topicFilter, channelId); + continue; + } + + synchronized (topicCache) { + Set channelIdSet = topicCache.get(topicFilter); + if (channelIdSet != null) { + channelIdSet.remove(channelId); + if (channelIdSet.isEmpty()) { + topicCache.remove(topicFilter); + } + } + } + } + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/notify/MessageNotifyAction.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/notify/MessageNotifyAction.java new file mode 100644 index 00000000000..299abb76b0f --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/notify/MessageNotifyAction.java @@ -0,0 +1,88 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.session.notify; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.rocketmq.mqtt.common.model.MessageEvent; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.cs.session.QueueFresh; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.loop.QueueCache; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.apache.rocketmq.mqtt.cs.session.match.MatchAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +@Component +public class MessageNotifyAction { + private static Logger logger = LoggerFactory.getLogger(MessageNotifyAction.class); + + @Resource + private MatchAction matchAction; + + @Resource + private SessionLoop sessionLoop; + + @Resource + private QueueCache queueCache; + + @Resource + private QueueFresh queueFresh; + + public void notify(List events) { + if (events == null || events.isEmpty()) { + return; + } + for (MessageEvent event : events) { + Set> result = matchAction.matchClients( + TopicUtils.normalizeTopic(event.getPubTopic()), event.getNamespace()); + if (result == null || result.isEmpty()) { + continue; + } + for (Pair pair : result) { + Session session = pair.getLeft(); + Subscription subscription = pair.getRight(); + Set set = queueFresh.freshQueue(session, subscription); + if (set == null || set.isEmpty()) { + continue; + } + for (Queue queue : set) { + if (isTargetQueue(queue, event)) { + queueCache.refreshCache(Pair.of(queue, session)); + sessionLoop.notifyPullMessage(session, subscription, queue); + } + } + } + } + } + + private boolean isTargetQueue(Queue queue, MessageEvent event) { + return Objects.equals(queue.getBrokerName(), event.getBrokerName()); + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/MqttServer.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/MqttServer.java new file mode 100644 index 00000000000..1273989da90 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/MqttServer.java @@ -0,0 +1,132 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.starter; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.mqtt.MqttDecoder; +import io.netty.handler.codec.mqtt.MqttEncoder; +import io.netty.handler.stream.ChunkedWriteHandler; +import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.channel.ConnectHandler; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.protocol.mqtt.MqttPacketDispatcher; +import org.apache.rocketmq.mqtt.cs.protocol.ws.WebSocketServerHandler; +import org.apache.rocketmq.mqtt.cs.protocol.ws.WebsocketEncoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.net.InetSocketAddress; + +@Service +public class MqttServer { + private static Logger logger = LoggerFactory.getLogger(MqttServer.class); + + private ServerBootstrap serverBootstrap = new ServerBootstrap(); + private ServerBootstrap wsServerBootstrap = new ServerBootstrap(); + + @Resource + private ConnectHandler connectHandler; + + @Resource + private ConnectConf connectConf; + + @Resource + private MqttPacketDispatcher mqttPacketDispatcher; + + @Resource + private WebSocketServerHandler webSocketServerHandler; + + @Resource + private ChannelManager channelManager; + + @PostConstruct + public void init() throws Exception { + start(); + startWs(); + } + + private void start() { + int port = connectConf.getMqttPort(); + serverBootstrap + .group(new NioEventLoopGroup(connectConf.getNettySelectThreadNum()), new NioEventLoopGroup(connectConf.getNettyWorkerThreadNum())) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 8 * 1024) + .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) + .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK,new WriteBufferWaterMark(connectConf.getLowWater(), connectConf.getHighWater())) + .childOption(ChannelOption.TCP_NODELAY, true) + .localAddress(new InetSocketAddress(port)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast("connectHandler", connectHandler); + pipeline.addLast("decoder", new MqttDecoder(connectConf.getMaxPacketSizeInByte())); + pipeline.addLast("encoder", MqttEncoder.INSTANCE); + pipeline.addLast("dispatcher", mqttPacketDispatcher); + } + }); + serverBootstrap.bind(); + logger.warn("start mqtt server , port:{}", port); + } + + private void startWs() { + int port = connectConf.getMqttWsPort(); + wsServerBootstrap + .group(new NioEventLoopGroup(connectConf.getNettySelectThreadNum()), new NioEventLoopGroup(connectConf.getNettyWorkerThreadNum())) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 8 * 1024) + .option(ChannelOption.SO_KEEPALIVE, true) + .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) + .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK,new WriteBufferWaterMark(connectConf.getLowWater(), connectConf.getHighWater())) + .childOption(ChannelOption.TCP_NODELAY, true) + .localAddress(new InetSocketAddress(port)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast("connectHandler", connectHandler); + pipeline.addLast("http-codec", new HttpServerCodec(1024, 32 * 1024, connectConf.getMaxPacketSizeInByte() * 2, true)); + pipeline.addLast("aggregator", new HttpObjectAggregator(connectConf.getMaxPacketSizeInByte() * 2)); + pipeline.addLast("http-chunked", new ChunkedWriteHandler()); + pipeline.addLast("websocket-handler", webSocketServerHandler); + pipeline.addLast("websocket-encoder", new WebsocketEncoder()); + pipeline.addLast("decoder", new MqttDecoder(connectConf.getMaxPacketSizeInByte())); + pipeline.addLast("encoder", MqttEncoder.INSTANCE); + pipeline.addLast("dispatcher", mqttPacketDispatcher); + } + }); + wsServerBootstrap.bind(); + logger.warn("start mqtt ws server , port:{}", port); + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/RpcServer.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/RpcServer.java new file mode 100644 index 00000000000..bd4ba1b3d6c --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/RpcServer.java @@ -0,0 +1,65 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.starter; + + +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.protocol.rpc.RpcPacketDispatcher; +import org.apache.rocketmq.remoting.netty.NettyRemotingServer; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@Service +public class RpcServer { + private static Logger logger = LoggerFactory.getLogger(RpcServer.class); + + @Resource + private ConnectConf connectConf; + + @Resource + private RpcPacketDispatcher rpcPacketDispatcher; + + private NettyRemotingServer remotingServer; + private BlockingQueue csBridgeRpcQueue; + + @PostConstruct + public void start() { + NettyServerConfig nettyServerConfig = new NettyServerConfig(); + nettyServerConfig.setListenPort(connectConf.getRpcListenPort()); + remotingServer = new NettyRemotingServer(nettyServerConfig); + csBridgeRpcQueue = new LinkedBlockingQueue<>(10000); + ThreadPoolExecutor executor = new ThreadPoolExecutor(8, 16, 1, TimeUnit.MINUTES, + csBridgeRpcQueue, new ThreadFactoryImpl("Rpc_Server_Thread_")); + remotingServer.registerDefaultProcessor(rpcPacketDispatcher, executor); + remotingServer.start(); + logger.warn("start rpc server , port:{}", connectConf.getRpcListenPort()); + } + +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/Startup.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/Startup.java new file mode 100644 index 00000000000..2c74958a0eb --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/Startup.java @@ -0,0 +1,36 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.starter; + +import org.apache.rocketmq.client.log.ClientLogger; +import org.springframework.context.support.ClassPathXmlApplicationContext; + + +public class Startup { + + public static void main(String[] args) { + System.setProperty(ClientLogger.CLIENT_LOG_USESLF4J, "true"); + + ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml"); + + System.out.println("start rocketmq mqtt ..."); + } + +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestDefaultChannelManager.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestDefaultChannelManager.java new file mode 100644 index 00000000000..bb3acfbca67 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestDefaultChannelManager.java @@ -0,0 +1,60 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.test; + +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.Timeout; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.reflect.MethodUtils; +import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.channel.DefaultChannelManager; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.session.infly.RetryDriver; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.InvocationTargetException; + +import static org.mockito.Mockito.*; + +public class TestDefaultChannelManager { + + @Test + public void test() throws IllegalAccessException, InterruptedException, InvocationTargetException, NoSuchMethodException { + DefaultChannelManager defaultChannelManager = new DefaultChannelManager(); + SessionLoop sessionLoop = mock(SessionLoop.class); + FieldUtils.writeDeclaredField(defaultChannelManager, "sessionLoop", sessionLoop, true); + FieldUtils.writeDeclaredField(defaultChannelManager, "connectConf", mock(ConnectConf.class), true); + FieldUtils.writeDeclaredField(defaultChannelManager, "retryDriver", mock(RetryDriver.class), true); + FieldUtils.writeStaticField(DefaultChannelManager.class, "MinBlankChannelSeconds", 1, true); + defaultChannelManager.init(); + NioSocketChannel channel = spy(new NioSocketChannel()); + when(channel.isActive()).thenReturn(false); + ChannelInfo.setClientId(channel, "test"); + ChannelInfo.setKeepLive(channel, 0); + defaultChannelManager.addChannel(channel); + MethodUtils.invokeMethod(defaultChannelManager, true, "doPing", mock(Timeout.class), channel); + verify(sessionLoop).unloadSession(anyString(), anyString()); + } + +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestInFlyCache.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestInFlyCache.java new file mode 100644 index 00000000000..a3fb6e8d939 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestInFlyCache.java @@ -0,0 +1,49 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.test; + +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.cs.session.infly.InFlyCache; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.Mockito.mock; + +@RunWith(MockitoJUnitRunner.class) +public class TestInFlyCache { + + @Test + public void test() { + InFlyCache inFlyCache = new InFlyCache(); + inFlyCache.put(InFlyCache.CacheType.PUB, "test", 1); + Assert.assertTrue(inFlyCache.contains(InFlyCache.CacheType.PUB, "test", 1)); + + inFlyCache.getPendingDownCache().put("test", 1, mock(Subscription.class), mock(Queue.class), mock(Message.class)); + Assert.assertTrue(null != inFlyCache.getPendingDownCache().get("test", 1)); + + inFlyCache.getPendingDownCache().remove("test", 1); + Assert.assertTrue(null == inFlyCache.getPendingDownCache().get("test", 1)); + + } +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestMatchAction.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestMatchAction.java new file mode 100644 index 00000000000..0369b70b43c --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestMatchAction.java @@ -0,0 +1,69 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.test; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.apache.rocketmq.mqtt.cs.session.match.MatchAction; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TestMatchAction { + + @Mock + private SessionLoop sessionLoop; + + @Test + public void test() throws IllegalAccessException { + MatchAction matchAction = new MatchAction(); + FieldUtils.writeDeclaredField(matchAction, "sessionLoop", sessionLoop, true); + + Session session = mock(Session.class); + when(session.getChannelId()).thenReturn("test"); + when(sessionLoop.getSession(any())).thenReturn(session); + Subscription subscription = new Subscription("test"); + Set subscriptions = new HashSet<>(Arrays.asList(subscription)); + when(session.subscriptionSnapshot()).thenReturn(subscriptions); + + matchAction.addSubscription(session, subscriptions); + Set> set = matchAction.matchClients("test",""); + Assert.assertFalse(set.isEmpty()); + + matchAction.removeSubscription(session,subscriptions); + set = matchAction.matchClients("test",""); + Assert.assertTrue(set.isEmpty()); + } + +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestMessageNotifyAction.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestMessageNotifyAction.java new file mode 100644 index 00000000000..c9694830d55 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestMessageNotifyAction.java @@ -0,0 +1,87 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.test; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.model.MessageEvent; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.cs.session.QueueFresh; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.loop.QueueCache; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.apache.rocketmq.mqtt.cs.session.match.MatchAction; +import org.apache.rocketmq.mqtt.cs.session.notify.MessageNotifyAction; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestMessageNotifyAction { + + @Mock + private SessionLoop sessionLoop; + + @Mock + private QueueFresh queueFresh; + + @Mock + private QueueCache queueCache; + + @Test + public void test() throws IllegalAccessException { + MatchAction matchAction = new MatchAction(); + FieldUtils.writeDeclaredField(matchAction, "sessionLoop", sessionLoop, true); + + MessageNotifyAction messageNotifyAction = new MessageNotifyAction(); + FieldUtils.writeDeclaredField(messageNotifyAction, "sessionLoop", sessionLoop, true); + FieldUtils.writeDeclaredField(messageNotifyAction, "queueFresh", queueFresh, true); + FieldUtils.writeDeclaredField(messageNotifyAction, "queueCache", queueCache, true); + FieldUtils.writeDeclaredField(messageNotifyAction, "matchAction", matchAction, true); + + Session session = mock(Session.class); + when(session.getChannelId()).thenReturn("test"); + when(sessionLoop.getSession(any())).thenReturn(session); + Subscription subscription = new Subscription("test"); + Set subscriptions = new HashSet<>(Arrays.asList(subscription)); + when(session.subscriptionSnapshot()).thenReturn(subscriptions); + matchAction.addSubscription(session, subscriptions); + + Queue queue = new Queue(0, "test", "test"); + when(queueFresh.freshQueue(eq(session), eq(subscription))).thenReturn(new HashSet<>(Arrays.asList(queue))); + + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setPubTopic("test"); + messageEvent.setBrokerName("test"); + messageEvent.setQueueId(0); + messageNotifyAction.notify(Arrays.asList(messageEvent)); + verify(sessionLoop).notifyPullMessage(eq(session), eq(subscription), eq(queue)); + } + +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestMqttMsgId.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestMqttMsgId.java new file mode 100644 index 00000000000..b4efbc4daac --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestMqttMsgId.java @@ -0,0 +1,38 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.test; + +import org.apache.rocketmq.mqtt.cs.session.infly.MqttMsgId; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class TestMqttMsgId { + + @Test + public void test() { + MqttMsgId mqttMsgId = new MqttMsgId(); + mqttMsgId.init(); + int id = mqttMsgId.nextId("test"); + Assert.assertFalse(mqttMsgId.nextId("test") == id); + } +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestPushAction.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestPushAction.java new file mode 100644 index 00000000000..de7f988ea74 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestPushAction.java @@ -0,0 +1,96 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.test; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.infly.InFlyCache; +import org.apache.rocketmq.mqtt.cs.session.infly.MqttMsgId; +import org.apache.rocketmq.mqtt.cs.session.infly.PushAction; +import org.apache.rocketmq.mqtt.cs.session.infly.RetryDriver; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestPushAction { + + @Mock + private MqttMsgId mqttMsgId; + + @Mock + private RetryDriver retryDriver; + + @Mock + private InFlyCache inFlyCache; + + @Mock + private ConnectConf connectConf; + + private PushAction pushAction = new PushAction(); + + @Before + public void before() throws IllegalAccessException { + FieldUtils.writeDeclaredField(pushAction, "mqttMsgId", mqttMsgId, true); + FieldUtils.writeDeclaredField(pushAction, "retryDriver", retryDriver, true); + FieldUtils.writeDeclaredField(pushAction, "inFlyCache", inFlyCache, true); + FieldUtils.writeDeclaredField(pushAction, "connectConf", connectConf, true); + } + + @Test + public void testMessageArrive() { + Session session = mock(Session.class); + Subscription subscription = mock(Subscription.class); + Queue queue = mock(Queue.class); + List messages = new ArrayList<>(); + messages.add(mock(Message.class)); + when(session.pendMessageList(any(), any())).thenReturn(messages); + when(connectConf.isOrder()).thenReturn(false); + PushAction spyPushAction = spy(pushAction); + doNothing().when(spyPushAction).push(any(), any(), any(), any()); + spyPushAction.messageArrive(session, subscription, queue); + verify(spyPushAction, atLeastOnce()).push(any(), any(), any(), any()); + } + + @Test + public void testPush() { + Session session = mock(Session.class); + when(session.getChannelId()).thenReturn("test"); + when(session.getClientId()).thenReturn("test"); + PushAction spyPushAction = spy(pushAction); + doNothing().when(spyPushAction).write(any(), any(), anyInt(), anyInt(), any()); + when(inFlyCache.getPendingDownCache()).thenReturn(new InFlyCache().getPendingDownCache()); + spyPushAction.push(mock(Message.class), mock(Subscription.class), session, mock(Queue.class)); + verify(spyPushAction, atLeastOnce()).write(any(), any(), anyInt(), anyInt(), any()); + } + +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestQueueCache.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestQueueCache.java new file mode 100644 index 00000000000..9f1a2c6a4f6 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestQueueCache.java @@ -0,0 +1,112 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.test; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.model.*; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.loop.QueueCache; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TestQueueCache { + + @Mock + private ConnectConf connectConf; + + @Mock + private LmqQueueStore lmqQueueStore; + + private QueueCache queueCache = new QueueCache(); + + @Before + public void before() throws IllegalAccessException { + FieldUtils.writeDeclaredField(queueCache, "connectConf", connectConf, true); + FieldUtils.writeDeclaredField(queueCache, "lmqQueueStore", lmqQueueStore, true); + queueCache.init(); + } + + @Test + public void test() throws InterruptedException, ExecutionException, TimeoutException { + when(connectConf.getQueueCacheSize()).thenReturn(32); + when(connectConf.getPullBatchSize()).thenReturn(32); + QueueOffset queueOffset = new QueueOffset(); + Queue queue = new Queue(); + queue.setQueueName("test"); + queue.setBrokerName("test"); + + List messageList = new ArrayList<>(); + messageList.add(new Message()); + messageList.add(new Message()); + messageList.get(0).setOffset(1); + messageList.get(1).setOffset(2); + + CompletableFuture resultPullLast = new CompletableFuture<>(); + PullResult pullResult = new PullResult(); + pullResult.setCode(PullResult.PULL_SUCCESS); + pullResult.setMessageList(messageList.subList(0, 1)); + resultPullLast.complete(pullResult); + when(lmqQueueStore.pullLastMessages(any(), any(), anyLong())).thenReturn(resultPullLast); + + CompletableFuture resultPull = new CompletableFuture<>(); + pullResult = new PullResult(); + pullResult.setCode(PullResult.PULL_SUCCESS); + pullResult.setMessageList(messageList.subList(1, messageList.size())); + resultPull.complete(pullResult); + when(lmqQueueStore.pullMessage(any(), any(), any(), anyLong())).thenReturn(resultPull); + + Session session = mock(Session.class); + queueCache.refreshCache(Pair.of(queue, session)); + Thread.sleep(1000); + CompletableFuture callBackResult = new CompletableFuture<>(); + queueOffset.setOffset(1); + queueCache.pullMessage(session, new Subscription("test"), queue, queueOffset, 32, callBackResult); + pullResult = callBackResult.get(1, TimeUnit.SECONDS); + Assert.assertTrue(pullResult.getMessageList().get(0).getOffset() == 1); + + queueCache.refreshCache(Pair.of(queue, session)); + Thread.sleep(1000); + callBackResult = new CompletableFuture<>(); + queueOffset.setOffset(2); + queueCache.pullMessage(session, new Subscription("test"), queue, queueOffset, 32, callBackResult); + pullResult = callBackResult.get(1, TimeUnit.SECONDS); + Assert.assertTrue(pullResult.getMessageList().get(0).getOffset() == 2); + } + +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestRetryDriver.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestRetryDriver.java new file mode 100644 index 00000000000..cb7d37f08d2 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestRetryDriver.java @@ -0,0 +1,91 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.test; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.reflect.MethodUtils; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.infly.PushAction; +import org.apache.rocketmq.mqtt.cs.session.infly.RetryDriver; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.CompletableFuture; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestRetryDriver { + + @Mock + private SessionLoop sessionLoop; + + @Mock + private PushAction pushAction; + + @Mock + private ConnectConf connectConf; + + @Mock + private LmqQueueStore lmqQueueStore; + + private RetryDriver retryDriver = new RetryDriver(); + + @Before + public void before() throws IllegalAccessException { + FieldUtils.writeDeclaredField(retryDriver, "sessionLoop", sessionLoop, true); + FieldUtils.writeDeclaredField(retryDriver, "pushAction", pushAction, true); + FieldUtils.writeDeclaredField(retryDriver, "connectConf", connectConf, true); + FieldUtils.writeDeclaredField(retryDriver, "lmqQueueStore", lmqQueueStore, true); + + when(connectConf.getRetryIntervalSeconds()).thenReturn(1); + when(connectConf.getMaxRetryTime()).thenReturn(1); + retryDriver.init(); + } + + @Test + public void test() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, InterruptedException { + Session session = mock(Session.class); + when(session.getChannelId()).thenReturn("test"); + when(session.isDestroyed()).thenReturn(false); + when(sessionLoop.getSession(any())).thenReturn(session); + when(lmqQueueStore.putMessage(any(), any())).thenReturn(mock(CompletableFuture.class)); + Message message = mock(Message.class); + when(message.copy()).thenReturn(mock(Message.class)); + retryDriver.mountPublish(1,message , 1, "test", mock(Subscription.class)); + Thread.sleep(3000); + MethodUtils.invokeMethod(retryDriver, true, "doRetryCache"); + verify(pushAction, atLeastOnce()).write(any(), any(), eq(1), eq(1), any()); + Thread.sleep(3000); + MethodUtils.invokeMethod(retryDriver, true, "doRetryCache"); + verify(lmqQueueStore, atLeastOnce()).putMessage(any(), any()); + } + +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestSession.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestSession.java new file mode 100644 index 00000000000..cfcfa6e6960 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestSession.java @@ -0,0 +1,69 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.test; + +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.QueueOffset; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestSession { + + @Test + public void test() { + Session session = new Session(); + + Set subscriptions = new HashSet<>(); + Subscription subscription = new Subscription("test"); + subscriptions.add(subscription); + session.addSubscription(subscriptions); + Set subscriptionSnapshot = session.subscriptionSnapshot(); + Assert.assertTrue(subscriptionSnapshot.iterator().next().equals(subscription)); + + Queue queue = new Queue(0, "test", "test"); + QueueOffset queueOffset = new QueueOffset(); + Map offsetMap = new HashMap<>(); + offsetMap.put(queue, queueOffset); + session.addOffset(subscription.toQueueName(), offsetMap); + Assert.assertTrue(queueOffset.equals(session.getQueueOffset(subscription, queue))); + + session.freshQueue(subscription, new HashSet<>(Arrays.asList(queue))); + List messages = new ArrayList<>(); + Message message = new Message(); + message.setOffset(1); + messages.add(message); + session.addSendingMessages(subscription, queue, messages); + Assert.assertFalse(session.sendingMessageIsEmpty(subscription, queue)); + Assert.assertTrue(message.equals(session.nextSendMessageByOrder(subscription,queue))); + Assert.assertTrue(message.equals(session.pendMessageList(subscription,queue).iterator().next())); + + session.ack(subscription,queue,1); + Assert.assertTrue(session.sendingMessageIsEmpty(subscription, queue)); + } + +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestSessionLoopImpl.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestSessionLoopImpl.java new file mode 100644 index 00000000000..b3eb307d858 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/TestSessionLoopImpl.java @@ -0,0 +1,157 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.cs.test; + +import io.netty.channel.socket.nio.NioSocketChannel; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.facade.LmqOffsetStore; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.QueueOffset; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.session.QueueFresh; +import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.infly.InFlyCache; +import org.apache.rocketmq.mqtt.cs.session.loop.QueueCache; +import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoopImpl; +import org.apache.rocketmq.mqtt.cs.session.match.MatchAction; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestSessionLoopImpl { + + @Mock + private MatchAction matchAction; + + @Mock + private QueueFresh queueFresh; + + @Mock + private InFlyCache inFlyCache; + + @Mock + private LmqOffsetStore lmqOffsetStore; + + @Mock + private LmqQueueStore lmqQueueStore; + + @Mock + private QueueCache queueCache; + + @Mock + private ConnectConf connectConf; + + private SessionLoopImpl sessionLoop = new SessionLoopImpl(); + + @Before + public void before() throws IllegalAccessException { + FieldUtils.writeDeclaredField(sessionLoop, "matchAction", matchAction, true); + FieldUtils.writeDeclaredField(sessionLoop, "queueFresh", queueFresh, true); + FieldUtils.writeDeclaredField(sessionLoop, "inFlyCache", inFlyCache, true); + FieldUtils.writeDeclaredField(sessionLoop, "lmqOffsetStore", lmqOffsetStore, true); + FieldUtils.writeDeclaredField(sessionLoop, "lmqQueueStore", lmqQueueStore, true); + FieldUtils.writeDeclaredField(sessionLoop, "queueCache", queueCache, true); + FieldUtils.writeDeclaredField(sessionLoop, "connectConf", connectConf, true); + + } + + @Test + public void testSessionLoad() throws IllegalAccessException { + SessionLoopImpl spySessionLoop = spy(sessionLoop); + + NioSocketChannel channel = spy(new NioSocketChannel()); + when(channel.isActive()).thenReturn(true); + spySessionLoop.loadSession("test", channel); + + Field field = FieldUtils.getField(SessionLoopImpl.class, "sessionMap", true); + Map sessionMap = (Map) field.get(spySessionLoop); + Assert.assertFalse(sessionMap.isEmpty()); + + List sessionList = spySessionLoop.getSessionList("test"); + Assert.assertFalse(sessionList.isEmpty()); + + spySessionLoop.unloadSession("test", sessionMap.keySet().iterator().next()); + Assert.assertTrue(sessionMap.isEmpty()); + } + + @Test + public void testAddSubscription() { + SessionLoopImpl spySessionLoop = spy(sessionLoop); + Session session = mock(Session.class); + NioSocketChannel channel = spy(new NioSocketChannel()); + when(session.getChannel()).thenReturn(channel); + when(spySessionLoop.getSession(anyString())).thenReturn(session); + QueueOffset queueOffset = new QueueOffset(); + when(session.getQueueOffset(any(), any())).thenReturn(queueOffset); + Map queueOffsets = new HashMap<>(); + queueOffsets.put(new Queue(), queueOffset); + when(session.getQueueOffset(any())).thenReturn(queueOffsets); + + CompletableFuture maxIdResult = new CompletableFuture<>(); + maxIdResult.complete(1L); + when(lmqQueueStore.queryQueueMaxOffset(any())).thenReturn(maxIdResult); + spySessionLoop.addSubscription("test", new HashSet<>(Arrays.asList(new Subscription()))); + Assert.assertTrue(queueOffset.isInitialized()); + } + + @Test + public void testNotifyPullMessage() throws InterruptedException { + SessionLoopImpl spySessionLoop = spy(sessionLoop); + + Session session = mock(Session.class); + when(session.getLoadStatusMap()).thenReturn(new ConcurrentHashMap<>()); + QueueOffset queueOffset = new QueueOffset(); + queueOffset.setInitialized(); + when(session.getQueueOffset(any(), any())).thenReturn(queueOffset); + Map queueOffsets = new HashMap<>(); + Queue queue = new Queue(); + queueOffsets.put(queue, queueOffset); + when(session.sendingMessageIsEmpty(any(), any())).thenReturn(true); + NioSocketChannel channel = spy(new NioSocketChannel()); + when(session.getChannel()).thenReturn(channel); + when(channel.isActive()).thenReturn(true); + + CompletableFuture> getOffsetResult = new CompletableFuture<>(); + when(lmqOffsetStore.getOffset(any(), any())).thenReturn(getOffsetResult); + + spySessionLoop.init(); + spySessionLoop.notifyPullMessage(session, new Subscription(), queue); + + getOffsetResult.complete(queueOffsets); + + Thread.sleep(1000); + + verify(queueCache, atLeastOnce()).pullMessage(any(), any(), any(), any(), anyInt(), any()); + + } +} diff --git a/mqtt-ds/pom.xml b/mqtt-ds/pom.xml new file mode 100644 index 00000000000..7aac1216cde --- /dev/null +++ b/mqtt-ds/pom.xml @@ -0,0 +1,63 @@ + + + + rocketmq-mqtt + org.apache.rocketmq + 1.0.0-SNAPSHOT + + 4.0.0 + + mqtt-ds + + + org.apache.rocketmq + mqtt-common + + + org.apache.rocketmq + rocketmq-client + + + org.apache.rocketmq + rocketmq-tools + + + io.netty + netty-all + + + org.springframework + spring-core + + + org.springframework + spring-context + + + org.springframework + spring-beans + + + com.github.ben-manes.caffeine + caffeine + + + junit + junit + test + + + org.mockito + mockito-core + test + + + + + 8 + 8 + + + \ No newline at end of file diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/auth/AuthManagerSample.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/auth/AuthManagerSample.java new file mode 100644 index 00000000000..bde3d1c8174 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/auth/AuthManagerSample.java @@ -0,0 +1,96 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.auth; + +import io.netty.handler.codec.mqtt.MqttConnectMessage; +import io.netty.handler.codec.mqtt.MqttConnectPayload; +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import io.netty.handler.codec.mqtt.MqttMessage; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.mqtt.common.facade.AuthManager; +import org.apache.rocketmq.mqtt.common.hook.AbstractUpstreamHook; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.hook.UpstreamHookEnum; +import org.apache.rocketmq.mqtt.common.hook.UpstreamHookManager; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; +import org.apache.rocketmq.mqtt.common.model.Remark; +import org.apache.rocketmq.mqtt.common.util.HmacSHA1Util; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; + +import javax.annotation.Resource; +import java.util.Objects; +import java.util.concurrent.*; + +/** + * A Sample For Auth, Check sign + */ +public class AuthManagerSample extends AbstractUpstreamHook implements AuthManager { + + @Resource + private UpstreamHookManager upstreamHookManager; + + @Resource + private ServiceConf serviceConf; + + public Executor executor; + + public void init() { + executor = new ThreadPoolExecutor( + 8, + 16, + 1, + TimeUnit.MINUTES, + new LinkedBlockingQueue<>(10000), + new ThreadFactoryImpl("AuthHook_")); + register(); + } + + @Override + public void register() { + upstreamHookManager.addHook(UpstreamHookEnum.AUTH.ordinal(), this); + } + + @Override + public CompletableFuture processMqttMessage(MqttMessageUpContext context, MqttMessage message) { + return CompletableFuture.supplyAsync(() -> doAuth(message), executor); + } + + @Override + public HookResult doAuth(MqttMessage message) { + if (message instanceof MqttConnectMessage) { + MqttConnectMessage mqttConnectMessage = (MqttConnectMessage) message; + MqttConnectPayload mqttConnectPayload = mqttConnectMessage.payload(); + String clientId = mqttConnectPayload.clientIdentifier(); + String username = mqttConnectPayload.userName(); + byte[] password = mqttConnectPayload.passwordInBytes(); + boolean validateSign = false; + try { + validateSign = HmacSHA1Util.validateSign(clientId, password, serviceConf.getSecretKey()); + } catch (Exception e) { + logger.error("", e); + } + if (!Objects.equals(username, serviceConf.getUsername()) || !validateSign) { + return new HookResult(HookResult.FAIL, MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD.byteValue(), Remark.AUTH_FAILED, null); + } + } + return new HookResult(HookResult.SUCCESS, null, null); + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/config/ServiceConf.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/config/ServiceConf.java new file mode 100644 index 00000000000..b8996143025 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/config/ServiceConf.java @@ -0,0 +1,139 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.config; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.rmi.RemoteException; +import java.util.Properties; + +@Component +public class ServiceConf { + private static final String CONF_FILE_NAME = "service.conf"; + private File confFile; + private Properties properties; + private int authThreadNum = 32; + private int csRpcPort = 7001; + private int eventNotifyRetryMaxTime = 3; + private String eventNotifyRetryTopic; + private String clientRetryTopic; + private String clientP2pTopic; + private String username; + private String secretKey; + + public ServiceConf() throws IOException { + ClassPathResource classPathResource = new ClassPathResource(CONF_FILE_NAME); + InputStream in = classPathResource.getInputStream(); + Properties properties = new Properties(); + properties.load(in); + in.close(); + this.properties = properties; + MixAll.properties2Object(properties, this); + this.confFile = new File(classPathResource.getURL().getFile()); + if (StringUtils.isBlank(clientRetryTopic)) { + throw new RemoteException("clientRetryTopic is blank"); + } + if (StringUtils.isBlank(eventNotifyRetryTopic)) { + throw new RemoteException("eventNotifyRetryTopic is blank"); + } + } + + public File getConfFile() { + return confFile; + } + + public int getAuthThreadNum() { + return authThreadNum; + } + + public void setAuthThreadNum(int authThreadNum) { + this.authThreadNum = authThreadNum; + } + + public Properties getProperties() { + return properties; + } + + public void setProperties(Properties properties) { + this.properties = properties; + } + + public int getCsRpcPort() { + return csRpcPort; + } + + public void setCsRpcPort(int csRpcPort) { + this.csRpcPort = csRpcPort; + } + + public int getEventNotifyRetryMaxTime() { + return eventNotifyRetryMaxTime; + } + + public void setEventNotifyRetryMaxTime(int eventNotifyRetryMaxTime) { + this.eventNotifyRetryMaxTime = eventNotifyRetryMaxTime; + } + + public String getEventNotifyRetryTopic() { + return eventNotifyRetryTopic; + } + + public void setEventNotifyRetryTopic(String eventNotifyRetryTopic) { + this.eventNotifyRetryTopic = eventNotifyRetryTopic; + } + + public String getClientRetryTopic() { + return clientRetryTopic; + } + + public void setClientRetryTopic(String clientRetryTopic) { + this.clientRetryTopic = clientRetryTopic; + } + + public String getClientP2pTopic() { + return clientP2pTopic; + } + + public void setClientP2pTopic(String clientP2pTopic) { + this.clientP2pTopic = clientP2pTopic; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/config/ServiceConfListener.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/config/ServiceConfListener.java new file mode 100644 index 00000000000..43c77bc43ce --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/config/ServiceConfListener.java @@ -0,0 +1,73 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.config; + +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Properties; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + + +@Component +public class ServiceConfListener { + private static Logger logger = LoggerFactory.getLogger(ServiceConfListener.class); + + @Resource + private ServiceConf serviceConf; + + private File confFile; + private ScheduledThreadPoolExecutor scheduler; + private AtomicLong gmt = new AtomicLong(); + + @PostConstruct + public void start() { + confFile = serviceConf.getConfFile(); + gmt.set(confFile.lastModified()); + scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("ServiceConfListener")); + scheduler.scheduleWithFixedDelay(() -> { + try { + if (gmt.get() == confFile.lastModified()) { + return; + } + gmt.set(confFile.lastModified()); + InputStream in = new FileInputStream(confFile.getAbsoluteFile()); + Properties properties = new Properties(); + properties.load(in); + in.close(); + MixAll.properties2Object(properties, serviceConf); + logger.warn("UpdateConf:{}", confFile.getAbsolutePath()); + } catch (Exception e) { + logger.error("", e); + } + }, 3, 3, TimeUnit.SECONDS); + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/FirstTopicManager.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/FirstTopicManager.java new file mode 100644 index 00000000000..e7994d8de98 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/FirstTopicManager.java @@ -0,0 +1,162 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.meta; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.protocol.ResponseCode; +import org.apache.rocketmq.common.protocol.route.BrokerData; +import org.apache.rocketmq.common.protocol.route.QueueData; +import org.apache.rocketmq.common.protocol.route.TopicRouteData; +import org.apache.rocketmq.mqtt.common.facade.MetaPersistManager; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; +import org.apache.rocketmq.mqtt.ds.mq.MqFactory; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@Component +public class FirstTopicManager { + private static Logger logger = LoggerFactory.getLogger(FirstTopicManager.class); + private Cache topicExistCache; + private Cache topicNotExistCache; + private DefaultMQAdminExt defaultMQAdminExt; + private Map> brokerAddressMap = new ConcurrentHashMap<>(); + private Map> readableBrokers = new ConcurrentHashMap<>(); + private ScheduledThreadPoolExecutor scheduler; + + @Resource + private ServiceConf serviceConf; + + @Resource + private MetaPersistManager metaPersistManager; + + @PostConstruct + public void init() throws MQClientException { + topicExistCache = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(1, TimeUnit.MINUTES).build(); + topicNotExistCache = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(1, TimeUnit.MINUTES).build(); + defaultMQAdminExt = MqFactory.buildDefaultMQAdminExt("TopicCheck", serviceConf.getProperties()); + defaultMQAdminExt.start(); + + scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("refreshStoreBroker")); + scheduler.scheduleWithFixedDelay(() -> { + Set copy = new HashSet<>(); + copy.add(serviceConf.getClientRetryTopic()); + copy.add(serviceConf.getClientP2pTopic()); + Set allFirstTopics = metaPersistManager.getAllFirstTopics(); + if (allFirstTopics != null) { + copy.addAll(allFirstTopics); + } + for (String firstTopic : copy) { + updateTopicRoute(firstTopic); + } + }, 0, 5, TimeUnit.SECONDS); + } + + public void checkFirstTopicIfCreated(String firstTopic) { + if (topicExistCache.getIfPresent(firstTopic) != null) { + return; + } + if (topicNotExistCache.getIfPresent(firstTopic) != null) { + throw new TopicNotExistException(firstTopic + " NotExist"); + } + try { + TopicRouteData topicRouteData = defaultMQAdminExt.examineTopicRouteInfo(firstTopic); + if (topicRouteData == null || topicRouteData.getBrokerDatas() == null || topicRouteData.getBrokerDatas().isEmpty()) { + topicNotExistCache.put(firstTopic, new Object()); + throw new TopicNotExistException(firstTopic + " NotExist"); + } + updateTopicRoute(firstTopic, topicRouteData); + topicExistCache.put(firstTopic, topicRouteData); + } catch (MQClientException e) { + if (ResponseCode.TOPIC_NOT_EXIST == e.getResponseCode()) { + topicNotExistCache.put(firstTopic, new Object()); + throw new TopicNotExistException(firstTopic + " NotExist"); + } + } catch (Exception e) { + logger.error("check topic {} exception", firstTopic, e); + } + } + + private void updateTopicRoute(String firstTopic) { + try { + TopicRouteData topicRouteData = defaultMQAdminExt.examineTopicRouteInfo(firstTopic); + updateTopicRoute(firstTopic, topicRouteData); + } catch (MQClientException t) { + if (t.getResponseCode() == ResponseCode.TOPIC_NOT_EXIST) { + brokerAddressMap.remove(firstTopic); + readableBrokers.remove(firstTopic); + } + } catch (Throwable throwable) { + logger.error("", throwable); + } + } + + private void updateTopicRoute(String firstTopic, TopicRouteData topicRouteData) { + if (topicRouteData == null || firstTopic == null) { + return; + } + Map tmp = new ConcurrentHashMap<>(); + for (BrokerData brokerData : topicRouteData.getBrokerDatas()) { + tmp.put(brokerData.getBrokerName(), brokerData.getBrokerAddrs().get(MixAll.MASTER_ID)); + } + brokerAddressMap.put(firstTopic, tmp); + Set tmpBrokers = new HashSet<>(); + for (QueueData qd : topicRouteData.getQueueDatas()) { + if (PermName.isReadable(qd.getPerm())) { + tmpBrokers.add(qd.getBrokerName()); + } + } + readableBrokers.put(firstTopic, tmpBrokers); + } + + public Map getBrokerAddressMap(String firstTopic) { + Map copy = new ConcurrentHashMap<>(); + Map map = brokerAddressMap.get(firstTopic); + if (map != null) { + copy.putAll(map); + } + return copy; + } + + public Set getReadableBrokers(String firstTopic) { + Set copy = new HashSet<>(); + Set set = readableBrokers.get(firstTopic); + if (set != null) { + copy.addAll(set); + } + return copy; + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/MetaPersistManagerSample.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/MetaPersistManagerSample.java new file mode 100644 index 00000000000..3856f8fb35e --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/MetaPersistManagerSample.java @@ -0,0 +1,128 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.meta; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.protocol.ResponseCode; +import org.apache.rocketmq.mqtt.common.facade.MetaPersistManager; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; +import org.apache.rocketmq.mqtt.ds.mq.MqFactory; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Resource; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * A Sample For Meta Manager, Persisted In namesrv KV Config + */ +public class MetaPersistManagerSample implements MetaPersistManager { + private static Logger logger = LoggerFactory.getLogger(MetaPersistManagerSample.class); + private volatile Map> wildcardCache = new ConcurrentHashMap<>(); + private volatile Set firstTopics = new HashSet<>(); + private volatile Set connectNodeSet = new HashSet<>(); + private DefaultMQAdminExt defaultMQAdminExt; + private ScheduledThreadPoolExecutor scheduler; + private static final String RMQ_NAMESPACE = "LMQ"; + private static final String KEY_LMQ_ALL_FIRST_TOPICS = "ALL_FIRST_TOPICS"; + private static final String KEY_LMQ_CONNECT_NODES = "LMQ_CONNECT_NODES"; + private static final String VALUE_SPLITTER = ","; + + @Resource + private ServiceConf serviceConf; + + public void init() throws MQClientException, RemotingException, InterruptedException { + defaultMQAdminExt = MqFactory.buildDefaultMQAdminExt("MetaLoad", serviceConf.getProperties()); + defaultMQAdminExt.start(); + refreshMeta(); + scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("refreshMeta")); + scheduler.scheduleWithFixedDelay(() -> { + try { + refreshMeta(); + } catch (Throwable t) { + logger.error("", t); + } + }, 5, 5, TimeUnit.SECONDS); + } + + private void refreshMeta() throws RemotingException, InterruptedException, MQClientException { + String value = defaultMQAdminExt.getKVConfig(RMQ_NAMESPACE, KEY_LMQ_ALL_FIRST_TOPICS); + if (value == null) { + return; + } + String[] topics = value.split(VALUE_SPLITTER); + Set tmpFirstTopics = new HashSet<>(); + Map> tmpWildcardCache = new ConcurrentHashMap<>(); + for (String topic : topics) { + tmpFirstTopics.add(topic); + try { + String wildcardValue = defaultMQAdminExt.getKVConfig(RMQ_NAMESPACE, topic); + String[] wildcards = wildcardValue.split(VALUE_SPLITTER); + Set tmpWildcards = new HashSet<>(); + for (String wildcard : wildcards) { + tmpWildcards.add(TopicUtils.normalizeTopic(wildcard)); + } + tmpWildcardCache.put(topic, tmpWildcards); + } catch (MQClientException e) { + if (ResponseCode.QUERY_NOT_FOUND == e.getResponseCode()) { + continue; + } + throw e; + } + } + firstTopics = tmpFirstTopics; + wildcardCache = tmpWildcardCache; + value = defaultMQAdminExt.getKVConfig(RMQ_NAMESPACE, KEY_LMQ_CONNECT_NODES); + if (StringUtils.isNotBlank(value)) { + String[] ss = StringUtils.split(value, VALUE_SPLITTER); + Set set = new HashSet<>(); + for (String s : ss) { + set.add(s); + } + connectNodeSet = set; + } + } + + @Override + public Set getWildcards(String firstTopic) { + return wildcardCache.get(firstTopic); + } + + @Override + public Set getAllFirstTopics() { + return firstTopics; + } + + @Override + public Set getConnectNodeSet() { + return connectNodeSet; + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/TopicNotExistException.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/TopicNotExistException.java new file mode 100644 index 00000000000..f0d537729ae --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/TopicNotExistException.java @@ -0,0 +1,41 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.meta; + +public class TopicNotExistException extends RuntimeException{ + public TopicNotExistException() { + } + + public TopicNotExistException(String message) { + super(message); + } + + public TopicNotExistException(String message, Throwable cause) { + super(message, cause); + } + + public TopicNotExistException(Throwable cause) { + super(cause); + } + + public TopicNotExistException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/WildcardManager.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/WildcardManager.java new file mode 100644 index 00000000000..545ea1f2010 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/meta/WildcardManager.java @@ -0,0 +1,129 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.meta; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.mqtt.common.facade.MetaPersistManager; +import org.apache.rocketmq.mqtt.common.model.MqttTopic; +import org.apache.rocketmq.mqtt.common.model.Trie; +import org.apache.rocketmq.mqtt.common.util.StatUtil; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@Component +public class WildcardManager { + private static Logger logger = LoggerFactory.getLogger(WildcardManager.class); + private Map> wildCardTrie = new ConcurrentHashMap<>(); + private ScheduledThreadPoolExecutor scheduler; + + @Resource + private MetaPersistManager metaPersistManager; + + @PostConstruct + public void init() { + scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("loadWildcard_thread_")); + scheduler.scheduleWithFixedDelay(() -> refreshLoadWildcard(), 0, 5, TimeUnit.SECONDS); + } + + private void refreshLoadWildcard() { + try { + Set topics = metaPersistManager.getAllFirstTopics(); + if (topics == null) { + return; + } + topics.forEach(firstTopic -> refreshWildcards(firstTopic)); + } catch (Exception e) { + logger.error("", e); + } + } + + private void refreshWildcards(String firstTopic) { + Trie trie = new Trie<>(); + Trie old = wildCardTrie.putIfAbsent(firstTopic, trie); + if (old != null) { + trie = old; + } + Set wildcards = metaPersistManager.getWildcards(firstTopic); + if (wildcards != null && !wildcards.isEmpty()) { + for (String each : wildcards) { + trie.addNode(each, 0, ""); + } + } + //clean unused key + Trie finalTrie = trie; + trie.traverseAll((path, nodeKey) -> { + if (!wildcards.contains(path)) { + finalTrie.deleteNode(path, nodeKey); + } + }); + } + + public Set matchQueueSetByMsgTopic(String pubTopic, String namespace) { + if (StringUtils.isBlank(pubTopic)) { + return new HashSet<>(); + } + String queueName = pubTopic; + MqttTopic mqttTopic = TopicUtils.decode(pubTopic); + String secondTopic = TopicUtils.normalizeSecondTopic(mqttTopic.getSecondTopic()); + if (TopicUtils.isP2P(secondTopic)) { + String p2Peer = TopicUtils.getP2Peer(mqttTopic, namespace); + queueName = TopicUtils.getP2pTopic(p2Peer); + } + Set queueNames = new HashSet<>(); + queueNames.add(queueName); + + if (!TopicUtils.isP2P(secondTopic)) { + Set wildcards = matchWildcards(pubTopic); + if (wildcards != null && !wildcards.isEmpty()) { + for (String wildcard : wildcards) { + queueNames.add(wildcard); + } + } + } + return queueNames; + } + + private Set matchWildcards(String topic) { + long start = System.currentTimeMillis(); + try { + MqttTopic mqttTopic = TopicUtils.decode(topic); + Trie trie = wildCardTrie.get(mqttTopic.getFirstTopic()); + if (trie == null) { + return new HashSet<>(); + } + return trie.getNodePath(topic); + } finally { + StatUtil.addInvoke("MatchWildcards", System.currentTimeMillis() - start); + } + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqAdmin.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqAdmin.java new file mode 100644 index 00000000000..9eb4336f19b --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqAdmin.java @@ -0,0 +1,53 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.mq; + + + +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; + +import java.util.Properties; + +public class MqAdmin{ + private DefaultMQAdminExt defaultMQAdminExt; + + public MqAdmin(Properties properties) { + defaultMQAdminExt = new DefaultMQAdminExt(); + defaultMQAdminExt.setVipChannelEnabled(false); + defaultMQAdminExt.setNamesrvAddr(properties.getProperty("NAMESRV_ADDR")); + } + + public DefaultMQAdminExt getDefaultMQAdminExt() { + return defaultMQAdminExt; + } + + public void setAdminGroup(String group){ + defaultMQAdminExt.setAdminExtGroup(group); + } + + public void start() { + try { + defaultMQAdminExt.start(); + } catch (MQClientException e) { + throw new RuntimeException(e); + } + } +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqConsumer.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqConsumer.java new file mode 100644 index 00000000000..8f4ad1ea0eb --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqConsumer.java @@ -0,0 +1,84 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.mq; + + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.MessageListener; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.UtilAll; + +import java.util.Properties; + +public class MqConsumer { + public static final String THREAD_NUM_KEY = "threadNum"; + private DefaultMQPushConsumer defaultMQPushConsumer; + + public MqConsumer(Properties properties) { + defaultMQPushConsumer = new DefaultMQPushConsumer(); + defaultMQPushConsumer.setNamesrvAddr(properties.getProperty("NAMESRV_ADDR")); + defaultMQPushConsumer.setConsumeMessageBatchMaxSize(1); + defaultMQPushConsumer.setPullBatchSize(Integer.parseInt(properties.getProperty("pullBatch", "64"))); + if (properties.get(THREAD_NUM_KEY) != null) { + defaultMQPushConsumer.setConsumeThreadMin(Integer.valueOf((String)properties.get("threadNum"))); + defaultMQPushConsumer.setConsumeThreadMax(Integer.valueOf((String)properties.get("threadNum"))); + } + defaultMQPushConsumer.setInstanceName(this.buildIntanceName()); + defaultMQPushConsumer.setVipChannelEnabled(false); + } + + public String buildIntanceName() { + return Integer.toString(UtilAll.getPid()) + + "#" + System.nanoTime(); + } + + public void setConsumerGroup(String consumerGroup) { + defaultMQPushConsumer.setConsumerGroup(consumerGroup); + } + + public DefaultMQPushConsumer getDefaultMQPushConsumer() { + return defaultMQPushConsumer; + } + + public void setMessageListener(MessageListener messageListener) { + if (messageListener instanceof MessageListenerOrderly) { + defaultMQPushConsumer.registerMessageListener((MessageListenerOrderly)messageListener); + } else { + defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently)messageListener); + } + } + + public void start() { + try { + defaultMQPushConsumer.start(); + } catch (MQClientException e) { + throw new RuntimeException(e); + } + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + defaultMQPushConsumer.shutdown(); + } + }); + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqFactory.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqFactory.java new file mode 100644 index 00000000000..691b61cc451 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqFactory.java @@ -0,0 +1,114 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.mq; + + +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.MessageListener; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; + +import java.util.Properties; + + +public class MqFactory { + public static synchronized DefaultMQProducer buildDefaultMQProducer(String group, Properties properties) { + MqProducer mqProducer = new MqProducer(properties); + mqProducer.setProducerGroup(group); + return mqProducer.getDefaultMQProducer(); + } + + public static synchronized DefaultMQAdminExt buildDefaultMQAdminExt(String group, Properties properties) { + MqAdmin mqadmin = new MqAdmin(properties); + mqadmin.setAdminGroup(group); + return mqadmin.getDefaultMQAdminExt(); + } + + public static synchronized DefaultMQPushConsumer buildDefaultMQPushConsumer(String group, Properties properties, + MessageListener messageListener) { + MqConsumer mqConsumer = new MqConsumer(properties); + mqConsumer.setConsumerGroup(group); + mqConsumer.setMessageListener(messageListener); + return mqConsumer.getDefaultMQPushConsumer(); + } + + public static synchronized DefaultMQPullConsumer buildDefaultMQPullConsumer(String group, Properties properties) { + MqPullConsumer mqConsumer = new MqPullConsumer(properties); + mqConsumer.setConsumerGroup(group); + return mqConsumer.getDefaultMQPullConsumer(); + } + + public static DefaultMQProducer buildDefaultMQProducer(String group, String nameSrv) { + DefaultMQProducer defaultMQProducer = new DefaultMQProducer(); + defaultMQProducer.setNamesrvAddr(nameSrv); + defaultMQProducer.setInstanceName(buildIntanceName()); + defaultMQProducer.setVipChannelEnabled(false); + defaultMQProducer.setProducerGroup(group); + return defaultMQProducer; + } + + public static DefaultMQPushConsumer buildDefaultMQPushConsumer(String group, String nameSrv, + MessageListener messageListener, Properties properties) { + DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer(); + defaultMQPushConsumer.setNamesrvAddr(nameSrv); + defaultMQPushConsumer.setConsumeMessageBatchMaxSize(1); + defaultMQPushConsumer.setPullBatchSize(Integer.parseInt(properties.getProperty("pullBatch", "64"))); + if (properties.get(MqConsumer.THREAD_NUM_KEY) != null) { + defaultMQPushConsumer.setConsumeThreadMin(Integer.valueOf((String) properties.get("threadNum"))); + defaultMQPushConsumer.setConsumeThreadMax(Integer.valueOf((String) properties.get("threadNum"))); + } + defaultMQPushConsumer.setInstanceName(buildIntanceName()); + defaultMQPushConsumer.setVipChannelEnabled(false); + defaultMQPushConsumer.setConsumerGroup(group); + if (messageListener instanceof MessageListenerOrderly) { + defaultMQPushConsumer.registerMessageListener((MessageListenerOrderly) messageListener); + } else { + defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently) messageListener); + } + return defaultMQPushConsumer; + } + + public static DefaultMQPullConsumer buildDefaultMQPullConsumer(String group, String nameSrv) { + DefaultMQPullConsumer defaultMQPullConsumer = new DefaultMQPullConsumer(); + defaultMQPullConsumer.setNamesrvAddr(nameSrv); + defaultMQPullConsumer.setInstanceName(buildIntanceName()); + defaultMQPullConsumer.setVipChannelEnabled(false); + defaultMQPullConsumer.setConsumerGroup(group); + return defaultMQPullConsumer; + } + + public static DefaultMQAdminExt buildDefaultMQAdminExt(String group, String nameSrv) { + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(); + defaultMQAdminExt.setNamesrvAddr(nameSrv); + defaultMQAdminExt.setVipChannelEnabled(false); + defaultMQAdminExt.setAdminExtGroup(group); + return defaultMQAdminExt; + } + + public static String buildIntanceName() { + return Integer.toString(UtilAll.getPid()) + + "#" + System.nanoTime(); + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqProducer.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqProducer.java new file mode 100644 index 00000000000..88d8dcc348f --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqProducer.java @@ -0,0 +1,62 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.mq; + + +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.UtilAll; + +import java.util.Properties; + + +public class MqProducer { + + private DefaultMQProducer defaultMQProducer; + + public MqProducer(Properties properties) { + defaultMQProducer = new DefaultMQProducer(); + defaultMQProducer.setNamesrvAddr(properties.getProperty("NAMESRV_ADDR")); + defaultMQProducer.setInstanceName(buildIntanceName()); + defaultMQProducer.setVipChannelEnabled(false); + } + + public String buildIntanceName() { + return Integer.toString(UtilAll.getPid()) + + "#" + System.nanoTime(); + } + + public DefaultMQProducer getDefaultMQProducer() { + return defaultMQProducer; + } + + public void setProducerGroup(String producerGroup) { + defaultMQProducer.setProducerGroup(producerGroup); + } + + public void start() { + try { + defaultMQProducer.start(); + } catch (MQClientException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqPullConsumer.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqPullConsumer.java new file mode 100644 index 00000000000..3d43aa88c9a --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/mq/MqPullConsumer.java @@ -0,0 +1,66 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.mq; + +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.UtilAll; + +import java.util.Properties; + + +public class MqPullConsumer { + private DefaultMQPullConsumer defaultMQPullConsumer; + + public MqPullConsumer(Properties properties) { + defaultMQPullConsumer = new DefaultMQPullConsumer(); + defaultMQPullConsumer.setNamesrvAddr(properties.getProperty("NAMESRV_ADDR")); + defaultMQPullConsumer.setInstanceName(this.buildIntanceName()); + defaultMQPullConsumer.setVipChannelEnabled(false); + } + + public String buildIntanceName() { + return Integer.toString(UtilAll.getPid()) + + "#" + System.nanoTime(); + } + + public void setConsumerGroup(String consumerGroup) { + defaultMQPullConsumer.setConsumerGroup(consumerGroup); + } + + public DefaultMQPullConsumer getDefaultMQPullConsumer() { + return defaultMQPullConsumer; + } + + public void start() { + try { + defaultMQPullConsumer.start(); + } catch (MQClientException e) { + throw new RuntimeException(e); + } + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + defaultMQPullConsumer.shutdown(); + } + }); + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/notify/NotifyManager.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/notify/NotifyManager.java new file mode 100644 index 00000000000..e88444521a1 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/notify/NotifyManager.java @@ -0,0 +1,286 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.notify; + +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.facade.MetaPersistManager; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.MessageEvent; +import org.apache.rocketmq.mqtt.common.model.RpcCode; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; +import org.apache.rocketmq.mqtt.ds.meta.FirstTopicManager; +import org.apache.rocketmq.mqtt.ds.meta.TopicNotExistException; +import org.apache.rocketmq.mqtt.ds.mq.MqFactory; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyRemotingClient; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + + +@Component +public class NotifyManager { + private static Logger logger = LoggerFactory.getLogger(NotifyManager.class); + private DefaultMQPushConsumer defaultMQPushConsumer; + private String dispatcherConsumerGroup = MixAll.CID_RMQ_SYS_PREFIX + "mqtt_event"; + private ScheduledThreadPoolExecutor scheduler; + private Set topics = new HashSet<>(); + private Map nodeFail = new ConcurrentHashMap<>(); + private final int NODE_FAIL_MAX_NUM = 3; + private NettyRemotingClient remotingClient; + private DefaultMQProducer defaultMQProducer; + + + @Resource + private ServiceConf serviceConf; + + @Resource + private MetaPersistManager metaPersistManager; + + @Resource + private FirstTopicManager firstTopicManager; + + @PostConstruct + public void init() throws MQClientException { + defaultMQPushConsumer = MqFactory.buildDefaultMQPushConsumer(dispatcherConsumerGroup, serviceConf.getProperties(), new Dispatcher()); + defaultMQPushConsumer.setPullInterval(1); + defaultMQPushConsumer.setConsumeMessageBatchMaxSize(64); + defaultMQPushConsumer.setPullBatchSize(32); + defaultMQPushConsumer.setConsumeThreadMin(32); + defaultMQPushConsumer.setConsumeThreadMax(64); + + defaultMQProducer = MqFactory.buildDefaultMQProducer(MixAll.CID_RMQ_SYS_PREFIX + "NotifyRetrySend", serviceConf.getProperties()); + defaultMQProducer.start(); + + try { + defaultMQPushConsumer.start(); + defaultMQProducer.start(); + } catch (Exception e) { + logger.error("", e); + } + + scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("Refresh_Notify_Topic_")); + scheduler.scheduleWithFixedDelay(() -> { + try { + refresh(); + } catch (Exception e) { + logger.error("", e); + } + }, 0, 5, TimeUnit.SECONDS); + + NettyClientConfig config = new NettyClientConfig(); + remotingClient = new NettyRemotingClient(config); + remotingClient.start(); + } + + private void refresh() throws MQClientException { + Set tmp = metaPersistManager.getAllFirstTopics(); + if (tmp == null || tmp.isEmpty()) { + return; + } + Set _topicList = new HashSet<>(); + for (String topic : tmp) { + try { + if (topic.equals(serviceConf.getClientRetryTopic())) { + // notify by RetryDriver self + continue; + } + firstTopicManager.checkFirstTopicIfCreated(topic); + _topicList.add(topic); + if (!topics.contains(topic)) { + subscribe(topic); + topics.add(topic); + } + } catch (TopicNotExistException e) { + logger.error("", e); + } + } + Iterator iterator = topics.iterator(); + while (iterator.hasNext()) { + String topic = iterator.next(); + if (!_topicList.contains(topic)) { + iterator.remove(); + unsubscribe(topic); + } + } + } + + private void subscribe(String topic) throws MQClientException { + defaultMQPushConsumer.subscribe(topic, "*"); + logger.warn("subscribe:{}", topic); + } + + private void unsubscribe(String topic) { + try { + logger.warn("unsubscribe:{}", topic); + defaultMQPushConsumer.unsubscribe(topic); + defaultMQPushConsumer.getDefaultMQPushConsumerImpl().getRebalanceImpl().getTopicSubscribeInfoTable().remove(topic); + defaultMQPushConsumer.getDefaultMQPushConsumerImpl().getmQClientFactory().getDefaultMQProducer() + .getDefaultMQProducerImpl().getTopicPublishInfoTable().remove(topic); + } catch (Exception e) { + logger.error("{}", topic, e); + } + } + + class Dispatcher implements MessageListenerConcurrently { + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + try { + Set messageEvents = new HashSet<>(); + for (MessageExt message : msgs) { + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setBrokerName(context.getMessageQueue().getBrokerName()); + setPubTopic(messageEvent, message); + String namespace = message.getUserProperty(Constants.PROPERTY_NAMESPACE); + messageEvent.setNamespace(namespace); + messageEvents.add(messageEvent); + } + notifyMessage(messageEvents); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } catch (Exception e) { + logger.error("", e); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + } + } + + private void setPubTopic(MessageEvent messageEvent, MessageExt message) { + if (StringUtils.isNotBlank(message.getUserProperty(Constants.PROPERTY_ORIGIN_MQTT_TOPIC))) { + // from mqtt + messageEvent.setPubTopic(message.getUserProperty(Constants.PROPERTY_ORIGIN_MQTT_TOPIC)); + return; + } + if (StringUtils.isNotBlank(message.getUserProperty(LmqQueueStore.PROPERTY_INNER_MULTI_DISPATCH))) { + // maybe from rmq + String s = message.getUserProperty(LmqQueueStore.PROPERTY_INNER_MULTI_DISPATCH); + String[] lmqSet = s.split(LmqQueueStore.MULTI_DISPATCH_QUEUE_SPLITTER); + for (String lmq : lmqSet) { + if (TopicUtils.isWildCard(lmq)) { + continue; + } + if (!lmq.contains(LmqQueueStore.LMQ_PREFIX)) { + continue; + } + messageEvent.setPubTopic(lmq.replace(LmqQueueStore.LMQ_PREFIX, "")); + } + } + } + + public void notifyMessage(Set messageEvents) throws + MQBrokerException, RemotingException, InterruptedException, MQClientException { + Set connectorNodes = metaPersistManager.getConnectNodeSet(); + if (connectorNodes == null || connectorNodes.isEmpty()) { + throw new RemotingException("No Connect Nodes"); + } + for (String node : connectorNodes) { + boolean result = false; + try { + AtomicInteger nodeFailCount = nodeFail.get(node); + if (nodeFailCount == null) { + nodeFailCount = new AtomicInteger(); + nodeFail.putIfAbsent(node, nodeFailCount); + } + if (nodeFailCount.get() > NODE_FAIL_MAX_NUM) { + sendEventRetryMsg(messageEvents, 1, node, 0); + continue; + } + if (result = doNotify(node, messageEvents)) { + nodeFailCount.set(0); + continue; + } + nodeFailCount.incrementAndGet(); + } catch (Exception e) { + logger.error("", e); + result = false; + } finally { + if (!result) { + sendEventRetryMsg(messageEvents, 1, node, 0); + } + } + } + } + + protected boolean doNotify(String node, Set messageEvents) { + Set connectorNodes = metaPersistManager.getConnectNodeSet(); + if (connectorNodes == null || connectorNodes.isEmpty()) { + return false; + } + if (!connectorNodes.contains(node)) { + return true; + } + try { + RemotingCommand eventCommand = createMsgEventCommand(messageEvents); + RemotingCommand response = remotingClient.invokeSync(node + ":" + serviceConf.getCsRpcPort(), eventCommand, 1000); + return response.getCode() == RpcCode.SUCCESS; + } catch (Exception e) { + logger.error("fail notify {}", node, e); + return false; + } + } + + private RemotingCommand createMsgEventCommand(Set messageEvents) { + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RpcCode.CMD_NOTIFY_MQTT_MESSAGE, + null); + remotingCommand.setBody(JSONObject.toJSONString(messageEvents).getBytes(StandardCharsets.UTF_8)); + return remotingCommand; + } + + protected void sendEventRetryMsg(Set messageEvents, int delayLevel, String node, int retryTime) + throws InterruptedException, RemotingException, MQClientException, + MQBrokerException { + if (retryTime >= serviceConf.getEventNotifyRetryMaxTime()) { + return; + } + Message message = new Message(); + message.setTopic(serviceConf.getEventNotifyRetryTopic()); + message.setBody(JSONObject.toJSONString(messageEvents).getBytes(StandardCharsets.UTF_8)); + message.setDelayTimeLevel(delayLevel); + message.putUserProperty(Constants.PROPERTY_MQTT_MSG_EVENT_RETRY_NODE, node); + message.putUserProperty(Constants.PROPERTY_MQTT_MSG_EVENT_RETRY_TIME, String.valueOf(retryTime + 1)); + defaultMQProducer.send(message); + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/notify/NotifyRetryManager.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/notify/NotifyRetryManager.java new file mode 100644 index 00000000000..4b31d6e9ee2 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/notify/NotifyRetryManager.java @@ -0,0 +1,97 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.notify; + +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.MessageEvent; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; +import org.apache.rocketmq.mqtt.ds.mq.MqFactory; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Component +public class NotifyRetryManager { + private static Logger logger = LoggerFactory.getLogger(NotifyRetryManager.class); + private DefaultMQPushConsumer defaultMQPushConsumer; + + @Resource + private NotifyManager notifyManager; + + @Resource + private ServiceConf serviceConf; + + @PostConstruct + public void init() throws MQClientException { + defaultMQPushConsumer = MqFactory.buildDefaultMQPushConsumer(MixAll.CID_RMQ_SYS_PREFIX + "notify_retry", + serviceConf.getProperties(), new RetryNotify()); + defaultMQPushConsumer.subscribe(serviceConf.getEventNotifyRetryTopic(), "*"); + defaultMQPushConsumer.start(); + } + + class RetryNotify implements MessageListenerConcurrently { + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + try { + for (MessageExt messageExt : msgs) { + doRetryNotify(messageExt); + } + } catch (Exception e) { + logger.error("", e); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + } + + private void doRetryNotify(MessageExt messageExt) + throws InterruptedException, RemotingException, MQClientException, MQBrokerException { + String payload = new String(messageExt.getBody(), StandardCharsets.UTF_8); + Set events = new HashSet<>(JSONObject.parseArray(payload, MessageEvent.class)); + String node = messageExt.getUserProperty(Constants.PROPERTY_MQTT_MSG_EVENT_RETRY_NODE); + String retryTime = messageExt.getUserProperty(Constants.PROPERTY_MQTT_MSG_EVENT_RETRY_TIME); + if (StringUtils.isBlank(node)) { + return; + } + if (notifyManager.doNotify(node, events)) { + return; + } + notifyManager.sendEventRetryMsg(events, 2, node, retryTime != null ? Integer.parseInt(retryTime) : 1); + } +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/store/LmqOffsetStoreManager.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/store/LmqOffsetStoreManager.java new file mode 100644 index 00000000000..4d5e8923a1c --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/store/LmqOffsetStoreManager.java @@ -0,0 +1,155 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.store; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.protocol.ResponseCode; +import org.apache.rocketmq.common.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.common.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.mqtt.common.facade.LmqOffsetStore; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.QueueOffset; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; +import org.apache.rocketmq.mqtt.ds.meta.FirstTopicManager; +import org.apache.rocketmq.mqtt.ds.mq.MqFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@Component +public class LmqOffsetStoreManager implements LmqOffsetStore { + private static Logger logger = LoggerFactory.getLogger(LmqOffsetStoreManager.class); + private DefaultMQPullConsumer defaultMQPullConsumer; + + @Resource + private ServiceConf serviceConf; + + @Resource + private FirstTopicManager firstTopicManager; + + @PostConstruct + public void init() throws MQClientException { + defaultMQPullConsumer = MqFactory.buildDefaultMQPullConsumer(MixAll.CID_RMQ_SYS_PREFIX + "LMQ_OFFSET" + + "", serviceConf.getProperties()); + defaultMQPullConsumer.setConsumerPullTimeoutMillis(2000); + defaultMQPullConsumer.start(); + } + + @Override + public void save(String clientId, Map> offsetMap) { + if (offsetMap == null || offsetMap.isEmpty()) { + return; + } + for (Map.Entry> entry : offsetMap.entrySet()) { + Map tmpBrokerAddressMap = findBrokers(entry.getKey()); + if (tmpBrokerAddressMap == null || tmpBrokerAddressMap.isEmpty()) { + continue; + } + for (Map.Entry each : entry.getValue().entrySet()) { + try { + Queue queue = each.getKey(); + if (StringUtils.isBlank(queue.getBrokerName())) { + continue; + } + String brokerAddress = tmpBrokerAddressMap.get(queue.getBrokerName()); + QueueOffset queueOffset = each.getValue(); + UpdateConsumerOffsetRequestHeader updateHeader = new UpdateConsumerOffsetRequestHeader(); + updateHeader.setTopic(queue.getQueueName()); + updateHeader.setConsumerGroup(LmqQueueStore.LMQ_PREFIX + clientId); + updateHeader.setQueueId((int) queue.getQueueId()); + updateHeader.setCommitOffset(queueOffset.getOffset()); + defaultMQPullConsumer + .getDefaultMQPullConsumerImpl() + .getRebalanceImpl() + .getmQClientFactory() + .getMQClientAPIImpl().updateConsumerOffset(brokerAddress, updateHeader, 1000); + } catch (Exception e) { + logger.error("", e); + } + } + } + } + + @Override + public CompletableFuture> getOffset(String clientId, Subscription subscription) { + return CompletableFuture.supplyAsync(() -> { + Map map = new HashMap<>(); + Map tmpBrokerAddressMap = findBrokers(subscription); + if (tmpBrokerAddressMap == null || tmpBrokerAddressMap.isEmpty()) { + return map; + } + for (Map.Entry entry : tmpBrokerAddressMap.entrySet()) { + Queue queue = new Queue(0, subscription.toQueueName(), entry.getKey()); + String brokerAddress = entry.getValue(); + QueueOffset queueOffset = new QueueOffset(); + map.put(queue, queueOffset); + try { + QueryConsumerOffsetRequestHeader queryHeader = new QueryConsumerOffsetRequestHeader(); + queryHeader.setTopic(queue.getQueueName()); + queryHeader.setConsumerGroup(LmqQueueStore.LMQ_PREFIX + clientId); + queryHeader.setQueueId((int) queue.getQueueId()); + long offset = defaultMQPullConsumer + .getDefaultMQPullConsumerImpl() + .getRebalanceImpl() + .getmQClientFactory() + .getMQClientAPIImpl() + .queryConsumerOffset(brokerAddress, queryHeader, 1000); + queueOffset.setOffset(offset); + } catch (MQBrokerException e) { + if (ResponseCode.QUERY_NOT_FOUND == e.getResponseCode()) { + queueOffset.setOffset(Long.MAX_VALUE); + } + } catch (Exception e) { + logger.error("{}", clientId, e); + throw new RuntimeException(e); + } + } + return map; + }); + } + + private Map findBrokers(Subscription subscription) { + String firstTopic = subscription.toFirstTopic(); + if (subscription.isRetry()) { + firstTopic = serviceConf.getClientRetryTopic(); + } + if (subscription.isP2p()) { + if (StringUtils.isNotBlank(serviceConf.getClientP2pTopic())) { + firstTopic = serviceConf.getClientP2pTopic(); + } else { + firstTopic = serviceConf.getClientRetryTopic(); + } + } + return firstTopicManager.getBrokerAddressMap(firstTopic); + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/store/LmqQueueStoreManager.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/store/LmqQueueStoreManager.java new file mode 100644 index 00000000000..fac7c827b0f --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/store/LmqQueueStoreManager.java @@ -0,0 +1,427 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.store; + +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.PullCallback; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.FindBrokerResult; +import org.apache.rocketmq.client.impl.consumer.PullAPIWrapper; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.common.sysflag.PullSysFlag; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.model.*; +import org.apache.rocketmq.mqtt.common.util.NamespaceUtil; +import org.apache.rocketmq.mqtt.common.util.StatUtil; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; +import org.apache.rocketmq.mqtt.ds.meta.FirstTopicManager; +import org.apache.rocketmq.mqtt.ds.mq.MqFactory; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +@Component +public class LmqQueueStoreManager implements LmqQueueStore { + private static Logger logger = LoggerFactory.getLogger(LmqQueueStoreManager.class); + private PullAPIWrapper pullAPIWrapper; + private DefaultMQPullConsumer defaultMQPullConsumer; + private DefaultMQProducer defaultMQProducer; + private String consumerGroup = MixAll.CID_RMQ_SYS_PREFIX + "LMQ_PULL"; + private Map> topic2Brokers = new ConcurrentHashMap<>(); + + @Resource + private ServiceConf serviceConf; + + @Resource + private FirstTopicManager firstTopicManager; + + @PostConstruct + public void init() throws MQClientException { + defaultMQPullConsumer = MqFactory.buildDefaultMQPullConsumer(consumerGroup, serviceConf.getProperties()); + defaultMQPullConsumer.setConsumerPullTimeoutMillis(2000); + defaultMQPullConsumer.start(); + pullAPIWrapper = defaultMQPullConsumer.getDefaultMQPullConsumerImpl().getPullAPIWrapper(); + + defaultMQProducer = MqFactory.buildDefaultMQProducer("GID_LMQ_SEND", serviceConf.getProperties()); + defaultMQProducer.setRetryTimesWhenSendAsyncFailed(0); + defaultMQProducer.start(); + } + + private org.apache.rocketmq.common.message.Message toMQMessage(Message finalMessage) { + Message message = finalMessage.copy(); + org.apache.rocketmq.common.message.Message mqMessage = new org.apache.rocketmq.common.message.Message(finalMessage.getFirstTopic(), message.getPayload()); + MessageAccessor.putProperty(mqMessage, MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, message.getMsgId()); + mqMessage.putUserProperty(Constants.PROPERTY_ORIGIN_MQTT_TOPIC, message.getOriginTopic()); + if (message.getUserProperty(Message.extPropertyQoS) != null) { + mqMessage.putUserProperty(Constants.PROPERTY_MQTT_QOS, message.getUserProperty(Message.extPropertyQoS)); + } + if (message.getUserProperty(Message.extPropertyCleanSessionFlag) != null) { + mqMessage.putUserProperty(Constants.PROPERTY_MQTT_CLEAN_SESSION, message.getUserProperty(Message.extPropertyCleanSessionFlag)); + } + if (message.getUserProperty(Message.extPropertyClientId) != null) { + mqMessage.putUserProperty(Constants.PROPERTY_MQTT_CLIENT, NamespaceUtil.decodeOriginResource(message.getUserProperty(Message.extPropertyClientId))); + message.clearUserProperty(Message.extPropertyClientId); + } + mqMessage.putUserProperty(Constants.PROPERTY_MQTT_RETRY_TIMES, String.valueOf(message.getRetry())); + Map userProps = message.getUserProperties(); + if (userProps != null && !userProps.isEmpty()) { + mqMessage.putUserProperty(Constants.PROPERTY_MQTT_EXT_DATA, JSONObject.toJSONString(userProps)); + } + return mqMessage; + } + + private Message toLmqMessage(Queue queue, MessageExt mqMessage) { + Message message = new Message(); + message.setMsgId(mqMessage.getMsgId()); + message.setOffset(parseLmqOffset(queue, mqMessage)); + if (StringUtils.isNotBlank(mqMessage.getUserProperty(Constants.PROPERTY_ORIGIN_MQTT_TOPIC))) { + message.setOriginTopic(mqMessage.getUserProperty(Constants.PROPERTY_ORIGIN_MQTT_TOPIC)); + } else if (StringUtils.isNotBlank(message.getUserProperty(LmqQueueStore.PROPERTY_INNER_MULTI_DISPATCH))) { + // maybe from rmq + String s = message.getUserProperty(LmqQueueStore.PROPERTY_INNER_MULTI_DISPATCH); + String[] lmqSet = s.split(LmqQueueStore.MULTI_DISPATCH_QUEUE_SPLITTER); + for (String lmq : lmqSet) { + if (TopicUtils.isWildCard(lmq)) { + continue; + } + message.setOriginTopic(lmq.replace(LmqQueueStore.LMQ_PREFIX, "")); + } + } + message.setFirstTopic(mqMessage.getTopic()); + message.setPayload(mqMessage.getBody()); + message.setStoreTimestamp(mqMessage.getStoreTimestamp()); + message.setBornTimestamp(mqMessage.getBornTimestamp()); + if (StringUtils.isNotBlank(mqMessage.getUserProperty(Constants.PROPERTY_MQTT_RETRY_TIMES))) { + message.setRetry(Integer.parseInt(mqMessage.getUserProperty(Constants.PROPERTY_MQTT_RETRY_TIMES))); + } + if (StringUtils.isNotBlank(mqMessage.getUserProperty(Constants.PROPERTY_MQTT_EXT_DATA))) { + message.getUserProperties().putAll( + JSONObject.parseObject(mqMessage.getUserProperty(Constants.PROPERTY_MQTT_EXT_DATA), + new TypeReference>() { + })); + } + return message; + } + + private long parseLmqOffset(Queue queue, MessageExt mqMessage) { + String multiDispatchQueue = mqMessage.getProperty(PROPERTY_INNER_MULTI_DISPATCH); + if (StringUtils.isBlank(multiDispatchQueue)) { + return mqMessage.getQueueOffset(); + } + String multiQueueOffset = mqMessage.getProperty(PROPERTY_INNER_MULTI_QUEUE_OFFSET); + if (StringUtils.isBlank(multiQueueOffset)) { + return mqMessage.getQueueOffset(); + } + String[] queues = multiDispatchQueue.split(MULTI_DISPATCH_QUEUE_SPLITTER); + String[] queueOffsets = multiQueueOffset.split(MULTI_DISPATCH_QUEUE_SPLITTER); + for (int i = 0; i < queues.length; i++) { + if ((LMQ_PREFIX + queue.getQueueName()).equals(queues[i])) { + return Long.parseLong(queueOffsets[i]); + } + } + return mqMessage.getQueueOffset(); + } + + @Override + public CompletableFuture putMessage(Set queues, Message message) { + CompletableFuture result = new CompletableFuture<>(); + org.apache.rocketmq.common.message.Message mqMessage = toMQMessage(message); + mqMessage.setTags(Constants.MQTT_TAG); + mqMessage.putUserProperty(PROPERTY_INNER_MULTI_DISPATCH, + StringUtils.join( + queues.stream().map(s -> LMQ_PREFIX + s).collect(Collectors.toSet()), + MULTI_DISPATCH_QUEUE_SPLITTER)); + try { + long start = System.currentTimeMillis(); + defaultMQProducer.send(mqMessage, + new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + result.complete(toStoreResult(sendResult)); + StatUtil.addInvoke("lmqWrite", System.currentTimeMillis() - start); + } + + @Override + public void onException(Throwable e) { + logger.error("", e); + result.completeExceptionally(e); + StatUtil.addInvoke("lmqWrite", System.currentTimeMillis() - start, false); + } + }); + } catch (Throwable e) { + result.completeExceptionally(e); + } + return result; + } + + @Override + public CompletableFuture pullMessage(String firstTopic, Queue queue, QueueOffset queueOffset, long count) { + CompletableFuture result = new CompletableFuture<>(); + try { + MessageQueue messageQueue = new MessageQueue(firstTopic, queue.getBrokerName(), (int) queue.getQueueId()); + long start = System.currentTimeMillis(); + String lmqTopic = LMQ_PREFIX + queue.getQueueName(); + pull(lmqTopic, messageQueue, queueOffset.getOffset(), (int) count, new PullCallback() { + @Override + public void onSuccess(org.apache.rocketmq.client.consumer.PullResult pullResult) { + result.complete(toLmqPullResult(queue, pullResult)); + StatUtil.addInvoke("lmqPull", System.currentTimeMillis() - start); + StatUtil.addPv(pullResult.getPullStatus().name(), 1); + } + + @Override + public void onException(Throwable e) { + logger.error("", e); + result.completeExceptionally(e); + StatUtil.addInvoke("lmqPull", System.currentTimeMillis() - start, false); + } + }); + } catch (Throwable e) { + result.completeExceptionally(e); + } + return result; + } + + @Override + public CompletableFuture pullLastMessages(String firstTopic, Queue queue, long count) { + CompletableFuture maxResult = queryQueueMaxOffset(queue); + return maxResult.thenCompose(maxId -> { + long begin = maxId - count; + if (begin < 0) { + begin = 0; + } + QueueOffset queueOffset = new QueueOffset(); + queueOffset.setOffset(begin); + return pullMessage(firstTopic, queue, queueOffset, count); + }); + } + + @Override + public CompletableFuture queryQueueMaxOffset(Queue queue) { + return CompletableFuture.supplyAsync(() -> { + try { + return maxOffset(queue); + } catch (MQClientException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public Set getReadableBrokers(String firstTopic) { + return firstTopicManager.getReadableBrokers(firstTopic); + } + + @Override + public String getClientRetryTopic() { + return serviceConf.getClientRetryTopic(); + } + + @Override + public String getClientP2pTopic() { + return serviceConf.getClientP2pTopic(); + } + + + private StoreResult toStoreResult(SendResult sendResult) { + StoreResult storeResult = new StoreResult(); + Queue queue = new Queue(); + queue.setQueueId(sendResult.getMessageQueue().getQueueId()); + queue.setBrokerName(sendResult.getMessageQueue().getBrokerName()); + storeResult.setQueue(queue); + storeResult.setMsgId(sendResult.getMsgId()); + return storeResult; + } + + private PullResult toLmqPullResult(Queue queue, org.apache.rocketmq.client.consumer.PullResult pullResult) { + PullResult lmqPullResult = new PullResult(); + if (PullStatus.OFFSET_ILLEGAL.equals(pullResult.getPullStatus())) { + lmqPullResult.setCode(PullResult.PULL_OFFSET_MOVED); + QueueOffset nextQueueOffset = new QueueOffset(); + nextQueueOffset.setOffset(pullResult.getNextBeginOffset()); + lmqPullResult.setNextQueueOffset(nextQueueOffset); + } else { + lmqPullResult.setCode(PullResult.PULL_SUCCESS); + } + List messageExtList = pullResult.getMsgFoundList(); + if (messageExtList != null && !messageExtList.isEmpty()) { + List messageList = new ArrayList<>(messageExtList.size()); + for (MessageExt messageExt : messageExtList) { + Message lmqMessage = toLmqMessage(queue, messageExt); + messageList.add(lmqMessage); + } + lmqPullResult.setMessageList(messageList); + } + return lmqPullResult; + } + + private void pull(String lmqTopic, MessageQueue mq, long offset, int maxNums, PullCallback pullCallback) + throws MQClientException, RemotingException, InterruptedException { + try { + int sysFlag = PullSysFlag.buildSysFlag(false, false, true, false); + long timeoutMillis = 3000L; + pullKernelImpl( + lmqTopic, + mq, + "*", + "TAG", + 0L, + offset, + maxNums, + sysFlag, + 0, + 5000L, + timeoutMillis, + CommunicationMode.ASYNC, + new PullCallback() { + @Override + public void onSuccess(org.apache.rocketmq.client.consumer.PullResult pullResult) { + org.apache.rocketmq.client.consumer.PullResult userPullResult = pullAPIWrapper.processPullResult(mq, pullResult, new SubscriptionData()); + pullCallback.onSuccess(userPullResult); + } + + @Override + public void onException(Throwable e) { + pullCallback.onException(e); + } + }); + } catch (MQBrokerException e) { + throw new MQClientException("pullAsync unknow exception", e); + } + } + + public org.apache.rocketmq.client.consumer.PullResult pullKernelImpl( + final String lmqTopic, + final MessageQueue mq, + final String subExpression, + final String expressionType, + final long subVersion, + final long offset, + final int maxNums, + final int sysFlag, + final long commitOffset, + final long brokerSuspendMaxTimeMillis, + final long timeoutMillis, + final CommunicationMode communicationMode, + final PullCallback pullCallback + ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + MQClientInstance mQClientFactory = defaultMQPullConsumer.getDefaultMQPullConsumerImpl().getRebalanceImpl().getmQClientFactory(); + FindBrokerResult findBrokerResult = + mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), + pullAPIWrapper.recalculatePullFromWhichNode(mq), false); + if (null == findBrokerResult) { + mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); + findBrokerResult = + mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), + pullAPIWrapper.recalculatePullFromWhichNode(mq), false); + } + + if (findBrokerResult != null) { + { + // check version + if (!ExpressionType.isTagType(expressionType) + && findBrokerResult.getBrokerVersion() < MQVersion.Version.V4_1_0_SNAPSHOT.ordinal()) { + throw new MQClientException("The broker[" + mq.getBrokerName() + ", " + + findBrokerResult.getBrokerVersion() + "] does not upgrade to support for filter message by " + expressionType, null); + } + } + int sysFlagInner = sysFlag; + + if (findBrokerResult.isSlave()) { + sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner); + } + + PullMessageRequestHeader requestHeader = new PullMessageRequestHeader(); + requestHeader.setConsumerGroup(this.consumerGroup); + requestHeader.setTopic(lmqTopic); + requestHeader.setQueueId(mq.getQueueId()); + requestHeader.setQueueOffset(offset); + requestHeader.setMaxMsgNums(maxNums); + requestHeader.setSysFlag(sysFlagInner); + requestHeader.setCommitOffset(commitOffset); + requestHeader.setSuspendTimeoutMillis(brokerSuspendMaxTimeMillis); + requestHeader.setSubscription(subExpression); + requestHeader.setSubVersion(subVersion); + requestHeader.setExpressionType(expressionType); + + String brokerAddr = findBrokerResult.getBrokerAddr(); + org.apache.rocketmq.client.consumer.PullResult pullResult = + mQClientFactory.getMQClientAPIImpl().pullMessage( + brokerAddr, + requestHeader, + timeoutMillis, + communicationMode, + pullCallback); + + return pullResult; + } + throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null); + } + + private long maxOffset(Queue queue) throws MQClientException { + String lmqTopic = LMQ_PREFIX + queue.getQueueName(); + MQClientInstance mQClientFactory = defaultMQPullConsumer.getDefaultMQPullConsumerImpl().getRebalanceImpl().getmQClientFactory(); + String brokerAddr = mQClientFactory.findBrokerAddressInPublish(queue.getBrokerName()); + if (null == brokerAddr) { + mQClientFactory.updateTopicRouteInfoFromNameServer(queue.toFirstTopic()); + brokerAddr = mQClientFactory.findBrokerAddressInPublish(queue.getBrokerName()); + } + + if (brokerAddr != null) { + try { + return mQClientFactory.getMQClientAPIImpl().getMaxOffset(brokerAddr, lmqTopic, (int) queue.getQueueId(), 3000L); + } catch (Exception e) { + throw new MQClientException("Invoke Broker[" + brokerAddr + "] exception", e); + } + } + + throw new MQClientException("The broker[" + queue.getBrokerName() + "] not exist", null); + } +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/UpstreamProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/UpstreamProcessor.java new file mode 100644 index 00000000000..05763fe3421 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/UpstreamProcessor.java @@ -0,0 +1,30 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.upstream; + +import io.netty.handler.codec.mqtt.MqttMessage; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; + +import java.util.concurrent.CompletableFuture; + +public interface UpstreamProcessor { + CompletableFuture process(MqttMessageUpContext context, MqttMessage message); +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/UpstreamProcessorManager.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/UpstreamProcessorManager.java new file mode 100644 index 00000000000..f02f7b7955e --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/UpstreamProcessorManager.java @@ -0,0 +1,79 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.upstream; + +import io.netty.handler.codec.mqtt.*; +import org.apache.rocketmq.mqtt.common.hook.*; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; +import org.apache.rocketmq.mqtt.ds.upstream.processor.*; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.concurrent.CompletableFuture; + +@Component +public class UpstreamProcessorManager extends AbstractUpstreamHook { + + @Resource + private UpstreamHookManager upstreamHookManager; + + @Resource + private ConnectProcessor connectProcessor; + + @Resource + private PublishProcessor publishProcessor; + + @Resource + private SubscribeProcessor subscribeProcessor; + + @Resource + private UnSubscribeProcessor unSubscribeProcessor; + + @Resource + private DisconnectProcessor disconnectProcessor; + + @PostConstruct + @Override + public void register() { + upstreamHookManager.addHook(UpstreamHookEnum.UPSTREAM_PROCESS.ordinal(), this); + } + + @Override + public CompletableFuture processMqttMessage(MqttMessageUpContext context, MqttMessage message) { + switch (message.fixedHeader().messageType()) { + case CONNECT: + return connectProcessor.process(context, message); + case PUBLISH: + return publishProcessor.process(context, message); + case SUBSCRIBE: + return subscribeProcessor.process(context, message); + case UNSUBSCRIBE: + return unSubscribeProcessor.process(context, message); + case DISCONNECT: + return disconnectProcessor.process(context, message); + default: + } + CompletableFuture hookResult = new CompletableFuture<>(); + hookResult.complete(new HookResult(HookResult.FAIL, "InvalidMqttMsgType", null)); + return hookResult; + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/BaseProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/BaseProcessor.java new file mode 100644 index 00000000000..77b3c955bd9 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/BaseProcessor.java @@ -0,0 +1,37 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.upstream.processor; + +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.mqtt.ds.upstream.UpstreamProcessor; + +import java.util.concurrent.*; + +public abstract class BaseProcessor implements UpstreamProcessor { + + protected ThreadPoolExecutor executor = new ThreadPoolExecutor( + 8, + 16, + 1, + TimeUnit.MINUTES, + new LinkedBlockingQueue<>(10000), + new ThreadFactoryImpl("UpstreamBaseProcessor_")); + +} \ No newline at end of file diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/ConnectProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/ConnectProcessor.java new file mode 100644 index 00000000000..664eb1f6994 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/ConnectProcessor.java @@ -0,0 +1,46 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.upstream.processor; + +import io.netty.handler.codec.mqtt.MqttMessage; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; +import org.apache.rocketmq.mqtt.ds.upstream.UpstreamProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.CompletableFuture; + +@Component +public class ConnectProcessor extends BaseProcessor implements UpstreamProcessor { + private static Logger logger = LoggerFactory.getLogger(ConnectProcessor.class); + + @Resource + private ServiceConf serviceConf; + + @Override + public CompletableFuture process(MqttMessageUpContext context, MqttMessage message) { + return HookResult.newHookResult(HookResult.SUCCESS, null, null); + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/DisconnectProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/DisconnectProcessor.java new file mode 100644 index 00000000000..7c597cd724a --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/DisconnectProcessor.java @@ -0,0 +1,37 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.upstream.processor; + +import io.netty.handler.codec.mqtt.MqttMessage; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; +import org.apache.rocketmq.mqtt.ds.upstream.UpstreamProcessor; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +public class DisconnectProcessor extends BaseProcessor implements UpstreamProcessor { + + @Override + public CompletableFuture process(MqttMessageUpContext context, MqttMessage message) { + return HookResult.newHookResult(HookResult.SUCCESS, null, null); + } +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/PublishProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/PublishProcessor.java new file mode 100644 index 00000000000..512ffa06de1 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/PublishProcessor.java @@ -0,0 +1,77 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.upstream.processor; + +import com.alibaba.fastjson.JSONObject; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttPublishMessage; +import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; +import org.apache.rocketmq.mqtt.common.model.MqttTopic; +import org.apache.rocketmq.mqtt.common.model.StoreResult; +import org.apache.rocketmq.mqtt.common.util.MessageUtil; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.ds.meta.FirstTopicManager; +import org.apache.rocketmq.mqtt.ds.meta.WildcardManager; +import org.apache.rocketmq.mqtt.ds.upstream.UpstreamProcessor; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.nio.charset.StandardCharsets; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +@Component +public class PublishProcessor implements UpstreamProcessor { + + @Resource + private LmqQueueStore lmqQueueStore; + + @Resource + private WildcardManager wildcardManager; + + @Resource + private FirstTopicManager firstTopicManager; + + + @Override + public CompletableFuture process(MqttMessageUpContext context, MqttMessage mqttMessage) { + MqttPublishMessage mqttPublishMessage = (MqttPublishMessage) mqttMessage; + String msgId = MessageClientIDSetter.createUniqID(); + MqttPublishVariableHeader variableHeader = mqttPublishMessage.variableHeader(); + String originTopic = variableHeader.topicName(); + String pubTopic = TopicUtils.normalizeTopic(originTopic); + MqttTopic mqttTopic = TopicUtils.decode(pubTopic); + firstTopicManager.checkFirstTopicIfCreated(mqttTopic.getFirstTopic()); + Set queueNames = wildcardManager.matchQueueSetByMsgTopic(pubTopic, context.getNamespace()); + Message message = MessageUtil.toMessage(mqttPublishMessage); + message.setMsgId(msgId); + message.setBornTimestamp(System.currentTimeMillis()); + message.setFirstTopic(mqttTopic.getFirstTopic()); + CompletableFuture storeResult = lmqQueueStore.putMessage(queueNames, message); + return storeResult.thenCompose(storeResult1 -> HookResult.newHookResult(HookResult.SUCCESS, null, + JSONObject.toJSONString(storeResult1).getBytes(StandardCharsets.UTF_8))); + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/SubscribeProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/SubscribeProcessor.java new file mode 100644 index 00000000000..53182144a2b --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/SubscribeProcessor.java @@ -0,0 +1,56 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.upstream.processor; + +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttSubscribeMessage; +import io.netty.handler.codec.mqtt.MqttSubscribePayload; +import io.netty.handler.codec.mqtt.MqttTopicSubscription; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; +import org.apache.rocketmq.mqtt.common.model.MqttTopic; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.ds.meta.FirstTopicManager; +import org.apache.rocketmq.mqtt.ds.upstream.UpstreamProcessor; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Component +public class SubscribeProcessor implements UpstreamProcessor { + + @Resource + private FirstTopicManager firstTopicManager; + + @Override + public CompletableFuture process(MqttMessageUpContext context, MqttMessage message) { + MqttSubscribeMessage mqttSubscribeMessage = (MqttSubscribeMessage) message; + MqttSubscribePayload payload = mqttSubscribeMessage.payload(); + List mqttTopicSubscriptions = payload.topicSubscriptions(); + for (MqttTopicSubscription mqttTopicSubscription : mqttTopicSubscriptions) { + String topicFilter = TopicUtils.normalizeTopic(mqttTopicSubscription.topicName()); + MqttTopic mqttTopic = TopicUtils.decode(topicFilter); + firstTopicManager.checkFirstTopicIfCreated(mqttTopic.getFirstTopic()); + } + return HookResult.newHookResult(HookResult.SUCCESS, null, null); + } +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/UnSubscribeProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/UnSubscribeProcessor.java new file mode 100644 index 00000000000..1c60d202980 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/processor/UnSubscribeProcessor.java @@ -0,0 +1,53 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.upstream.processor; + +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttUnsubscribePayload; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.MqttMessageUpContext; +import org.apache.rocketmq.mqtt.common.model.MqttTopic; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.ds.meta.FirstTopicManager; +import org.apache.rocketmq.mqtt.ds.upstream.UpstreamProcessor; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.CompletableFuture; + +@Component +public class UnSubscribeProcessor implements UpstreamProcessor { + + @Resource + private FirstTopicManager firstTopicManager; + + @Override + public CompletableFuture process(MqttMessageUpContext context, MqttMessage message) { + MqttUnsubscribePayload payload = (MqttUnsubscribePayload) message.payload(); + if (payload.topics() != null && !payload.topics().isEmpty()) { + for (String topic : payload.topics()) { + String topicFilter = TopicUtils.normalizeTopic(topic); + MqttTopic mqttTopic = TopicUtils.decode(topicFilter); + firstTopicManager.checkFirstTopicIfCreated(mqttTopic.getFirstTopic()); + } + } + return HookResult.newHookResult(HookResult.SUCCESS, null, null); + } +} diff --git a/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestFirstTopicManager.java b/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestFirstTopicManager.java new file mode 100644 index 00000000000..88c9427631a --- /dev/null +++ b/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestFirstTopicManager.java @@ -0,0 +1,96 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.test; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.protocol.route.BrokerData; +import org.apache.rocketmq.common.protocol.route.QueueData; +import org.apache.rocketmq.common.protocol.route.TopicRouteData; +import org.apache.rocketmq.mqtt.common.facade.MetaPersistManager; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; +import org.apache.rocketmq.mqtt.ds.meta.FirstTopicManager; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TestFirstTopicManager { + + @Mock + private MetaPersistManager metaPersistManager; + + @Mock + private ServiceConf serviceConf; + + @Test + public void test() throws IllegalAccessException, RemotingException, InterruptedException, MQClientException { + FirstTopicManager firstTopicManager = new FirstTopicManager(); + DefaultMQAdminExt defaultMQAdminExt = mock(DefaultMQAdminExt.class); + + Cache topicExistCache = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(1, TimeUnit.MINUTES).build(); + Cache topicNotExistCache = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(1, TimeUnit.MINUTES).build(); + + FieldUtils.writeDeclaredField(firstTopicManager, "defaultMQAdminExt", defaultMQAdminExt, true); + FieldUtils.writeDeclaredField(firstTopicManager, "topicExistCache", topicExistCache, true); + FieldUtils.writeDeclaredField(firstTopicManager, "topicNotExistCache", topicNotExistCache, true); + + TopicRouteData topicRouteData = new TopicRouteData(); + + List brokerDatas = new ArrayList<>(); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, "test"); + BrokerData brokerData = new BrokerData("test", "test", brokerAddrs); + brokerDatas.add(brokerData); + topicRouteData.setBrokerDatas(brokerDatas); + + QueueData queueData = new QueueData(); + queueData.setPerm(6); + queueData.setReadQueueNums(1); + queueData.setBrokerName("test"); + List queueDatas = new ArrayList<>(); + queueDatas.add(queueData); + topicRouteData.setQueueDatas(queueDatas); + + when(defaultMQAdminExt.examineTopicRouteInfo(any())).thenReturn(topicRouteData); + firstTopicManager.checkFirstTopicIfCreated("test"); + + Assert.assertFalse(topicExistCache.getIfPresent("test") == null); + Assert.assertTrue("test".equals(firstTopicManager.getBrokerAddressMap("test").keySet().iterator().next())); + Assert.assertTrue("test".equals(firstTopicManager.getReadableBrokers("test").iterator().next())); + } + +} diff --git a/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestLmqQueueStoreManager.java b/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestLmqQueueStoreManager.java new file mode 100644 index 00000000000..01bc1060063 --- /dev/null +++ b/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestLmqQueueStoreManager.java @@ -0,0 +1,115 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.test; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.FindBrokerResult; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.consumer.DefaultMQPullConsumerImpl; +import org.apache.rocketmq.client.impl.consumer.PullAPIWrapper; +import org.apache.rocketmq.client.impl.consumer.RebalanceImpl; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.QueueOffset; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; +import org.apache.rocketmq.mqtt.ds.meta.FirstTopicManager; +import org.apache.rocketmq.mqtt.ds.store.LmqQueueStoreManager; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static org.apache.rocketmq.mqtt.common.facade.LmqQueueStore.PROPERTY_INNER_MULTI_DISPATCH; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestLmqQueueStoreManager { + + @Mock + private FirstTopicManager firstTopicManager; + + @Mock + private ServiceConf serviceConf; + + @Mock + private PullAPIWrapper pullAPIWrapper; + + @Mock + private DefaultMQPullConsumer defaultMQPullConsumer; + + @Mock + private DefaultMQProducer defaultMQProducer; + + private LmqQueueStoreManager lmqQueueStoreManager; + + @Before + public void before() throws IllegalAccessException { + lmqQueueStoreManager = new LmqQueueStoreManager(); + FieldUtils.writeDeclaredField(lmqQueueStoreManager, "firstTopicManager", firstTopicManager, true); + FieldUtils.writeDeclaredField(lmqQueueStoreManager, "serviceConf", serviceConf, true); + FieldUtils.writeDeclaredField(lmqQueueStoreManager, "pullAPIWrapper", pullAPIWrapper, true); + FieldUtils.writeDeclaredField(lmqQueueStoreManager, "defaultMQPullConsumer", defaultMQPullConsumer, true); + FieldUtils.writeDeclaredField(lmqQueueStoreManager, "defaultMQProducer", defaultMQProducer, true); + } + + @Test + public void testPutMessage() throws RemotingException, InterruptedException, MQClientException { + Set queues = new HashSet<>(Arrays.asList("test")); + Message message = new Message(); + message.setOriginTopic("test"); + lmqQueueStoreManager.putMessage(queues, message); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(org.apache.rocketmq.common.message.Message.class); + verify(defaultMQProducer).send(argumentCaptor.capture(), any(SendCallback.class)); + Assert.assertTrue(null != argumentCaptor.getValue().getUserProperty(PROPERTY_INNER_MULTI_DISPATCH)); + } + + @Test + public void testPullMessage() throws MQBrokerException, RemotingException, InterruptedException { + DefaultMQPullConsumerImpl defaultMQPullConsumerImpl = mock(DefaultMQPullConsumerImpl.class); + when(defaultMQPullConsumer.getDefaultMQPullConsumerImpl()).thenReturn(defaultMQPullConsumerImpl); + RebalanceImpl rebalanceImpl = mock(RebalanceImpl.class); + when(defaultMQPullConsumerImpl.getRebalanceImpl()).thenReturn(rebalanceImpl); + MQClientInstance mqClientInstance = mock(MQClientInstance.class); + when(rebalanceImpl.getmQClientFactory()).thenReturn(mqClientInstance); + MQClientAPIImpl mqClientAPI = mock(MQClientAPIImpl.class); + when(mqClientInstance.getMQClientAPIImpl()).thenReturn(mqClientAPI); + when(mqClientInstance.findBrokerAddressInSubscribe(any(), anyLong(), anyBoolean())).thenReturn(new FindBrokerResult("test", false)); + + lmqQueueStoreManager.pullMessage("test", new Queue(), new QueueOffset(), 1); + + verify(mqClientAPI).pullMessage(any(), any(), anyLong(), any(), any()); + } + +} diff --git a/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestNotifyManager.java b/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestNotifyManager.java new file mode 100644 index 00000000000..992e5e77717 --- /dev/null +++ b/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestNotifyManager.java @@ -0,0 +1,90 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.test; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.reflect.MethodUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.mqtt.common.facade.MetaPersistManager; +import org.apache.rocketmq.mqtt.common.model.MessageEvent; +import org.apache.rocketmq.mqtt.common.model.RpcCode; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; +import org.apache.rocketmq.mqtt.ds.meta.FirstTopicManager; +import org.apache.rocketmq.mqtt.ds.notify.NotifyManager; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.netty.NettyRemotingClient; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.HashSet; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestNotifyManager { + + @Mock + private MetaPersistManager metaPersistManager; + + @Mock + private FirstTopicManager firstTopicManager; + + @Mock + private DefaultMQPushConsumer defaultMQPushConsumer; + + @Mock + private NettyRemotingClient remotingClient; + + @Mock + private ServiceConf serviceConf; + + @Test + public void test() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, + MQClientException, RemotingException, InterruptedException, MQBrokerException { + NotifyManager notifyManager = new NotifyManager(); + FieldUtils.writeDeclaredField(notifyManager, "metaPersistManager", metaPersistManager, true); + FieldUtils.writeDeclaredField(notifyManager, "firstTopicManager", firstTopicManager, true); + FieldUtils.writeDeclaredField(notifyManager, "defaultMQPushConsumer", defaultMQPushConsumer, true); + FieldUtils.writeDeclaredField(notifyManager, "remotingClient", remotingClient, true); + FieldUtils.writeDeclaredField(notifyManager, "serviceConf", serviceConf, true); + + when(metaPersistManager.getAllFirstTopics()).thenReturn(new HashSet<>(Arrays.asList("test"))); + + MethodUtils.invokeMethod(notifyManager, true, "refresh"); + verify(defaultMQPushConsumer).subscribe(any(), anyString()); + + when(metaPersistManager.getConnectNodeSet()).thenReturn(new HashSet<>(Arrays.asList("test"))); + RemotingCommand response = mock(RemotingCommand.class); + when(response.getCode()).thenReturn(RpcCode.SUCCESS); + when(remotingClient.invokeSync(any(), any(), anyLong())).thenReturn(response); + + notifyManager.notifyMessage(new HashSet<>(Arrays.asList(new MessageEvent()))); + verify(remotingClient).invokeSync(any(), any(), anyLong()); + } + +} diff --git a/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestWildcardManager.java b/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestWildcardManager.java new file mode 100644 index 00000000000..a13cb884994 --- /dev/null +++ b/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/TestWildcardManager.java @@ -0,0 +1,58 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.ds.test; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.facade.MetaPersistManager; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.ds.meta.WildcardManager; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TestWildcardManager { + + @Test + public void test() throws IllegalAccessException, InterruptedException { + WildcardManager wildcardManager = new WildcardManager(); + MetaPersistManager metaPersistManager = mock(MetaPersistManager.class); + FieldUtils.writeDeclaredField(wildcardManager, "metaPersistManager", metaPersistManager, true); + + when(metaPersistManager.getAllFirstTopics()).thenReturn(new HashSet<>(Arrays.asList("test"))); + when(metaPersistManager.getWildcards(any())).thenReturn(new HashSet<>(Arrays.asList(TopicUtils.normalizeTopic("test/+")))); + + wildcardManager.init(); + Thread.sleep(1000L); + + Set set = wildcardManager.matchQueueSetByMsgTopic(TopicUtils.normalizeTopic("test/a"),""); + Assert.assertTrue(set.contains(TopicUtils.normalizeTopic("test/+"))); + } + +} diff --git a/mqtt-example/pom.xml b/mqtt-example/pom.xml new file mode 100644 index 00000000000..a13b30daeae --- /dev/null +++ b/mqtt-example/pom.xml @@ -0,0 +1,30 @@ + + + + rocketmq-mqtt + org.apache.rocketmq + 1.0.0-SNAPSHOT + + 4.0.0 + + mqtt-example + + + 8 + 8 + + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.2 + + + org.apache.rocketmq + mqtt-common + + + \ No newline at end of file diff --git a/mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/MqttConsumer.java b/mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/MqttConsumer.java new file mode 100644 index 00000000000..fa3fbbccae7 --- /dev/null +++ b/mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/MqttConsumer.java @@ -0,0 +1,98 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.example; + +import org.apache.rocketmq.mqtt.common.util.HmacSHA1Util; +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class MqttConsumer { + public static void main(String[] args) throws MqttException, NoSuchAlgorithmException, InvalidKeyException { + String brokerUrl = System.getenv("brokerUrl"); + MemoryPersistence memoryPersistence = new MemoryPersistence(); + String firstTopic = System.getenv("firstTopic"); + String recvClientId = "recv01"; + MqttConnectOptions mqttConnectOptions = buildMqttConnectOptions(recvClientId); + MqttClient mqttClient = new MqttClient(brokerUrl, recvClientId, memoryPersistence); + mqttClient.setTimeToWait(5000L); + mqttClient.setCallback(new MqttCallbackExtended() { + @Override + public void connectComplete(boolean reconnect, String serverURI) { + System.out.println(recvClientId + " connect success to " + serverURI); + try { + final String topicFilter[] = {firstTopic + "/r1", firstTopic + "/r/+", firstTopic + "/r2"}; + final int[] qos = {1, 1, 2}; + mqttClient.subscribe(topicFilter, qos); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void connectionLost(Throwable throwable) { + throwable.printStackTrace(); + } + + @Override + public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception { + try { + String payload = new String(mqttMessage.getPayload()); + String[] ss = payload.split("_"); + System.out.println(now() + "receive:" + topic + "," + payload + + " ---- rt:" + (System.currentTimeMillis() - Long.parseLong(ss[1]))); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + } + }); + + try { + mqttClient.connect(mqttConnectOptions); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("connect fail"); + } + } + + private static MqttConnectOptions buildMqttConnectOptions(String clientId) throws NoSuchAlgorithmException, InvalidKeyException { + MqttConnectOptions connOpts = new MqttConnectOptions(); + connOpts.setCleanSession(true); + connOpts.setKeepAliveInterval(60); + connOpts.setAutomaticReconnect(true); + connOpts.setMaxInflight(10000); + connOpts.setUserName(System.getenv("username")); + connOpts.setPassword(HmacSHA1Util.macSignature(clientId, System.getenv("secretKey")).toCharArray()); + return connOpts; + } + + private static String now() { + SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); + return sf.format(new Date()) + "\t"; + } +} diff --git a/mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/MqttProducer.java b/mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/MqttProducer.java new file mode 100644 index 00000000000..452cc2c9d9a --- /dev/null +++ b/mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/MqttProducer.java @@ -0,0 +1,109 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.example; + +import org.apache.rocketmq.mqtt.common.util.HmacSHA1Util; +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; + +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class MqttProducer { + public static void main(String[] args) throws InterruptedException, MqttException, NoSuchAlgorithmException, InvalidKeyException { + MemoryPersistence memoryPersistence = new MemoryPersistence(); + String brokerUrl = System.getenv("brokerUrl"); + String firstTopic = System.getenv("firstTopic"); + String sendClientId = "send01"; + String recvClientId = "recv01"; + MqttConnectOptions mqttConnectOptions = buildMqttConnectOptions(sendClientId); + MqttClient mqttClient = new MqttClient(brokerUrl, sendClientId, memoryPersistence); + mqttClient.setTimeToWait(5000L); + mqttClient.setCallback(new MqttCallbackExtended() { + @Override + public void connectComplete(boolean reconnect, String serverURI) { + System.out.println(sendClientId + " connect success to " + serverURI); + } + + @Override + public void connectionLost(Throwable throwable) { + throwable.printStackTrace(); + } + + @Override + public void messageArrived(String topic, MqttMessage mqttMessage) { + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + } + }); + try { + mqttClient.connect(mqttConnectOptions); + } catch (Exception e) { + e.printStackTrace(); + } + long interval = 1000; + for (int i = 0; i < 1000; i++) { + String msg = "r1_" + System.currentTimeMillis() + "_" + i; + MqttMessage message = new MqttMessage(msg.getBytes(StandardCharsets.UTF_8)); + message.setQos(1); + String mqttSendTopic = firstTopic + "/r1"; + mqttClient.publish(mqttSendTopic, message); + System.out.println(now() + "send: " + mqttSendTopic + ", " + msg); + Thread.sleep(interval); + + mqttSendTopic = firstTopic + "/r/wc"; + msg = "wc_" + System.currentTimeMillis() + "_" + i; + MqttMessage messageWild = new MqttMessage(msg.getBytes(StandardCharsets.UTF_8)); + messageWild.setQos(1); + mqttClient.publish(mqttSendTopic, messageWild); + System.out.println(now() + "send: " + mqttSendTopic + ", " + msg); + Thread.sleep(interval); + + mqttSendTopic = firstTopic + "/r2"; + msg = "msgQ2_" + System.currentTimeMillis() + "_" + i; + message = new MqttMessage(msg.getBytes(StandardCharsets.UTF_8)); + message.setQos(2); + mqttClient.publish(mqttSendTopic, message); + System.out.println(now() + "send: " + mqttSendTopic + ", " + msg); + Thread.sleep(interval); + } + } + + private static MqttConnectOptions buildMqttConnectOptions(String clientId) throws NoSuchAlgorithmException, InvalidKeyException { + MqttConnectOptions connOpts = new MqttConnectOptions(); + connOpts.setCleanSession(true); + connOpts.setKeepAliveInterval(60); + connOpts.setAutomaticReconnect(true); + connOpts.setMaxInflight(10000); + connOpts.setUserName(System.getenv("username")); + connOpts.setPassword(HmacSHA1Util.macSignature(clientId, System.getenv("secretKey")).toCharArray()); + return connOpts; + } + + private static String now() { + SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); + return sf.format(new Date()) + "\t"; + } +} diff --git a/mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/RocketMQConsumer.java b/mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/RocketMQConsumer.java new file mode 100644 index 00000000000..a85c6ad54c0 --- /dev/null +++ b/mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/RocketMQConsumer.java @@ -0,0 +1,68 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.example; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.mqtt.common.model.Constants; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +public class RocketMQConsumer { + + public static void main(String[] args) throws MQClientException { + // Instantiate with specified consumer group name. + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("GID_test01"); + + // Specify name server addresses. + consumer.setNamesrvAddr(System.getenv("namesrv")); + + // Subscribe one more more topics to consume. + String firstTopic = System.getenv("firstTopic"); + consumer.subscribe(firstTopic, Constants.MQTT_TAG); + // Register callback to execute on arrival of messages fetched from brokers. + consumer.registerMessageListener(new MessageListenerConcurrently() { + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + MessageExt messageExt = msgs.get(0); + System.out.println(now() + "Receive: " + new String(messageExt.getBody())); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + //Launch the consumer instance. + consumer.start(); + + System.out.printf("Consumer Started.%n"); + } + + private static String now() { + SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); + return sf.format(new Date()) + "\t"; + } +} diff --git a/mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/RocketMQProducer.java b/mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/RocketMQProducer.java new file mode 100644 index 00000000000..f28db03ebbb --- /dev/null +++ b/mqtt-example/src/main/java/org/apache/rocketmq/mqtt/example/RocketMQProducer.java @@ -0,0 +1,110 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.apache.rocketmq.mqtt.example; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.remoting.exception.RemotingException; + +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +public class RocketMQProducer { + private static DefaultMQProducer producer; + private static String firstTopic = System.getenv("firstTopic"); + private static String recvClientId = "recv01"; + + public static void main(String[] args) throws Exception { + //Instantiate with a producer group name. + producer = new DefaultMQProducer("PID_TEST"); + // Specify name server addresses. + producer.setNamesrvAddr(System.getenv("namesrv")); + //Launch the instance. + producer.start(); + + for (int i = 0; i < 1000; i++) { + //Create a message instance, specifying topic, tag and message body. + + //Call send message to deliver message to one of brokers. + try { + sendMessage(i); + Thread.sleep(1000); + sendWithWildcardMessage(i); + Thread.sleep(1000); + } catch (Exception e) { + e.printStackTrace(); + } + } + //Shut down once the producer instance is not longer in use. + producer.shutdown(); + } + + private static void setLmq(Message msg, Set queues) { + msg.putUserProperty(LmqQueueStore.PROPERTY_INNER_MULTI_DISPATCH, + StringUtils.join( + queues.stream().map(s -> LmqQueueStore.LMQ_PREFIX + s).collect(Collectors.toSet()), + LmqQueueStore.MULTI_DISPATCH_QUEUE_SPLITTER)); + } + + private static void sendMessage(int i) throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + Message msg = new Message(firstTopic, + "MQ2MQTT", + ("MQ_" + System.currentTimeMillis() + "_" + i).getBytes(StandardCharsets.UTF_8)); + String secondTopic = "/r1"; + setLmq(msg, new HashSet<>(Arrays.asList(TopicUtils.wrapLmq(firstTopic, secondTopic)))); + SendResult sendResult = producer.send(msg); + System.out.println(now() + "sendMessage: " + new String(msg.getBody())); + } + + private static void sendWithWildcardMessage(int i) throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + Message msg = new Message(firstTopic, + "MQ2MQTT", + ("MQwc_" + System.currentTimeMillis() + "_" + i).getBytes(StandardCharsets.UTF_8)); + String secondTopic = "/r/wc"; + Set lmqSet = new HashSet<>(); + lmqSet.add(TopicUtils.wrapLmq(firstTopic, secondTopic)); + lmqSet.addAll(mapWildCardLmq(firstTopic, secondTopic)); + setLmq(msg, lmqSet); + SendResult sendResult = producer.send(msg); + System.out.println(now() + "sendWcMessage: " + new String(msg.getBody())); + } + + private static Set mapWildCardLmq(String firstTopic, String secondTopic) { + // todo by yourself + return new HashSet<>(Arrays.asList(TopicUtils.wrapLmq(firstTopic, "/r/+"))); + } + + private static String now() { + SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); + return sf.format(new Date()) + "\t"; + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000000..2fbc370cbfb --- /dev/null +++ b/pom.xml @@ -0,0 +1,189 @@ + + + 4.0.0 + + org.apache.rocketmq + rocketmq-mqtt + pom + 1.0.0-SNAPSHOT + + mqtt-common + mqtt-cs + mqtt-ds + mqtt-example + + + + 1.8 + 1.8 + 1.8 + 1.8 + UTF-8 + 4.3.16.RELEASE + 4.9.3 + + + + + + org.apache.rocketmq + mqtt-common + ${project.version} + + + org.apache.rocketmq + mqtt-ds + ${project.version} + + + org.apache.rocketmq + rocketmq-client + ${rocket.version} + + + org.apache.rocketmq + rocketmq-tools + ${rocket.version} + + + io.netty + netty-all + 4.1.43.Final + + + org.springframework + spring-core + ${spring.version} + + + org.springframework + spring-context + ${spring.version} + + + org.springframework + spring-beans + ${spring.version} + + + org.springframework + spring-jdbc + ${spring.version} + + + org.slf4j + slf4j-api + 1.7.15 + + + ch.qos.logback + logback-classic + 1.1.5 + + + ch.qos.logback + logback-core + 1.1.5 + + + org.apache.commons + commons-lang3 + 3.7 + + + com.alibaba + fastjson + 1.2.79 + + + com.github.ben-manes.caffeine + caffeine + 2.6.2 + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + com.github.ben-manes.caffeine + caffeine + 2.6.2 + + + commons-codec + commons-codec + 1.15 + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 2.28.2 + test + + + + + + + + maven-checkstyle-plugin + 2.17 + + + verify + verify + + style/rmq_checkstyle.xml + UTF-8 + true + true + false + + + check + + + + + + org.apache.rat + apache-rat-plugin + 0.12 + + + .travis.yml + CONTRIBUTING.md + bin/README.md + .github/* + pom.xml + style/** + README.md + dev/merge_rocketmq_pr.py + src/test/resources/certs/* + BUILDING + LICENSE + NOTICE + + + + + maven-assembly-plugin + + rocketmq-mqtt + + assembly.xml + + + + + + \ No newline at end of file