Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix 841 Part-2: C-Tree deepening #849

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions broker/pom.xml
Original file line number Diff line number Diff line change
@@ -187,6 +187,12 @@
<version>1.15</version>
</dependency>

<dependency>
<groupId>org.pcollections</groupId>
<artifactId>pcollections</artifactId>
<version>4.0.2</version>
</dependency>

<dependency>
<groupId>org.fusesource.mqtt-client</groupId>
<artifactId>mqtt-client</artifactId>
3 changes: 2 additions & 1 deletion broker/src/main/java/io/moquette/broker/PostOffice.java
Original file line number Diff line number Diff line change
@@ -58,6 +58,7 @@
import java.util.stream.Collectors;

import static io.moquette.broker.Utils.messageId;
import io.moquette.broker.subscriptions.SubscriptionCollection;
import static io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader.from;
import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE;
import static io.netty.handler.codec.mqtt.MqttQoS.EXACTLY_ONCE;
@@ -842,7 +843,7 @@ private RoutingResults publish2Subscribers(String publisherClientId,
final boolean retainPublish = msg.fixedHeader().isRetain();
final Topic topic = new Topic(msg.variableHeader().topicName());
final MqttQoS publishingQos = msg.fixedHeader().qosLevel();
List<Subscription> topicMatchingSubscriptions = subscriptions.matchQosSharpening(topic);
SubscriptionCollection topicMatchingSubscriptions = subscriptions.matchWithoutQosSharpening(topic);
if (topicMatchingSubscriptions.isEmpty()) {
// no matching subscriptions, clean exit
LOG.trace("No matching subscriptions for topic: {}", topic);
119 changes: 41 additions & 78 deletions broker/src/main/java/io/moquette/broker/subscriptions/CNode.java
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@@ -33,52 +34,48 @@
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import org.pcollections.PMap;
import org.pcollections.TreePMap;

class CNode implements Comparable<CNode> {

public static final Random SECURE_RANDOM = new SecureRandom();
private final Token token;
private final List<INode> children;
// Sorted list of subscriptions. The sort is necessary for fast access, instead of linear scan.
private List<Subscription> subscriptions;
private PMap<String, INode> children;
// Map of subscriptions per clientId.
private PMap<String, Subscription> subscriptions;
// the list of SharedSubscription is sorted. The sort is necessary for fast access, instead of linear scan.
private Map<ShareName, List<SharedSubscription>> sharedSubscriptions;
private PMap<ShareName, List<SharedSubscription>> sharedSubscriptions;

CNode(Token token) {
this.children = new ArrayList<>();
this.subscriptions = new ArrayList<>();
this.sharedSubscriptions = new HashMap<>();
this.children = TreePMap.empty();
this.subscriptions = TreePMap.empty();
this.sharedSubscriptions = TreePMap.empty();
this.token = token;
}

//Copy constructor
private CNode(Token token, List<INode> children, List<Subscription> subscriptions, Map<ShareName,
List<SharedSubscription>> sharedSubscriptions) {
private CNode(Token token, PMap<String, INode> children, PMap<String, Subscription> subscriptions, PMap<ShareName, List<SharedSubscription>> sharedSubscriptions) {
this.token = token; // keep reference, root comparison in directory logic relies on it for now.
this.subscriptions = new ArrayList<>(subscriptions);
this.sharedSubscriptions = new HashMap<>(sharedSubscriptions);
this.children = new ArrayList<>(children);
this.subscriptions = subscriptions;
this.sharedSubscriptions = sharedSubscriptions;
this.children = children;
}

public Token getToken() {
return token;
}

List<INode> allChildren() {
return new ArrayList<>(this.children);
Collection<INode> allChildren() {
return this.children.values();
}

Optional<INode> childOf(Token token) {
int idx = findIndexForToken(token);
if (idx < 0) {
INode value = children.get(token.name);
if (value == null) {
return Optional.empty();
}
return Optional.of(children.get(idx));
}

private int findIndexForToken(Token token) {
final INode tempTokenNode = new INode(new CNode(token));
return Collections.binarySearch(children, tempTokenNode, (INode node, INode tokenHolder) -> node.mainNode().token.compareTo(tokenHolder.mainNode().token));
return Optional.of(value);
}

@Override
@@ -91,35 +88,23 @@ CNode copy() {
}

public void add(INode newINode) {
int idx = findIndexForToken(newINode.mainNode().token);
if (idx < 0) {
children.add(-1 - idx, newINode);
} else {
children.add(idx, newINode);
}
final String tokenName = newINode.mainNode().token.name;
children = children.plus(tokenName, newINode);
}

public INode remove(INode node) {
int idx = findIndexForToken(node.mainNode().token);
return this.children.remove(idx);
final String tokenName = node.mainNode().token.name;
INode toRemove = children.get(tokenName);
children = children.minus(tokenName);
return toRemove;
}

private List<Subscription> sharedSubscriptions() {
List<Subscription> selectedSubscriptions = new ArrayList<>(sharedSubscriptions.size());
// for each sharedSubscription related to a ShareName, select one subscription
for (Map.Entry<ShareName, List<SharedSubscription>> subsForName : sharedSubscriptions.entrySet()) {
List<SharedSubscription> list = subsForName.getValue();
final String shareName = subsForName.getKey().getShareName();
// select a subscription randomly
int randIdx = SECURE_RANDOM.nextInt(list.size());
SharedSubscription sub = list.get(randIdx);
selectedSubscriptions.add(sub.createSubscription());
}
return selectedSubscriptions;
public PMap<String, Subscription> getSubscriptions() {
return subscriptions;
}

List<Subscription> subscriptions() {
return subscriptions;
public PMap<ShareName, List<SharedSubscription>> getSharedSubscriptions() {
return sharedSubscriptions;
}

// Mutating operation
@@ -141,25 +126,23 @@ CNode addSubscription(SubscriptionRequest request) {
final Subscription newSubscription = request.subscription();

// if already contains one with same topic and same client, keep that with higher QoS
int idx = Collections.binarySearch(subscriptions, newSubscription);
if (idx >= 0) {
final Subscription existing = subscriptions.get(newSubscription.clientId);
if (existing != null) {
// Subscription already exists
final Subscription existing = subscriptions.get(idx);
if (needsToUpdateExistingSubscription(newSubscription, existing)) {
subscriptions.set(idx, newSubscription);
subscriptions = subscriptions.plus(newSubscription.clientId, newSubscription);
}
} else {
// insert into the expected index so that the sorting is maintained
this.subscriptions.add(-1 - idx, newSubscription);
subscriptions = subscriptions.plus(newSubscription.clientId, newSubscription);
}
}
return this;
}

private static boolean needsToUpdateExistingSubscription(Subscription newSubscription, Subscription existing) {
if ((newSubscription.hasSubscriptionIdentifier() && existing.hasSubscriptionIdentifier()) &&
newSubscription.getSubscriptionIdentifier().equals(existing.getSubscriptionIdentifier())
) {
if ((newSubscription.hasSubscriptionIdentifier() && existing.hasSubscriptionIdentifier())
&& newSubscription.getSubscriptionIdentifier().equals(existing.getSubscriptionIdentifier())) {
// if subscription identifier hasn't changed,
// then check QoS but don't lower the requested QoS level
return existing.option().qos().value() < newSubscription.option().qos().value();
@@ -177,8 +160,8 @@ private static boolean needsToUpdateExistingSubscription(Subscription newSubscri
* AND at least one subscription is actually present for that clientId
* */
boolean containsOnly(String clientId) {
for (Subscription sub : this.subscriptions) {
if (!sub.clientId.equals(clientId)) {
for (String sub : this.subscriptions.keySet()) {
if (!sub.equals(clientId)) {
return false;
}
}
@@ -207,12 +190,7 @@ private static SharedSubscription wrapKey(String clientId) {

//TODO this is equivalent to negate(containsOnly(clientId))
private boolean containsSubscriptionsForClient(String clientId) {
for (Subscription sub : this.subscriptions) {
if (sub.clientId.equals(clientId)) {
return true;
}
}
return false;
return subscriptions.containsKey(clientId);
}

void removeSubscriptionsFor(UnsubscribeRequest request) {
@@ -226,20 +204,12 @@ void removeSubscriptionsFor(UnsubscribeRequest request) {
subscriptionsForName.removeAll(toRemove);

if (subscriptionsForName.isEmpty()) {
this.sharedSubscriptions.remove(request.getSharedName());
sharedSubscriptions = sharedSubscriptions.minus(request.getSharedName());
} else {
this.sharedSubscriptions.replace(request.getSharedName(), subscriptionsForName);
sharedSubscriptions = sharedSubscriptions.plus(request.getSharedName(), subscriptionsForName);
}
} else {
// collect Subscription instances to remove
Set<Subscription> toRemove = new HashSet<>();
for (Subscription sub : this.subscriptions) {
if (sub.clientId.equals(clientId)) {
toRemove.add(sub);
}
}
// effectively remove the instances
this.subscriptions.removeAll(toRemove);
subscriptions = subscriptions.minus(clientId);
}
}

@@ -248,11 +218,4 @@ public int compareTo(CNode o) {
return token.compareTo(o.token);
}

public List<Subscription> sharedAndNonSharedSubscriptions() {
List<Subscription> shared = sharedSubscriptions();
List<Subscription> returnedSubscriptions = new ArrayList<>(subscriptions.size() + shared.size());
returnedSubscriptions.addAll(subscriptions);
returnedSubscriptions.addAll(shared);
return returnedSubscriptions;
}
}
39 changes: 26 additions & 13 deletions broker/src/main/java/io/moquette/broker/subscriptions/CTrie.java
Original file line number Diff line number Diff line change
@@ -118,6 +118,7 @@ public SubscriptionIdentifier getSubscriptionIdentifier() {
* Models a request to unsubscribe a client, it's carrier for the Subscription
* */
public final static class UnsubscribeRequest {

private final Topic topicFilter;
private final String clientId;
private boolean shared = false;
@@ -231,44 +232,56 @@ private NavigationAction evaluate(Topic topicName, CNode cnode, int depth) {
return NavigationAction.STOP;
}

public List<Subscription> recursiveMatch(Topic topicName) {
return recursiveMatch(topicName, this.root, 0);
public SubscriptionCollection recursiveMatch(Topic topicName) {
SubscriptionCollection subscriptions = new SubscriptionCollection();
recursiveMatch(topicName, this.root, 0, subscriptions);
return subscriptions;
}

private List<Subscription> recursiveMatch(Topic topicName, INode inode, int depth) {
private void recursiveMatch(Topic topicName, INode inode, int depth, SubscriptionCollection target) {
CNode cnode = inode.mainNode();
if (cnode instanceof TNode) {
return Collections.emptyList();
return;
}
NavigationAction action = evaluate(topicName, cnode, depth);
if (action == NavigationAction.MATCH) {
return cnode.sharedAndNonSharedSubscriptions();
target.addNormalSubscriptions(cnode.getSubscriptions());
target.addSharedSubscriptions(cnode.getSharedSubscriptions());
return;
}
if (action == NavigationAction.STOP) {
return Collections.emptyList();
return;
}
Topic remainingTopic = (ROOT.equals(cnode.getToken())) ? topicName : topicName.exceptHeadToken();
List<Subscription> subscriptions = new ArrayList<>();
final boolean isRoot = ROOT.equals(cnode.getToken());
final boolean isSingle = Token.SINGLE.equals(cnode.getToken());
final boolean isMulti = Token.MULTI.equals(cnode.getToken());

Topic remainingTopic = isRoot
? topicName
: (isSingle || isMulti)
? topicName.exceptFullHeadToken()
: topicName.exceptHeadToken();
SubscriptionCollection subscriptions = new SubscriptionCollection();

// We should only consider the maximum three children children of
// type #, + or exact match
Optional<INode> subInode = cnode.childOf(Token.MULTI);
if (subInode.isPresent()) {
subscriptions.addAll(recursiveMatch(remainingTopic, subInode.get(), depth + 1));
recursiveMatch(remainingTopic, subInode.get(), depth + 1, target);
}
subInode = cnode.childOf(Token.SINGLE);
if (subInode.isPresent()) {
subscriptions.addAll(recursiveMatch(remainingTopic, subInode.get(), depth + 1));
recursiveMatch(remainingTopic, subInode.get(), depth + 1, target);
}
if (remainingTopic.isEmpty()) {
subscriptions.addAll(cnode.sharedAndNonSharedSubscriptions());
target.addNormalSubscriptions(cnode.getSubscriptions());
target.addSharedSubscriptions(cnode.getSharedSubscriptions());
} else {
subInode = cnode.childOf(remainingTopic.headToken());
if (subInode.isPresent()) {
subscriptions.addAll(recursiveMatch(remainingTopic, subInode.get(), depth + 1));
recursiveMatch(remainingTopic, subInode.get(), depth + 1, target);
}
}
return subscriptions;
}

/**
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -78,34 +78,10 @@ public void init(ISubscriptionsRepository subscriptionsRepository) {
* @return the list of matching subscriptions, or empty if not matching.
*/
@Override
public List<Subscription> matchWithoutQosSharpening(Topic topicName) {
public SubscriptionCollection matchWithoutQosSharpening(Topic topicName) {
return ctrie.recursiveMatch(topicName);
}

@Override
public List<Subscription> matchQosSharpening(Topic topicName) {
final List<Subscription> subscriptions = matchWithoutQosSharpening(topicName);

// for each session select the subscription with higher QoS
return selectSubscriptionsWithHigherQoSForEachSession(subscriptions);
}

private static List<Subscription> selectSubscriptionsWithHigherQoSForEachSession(List<Subscription> subscriptions) {
// for each session select the subscription with higher QoS
Map<String, Subscription> subsGroupedByClient = new HashMap<>();
for (Subscription sub : subscriptions) {
// If same client is subscribed to two different shared subscription that overlaps
// then it has to return both subscriptions, because the share name made them independent.
final String key = sub.clientAndShareName();
Subscription existingSub = subsGroupedByClient.get(key);
// update the selected subscriptions if not present or if it has a greater qos
if (existingSub == null || existingSub.qosLessThan(sub)) {
subsGroupedByClient.put(key, sub);
}
}
return new ArrayList<>(subsGroupedByClient.values());
}

@Override
public boolean add(String clientId, Topic filter, MqttSubscriptionOption option) {
SubscriptionRequest subRequest = SubscriptionRequest.buildNonShared(clientId, filter, option);
Original file line number Diff line number Diff line change
@@ -31,22 +31,21 @@ private String prettySubscriptions(CNode node) {
if (node instanceof TNode) {
return "TNode";
}
if (node.subscriptions().isEmpty()) {
if (node.getSubscriptions().isEmpty()) {
return StringUtil.EMPTY_STRING;
}
StringBuilder subScriptionsStr = new StringBuilder(" ~~[");
int counter = 0;
for (Subscription couple : node.subscriptions()) {
for (Subscription couple : node.getSubscriptions().values()) {
subScriptionsStr
.append("{filter=").append(couple.topicFilter).append(", ")
.append("option=").append(couple.option()).append(", ")
.append("client='").append(couple.clientId).append("'}");
counter++;
if (counter < node.subscriptions().size()) {
subScriptionsStr.append(";");
}
subScriptionsStr.append(";");
}
return subScriptionsStr.append("]").toString();
final int length = subScriptionsStr.length();
return subScriptionsStr.replace(length - 1, length, "]").toString();
}

private String indentTabs(int deep) {
Original file line number Diff line number Diff line change
@@ -27,9 +27,7 @@ public interface ISubscriptionsDirectory {

void init(ISubscriptionsRepository sessionsRepository);

List<Subscription> matchWithoutQosSharpening(Topic topic);

List<Subscription> matchQosSharpening(Topic topic);
SubscriptionCollection matchWithoutQosSharpening(Topic topic);

boolean add(String clientId, Topic filter, MqttSubscriptionOption option);

Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@
* Shared subscription's name.
*/
// It's public because used by PostOffice
public final class ShareName {
public final class ShareName implements Comparable<ShareName>{
private final String shareName;

public ShareName(String shareName) {
@@ -36,8 +36,8 @@ public boolean equals(Object o) {
return Objects.equals(shareName, (String) o);
}
if (getClass() != o.getClass()) return false;
ShareName shareName1 = (ShareName) o;
return Objects.equals(shareName, shareName1.shareName);
ShareName oShareName = (ShareName) o;
return Objects.equals(shareName, oShareName.shareName);
}

public String getShareName() {
@@ -55,4 +55,9 @@ public String toString() {
"shareName='" + shareName + '\'' +
'}';
}

@Override
public int compareTo(ShareName o) {
return shareName.compareTo(o.shareName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.broker.subscriptions;

import static io.moquette.broker.subscriptions.CNode.SECURE_RANDOM;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

/**
* A wrapper over multiple maps of normal subscriptions.
*/
public class SubscriptionCollection implements Iterable<Subscription> {

private final List<Map<String, Subscription>> normalSubscriptions = new ArrayList<>();
private final List<Map<ShareName, List<SharedSubscription>>> sharedSubscriptions = new ArrayList<>();

public boolean isEmpty() {
return normalSubscriptions.isEmpty() && sharedSubscriptions.isEmpty();
}

/**
* Calculates the number of subscriptions. Expensive, only use for tests!
* @return the number of subscriptions.
*/
public int size() {
int total = 0;
for (Map<String, Subscription> var : normalSubscriptions) {
total += var.size();
}
for (Map<ShareName, List<SharedSubscription>> var : sharedSubscriptions) {
total += var.size();
}
return total;
}

public void addNormalSubscriptions(Map<String, Subscription> subs) {
if (subs.isEmpty()) {
return;
}
normalSubscriptions.add(subs);
}

public void addSharedSubscriptions(Map<ShareName, List<SharedSubscription>> subs) {
if (sharedSubscriptions.isEmpty()) {
return;
}
sharedSubscriptions.add(subs);
}

private static Subscription selectRandom(List<SharedSubscription> list) {
// select a subscription randomly
int randIdx = SECURE_RANDOM.nextInt(list.size());
return list.get(randIdx).createSubscription();
}

@Override
public Iterator<Subscription> iterator() {
return new IteratorImpl(this);
}

private static class IteratorImpl implements Iterator<Subscription> {

private Iterator<Map<String, Subscription>> normapSubListIter;
private Iterator<Subscription> normalSubIter;

private Iterator<Map<ShareName, List<SharedSubscription>>> sharedSubMapIter;
private Iterator<List<SharedSubscription>> sharedSubIter;

public IteratorImpl(SubscriptionCollection parent) {
normapSubListIter = parent.normalSubscriptions.iterator();
sharedSubMapIter = parent.sharedSubscriptions.iterator();
}

@Override
public boolean hasNext() {
if (normalSubIter != null && normalSubIter.hasNext()) {
return true;
}
if (sharedSubIter != null && sharedSubIter.hasNext()) {
return true;
}
if (normapSubListIter != null) {
if (normapSubListIter.hasNext()) {
// Get the next normal subscriptions iterator.
Map<String, Subscription> next = normapSubListIter.next();
normalSubIter = next.values().iterator();
return true;
} else {
// Reached the end of the normal subscriptions lists.
normapSubListIter = null;
}
}
if (sharedSubMapIter != null) {
if (sharedSubMapIter.hasNext()) {
Map<ShareName, List<SharedSubscription>> next = sharedSubMapIter.next();
sharedSubIter = next.values().iterator();
return true;
} else {
sharedSubMapIter = null;
}
}
return false;
}

@Override
public Subscription next() {
if (normalSubIter != null) {
return normalSubIter.next();
}
if (sharedSubIter != null) {
return selectRandom(sharedSubIter.next());
}
throw new NoSuchElementException("Fetched past the end of Iterator, make sure to call hasNext!");
}
}

}
Original file line number Diff line number Diff line change
@@ -19,11 +19,11 @@

class SubscriptionCounterVisitor implements CTrie.IVisitor<Integer> {

private AtomicInteger accumulator = new AtomicInteger(0);
private final AtomicInteger accumulator = new AtomicInteger(0);

@Override
public void visit(CNode node, int deep) {
accumulator.addAndGet(node.subscriptions().size());
accumulator.addAndGet(node.getSubscriptions().size());
}

@Override
14 changes: 14 additions & 0 deletions broker/src/main/java/io/moquette/broker/subscriptions/Token.java
Original file line number Diff line number Diff line change
@@ -25,15 +25,29 @@ public class Token implements Comparable<Token> {
static final Token MULTI = new Token("#");
static final Token SINGLE = new Token("+");
final String name;
boolean lastSubToken;

protected Token(String s) {
this(s, true);
}

protected Token(String s, boolean isLastSub) {
name = s;
lastSubToken = isLastSub;
}

protected String name() {
return name;
}

protected void setLastSubToken(boolean lastSubToken) {
this.lastSubToken = lastSubToken;
}

protected boolean isLastSubToken() {
return lastSubToken;
}

@Override
public int hashCode() {
int hash = 7;
76 changes: 66 additions & 10 deletions broker/src/main/java/io/moquette/broker/subscriptions/Topic.java
Original file line number Diff line number Diff line change
@@ -31,6 +31,8 @@ public class Topic implements Serializable, Comparable<Topic> {

private static final Logger LOG = LoggerFactory.getLogger(Topic.class);

public static int MAX_TOKEN_LENGTH = 4;

private static final long serialVersionUID = 2438799283749822L;

private final String topic;
@@ -55,7 +57,7 @@ public Topic(String topic) {

Topic(List<Token> tokens) {
this.tokens = tokens;
List<String> strTokens = tokens.stream().map(Token::toString).collect(Collectors.toList());
List<String> strTokens = fullTokens().stream().map(Token::toString).collect(Collectors.toList());
this.topic = String.join("/", strTokens);
this.valid = true;
}
@@ -74,7 +76,24 @@ public List<Token> getTokens() {
return tokens;
}

private List<Token> parseTopic(String topic) throws ParseException {
public List<Token> fullTokens() {
List<Token> fullTokens = new ArrayList<>();
String currentToken = null;
for (Token token : getTokens()) {
if (currentToken == null) {
currentToken = token.name;
} else {
currentToken += token.name;
}
if (token.isLastSubToken()) {
fullTokens.add(new Token(currentToken, true));
currentToken = null;
}
}
return fullTokens;
}

private static List<Token> parseTopic(String topic) throws ParseException {
if (topic.length() == 0) {
throw new ParseException("Bad format of topic, topic MUST be at least 1 character [MQTT-4.7.3-1] and " +
"this was empty", 0);
@@ -117,7 +136,18 @@ private List<Token> parseTopic(String topic) throws ParseException {
} else if (s.contains("+")) {
throw new ParseException("Bad format of topic, invalid subtopic name: " + s, i);
} else {
res.add(new Token(s));
final int l = s.length();
int start = 0;
Token token = null;
while (start < l) {
int end = Math.min(start + MAX_TOKEN_LENGTH, l);
final String subToken = s.substring(start, end);
token = new Token(subToken, false);
res.add(token);
start = end;
}
// Can't be null because s can't be empty.
token.setLastSubToken(true);
}
}

@@ -151,6 +181,22 @@ public Topic exceptHeadToken() {
return new Topic(tokensCopy);
}

/**
* @return a new Topic corresponding to this less than the full head token, skipping any sub-tokens.
*/
public Topic exceptFullHeadToken() {
List<Token> tokens = getTokens();
if (tokens.isEmpty()) {
return new Topic(Collections.emptyList());
}
List<Token> tokensCopy = new ArrayList<>(tokens);
Token removed;
do {
removed = tokensCopy.remove(0);
} while (!removed.isLastSubToken() && !tokensCopy.isEmpty());
return new Topic(tokensCopy);
}

public boolean isValid() {
if (tokens == null)
getTokens();
@@ -169,27 +215,37 @@ public boolean isValid() {
public boolean match(Topic subscriptionTopic) {
List<Token> msgTokens = getTokens();
List<Token> subscriptionTokens = subscriptionTopic.getTokens();
// Due to sub-tokens and the + wildcard, indexes may differ.
int i = 0;
for (; i < subscriptionTokens.size(); i++) {
int m = 0;
for (; i < subscriptionTokens.size(); i++, m++) {
Token subToken = subscriptionTokens.get(i);
if (!Token.MULTI.equals(subToken) && !Token.SINGLE.equals(subToken)) {
if (i >= msgTokens.size()) {
if (m >= msgTokens.size()) {
return false;
}
Token msgToken = msgTokens.get(i);
Token msgToken = msgTokens.get(m);
if (!msgToken.equals(subToken)) {
return false;
}
} else {
if (Token.MULTI.equals(subToken)) {
return true;
}
// if (Token.SINGLE.equals(subToken)) {
// // skip a step forward
// }
if (m >= msgTokens.size()) {
return false;
}
if (Token.SINGLE.equals(subToken)) {
// skip to the next full token in the message topic
Token msgToken = msgTokens.get(m);
while (!msgToken.isLastSubToken()) {
m++;
msgToken = msgTokens.get(m);
}
}
}
}
return i == msgTokens.size();
return m == msgTokens.size();
}

@Override
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@
import static io.moquette.broker.MQTTConnectionPublishTest.memorySessionsRepository;
import static io.moquette.BrokerConstants.NO_BUFFER_FLUSH;
import static io.moquette.broker.PostOfficeUnsubscribeTest.CONFIG;
import io.moquette.broker.subscriptions.SubscriptionCollection;
import static io.netty.handler.codec.mqtt.MqttQoS.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singleton;
@@ -338,7 +339,7 @@ protected void subscribe(MQTTConnection connection, String topic, MqttQoS desire
final String clientId = connection.getClientId();
Subscription expectedSubscription = new Subscription(clientId, new Topic(topic), MqttSubscriptionOption.onlyFromQos(desiredQos));

final List<Subscription> matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic));
final SubscriptionCollection matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic));
assertEquals(1, matchedSubscriptions.size());
final Subscription onlyMatchedSubscription = matchedSubscriptions.iterator().next();
assertEquals(expectedSubscription, onlyMatchedSubscription);
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@
import static io.moquette.broker.MQTTConnectionPublishTest.memorySessionsRepository;
import static io.moquette.BrokerConstants.NO_BUFFER_FLUSH;
import static io.moquette.broker.PostOfficeUnsubscribeTest.CONFIG;
import io.moquette.broker.subscriptions.SubscriptionCollection;
import static io.netty.handler.codec.mqtt.MqttQoS.AT_LEAST_ONCE;
import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE;
import static io.netty.handler.codec.mqtt.MqttQoS.EXACTLY_ONCE;
@@ -198,7 +199,7 @@ protected void subscribe(MQTTConnection connection, String topic, MqttQoS desire
final String clientId = connection.getClientId();
Subscription expectedSubscription = new Subscription(clientId, new Topic(topic), MqttSubscriptionOption.onlyFromQos(desiredQos));

final List<Subscription> matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic));
final SubscriptionCollection matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic));
assertEquals(1, matchedSubscriptions.size());
final Subscription onlyMatchedSubscription = matchedSubscriptions.iterator().next();
assertEquals(expectedSubscription, onlyMatchedSubscription);
Original file line number Diff line number Diff line change
@@ -43,6 +43,7 @@
import static io.moquette.BrokerConstants.NO_BUFFER_FLUSH;
import static io.moquette.broker.PostOfficePublishTest.ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID;
import static io.moquette.broker.PostOfficePublishTest.SUBSCRIBER_ID;
import io.moquette.broker.subscriptions.SubscriptionCollection;
import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE;
import static io.netty.handler.codec.mqtt.MqttQoS.EXACTLY_ONCE;
import static java.util.Collections.singleton;
@@ -146,7 +147,7 @@ protected void subscribe(EmbeddedChannel channel, String topic, MqttQoS desiredQ
final String clientId = NettyUtils.clientID(channel);
Subscription expectedSubscription = new Subscription(clientId, new Topic(topic), MqttSubscriptionOption.onlyFromQos(desiredQos));

final List<Subscription> matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic));
final SubscriptionCollection matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic));
assertEquals(1, matchedSubscriptions.size());
final Subscription onlyMatchedSubscription = matchedSubscriptions.iterator().next();
assertEquals(expectedSubscription, onlyMatchedSubscription);
@@ -166,7 +167,7 @@ protected void subscribe(MQTTConnection connection, String topic, MqttQoS desire
final String clientId = connection.getClientId();
Subscription expectedSubscription = new Subscription(clientId, new Topic(topic), MqttSubscriptionOption.onlyFromQos(desiredQos));

final List<Subscription> matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic));
final SubscriptionCollection matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic));
assertEquals(1, matchedSubscriptions.size());
final Subscription onlyMatchedSubscription = matchedSubscriptions.iterator().next();
assertEquals(expectedSubscription, onlyMatchedSubscription);
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@
import static io.moquette.broker.MQTTConnectionPublishTest.memorySessionsRepository;
import static io.moquette.BrokerConstants.NO_BUFFER_FLUSH;
import static io.moquette.broker.PostOfficePublishTest.PUBLISHER_ID;
import io.moquette.broker.subscriptions.SubscriptionCollection;
import static io.netty.handler.codec.mqtt.MqttQoS.*;
import static java.util.Collections.*;
import java.util.List;
@@ -125,7 +126,7 @@ protected void subscribe(MQTTConnection connection, String topic, MqttQoS desire
final String clientId = connection.getClientId();
Subscription expectedSubscription = new Subscription(clientId, new Topic(topic), MqttSubscriptionOption.onlyFromQos(desiredQos));

final List<Subscription> matchedSubscriptions = subscriptions.matchQosSharpening(new Topic(topic));
final SubscriptionCollection matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic));
assertEquals(1, matchedSubscriptions.size());
//assertTrue(matchedSubscriptions.size() >=1);
final Subscription onlyMatchedSubscription = matchedSubscriptions.iterator().next();
Original file line number Diff line number Diff line change
@@ -54,7 +54,7 @@ public void whenManySharedSubscriptionsOfDifferentShareNameMatchATopicThenOneSub
sut.addShared("TempSensor1", new ShareName("temp_sensors"), asTopic("/livingroom"), asOption(MqttQoS.AT_MOST_ONCE));
sut.addShared("TempSensor1", new ShareName("livingroom_devices"), asTopic("/livingroom"), asOption(MqttQoS.AT_MOST_ONCE));

List<Subscription> matchingSubscriptions = sut.matchWithoutQosSharpening(asTopic("/livingroom"));
SubscriptionCollection matchingSubscriptions = sut.matchWithoutQosSharpening(asTopic("/livingroom"));
assertThat(matchingSubscriptions)
.containsOnly(SubscriptionTestUtils.asSubscription("TempSensor1", "/livingroom", "temp_sensors"),
SubscriptionTestUtils.asSubscription("TempSensor1", "/livingroom", "livingroom_devices"))
@@ -71,7 +71,7 @@ public void givenSessionHasMultipleSharedSubscriptionWhenTheClientIsRemovedThenN
sut.removeSharedSubscriptionsForClient(clientId);

// Verify
List<Subscription> matchingSubscriptions = sut.matchWithoutQosSharpening(asTopic("/livingroom"));
SubscriptionCollection matchingSubscriptions = sut.matchWithoutQosSharpening(asTopic("/livingroom"));
assertThat(matchingSubscriptions).isEmpty();
}

@@ -82,19 +82,19 @@ public void givenSubscriptionWithSubscriptionIdWhenNewSubscriptionIsProcessedThe
new SubscriptionIdentifier(1));

// verify it contains the subscription identifier
final List<Subscription> matchingSubscriptions = sut.matchQosSharpening(asTopic("client/test/b"));
final SubscriptionCollection matchingSubscriptions = sut.matchWithoutQosSharpening(asTopic("client/test/b"));
verifySubscriptionIdentifierIsPresent(matchingSubscriptions, new SubscriptionIdentifier(1), "share_temp");

// update the subscription of same clientId on same topic filter but with different subscription identifier
sut.addShared("client", new ShareName("share_temp"), asTopic("client/test/b"), asOption(MqttQoS.AT_MOST_ONCE),
new SubscriptionIdentifier(123));

// verify the subscription identifier is updated
final List<Subscription> reloadedSubscriptions = sut.matchQosSharpening(asTopic("client/test/b"));
final SubscriptionCollection reloadedSubscriptions = sut.matchWithoutQosSharpening(asTopic("client/test/b"));
verifySubscriptionIdentifierIsPresent(reloadedSubscriptions, new SubscriptionIdentifier(123), "share_temp");
}

private static void verifySubscriptionIdentifierIsPresent(List<Subscription> matchingSubscriptions, SubscriptionIdentifier subscriptionIdentifier, String expectedShareName) {
private static void verifySubscriptionIdentifierIsPresent(SubscriptionCollection matchingSubscriptions, SubscriptionIdentifier subscriptionIdentifier, String expectedShareName) {
assertAll("subscription contains the subscription identifier",
() -> assertEquals(1, matchingSubscriptions.size()),
() -> assertEquals(expectedShareName, matchingSubscriptions.iterator().next().shareName),
@@ -111,13 +111,13 @@ public void givenSubscriptionWithSubscriptionIdWhenNewSubscriptionWithoutSubscri

// verify it contains the subscription identifier
SubscriptionIdentifier expectedSubscriptionId = new SubscriptionIdentifier(1);
verifySubscriptionIdentifierIsPresent(sut.matchQosSharpening(asTopic("client/test/b")), expectedSubscriptionId, "share_temp");
verifySubscriptionIdentifierIsPresent(sut.matchWithoutQosSharpening(asTopic("client/test/b")), expectedSubscriptionId, "share_temp");

// update the subscription of same clientId on same topic filter but removing subscription identifier
sut.addShared("client", new ShareName("share_temp"), asTopic("client/test/b"), asOption(MqttQoS.AT_MOST_ONCE));

// verify the subscription identifier is removed
final List<Subscription> reloadedSubscriptions = sut.matchQosSharpening(asTopic("client/test/b"));
final SubscriptionCollection reloadedSubscriptions = sut.matchWithoutQosSharpening(asTopic("client/test/b"));
assertAll("subscription doesn't contain subscription identifier",
() -> assertEquals(1, reloadedSubscriptions.size()),
() -> assertFalse(reloadedSubscriptions.iterator().next().hasSubscriptionIdentifier())

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -15,7 +15,6 @@
*/
package io.moquette.broker.subscriptions;


import io.moquette.broker.ISubscriptionsRepository;
import io.moquette.persistence.MemorySubscriptionsRepository;
import io.netty.handler.codec.mqtt.MqttQoS;
@@ -27,6 +26,7 @@

import static io.moquette.broker.subscriptions.CTrieSharedSubscriptionDirectoryMatchingTest.asOption;
import static io.moquette.broker.subscriptions.Topic.asTopic;
import java.util.ArrayList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

@@ -198,7 +198,7 @@ public void testOverlappingSubscriptions() {
sut.add(specificSub.clientId, specificSub.topicFilter, specificSub.option());

//Exercise
final List<Subscription> matchingForSpecific = sut.matchQosSharpening(asTopic("a/b"));
final SubscriptionCollection matchingForSpecific = sut.matchWithoutQosSharpening(asTopic("a/b"));

// Verify
assertThat(matchingForSpecific.size()).isEqualTo(1);
@@ -226,7 +226,7 @@ public void removeSubscription_sameClients_subscribedSameTopic() {
sut.removeSubscription(asTopic("/topic"), "Sensor1");

// Verify
final List<Subscription> matchingSubscriptions = sut.matchWithoutQosSharpening(asTopic("/topic"));
final SubscriptionCollection matchingSubscriptions = sut.matchWithoutQosSharpening(asTopic("/topic"));
assertThat(matchingSubscriptions).isEmpty();
}

@@ -244,14 +244,17 @@ public void duplicatedSubscriptionsWithDifferentQos() {
this.sut.add("client1", asTopic("client/test/b"), asOption(MqttQoS.EXACTLY_ONCE));

// Verify
List<Subscription> subscriptions = this.sut.matchQosSharpening(asTopic("client/test/b"));
SubscriptionCollection subscriptions = this.sut.matchWithoutQosSharpening(asTopic("client/test/b"));
assertThat(subscriptions).contains(client1SubQoS2);
assertThat(subscriptions).contains(client2Sub);

final Optional<Subscription> matchingClient1Sub = subscriptions
.stream()
.filter(s -> s.equals(client1SubQoS0))
.findFirst();
Optional<Subscription> matchingClient1Sub = Optional.empty();
for (Subscription sub : subscriptions) {
if (sub.equals(client1SubQoS0)) {
matchingClient1Sub = Optional.of(sub);
break;
}
}
assertTrue(matchingClient1Sub.isPresent());
Subscription client1Sub = matchingClient1Sub.get();

@@ -267,18 +270,18 @@ public void givenSubscriptionWithSubscriptionIdWhenNewSubscriptionIsProcessedThe
sut.add("client", asTopic("client/test/b"), asOption(MqttQoS.AT_MOST_ONCE), new SubscriptionIdentifier(1));

// verify it contains the subscription identifier
final List<Subscription> matchingSubscriptions = sut.matchQosSharpening(asTopic("client/test/b"));
final SubscriptionCollection matchingSubscriptions = sut.matchWithoutQosSharpening(asTopic("client/test/b"));
verifySubscriptionIdentifierIsPresent(matchingSubscriptions, new SubscriptionIdentifier(1));

// update the subscription of same clientId on same topic filter but with different subscription identifier
sut.add("client", asTopic("client/test/b"), asOption(MqttQoS.AT_MOST_ONCE), new SubscriptionIdentifier(123));

// verify the subscription identifier is updated
final List<Subscription> reloadedSubscriptions = sut.matchQosSharpening(asTopic("client/test/b"));
final SubscriptionCollection reloadedSubscriptions = sut.matchWithoutQosSharpening(asTopic("client/test/b"));
verifySubscriptionIdentifierIsPresent(reloadedSubscriptions, new SubscriptionIdentifier(123));
}

private static void verifySubscriptionIdentifierIsPresent(List<Subscription> matchingSubscriptions, SubscriptionIdentifier subscriptionIdentifier) {
private static void verifySubscriptionIdentifierIsPresent(SubscriptionCollection matchingSubscriptions, SubscriptionIdentifier subscriptionIdentifier) {
assertAll("subscription contains the subscription identifier",
() -> assertEquals(1, matchingSubscriptions.size()),
() -> assertTrue(matchingSubscriptions.iterator().next().hasSubscriptionIdentifier()),
@@ -293,13 +296,13 @@ public void givenSubscriptionWithSubscriptionIdWhenNewSubscriptionWithoutSubscri

// verify it contains the subscription identifier
SubscriptionIdentifier expectedSubscriptionId = new SubscriptionIdentifier(1);
verifySubscriptionIdentifierIsPresent(sut.matchQosSharpening(asTopic("client/test/b")), expectedSubscriptionId);
verifySubscriptionIdentifierIsPresent(sut.matchWithoutQosSharpening(asTopic("client/test/b")), expectedSubscriptionId);

// update the subscription of same clientId on same topic filter but removing subscription identifier
sut.add("client", asTopic("client/test/b"), asOption(MqttQoS.AT_MOST_ONCE));

// verify the subscription identifier is removed
final List<Subscription> reloadedSubscriptions = sut.matchQosSharpening(asTopic("client/test/b"));
final SubscriptionCollection reloadedSubscriptions = sut.matchWithoutQosSharpening(asTopic("client/test/b"));
assertAll("subscription doesn't contain subscription identifier",
() -> assertEquals(1, reloadedSubscriptions.size()),
() -> assertFalse(reloadedSubscriptions.iterator().next().hasSubscriptionIdentifier())
Original file line number Diff line number Diff line change
@@ -15,7 +15,6 @@
*/
package io.moquette.broker.subscriptions;


import io.moquette.broker.subscriptions.CTrie.SubscriptionRequest;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.handler.codec.mqtt.MqttSubscriptionOption;
@@ -26,11 +25,11 @@

import static io.moquette.broker.subscriptions.SubscriptionTestUtils.asSubscription;
import static io.moquette.broker.subscriptions.Topic.asTopic;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.pcollections.PMap;

public class CTrieTest {

@@ -51,15 +50,15 @@ public void testAddOnSecondLayerWithEmptyTokenOnEmptyTree() {
final Optional<CNode> matchedNode = sut.lookup(asTopic("/"));
assertTrue(matchedNode.isPresent(), "Node on path / must be present");
//verify structure, only root INode and the first CNode should be present
assertThat(this.sut.root.mainNode().subscriptions()).isEmpty();
assertThat(this.sut.root.mainNode().getSubscriptions()).isEmpty();
assertThat(this.sut.root.mainNode().allChildren()).isNotEmpty();

INode firstLayer = this.sut.root.mainNode().allChildren().get(0);
assertThat(firstLayer.mainNode().subscriptions()).isEmpty();
INode firstLayer = this.sut.root.mainNode().allChildren().stream().findFirst().get();
assertThat(firstLayer.mainNode().getSubscriptions()).isEmpty();
assertThat(firstLayer.mainNode().allChildren()).isNotEmpty();

INode secondLayer = firstLayer.mainNode().allChildren().get(0);
assertThat(secondLayer.mainNode().subscriptions()).isNotEmpty();
INode secondLayer = firstLayer.mainNode().allChildren().stream().findFirst().get();
assertThat(secondLayer.mainNode().getSubscriptions()).isNotEmpty();
assertThat(secondLayer.mainNode().allChildren()).isEmpty();
}

@@ -72,7 +71,7 @@ public void testAddFirstLayerNodeOnEmptyTree() {
//Verify
final Optional<CNode> matchedNode = sut.lookup(asTopic("/temp"));
assertTrue(matchedNode.isPresent(), "Node on path /temp must be present");
assertFalse(matchedNode.get().subscriptions().isEmpty());
assertFalse(matchedNode.get().getSubscriptions().isEmpty());
}

@Test
@@ -99,8 +98,8 @@ public void testAddNewSubscriptionOnExistingNode() {
//Verify
final Optional<CNode> matchedNode = sut.lookup(asTopic("/temp"));
assertTrue(matchedNode.isPresent(), "Node on path /temp must be present");
final List<Subscription> subscriptions = matchedNode.get().subscriptions();
assertTrue(subscriptions.contains(asSubscription("TempSensor2", "/temp")));
final PMap<String, Subscription> subscriptions = matchedNode.get().getSubscriptions();
assertTrue(subscriptions.containsValue(asSubscription("TempSensor2", "/temp")));
}

@Test
@@ -117,8 +116,8 @@ public void testAddNewDeepNodes() {
//Verify
final Optional<CNode> matchedNode = sut.lookup(asTopic("/italy/happiness"));
assertTrue(matchedNode.isPresent(), "Node on path /italy/happiness must be present");
final List<Subscription> subscriptions = matchedNode.get().subscriptions();
assertTrue(subscriptions.contains(asSubscription("HappinessSensor", "/italy/happiness")));
final PMap<String, Subscription> subscriptions = matchedNode.get().getSubscriptions();
assertTrue(subscriptions.containsValue(asSubscription("HappinessSensor", "/italy/happiness")));
}

static SubscriptionRequest clientSubOnTopic(String clientID, String topicFilter) {
@@ -191,7 +190,7 @@ public void givenTreeWithSomeNodeHierarchyWhenRemoveContainedSubscriptionThenNod
sut.removeFromTree(CTrie.UnsubscribeRequest.buildNonShared("TempSensor1", asTopic("/temp/1")));

sut.removeFromTree(CTrie.UnsubscribeRequest.buildNonShared("TempSensor1", asTopic("/temp/1")));
final List<Subscription> matchingSubs = sut.recursiveMatch(asTopic("/temp/2"));
final SubscriptionCollection matchingSubs = sut.recursiveMatch(asTopic("/temp/2"));

//Verify
final Subscription expectedMatchingsub = new Subscription("TempSensor1", asTopic("/temp/2"), MqttSubscriptionOption.onlyFromQos(MqttQoS.AT_MOST_ONCE));
@@ -208,8 +207,8 @@ public void givenTreeWithSomeNodeHierarchWhenRemoveContainedSubscriptionSmallerT
//Exercise
sut.removeFromTree(CTrie.UnsubscribeRequest.buildNonShared("TempSensor1", asTopic("/temp")));

final List<Subscription> matchingSubs1 = sut.recursiveMatch(asTopic("/temp/1"));
final List<Subscription> matchingSubs2 = sut.recursiveMatch(asTopic("/temp/2"));
final SubscriptionCollection matchingSubs1 = sut.recursiveMatch(asTopic("/temp/1"));
final SubscriptionCollection matchingSubs2 = sut.recursiveMatch(asTopic("/temp/2"));

//Verify
// not clear to me, but I believe /temp unsubscribe should not unsub you from downstream /temp/1 or /temp/2
@@ -237,7 +236,7 @@ public void testMatchSubscriptionNoWildcards() {
sut.addToTree(newSubscription);

//Exercise
final List<Subscription> matchingSubs = sut.recursiveMatch(asTopic("/temp"));
final SubscriptionCollection matchingSubs = sut.recursiveMatch(asTopic("/temp"));

//Verify
final Subscription expectedMatchingsub = new Subscription("TempSensor1", asTopic("/temp"), MqttSubscriptionOption.onlyFromQos(MqttQoS.AT_MOST_ONCE));
@@ -252,8 +251,8 @@ public void testRemovalInnerTopicOffRootSameClient() {
sut.addToTree(newSubscription);

//Exercise
final List<Subscription> matchingSubs1 = sut.recursiveMatch(asTopic("temp"));
final List<Subscription> matchingSubs2 = sut.recursiveMatch(asTopic("temp/1"));
final SubscriptionCollection matchingSubs1 = sut.recursiveMatch(asTopic("temp"));
final SubscriptionCollection matchingSubs2 = sut.recursiveMatch(asTopic("temp/1"));

//Verify
final Subscription expectedMatchingsub1 = new Subscription("TempSensor1", asTopic("temp"), MqttSubscriptionOption.onlyFromQos(MqttQoS.AT_MOST_ONCE));
@@ -265,8 +264,8 @@ public void testRemovalInnerTopicOffRootSameClient() {
sut.removeFromTree(CTrie.UnsubscribeRequest.buildNonShared("TempSensor1", asTopic("temp")));

//Exercise
final List<Subscription> matchingSubs3 = sut.recursiveMatch(asTopic("temp"));
final List<Subscription> matchingSubs4 = sut.recursiveMatch(asTopic("temp/1"));
final SubscriptionCollection matchingSubs3 = sut.recursiveMatch(asTopic("temp"));
final SubscriptionCollection matchingSubs4 = sut.recursiveMatch(asTopic("temp/1"));

assertThat(matchingSubs3).doesNotContain(expectedMatchingsub1);
assertThat(matchingSubs4).contains(expectedMatchingsub2);
@@ -280,8 +279,8 @@ public void testRemovalInnerTopicOffRootDiffClient() {
sut.addToTree(newSubscription);

//Exercise
final List<Subscription> matchingSubs1 = sut.recursiveMatch(asTopic("temp"));
final List<Subscription> matchingSubs2 = sut.recursiveMatch(asTopic("temp/1"));
final SubscriptionCollection matchingSubs1 = sut.recursiveMatch(asTopic("temp"));
final SubscriptionCollection matchingSubs2 = sut.recursiveMatch(asTopic("temp/1"));

//Verify
final Subscription expectedMatchingsub1 = new Subscription("TempSensor1", asTopic("temp"), MqttSubscriptionOption.onlyFromQos(MqttQoS.AT_MOST_ONCE));
@@ -293,8 +292,8 @@ public void testRemovalInnerTopicOffRootDiffClient() {
sut.removeFromTree(CTrie.UnsubscribeRequest.buildNonShared("TempSensor1", asTopic("temp")));

//Exercise
final List<Subscription> matchingSubs3 = sut.recursiveMatch(asTopic("temp"));
final List<Subscription> matchingSubs4 = sut.recursiveMatch(asTopic("temp/1"));
final SubscriptionCollection matchingSubs3 = sut.recursiveMatch(asTopic("temp"));
final SubscriptionCollection matchingSubs4 = sut.recursiveMatch(asTopic("temp/1"));

assertThat(matchingSubs3).doesNotContain(expectedMatchingsub1);
assertThat(matchingSubs4).contains(expectedMatchingsub2);
@@ -308,8 +307,8 @@ public void testRemovalOuterTopicOffRootDiffClient() {
sut.addToTree(newSubscription);

//Exercise
final List<Subscription> matchingSubs1 = sut.recursiveMatch(asTopic("temp"));
final List<Subscription> matchingSubs2 = sut.recursiveMatch(asTopic("temp/1"));
final SubscriptionCollection matchingSubs1 = sut.recursiveMatch(asTopic("temp"));
final SubscriptionCollection matchingSubs2 = sut.recursiveMatch(asTopic("temp/1"));

//Verify
final Subscription expectedMatchingsub1 = new Subscription("TempSensor1", asTopic("temp"), MqttSubscriptionOption.onlyFromQos(MqttQoS.AT_MOST_ONCE));
@@ -321,8 +320,8 @@ public void testRemovalOuterTopicOffRootDiffClient() {
sut.removeFromTree(CTrie.UnsubscribeRequest.buildNonShared("TempSensor2", asTopic("temp/1")));

//Exercise
final List<Subscription> matchingSubs3 = sut.recursiveMatch(asTopic("temp"));
final List<Subscription> matchingSubs4 = sut.recursiveMatch(asTopic("temp/1"));
final SubscriptionCollection matchingSubs3 = sut.recursiveMatch(asTopic("temp"));
final SubscriptionCollection matchingSubs4 = sut.recursiveMatch(asTopic("temp/1"));

assertThat(matchingSubs3).contains(expectedMatchingsub1);
assertThat(matchingSubs4).doesNotContain(expectedMatchingsub2);
Original file line number Diff line number Diff line change
@@ -145,7 +145,7 @@ public TopicAssert doesNotMatch(String topic) {
}

public TopicAssert containsToken(Object... tokens) {
Assertions.assertThat(actual.getTokens()).containsExactly(asArray(tokens));
Assertions.assertThat(actual.fullTokens()).containsExactly(asArray(tokens));

return myself;
}
Original file line number Diff line number Diff line change
@@ -35,9 +35,13 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RequestResponseTest extends AbstractServerIntegrationWithoutClientFixture {

private static final Logger LOGGER = LoggerFactory.getLogger(RequestResponseTest.class.getName());

@Test
public void givenRequestResponseProtocolWhenRequestIsIssueThenTheResponderReply() throws InterruptedException {
final Mqtt5BlockingClient requester = createHiveBlockingClient("requester");
@@ -60,9 +64,11 @@ private static void responderRepliesToRequesterPublish(Mqtt5BlockingClient respo
.topicFilter("requester/door/open")
.qos(MqttQos.AT_LEAST_ONCE)
.build();
LOGGER.info("Subscribing to on requester/door/open");
responder.toAsync().subscribe(subscribeToRequest,
(Mqtt5Publish pub) -> {
assertTrue(pub.getResponseTopic().isPresent(), "Response topic MUST defined in request publish");
LOGGER.info("Responding on {}", pub.getResponseTopic().get());
Mqtt5PublishResult responseResult = responder.publishWith()
.topic(pub.getResponseTopic().get())
.payload("OK".getBytes(StandardCharsets.UTF_8))