Big refactory on session management:

- introduced a unique point of access and management of sessions state (SessionRepository)
- separated behaiour for sessions with clean flag to false into DurableSession
- separated behaviour for session with clean flag to true into TransientSession
This commit is contained in:
andsel
2017-10-14 17:37:26 +02:00
parent 6bacce6cec
commit 1b272e5849
43 changed files with 1107 additions and 1710 deletions
@@ -15,15 +15,16 @@
*/
package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple;
import java.util.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class CNode {
Token token;
private List<INode> children;
Set<ClientTopicCouple> subscriptions = new HashSet<>();
Set<Subscription> subscriptions = new HashSet<>();
//private int subtreeSubscriptions;
@@ -33,7 +34,7 @@ class CNode {
}
//Copy constructor
private CNode(Token token, List<INode> children, Set<ClientTopicCouple> subscriptions) {
private CNode(Token token, List<INode> children, Set<Subscription> subscriptions) {
this.token = token;
this.children = children;
this.subscriptions = subscriptions;
@@ -64,10 +65,7 @@ class CNode {
}
private boolean equals(Token token) {
if (this.token == null) {
return false;
}
return this.token.equals(token);
return this.token != null && this.token.equals(token);
}
CNode copy() {
@@ -79,7 +77,7 @@ class CNode {
}
CNode addSubscription(String clientId, Topic topic) {
this.subscriptions.add(new ClientTopicCouple(clientId, topic));
this.subscriptions.add(new Subscription(clientId, topic));
return this;
}
@@ -87,8 +85,8 @@ class CNode {
* @return true iff the subscriptions contained in this node are owned by clientId
* */
boolean containsOnly(String clientId) {
for (ClientTopicCouple sub : this.subscriptions) {
if (!sub.clientID.equals(clientId)) {
for (Subscription sub : this.subscriptions) {
if (!sub.clientId.equals(clientId)) {
return false;
}
}
@@ -97,8 +95,8 @@ class CNode {
//TODO this is equivalent to negate(containsOnly(clientId))
public boolean contains(String clientId) {
for (ClientTopicCouple sub : this.subscriptions) {
if (sub.clientID.equals(clientId)) {
for (Subscription sub : this.subscriptions) {
if (sub.clientId.equals(clientId)) {
return true;
}
}
@@ -106,23 +104,12 @@ class CNode {
}
void removeSubscriptionsFor(String clientId) {
Set<ClientTopicCouple> toRemove = new HashSet<>();
for (ClientTopicCouple sub : this.subscriptions) {
if (sub.clientID.equals(clientId)) {
Set<Subscription> toRemove = new HashSet<>();
for (Subscription sub : this.subscriptions) {
if (sub.clientId.equals(clientId)) {
toRemove.add(sub);
}
}
this.subscriptions.removeAll(toRemove);
}
// public List<INode> childrenMatching(Token token) {
// List<INode> res = new ArrayList<>();
// for (INode iNode : children) {
// final CNode child = iNode.mainNode();
// if (child.equals(token) || (child.token == Token.MULTI || child.token == Token.SINGLE)) {
// res.add(iNode);
// }
// }
// return res;
// }
}
@@ -15,9 +15,8 @@
*/
package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.ISubscriptionsStore;
import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple;
import io.moquette.spi.ClientSession;
import io.moquette.spi.impl.SessionsRepository;
import io.netty.util.internal.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -25,14 +24,16 @@ import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import static java.lang.String.format;
public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
private static final Logger LOG = LoggerFactory.getLogger(CTrieSubscriptionDirectory.class);
private static final Token ROOT = new Token("root");
protected INode root;
private volatile ISubscriptionsStore subscriptionsStore;
INode root;
private volatile SessionsRepository sessionsRepository;
private interface IVisitor<T> {
@@ -57,10 +58,10 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
}
StringBuilder subScriptionsStr = new StringBuilder(" ~~[");
int counter = 0;
for (ClientTopicCouple couple : node.subscriptions) {
for (Subscription couple : node.subscriptions) {
subScriptionsStr
.append("{filter=").append(couple.topicFilter).append(", ")
.append("client='").append(couple.clientID).append("'}");
.append("client='").append(couple.clientId).append("'}");
counter++;
if (counter < node.subscriptions.size()) {
subScriptionsStr.append(";");
@@ -90,24 +91,24 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
OK, REPEAT
}
public void init(ISessionsStore sessionsStore) {
public void init(SessionsRepository sessionsRepository) {
LOG.info("Initializing CTrie");
final CNode mainNode = new CNode();
mainNode.token = ROOT;
this.root = new INode(mainNode);
LOG.info("Initializing subscriptions store...");
this.subscriptionsStore = sessionsStore.subscriptionStore();
List<ClientTopicCouple> subscriptions = this.subscriptionsStore.listAllSubscriptions();
this.sessionsRepository = sessionsRepository;
// reload any subscriptions persisted
if (LOG.isTraceEnabled()) {
LOG.trace("Reloading all stored subscriptions. SubscriptionTree = {}", dumpTree());
}
for (ClientTopicCouple clientTopic : subscriptions) {
LOG.info("Re-subscribing client to topic CId={}, topicFilter={}", clientTopic.clientID,
clientTopic.topicFilter);
add(clientTopic);
for (ClientSession session : this.sessionsRepository.getAllSessions()) {
for (Subscription subscription : session.getSubscriptions()) {
LOG.info("Re-subscribing client to topic CId={}, topicFilter={}", subscription.clientId,
subscription.topicFilter);
add(subscription);
}
}
if (LOG.isTraceEnabled()) {
LOG.trace("Stored subscriptions have been reloaded. SubscriptionTree = {}", dumpTree());
@@ -145,25 +146,31 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
* @return the list of matching subscriptions, or empty if not matching.
*/
Set<Subscription> match(Topic topic) {
final Set<ClientTopicCouple> matchingSubs = recursiveMatch(topic, this.root);
final Set<Subscription> matchingSubs = recursiveMatch(topic, this.root);
// remove the overlapping subscriptions, selecting ones with greatest qos
Map<String, Subscription> subsForClient = new HashMap<>();
for (ClientTopicCouple matchingCouple : matchingSubs) {
Subscription existingSub = subsForClient.get(matchingCouple.clientID);
Subscription sub = this.subscriptionsStore.getSubscription(matchingCouple);
if (sub == null) {
// if the m_sessionStore hasn't the sub because the client disconnected
for (Subscription matchingSub : matchingSubs) {
Subscription existingSub = subsForClient.get(matchingSub.clientId);
final ClientSession subscribedSession = this.sessionsRepository.sessionForClient(matchingSub.clientId);
if (subscribedSession == null) {
//clean session disconnected
continue;
}
Subscription sub = subscribedSession.findSubscriptionByTopicFilter(matchingSub);
if (sub == null) {
final String excpMesg = format("Target session %s is connected but doesn't anymore subscribed to %s",
matchingSub.clientId, matchingSub);
throw new IllegalStateException(excpMesg);
}
// update the selected subscriptions if not present or if has a greater qos
if (existingSub == null || existingSub.getRequestedQos().value() < sub.getRequestedQos().value()) {
subsForClient.put(matchingCouple.clientID, sub);
if (existingSub == null || existingSub.qosLessThan(sub)) {
subsForClient.put(sub.clientId, sub);
}
}
return new HashSet<>(subsForClient.values());
}
Set<ClientTopicCouple> recursiveMatch(Topic topic, INode inode) {
Set<Subscription> recursiveMatch(Topic topic, INode inode) {
CNode cnode = inode.mainNode();
if (cnode.token == Token.MULTI) {
return cnode.subscriptions;
@@ -179,7 +186,7 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
return Collections.emptySet();
}
Topic remainingTopic = (cnode.token == ROOT) ? topic : topic.exceptHeadToken();
Set<ClientTopicCouple> subscriptions = new HashSet<>();
Set<Subscription> subscriptions = new HashSet<>();
if (remainingTopic.isEmpty()) {
subscriptions.addAll(cnode.subscriptions);
}
@@ -190,10 +197,10 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
}
public void add(ClientTopicCouple newSubscription) {
public void add(Subscription newSubscription) {
Action res;
do {
res = insert(newSubscription.clientID, newSubscription.topicFilter, this.root, newSubscription.topicFilter);
res = insert(newSubscription.clientId, newSubscription.topicFilter, this.root, newSubscription.topicFilter);
} while (res == Action.REPEAT);
}
@@ -15,16 +15,15 @@
*/
package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.ISubscriptionsStore;
import io.moquette.spi.impl.SessionsRepository;
import java.util.List;
public interface ISubscriptionsDirectory {
void init(ISessionsStore sessionsStore);
void init(SessionsRepository sessionsRepository);
void add(ISubscriptionsStore.ClientTopicCouple newSubscription);
void add(Subscription newSubscription);
void removeSubscription(Topic topic, String clientID);
@@ -16,8 +16,8 @@
package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple;
import io.netty.handler.codec.mqtt.MqttQoS;
import java.io.Serializable;
/**
@@ -26,10 +26,10 @@ import java.io.Serializable;
public final class Subscription implements Serializable {
private static final long serialVersionUID = -3383457629635732794L;
final MqttQoS requestedQos; // max QoS acceptable
private final MqttQoS requestedQos; // max QoS acceptable
final String clientId;
final Topic topicFilter;
final boolean active;
private final boolean active;
public Subscription(String clientId, Topic topicFilter, MqttQoS requestedQos) {
this.requestedQos = requestedQos;
@@ -45,6 +45,16 @@ public final class Subscription implements Serializable {
this.active = orig.active;
}
/**
* Constructor with undefined maximum QoS
* */
public Subscription(String clientId, Topic topicFilter) {
this.requestedQos = null;
this.clientId = clientId;
this.topicFilter = topicFilter;
this.active = true;
}
public String getClientId() {
return clientId;
}
@@ -61,6 +71,10 @@ public final class Subscription implements Serializable {
return active;
}
public boolean qosLessThan(Subscription sub) {
return requestedQos.value() < sub.requestedQos.value();
}
@Override
public boolean equals(Object o) {
if (this == o)
@@ -100,8 +114,4 @@ public final class Subscription implements Serializable {
return null;
}
}
public ClientTopicCouple asClientTopicCouple() {
return new ClientTopicCouple(this.clientId, this.topicFilter);
}
}
@@ -1,243 +0,0 @@
/*
* Copyright (c) 2012-2017 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.spi.impl.subscriptions;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.ISubscriptionsStore;
import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicReference;
/**
* Represents a tree of topics subscriptions.
*/
public class SubscriptionsDirectory implements ISubscriptionsDirectory {
static class NodeCouple {
final TreeNode root;
final TreeNode createdNode;
NodeCouple(TreeNode root, TreeNode createdNode) {
this.root = root;
this.createdNode = createdNode;
}
}
public interface IVisitor<T> {
void visit(TreeNode node, int deep);
T getResult();
}
private class DumpTreeVisitor implements IVisitor<String> {
String s = "";
@Override
public void visit(TreeNode node, int deep) {
StringBuilder subScriptionsStr = new StringBuilder();
String indentTabs = indentTabs(deep);
for (ClientTopicCouple couple : node.m_subscriptions) {
subScriptionsStr.append(indentTabs).append(couple.toString()).append("\n");
}
s += node.getToken() == null ? "" : node.getToken().toString();
s += "\n" + (node.m_subscriptions.isEmpty() ? indentTabs : "")
+ subScriptionsStr /* + "\n" */;
}
private String indentTabs(int deep) {
StringBuilder s = new StringBuilder();
for (int i = 0; i < deep; i++) {
s.append("\t");
// s += "--";
}
return s.toString();
}
@Override
public String getResult() {
return s;
}
}
private AtomicReference<TreeNode> subscriptions = new AtomicReference<>(new TreeNode());
private static final Logger LOG = LoggerFactory.getLogger(SubscriptionsDirectory.class);
private volatile ISubscriptionsStore subscriptionsStore;
/**
* Initialize the subscription tree with the list of subscriptions. Maintained for compatibility
* reasons.
*
* @param sessionsStore
* to be used as backing store from the subscription store.
*/
@Override
public void init(ISessionsStore sessionsStore) {
LOG.info("Initializing subscriptions store...");
this.subscriptionsStore = sessionsStore.subscriptionStore();
List<ClientTopicCouple> subscriptions = this.subscriptionsStore.listAllSubscriptions();
// reload any subscriptions persisted
if (LOG.isTraceEnabled()) {
LOG.trace("Reloading all stored subscriptions. SubscriptionTree = {}", dumpTree());
}
for (ClientTopicCouple clientTopic : subscriptions) {
LOG.info("Re-subscribing client to topic CId={}, topicFilter={}", clientTopic.clientID,
clientTopic.topicFilter);
add(clientTopic);
}
if (LOG.isTraceEnabled()) {
LOG.trace("Stored subscriptions have been reloaded. SubscriptionTree = {}", dumpTree());
}
}
@Override
public void add(ClientTopicCouple newSubscription) {
/*
* The topic filters have already been validated at the ProtocolProcessor. We can assume
* they are valid.
*/
TreeNode oldRoot;
NodeCouple couple;
do {
oldRoot = subscriptions.get();
couple = recreatePath(newSubscription.topicFilter, oldRoot);
couple.createdNode.addSubscription(newSubscription); //createdNode could be null?
couple.root.recalculateSubscriptionsSize();
//spin lock repeating till we can, swap root, if can't swap just re-do the operation
} while (!subscriptions.compareAndSet(oldRoot, couple.root));
LOG.debug("A subscription has been added. Root = {}, oldRoot = {}.", couple.root, oldRoot);
}
NodeCouple recreatePath(Topic topic, final TreeNode oldRoot) {
final TreeNode newRoot = oldRoot.copy();
TreeNode parent = newRoot;
TreeNode current = newRoot;
for (Token token : topic.getTokens()) {
TreeNode matchingChildren;
// check if a children with the same token already exists
if ((matchingChildren = current.childWithToken(token)) != null) {
// copy the traversed node
current = matchingChildren.copy();
// update the child just added in the children list
parent.updateChild(matchingChildren, current);
parent = current;
} else {
// create a new node for the newly inserted token
matchingChildren = new TreeNode();
matchingChildren.setToken(token);
current.addChild(matchingChildren);
current = matchingChildren;
}
}
return new NodeCouple(newRoot, current);
}
@Override
public void removeSubscription(Topic topic, String clientID) {
/*
* The topic filters have already been validated at the ProtocolProcessor. We can assume
* they are valid.
*/
TreeNode oldRoot;
NodeCouple couple;
do {
oldRoot = subscriptions.get();
couple = recreatePath(topic, oldRoot);
couple.createdNode.remove(new ClientTopicCouple(clientID, topic));
couple.root.recalculateSubscriptionsSize();
//spin lock repeating till we can, swap root, if can't swap just re-do the operation
} while (!subscriptions.compareAndSet(oldRoot, couple.root));
}
// /**
// * Visit the topics tree to remove matching subscriptions with clientID. It's a mutating
// * structure operation so create a new subscription tree (partial or total).
// *
// * @param clientID
// * the client ID to remove.
// */
// void removeForClient(String clientID) {
// TreeNode oldRoot;
// TreeNode newRoot;
// do {
// oldRoot = subscriptions.get();
// newRoot = oldRoot.removeClientSubscriptions(clientID);
// // spin lock repeating till we can, swap root, if can't swap just re-do the operation
// } while (!subscriptions.compareAndSet(oldRoot, newRoot));
// }
/**
* Given a topic string return the clients subscriptions that matches it. Topic string can't
* contain character # and + because they are reserved to listeners subscriptions, and not topic
* publishing.
*
* @param topic
* to use fo searching matching subscriptions.
* @return the list of matching subscriptions, or empty if not matching.
*/
@Override
public List<Subscription> matches(Topic topic) {
Queue<Token> tokenQueue = new LinkedBlockingDeque<>(topic.getTokens());
List<ClientTopicCouple> matchingSubs = new ArrayList<>();
subscriptions.get().matches(tokenQueue, matchingSubs);
// remove the overlapping subscriptions, selecting ones with greatest qos
Map<String, Subscription> subsForClient = new HashMap<>();
for (ClientTopicCouple matchingCouple : matchingSubs) {
Subscription existingSub = subsForClient.get(matchingCouple.clientID);
Subscription sub = this.subscriptionsStore.getSubscription(matchingCouple);
if (sub == null) {
// if the m_sessionStore hasn't the sub because the client disconnected
continue;
}
// update the selected subscriptions if not present or if has a greater qos
if (existingSub == null || existingSub.getRequestedQos().value() < sub.getRequestedQos().value()) {
subsForClient.put(matchingCouple.clientID, sub);
}
}
return new ArrayList<>(subsForClient.values());
}
@Override
public int size() {
return subscriptions.get().size();
}
@Override
public String dumpTree() {
DumpTreeVisitor visitor = new DumpTreeVisitor();
bfsVisit(subscriptions.get(), visitor, 0);
return visitor.getResult();
}
private void bfsVisit(TreeNode node, IVisitor<?> visitor, int deep) {
if (node == null) {
return;
}
visitor.visit(node, deep);
for (TreeNode child : node.m_children) {
bfsVisit(child, visitor, ++deep);
}
}
}
@@ -1,180 +0,0 @@
/*
* Copyright (c) 2012-2017 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.spi.impl.subscriptions;
import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
class TreeNode {
Token m_token;
List<TreeNode> m_children = new ArrayList<>();
Set<ClientTopicCouple> m_subscriptions = new HashSet<>();
private int subtreeSubscriptions;
TreeNode() {
}
Token getToken() {
return m_token;
}
void setToken(Token topic) {
this.m_token = topic;
}
void addSubscription(ClientTopicCouple s) {
m_subscriptions.add(s);
this.subtreeSubscriptions++;
}
void addChild(TreeNode child) {
m_children.add(child);
this.subtreeSubscriptions += child.subtreeSubscriptions;
}
/**
* Creates a shallow copy of the current node. Copy the token and the children.
*/
TreeNode copy() {
final TreeNode copy = new TreeNode();
copy.m_children = new ArrayList<>(m_children);
copy.m_subscriptions = new HashSet<>(m_subscriptions);
copy.m_token = m_token;
copy.subtreeSubscriptions = this.subtreeSubscriptions;
return copy;
}
/**
* Search for children that has the specified token, if not found return null;
*/
TreeNode childWithToken(Token token) {
for (TreeNode child : m_children) {
if (child.getToken().equals(token)) {
return child;
}
}
return null;
}
void updateChild(TreeNode oldChild, TreeNode newChild) {
m_children.remove(oldChild);
m_children.add(newChild);
this.subtreeSubscriptions += newChild.subtreeSubscriptions - oldChild.subtreeSubscriptions;
}
Collection<ClientTopicCouple> subscriptions() {
return m_subscriptions;
}
public void remove(ClientTopicCouple clientTopicCouple) {
m_subscriptions.remove(clientTopicCouple);
this.subtreeSubscriptions--;
}
// TODO smell a query method that return the result modifing the parameter (matchingSubs)
void matches(Queue<Token> tokens, List<ClientTopicCouple> matchingSubs) {
Token t = tokens.poll();
// check if t is null <=> tokens finished
if (t == null) {
matchingSubs.addAll(m_subscriptions);
// check if it has got a MULTI child and add its subscriptions
for (TreeNode n : m_children) {
if (n.getToken() == Token.MULTI || n.getToken() == Token.SINGLE) {
matchingSubs.addAll(n.subscriptions());
}
}
return;
}
// we are on MULTI, than add subscriptions and return
if (m_token == Token.MULTI) {
matchingSubs.addAll(m_subscriptions);
return;
}
for (TreeNode n : m_children) {
if (n.getToken().match(t)) {
// Create a copy of token, else if navigate 2 sibling it
// consumes 2 elements on the queue instead of one
n.matches(new LinkedBlockingQueue<>(tokens), matchingSubs);
// TODO don't create a copy n.matches(tokens, matchingSubs);
}
}
}
/**
* Return the number of registered subscriptions
*/
int size() {
return this.subtreeSubscriptions;
}
/**
* Create a copied subtree rooted on this node but purged of clientID's subscriptions.
*/
TreeNode removeClientSubscriptions(String clientID) {
// collect what to delete and then delete to avoid ConcurrentModification
TreeNode newSubRoot = this.copy();
remoteSubscriptions(clientID, newSubRoot);
removeSubscriptionFromChildren(clientID, newSubRoot);
return newSubRoot;
}
private void removeSubscriptionFromChildren(String clientID, TreeNode newSubRoot) {
int newSubtreeSubscriptions = 0;
//go deep
List<TreeNode> newChildren = new ArrayList<>(newSubRoot.m_children.size());
for (TreeNode child : newSubRoot.m_children) {
final TreeNode purgedSubtree = child.removeClientSubscriptions(clientID);
if (purgedSubtree.size() != 0) {
newSubtreeSubscriptions += purgedSubtree.size();
newChildren.add(purgedSubtree);
}
}
newSubRoot.m_children = newChildren;
newSubRoot.subtreeSubscriptions += newSubtreeSubscriptions;
}
private void remoteSubscriptions(String clientID, TreeNode newSubRoot) {
List<ClientTopicCouple> subsToRemove = new ArrayList<>();
for (ClientTopicCouple s : newSubRoot.m_subscriptions) {
if (s.clientID.equals(clientID)) {
subsToRemove.add(s);
}
}
for (ClientTopicCouple s : subsToRemove) {
newSubRoot.m_subscriptions.remove(s);
}
newSubRoot.subtreeSubscriptions = newSubRoot.m_subscriptions.size();
}
int recalculateSubscriptionsSize() {
int res = m_subscriptions.size();
for (TreeNode child : m_children) {
res += child.recalculateSubscriptionsSize();
}
this.subtreeSubscriptions = res;
return res;
}
}