diff --git a/broker/src/main/java/io/moquette/connections/IConnectionsManager.java b/broker/src/main/java/io/moquette/connections/IConnectionsManager.java index a19587e8..da20d252 100644 --- a/broker/src/main/java/io/moquette/connections/IConnectionsManager.java +++ b/broker/src/main/java/io/moquette/connections/IConnectionsManager.java @@ -55,14 +55,6 @@ public interface IConnectionsManager { */ boolean closeConnection(String clientID, boolean closeImmediately); - /** - * Returns the state of the session of a given client. - * - * @param clientID - * @return - */ - MqttSession getSessionStatus(String clientID); - /** * Returns the state of all the sessions * diff --git a/broker/src/main/java/io/moquette/persistence/MemorySessionStore.java b/broker/src/main/java/io/moquette/persistence/MemorySessionStore.java index 2c013840..c1c7306c 100644 --- a/broker/src/main/java/io/moquette/persistence/MemorySessionStore.java +++ b/broker/src/main/java/io/moquette/persistence/MemorySessionStore.java @@ -17,7 +17,6 @@ package io.moquette.persistence; import io.moquette.server.Constants; -import io.moquette.spi.ClientSession; import io.moquette.spi.IMessagesStore.StoredMessage; import io.moquette.spi.ISessionsStore; import io.moquette.spi.ISubscriptionsStore; @@ -30,7 +29,6 @@ import java.util.*; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore { @@ -38,24 +36,23 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore { class Session { final String clientID; - final ClientSession clientSession; final Map subscriptions = new ConcurrentHashMap<>(); - final AtomicReference persistentSession = new AtomicReference<>(null); + boolean cleanSession; final BlockingQueue queue = new ArrayBlockingQueue<>(Constants.MAX_MESSAGE_QUEUE); final Map secondPhaseStore = new ConcurrentHashMap<>(); final Map outboundFlightMessages = Collections.synchronizedMap(new HashMap()); final Map inboundFlightMessages = new ConcurrentHashMap<>(); - Session(String clientID, ClientSession clientSession) { + Session(String clientID, boolean cleanSession) { this.clientID = clientID; - this.clientSession = clientSession; + this.cleanSession = cleanSession; } } private final Map sessions = new ConcurrentHashMap<>(); - public MemorySessionStore() { + MemorySessionStore() { } private Session getSession(String clientID) { @@ -110,64 +107,57 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore { } @Override - public ClientSession createNewSession(String clientID, boolean cleanSession) { - LOG.debug("createNewSession for client <{}>", clientID); - Session session = sessions.get(clientID); - if (session != null) { - LOG.error("already exists a session for client <{}>, bad condition", clientID); - throw new IllegalArgumentException("Can't create a session with the ID of an already existing" + clientID); - } - LOG.debug("clientID {} is a newcome, creating it's empty subscriptions set", clientID); - session = new Session(clientID, new ClientSession(clientID, this, this, cleanSession)); - session.persistentSession.set(new PersistentSession(cleanSession)); - sessions.put(clientID, session); - return session.clientSession; + public void createNewDurableSession(String clientID) { + Session innerSession = new Session(clientID, false); + sessions.put(clientID, innerSession); } @Override - public ClientSession sessionForClient(String clientID) { - if (!sessions.containsKey(clientID)) { - LOG.error("Can't find the session for client <{}>", clientID); - return null; - } - - PersistentSession storedSession = sessions.get(clientID).persistentSession.get(); - return new ClientSession(clientID, this, this, storedSession.cleanSession); + public void removeDurableSession(String clientId) { + this.sessions.remove(clientId); + this.wipeSubscriptions(clientId); } @Override - public Collection getAllSessions() { - Collection result = new ArrayList<>(); + public void updateCleanStatus(String clientId, boolean newCleanStatus) { + sessions.get(clientId).cleanSession = newCleanStatus; + } + + @Override + public PersistentSession loadSessionByKey(String clientID) { + return new PersistentSession(clientID, sessions.get(clientID).cleanSession); + } + + @Override + public Collection listAllSessions() { + Collection result = new ArrayList<>(); for (Session entry : sessions.values()) { - result.add(new ClientSession(entry.clientID, this, this, entry.persistentSession.get().cleanSession)); + result.add(new PersistentSession(entry.clientID, entry.cleanSession)); } return result; } @Override - public void updateCleanStatus(String clientID, boolean cleanSession) { - if (!sessions.containsKey(clientID)) { - LOG.error("Can't find the session for client <{}>", clientID); - return; - } - - sessions.get(clientID).persistentSession.set(new PersistentSession(cleanSession)); - } - - @Override - public List listAllSubscriptions() { - List allSubscriptions = new ArrayList<>(); + public List listAllSubscriptions() { + List allSubscriptions = new ArrayList<>(); for (Session entry : sessions.values()) { - for (Subscription sub : entry.subscriptions.values()) { - allSubscriptions.add(sub.asClientTopicCouple()); - } + allSubscriptions.addAll(entry.subscriptions.values()); } return allSubscriptions; } @Override - public Subscription getSubscription(ClientTopicCouple couple) { - String clientID = couple.clientID; + public Collection listClientSubscriptions(String clientID) { + final Session session = sessions.get(clientID); + if (session == null) { + throw new IllegalStateException("Asking for subscriptions of not persisted client: " + clientID); + } + return session.subscriptions.values(); + } + + @Override + public Subscription reload(Subscription subcription) { + String clientID = subcription.getClientId(); if (!sessions.containsKey(clientID)) { LOG.error("Can't find the session for client <{}>", clientID); return null; @@ -177,17 +167,9 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore { if (subscriptions == null || subscriptions.isEmpty()) { return null; } - return subscriptions.get(couple.topicFilter); + return subscriptions.get(subcription.getTopicFilter()); } - @Override - public List getSubscriptions() { - List subscriptions = new ArrayList<>(); - for (Session entry : sessions.values()) { - subscriptions.addAll(entry.subscriptions.values()); - } - return subscriptions; - } @Override public StoredMessage inFlightAck(String clientID, int messageID) { @@ -234,7 +216,10 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore { @Override public void dropQueue(String clientID) { - sessions.get(clientID).queue.clear(); + final Session session = sessions.get(clientID); + session.queue.clear(); + session.outboundFlightMessages.clear(); + session.inboundFlightMessages.clear(); } @Override @@ -251,7 +236,7 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore { } @Override - public StoredMessage secondPhaseAcknowledged(String clientID, int messageID) { + public StoredMessage completeReleasedPublish(String clientID, int messageID) { LOG.info("Acknowledged message in second phase, clientID <{}> messageID {}", clientID, messageID); return getSession(clientID).secondPhaseStore.remove(messageID); } @@ -264,35 +249,13 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore { return 0; } - return session.inboundFlightMessages.size() + session.secondPhaseStore.size() - + session.outboundFlightMessages.size(); + return session.inboundFlightMessages.size() + +// countPubReleaseWaitingPubComplete(clientID) + + session.outboundFlightMessages.size(); } @Override - public StoredMessage inboundInflight(String clientID, int messageID) { - return getSession(clientID).inboundFlightMessages.get(messageID); - } - - @Override - public void markAsInboundInflight(String clientID, int messageID, StoredMessage msg) { - if (!sessions.containsKey(clientID)) - LOG.error("Can't find the session for client <{}>", clientID); - - sessions.get(clientID).inboundFlightMessages.put(messageID, msg); - } - - @Override - public int getPendingPublishMessagesNo(String clientID) { - if (!sessions.containsKey(clientID)) { - LOG.error("Can't find the session for client <{}>", clientID); - return 0; - } - - return sessions.get(clientID).queue.size(); - } - - @Override - public int getSecondPhaseAckPendingMessages(String clientID) { + public int countPubReleaseWaitingPubComplete(String clientID) { if (!sessions.containsKey(clientID)) { LOG.error("Can't find the session for client <{}>", clientID); return 0; @@ -302,7 +265,7 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore { } @Override - public void cleanSession(String clientID) { + public void removeTemporaryQoS2(String clientID) { LOG.debug("Session cleanup for client <{}>", clientID); Session session = sessions.get(clientID); @@ -311,18 +274,8 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore { return; } - // remove also the messages stored of type QoS1/2 - LOG.info("Removing stored messages with QoS 1 and 2. ClientId={}", clientID); - + LOG.info("Removing stored messages with QoS 2. ClientId={}", clientID); session.secondPhaseStore.clear(); - session.outboundFlightMessages.clear(); - session.inboundFlightMessages.clear(); - - LOG.info("Wiping existing subscriptions. ClientId={}", clientID); - wipeSubscriptions(clientID); - - //remove also the enqueued messages - dropQueue(clientID); // TODO this missing last step breaks the junit test //sessions.remove(clientID); diff --git a/broker/src/main/java/io/moquette/persistence/PersistentSession.java b/broker/src/main/java/io/moquette/persistence/PersistentSession.java index cab0ba21..bbd39565 100644 --- a/broker/src/main/java/io/moquette/persistence/PersistentSession.java +++ b/broker/src/main/java/io/moquette/persistence/PersistentSession.java @@ -25,8 +25,10 @@ public class PersistentSession implements Serializable { private static final long serialVersionUID = 5052054783220481854L; public final boolean cleanSession; + public final String clientID; - public PersistentSession(boolean cleanSession) { + public PersistentSession(String clientID, boolean cleanSession) { + this.clientID = clientID; this.cleanSession = cleanSession; } } diff --git a/broker/src/main/java/io/moquette/server/ConnectionDescriptor.java b/broker/src/main/java/io/moquette/server/ConnectionDescriptor.java index e18a6d8b..33b20328 100644 --- a/broker/src/main/java/io/moquette/server/ConnectionDescriptor.java +++ b/broker/src/main/java/io/moquette/server/ConnectionDescriptor.java @@ -126,7 +126,7 @@ public class ConnectionDescriptor { @Override public String toString() { - return "ConnectionDescriptor{" + "clientID=" + clientID + ", cleanSession=" + cleanSession + ", state=" + return "ConnectionDescriptor{" + "clientID=" + clientID + ", removeTemporaryQoS2=" + cleanSession + ", state=" + channelState.get() + '}'; } diff --git a/broker/src/main/java/io/moquette/server/ConnectionDescriptorStore.java b/broker/src/main/java/io/moquette/server/ConnectionDescriptorStore.java index 7f0f4dea..209ee96c 100644 --- a/broker/src/main/java/io/moquette/server/ConnectionDescriptorStore.java +++ b/broker/src/main/java/io/moquette/server/ConnectionDescriptorStore.java @@ -23,12 +23,13 @@ import io.moquette.connections.MqttSubscription; import io.moquette.server.netty.metrics.BytesMetrics; import io.moquette.server.netty.metrics.MessageMetrics; import io.moquette.spi.ClientSession; -import io.moquette.spi.ISessionsStore; +import io.moquette.spi.impl.SessionsRepository; import io.moquette.spi.impl.subscriptions.Subscription; import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttMessageType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; @@ -39,11 +40,11 @@ public class ConnectionDescriptorStore implements IConnectionsManager { private static final Logger LOG = LoggerFactory.getLogger(ConnectionDescriptorStore.class); private final ConcurrentMap connectionDescriptors; - private final ISessionsStore sessionsStore; + private final SessionsRepository sessionsRepository; - public ConnectionDescriptorStore(ISessionsStore sessionsStore) { + public ConnectionDescriptorStore(SessionsRepository sessionsRepository) { this.connectionDescriptors = new ConcurrentHashMap<>(); - this.sessionsStore = sessionsStore; + this.sessionsRepository = sessionsRepository; } public boolean sendMessage(MqttMessage message, Integer messageID, String clientID) { @@ -124,22 +125,11 @@ public class ConnectionDescriptorStore implements IConnectionsManager { } } - @Override - public MqttSession getSessionStatus(String clientID) { - LOG.info("Retrieving status of session. CId=<{}>", clientID); - ClientSession session = sessionsStore.sessionForClient(clientID); - if (session == null) { - LOG.error("MQTT client ID doesn't have an associated session. CId=<{}>", clientID); - return null; - } - return buildMqttSession(session); - } - @Override public Collection getSessions() { LOG.info("Retrieving status of all sessions."); Collection result = new ArrayList<>(); - for (ClientSession session : sessionsStore.getAllSessions()) { + for (ClientSession session : this.sessionsRepository.getAllSessions()) { result.add(buildMqttSession(session)); } return result; @@ -165,7 +155,7 @@ public class ConnectionDescriptorStore implements IConnectionsManager { result.setConnectionEstablished(false); } result.setPendingPublishMessagesNo(session.getPendingPublishMessagesNo()); - result.setSecondPhaseAckPendingMessages(session.getSecondPhaseAckPendingMessages()); + result.setSecondPhaseAckPendingMessages(session.countPubReleaseWaitingPubComplete()); result.setInflightMessages(session.getInflightMessagesNo()); return result; } diff --git a/broker/src/main/java/io/moquette/server/Constants.java b/broker/src/main/java/io/moquette/server/Constants.java index e2b2cd15..9e345979 100644 --- a/broker/src/main/java/io/moquette/server/Constants.java +++ b/broker/src/main/java/io/moquette/server/Constants.java @@ -22,7 +22,7 @@ package io.moquette.server; public final class Constants { public static final String ATTR_CLIENTID = "ClientID"; - public static final String CLEAN_SESSION = "cleanSession"; + public static final String CLEAN_SESSION = "removeTemporaryQoS2"; public static final String KEEP_ALIVE = "keepAlive"; public static final int MAX_MESSAGE_QUEUE = 1024; // number of messages diff --git a/broker/src/main/java/io/moquette/spi/ClientSession.java b/broker/src/main/java/io/moquette/spi/ClientSession.java index 172a1e63..042110fa 100644 --- a/broker/src/main/java/io/moquette/spi/ClientSession.java +++ b/broker/src/main/java/io/moquette/spi/ClientSession.java @@ -16,14 +16,10 @@ package io.moquette.spi; -import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple; import io.moquette.spi.impl.subscriptions.Subscription; import io.moquette.spi.impl.subscriptions.Topic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.HashSet; -import java.util.Queue; import java.util.Set; /** @@ -40,164 +36,51 @@ import java.util.Set; *
  • Optionally, QoS 0 messages pending transmission to the Client.
  • * */ -public class ClientSession { - - class OutboundFlightZone { - - /** - * Save the binding messageID, clientID - message - * - * @param messageID the packet ID used in transmission - * @param msg the message to put in flight zone - */ - void waitingAck(int messageID, IMessagesStore.StoredMessage msg) { - if (LOG.isTraceEnabled()) { - LOG.trace("Adding to inflight {}, guid <{}>", messageID, msg.getGuid()); - } - m_sessionsStore.inFlight(ClientSession.this.clientID, messageID, msg); - } - - IMessagesStore.StoredMessage acknowledged(int messageID) { - if (LOG.isTraceEnabled()) - LOG.trace("Acknowledging inflight, clientID <{}> messageID {}", ClientSession.this.clientID, messageID); - return m_sessionsStore.inFlightAck(ClientSession.this.clientID, messageID); - } - } - - class InboundFlightZone { - - public IMessagesStore.StoredMessage lookup(int messageID) { - return m_sessionsStore.inboundInflight(clientID, messageID); - } - - public void waitingRel(int messageID, IMessagesStore.StoredMessage msg) { - m_sessionsStore.markAsInboundInflight(clientID, messageID, msg); - } - } - - private static final Logger LOG = LoggerFactory.getLogger(ClientSession.class); +public abstract class ClientSession { public final String clientID; - private final ISessionsStore m_sessionsStore; + protected Set subscriptions = new HashSet<>(); - private Set subscriptions = new HashSet<>(); - - private final ISubscriptionsStore subscriptionsStore; - private volatile boolean cleanSession; - - private final OutboundFlightZone outboundFlightZone; - private final InboundFlightZone inboundFlightZone; - - public ClientSession(String clientID, ISessionsStore sessionsStore, ISubscriptionsStore subscriptionsStore, - boolean cleanSession) { + public ClientSession(String clientID) { this.clientID = clientID; - this.m_sessionsStore = sessionsStore; - this.subscriptionsStore = subscriptionsStore; - this.cleanSession = cleanSession; - this.outboundFlightZone = new OutboundFlightZone(); - this.inboundFlightZone = new InboundFlightZone(); } - /** - * Return the list of persisted publishes for the given clientID. For QoS1 and QoS2 with clean - * session flag, this method return the list of missed publish events while the client was - * disconnected. - * - * @return the list of messages to be delivered for client related to the session. - */ - public Queue queue() { - LOG.info("Retrieving enqueued messages. CId={}", clientID); - return this.m_sessionsStore.queue(clientID); - } - - @Override - public String toString() { - return "ClientSession{clientID='" + clientID + "'}"; - } - - public boolean subscribe(Subscription newSubscription) { - LOG.info("Adding new subscription. CId={}, topics={}, qos={}", newSubscription.getClientId(), - newSubscription.getTopicFilter(), newSubscription.getRequestedQos()); - boolean validTopic = newSubscription.getTopicFilter().isValid(); - if (!validTopic) { - LOG.warn("The topic filter is not valid. CId={}, topics={}", newSubscription.getClientId(), - newSubscription.getTopicFilter()); - // send SUBACK with 0x80 for this topic filter - return false; - } - ClientTopicCouple matchingCouple = new ClientTopicCouple(this.clientID, newSubscription.getTopicFilter()); - Subscription existingSub = subscriptionsStore.getSubscription(matchingCouple); - // update the selected subscriptions if not present or if has a greater qos - if (existingSub == null || existingSub.getRequestedQos().value() < newSubscription.getRequestedQos().value()) { - if (existingSub != null) { - LOG.info("Subscription already existed with a lower QoS value. It will be updated. CId={}, " + - "topics={}, existingQos={}, newQos={}", newSubscription.getClientId(), - newSubscription.getTopicFilter(), existingSub.getRequestedQos(), newSubscription.getRequestedQos()); - subscriptions.remove(newSubscription); - } - subscriptions.add(newSubscription); - subscriptionsStore.addNewSubscription(newSubscription); - } - return true; - } - - public void unsubscribeFrom(Topic topicFilter) { - LOG.info("Removing subscription. CId={}, topics={}", clientID, topicFilter); - subscriptionsStore.removeSubscription(topicFilter, clientID); - Set subscriptionsToRemove = new HashSet<>(); + public Subscription findSubscriptionByTopicFilter(Subscription matchingSub) { for (Subscription sub : this.subscriptions) { - if (sub.getTopicFilter().equals(topicFilter)) { - subscriptionsToRemove.add(sub); + if (matchingSub.equals(sub)) { + return sub; } } - subscriptions.removeAll(subscriptionsToRemove); + return null; } - public void disconnect() { - if (this.cleanSession) { - LOG.info("Client disconnected. Removing its subscriptions. CId={}", clientID); - // cleanup topic subscriptions - cleanSession(); - } + public Set getSubscriptions() { + return subscriptions; } - public void cleanSession() { - m_sessionsStore.cleanSession(this.clientID); - } + public abstract boolean isCleanSession(); - public boolean isCleanSession() { - return this.cleanSession; - } + public abstract boolean subscribe(Subscription newSubscription); - public void cleanSession(boolean cleanSession) { - this.cleanSession = cleanSession; - this.m_sessionsStore.updateCleanStatus(this.clientID, cleanSession); - } + public abstract void unsubscribeFrom(Topic topicFilter); - public int nextPacketId() { - return this.m_sessionsStore.nextPacketID(this.clientID); - } + public abstract void disconnect(); - public IMessagesStore.StoredMessage inFlightAcknowledged(int messageID) { - return outboundFlightZone.acknowledged(messageID); - } + public abstract void cleanSession(); + + protected abstract int nextPacketId(); + + public abstract IMessagesStore.StoredMessage inFlightAcknowledged(int messageID); /** - * Mark the message identified by guid as publish in flight. + * Mark the message as publish in flight. * * @return the packetID for the message in flight. * */ - public int inFlightAckWaiting(IMessagesStore.StoredMessage msg) { - LOG.debug("Adding message ot inflight zone. CId={}", clientID); - int messageId = ClientSession.this.nextPacketId(); - outboundFlightZone.waitingAck(messageId, msg); - return messageId; - } + public abstract int inFlightAckWaiting(IMessagesStore.StoredMessage msg); - public IMessagesStore.StoredMessage secondPhaseAcknowledged(int messageID) { - return m_sessionsStore.secondPhaseAcknowledged(clientID, messageID); - } + public abstract IMessagesStore.StoredMessage completeReleasedPublish(int messageID); /** * Enqueue a message to be sent to the client. @@ -205,37 +88,31 @@ public class ClientSession { * @param message * the message to enqueue. */ - public void enqueue(IMessagesStore.StoredMessage message) { - this.m_sessionsStore.queue(this.clientID).add(message); - } + public abstract void enqueue(IMessagesStore.StoredMessage message); - public IMessagesStore.StoredMessage inboundInflight(int messageID) { - return inboundFlightZone.lookup(messageID); - } + /** + * @return the next message queued to this client or null if empty. + * */ + public abstract EnqueuedMessage poll(); - public void markAsInboundInflight(int messageID, IMessagesStore.StoredMessage msg) { - inboundFlightZone.waitingRel(messageID, msg); - } + public abstract boolean isEmptyQueue(); - public void moveInFlightToSecondPhaseAckWaiting(int messageID, IMessagesStore.StoredMessage msg) { - m_sessionsStore.moveInFlightToSecondPhaseAckWaiting(this.clientID, messageID, msg); - } + public abstract void dropQueue(); - public Set getSubscriptions() { - //TODO WARN this doesn'load anything from DB - return subscriptions; - } + public abstract IMessagesStore.StoredMessage inboundInflight(int messageID); - public int getPendingPublishMessagesNo() { - return m_sessionsStore.getPendingPublishMessagesNo(clientID); - } + public abstract void markAsInboundInflight(int messageID, IMessagesStore.StoredMessage msg); - public int getSecondPhaseAckPendingMessages() { - return m_sessionsStore.getSecondPhaseAckPendingMessages(clientID); - } + public abstract void moveInFlightToSecondPhaseAckWaiting(int messageID, IMessagesStore.StoredMessage msg); - public int getInflightMessagesNo() { - return m_sessionsStore.getInflightMessagesNo(clientID); - } + public abstract int getPendingPublishMessagesNo(); + public abstract int countPubReleaseWaitingPubComplete(); + + public abstract int getInflightMessagesNo(); + + @Override + public String toString() { + return getClass().getSimpleName() + "{clientID='" + clientID + "'}"; + } } diff --git a/broker/src/main/java/io/moquette/spi/DurableSession.java b/broker/src/main/java/io/moquette/spi/DurableSession.java new file mode 100644 index 00000000..fe1558be --- /dev/null +++ b/broker/src/main/java/io/moquette/spi/DurableSession.java @@ -0,0 +1,229 @@ +/* + * 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; + +import io.moquette.spi.impl.subscriptions.Subscription; +import io.moquette.spi.impl.subscriptions.Topic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class DurableSession extends ClientSession { + + class OutboundFlightZone { + + /** + * Save the binding messageID, clientID - message + * + * @param messageID the packet ID used in transmission + * @param msg the message to put in flight zone + */ + void waitingAck(int messageID, IMessagesStore.StoredMessage msg) { + if (LOG.isTraceEnabled()) { + LOG.trace("Adding to inflight {}, guid <{}>", messageID, msg.getGuid()); + } + sessionsStore.inFlight(clientID, messageID, msg); + } + + IMessagesStore.StoredMessage acknowledged(int messageID) { + if (LOG.isTraceEnabled()) { + LOG.trace("Acknowledging inflight, clientID <{}> messageID {}", clientID, messageID); + } + return sessionsStore.inFlightAck(clientID, messageID); + } + } + + class InboundFlightZone { + + final Map inboundFlightMessages = new ConcurrentHashMap<>(); + + IMessagesStore.StoredMessage lookup(int messageID) { + return inboundFlightMessages.get(messageID); + } + + void waitingRel(int messageID, IMessagesStore.StoredMessage msg) { + inboundFlightMessages.put(messageID, msg); + } + } + + + private static final Logger LOG = LoggerFactory.getLogger(DurableSession.class); + + private final ISessionsStore sessionsStore; + private final ISubscriptionsStore subscriptionsStore; + + private final OutboundFlightZone outboundFlightZone; + private final InboundFlightZone inboundFlightZone; + + public DurableSession(String clientID, ISessionsStore sessions, ISubscriptionsStore subscriptionsStore) { + super(clientID); + this.subscriptionsStore = subscriptionsStore; + this.sessionsStore = sessions; + this.outboundFlightZone = new OutboundFlightZone(); + this.inboundFlightZone = new InboundFlightZone(); + } + + public void reloadAllSubscriptionsFromStore() { + Collection reloadedSubscriptions = this.subscriptionsStore.listClientSubscriptions(this.clientID); + this.subscriptions.addAll(reloadedSubscriptions); + } + + @Override + public boolean isCleanSession() { + return false; + } + + @Override + public boolean subscribe(Subscription newSubscription) { + LOG.info("Adding new subscription. CId={}, topics={}, qos={}", newSubscription.getClientId(), + newSubscription.getTopicFilter(), newSubscription.getRequestedQos()); + boolean validTopic = newSubscription.getTopicFilter().isValid(); + if (!validTopic) { + LOG.warn("The topic filter is not valid. CId={}, topics={}", newSubscription.getClientId(), + newSubscription.getTopicFilter()); + // send SUBACK with 0x80 for this topic filter + return false; + } + Subscription existingSub = subscriptionsStore.reload(newSubscription); + // update the selected subscriptions if not present or if has a greater qos + if (existingSub == null || existingSub.qosLessThan(newSubscription)) { + if (existingSub != null) { + LOG.info("Subscription already existed with a lower QoS value. It will be updated. CId={}, " + + "topics={}, existingQos={}, newQos={}", newSubscription.getClientId(), + newSubscription.getTopicFilter(), existingSub.getRequestedQos(), newSubscription.getRequestedQos()); + subscriptions.remove(newSubscription); + } + subscriptions.add(newSubscription); + subscriptionsStore.addNewSubscription(newSubscription); + } + return true; + } + + @Override + public void unsubscribeFrom(Topic topicFilter) { + LOG.info("Removing subscription. CId={}, topics={}", clientID, topicFilter); + subscriptionsStore.removeSubscription(topicFilter, clientID); + Set subscriptionsToRemove = new HashSet<>(); + for (Subscription sub : this.subscriptions) { + if (sub.getTopicFilter().equals(topicFilter)) { + subscriptionsToRemove.add(sub); + } + } + subscriptions.removeAll(subscriptionsToRemove); + } + + @Override + public void cleanSession() { + sessionsStore.removeTemporaryQoS2(this.clientID); + LOG.info("Wiping existing subscriptions. ClientId={}", clientID); + subscriptionsStore.wipeSubscriptions(clientID); + + LOG.info("Removing queues. ClientId={}", clientID); + sessionsStore.dropQueue(clientID); + } + + @Override + public void disconnect() { + LOG.info("Client disconnected. Removing its subscriptions. CId={}", clientID); + // cleanup topic subscriptions + cleanSession(); + } + + @Override + protected int nextPacketId() { + return this.sessionsStore.nextPacketID(this.clientID); + } + + @Override + public IMessagesStore.StoredMessage inFlightAcknowledged(int messageID) { + return outboundFlightZone.acknowledged(messageID); + } + + @Override + public int inFlightAckWaiting(IMessagesStore.StoredMessage msg) { + LOG.debug("Adding message to inflight zone. CId={}", clientID); + int messageId = this.nextPacketId(); + outboundFlightZone.waitingAck(messageId, msg); + return messageId; + } + + @Override + public IMessagesStore.StoredMessage inboundInflight(int messageID) { + return inboundFlightZone.lookup(messageID); + } + + @Override + public void markAsInboundInflight(int messageID, IMessagesStore.StoredMessage msg) { + inboundFlightZone.waitingRel(messageID, msg); + } + + @Override + public void moveInFlightToSecondPhaseAckWaiting(int messageID, IMessagesStore.StoredMessage msg) { + this.sessionsStore.moveInFlightToSecondPhaseAckWaiting(this.clientID, messageID, msg); + } + + @Override + public boolean isEmptyQueue() { + return this.sessionsStore.queue(clientID).isEmpty(); + } + + @Override + public void dropQueue() { + LOG.debug("Removing messages of session. CId={}", this.clientID); + this.sessionsStore.dropQueue(this.clientID); + LOG.debug("Messages of the session have been removed. CId={}", this.clientID); + } + + @Override + public EnqueuedMessage poll() { + IMessagesStore.StoredMessage msg = this.sessionsStore.queue(clientID).poll(); + if (msg == null) { + return null; + } + int messageId = this.inFlightAckWaiting(msg); + return new EnqueuedMessage(msg, messageId); + } + + @Override + public void enqueue(IMessagesStore.StoredMessage message) { + this.sessionsStore.queue(this.clientID).add(message); + } + + @Override + public IMessagesStore.StoredMessage completeReleasedPublish(int messageID) { + return this.sessionsStore.completeReleasedPublish(clientID, messageID); + } + + @Override + public int getPendingPublishMessagesNo() { + return this.sessionsStore.queue(clientID).size(); + } + + @Override + public int countPubReleaseWaitingPubComplete() { + return this.sessionsStore.countPubReleaseWaitingPubComplete(clientID); + } + + @Override + public int getInflightMessagesNo() { + return this.sessionsStore.getInflightMessagesNo(clientID); + } +} diff --git a/broker/src/main/java/io/moquette/spi/EnqueuedMessage.java b/broker/src/main/java/io/moquette/spi/EnqueuedMessage.java new file mode 100644 index 00000000..dfb1f316 --- /dev/null +++ b/broker/src/main/java/io/moquette/spi/EnqueuedMessage.java @@ -0,0 +1,28 @@ +/* + * 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; + +public class EnqueuedMessage { + + public final IMessagesStore.StoredMessage msg; + public final int messageId; + + EnqueuedMessage(IMessagesStore.StoredMessage msg, int messageId) { + this.msg = msg; + this.messageId = messageId; + } +} diff --git a/broker/src/main/java/io/moquette/spi/IMessagesStore.java b/broker/src/main/java/io/moquette/spi/IMessagesStore.java index b8da62e1..74f21dbf 100644 --- a/broker/src/main/java/io/moquette/spi/IMessagesStore.java +++ b/broker/src/main/java/io/moquette/spi/IMessagesStore.java @@ -21,7 +21,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttQoS; import java.io.Serializable; -import java.nio.ByteBuffer; import java.util.Collection; /** diff --git a/broker/src/main/java/io/moquette/spi/ISessionsStore.java b/broker/src/main/java/io/moquette/spi/ISessionsStore.java index dff911e9..d1053055 100644 --- a/broker/src/main/java/io/moquette/spi/ISessionsStore.java +++ b/broker/src/main/java/io/moquette/spi/ISessionsStore.java @@ -16,6 +16,7 @@ package io.moquette.spi; +import io.moquette.persistence.PersistentSession; import io.moquette.spi.IMessagesStore.StoredMessage; import java.util.Collection; @@ -30,30 +31,27 @@ public interface ISessionsStore { ISubscriptionsStore subscriptionStore(); - void updateCleanStatus(String clientID, boolean cleanSession); - /** * @param clientID * the session client ID. - * @return true iff there are subscriptions persisted with clientID + * @return true iff there is a session stored for the client */ boolean contains(String clientID); - ClientSession createNewSession(String clientID, boolean cleanSession); + void createNewDurableSession(String clientID); - /** - * @param clientID - * the client owning the session. - * @return the session for the given clientID, null if not found. - */ - ClientSession sessionForClient(String clientID); + void removeDurableSession(String clientId); + + void updateCleanStatus(String clientID, boolean cleanSession); + + PersistentSession loadSessionByKey(String clientID); /** * Returns all the sessions * * @return the collection of all stored client sessions. */ - Collection getAllSessions(); + Collection listAllSessions(); StoredMessage inFlightAck(String clientID, int messageID); @@ -98,7 +96,7 @@ public interface ISessionsStore { * the message ID that reached the second phase. * @return the guid of message just acked. */ - StoredMessage secondPhaseAcknowledged(String clientID, int messageID); + StoredMessage completeReleasedPublish(String clientID, int messageID); /** * Returns the number of inflight messages for the given client ID @@ -108,28 +106,13 @@ public interface ISessionsStore { */ int getInflightMessagesNo(String clientID); - /** - * @return the inflight inbound (PUBREL for Qos2) message. - * */ - IMessagesStore.StoredMessage inboundInflight(String clientID, int messageID); - - void markAsInboundInflight(String clientID, int messageID, StoredMessage msg); - - /** - * Returns the size of the session queue for the given client ID - * - * @param clientID target client. - * @return count of enqueued publish messages. - */ - int getPendingPublishMessagesNo(String clientID); - /** * Returns the number of second-phase ACK pending messages for the given client ID * * @param clientID target client. * @return count of pending in flight publish messages. */ - int getSecondPhaseAckPendingMessages(String clientID); + int countPubReleaseWaitingPubComplete(String clientID); - void cleanSession(String clientID); + void removeTemporaryQoS2(String clientID); } diff --git a/broker/src/main/java/io/moquette/spi/ISubscriptionsStore.java b/broker/src/main/java/io/moquette/spi/ISubscriptionsStore.java index d0975de8..5dc0597f 100644 --- a/broker/src/main/java/io/moquette/spi/ISubscriptionsStore.java +++ b/broker/src/main/java/io/moquette/spi/ISubscriptionsStore.java @@ -3,46 +3,10 @@ package io.moquette.spi; import io.moquette.spi.impl.subscriptions.Subscription; import io.moquette.spi.impl.subscriptions.Topic; +import java.util.Collection; import java.util.List; public interface ISubscriptionsStore { - class ClientTopicCouple { - - public final Topic topicFilter; - public final String clientID; - - public ClientTopicCouple(String clientID, Topic topicFilter) { - this.clientID = clientID; - this.topicFilter = topicFilter; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - - ClientTopicCouple that = (ClientTopicCouple) o; - - if (topicFilter != null ? !topicFilter.equals(that.topicFilter) : that.topicFilter != null) - return false; - return !(clientID != null ? !clientID.equals(that.clientID) : that.clientID != null); - } - - @Override - public int hashCode() { - int result = topicFilter != null ? topicFilter.hashCode() : 0; - result = 31 * result + (clientID != null ? clientID.hashCode() : 0); - return result; - } - - @Override - public String toString() { - return "ClientTopicCouple{" + "topicFilter='" + topicFilter + '\'' + ", clientID='" + clientID + '\'' + '}'; - } - } - /** * Add a new subscription to the session @@ -65,26 +29,25 @@ public interface ISubscriptionsStore { /** * Remove all the subscriptions of the session * - * @param sessionID + * @param clientID * the client ID */ - void wipeSubscriptions(String sessionID); + void wipeSubscriptions(String clientID); /** * Return all topic filters to recreate the subscription tree. */ - List listAllSubscriptions(); + List listAllSubscriptions(); /** - * @param couple - * the subscription descriptor. + * Load from storage all the subscriptions of the specified client. + * */ + Collection listClientSubscriptions(String clientID); + + /** + * @param subcription + * the subscription to reaload from storage. * @return the subscription stored by clientID and topicFilter, if any else null; */ - Subscription getSubscription(ClientTopicCouple couple); - - /* - * @return all subscriptions stored. - */ - List getSubscriptions(); - + Subscription reload(Subscription subcription); } diff --git a/broker/src/main/java/io/moquette/spi/TransientSession.java b/broker/src/main/java/io/moquette/spi/TransientSession.java new file mode 100644 index 00000000..9a064c2c --- /dev/null +++ b/broker/src/main/java/io/moquette/spi/TransientSession.java @@ -0,0 +1,185 @@ +/* + * 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; + +import io.moquette.spi.IMessagesStore.StoredMessage; +import io.moquette.spi.impl.subscriptions.Subscription; +import io.moquette.spi.impl.subscriptions.Topic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class TransientSession extends ClientSession { + + private static final Logger LOG = LoggerFactory.getLogger(TransientSession.class); + + private Queue messagesQueue = new LinkedList<>(); + + private final AtomicInteger packetGenerator = new AtomicInteger(1); + + private final ConcurrentMap inboundInflightMap = new ConcurrentHashMap<>(); + private final ConcurrentMap outboundInflightMap = new ConcurrentHashMap<>(); + private final Map secondPhaseStore = new ConcurrentHashMap<>(); + + public TransientSession(String clientID) { + super(clientID); + } + + @Override + public void disconnect() {} + + @Override + public boolean isCleanSession() { + return true; + } + + @Override + public boolean subscribe(Subscription newSubscription) { + LOG.info("Adding new subscription. CId={}, topics={}, qos={}", newSubscription.getClientId(), + newSubscription.getTopicFilter(), newSubscription.getRequestedQos()); + boolean validTopic = newSubscription.getTopicFilter().isValid(); + if (!validTopic) { + LOG.warn("The topic filter is not valid. CId={}, topics={}", newSubscription.getClientId(), + newSubscription.getTopicFilter()); + // send SUBACK with 0x80 for this topic filter + return false; + } + final Optional mathingExisting = this.subscriptions.stream() + .filter(s -> s.equals(newSubscription)) + .findFirst(); + if (mathingExisting.isPresent()) { + Subscription existingSub = mathingExisting.get(); + if (existingSub.qosLessThan(newSubscription)) { + LOG.info("Subscription already existed with a lower QoS value. It will be updated. CId={}, " + + "topics={}, existingQos={}, newQos={}", newSubscription.getClientId(), + newSubscription.getTopicFilter(), existingSub.getRequestedQos(), newSubscription.getRequestedQos()); + subscriptions.remove(newSubscription); + } + } + + subscriptions.add(newSubscription); + return true; + } + + @Override + public void unsubscribeFrom(Topic topicFilter) { + LOG.info("Removing subscription. CId={}, topics={}", clientID, topicFilter); + Set subscriptionsToRemove = new HashSet<>(); + for (Subscription sub : this.subscriptions) { + if (sub.getTopicFilter().equals(topicFilter)) { + subscriptionsToRemove.add(sub); + } + } + subscriptions.removeAll(subscriptionsToRemove); + } + + @Override + public boolean isEmptyQueue() { + return this.messagesQueue.isEmpty(); + } + + @Override + public void enqueue(StoredMessage message) { + this.messagesQueue.offer(message); + } + + @Override + public EnqueuedMessage poll() { + IMessagesStore.StoredMessage msg = this.messagesQueue.poll(); + if (msg == null) { + return null; + } + int messageId = this.inFlightAckWaiting(msg); + return new EnqueuedMessage(msg, messageId); + } + + @Override + public void dropQueue() { + LOG.debug("Removing messages of session. CId={}", this.clientID); + this.messagesQueue = null; + LOG.debug("Messages of the session have been removed. CId={}", this.clientID); + } + + @Override + public void cleanSession() { + LOG.info("Transient sessiom, wiping existing subscriptions. ClientId={}", clientID); + this.subscriptions.clear(); + + //TODO +// LOG.info("Removing queues. ClientId={}", clientID); +// m_sessionsStore.dropQueue(clientID); + } + + @Override + protected int nextPacketId() { + return this.packetGenerator.getAndIncrement(); + } + + @Override + public int inFlightAckWaiting(StoredMessage msg) { + LOG.debug("Adding message to inflight zone. CId={}", clientID); + int messageId = this.nextPacketId(); + this.outboundInflightMap.put(messageId, msg); + return messageId; + } + + @Override + public IMessagesStore.StoredMessage inFlightAcknowledged(int messageID) { + LOG.debug("Removing message to inflight zone. CId={}, messageID={}", clientID, messageID); + return this.outboundInflightMap.remove(messageID); + } + + @Override + public void markAsInboundInflight(int messageID, IMessagesStore.StoredMessage msg) { + this.inboundInflightMap.put(messageID, msg); + } + + @Override + public IMessagesStore.StoredMessage inboundInflight(int messageID) { + return this.inboundInflightMap.remove(messageID); + } + + @Override + public void moveInFlightToSecondPhaseAckWaiting(int messageID, IMessagesStore.StoredMessage msg) { + this.secondPhaseStore.put(messageID, msg); + this.outboundInflightMap.put(messageID, msg); + } + + @Override + public IMessagesStore.StoredMessage completeReleasedPublish(int messageID) { + LOG.info("Acknowledged message in second phase, clientID <{}> messageID {}", clientID, messageID); + return this.secondPhaseStore.remove(messageID); + } + + @Override + public int getPendingPublishMessagesNo() { + return this.messagesQueue.size(); + } + + @Override + public int countPubReleaseWaitingPubComplete() { + return this.outboundInflightMap.size(); + } + + @Override + public int getInflightMessagesNo() { + return this.inboundInflightMap.size(); + } +} diff --git a/broker/src/main/java/io/moquette/spi/impl/InternalRepublisher.java b/broker/src/main/java/io/moquette/spi/impl/InternalRepublisher.java index 071db32f..1ca8a0d3 100644 --- a/broker/src/main/java/io/moquette/spi/impl/InternalRepublisher.java +++ b/broker/src/main/java/io/moquette/spi/impl/InternalRepublisher.java @@ -17,6 +17,7 @@ package io.moquette.spi.impl; import io.moquette.spi.ClientSession; +import io.moquette.spi.EnqueuedMessage; import io.moquette.spi.IMessagesStore; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.mqtt.*; @@ -24,7 +25,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; -import java.util.Queue; class InternalRepublisher { @@ -53,17 +53,22 @@ class InternalRepublisher { } } - void publishStored(ClientSession clientSession, Queue publishedEvents) { - IMessagesStore.StoredMessage pubEvt; - while ((pubEvt = publishedEvents.poll()) != null) { + void publishStored(ClientSession clientSession) { + if (clientSession.isEmptyQueue()) { + LOG.info("There are no stored publish events to CId={}", clientSession.clientID); + return; + } + + EnqueuedMessage pubEvt; + while ((pubEvt = clientSession.poll()) != null) { // put in flight zone LOG.debug("Adding message ot inflight zone. ClientId={}, guid={}, topic={}", clientSession.clientID, - pubEvt.getGuid(), pubEvt.getTopic()); - int messageId = clientSession.inFlightAckWaiting(pubEvt); - MqttPublishMessage publishMsg = notRetainedPublish(pubEvt); + pubEvt.msg.getGuid(), pubEvt.msg.getTopic()); + + MqttPublishMessage publishMsg = notRetainedPublish(pubEvt.msg); // set the PacketIdentifier only for QoS > 0 if (publishMsg.fixedHeader().qosLevel() != MqttQoS.AT_MOST_ONCE) { - publishMsg = notRetainedPublish(pubEvt, messageId); + publishMsg = notRetainedPublish(pubEvt.msg, pubEvt.messageId); } this.messageSender.sendPublish(clientSession, publishMsg); } diff --git a/broker/src/main/java/io/moquette/spi/impl/MessagesPublisher.java b/broker/src/main/java/io/moquette/spi/impl/MessagesPublisher.java index 5a4908cd..15721d11 100644 --- a/broker/src/main/java/io/moquette/spi/impl/MessagesPublisher.java +++ b/broker/src/main/java/io/moquette/spi/impl/MessagesPublisher.java @@ -19,7 +19,6 @@ package io.moquette.spi.impl; import io.moquette.server.ConnectionDescriptorStore; import io.moquette.spi.ClientSession; import io.moquette.spi.IMessagesStore; -import io.moquette.spi.ISessionsStore; import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; import io.moquette.spi.impl.subscriptions.Subscription; import io.moquette.spi.impl.subscriptions.Topic; @@ -27,23 +26,26 @@ import io.netty.buffer.ByteBuf; import io.netty.handler.codec.mqtt.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import java.util.List; + import static io.moquette.spi.impl.ProtocolProcessor.lowerQosToTheSubscriptionDesired; class MessagesPublisher { private static final Logger LOG = LoggerFactory.getLogger(MessagesPublisher.class); private final ConnectionDescriptorStore connectionDescriptors; - private final ISessionsStore m_sessionsStore; private final PersistentQueueMessageSender messageSender; private final ISubscriptionsDirectory subscriptions; + private SessionsRepository sessionsRepository; - public MessagesPublisher(ConnectionDescriptorStore connectionDescriptors, ISessionsStore sessionsStore, - PersistentQueueMessageSender messageSender, ISubscriptionsDirectory subscriptions) { + public MessagesPublisher(ConnectionDescriptorStore connectionDescriptors, + PersistentQueueMessageSender messageSender, ISubscriptionsDirectory subscriptions, + SessionsRepository sessionsRepository) { this.connectionDescriptors = connectionDescriptors; - this.m_sessionsStore = sessionsStore; this.messageSender = messageSender; this.subscriptions = subscriptions; + this.sessionsRepository = sessionsRepository; } static MqttPublishMessage notRetainedPublish(String topic, MqttQoS qos, ByteBuf message) { @@ -77,7 +79,7 @@ class MessagesPublisher { for (final Subscription sub : topicMatchingSubscriptions) { MqttQoS qos = lowerQosToTheSubscriptionDesired(sub, publishingQos); - ClientSession targetSession = m_sessionsStore.sessionForClient(sub.getClientId()); + ClientSession targetSession = this.sessionsRepository.sessionForClient(sub.getClientId()); boolean targetIsActive = this.connectionDescriptors.isConnected(sub.getClientId()); //TODO move all this logic into messageSender, which puts into the flightZone only the messages that pull out of the queue. diff --git a/broker/src/main/java/io/moquette/spi/impl/PersistentQueueMessageSender.java b/broker/src/main/java/io/moquette/spi/impl/PersistentQueueMessageSender.java index bb354a06..5f0c2ecc 100644 --- a/broker/src/main/java/io/moquette/spi/impl/PersistentQueueMessageSender.java +++ b/broker/src/main/java/io/moquette/spi/impl/PersistentQueueMessageSender.java @@ -51,11 +51,11 @@ class PersistentQueueMessageSender { if (!messageDelivered) { if (qos != AT_MOST_ONCE && !clientsession.isCleanSession()) { LOG.warn("PUBLISH message could not be delivered. It will be stored. MessageId={}, CId={}, topic={}, " - + "qos={}, cleanSession={}", messageId, clientId, topicName, qos, false); + + "qos={}, removeTemporaryQoS2={}", messageId, clientId, topicName, qos, false); clientsession.enqueue(asStoredMessage(pubMessage)); } else { LOG.warn("PUBLISH message could not be delivered. It will be discarded. MessageId={}, CId={}, topic={}, " + - "qos={}, cleanSession={}", messageId, clientId, topicName, qos, true); + "qos={}, removeTemporaryQoS2={}", messageId, clientId, topicName, qos, true); } } } diff --git a/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessor.java b/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessor.java index 561e4b85..f83ead51 100644 --- a/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessor.java +++ b/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessor.java @@ -125,8 +125,8 @@ public class ProtocolProcessor { private static final Logger LOG = LoggerFactory.getLogger(ProtocolProcessor.class); - protected ConnectionDescriptorStore connectionDescriptors; - protected ConcurrentMap subscriptionInCourse; + private ConnectionDescriptorStore connectionDescriptors; + private ConcurrentMap subscriptionInCourse; private ISubscriptionsDirectory subscriptions; private ISubscriptionsStore subscriptionStore; @@ -146,6 +146,7 @@ public class ProtocolProcessor { private Qos2PublishHandler qos2PublishHandler; private MessagesPublisher messagesPublisher; private InternalRepublisher internalRepublisher; + SessionsRepository sessionsRepository; // maps clientID to Will testament, if specified on CONNECT private ConcurrentMap m_willStore = new ConcurrentHashMap<>(); @@ -155,23 +156,16 @@ public class ProtocolProcessor { public void init(ISubscriptionsDirectory subscriptions, IMessagesStore storageService, ISessionsStore sessionsStore, IAuthenticator authenticator, boolean allowAnonymous, IAuthorizator authorizator, - BrokerInterceptor interceptor) { + BrokerInterceptor interceptor, SessionsRepository sessionsRepository) { init(subscriptions, storageService, sessionsStore, authenticator, allowAnonymous, false, authorizator, - interceptor, null); + interceptor, sessionsRepository); } public void init(ISubscriptionsDirectory subscriptions, IMessagesStore storageService, ISessionsStore sessionsStore, IAuthenticator authenticator, boolean allowAnonymous, boolean allowZeroByteClientId, - IAuthorizator authorizator, BrokerInterceptor interceptor) { - init(subscriptions, storageService, sessionsStore, authenticator, allowAnonymous, allowZeroByteClientId, - authorizator, interceptor, null); - } - - public void init(ISubscriptionsDirectory subscriptions, IMessagesStore storageService, ISessionsStore sessionsStore, - IAuthenticator authenticator, boolean allowAnonymous, boolean allowZeroByteClientId, - IAuthorizator authorizator, BrokerInterceptor interceptor, String serverPort) { - init(new ConnectionDescriptorStore(sessionsStore), subscriptions, storageService, sessionsStore, authenticator, - allowAnonymous, allowZeroByteClientId, authorizator, interceptor, serverPort); + IAuthorizator authorizator, BrokerInterceptor interceptor, SessionsRepository sessionsRepository) { + init(new ConnectionDescriptorStore(sessionsRepository), subscriptions, storageService, sessionsStore, authenticator, + allowAnonymous, allowZeroByteClientId, authorizator, interceptor, sessionsRepository); } /** @@ -193,9 +187,9 @@ public class ProtocolProcessor { * to notify events to an intercept handler */ void init(ConnectionDescriptorStore connectionDescriptors, ISubscriptionsDirectory subscriptions, - IMessagesStore storageService, ISessionsStore sessionsStore, IAuthenticator authenticator, - boolean allowAnonymous, boolean allowZeroByteClientId, IAuthorizator authorizator, - BrokerInterceptor interceptor, String serverPort) { + IMessagesStore storageService, ISessionsStore sessionsStore, IAuthenticator authenticator, + boolean allowAnonymous, boolean allowZeroByteClientId, IAuthorizator authorizator, + BrokerInterceptor interceptor, SessionsRepository sessionsRepository) { LOG.info("Initializing MQTT protocol processor..."); this.connectionDescriptors = connectionDescriptors; this.subscriptionInCourse = new ConcurrentHashMap<>(); @@ -212,10 +206,12 @@ public class ProtocolProcessor { m_sessionsStore = sessionsStore; subscriptionStore = sessionsStore.subscriptionStore(); + this.sessionsRepository = sessionsRepository; + LOG.info("Initializing messages publisher..."); final PersistentQueueMessageSender messageSender = new PersistentQueueMessageSender(this.connectionDescriptors); - this.messagesPublisher = new MessagesPublisher(connectionDescriptors, sessionsStore, messageSender, - subscriptions); + this.messagesPublisher = new MessagesPublisher(connectionDescriptors, messageSender, + subscriptions, this.sessionsRepository); LOG.info("Initializing QoS publish handlers..."); this.qos0PublishHandler = new Qos0PublishHandler(m_authorizator, m_messagesStore, m_interceptor, @@ -223,7 +219,7 @@ public class ProtocolProcessor { this.qos1PublishHandler = new Qos1PublishHandler(m_authorizator, m_messagesStore, m_interceptor, this.connectionDescriptors, this.messagesPublisher); this.qos2PublishHandler = new Qos2PublishHandler(m_authorizator, subscriptions, m_messagesStore, m_interceptor, - this.connectionDescriptors, m_sessionsStore, this.messagesPublisher); + this.connectionDescriptors, this.messagesPublisher, this.sessionsRepository); LOG.info("Initializing internal republisher..."); this.internalRepublisher = new InternalRepublisher(messageSender); @@ -244,8 +240,9 @@ public class ProtocolProcessor { return; } + final boolean cleanSession = msg.variableHeader().isCleanSession(); if (clientId == null || clientId.length() == 0) { - if (!msg.variableHeader().isCleanSession() || !this.allowZeroByteClientId) { + if (!cleanSession || !this.allowZeroByteClientId) { MqttConnAckMessage badId = connAck(CONNECTION_REFUSED_IDENTIFIER_REJECTED); channel.writeAndFlush(badId); @@ -265,8 +262,7 @@ public class ProtocolProcessor { return; } - ConnectionDescriptor descriptor = new ConnectionDescriptor(clientId, channel, - msg.variableHeader().isCleanSession()); + ConnectionDescriptor descriptor = new ConnectionDescriptor(clientId, channel, cleanSession); ConnectionDescriptor existing = this.connectionDescriptors.addConnection(descriptor); if (existing != null) { LOG.info("Client ID is being used in an existing connection, force to be closed. CId={}", clientId); @@ -285,11 +281,11 @@ public class ProtocolProcessor { m_interceptor.notifyClientConnected(msg); - final ClientSession clientSession = createOrLoadClientSession(descriptor, msg, clientId); - if (clientSession == null) { + if (!descriptor.assignState(SENDACK, SESSION_CREATED)) { channel.close(); return; } + final ClientSession clientSession = this.sessionsRepository.createOrLoadClientSession(clientId, cleanSession); if (!republish(descriptor, msg, clientSession)) { channel.close(); @@ -345,27 +341,30 @@ public class ProtocolProcessor { } private boolean sendAck(ConnectionDescriptor descriptor, MqttConnectMessage msg, final String clientId) { - LOG.info("Sending connect ACK. CId={}", clientId); + LOG.info("Sending CONNACK. CId={}", clientId); final boolean success = descriptor.assignState(DISCONNECTED, SENDACK); if (!success) { return false; } MqttConnAckMessage okResp; - ClientSession clientSession = m_sessionsStore.sessionForClient(clientId); + ClientSession clientSession = this.sessionsRepository.sessionForClient(clientId); boolean isSessionAlreadyStored = clientSession != null; - if (!msg.variableHeader().isCleanSession() && isSessionAlreadyStored) { + final boolean msgCleanSessionFlag = msg.variableHeader().isCleanSession(); + if (!msgCleanSessionFlag && isSessionAlreadyStored) { okResp = connAckWithSessionPresent(CONNECTION_ACCEPTED); } else { okResp = connAck(CONNECTION_ACCEPTED); } - if (isSessionAlreadyStored) { - LOG.info("Cleaning session. CId={}", clientId); - clientSession.cleanSession(msg.variableHeader().isCleanSession()); - } descriptor.writeAndFlush(okResp); - LOG.info("The connect ACK has been sent. CId={}", clientId); + LOG.info("CONNACK has been sent. CId={}", clientId); + + if (isSessionAlreadyStored && msgCleanSessionFlag) { + for (Subscription existingSub : clientSession.getSubscriptions()) { + this.subscriptions.removeSubscription(existingSub.getTopicFilter(), clientId); + } + } return true; } @@ -373,15 +372,12 @@ public class ProtocolProcessor { int keepAlive = msg.variableHeader().keepAliveTimeSeconds(); LOG.info("Configuring connection. CId={}", clientId); NettyUtils.keepAlive(channel, keepAlive); - // session.attr(NettyUtils.ATTR_KEY_CLEANSESSION).set(msg.variableHeader().isCleanSession()); NettyUtils.cleanSession(channel, msg.variableHeader().isCleanSession()); - // used to track the client in the subscription and publishing phases. - // session.attr(NettyUtils.ATTR_KEY_CLIENTID).set(msg.getClientID()); NettyUtils.clientID(channel, clientId); int idleTime = Math.round(keepAlive * 1.5f); setIdleTime(channel.pipeline(), idleTime); - LOG.debug("The connection has been configured CId={}, keepAlive={}, cleanSession={}, idleTime={}", + LOG.debug("The connection has been configured CId={}, keepAlive={}, removeTemporaryQoS2={}, idleTime={}", clientId, keepAlive, msg.variableHeader().isCleanSession(), idleTime); } @@ -401,25 +397,6 @@ public class ProtocolProcessor { } } - private ClientSession createOrLoadClientSession(ConnectionDescriptor descriptor, MqttConnectMessage msg, - String clientId) { - final boolean success = descriptor.assignState(SENDACK, SESSION_CREATED); - if (!success) { - return null; - } - - ClientSession clientSession = m_sessionsStore.sessionForClient(clientId); - boolean isSessionAlreadyStored = clientSession != null; - if (!isSessionAlreadyStored) { - clientSession = m_sessionsStore.createNewSession(clientId, msg.variableHeader().isCleanSession()); - } - if (msg.variableHeader().isCleanSession()) { - LOG.info("Cleaning session. CId={}", clientId); - clientSession.cleanSession(); - } - return clientSession; - } - private boolean republish(ConnectionDescriptor descriptor, MqttConnectMessage msg, ClientSession clientSession) { final boolean success = descriptor.assignState(SESSION_CREATED, MESSAGES_REPUBLISHED); if (!success) { @@ -428,7 +405,8 @@ public class ProtocolProcessor { if (!msg.variableHeader().isCleanSession()) { // force the republish of stored QoS1 and QoS2 - republishStoredInSession(clientSession); + LOG.info("Republishing stored publish events. CId={}", clientSession.clientID); + this.internalRepublisher.publishStored(clientSession); } int flushIntervalMs = 500/* (keepAlive * 1000) / 2 */; descriptor.setupAutoFlusher(flushIntervalMs); @@ -447,27 +425,13 @@ public class ProtocolProcessor { pipeline.addFirst("idleStateHandler", new IdleStateHandler(idleTime, 0, 0)); } - /** - * Republish QoS1 and QoS2 messages stored into the session for the clientID. - */ - private void republishStoredInSession(ClientSession clientSession) { - LOG.info("Republishing stored publish events. CId={}", clientSession.clientID); - Queue publishedEvents = clientSession.queue(); - if (publishedEvents.isEmpty()) { - LOG.info("There are no stored publish events to CId={}", clientSession.clientID); - return; - } - - this.internalRepublisher.publishStored(clientSession, publishedEvents); - } - public void processPubAck(Channel channel, MqttPubAckMessage msg) { String clientID = NettyUtils.clientID(channel); int messageID = msg.variableHeader().messageId(); String username = NettyUtils.userName(channel); LOG.trace("retrieving inflight for messageID <{}>", messageID); - ClientSession targetSession = m_sessionsStore.sessionForClient(clientID); + ClientSession targetSession = this.sessionsRepository.sessionForClient(clientID); StoredMessage inflightMsg = targetSession.inFlightAcknowledged(messageID); String topic = inflightMsg.getTopic(); @@ -531,16 +495,12 @@ public class ProtocolProcessor { final Topic topic = new Topic(msg.variableHeader().topicName()); LOG.info("Sending PUBLISH message. Topic={}, qos={}", topic, qos); - MessageGUID guid = null; IMessagesStore.StoredMessage toStoreMsg = asStoredMessage(msg); if (clientId == null || clientId.isEmpty()) { toStoreMsg.setClientID("BROKER_SELF"); } else { toStoreMsg.setClientID(clientId); } -// if (qos == EXACTLY_ONCE) { // QoS2 -// guid = m_messagesStore.storePublishForFuture(toStoreMsg); -// } this.messagesPublisher.publish2Subscribers(toStoreMsg, topic); if (!msg.fixedHeader().isRetain()) { @@ -551,10 +511,6 @@ public class ProtocolProcessor { m_messagesStore.cleanRetained(topic); return; } -// if (guid == null) { -// // before wasn't stored -// guid = m_messagesStore.storePublishForFuture(toStoreMsg); -// } m_messagesStore.storeRetained(topic, toStoreMsg); } @@ -598,9 +554,10 @@ public class ProtocolProcessor { public void processPubRec(Channel channel, MqttMessage msg) { String clientID = NettyUtils.clientID(channel); - ClientSession targetSession = m_sessionsStore.sessionForClient(clientID); - // remove from the inflight and move to the QoS2 second phase queue int messageID = messageId(msg); + LOG.debug("Processing PUBREC message. CId={}, messageId={}", clientID, messageID); + ClientSession targetSession = this.sessionsRepository.sessionForClient(clientID); + // remove from the inflight and move to the QoS2 second phase queue StoredMessage ackedMsg = targetSession.inFlightAcknowledged(messageID); targetSession.moveInFlightToSecondPhaseAckWaiting(messageID, ackedMsg); // once received a PUBREC reply with a PUBREL(messageID) @@ -616,12 +573,13 @@ public class ProtocolProcessor { int messageID = messageId(msg); LOG.debug("Processing PUBCOMP message. CId={}, messageId={}", clientID, messageID); // once received the PUBCOMP then remove the message from the temp memory - ClientSession targetSession = m_sessionsStore.sessionForClient(clientID); - StoredMessage inflightMsg = targetSession.secondPhaseAcknowledged(messageID); + ClientSession targetSession = this.sessionsRepository.sessionForClient(clientID); + StoredMessage inflightMsg = targetSession.completeReleasedPublish(messageID); String username = NettyUtils.userName(channel); String topic = inflightMsg.getTopic(); - m_interceptor - .notifyMessageAcknowledged(new InterceptAcknowledgedMessage(inflightMsg, topic, username, messageID)); + final InterceptAcknowledgedMessage interceptAckMsg = new InterceptAcknowledgedMessage(inflightMsg, topic, + username, messageID); + m_interceptor.notifyMessageAcknowledged(interceptAckMsg); } public void processDisconnect(Channel channel) throws InterruptedException { @@ -671,6 +629,7 @@ public class ProtocolProcessor { LOG.warn("Another descriptor has been inserted. CId={}", clientID); return; } + this.sessionsRepository.disconnect(clientID); LOG.info("DISCONNECT message has been processed. CId={}", clientID); } @@ -684,7 +643,7 @@ public class ProtocolProcessor { if (descriptor.cleanSession) { LOG.info("Removing saved subscriptions. CId={}", descriptor.clientID); subscriptionStore.wipeSubscriptions(clientID); - LOG.info("The saved subscriptions have been removed. CId={}", descriptor.clientID); + LOG.info("Saved subscriptions have been removed. CId={}", descriptor.clientID); } return true; } @@ -695,10 +654,12 @@ public class ProtocolProcessor { return false; } - if (descriptor.cleanSession) { - LOG.debug("Removing messages of session. CId={}", descriptor.clientID); - this.m_sessionsStore.dropQueue(clientID); - LOG.debug("The messages of the session have been removed. CId={}", descriptor.clientID); + final ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID); + if (clientSession.isCleanSession()) { + clientSession.dropQueue(); +// LOG.debug("Removing messages of session. CId={}", descriptor.clientID); +// this.m_sessionsStore.dropQueue(clientID); +// LOG.debug("Messages of the session have been removed. CId={}", descriptor.clientID); } return true; } @@ -747,7 +708,7 @@ public class ProtocolProcessor { LOG.info("Processing UNSUBSCRIBE message. CId={}, topics={}", clientID, topics); - ClientSession clientSession = m_sessionsStore.sessionForClient(clientID); + ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID); for (String t : topics) { Topic topic = new Topic(t); boolean validTopic = topic.isValid(); @@ -801,7 +762,7 @@ public class ProtocolProcessor { // save session, persist subscriptions from session for (Subscription subscription : newSubscriptions) { - subscriptions.add(subscription.asClientTopicCouple()); + subscriptions.add(subscription); } LOG.info("Sending SUBACK response CId={}, messageId={}", clientID, messageID); @@ -819,7 +780,7 @@ public class ProtocolProcessor { } private List doStoreSubscription(List ackTopics, String clientID) { - ClientSession clientSession = m_sessionsStore.sessionForClient(clientID); + ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID); List newSubscriptions = new ArrayList<>(); for (MqttTopicSubscription req : ackTopics) { @@ -827,10 +788,8 @@ public class ProtocolProcessor { if (req.qualityOfService() == FAILURE) { continue; } - Subscription newSubscription = new Subscription( - clientID, - new Topic(req.topicName()), - req.qualityOfService()); + Subscription newSubscription = new Subscription(clientID, new Topic(req.topicName()), + req.qualityOfService()); clientSession.subscribe(newSubscription); newSubscriptions.add(newSubscription); @@ -848,7 +807,7 @@ public class ProtocolProcessor { * @return the list of verified topics for the given subscribe message. */ private List doVerify(String clientID, String username, MqttSubscribeMessage msg) { - ClientSession clientSession = m_sessionsStore.sessionForClient(clientID); + ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID); List ackTopics = new ArrayList<>(); final int messageId = messageId(msg); @@ -903,7 +862,7 @@ public class ProtocolProcessor { LOG.info("Publishing retained messages CId={}, topics={}, messagesNo={}", newSubscription.getClientId(), newSubscription.getTopicFilter(), messages.size()); } - ClientSession targetSession = m_sessionsStore.sessionForClient(newSubscription.getClientId()); + ClientSession targetSession = this.sessionsRepository.sessionForClient(newSubscription.getClientId()); this.internalRepublisher.publishRetained(targetSession, messages); // notify the Observables @@ -912,16 +871,16 @@ public class ProtocolProcessor { public void notifyChannelWritable(Channel channel) { String clientID = NettyUtils.clientID(channel); - ClientSession clientSession = m_sessionsStore.sessionForClient(clientID); + ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID); boolean emptyQueue = false; while (channel.isWritable() && !emptyQueue) { - StoredMessage msg = clientSession.queue().poll(); + EnqueuedMessage msg = clientSession.poll(); if (msg == null) { emptyQueue = true; } else { // recreate a publish from stored publish in queue - MqttPublishMessage pubMsg = createPublishForQos( msg.getTopic(), msg.getQos(), msg.getPayload(), - msg.isRetained(), 0); + MqttPublishMessage pubMsg = createPublishForQos(msg.msg.getTopic(), msg.msg.getQos(), msg.msg.getPayload(), + msg.msg.isRetained(), msg.messageId); channel.write(pubMsg); } } diff --git a/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessorBootstrapper.java b/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessorBootstrapper.java index 74ca0ebe..f91eb69a 100644 --- a/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessorBootstrapper.java +++ b/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessorBootstrapper.java @@ -30,7 +30,6 @@ import io.moquette.spi.impl.security.*; import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory; import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; import io.moquette.spi.impl.subscriptions.Subscription; -import io.moquette.spi.impl.subscriptions.SubscriptionsDirectory; import io.moquette.spi.security.IAuthenticator; import io.moquette.spi.security.IAuthorizator; import org.slf4j.Logger; @@ -98,6 +97,7 @@ public class ProtocolProcessorBootstrapper { store.initStore(); messagesStore = store.messagesStore(); m_sessionsStore = store.sessionsStore(); + SessionsRepository sessionsRepository = new SessionsRepository(this.m_sessionsStore); this.subscriptionsStore = m_sessionsStore.subscriptionStore(); storeShutdown = new Runnable() { @@ -121,7 +121,7 @@ public class ProtocolProcessorBootstrapper { LOG.info("Initializing subscriptions store..."); ISubscriptionsDirectory subscriptions = new CTrieSubscriptionDirectory(); - subscriptions.init(m_sessionsStore); + subscriptions.init(sessionsRepository); LOG.info("Configuring MQTT authenticator..."); String authenticatorClassName = props.getProperty(BrokerConstants.AUTHENTICATOR_CLASS_NAME, ""); @@ -164,7 +164,7 @@ public class ProtocolProcessorBootstrapper { } LOG.info("Initializing connection descriptor store..."); - connectionDescriptors = new ConnectionDescriptorStore(m_sessionsStore); + connectionDescriptors = new ConnectionDescriptorStore(sessionsRepository); LOG.info("Initializing MQTT protocol processor..."); boolean allowAnonymous = Boolean @@ -173,7 +173,7 @@ public class ProtocolProcessorBootstrapper { .parseBoolean(props.getProperty(BrokerConstants.ALLOW_ZERO_BYTE_CLIENT_ID_PROPERTY_NAME, "false")); m_processor.init(connectionDescriptors, subscriptions, messagesStore, m_sessionsStore, authenticator, allowAnonymous, allowZeroByteClientId, authorizator, interceptor, - props.getProperty(BrokerConstants.PORT_PROPERTY_NAME)); + sessionsRepository); return m_processor; } @@ -226,8 +226,7 @@ public class ProtocolProcessorBootstrapper { return null; } catch (NoSuchMethodException | InvocationTargetException e) { try { - LOG.info("Invoking default constructor. ClassName={}, interfaceName={}", - className, intrface.getName()); + LOG.info("Invoking default constructor. ClassName={}, interfaceName={}", className, intrface.getName()); // fallback to default constructor instance = this.getClass().getClassLoader() .loadClass(className) @@ -248,7 +247,7 @@ public class ProtocolProcessorBootstrapper { } public List getSubscriptions() { - return this.subscriptionsStore.getSubscriptions(); + return this.subscriptionsStore.listAllSubscriptions(); } public void shutdown() { diff --git a/broker/src/main/java/io/moquette/spi/impl/Qos1PublishHandler.java b/broker/src/main/java/io/moquette/spi/impl/Qos1PublishHandler.java index 9bd8c34f..f7c5c9dc 100644 --- a/broker/src/main/java/io/moquette/spi/impl/Qos1PublishHandler.java +++ b/broker/src/main/java/io/moquette/spi/impl/Qos1PublishHandler.java @@ -93,7 +93,7 @@ class Qos1PublishHandler extends QosPublishHandler { throw new RuntimeException("Internal bad error, found connectionDescriptors to null while it should " + "be initialized, somewhere it's overwritten!!"); } - LOG.debug("clientIDs are {}", connectionDescriptors); + LOG.trace("connected clientIDs are {}", connectionDescriptors.getConnectedClientIds()); if (!connectionDescriptors.isConnected(clientId)) { throw new RuntimeException(String.format("Can't find a ConnectionDescriptor for client %s in cache %s", clientId, connectionDescriptors)); diff --git a/broker/src/main/java/io/moquette/spi/impl/Qos2PublishHandler.java b/broker/src/main/java/io/moquette/spi/impl/Qos2PublishHandler.java index 1490fae3..424b2410 100644 --- a/broker/src/main/java/io/moquette/spi/impl/Qos2PublishHandler.java +++ b/broker/src/main/java/io/moquette/spi/impl/Qos2PublishHandler.java @@ -43,20 +43,20 @@ class Qos2PublishHandler extends QosPublishHandler { private final IMessagesStore m_messagesStore; private final BrokerInterceptor m_interceptor; private final ConnectionDescriptorStore connectionDescriptors; - private final ISessionsStore m_sessionsStore; private final MessagesPublisher publisher; + private final SessionsRepository sessionsRepository; public Qos2PublishHandler(IAuthorizator authorizator, ISubscriptionsDirectory subscriptions, IMessagesStore messagesStore, BrokerInterceptor interceptor, - ConnectionDescriptorStore connectionDescriptors, ISessionsStore sessionsStore, - MessagesPublisher messagesPublisher) { + ConnectionDescriptorStore connectionDescriptors, + MessagesPublisher messagesPublisher, SessionsRepository sessionsRepository) { super(authorizator); this.subscriptions = subscriptions; this.m_messagesStore = messagesStore; this.m_interceptor = interceptor; this.connectionDescriptors = connectionDescriptors; - this.m_sessionsStore = sessionsStore; this.publisher = messagesPublisher; + this.sessionsRepository = sessionsRepository; } void receivedPublishQos2(Channel channel, MqttPublishMessage msg) { @@ -78,8 +78,7 @@ class Qos2PublishHandler extends QosPublishHandler { LOG.trace("payload={}, subs Tree={}", payload2Str(toStoreMsg.getPayload()), subscriptions.dumpTree()); } - m_sessionsStore.sessionForClient(clientID) - .markAsInboundInflight(messageID, toStoreMsg); + this.sessionsRepository.sessionForClient(clientID).markAsInboundInflight(messageID, toStoreMsg); sendPubRec(clientID, messageID); @@ -105,7 +104,7 @@ class Qos2PublishHandler extends QosPublishHandler { String clientID = NettyUtils.clientID(channel); int messageID = messageId(msg); LOG.info("Processing PUBREL message. CId={}, messageId={}", clientID, messageID); - ClientSession targetSession = m_sessionsStore.sessionForClient(clientID); + ClientSession targetSession = this.sessionsRepository.sessionForClient(clientID); IMessagesStore.StoredMessage evt = targetSession.inboundInflight(messageID); if (evt == null) { LOG.warn("Can't find inbound inflight message for CId={}, messageId={}", clientID, messageID); diff --git a/broker/src/main/java/io/moquette/spi/impl/SessionsRepository.java b/broker/src/main/java/io/moquette/spi/impl/SessionsRepository.java new file mode 100644 index 00000000..6ec89414 --- /dev/null +++ b/broker/src/main/java/io/moquette/spi/impl/SessionsRepository.java @@ -0,0 +1,126 @@ +/* + * 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; + +import io.moquette.persistence.PersistentSession; +import io.moquette.spi.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class SessionsRepository { + + private static final Logger LOG = LoggerFactory.getLogger(SessionsRepository.class); + private final ISessionsStore sessions; + private ISubscriptionsStore subscriptionsStore; + private final Map sessionsCache = new ConcurrentHashMap<>(); + + public SessionsRepository(ISessionsStore sessionsStore) { + this.sessions = sessionsStore; + this.subscriptionsStore = sessionsStore.subscriptionStore(); + } + + public ClientSession sessionForClient(String clientID) { + LOG.debug("Retrieving session. CId={}", clientID); + if (this.sessionsCache.containsKey(clientID)) { + return this.sessionsCache.get(clientID); + } + + if (!this.sessions.contains(clientID)) { + LOG.warn("Session does not exist. CId={}", clientID); + return null; + } + PersistentSession storedSession = this.sessions.loadSessionByKey(clientID); + return newClientSessionAndCacheIt(storedSession.clientID, storedSession.cleanSession); + } + + private ClientSession newClientSessionAndCacheIt(String clientID, boolean cleanSession) { + ClientSession session; + if (cleanSession) { + session = new TransientSession(clientID); + } else { + DurableSession durableSession = new DurableSession(clientID, this.sessions, this.subscriptionsStore); + durableSession.reloadAllSubscriptionsFromStore(); + session = durableSession; + } + this.sessionsCache.put(clientID, session); + return session; + } + + public ClientSession createNewSession(String clientID, boolean cleanSession) { + if (sessions.contains(clientID)) { + LOG.error("Unable to create a new session: the client ID is already in use. CId={}, cleanSession={}", + clientID, cleanSession); + throw new IllegalArgumentException("Can't create a session with the ID of an already existing" + clientID); + } + LOG.debug("Creating new session. CId={}, cleanSession={}", clientID, cleanSession); + if (!cleanSession) { + sessions.createNewDurableSession(clientID); + } + return newClientSessionAndCacheIt(clientID, cleanSession); + } + + public Collection getAllSessions() { + Collection result = new ArrayList<>(); + for (PersistentSession persistentSession : this.sessions.listAllSessions()) { + result.add(sessionForClient(persistentSession.clientID)); + } + return result; + } + + private void updateCleanStatus(String clientID, boolean cleanSession) { + LOG.info("Updating cleanSession flag. CId={}, cleanSession={}", clientID, cleanSession); + this.sessions.updateCleanStatus(clientID, cleanSession); + } + + ClientSession createOrLoadClientSession(String clientId, boolean cleanSession) { + ClientSession clientSession = this.sessionForClient(clientId); + if (clientSession == null) { + clientSession = this.createNewSession(clientId, cleanSession); + } else { + //session was already present + if (!clientSession.isCleanSession() && cleanSession) { + //remove from storage & create new transient session + //TODO existing subscription from durable session has to be copied into the new transient? + sessions.removeDurableSession(clientId); + clientSession = this.createNewSession(clientId, true); + } /*else { + clientSession.cleanSession(cleanSession); + this.updateCleanStatus(clientId, cleanSession); + }*/ + } + if (cleanSession) { + LOG.info("Cleaning session. CId={}", clientId); + clientSession.cleanSession(); + } + return clientSession; + } + + public void disconnect(String clientId) { + LOG.debug("Removing session from repository's cache"); + ClientSession clientSession = this.sessionForClient(clientId); + if (clientSession == null) { + return; + } + + sessionsCache.remove(clientId); + } +} diff --git a/broker/src/main/java/io/moquette/spi/impl/subscriptions/CNode.java b/broker/src/main/java/io/moquette/spi/impl/subscriptions/CNode.java index c4322000..74a29df0 100644 --- a/broker/src/main/java/io/moquette/spi/impl/subscriptions/CNode.java +++ b/broker/src/main/java/io/moquette/spi/impl/subscriptions/CNode.java @@ -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 children; - Set subscriptions = new HashSet<>(); + Set subscriptions = new HashSet<>(); //private int subtreeSubscriptions; @@ -33,7 +34,7 @@ class CNode { } //Copy constructor - private CNode(Token token, List children, Set subscriptions) { + private CNode(Token token, List children, Set 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 toRemove = new HashSet<>(); - for (ClientTopicCouple sub : this.subscriptions) { - if (sub.clientID.equals(clientId)) { + Set toRemove = new HashSet<>(); + for (Subscription sub : this.subscriptions) { + if (sub.clientId.equals(clientId)) { toRemove.add(sub); } } this.subscriptions.removeAll(toRemove); } - -// public List childrenMatching(Token token) { -// List 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; -// } } diff --git a/broker/src/main/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectory.java b/broker/src/main/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectory.java index 9f349a36..56968c61 100644 --- a/broker/src/main/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectory.java +++ b/broker/src/main/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectory.java @@ -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 { @@ -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 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 match(Topic topic) { - final Set matchingSubs = recursiveMatch(topic, this.root); + final Set matchingSubs = recursiveMatch(topic, this.root); // remove the overlapping subscriptions, selecting ones with greatest qos Map 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 recursiveMatch(Topic topic, INode inode) { + Set 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 subscriptions = new HashSet<>(); + Set 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); } diff --git a/broker/src/main/java/io/moquette/spi/impl/subscriptions/ISubscriptionsDirectory.java b/broker/src/main/java/io/moquette/spi/impl/subscriptions/ISubscriptionsDirectory.java index 3452ba8e..82bc8e9a 100644 --- a/broker/src/main/java/io/moquette/spi/impl/subscriptions/ISubscriptionsDirectory.java +++ b/broker/src/main/java/io/moquette/spi/impl/subscriptions/ISubscriptionsDirectory.java @@ -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); diff --git a/broker/src/main/java/io/moquette/spi/impl/subscriptions/Subscription.java b/broker/src/main/java/io/moquette/spi/impl/subscriptions/Subscription.java index 37f7392a..30597ce8 100644 --- a/broker/src/main/java/io/moquette/spi/impl/subscriptions/Subscription.java +++ b/broker/src/main/java/io/moquette/spi/impl/subscriptions/Subscription.java @@ -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); - } } diff --git a/broker/src/main/java/io/moquette/spi/impl/subscriptions/SubscriptionsDirectory.java b/broker/src/main/java/io/moquette/spi/impl/subscriptions/SubscriptionsDirectory.java deleted file mode 100644 index bc3c1aa6..00000000 --- a/broker/src/main/java/io/moquette/spi/impl/subscriptions/SubscriptionsDirectory.java +++ /dev/null @@ -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 { - - void visit(TreeNode node, int deep); - - T getResult(); - } - - private class DumpTreeVisitor implements IVisitor { - - 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 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 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 matches(Topic topic) { - Queue tokenQueue = new LinkedBlockingDeque<>(topic.getTokens()); - List matchingSubs = new ArrayList<>(); - subscriptions.get().matches(tokenQueue, matchingSubs); - - // remove the overlapping subscriptions, selecting ones with greatest qos - Map 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); - } - } -} diff --git a/broker/src/main/java/io/moquette/spi/impl/subscriptions/TreeNode.java b/broker/src/main/java/io/moquette/spi/impl/subscriptions/TreeNode.java deleted file mode 100644 index 764264c9..00000000 --- a/broker/src/main/java/io/moquette/spi/impl/subscriptions/TreeNode.java +++ /dev/null @@ -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 m_children = new ArrayList<>(); - Set 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 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 tokens, List 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 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 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; - } -} diff --git a/broker/src/test/java/io/moquette/persistence/MessageStoreTCK.java b/broker/src/test/java/io/moquette/persistence/MessageStoreTCK.java index 322e7d99..882440d9 100644 --- a/broker/src/test/java/io/moquette/persistence/MessageStoreTCK.java +++ b/broker/src/test/java/io/moquette/persistence/MessageStoreTCK.java @@ -2,7 +2,7 @@ package io.moquette.persistence; import io.moquette.spi.*; import io.moquette.spi.IMessagesStore.StoredMessage; -import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple; +import io.moquette.spi.impl.SessionsRepository; import io.moquette.spi.impl.subscriptions.Subscription; import io.moquette.spi.impl.subscriptions.Topic; import io.netty.buffer.ByteBuf; @@ -25,10 +25,11 @@ public abstract class MessageStoreTCK { public static final String TEST_CLIENT = "TestClient"; protected ISessionsStore sessionsStore; protected IMessagesStore messagesStore; + protected SessionsRepository sessionsRepository; @Test public void testDropMessagesInSessionDoesntCleanAnyRetainedStoredMessages() { - final ClientSession session = sessionsStore.createNewSession(TEST_CLIENT, true); + final ClientSession session = sessionsRepository.createNewSession(TEST_CLIENT, true); StoredMessage publishToStore = new StoredMessage("Hello".getBytes(), EXACTLY_ONCE, "/topic"); publishToStore.setClientID(TEST_CLIENT); publishToStore.setRetained(true); @@ -72,7 +73,7 @@ public abstract class MessageStoreTCK { @Test public void givenSubscriptionAlreadyStoredIsOverwrittenByAnotherWithSameTopic() { - ClientSession session1 = sessionsStore.createNewSession("SESSION_ID_1", true); + ClientSession session1 = sessionsRepository.createNewSession("SESSION_ID_1", false); // Subscribe on /topic with QOSType.MOST_ONE Subscription oldSubscription = new Subscription(session1.clientID, new Topic("/topic"), AT_MOST_ONCE); @@ -84,9 +85,9 @@ public abstract class MessageStoreTCK { // Verify final ISubscriptionsStore subscriptionsStore = sessionsStore.subscriptionStore(); - List subscriptions = subscriptionsStore.listAllSubscriptions(); + List subscriptions = subscriptionsStore.listAllSubscriptions(); assertEquals(1, subscriptions.size()); - Subscription sub = subscriptionsStore.getSubscription(subscriptions.get(0)); + Subscription sub = subscriptions.get(0); assertEquals(overridingSubscription.getRequestedQos(), sub.getRequestedQos()); } } diff --git a/broker/src/test/java/io/moquette/spi/ClientSessionTest.java b/broker/src/test/java/io/moquette/spi/ClientSessionTest.java index 22fe2734..f22d5451 100644 --- a/broker/src/test/java/io/moquette/spi/ClientSessionTest.java +++ b/broker/src/test/java/io/moquette/spi/ClientSessionTest.java @@ -17,7 +17,11 @@ package io.moquette.spi; import io.moquette.persistence.MemoryStorageService; -import io.moquette.spi.impl.subscriptions.*; +import io.moquette.spi.impl.SessionsRepository; +import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory; +import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; +import io.moquette.spi.impl.subscriptions.Subscription; +import io.moquette.spi.impl.subscriptions.Topic; import org.junit.Before; import org.junit.Test; @@ -29,38 +33,46 @@ import static org.junit.Assert.assertEquals; public class ClientSessionTest { - ClientSession session1; - ClientSession session2; - ISessionsStore sessionsStore; - ISubscriptionsDirectory store; + private ISubscriptionsDirectory subscriptionsDirectory; + private SessionsRepository sessionsRepository; @Before public void setUp() { - store = new CTrieSubscriptionDirectory(); + subscriptionsDirectory = new CTrieSubscriptionDirectory(); MemoryStorageService storageService = new MemoryStorageService(null, null); - this.sessionsStore = storageService.sessionsStore(); - store.init(sessionsStore); - - session1 = sessionsStore.createNewSession("SESSION_ID_1", true); - session2 = sessionsStore.createNewSession("SESSION_ID_2", true); + ISessionsStore sessionsStore = storageService.sessionsStore(); + sessionsRepository = new SessionsRepository(sessionsStore); + subscriptionsDirectory.init(sessionsRepository); } @Test - public void overridingSubscriptions() { + public void overridingSubscriptionsForTransientSession() { + ClientSession sessionClean = sessionsRepository.createNewSession("SESSION_ID_1", true); + checkOverridingSubcription(sessionClean); + } + + @Test + public void overridingSubscriptionsForPersistentSession() { + ClientSession sessionClean = sessionsRepository.createNewSession("SESSION_ID_1", false); + checkOverridingSubcription(sessionClean); + } + + private void checkOverridingSubcription(ClientSession sessionClean) { // Subscribe on /topic with QOSType.MOST_ONE - Subscription oldSubscription = new Subscription(session1.clientID, new Topic("/topic"), AT_MOST_ONCE); - session1.subscribe(oldSubscription); - store.add(oldSubscription.asClientTopicCouple()); + Subscription oldSubscription = new Subscription(sessionClean.clientID, new Topic("/topic"), AT_MOST_ONCE); + sessionClean.subscribe(oldSubscription); + subscriptionsDirectory.add(oldSubscription); // Subscribe on /topic again that overrides the previous subscription. - Subscription overrindingSubscription = new Subscription(session1.clientID, new Topic("/topic"), EXACTLY_ONCE); - session1.subscribe(overrindingSubscription); - store.add(overrindingSubscription.asClientTopicCouple()); + Subscription overrindingSubscription = new Subscription(sessionClean.clientID, new Topic("/topic"), EXACTLY_ONCE); + sessionClean.subscribe(overrindingSubscription); + subscriptionsDirectory.add(overrindingSubscription); // Verify - List subscriptions = store.matches(new Topic("/topic")); - assertEquals(1, subscriptions.size()); + List subscriptions = subscriptionsDirectory.matches(new Topic("/topic")); + assertEquals("Only one subscription must match /topic", 1, subscriptions.size()); Subscription sub = subscriptions.get(0); - assertEquals(overrindingSubscription.getRequestedQos(), sub.getRequestedQos()); + assertEquals("Matching subscription MUST have higher QoS", + overrindingSubscription.getRequestedQos(), sub.getRequestedQos()); } } diff --git a/broker/src/test/java/io/moquette/spi/impl/AbstractProtocolProcessorCommonUtils.java b/broker/src/test/java/io/moquette/spi/impl/AbstractProtocolProcessorCommonUtils.java index d9e5f24f..6d7096cd 100644 --- a/broker/src/test/java/io/moquette/spi/impl/AbstractProtocolProcessorCommonUtils.java +++ b/broker/src/test/java/io/moquette/spi/impl/AbstractProtocolProcessorCommonUtils.java @@ -20,7 +20,6 @@ import io.moquette.persistence.MemoryStorageService; import io.moquette.server.netty.NettyUtils; import io.moquette.spi.IMessagesStore; import io.moquette.spi.ISessionsStore; -import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple; import io.moquette.spi.impl.security.PermitAllAuthorizator; import io.moquette.spi.impl.subscriptions.*; import io.netty.buffer.Unpooled; @@ -56,6 +55,7 @@ abstract class AbstractProtocolProcessorCommonUtils { ISessionsStore m_sessionStore; ISubscriptionsDirectory subscriptions; MockAuthenticator m_mockAuthenticator; + protected SessionsRepository sessionsRepository; protected void initializeProcessorAndSubsystems() { m_channel = new EmbeddedChannel(); @@ -68,6 +68,8 @@ abstract class AbstractProtocolProcessorCommonUtils { m_sessionStore = memStorage.sessionsStore(); // m_messagesStore.initStore(); + sessionsRepository = new SessionsRepository(m_sessionStore); + Set clientIds = new HashSet<>(); clientIds.add(FAKE_CLIENT_ID); clientIds.add(FAKE_CLIENT_ID2); @@ -76,14 +78,15 @@ abstract class AbstractProtocolProcessorCommonUtils { m_mockAuthenticator = new MockAuthenticator(clientIds, users); subscriptions = new CTrieSubscriptionDirectory(); - subscriptions.init(memStorage.sessionsStore()); + subscriptions.init(sessionsRepository); m_processor = new ProtocolProcessor(); m_processor.init(subscriptions, m_messagesStore, m_sessionStore, m_mockAuthenticator, true, - new PermitAllAuthorizator(), NO_OBSERVERS_INTERCEPTOR); + new PermitAllAuthorizator(), NO_OBSERVERS_INTERCEPTOR, sessionsRepository); } protected void verifyNoPublishIsReceived() { - assertNull("Received an out message from processor while not expected", m_channel.readOutbound()); + final Object messageReceived = m_channel.readOutbound(); + assertNull("Received an out message from processor while not expected", messageReceived); } protected void subscribe(String topic, MqttQoS desiredQos) { @@ -106,8 +109,7 @@ abstract class AbstractProtocolProcessorCommonUtils { protected static void verifySubscriptionExists(Channel channel, ISessionsStore sessionsStore, Subscription expectedSubscription) { final String clientId = NettyUtils.clientID(channel); - final Subscription subscription = sessionsStore.subscriptionStore() - .getSubscription(new ClientTopicCouple(clientId, expectedSubscription.getTopicFilter())); + final Subscription subscription = sessionsStore.subscriptionStore().reload(expectedSubscription); assertEquals(expectedSubscription, subscription); } diff --git a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessorTest.java b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessorTest.java index 2589d1ac..4741d98b 100644 --- a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessorTest.java +++ b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessorTest.java @@ -22,9 +22,9 @@ import io.moquette.server.netty.NettyUtils; import io.moquette.spi.IMessagesStore; import io.moquette.spi.IMessagesStore.StoredMessage; import io.moquette.spi.impl.security.PermitAllAuthorizator; +import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory; import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; import io.moquette.spi.impl.subscriptions.Subscription; -import io.moquette.spi.impl.subscriptions.SubscriptionsDirectory; import io.moquette.spi.impl.subscriptions.Topic; import io.moquette.spi.security.IAuthorizator; import io.netty.buffer.Unpooled; @@ -33,7 +33,10 @@ import io.netty.handler.codec.mqtt.*; import org.junit.Before; import org.junit.Test; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import static io.moquette.spi.impl.NettyChannelAssertions.assertConnAckAccepted; import static io.moquette.spi.impl.ProtocolProcessor.lowerQosToTheSubscriptionDesired; @@ -69,7 +72,7 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils final Subscription subscription = new Subscription(FAKE_CLIENT_ID, new Topic(FAKE_TOPIC), AT_MOST_ONCE); // subscriptions.matches(topic) redefine the method to return true - ISubscriptionsDirectory subs = new SubscriptionsDirectory() { + ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() { @Override public List matches(Topic topic) { @@ -83,9 +86,10 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils // simulate a connect that register a clientID to an IoSession MemoryStorageService storageService = new MemoryStorageService(null, null); - subs.init(storageService.sessionsStore()); + SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore()); + subs.init(sessionsRepository); m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), - NO_OBSERVERS_INTERCEPTOR); + NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository); connect_v3_1(); @@ -105,7 +109,7 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils AT_MOST_ONCE); // subscriptions.matches(topic) redefine the method to return true - ISubscriptionsDirectory subs = new SubscriptionsDirectory() { + ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() { @Override public List matches(Topic topic) { @@ -119,9 +123,10 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils // simulate a connect that register a clientID to an IoSession MemoryStorageService storageService = new MemoryStorageService(null, null); - subs.init(storageService.sessionsStore()); + SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore()); + subs.init(sessionsRepository); m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), - NO_OBSERVERS_INTERCEPTOR); + NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository); EmbeddedChannel firstReceiverChannel = new EmbeddedChannel(); MqttConnectMessage connectMessage = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1) @@ -174,7 +179,7 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils .thenReturn(false); m_processor.init(subscriptions, m_messagesStore, m_sessionStore, m_mockAuthenticator, true, mockAuthorizator, - NO_OBSERVERS_INTERCEPTOR); + NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository); connect(); @@ -229,7 +234,7 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils new Subscription(FAKE_PUBLISHER_ID, new Topic(FAKE_TOPIC), AT_MOST_ONCE); // subscriptions.matches(topic) redefine the method to return true - ISubscriptionsDirectory subs = new SubscriptionsDirectory() { + ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() { @Override public List matches(Topic topic) { @@ -241,11 +246,12 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils } }; MemoryStorageService storageService = new MemoryStorageService(null, null); - subs.init(storageService.sessionsStore()); + SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore()); + subs.init(sessionsRepository); // simulate a connect that register a clientID to an IoSession m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), - NO_OBSERVERS_INTERCEPTOR); + NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository); connect_v3_1_asClient(FAKE_PUBLISHER_ID); publishToAs(FAKE_PUBLISHER_ID, FAKE_TOPIC, AT_MOST_ONCE, true); NettyUtils.cleanSession(m_channel, false); @@ -259,7 +265,7 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils @Test public void testRepublishAndConsumePersistedMessages_onReconnect() { - ISubscriptionsDirectory subs = mock(SubscriptionsDirectory.class); + ISubscriptionsDirectory subs = mock(ISubscriptionsDirectory.class); List emptySubs = Collections.emptyList(); when(subs.matches(any(Topic.class))).thenReturn(emptySubs); @@ -269,27 +275,26 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils m_messagesStore.storeRetained(new Topic("/topic"), retainedMessage); m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), - NO_OBSERVERS_INTERCEPTOR); + NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository); connect_v3_1_asClient(FAKE_PUBLISHER_ID); // Verify no messages are still stored - Queue messages = m_sessionStore.queue(FAKE_PUBLISHER_ID); - assertTrue(messages.isEmpty()); + assertTrue(this.sessionsRepository.sessionForClient(FAKE_PUBLISHER_ID).isEmptyQueue()); } @Test public void publishNoPublishToInactiveSession() { // create an inactive session for Subscriber - m_sessionStore.createNewSession("Subscriber", false); - - ISubscriptionsDirectory mockedSubscriptions = mock(SubscriptionsDirectory.class); + ISubscriptionsDirectory mockedSubscriptions = mock(ISubscriptionsDirectory.class); Subscription inactiveSub = new Subscription("Subscriber", new Topic("/topic"), MqttQoS.AT_LEAST_ONCE); List inactiveSubscriptions = Collections.singletonList(inactiveSub); when(mockedSubscriptions.matches(eq(new Topic("/topic")))).thenReturn(inactiveSubscriptions); m_processor = new ProtocolProcessor(); m_processor.init(mockedSubscriptions, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), - NO_OBSERVERS_INTERCEPTOR); + NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository); + + m_processor.sessionsRepository.createNewSession("Subscriber", false); // Exercise connectAsClient("Publisher"); diff --git a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_CONNECT_Test.java b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_CONNECT_Test.java index 77728487..5a640fad 100644 --- a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_CONNECT_Test.java +++ b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_CONNECT_Test.java @@ -22,7 +22,10 @@ import io.moquette.spi.ClientSession; import io.moquette.spi.IMessagesStore; import io.moquette.spi.ISessionsStore; import io.moquette.spi.impl.security.PermitAllAuthorizator; -import io.moquette.spi.impl.subscriptions.*; +import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory; +import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; +import io.moquette.spi.impl.subscriptions.Subscription; +import io.moquette.spi.impl.subscriptions.Topic; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.mqtt.*; import org.junit.Before; @@ -62,10 +65,11 @@ public class ProtocolProcessor_CONNECT_Test { m_mockAuthenticator = new MockAuthenticator(singleton(FAKE_CLIENT_ID), singletonMap(TEST_USER, TEST_PWD)); subscriptions = new CTrieSubscriptionDirectory(); - subscriptions.init(m_sessionStore); + SessionsRepository sessionsRepository = new SessionsRepository(m_sessionStore); + subscriptions.init(sessionsRepository); m_processor = new ProtocolProcessor(); m_processor.init(subscriptions, m_messagesStore, m_sessionStore, m_mockAuthenticator, true, - new PermitAllAuthorizator(), NO_OBSERVERS_INTERCEPTOR); + new PermitAllAuthorizator(), NO_OBSERVERS_INTERCEPTOR, new SessionsRepository(this.m_sessionStore)); } @Test @@ -155,7 +159,7 @@ public class ProtocolProcessor_CONNECT_Test { protected void reinitProcessorProhibitingAnonymousClients() { m_processor.init(subscriptions, m_messagesStore, m_sessionStore, m_mockAuthenticator, false, - new PermitAllAuthorizator(), NO_OBSERVERS_INTERCEPTOR); + new PermitAllAuthorizator(), NO_OBSERVERS_INTERCEPTOR, new SessionsRepository(this.m_sessionStore)); } @Test @@ -203,7 +207,7 @@ public class ProtocolProcessor_CONNECT_Test { assertEqualsConnAck(CONNECTION_ACCEPTED, m_session.readOutbound()); // Verify client session is clean false - ClientSession session = m_sessionStore.sessionForClient(FAKE_CLIENT_ID); + ClientSession session = m_processor.sessionsRepository.sessionForClient(FAKE_CLIENT_ID); assertFalse(session.isCleanSession()); // Verify @@ -320,7 +324,7 @@ public class ProtocolProcessor_CONNECT_Test { protected void reinitProtocolProcessorWithZeroLengthClientIdAndAnonymousClients() { m_processor.init(subscriptions, m_messagesStore, m_sessionStore, m_mockAuthenticator, true, true, - new PermitAllAuthorizator(), NO_OBSERVERS_INTERCEPTOR); + new PermitAllAuthorizator(), NO_OBSERVERS_INTERCEPTOR, new SessionsRepository(this.m_sessionStore)); } @Test diff --git a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_InternalPublish_Test.java b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_InternalPublish_Test.java index d6dff32f..f0732c3f 100644 --- a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_InternalPublish_Test.java +++ b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_InternalPublish_Test.java @@ -177,13 +177,13 @@ public class ProtocolProcessor_InternalPublish_Test extends AbstractProtocolProc public void testClientSubscribeWithoutCleanSession() throws Exception { subscribe("foo", AT_MOST_ONCE); disconnect(); - assertEquals(1, m_processor.getSessionsStore().subscriptionStore().getSubscriptions().size()); + assertEquals(1, m_sessionStore.subscriptionStore().listAllSubscriptions().size()); m_channel = new EmbeddedChannel(); NettyUtils.clientID(m_channel, FAKE_CLIENT_ID); connectNoCleanSession(); - assertEquals(1, m_processor.getSessionsStore().subscriptionStore().getSubscriptions().size()); + assertEquals(1, m_sessionStore.subscriptionStore().listAllSubscriptions().size()); internalPublishTo("foo", MqttQoS.AT_MOST_ONCE, false); verifyPublishIsReceived(); } diff --git a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_VariousUseCases_Test.java b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_VariousUseCases_Test.java index 140b89a2..5f173502 100644 --- a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_VariousUseCases_Test.java +++ b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_VariousUseCases_Test.java @@ -15,18 +15,14 @@ */ package io.moquette.spi.impl; -import io.moquette.server.MessageCollector; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.mqtt.MqttQoS; -import org.eclipse.paho.client.mqttv3.MqttClient; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import static io.netty.handler.codec.mqtt.MqttQoS.*; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; public class ProtocolProcessor_VariousUseCases_Test extends AbstractProtocolProcessorCommonUtils { @@ -43,8 +39,6 @@ public class ProtocolProcessor_VariousUseCases_Test extends AbstractProtocolProc // reconnect and publish EmbeddedChannel anotherChannel = new EmbeddedChannel(); -// NettyUtils.clientID(anotherChannel, "TestClient"); -// NettyUtils.cleanSession(anotherChannel, false); connectAsClient(anotherChannel, "TestClient"); publishToAs(anotherChannel, "TestClient", "/topic", AT_MOST_ONCE, false); diff --git a/broker/src/test/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectoryMatchingTest.java b/broker/src/test/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectoryMatchingTest.java index e4ee57cf..78f2a486 100644 --- a/broker/src/test/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectoryMatchingTest.java +++ b/broker/src/test/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectoryMatchingTest.java @@ -18,13 +18,11 @@ import io.moquette.persistence.MemoryStorageService; import io.moquette.spi.ClientSession; import io.moquette.spi.ISessionsStore; import io.moquette.spi.ISubscriptionsStore; -import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple; +import io.moquette.spi.impl.SessionsRepository; import io.netty.handler.codec.mqtt.MqttQoS; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; -import java.util.List; import java.util.Optional; import java.util.Set; @@ -37,28 +35,27 @@ import static org.junit.Assert.assertTrue; public class CTrieSubscriptionDirectoryMatchingTest { private CTrieSubscriptionDirectory sut; - private ISessionsStore sessionsStore; private ISubscriptionsStore subscriptionsStore; - private MemoryStorageService storageService; + private SessionsRepository sessionsRepository; @Before public void setUp() { sut = new CTrieSubscriptionDirectory(); - this.storageService = new MemoryStorageService(null, null); - this.sessionsStore = storageService.sessionsStore(); - sut.init(sessionsStore); - - this.subscriptionsStore = this.sessionsStore.subscriptionStore(); + MemoryStorageService storageService = new MemoryStorageService(null, null); + ISessionsStore sessionsStore = storageService.sessionsStore(); + this.subscriptionsStore = sessionsStore.subscriptionStore(); + this.sessionsRepository = new SessionsRepository(sessionsStore); + sut.init(this.sessionsRepository); } @Test public void testMatchSimple() { - ClientTopicCouple slashSub = clientSubOnTopic("TempSensor1", "/"); + Subscription slashSub = clientSubOnTopic("TempSensor1", "/"); sut.add(slashSub); assertThat(sut.recursiveMatch(asTopic("finance"), sut.root)).isEmpty(); - ClientTopicCouple slashFinanceSub = clientSubOnTopic("TempSensor1", "/finance"); + Subscription slashFinanceSub = clientSubOnTopic("TempSensor1", "/finance"); sut.add(slashFinanceSub); assertThat(sut.recursiveMatch(asTopic("finance"), sut.root)).isEmpty(); @@ -68,19 +65,19 @@ public class CTrieSubscriptionDirectoryMatchingTest { @Test public void testMatchSimpleMulti() { - ClientTopicCouple anySub = clientSubOnTopic("TempSensor1", "#"); + Subscription anySub = clientSubOnTopic("TempSensor1", "#"); sut.add(anySub); assertThat(sut.recursiveMatch(asTopic("finance"), sut.root)).contains(anySub); - ClientTopicCouple financeAnySub = clientSubOnTopic("TempSensor1", "finance/#"); + Subscription financeAnySub = clientSubOnTopic("TempSensor1", "finance/#"); sut.add(financeAnySub); assertThat(sut.recursiveMatch(asTopic("finance"), sut.root)).containsExactlyInAnyOrder(financeAnySub, anySub); } @Test public void testMatchingDeepMulti_one_layer() { - ClientTopicCouple anySub = clientSubOnTopic("AllSensor1", "#"); - ClientTopicCouple financeAnySub = clientSubOnTopic("FinanceSensor", "finance/#"); + Subscription anySub = clientSubOnTopic("AllSensor1", "#"); + Subscription financeAnySub = clientSubOnTopic("FinanceSensor", "finance/#"); sut.add(anySub); sut.add(financeAnySub); @@ -93,7 +90,7 @@ public class CTrieSubscriptionDirectoryMatchingTest { @Test public void testMatchingDeepMulti_two_layer() { - ClientTopicCouple financeAnySub = clientSubOnTopic("FinanceSensor", "finance/stock/#"); + Subscription financeAnySub = clientSubOnTopic("FinanceSensor", "finance/stock/#"); sut.add(financeAnySub); // Verify @@ -102,18 +99,18 @@ public class CTrieSubscriptionDirectoryMatchingTest { @Test public void testMatchSimpleSingle() { - ClientTopicCouple anySub = clientSubOnTopic("AnySensor", "+"); + Subscription anySub = clientSubOnTopic("AnySensor", "+"); sut.add(anySub); assertThat(sut.recursiveMatch(asTopic("finance"), sut.root)).containsExactly(anySub); - ClientTopicCouple financeOne = clientSubOnTopic("AnySensor", "finance/+"); + Subscription financeOne = clientSubOnTopic("AnySensor", "finance/+"); sut.add(financeOne); assertThat(sut.recursiveMatch(asTopic("finance/stock"), sut.root)).containsExactly(financeOne); } @Test public void testMatchManySingle() { - ClientTopicCouple manySub = clientSubOnTopic("AnySensor", "+/+"); + Subscription manySub = clientSubOnTopic("AnySensor", "+/+"); sut.add(manySub); // verify @@ -122,9 +119,9 @@ public class CTrieSubscriptionDirectoryMatchingTest { @Test public void testMatchSlashSingle() { - ClientTopicCouple slashPlusSub = clientSubOnTopic("AnySensor", "/+"); + Subscription slashPlusSub = clientSubOnTopic("AnySensor", "/+"); sut.add(slashPlusSub); - ClientTopicCouple anySub = clientSubOnTopic("AnySensor", "+"); + Subscription anySub = clientSubOnTopic("AnySensor", "+"); sut.add(anySub); // Verify @@ -134,9 +131,9 @@ public class CTrieSubscriptionDirectoryMatchingTest { @Test public void testMatchManyDeepSingle() { - ClientTopicCouple slashPlusSub = clientSubOnTopic("FinanceSensor1", "/finance/+/ibm"); + Subscription slashPlusSub = clientSubOnTopic("FinanceSensor1", "/finance/+/ibm"); sut.add(slashPlusSub); - ClientTopicCouple slashPlusDeepSub = clientSubOnTopic("FinanceSensor2", "/+/stock/+"); + Subscription slashPlusDeepSub = clientSubOnTopic("FinanceSensor2", "/+/stock/+"); sut.add(slashPlusDeepSub); // Verify @@ -146,7 +143,7 @@ public class CTrieSubscriptionDirectoryMatchingTest { @Test public void testMatchSimpleMulti_allTheTree() { - ClientTopicCouple sub = clientSubOnTopic("AnySensor1", "#"); + Subscription sub = clientSubOnTopic("AnySensor1", "#"); sut.add(sub); assertThat(sut.recursiveMatch(asTopic("finance"), sut.root)).isNotEmpty(); @@ -181,9 +178,10 @@ public class CTrieSubscriptionDirectoryMatchingTest { sut = new CTrieSubscriptionDirectory(); MemoryStorageService memStore = new MemoryStorageService(null, null); ISessionsStore aSessionsStore = memStore.sessionsStore(); - sut.init(aSessionsStore); + SessionsRepository sessionsRepository = new SessionsRepository(aSessionsStore); + sut.init(sessionsRepository); - ClientTopicCouple sub = clientSubOnTopic("AnySensor1", s); + Subscription sub = clientSubOnTopic("AnySensor1", s); sut.add(sub); assertThat(sut.recursiveMatch(asTopic(t), sut.root)).isNotEmpty(); @@ -193,9 +191,10 @@ public class CTrieSubscriptionDirectoryMatchingTest { sut = new CTrieSubscriptionDirectory(); MemoryStorageService memStore = new MemoryStorageService(null, null); ISessionsStore aSessionsStore = memStore.sessionsStore(); - sut.init(aSessionsStore); + SessionsRepository sessionsRepository = new SessionsRepository(aSessionsStore); + sut.init(sessionsRepository); - ClientTopicCouple sub = clientSubOnTopic("AnySensor1", subscription); + Subscription sub = clientSubOnTopic("AnySensor1", subscription); sut.add(sub); assertThat(sut.recursiveMatch(asTopic(topic), sut.root)).isEmpty(); @@ -203,47 +202,50 @@ public class CTrieSubscriptionDirectoryMatchingTest { @Test public void testOverlappingSubscriptions() { - Subscription sub = new Subscription("Sensor1", asTopic("a/+"), MqttQoS.AT_MOST_ONCE); - ClientTopicCouple genericSub = sub.asClientTopicCouple(); - this.subscriptionsStore.addNewSubscription(sub); - storageService.sessionsStore().createNewSession("Sensor1", false); + Subscription genericSub = new Subscription("Sensor1", asTopic("a/+"), MqttQoS.AT_MOST_ONCE); + this.subscriptionsStore.addNewSubscription(genericSub); + final ClientSession sensorSession = this.sessionsRepository.createNewSession("Sensor1", false); + sensorSession.subscribe(genericSub); sut.add(genericSub); - Subscription sub2 = new Subscription("Sensor1", asTopic("a/b"), MqttQoS.AT_MOST_ONCE); - ClientTopicCouple specificSub = sub2.asClientTopicCouple(); - this.subscriptionsStore.addNewSubscription(sub2); + Subscription specificSub = new Subscription("Sensor1", asTopic("a/b"), MqttQoS.AT_MOST_ONCE); + sensorSession.subscribe(specificSub); + this.subscriptionsStore.addNewSubscription(specificSub); sut.add(specificSub); + //Exercise + final Set matchingForSpecific = sut.match(asTopic("a/b")); + // Verify - assertThat(sut.match(asTopic("a/b")).size()).isEqualTo(1); + assertThat(matchingForSpecific.size()).isEqualTo(1); } @Test public void removeSubscription_withDifferentClients_subscribedSameTopic() { - ClientTopicCouple slashSub = clientSubOnTopic("Sensor1", "/topic"); + Subscription slashSub = clientSubOnTopic("Sensor1", "/topic"); sut.add(slashSub); - ClientTopicCouple slashSub2 = clientSubOnTopic("Sensor2", "/topic"); + Subscription slashSub2 = clientSubOnTopic("Sensor2", "/topic"); sut.add(slashSub2); // Exercise - sut.removeSubscription(asTopic("/topic"), slashSub2.clientID); + sut.removeSubscription(asTopic("/topic"), slashSub2.clientId); // Verify - ClientTopicCouple remainedSubscription = sut.recursiveMatch(asTopic("/topic"), sut.root).iterator().next(); - assertThat(remainedSubscription.clientID).isEqualTo(slashSub.clientID); - assertEquals(slashSub.clientID, remainedSubscription.clientID); + Subscription remainedSubscription = sut.recursiveMatch(asTopic("/topic"), sut.root).iterator().next(); + assertThat(remainedSubscription.clientId).isEqualTo(slashSub.clientId); + assertEquals(slashSub.clientId, remainedSubscription.clientId); } @Test public void removeSubscription_sameClients_subscribedSameTopic() { - ClientTopicCouple slashSub = clientSubOnTopic("Sensor1", "/topic"); + Subscription slashSub = clientSubOnTopic("Sensor1", "/topic"); sut.add(slashSub); // Exercise - sut.removeSubscription(asTopic("/topic"), slashSub.clientID); + sut.removeSubscription(asTopic("/topic"), slashSub.clientId); // Verify - final Set matchingSubscriptions = sut.recursiveMatch(asTopic("/topic"), sut.root); + final Set matchingSubscriptions = sut.recursiveMatch(asTopic("/topic"), sut.root); assertThat(matchingSubscriptions).isEmpty(); } @@ -252,18 +254,18 @@ public class CTrieSubscriptionDirectoryMatchingTest { */ @Test public void duplicatedSubscriptionsWithDifferentQos() { - ClientSession session2 = sessionsStore.createNewSession("client2", true); + ClientSession session2 = this.sessionsRepository.createNewSession("client2", true); Subscription client2Sub = new Subscription("client2", asTopic("client/test/b"), MqttQoS.AT_MOST_ONCE); session2.subscribe(client2Sub); - this.sut.add(client2Sub.asClientTopicCouple()); - ClientSession session1 = sessionsStore.createNewSession("client1", true); + this.sut.add(client2Sub); + ClientSession session1 = this.sessionsRepository.createNewSession("client1", true); Subscription client1SubQoS0 = new Subscription("client1", asTopic("client/test/b"), MqttQoS.AT_MOST_ONCE); session1.subscribe(client1SubQoS0); - this.sut.add(client1SubQoS0.asClientTopicCouple()); + this.sut.add(client1SubQoS0); Subscription client1SubQoS2 = new Subscription("client1", asTopic("client/test/b"), MqttQoS.EXACTLY_ONCE); session1.subscribe(client1SubQoS2); - this.sut.add(client1SubQoS2.asClientTopicCouple()); + this.sut.add(client1SubQoS2); // Verify Set subscriptions = this.sut.match(asTopic("client/test/b")); diff --git a/broker/src/test/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectoryTest.java b/broker/src/test/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectoryTest.java index f9f39e11..954ab7a8 100644 --- a/broker/src/test/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectoryTest.java +++ b/broker/src/test/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectoryTest.java @@ -16,7 +16,7 @@ import io.moquette.persistence.MemoryStorageService; import io.moquette.spi.ISessionsStore; -import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple; +import io.moquette.spi.impl.SessionsRepository; import org.junit.Before; import org.junit.Test; @@ -24,8 +24,9 @@ import java.util.Optional; import java.util.Set; import static io.moquette.spi.impl.subscriptions.Topic.asTopic; -import static org.junit.Assert.*; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public class CTrieSubscriptionDirectoryTest { @@ -36,7 +37,8 @@ public class CTrieSubscriptionDirectoryTest { sut = new CTrieSubscriptionDirectory(); MemoryStorageService memStore = new MemoryStorageService(null, null); ISessionsStore aSessionsStore = memStore.sessionsStore(); - sut.init(aSessionsStore); + SessionsRepository sessionsRepository = new SessionsRepository(aSessionsStore); + sut.init(sessionsRepository); } @Test @@ -73,7 +75,7 @@ public class CTrieSubscriptionDirectoryTest { @Test public void testLookup() { - final ClientTopicCouple existingSubscription = clientSubOnTopic("TempSensor1", "/temp"); + final Subscription existingSubscription = clientSubOnTopic("TempSensor1", "/temp"); sut.add(existingSubscription); //Exercise @@ -85,17 +87,17 @@ public class CTrieSubscriptionDirectoryTest { @Test public void testAddNewSubscriptionOnExistingNode() { - final ClientTopicCouple existingSubscription = clientSubOnTopic("TempSensor1", "/temp"); + final Subscription existingSubscription = clientSubOnTopic("TempSensor1", "/temp"); sut.add(existingSubscription); //Exercise - final ClientTopicCouple newSubscription = clientSubOnTopic("TempSensor2", "/temp"); + final Subscription newSubscription = clientSubOnTopic("TempSensor2", "/temp"); sut.add(newSubscription); //Verify final Optional matchedNode = sut.lookup(asTopic("/temp")); assertTrue("Node on path /temp must be present", matchedNode.isPresent()); - final Set subscriptions = matchedNode.get().subscriptions; + final Set subscriptions = matchedNode.get().subscriptions; assertTrue(subscriptions.contains(newSubscription)); } @@ -104,18 +106,18 @@ public class CTrieSubscriptionDirectoryTest { sut.add(clientSubOnTopic("TempSensorRM", "/italy/roma/temp")); sut.add(clientSubOnTopic("TempSensorFI", "/italy/firenze/temp")); sut.add(clientSubOnTopic("HumSensorFI", "/italy/roma/humidity")); - final ClientTopicCouple happinessSensor = clientSubOnTopic("HappinessSensor", "/italy/happiness"); + final Subscription happinessSensor = clientSubOnTopic("HappinessSensor", "/italy/happiness"); sut.add(happinessSensor); //Verify final Optional matchedNode = sut.lookup(asTopic("/italy/happiness")); assertTrue("Node on path /italy/happiness must be present", matchedNode.isPresent()); - final Set subscriptions = matchedNode.get().subscriptions; + final Set subscriptions = matchedNode.get().subscriptions; assertTrue(subscriptions.contains(happinessSensor)); } - static ClientTopicCouple clientSubOnTopic(String clientID, String topicName) { - return new ClientTopicCouple(clientID, asTopic(topicName)); + static Subscription clientSubOnTopic(String clientID, String topicName) { + return new Subscription(clientID, asTopic(topicName), null); } @Test @@ -135,10 +137,10 @@ public class CTrieSubscriptionDirectoryTest { sut.add(clientSubOnTopic("TempSensor1", "/temp")); //Exercise - final Set matchingSubs = sut.recursiveMatch(asTopic("/temp"), sut.root); + final Set matchingSubs = sut.recursiveMatch(asTopic("/temp"), sut.root); //Verify - final ClientTopicCouple expectedMatchingsub = new ClientTopicCouple("TempSensor1", asTopic("/temp")); + final Subscription expectedMatchingsub = new Subscription("TempSensor1", asTopic("/temp")); assertThat(matchingSubs).contains(expectedMatchingsub); } } diff --git a/broker/src/test/java/io/moquette/spi/impl/subscriptions/SubscriptionsDirectoryTest.java b/broker/src/test/java/io/moquette/spi/impl/subscriptions/SubscriptionsDirectoryTest.java deleted file mode 100644 index 4373b527..00000000 --- a/broker/src/test/java/io/moquette/spi/impl/subscriptions/SubscriptionsDirectoryTest.java +++ /dev/null @@ -1,332 +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.persistence.MemoryStorageService; -import io.moquette.spi.ClientSession; -import io.moquette.spi.ISessionsStore; -import io.moquette.spi.ISubscriptionsStore; -import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple; -import io.netty.handler.codec.mqtt.MqttQoS; -import org.junit.Before; -import org.junit.Test; - -import java.io.IOException; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -public class SubscriptionsDirectoryTest { - - private SubscriptionsDirectory store; - private ISessionsStore sessionsStore; - private ISubscriptionsStore subscriptionsStore; - - public SubscriptionsDirectoryTest() { - } - - @Before - public void setUp() throws IOException { - store = new SubscriptionsDirectory(); - MemoryStorageService storageService = new MemoryStorageService(null, null); - this.sessionsStore = storageService.sessionsStore(); - store.init(sessionsStore); - - this.subscriptionsStore = this.sessionsStore.subscriptionStore(); - - storageService.sessionsStore().createNewSession("FAKE_CLI_ID_1", false); - storageService.sessionsStore().createNewSession("FAKE_CLI_ID_2", false); - } - - @Test - public void testMatchSimple() { - Subscription slashSub = new Subscription("FAKE_CLI_ID_1", new Topic("/"), MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(slashSub); - store.add(slashSub.asClientTopicCouple()); - assertThat(store.matches(new Topic("finance"))).isEmpty(); - - Subscription slashFinanceSub = new Subscription("FAKE_CLI_ID_1", new Topic("/finance"), MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(slashFinanceSub); - store.add(slashFinanceSub.asClientTopicCouple()); - assertThat(store.matches(new Topic("finance"))).isEmpty(); - - assertThat(store.matches(new Topic("/finance"))).contains(slashFinanceSub); - assertThat(store.matches(new Topic("/"))).contains(slashSub); - } - - @Test - public void testMatchSimpleMulti() { - Subscription anySub = new Subscription("FAKE_CLI_ID_1", new Topic("#"), MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(anySub); - store.add(anySub.asClientTopicCouple()); - assertThat(store.matches(new Topic("finance"))).contains(anySub); - - Subscription financeAnySub = new Subscription("FAKE_CLI_ID_2", new Topic("finance/#"), MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(financeAnySub); - store.add(financeAnySub.asClientTopicCouple()); - assertThat(store.matches(new Topic("finance"))).containsExactlyInAnyOrder(financeAnySub, anySub); - } - - @Test - public void testMatchingDeepMulti_one_layer() { - Subscription anySub = new Subscription("FAKE_CLI_ID_1", new Topic("#"), MqttQoS.AT_MOST_ONCE); - Subscription financeAnySub = new Subscription("FAKE_CLI_ID_2", new Topic("finance/#"), MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(anySub); - this.subscriptionsStore.addNewSubscription(financeAnySub); - store.add(anySub.asClientTopicCouple()); - store.add(financeAnySub.asClientTopicCouple()); - - // Verify - assertThat(store.matches(new Topic("finance/stock"))).containsExactlyInAnyOrder(financeAnySub, anySub); - assertThat(store.matches(new Topic("finance/stock/ibm"))).containsExactlyInAnyOrder(financeAnySub, anySub); - } - - @Test - public void testMatchingDeepMulti_two_layer() { - Subscription financeAnySub = new Subscription( - "FAKE_CLI_ID_1", - new Topic("finance/stock/#"), - MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(financeAnySub); - store.add(financeAnySub.asClientTopicCouple()); - - // Verify - assertThat(store.matches(new Topic("finance/stock/ibm"))).containsExactly(financeAnySub); - } - - @Test - public void testMatchSimpleSingle() { - Subscription anySub = new Subscription("FAKE_CLI_ID_1", new Topic("+"), MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(anySub); - store.add(anySub.asClientTopicCouple()); - assertThat(store.matches(new Topic("finance"))).containsExactly(anySub); - - Subscription financeOne = new Subscription("FAKE_CLI_ID_1", new Topic("finance/+"), MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(financeOne); - store.add(financeOne.asClientTopicCouple()); - assertThat(store.matches(new Topic("finance/stock"))).containsExactly(financeOne); - } - - @Test - public void testMatchManySingle() { - Subscription manySub = new Subscription("FAKE_CLI_ID_1", new Topic("+/+"), MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(manySub); - store.add(manySub.asClientTopicCouple()); - - // verify - assertThat(store.matches(new Topic("/finance"))).contains(manySub); - } - - @Test - public void testMatchSlashSingle() { - Subscription slashPlusSub = new Subscription("FAKE_CLI_ID_1", new Topic("/+"), MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(slashPlusSub); - store.add(slashPlusSub.asClientTopicCouple()); - Subscription anySub = new Subscription("FAKE_CLI_ID_1", new Topic("+"), MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(anySub); - store.add(anySub.asClientTopicCouple()); - - // Verify - assertThat(store.matches(new Topic("/finance"))).containsOnly(slashPlusSub); - assertThat(store.matches(new Topic("/finance"))).doesNotContain(anySub); - } - - @Test - public void testMatchManyDeepSingle() { - Subscription slashPlusSub = new Subscription( - "FAKE_CLI_ID_1", - new Topic("/finance/+/ibm"), - MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(slashPlusSub); - store.add(slashPlusSub.asClientTopicCouple()); - - Subscription slashPlusDeepSub = new Subscription( - "FAKE_CLI_ID_2", - new Topic("/+/stock/+"), - MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(slashPlusDeepSub); - store.add(slashPlusDeepSub.asClientTopicCouple()); - - // Verify - assertThat(store.matches(new Topic("/finance/stock/ibm"))) - .containsExactlyInAnyOrder(slashPlusSub, slashPlusDeepSub); - } - - @Test - public void testMatchSimpleMulti_allTheTree() { - Subscription sub = new Subscription("FAKE_CLI_ID_1", new Topic("#"), MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(sub); - store.add(sub.asClientTopicCouple()); - assertThat(store.matches(new Topic("finance"))).isNotEmpty(); - assertThat(store.matches(new Topic("finance/ibm"))).isNotEmpty(); - } - - @Test - public void testMatchSimpleMulti_zeroLevel() { - // check MULTI in case of zero level match - Subscription sub = new Subscription("FAKE_CLI_ID_1", new Topic("finance/#"), MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(sub); - store.add(sub.asClientTopicCouple()); - assertThat(store.matches(new Topic("finance"))).isNotEmpty(); - } - - @Test - public void rogerLightTopicMatches() { - assertMatch("foo/bar", "foo/bar"); - assertMatch("foo/bar", "foo/bar"); - assertMatch("foo/+", "foo/bar"); - assertMatch("foo/+/baz", "foo/bar/baz"); - assertMatch("foo/+/#", "foo/bar/baz"); - assertMatch("#", "foo/bar/baz"); - - assertNotMatch("foo/bar", "foo"); - assertNotMatch("foo/+", "foo/bar/baz"); - assertNotMatch("foo/+/baz", "foo/bar/bar"); - assertNotMatch("foo/+/#", "fo2/bar/baz"); - - assertMatch("#", "/foo/bar"); - assertMatch("/#", "/foo/bar"); - assertNotMatch("/#", "foo/bar"); - - assertMatch("foo//bar", "foo//bar"); - assertMatch("foo//+", "foo//bar"); - assertMatch("foo/+/+/baz", "foo///baz"); - assertMatch("foo/bar/+", "foo/bar/"); - } - - private void assertMatch(String s, String t) { - Topic subscription = new Topic(s); - Topic topic = new Topic(t); - store = new SubscriptionsDirectory(); - MemoryStorageService memStore = new MemoryStorageService(null, null); - ISessionsStore aSessionsStore = memStore.sessionsStore(); - aSessionsStore.createNewSession("FAKE_CLI_ID_1", false); - store.init(aSessionsStore); - Subscription sub = new Subscription("FAKE_CLI_ID_1", subscription, MqttQoS.AT_MOST_ONCE); - aSessionsStore.subscriptionStore().addNewSubscription(sub); - store.add(sub.asClientTopicCouple()); - assertThat(store.matches(topic)).isNotEmpty(); - } - - private void assertNotMatch(String s, String t) { - Topic subscription = new Topic(s); - Topic topic = new Topic(t); - store = new SubscriptionsDirectory(); - MemoryStorageService memStore = new MemoryStorageService(null, null); - store.init(memStore.sessionsStore()); - Subscription sub = new Subscription("FAKE_CLI_ID_1", subscription, MqttQoS.AT_MOST_ONCE); - this.subscriptionsStore.addNewSubscription(sub); - store.add(sub.asClientTopicCouple()); - assertThat(store.matches(topic)).isEmpty(); - } - - @Test - public void testOverlappingSubscriptions() { - Subscription genericSub = new Subscription("FAKE_CLI_ID_1", new Topic("a/+"), MqttQoS.EXACTLY_ONCE); - this.subscriptionsStore.addNewSubscription(genericSub); - store.add(genericSub.asClientTopicCouple()); - Subscription specificSub = new Subscription("FAKE_CLI_ID_1", new Topic("a/b"), MqttQoS.AT_LEAST_ONCE); - this.subscriptionsStore.addNewSubscription(specificSub); - store.add(specificSub.asClientTopicCouple()); - - // Verify - assertThat(store.matches(new Topic("a/b")).size()).isEqualTo(1); - } - - @Test - public void removeSubscription_withDifferentClients_subscribedSameTopic() { - ISubscriptionsDirectory aStore = new SubscriptionsDirectory(); - MemoryStorageService memStore = new MemoryStorageService(null, null); - ISessionsStore sessionsStore = memStore.sessionsStore(); - aStore.init(sessionsStore); - // subscribe a not active clientID1 to /topic - sessionsStore.createNewSession("FAKE_CLI_ID_1", true); - Subscription slashSub = new Subscription("FAKE_CLI_ID_1", new Topic("/topic"), MqttQoS.AT_MOST_ONCE); - sessionsStore.subscriptionStore().addNewSubscription(slashSub); - aStore.add(slashSub.asClientTopicCouple()); - - // subscribe an active clientID2 to /topic - Subscription slashSub2 = new Subscription("FAKE_CLI_ID_2", new Topic("/topic"), MqttQoS.AT_MOST_ONCE); - sessionsStore.subscriptionStore().addNewSubscription(slashSub2); - aStore.add(new ClientTopicCouple("FAKE_CLI_ID_2", new Topic("/topic"))); - - // Exercise - aStore.removeSubscription(new Topic("/topic"), slashSub2.getClientId()); - - // Verify - Subscription remainedSubscription = aStore.matches(new Topic("/topic")).get(0); - assertThat(remainedSubscription.getClientId()).isEqualTo(slashSub.getClientId()); - } - - /* - * Test for Issue #49 - */ - @Test - public void duplicatedSubscriptionsWithDifferentQos() { - ClientSession session2 = sessionsStore.createNewSession("client2", true); - Subscription client2Sub = new Subscription("client2", new Topic("client/test/b"), MqttQoS.AT_MOST_ONCE); - session2.subscribe(client2Sub); - store.add(client2Sub.asClientTopicCouple()); - ClientSession session1 = sessionsStore.createNewSession("client1", true); - Subscription client1SubQoS0 = new Subscription("client1", new Topic("client/test/b"), MqttQoS.AT_MOST_ONCE); - session1.subscribe(client1SubQoS0); - store.add(client1SubQoS0.asClientTopicCouple()); - - Subscription client1SubQoS2 = new Subscription("client1", new Topic("client/test/b"), MqttQoS.EXACTLY_ONCE); - session1.subscribe(client1SubQoS2); - store.add(client1SubQoS2.asClientTopicCouple()); - - System.out.println(store.dumpTree()); - - // Verify - List subscriptions = store.matches(new Topic("client/test/b")); - assertThat(subscriptions).contains(client1SubQoS2); - assertThat(subscriptions).contains(client2Sub); - - Subscription client1Sub = subscriptions.get(subscriptions.indexOf(client1SubQoS0)); - assertThat(client1SubQoS0.getRequestedQos()).isNotEqualTo(client1Sub.getRequestedQos()); - - // client1SubQoS2 should override client1SubQoS0 - assertThat(client1Sub.getRequestedQos()).isEqualTo(client1SubQoS2.getRequestedQos()); - } - - @Test - public void testRecreatePath_emptyRoot() { - TreeNode oldRoot = new TreeNode(); - final SubscriptionsDirectory.NodeCouple resp = store.recreatePath(new Topic("/finance"), oldRoot); - - // Verify - assertThat(resp.root).isNotNull(); - assertThat(resp.root.m_token).isNull(); - assertThat(resp.root.m_children).hasSize(1); - assertThat(resp.root.m_children.get(0).m_children).contains(resp.createdNode); - } - - @Test - public void testRecreatePath_1layer_tree() { - TreeNode oldRoot = new TreeNode(); - final SubscriptionsDirectory.NodeCouple respFinance = store.recreatePath(new Topic("/finance"), oldRoot); - final SubscriptionsDirectory.NodeCouple respPlus = store.recreatePath(new Topic("/+"), respFinance.root); - - // Verify - assertThat(respPlus.root).isNotNull(); - assertThat(respPlus.root.m_token).isNull(); - assertThat(respPlus.root.m_children.size()).isEqualTo(1); - assertThat(respPlus.root.m_children.get(0).m_children).contains(respPlus.createdNode); - assertThat(respPlus.root.m_children.get(0).m_children).contains(respFinance.createdNode); - } -} diff --git a/broker/src/test/java/io/moquette/spi/impl/subscriptions/TreeNodeTest.java b/broker/src/test/java/io/moquette/spi/impl/subscriptions/TreeNodeTest.java deleted file mode 100644 index f3e95f9f..00000000 --- a/broker/src/test/java/io/moquette/spi/impl/subscriptions/TreeNodeTest.java +++ /dev/null @@ -1,76 +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.junit.Test; - -import static org.junit.Assert.*; - -public class TreeNodeTest { - - @Test - public void testRemoveTreeNodesAfterEmptySubscriptions() { - TreeNode root = new TreeNode(); - TreeNode subNode1 = new TreeNode(); - - subNode1.addSubscription(new ClientTopicCouple("Client1", new Topic("/topic1"))); - root.addChild(subNode1); - - TreeNode updatedRoot = root.removeClientSubscriptions("Client1"); - - assertTrue("Empty subscriptions node should be removed", updatedRoot.m_children.isEmpty()); - } - - @Test - public void testRemoveTreeNodesAfterEmptySubscriptions_multiLayer() { - TreeNode root = new TreeNode(); - TreeNode intermediateEmptyNode = new TreeNode(); - TreeNode subNode1 = new TreeNode(); - - subNode1.addSubscription(new ClientTopicCouple("Client1", new Topic("/sub/topic1"))); - intermediateEmptyNode.addChild(subNode1); - root.addChild(intermediateEmptyNode); - - TreeNode updatedRoot = root.removeClientSubscriptions("Client1"); - - assertTrue("Empty subscriptions node should be removed", updatedRoot.m_children.isEmpty()); - } - - @Test - public void testRemoveTreeNodesAfterEmptySubscriptions_multiLayer_doesntRemoveIntermediateNodes() { - TreeNode root = new TreeNode(); - TreeNode intermediateEmptyNode = new TreeNode(); - - TreeNode subNode1 = new TreeNode(); - subNode1.addSubscription(new ClientTopicCouple("Client1", new Topic("/sub/topic1"))); - intermediateEmptyNode.addChild(subNode1); - - TreeNode subNode2 = new TreeNode(); - subNode2.addSubscription(new ClientTopicCouple("Client2", new Topic("/sub/topic2"))); - intermediateEmptyNode.addChild(subNode2); - root.addChild(intermediateEmptyNode); - - TreeNode updatedRoot = root.removeClientSubscriptions("Client1"); - - assertFalse("Remove a subscription shouldn't remove intermediate empty nodes", - updatedRoot.m_children.isEmpty()); - } - -} diff --git a/h2_storage/src/main/java/io/moquette/persistence/h2/H2SessionsStore.java b/h2_storage/src/main/java/io/moquette/persistence/h2/H2SessionsStore.java index 7a6840d1..9733773c 100644 --- a/h2_storage/src/main/java/io/moquette/persistence/h2/H2SessionsStore.java +++ b/h2_storage/src/main/java/io/moquette/persistence/h2/H2SessionsStore.java @@ -16,7 +16,6 @@ package io.moquette.persistence.h2; import io.moquette.persistence.PersistentSession; -import io.moquette.spi.ClientSession; import io.moquette.spi.IMessagesStore; import io.moquette.spi.IMessagesStore.StoredMessage; import io.moquette.spi.ISessionsStore; @@ -45,7 +44,7 @@ public class H2SessionsStore implements ISessionsStore, ISubscriptionsStore { // maps clientID->[MessageId -> guid] private ConcurrentMap> secondPhaseStore; - public H2SessionsStore(MVStore mvStore) { + H2SessionsStore(MVStore mvStore) { this.mvStore = mvStore; } @@ -63,12 +62,7 @@ public class H2SessionsStore implements ISessionsStore, ISubscriptionsStore { return this; } - @Override - public void updateCleanStatus(String clientID, boolean cleanSession) { - LOG.info("Updating cleanSession flag. CId={}, cleanSession={}", clientID, cleanSession); - this.sessions.put(clientID, new PersistentSession(cleanSession)); - } - + //TODO move in the subscription directory @Override public void addNewSubscription(Subscription newSubscription) { LOG.info("Adding new subscription CId={}, topics={}", newSubscription.getClientId(), @@ -83,6 +77,7 @@ public class H2SessionsStore implements ISessionsStore, ISubscriptionsStore { } } + //TODO move in the subscription directory @Override public void removeSubscription(Topic topicFilter, String clientID) { LOG.info("Removing subscription. CId={}, topics={}", clientID, topicFilter); @@ -111,23 +106,7 @@ public class H2SessionsStore implements ISessionsStore, ISubscriptionsStore { } @Override - public List listAllSubscriptions() { - LOG.info("Retrieving existing subscriptions"); - final List allSubscriptions = new ArrayList<>(); - for (String clientID : this.sessions.keySet()) { - ConcurrentMap clientSubscriptions = this.mvStore.openMap("subscriptions_" + clientID); - for (Topic topicFilter : clientSubscriptions.keySet()) { - allSubscriptions.add(new ClientTopicCouple(clientID, topicFilter)); - } - } - if (LOG.isTraceEnabled()) { - LOG.trace("The existing subscriptions have been retrieved. Result={}", allSubscriptions); - } - return allSubscriptions; - } - - @Override - public List getSubscriptions() { + public List listAllSubscriptions() { LOG.debug("Retrieving existing subscriptions..."); List subscriptions = new ArrayList<>(); for (String clientID : this.sessions.keySet()) { @@ -139,47 +118,51 @@ public class H2SessionsStore implements ISessionsStore, ISubscriptionsStore { } @Override - public Subscription getSubscription(ClientTopicCouple couple) { - ConcurrentMap clientSubscriptions = this.mvStore.openMap("subscriptions_" + couple.clientID); - LOG.debug("Retrieving subscriptions CId={}, subscriptions={}", couple.clientID, clientSubscriptions); - return clientSubscriptions.get(couple.topicFilter); + public Collection listClientSubscriptions(String clientID) { + ConcurrentMap clientSubscriptions = this.mvStore.openMap("subscriptions_" + clientID); + if (clientSubscriptions == null) { + throw new IllegalStateException("Asking for subscriptions of not persisted client: " + clientID); + } + return clientSubscriptions.values(); + } + + @Override + public Subscription reload(Subscription subcription) { + ConcurrentMap clientSubscriptions = this.mvStore.openMap("subscriptions_" + subcription.getClientId()); + LOG.debug("Retrieving subscriptions CId={}, subscriptions={}", subcription.getClientId(), clientSubscriptions); + return clientSubscriptions.get(subcription.getTopicFilter()); } @Override public boolean contains(String clientID) { - return this.mvStore.hasMap("subscriptions_" + clientID); + return this.sessions.containsKey(clientID); } @Override - public ClientSession createNewSession(String clientID, boolean cleanSession) { - if (sessions.containsKey(clientID)) { - LOG.error("Unable to create a new session: the client ID is already in use. CId={}, cleanSession={}", - clientID, cleanSession); - throw new IllegalArgumentException("Can't create a session with the ID of an already existing" + clientID); - } - LOG.debug("Creating new session. CId={}, cleanSession={}", clientID, cleanSession); - sessions.putIfAbsent(clientID, new PersistentSession(cleanSession)); - return new ClientSession(clientID, this, this, cleanSession); + public void createNewDurableSession(String clientID) { + this.sessions.putIfAbsent(clientID, new PersistentSession(clientID, false)); } @Override - public ClientSession sessionForClient(String clientID) { - LOG.debug("Retrieving session. CId={}", clientID); - if (!this.sessions.containsKey(clientID)) { - LOG.warn("Session does not exist. CId={}", clientID); - return null; - } - PersistentSession storedSession = this.sessions.get(clientID); - return new ClientSession(clientID, this, this, storedSession.cleanSession); + public void removeDurableSession(String clientId) { + this.sessions.remove(clientId); + this.wipeSubscriptions(clientId); } @Override - public Collection getAllSessions() { - Collection result = new ArrayList<>(); - for (Map.Entry entry : this.sessions.entrySet()) { - result.add(new ClientSession(entry.getKey(), this, this, entry.getValue().cleanSession)); - } - return result; + public void updateCleanStatus(String clientId, boolean newCleanStatus) { + PersistentSession updatedSession = new PersistentSession(clientId, newCleanStatus); + this.sessions.put(clientId, updatedSession); + } + + @Override + public PersistentSession loadSessionByKey(String clientID) { + return this.sessions.get(clientID); + } + + @Override + public Collection listAllSessions() { + return this.sessions.values(); } @Override @@ -238,6 +221,8 @@ public class H2SessionsStore implements ISessionsStore, ISubscriptionsStore { @Override public void dropQueue(String clientID) { H2PersistentQueue.dropQueue(this.mvStore, clientID); + this.outboundFlightMessages.remove(clientID); + this.inFlightIds.remove(clientID); } @Override @@ -255,7 +240,7 @@ public class H2SessionsStore implements ISessionsStore, ISubscriptionsStore { } @Override - public StoredMessage secondPhaseAcknowledged(String clientID, int messageID) { + public StoredMessage completeReleasedPublish(String clientID, int messageID) { LOG.debug("Processing second phase ACK CId={}, messageId={}", clientID, messageID); final ConcurrentMap m = this.secondPhaseStore.get(clientID); if (m == null) { @@ -278,10 +263,7 @@ public class H2SessionsStore implements ISessionsStore, ISubscriptionsStore { totalInflight += inflightPerClient.size(); } - Map secondPhaseInFlight = this.secondPhaseStore.get(clientID); - if (secondPhaseInFlight != null) { - totalInflight += secondPhaseInFlight.size(); - } +// totalInflight += countPubReleaseWaitingPubComplete(clientID); Map outboundPerClient = outboundFlightMessages.get(clientID); if (outboundPerClient != null) { @@ -296,44 +278,16 @@ public class H2SessionsStore implements ISessionsStore, ISubscriptionsStore { } @Override - public StoredMessage inboundInflight(String clientID, int messageID) { - LOG.debug("Mapping inbound message ID to GUID CId={}, messageId={}", clientID, messageID); - ConcurrentMap messageIdToGuid = this.mvStore.openMap(inboundStoreForClient(clientID)); - return messageIdToGuid.get(messageID); - } - - @Override - public void markAsInboundInflight(String clientID, int messageID, StoredMessage msg) { - ConcurrentMap messageIdToGuid = this.mvStore.openMap(inboundStoreForClient(clientID)); - messageIdToGuid.put(messageID, msg); - } - - @Override - public int getPendingPublishMessagesNo(String clientID) { - return queue(clientID).size(); - } - - @Override - public int getSecondPhaseAckPendingMessages(String clientID) { + public int countPubReleaseWaitingPubComplete(String clientID) { if (!this.secondPhaseStore.containsKey(clientID)) return 0; return this.secondPhaseStore.get(clientID).size(); } @Override - public void cleanSession(String clientID) { - // remove also the messages stored of type QoS1/2 - LOG.info("Removing stored messages with QoS 1 and 2. ClientId={}", clientID); - + public void removeTemporaryQoS2(String clientID) { + LOG.info("Removing stored messages with QoS 2. ClientId={}", clientID); this.secondPhaseStore.remove(clientID); - this.outboundFlightMessages.remove(clientID); - this.inFlightIds.remove(clientID); - - LOG.info("Wiping existing subscriptions. ClientId={}", clientID); - wipeSubscriptions(clientID); - - //remove also the enqueued messages - dropQueue(clientID); } } diff --git a/h2_storage/src/test/java/io/moquette/persistence/h2/H2MessagesStoreTest.java b/h2_storage/src/test/java/io/moquette/persistence/h2/H2MessagesStoreTest.java index 08ab010e..9131d5b0 100644 --- a/h2_storage/src/test/java/io/moquette/persistence/h2/H2MessagesStoreTest.java +++ b/h2_storage/src/test/java/io/moquette/persistence/h2/H2MessagesStoreTest.java @@ -19,6 +19,7 @@ import io.moquette.BrokerConstants; import io.moquette.persistence.MessageStoreTCK; import io.moquette.server.config.IConfig; import io.moquette.server.config.MemoryConfig; +import io.moquette.spi.impl.SessionsRepository; import org.junit.After; import org.junit.Before; @@ -47,6 +48,7 @@ public class H2MessagesStoreTest extends MessageStoreTCK { storageService.initStore(); messagesStore = storageService.messagesStore(); sessionsStore = storageService.sessionsStore(); + this.sessionsRepository = new SessionsRepository(sessionsStore); } @After diff --git a/mapdb_storage/src/main/java/io/moquette/persistence/mapdb/MapDBSessionsStore.java b/mapdb_storage/src/main/java/io/moquette/persistence/mapdb/MapDBSessionsStore.java index d68cd606..521209cf 100644 --- a/mapdb_storage/src/main/java/io/moquette/persistence/mapdb/MapDBSessionsStore.java +++ b/mapdb_storage/src/main/java/io/moquette/persistence/mapdb/MapDBSessionsStore.java @@ -17,7 +17,6 @@ package io.moquette.persistence.mapdb; import io.moquette.persistence.PersistentSession; -import io.moquette.spi.ClientSession; import io.moquette.spi.IMessagesStore.StoredMessage; import io.moquette.spi.ISessionsStore; import io.moquette.spi.ISubscriptionsStore; @@ -98,35 +97,12 @@ class MapDBSessionsStore implements ISessionsStore, ISubscriptionsStore { m_db.delete("subscriptions_" + clientID); if (LOG.isDebugEnabled()) { LOG.debug("Subscriptions have been removed. ClientId={}, clientSubscriptions={}", clientID, - m_db.getHashMap("subscriptions_" + clientID)); + m_db.getHashMap("subscriptions_" + clientID)); } } @Override - public List listAllSubscriptions() { - LOG.debug("Retrieving existing subscriptions"); - final List allSubscriptions = new ArrayList<>(); - for (String clientID : m_persistentSessions.keySet()) { - ConcurrentMap clientSubscriptions = m_db.getHashMap("subscriptions_" + clientID); - for (Topic topicFilter : clientSubscriptions.keySet()) { - allSubscriptions.add(new ClientTopicCouple(clientID, topicFilter)); - } - } - if (LOG.isTraceEnabled()) { - LOG.trace("The existing subscriptions have been retrieved. Result={}", allSubscriptions); - } - return allSubscriptions; - } - - @Override - public Subscription getSubscription(ClientTopicCouple couple) { - ConcurrentMap clientSubscriptions = m_db.getHashMap("subscriptions_" + couple.clientID); - LOG.debug("Retrieving subscriptions. CId={}, subscriptions={}", couple.clientID, clientSubscriptions); - return clientSubscriptions.get(couple.topicFilter); - } - - @Override - public List getSubscriptions() { + public List listAllSubscriptions() { LOG.debug("Retrieving existing subscriptions..."); List subscriptions = new ArrayList<>(); for (String clientID : m_persistentSessions.keySet()) { @@ -137,48 +113,52 @@ class MapDBSessionsStore implements ISessionsStore, ISubscriptionsStore { return subscriptions; } + @Override + public Collection listClientSubscriptions(String clientID) { + ConcurrentMap clientSubscriptions = m_db.getHashMap("subscriptions_" + clientID); + if (clientSubscriptions == null) { + throw new IllegalStateException("Asking for subscriptions of not persisted client: " + clientID); + } + return clientSubscriptions.values(); + } + + @Override + public Subscription reload(Subscription subcription) { + ConcurrentMap clientSubscriptions = m_db.getHashMap("subscriptions_" + subcription.getClientId()); + LOG.debug("Retrieving subscriptions. CId={}, subscriptions={}", subcription.getClientId(), clientSubscriptions); + return clientSubscriptions.get(subcription.getTopicFilter()); + } + @Override public boolean contains(String clientID) { - return m_db.exists("subscriptions_" + clientID); + return m_persistentSessions.containsKey(clientID); } @Override - public ClientSession createNewSession(String clientID, boolean cleanSession) { - if (m_persistentSessions.containsKey(clientID)) { - LOG.error("Unable to create a new session: the client ID is already in use. ClientId={}, cleanSession={}", - clientID, cleanSession); - throw new IllegalArgumentException("Can't create a session with the ID of an already existing" + clientID); - } - LOG.debug("Creating new session. CId={}, cleanSession={}", clientID, cleanSession); - m_persistentSessions.putIfAbsent(clientID, new PersistentSession(cleanSession)); - return new ClientSession(clientID, this, this, cleanSession); + public void createNewDurableSession(String clientID) { + m_persistentSessions.putIfAbsent(clientID, new PersistentSession(clientID, false)); } @Override - public ClientSession sessionForClient(String clientID) { - LOG.debug("Retrieving session CId={}", clientID); - if (!m_persistentSessions.containsKey(clientID)) { - LOG.warn("Session does not exist CId={}", clientID); - return null; - } - - PersistentSession storedSession = m_persistentSessions.get(clientID); - return new ClientSession(clientID, this, this, storedSession.cleanSession); + public void removeDurableSession(String clientId) { + this.m_persistentSessions.remove(clientId); + this.wipeSubscriptions(clientId); } @Override - public Collection getAllSessions() { - Collection result = new ArrayList<>(); - for (Map.Entry entry : m_persistentSessions.entrySet()) { - result.add(new ClientSession(entry.getKey(), this, this, entry.getValue().cleanSession)); - } - return result; + public void updateCleanStatus(String clientId, boolean newCleanStatus) { + PersistentSession updatedSession = new PersistentSession(clientId, newCleanStatus); + m_persistentSessions.put(clientId, updatedSession); } @Override - public void updateCleanStatus(String clientID, boolean cleanSession) { - LOG.info("Updating cleanSession flag. CId={}, cleanSession={}", clientID, cleanSession); - m_persistentSessions.put(clientID, new PersistentSession(cleanSession)); + public PersistentSession loadSessionByKey(String clientID) { + return this.m_persistentSessions.get(clientID); + } + + @Override + public Collection listAllSessions() { + return this.m_persistentSessions.values(); } /** @@ -243,6 +223,8 @@ class MapDBSessionsStore implements ISessionsStore, ISubscriptionsStore { public void dropQueue(String clientID) { LOG.info("Removing pending messages. CId={}", clientID); this.m_db.delete(clientID); + outboundFlightMessages.remove(clientID); + m_inFlightIds.remove(clientID); } @Override @@ -260,7 +242,7 @@ class MapDBSessionsStore implements ISessionsStore, ISubscriptionsStore { } @Override - public StoredMessage secondPhaseAcknowledged(String clientID, int messageID) { + public StoredMessage completeReleasedPublish(String clientID, int messageID) { LOG.debug("Processing second phase ACK CId={}, messageId={}", clientID, messageID); final ConcurrentMap m = this.m_secondPhaseStore.get(clientID); if (m == null) { @@ -283,10 +265,7 @@ class MapDBSessionsStore implements ISessionsStore, ISubscriptionsStore { totalInflight += inflightPerClient.size(); } - Map secondPhaseInFlight = this.m_secondPhaseStore.get(clientID); - if (secondPhaseInFlight != null) { - totalInflight += secondPhaseInFlight.size(); - } +// totalInflight += countPubReleaseWaitingPubComplete(clientID); Map outboundPerClient = outboundFlightMessages.get(clientID); if (outboundPerClient != null) { @@ -297,46 +276,19 @@ class MapDBSessionsStore implements ISessionsStore, ISubscriptionsStore { } @Override - public StoredMessage inboundInflight(String clientID, int messageID) { - LOG.debug("Mapping inbound message ID to GUID CId={}, messageId={}", clientID, messageID); - ConcurrentMap messageIdToGuid = m_db.getHashMap(inboundMessageId2MessagesMapName(clientID)); - return messageIdToGuid.get(messageID); - } - - @Override - public void markAsInboundInflight(String clientID, int messageID, StoredMessage msg) { - ConcurrentMap messageIdToGuid = m_db.getHashMap(inboundMessageId2MessagesMapName(clientID)); - messageIdToGuid.put(messageID, msg); - } - - @Override - public int getPendingPublishMessagesNo(String clientID) { - return queue(clientID).size(); - } - - @Override - public int getSecondPhaseAckPendingMessages(String clientID) { + public int countPubReleaseWaitingPubComplete(String clientID) { if (!m_secondPhaseStore.containsKey(clientID)) return 0; return m_secondPhaseStore.get(clientID).size(); } @Override - public void cleanSession(String clientID) { - // remove also the messages stored of type QoS1/2 - LOG.info("Removing stored messages with QoS 1 and 2. ClientId={}", clientID); + public void removeTemporaryQoS2(String clientID) { + LOG.info("Removing stored messages with QoS 2. ClientId={}", clientID); m_secondPhaseStore.remove(clientID); - outboundFlightMessages.remove(clientID); - m_inFlightIds.remove(clientID); - - LOG.info("Wiping existing subscriptions. ClientId={}", clientID); - wipeSubscriptions(clientID); - - //remove also the enqueued messages - dropQueue(clientID); } - static String inboundMessageId2MessagesMapName(String clientID) { + private static String inboundMessageId2MessagesMapName(String clientID) { return "inboundInflight_" + clientID; } } diff --git a/mapdb_storage/src/test/java/io/moquette/persistence/mapdb/MapDBPersistentStoreTest.java b/mapdb_storage/src/test/java/io/moquette/persistence/mapdb/MapDBPersistentStoreTest.java index 958d26b5..97fea12f 100644 --- a/mapdb_storage/src/test/java/io/moquette/persistence/mapdb/MapDBPersistentStoreTest.java +++ b/mapdb_storage/src/test/java/io/moquette/persistence/mapdb/MapDBPersistentStoreTest.java @@ -21,6 +21,7 @@ import io.moquette.persistence.MessageStoreTCK; import io.moquette.server.config.IConfig; import io.moquette.server.config.MemoryConfig; import io.moquette.spi.IMessagesStore.StoredMessage; +import io.moquette.spi.impl.SessionsRepository; import io.netty.handler.codec.mqtt.MqttQoS; import org.junit.After; import org.junit.Before; @@ -56,6 +57,7 @@ public class MapDBPersistentStoreTest extends MessageStoreTCK { m_storageService.initStore(); messagesStore = m_storageService.messagesStore(); sessionsStore = m_storageService.sessionsStore(); + this.sessionsRepository = new SessionsRepository(sessionsStore); } @After diff --git a/perf/src/main/java/io/moquette/spi/impl/subscriptions/PerformanceTest.java b/perf/src/main/java/io/moquette/spi/impl/subscriptions/PerformanceTest.java index d2365786..bedb259e 100644 --- a/perf/src/main/java/io/moquette/spi/impl/subscriptions/PerformanceTest.java +++ b/perf/src/main/java/io/moquette/spi/impl/subscriptions/PerformanceTest.java @@ -22,6 +22,7 @@ import java.lang.management.ThreadMXBean; import io.moquette.persistence.MemoryStorageService; import io.moquette.spi.ISessionsStore; +import io.moquette.spi.impl.SessionsRepository; import io.netty.handler.codec.mqtt.MqttQoS; public final class PerformanceTest { @@ -34,10 +35,11 @@ public final class PerformanceTest { cpufreq-set -g performance -u 800MHz -c 3 */ public static void main(String[] args) throws IOException, InterruptedException { - ISubscriptionsDirectory store = new SubscriptionsDirectory(); + ISubscriptionsDirectory store = new CTrieSubscriptionDirectory(); MemoryStorageService memStore = new MemoryStorageService(null, null); ISessionsStore aSessionsStore = memStore.sessionsStore(); - store.init(aSessionsStore); + SessionsRepository sessionsRepository = new SessionsRepository(aSessionsStore); + store.init(sessionsRepository); int times = 10000; @@ -57,7 +59,7 @@ public final class PerformanceTest { Subscription sub = new Subscription("CLI_ID_" + (i % users), topics[i], MqttQoS.AT_MOST_ONCE); aSessionsStore.subscriptionStore().addNewSubscription(sub); - store.add(sub.asClientTopicCouple()); + store.add(sub); } long min = Long.MAX_VALUE;