Merge pull request #321 from AndreasMager/add-session-class-for-memory-session-store

MemorySessionStore: Session class added
This commit is contained in:
Andrea Selva
2017-06-05 08:57:14 +02:00
committed by GitHub
7 changed files with 169 additions and 126 deletions
@@ -21,7 +21,6 @@ import io.moquette.spi.ClientSession;
import io.moquette.spi.IMessagesStore.StoredMessage; import io.moquette.spi.IMessagesStore.StoredMessage;
import io.moquette.spi.ISessionsStore; import io.moquette.spi.ISessionsStore;
import io.moquette.spi.ISubscriptionsStore; import io.moquette.spi.ISubscriptionsStore;
import io.moquette.spi.impl.Utils;
import io.moquette.spi.impl.subscriptions.Subscription; import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.impl.subscriptions.Topic; import io.moquette.spi.impl.subscriptions.Topic;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -30,37 +29,48 @@ import org.slf4j.LoggerFactory;
import java.util.*; import java.util.*;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore { public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore {
private static final Logger LOG = LoggerFactory.getLogger(MemorySessionStore.class); private static final Logger LOG = LoggerFactory.getLogger(MemorySessionStore.class);
private Map<String, Map<Topic, Subscription>> m_persistentSubscriptions = new HashMap<>(); class Session {
final String clientID;
final ClientSession clientSession;
final Map<Topic, Subscription> subscriptions = new ConcurrentHashMap<>();
final AtomicReference<PersistentSession> persistentSession = new AtomicReference<>(null);
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<>();
private Map<String, PersistentSession> m_persistentSessions = new HashMap<>(); Session(String clientID, ClientSession clientSession) {
this.clientID = clientID;
this.clientSession = clientSession;
}
}
// maps clientID->BlockingQueue private final Map<String, Session> sessions = new ConcurrentHashMap<>();
private Map<String, BlockingQueue<StoredMessage>> queues = new HashMap<>();
// maps clientID->[MessageId -> msg]
private Map<String, Map<Integer, StoredMessage>> m_secondPhaseStore = new HashMap<>();
// maps clientID->[MessageId -> msg]
private Map<String, Map<Integer, StoredMessage>> outboundFlightMessages = new HashMap<>();
// maps clientID->[MessageId -> msg]
private Map<String, Map<Integer, StoredMessage>> inboundFlightMessages = new HashMap<>();
public MemorySessionStore() { public MemorySessionStore() {
} }
private Session getSession(String clientID) {
Session session = sessions.get(clientID);
if (session == null) {
LOG.error("Can't find the session for client <{}>", clientID);
throw new RuntimeException("Can't find the session for client <" + clientID + ">");
}
return session;
}
@Override @Override
public void removeSubscription(Topic topic, String clientID) { public void removeSubscription(Topic topic, String clientID) {
LOG.debug("removeSubscription topic filter: {} for clientID: {}", topic, clientID); LOG.debug("removeSubscription topic filter: {} for clientID: {}", topic, clientID);
if (!m_persistentSubscriptions.containsKey(clientID)) { getSession(clientID).subscriptions.remove(topic);
return;
}
Map<Topic, Subscription> clientSubscriptions = m_persistentSubscriptions.get(clientID);
clientSubscriptions.remove(topic);
} }
@Override @Override
@@ -75,65 +85,80 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore {
@Override @Override
public void addNewSubscription(Subscription newSubscription) { public void addNewSubscription(Subscription newSubscription) {
final String clientID = newSubscription.getClientId(); final String clientID = newSubscription.getClientId();
if (!m_persistentSubscriptions.containsKey(clientID)) { Session session = sessions.get(clientID);
m_persistentSubscriptions.put(clientID, new HashMap<Topic, Subscription>()); if (session == null) {
LOG.error("Can't find the session for client <{}>", clientID);
return;
} }
m_persistentSubscriptions.get(clientID).put(newSubscription.getTopicFilter(), newSubscription); session.subscriptions.put(newSubscription.getTopicFilter(), newSubscription);
} }
@Override @Override
public void wipeSubscriptions(String clientID) { public void wipeSubscriptions(String clientID) {
m_persistentSubscriptions.remove(clientID); if (!sessions.containsKey(clientID)) {
LOG.error("Can't find the session for client <{}>", clientID);
return;
}
sessions.get(clientID).subscriptions.clear();
} }
@Override @Override
public boolean contains(String clientID) { public boolean contains(String clientID) {
return m_persistentSubscriptions.containsKey(clientID); return sessions.containsKey(clientID);
} }
@Override @Override
public ClientSession createNewSession(String clientID, boolean cleanSession) { public ClientSession createNewSession(String clientID, boolean cleanSession) {
LOG.debug("createNewSession for client <{}>", clientID); LOG.debug("createNewSession for client <{}>", clientID);
if (m_persistentSessions.containsKey(clientID)) { Session session = sessions.get(clientID);
if (session != null) {
LOG.error("already exists a session for client <{}>, bad condition", clientID); 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); 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); LOG.debug("clientID {} is a newcome, creating it's empty subscriptions set", clientID);
m_persistentSubscriptions.put(clientID, new HashMap<Topic, Subscription>()); session = new Session(clientID, new ClientSession(clientID, this, this, cleanSession));
m_persistentSessions.put(clientID, new PersistentSession(cleanSession)); session.persistentSession.set(new PersistentSession(cleanSession));
return new ClientSession(clientID, this, this, cleanSession); sessions.put(clientID, session);
return session.clientSession;
} }
@Override @Override
public ClientSession sessionForClient(String clientID) { public ClientSession sessionForClient(String clientID) {
if (!m_persistentSessions.containsKey(clientID)) { if (!sessions.containsKey(clientID)) {
LOG.error("Can't find the session for client <{}>", clientID);
return null; return null;
} }
PersistentSession storedSession = m_persistentSessions.get(clientID); PersistentSession storedSession = sessions.get(clientID).persistentSession.get();
return new ClientSession(clientID, this, this, storedSession.cleanSession); return new ClientSession(clientID, this, this, storedSession.cleanSession);
} }
@Override @Override
public Collection<ClientSession> getAllSessions() { public Collection<ClientSession> getAllSessions() {
Collection<ClientSession> result = new ArrayList<>(); Collection<ClientSession> result = new ArrayList<>();
for (Map.Entry<String, PersistentSession> entry : m_persistentSessions.entrySet()) { for (Session entry : sessions.values()) {
result.add(new ClientSession(entry.getKey(), this, this, entry.getValue().cleanSession)); result.add(new ClientSession(entry.clientID, this, this, entry.persistentSession.get().cleanSession));
} }
return result; return result;
} }
@Override @Override
public void updateCleanStatus(String clientID, boolean cleanSession) { public void updateCleanStatus(String clientID, boolean cleanSession) {
m_persistentSessions.put(clientID, new PersistentSession(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 @Override
public List<ClientTopicCouple> listAllSubscriptions() { public List<ClientTopicCouple> listAllSubscriptions() {
List<ClientTopicCouple> allSubscriptions = new ArrayList<>(); List<ClientTopicCouple> allSubscriptions = new ArrayList<>();
for (Map.Entry<String, Map<Topic, Subscription>> entry : m_persistentSubscriptions.entrySet()) { for (Session entry : sessions.values()) {
for (Subscription sub : entry.getValue().values()) { for (Subscription sub : entry.subscriptions.values()) {
allSubscriptions.add(sub.asClientTopicCouple()); allSubscriptions.add(sub.asClientTopicCouple());
} }
} }
@@ -142,7 +167,13 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore {
@Override @Override
public Subscription getSubscription(ClientTopicCouple couple) { public Subscription getSubscription(ClientTopicCouple couple) {
Map<Topic, Subscription> subscriptions = m_persistentSubscriptions.get(couple.clientID); String clientID = couple.clientID;
if (!sessions.containsKey(clientID)) {
LOG.error("Can't find the session for client <{}>", clientID);
return null;
}
Map<Topic, Subscription> subscriptions = sessions.get(clientID).subscriptions;
if (subscriptions == null || subscriptions.isEmpty()) { if (subscriptions == null || subscriptions.isEmpty()) {
return null; return null;
} }
@@ -152,32 +183,26 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore {
@Override @Override
public List<Subscription> getSubscriptions() { public List<Subscription> getSubscriptions() {
List<Subscription> subscriptions = new ArrayList<>(); List<Subscription> subscriptions = new ArrayList<>();
for (Map.Entry<String, Map<Topic, Subscription>> entry : m_persistentSubscriptions.entrySet()) { for (Session entry : sessions.values()) {
subscriptions.addAll(entry.getValue().values()); subscriptions.addAll(entry.subscriptions.values());
} }
return subscriptions; return subscriptions;
} }
@Override @Override
public StoredMessage inFlightAck(String clientID, int messageID) { public StoredMessage inFlightAck(String clientID, int messageID) {
Map<Integer, StoredMessage> m = this.outboundFlightMessages.get(clientID); return getSession(clientID).outboundFlightMessages.remove(messageID);
if (m == null) {
LOG.error("Can't find the inFlight record for client <{}>", clientID);
throw new RuntimeException("Can't find the inFlight record for client <" + clientID + ">");
}
StoredMessage msg = m.remove(messageID);
this.outboundFlightMessages.put(clientID, m);
return msg;
} }
@Override @Override
public void inFlight(String clientID, int messageID, StoredMessage msg) { public void inFlight(String clientID, int messageID, StoredMessage msg) {
Map<Integer, StoredMessage> messages = outboundFlightMessages.get(clientID); Session session = sessions.get(clientID);
if (messages == null) { if (session == null) {
messages = new HashMap<>(); LOG.error("Can't find the session for client <{}>", clientID);
return;
} }
messages.put(messageID, msg);
outboundFlightMessages.put(clientID, messages); session.outboundFlightMessages.put(messageID, msg);
} }
/** /**
@@ -185,14 +210,13 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore {
*/ */
@Override @Override
public int nextPacketID(String clientID) { public int nextPacketID(String clientID) {
Map<Integer, StoredMessage> m = this.outboundFlightMessages.get(clientID); if (!sessions.containsKey(clientID)) {
if (m == null) { LOG.error("Can't find the session for client <{}>", clientID);
m = new HashMap<>(); return -1;
int nextPacketId = 1;
m.put(nextPacketId, null);
return nextPacketId;
} }
int maxId = m.keySet().isEmpty() ? 0 :Collections.max(m.keySet());
Map<Integer, StoredMessage> m = sessions.get(clientID).outboundFlightMessages;
int maxId = m.keySet().isEmpty() ? 0 : Collections.max(m.keySet());
int nextPacketId = (maxId + 1) % 0xFFFF; int nextPacketId = (maxId + 1) % 0xFFFF;
m.put(nextPacketId, null); m.put(nextPacketId, null);
return nextPacketId; return nextPacketId;
@@ -200,104 +224,107 @@ public class MemorySessionStore implements ISessionsStore, ISubscriptionsStore {
@Override @Override
public BlockingQueue<StoredMessage> queue(String clientID) { public BlockingQueue<StoredMessage> queue(String clientID) {
final ArrayBlockingQueue<StoredMessage> emptyQueue = new ArrayBlockingQueue<>(Constants.MAX_MESSAGE_QUEUE); if (!sessions.containsKey(clientID)) {
BlockingQueue<StoredMessage> messagesQueue = Utils.defaultGet(queues, clientID, emptyQueue); LOG.error("Can't find the session for client <{}>", clientID);
queues.put(clientID, messagesQueue); return null;
return messagesQueue; }
return sessions.get(clientID).queue;
} }
@Override @Override
public void dropQueue(String clientID) { public void dropQueue(String clientID) {
queues.remove(clientID); sessions.get(clientID).queue.clear();
} }
@Override @Override
public void moveInFlightToSecondPhaseAckWaiting(String clientID, int messageID, StoredMessage msg) { public void moveInFlightToSecondPhaseAckWaiting(String clientID, int messageID, StoredMessage msg) {
LOG.info("Moving msg inflight second phase store, clientID <{}> messageID {}", clientID, messageID); LOG.info("Moving msg inflight second phase store, clientID <{}> messageID {}", clientID, messageID);
Map<Integer, StoredMessage> m = this.m_secondPhaseStore.get(clientID); Session session = sessions.get(clientID);
if (m == null) { if (session == null) {
m = new HashMap<>(); LOG.error("Can't find the session for client <{}>", clientID);
return;
} }
m.put(messageID, msg);
this.outboundFlightMessages.put(clientID, m); session.secondPhaseStore.put(messageID, msg);
session.outboundFlightMessages.put(messageID, msg);
} }
@Override @Override
public StoredMessage secondPhaseAcknowledged(String clientID, int messageID) { public StoredMessage secondPhaseAcknowledged(String clientID, int messageID) {
LOG.info("Acknowledged message in second phase, clientID <{}> messageID {}", clientID, messageID); LOG.info("Acknowledged message in second phase, clientID <{}> messageID {}", clientID, messageID);
final Map<Integer, StoredMessage> m = this.m_secondPhaseStore.get(clientID); return getSession(clientID).secondPhaseStore.remove(messageID);
if (m == null) {
String error = String.format("Can't find the inFlight record for client <%s> during the second phase " +
"acking of QoS2 pub", clientID);
LOG.error(error);
throw new RuntimeException(error);
}
StoredMessage msg = m.remove(messageID);
m_secondPhaseStore.put(clientID, m);
return msg;
} }
@Override @Override
public int getInflightMessagesNo(String clientID) { public int getInflightMessagesNo(String clientID) {
int totalInflight = 0; Session session = sessions.get(clientID);
Map<Integer, StoredMessage> inflightPerClient = this.inboundFlightMessages.get(clientID); if (session == null) {
if (inflightPerClient != null) { LOG.error("Can't find the session for client <{}>", clientID);
totalInflight += inflightPerClient.size(); return 0;
} }
Map<Integer, StoredMessage> secondPhaseInFlight = this.m_secondPhaseStore.get(clientID); return session.inboundFlightMessages.size() + session.secondPhaseStore.size()
if (secondPhaseInFlight != null) { + session.outboundFlightMessages.size();
totalInflight += secondPhaseInFlight.size();
}
Map<Integer, StoredMessage> outboundPerClient = outboundFlightMessages.get(clientID);
if (outboundPerClient != null) {
totalInflight += outboundPerClient.size();
}
return totalInflight;
} }
@Override @Override
public StoredMessage inboundInflight(String clientID, int messageID) { public StoredMessage inboundInflight(String clientID, int messageID) {
Map<Integer, StoredMessage> inflightPerClient = this.inboundFlightMessages.get(clientID); return getSession(clientID).inboundFlightMessages.get(messageID);
if (inflightPerClient == null) {
String error = String.format("Can't find inbound inflight zone for client <%s>", clientID);
LOG.error(error);
throw new RuntimeException(error);
}
return inflightPerClient.get(messageID);
} }
@Override @Override
public void markAsInboundInflight(String clientID, int messageID, StoredMessage msg) { public void markAsInboundInflight(String clientID, int messageID, StoredMessage msg) {
Map<Integer, StoredMessage> inflightPerClient = this.inboundFlightMessages.get(clientID); if (!sessions.containsKey(clientID))
if (inflightPerClient == null) { LOG.error("Can't find the session for client <{}>", clientID);
inflightPerClient = new HashMap<>();
}
inflightPerClient.put(messageID, msg); sessions.get(clientID).inboundFlightMessages.put(messageID, msg);
inboundFlightMessages.put(clientID, inflightPerClient);
} }
@Override @Override
public int getPendingPublishMessagesNo(String clientID) { public int getPendingPublishMessagesNo(String clientID) {
return queues.get(clientID).size(); if (!sessions.containsKey(clientID)) {
LOG.error("Can't find the session for client <{}>", clientID);
return 0;
}
return sessions.get(clientID).queue.size();
} }
@Override @Override
public int getSecondPhaseAckPendingMessages(String clientID) { public int getSecondPhaseAckPendingMessages(String clientID) {
Map<Integer, StoredMessage> pendingAcks = m_secondPhaseStore.get(clientID); if (!sessions.containsKey(clientID)) {
if (pendingAcks == null) LOG.error("Can't find the session for client <{}>", clientID);
return 0; return 0;
else }
return pendingAcks.size();
return sessions.get(clientID).secondPhaseStore.size();
} }
@Override @Override
public void dropInFlightMessagesInSession(String clientID) { public void cleanSession(String clientID) {
outboundFlightMessages.remove(clientID); LOG.error("Fooooooooo <{}>", clientID);
inboundFlightMessages.remove(clientID);
Session session = sessions.get(clientID);
if (session == null) {
LOG.error("Can't find the session for client <{}>", clientID);
return;
}
// remove also the messages stored of type QoS1/2
LOG.info("Removing stored messages with QoS 1 and 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);
} }
} }
@@ -86,6 +86,7 @@ public class NettyMQTTHandler extends ChannelInboundHandlerAdapter {
} catch (Throwable ex) { } catch (Throwable ex) {
LOG.error("Exception was caught while processing MQTT message, " + ex.getCause(), ex); LOG.error("Exception was caught while processing MQTT message, " + ex.getCause(), ex);
ctx.fireExceptionCaught(ex); ctx.fireExceptionCaught(ex);
ctx.close();
} }
} }
@@ -165,15 +165,7 @@ public class ClientSession {
} }
public void cleanSession() { public void cleanSession() {
LOG.info("Wiping existing subscriptions. ClientId={}", this.clientID); m_sessionsStore.cleanSession(this.clientID);
subscriptionsStore.wipeSubscriptions(this.clientID);
// remove also the messages stored of type QoS1/2
LOG.info("Removing stored messages with QoS 1 and 2. ClientId={}", this.clientID);
m_sessionsStore.dropInFlightMessagesInSession(this.clientID);
//remove also the enqueued messages
this.m_sessionsStore.dropQueue(this.clientID);
} }
public boolean isCleanSession() { public boolean isCleanSession() {
@@ -131,5 +131,5 @@ public interface ISessionsStore {
*/ */
int getSecondPhaseAckPendingMessages(String clientID); int getSecondPhaseAckPendingMessages(String clientID);
void dropInFlightMessagesInSession(String clientID); void cleanSession(String clientID);
} }
@@ -47,6 +47,9 @@ public class SubscriptionsDirectoryTest {
store.init(sessionsStore); store.init(sessionsStore);
this.subscriptionsStore = this.sessionsStore.subscriptionStore(); this.subscriptionsStore = this.sessionsStore.subscriptionStore();
storageService.sessionsStore().createNewSession("FAKE_CLI_ID_1", false);
storageService.sessionsStore().createNewSession("FAKE_CLI_ID_2", false);
} }
@Test @Test
@@ -211,6 +214,7 @@ public class SubscriptionsDirectoryTest {
store = new SubscriptionsDirectory(); store = new SubscriptionsDirectory();
MemoryStorageService memStore = new MemoryStorageService(); MemoryStorageService memStore = new MemoryStorageService();
ISessionsStore aSessionsStore = memStore.sessionsStore(); ISessionsStore aSessionsStore = memStore.sessionsStore();
aSessionsStore.createNewSession("FAKE_CLI_ID_1", false);
store.init(aSessionsStore); store.init(aSessionsStore);
Subscription sub = new Subscription("FAKE_CLI_ID_1", subscription, MqttQoS.AT_MOST_ONCE); Subscription sub = new Subscription("FAKE_CLI_ID_1", subscription, MqttQoS.AT_MOST_ONCE);
aSessionsStore.subscriptionStore().addNewSubscription(sub); aSessionsStore.subscriptionStore().addNewSubscription(sub);
@@ -362,9 +362,19 @@ public class H2SessionsStore implements ISessionsStore, ISubscriptionsStore {
} }
@Override @Override
public void dropInFlightMessagesInSession(String clientID) { 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);
this.secondPhaseStore.remove(clientID); this.secondPhaseStore.remove(clientID);
this.outboundFlightMessages.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);
} }
} }
@@ -323,9 +323,18 @@ class MapDBSessionsStore implements ISessionsStore, ISubscriptionsStore {
} }
@Override @Override
public void dropInFlightMessagesInSession(String clientID) { 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);
m_secondPhaseStore.remove(clientID); m_secondPhaseStore.remove(clientID);
outboundFlightMessages.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) { static String inboundMessageId2MessagesMapName(String clientID) {