From 26129f5b43a95555141670643585734bb813222c Mon Sep 17 00:00:00 2001 From: Maksim Kashapov Date: Wed, 19 Nov 2025 13:42:49 +0100 Subject: [PATCH 01/38] [broker-30] Implement delivering retained messages --- .../config/MqttBrokerSpringConfig.java | 6 +- .../tree/ConcurrentSubscriptionTree.java} | 10 +- .../tree/TopicFilterNode.java} | 50 +++++----- .../tree/TopicFilterTreeBase.java} | 12 +-- .../model/subscribtion/tree/package-info.java | 4 + .../mqtt/model/topic/TopicFilter.java | 4 +- .../tree/ConcurrentRetainedMessageTree.java | 31 ++++++ .../model/topic/tree/TopicMessageNode.java | 94 +++++++++++++++++++ .../model/topic/tree/TopicTreeTest.groovy | 19 ++-- .../service/PublishDeliveringService.java | 3 + .../impl/DefaultPublishDeliveringService.java | 14 ++- .../impl/InMemorySubscriptionService.java | 8 +- .../impl/SubscribeMqttInMessageHandler.java | 43 ++++++++- .../SubscribeMqttInMessageHandlerTest.groovy | 87 +++++++++++++++-- 14 files changed, 321 insertions(+), 64 deletions(-) rename model/src/main/java/javasabr/mqtt/model/{topic/tree/ConcurrentTopicTree.java => subscribtion/tree/ConcurrentSubscriptionTree.java} (86%) rename model/src/main/java/javasabr/mqtt/model/{topic/tree/TopicNode.java => subscribtion/tree/TopicFilterNode.java} (71%) rename model/src/main/java/javasabr/mqtt/model/{topic/tree/TopicTreeBase.java => subscribtion/tree/TopicFilterTreeBase.java} (93%) create mode 100644 model/src/main/java/javasabr/mqtt/model/subscribtion/tree/package-info.java create mode 100644 model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java create mode 100644 model/src/main/java/javasabr/mqtt/model/topic/tree/TopicMessageNode.java diff --git a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java index ddf6f743..b809f587 100644 --- a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java +++ b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java @@ -5,6 +5,7 @@ import javasabr.mqtt.model.MqttProperties; import javasabr.mqtt.model.MqttServerConnectionConfig; import javasabr.mqtt.model.QoS; +import javasabr.mqtt.model.topic.tree.ConcurrentRetainedMessageTree; import javasabr.mqtt.network.MqttClientFactory; import javasabr.mqtt.network.MqttConnection; import javasabr.mqtt.network.MqttConnectionFactory; @@ -178,8 +179,9 @@ MqttInMessageHandler disconnectMqttInMessageHandler(MessageOutFactoryService mes MqttInMessageHandler subscribeMqttInMessageHandler( SubscriptionService subscriptionService, MessageOutFactoryService messageOutFactoryService, - TopicService topicService) { - return new SubscribeMqttInMessageHandler(subscriptionService, messageOutFactoryService, topicService); + TopicService topicService, + PublishDeliveringService publishDeliveringService) { + return new SubscribeMqttInMessageHandler(subscriptionService, messageOutFactoryService, topicService, publishDeliveringService); } @Bean diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentTopicTree.java b/model/src/main/java/javasabr/mqtt/model/subscribtion/tree/ConcurrentSubscriptionTree.java similarity index 86% rename from model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentTopicTree.java rename to model/src/main/java/javasabr/mqtt/model/subscribtion/tree/ConcurrentSubscriptionTree.java index b095419a..f55a5548 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentTopicTree.java +++ b/model/src/main/java/javasabr/mqtt/model/subscribtion/tree/ConcurrentSubscriptionTree.java @@ -1,4 +1,4 @@ -package javasabr.mqtt.model.topic.tree; +package javasabr.mqtt.model.subscribtion.tree; import javasabr.mqtt.model.subscriber.SingleSubscriber; import javasabr.mqtt.model.subscribtion.Subscription; @@ -13,12 +13,12 @@ import org.jspecify.annotations.Nullable; @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) -public class ConcurrentTopicTree implements ThreadSafe { +public class ConcurrentSubscriptionTree implements ThreadSafe { - TopicNode rootNode; + TopicFilterNode rootNode; - public ConcurrentTopicTree() { - this.rootNode = new TopicNode(); + public ConcurrentSubscriptionTree() { + this.rootNode = new TopicFilterNode(); } @Nullable diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/TopicNode.java b/model/src/main/java/javasabr/mqtt/model/subscribtion/tree/TopicFilterNode.java similarity index 71% rename from model/src/main/java/javasabr/mqtt/model/topic/tree/TopicNode.java rename to model/src/main/java/javasabr/mqtt/model/subscribtion/tree/TopicFilterNode.java index 03a624b1..431e1d87 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/TopicNode.java +++ b/model/src/main/java/javasabr/mqtt/model/subscribtion/tree/TopicFilterNode.java @@ -1,4 +1,4 @@ -package javasabr.mqtt.model.topic.tree; +package javasabr.mqtt.model.subscribtion.tree; import java.util.function.Supplier; import javasabr.mqtt.base.util.DebugUtils; @@ -22,16 +22,16 @@ @Getter(AccessLevel.PACKAGE) @Accessors(fluent = true, chain = false) @FieldDefaults(level = AccessLevel.PRIVATE) -class TopicNode extends TopicTreeBase { +class TopicFilterNode extends TopicFilterTreeBase { - private final static Supplier TOPIC_NODE_FACTORY = TopicNode::new; + private final static Supplier TOPIC_NODE_FACTORY = TopicFilterNode::new; static { DebugUtils.registerIncludedFields("childNodes", "subscribers"); } @Nullable - volatile LockableRefToRefDictionary childNodes; + volatile LockableRefToRefDictionary childNodes; @Nullable volatile LockableArray subscribers; @@ -43,7 +43,7 @@ public SingleSubscriber subscribe(int level, SubscriptionOwner owner, Subscripti if (level == topicFilter.levelsCount()) { return addSubscriber(getOrCreateSubscribers(), owner, subscription, topicFilter); } - TopicNode childNode = getOrCreateChildNode(topicFilter.segment(level)); + TopicFilterNode childNode = getOrCreateChildNode(topicFilter.segment(level)); return childNode.subscribe(level + 1, owner, subscription, topicFilter); } @@ -51,7 +51,7 @@ public boolean unsubscribe(int level, SubscriptionOwner owner, TopicFilter topic if (level == topicFilter.levelsCount()) { return removeSubscriber(subscribers(), owner, topicFilter); } - TopicNode childNode = getOrCreateChildNode(topicFilter.segment(level)); + TopicFilterNode childNode = getOrCreateChildNode(topicFilter.segment(level)); return childNode.unsubscribe(level + 1, owner, topicFilter); } @@ -67,14 +67,14 @@ private void exactlyTopicMatch( int lastLevel, MutableArray result) { String segment = topicName.segment(level); - TopicNode topicNode = childNode(segment); - if (topicNode == null) { + TopicFilterNode topicFilterNode = childNode(segment); + if (topicFilterNode == null) { return; } if (level == lastLevel) { - appendSubscribersTo(result, topicNode); + appendSubscribersTo(result, topicFilterNode); } else if (level < lastLevel) { - topicNode.matchesTo(level + 1, topicName, lastLevel, result); + topicFilterNode.matchesTo(level + 1, topicName, lastLevel, result); } } @@ -83,31 +83,31 @@ private void singleWildcardTopicMatch( TopicName topicName, int lastLevel, MutableArray result) { - TopicNode topicNode = childNode(TopicFilter.SINGLE_LEVEL_WILDCARD); - if (topicNode == null) { + TopicFilterNode topicFilterNode = childNode(TopicFilter.SINGLE_LEVEL_WILDCARD); + if (topicFilterNode == null) { return; } if (level == lastLevel) { - appendSubscribersTo(result, topicNode); + appendSubscribersTo(result, topicFilterNode); } else if (level < lastLevel) { - topicNode.matchesTo(level + 1, topicName, lastLevel, result); + topicFilterNode.matchesTo(level + 1, topicName, lastLevel, result); } } private void multiWildcardTopicMatch(MutableArray result) { - TopicNode topicNode = childNode(TopicFilter.MULTI_LEVEL_WILDCARD); - if (topicNode != null) { - appendSubscribersTo(result, topicNode); + TopicFilterNode topicFilterNode = childNode(TopicFilter.MULTI_LEVEL_WILDCARD); + if (topicFilterNode != null) { + appendSubscribersTo(result, topicFilterNode); } } - private TopicNode getOrCreateChildNode(String segment) { - LockableRefToRefDictionary childNodes = getOrCreateChildNodes(); + private TopicFilterNode getOrCreateChildNode(String segment) { + LockableRefToRefDictionary childNodes = getOrCreateChildNodes(); long stamp = childNodes.readLock(); try { - TopicNode topicNode = childNodes.get(segment); - if (topicNode != null) { - return topicNode; + TopicFilterNode topicFilterNode = childNodes.get(segment); + if (topicFilterNode != null) { + return topicFilterNode; } } finally { childNodes.readUnlock(stamp); @@ -122,8 +122,8 @@ private TopicNode getOrCreateChildNode(String segment) { } @Nullable - private TopicNode childNode(String segment) { - LockableRefToRefDictionary childNodes = childNodes(); + private TopicFilterNode childNode(String segment) { + LockableRefToRefDictionary childNodes = childNodes(); if (childNodes == null) { return null; } @@ -135,7 +135,7 @@ private TopicNode childNode(String segment) { } } - private LockableRefToRefDictionary getOrCreateChildNodes() { + private LockableRefToRefDictionary getOrCreateChildNodes() { if (childNodes == null) { synchronized (this) { if (childNodes == null) { diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/TopicTreeBase.java b/model/src/main/java/javasabr/mqtt/model/subscribtion/tree/TopicFilterTreeBase.java similarity index 93% rename from model/src/main/java/javasabr/mqtt/model/topic/tree/TopicTreeBase.java rename to model/src/main/java/javasabr/mqtt/model/subscribtion/tree/TopicFilterTreeBase.java index 7f0dbb23..f035052c 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/TopicTreeBase.java +++ b/model/src/main/java/javasabr/mqtt/model/subscribtion/tree/TopicFilterTreeBase.java @@ -1,4 +1,4 @@ -package javasabr.mqtt.model.topic.tree; +package javasabr.mqtt.model.subscribtion.tree; import java.util.Objects; import javasabr.mqtt.model.QoS; @@ -18,7 +18,7 @@ @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) -abstract class TopicTreeBase { +abstract class TopicFilterTreeBase { /** * @return previous subscriber with the same owner @@ -66,7 +66,7 @@ private static void addSharedSubscriber( String group = sharedTopicFilter.shareName(); SharedSubscriber sharedSubscriber = (SharedSubscriber) subscribers .iterations() - .findAny(group, TopicTreeBase::isSharedSubscriberWithGroup); + .findAny(group, TopicFilterTreeBase::isSharedSubscriberWithGroup); if (sharedSubscriber == null) { sharedSubscriber = new SharedSubscriber(sharedTopicFilter); @@ -76,8 +76,8 @@ private static void addSharedSubscriber( sharedSubscriber.addSubscriber(new SingleSubscriber(owner, subscription)); } - protected static void appendSubscribersTo(MutableArray result, TopicNode topicNode) { - LockableArray subscribers = topicNode.subscribers(); + protected static void appendSubscribersTo(MutableArray result, TopicFilterNode topicFilterNode) { + LockableArray subscribers = topicFilterNode.subscribers(); if (subscribers == null) { return; } @@ -125,7 +125,7 @@ private static boolean removeSharedSubscriber( String group = sharedTopicFilter.shareName(); SharedSubscriber sharedSubscriber = (SharedSubscriber) subscribers .iterations() - .findAny(group, TopicTreeBase::isSharedSubscriberWithGroup); + .findAny(group, TopicFilterTreeBase::isSharedSubscriberWithGroup); if (sharedSubscriber != null) { boolean removed = sharedSubscriber.removeSubscriberWithOwner(owner); if (sharedSubscriber.isEmpty()) { diff --git a/model/src/main/java/javasabr/mqtt/model/subscribtion/tree/package-info.java b/model/src/main/java/javasabr/mqtt/model/subscribtion/tree/package-info.java new file mode 100644 index 00000000..692a51cf --- /dev/null +++ b/model/src/main/java/javasabr/mqtt/model/subscribtion/tree/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package javasabr.mqtt.model.subscribtion.tree; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/model/src/main/java/javasabr/mqtt/model/topic/TopicFilter.java b/model/src/main/java/javasabr/mqtt/model/topic/TopicFilter.java index fa9c1b6f..da653f8a 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/TopicFilter.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/TopicFilter.java @@ -11,9 +11,9 @@ public class TopicFilter extends AbstractTopic { public static final String MULTI_LEVEL_WILDCARD = "#"; - public static final char MULTI_LEVEL_WILDCARD_CHAR = '#'; + public static final char MULTI_LEVEL_WILDCARD_CHAR = MULTI_LEVEL_WILDCARD.charAt(0); public static final String SINGLE_LEVEL_WILDCARD = "+"; - public static final char SINGLE_LEVEL_WILDCARD_CHAR = '+'; + public static final char SINGLE_LEVEL_WILDCARD_CHAR = SINGLE_LEVEL_WILDCARD.charAt(0); public static final String SPECIAL = "$"; public static final TopicFilter INVALID_TOPIC_FILTER = new TopicFilter("$invalid$") { diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java new file mode 100644 index 00000000..a5bed489 --- /dev/null +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java @@ -0,0 +1,31 @@ +package javasabr.mqtt.model.topic.tree; + +import javasabr.mqtt.model.publishing.Publish; +import javasabr.mqtt.model.topic.TopicFilter; +import javasabr.mqtt.model.topic.TopicName; +import javasabr.rlib.common.ThreadSafe; +import lombok.AccessLevel; +import lombok.experimental.FieldDefaults; +import org.jspecify.annotations.Nullable; + +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class ConcurrentRetainedMessageTree implements ThreadSafe { + + TopicMessageNode rootNode; + + public ConcurrentRetainedMessageTree() { + this.rootNode = new TopicMessageNode(); + } + + public void retainMessage(Publish message) { + rootNode.retainMessage(0, message, message.topicName()); + } + + public @Nullable Publish getRetainedMessage(TopicName topicName) { + return rootNode.getRetainedMessage(0, topicName); + } + + public @Nullable Publish getRetainedMessage(TopicFilter topicFilter) { + return rootNode.getRetainedMessage(0, topicFilter); + } +} diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/TopicMessageNode.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/TopicMessageNode.java new file mode 100644 index 00000000..55cad0ec --- /dev/null +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/TopicMessageNode.java @@ -0,0 +1,94 @@ +package javasabr.mqtt.model.topic.tree; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import javasabr.mqtt.base.util.DebugUtils; +import javasabr.mqtt.model.publishing.Publish; +import javasabr.mqtt.model.topic.TopicFilter; +import javasabr.mqtt.model.topic.TopicName; +import javasabr.rlib.collections.dictionary.DictionaryFactory; +import javasabr.rlib.collections.dictionary.LockableRefToRefDictionary; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.jspecify.annotations.Nullable; + +@Getter(AccessLevel.PACKAGE) +@Accessors(fluent = true, chain = false) +@FieldDefaults(level = AccessLevel.PRIVATE) +class TopicMessageNode { + + private final static Supplier TOPIC_NODE_FACTORY = TopicMessageNode::new; + + static { + DebugUtils.registerIncludedFields("childNodes", "retainedMessage"); + } + + @Nullable + volatile LockableRefToRefDictionary childNodes; + final AtomicReference<@Nullable Publish> retainedMessage = new AtomicReference<>(); + + public void retainMessage(int level, Publish message, TopicName topicFilter) { + if (level + 1 == topicFilter.levelsCount()) { + retainedMessage.set(message); + return; + } + TopicMessageNode childNode = getOrCreateChildNode(topicFilter.segment(level)); + childNode.retainMessage(level + 1, message, topicFilter); + } + + @Nullable + public Publish getRetainedMessage(int level, TopicName topicName) { + if (level + 1 == topicName.levelsCount()) { + return retainedMessage.get(); + } + TopicMessageNode childNode = getOrCreateChildNode(topicName.segment(level)); + return childNode.getRetainedMessage(level + 1, topicName); + } + + @Nullable + public Publish getRetainedMessage(int level, TopicFilter topicName) { + if (level + 1 == topicName.levelsCount()) { + return retainedMessage.get(); + } + TopicMessageNode childNode = getOrCreateChildNode(topicName.segment(level)); + return childNode.getRetainedMessage(level + 1, topicName); + } + + private TopicMessageNode getOrCreateChildNode(String segment) { + LockableRefToRefDictionary childNodes = getOrCreateChildNodes(); + long stamp = childNodes.readLock(); + try { + TopicMessageNode topicFilterNode = childNodes.get(segment); + if (topicFilterNode != null) { + return topicFilterNode; + } + } finally { + childNodes.readUnlock(stamp); + } + stamp = childNodes.writeLock(); + try { + return childNodes.getOrCompute(segment, TOPIC_NODE_FACTORY); + } finally { + childNodes.writeUnlock(stamp); + } + } + + private LockableRefToRefDictionary getOrCreateChildNodes() { + if (childNodes == null) { + synchronized (this) { + if (childNodes == null) { + childNodes = DictionaryFactory.stampedLockBasedRefToRefDictionary(); + } + } + } + //noinspection ConstantConditions + return childNodes; + } + + @Override + public String toString() { + return DebugUtils.toJsonString(this); + } +} diff --git a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/TopicTreeTest.groovy b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/TopicTreeTest.groovy index fe5aa51f..1b220202 100644 --- a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/TopicTreeTest.groovy +++ b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/TopicTreeTest.groovy @@ -6,6 +6,7 @@ import javasabr.mqtt.model.SubscribeRetainHandling import javasabr.mqtt.model.subscriber.SingleSubscriber import javasabr.mqtt.model.subscribtion.Subscription import javasabr.mqtt.model.subscribtion.SubscriptionOwner +import javasabr.mqtt.model.subscribtion.tree.ConcurrentSubscriptionTree import javasabr.mqtt.model.subscription.TestSubscriptionOwner import javasabr.mqtt.model.topic.SharedTopicFilter import javasabr.mqtt.model.topic.TopicFilter @@ -20,7 +21,7 @@ class TopicTreeTest extends UnitSpecification { String topicName, List expectedOwners) { given: - ConcurrentTopicTree topicTree = new ConcurrentTopicTree() + ConcurrentSubscriptionTree topicTree = new ConcurrentSubscriptionTree() subscriptions.eachWithIndex { Subscription subscription, int i -> topicTree.subscribe(owners.get(i), subscription) } @@ -108,7 +109,7 @@ class TopicTreeTest extends UnitSpecification { String topicName, List expectedOwners) { given: - ConcurrentTopicTree topicTree = new ConcurrentTopicTree() + ConcurrentSubscriptionTree topicTree = new ConcurrentSubscriptionTree() subscriptions.eachWithIndex { Subscription subscription, int i -> topicTree.subscribe(owners.get(i), subscription) } @@ -213,7 +214,7 @@ class TopicTreeTest extends UnitSpecification { String topicName, List expectedOwners) { given: - ConcurrentTopicTree topicTree = new ConcurrentTopicTree() + ConcurrentSubscriptionTree topicTree = new ConcurrentSubscriptionTree() subscriptions.eachWithIndex { Subscription subscription, int i -> topicTree.subscribe(owners.get(i), subscription) } @@ -327,7 +328,7 @@ class TopicTreeTest extends UnitSpecification { String topicName, List expectedSubscribers) { given: - ConcurrentTopicTree topicTree = new ConcurrentTopicTree() + ConcurrentSubscriptionTree topicTree = new ConcurrentSubscriptionTree() subscriptions.eachWithIndex { Subscription subscription, int i -> topicTree.subscribe(owners.get(i), subscription) } @@ -434,7 +435,7 @@ class TopicTreeTest extends UnitSpecification { given: def group1 = ["id1", "id2", "id3", "id4", "id5"] def group2 = ["id6", "id7", "id8", "id9", "id10"] - ConcurrentTopicTree topicTree = new ConcurrentTopicTree() + ConcurrentSubscriptionTree topicTree = new ConcurrentSubscriptionTree() topicTree.subscribe(makeOwner("id1"), makeSharedSubscription('$share/group1/topic/name1')) topicTree.subscribe(makeOwner("id2"), makeSharedSubscription('$share/group1/topic/name1')) topicTree.subscribe(makeOwner("id3"), makeSharedSubscription('$share/group1/topic/name1')) @@ -467,7 +468,7 @@ class TopicTreeTest extends UnitSpecification { def "should subscribe and unsubscribe simple topic correctly correctly"() { given: - ConcurrentTopicTree topicTree = new ConcurrentTopicTree() + ConcurrentSubscriptionTree topicTree = new ConcurrentSubscriptionTree() topicTree.subscribe(makeOwner("id1"), makeSubscription('topic/name1')) topicTree.subscribe(makeOwner("id2"), makeSubscription('topic/name1')) topicTree.subscribe(makeOwner("id3"), makeSubscription('topic/name1')) @@ -504,7 +505,7 @@ class TopicTreeTest extends UnitSpecification { def "should subscribe and unsubscribe shared topic correctly correctly"() { given: - ConcurrentTopicTree topicTree = new ConcurrentTopicTree() + ConcurrentSubscriptionTree topicTree = new ConcurrentSubscriptionTree() topicTree.subscribe(makeOwner("id1"), makeSharedSubscription('$share/group1/topic/name1')) topicTree.subscribe(makeOwner("id2"), makeSharedSubscription('$share/group1/topic/name1')) topicTree.subscribe(makeOwner("id3"), makeSharedSubscription('$share/group1/topic/name1')) @@ -541,7 +542,7 @@ class TopicTreeTest extends UnitSpecification { def "should replace the same subscriptions"() { given: - ConcurrentTopicTree topicTree = new ConcurrentTopicTree() + ConcurrentSubscriptionTree topicTree = new ConcurrentSubscriptionTree() def owner1 = makeOwner("id1") def originalSub = makeSubscription('topic/name1') def replacementSub = makeSubscription('topic/name1') @@ -569,7 +570,7 @@ class TopicTreeTest extends UnitSpecification { def "should extend shared subscription group on multiply subscribing by the same topic"() { given: - ConcurrentTopicTree topicTree = new ConcurrentTopicTree() + ConcurrentSubscriptionTree topicTree = new ConcurrentSubscriptionTree() def owner1 = makeOwner("id1") def owner2 = makeOwner("id2") topicTree.subscribe(owner1, makeSharedSubscription('$share/group1/topic/name1')) diff --git a/service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java b/service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java index 8110b7bf..f051d8a3 100644 --- a/service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java +++ b/service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java @@ -2,9 +2,12 @@ import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.subscriber.SingleSubscriber; +import javasabr.mqtt.model.topic.TopicFilter; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; public interface PublishDeliveringService { PublishHandlingResult startDelivering(Publish publish, SingleSubscriber subscriber); + + PublishHandlingResult deliverRetainedMessages(TopicFilter topicFilter, SingleSubscriber subscriber); } diff --git a/service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java b/service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java index 8e0a7529..c66d3f49 100644 --- a/service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java +++ b/service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java @@ -4,6 +4,8 @@ import javasabr.mqtt.model.QoS; import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.subscriber.SingleSubscriber; +import javasabr.mqtt.model.topic.TopicFilter; +import javasabr.mqtt.model.topic.tree.ConcurrentRetainedMessageTree; import javasabr.mqtt.service.PublishDeliveringService; import javasabr.mqtt.service.publish.handler.MqttPublishOutMessageHandler; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; @@ -18,6 +20,7 @@ public class DefaultPublishDeliveringService implements PublishDeliveringService @Nullable MqttPublishOutMessageHandler[] publishOutMessageHandlers; + ConcurrentRetainedMessageTree topicTree; public DefaultPublishDeliveringService( Collection knownPublishOutHandlers) { @@ -39,7 +42,7 @@ public DefaultPublishDeliveringService( } handlers[qos.level()] = knownPublishOutHandler; } - + this.topicTree = new ConcurrentRetainedMessageTree(); this.publishOutMessageHandlers = handlers; log.info(publishOutMessageHandlers, DefaultPublishDeliveringService::buildServiceDescription); } @@ -47,6 +50,9 @@ public DefaultPublishDeliveringService( @Override public PublishHandlingResult startDelivering(Publish publish, SingleSubscriber subscriber) { try { + if (publish.retained()) { + topicTree.retainMessage(publish); + } //noinspection DataFlowIssue return publishOutMessageHandlers[subscriber.qos().level()].handle(publish, subscriber); } catch (IndexOutOfBoundsException | NullPointerException ex) { @@ -55,6 +61,12 @@ public PublishHandlingResult startDelivering(Publish publish, SingleSubscriber s } } + @Override + public PublishHandlingResult deliverRetainedMessages(TopicFilter topicFilter, SingleSubscriber subscriber) { + Publish retainedMessage = topicTree.getRetainedMessage(topicFilter); + return startDelivering(retainedMessage, subscriber); + } + private static String buildServiceDescription( @Nullable MqttPublishOutMessageHandler[] publishOutMessageHandlers) { var builder = new StringBuilder(); diff --git a/service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index cb1d44ff..3cb1aa4b 100644 --- a/service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -12,7 +12,7 @@ import javasabr.mqtt.model.topic.SharedTopicFilter; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.mqtt.model.topic.TopicName; -import javasabr.mqtt.model.topic.tree.ConcurrentTopicTree; +import javasabr.mqtt.model.subscribtion.tree.ConcurrentSubscriptionTree; import javasabr.mqtt.network.MqttClient; import javasabr.mqtt.network.session.ActiveSubscriptions; import javasabr.mqtt.network.session.MqttSession; @@ -25,16 +25,16 @@ import lombok.experimental.FieldDefaults; /** - * In memory subscription service based on {@link ConcurrentTopicTree} + * In memory subscription service based on {@link ConcurrentSubscriptionTree} */ @CustomLog @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) public class InMemorySubscriptionService implements SubscriptionService { - ConcurrentTopicTree topicTree; + ConcurrentSubscriptionTree topicTree; public InMemorySubscriptionService() { - this.topicTree = new ConcurrentTopicTree(); + this.topicTree = new ConcurrentSubscriptionTree(); } @Override diff --git a/service/src/main/java/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandler.java b/service/src/main/java/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandler.java index 9e5528d7..949bdf48 100644 --- a/service/src/main/java/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandler.java +++ b/service/src/main/java/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandler.java @@ -9,6 +9,7 @@ import javasabr.mqtt.model.QoS; import javasabr.mqtt.model.reason.code.DisconnectReasonCode; import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode; +import javasabr.mqtt.model.subscriber.SingleSubscriber; import javasabr.mqtt.model.subscribtion.RequestedSubscription; import javasabr.mqtt.model.subscribtion.Subscription; import javasabr.mqtt.model.topic.TopicFilter; @@ -20,8 +21,10 @@ import javasabr.mqtt.network.session.MessageTacker; import javasabr.mqtt.network.session.MqttSession; import javasabr.mqtt.service.MessageOutFactoryService; +import javasabr.mqtt.service.PublishDeliveringService; import javasabr.mqtt.service.SubscriptionService; import javasabr.mqtt.service.TopicService; +import javasabr.mqtt.service.publish.handler.PublishHandlingResult; import javasabr.rlib.collections.array.Array; import javasabr.rlib.collections.array.ArrayFactory; import javasabr.rlib.collections.array.MutableArray; @@ -38,16 +41,19 @@ public class SubscribeMqttInMessageHandler extends SHARED_SUBSCRIPTIONS_NOT_SUPPORTED, WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED); + PublishDeliveringService publishDeliveringService; SubscriptionService subscriptionService; TopicService topicService; public SubscribeMqttInMessageHandler( SubscriptionService subscriptionService, MessageOutFactoryService messageOutFactoryService, - TopicService topicService) { + TopicService topicService, + PublishDeliveringService publishDeliveringService) { super(ExternalMqttClient.class, SubscribeMqttInMessage.class, messageOutFactoryService); this.subscriptionService = subscriptionService; this.topicService = topicService; + this.publishDeliveringService = publishDeliveringService; } @Override @@ -92,6 +98,7 @@ protected void processValidMessage( .subscribe(client, session, subscriptions); sendSubscribeResults(client, session, subscribeMessage, subscribeResults); + sendRetainedMessages(client, subscribeMessage, subscribeResults, subscriptions); SubscribeAckReasonCode anyReasonToDisconnect = subscribeResults .iterations() @@ -174,4 +181,38 @@ private void sendSubscribeResults( .inMessageTracker() .remove(messageId)); } + + private void sendRetainedMessages( + ExternalMqttClient client, + SubscribeMqttInMessage subscribeMessage, + Array subscribeResults, + Array subs) { + int count = 0; + PublishHandlingResult errorResult = null; + Array subscriptions = subscribeMessage.subscriptions(); + for (int i = 0; i < subscribeMessage.subscriptionsCount(); i++) { + RequestedSubscription requestedSubscription = subscriptions.get(i); + SubscribeAckReasonCode subscribeAckReasonCode = subscribeResults.get(i); + Subscription subscription = subs.get(i); + if (subscribeAckReasonCode.ordinal() < 3) { + TopicFilter topicFilter = TopicFilter.valueOf(requestedSubscription.rawTopicFilter()); + SingleSubscriber singleSubscriber = new SingleSubscriber(client, subscription); + PublishHandlingResult result = publishDeliveringService.deliverRetainedMessages(topicFilter, singleSubscriber); + if (result.error()) { + errorResult = result; + } else if(result == PublishHandlingResult.SUCCESS) { + count++; + } + if (errorResult != null) { + log.debug(client.clientId(), errorResult, + "[%s] Found final error:[%s] during sending retained messages"::formatted); + // handleError(client, publish, errorResult); + } else { + log.debug(client.clientId(), count, + "[%s] Successfully started delivering retained messages to [%s] subscribers"::formatted); + // handleSuccessfulResult(client, publish, count); + } + } + } + } } diff --git a/service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandlerTest.groovy b/service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandlerTest.groovy index c740fdc3..1ab9d29e 100644 --- a/service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandlerTest.groovy +++ b/service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandlerTest.groovy @@ -1,22 +1,32 @@ package javasabr.mqtt.service.message.handler.impl import javasabr.mqtt.model.MqttVersion +import javasabr.mqtt.model.PayloadFormat import javasabr.mqtt.model.QoS +import javasabr.mqtt.model.SubscribeRetainHandling +import javasabr.mqtt.model.publishing.Publish import javasabr.mqtt.model.reason.code.DisconnectReasonCode import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode +import javasabr.mqtt.model.subscriber.SingleSubscriber import javasabr.mqtt.model.subscribtion.RequestedSubscription +import javasabr.mqtt.model.subscribtion.Subscription +import javasabr.mqtt.model.topic.TopicName import javasabr.mqtt.network.message.in.SubscribeMqttInMessage import javasabr.mqtt.network.message.out.DisconnectMqtt5OutMessage +import javasabr.mqtt.network.message.out.PublishMqtt5OutMessage import javasabr.mqtt.network.message.out.SubscribeAckMqtt5OutMessage import javasabr.mqtt.network.util.ExtraErrorReasons import javasabr.mqtt.service.IntegrationServiceSpecification import javasabr.mqtt.service.TestExternalMqttClient import javasabr.rlib.collections.array.Array +import javasabr.rlib.collections.array.IntArray import javasabr.rlib.collections.array.MutableArray import javasabr.rlib.common.util.ThreadUtils import javasabr.rlib.logger.api.LoggerLevel import javasabr.rlib.logger.api.LoggerManager +import static java.nio.charset.StandardCharsets.UTF_8 + class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification { static { @@ -30,7 +40,8 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService) + defaultTopicService, + publishDeliveringService) def mqttClient = mqttConnection.client() as TestExternalMqttClient mqttClient.session(null) when: @@ -49,7 +60,8 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService) + defaultTopicService, + publishDeliveringService) def expectedMessageId = 15 def mqttClient = mqttConnection.client() as TestExternalMqttClient def session = mqttClient.session() @@ -80,7 +92,8 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService) + defaultTopicService, + publishDeliveringService) def expectedMessageId = 15 def mqttClient = mqttConnection.client() as TestExternalMqttClient when: @@ -110,7 +123,8 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService) + defaultTopicService, + publishDeliveringService) def expectedMessageId = 15 def mqttClient = mqttConnection.client() as TestExternalMqttClient when: @@ -140,7 +154,8 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService) + defaultTopicService, + publishDeliveringService) def expectedMessageId = 15 def mqttClient = mqttConnection.client() as TestExternalMqttClient when: @@ -173,7 +188,8 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService) + defaultTopicService, + publishDeliveringService) def expectedMessageId = 15 def mqttClient = mqttConnection.client() as TestExternalMqttClient when: @@ -204,7 +220,8 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService) + defaultTopicService, + publishDeliveringService) def mqttClient = mqttConnection.client() as TestExternalMqttClient when: def subscribeMessage = new SubscribeMqttInMessage(0 as byte) @@ -222,7 +239,8 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService) + defaultTopicService, + publishDeliveringService) def expectedMessageId = 15 def mqttClient = mqttConnection.client() as TestExternalMqttClient when: @@ -260,7 +278,8 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService) + defaultTopicService, + publishDeliveringService) def expectedMessageId = 15 def mqttClient = mqttConnection.client() as TestExternalMqttClient mqttClient.returnCompletedFeatures(false) @@ -292,4 +311,54 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification && reasonCodes2.get(0) == SubscribeAckReasonCode.PACKET_IDENTIFIER_IN_USE && subscribeAck2.messageId() == expectedMessageId } + + def "should deliver retained messages"() { + given: + def mqttConnection = mockedExternalConnection(MqttVersion.MQTT_5) + def messageHandler = new SubscribeMqttInMessageHandler( + defaultSubscriptionService, + defaultMessageOutFactoryService, + defaultTopicService, + publishDeliveringService) + def expectedMessageId = 15 + def mqttClient = mqttConnection.client() as TestExternalMqttClient + mqttClient.returnCompletedFeatures(false) + when: + Publish publish = new Publish( + 1, + QoS.AT_MOST_ONCE, + TopicName.valueOf("topic2"), + null, + "payload".getBytes(UTF_8), + false, + true, + null, + IntArray.of(30), + null, + 60000, + 1, + PayloadFormat.UTF8_STRING, + Array.of()); + Subscription subscription = new Subscription( + defaultTopicService.createTopicFilter(mqttClient, "topic2"), + 30, + QoS.EXACTLY_ONCE, + SubscribeRetainHandling.SEND, + true, + true); + SingleSubscriber subscriber = new SingleSubscriber(mqttClient, subscription); + publishDeliveringService.startDelivering(publish, subscriber) + + def subscribeMessage = new SubscribeMqttInMessage(SubscribeMqttInMessage.MESSAGE_FLAGS) {{ + this.messageId = 1 + this.subscriptions = MutableArray.ofType(RequestedSubscription) + this.subscriptions.addAll(Array.of(RequestedSubscription.minimal("topic2", QoS.EXACTLY_ONCE))) + }} + messageHandler.processValidMessage(mqttConnection, subscribeMessage) + then: + mqttClient.nextSentMessage(PublishMqtt5OutMessage) + mqttClient.nextSentMessage(SubscribeAckMqtt5OutMessage) + def retainedMessageDelivery = mqttClient.nextSentMessage(PublishMqtt5OutMessage) + retainedMessageDelivery.messageId() == 2 + } } From c6ada4d461288fca6306978a485bc9402f4384d5 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov Date: Wed, 19 Nov 2025 19:05:56 +0100 Subject: [PATCH 02/38] [broker-30] Rewrite retained messages collecting --- .../mqtt/model/topic/AbstractTopic.java | 2 +- .../tree/ConcurrentRetainedMessageTree.java | 22 +-- .../model/topic/tree/RetainedMessageNode.java | 150 ++++++++++++++++++ .../model/topic/tree/TopicMessageNode.java | 94 ----------- .../service/PublishDeliveringService.java | 3 +- .../impl/DefaultPublishDeliveringService.java | 20 ++- .../impl/SubscribeMqttInMessageHandler.java | 18 ++- 7 files changed, 187 insertions(+), 122 deletions(-) create mode 100644 model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java delete mode 100644 model/src/main/java/javasabr/mqtt/model/topic/tree/TopicMessageNode.java diff --git a/model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java b/model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java index 7ff9a93e..9cdb4370 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java @@ -41,7 +41,7 @@ public int levelsCount() { return segments.length; } - String lastSegment() { + public String lastSegment() { return segments[segments.length - 1]; } diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java index a5bed489..78c78f11 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java @@ -2,30 +2,30 @@ import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.topic.TopicFilter; -import javasabr.mqtt.model.topic.TopicName; +import javasabr.rlib.collections.array.Array; +import javasabr.rlib.collections.array.MutableArray; import javasabr.rlib.common.ThreadSafe; import lombok.AccessLevel; import lombok.experimental.FieldDefaults; -import org.jspecify.annotations.Nullable; @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) public class ConcurrentRetainedMessageTree implements ThreadSafe { - TopicMessageNode rootNode; + RetainedMessageNode rootNode; public ConcurrentRetainedMessageTree() { - this.rootNode = new TopicMessageNode(); + this.rootNode = new RetainedMessageNode(); } public void retainMessage(Publish message) { - rootNode.retainMessage(0, message, message.topicName()); + if (message.retained()) { + rootNode.retainMessage(0, message, message.topicName()); + } } - public @Nullable Publish getRetainedMessage(TopicName topicName) { - return rootNode.getRetainedMessage(0, topicName); - } - - public @Nullable Publish getRetainedMessage(TopicFilter topicFilter) { - return rootNode.getRetainedMessage(0, topicFilter); + public Array getRetainedMessage(TopicFilter topicFilter) { + var resultArray = MutableArray.ofType(Publish.class); + rootNode.collectRetainedMessages(0, topicFilter, topicFilter.levelsCount() - 1, resultArray); + return resultArray; } } diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java new file mode 100644 index 00000000..0c362fc9 --- /dev/null +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java @@ -0,0 +1,150 @@ +package javasabr.mqtt.model.topic.tree; + +import static javasabr.mqtt.model.topic.TopicFilter.MULTI_LEVEL_WILDCARD; +import static javasabr.mqtt.model.topic.TopicFilter.SINGLE_LEVEL_WILDCARD; + +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import javasabr.mqtt.base.util.DebugUtils; +import javasabr.mqtt.model.publishing.Publish; +import javasabr.mqtt.model.topic.TopicFilter; +import javasabr.mqtt.model.topic.TopicName; +import javasabr.rlib.collections.array.MutableArray; +import javasabr.rlib.collections.dictionary.DictionaryFactory; +import javasabr.rlib.collections.dictionary.LockableRefToRefDictionary; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.jspecify.annotations.Nullable; + +@Getter(AccessLevel.PACKAGE) +@Accessors(fluent = true, chain = false) +@FieldDefaults(level = AccessLevel.PRIVATE) +class RetainedMessageNode { + + private final static Supplier TOPIC_NODE_FACTORY = RetainedMessageNode::new; + + static { + DebugUtils.registerIncludedFields("childNodes", "retainedMessage"); + } + + @Nullable + volatile LockableRefToRefDictionary childNodes; + final AtomicReference<@Nullable Publish> retainedMessage = new AtomicReference<>(); + + public void retainMessage(int level, Publish message, TopicName topicName) { + if (level + 1 == topicName.levelsCount()) { + retainedMessage.set(message.payload().length == 0 ? null : message); + return; + } + RetainedMessageNode childNode = getOrCreateChildNode(topicName.segment(level)); + childNode.retainMessage(level + 1, message, topicName); + } + + public void collectRetainedMessages(int level, TopicFilter topicFilter, int lastLevel, MutableArray result) { + String segment = topicFilter.segment(level); + Publish publish = retainedMessage.get(); + if (Objects.equals(segment, MULTI_LEVEL_WILDCARD)) { + collectAllMessages(this, result); + } else if (Objects.equals(segment, SINGLE_LEVEL_WILDCARD)) { + var childNodes = childNodes(); + if (childNodes == null) { + return; + } + long stamp = childNodes.readLock(); + try { + for (RetainedMessageNode n : childNodes) { + n.collectRetainedMessages(level + 1, topicFilter, lastLevel, result); + } + } finally { + childNodes.readUnlock(stamp); + } + } else if (level == lastLevel && publish != null && Objects.equals(segment, publish.topicName().lastSegment())) { + result.add(publish); + } else { + RetainedMessageNode topicFilterNode = childNode(segment); + if (topicFilterNode == null) { + return; + } + topicFilterNode.collectRetainedMessages(level + 1, topicFilter, lastLevel, result); + } + } + + private void collectAllMessages(RetainedMessageNode node, MutableArray result) { + Queue queue = new PriorityQueue<>(); + queue.add(node); + while (!queue.isEmpty()) { + RetainedMessageNode poll = queue.poll(); + Publish message = poll.retainedMessage.get(); + if (message != null) { + result.add(message); + } + var childNodes = poll.childNodes(); + if (childNodes == null) { + continue; + } + long stamp = childNodes.readLock(); + try { + for (RetainedMessageNode n : childNodes) { + queue.add(n); + } + } finally { + childNodes.readUnlock(stamp); + } + } + } + + @Nullable + private RetainedMessageNode childNode(String segment) { + LockableRefToRefDictionary childNodes = childNodes(); + if (childNodes == null) { + return null; + } + long stamp = childNodes.readLock(); + try { + return childNodes.get(segment); + } finally { + childNodes.readUnlock(stamp); + } + } + + private RetainedMessageNode getOrCreateChildNode(String segment) { + LockableRefToRefDictionary childNodes = getOrCreateChildNodes(); + long stamp = childNodes.readLock(); + try { + RetainedMessageNode topicFilterNode = childNodes.get(segment); + if (topicFilterNode != null) { + return topicFilterNode; + } + } finally { + childNodes.readUnlock(stamp); + } + stamp = childNodes.writeLock(); + try { + return childNodes.getOrCompute(segment, TOPIC_NODE_FACTORY); + } finally { + childNodes.writeUnlock(stamp); + } + } + + private LockableRefToRefDictionary getOrCreateChildNodes() { + if (childNodes == null) { + synchronized (this) { + if (childNodes == null) { + childNodes = DictionaryFactory.stampedLockBasedRefToRefDictionary(); + } + } + } + //noinspection ConstantConditions + return childNodes; + } + + @Override + public String toString() { + return DebugUtils.toJsonString(this); + } +} diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/TopicMessageNode.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/TopicMessageNode.java deleted file mode 100644 index 55cad0ec..00000000 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/TopicMessageNode.java +++ /dev/null @@ -1,94 +0,0 @@ -package javasabr.mqtt.model.topic.tree; - -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; -import javasabr.mqtt.base.util.DebugUtils; -import javasabr.mqtt.model.publishing.Publish; -import javasabr.mqtt.model.topic.TopicFilter; -import javasabr.mqtt.model.topic.TopicName; -import javasabr.rlib.collections.dictionary.DictionaryFactory; -import javasabr.rlib.collections.dictionary.LockableRefToRefDictionary; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.experimental.Accessors; -import lombok.experimental.FieldDefaults; -import org.jspecify.annotations.Nullable; - -@Getter(AccessLevel.PACKAGE) -@Accessors(fluent = true, chain = false) -@FieldDefaults(level = AccessLevel.PRIVATE) -class TopicMessageNode { - - private final static Supplier TOPIC_NODE_FACTORY = TopicMessageNode::new; - - static { - DebugUtils.registerIncludedFields("childNodes", "retainedMessage"); - } - - @Nullable - volatile LockableRefToRefDictionary childNodes; - final AtomicReference<@Nullable Publish> retainedMessage = new AtomicReference<>(); - - public void retainMessage(int level, Publish message, TopicName topicFilter) { - if (level + 1 == topicFilter.levelsCount()) { - retainedMessage.set(message); - return; - } - TopicMessageNode childNode = getOrCreateChildNode(topicFilter.segment(level)); - childNode.retainMessage(level + 1, message, topicFilter); - } - - @Nullable - public Publish getRetainedMessage(int level, TopicName topicName) { - if (level + 1 == topicName.levelsCount()) { - return retainedMessage.get(); - } - TopicMessageNode childNode = getOrCreateChildNode(topicName.segment(level)); - return childNode.getRetainedMessage(level + 1, topicName); - } - - @Nullable - public Publish getRetainedMessage(int level, TopicFilter topicName) { - if (level + 1 == topicName.levelsCount()) { - return retainedMessage.get(); - } - TopicMessageNode childNode = getOrCreateChildNode(topicName.segment(level)); - return childNode.getRetainedMessage(level + 1, topicName); - } - - private TopicMessageNode getOrCreateChildNode(String segment) { - LockableRefToRefDictionary childNodes = getOrCreateChildNodes(); - long stamp = childNodes.readLock(); - try { - TopicMessageNode topicFilterNode = childNodes.get(segment); - if (topicFilterNode != null) { - return topicFilterNode; - } - } finally { - childNodes.readUnlock(stamp); - } - stamp = childNodes.writeLock(); - try { - return childNodes.getOrCompute(segment, TOPIC_NODE_FACTORY); - } finally { - childNodes.writeUnlock(stamp); - } - } - - private LockableRefToRefDictionary getOrCreateChildNodes() { - if (childNodes == null) { - synchronized (this) { - if (childNodes == null) { - childNodes = DictionaryFactory.stampedLockBasedRefToRefDictionary(); - } - } - } - //noinspection ConstantConditions - return childNodes; - } - - @Override - public String toString() { - return DebugUtils.toJsonString(this); - } -} diff --git a/service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java b/service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java index f051d8a3..b5f65426 100644 --- a/service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java +++ b/service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java @@ -4,10 +4,11 @@ import javasabr.mqtt.model.subscriber.SingleSubscriber; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; +import javasabr.rlib.collections.array.Array; public interface PublishDeliveringService { PublishHandlingResult startDelivering(Publish publish, SingleSubscriber subscriber); - PublishHandlingResult deliverRetainedMessages(TopicFilter topicFilter, SingleSubscriber subscriber); + Array deliverRetainedMessages(TopicFilter topicFilter, SingleSubscriber subscriber); } diff --git a/service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java b/service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java index c66d3f49..9c45b9b7 100644 --- a/service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java +++ b/service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java @@ -9,6 +9,8 @@ import javasabr.mqtt.service.PublishDeliveringService; import javasabr.mqtt.service.publish.handler.MqttPublishOutMessageHandler; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; +import javasabr.rlib.collections.array.Array; +import javasabr.rlib.collections.array.MutableArray; import lombok.AccessLevel; import lombok.CustomLog; import lombok.experimental.FieldDefaults; @@ -20,7 +22,7 @@ public class DefaultPublishDeliveringService implements PublishDeliveringService @Nullable MqttPublishOutMessageHandler[] publishOutMessageHandlers; - ConcurrentRetainedMessageTree topicTree; + ConcurrentRetainedMessageTree retainedMessageTree; public DefaultPublishDeliveringService( Collection knownPublishOutHandlers) { @@ -42,7 +44,7 @@ public DefaultPublishDeliveringService( } handlers[qos.level()] = knownPublishOutHandler; } - this.topicTree = new ConcurrentRetainedMessageTree(); + this.retainedMessageTree = new ConcurrentRetainedMessageTree(); this.publishOutMessageHandlers = handlers; log.info(publishOutMessageHandlers, DefaultPublishDeliveringService::buildServiceDescription); } @@ -50,9 +52,7 @@ public DefaultPublishDeliveringService( @Override public PublishHandlingResult startDelivering(Publish publish, SingleSubscriber subscriber) { try { - if (publish.retained()) { - topicTree.retainMessage(publish); - } + retainedMessageTree.retainMessage(publish); //noinspection DataFlowIssue return publishOutMessageHandlers[subscriber.qos().level()].handle(publish, subscriber); } catch (IndexOutOfBoundsException | NullPointerException ex) { @@ -62,9 +62,13 @@ public PublishHandlingResult startDelivering(Publish publish, SingleSubscriber s } @Override - public PublishHandlingResult deliverRetainedMessages(TopicFilter topicFilter, SingleSubscriber subscriber) { - Publish retainedMessage = topicTree.getRetainedMessage(topicFilter); - return startDelivering(retainedMessage, subscriber); + public Array deliverRetainedMessages(TopicFilter topicFilter, SingleSubscriber subscriber) { + Array retainedMessage = retainedMessageTree.getRetainedMessage(topicFilter); + MutableArray result = MutableArray.ofType(PublishHandlingResult.class); + for (Publish message : retainedMessage) { + result.add(startDelivering(message, subscriber)); + } + return result; } private static String buildServiceDescription( diff --git a/service/src/main/java/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandler.java b/service/src/main/java/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandler.java index 949bdf48..67eab259 100644 --- a/service/src/main/java/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandler.java +++ b/service/src/main/java/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandler.java @@ -194,23 +194,27 @@ private void sendRetainedMessages( RequestedSubscription requestedSubscription = subscriptions.get(i); SubscribeAckReasonCode subscribeAckReasonCode = subscribeResults.get(i); Subscription subscription = subs.get(i); - if (subscribeAckReasonCode.ordinal() < 3) { - TopicFilter topicFilter = TopicFilter.valueOf(requestedSubscription.rawTopicFilter()); - SingleSubscriber singleSubscriber = new SingleSubscriber(client, subscription); - PublishHandlingResult result = publishDeliveringService.deliverRetainedMessages(topicFilter, singleSubscriber); + if (subscribeAckReasonCode.ordinal() > 2) { + // TODO handle error + continue; + } + TopicFilter topicFilter = TopicFilter.valueOf(requestedSubscription.rawTopicFilter()); + SingleSubscriber singleSubscriber = new SingleSubscriber(client, subscription); + var results = publishDeliveringService.deliverRetainedMessages(topicFilter, singleSubscriber); + for (PublishHandlingResult result : results) { if (result.error()) { errorResult = result; - } else if(result == PublishHandlingResult.SUCCESS) { + } else if (result == PublishHandlingResult.SUCCESS) { count++; } if (errorResult != null) { log.debug(client.clientId(), errorResult, "[%s] Found final error:[%s] during sending retained messages"::formatted); - // handleError(client, publish, errorResult); + // TODO handleError(client, publish, errorResult); } else { log.debug(client.clientId(), count, "[%s] Successfully started delivering retained messages to [%s] subscribers"::formatted); - // handleSuccessfulResult(client, publish, count); + // TODO handleSuccessfulResult(client, publish, count); } } } From 01bac5bffc8ec7d39213170e28e6f3565f01e159 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov Date: Wed, 19 Nov 2025 21:48:58 +0100 Subject: [PATCH 03/38] [broker-30] Cleanup code after merge --- .../mqtt/model/subscriber/tree/ConcurrentSubscriberTree.java | 1 - .../javasabr/mqtt/model/subscriber/tree/SubscriberNode.java | 5 ----- .../src/main/java/javasabr/mqtt/model/topic/TopicFilter.java | 4 ++-- .../java/javasabr/mqtt/model/topic/tree/package-info.java | 2 +- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/ConcurrentSubscriberTree.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/ConcurrentSubscriberTree.java index 5edb2233..c5e52e00 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/ConcurrentSubscriberTree.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/ConcurrentSubscriberTree.java @@ -13,7 +13,6 @@ import org.jspecify.annotations.Nullable; @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) - public class ConcurrentSubscriberTree implements ThreadSafe { SubscriberNode rootNode; diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java index a3ac5beb..ddd62086 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java @@ -4,7 +4,6 @@ import javasabr.mqtt.base.util.DebugUtils; import javasabr.mqtt.model.subscriber.SingleSubscriber; import javasabr.mqtt.model.subscriber.Subscriber; -import javasabr.mqtt.model.subscriber.tree.SubscriberTreeBase; import javasabr.mqtt.model.subscribtion.Subscription; import javasabr.mqtt.model.subscribtion.SubscriptionOwner; import javasabr.mqtt.model.topic.TopicFilter; @@ -44,7 +43,6 @@ public SingleSubscriber subscribe(int level, SubscriptionOwner owner, Subscripti if (level == topicFilter.levelsCount()) { return addSubscriber(getOrCreateSubscribers(), owner, subscription, topicFilter); } - SubscriberNode childNode = getOrCreateChildNode(topicFilter.segment(level)); return childNode.subscribe(level + 1, owner, subscription, topicFilter); } @@ -53,7 +51,6 @@ public boolean unsubscribe(int level, SubscriptionOwner owner, TopicFilter topic if (level == topicFilter.levelsCount()) { return removeSubscriber(subscribers(), owner, topicFilter); } - SubscriberNode childNode = getOrCreateChildNode(topicFilter.segment(level)); return childNode.unsubscribe(level + 1, owner, topicFilter); } @@ -86,7 +83,6 @@ private void singleWildcardTopicMatch( TopicName topicName, int lastLevel, MutableArray result) { - SubscriberNode subscriberNode = childNode(TopicFilter.SINGLE_LEVEL_WILDCARD); if (subscriberNode == null) { return; @@ -99,7 +95,6 @@ private void singleWildcardTopicMatch( } private void multiWildcardTopicMatch(MutableArray result) { - SubscriberNode subscriberNode = childNode(TopicFilter.MULTI_LEVEL_WILDCARD); if (subscriberNode != null) { appendSubscribersTo(result, subscriberNode); diff --git a/model/src/main/java/javasabr/mqtt/model/topic/TopicFilter.java b/model/src/main/java/javasabr/mqtt/model/topic/TopicFilter.java index da653f8a..fa9c1b6f 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/TopicFilter.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/TopicFilter.java @@ -11,9 +11,9 @@ public class TopicFilter extends AbstractTopic { public static final String MULTI_LEVEL_WILDCARD = "#"; - public static final char MULTI_LEVEL_WILDCARD_CHAR = MULTI_LEVEL_WILDCARD.charAt(0); + public static final char MULTI_LEVEL_WILDCARD_CHAR = '#'; public static final String SINGLE_LEVEL_WILDCARD = "+"; - public static final char SINGLE_LEVEL_WILDCARD_CHAR = SINGLE_LEVEL_WILDCARD.charAt(0); + public static final char SINGLE_LEVEL_WILDCARD_CHAR = '+'; public static final String SPECIAL = "$"; public static final TopicFilter INVALID_TOPIC_FILTER = new TopicFilter("$invalid$") { diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/package-info.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/package-info.java index 95d9e9e6..1df48806 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/package-info.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/package-info.java @@ -1,4 +1,4 @@ @NullMarked package javasabr.mqtt.model.topic.tree; -import org.jspecify.annotations.NullMarked; \ No newline at end of file +import org.jspecify.annotations.NullMarked; From c903510a753ac3b60e7d2c4129ec26444c879043 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov Date: Thu, 20 Nov 2025 08:15:35 +0100 Subject: [PATCH 04/38] [broker-30] Fix corner cases in retained messages --- .../tree/ConcurrentRetainedMessageTree.java | 2 +- .../model/topic/tree/RetainedMessageNode.java | 45 ++++--- .../topic/tree/RetainedMessageTreeTest.groovy | 122 ++++++++++++++++++ 3 files changed, 151 insertions(+), 18 deletions(-) create mode 100644 model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java index 78c78f11..6f38475f 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java @@ -25,7 +25,7 @@ public void retainMessage(Publish message) { public Array getRetainedMessage(TopicFilter topicFilter) { var resultArray = MutableArray.ofType(Publish.class); - rootNode.collectRetainedMessages(0, topicFilter, topicFilter.levelsCount() - 1, resultArray); + rootNode.collectRetainedMessages(0, topicFilter, resultArray); return resultArray; } } diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java index 0c362fc9..cc6b4b68 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java @@ -3,6 +3,7 @@ import static javasabr.mqtt.model.topic.TopicFilter.MULTI_LEVEL_WILDCARD; import static javasabr.mqtt.model.topic.TopicFilter.SINGLE_LEVEL_WILDCARD; +import java.util.LinkedList; import java.util.Objects; import java.util.PriorityQueue; import java.util.Queue; @@ -37,19 +38,22 @@ class RetainedMessageNode { final AtomicReference<@Nullable Publish> retainedMessage = new AtomicReference<>(); public void retainMessage(int level, Publish message, TopicName topicName) { - if (level + 1 == topicName.levelsCount()) { - retainedMessage.set(message.payload().length == 0 ? null : message); - return; + var child = getOrCreateChildNode(topicName.segment(level)); + boolean isLeaf = (level + 1 == topicName.levelsCount()); + if (isLeaf) { + if (Objects.equals(message.topicName().lastSegment(), topicName.lastSegment())) { + child.retainedMessage.set(message.payload().length == 0 ? null : message); + } + } else { + child.retainMessage(level + 1, message, topicName); } - RetainedMessageNode childNode = getOrCreateChildNode(topicName.segment(level)); - childNode.retainMessage(level + 1, message, topicName); } - public void collectRetainedMessages(int level, TopicFilter topicFilter, int lastLevel, MutableArray result) { + public void collectRetainedMessages(int level, TopicFilter topicFilter, MutableArray result) { String segment = topicFilter.segment(level); - Publish publish = retainedMessage.get(); if (Objects.equals(segment, MULTI_LEVEL_WILDCARD)) { collectAllMessages(this, result); + return; } else if (Objects.equals(segment, SINGLE_LEVEL_WILDCARD)) { var childNodes = childNodes(); if (childNodes == null) { @@ -57,25 +61,32 @@ public void collectRetainedMessages(int level, TopicFilter topicFilter, int last } long stamp = childNodes.readLock(); try { - for (RetainedMessageNode n : childNodes) { - n.collectRetainedMessages(level + 1, topicFilter, lastLevel, result); + for (RetainedMessageNode childNode : childNodes) { + childNode.collectRetainedMessages(level + 1, topicFilter, result); } } finally { childNodes.readUnlock(stamp); } - } else if (level == lastLevel && publish != null && Objects.equals(segment, publish.topicName().lastSegment())) { - result.add(publish); - } else { - RetainedMessageNode topicFilterNode = childNode(segment); - if (topicFilterNode == null) { - return; + return; + } + int lastLevel = topicFilter.levelsCount() - 1; + RetainedMessageNode retainedMessageNode = childNode(segment); + if (retainedMessageNode == null || level > lastLevel) { + return; + } + boolean isLeaf = (level == lastLevel); + if (isLeaf) { + Publish publish = retainedMessageNode.retainedMessage.get(); + if(publish != null && Objects.equals(segment, publish.topicName().lastSegment())){ + result.add(publish); } - topicFilterNode.collectRetainedMessages(level + 1, topicFilter, lastLevel, result); + } else { + retainedMessageNode.collectRetainedMessages(level + 1, topicFilter, result); } } private void collectAllMessages(RetainedMessageNode node, MutableArray result) { - Queue queue = new PriorityQueue<>(); + Queue queue = new LinkedList<>(); queue.add(node); while (!queue.isEmpty()) { RetainedMessageNode poll = queue.poll(); diff --git a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy new file mode 100644 index 00000000..1baf0582 --- /dev/null +++ b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy @@ -0,0 +1,122 @@ +package javasabr.mqtt.model.topic.tree + + +import javasabr.mqtt.model.PayloadFormat +import javasabr.mqtt.model.QoS +import javasabr.mqtt.model.publishing.Publish +import javasabr.mqtt.model.topic.TopicFilter +import javasabr.mqtt.model.topic.TopicName +import javasabr.mqtt.test.support.UnitSpecification +import javasabr.rlib.collections.array.Array +import javasabr.rlib.collections.array.IntArray + +import static java.nio.charset.StandardCharsets.UTF_8 + +class RetainedMessageTreeTest extends UnitSpecification { + + def "should fetch retained messages by topic filter"( + List messages, + String topicFilter, + List expectedMessages) { + given: + ConcurrentRetainedMessageTree retainedMessageTree = new ConcurrentRetainedMessageTree(); + messages.eachWithIndex { Publish message, int i -> + retainedMessageTree.retainMessage(message) + } + when: + def retainedMessages = retainedMessageTree.getRetainedMessage(TopicFilter.valueOf(topicFilter)) + .collect { it } + then: + retainedMessages.size() == expectedMessages.size() + for (int i = 0; i < retainedMessages.size(); i++) { + assert retainedMessages.get(i).topicName() == expectedMessages.get(i).topicName() + } + where: + topicFilter << [ + "/topic/segment1", + "/topic/segment2", + "/topic/segment3", + "/topic/+/segment2", + "/topic/#" + ] + messages << [ + [ + makePublish("/topic/segment1"), + makePublish("/topic/segment2"), + makePublish("/topic/segment1/segment2"), + makePublish("/topic/"), + makePublish("/topic") + ], + [ + makePublish("/topic/segment1"), + makePublish("/topic/segment2"), + makePublish("/topic/segment1/segment2"), + makePublish("/topic/"), + makePublish("/topic/segment2"), + makePublish("/"), + makePublish("/topic/segment2/segment1") + ], + [ + makePublish("/topic/segment1"), + makePublish("/topic/segment2"), + makePublish("/topic/segment3"), + makePublish("/topic/segment3"), + makePublish("/topic/segment3"), + makePublish("/topic/segment3") + ], + [ + makePublish("/topic/segment1"), + makePublish("/topic/segment2"), + makePublish("/topic/segment1/segment2"), + makePublish("/topic/segment500/segment2"), + makePublish("/topic/"), + makePublish("/topic") + ], + [ + makePublish("/topic1/segment1"), + makePublish("/topic/segment2"), + makePublish("/topic2/segment1/segment2"), + makePublish("/topic/segment3"), + makePublish("/topic/segment1/segment2") + ] + ] + expectedMessages << [ + [ + makePublish("/topic/segment1") + ], + [ + makePublish("/topic/segment2") + ], + [ + makePublish("/topic/segment3") + ], + [ + makePublish("/topic/segment1/segment2"), + makePublish("/topic/segment500/segment2") + ], + [ + makePublish("/topic/segment2"), + makePublish("/topic/segment3"), + makePublish("/topic/segment1/segment2") + ] + ] + } + + static def makePublish(String topicName) { + return new Publish( + 1, + QoS.AT_MOST_ONCE, + TopicName.valueOf(topicName), + null, + "payload".getBytes(UTF_8), + false, + true, + null, + IntArray.of(30), + null, + 60000, + 1, + PayloadFormat.UTF8_STRING, + Array.of()); + } +} From e1f5c2a4fc148be5c421d3df3041f65a4cad69f3 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov Date: Thu, 20 Nov 2025 10:35:31 +0100 Subject: [PATCH 05/38] [broker-30] Handle SubscribeRetainHandling --- .../config/MqttBrokerSpringConfig.java | 9 ++-- .../impl/InMemorySubscriptionService.java | 49 ++++++++++++++++++- .../impl/SubscribeMqttInMessageHandler.java | 47 +----------------- 3 files changed, 52 insertions(+), 53 deletions(-) diff --git a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java index b809f587..e8e7597f 100644 --- a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java +++ b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java @@ -99,8 +99,8 @@ AuthenticationService authenticationService( } @Bean - SubscriptionService subscriptionService() { - return new InMemorySubscriptionService(); + SubscriptionService subscriptionService(PublishDeliveringService publishDeliveringService) { + return new InMemorySubscriptionService(publishDeliveringService); } @Bean @@ -179,9 +179,8 @@ MqttInMessageHandler disconnectMqttInMessageHandler(MessageOutFactoryService mes MqttInMessageHandler subscribeMqttInMessageHandler( SubscriptionService subscriptionService, MessageOutFactoryService messageOutFactoryService, - TopicService topicService, - PublishDeliveringService publishDeliveringService) { - return new SubscribeMqttInMessageHandler(subscriptionService, messageOutFactoryService, topicService, publishDeliveringService); + TopicService topicService) { + return new SubscribeMqttInMessageHandler(subscriptionService, messageOutFactoryService, topicService); } @Bean diff --git a/service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index 78647c51..3128e574 100644 --- a/service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -1,5 +1,7 @@ package javasabr.mqtt.service.impl; +import static javasabr.mqtt.model.SubscribeRetainHandling.SEND; +import static javasabr.mqtt.model.SubscribeRetainHandling.SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST; import static javasabr.mqtt.model.reason.code.UnsubscribeAckReasonCode.NO_SUBSCRIPTION_EXISTED; import static javasabr.mqtt.model.reason.code.UnsubscribeAckReasonCode.SUCCESS; @@ -8,15 +10,17 @@ import javasabr.mqtt.model.reason.code.UnsubscribeAckReasonCode; import javasabr.mqtt.model.subscriber.SingleSubscriber; import javasabr.mqtt.model.subscriber.Subscriber; +import javasabr.mqtt.model.subscriber.tree.ConcurrentSubscriberTree; import javasabr.mqtt.model.subscribtion.Subscription; import javasabr.mqtt.model.topic.SharedTopicFilter; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.mqtt.model.topic.TopicName; -import javasabr.mqtt.model.subscriber.tree.ConcurrentSubscriberTree; import javasabr.mqtt.network.MqttClient; import javasabr.mqtt.network.session.ActiveSubscriptions; import javasabr.mqtt.network.session.MqttSession; +import javasabr.mqtt.service.PublishDeliveringService; import javasabr.mqtt.service.SubscriptionService; +import javasabr.mqtt.service.publish.handler.PublishHandlingResult; import javasabr.rlib.collections.array.Array; import javasabr.rlib.collections.array.ArrayFactory; import javasabr.rlib.collections.array.MutableArray; @@ -31,10 +35,12 @@ @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) public class InMemorySubscriptionService implements SubscriptionService { + PublishDeliveringService publishDeliveringService; ConcurrentSubscriberTree subscriberTree; - public InMemorySubscriptionService() { + public InMemorySubscriptionService(PublishDeliveringService publishDeliveringService) { this.subscriberTree = new ConcurrentSubscriberTree(); + this.publishDeliveringService = publishDeliveringService; } @Override @@ -84,6 +90,10 @@ private SubscribeAckReasonCode addSubscription(MqttClient client, MqttSession se if (previous != null) { activeSubscriptions.remove(previous.subscription()); } + if ((subscription.retainHandling() == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previous != null) + || subscription.retainHandling() == SEND) { + sendRetainedMessages(client, subscription); + } activeSubscriptions.add(subscription); return subscription.qos().subscribeAckReasonCode(); } @@ -137,4 +147,39 @@ public void restoreSubscriptions(MqttClient client, MqttSession session) { subscriberTree.subscribe(client, subscription); } } + + private void sendRetainedMessages(MqttClient client, Subscription subscription) { + int count = 0; + PublishHandlingResult errorResult = null; + if (subscription + .qos() + .subscribeAckReasonCode() + .ordinal() > 2) { + // TODO handle error ? + return; + } + SingleSubscriber singleSubscriber = new SingleSubscriber(client, subscription); + var results = publishDeliveringService.deliverRetainedMessages(subscription.topicFilter(), singleSubscriber); + for (PublishHandlingResult result : results) { + if (result.error()) { + errorResult = result; + } else if (result == PublishHandlingResult.SUCCESS) { + count++; + } + if (errorResult != null) { + log.debug( + client.clientId(), + errorResult, + "[%s] Found final error:[%s] during sending retained messages"::formatted); + // TODO handleError(client, publish, errorResult); + } else { + log.debug( + client.clientId(), + count, + "[%s] Successfully started delivering retained messages to [%s] subscribers"::formatted); + // TODO handleSuccessfulResult(client, publish, count); + } + + } + } } diff --git a/service/src/main/java/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandler.java b/service/src/main/java/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandler.java index 67eab259..9e5528d7 100644 --- a/service/src/main/java/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandler.java +++ b/service/src/main/java/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandler.java @@ -9,7 +9,6 @@ import javasabr.mqtt.model.QoS; import javasabr.mqtt.model.reason.code.DisconnectReasonCode; import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode; -import javasabr.mqtt.model.subscriber.SingleSubscriber; import javasabr.mqtt.model.subscribtion.RequestedSubscription; import javasabr.mqtt.model.subscribtion.Subscription; import javasabr.mqtt.model.topic.TopicFilter; @@ -21,10 +20,8 @@ import javasabr.mqtt.network.session.MessageTacker; import javasabr.mqtt.network.session.MqttSession; import javasabr.mqtt.service.MessageOutFactoryService; -import javasabr.mqtt.service.PublishDeliveringService; import javasabr.mqtt.service.SubscriptionService; import javasabr.mqtt.service.TopicService; -import javasabr.mqtt.service.publish.handler.PublishHandlingResult; import javasabr.rlib.collections.array.Array; import javasabr.rlib.collections.array.ArrayFactory; import javasabr.rlib.collections.array.MutableArray; @@ -41,19 +38,16 @@ public class SubscribeMqttInMessageHandler extends SHARED_SUBSCRIPTIONS_NOT_SUPPORTED, WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED); - PublishDeliveringService publishDeliveringService; SubscriptionService subscriptionService; TopicService topicService; public SubscribeMqttInMessageHandler( SubscriptionService subscriptionService, MessageOutFactoryService messageOutFactoryService, - TopicService topicService, - PublishDeliveringService publishDeliveringService) { + TopicService topicService) { super(ExternalMqttClient.class, SubscribeMqttInMessage.class, messageOutFactoryService); this.subscriptionService = subscriptionService; this.topicService = topicService; - this.publishDeliveringService = publishDeliveringService; } @Override @@ -98,7 +92,6 @@ protected void processValidMessage( .subscribe(client, session, subscriptions); sendSubscribeResults(client, session, subscribeMessage, subscribeResults); - sendRetainedMessages(client, subscribeMessage, subscribeResults, subscriptions); SubscribeAckReasonCode anyReasonToDisconnect = subscribeResults .iterations() @@ -181,42 +174,4 @@ private void sendSubscribeResults( .inMessageTracker() .remove(messageId)); } - - private void sendRetainedMessages( - ExternalMqttClient client, - SubscribeMqttInMessage subscribeMessage, - Array subscribeResults, - Array subs) { - int count = 0; - PublishHandlingResult errorResult = null; - Array subscriptions = subscribeMessage.subscriptions(); - for (int i = 0; i < subscribeMessage.subscriptionsCount(); i++) { - RequestedSubscription requestedSubscription = subscriptions.get(i); - SubscribeAckReasonCode subscribeAckReasonCode = subscribeResults.get(i); - Subscription subscription = subs.get(i); - if (subscribeAckReasonCode.ordinal() > 2) { - // TODO handle error - continue; - } - TopicFilter topicFilter = TopicFilter.valueOf(requestedSubscription.rawTopicFilter()); - SingleSubscriber singleSubscriber = new SingleSubscriber(client, subscription); - var results = publishDeliveringService.deliverRetainedMessages(topicFilter, singleSubscriber); - for (PublishHandlingResult result : results) { - if (result.error()) { - errorResult = result; - } else if (result == PublishHandlingResult.SUCCESS) { - count++; - } - if (errorResult != null) { - log.debug(client.clientId(), errorResult, - "[%s] Found final error:[%s] during sending retained messages"::formatted); - // TODO handleError(client, publish, errorResult); - } else { - log.debug(client.clientId(), count, - "[%s] Successfully started delivering retained messages to [%s] subscribers"::formatted); - // TODO handleSuccessfulResult(client, publish, count); - } - } - } - } } From a1b87dd99f6272247635bcb2500eeebc37c8515d Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:10:45 +0100 Subject: [PATCH 06/38] [broker-30] Fix build --- .../config/MqttBrokerSpringConfig.java | 69 ++++---------- .../mqtt/service/SubscriptionService.java | 3 - .../impl/InMemorySubscriptionService.java | 9 -- .../AbstractMqttPublishOutMessageHandler.java | 12 ++- ...PersistedMqttPublishOutMessageHandler.java | 13 +-- .../Qos0MqttPublishOutMessageHandler.java | 7 +- .../Qos1MqttPublishOutMessageHandler.java | 7 +- .../Qos2MqttPublishOutMessageHandler.java | 7 +- .../IntegrationServiceSpecification.groovy | 12 +-- .../InMemorySubscriptionServiceTest.groovy | 20 ++--- .../SubscribeMqttInMessageHandlerTest.groovy | 90 ++----------------- ...UnsubscribeMqttInMessageHandlerTest.groovy | 2 +- 12 files changed, 64 insertions(+), 187 deletions(-) diff --git a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java index a6961877..756014ba 100644 --- a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java +++ b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java @@ -93,7 +93,8 @@ CredentialSource credentialSource( @Bean AuthenticationService authenticationService( CredentialSource credentialSource, - @Value("${authentication.allow.anonymous:false}") boolean allowAnonymousAuth) { + @Value("${authentication.allow.anonymous:false}") + boolean allowAnonymousAuth) { return new SimpleAuthenticationService(credentialSource, allowAnonymousAuth); } @@ -153,10 +154,7 @@ MqttInMessageHandler publishMqttInMessageHandler( PublishReceivingService publishReceivingService, MessageOutFactoryService messageOutFactoryService, TopicService topicService) { - return new PublishMqttInMessageHandler( - publishReceivingService, - messageOutFactoryService, - topicService); + return new PublishMqttInMessageHandler(publishReceivingService, messageOutFactoryService, topicService); } @Bean @@ -187,10 +185,7 @@ MqttInMessageHandler unsubscribeMqttInMessageHandler( SubscriptionService subscriptionService, MessageOutFactoryService messageOutFactoryService, TopicService topicService) { - return new UnsubscribeMqttInMessageHandler( - subscriptionService, - messageOutFactoryService, - topicService); + return new UnsubscribeMqttInMessageHandler(subscriptionService, messageOutFactoryService, topicService); } @Bean @@ -199,24 +194,18 @@ ConnectionService externalMqttConnectionService(Collection findSubscribers(TopicName topicName) { return findSubscribersTo(MutableArray.ofType(SingleSubscriber.class), topicName); } diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index c1234fe4..1c2e3e74 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -12,7 +12,6 @@ import javasabr.mqtt.model.session.ActiveSubscriptions; import javasabr.mqtt.model.session.MqttSession; import javasabr.mqtt.model.subscriber.SingleSubscriber; -import javasabr.mqtt.model.subscriber.Subscriber; import javasabr.mqtt.model.subscriber.tree.ConcurrentSubscriberTree; import javasabr.mqtt.model.subscription.Subscription; import javasabr.mqtt.model.topic.SharedTopicFilter; @@ -44,14 +43,6 @@ public InMemorySubscriptionService(PublishDeliveringService publishDeliveringSer this.publishDeliveringService = publishDeliveringService; } - @Override - public NetworkMqttUser resolveClient(Subscriber subscriber) { - if (subscriber instanceof SingleSubscriber single) { - return (NetworkMqttUser) single.user(); - } - throw new IllegalArgumentException("Unexpected subscriber: " + subscriber); - } - @Override public Array findSubscribersTo(MutableArray container, TopicName topicName) { Array matched = subscriberTree.matches(topicName); diff --git a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishOutMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishOutMessageHandler.java index 613e1f96..2b329bc6 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishOutMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishOutMessageHandler.java @@ -3,10 +3,10 @@ import javasabr.mqtt.model.MqttProperties; import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.subscriber.SingleSubscriber; +import javasabr.mqtt.model.subscriber.Subscriber; import javasabr.mqtt.network.message.out.MqttOutMessage; import javasabr.mqtt.network.user.NetworkMqttUser; import javasabr.mqtt.service.MessageOutFactoryService; -import javasabr.mqtt.service.SubscriptionService; import javasabr.mqtt.service.publish.handler.MqttPublishOutMessageHandler; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; import lombok.AccessLevel; @@ -22,12 +22,18 @@ public abstract class AbstractMqttPublishOutMessageHandler expectedUser; - SubscriptionService subscriptionService; MessageOutFactoryService messageOutFactoryService; + private static NetworkMqttUser resolveClient(Subscriber subscriber) { + if (subscriber instanceof SingleSubscriber single) { + return (NetworkMqttUser) single.user(); + } + throw new IllegalArgumentException("Unexpected subscriber: " + subscriber); + } + @Override public PublishHandlingResult handle(Publish publish, SingleSubscriber subscriber) { - NetworkMqttUser user = subscriptionService.resolveClient(subscriber); + NetworkMqttUser user = resolveClient(subscriber); if (!expectedUser.isInstance(user)) { log.warning(user, "Accepted not expected client:[%s]"::formatted); return PublishHandlingResult.NOT_EXPECTED_CLIENT; diff --git a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/PersistedMqttPublishOutMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/PersistedMqttPublishOutMessageHandler.java index 13a5b252..9f572f5b 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/PersistedMqttPublishOutMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/PersistedMqttPublishOutMessageHandler.java @@ -8,7 +8,6 @@ import javasabr.mqtt.network.session.NetworkMqttSession.PendingMessageHandler; import javasabr.mqtt.network.user.NetworkMqttUser; import javasabr.mqtt.service.MessageOutFactoryService; -import javasabr.mqtt.service.SubscriptionService; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; import lombok.AccessLevel; import lombok.experimental.FieldDefaults; @@ -20,15 +19,14 @@ public abstract class PersistedMqttPublishOutMessageHandler extends PendingMessageHandler pendingMessageHandler; - protected PersistedMqttPublishOutMessageHandler( - SubscriptionService subscriptionService, - MessageOutFactoryService messageOutFactoryService) { - super(ExternalNetworkMqttUser.class, subscriptionService, messageOutFactoryService); + protected PersistedMqttPublishOutMessageHandler(MessageOutFactoryService messageOutFactoryService) { + super(ExternalNetworkMqttUser.class, messageOutFactoryService); this.pendingMessageHandler = new PendingMessageHandler() { @Override public boolean handleResponse(NetworkMqttUser user, TrackableMqttMessage response) { return handleReceivedResponse(user, response); } + @Override public void resend(NetworkMqttUser user, Publish publish) { tryToDeliverAgain(user, publish); @@ -45,10 +43,7 @@ protected Publish reconstruct(NetworkMqttUser user, Publish original) { } return original.with( // generate new uniq packet id per client - session.generateMessageId(), - qos(), - false, - MqttProperties.TOPIC_ALIAS_NOT_SET); + session.generateMessageId(), qos(), false, MqttProperties.TOPIC_ALIAS_NOT_SET); } @Override diff --git a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos0MqttPublishOutMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos0MqttPublishOutMessageHandler.java index ad09e51d..e31dd20b 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos0MqttPublishOutMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos0MqttPublishOutMessageHandler.java @@ -4,15 +4,12 @@ import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.network.impl.ExternalNetworkMqttUser; import javasabr.mqtt.service.MessageOutFactoryService; -import javasabr.mqtt.service.SubscriptionService; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; public class Qos0MqttPublishOutMessageHandler extends AbstractMqttPublishOutMessageHandler { - public Qos0MqttPublishOutMessageHandler( - SubscriptionService subscriptionService, - MessageOutFactoryService messageOutFactoryService) { - super(ExternalNetworkMqttUser.class, subscriptionService, messageOutFactoryService); + public Qos0MqttPublishOutMessageHandler(MessageOutFactoryService messageOutFactoryService) { + super(ExternalNetworkMqttUser.class, messageOutFactoryService); } @Override diff --git a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishOutMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishOutMessageHandler.java index 4aec8dd7..826ee01d 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishOutMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishOutMessageHandler.java @@ -5,14 +5,11 @@ import javasabr.mqtt.network.message.in.PublishAckMqttInMessage; import javasabr.mqtt.network.user.NetworkMqttUser; import javasabr.mqtt.service.MessageOutFactoryService; -import javasabr.mqtt.service.SubscriptionService; public class Qos1MqttPublishOutMessageHandler extends PersistedMqttPublishOutMessageHandler { - public Qos1MqttPublishOutMessageHandler( - SubscriptionService subscriptionService, - MessageOutFactoryService messageOutFactoryService) { - super(subscriptionService, messageOutFactoryService); + public Qos1MqttPublishOutMessageHandler(MessageOutFactoryService messageOutFactoryService) { + super(messageOutFactoryService); } @Override diff --git a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishOutMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishOutMessageHandler.java index cb9584f3..ecb65c0e 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishOutMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishOutMessageHandler.java @@ -8,14 +8,11 @@ import javasabr.mqtt.network.message.in.PublishReceivedMqttInMessage; import javasabr.mqtt.network.user.NetworkMqttUser; import javasabr.mqtt.service.MessageOutFactoryService; -import javasabr.mqtt.service.SubscriptionService; public class Qos2MqttPublishOutMessageHandler extends PersistedMqttPublishOutMessageHandler { - public Qos2MqttPublishOutMessageHandler( - SubscriptionService subscriptionService, - MessageOutFactoryService messageOutFactoryService) { - super(subscriptionService, messageOutFactoryService); + public Qos2MqttPublishOutMessageHandler(MessageOutFactoryService messageOutFactoryService) { + super(messageOutFactoryService); } @Override diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/IntegrationServiceSpecification.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/IntegrationServiceSpecification.groovy index f1054d75..577388b3 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/IntegrationServiceSpecification.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/IntegrationServiceSpecification.groovy @@ -44,8 +44,7 @@ abstract class IntegrationServiceSpecification extends Specification { @Shared def defaultTopicService = new DefaultTopicService() - @Shared - def defaultSubscriptionService = new InMemorySubscriptionService() + @Shared def defaultMessageOutFactoryService = new DefaultMessageOutFactoryService([ @@ -55,11 +54,14 @@ abstract class IntegrationServiceSpecification extends Specification { @Shared def defaultPublishDeliveringService = new DefaultPublishDeliveringService([ - new Qos0MqttPublishOutMessageHandler(defaultSubscriptionService, defaultMessageOutFactoryService), - new Qos1MqttPublishOutMessageHandler(defaultSubscriptionService, defaultMessageOutFactoryService), - new Qos2MqttPublishOutMessageHandler(defaultSubscriptionService, defaultMessageOutFactoryService) + new Qos0MqttPublishOutMessageHandler(defaultMessageOutFactoryService), + new Qos1MqttPublishOutMessageHandler(defaultMessageOutFactoryService), + new Qos2MqttPublishOutMessageHandler(defaultMessageOutFactoryService) ]) + @Shared + def defaultSubscriptionService = new InMemorySubscriptionService(defaultPublishDeliveringService) + @Shared def qos0MqttPublishInMessageHandler = new Qos0MqttPublishInMessageHandler( defaultSubscriptionService, diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy index 6e97e1de..e473f024 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy @@ -12,8 +12,6 @@ import javasabr.rlib.collections.array.Array class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { - SubscriptionService subscriptionService = new InMemorySubscriptionService() - def "should subscribe with expected results in default settings"() { given: def serverConfig = defaultExternalServerConnectionConfig @@ -49,7 +47,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true, true)) when: - def result = subscriptionService + def result = defaultSubscriptionService .subscribe(mqttUser, mqttUser.session(), subscriptions) then: result.size() == 4 @@ -104,7 +102,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true, true)) when: - def result = subscriptionService + def result = defaultSubscriptionService .subscribe(mqttUser, mqttUser.session(), subscriptions) then: result.size() == 5 @@ -152,7 +150,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true) def subscriptions = Array.of(sub1, sub2, sub3, sub4) when: - def result = subscriptionService + def result = defaultSubscriptionService .subscribe(mqttUser, mqttUser.session(), subscriptions) then: result.size() == 4 @@ -200,14 +198,14 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { SubscribeRetainHandling.SEND, true, true)) - subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) def topicsToUnsubscribe = Array.of( defaultTopicService.createTopicFilter(mqttUser, "topic/filter/1"), defaultTopicService.createTopicFilter(mqttUser, "topic/filter/3"), defaultTopicService.createTopicFilter(mqttUser, "topic/filter/notexist"), defaultTopicService.createTopicFilter(mqttUser, "topic/filter/invalid##")) when: - def result = subscriptionService + def result = defaultSubscriptionService .unsubscribe(mqttUser, mqttUser.session(), topicsToUnsubscribe) then: result.size() == 4 @@ -251,13 +249,13 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { defaultTopicService.createTopicFilter(mqttUser, "topic/filter/1"), defaultTopicService.createTopicFilter(mqttUser, "topic/filter/3")) when: - subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) def storedSubscriptions = activeSubscriptions.subscriptions() then: storedSubscriptions.size() == 3 storedSubscriptions == subscriptions when: - subscriptionService.unsubscribe(mqttUser, mqttUser.session(), topicsToUnsubscribe) + defaultSubscriptionService.unsubscribe(mqttUser, mqttUser.session(), topicsToUnsubscribe) storedSubscriptions = activeSubscriptions.subscriptions() then: storedSubscriptions.size() == 1 @@ -313,13 +311,13 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { subscriptions.get(1), subscriptions2.get(1)) when: - subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) def storedSubscriptions = activeSubscriptions.subscriptions() then: storedSubscriptions.size() == 3 storedSubscriptions == subscriptions when: - subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions2) + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions2) storedSubscriptions = activeSubscriptions.subscriptions() then: storedSubscriptions.size() == 3 diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandlerTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandlerTest.groovy index 6a3bb5a0..df3539ba 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandlerTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/SubscribeMqttInMessageHandlerTest.groovy @@ -1,36 +1,23 @@ package javasabr.mqtt.service.message.handler.impl import javasabr.mqtt.model.MqttVersion -import javasabr.mqtt.model.PayloadFormat import javasabr.mqtt.model.QoS -import javasabr.mqtt.model.SubscribeRetainHandling -import javasabr.mqtt.model.publishing.Publish -import javasabr.mqtt.model.reason.code.DisconnectReasonCode -import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode -import javasabr.mqtt.model.subscriber.SingleSubscriber -import javasabr.mqtt.model.subscribtion.RequestedSubscription -import javasabr.mqtt.model.subscribtion.Subscription -import javasabr.mqtt.model.topic.TopicName import javasabr.mqtt.model.message.MqttMessageType import javasabr.mqtt.model.reason.code.DisconnectReasonCode import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode import javasabr.mqtt.model.subscription.RequestedSubscription import javasabr.mqtt.network.message.in.SubscribeMqttInMessage import javasabr.mqtt.network.message.out.DisconnectMqtt5OutMessage -import javasabr.mqtt.network.message.out.PublishMqtt5OutMessage import javasabr.mqtt.network.message.out.SubscribeAckMqtt5OutMessage import javasabr.mqtt.network.util.ExtraErrorReasons import javasabr.mqtt.service.IntegrationServiceSpecification import javasabr.mqtt.service.TestExternalNetworkMqttUser import javasabr.rlib.collections.array.Array -import javasabr.rlib.collections.array.IntArray import javasabr.rlib.collections.array.MutableArray import javasabr.rlib.common.util.ThreadUtils import javasabr.rlib.logger.api.LoggerLevel import javasabr.rlib.logger.api.LoggerManager -import static java.nio.charset.StandardCharsets.UTF_8 - class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification { static { @@ -44,8 +31,7 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService, - publishDeliveringService) + defaultTopicService) def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser mqttUser.session(null) when: @@ -64,8 +50,7 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService, - publishDeliveringService) + defaultTopicService) def expectedMessageId = 15 def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser def session = mqttUser.session() @@ -97,8 +82,7 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService, - publishDeliveringService) + defaultTopicService) def expectedMessageId = 15 def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser when: @@ -128,8 +112,7 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService, - publishDeliveringService) + defaultTopicService) def expectedMessageId = 15 def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser when: @@ -159,8 +142,7 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService, - publishDeliveringService) + defaultTopicService) def expectedMessageId = 15 def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser when: @@ -193,8 +175,7 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService, - publishDeliveringService) + defaultTopicService) def expectedMessageId = 15 def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser when: @@ -225,8 +206,7 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService, - publishDeliveringService) + defaultTopicService) def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser when: def subscribeMessage = new SubscribeMqttInMessage(0 as byte) @@ -244,8 +224,7 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService, - publishDeliveringService) + defaultTopicService) def expectedMessageId = 15 def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser when: @@ -283,8 +262,7 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification def messageHandler = new SubscribeMqttInMessageHandler( defaultSubscriptionService, defaultMessageOutFactoryService, - defaultTopicService, - publishDeliveringService) + defaultTopicService) def expectedMessageId = 15 def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser mqttUser.returnCompletedFeatures(false) @@ -316,54 +294,4 @@ class SubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecification reasonCodes2.get(0) == SubscribeAckReasonCode.PACKET_IDENTIFIER_IN_USE subscribeAck2.messageId() == expectedMessageId } - - def "should deliver retained messages"() { - given: - def mqttConnection = mockedExternalConnection(MqttVersion.MQTT_5) - def messageHandler = new SubscribeMqttInMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService, - defaultTopicService, - publishDeliveringService) - def expectedMessageId = 15 - def mqttClient = mqttConnection.client() as TestExternalMqttClient - mqttClient.returnCompletedFeatures(false) - when: - Publish publish = new Publish( - 1, - QoS.AT_MOST_ONCE, - TopicName.valueOf("topic2"), - null, - "payload".getBytes(UTF_8), - false, - true, - null, - IntArray.of(30), - null, - 60000, - 1, - PayloadFormat.UTF8_STRING, - Array.of()); - Subscription subscription = new Subscription( - defaultTopicService.createTopicFilter(mqttClient, "topic2"), - 30, - QoS.EXACTLY_ONCE, - SubscribeRetainHandling.SEND, - true, - true); - SingleSubscriber subscriber = new SingleSubscriber(mqttClient, subscription); - publishDeliveringService.startDelivering(publish, subscriber) - - def subscribeMessage = new SubscribeMqttInMessage(SubscribeMqttInMessage.MESSAGE_FLAGS) {{ - this.messageId = 1 - this.subscriptions = MutableArray.ofType(RequestedSubscription) - this.subscriptions.addAll(Array.of(RequestedSubscription.minimal("topic2", QoS.EXACTLY_ONCE))) - }} - messageHandler.processValidMessage(mqttConnection, subscribeMessage) - then: - mqttClient.nextSentMessage(PublishMqtt5OutMessage) - mqttClient.nextSentMessage(SubscribeAckMqtt5OutMessage) - def retainedMessageDelivery = mqttClient.nextSentMessage(PublishMqtt5OutMessage) - retainedMessageDelivery.messageId() == 2 - } } diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/UnsubscribeMqttInMessageHandlerTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/UnsubscribeMqttInMessageHandlerTest.groovy index 2ebca07c..09a94c0a 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/UnsubscribeMqttInMessageHandlerTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/UnsubscribeMqttInMessageHandlerTest.groovy @@ -69,7 +69,7 @@ class UnsubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecificatio def "should response with expected results"() { given: def mqttConnection = mockedExternalConnection(MqttVersion.MQTT_5) - def subscriptionService = new InMemorySubscriptionService() + def subscriptionService = new InMemorySubscriptionService(defaultPublishDeliveringService) def messageHandler = new UnsubscribeMqttInMessageHandler( subscriptionService, defaultMessageOutFactoryService, From be8070bc9ce241846e7b174fd0ae9eb6302c6072 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:56:47 +0100 Subject: [PATCH 07/38] [broker-30] SubscriberNode refactoring --- .../model/subscriber/tree/SubscriberNode.java | 85 ++++++++----------- .../subscriber/tree/SubscriberTreeBase.java | 30 +++---- 2 files changed, 45 insertions(+), 70 deletions(-) diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java index 4a6579c2..3a0ec8bc 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java @@ -22,7 +22,7 @@ @Getter(AccessLevel.PACKAGE) @Accessors(fluent = true, chain = false) @FieldDefaults(level = AccessLevel.PRIVATE) -class SubscriberNode extends SubscriberTreeBase { +public class SubscriberNode extends SubscriberTreeBase { private final static Supplier SUBSCRIBER_NODE_FACTORY = SubscriberNode::new; @@ -39,7 +39,7 @@ class SubscriberNode extends SubscriberTreeBase { * @return the previous subscription from the same owner */ @Nullable - public SingleSubscriber subscribe(int level, MqttUser owner, Subscription subscription, TopicFilter topicFilter) { + protected SingleSubscriber subscribe(int level, MqttUser owner, Subscription subscription, TopicFilter topicFilter) { if (level == topicFilter.levelsCount()) { return addSubscriber(getOrCreateSubscribers(), owner, subscription, topicFilter); } @@ -47,7 +47,7 @@ public SingleSubscriber subscribe(int level, MqttUser owner, Subscription subscr return childNode.subscribe(level + 1, owner, subscription, topicFilter); } - public boolean unsubscribe(int level, MqttUser owner, TopicFilter topicFilter) { + protected boolean unsubscribe(int level, MqttUser owner, TopicFilter topicFilter) { if (level == topicFilter.levelsCount()) { return removeSubscriber(subscribers(), owner, topicFilter); } @@ -56,51 +56,28 @@ public boolean unsubscribe(int level, MqttUser owner, TopicFilter topicFilter) { } protected void matchesTo(int level, TopicName topicName, int lastLevel, MutableArray container) { - exactlyTopicMatch(level, topicName, lastLevel, container); - singleWildcardTopicMatch(level, topicName, lastLevel, container); - multiWildcardTopicMatch(container); + collectMatchingSubscribers(topicName.segment(level), level, topicName, lastLevel, container); + collectMatchingSubscribers(TopicFilter.SINGLE_LEVEL_WILDCARD, level, topicName, lastLevel, container); + collectMatchingSubscribers(TopicFilter.MULTI_LEVEL_WILDCARD, level, topicName, lastLevel, container); } - private void exactlyTopicMatch( + private void collectMatchingSubscribers( + String segment, int level, TopicName topicName, int lastLevel, MutableArray result) { - String segment = topicName.segment(level); SubscriberNode subscriberNode = childNode(segment); if (subscriberNode == null) { return; } - if (level == lastLevel) { + if (level == lastLevel || TopicFilter.MULTI_LEVEL_WILDCARD.equals(segment)) { appendSubscribersTo(result, subscriberNode); } else if (level < lastLevel) { subscriberNode.matchesTo(level + 1, topicName, lastLevel, result); } } - private void singleWildcardTopicMatch( - int level, - TopicName topicName, - int lastLevel, - MutableArray result) { - SubscriberNode subscriberNode = childNode(TopicFilter.SINGLE_LEVEL_WILDCARD); - if (subscriberNode == null) { - return; - } - if (level == lastLevel) { - appendSubscribersTo(result, subscriberNode); - } else if (level < lastLevel) { - subscriberNode.matchesTo(level + 1, topicName, lastLevel, result); - } - } - - private void multiWildcardTopicMatch(MutableArray result) { - SubscriberNode subscriberNode = childNode(TopicFilter.MULTI_LEVEL_WILDCARD); - if (subscriberNode != null) { - appendSubscribersTo(result, subscriberNode); - } - } - private SubscriberNode getOrCreateChildNode(String segment) { LockableRefToRefDictionary childNodes = getOrCreateChildNodes(); long stamp = childNodes.readLock(); @@ -122,40 +99,46 @@ private SubscriberNode getOrCreateChildNode(String segment) { @Nullable private SubscriberNode childNode(String segment) { - LockableRefToRefDictionary childNodes = childNodes(); - if (childNodes == null) { + LockableRefToRefDictionary localChildNodes = childNodes; + if (localChildNodes == null) { return null; } - long stamp = childNodes.readLock(); + long stamp = localChildNodes.readLock(); try { - return childNodes.get(segment); + return localChildNodes.get(segment); } finally { - childNodes.readUnlock(stamp); + localChildNodes.readUnlock(stamp); } } private LockableRefToRefDictionary getOrCreateChildNodes() { - if (childNodes == null) { - synchronized (this) { - if (childNodes == null) { - childNodes = DictionaryFactory.stampedLockBasedRefToRefDictionary(); - } + LockableRefToRefDictionary localChildNodes = childNodes; + if (localChildNodes != null) { + return localChildNodes; + } + synchronized (this) { + localChildNodes = childNodes; + if (localChildNodes == null) { + localChildNodes = DictionaryFactory.stampedLockBasedRefToRefDictionary(); + childNodes = localChildNodes; } + return localChildNodes; } - //noinspection ConstantConditions - return childNodes; } private LockableArray getOrCreateSubscribers() { - if (subscribers == null) { - synchronized (this) { - if (subscribers == null) { - subscribers = ArrayFactory.stampedLockBasedArray(Subscriber.class); - } + LockableArray localSubscribers = subscribers; + if (localSubscribers != null) { + return localSubscribers; + } + synchronized (this) { + localSubscribers = subscribers; + if (localSubscribers == null) { + localSubscribers = ArrayFactory.stampedLockBasedArray(Subscriber.class); + subscribers = localSubscribers; } + return localSubscribers; } - //noinspection ConstantConditions - return subscribers; } @Override diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java index 972b696b..9ae8c953 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java @@ -45,9 +45,7 @@ protected static SingleSubscriber addSubscriber( } @Nullable - private static SingleSubscriber removePreviousIfExist( - LockableArray subscribers, - MqttUser user) { + private static SingleSubscriber removePreviousIfExist(LockableArray subscribers, MqttUser user) { int index = subscribers.indexOf(Subscriber::resolveUser, user); if (index < 0) { return null; @@ -84,10 +82,7 @@ protected static void appendSubscribersTo(MutableArray result, long stamp = subscribers.readLock(); try { for (Subscriber subscriber : subscribers) { - SingleSubscriber singleSubscriber = subscriber.resolveSingle(); - if (removeDuplicateWithLowerQoS(result, singleSubscriber)) { - result.add(singleSubscriber); - } + addOrReplaceIfLowerQos(result, subscriber); } } finally { subscribers.readUnlock(stamp); @@ -141,23 +136,20 @@ private static boolean isSharedSubscriberWithGroup(Subscriber subscriber, String return subscriber instanceof SharedSubscriber shared && Objects.equals(group, shared.group()); } - private static boolean removeDuplicateWithLowerQoS( - MutableArray result, SingleSubscriber candidate) { - + private static void addOrReplaceIfLowerQos(MutableArray result, Subscriber subscriber) { + SingleSubscriber candidate = subscriber.resolveSingle(); int found = result.indexOf(SingleSubscriber::user, candidate.user()); if (found == -1) { - return true; + result.add(candidate); + return; } - QoS candidateQos = candidate.qos(); - SingleSubscriber exist = result.get(found); - QoS existeQos = exist.qos(); - - if (existeQos.ordinal() < candidateQos.ordinal()) { + QoS existedQos = result + .get(found) + .qos(); + if (existedQos.ordinal() < candidateQos.ordinal()) { result.remove(found); - return true; + result.add(candidate); } - - return false; } } From 0c219dd553fdac05d1084630c7147063902a578c Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:59:20 +0100 Subject: [PATCH 08/38] [broker-30] Revert wrong access level modifier --- .../javasabr/mqtt/model/subscriber/tree/SubscriberNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java index 3a0ec8bc..2bdb4314 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java @@ -22,7 +22,7 @@ @Getter(AccessLevel.PACKAGE) @Accessors(fluent = true, chain = false) @FieldDefaults(level = AccessLevel.PRIVATE) -public class SubscriberNode extends SubscriberTreeBase { +class SubscriberNode extends SubscriberTreeBase { private final static Supplier SUBSCRIBER_NODE_FACTORY = SubscriberNode::new; From 62900cac43c9f24237aa9df796de5acad0155b10 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:50:12 +0100 Subject: [PATCH 09/38] [broker-30] Avoid tree branch locking --- .../model/subscriber/tree/SubscriberNode.java | 4 +- .../mqtt/model/topic/AbstractTopic.java | 2 +- .../model/topic/tree/RetainedMessageNode.java | 91 +++++++++---------- 3 files changed, 46 insertions(+), 51 deletions(-) diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java index 2bdb4314..fdcad600 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java @@ -67,7 +67,7 @@ private void collectMatchingSubscribers( TopicName topicName, int lastLevel, MutableArray result) { - SubscriberNode subscriberNode = childNode(segment); + SubscriberNode subscriberNode = getChildNode(segment); if (subscriberNode == null) { return; } @@ -98,7 +98,7 @@ private SubscriberNode getOrCreateChildNode(String segment) { } @Nullable - private SubscriberNode childNode(String segment) { + private SubscriberNode getChildNode(String segment) { LockableRefToRefDictionary localChildNodes = childNodes; if (localChildNodes == null) { return null; diff --git a/model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java b/model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java index 737b0bf6..cea0f54d 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java @@ -41,7 +41,7 @@ public int levelsCount() { return segments.length; } - public String lastSegment() { + String lastSegment() { return segments[segments.length - 1]; } diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java index cc6b4b68..d02fc9e4 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java @@ -1,11 +1,6 @@ package javasabr.mqtt.model.topic.tree; -import static javasabr.mqtt.model.topic.TopicFilter.MULTI_LEVEL_WILDCARD; -import static javasabr.mqtt.model.topic.TopicFilter.SINGLE_LEVEL_WILDCARD; - import java.util.LinkedList; -import java.util.Objects; -import java.util.PriorityQueue; import java.util.Queue; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -13,6 +8,7 @@ import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.mqtt.model.topic.TopicName; +import javasabr.rlib.collections.array.ArrayFactory; import javasabr.rlib.collections.array.MutableArray; import javasabr.rlib.collections.dictionary.DictionaryFactory; import javasabr.rlib.collections.dictionary.LockableRefToRefDictionary; @@ -39,49 +35,47 @@ class RetainedMessageNode { public void retainMessage(int level, Publish message, TopicName topicName) { var child = getOrCreateChildNode(topicName.segment(level)); - boolean isLeaf = (level + 1 == topicName.levelsCount()); - if (isLeaf) { - if (Objects.equals(message.topicName().lastSegment(), topicName.lastSegment())) { - child.retainedMessage.set(message.payload().length == 0 ? null : message); - } + boolean isLastLevel = (level + 1 == topicName.levelsCount()); + if (isLastLevel) { + child.retainedMessage.set(message.payload().length == 0 ? null : message); } else { child.retainMessage(level + 1, message, topicName); } } public void collectRetainedMessages(int level, TopicFilter topicFilter, MutableArray result) { - String segment = topicFilter.segment(level); - if (Objects.equals(segment, MULTI_LEVEL_WILDCARD)) { - collectAllMessages(this, result); - return; - } else if (Objects.equals(segment, SINGLE_LEVEL_WILDCARD)) { - var childNodes = childNodes(); - if (childNodes == null) { - return; - } - long stamp = childNodes.readLock(); - try { - for (RetainedMessageNode childNode : childNodes) { - childNode.collectRetainedMessages(level + 1, topicFilter, result); - } - } finally { - childNodes.readUnlock(stamp); + if (level == topicFilter.levelsCount()) { + Publish publish = retainedMessage.get(); + if (publish != null) { + result.add(publish); } return; } - int lastLevel = topicFilter.levelsCount() - 1; - RetainedMessageNode retainedMessageNode = childNode(segment); - if (retainedMessageNode == null || level > lastLevel) { + String segment = topicFilter.segment(level); + boolean isOneCharSegment = segment.length() == 1; + if (isOneCharSegment && segment.charAt(0) == TopicFilter.MULTI_LEVEL_WILDCARD_CHAR) { + collectAllMessages(this, result); return; } - boolean isLeaf = (level == lastLevel); - if (isLeaf) { - Publish publish = retainedMessageNode.retainedMessage.get(); - if(publish != null && Objects.equals(segment, publish.topicName().lastSegment())){ - result.add(publish); + if (isOneCharSegment && segment.charAt(0) == TopicFilter.SINGLE_LEVEL_WILDCARD_CHAR) { + var localChildNodes = childNodes; + if (localChildNodes != null) { + var nextChildNodes = ArrayFactory.mutableArray(RetainedMessageNode.class); + long stamp = localChildNodes.readLock(); + try { + localChildNodes.values(nextChildNodes); + } finally { + localChildNodes.readUnlock(stamp); + } + for (RetainedMessageNode childNode : nextChildNodes) { + childNode.collectRetainedMessages(level + 1, topicFilter, result); + } } } else { - retainedMessageNode.collectRetainedMessages(level + 1, topicFilter, result); + RetainedMessageNode retainedMessageNode = getChildNode(segment); + if (retainedMessageNode != null) { + retainedMessageNode.collectRetainedMessages(level + 1, topicFilter, result); + } } } @@ -100,9 +94,7 @@ private void collectAllMessages(RetainedMessageNode node, MutableArray } long stamp = childNodes.readLock(); try { - for (RetainedMessageNode n : childNodes) { - queue.add(n); - } + childNodes.values(queue); } finally { childNodes.readUnlock(stamp); } @@ -110,8 +102,8 @@ private void collectAllMessages(RetainedMessageNode node, MutableArray } @Nullable - private RetainedMessageNode childNode(String segment) { - LockableRefToRefDictionary childNodes = childNodes(); + private RetainedMessageNode getChildNode(String segment) { + var childNodes = childNodes(); if (childNodes == null) { return null; } @@ -124,7 +116,7 @@ private RetainedMessageNode childNode(String segment) { } private RetainedMessageNode getOrCreateChildNode(String segment) { - LockableRefToRefDictionary childNodes = getOrCreateChildNodes(); + var childNodes = getOrCreateChildNodes(); long stamp = childNodes.readLock(); try { RetainedMessageNode topicFilterNode = childNodes.get(segment); @@ -143,15 +135,18 @@ private RetainedMessageNode getOrCreateChildNode(String segment) { } private LockableRefToRefDictionary getOrCreateChildNodes() { - if (childNodes == null) { - synchronized (this) { - if (childNodes == null) { - childNodes = DictionaryFactory.stampedLockBasedRefToRefDictionary(); - } + var current = childNodes; + if (current != null) { + return current; + } + synchronized (this) { + current = childNodes; + if (current == null) { + current = DictionaryFactory.stampedLockBasedRefToRefDictionary(); + childNodes = current; } + return current; } - //noinspection ConstantConditions - return childNodes; } @Override From 7314ed1844e4c4834ef3839172fc5804dbdf1118 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Sun, 7 Dec 2025 15:12:39 +0100 Subject: [PATCH 10/38] [broker-30] Introduce AbstractTrieNode --- .../impl/InMemorySubscriptionService.java | 30 ++++---- .../javasabr/mqtt/model/AbstractTrieNode.java | 72 +++++++++++++++++++ .../model/topic/tree/RetainedMessageNode.java | 69 +++--------------- 3 files changed, 96 insertions(+), 75 deletions(-) create mode 100644 model/src/main/java/javasabr/mqtt/model/AbstractTrieNode.java diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index e628f6dd..90343895 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -81,12 +81,14 @@ private SubscribeAckReasonCode addSubscription(MqttUser user, MqttSession sessio if (previous != null) { activeSubscriptions.remove(previous.subscription()); } - if ((subscription.retainHandling() == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previous != null) + if ((subscription.retainHandling() == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previous == null) || subscription.retainHandling() == SEND) { sendRetainedMessages(user, subscription); } activeSubscriptions.add(subscription); - return subscription.qos().subscribeAckReasonCode(); + return subscription + .qos() + .subscribeAckReasonCode(); } @Override @@ -140,15 +142,19 @@ public void restoreSubscriptions(MqttUser user, MqttSession session) { } private void sendRetainedMessages(MqttUser user, Subscription subscription) { - int count = 0; - PublishHandlingResult errorResult = null; - if (subscription + SubscribeAckReasonCode subscribeAckReasonCode = subscription .qos() - .subscribeAckReasonCode() - .ordinal() > 2) { - // TODO handle error ? + .subscribeAckReasonCode(); + if (subscribeAckReasonCode.ordinal() > 2) { + log.debug( + user.clientId(), + subscription, + subscribeAckReasonCode, + "[%s] Unable to send retained messages for [%s] due to wrong subscribeAckReasonCode [%s]"::formatted); return; } + int count = 0; + PublishHandlingResult errorResult = null; SingleSubscriber singleSubscriber = new SingleSubscriber(user, subscription); var results = publishDeliveringService.deliverRetainedMessages(subscription.topicFilter(), singleSubscriber); for (PublishHandlingResult result : results) { @@ -158,19 +164,13 @@ private void sendRetainedMessages(MqttUser user, Subscription subscription) { count++; } if (errorResult != null) { - log.debug( - user.clientId(), - errorResult, - "[%s] Found final error:[%s] during sending retained messages"::formatted); - // TODO handleError(client, publish, errorResult); + log.debug(user.clientId(), errorResult, "[%s] Error occurred [%s] during sending retained messages"::formatted); } else { log.debug( user.clientId(), count, "[%s] Successfully started delivering retained messages to [%s] subscribers"::formatted); - // TODO handleSuccessfulResult(client, publish, count); } - } } } diff --git a/model/src/main/java/javasabr/mqtt/model/AbstractTrieNode.java b/model/src/main/java/javasabr/mqtt/model/AbstractTrieNode.java new file mode 100644 index 00000000..b65559ab --- /dev/null +++ b/model/src/main/java/javasabr/mqtt/model/AbstractTrieNode.java @@ -0,0 +1,72 @@ +package javasabr.mqtt.model; + +import java.util.function.Supplier; +import javasabr.mqtt.base.util.DebugUtils; +import javasabr.rlib.collections.dictionary.DictionaryFactory; +import javasabr.rlib.collections.dictionary.LockableRefToRefDictionary; +import lombok.Getter; +import lombok.experimental.Accessors; +import org.jspecify.annotations.Nullable; + +@Getter +@Accessors +public abstract class AbstractTrieNode { + + @Nullable + volatile LockableRefToRefDictionary childNodes; + + protected abstract Supplier getNodeFactory(); + + private LockableRefToRefDictionary getOrCreateChildNodes() { + var current = childNodes; + if (current != null) { + return current; + } + synchronized (this) { + current = childNodes; + if (current == null) { + current = DictionaryFactory.stampedLockBasedRefToRefDictionary(); + childNodes = current; + } + return current; + } + } + + protected T getOrCreateChildNode(String segment) { + var childNodes = getOrCreateChildNodes(); + long stamp = childNodes.readLock(); + try { + T topicFilterNode = childNodes.get(segment); + if (topicFilterNode != null) { + return topicFilterNode; + } + } finally { + childNodes.readUnlock(stamp); + } + stamp = childNodes.writeLock(); + try { + return childNodes.getOrCompute(segment, getNodeFactory()); + } finally { + childNodes.writeUnlock(stamp); + } + } + + @Nullable + protected T getChildNode(String segment) { + var localChildNodes = childNodes; + if (localChildNodes == null) { + return null; + } + long stamp = localChildNodes.readLock(); + try { + return localChildNodes.get(segment); + } finally { + localChildNodes.readUnlock(stamp); + } + } + + @Override + public String toString() { + return DebugUtils.toJsonString(this); + } +} diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java index d02fc9e4..af625993 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java @@ -5,13 +5,12 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import javasabr.mqtt.base.util.DebugUtils; +import javasabr.mqtt.model.AbstractTrieNode; import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.mqtt.model.topic.TopicName; import javasabr.rlib.collections.array.ArrayFactory; import javasabr.rlib.collections.array.MutableArray; -import javasabr.rlib.collections.dictionary.DictionaryFactory; -import javasabr.rlib.collections.dictionary.LockableRefToRefDictionary; import lombok.AccessLevel; import lombok.Getter; import lombok.experimental.Accessors; @@ -21,18 +20,21 @@ @Getter(AccessLevel.PACKAGE) @Accessors(fluent = true, chain = false) @FieldDefaults(level = AccessLevel.PRIVATE) -class RetainedMessageNode { +class RetainedMessageNode extends AbstractTrieNode { - private final static Supplier TOPIC_NODE_FACTORY = RetainedMessageNode::new; + private final static Supplier NODE_FACTORY = RetainedMessageNode::new; static { DebugUtils.registerIncludedFields("childNodes", "retainedMessage"); } - @Nullable - volatile LockableRefToRefDictionary childNodes; final AtomicReference<@Nullable Publish> retainedMessage = new AtomicReference<>(); + @Override + protected Supplier getNodeFactory() { + return NODE_FACTORY; + } + public void retainMessage(int level, Publish message, TopicName topicName) { var child = getOrCreateChildNode(topicName.segment(level)); boolean isLastLevel = (level + 1 == topicName.levelsCount()); @@ -58,7 +60,7 @@ public void collectRetainedMessages(int level, TopicFilter topicFilter, MutableA return; } if (isOneCharSegment && segment.charAt(0) == TopicFilter.SINGLE_LEVEL_WILDCARD_CHAR) { - var localChildNodes = childNodes; + var localChildNodes = childNodes(); if (localChildNodes != null) { var nextChildNodes = ArrayFactory.mutableArray(RetainedMessageNode.class); long stamp = localChildNodes.readLock(); @@ -100,57 +102,4 @@ private void collectAllMessages(RetainedMessageNode node, MutableArray } } } - - @Nullable - private RetainedMessageNode getChildNode(String segment) { - var childNodes = childNodes(); - if (childNodes == null) { - return null; - } - long stamp = childNodes.readLock(); - try { - return childNodes.get(segment); - } finally { - childNodes.readUnlock(stamp); - } - } - - private RetainedMessageNode getOrCreateChildNode(String segment) { - var childNodes = getOrCreateChildNodes(); - long stamp = childNodes.readLock(); - try { - RetainedMessageNode topicFilterNode = childNodes.get(segment); - if (topicFilterNode != null) { - return topicFilterNode; - } - } finally { - childNodes.readUnlock(stamp); - } - stamp = childNodes.writeLock(); - try { - return childNodes.getOrCompute(segment, TOPIC_NODE_FACTORY); - } finally { - childNodes.writeUnlock(stamp); - } - } - - private LockableRefToRefDictionary getOrCreateChildNodes() { - var current = childNodes; - if (current != null) { - return current; - } - synchronized (this) { - current = childNodes; - if (current == null) { - current = DictionaryFactory.stampedLockBasedRefToRefDictionary(); - childNodes = current; - } - return current; - } - } - - @Override - public String toString() { - return DebugUtils.toJsonString(this); - } } From 901e865f6cb952874ca80a6445eafcf8aaeef0ef Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Sun, 7 Dec 2025 15:17:39 +0100 Subject: [PATCH 11/38] [broker-30] Remove redundant code from SubscriberNode --- .../model/subscriber/tree/SubscriberNode.java | 64 ++----------------- .../subscriber/tree/SubscriberTreeBase.java | 3 +- 2 files changed, 8 insertions(+), 59 deletions(-) diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java index fdcad600..d2698597 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java @@ -11,8 +11,6 @@ import javasabr.rlib.collections.array.ArrayFactory; import javasabr.rlib.collections.array.LockableArray; import javasabr.rlib.collections.array.MutableArray; -import javasabr.rlib.collections.dictionary.DictionaryFactory; -import javasabr.rlib.collections.dictionary.LockableRefToRefDictionary; import lombok.AccessLevel; import lombok.Getter; import lombok.experimental.Accessors; @@ -24,17 +22,20 @@ @FieldDefaults(level = AccessLevel.PRIVATE) class SubscriberNode extends SubscriberTreeBase { - private final static Supplier SUBSCRIBER_NODE_FACTORY = SubscriberNode::new; + private final static Supplier NODE_FACTORY = SubscriberNode::new; static { DebugUtils.registerIncludedFields("childNodes", "subscribers"); } - @Nullable - volatile LockableRefToRefDictionary childNodes; @Nullable volatile LockableArray subscribers; + @Override + protected Supplier getNodeFactory() { + return NODE_FACTORY; + } + /** * @return the previous subscription from the same owner */ @@ -78,54 +79,6 @@ private void collectMatchingSubscribers( } } - private SubscriberNode getOrCreateChildNode(String segment) { - LockableRefToRefDictionary childNodes = getOrCreateChildNodes(); - long stamp = childNodes.readLock(); - try { - SubscriberNode subscriberNode = childNodes.get(segment); - if (subscriberNode != null) { - return subscriberNode; - } - } finally { - childNodes.readUnlock(stamp); - } - stamp = childNodes.writeLock(); - try { - return childNodes.getOrCompute(segment, SUBSCRIBER_NODE_FACTORY); - } finally { - childNodes.writeUnlock(stamp); - } - } - - @Nullable - private SubscriberNode getChildNode(String segment) { - LockableRefToRefDictionary localChildNodes = childNodes; - if (localChildNodes == null) { - return null; - } - long stamp = localChildNodes.readLock(); - try { - return localChildNodes.get(segment); - } finally { - localChildNodes.readUnlock(stamp); - } - } - - private LockableRefToRefDictionary getOrCreateChildNodes() { - LockableRefToRefDictionary localChildNodes = childNodes; - if (localChildNodes != null) { - return localChildNodes; - } - synchronized (this) { - localChildNodes = childNodes; - if (localChildNodes == null) { - localChildNodes = DictionaryFactory.stampedLockBasedRefToRefDictionary(); - childNodes = localChildNodes; - } - return localChildNodes; - } - } - private LockableArray getOrCreateSubscribers() { LockableArray localSubscribers = subscribers; if (localSubscribers != null) { @@ -140,9 +93,4 @@ private LockableArray getOrCreateSubscribers() { return localSubscribers; } } - - @Override - public String toString() { - return DebugUtils.toJsonString(this); - } } diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java index 9ae8c953..03236958 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java @@ -1,6 +1,7 @@ package javasabr.mqtt.model.subscriber.tree; import java.util.Objects; +import javasabr.mqtt.model.AbstractTrieNode; import javasabr.mqtt.model.MqttUser; import javasabr.mqtt.model.QoS; import javasabr.mqtt.model.subscriber.SharedSubscriber; @@ -18,7 +19,7 @@ @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) -abstract class SubscriberTreeBase { +abstract class SubscriberTreeBase extends AbstractTrieNode { /** * @return previous subscriber with the same user From cc91db27e2ae06c82b736817cc4130b9173aaacf Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Sun, 7 Dec 2025 16:10:11 +0100 Subject: [PATCH 12/38] [broker-30] Revert redundant changes --- application/build.gradle | 4 ---- .../mqtt/service/IntegrationServiceSpecification.groovy | 2 -- 2 files changed, 6 deletions(-) diff --git a/application/build.gradle b/application/build.gradle index 3f0b7d88..3caeade8 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -25,7 +25,3 @@ configurations.each { it.exclude group: "org.slf4j", module: "slf4j-log4j12" it.exclude group: "org.springframework.boot", module: "spring-boot-starter-logging" } - -bootJar { - mainClass = 'javasabr.mqtt.broker.application.MqttBrokerApplication' -} diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/IntegrationServiceSpecification.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/IntegrationServiceSpecification.groovy index 577388b3..c44fc2bb 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/IntegrationServiceSpecification.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/IntegrationServiceSpecification.groovy @@ -44,8 +44,6 @@ abstract class IntegrationServiceSpecification extends Specification { @Shared def defaultTopicService = new DefaultTopicService() - - @Shared def defaultMessageOutFactoryService = new DefaultMessageOutFactoryService([ new Mqtt311MessageOutFactory(), From 42af01437f70accb5a4a1a766622d3bfc3a86708 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Sun, 7 Dec 2025 16:36:53 +0100 Subject: [PATCH 13/38] [broker-30] Improve AbstractTrieNode --- .../impl/DefaultPublishDeliveringService.java | 4 ++- .../javasabr/mqtt/model/AbstractTrieNode.java | 29 ++++++++++++++++--- .../tree/ConcurrentRetainedMessageTree.java | 6 ++-- .../model/topic/tree/RetainedMessageNode.java | 26 ++++------------- .../out/ConnectAckMqtt5OutMessageTest.groovy | 1 + 5 files changed, 37 insertions(+), 29 deletions(-) diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java index 9c45b9b7..06d9167b 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java @@ -52,7 +52,9 @@ public DefaultPublishDeliveringService( @Override public PublishHandlingResult startDelivering(Publish publish, SingleSubscriber subscriber) { try { - retainedMessageTree.retainMessage(publish); + if (publish.retained()) { + retainedMessageTree.retainMessage(publish); + } //noinspection DataFlowIssue return publishOutMessageHandlers[subscriber.qos().level()].handle(publish, subscriber); } catch (IndexOutOfBoundsException | NullPointerException ex) { diff --git a/model/src/main/java/javasabr/mqtt/model/AbstractTrieNode.java b/model/src/main/java/javasabr/mqtt/model/AbstractTrieNode.java index b65559ab..75f835a2 100644 --- a/model/src/main/java/javasabr/mqtt/model/AbstractTrieNode.java +++ b/model/src/main/java/javasabr/mqtt/model/AbstractTrieNode.java @@ -1,15 +1,12 @@ package javasabr.mqtt.model; +import java.util.Collection; import java.util.function.Supplier; import javasabr.mqtt.base.util.DebugUtils; import javasabr.rlib.collections.dictionary.DictionaryFactory; import javasabr.rlib.collections.dictionary.LockableRefToRefDictionary; -import lombok.Getter; -import lombok.experimental.Accessors; import org.jspecify.annotations.Nullable; -@Getter -@Accessors public abstract class AbstractTrieNode { @Nullable @@ -51,6 +48,30 @@ protected T getOrCreateChildNode(String segment) { } } + protected void collectChildNodes(Collection resultCollection) { + var localChildNodes = childNodes; + if (localChildNodes == null) { + return; + } + long stamp = localChildNodes.readLock(); + try { + localChildNodes.values(resultCollection); + } finally { + localChildNodes.readUnlock(stamp); + } + } + + @Nullable + protected Collection getChildNodes(Supplier> resultCollectionFactory) { + var localChildNodes = childNodes; + if (localChildNodes == null) { + return null; + } + Collection resultCollection = resultCollectionFactory.get(); + collectChildNodes(resultCollection); + return resultCollection; + } + @Nullable protected T getChildNode(String segment) { var localChildNodes = childNodes; diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java index 6f38475f..ea45b880 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java @@ -18,14 +18,12 @@ public ConcurrentRetainedMessageTree() { } public void retainMessage(Publish message) { - if (message.retained()) { - rootNode.retainMessage(0, message, message.topicName()); - } + rootNode.retainMessage(0, message, message.topicName()); } public Array getRetainedMessage(TopicFilter topicFilter) { var resultArray = MutableArray.ofType(Publish.class); rootNode.collectRetainedMessages(0, topicFilter, resultArray); - return resultArray; + return Array.copyOf(resultArray); } } diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java index af625993..717fdea3 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java @@ -9,8 +9,10 @@ import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.mqtt.model.topic.TopicName; +import javasabr.rlib.collections.array.Array; import javasabr.rlib.collections.array.ArrayFactory; import javasabr.rlib.collections.array.MutableArray; +import javasabr.rlib.collections.deque.DequeFactory; import lombok.AccessLevel; import lombok.Getter; import lombok.experimental.Accessors; @@ -60,16 +62,9 @@ public void collectRetainedMessages(int level, TopicFilter topicFilter, MutableA return; } if (isOneCharSegment && segment.charAt(0) == TopicFilter.SINGLE_LEVEL_WILDCARD_CHAR) { - var localChildNodes = childNodes(); + var localChildNodes = getChildNodes(() -> ArrayFactory.mutableArray(RetainedMessageNode.class)); if (localChildNodes != null) { - var nextChildNodes = ArrayFactory.mutableArray(RetainedMessageNode.class); - long stamp = localChildNodes.readLock(); - try { - localChildNodes.values(nextChildNodes); - } finally { - localChildNodes.readUnlock(stamp); - } - for (RetainedMessageNode childNode : nextChildNodes) { + for (RetainedMessageNode childNode : localChildNodes) { childNode.collectRetainedMessages(level + 1, topicFilter, result); } } @@ -82,7 +77,7 @@ public void collectRetainedMessages(int level, TopicFilter topicFilter, MutableA } private void collectAllMessages(RetainedMessageNode node, MutableArray result) { - Queue queue = new LinkedList<>(); + Queue queue = DequeFactory.arrayBasedBased(RetainedMessageNode.class); queue.add(node); while (!queue.isEmpty()) { RetainedMessageNode poll = queue.poll(); @@ -90,16 +85,7 @@ private void collectAllMessages(RetainedMessageNode node, MutableArray if (message != null) { result.add(message); } - var childNodes = poll.childNodes(); - if (childNodes == null) { - continue; - } - long stamp = childNodes.readLock(); - try { - childNodes.values(queue); - } finally { - childNodes.readUnlock(stamp); - } + poll.collectChildNodes(queue); } } } diff --git a/network/src/test/groovy/javasabr/mqtt/network/message/out/ConnectAckMqtt5OutMessageTest.groovy b/network/src/test/groovy/javasabr/mqtt/network/message/out/ConnectAckMqtt5OutMessageTest.groovy index 789e0077..8d94d698 100644 --- a/network/src/test/groovy/javasabr/mqtt/network/message/out/ConnectAckMqtt5OutMessageTest.groovy +++ b/network/src/test/groovy/javasabr/mqtt/network/message/out/ConnectAckMqtt5OutMessageTest.groovy @@ -1,5 +1,6 @@ package javasabr.mqtt.network.message.out + import javasabr.mqtt.model.MqttVersion import javasabr.mqtt.model.QoS import javasabr.mqtt.model.message.MqttMessageType From b1aa456b14fe781492dbede05269f18b498e7b8c Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:29:15 +0100 Subject: [PATCH 14/38] [broker-30] Implement retainAsPublished support --- .../mqtt/service/PublishDeliveringService.java | 2 +- .../impl/DefaultPublishDeliveringService.java | 14 +++++++++----- .../impl/InMemorySubscriptionService.java | 2 +- .../mqtt/model/publishing/Publish.java | 18 ++++++++++++++++++ .../tree/ConcurrentRetainedMessageTree.java | 5 +++-- .../model/topic/tree/RetainedMessageNode.java | 12 ++++++------ .../topic/tree/RetainedMessageTreeTest.groovy | 4 +++- 7 files changed, 41 insertions(+), 16 deletions(-) diff --git a/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java b/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java index b5f65426..b222f388 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java @@ -10,5 +10,5 @@ public interface PublishDeliveringService { PublishHandlingResult startDelivering(Publish publish, SingleSubscriber subscriber); - Array deliverRetainedMessages(TopicFilter topicFilter, SingleSubscriber subscriber); + Array deliverRetainedMessages(SingleSubscriber subscriber); } diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java index 06d9167b..fedaa206 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java @@ -1,10 +1,11 @@ package javasabr.mqtt.service.impl; import java.util.Collection; +import java.util.function.Function; import javasabr.mqtt.model.QoS; import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.subscriber.SingleSubscriber; -import javasabr.mqtt.model.topic.TopicFilter; +import javasabr.mqtt.model.subscription.Subscription; import javasabr.mqtt.model.topic.tree.ConcurrentRetainedMessageTree; import javasabr.mqtt.service.PublishDeliveringService; import javasabr.mqtt.service.publish.handler.MqttPublishOutMessageHandler; @@ -64,13 +65,16 @@ public PublishHandlingResult startDelivering(Publish publish, SingleSubscriber s } @Override - public Array deliverRetainedMessages(TopicFilter topicFilter, SingleSubscriber subscriber) { - Array retainedMessage = retainedMessageTree.getRetainedMessage(topicFilter); + public Array deliverRetainedMessages(SingleSubscriber subscriber) { + Subscription subscription = subscriber.subscription(); + boolean retainAsPublished = subscription.retainAsPublished(); + Function transformer = retainAsPublished ? Function.identity() : Publish::withoutRetain; + Array retainedMessages = retainedMessageTree.getRetainedMessage(subscription.topicFilter(), transformer); MutableArray result = MutableArray.ofType(PublishHandlingResult.class); - for (Publish message : retainedMessage) { + for (Publish message : retainedMessages) { result.add(startDelivering(message, subscriber)); } - return result; + return Array.copyOf(result); } private static String buildServiceDescription( diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index 90343895..043a0266 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -156,7 +156,7 @@ private void sendRetainedMessages(MqttUser user, Subscription subscription) { int count = 0; PublishHandlingResult errorResult = null; SingleSubscriber singleSubscriber = new SingleSubscriber(user, subscription); - var results = publishDeliveringService.deliverRetainedMessages(subscription.topicFilter(), singleSubscriber); + var results = publishDeliveringService.deliverRetainedMessages(singleSubscriber); for (PublishHandlingResult result : results) { if (result.error()) { errorResult = result; diff --git a/model/src/main/java/javasabr/mqtt/model/publishing/Publish.java b/model/src/main/java/javasabr/mqtt/model/publishing/Publish.java index 2e10c730..db8d646d 100644 --- a/model/src/main/java/javasabr/mqtt/model/publishing/Publish.java +++ b/model/src/main/java/javasabr/mqtt/model/publishing/Publish.java @@ -92,6 +92,24 @@ public Publish with(int messageId, QoS qos, boolean duplicated, int topicAlias) userProperties); } + public Publish withoutRetain() { + return new Publish( + messageId, + qos, + topicName, + responseTopicName, + payload, + duplicated, + false, + contentType, + subscriptionIds, + correlationData, + messageExpiryInterval, + topicAlias, + payloadFormat, + userProperties); + } + @Override public String toString() { return DebugUtils.toJsonString(this); diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java index ea45b880..6831d3a3 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java @@ -1,5 +1,6 @@ package javasabr.mqtt.model.topic.tree; +import java.util.function.Function; import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.rlib.collections.array.Array; @@ -21,9 +22,9 @@ public void retainMessage(Publish message) { rootNode.retainMessage(0, message, message.topicName()); } - public Array getRetainedMessage(TopicFilter topicFilter) { + public Array getRetainedMessage(TopicFilter topicFilter, Function publishTransformer) { var resultArray = MutableArray.ofType(Publish.class); - rootNode.collectRetainedMessages(0, topicFilter, resultArray); + rootNode.collectRetainedMessages(0, topicFilter, resultArray, publishTransformer); return Array.copyOf(resultArray); } } diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java index 717fdea3..8652f51f 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java @@ -1,15 +1,14 @@ package javasabr.mqtt.model.topic.tree; -import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.function.Supplier; import javasabr.mqtt.base.util.DebugUtils; import javasabr.mqtt.model.AbstractTrieNode; import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.mqtt.model.topic.TopicName; -import javasabr.rlib.collections.array.Array; import javasabr.rlib.collections.array.ArrayFactory; import javasabr.rlib.collections.array.MutableArray; import javasabr.rlib.collections.deque.DequeFactory; @@ -47,11 +46,12 @@ public void retainMessage(int level, Publish message, TopicName topicName) { } } - public void collectRetainedMessages(int level, TopicFilter topicFilter, MutableArray result) { + public void collectRetainedMessages(int level, TopicFilter topicFilter, MutableArray result, + Function publishTransformer) { if (level == topicFilter.levelsCount()) { Publish publish = retainedMessage.get(); if (publish != null) { - result.add(publish); + result.add(publishTransformer.apply(publish)); } return; } @@ -65,13 +65,13 @@ public void collectRetainedMessages(int level, TopicFilter topicFilter, MutableA var localChildNodes = getChildNodes(() -> ArrayFactory.mutableArray(RetainedMessageNode.class)); if (localChildNodes != null) { for (RetainedMessageNode childNode : localChildNodes) { - childNode.collectRetainedMessages(level + 1, topicFilter, result); + childNode.collectRetainedMessages(level + 1, topicFilter, result, publishTransformer); } } } else { RetainedMessageNode retainedMessageNode = getChildNode(segment); if (retainedMessageNode != null) { - retainedMessageNode.collectRetainedMessages(level + 1, topicFilter, result); + retainedMessageNode.collectRetainedMessages(level + 1, topicFilter, result, publishTransformer); } } } diff --git a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy index 1baf0582..705710e5 100644 --- a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy +++ b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy @@ -10,6 +10,8 @@ import javasabr.mqtt.test.support.UnitSpecification import javasabr.rlib.collections.array.Array import javasabr.rlib.collections.array.IntArray +import java.util.function.Function + import static java.nio.charset.StandardCharsets.UTF_8 class RetainedMessageTreeTest extends UnitSpecification { @@ -24,7 +26,7 @@ class RetainedMessageTreeTest extends UnitSpecification { retainedMessageTree.retainMessage(message) } when: - def retainedMessages = retainedMessageTree.getRetainedMessage(TopicFilter.valueOf(topicFilter)) + def retainedMessages = retainedMessageTree.getRetainedMessage(TopicFilter.valueOf(topicFilter), Function.identity()) .collect { it } then: retainedMessages.size() == expectedMessages.size() From f2f2f67d91a613f82e7f4b31b190d41ccd44fa9b Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:33:17 +0100 Subject: [PATCH 15/38] [broker-30] Update debug message --- .../mqtt/service/impl/InMemorySubscriptionService.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index 043a0266..a06c698e 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -145,9 +145,10 @@ private void sendRetainedMessages(MqttUser user, Subscription subscription) { SubscribeAckReasonCode subscribeAckReasonCode = subscription .qos() .subscribeAckReasonCode(); + String clientId = user.clientId(); if (subscribeAckReasonCode.ordinal() > 2) { log.debug( - user.clientId(), + clientId, subscription, subscribeAckReasonCode, "[%s] Unable to send retained messages for [%s] due to wrong subscribeAckReasonCode [%s]"::formatted); @@ -164,12 +165,9 @@ private void sendRetainedMessages(MqttUser user, Subscription subscription) { count++; } if (errorResult != null) { - log.debug(user.clientId(), errorResult, "[%s] Error occurred [%s] during sending retained messages"::formatted); + log.debug(clientId, errorResult, "[%s] Error occurred [%s] during sending retained messages"::formatted); } else { - log.debug( - user.clientId(), - count, - "[%s] Successfully started delivering retained messages to [%s] subscribers"::formatted); + log.debug(clientId, count, "[%s] Delivering [%s] retained messages has been started"::formatted); } } } From bf837eb5b4ca790502da9d30313019fe52031cda Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:49:45 +0100 Subject: [PATCH 16/38] [broker-30] Small refactoring --- .../mqtt/model/topic/tree/RetainedMessageNode.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java index 8652f51f..52dd6883 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java @@ -29,6 +29,10 @@ class RetainedMessageNode extends AbstractTrieNode { DebugUtils.registerIncludedFields("childNodes", "retainedMessage"); } + private static MutableArray childNodesFactory() { + return ArrayFactory.mutableArray(RetainedMessageNode.class); + } + final AtomicReference<@Nullable Publish> retainedMessage = new AtomicReference<>(); @Override @@ -46,8 +50,11 @@ public void retainMessage(int level, Publish message, TopicName topicName) { } } - public void collectRetainedMessages(int level, TopicFilter topicFilter, MutableArray result, - Function publishTransformer) { + public void collectRetainedMessages( + int level, + TopicFilter topicFilter, + MutableArray result, + Function publishTransformer) { if (level == topicFilter.levelsCount()) { Publish publish = retainedMessage.get(); if (publish != null) { @@ -62,7 +69,7 @@ public void collectRetainedMessages(int level, TopicFilter topicFilter, MutableA return; } if (isOneCharSegment && segment.charAt(0) == TopicFilter.SINGLE_LEVEL_WILDCARD_CHAR) { - var localChildNodes = getChildNodes(() -> ArrayFactory.mutableArray(RetainedMessageNode.class)); + var localChildNodes = getChildNodes(RetainedMessageNode::childNodesFactory); if (localChildNodes != null) { for (RetainedMessageNode childNode : localChildNodes) { childNode.collectRetainedMessages(level + 1, topicFilter, result, publishTransformer); From cc0833954d9a631e453ae1d9e2daa21efbecb4ef Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Sun, 7 Dec 2025 19:02:46 +0100 Subject: [PATCH 17/38] [broker-30] Avoid double message retaining --- .../service/PublishDeliveringService.java | 1 - .../impl/DefaultPublishDeliveringService.java | 24 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java b/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java index b222f388..bcc83dbd 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java @@ -2,7 +2,6 @@ import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.subscriber.SingleSubscriber; -import javasabr.mqtt.model.topic.TopicFilter; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; import javasabr.rlib.collections.array.Array; diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java index fedaa206..e9bdce16 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java @@ -52,16 +52,10 @@ public DefaultPublishDeliveringService( @Override public PublishHandlingResult startDelivering(Publish publish, SingleSubscriber subscriber) { - try { - if (publish.retained()) { - retainedMessageTree.retainMessage(publish); - } - //noinspection DataFlowIssue - return publishOutMessageHandlers[subscriber.qos().level()].handle(publish, subscriber); - } catch (IndexOutOfBoundsException | NullPointerException ex) { - log.warning(publish, "Received not supported publish message:[%s]"::formatted); - return PublishHandlingResult.UNSPECIFIED_ERROR; + if (publish.retained()) { + retainedMessageTree.retainMessage(publish); } + return startDeliveringWithoutRetain(publish, subscriber); } @Override @@ -72,11 +66,21 @@ public Array deliverRetainedMessages(SingleSubscriber sub Array retainedMessages = retainedMessageTree.getRetainedMessage(subscription.topicFilter(), transformer); MutableArray result = MutableArray.ofType(PublishHandlingResult.class); for (Publish message : retainedMessages) { - result.add(startDelivering(message, subscriber)); + result.add(startDeliveringWithoutRetain(message, subscriber)); } return Array.copyOf(result); } + private PublishHandlingResult startDeliveringWithoutRetain(Publish publish, SingleSubscriber subscriber) { + try { + //noinspection DataFlowIssue + return publishOutMessageHandlers[subscriber.qos().level()].handle(publish, subscriber); + } catch (IndexOutOfBoundsException | NullPointerException ex) { + log.warning(publish, "Received not supported publish message:[%s]"::formatted); + return PublishHandlingResult.UNSPECIFIED_ERROR; + } + } + private static String buildServiceDescription( @Nullable MqttPublishOutMessageHandler[] publishOutMessageHandlers) { var builder = new StringBuilder(); From bacefb73588947b997229343d3e786fd5bf7a7fc Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Mon, 8 Dec 2025 07:55:20 +0100 Subject: [PATCH 18/38] [broker-30] Add more tests --- acl-groovy-dsl/src/main/resources/acl.gdsl | 2 +- core-service/build.gradle | 3 +- .../InMemorySubscriptionServiceTest.groovy | 132 +++++++++++++++++- .../topic/tree/RetainedMessageTreeTest.groovy | 25 +--- .../subscription/TestPublishFactory.groovy | 68 +++++++++ 5 files changed, 203 insertions(+), 27 deletions(-) create mode 100644 model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestPublishFactory.groovy diff --git a/acl-groovy-dsl/src/main/resources/acl.gdsl b/acl-groovy-dsl/src/main/resources/acl.gdsl index c564ff32..3c4c46af 100644 --- a/acl-groovy-dsl/src/main/resources/acl.gdsl +++ b/acl-groovy-dsl/src/main/resources/acl.gdsl @@ -33,7 +33,7 @@ contributor(context(scope: scriptScope())) { && !enclosingCall("allOf") && !enclosingCall("anyOf")) { method name: 'topicFilter', type: 'javasabr.mqtt.service.acl.builder.SubscribeRuleBuilder', - params: [string: 'javasabr.mqtt.model.acl.matcher.ValueMatcher...'], + params: [string: 'javasabr.mqtt.model.acl.matcher.TopicFilterMatcher...'], doc: 'Set of topic filters matching by rule' method name: 'match', type: 'javasabr.mqtt.model.acl.matcher.TopicFilterMatcher', diff --git a/core-service/build.gradle b/core-service/build.gradle index 04b7b9e8..155f6ee6 100644 --- a/core-service/build.gradle +++ b/core-service/build.gradle @@ -12,4 +12,5 @@ dependencies { testImplementation projects.testSupport testImplementation testFixtures(projects.network) -} \ No newline at end of file + testImplementation testFixtures(projects.model) +} diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy index e473f024..909533f7 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy @@ -5,9 +5,12 @@ import javasabr.mqtt.model.QoS import javasabr.mqtt.model.SubscribeRetainHandling import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode import javasabr.mqtt.model.reason.code.UnsubscribeAckReasonCode +import javasabr.mqtt.model.subscriber.SingleSubscriber import javasabr.mqtt.model.subscription.Subscription +import javasabr.mqtt.model.subscription.TestPublishFactory +import javasabr.mqtt.network.message.out.PublishMqtt5OutMessage import javasabr.mqtt.service.IntegrationServiceSpecification -import javasabr.mqtt.service.SubscriptionService +import javasabr.mqtt.service.TestExternalNetworkMqttUser import javasabr.rlib.collections.array.Array class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { @@ -323,4 +326,131 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { storedSubscriptions.size() == 3 storedSubscriptions ==~ resultSubscriptions } + + def "should only deliver 'send-if-subscription-does-not-exist' Subscribe Retain Handling once"() { + given: + def serverConfig = defaultExternalServerConnectionConfig + def mqttConnection = mockedExternalConnection(serverConfig, MqttVersion.MQTT_5) + def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser + def subscription = new Subscription( + defaultTopicService.createTopicFilter(mqttUser, "topic/filter/1"), + 30, + QoS.AT_MOST_ONCE, + SubscribeRetainHandling.SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST, + true, + true) + def subscriptions = Array.of( + subscription, + new Subscription( + defaultTopicService.createTopicFilter(mqttUser, "topic/filter/2"), + 30, + QoS.AT_LEAST_ONCE, + SubscribeRetainHandling.SEND, + true, + true), + new Subscription( + defaultTopicService.createTopicFilter(mqttUser, "topic/filter/3"), + 30, + QoS.EXACTLY_ONCE, + SubscribeRetainHandling.DO_NOT_SEND, + true, + true)) + and: + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") + def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") + defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + defaultPublishDeliveringService.startDelivering(publishWithoutRetain, new SingleSubscriber(mqttUser, subscription)) + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + when: + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + then: + def firstPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + firstPublishMessage.payload() == publishWithRetain.payload() + and: + def secondPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + secondPublishMessage.payload() == publishWithoutRetain.payload() + and: + def thirdPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + thirdPublishMessage.payload() == publishWithRetain.payload() + and: + mqttUser.isEmpty() + } + + def "should always deliver 'send' Subscribe Retain Handling"() { + given: + def serverConfig = defaultExternalServerConnectionConfig + def mqttConnection = mockedExternalConnection(serverConfig, MqttVersion.MQTT_5) + def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser + def subscription = new Subscription( + defaultTopicService.createTopicFilter(mqttUser, "topic/filter/1"), + 30, + QoS.AT_MOST_ONCE, + SubscribeRetainHandling.SEND, + true, + true) + def subscriptions = Array.of(subscription) + and: + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") + defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + when: + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + then: + def firstSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + firstSentMessage.payload() == publishWithRetain.payload() + and: + def thirdSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + thirdSentMessage.payload() == publishWithRetain.payload() + and: + def fourthSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + fourthSentMessage.payload() == publishWithRetain.payload() + and: + mqttUser.isEmpty() + } + + def "should not deliver 'do-not-send' Subscribe Retain Handling"() { + given: + def serverConfig = defaultExternalServerConnectionConfig + def mqttConnection = mockedExternalConnection(serverConfig, MqttVersion.MQTT_5) + def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser + def subscription = new Subscription( + defaultTopicService.createTopicFilter(mqttUser, "topic/filter/1"), + 30, + QoS.AT_MOST_ONCE, + SubscribeRetainHandling.DO_NOT_SEND, + true, + true) + def subscriptions = Array.of( + subscription, + new Subscription( + defaultTopicService.createTopicFilter(mqttUser, "topic/filter/2"), + 30, + QoS.AT_LEAST_ONCE, + SubscribeRetainHandling.SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST, + true, + true), + new Subscription( + defaultTopicService.createTopicFilter(mqttUser, "topic/filter/3"), + 30, + QoS.EXACTLY_ONCE, + SubscribeRetainHandling.SEND, + true, + true)) + and: + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") + def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") + defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + defaultPublishDeliveringService.startDelivering(publishWithoutRetain, new SingleSubscriber(mqttUser, subscription)) + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + when: + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + then: + def firstPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + firstPublishMessage.payload() == publishWithRetain.payload() + and: + def secondPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + secondPublishMessage.payload() == publishWithoutRetain.payload() + and: + mqttUser.isEmpty() + } } diff --git a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy index 705710e5..03dde52d 100644 --- a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy +++ b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy @@ -1,18 +1,13 @@ package javasabr.mqtt.model.topic.tree -import javasabr.mqtt.model.PayloadFormat -import javasabr.mqtt.model.QoS import javasabr.mqtt.model.publishing.Publish import javasabr.mqtt.model.topic.TopicFilter -import javasabr.mqtt.model.topic.TopicName import javasabr.mqtt.test.support.UnitSpecification -import javasabr.rlib.collections.array.Array -import javasabr.rlib.collections.array.IntArray import java.util.function.Function -import static java.nio.charset.StandardCharsets.UTF_8 +import static javasabr.mqtt.model.subscription.TestPublishFactory.makePublish class RetainedMessageTreeTest extends UnitSpecification { @@ -103,22 +98,4 @@ class RetainedMessageTreeTest extends UnitSpecification { ] ] } - - static def makePublish(String topicName) { - return new Publish( - 1, - QoS.AT_MOST_ONCE, - TopicName.valueOf(topicName), - null, - "payload".getBytes(UTF_8), - false, - true, - null, - IntArray.of(30), - null, - 60000, - 1, - PayloadFormat.UTF8_STRING, - Array.of()); - } } diff --git a/model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestPublishFactory.groovy b/model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestPublishFactory.groovy new file mode 100644 index 00000000..6e99a97e --- /dev/null +++ b/model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestPublishFactory.groovy @@ -0,0 +1,68 @@ +package javasabr.mqtt.model.subscription + +import javasabr.mqtt.model.PayloadFormat +import javasabr.mqtt.model.QoS +import javasabr.mqtt.model.publishing.Publish +import javasabr.mqtt.model.topic.TopicName +import javasabr.rlib.collections.array.Array +import javasabr.rlib.collections.array.IntArray + +import static java.nio.charset.StandardCharsets.UTF_8 + +class TestPublishFactory { + + static def makePublish(String topicName) { + return new Publish( + 1, + QoS.AT_MOST_ONCE, + TopicName.valueOf(topicName), + null, + "payload".getBytes(UTF_8), + false, + true, + null, + IntArray.of(30), + null, + 60000, + 1, + PayloadFormat.UTF8_STRING, + Array.of()); + } + + static def makePublishWithRetain(String topicName, String payload) { + return new Publish( + 1, + QoS.AT_MOST_ONCE, + TopicName.valueOf(topicName), + null, + payload.getBytes(UTF_8), + false, + true, + null, + IntArray.of(30), + null, + 60000, + 1, + PayloadFormat.UTF8_STRING, + Array.of()); + } + + static def makePublishWithoutRetain(String topicName, String payload) { + return new Publish( + 1, + QoS.AT_MOST_ONCE, + TopicName.valueOf(topicName), + null, + payload.getBytes(UTF_8), + false, + false, + null, + IntArray.of(30), + null, + 60000, + 1, + PayloadFormat.UTF8_STRING, + Array.of()); + } + +} From 1d3724269e6ed602a89ea58659f1f47503c14de8 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Mon, 8 Dec 2025 08:07:27 +0100 Subject: [PATCH 19/38] [broker-30] Fix tests --- ...os1MqttPublishOutMessageHandlerTest.groovy | 20 ++++--------- ...os2MqttPublishOutMessageHandlerTest.groovy | 28 +++++-------------- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishOutMessageHandlerTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishOutMessageHandlerTest.groovy index d1adf56a..406e7936 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishOutMessageHandlerTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishOutMessageHandlerTest.groovy @@ -22,9 +22,7 @@ class Qos1MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandl def "should deliver publish to subscriber"() { given: - def publishOutHandler = new Qos1MqttPublishOutMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService) + def publishOutHandler = new Qos1MqttPublishOutMessageHandler(defaultMessageOutFactoryService) def connection = mockedExternalConnection(MqttVersion.MQTT_5) def user = connection.user() as TestExternalNetworkMqttUser def testTopicName = defaultTopicService.createTopicName(user, "Qos1MqttPublishOutMessageHandlerTest/1") @@ -50,9 +48,7 @@ class Qos1MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandl def "should wait for ack response for publish"() { given: - def publishOutHandler = new Qos1MqttPublishOutMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService) + def publishOutHandler = new Qos1MqttPublishOutMessageHandler(defaultMessageOutFactoryService) def connection = mockedExternalConnection(MqttVersion.MQTT_5) def user = connection.user() as TestExternalNetworkMqttUser def session = user.session() @@ -98,9 +94,7 @@ class Qos1MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandl def "should correctly handle publish ack when no stored trackable meta about the publish"() { given: - def publishOutHandler = new Qos1MqttPublishOutMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService) + def publishOutHandler = new Qos1MqttPublishOutMessageHandler(defaultMessageOutFactoryService) def connection = mockedExternalConnection(MqttVersion.MQTT_5) def user = connection.user() as TestExternalNetworkMqttUser def session = user.session() @@ -149,9 +143,7 @@ class Qos1MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandl def "should handle as protocol error receiving unexpected response message"() { given: - def publishOutHandler = new Qos1MqttPublishOutMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService) + def publishOutHandler = new Qos1MqttPublishOutMessageHandler(defaultMessageOutFactoryService) def connection = mockedExternalConnection(MqttVersion.MQTT_5) def user = connection.user() as TestExternalNetworkMqttUser def session = user.session() @@ -191,9 +183,7 @@ class Qos1MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandl def "should handle as protocol error for unexpected flow state"() { given: - def publishOutHandler = new Qos1MqttPublishOutMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService) + def publishOutHandler = new Qos1MqttPublishOutMessageHandler(defaultMessageOutFactoryService) def connection = mockedExternalConnection(MqttVersion.MQTT_5) def user = connection.user() as TestExternalNetworkMqttUser def session = user.session() diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishOutMessageHandlerTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishOutMessageHandlerTest.groovy index d6d0ef56..1de5d99b 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishOutMessageHandlerTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishOutMessageHandlerTest.groovy @@ -25,9 +25,7 @@ class Qos2MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandl def "should deliver publish to subscriber"() { given: - def publishOutHandler = new Qos2MqttPublishOutMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService) + def publishOutHandler = new Qos2MqttPublishOutMessageHandler(defaultMessageOutFactoryService) def connection = mockedExternalConnection(MqttVersion.MQTT_5) def user = connection.user() as TestExternalNetworkMqttUser def testTopicName = defaultTopicService.createTopicName(user, "Qos2MqttPublishOutMessageHandlerTest/1") @@ -53,9 +51,7 @@ class Qos2MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandl def "should wait for receive-complete responses for publish"() { given: - def publishOutHandler = new Qos2MqttPublishOutMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService) + def publishOutHandler = new Qos2MqttPublishOutMessageHandler(defaultMessageOutFactoryService) def connection = mockedExternalConnection(MqttVersion.MQTT_5) def user = connection.user() as TestExternalNetworkMqttUser def session = user.session() @@ -124,9 +120,7 @@ class Qos2MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandl def "should correctly handle publish receive when no stored trackable meta about the publish"() { given: - def publishOutHandler = new Qos2MqttPublishOutMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService) + def publishOutHandler = new Qos2MqttPublishOutMessageHandler(defaultMessageOutFactoryService) def connection = mockedExternalConnection(MqttVersion.MQTT_5) def user = connection.user() as TestExternalNetworkMqttUser def session = user.session() @@ -176,9 +170,7 @@ class Qos2MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandl def "should handle as protocol error receiving unexpected response message for first stage"() { given: - def publishOutHandler = new Qos2MqttPublishOutMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService) + def publishOutHandler = new Qos2MqttPublishOutMessageHandler(defaultMessageOutFactoryService) def connection = mockedExternalConnection(MqttVersion.MQTT_5) def user = connection.user() as TestExternalNetworkMqttUser def session = user.session() @@ -218,9 +210,7 @@ class Qos2MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandl def "should handle as protocol error receiving unexpected response message for second stage"() { given: - def publishOutHandler = new Qos2MqttPublishOutMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService) + def publishOutHandler = new Qos2MqttPublishOutMessageHandler(defaultMessageOutFactoryService) def connection = mockedExternalConnection(MqttVersion.MQTT_5) def user = connection.user() as TestExternalNetworkMqttUser def session = user.session() @@ -271,9 +261,7 @@ class Qos2MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandl def "should handle as protocol error for unexpected flow state for publish received"() { given: - def publishOutHandler = new Qos2MqttPublishOutMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService) + def publishOutHandler = new Qos2MqttPublishOutMessageHandler(defaultMessageOutFactoryService) def connection = mockedExternalConnection(MqttVersion.MQTT_5) def user = connection.user() as TestExternalNetworkMqttUser def session = user.session() @@ -316,9 +304,7 @@ class Qos2MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandl def "should handle as protocol error for unexpected flow state for publish complete"() { given: - def publishOutHandler = new Qos2MqttPublishOutMessageHandler( - defaultSubscriptionService, - defaultMessageOutFactoryService) + def publishOutHandler = new Qos2MqttPublishOutMessageHandler(defaultMessageOutFactoryService) def connection = mockedExternalConnection(MqttVersion.MQTT_5) def user = connection.user() as TestExternalNetworkMqttUser def session = user.session() From c3ab5c548e4464c7dce94e845daf5c6ecb0f1a83 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:19:38 +0100 Subject: [PATCH 20/38] [broker-30] Fix retainAsPublished logic --- .../impl/DefaultPublishDeliveringService.java | 9 +- .../InMemorySubscriptionServiceTest.groovy | 88 +++++++++++++++++-- .../mqtt/model/subscription/Subscription.java | 8 +- .../tree/ConcurrentRetainedMessageTree.java | 4 +- .../model/topic/tree/RetainedMessageNode.java | 10 +-- .../topic/tree/RetainedMessageTreeTest.groovy | 5 +- 6 files changed, 101 insertions(+), 23 deletions(-) diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java index e9bdce16..eb6e8218 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java @@ -53,7 +53,10 @@ public DefaultPublishDeliveringService( @Override public PublishHandlingResult startDelivering(Publish publish, SingleSubscriber subscriber) { if (publish.retained()) { - retainedMessageTree.retainMessage(publish); + Subscription subscription = subscriber.subscription(); + boolean retainAsPublished = subscription.retainAsPublished(); + Function transformer = retainAsPublished ? Function.identity() : Publish::withoutRetain; + retainedMessageTree.retainMessage(transformer.apply(publish)); } return startDeliveringWithoutRetain(publish, subscriber); } @@ -61,9 +64,7 @@ public PublishHandlingResult startDelivering(Publish publish, SingleSubscriber s @Override public Array deliverRetainedMessages(SingleSubscriber subscriber) { Subscription subscription = subscriber.subscription(); - boolean retainAsPublished = subscription.retainAsPublished(); - Function transformer = retainAsPublished ? Function.identity() : Publish::withoutRetain; - Array retainedMessages = retainedMessageTree.getRetainedMessage(subscription.topicFilter(), transformer); + Array retainedMessages = retainedMessageTree.getRetainedMessage(subscription.topicFilter()); MutableArray result = MutableArray.ofType(PublishHandlingResult.class); for (Publish message : retainedMessages) { result.add(startDeliveringWithoutRetain(message, subscriber)); diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy index 909533f7..70ef861a 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy @@ -357,12 +357,13 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true)) and: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + and: + def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") defaultPublishDeliveringService.startDelivering(publishWithoutRetain, new SingleSubscriber(mqttUser, subscription)) - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: def firstPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) firstPublishMessage.payload() == publishWithRetain.payload() @@ -392,9 +393,9 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { and: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: def firstSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) firstSentMessage.payload() == publishWithRetain.payload() @@ -438,12 +439,13 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true)) and: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + and: + def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") defaultPublishDeliveringService.startDelivering(publishWithoutRetain, new SingleSubscriber(mqttUser, subscription)) - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: def firstPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) firstPublishMessage.payload() == publishWithRetain.payload() @@ -453,4 +455,80 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { and: mqttUser.isEmpty() } + + def "should reset retain flag if 'retain as published' is false"() { + given: + def serverConfig = defaultExternalServerConnectionConfig + def mqttConnection = mockedExternalConnection(serverConfig, MqttVersion.MQTT_5) + def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser + def subscription = new Subscription( + defaultTopicService.createTopicFilter(mqttUser, "topic/filter/1"), + 30, + QoS.AT_MOST_ONCE, + SubscribeRetainHandling.SEND, + true, + false) + def subscriptions = Array.of(subscription) + and: + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") + defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + when: + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + then: + def firstSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + firstSentMessage.payload() == publishWithRetain.payload() + firstSentMessage.retain() + and: + def thirdSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + thirdSentMessage.payload() == publishWithRetain.payload() + !thirdSentMessage.retain() + and: + mqttUser.isEmpty() + } + + def "should keep retain flag if 'retain as published' is true"() { + given: + def serverConfig = defaultExternalServerConnectionConfig + def mqttConnection = mockedExternalConnection(serverConfig, MqttVersion.MQTT_5) + def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser + def subscription = new Subscription( + defaultTopicService.createTopicFilter(mqttUser, "topic/filter/1"), + 30, + QoS.AT_MOST_ONCE, + SubscribeRetainHandling.SEND, + true, + true) + def subscriptions = Array.of(subscription) + + when: + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") + defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + then: + def firstSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + firstSentMessage.payload() == publishWithRetain.payload() + firstSentMessage.retain() + and: + def secondSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + secondSentMessage.payload() == publishWithRetain.payload() + secondSentMessage.retain() + and: + mqttUser.isEmpty() + + when: + def publishWithRetain2 = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload2") + defaultPublishDeliveringService.startDelivering(publishWithRetain2, new SingleSubscriber(mqttUser, subscription)) + defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + then: + def thirdSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + thirdSentMessage.payload() == publishWithRetain2.payload() + thirdSentMessage.retain() + and: + def fourthSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + fourthSentMessage.payload() == publishWithRetain2.payload() + fourthSentMessage.retain() + and: + mqttUser.isEmpty() + + } } diff --git a/model/src/main/java/javasabr/mqtt/model/subscription/Subscription.java b/model/src/main/java/javasabr/mqtt/model/subscription/Subscription.java index 69439978..c66a6548 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscription/Subscription.java +++ b/model/src/main/java/javasabr/mqtt/model/subscription/Subscription.java @@ -33,8 +33,12 @@ public record Subscription( boolean noLocal, /* If true, Application Messages forwarded using this subscription keep the RETAIN flag they were published with. If - false, Application Messages forwarded using this subscription have the RETAIN flag set to 0. Retained messages sent - when the subscription is established have the RETAIN flag set to 1. + false, Application Messages forwarded using this subscription have the RETAIN flag set to 0. + + Bit 3 of the Subscription Options represents the Retain As Published option. + If 1, Application Messages forwarded using this subscription keep the RETAIN flag they were published with. + If 0, Application Messages forwarded using this subscription have the RETAIN flag set to 0. + Retained messages sent when the subscription is established have the RETAIN flag set to 1. */ boolean retainAsPublished) { diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java index 6831d3a3..da9317da 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java @@ -22,9 +22,9 @@ public void retainMessage(Publish message) { rootNode.retainMessage(0, message, message.topicName()); } - public Array getRetainedMessage(TopicFilter topicFilter, Function publishTransformer) { + public Array getRetainedMessage(TopicFilter topicFilter) { var resultArray = MutableArray.ofType(Publish.class); - rootNode.collectRetainedMessages(0, topicFilter, resultArray, publishTransformer); + rootNode.collectRetainedMessages(0, topicFilter, resultArray); return Array.copyOf(resultArray); } } diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java index 52dd6883..e4436ee4 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java @@ -2,7 +2,6 @@ import java.util.Queue; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; import java.util.function.Supplier; import javasabr.mqtt.base.util.DebugUtils; import javasabr.mqtt.model.AbstractTrieNode; @@ -53,12 +52,11 @@ public void retainMessage(int level, Publish message, TopicName topicName) { public void collectRetainedMessages( int level, TopicFilter topicFilter, - MutableArray result, - Function publishTransformer) { + MutableArray result) { if (level == topicFilter.levelsCount()) { Publish publish = retainedMessage.get(); if (publish != null) { - result.add(publishTransformer.apply(publish)); + result.add(publish); } return; } @@ -72,13 +70,13 @@ public void collectRetainedMessages( var localChildNodes = getChildNodes(RetainedMessageNode::childNodesFactory); if (localChildNodes != null) { for (RetainedMessageNode childNode : localChildNodes) { - childNode.collectRetainedMessages(level + 1, topicFilter, result, publishTransformer); + childNode.collectRetainedMessages(level + 1, topicFilter, result); } } } else { RetainedMessageNode retainedMessageNode = getChildNode(segment); if (retainedMessageNode != null) { - retainedMessageNode.collectRetainedMessages(level + 1, topicFilter, result, publishTransformer); + retainedMessageNode.collectRetainedMessages(level + 1, topicFilter, result); } } } diff --git a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy index 03dde52d..02dbca81 100644 --- a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy +++ b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy @@ -1,12 +1,9 @@ package javasabr.mqtt.model.topic.tree - import javasabr.mqtt.model.publishing.Publish import javasabr.mqtt.model.topic.TopicFilter import javasabr.mqtt.test.support.UnitSpecification -import java.util.function.Function - import static javasabr.mqtt.model.subscription.TestPublishFactory.makePublish class RetainedMessageTreeTest extends UnitSpecification { @@ -21,7 +18,7 @@ class RetainedMessageTreeTest extends UnitSpecification { retainedMessageTree.retainMessage(message) } when: - def retainedMessages = retainedMessageTree.getRetainedMessage(TopicFilter.valueOf(topicFilter), Function.identity()) + def retainedMessages = retainedMessageTree.getRetainedMessage(TopicFilter.valueOf(topicFilter)) .collect { it } then: retainedMessages.size() == expectedMessages.size() From b829090e5182c07eb044d327af41fb841108f685 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:41:25 +0100 Subject: [PATCH 21/38] [broker-30] Add tests --- .../impl/InMemorySubscriptionService.java | 24 ++--- .../InMemorySubscriptionServiceTest.groovy | 91 +++++++++++++++++-- 2 files changed, 89 insertions(+), 26 deletions(-) diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index a06c698e..6a2780a0 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -7,6 +7,7 @@ import javasabr.mqtt.model.MqttClientConnectionConfig; import javasabr.mqtt.model.MqttUser; +import javasabr.mqtt.model.QoS; import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode; import javasabr.mqtt.model.reason.code.UnsubscribeAckReasonCode; import javasabr.mqtt.model.session.ActiveSubscriptions; @@ -81,13 +82,13 @@ private SubscribeAckReasonCode addSubscription(MqttUser user, MqttSession sessio if (previous != null) { activeSubscriptions.remove(previous.subscription()); } - if ((subscription.retainHandling() == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previous == null) - || subscription.retainHandling() == SEND) { + QoS subscriptionQoS = subscription.qos(); + if (subscriptionQoS.ordinal() <= 2 && (subscription.retainHandling() == SEND || + (subscription.retainHandling() == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previous == null))) { sendRetainedMessages(user, subscription); } activeSubscriptions.add(subscription); - return subscription - .qos() + return subscriptionQoS .subscribeAckReasonCode(); } @@ -142,19 +143,8 @@ public void restoreSubscriptions(MqttUser user, MqttSession session) { } private void sendRetainedMessages(MqttUser user, Subscription subscription) { - SubscribeAckReasonCode subscribeAckReasonCode = subscription - .qos() - .subscribeAckReasonCode(); - String clientId = user.clientId(); - if (subscribeAckReasonCode.ordinal() > 2) { - log.debug( - clientId, - subscription, - subscribeAckReasonCode, - "[%s] Unable to send retained messages for [%s] due to wrong subscribeAckReasonCode [%s]"::formatted); - return; - } int count = 0; + String clientId = user.clientId(); PublishHandlingResult errorResult = null; SingleSubscriber singleSubscriber = new SingleSubscriber(user, subscription); var results = publishDeliveringService.deliverRetainedMessages(singleSubscriber); @@ -167,7 +157,7 @@ private void sendRetainedMessages(MqttUser user, Subscription subscription) { if (errorResult != null) { log.debug(clientId, errorResult, "[%s] Error occurred [%s] during sending retained messages"::formatted); } else { - log.debug(clientId, count, "[%s] Delivering [%s] retained messages has been started"::formatted); + log.debug(clientId, count, "[%s] Delivering of [%s] retained message has been started"::formatted); } } } diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy index 70ef861a..0dbbd7c9 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy @@ -8,6 +8,10 @@ import javasabr.mqtt.model.reason.code.UnsubscribeAckReasonCode import javasabr.mqtt.model.subscriber.SingleSubscriber import javasabr.mqtt.model.subscription.Subscription import javasabr.mqtt.model.subscription.TestPublishFactory +import javasabr.mqtt.model.topic.TopicFilter +import javasabr.mqtt.model.topic.TopicName +import javasabr.mqtt.network.handler.NetworkMqttUserReleaseHandler +import javasabr.mqtt.network.impl.InternalNetworkMqttUser import javasabr.mqtt.network.message.out.PublishMqtt5OutMessage import javasabr.mqtt.service.IntegrationServiceSpecification import javasabr.mqtt.service.TestExternalNetworkMqttUser @@ -514,21 +518,90 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { secondSentMessage.retain() and: mqttUser.isEmpty() + } + def "should not send retained messages in case of invalid QoS"() { + given: + def serverConfig = defaultExternalServerConnectionConfig + def mqttConnection = mockedExternalConnection(serverConfig, MqttVersion.MQTT_5) + def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser + def subscription = new Subscription( + defaultTopicService.createTopicFilter(mqttUser, "topic/filter/1"), + 30, + QoS.INVALID, + SubscribeRetainHandling.SEND, + true, + true) + def subscriptions = Array.of(subscription) + and: + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") + defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) when: - def publishWithRetain2 = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload2") - defaultPublishDeliveringService.startDelivering(publishWithRetain2, new SingleSubscriber(mqttUser, subscription)) defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: - def thirdSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) - thirdSentMessage.payload() == publishWithRetain2.payload() - thirdSentMessage.retain() + mqttUser.isEmpty() + } + + def "should clean and restore subscriptions"() { + given: + def serverConfig = defaultExternalServerConnectionConfig + def mqttConnection = mockedExternalConnection(serverConfig, MqttVersion.MQTT_5) + def expectedUser = mqttConnection.user() as TestExternalNetworkMqttUser + def expectedSubscription = new Subscription( + TopicFilter.valueOf("topic"), + 30, + QoS.AT_MOST_ONCE, + SubscribeRetainHandling.SEND, + true, + true) + when: + defaultSubscriptionService.subscribe(expectedUser, expectedUser.session(), Array.of(expectedSubscription)) + def subscribers = defaultSubscriptionService.findSubscribers(TopicName.valueOf("topic")) + then: + !subscribers.isEmpty() + with(subscribers[0]) { + user() == expectedUser + subscription() == expectedSubscription + } + when: + defaultSubscriptionService.cleanSubscriptions(expectedUser, expectedUser.session()) + subscribers = defaultSubscriptionService.findSubscribers(TopicName.valueOf("topic")) + then: + subscribers.isEmpty() + + when: + defaultSubscriptionService.restoreSubscriptions(expectedUser, expectedUser.session()) + subscribers = defaultSubscriptionService.findSubscribers(TopicName.valueOf("topic")) + then: + !subscribers.isEmpty() + with(subscribers[0]) { + user() == expectedUser + subscription() == expectedSubscription + } + } + def "should suppress retained message delivering failure"() { + given: + def serverConfig = defaultExternalServerConnectionConfig + def mqttConnection = mockedExternalConnection(serverConfig, MqttVersion.MQTT_5) + def mqttUser = mqttConnection.user() as TestExternalNetworkMqttUser + def anotherUser = new InternalNetworkMqttUser(mqttConnection, Mock(NetworkMqttUserReleaseHandler)) + def subscription = new Subscription( + defaultTopicService.createTopicFilter(mqttUser, "topic/filter/1"), + 30, + QoS.AT_MOST_ONCE, + SubscribeRetainHandling.SEND, + true, + true) + def subscriptions = Array.of(subscription) and: - def fourthSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) - fourthSentMessage.payload() == publishWithRetain2.payload() - fourthSentMessage.retain() + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") + defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + when: + defaultSubscriptionService.subscribe(anotherUser, mqttUser.session(), subscriptions) + then: + def firstSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + firstSentMessage.payload() == publishWithRetain.payload() and: mqttUser.isEmpty() - } } From 26f45dcb22fad5ddbb8d9e40a8ddde1aa786259f Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:27:14 +0100 Subject: [PATCH 22/38] [broker-30] Introduce RetainMessageService --- .../config/MqttBrokerSpringConfig.java | 55 ++++++++++------ .../service/PublishDeliveringService.java | 2 - .../mqtt/service/RetainMessageService.java | 14 +++++ .../impl/DefaultPublishDeliveringService.java | 28 --------- .../impl/DefaultRetainMessageService.java | 46 ++++++++++++++ .../impl/InMemorySubscriptionService.java | 10 +-- .../AbstractMqttPublishInMessageHandler.java | 3 + .../impl/Qos0MqttPublishInMessageHandler.java | 11 +++- .../impl/Qos1MqttPublishInMessageHandler.java | 6 +- .../impl/Qos2MqttPublishInMessageHandler.java | 63 +++++++++++-------- .../TrackableMqttPublishInMessageHandler.java | 15 +++-- .../IntegrationServiceSpecification.groovy | 21 ++++--- .../InMemorySubscriptionServiceTest.groovy | 28 +++------ ...UnsubscribeMqttInMessageHandlerTest.groovy | 2 +- ...Qos0MqttPublishInMessageHandlerTest.groovy | 6 +- ...Qos1MqttPublishInMessageHandlerTest.groovy | 15 +++-- ...Qos2MqttPublishInMessageHandlerTest.groovy | 18 ++++-- .../tree/ConcurrentRetainedMessageTree.java | 1 - 18 files changed, 211 insertions(+), 133 deletions(-) create mode 100644 core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java create mode 100644 core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java diff --git a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java index 2dbad30c..e5f7ea38 100644 --- a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java +++ b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java @@ -17,6 +17,7 @@ import javasabr.mqtt.service.MessageOutFactoryService; import javasabr.mqtt.service.PublishDeliveringService; import javasabr.mqtt.service.PublishReceivingService; +import javasabr.mqtt.service.RetainMessageService; import javasabr.mqtt.service.SubscriptionService; import javasabr.mqtt.service.TopicService; import javasabr.mqtt.service.handler.client.ExternalNetworkMqttUserReleaseHandler; @@ -25,6 +26,7 @@ import javasabr.mqtt.service.impl.DefaultMqttConnectionFactory; import javasabr.mqtt.service.impl.DefaultPublishDeliveringService; import javasabr.mqtt.service.impl.DefaultPublishReceivingService; +import javasabr.mqtt.service.impl.DefaultRetainMessageService; import javasabr.mqtt.service.impl.DefaultTopicService; import javasabr.mqtt.service.impl.ExternalNetworkMqttUserFactory; import javasabr.mqtt.service.impl.FileCredentialsSource; @@ -99,8 +101,13 @@ AuthenticationService authenticationService( } @Bean - SubscriptionService subscriptionService(PublishDeliveringService publishDeliveringService) { - return new InMemorySubscriptionService(publishDeliveringService); + SubscriptionService subscriptionService(RetainMessageService retainMessageService) { + return new InMemorySubscriptionService(retainMessageService); + } + + @Bean + RetainMessageService retainMessageService(PublishDeliveringService publishDeliveringService) { + return new DefaultRetainMessageService(publishDeliveringService); } @Bean @@ -218,24 +225,39 @@ PublishDeliveringService publishDeliveringService( MqttPublishInMessageHandler qos0MqttPublishInMessageHandler( SubscriptionService subscriptionService, PublishDeliveringService publishDeliveringService, - MessageOutFactoryService messageOutFactoryService) { - return new Qos0MqttPublishInMessageHandler(subscriptionService, publishDeliveringService, messageOutFactoryService); + MessageOutFactoryService messageOutFactoryService, + RetainMessageService retainMessageService) { + return new Qos0MqttPublishInMessageHandler( + subscriptionService, + publishDeliveringService, + messageOutFactoryService, + retainMessageService); } @Bean MqttPublishInMessageHandler qos1MqttPublishInMessageHandler( SubscriptionService subscriptionService, PublishDeliveringService publishDeliveringService, - MessageOutFactoryService messageOutFactoryService) { - return new Qos1MqttPublishInMessageHandler(subscriptionService, publishDeliveringService, messageOutFactoryService); + MessageOutFactoryService messageOutFactoryService, + RetainMessageService retainMessageService) { + return new Qos1MqttPublishInMessageHandler( + subscriptionService, + publishDeliveringService, + messageOutFactoryService, + retainMessageService); } @Bean MqttPublishInMessageHandler qos2MqttPublishInMessageHandler( SubscriptionService subscriptionService, PublishDeliveringService publishDeliveringService, - MessageOutFactoryService messageOutFactoryService) { - return new Qos2MqttPublishInMessageHandler(subscriptionService, publishDeliveringService, messageOutFactoryService); + MessageOutFactoryService messageOutFactoryService, + RetainMessageService retainMessageService) { + return new Qos2MqttPublishInMessageHandler( + subscriptionService, + publishDeliveringService, + messageOutFactoryService, + retainMessageService); } @Bean @@ -268,10 +290,7 @@ MqttServerConnectionConfig externalConnectionConfig(Environment env) { "mqtt.external.connection.receive.maximum", int.class, MqttProperties.RECEIVE_MAXIMUM_PUBLISHES_DEFAULT), - env.getProperty( - "mqtt.external.connection.topic.alias.maximum", - int.class, - 0), + env.getProperty("mqtt.external.connection.topic.alias.maximum", int.class, 0), env.getProperty( "mqtt.external.connection.default.session.expiration.time", long.class, @@ -284,18 +303,14 @@ MqttServerConnectionConfig externalConnectionConfig(Environment env) { "mqtt.external.connection.sessions.enabled", boolean.class, MqttProperties.SESSIONS_ENABLED_DEFAULT), - env.getProperty( - "mqtt.external.connection.retain.available", - boolean.class, - false), // set false because currently it's not implemented and we should not allow for clients to use it + env.getProperty("mqtt.external.connection.retain.available", boolean.class, false), + // set false because currently it's not implemented and we should not allow for clients to use it env.getProperty( "mqtt.external.connection.wildcard.subscription.available", boolean.class, MqttProperties.WILDCARD_SUBSCRIPTION_AVAILABLE_DEFAULT), - env.getProperty( - "mqtt.external.connection.subscription.id.available", - boolean.class, - false), // set false because currently it's not implemented and we should not allow for clients to use it + env.getProperty("mqtt.external.connection.subscription.id.available", boolean.class, false), + // set false because currently it's not implemented and we should not allow for clients to use it env.getProperty( "mqtt.external.connection.shared.subscription.available", boolean.class, diff --git a/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java b/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java index bcc83dbd..8b7c8d39 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java @@ -8,6 +8,4 @@ public interface PublishDeliveringService { PublishHandlingResult startDelivering(Publish publish, SingleSubscriber subscriber); - - Array deliverRetainedMessages(SingleSubscriber subscriber); } diff --git a/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java b/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java new file mode 100644 index 00000000..b8eaee53 --- /dev/null +++ b/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java @@ -0,0 +1,14 @@ +package javasabr.mqtt.service; + +import javasabr.mqtt.model.publishing.Publish; +import javasabr.mqtt.model.subscriber.SingleSubscriber; +import javasabr.mqtt.model.subscription.Subscription; +import javasabr.mqtt.service.publish.handler.PublishHandlingResult; +import javasabr.rlib.collections.array.Array; + +public interface RetainMessageService { + + void retainMessage(Publish publish, Subscription subscription); + + Array deliverRetainedMessages(SingleSubscriber subscriber); +} diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java index eb6e8218..59b6fcce 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java @@ -1,17 +1,12 @@ package javasabr.mqtt.service.impl; import java.util.Collection; -import java.util.function.Function; import javasabr.mqtt.model.QoS; import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.subscriber.SingleSubscriber; -import javasabr.mqtt.model.subscription.Subscription; -import javasabr.mqtt.model.topic.tree.ConcurrentRetainedMessageTree; import javasabr.mqtt.service.PublishDeliveringService; import javasabr.mqtt.service.publish.handler.MqttPublishOutMessageHandler; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; -import javasabr.rlib.collections.array.Array; -import javasabr.rlib.collections.array.MutableArray; import lombok.AccessLevel; import lombok.CustomLog; import lombok.experimental.FieldDefaults; @@ -23,7 +18,6 @@ public class DefaultPublishDeliveringService implements PublishDeliveringService @Nullable MqttPublishOutMessageHandler[] publishOutMessageHandlers; - ConcurrentRetainedMessageTree retainedMessageTree; public DefaultPublishDeliveringService( Collection knownPublishOutHandlers) { @@ -45,34 +39,12 @@ public DefaultPublishDeliveringService( } handlers[qos.level()] = knownPublishOutHandler; } - this.retainedMessageTree = new ConcurrentRetainedMessageTree(); this.publishOutMessageHandlers = handlers; log.info(publishOutMessageHandlers, DefaultPublishDeliveringService::buildServiceDescription); } @Override public PublishHandlingResult startDelivering(Publish publish, SingleSubscriber subscriber) { - if (publish.retained()) { - Subscription subscription = subscriber.subscription(); - boolean retainAsPublished = subscription.retainAsPublished(); - Function transformer = retainAsPublished ? Function.identity() : Publish::withoutRetain; - retainedMessageTree.retainMessage(transformer.apply(publish)); - } - return startDeliveringWithoutRetain(publish, subscriber); - } - - @Override - public Array deliverRetainedMessages(SingleSubscriber subscriber) { - Subscription subscription = subscriber.subscription(); - Array retainedMessages = retainedMessageTree.getRetainedMessage(subscription.topicFilter()); - MutableArray result = MutableArray.ofType(PublishHandlingResult.class); - for (Publish message : retainedMessages) { - result.add(startDeliveringWithoutRetain(message, subscriber)); - } - return Array.copyOf(result); - } - - private PublishHandlingResult startDeliveringWithoutRetain(Publish publish, SingleSubscriber subscriber) { try { //noinspection DataFlowIssue return publishOutMessageHandlers[subscriber.qos().level()].handle(publish, subscriber); diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java new file mode 100644 index 00000000..50b252ea --- /dev/null +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java @@ -0,0 +1,46 @@ +package javasabr.mqtt.service.impl; + +import java.util.function.Function; +import javasabr.mqtt.model.publishing.Publish; +import javasabr.mqtt.model.subscriber.SingleSubscriber; +import javasabr.mqtt.model.subscription.Subscription; +import javasabr.mqtt.model.topic.tree.ConcurrentRetainedMessageTree; +import javasabr.mqtt.service.PublishDeliveringService; +import javasabr.mqtt.service.RetainMessageService; +import javasabr.mqtt.service.publish.handler.PublishHandlingResult; +import javasabr.rlib.collections.array.Array; +import javasabr.rlib.collections.array.MutableArray; +import lombok.AccessLevel; +import lombok.experimental.FieldDefaults; + +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class DefaultRetainMessageService implements RetainMessageService { + + PublishDeliveringService defaultPublishDeliveringService; + ConcurrentRetainedMessageTree retainedMessageTree; + + public DefaultRetainMessageService(PublishDeliveringService defaultPublishDeliveringService) { + this.defaultPublishDeliveringService = defaultPublishDeliveringService; + this.retainedMessageTree = new ConcurrentRetainedMessageTree(); + } + + @Override + public void retainMessage(Publish publish, Subscription subscription) { + if (publish.retained()) { + boolean retainAsPublished = subscription.retainAsPublished(); + Function transformer = retainAsPublished ? Function.identity() : Publish::withoutRetain; + retainedMessageTree.retainMessage(transformer.apply(publish)); + } + } + + @Override + public Array deliverRetainedMessages(SingleSubscriber subscriber) { + Subscription subscription = subscriber.subscription(); + Array retainedMessages = retainedMessageTree.getRetainedMessage(subscription.topicFilter()); + MutableArray result = MutableArray.ofType(PublishHandlingResult.class); + for (Publish message : retainedMessages) { + result.add(defaultPublishDeliveringService.startDelivering(message, subscriber)); + } + return Array.copyOf(result); + } +} diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index 6a2780a0..ef8b5b90 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -18,7 +18,7 @@ import javasabr.mqtt.model.topic.SharedTopicFilter; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.mqtt.model.topic.TopicName; -import javasabr.mqtt.service.PublishDeliveringService; +import javasabr.mqtt.service.RetainMessageService; import javasabr.mqtt.service.SubscriptionService; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; import javasabr.rlib.collections.array.Array; @@ -35,12 +35,12 @@ @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) public class InMemorySubscriptionService implements SubscriptionService { - PublishDeliveringService publishDeliveringService; + RetainMessageService retainMessageService; ConcurrentSubscriberTree subscriberTree; - public InMemorySubscriptionService(PublishDeliveringService publishDeliveringService) { + public InMemorySubscriptionService(RetainMessageService retainMessageService) { this.subscriberTree = new ConcurrentSubscriberTree(); - this.publishDeliveringService = publishDeliveringService; + this.retainMessageService = retainMessageService; } @Override @@ -147,7 +147,7 @@ private void sendRetainedMessages(MqttUser user, Subscription subscription) { String clientId = user.clientId(); PublishHandlingResult errorResult = null; SingleSubscriber singleSubscriber = new SingleSubscriber(user, subscription); - var results = publishDeliveringService.deliverRetainedMessages(singleSubscriber); + var results = retainMessageService.deliverRetainedMessages(singleSubscriber); for (PublishHandlingResult result : results) { if (result.error()) { errorResult = result; diff --git a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishInMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishInMessageHandler.java index ab91e42d..68819b53 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishInMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishInMessageHandler.java @@ -10,6 +10,7 @@ import javasabr.mqtt.network.user.NetworkMqttUser; import javasabr.mqtt.service.MessageOutFactoryService; import javasabr.mqtt.service.PublishDeliveringService; +import javasabr.mqtt.service.RetainMessageService; import javasabr.mqtt.service.SubscriptionService; import javasabr.mqtt.service.publish.handler.MqttPublishInMessageHandler; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; @@ -29,6 +30,7 @@ public abstract class AbstractMqttPublishInMessageHandler { @@ -17,8 +18,14 @@ public class Qos0MqttPublishInMessageHandler extends AbstractMqttPublishInMessag public Qos0MqttPublishInMessageHandler( SubscriptionService subscriptionService, PublishDeliveringService publishDeliveringService, - MessageOutFactoryService messageOutFactoryService) { - super(ExternalNetworkMqttUser.class, subscriptionService, publishDeliveringService, messageOutFactoryService); + MessageOutFactoryService messageOutFactoryService, + RetainMessageService retainMessageService) { + super( + ExternalNetworkMqttUser.class, + subscriptionService, + publishDeliveringService, + messageOutFactoryService, + retainMessageService); } @Override diff --git a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishInMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishInMessageHandler.java index 76c1446d..62633a75 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishInMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishInMessageHandler.java @@ -11,6 +11,7 @@ import javasabr.mqtt.network.session.NetworkMqttSession; import javasabr.mqtt.service.MessageOutFactoryService; import javasabr.mqtt.service.PublishDeliveringService; +import javasabr.mqtt.service.RetainMessageService; import javasabr.mqtt.service.SubscriptionService; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; import lombok.AccessLevel; @@ -24,8 +25,9 @@ public class Qos1MqttPublishInMessageHandler extends TrackableMqttPublishInMessa public Qos1MqttPublishInMessageHandler( SubscriptionService subscriptionService, PublishDeliveringService publishDeliveringService, - MessageOutFactoryService messageOutFactoryService) { - super(ExternalNetworkMqttUser.class, subscriptionService, publishDeliveringService, messageOutFactoryService); + MessageOutFactoryService messageOutFactoryService, + RetainMessageService retainMessageService) { + super(ExternalNetworkMqttUser.class, subscriptionService, publishDeliveringService, messageOutFactoryService, retainMessageService); } @Override diff --git a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishInMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishInMessageHandler.java index 532b0b8f..df446c5d 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishInMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishInMessageHandler.java @@ -19,6 +19,7 @@ import javasabr.mqtt.network.session.NetworkMqttSession; import javasabr.mqtt.service.MessageOutFactoryService; import javasabr.mqtt.service.PublishDeliveringService; +import javasabr.mqtt.service.RetainMessageService; import javasabr.mqtt.service.SubscriptionService; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; import lombok.AccessLevel; @@ -34,8 +35,14 @@ public class Qos2MqttPublishInMessageHandler extends TrackableMqttPublishInMessa public Qos2MqttPublishInMessageHandler( SubscriptionService subscriptionService, PublishDeliveringService publishDeliveringService, - MessageOutFactoryService messageOutFactoryService) { - super(ExternalNetworkMqttUser.class, subscriptionService, publishDeliveringService, messageOutFactoryService); + MessageOutFactoryService messageOutFactoryService, + RetainMessageService retainMessageService) { + super( + ExternalNetworkMqttUser.class, + subscriptionService, + publishDeliveringService, + messageOutFactoryService, + retainMessageService); this.trackableMessageCallback = this::handleReceivedTrackableMessage; } @@ -67,17 +74,15 @@ protected boolean validateImpl(ExternalNetworkMqttUser user, NetworkMqttSession } @Override - protected void handleNoMatchedSubscribers( - ExternalNetworkMqttUser user, - NetworkMqttSession session, - Publish publish) { + protected void handleNoMatchedSubscribers(ExternalNetworkMqttUser user, NetworkMqttSession session, Publish publish) { super.handleNoMatchedSubscribers(user, session, publish); var reasonCode = PublishReceivedReasonCode.NO_MATCHING_SUBSCRIBERS; updateSessionState(session, publish, reasonCode); sendFeedback( - user, messageOutFactoryService - .resolveFactory(user) - .newPublishReceived(publish.messageId(), reasonCode)); + user, + messageOutFactoryService + .resolveFactory(user) + .newPublishReceived(publish.messageId(), reasonCode)); } @Override @@ -90,9 +95,10 @@ protected void handleSuccess( var reasonCode = PublishReceivedReasonCode.SUCCESS; updateSessionState(session, publish, reasonCode); sendFeedback( - user, messageOutFactoryService - .resolveFactory(user) - .newPublishReceived(publish.messageId(), PublishReceivedReasonCode.SUCCESS)); + user, + messageOutFactoryService + .resolveFactory(user) + .newPublishReceived(publish.messageId(), PublishReceivedReasonCode.SUCCESS)); } private void updateSessionState(NetworkMqttSession session, Publish publish, PublishReceivedReasonCode reasonCode) { @@ -118,22 +124,25 @@ protected void handleError( MessageTacker messageTacker = session.inMessageTracker(); messageTacker.update(messageId, MqttMessageType.PUBLISH, reasonCode); - sendFeedback(user, session, messageOutFactoryService - .resolveFactory(user) - .newPublishReceived(messageId, reasonCode), messageId); + sendFeedback( + user, + session, + messageOutFactoryService + .resolveFactory(user) + .newPublishReceived(messageId, reasonCode), + messageId); } - private void handleDuplicated( - ExternalNetworkMqttUser user, - int messageId, - TrackedMessageMeta alreadyInProcess) { + private void handleDuplicated(ExternalNetworkMqttUser user, int messageId, TrackedMessageMeta alreadyInProcess) { PublishReceivedReasonCode reasonCode = PublishReceivedReasonCode.SUCCESS; if (alreadyInProcess.reasonCode() instanceof PublishReceivedReasonCode receivedReasonCode) { reasonCode = receivedReasonCode; } - sendFeedback(user, messageOutFactoryService - .resolveFactory(user) - .newPublishReceived(messageId, reasonCode)); + sendFeedback( + user, + messageOutFactoryService + .resolveFactory(user) + .newPublishReceived(messageId, reasonCode)); } private void handleMessageIdIsInUse(ExternalNetworkMqttUser user, int messageId) { @@ -154,7 +163,10 @@ private boolean handleReceivedTrackableMessage(MqttUser user, MqttSession sessio } if (messageMeta.messageType() != MqttMessageType.PUBLISH) { - log.warning(networkUser.clientId(), messageMeta, messageId, + log.warning( + networkUser.clientId(), + messageMeta, + messageId, "[%s] Not expected tracked message meta:[%s] for messageId:[%d]"::formatted); return true; } else if (!(message instanceof PublishReleaseMqttInMessage release)) { @@ -162,10 +174,7 @@ private boolean handleReceivedTrackableMessage(MqttUser user, MqttSession sessio return true; } - messageTacker.update( - messageId, - MqttMessageType.PUBLISH_COMPLETE, - PublishCompletedReasonCode.SUCCESS); + messageTacker.update(messageId, MqttMessageType.PUBLISH_COMPLETE, PublishCompletedReasonCode.SUCCESS); MqttOutMessage response = messageOutFactoryService .resolveFactory(networkUser) diff --git a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/TrackableMqttPublishInMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/TrackableMqttPublishInMessageHandler.java index 9b768a65..57e0b1a1 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/TrackableMqttPublishInMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/TrackableMqttPublishInMessageHandler.java @@ -11,17 +11,24 @@ import javasabr.mqtt.network.user.NetworkMqttUser; import javasabr.mqtt.service.MessageOutFactoryService; import javasabr.mqtt.service.PublishDeliveringService; +import javasabr.mqtt.service.RetainMessageService; import javasabr.mqtt.service.SubscriptionService; -public abstract class TrackableMqttPublishInMessageHandler - extends AbstractMqttPublishInMessageHandler { +public abstract class TrackableMqttPublishInMessageHandler extends + AbstractMqttPublishInMessageHandler { public TrackableMqttPublishInMessageHandler( Class expectedClientType, SubscriptionService subscriptionService, PublishDeliveringService publishDeliveringService, - MessageOutFactoryService messageOutFactoryService) { - super(expectedClientType, subscriptionService, publishDeliveringService, messageOutFactoryService); + MessageOutFactoryService messageOutFactoryService, + RetainMessageService retainMessageService) { + super( + expectedClientType, + subscriptionService, + publishDeliveringService, + messageOutFactoryService, + retainMessageService); } @Override diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/IntegrationServiceSpecification.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/IntegrationServiceSpecification.groovy index c44fc2bb..c9826a11 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/IntegrationServiceSpecification.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/IntegrationServiceSpecification.groovy @@ -11,6 +11,7 @@ import javasabr.mqtt.network.handler.NetworkMqttUserReleaseHandler import javasabr.mqtt.service.impl.DefaultMessageOutFactoryService import javasabr.mqtt.service.impl.DefaultPublishDeliveringService import javasabr.mqtt.service.impl.DefaultPublishReceivingService +import javasabr.mqtt.service.impl.DefaultRetainMessageService import javasabr.mqtt.service.impl.DefaultTopicService import javasabr.mqtt.service.impl.InMemorySubscriptionService import javasabr.mqtt.service.message.handler.impl.PublishReleaseMqttInMessageHandler @@ -39,7 +40,7 @@ abstract class IntegrationServiceSpecification extends Specification { def testPayload = "testpayload".getBytes(StandardCharsets.UTF_8) @Shared - def clientIdGenerator = new AtomicInteger(); + def clientIdGenerator = new AtomicInteger() @Shared def defaultTopicService = new DefaultTopicService() @@ -58,13 +59,17 @@ abstract class IntegrationServiceSpecification extends Specification { ]) @Shared - def defaultSubscriptionService = new InMemorySubscriptionService(defaultPublishDeliveringService) + def defaultRetainMessageService = new DefaultRetainMessageService(defaultPublishDeliveringService) + + @Shared + def defaultSubscriptionService = new InMemorySubscriptionService(defaultRetainMessageService) @Shared def qos0MqttPublishInMessageHandler = new Qos0MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService); + defaultMessageOutFactoryService, + defaultRetainMessageService) @Shared def publishReceivingService = new DefaultPublishReceivingService([ @@ -72,21 +77,23 @@ abstract class IntegrationServiceSpecification extends Specification { new Qos1MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService), + defaultMessageOutFactoryService, + defaultRetainMessageService), new Qos2MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) ]) @Shared - def defaultPublishReleaseMqttInMessageHandler = new PublishReleaseMqttInMessageHandler(defaultMessageOutFactoryService); + def defaultPublishReleaseMqttInMessageHandler = new PublishReleaseMqttInMessageHandler(defaultMessageOutFactoryService) @Shared def defaultBufferAllocator = new DefaultBufferAllocator(SimpleServerNetworkConfig.builder().build()) @Shared - def defaultMqttSessionService = new InMemoryMqttSessionService(60_000); + def defaultMqttSessionService = new InMemoryMqttSessionService(60_000) @Shared def defaultExternalServerConnectionConfig = new MqttServerConnectionConfig( diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy index 0dbbd7c9..05f0882a 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy @@ -361,20 +361,14 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true)) and: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + defaultRetainMessageService.retainMessage(publishWithRetain, subscription) and: def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") - defaultPublishDeliveringService.startDelivering(publishWithoutRetain, new SingleSubscriber(mqttUser, subscription)) + defaultRetainMessageService.retainMessage(publishWithoutRetain, subscription) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: - def firstPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) - firstPublishMessage.payload() == publishWithRetain.payload() - and: - def secondPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) - secondPublishMessage.payload() == publishWithoutRetain.payload() - and: def thirdPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) thirdPublishMessage.payload() == publishWithRetain.payload() and: @@ -475,17 +469,13 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { def subscriptions = Array.of(subscription) and: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + defaultRetainMessageService.retainMessage(publishWithRetain, subscription) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: - def firstSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) - firstSentMessage.payload() == publishWithRetain.payload() - firstSentMessage.retain() - and: - def thirdSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) - thirdSentMessage.payload() == publishWithRetain.payload() - !thirdSentMessage.retain() + def sentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + sentMessage.payload() == publishWithRetain.payload() + !sentMessage.retain() and: mqttUser.isEmpty() } @@ -506,13 +496,9 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { when: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + defaultRetainMessageService.retainMessage(publishWithRetain, subscription) defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: - def firstSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) - firstSentMessage.payload() == publishWithRetain.payload() - firstSentMessage.retain() - and: def secondSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) secondSentMessage.payload() == publishWithRetain.payload() secondSentMessage.retain() diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/UnsubscribeMqttInMessageHandlerTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/UnsubscribeMqttInMessageHandlerTest.groovy index 09a94c0a..febfe532 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/UnsubscribeMqttInMessageHandlerTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/message/handler/impl/UnsubscribeMqttInMessageHandlerTest.groovy @@ -69,7 +69,7 @@ class UnsubscribeMqttInMessageHandlerTest extends IntegrationServiceSpecificatio def "should response with expected results"() { given: def mqttConnection = mockedExternalConnection(MqttVersion.MQTT_5) - def subscriptionService = new InMemorySubscriptionService(defaultPublishDeliveringService) + def subscriptionService = new InMemorySubscriptionService(defaultRetainMessageService) def messageHandler = new UnsubscribeMqttInMessageHandler( subscriptionService, defaultMessageOutFactoryService, diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos0MqttPublishInMessageHandlerTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos0MqttPublishInMessageHandlerTest.groovy index fd2d8600..349f9fe4 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos0MqttPublishInMessageHandlerTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos0MqttPublishInMessageHandlerTest.groovy @@ -15,7 +15,8 @@ class Qos0MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos0MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def subscriber1 = mockedExternalConnection(MqttVersion.MQTT_5) def subscriber2 = mockedExternalConnection(MqttVersion.MQTT_5) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) @@ -50,7 +51,8 @@ class Qos0MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos0MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) def user = publisher.user() as TestExternalNetworkMqttUser def topicName = defaultTopicService.createTopicName(user, "Qos0MqttPublishInMessageHandlerTest/2") diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishInMessageHandlerTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishInMessageHandlerTest.groovy index d86426e6..75df3392 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishInMessageHandlerTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishInMessageHandlerTest.groovy @@ -23,7 +23,8 @@ class Qos1MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos1MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def subscriber1 = mockedExternalConnection(MqttVersion.MQTT_5) def subscriber2 = mockedExternalConnection(MqttVersion.MQTT_5) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) @@ -68,7 +69,8 @@ class Qos1MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos1MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) def user = publisher.user() as TestExternalNetworkMqttUser def topicName = defaultTopicService.createTopicName(user, "Qos1MqttPublishInMessageHandlerTest/2") @@ -92,7 +94,8 @@ class Qos1MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos1MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) def user = publisher.user() as TestExternalNetworkMqttUser def topicName = defaultTopicService.createTopicName(user, "Qos1MqttPublishInMessageHandlerTest/3") @@ -115,7 +118,8 @@ class Qos1MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos1MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) def user = publisher.user() as TestExternalNetworkMqttUser def topicName = defaultTopicService.createTopicName(user, "Qos1MqttPublishInMessageHandlerTest/4") @@ -141,7 +145,8 @@ class Qos1MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos1MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) def user = publisher.user() as TestExternalNetworkMqttUser def topicName = defaultTopicService.createTopicName(user, "Qos1MqttPublishInMessageHandlerTest/5") diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishInMessageHandlerTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishInMessageHandlerTest.groovy index fdae20e6..2e6ea731 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishInMessageHandlerTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos2MqttPublishInMessageHandlerTest.groovy @@ -27,7 +27,8 @@ class Qos2MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos2MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def subscriber1 = mockedExternalConnection(MqttVersion.MQTT_5) def subscriber2 = mockedExternalConnection(MqttVersion.MQTT_5) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) @@ -87,7 +88,8 @@ class Qos2MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos2MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) def user = publisher.user() as TestExternalNetworkMqttUser def topicName = defaultTopicService.createTopicName(user, "Qos2MqttPublishInMessageHandlerTest/2") @@ -126,7 +128,8 @@ class Qos2MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos2MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) def user = publisher.user() as TestExternalNetworkMqttUser def topicName = defaultTopicService.createTopicName(user, "Qos2MqttPublishInMessageHandlerTest/3") @@ -149,7 +152,8 @@ class Qos2MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos2MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) def user = publisher.user() as TestExternalNetworkMqttUser def topicName = defaultTopicService.createTopicName(user, "Qos2MqttPublishInMessageHandlerTest/4") @@ -175,7 +179,8 @@ class Qos2MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos2MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) def user = publisher.user() as TestExternalNetworkMqttUser def topicName = defaultTopicService.createTopicName(user, "Qos2MqttPublishInMessageHandlerTest/5") @@ -203,7 +208,8 @@ class Qos2MqttPublishInMessageHandlerTest extends QosMqttPublishInMessageHandler def publishInHandler = new Qos2MqttPublishInMessageHandler( defaultSubscriptionService, defaultPublishDeliveringService, - defaultMessageOutFactoryService) + defaultMessageOutFactoryService, + defaultRetainMessageService) def publisher = mockedExternalConnection(MqttVersion.MQTT_5) def user = publisher.user() as TestExternalNetworkMqttUser def topicName = defaultTopicService.createTopicName(user, "Qos2MqttPublishInMessageHandlerTest/5") diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java index da9317da..ea45b880 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/ConcurrentRetainedMessageTree.java @@ -1,6 +1,5 @@ package javasabr.mqtt.model.topic.tree; -import java.util.function.Function; import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.rlib.collections.array.Array; From 6c16962b27bc0740978857738e260b550e27ece9 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Tue, 9 Dec 2025 01:53:49 +0100 Subject: [PATCH 23/38] [broker-30] Rework retainAsPublished handling --- .../mqtt/service/RetainMessageService.java | 2 +- .../impl/DefaultRetainMessageService.java | 11 +-- .../AbstractMqttPublishInMessageHandler.java | 3 +- .../InMemorySubscriptionServiceTest.groovy | 26 +++--- .../topic/tree/RetainedMessageTreeTest.groovy | 84 +++++++++---------- .../subscription/TestPublishFactory.groovy | 7 +- 6 files changed, 67 insertions(+), 66 deletions(-) diff --git a/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java b/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java index b8eaee53..9debf1dc 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java @@ -8,7 +8,7 @@ public interface RetainMessageService { - void retainMessage(Publish publish, Subscription subscription); + void retainMessage(Publish publish); Array deliverRetainedMessages(SingleSubscriber subscriber); } diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java index 50b252ea..c77cb228 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java @@ -1,6 +1,5 @@ package javasabr.mqtt.service.impl; -import java.util.function.Function; import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.subscriber.SingleSubscriber; import javasabr.mqtt.model.subscription.Subscription; @@ -25,20 +24,22 @@ public DefaultRetainMessageService(PublishDeliveringService defaultPublishDelive } @Override - public void retainMessage(Publish publish, Subscription subscription) { + public void retainMessage(Publish publish) { if (publish.retained()) { - boolean retainAsPublished = subscription.retainAsPublished(); - Function transformer = retainAsPublished ? Function.identity() : Publish::withoutRetain; - retainedMessageTree.retainMessage(transformer.apply(publish)); + retainedMessageTree.retainMessage(publish); } } @Override public Array deliverRetainedMessages(SingleSubscriber subscriber) { Subscription subscription = subscriber.subscription(); + boolean retainAsPublished = subscription.retainAsPublished(); Array retainedMessages = retainedMessageTree.getRetainedMessage(subscription.topicFilter()); MutableArray result = MutableArray.ofType(PublishHandlingResult.class); for (Publish message : retainedMessages) { + if (!retainAsPublished) { + message = message.withoutRetain(); + } result.add(defaultPublishDeliveringService.startDelivering(message, subscriber)); } return Array.copyOf(result); diff --git a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishInMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishInMessageHandler.java index 68819b53..4487be2d 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishInMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishInMessageHandler.java @@ -54,6 +54,8 @@ protected boolean validateImpl(U user, NetworkMqttSession session, Publish publi } protected void handleImpl(U user, NetworkMqttSession session, Publish publish) { + retainMessageService.retainMessage(publish); + TopicName topicName = publish.topicName(); Array subscribers = subscriptionService.findSubscribers(topicName); if (subscribers.isEmpty()) { @@ -105,7 +107,6 @@ protected PublishHandlingResult checkSubscriber( } protected void startDelivering(Publish publish, SingleSubscriber subscriber) { - retainMessageService.retainMessage(publish, subscriber.subscription()); publishDeliveringService.startDelivering(publish, subscriber); } diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy index 05f0882a..8eefe0ec 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy @@ -360,11 +360,11 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true, true)) and: - def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultRetainMessageService.retainMessage(publishWithRetain, subscription) + def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") + defaultRetainMessageService.retainMessage(publishWithRetain) and: - def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") - defaultRetainMessageService.retainMessage(publishWithoutRetain, subscription) + def publishWithoutRetain = TestPublishFactory.createPublishWithoutRetain("topic/filter/1", "payload2") + defaultRetainMessageService.retainMessage(publishWithoutRetain) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) @@ -389,7 +389,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true) def subscriptions = Array.of(subscription) and: - def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") + def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) @@ -436,10 +436,10 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true, true)) and: - def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") + def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) and: - def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") + def publishWithoutRetain = TestPublishFactory.createPublishWithoutRetain("topic/filter/1", "payload2") defaultPublishDeliveringService.startDelivering(publishWithoutRetain, new SingleSubscriber(mqttUser, subscription)) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) @@ -468,8 +468,8 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { false) def subscriptions = Array.of(subscription) and: - def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultRetainMessageService.retainMessage(publishWithRetain, subscription) + def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") + defaultRetainMessageService.retainMessage(publishWithRetain) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: @@ -495,8 +495,8 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { def subscriptions = Array.of(subscription) when: - def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultRetainMessageService.retainMessage(publishWithRetain, subscription) + def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") + defaultRetainMessageService.retainMessage(publishWithRetain) defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: def secondSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) @@ -520,7 +520,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true) def subscriptions = Array.of(subscription) and: - def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") + def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) @@ -580,7 +580,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true) def subscriptions = Array.of(subscription) and: - def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") + def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) when: defaultSubscriptionService.subscribe(anotherUser, mqttUser.session(), subscriptions) diff --git a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy index 02dbca81..a006626b 100644 --- a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy +++ b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy @@ -4,17 +4,17 @@ import javasabr.mqtt.model.publishing.Publish import javasabr.mqtt.model.topic.TopicFilter import javasabr.mqtt.test.support.UnitSpecification -import static javasabr.mqtt.model.subscription.TestPublishFactory.makePublish +import static javasabr.mqtt.model.subscription.TestPublishFactory.createPublish class RetainedMessageTreeTest extends UnitSpecification { def "should fetch retained messages by topic filter"( - List messages, + List messages, String topicFilter, - List expectedMessages) { + List expectedMessages) { given: ConcurrentRetainedMessageTree retainedMessageTree = new ConcurrentRetainedMessageTree(); - messages.eachWithIndex { Publish message, int i -> + messages.collect { createPublish(it) }.eachWithIndex { Publish message, int i -> retainedMessageTree.retainMessage(message) } when: @@ -23,7 +23,7 @@ class RetainedMessageTreeTest extends UnitSpecification { then: retainedMessages.size() == expectedMessages.size() for (int i = 0; i < retainedMessages.size(); i++) { - assert retainedMessages.get(i).topicName() == expectedMessages.get(i).topicName() + assert retainedMessages[i].topicName().rawTopic() == expectedMessages[i] } where: topicFilter << [ @@ -35,63 +35,63 @@ class RetainedMessageTreeTest extends UnitSpecification { ] messages << [ [ - makePublish("/topic/segment1"), - makePublish("/topic/segment2"), - makePublish("/topic/segment1/segment2"), - makePublish("/topic/"), - makePublish("/topic") + "/topic/segment1", + "/topic/segment2", + "/topic/segment1/segment2", + "/topic/", + "/topic" ], [ - makePublish("/topic/segment1"), - makePublish("/topic/segment2"), - makePublish("/topic/segment1/segment2"), - makePublish("/topic/"), - makePublish("/topic/segment2"), - makePublish("/"), - makePublish("/topic/segment2/segment1") + "/topic/segment1", + "/topic/segment2", + "/topic/segment1/segment2", + "/topic/", + "/topic/segment2", + "/", + "/topic/segment2/segment1" ], [ - makePublish("/topic/segment1"), - makePublish("/topic/segment2"), - makePublish("/topic/segment3"), - makePublish("/topic/segment3"), - makePublish("/topic/segment3"), - makePublish("/topic/segment3") + "/topic/segment1", + "/topic/segment2", + "/topic/segment3", + "/topic/segment3", + "/topic/segment3", + "/topic/segment3" ], [ - makePublish("/topic/segment1"), - makePublish("/topic/segment2"), - makePublish("/topic/segment1/segment2"), - makePublish("/topic/segment500/segment2"), - makePublish("/topic/"), - makePublish("/topic") + "/topic/segment1", + "/topic/segment2", + "/topic/segment1/segment2", + "/topic/segment500/segment2", + "/topic/", + "/topic" ], [ - makePublish("/topic1/segment1"), - makePublish("/topic/segment2"), - makePublish("/topic2/segment1/segment2"), - makePublish("/topic/segment3"), - makePublish("/topic/segment1/segment2") + "/topic1/segment1", + "/topic/segment2", + "/topic2/segment1/segment2", + "/topic/segment3", + "/topic/segment1/segment2" ] ] expectedMessages << [ [ - makePublish("/topic/segment1") + "/topic/segment1" ], [ - makePublish("/topic/segment2") + "/topic/segment2" ], [ - makePublish("/topic/segment3") + "/topic/segment3" ], [ - makePublish("/topic/segment1/segment2"), - makePublish("/topic/segment500/segment2") + "/topic/segment1/segment2", + "/topic/segment500/segment2" ], [ - makePublish("/topic/segment2"), - makePublish("/topic/segment3"), - makePublish("/topic/segment1/segment2") + "/topic/segment2", + "/topic/segment3", + "/topic/segment1/segment2" ] ] } diff --git a/model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestPublishFactory.groovy b/model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestPublishFactory.groovy index 6e99a97e..3d705784 100644 --- a/model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestPublishFactory.groovy +++ b/model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestPublishFactory.groovy @@ -11,7 +11,7 @@ import static java.nio.charset.StandardCharsets.UTF_8 class TestPublishFactory { - static def makePublish(String topicName) { + static def createPublish(String topicName) { return new Publish( 1, QoS.AT_MOST_ONCE, @@ -29,7 +29,7 @@ class TestPublishFactory { Array.of()); } - static def makePublishWithRetain(String topicName, String payload) { + static def createPublishWithRetain(String topicName, String payload) { return new Publish( 1, QoS.AT_MOST_ONCE, @@ -47,7 +47,7 @@ class TestPublishFactory { Array.of()); } - static def makePublishWithoutRetain(String topicName, String payload) { + static def createPublishWithoutRetain(String topicName, String payload) { return new Publish( 1, QoS.AT_MOST_ONCE, @@ -64,5 +64,4 @@ class TestPublishFactory { PayloadFormat.UTF8_STRING, Array.of()); } - } From 31ff664058b877ececbe4e4a6a88e20dc4f2f4af Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:55:13 +0100 Subject: [PATCH 24/38] [broker-30] Fix formatting --- .../application/config/MqttBrokerSpringConfig.java | 3 +-- .../mqtt/service/PublishDeliveringService.java | 1 - .../impl/DefaultPublishDeliveringService.java | 1 + .../service/impl/InMemorySubscriptionService.java | 7 +++---- .../impl/Qos1MqttPublishInMessageHandler.java | 12 +++++++----- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java index e5f7ea38..056ba91e 100644 --- a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java +++ b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java @@ -343,8 +343,7 @@ NetworkMqttUserFactory externalClientFactory(NetworkMqttUserReleaseHandler exter MqttConnectionFactory externalConnectionFactory( MqttServerConnectionConfig externalServerConnectionConfig, NetworkMqttUserFactory mqttUserFactory, - @Value("${mqtt.external.connection.max.packets.by.read:100}") - int maxPacketsByRead) { + @Value("${mqtt.external.connection.max.packets.by.read:100}") int maxPacketsByRead) { return new DefaultMqttConnectionFactory(externalServerConnectionConfig, mqttUserFactory, maxPacketsByRead); } diff --git a/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java b/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java index 8b7c8d39..8110b7bf 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java @@ -3,7 +3,6 @@ import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.subscriber.SingleSubscriber; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; -import javasabr.rlib.collections.array.Array; public interface PublishDeliveringService { diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java index 59b6fcce..8e0a7529 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultPublishDeliveringService.java @@ -39,6 +39,7 @@ public DefaultPublishDeliveringService( } handlers[qos.level()] = knownPublishOutHandler; } + this.publishOutMessageHandlers = handlers; log.info(publishOutMessageHandlers, DefaultPublishDeliveringService::buildServiceDescription); } diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index ef8b5b90..6f8cc047 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -83,13 +83,12 @@ private SubscribeAckReasonCode addSubscription(MqttUser user, MqttSession sessio activeSubscriptions.remove(previous.subscription()); } QoS subscriptionQoS = subscription.qos(); - if (subscriptionQoS.ordinal() <= 2 && (subscription.retainHandling() == SEND || - (subscription.retainHandling() == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previous == null))) { + if (subscriptionQoS.ordinal() <= 2 && (subscription.retainHandling() == SEND || ( + subscription.retainHandling() == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previous == null))) { sendRetainedMessages(user, subscription); } activeSubscriptions.add(subscription); - return subscriptionQoS - .subscribeAckReasonCode(); + return subscriptionQoS.subscribeAckReasonCode(); } @Override diff --git a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishInMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishInMessageHandler.java index 62633a75..b5b76333 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishInMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos1MqttPublishInMessageHandler.java @@ -27,7 +27,12 @@ public Qos1MqttPublishInMessageHandler( PublishDeliveringService publishDeliveringService, MessageOutFactoryService messageOutFactoryService, RetainMessageService retainMessageService) { - super(ExternalNetworkMqttUser.class, subscriptionService, publishDeliveringService, messageOutFactoryService, retainMessageService); + super( + ExternalNetworkMqttUser.class, + subscriptionService, + publishDeliveringService, + messageOutFactoryService, + retainMessageService); } @Override @@ -55,10 +60,7 @@ protected boolean validateImpl(ExternalNetworkMqttUser user, NetworkMqttSession } @Override - protected void handleNoMatchedSubscribers( - ExternalNetworkMqttUser user, - NetworkMqttSession session, - Publish publish) { + protected void handleNoMatchedSubscribers(ExternalNetworkMqttUser user, NetworkMqttSession session, Publish publish) { super.handleNoMatchedSubscribers(user, session, publish); int messageId = publish.messageId(); MqttOutMessage response = messageOutFactoryService From 8191bc86789422c6b390a49ed6616f49bdd6a103 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:43:44 +0100 Subject: [PATCH 25/38] [broker-30] Improve RetainedMessageTreeTest --- .../topic/tree/RetainedMessageTreeTest.groovy | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy index a006626b..0c2bf382 100644 --- a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy +++ b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy @@ -1,32 +1,28 @@ package javasabr.mqtt.model.topic.tree -import javasabr.mqtt.model.publishing.Publish +import javasabr.mqtt.model.subscription.TestPublishFactory import javasabr.mqtt.model.topic.TopicFilter import javasabr.mqtt.test.support.UnitSpecification -import static javasabr.mqtt.model.subscription.TestPublishFactory.createPublish - class RetainedMessageTreeTest extends UnitSpecification { def "should fetch retained messages by topic filter"( List messages, - String topicFilter, + String rawTopicFilter, List expectedMessages) { given: ConcurrentRetainedMessageTree retainedMessageTree = new ConcurrentRetainedMessageTree(); - messages.collect { createPublish(it) }.eachWithIndex { Publish message, int i -> - retainedMessageTree.retainMessage(message) - } + messages.collect(TestPublishFactory::createPublish).each(retainedMessageTree::retainMessage) + def topicFilter = TopicFilter.valueOf(rawTopicFilter) when: - def retainedMessages = retainedMessageTree.getRetainedMessage(TopicFilter.valueOf(topicFilter)) - .collect { it } + def retainedMessages = retainedMessageTree.getRetainedMessage(topicFilter) then: retainedMessages.size() == expectedMessages.size() - for (int i = 0; i < retainedMessages.size(); i++) { - assert retainedMessages[i].topicName().rawTopic() == expectedMessages[i] + verifyEach(retainedMessages) { publish, index -> + publish.topicName().rawTopic() == expectedMessages[index] } where: - topicFilter << [ + rawTopicFilter << [ "/topic/segment1", "/topic/segment2", "/topic/segment3", From c7c72d355bd0cd09cd513ff3dcd19f99440c8ccd Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:23:26 +0100 Subject: [PATCH 26/38] [broker-30] Improve code readability --- .../broker/application/config/MqttBrokerSpringConfig.java | 3 +-- .../mqtt/service/impl/InMemorySubscriptionService.java | 7 +++++-- model/src/main/java/javasabr/mqtt/model/QoS.java | 7 +++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java index 056ba91e..eb114842 100644 --- a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java +++ b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java @@ -95,8 +95,7 @@ CredentialSource credentialSource( @Bean AuthenticationService authenticationService( CredentialSource credentialSource, - @Value("${authentication.allow.anonymous:false}") - boolean allowAnonymousAuth) { + @Value("${authentication.allow.anonymous:false}") boolean allowAnonymousAuth) { return new SimpleAuthenticationService(credentialSource, allowAnonymousAuth); } diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index 6f8cc047..f8daf635 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -8,6 +8,7 @@ import javasabr.mqtt.model.MqttClientConnectionConfig; import javasabr.mqtt.model.MqttUser; import javasabr.mqtt.model.QoS; +import javasabr.mqtt.model.SubscribeRetainHandling; import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode; import javasabr.mqtt.model.reason.code.UnsubscribeAckReasonCode; import javasabr.mqtt.model.session.ActiveSubscriptions; @@ -83,8 +84,10 @@ private SubscribeAckReasonCode addSubscription(MqttUser user, MqttSession sessio activeSubscriptions.remove(previous.subscription()); } QoS subscriptionQoS = subscription.qos(); - if (subscriptionQoS.ordinal() <= 2 && (subscription.retainHandling() == SEND || ( - subscription.retainHandling() == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previous == null))) { + SubscribeRetainHandling retainHandling = subscription.retainHandling(); + boolean isRetainHandlingSatisfied = + retainHandling == SEND || (retainHandling == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previous == null); + if (subscriptionQoS.isValid() && isRetainHandlingSatisfied) { sendRetainedMessages(user, subscription); } activeSubscriptions.add(subscription); diff --git a/model/src/main/java/javasabr/mqtt/model/QoS.java b/model/src/main/java/javasabr/mqtt/model/QoS.java index 8da7dd94..d8159f69 100644 --- a/model/src/main/java/javasabr/mqtt/model/QoS.java +++ b/model/src/main/java/javasabr/mqtt/model/QoS.java @@ -19,8 +19,7 @@ public enum QoS implements NumberedEnum { EXACTLY_ONCE(2, SubscribeAckReasonCode.GRANTED_QOS_2), INVALID(3, SubscribeAckReasonCode.IMPLEMENTATION_SPECIFIC_ERROR); - private static final NumberedEnumMap NUMBERED_MAP = - new NumberedEnumMap<>(QoS.class); + private static final NumberedEnumMap NUMBERED_MAP = new NumberedEnumMap<>(QoS.class); public static QoS ofCode(int level) { return NUMBERED_MAP.resolve(level, QoS.INVALID); @@ -45,4 +44,8 @@ public boolean isLowerThan(QoS another) { public boolean isHigherThan(QoS another) { return level > another.level; } + + public boolean isValid() { + return this != INVALID; + } } From 8a23e97ae9d3a57bbe6868ee1b37d5c7612bfcb8 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:50:08 +0100 Subject: [PATCH 27/38] [broker-30] Extract creation of SingleSubscriber to SubscriptionService --- .../impl/InMemorySubscriptionService.java | 21 +++--- .../InMemorySubscriptionServiceTest.groovy | 18 ++--- .../tree/ConcurrentSubscriberTree.java | 5 +- .../model/subscriber/tree/SubscriberNode.java | 7 +- .../subscriber/tree/SubscriberTreeBase.java | 15 ++--- .../topic/tree/RetainedMessageTreeTest.groovy | 2 +- .../topic/tree/SubscriberTreeTest.groovy | 66 +++++++++++-------- .../subscription/TestPublishFactory.groovy | 6 +- 8 files changed, 74 insertions(+), 66 deletions(-) diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index f8daf635..444913bb 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -79,16 +79,17 @@ private SubscribeAckReasonCode addSubscription(MqttUser user, MqttSession sessio return SubscribeAckReasonCode.WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED; } ActiveSubscriptions activeSubscriptions = session.activeSubscriptions(); - SingleSubscriber previous = subscriberTree.subscribe(user, subscription); - if (previous != null) { - activeSubscriptions.remove(previous.subscription()); + SingleSubscriber newSubscriber = new SingleSubscriber(user, subscription); + SingleSubscriber previousSubscriber = subscriberTree.subscribe(newSubscriber); + if (previousSubscriber != null) { + activeSubscriptions.remove(previousSubscriber.subscription()); } QoS subscriptionQoS = subscription.qos(); SubscribeRetainHandling retainHandling = subscription.retainHandling(); boolean isRetainHandlingSatisfied = - retainHandling == SEND || (retainHandling == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previous == null); + retainHandling == SEND || (retainHandling == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previousSubscriber == null); if (subscriptionQoS.isValid() && isRetainHandlingSatisfied) { - sendRetainedMessages(user, subscription); + sendRetainedMessages(newSubscriber); } activeSubscriptions.add(subscription); return subscriptionQoS.subscribeAckReasonCode(); @@ -140,17 +141,17 @@ public void restoreSubscriptions(MqttUser user, MqttSession session) { .activeSubscriptions() .subscriptions(); for (Subscription subscription : subscriptions) { - subscriberTree.subscribe(user, subscription); + SingleSubscriber singleSubscriber = new SingleSubscriber(user, subscription); + subscriberTree.subscribe(singleSubscriber); } } - private void sendRetainedMessages(MqttUser user, Subscription subscription) { + private void sendRetainedMessages(SingleSubscriber singleSubscriber) { int count = 0; - String clientId = user.clientId(); - PublishHandlingResult errorResult = null; - SingleSubscriber singleSubscriber = new SingleSubscriber(user, subscription); + String clientId = singleSubscriber.user().clientId(); var results = retainMessageService.deliverRetainedMessages(singleSubscriber); for (PublishHandlingResult result : results) { + PublishHandlingResult errorResult = null; if (result.error()) { errorResult = result; } else if (result == PublishHandlingResult.SUCCESS) { diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy index 8eefe0ec..75435b6c 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy @@ -360,10 +360,10 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true, true)) and: - def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") defaultRetainMessageService.retainMessage(publishWithRetain) and: - def publishWithoutRetain = TestPublishFactory.createPublishWithoutRetain("topic/filter/1", "payload2") + def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") defaultRetainMessageService.retainMessage(publishWithoutRetain) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) @@ -389,7 +389,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true) def subscriptions = Array.of(subscription) and: - def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) @@ -436,10 +436,10 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true, true)) and: - def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) and: - def publishWithoutRetain = TestPublishFactory.createPublishWithoutRetain("topic/filter/1", "payload2") + def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") defaultPublishDeliveringService.startDelivering(publishWithoutRetain, new SingleSubscriber(mqttUser, subscription)) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) @@ -468,7 +468,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { false) def subscriptions = Array.of(subscription) and: - def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") defaultRetainMessageService.retainMessage(publishWithRetain) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) @@ -495,7 +495,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { def subscriptions = Array.of(subscription) when: - def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") defaultRetainMessageService.retainMessage(publishWithRetain) defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: @@ -520,7 +520,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true) def subscriptions = Array.of(subscription) and: - def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) when: defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) @@ -580,7 +580,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true) def subscriptions = Array.of(subscription) and: - def publishWithRetain = TestPublishFactory.createPublishWithRetain("topic/filter/1", "payload1") + def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) when: defaultSubscriptionService.subscribe(anotherUser, mqttUser.session(), subscriptions) diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/ConcurrentSubscriberTree.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/ConcurrentSubscriberTree.java index 307db58c..1f6d4c74 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/ConcurrentSubscriberTree.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/ConcurrentSubscriberTree.java @@ -2,7 +2,6 @@ import javasabr.mqtt.model.MqttUser; import javasabr.mqtt.model.subscriber.SingleSubscriber; -import javasabr.mqtt.model.subscription.Subscription; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.mqtt.model.topic.TopicName; import javasabr.rlib.collections.array.Array; @@ -22,8 +21,8 @@ public ConcurrentSubscriberTree() { } @Nullable - public SingleSubscriber subscribe(MqttUser user, Subscription subscription) { - return rootNode.subscribe(0, user, subscription, subscription.topicFilter()); + public SingleSubscriber subscribe(SingleSubscriber subscriber) { + return rootNode.subscribe(0, subscriber, subscriber.subscription().topicFilter()); } public boolean unsubscribe(MqttUser user, TopicFilter topicFilter) { diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java index d2698597..2165e875 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java @@ -5,7 +5,6 @@ import javasabr.mqtt.model.MqttUser; import javasabr.mqtt.model.subscriber.SingleSubscriber; import javasabr.mqtt.model.subscriber.Subscriber; -import javasabr.mqtt.model.subscription.Subscription; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.mqtt.model.topic.TopicName; import javasabr.rlib.collections.array.ArrayFactory; @@ -40,12 +39,12 @@ protected Supplier getNodeFactory() { * @return the previous subscription from the same owner */ @Nullable - protected SingleSubscriber subscribe(int level, MqttUser owner, Subscription subscription, TopicFilter topicFilter) { + protected SingleSubscriber subscribe(int level, SingleSubscriber subscriber, TopicFilter topicFilter) { if (level == topicFilter.levelsCount()) { - return addSubscriber(getOrCreateSubscribers(), owner, subscription, topicFilter); + return addSubscriber(getOrCreateSubscribers(), subscriber, topicFilter); } SubscriberNode childNode = getOrCreateChildNode(topicFilter.segment(level)); - return childNode.subscribe(level + 1, owner, subscription, topicFilter); + return childNode.subscribe(level + 1, subscriber, topicFilter); } protected boolean unsubscribe(int level, MqttUser owner, TopicFilter topicFilter) { diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java index 03236958..926a55cb 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java @@ -7,7 +7,6 @@ import javasabr.mqtt.model.subscriber.SharedSubscriber; import javasabr.mqtt.model.subscriber.SingleSubscriber; import javasabr.mqtt.model.subscriber.Subscriber; -import javasabr.mqtt.model.subscription.Subscription; import javasabr.mqtt.model.topic.SharedTopicFilter; import javasabr.mqtt.model.topic.TopicFilter; import javasabr.rlib.collections.array.LockableArray; @@ -27,17 +26,16 @@ abstract class SubscriberTreeBase extends AbstractTrieNode { @Nullable protected static SingleSubscriber addSubscriber( LockableArray subscribers, - MqttUser user, - Subscription subscription, + SingleSubscriber subscriber, TopicFilter topicFilter) { long stamp = subscribers.writeLock(); try { if (topicFilter instanceof SharedTopicFilter stf) { - addSharedSubscriber(subscribers, user, subscription, stf); + addSharedSubscriber(subscribers, subscriber, stf); return null; } else { - SingleSubscriber previous = removePreviousIfExist(subscribers, user); - subscribers.add(new SingleSubscriber(user, subscription)); + SingleSubscriber previous = removePreviousIfExist(subscribers, subscriber.user()); + subscribers.add(subscriber); return previous; } } finally { @@ -58,8 +56,7 @@ private static SingleSubscriber removePreviousIfExist(LockableArray private static void addSharedSubscriber( LockableArray subscribers, - MqttUser user, - Subscription subscription, + SingleSubscriber subscriber, SharedTopicFilter sharedTopicFilter) { String group = sharedTopicFilter.shareName(); @@ -72,7 +69,7 @@ private static void addSharedSubscriber( subscribers.add(sharedSubscriber); } - sharedSubscriber.addSubscriber(new SingleSubscriber(user, subscription)); + sharedSubscriber.addSubscriber(subscriber); } protected static void appendSubscribersTo(MutableArray result, SubscriberNode subscriberNode) { diff --git a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy index 0c2bf382..9a098f5a 100644 --- a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy +++ b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy @@ -12,7 +12,7 @@ class RetainedMessageTreeTest extends UnitSpecification { List expectedMessages) { given: ConcurrentRetainedMessageTree retainedMessageTree = new ConcurrentRetainedMessageTree(); - messages.collect(TestPublishFactory::createPublish).each(retainedMessageTree::retainMessage) + messages.collect(TestPublishFactory::makePublish).each(retainedMessageTree::retainMessage) def topicFilter = TopicFilter.valueOf(rawTopicFilter) when: def retainedMessages = retainedMessageTree.getRetainedMessage(topicFilter) diff --git a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/SubscriberTreeTest.groovy b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/SubscriberTreeTest.groovy index cfb17623..f8ebc0cc 100644 --- a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/SubscriberTreeTest.groovy +++ b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/SubscriberTreeTest.groovy @@ -23,7 +23,7 @@ class SubscriberTreeTest extends UnitSpecification { given: ConcurrentSubscriberTree subscriberTree = new ConcurrentSubscriberTree() subscriptions.eachWithIndex { Subscription subscription, int i -> - subscriberTree.subscribe(users.get(i), subscription) + subscriberTree.subscribe(new SingleSubscriber(users.get(i), subscription)) } when: def found = subscriberTree.matches(TopicName.valueOf(topicName)) @@ -36,6 +36,7 @@ class SubscriberTreeTest extends UnitSpecification { "/topic/segment2", "/topic/segment3" ] + //noinspection GroovyAssignabilityCheck subscriptions << [ [ makeSubscription("/topic/segment1"), @@ -62,6 +63,7 @@ class SubscriberTreeTest extends UnitSpecification { makeSubscription("/topic/segment3") ] ] + //noinspection GroovyAssignabilityCheck users << [ [ makeUser("id1"), @@ -88,6 +90,7 @@ class SubscriberTreeTest extends UnitSpecification { makeUser("id4") ] ] + //noinspection GroovyAssignabilityCheck expectedUsers << [ [ makeUser("id1") @@ -111,7 +114,7 @@ class SubscriberTreeTest extends UnitSpecification { given: ConcurrentSubscriberTree subscriberTree = new ConcurrentSubscriberTree() subscriptions.eachWithIndex { Subscription subscription, int i -> - subscriberTree.subscribe(users.get(i), subscription) + subscriberTree.subscribe(new SingleSubscriber(users.get(i), subscription)) } when: def found = subscriberTree.matches(TopicName.valueOf(topicName)) @@ -124,6 +127,7 @@ class SubscriberTreeTest extends UnitSpecification { "/topic/segment2", "/topic/segment3" ] + //noinspection GroovyAssignabilityCheck subscriptions << [ [ makeSubscription("/topic/segment1"), @@ -156,6 +160,7 @@ class SubscriberTreeTest extends UnitSpecification { makeSubscription("/topic2/+") ] ] + //noinspection GroovyAssignabilityCheck users << [ [ makeUser("id1"), @@ -188,6 +193,7 @@ class SubscriberTreeTest extends UnitSpecification { makeUser("id8") ] ] + //noinspection GroovyAssignabilityCheck expectedUsers << [ [ makeUser("id1"), @@ -216,7 +222,7 @@ class SubscriberTreeTest extends UnitSpecification { given: ConcurrentSubscriberTree subscriberTree = new ConcurrentSubscriberTree() subscriptions.eachWithIndex { Subscription subscription, int i -> - subscriberTree.subscribe(users.get(i), subscription) + subscriberTree.subscribe(new SingleSubscriber(users.get(i), subscription)) } when: def found = subscriberTree.matches(TopicName.valueOf(topicName)) @@ -229,6 +235,7 @@ class SubscriberTreeTest extends UnitSpecification { "/topic/segment3/segment4", "/topic/segment2" ] + //noinspection GroovyAssignabilityCheck subscriptions << [ [ makeSubscription("/topic/segment1/segment2"), @@ -264,6 +271,7 @@ class SubscriberTreeTest extends UnitSpecification { makeSubscription("/topic/segment3/#") ] ] + //noinspection GroovyAssignabilityCheck users << [ [ makeUser("id1"), @@ -299,6 +307,7 @@ class SubscriberTreeTest extends UnitSpecification { makeUser("id9") ] ] + //noinspection GroovyAssignabilityCheck expectedUsers << [ [ makeUser("id1"), @@ -330,7 +339,7 @@ class SubscriberTreeTest extends UnitSpecification { given: ConcurrentSubscriberTree subscriberTree = new ConcurrentSubscriberTree() subscriptions.eachWithIndex { Subscription subscription, int i -> - subscriberTree.subscribe(users.get(i), subscription) + subscriberTree.subscribe(new SingleSubscriber(users.get(i), subscription)) } when: def found = subscriberTree.matches(TopicName.valueOf(topicName)) @@ -342,6 +351,7 @@ class SubscriberTreeTest extends UnitSpecification { "/topic/segment3", "/topic/segment2/" ] + //noinspection GroovyAssignabilityCheck subscriptions << [ [ makeSubscription("/topic/segment1/segment2", 2), @@ -377,6 +387,7 @@ class SubscriberTreeTest extends UnitSpecification { makeSubscription("/topic/#", 0) ] ] + //noinspection GroovyAssignabilityCheck users << [ [ makeUser("id1"), @@ -412,6 +423,7 @@ class SubscriberTreeTest extends UnitSpecification { makeUser("id3") ] ] + //noinspection GroovyAssignabilityCheck expectedSubscribers << [ [ new SingleSubscriber(makeUser("id1"), makeSubscription("/topic/segment1/segment2", 2)), @@ -436,16 +448,16 @@ class SubscriberTreeTest extends UnitSpecification { def group1 = ["id1", "id2", "id3", "id4", "id5"] def group2 = ["id6", "id7", "id8", "id9", "id10"] ConcurrentSubscriberTree subscriberTree = new ConcurrentSubscriberTree() - subscriberTree.subscribe(makeUser("id1"), makeSharedSubscription('$share/group1/topic/name1')) - subscriberTree.subscribe(makeUser("id2"), makeSharedSubscription('$share/group1/topic/name1')) - subscriberTree.subscribe(makeUser("id3"), makeSharedSubscription('$share/group1/topic/name1')) - subscriberTree.subscribe(makeUser("id4"), makeSharedSubscription('$share/group1/topic/name1')) - subscriberTree.subscribe(makeUser("id5"), makeSharedSubscription('$share/group1/topic/name1')) - subscriberTree.subscribe(makeUser("id6"), makeSharedSubscription('$share/group2/topic/name1')) - subscriberTree.subscribe(makeUser("id7"), makeSharedSubscription('$share/group2/topic/name1')) - subscriberTree.subscribe(makeUser("id8"), makeSharedSubscription('$share/group2/topic/name1')) - subscriberTree.subscribe(makeUser("id9"), makeSharedSubscription('$share/group2/topic/name1')) - subscriberTree.subscribe(makeUser("id10"), makeSharedSubscription('$share/group2/topic/name1')) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id1"), makeSharedSubscription('$share/group1/topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id2"), makeSharedSubscription('$share/group1/topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id3"), makeSharedSubscription('$share/group1/topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id4"), makeSharedSubscription('$share/group1/topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id5"), makeSharedSubscription('$share/group1/topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id6"), makeSharedSubscription('$share/group2/topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id7"), makeSharedSubscription('$share/group2/topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id8"), makeSharedSubscription('$share/group2/topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id9"), makeSharedSubscription('$share/group2/topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id10"), makeSharedSubscription('$share/group2/topic/name1'))) when: def matched = subscriberTree .matches(TopicName.valueOf("topic/name1")) @@ -469,9 +481,9 @@ class SubscriberTreeTest extends UnitSpecification { def "should subscribe and unsubscribe simple topic correctly correctly"() { given: ConcurrentSubscriberTree subscriberTree = new ConcurrentSubscriberTree() - subscriberTree.subscribe(makeUser("id1"), makeSubscription('topic/name1')) - subscriberTree.subscribe(makeUser("id2"), makeSubscription('topic/name1')) - subscriberTree.subscribe(makeUser("id3"), makeSubscription('topic/name1')) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id1"), makeSubscription('topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id2"), makeSubscription('topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id3"), makeSubscription('topic/name1'))) when: def matched = subscriberTree .matches(TopicName.valueOf("topic/name1")) @@ -506,9 +518,9 @@ class SubscriberTreeTest extends UnitSpecification { def "should subscribe and unsubscribe shared topic correctly correctly"() { given: ConcurrentSubscriberTree subscriberTree = new ConcurrentSubscriberTree() - subscriberTree.subscribe(makeUser("id1"), makeSharedSubscription('$share/group1/topic/name1')) - subscriberTree.subscribe(makeUser("id2"), makeSharedSubscription('$share/group1/topic/name1')) - subscriberTree.subscribe(makeUser("id3"), makeSharedSubscription('$share/group1/topic/name1')) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id1"), makeSharedSubscription('$share/group1/topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id2"), makeSharedSubscription('$share/group1/topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id3"), makeSharedSubscription('$share/group1/topic/name1'))) when: def matched = subscriberTree .matches(TopicName.valueOf("topic/name1")) @@ -546,10 +558,10 @@ class SubscriberTreeTest extends UnitSpecification { def owner1 = makeUser("id1") def originalSub = makeSubscription('topic/name1') def replacementSub = makeSubscription('topic/name1') - subscriberTree.subscribe(makeUser("id2"), makeSubscription('topic/name1')) - subscriberTree.subscribe(makeUser("id3"), makeSubscription('topic/name1')) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id2"), makeSubscription('topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(makeUser("id3"), makeSubscription('topic/name1'))) when: - def previous = subscriberTree.subscribe(owner1, originalSub) + def previous = subscriberTree.subscribe(new SingleSubscriber(owner1, originalSub)) def matched = subscriberTree .matches(TopicName.valueOf("topic/name1")) .toSet() @@ -557,7 +569,7 @@ class SubscriberTreeTest extends UnitSpecification { matched.size() == 3 previous == null; when: - previous = subscriberTree.subscribe(owner1, replacementSub) + previous = subscriberTree.subscribe(new SingleSubscriber(owner1, replacementSub)) matched = subscriberTree .matches(TopicName.valueOf("topic/name1")) .toSet() @@ -573,8 +585,8 @@ class SubscriberTreeTest extends UnitSpecification { ConcurrentSubscriberTree subscriberTree = new ConcurrentSubscriberTree() def owner1 = makeUser("id1") def owner2 = makeUser("id2") - subscriberTree.subscribe(owner1, makeSharedSubscription('$share/group1/topic/name1')) - subscriberTree.subscribe(owner2, makeSharedSubscription('$share/group1/topic/name1')) + subscriberTree.subscribe(new SingleSubscriber(owner1, makeSharedSubscription('$share/group1/topic/name1'))) + subscriberTree.subscribe(new SingleSubscriber(owner2, makeSharedSubscription('$share/group1/topic/name1'))) when: def matched = subscriberTree .matches(TopicName.valueOf("topic/name1")) @@ -597,7 +609,7 @@ class SubscriberTreeTest extends UnitSpecification { matched.size() == 1 matched.first().user() == owner2 when: - subscriberTree.subscribe(owner1, makeSharedSubscription('$share/group1/topic/name1')) + subscriberTree.subscribe(new SingleSubscriber(owner1, makeSharedSubscription('$share/group1/topic/name1'))) matched = subscriberTree .matches(TopicName.valueOf("topic/name1")) .toSet() diff --git a/model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestPublishFactory.groovy b/model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestPublishFactory.groovy index 3d705784..257ab27d 100644 --- a/model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestPublishFactory.groovy +++ b/model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestPublishFactory.groovy @@ -11,7 +11,7 @@ import static java.nio.charset.StandardCharsets.UTF_8 class TestPublishFactory { - static def createPublish(String topicName) { + static def makePublish(String topicName) { return new Publish( 1, QoS.AT_MOST_ONCE, @@ -29,7 +29,7 @@ class TestPublishFactory { Array.of()); } - static def createPublishWithRetain(String topicName, String payload) { + static def makePublishWithRetain(String topicName, String payload) { return new Publish( 1, QoS.AT_MOST_ONCE, @@ -47,7 +47,7 @@ class TestPublishFactory { Array.of()); } - static def createPublishWithoutRetain(String topicName, String payload) { + static def makePublishWithoutRetain(String topicName, String payload) { return new Publish( 1, QoS.AT_MOST_ONCE, From 57eeb77a9c0e6b0701e16bada0695cace1898c3c Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:02:14 +0100 Subject: [PATCH 28/38] [broker-30] Revert unnecessary changes --- .../mqtt/service/impl/InMemorySubscriptionServiceTest.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy index 75435b6c..f2de3ee0 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy @@ -493,7 +493,6 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true, true) def subscriptions = Array.of(subscription) - when: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") defaultRetainMessageService.retainMessage(publishWithRetain) From 28e73a5c9c02b3d9a280c1dac8b0a036d88e566d Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:15:52 +0100 Subject: [PATCH 29/38] [broker-30] Fix QoS comparison --- .../mqtt/model/subscriber/tree/SubscriberTreeBase.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java index 926a55cb..15b4aa8d 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberTreeBase.java @@ -142,10 +142,8 @@ private static void addOrReplaceIfLowerQos(MutableArray result return; } QoS candidateQos = candidate.qos(); - QoS existedQos = result - .get(found) - .qos(); - if (existedQos.ordinal() < candidateQos.ordinal()) { + QoS existedQos = result.get(found).qos(); + if (existedQos.isLowerThan(candidateQos)) { result.remove(found); result.add(candidate); } From f4a67272e7df10c4c22178659d053b905f0f2d8d Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:49:43 +0100 Subject: [PATCH 30/38] [broker-30] Update tests --- .../InMemorySubscriptionServiceTest.groovy | 85 +++++++++---------- 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy index f2de3ee0..cbfb2582 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy @@ -5,7 +5,6 @@ import javasabr.mqtt.model.QoS import javasabr.mqtt.model.SubscribeRetainHandling import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode import javasabr.mqtt.model.reason.code.UnsubscribeAckReasonCode -import javasabr.mqtt.model.subscriber.SingleSubscriber import javasabr.mqtt.model.subscription.Subscription import javasabr.mqtt.model.subscription.TestPublishFactory import javasabr.mqtt.model.topic.TopicFilter @@ -19,6 +18,9 @@ import javasabr.rlib.collections.array.Array class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { + def retainMessageService = new DefaultRetainMessageService(defaultPublishDeliveringService) + def subscriptionService = new InMemorySubscriptionService(retainMessageService) + def "should subscribe with expected results in default settings"() { given: def serverConfig = defaultExternalServerConnectionConfig @@ -54,7 +56,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true, true)) when: - def result = defaultSubscriptionService + def result = subscriptionService .subscribe(mqttUser, mqttUser.session(), subscriptions) then: result.size() == 4 @@ -109,7 +111,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true, true)) when: - def result = defaultSubscriptionService + def result = subscriptionService .subscribe(mqttUser, mqttUser.session(), subscriptions) then: result.size() == 5 @@ -157,7 +159,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true) def subscriptions = Array.of(sub1, sub2, sub3, sub4) when: - def result = defaultSubscriptionService + def result = subscriptionService .subscribe(mqttUser, mqttUser.session(), subscriptions) then: result.size() == 4 @@ -205,14 +207,14 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { SubscribeRetainHandling.SEND, true, true)) - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) def topicsToUnsubscribe = Array.of( defaultTopicService.createTopicFilter(mqttUser, "topic/filter/1"), defaultTopicService.createTopicFilter(mqttUser, "topic/filter/3"), defaultTopicService.createTopicFilter(mqttUser, "topic/filter/notexist"), defaultTopicService.createTopicFilter(mqttUser, "topic/filter/invalid##")) when: - def result = defaultSubscriptionService + def result = subscriptionService .unsubscribe(mqttUser, mqttUser.session(), topicsToUnsubscribe) then: result.size() == 4 @@ -256,13 +258,13 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { defaultTopicService.createTopicFilter(mqttUser, "topic/filter/1"), defaultTopicService.createTopicFilter(mqttUser, "topic/filter/3")) when: - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) def storedSubscriptions = activeSubscriptions.subscriptions() then: storedSubscriptions.size() == 3 storedSubscriptions == subscriptions when: - defaultSubscriptionService.unsubscribe(mqttUser, mqttUser.session(), topicsToUnsubscribe) + subscriptionService.unsubscribe(mqttUser, mqttUser.session(), topicsToUnsubscribe) storedSubscriptions = activeSubscriptions.subscriptions() then: storedSubscriptions.size() == 1 @@ -318,13 +320,13 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { subscriptions.get(1), subscriptions2.get(1)) when: - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) def storedSubscriptions = activeSubscriptions.subscriptions() then: storedSubscriptions.size() == 3 storedSubscriptions == subscriptions when: - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions2) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions2) storedSubscriptions = activeSubscriptions.subscriptions() then: storedSubscriptions.size() == 3 @@ -361,13 +363,13 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true)) and: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultRetainMessageService.retainMessage(publishWithRetain) + retainMessageService.retainMessage(publishWithRetain) and: def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") - defaultRetainMessageService.retainMessage(publishWithoutRetain) + retainMessageService.retainMessage(publishWithoutRetain) when: - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: def thirdPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) thirdPublishMessage.payload() == publishWithRetain.payload() @@ -390,19 +392,16 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { def subscriptions = Array.of(subscription) and: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + retainMessageService.retainMessage(publishWithRetain) when: - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: def firstSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) firstSentMessage.payload() == publishWithRetain.payload() and: def thirdSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) thirdSentMessage.payload() == publishWithRetain.payload() - and: - def fourthSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) - fourthSentMessage.payload() == publishWithRetain.payload() and: mqttUser.isEmpty() } @@ -437,20 +436,14 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true)) and: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + retainMessageService.retainMessage(publishWithRetain) and: def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") - defaultPublishDeliveringService.startDelivering(publishWithoutRetain, new SingleSubscriber(mqttUser, subscription)) + retainMessageService.retainMessage(publishWithoutRetain) when: - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: - def firstPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) - firstPublishMessage.payload() == publishWithRetain.payload() - and: - def secondPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) - secondPublishMessage.payload() == publishWithoutRetain.payload() - and: mqttUser.isEmpty() } @@ -469,9 +462,9 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { def subscriptions = Array.of(subscription) and: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultRetainMessageService.retainMessage(publishWithRetain) + retainMessageService.retainMessage(publishWithRetain) when: - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: def sentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) sentMessage.payload() == publishWithRetain.payload() @@ -495,8 +488,8 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { def subscriptions = Array.of(subscription) when: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultRetainMessageService.retainMessage(publishWithRetain) - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + retainMessageService.retainMessage(publishWithRetain) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: def secondSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) secondSentMessage.payload() == publishWithRetain.payload() @@ -520,9 +513,9 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { def subscriptions = Array.of(subscription) and: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + retainMessageService.retainMessage(publishWithRetain) when: - defaultSubscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) + subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: mqttUser.isEmpty() } @@ -540,8 +533,8 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { true, true) when: - defaultSubscriptionService.subscribe(expectedUser, expectedUser.session(), Array.of(expectedSubscription)) - def subscribers = defaultSubscriptionService.findSubscribers(TopicName.valueOf("topic")) + subscriptionService.subscribe(expectedUser, expectedUser.session(), Array.of(expectedSubscription)) + def subscribers = subscriptionService.findSubscribers(TopicName.valueOf("topic")) then: !subscribers.isEmpty() with(subscribers[0]) { @@ -549,14 +542,14 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { subscription() == expectedSubscription } when: - defaultSubscriptionService.cleanSubscriptions(expectedUser, expectedUser.session()) - subscribers = defaultSubscriptionService.findSubscribers(TopicName.valueOf("topic")) + subscriptionService.cleanSubscriptions(expectedUser, expectedUser.session()) + subscribers = subscriptionService.findSubscribers(TopicName.valueOf("topic")) then: subscribers.isEmpty() when: - defaultSubscriptionService.restoreSubscriptions(expectedUser, expectedUser.session()) - subscribers = defaultSubscriptionService.findSubscribers(TopicName.valueOf("topic")) + subscriptionService.restoreSubscriptions(expectedUser, expectedUser.session()) + subscribers = subscriptionService.findSubscribers(TopicName.valueOf("topic")) then: !subscribers.isEmpty() with(subscribers[0]) { @@ -564,6 +557,7 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { subscription() == expectedSubscription } } + def "should suppress retained message delivering failure"() { given: def serverConfig = defaultExternalServerConnectionConfig @@ -580,13 +574,10 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { def subscriptions = Array.of(subscription) and: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") - defaultPublishDeliveringService.startDelivering(publishWithRetain, new SingleSubscriber(mqttUser, subscription)) + retainMessageService.retainMessage(publishWithRetain) when: - defaultSubscriptionService.subscribe(anotherUser, mqttUser.session(), subscriptions) + subscriptionService.subscribe(anotherUser, mqttUser.session(), subscriptions) then: - def firstSentMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) - firstSentMessage.payload() == publishWithRetain.payload() - and: mqttUser.isEmpty() } } From 751624f13cc892c3c4b47c5106f6bd09e56ee536 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:55:00 +0100 Subject: [PATCH 31/38] [broker-30] Introduce setRetainedMessage and clearRetainedMessage --- .../mqtt/model/topic/tree/RetainedMessageNode.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java index e4436ee4..18be847e 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java @@ -43,12 +43,24 @@ public void retainMessage(int level, Publish message, TopicName topicName) { var child = getOrCreateChildNode(topicName.segment(level)); boolean isLastLevel = (level + 1 == topicName.levelsCount()); if (isLastLevel) { - child.retainedMessage.set(message.payload().length == 0 ? null : message); + if (message.payload().length == 0) { + child.clearRetainedMessage(); + } else { + child.setRetainedMessage(message); + } } else { child.retainMessage(level + 1, message, topicName); } } + private void setRetainedMessage(Publish value) { + retainedMessage.set(value); + } + + private void clearRetainedMessage() { + retainedMessage.set(null); + } + public void collectRetainedMessages( int level, TopicFilter topicFilter, From 649f57ec69ed5ecea4c368149ffb5f1dd0f0fb4c Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:56:55 +0100 Subject: [PATCH 32/38] [broker-30] Move publish.retained() check to caller --- .../mqtt/service/RetainMessageService.java | 3 ++- .../impl/DefaultRetainMessageService.java | 19 ++++++++++--------- .../AbstractMqttPublishInMessageHandler.java | 5 +++-- .../InMemorySubscriptionServiceTest.groovy | 7 ++----- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java b/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java index 9debf1dc..929f65e8 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java @@ -2,6 +2,7 @@ import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.subscriber.SingleSubscriber; +import javasabr.mqtt.model.subscriber.Subscriber; import javasabr.mqtt.model.subscription.Subscription; import javasabr.mqtt.service.publish.handler.PublishHandlingResult; import javasabr.rlib.collections.array.Array; @@ -10,5 +11,5 @@ public interface RetainMessageService { void retainMessage(Publish publish); - Array deliverRetainedMessages(SingleSubscriber subscriber); + Array deliverRetainedMessages(Subscriber subscriber); } diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java index c77cb228..870c2af9 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java @@ -2,6 +2,7 @@ import javasabr.mqtt.model.publishing.Publish; import javasabr.mqtt.model.subscriber.SingleSubscriber; +import javasabr.mqtt.model.subscriber.Subscriber; import javasabr.mqtt.model.subscription.Subscription; import javasabr.mqtt.model.topic.tree.ConcurrentRetainedMessageTree; import javasabr.mqtt.service.PublishDeliveringService; @@ -15,24 +16,24 @@ @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) public class DefaultRetainMessageService implements RetainMessageService { - PublishDeliveringService defaultPublishDeliveringService; + PublishDeliveringService publishDeliveringService; ConcurrentRetainedMessageTree retainedMessageTree; - public DefaultRetainMessageService(PublishDeliveringService defaultPublishDeliveringService) { - this.defaultPublishDeliveringService = defaultPublishDeliveringService; + public DefaultRetainMessageService(PublishDeliveringService publishDeliveringService) { + this.publishDeliveringService = publishDeliveringService; this.retainedMessageTree = new ConcurrentRetainedMessageTree(); } @Override public void retainMessage(Publish publish) { - if (publish.retained()) { - retainedMessageTree.retainMessage(publish); - } + retainedMessageTree.retainMessage(publish); + } @Override - public Array deliverRetainedMessages(SingleSubscriber subscriber) { - Subscription subscription = subscriber.subscription(); + public Array deliverRetainedMessages(Subscriber subscriber) { + SingleSubscriber singleSubscriber = subscriber.resolveSingle(); + Subscription subscription = singleSubscriber.subscription(); boolean retainAsPublished = subscription.retainAsPublished(); Array retainedMessages = retainedMessageTree.getRetainedMessage(subscription.topicFilter()); MutableArray result = MutableArray.ofType(PublishHandlingResult.class); @@ -40,7 +41,7 @@ public Array deliverRetainedMessages(SingleSubscriber sub if (!retainAsPublished) { message = message.withoutRetain(); } - result.add(defaultPublishDeliveringService.startDelivering(message, subscriber)); + result.add(publishDeliveringService.startDelivering(message, singleSubscriber)); } return Array.copyOf(result); } diff --git a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishInMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishInMessageHandler.java index 4487be2d..a56f51aa 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishInMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/AbstractMqttPublishInMessageHandler.java @@ -54,8 +54,9 @@ protected boolean validateImpl(U user, NetworkMqttSession session, Publish publi } protected void handleImpl(U user, NetworkMqttSession session, Publish publish) { - retainMessageService.retainMessage(publish); - + if (publish.retained()) { + retainMessageService.retainMessage(publish); + } TopicName topicName = publish.topicName(); Array subscribers = subscriptionService.findSubscribers(topicName); if (subscribers.isEmpty()) { diff --git a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy index cbfb2582..344edd52 100644 --- a/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy +++ b/core-service/src/test/groovy/javasabr/mqtt/service/impl/InMemorySubscriptionServiceTest.groovy @@ -364,15 +364,12 @@ class InMemorySubscriptionServiceTest extends IntegrationServiceSpecification { and: def publishWithRetain = TestPublishFactory.makePublishWithRetain("topic/filter/1", "payload1") retainMessageService.retainMessage(publishWithRetain) - and: - def publishWithoutRetain = TestPublishFactory.makePublishWithoutRetain("topic/filter/1", "payload2") - retainMessageService.retainMessage(publishWithoutRetain) when: subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) subscriptionService.subscribe(mqttUser, mqttUser.session(), subscriptions) then: - def thirdPublishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) - thirdPublishMessage.payload() == publishWithRetain.payload() + def publishMessage = mqttUser.nextSentMessage(PublishMqtt5OutMessage) + publishMessage.payload() == publishWithRetain.payload() and: mqttUser.isEmpty() } From b4e1232c5324fe39ad1ea7a7eb9c2a31364ba003 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:58:13 +0100 Subject: [PATCH 33/38] [broker-30] Extract isRetainHandlingSatisfied() method --- .../impl/InMemorySubscriptionService.java | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index 444913bb..54064fc4 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -7,13 +7,13 @@ import javasabr.mqtt.model.MqttClientConnectionConfig; import javasabr.mqtt.model.MqttUser; -import javasabr.mqtt.model.QoS; import javasabr.mqtt.model.SubscribeRetainHandling; import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode; import javasabr.mqtt.model.reason.code.UnsubscribeAckReasonCode; import javasabr.mqtt.model.session.ActiveSubscriptions; import javasabr.mqtt.model.session.MqttSession; import javasabr.mqtt.model.subscriber.SingleSubscriber; +import javasabr.mqtt.model.subscriber.Subscriber; import javasabr.mqtt.model.subscriber.tree.ConcurrentSubscriberTree; import javasabr.mqtt.model.subscription.Subscription; import javasabr.mqtt.model.topic.SharedTopicFilter; @@ -28,6 +28,7 @@ import lombok.AccessLevel; import lombok.CustomLog; import lombok.experimental.FieldDefaults; +import org.jspecify.annotations.Nullable; /** * In memory subscription service based on {@link ConcurrentSubscriberTree} @@ -84,15 +85,9 @@ private SubscribeAckReasonCode addSubscription(MqttUser user, MqttSession sessio if (previousSubscriber != null) { activeSubscriptions.remove(previousSubscriber.subscription()); } - QoS subscriptionQoS = subscription.qos(); - SubscribeRetainHandling retainHandling = subscription.retainHandling(); - boolean isRetainHandlingSatisfied = - retainHandling == SEND || (retainHandling == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previousSubscriber == null); - if (subscriptionQoS.isValid() && isRetainHandlingSatisfied) { - sendRetainedMessages(newSubscriber); - } + sendRetainedMessages(newSubscriber, previousSubscriber); activeSubscriptions.add(subscription); - return subscriptionQoS.subscribeAckReasonCode(); + return subscription.qos().subscribeAckReasonCode(); } @Override @@ -116,9 +111,7 @@ private UnsubscribeAckReasonCode removeSubscription(MqttUser user, MqttSession s if (topicFilter.isInvalid()) { return UnsubscribeAckReasonCode.TOPIC_FILTER_INVALID; } else if (subscriberTree.unsubscribe(user, topicFilter)) { - session - .activeSubscriptions() - .removeByTopicFilter(topicFilter); + session.activeSubscriptions().removeByTopicFilter(topicFilter); return SUCCESS; } else { return NO_SUBSCRIPTION_EXISTED; @@ -127,9 +120,7 @@ private UnsubscribeAckReasonCode removeSubscription(MqttUser user, MqttSession s @Override public void cleanSubscriptions(MqttUser user, MqttSession session) { - Array subscriptions = session - .activeSubscriptions() - .subscriptions(); + Array subscriptions = session.activeSubscriptions().subscriptions(); for (Subscription subscription : subscriptions) { subscriberTree.unsubscribe(user, subscription.topicFilter()); } @@ -137,19 +128,27 @@ public void cleanSubscriptions(MqttUser user, MqttSession session) { @Override public void restoreSubscriptions(MqttUser user, MqttSession session) { - Array subscriptions = session - .activeSubscriptions() - .subscriptions(); + Array subscriptions = session.activeSubscriptions().subscriptions(); for (Subscription subscription : subscriptions) { SingleSubscriber singleSubscriber = new SingleSubscriber(user, subscription); subscriberTree.subscribe(singleSubscriber); } } - private void sendRetainedMessages(SingleSubscriber singleSubscriber) { + private static boolean isRetainHandlingSatisfied(Subscription subscription, @Nullable Subscriber previousSubscriber) { + SubscribeRetainHandling retainHandling = subscription.retainHandling(); + return retainHandling == SEND || (retainHandling == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST + && previousSubscriber == null); + } + + private void sendRetainedMessages(Subscriber newSubscriber, @Nullable Subscriber previousSubscriber) { + Subscription subscription = newSubscriber.resolveSingle().subscription(); + if (!subscription.qos().isValid() || !isRetainHandlingSatisfied(subscription, previousSubscriber)) { + return; + } int count = 0; - String clientId = singleSubscriber.user().clientId(); - var results = retainMessageService.deliverRetainedMessages(singleSubscriber); + String clientId = newSubscriber.resolveSingle().user().clientId(); + var results = retainMessageService.deliverRetainedMessages(newSubscriber); for (PublishHandlingResult result : results) { PublishHandlingResult errorResult = null; if (result.error()) { From 1f3d50a6ad27652d7ba87d2afe79d686b56e727b Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Wed, 10 Dec 2025 22:04:07 +0100 Subject: [PATCH 34/38] [broker-30] Revert formatting --- .../config/MqttBrokerSpringConfig.java | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java index e1665d7b..d45c5d55 100644 --- a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java +++ b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java @@ -250,7 +250,10 @@ MqttInMessageHandler unsubscribeMqttInMessageHandler( SubscriptionService subscriptionService, MessageOutFactoryService messageOutFactoryService, TopicService topicService) { - return new UnsubscribeMqttInMessageHandler(subscriptionService, messageOutFactoryService, topicService); + return new UnsubscribeMqttInMessageHandler( + subscriptionService, + messageOutFactoryService, + topicService); } @Bean @@ -340,15 +343,30 @@ MqttServerConnectionConfig externalConnectionConfig(Environment env) { "mqtt.external.connection.max.message.size", int.class, MqttProperties.MAXIMUM_MESSAGE_SIZE_DEFAULT), - env.getProperty("mqtt.external.connection.max.string.length", int.class, MqttProperties.MAXIMUM_STRING_LENGTH), - env.getProperty("mqtt.external.connection.max.binary.size", int.class, MqttProperties.MAXIMUM_BINARY_SIZE), - env.getProperty("mqtt.external.connection.max.topic.levels", int.class, MqttProperties.MAXIMUM_TOPIC_LEVELS), - env.getProperty("mqtt.external.connection.min.keep.alive", int.class, MqttProperties.SERVER_KEEP_ALIVE_DEFAULT), + env.getProperty( + "mqtt.external.connection.max.string.length", + int.class, + MqttProperties.MAXIMUM_STRING_LENGTH), + env.getProperty( + "mqtt.external.connection.max.binary.size", + int.class, + MqttProperties.MAXIMUM_BINARY_SIZE), + env.getProperty( + "mqtt.external.connection.max.topic.levels", + int.class, + MqttProperties.MAXIMUM_TOPIC_LEVELS), + env.getProperty( + "mqtt.external.connection.min.keep.alive", + int.class, + MqttProperties.SERVER_KEEP_ALIVE_DEFAULT), env.getProperty( "mqtt.external.connection.receive.maximum", int.class, MqttProperties.RECEIVE_MAXIMUM_PUBLISHES_DEFAULT), - env.getProperty("mqtt.external.connection.topic.alias.maximum", int.class, 0), + env.getProperty( + "mqtt.external.connection.topic.alias.maximum", + int.class, + 0), env.getProperty( "mqtt.external.connection.default.session.expiration.time", long.class, @@ -361,14 +379,18 @@ MqttServerConnectionConfig externalConnectionConfig(Environment env) { "mqtt.external.connection.sessions.enabled", boolean.class, MqttProperties.SESSIONS_ENABLED_DEFAULT), - env.getProperty("mqtt.external.connection.retain.available", boolean.class, false), - // set false because currently it's not implemented and we should not allow for clients to use it + env.getProperty( + "mqtt.external.connection.retain.available", + boolean.class, + false), // set false because currently it's not implemented and we should not allow for clients to use it env.getProperty( "mqtt.external.connection.wildcard.subscription.available", boolean.class, MqttProperties.WILDCARD_SUBSCRIPTION_AVAILABLE_DEFAULT), - env.getProperty("mqtt.external.connection.subscription.id.available", boolean.class, false), - // set false because currently it's not implemented and we should not allow for clients to use it + env.getProperty( + "mqtt.external.connection.subscription.id.available", + boolean.class, + false), // set false because currently it's not implemented and we should not allow for clients to use it env.getProperty( "mqtt.external.connection.shared.subscription.available", boolean.class, From 588aae433c550910c6d83e840faed39ea68dcd72 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Wed, 10 Dec 2025 22:34:17 +0100 Subject: [PATCH 35/38] [broker-30] Revert SubscriberNode --- .../model/subscriber/tree/SubscriberNode.java | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java index 2165e875..5d3c74bb 100644 --- a/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java +++ b/model/src/main/java/javasabr/mqtt/model/subscriber/tree/SubscriberNode.java @@ -56,28 +56,51 @@ protected boolean unsubscribe(int level, MqttUser owner, TopicFilter topicFilter } protected void matchesTo(int level, TopicName topicName, int lastLevel, MutableArray container) { - collectMatchingSubscribers(topicName.segment(level), level, topicName, lastLevel, container); - collectMatchingSubscribers(TopicFilter.SINGLE_LEVEL_WILDCARD, level, topicName, lastLevel, container); - collectMatchingSubscribers(TopicFilter.MULTI_LEVEL_WILDCARD, level, topicName, lastLevel, container); + exactlyTopicMatch(level, topicName, lastLevel, container); + singleWildcardTopicMatch(level, topicName, lastLevel, container); + multiWildcardTopicMatch(container); } - private void collectMatchingSubscribers( - String segment, + private void exactlyTopicMatch( int level, TopicName topicName, int lastLevel, MutableArray result) { + String segment = topicName.segment(level); SubscriberNode subscriberNode = getChildNode(segment); if (subscriberNode == null) { return; } - if (level == lastLevel || TopicFilter.MULTI_LEVEL_WILDCARD.equals(segment)) { + if (level == lastLevel) { appendSubscribersTo(result, subscriberNode); } else if (level < lastLevel) { subscriberNode.matchesTo(level + 1, topicName, lastLevel, result); } } + private void singleWildcardTopicMatch( + int level, + TopicName topicName, + int lastLevel, + MutableArray result) { + SubscriberNode subscriberNode = getChildNode(TopicFilter.SINGLE_LEVEL_WILDCARD); + if (subscriberNode == null) { + return; + } + if (level == lastLevel) { + appendSubscribersTo(result, subscriberNode); + } else if (level < lastLevel) { + subscriberNode.matchesTo(level + 1, topicName, lastLevel, result); + } + } + + private void multiWildcardTopicMatch(MutableArray result) { + SubscriberNode subscriberNode = getChildNode(TopicFilter.MULTI_LEVEL_WILDCARD); + if (subscriberNode != null) { + appendSubscribersTo(result, subscriberNode); + } + } + private LockableArray getOrCreateSubscribers() { LockableArray localSubscribers = subscribers; if (localSubscribers != null) { From 07ce54d714d839e601d9568545ab5981ad3f240d Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:41:34 +0100 Subject: [PATCH 36/38] [broker-30] Reduce memory allocation of RetainedMessageNode --- .../model/topic/tree/RetainedMessageNode.java | 70 +++++++++++-------- .../topic/tree/RetainedMessageTreeTest.groovy | 8 ++- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java index 18be847e..5b2f81c0 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/tree/RetainedMessageNode.java @@ -1,6 +1,5 @@ package javasabr.mqtt.model.topic.tree; -import java.util.Queue; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import javasabr.mqtt.base.util.DebugUtils; @@ -10,7 +9,6 @@ import javasabr.mqtt.model.topic.TopicName; import javasabr.rlib.collections.array.ArrayFactory; import javasabr.rlib.collections.array.MutableArray; -import javasabr.rlib.collections.deque.DequeFactory; import lombok.AccessLevel; import lombok.Getter; import lombok.experimental.Accessors; @@ -61,10 +59,7 @@ private void clearRetainedMessage() { retainedMessage.set(null); } - public void collectRetainedMessages( - int level, - TopicFilter topicFilter, - MutableArray result) { + public void collectRetainedMessages(int level, TopicFilter topicFilter, MutableArray result) { if (level == topicFilter.levelsCount()) { Publish publish = retainedMessage.get(); if (publish != null) { @@ -73,36 +68,51 @@ public void collectRetainedMessages( return; } String segment = topicFilter.segment(level); - boolean isOneCharSegment = segment.length() == 1; - if (isOneCharSegment && segment.charAt(0) == TopicFilter.MULTI_LEVEL_WILDCARD_CHAR) { - collectAllMessages(this, result); - return; - } - if (isOneCharSegment && segment.charAt(0) == TopicFilter.SINGLE_LEVEL_WILDCARD_CHAR) { - var localChildNodes = getChildNodes(RetainedMessageNode::childNodesFactory); - if (localChildNodes != null) { - for (RetainedMessageNode childNode : localChildNodes) { - childNode.collectRetainedMessages(level + 1, topicFilter, result); - } - } + boolean isOneChar = segment.length() == 1; + if (isOneChar && segment.charAt(0) == TopicFilter.SINGLE_LEVEL_WILDCARD_CHAR) { + collectAllChildren(level, topicFilter, result); + } else if (isOneChar && segment.charAt(0) == TopicFilter.MULTI_LEVEL_WILDCARD_CHAR) { + collectEverything(this, result); } else { - RetainedMessageNode retainedMessageNode = getChildNode(segment); - if (retainedMessageNode != null) { - retainedMessageNode.collectRetainedMessages(level + 1, topicFilter, result); + collectExactSegment(level, segment, topicFilter, result); + } + } + + private void collectExactSegment( + int level, + String segment, + TopicFilter topicFilter, + MutableArray result) { + RetainedMessageNode retainedMessageNode = getChildNode(segment); + if (retainedMessageNode != null) { + retainedMessageNode.collectRetainedMessages(level + 1, topicFilter, result); + } + } + + private void collectAllChildren(int level, TopicFilter topicFilter, MutableArray result) { + var localChildNodes = getChildNodes(RetainedMessageNode::childNodesFactory); + if (localChildNodes != null) { + for (RetainedMessageNode childNode : localChildNodes) { + childNode.collectRetainedMessages(level + 1, topicFilter, result); } } } - private void collectAllMessages(RetainedMessageNode node, MutableArray result) { - Queue queue = DequeFactory.arrayBasedBased(RetainedMessageNode.class); - queue.add(node); - while (!queue.isEmpty()) { - RetainedMessageNode poll = queue.poll(); - Publish message = poll.retainedMessage.get(); - if (message != null) { - result.add(message); + private void collectEverything(RetainedMessageNode node, MutableArray result) { + collectEverythingDfs(node, result); + } + + private void collectEverythingDfs(RetainedMessageNode node, MutableArray result) { + Publish message = node.retainedMessage.get(); + if (message != null) { + result.add(message); + } + + var childNodes = node.getChildNodes(RetainedMessageNode::childNodesFactory); + if (childNodes != null) { + for (RetainedMessageNode childNode : childNodes) { + collectEverythingDfs(childNode, result); } - poll.collectChildNodes(queue); } } } diff --git a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy index 9a098f5a..c82034c6 100644 --- a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy +++ b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/RetainedMessageTreeTest.groovy @@ -11,7 +11,7 @@ class RetainedMessageTreeTest extends UnitSpecification { String rawTopicFilter, List expectedMessages) { given: - ConcurrentRetainedMessageTree retainedMessageTree = new ConcurrentRetainedMessageTree(); + ConcurrentRetainedMessageTree retainedMessageTree = new ConcurrentRetainedMessageTree() messages.collect(TestPublishFactory::makePublish).each(retainedMessageTree::retainMessage) def topicFilter = TopicFilter.valueOf(rawTopicFilter) when: @@ -29,6 +29,7 @@ class RetainedMessageTreeTest extends UnitSpecification { "/topic/+/segment2", "/topic/#" ] + //noinspection GroovyAssignabilityCheck messages << [ [ "/topic/segment1", @@ -70,6 +71,7 @@ class RetainedMessageTreeTest extends UnitSpecification { "/topic/segment1/segment2" ] ] + //noinspection GroovyAssignabilityCheck expectedMessages << [ [ "/topic/segment1" @@ -85,9 +87,9 @@ class RetainedMessageTreeTest extends UnitSpecification { "/topic/segment500/segment2" ], [ + "/topic/segment1/segment2", "/topic/segment2", - "/topic/segment3", - "/topic/segment1/segment2" + "/topic/segment3" ] ] } From 17359d313c1e584621454e6fcb60544ec09b6e39 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Sat, 13 Dec 2025 00:10:43 +0100 Subject: [PATCH 37/38] [broker-30] Refactoring --- .../mqtt/service/RetainMessageService.java | 2 +- .../impl/DefaultRetainMessageService.java | 5 +- .../impl/InMemorySubscriptionService.java | 52 +++++++------------ .../mqtt/model/topic/AbstractTopic.java | 4 ++ .../mqtt/model/topic/SharedTopicFilter.java | 5 ++ 5 files changed, 32 insertions(+), 36 deletions(-) diff --git a/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java b/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java index 929f65e8..df16d4f7 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/RetainMessageService.java @@ -11,5 +11,5 @@ public interface RetainMessageService { void retainMessage(Publish publish); - Array deliverRetainedMessages(Subscriber subscriber); + void deliverRetainedMessages(Subscriber subscriber); } diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java index 870c2af9..9056d870 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/DefaultRetainMessageService.java @@ -31,7 +31,7 @@ public void retainMessage(Publish publish) { } @Override - public Array deliverRetainedMessages(Subscriber subscriber) { + public void deliverRetainedMessages(Subscriber subscriber) { SingleSubscriber singleSubscriber = subscriber.resolveSingle(); Subscription subscription = singleSubscriber.subscription(); boolean retainAsPublished = subscription.retainAsPublished(); @@ -41,8 +41,7 @@ public Array deliverRetainedMessages(Subscriber subscribe if (!retainAsPublished) { message = message.withoutRetain(); } - result.add(publishDeliveringService.startDelivering(message, singleSubscriber)); + publishDeliveringService.startDelivering(message, singleSubscriber); } - return Array.copyOf(result); } } diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java index 54064fc4..90e7c840 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java +++ b/core-service/src/main/java/javasabr/mqtt/service/impl/InMemorySubscriptionService.java @@ -21,7 +21,6 @@ import javasabr.mqtt.model.topic.TopicName; import javasabr.mqtt.service.RetainMessageService; import javasabr.mqtt.service.SubscriptionService; -import javasabr.mqtt.service.publish.handler.PublishHandlingResult; import javasabr.rlib.collections.array.Array; import javasabr.rlib.collections.array.ArrayFactory; import javasabr.rlib.collections.array.MutableArray; @@ -85,7 +84,9 @@ private SubscribeAckReasonCode addSubscription(MqttUser user, MqttSession sessio if (previousSubscriber != null) { activeSubscriptions.remove(previousSubscriber.subscription()); } - sendRetainedMessages(newSubscriber, previousSubscriber); + if (isRetainHandlingRequired(subscription, previousSubscriber)) { + retainMessageService.deliverRetainedMessages(newSubscriber); + } activeSubscriptions.add(subscription); return subscription.qos().subscribeAckReasonCode(); } @@ -111,7 +112,9 @@ private UnsubscribeAckReasonCode removeSubscription(MqttUser user, MqttSession s if (topicFilter.isInvalid()) { return UnsubscribeAckReasonCode.TOPIC_FILTER_INVALID; } else if (subscriberTree.unsubscribe(user, topicFilter)) { - session.activeSubscriptions().removeByTopicFilter(topicFilter); + session + .activeSubscriptions() + .removeByTopicFilter(topicFilter); return SUCCESS; } else { return NO_SUBSCRIPTION_EXISTED; @@ -120,7 +123,9 @@ private UnsubscribeAckReasonCode removeSubscription(MqttUser user, MqttSession s @Override public void cleanSubscriptions(MqttUser user, MqttSession session) { - Array subscriptions = session.activeSubscriptions().subscriptions(); + Array subscriptions = session + .activeSubscriptions() + .subscriptions(); for (Subscription subscription : subscriptions) { subscriberTree.unsubscribe(user, subscription.topicFilter()); } @@ -128,39 +133,22 @@ public void cleanSubscriptions(MqttUser user, MqttSession session) { @Override public void restoreSubscriptions(MqttUser user, MqttSession session) { - Array subscriptions = session.activeSubscriptions().subscriptions(); + Array subscriptions = session + .activeSubscriptions() + .subscriptions(); for (Subscription subscription : subscriptions) { - SingleSubscriber singleSubscriber = new SingleSubscriber(user, subscription); - subscriberTree.subscribe(singleSubscriber); + subscriberTree.subscribe(new SingleSubscriber(user, subscription)); } } - private static boolean isRetainHandlingSatisfied(Subscription subscription, @Nullable Subscriber previousSubscriber) { - SubscribeRetainHandling retainHandling = subscription.retainHandling(); + private static boolean isRetainHandlingRequired( + Subscription newSubscription, + @Nullable Subscriber previousSubscriber) { + if (newSubscription.topicFilter().isShared() || !newSubscription.qos().isValid()) { + return false; + } + SubscribeRetainHandling retainHandling = newSubscription.retainHandling(); return retainHandling == SEND || (retainHandling == SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST && previousSubscriber == null); } - - private void sendRetainedMessages(Subscriber newSubscriber, @Nullable Subscriber previousSubscriber) { - Subscription subscription = newSubscriber.resolveSingle().subscription(); - if (!subscription.qos().isValid() || !isRetainHandlingSatisfied(subscription, previousSubscriber)) { - return; - } - int count = 0; - String clientId = newSubscriber.resolveSingle().user().clientId(); - var results = retainMessageService.deliverRetainedMessages(newSubscriber); - for (PublishHandlingResult result : results) { - PublishHandlingResult errorResult = null; - if (result.error()) { - errorResult = result; - } else if (result == PublishHandlingResult.SUCCESS) { - count++; - } - if (errorResult != null) { - log.debug(clientId, errorResult, "[%s] Error occurred [%s] during sending retained messages"::formatted); - } else { - log.debug(clientId, count, "[%s] Delivering of [%s] retained message has been started"::formatted); - } - } - } } diff --git a/model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java b/model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java index 2b1f313e..ece98cca 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java @@ -32,6 +32,10 @@ protected AbstractTopic(String rawTopicName) { rawTopic = rawTopicName; } + public boolean isShared(){ + return false; + } + public String segment(int level) { return segments[level]; } diff --git a/model/src/main/java/javasabr/mqtt/model/topic/SharedTopicFilter.java b/model/src/main/java/javasabr/mqtt/model/topic/SharedTopicFilter.java index e9e3ba7d..b68c4689 100644 --- a/model/src/main/java/javasabr/mqtt/model/topic/SharedTopicFilter.java +++ b/model/src/main/java/javasabr/mqtt/model/topic/SharedTopicFilter.java @@ -28,6 +28,11 @@ public static SharedTopicFilter valueOf(String rawSharedTopicFilter) { return new SharedTopicFilter(rawTopicFilter, shareName); } + @Override + public boolean isShared(){ + return true; + } + public static boolean isShared(String rawTopicFilter) { return rawTopicFilter.startsWith(SharedTopicFilter.SHARE_KEYWORD); } From ffb1c83caa663b08a79313a99aff36a7927ecef5 Mon Sep 17 00:00:00 2001 From: Maksim Kashapov <56276969+crazyrokr@users.noreply.github.com> Date: Sat, 13 Dec 2025 00:55:03 +0100 Subject: [PATCH 38/38] [broker-30] Update tests --- .../topic/tree/SubscriberTreeTest.groovy | 82 ++++++++++++------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/SubscriberTreeTest.groovy b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/SubscriberTreeTest.groovy index f8ebc0cc..fe048055 100644 --- a/model/src/test/groovy/javasabr/mqtt/model/topic/tree/SubscriberTreeTest.groovy +++ b/model/src/test/groovy/javasabr/mqtt/model/topic/tree/SubscriberTreeTest.groovy @@ -15,6 +15,18 @@ import javasabr.mqtt.test.support.UnitSpecification class SubscriberTreeTest extends UnitSpecification { + static SingleSubscriber createSubscriber(String clientId, String rawTopicFilter) { + return createSubscriber(clientId, rawTopicFilter, QoS.AT_LEAST_ONCE.number()) + } + + static SingleSubscriber createSubscriber(String clientId, String rawTopicFilter, int qos) { + return new SingleSubscriber(makeUser(clientId), makeSubscription(rawTopicFilter, qos)) + } + + static SingleSubscriber createShareSubscriber(String clientId, String rawTopicFilter) { + return new SingleSubscriber(makeUser(clientId), makeSharedSubscription(rawTopicFilter)) + } + def "should match simple topic correctly"( List subscriptions, List users, @@ -426,19 +438,19 @@ class SubscriberTreeTest extends UnitSpecification { //noinspection GroovyAssignabilityCheck expectedSubscribers << [ [ - new SingleSubscriber(makeUser("id1"), makeSubscription("/topic/segment1/segment2", 2)), - new SingleSubscriber(makeUser("id2"), makeSubscription("/topic/segment1/#", 1)), - new SingleSubscriber(makeUser("id3"), makeSubscription("/topic/#", 0)), + createSubscriber("id1", "/topic/segment1/segment2", 2), + createSubscriber("id2", "/topic/segment1/#", 1), + createSubscriber("id3", "/topic/#", 0), ], [ - new SingleSubscriber(makeUser("id1"), makeSubscription("/topic/#", 0)), - new SingleSubscriber(makeUser("id2"), makeSubscription("/topic/#", 0)), - new SingleSubscriber(makeUser("id3"), makeSubscription("/topic/#", 0)), + createSubscriber("id1", "/topic/#", 0), + createSubscriber("id2", "/topic/#", 0), + createSubscriber("id3", "/topic/#", 0), ], [ - new SingleSubscriber(makeUser("id1"), makeSubscription("/topic/#", 0)), - new SingleSubscriber(makeUser("id2"), makeSubscription("/topic/#", 0)), - new SingleSubscriber(makeUser("id3"), makeSubscription("/topic/segment2/#", 1)), + createSubscriber("id1", "/topic/#", 0), + createSubscriber("id2", "/topic/#", 0), + createSubscriber("id3", "/topic/segment2/#", 1), ] ] } @@ -448,16 +460,16 @@ class SubscriberTreeTest extends UnitSpecification { def group1 = ["id1", "id2", "id3", "id4", "id5"] def group2 = ["id6", "id7", "id8", "id9", "id10"] ConcurrentSubscriberTree subscriberTree = new ConcurrentSubscriberTree() - subscriberTree.subscribe(new SingleSubscriber(makeUser("id1"), makeSharedSubscription('$share/group1/topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id2"), makeSharedSubscription('$share/group1/topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id3"), makeSharedSubscription('$share/group1/topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id4"), makeSharedSubscription('$share/group1/topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id5"), makeSharedSubscription('$share/group1/topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id6"), makeSharedSubscription('$share/group2/topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id7"), makeSharedSubscription('$share/group2/topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id8"), makeSharedSubscription('$share/group2/topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id9"), makeSharedSubscription('$share/group2/topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id10"), makeSharedSubscription('$share/group2/topic/name1'))) + subscriberTree.subscribe(createShareSubscriber("id1", '$share/group1/topic/name1')) + subscriberTree.subscribe(createShareSubscriber("id2", '$share/group1/topic/name1')) + subscriberTree.subscribe(createShareSubscriber("id3", '$share/group1/topic/name1')) + subscriberTree.subscribe(createShareSubscriber("id4", '$share/group1/topic/name1')) + subscriberTree.subscribe(createShareSubscriber("id5", '$share/group1/topic/name1')) + subscriberTree.subscribe(createShareSubscriber("id6", '$share/group2/topic/name1')) + subscriberTree.subscribe(createShareSubscriber("id7", '$share/group2/topic/name1')) + subscriberTree.subscribe(createShareSubscriber("id8", '$share/group2/topic/name1')) + subscriberTree.subscribe(createShareSubscriber("id9", '$share/group2/topic/name1')) + subscriberTree.subscribe(createShareSubscriber("id10", '$share/group2/topic/name1')) when: def matched = subscriberTree .matches(TopicName.valueOf("topic/name1")) @@ -481,9 +493,9 @@ class SubscriberTreeTest extends UnitSpecification { def "should subscribe and unsubscribe simple topic correctly correctly"() { given: ConcurrentSubscriberTree subscriberTree = new ConcurrentSubscriberTree() - subscriberTree.subscribe(new SingleSubscriber(makeUser("id1"), makeSubscription('topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id2"), makeSubscription('topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id3"), makeSubscription('topic/name1'))) + subscriberTree.subscribe(createSubscriber("id1", 'topic/name1')) + subscriberTree.subscribe(createSubscriber("id2", 'topic/name1')) + subscriberTree.subscribe(createSubscriber("id3", 'topic/name1')) when: def matched = subscriberTree .matches(TopicName.valueOf("topic/name1")) @@ -518,9 +530,9 @@ class SubscriberTreeTest extends UnitSpecification { def "should subscribe and unsubscribe shared topic correctly correctly"() { given: ConcurrentSubscriberTree subscriberTree = new ConcurrentSubscriberTree() - subscriberTree.subscribe(new SingleSubscriber(makeUser("id1"), makeSharedSubscription('$share/group1/topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id2"), makeSharedSubscription('$share/group1/topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id3"), makeSharedSubscription('$share/group1/topic/name1'))) + subscriberTree.subscribe(createShareSubscriber("id1", '$share/group1/topic/name1')) + subscriberTree.subscribe(createShareSubscriber("id2", '$share/group1/topic/name1')) + subscriberTree.subscribe(createShareSubscriber("id3", '$share/group1/topic/name1')) when: def matched = subscriberTree .matches(TopicName.valueOf("topic/name1")) @@ -529,8 +541,12 @@ class SubscriberTreeTest extends UnitSpecification { then: matched.size() == 1 when: - def id2WasUnsubscribed = subscriberTree.unsubscribe(makeUser("id2"), SharedTopicFilter.valueOf('$share/group1/topic/name1')) - def id3WasUnsubscribed = subscriberTree.unsubscribe(makeUser("id3"), SharedTopicFilter.valueOf('$share/group1/topic/name1')) + def id2WasUnsubscribed = subscriberTree.unsubscribe( + makeUser("id2"), + SharedTopicFilter.valueOf('$share/group1/topic/name1')) + def id3WasUnsubscribed = subscriberTree.unsubscribe( + makeUser("id3"), + SharedTopicFilter.valueOf('$share/group1/topic/name1')) matched = subscriberTree .matches(TopicName.valueOf("topic/name1")) .collect { it.user().toString() } @@ -540,8 +556,12 @@ class SubscriberTreeTest extends UnitSpecification { id2WasUnsubscribed id3WasUnsubscribed when: - def id1WasUnsubscribed = subscriberTree.unsubscribe(makeUser("id1"), SharedTopicFilter.valueOf('$share/group1/topic/name1')) - id3WasUnsubscribed = subscriberTree.unsubscribe(makeUser("id3"), SharedTopicFilter.valueOf('$share/group1/topic/name1')) + def id1WasUnsubscribed = subscriberTree.unsubscribe( + makeUser("id1"), + SharedTopicFilter.valueOf('$share/group1/topic/name1')) + id3WasUnsubscribed = subscriberTree.unsubscribe( + makeUser("id3"), + SharedTopicFilter.valueOf('$share/group1/topic/name1')) matched = subscriberTree .matches(TopicName.valueOf("topic/name1")) .collect { it.user().toString() } @@ -558,8 +578,8 @@ class SubscriberTreeTest extends UnitSpecification { def owner1 = makeUser("id1") def originalSub = makeSubscription('topic/name1') def replacementSub = makeSubscription('topic/name1') - subscriberTree.subscribe(new SingleSubscriber(makeUser("id2"), makeSubscription('topic/name1'))) - subscriberTree.subscribe(new SingleSubscriber(makeUser("id3"), makeSubscription('topic/name1'))) + subscriberTree.subscribe(createSubscriber("id2", 'topic/name1')) + subscriberTree.subscribe(createSubscriber("id3", 'topic/name1')) when: def previous = subscriberTree.subscribe(new SingleSubscriber(owner1, originalSub)) def matched = subscriberTree