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:
@@ -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;
|
||||
// }
|
||||
}
|
||||
|
||||
+34
-27
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+53
-51
@@ -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"));
|
||||
|
||||
+15
-13
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
-332
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user