Big refactory on session management:

- introduced a unique point of access and management of sessions state (SessionRepository)
- separated behaiour for sessions with clean flag to false into DurableSession
- separated behaviour for session with clean flag to true into TransientSession
This commit is contained in:
andsel
2017-10-14 17:37:26 +02:00
parent 6bacce6cec
commit 1b272e5849
43 changed files with 1107 additions and 1710 deletions
@@ -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
*
@@ -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<Topic, Subscription> subscriptions = new ConcurrentHashMap<>();
final AtomicReference<PersistentSession> persistentSession = new AtomicReference<>(null);
boolean cleanSession;
final BlockingQueue<StoredMessage> queue = new ArrayBlockingQueue<>(Constants.MAX_MESSAGE_QUEUE);
final Map<Integer, StoredMessage> secondPhaseStore = new ConcurrentHashMap<>();
final Map<Integer, StoredMessage> outboundFlightMessages =
Collections.synchronizedMap(new HashMap<Integer, StoredMessage>());
final Map<Integer, StoredMessage> 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<String, Session> 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<ClientSession> getAllSessions() {
Collection<ClientSession> 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<PersistentSession> listAllSessions() {
Collection<PersistentSession> 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<ClientTopicCouple> listAllSubscriptions() {
List<ClientTopicCouple> allSubscriptions = new ArrayList<>();
public List<Subscription> listAllSubscriptions() {
List<Subscription> 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<Subscription> 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<Subscription> getSubscriptions() {
List<Subscription> 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);
@@ -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;
}
}
@@ -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() + '}';
}
@@ -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<String, ConnectionDescriptor> 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<MqttSession> getSessions() {
LOG.info("Retrieving status of all sessions.");
Collection<MqttSession> 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;
}
@@ -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
@@ -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;
* <li>Optionally, QoS 0 messages pending transmission to the Client.</li>
* </ul>
*/
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<Subscription> subscriptions = new HashSet<>();
private Set<Subscription> 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<IMessagesStore.StoredMessage> 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<Subscription> 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<Subscription> 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<Subscription> 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 + "'}";
}
}
@@ -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<Integer, IMessagesStore.StoredMessage> 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<Subscription> 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<Subscription> 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);
}
}
@@ -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;
}
}
@@ -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;
/**
@@ -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<ClientSession> getAllSessions();
Collection<PersistentSession> 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);
}
@@ -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<ClientTopicCouple> listAllSubscriptions();
List<Subscription> listAllSubscriptions();
/**
* @param couple
* the subscription descriptor.
* Load from storage all the subscriptions of the specified client.
* */
Collection<Subscription> 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<Subscription> getSubscriptions();
Subscription reload(Subscription subcription);
}
@@ -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<StoredMessage> messagesQueue = new LinkedList<>();
private final AtomicInteger packetGenerator = new AtomicInteger(1);
private final ConcurrentMap<Integer, StoredMessage> inboundInflightMap = new ConcurrentHashMap<>();
private final ConcurrentMap<Integer, StoredMessage> outboundInflightMap = new ConcurrentHashMap<>();
private final Map<Integer, StoredMessage> 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<Subscription> 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<Subscription> 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();
}
}
@@ -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<IMessagesStore.StoredMessage> 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);
}
@@ -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.
@@ -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);
}
}
}
@@ -125,8 +125,8 @@ public class ProtocolProcessor {
private static final Logger LOG = LoggerFactory.getLogger(ProtocolProcessor.class);
protected ConnectionDescriptorStore connectionDescriptors;
protected ConcurrentMap<RunningSubscription, SubscriptionState> subscriptionInCourse;
private ConnectionDescriptorStore connectionDescriptors;
private ConcurrentMap<RunningSubscription, SubscriptionState> 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<String, WillMessage> 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<StoredMessage> 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<Subscription> doStoreSubscription(List<MqttTopicSubscription> ackTopics, String clientID) {
ClientSession clientSession = m_sessionsStore.sessionForClient(clientID);
ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID);
List<Subscription> 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<MqttTopicSubscription> doVerify(String clientID, String username, MqttSubscribeMessage msg) {
ClientSession clientSession = m_sessionsStore.sessionForClient(clientID);
ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID);
List<MqttTopicSubscription> 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);
}
}
@@ -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<Subscription> getSubscriptions() {
return this.subscriptionsStore.getSubscriptions();
return this.subscriptionsStore.listAllSubscriptions();
}
public void shutdown() {
@@ -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));
@@ -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);
@@ -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<String, ClientSession> 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<ClientSession> getAllSessions() {
Collection<ClientSession> 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);
}
}
@@ -15,15 +15,16 @@
*/
package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple;
import java.util.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class CNode {
Token token;
private List<INode> children;
Set<ClientTopicCouple> subscriptions = new HashSet<>();
Set<Subscription> subscriptions = new HashSet<>();
//private int subtreeSubscriptions;
@@ -33,7 +34,7 @@ class CNode {
}
//Copy constructor
private CNode(Token token, List<INode> children, Set<ClientTopicCouple> subscriptions) {
private CNode(Token token, List<INode> children, Set<Subscription> subscriptions) {
this.token = token;
this.children = children;
this.subscriptions = subscriptions;
@@ -64,10 +65,7 @@ class CNode {
}
private boolean equals(Token token) {
if (this.token == null) {
return false;
}
return this.token.equals(token);
return this.token != null && this.token.equals(token);
}
CNode copy() {
@@ -79,7 +77,7 @@ class CNode {
}
CNode addSubscription(String clientId, Topic topic) {
this.subscriptions.add(new ClientTopicCouple(clientId, topic));
this.subscriptions.add(new Subscription(clientId, topic));
return this;
}
@@ -87,8 +85,8 @@ class CNode {
* @return true iff the subscriptions contained in this node are owned by clientId
* */
boolean containsOnly(String clientId) {
for (ClientTopicCouple sub : this.subscriptions) {
if (!sub.clientID.equals(clientId)) {
for (Subscription sub : this.subscriptions) {
if (!sub.clientId.equals(clientId)) {
return false;
}
}
@@ -97,8 +95,8 @@ class CNode {
//TODO this is equivalent to negate(containsOnly(clientId))
public boolean contains(String clientId) {
for (ClientTopicCouple sub : this.subscriptions) {
if (sub.clientID.equals(clientId)) {
for (Subscription sub : this.subscriptions) {
if (sub.clientId.equals(clientId)) {
return true;
}
}
@@ -106,23 +104,12 @@ class CNode {
}
void removeSubscriptionsFor(String clientId) {
Set<ClientTopicCouple> toRemove = new HashSet<>();
for (ClientTopicCouple sub : this.subscriptions) {
if (sub.clientID.equals(clientId)) {
Set<Subscription> toRemove = new HashSet<>();
for (Subscription sub : this.subscriptions) {
if (sub.clientId.equals(clientId)) {
toRemove.add(sub);
}
}
this.subscriptions.removeAll(toRemove);
}
// public List<INode> childrenMatching(Token token) {
// List<INode> res = new ArrayList<>();
// for (INode iNode : children) {
// final CNode child = iNode.mainNode();
// if (child.equals(token) || (child.token == Token.MULTI || child.token == Token.SINGLE)) {
// res.add(iNode);
// }
// }
// return res;
// }
}
@@ -15,9 +15,8 @@
*/
package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.ISubscriptionsStore;
import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple;
import io.moquette.spi.ClientSession;
import io.moquette.spi.impl.SessionsRepository;
import io.netty.util.internal.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -25,14 +24,16 @@ import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import static java.lang.String.format;
public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
private static final Logger LOG = LoggerFactory.getLogger(CTrieSubscriptionDirectory.class);
private static final Token ROOT = new Token("root");
protected INode root;
private volatile ISubscriptionsStore subscriptionsStore;
INode root;
private volatile SessionsRepository sessionsRepository;
private interface IVisitor<T> {
@@ -57,10 +58,10 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
}
StringBuilder subScriptionsStr = new StringBuilder(" ~~[");
int counter = 0;
for (ClientTopicCouple couple : node.subscriptions) {
for (Subscription couple : node.subscriptions) {
subScriptionsStr
.append("{filter=").append(couple.topicFilter).append(", ")
.append("client='").append(couple.clientID).append("'}");
.append("client='").append(couple.clientId).append("'}");
counter++;
if (counter < node.subscriptions.size()) {
subScriptionsStr.append(";");
@@ -90,24 +91,24 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
OK, REPEAT
}
public void init(ISessionsStore sessionsStore) {
public void init(SessionsRepository sessionsRepository) {
LOG.info("Initializing CTrie");
final CNode mainNode = new CNode();
mainNode.token = ROOT;
this.root = new INode(mainNode);
LOG.info("Initializing subscriptions store...");
this.subscriptionsStore = sessionsStore.subscriptionStore();
List<ClientTopicCouple> subscriptions = this.subscriptionsStore.listAllSubscriptions();
this.sessionsRepository = sessionsRepository;
// reload any subscriptions persisted
if (LOG.isTraceEnabled()) {
LOG.trace("Reloading all stored subscriptions. SubscriptionTree = {}", dumpTree());
}
for (ClientTopicCouple clientTopic : subscriptions) {
LOG.info("Re-subscribing client to topic CId={}, topicFilter={}", clientTopic.clientID,
clientTopic.topicFilter);
add(clientTopic);
for (ClientSession session : this.sessionsRepository.getAllSessions()) {
for (Subscription subscription : session.getSubscriptions()) {
LOG.info("Re-subscribing client to topic CId={}, topicFilter={}", subscription.clientId,
subscription.topicFilter);
add(subscription);
}
}
if (LOG.isTraceEnabled()) {
LOG.trace("Stored subscriptions have been reloaded. SubscriptionTree = {}", dumpTree());
@@ -145,25 +146,31 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
* @return the list of matching subscriptions, or empty if not matching.
*/
Set<Subscription> match(Topic topic) {
final Set<ClientTopicCouple> matchingSubs = recursiveMatch(topic, this.root);
final Set<Subscription> matchingSubs = recursiveMatch(topic, this.root);
// remove the overlapping subscriptions, selecting ones with greatest qos
Map<String, Subscription> subsForClient = new HashMap<>();
for (ClientTopicCouple matchingCouple : matchingSubs) {
Subscription existingSub = subsForClient.get(matchingCouple.clientID);
Subscription sub = this.subscriptionsStore.getSubscription(matchingCouple);
if (sub == null) {
// if the m_sessionStore hasn't the sub because the client disconnected
for (Subscription matchingSub : matchingSubs) {
Subscription existingSub = subsForClient.get(matchingSub.clientId);
final ClientSession subscribedSession = this.sessionsRepository.sessionForClient(matchingSub.clientId);
if (subscribedSession == null) {
//clean session disconnected
continue;
}
Subscription sub = subscribedSession.findSubscriptionByTopicFilter(matchingSub);
if (sub == null) {
final String excpMesg = format("Target session %s is connected but doesn't anymore subscribed to %s",
matchingSub.clientId, matchingSub);
throw new IllegalStateException(excpMesg);
}
// update the selected subscriptions if not present or if has a greater qos
if (existingSub == null || existingSub.getRequestedQos().value() < sub.getRequestedQos().value()) {
subsForClient.put(matchingCouple.clientID, sub);
if (existingSub == null || existingSub.qosLessThan(sub)) {
subsForClient.put(sub.clientId, sub);
}
}
return new HashSet<>(subsForClient.values());
}
Set<ClientTopicCouple> recursiveMatch(Topic topic, INode inode) {
Set<Subscription> recursiveMatch(Topic topic, INode inode) {
CNode cnode = inode.mainNode();
if (cnode.token == Token.MULTI) {
return cnode.subscriptions;
@@ -179,7 +186,7 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
return Collections.emptySet();
}
Topic remainingTopic = (cnode.token == ROOT) ? topic : topic.exceptHeadToken();
Set<ClientTopicCouple> subscriptions = new HashSet<>();
Set<Subscription> subscriptions = new HashSet<>();
if (remainingTopic.isEmpty()) {
subscriptions.addAll(cnode.subscriptions);
}
@@ -190,10 +197,10 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
}
public void add(ClientTopicCouple newSubscription) {
public void add(Subscription newSubscription) {
Action res;
do {
res = insert(newSubscription.clientID, newSubscription.topicFilter, this.root, newSubscription.topicFilter);
res = insert(newSubscription.clientId, newSubscription.topicFilter, this.root, newSubscription.topicFilter);
} while (res == Action.REPEAT);
}
@@ -15,16 +15,15 @@
*/
package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.ISubscriptionsStore;
import io.moquette.spi.impl.SessionsRepository;
import java.util.List;
public interface ISubscriptionsDirectory {
void init(ISessionsStore sessionsStore);
void init(SessionsRepository sessionsRepository);
void add(ISubscriptionsStore.ClientTopicCouple newSubscription);
void add(Subscription newSubscription);
void removeSubscription(Topic topic, String clientID);
@@ -16,8 +16,8 @@
package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple;
import io.netty.handler.codec.mqtt.MqttQoS;
import java.io.Serializable;
/**
@@ -26,10 +26,10 @@ import java.io.Serializable;
public final class Subscription implements Serializable {
private static final long serialVersionUID = -3383457629635732794L;
final MqttQoS requestedQos; // max QoS acceptable
private final MqttQoS requestedQos; // max QoS acceptable
final String clientId;
final Topic topicFilter;
final boolean active;
private final boolean active;
public Subscription(String clientId, Topic topicFilter, MqttQoS requestedQos) {
this.requestedQos = requestedQos;
@@ -45,6 +45,16 @@ public final class Subscription implements Serializable {
this.active = orig.active;
}
/**
* Constructor with undefined maximum QoS
* */
public Subscription(String clientId, Topic topicFilter) {
this.requestedQos = null;
this.clientId = clientId;
this.topicFilter = topicFilter;
this.active = true;
}
public String getClientId() {
return clientId;
}
@@ -61,6 +71,10 @@ public final class Subscription implements Serializable {
return active;
}
public boolean qosLessThan(Subscription sub) {
return requestedQos.value() < sub.requestedQos.value();
}
@Override
public boolean equals(Object o) {
if (this == o)
@@ -100,8 +114,4 @@ public final class Subscription implements Serializable {
return null;
}
}
public ClientTopicCouple asClientTopicCouple() {
return new ClientTopicCouple(this.clientId, this.topicFilter);
}
}
@@ -1,243 +0,0 @@
/*
* Copyright (c) 2012-2017 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.ISubscriptionsStore;
import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicReference;
/**
* Represents a tree of topics subscriptions.
*/
public class SubscriptionsDirectory implements ISubscriptionsDirectory {
static class NodeCouple {
final TreeNode root;
final TreeNode createdNode;
NodeCouple(TreeNode root, TreeNode createdNode) {
this.root = root;
this.createdNode = createdNode;
}
}
public interface IVisitor<T> {
void visit(TreeNode node, int deep);
T getResult();
}
private class DumpTreeVisitor implements IVisitor<String> {
String s = "";
@Override
public void visit(TreeNode node, int deep) {
StringBuilder subScriptionsStr = new StringBuilder();
String indentTabs = indentTabs(deep);
for (ClientTopicCouple couple : node.m_subscriptions) {
subScriptionsStr.append(indentTabs).append(couple.toString()).append("\n");
}
s += node.getToken() == null ? "" : node.getToken().toString();
s += "\n" + (node.m_subscriptions.isEmpty() ? indentTabs : "")
+ subScriptionsStr /* + "\n" */;
}
private String indentTabs(int deep) {
StringBuilder s = new StringBuilder();
for (int i = 0; i < deep; i++) {
s.append("\t");
// s += "--";
}
return s.toString();
}
@Override
public String getResult() {
return s;
}
}
private AtomicReference<TreeNode> subscriptions = new AtomicReference<>(new TreeNode());
private static final Logger LOG = LoggerFactory.getLogger(SubscriptionsDirectory.class);
private volatile ISubscriptionsStore subscriptionsStore;
/**
* Initialize the subscription tree with the list of subscriptions. Maintained for compatibility
* reasons.
*
* @param sessionsStore
* to be used as backing store from the subscription store.
*/
@Override
public void init(ISessionsStore sessionsStore) {
LOG.info("Initializing subscriptions store...");
this.subscriptionsStore = sessionsStore.subscriptionStore();
List<ClientTopicCouple> subscriptions = this.subscriptionsStore.listAllSubscriptions();
// reload any subscriptions persisted
if (LOG.isTraceEnabled()) {
LOG.trace("Reloading all stored subscriptions. SubscriptionTree = {}", dumpTree());
}
for (ClientTopicCouple clientTopic : subscriptions) {
LOG.info("Re-subscribing client to topic CId={}, topicFilter={}", clientTopic.clientID,
clientTopic.topicFilter);
add(clientTopic);
}
if (LOG.isTraceEnabled()) {
LOG.trace("Stored subscriptions have been reloaded. SubscriptionTree = {}", dumpTree());
}
}
@Override
public void add(ClientTopicCouple newSubscription) {
/*
* The topic filters have already been validated at the ProtocolProcessor. We can assume
* they are valid.
*/
TreeNode oldRoot;
NodeCouple couple;
do {
oldRoot = subscriptions.get();
couple = recreatePath(newSubscription.topicFilter, oldRoot);
couple.createdNode.addSubscription(newSubscription); //createdNode could be null?
couple.root.recalculateSubscriptionsSize();
//spin lock repeating till we can, swap root, if can't swap just re-do the operation
} while (!subscriptions.compareAndSet(oldRoot, couple.root));
LOG.debug("A subscription has been added. Root = {}, oldRoot = {}.", couple.root, oldRoot);
}
NodeCouple recreatePath(Topic topic, final TreeNode oldRoot) {
final TreeNode newRoot = oldRoot.copy();
TreeNode parent = newRoot;
TreeNode current = newRoot;
for (Token token : topic.getTokens()) {
TreeNode matchingChildren;
// check if a children with the same token already exists
if ((matchingChildren = current.childWithToken(token)) != null) {
// copy the traversed node
current = matchingChildren.copy();
// update the child just added in the children list
parent.updateChild(matchingChildren, current);
parent = current;
} else {
// create a new node for the newly inserted token
matchingChildren = new TreeNode();
matchingChildren.setToken(token);
current.addChild(matchingChildren);
current = matchingChildren;
}
}
return new NodeCouple(newRoot, current);
}
@Override
public void removeSubscription(Topic topic, String clientID) {
/*
* The topic filters have already been validated at the ProtocolProcessor. We can assume
* they are valid.
*/
TreeNode oldRoot;
NodeCouple couple;
do {
oldRoot = subscriptions.get();
couple = recreatePath(topic, oldRoot);
couple.createdNode.remove(new ClientTopicCouple(clientID, topic));
couple.root.recalculateSubscriptionsSize();
//spin lock repeating till we can, swap root, if can't swap just re-do the operation
} while (!subscriptions.compareAndSet(oldRoot, couple.root));
}
// /**
// * Visit the topics tree to remove matching subscriptions with clientID. It's a mutating
// * structure operation so create a new subscription tree (partial or total).
// *
// * @param clientID
// * the client ID to remove.
// */
// void removeForClient(String clientID) {
// TreeNode oldRoot;
// TreeNode newRoot;
// do {
// oldRoot = subscriptions.get();
// newRoot = oldRoot.removeClientSubscriptions(clientID);
// // spin lock repeating till we can, swap root, if can't swap just re-do the operation
// } while (!subscriptions.compareAndSet(oldRoot, newRoot));
// }
/**
* Given a topic string return the clients subscriptions that matches it. Topic string can't
* contain character # and + because they are reserved to listeners subscriptions, and not topic
* publishing.
*
* @param topic
* to use fo searching matching subscriptions.
* @return the list of matching subscriptions, or empty if not matching.
*/
@Override
public List<Subscription> matches(Topic topic) {
Queue<Token> tokenQueue = new LinkedBlockingDeque<>(topic.getTokens());
List<ClientTopicCouple> matchingSubs = new ArrayList<>();
subscriptions.get().matches(tokenQueue, matchingSubs);
// remove the overlapping subscriptions, selecting ones with greatest qos
Map<String, Subscription> subsForClient = new HashMap<>();
for (ClientTopicCouple matchingCouple : matchingSubs) {
Subscription existingSub = subsForClient.get(matchingCouple.clientID);
Subscription sub = this.subscriptionsStore.getSubscription(matchingCouple);
if (sub == null) {
// if the m_sessionStore hasn't the sub because the client disconnected
continue;
}
// update the selected subscriptions if not present or if has a greater qos
if (existingSub == null || existingSub.getRequestedQos().value() < sub.getRequestedQos().value()) {
subsForClient.put(matchingCouple.clientID, sub);
}
}
return new ArrayList<>(subsForClient.values());
}
@Override
public int size() {
return subscriptions.get().size();
}
@Override
public String dumpTree() {
DumpTreeVisitor visitor = new DumpTreeVisitor();
bfsVisit(subscriptions.get(), visitor, 0);
return visitor.getResult();
}
private void bfsVisit(TreeNode node, IVisitor<?> visitor, int deep) {
if (node == null) {
return;
}
visitor.visit(node, deep);
for (TreeNode child : node.m_children) {
bfsVisit(child, visitor, ++deep);
}
}
}
@@ -1,180 +0,0 @@
/*
* Copyright (c) 2012-2017 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.ISubscriptionsStore.ClientTopicCouple;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
class TreeNode {
Token m_token;
List<TreeNode> m_children = new ArrayList<>();
Set<ClientTopicCouple> m_subscriptions = new HashSet<>();
private int subtreeSubscriptions;
TreeNode() {
}
Token getToken() {
return m_token;
}
void setToken(Token topic) {
this.m_token = topic;
}
void addSubscription(ClientTopicCouple s) {
m_subscriptions.add(s);
this.subtreeSubscriptions++;
}
void addChild(TreeNode child) {
m_children.add(child);
this.subtreeSubscriptions += child.subtreeSubscriptions;
}
/**
* Creates a shallow copy of the current node. Copy the token and the children.
*/
TreeNode copy() {
final TreeNode copy = new TreeNode();
copy.m_children = new ArrayList<>(m_children);
copy.m_subscriptions = new HashSet<>(m_subscriptions);
copy.m_token = m_token;
copy.subtreeSubscriptions = this.subtreeSubscriptions;
return copy;
}
/**
* Search for children that has the specified token, if not found return null;
*/
TreeNode childWithToken(Token token) {
for (TreeNode child : m_children) {
if (child.getToken().equals(token)) {
return child;
}
}
return null;
}
void updateChild(TreeNode oldChild, TreeNode newChild) {
m_children.remove(oldChild);
m_children.add(newChild);
this.subtreeSubscriptions += newChild.subtreeSubscriptions - oldChild.subtreeSubscriptions;
}
Collection<ClientTopicCouple> subscriptions() {
return m_subscriptions;
}
public void remove(ClientTopicCouple clientTopicCouple) {
m_subscriptions.remove(clientTopicCouple);
this.subtreeSubscriptions--;
}
// TODO smell a query method that return the result modifing the parameter (matchingSubs)
void matches(Queue<Token> tokens, List<ClientTopicCouple> matchingSubs) {
Token t = tokens.poll();
// check if t is null <=> tokens finished
if (t == null) {
matchingSubs.addAll(m_subscriptions);
// check if it has got a MULTI child and add its subscriptions
for (TreeNode n : m_children) {
if (n.getToken() == Token.MULTI || n.getToken() == Token.SINGLE) {
matchingSubs.addAll(n.subscriptions());
}
}
return;
}
// we are on MULTI, than add subscriptions and return
if (m_token == Token.MULTI) {
matchingSubs.addAll(m_subscriptions);
return;
}
for (TreeNode n : m_children) {
if (n.getToken().match(t)) {
// Create a copy of token, else if navigate 2 sibling it
// consumes 2 elements on the queue instead of one
n.matches(new LinkedBlockingQueue<>(tokens), matchingSubs);
// TODO don't create a copy n.matches(tokens, matchingSubs);
}
}
}
/**
* Return the number of registered subscriptions
*/
int size() {
return this.subtreeSubscriptions;
}
/**
* Create a copied subtree rooted on this node but purged of clientID's subscriptions.
*/
TreeNode removeClientSubscriptions(String clientID) {
// collect what to delete and then delete to avoid ConcurrentModification
TreeNode newSubRoot = this.copy();
remoteSubscriptions(clientID, newSubRoot);
removeSubscriptionFromChildren(clientID, newSubRoot);
return newSubRoot;
}
private void removeSubscriptionFromChildren(String clientID, TreeNode newSubRoot) {
int newSubtreeSubscriptions = 0;
//go deep
List<TreeNode> newChildren = new ArrayList<>(newSubRoot.m_children.size());
for (TreeNode child : newSubRoot.m_children) {
final TreeNode purgedSubtree = child.removeClientSubscriptions(clientID);
if (purgedSubtree.size() != 0) {
newSubtreeSubscriptions += purgedSubtree.size();
newChildren.add(purgedSubtree);
}
}
newSubRoot.m_children = newChildren;
newSubRoot.subtreeSubscriptions += newSubtreeSubscriptions;
}
private void remoteSubscriptions(String clientID, TreeNode newSubRoot) {
List<ClientTopicCouple> subsToRemove = new ArrayList<>();
for (ClientTopicCouple s : newSubRoot.m_subscriptions) {
if (s.clientID.equals(clientID)) {
subsToRemove.add(s);
}
}
for (ClientTopicCouple s : subsToRemove) {
newSubRoot.m_subscriptions.remove(s);
}
newSubRoot.subtreeSubscriptions = newSubRoot.m_subscriptions.size();
}
int recalculateSubscriptionsSize() {
int res = m_subscriptions.size();
for (TreeNode child : m_children) {
res += child.recalculateSubscriptionsSize();
}
this.subtreeSubscriptions = res;
return res;
}
}
@@ -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<ClientTopicCouple> subscriptions = subscriptionsStore.listAllSubscriptions();
List<Subscription> subscriptions = subscriptionsStore.listAllSubscriptions();
assertEquals(1, subscriptions.size());
Subscription sub = subscriptionsStore.getSubscription(subscriptions.get(0));
Subscription sub = subscriptions.get(0);
assertEquals(overridingSubscription.getRequestedQos(), sub.getRequestedQos());
}
}
@@ -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<Subscription> subscriptions = store.matches(new Topic("/topic"));
assertEquals(1, subscriptions.size());
List<Subscription> 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());
}
}
@@ -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<String> 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);
}
@@ -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<Subscription> 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<Subscription> 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<Subscription> 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<Subscription> 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<StoredMessage> 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<Subscription> 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");
@@ -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
@@ -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();
}
@@ -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);
@@ -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<Subscription> 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<ClientTopicCouple> matchingSubscriptions = sut.recursiveMatch(asTopic("/topic"), sut.root);
final Set<Subscription> 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<Subscription> subscriptions = this.sut.match(asTopic("client/test/b"));
@@ -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<CNode> matchedNode = sut.lookup(asTopic("/temp"));
assertTrue("Node on path /temp must be present", matchedNode.isPresent());
final Set<ClientTopicCouple> subscriptions = matchedNode.get().subscriptions;
final Set<Subscription> 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<CNode> matchedNode = sut.lookup(asTopic("/italy/happiness"));
assertTrue("Node on path /italy/happiness must be present", matchedNode.isPresent());
final Set<ClientTopicCouple> subscriptions = matchedNode.get().subscriptions;
final Set<Subscription> 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<ClientTopicCouple> matchingSubs = sut.recursiveMatch(asTopic("/temp"), sut.root);
final Set<Subscription> 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);
}
}
@@ -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<Subscription> 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);
}
}
@@ -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());
}
}
@@ -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<String, ConcurrentMap<Integer, StoredMessage>> 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<ClientTopicCouple> listAllSubscriptions() {
LOG.info("Retrieving existing subscriptions");
final List<ClientTopicCouple> allSubscriptions = new ArrayList<>();
for (String clientID : this.sessions.keySet()) {
ConcurrentMap<Topic, Subscription> 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<Subscription> getSubscriptions() {
public List<Subscription> listAllSubscriptions() {
LOG.debug("Retrieving existing subscriptions...");
List<Subscription> 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<Topic, Subscription> clientSubscriptions = this.mvStore.openMap("subscriptions_" + couple.clientID);
LOG.debug("Retrieving subscriptions CId={}, subscriptions={}", couple.clientID, clientSubscriptions);
return clientSubscriptions.get(couple.topicFilter);
public Collection<Subscription> listClientSubscriptions(String clientID) {
ConcurrentMap<Topic, Subscription> 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<Topic, Subscription> 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<ClientSession> getAllSessions() {
Collection<ClientSession> result = new ArrayList<>();
for (Map.Entry<String, PersistentSession> 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<PersistentSession> 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<Integer, StoredMessage> m = this.secondPhaseStore.get(clientID);
if (m == null) {
@@ -278,10 +263,7 @@ public class H2SessionsStore implements ISessionsStore, ISubscriptionsStore {
totalInflight += inflightPerClient.size();
}
Map<Integer, StoredMessage> secondPhaseInFlight = this.secondPhaseStore.get(clientID);
if (secondPhaseInFlight != null) {
totalInflight += secondPhaseInFlight.size();
}
// totalInflight += countPubReleaseWaitingPubComplete(clientID);
Map<Integer, StoredMessage> 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<Integer, StoredMessage> messageIdToGuid = this.mvStore.openMap(inboundStoreForClient(clientID));
return messageIdToGuid.get(messageID);
}
@Override
public void markAsInboundInflight(String clientID, int messageID, StoredMessage msg) {
ConcurrentMap<Integer, StoredMessage> 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);
}
}
@@ -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
@@ -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<ClientTopicCouple> listAllSubscriptions() {
LOG.debug("Retrieving existing subscriptions");
final List<ClientTopicCouple> allSubscriptions = new ArrayList<>();
for (String clientID : m_persistentSessions.keySet()) {
ConcurrentMap<Topic, Subscription> 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<Topic, Subscription> clientSubscriptions = m_db.getHashMap("subscriptions_" + couple.clientID);
LOG.debug("Retrieving subscriptions. CId={}, subscriptions={}", couple.clientID, clientSubscriptions);
return clientSubscriptions.get(couple.topicFilter);
}
@Override
public List<Subscription> getSubscriptions() {
public List<Subscription> listAllSubscriptions() {
LOG.debug("Retrieving existing subscriptions...");
List<Subscription> subscriptions = new ArrayList<>();
for (String clientID : m_persistentSessions.keySet()) {
@@ -137,48 +113,52 @@ class MapDBSessionsStore implements ISessionsStore, ISubscriptionsStore {
return subscriptions;
}
@Override
public Collection<Subscription> listClientSubscriptions(String clientID) {
ConcurrentMap<Topic, Subscription> 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<Topic, Subscription> 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<ClientSession> getAllSessions() {
Collection<ClientSession> result = new ArrayList<>();
for (Map.Entry<String, PersistentSession> 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<PersistentSession> 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<Integer, StoredMessage> m = this.m_secondPhaseStore.get(clientID);
if (m == null) {
@@ -283,10 +265,7 @@ class MapDBSessionsStore implements ISessionsStore, ISubscriptionsStore {
totalInflight += inflightPerClient.size();
}
Map<Integer, StoredMessage> secondPhaseInFlight = this.m_secondPhaseStore.get(clientID);
if (secondPhaseInFlight != null) {
totalInflight += secondPhaseInFlight.size();
}
// totalInflight += countPubReleaseWaitingPubComplete(clientID);
Map<Integer, StoredMessage> 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<Integer, StoredMessage> messageIdToGuid = m_db.getHashMap(inboundMessageId2MessagesMapName(clientID));
return messageIdToGuid.get(messageID);
}
@Override
public void markAsInboundInflight(String clientID, int messageID, StoredMessage msg) {
ConcurrentMap<Integer, StoredMessage> 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;
}
}
@@ -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
@@ -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;