Separated persistence layer, created submodule for MapDB persistence letting the broker use the in-memory message store

This commit is contained in:
andsel
2017-04-01 16:21:34 +02:00
parent 7ffb8fb43b
commit 5c6889fbd5
39 changed files with 477 additions and 499 deletions
@@ -0,0 +1,154 @@
/*
* 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.persistence.mapdb;
import io.moquette.spi.IMatchingCondition;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.MessageGUID;
import io.moquette.spi.impl.subscriptions.Topic;
import org.mapdb.DB;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
/**
* IMessagesStore implementation backed by MapDB.
*
* @author andrea
*/
class MapDBMessagesStore implements IMessagesStore {
private static final Logger LOG = LoggerFactory.getLogger(MapDBMessagesStore.class);
private DB m_db;
// maps clientID -> guid
private ConcurrentMap<Topic, MessageGUID> m_retainedStore;
// maps guid to message, it's message store
private ConcurrentMap<MessageGUID, IMessagesStore.StoredMessage> m_persistentMessageStore;
MapDBMessagesStore(DB db) {
m_db = db;
}
@Override
public void initStore() {
LOG.info("Initializing store...");
m_retainedStore = m_db.getHashMap("retained");
m_persistentMessageStore = m_db.getHashMap("persistedMessages");
}
@Override
public void storeRetained(Topic topic, MessageGUID guid) {
LOG.debug("Storing retained messages. Topic = {}, guid = {}.", topic, guid);
m_retainedStore.put(topic, guid);
}
@Override
public Collection<StoredMessage> searchMatching(IMatchingCondition condition) {
LOG.debug("Scanning retained messages...");
List<StoredMessage> results = new ArrayList<>();
for (Map.Entry<Topic, MessageGUID> entry : m_retainedStore.entrySet()) {
final MessageGUID guid = entry.getValue();
StoredMessage storedMsg = m_persistentMessageStore.get(guid);
if (condition.match(entry.getKey())) {
results.add(storedMsg);
}
}
if (LOG.isTraceEnabled()) {
LOG.trace("The retained messages have been scanned. MatchingMessages = {}.", results);
}
return results;
}
@Override
public MessageGUID storePublishForFuture(StoredMessage storedMessage) {
assert storedMessage.getClientID() != null : "The message to be persisted must have a valid client ID";
MessageGUID guid = new MessageGUID(UUID.randomUUID().toString());
storedMessage.setGuid(guid);
LOG.debug(
"Storing publish event. MqttClientId = {}, messageId = {}, guid = {}, topic = {}.",
storedMessage.getClientID(),
storedMessage.getMessageID(),
guid,
storedMessage.getTopic());
m_persistentMessageStore.put(guid, storedMessage);
ConcurrentMap<Integer, MessageGUID> messageIdToGuid = m_db
.getHashMap(messageId2GuidsMapName(storedMessage.getClientID()));
messageIdToGuid.put(storedMessage.getMessageID(), guid);
return guid;
}
@Override
public void dropMessagesInSession(String clientID) {
LOG.debug("Dropping stored messages. ClientId = {}.", clientID);
ConcurrentMap<Integer, MessageGUID> messageIdToGuid = m_db.getHashMap(messageId2GuidsMapName(clientID));
for (MessageGUID guid : messageIdToGuid.values()) {
removeStoredMessage(guid);
}
messageIdToGuid.clear();
}
void removeStoredMessage(MessageGUID guid) {
// remove only the not retained and no more referenced
StoredMessage storedMessage = m_persistentMessageStore.get(guid);
if (!storedMessage.isRetained()) {
LOG.debug(
"Dropping stored message. ClientId = {}, messageId = {}, guid = {}, topic = {}.",
storedMessage.getClientID(),
storedMessage.getMessageID(),
guid,
storedMessage.getTopic());
m_persistentMessageStore.remove(guid);
}
}
@Override
public StoredMessage getMessageByGuid(MessageGUID guid) {
LOG.debug("Retrieving stored message. Guid = {}.", guid);
return m_persistentMessageStore.get(guid);
}
@Override
public void cleanRetained(Topic topic) {
LOG.debug("Cleaning retained messages. Topic = {}.", topic);
m_retainedStore.remove(topic);
}
@Override
public int getPendingPublishMessages(String clientID) {
ConcurrentMap<Integer, MessageGUID> messageIdToGuidMap = m_db
.getHashMap(messageId2GuidsMapName(clientID));
return messageIdToGuidMap.size();
}
@Override
public MessageGUID mapToGuid(String clientID, int messageID) {
LOG.debug("Mapping message ID to GUID CId={}, messageId={}", clientID, messageID);
ConcurrentMap<Integer, MessageGUID> messageIdToGuid = m_db.getHashMap(messageId2GuidsMapName(clientID));
MessageGUID result = messageIdToGuid.get(messageID);
LOG.debug("Message ID has been mapped to a GUID CId={}, messageId={}, guid={}", clientID, messageID, result);
return result;
}
static String messageId2GuidsMapName(String clientID) {
return "guidsMapping_" + clientID;
}
}
@@ -0,0 +1,137 @@
/*
* 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.persistence.mapdb;
import io.moquette.BrokerConstants;
import io.moquette.server.config.IConfig;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.IStore;
import org.mapdb.DB;
import org.mapdb.DBMaker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* MapDB main persistence implementation
*/
public class MapDBPersistentStore implements IStore {
private static final Logger LOG = LoggerFactory.getLogger(MapDBPersistentStore.class);
private DB m_db;
private final String m_storePath;
private final int m_autosaveInterval; // in seconds
protected final ScheduledExecutorService m_scheduler;
private IMessagesStore m_messageStore;
private ISessionsStore m_sessionsStore;
public MapDBPersistentStore(IConfig props, ScheduledExecutorService scheduler) {
this.m_storePath = props.getProperty(BrokerConstants.PERSISTENT_STORE_PROPERTY_NAME, "");
this.m_autosaveInterval = Integer.parseInt(props.getProperty(BrokerConstants.AUTOSAVE_INTERVAL_PROPERTY_NAME, "30"));
this.m_scheduler = scheduler;
}
/**
* Factory method to create message store backed by MapDB
*
* @return the message store instance.
*/
@Override
public IMessagesStore messagesStore() {
return m_messageStore;
}
@Override
public ISessionsStore sessionsStore() {
return m_sessionsStore;
}
public void initStore() {
LOG.info("Initializing MapDB store...");
if (m_storePath == null || m_storePath.isEmpty()) {
LOG.warn("The MapDB store file path is empty. Using in-memory store.");
m_db = DBMaker.newMemoryDB().make();
} else {
File tmpFile;
try {
LOG.info("Using user-defined MapDB store file. Path = {}.", m_storePath);
tmpFile = new File(m_storePath);
boolean fileNewlyCreated = tmpFile.createNewFile();
LOG.warn("Using {} MapDB store file. Path = {}.", fileNewlyCreated ? "fresh" : "existing", m_storePath);
} catch (IOException ex) {
LOG.error(
"Unable to open MapDB store file. Path = {}, cause = {}, errorMessage = {}.",
m_storePath,
ex.getCause(),
ex.getMessage());
throw new RuntimeException(
"Can't create temp file for subscriptions storage [" + m_storePath + "]",
ex);
}
m_db = DBMaker.newFileDB(tmpFile).make();
}
LOG.info("Scheduling MapDB commit task...");
m_scheduler.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
LOG.debug("Committing to MapDB...");
m_db.commit();
}
}, this.m_autosaveInterval, this.m_autosaveInterval, TimeUnit.SECONDS);
// TODO check m_db is valid and
m_messageStore = new MapDBMessagesStore(m_db);
m_messageStore.initStore();
m_sessionsStore = new MapDBSessionsStore(m_db, m_messageStore);
m_sessionsStore.initStore();
}
@Override
public void close() {
if (this.m_db.isClosed()) {
LOG.warn("The MapDB store is already closed. Nothing will be done.");
return;
}
LOG.info("Performing last commit to MapDB...");
this.m_db.commit();
LOG.info("Closing MapDB store...");
this.m_db.close();
LOG.info("Stopping MapDB commit tasks...");
//TODO th scheduler must be stopped by the owning (the instance of Server)
//invalidate the added task
this.m_scheduler.shutdown();
try {
m_scheduler.awaitTermination(10L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
}
if (!m_scheduler.isTerminated()) {
LOG.warn("Forcing shutdown of MapDB commit tasks...");
m_scheduler.shutdown();
}
LOG.info("The MapDB store has been closed successfully.");
}
}
@@ -0,0 +1,325 @@
/*
* 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.persistence.mapdb;
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;
import io.moquette.spi.MessageGUID;
import io.moquette.spi.impl.Utils;
import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.impl.subscriptions.Topic;
import org.mapdb.DB;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* ISessionsStore implementation backed by MapDB.
*
* @author andrea
*/
class MapDBSessionsStore implements ISessionsStore {
private static final Logger LOG = LoggerFactory.getLogger(MapDBSessionsStore.class);
// maps clientID->[MessageId -> guid]
private ConcurrentMap<String, ConcurrentMap<Integer, MessageGUID>> m_inflightStore;
// map clientID <-> set of currently in flight packet identifiers
private Map<String, Set<Integer>> m_inFlightIds;
private ConcurrentMap<String, PersistentSession> m_persistentSessions;
// maps clientID->[MessageId -> guid]
private ConcurrentMap<String, Map<Integer, MessageGUID>> m_secondPhaseStore;
private final DB m_db;
private final IMessagesStore m_messagesStore;
MapDBSessionsStore(DB db, IMessagesStore messagesStore) {
m_db = db;
m_messagesStore = messagesStore;
}
@Override
public void initStore() {
m_inflightStore = m_db.getHashMap("inflight");
m_inFlightIds = m_db.getHashMap("inflightPacketIDs");
m_persistentSessions = m_db.getHashMap("sessions");
m_secondPhaseStore = m_db.getHashMap("secondPhase");
}
@Override
public void addNewSubscription(Subscription newSubscription) {
LOG.info("Adding new subscription. ClientId={}, topics={}", newSubscription.getClientId(),
newSubscription.getTopicFilter());
final String clientID = newSubscription.getClientId();
m_db.getHashMap("subscriptions_" + clientID).put(newSubscription.getTopicFilter(), newSubscription);
if (LOG.isTraceEnabled()) {
LOG.trace("Subscription has been added. ClientId={}, topics={}, clientSubscriptions={}",
newSubscription.getClientId(), newSubscription.getTopicFilter(),
m_db.getHashMap("subscriptions_" + clientID));
}
}
@Override
public void removeSubscription(Topic topicFilter, String clientID) {
LOG.info("Removing subscription. ClientId={}, topics={}", clientID, topicFilter);
if (!m_db.exists("subscriptions_" + clientID)) {
return;
}
m_db.getHashMap("subscriptions_" + clientID).remove(topicFilter);
if (LOG.isDebugEnabled()) {
LOG.debug("Subscription has been removed. ClientId={}, topics={}, clientSubscriptions={}", clientID,
topicFilter, m_db.getHashMap("subscriptions_" + clientID));
}
}
@Override
public void wipeSubscriptions(String clientID) {
LOG.info("Wiping subscriptions. ClientId={}", clientID);
m_db.delete("subscriptions_" + clientID);
if (LOG.isDebugEnabled()) {
LOG.debug("Subscriptions have been removed. ClientId={}, clientSubscriptions={}", clientID,
m_db.getHashMap("subscriptions_" + clientID));
}
}
@Override
public List<ClientTopicCouple> listAllSubscriptions() {
LOG.info("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.info("Retrieving subscriptions. ClientId={}, subscriptions={}", couple.clientID, clientSubscriptions);
return clientSubscriptions.get(couple.topicFilter);
}
@Override
public List<Subscription> getSubscriptions() {
LOG.info("Retrieving existing subscriptions...");
List<Subscription> subscriptions = new ArrayList<>();
for (String clientID : m_persistentSessions.keySet()) {
ConcurrentMap<Topic, Subscription> clientSubscriptions = m_db.getHashMap("subscriptions_" + clientID);
subscriptions.addAll(clientSubscriptions.values());
}
LOG.debug("The existing subscriptions have been retrieved. Result={}", subscriptions);
return subscriptions;
}
@Override
public boolean contains(String clientID) {
return m_db.exists("subscriptions_" + 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.info("Creating new session. ClientId={}, cleanSession={}", clientID, cleanSession);
m_persistentSessions.putIfAbsent(clientID, new PersistentSession(cleanSession));
return new ClientSession(clientID, m_messagesStore, this, cleanSession);
}
@Override
public ClientSession sessionForClient(String clientID) {
LOG.info("Retrieving session. ClientId={}", clientID);
if (!m_persistentSessions.containsKey(clientID)) {
LOG.warn("The session does not exist. ClientId={}", clientID);
return null;
}
PersistentSession storedSession = m_persistentSessions.get(clientID);
return new ClientSession(clientID, m_messagesStore, this, storedSession.cleanSession);
}
@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(), m_messagesStore, this, entry.getValue().cleanSession));
}
return result;
}
@Override
public void updateCleanStatus(String clientID, boolean cleanSession) {
LOG.info("Updating cleanSession flag. ClientId={}, cleanSession={}", clientID, cleanSession);
m_persistentSessions.put(clientID, new PersistentSession(cleanSession));
}
/**
* Return the next valid packetIdentifier for the given client session.
*/
@Override
public int nextPacketID(String clientID) {
if (LOG.isDebugEnabled()) {
LOG.debug("Generating next packet ID. ClientId={}", clientID);
}
Set<Integer> inFlightForClient = this.m_inFlightIds.get(clientID);
if (inFlightForClient == null) {
int nextPacketId = 1;
inFlightForClient = Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>());
inFlightForClient.add(nextPacketId);
this.m_inFlightIds.put(clientID, inFlightForClient);
return nextPacketId;
}
int maxId = inFlightForClient.isEmpty() ? 0 : Collections.max(inFlightForClient);
int nextPacketId = (maxId % 0xFFFF) + 1;
inFlightForClient.add(nextPacketId);
if (LOG.isDebugEnabled()) {
LOG.debug("The next packet ID has been generated. ClientId={}, result={}", clientID, nextPacketId);
}
return nextPacketId;
}
@Override
public void inFlightAck(String clientID, int messageID) {
LOG.debug("Acknowledging inflight message. ClientId={}, messageId={}", clientID, messageID);
Map<Integer, MessageGUID> m = this.m_inflightStore.get(clientID);
if (m == null) {
LOG.warn("Unable to retrieve inflight message record. ClientId={}, messageId={}", clientID, messageID);
return;
}
m.remove(messageID);
// remove from the ids store
Set<Integer> inFlightForClient = this.m_inFlightIds.get(clientID);
if (inFlightForClient != null) {
inFlightForClient.remove(messageID);
}
}
@Override
public void inFlight(String clientID, int messageID, MessageGUID guid) {
LOG.debug("Storing inflight message. ClientId={}, messageId={}, guid={}", clientID, messageID, guid);
ConcurrentMap<Integer, MessageGUID> m = this.m_inflightStore.get(clientID);
if (m == null) {
m = new ConcurrentHashMap<>();
}
m.put(messageID, guid);
LOG.info("storing inflight clientID <{}> messageID {} guid <{}>", clientID, messageID, guid);
this.m_inflightStore.put(clientID, m);
}
@Override
public BlockingQueue<StoredMessage> queue(String clientID) {
LOG.info("Queuing pending message. ClientId={}, guid={}", clientID);
return this.m_db.getQueue(clientID);
}
@Override
public void dropQueue(String clientID) {
LOG.info("Removing pending messages. ClientId={}", clientID);
this.m_db.delete(clientID);
}
@Override
public void moveInFlightToSecondPhaseAckWaiting(String clientID, int messageID) {
if (LOG.isDebugEnabled()) {
LOG.debug("Moving inflight message to 2nd phase ack state. ClientId={}, messageID={}", clientID, messageID);
}
Map<Integer, MessageGUID> m = this.m_inflightStore.get(clientID);
if (m == null) {
LOG.warn("Unable to retrieve inflight message record. ClientId={}, messageId={}", clientID, messageID);
return;
}
MessageGUID guid = m.remove(messageID);
// remove from the ids store
Set<Integer> inFlightForClient = this.m_inFlightIds.get(clientID);
if (inFlightForClient != null) {
inFlightForClient.remove(messageID);
}
final HashMap<Integer, MessageGUID> emptyGuids = new HashMap<>();
Map<Integer, MessageGUID> messageIDs = Utils.defaultGet(m_secondPhaseStore, clientID, emptyGuids);
messageIDs.put(messageID, guid);
m_secondPhaseStore.put(clientID, messageIDs);
}
@Override
public MessageGUID secondPhaseAcknowledged(String clientID, int messageID) {
if (LOG.isDebugEnabled()) {
LOG.debug("Processing second phase ACK CId={}, messageId={}", clientID, messageID);
}
final HashMap<Integer, MessageGUID> emptyGuids = new HashMap<>();
Map<Integer, MessageGUID> messageIDs = Utils.defaultGet(m_secondPhaseStore, clientID, emptyGuids);
MessageGUID guid = messageIDs.remove(messageID);
m_secondPhaseStore.put(clientID, messageIDs);
return guid;
}
@Override
public StoredMessage getInflightMessage(String clientID, int messageID) {
LOG.info("Retrieving inflight message CId={}, messageId={}", clientID, messageID);
Map<Integer, MessageGUID> clientEntries = m_inflightStore.get(clientID);
if (clientEntries == null) {
LOG.warn("The client has no inflight messages CId={}, messageId={}", clientID, messageID);
return null;
}
MessageGUID guid = clientEntries.get(messageID);
if (guid == null) {
LOG.warn("The message ID does not have an associated GUID. ClientId={}, messageId={}", clientID, messageID);
return null;
}
return m_messagesStore.getMessageByGuid(guid);
}
@Override
public int getInflightMessagesNo(String clientID) {
if (!m_inflightStore.containsKey(clientID))
return 0;
else
return m_inflightStore.get(clientID).size();
}
@Override
public int getPendingPublishMessagesNo(String clientID) {
return m_messagesStore.getPendingPublishMessages(clientID);
}
@Override
public int getSecondPhaseAckPendingMessages(String clientID) {
if (!m_secondPhaseStore.containsKey(clientID))
return 0;
else
return m_secondPhaseStore.get(clientID).size();
}
}
@@ -0,0 +1,196 @@
/*
* 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.persistence.mapdb;
import java.io.File;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import io.moquette.BrokerConstants;
import io.moquette.server.config.IConfig;
import io.moquette.server.config.MemoryConfig;
import io.moquette.spi.ClientSession;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.ISessionsStore.ClientTopicCouple;
import io.moquette.spi.MessageGUID;
import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.impl.subscriptions.Topic;
import io.netty.handler.codec.mqtt.MqttQoS;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import static io.moquette.BrokerConstants.PERSISTENT_STORE_PROPERTY_NAME;
import static org.junit.Assert.*;
/**
*
* @author andrea
*/
public class MapDBPersistentStoreTest {
public static final String TEST_CLIENT = "TestClient";
MapDBPersistentStore m_storageService;
ISessionsStore m_sessionsStore;
IMessagesStore m_messagesStore;
private ScheduledExecutorService scheduler;
@Before
public void setUp() throws Exception {
scheduler = Executors.newScheduledThreadPool(1);
cleanPersistenceFile(BrokerConstants.DEFAULT_PERSISTENT_PATH);
Properties props = new Properties();
props.setProperty(BrokerConstants.PERSISTENT_STORE_PROPERTY_NAME, BrokerConstants.DEFAULT_PERSISTENT_PATH);
IConfig conf = new MemoryConfig(props);
m_storageService = new MapDBPersistentStore(conf, scheduler);
m_storageService.initStore();
m_messagesStore = m_storageService.messagesStore();
m_sessionsStore = m_storageService.sessionsStore();
}
@After
public void tearDown() {
if (m_storageService != null) {
m_storageService.close();
}
scheduler.shutdown();
cleanPersistenceFile(BrokerConstants.DEFAULT_PERSISTENT_PATH);
}
public static void cleanPersistenceFile(String fileName) {
File dbFile = new File(fileName);
if (dbFile.exists()) {
dbFile.delete();
new File(fileName + ".p").delete();
new File(fileName + ".t").delete();
}
assertFalse(dbFile.exists());
}
@Test
public void overridingSubscriptions() {
ClientSession session1 = m_sessionsStore.createNewSession("SESSION_ID_1", true);
// Subscribe on /topic with QOSType.MOST_ONE
Subscription oldSubscription = new Subscription(session1.clientID, new Topic("/topic"), MqttQoS.AT_MOST_ONCE);
session1.subscribe(oldSubscription);
// Subscribe on /topic again that overrides the previous subscription.
Subscription overridingSubscription = new Subscription(
session1.clientID,
new Topic("/topic"),
MqttQoS.EXACTLY_ONCE);
session1.subscribe(overridingSubscription);
// Verify
List<ClientTopicCouple> subscriptions = m_sessionsStore.listAllSubscriptions();
assertEquals(1, subscriptions.size());
Subscription sub = m_sessionsStore.getSubscription(subscriptions.get(0));
assertEquals(overridingSubscription.getRequestedQos(), sub.getRequestedQos());
}
@Test
public void testNextPacketID_notExistingClientSession() {
int packetId = m_sessionsStore.nextPacketID("NOT_EXISTING_CLI");
assertEquals(1, packetId);
}
@Test
public void testNextPacketID_existingClientSession() {
// Force creation of inflight map for the CLIENT session
int packetId = m_sessionsStore.nextPacketID("CLIENT");
assertEquals(1, packetId);
// request a second packetID
packetId = m_sessionsStore.nextPacketID("CLIENT");
assertEquals(2, packetId);
}
@Test
public void testNextPacketID() {
// request a first ID
int packetId = m_sessionsStore.nextPacketID("CLIENT");
m_sessionsStore.inFlight("CLIENT", packetId, new MessageGUID("ABCDE")); // simulate an
// inflight
assertEquals(1, packetId);
// release the ID
m_sessionsStore.inFlightAck("CLIENT", packetId);
// request a second packetID, counter restarts from 0
packetId = m_sessionsStore.nextPacketID("CLIENT");
assertEquals(1, packetId);
}
@Test
public void testCloseShutdownCommitTask() throws InterruptedException {
m_storageService.close();
// verify the executor is shutdown
assertTrue(
"Storage service scheduler can't be stopped in 3 seconds",
m_storageService.m_scheduler.awaitTermination(3, TimeUnit.SECONDS));
assertTrue(m_storageService.m_scheduler.isTerminated());
}
@Test
public void testDropMessagesInSessionCleanAllNotRetainedStoredMessages() {
m_sessionsStore.createNewSession("TestClient", true);
IMessagesStore.StoredMessage publishToStore = new IMessagesStore.StoredMessage(
"Hello".getBytes(),
MqttQoS.EXACTLY_ONCE,
"/topic");
publishToStore.setClientID(TEST_CLIENT);
publishToStore.setMessageID(1);
publishToStore.setRetained(false);
MessageGUID guid = m_messagesStore.storePublishForFuture(publishToStore);
// Exercise
m_messagesStore.dropMessagesInSession("TestClient");
// Verify the message store for session is empty.
IMessagesStore.StoredMessage storedPublish = m_messagesStore.getMessageByGuid(guid);
assertNull("The stored message must'n be present anymore", storedPublish);
}
@Test
public void testDropMessagesInSessionDoesntCleanAnyRetainedStoredMessages() {
m_sessionsStore.createNewSession("TestClient", true);
IMessagesStore.StoredMessage publishToStore = new IMessagesStore.StoredMessage(
"Hello".getBytes(),
MqttQoS.EXACTLY_ONCE,
"/topic");
publishToStore.setClientID(TEST_CLIENT);
publishToStore.setMessageID(1);
publishToStore.setRetained(true);
MessageGUID guid = m_messagesStore.storePublishForFuture(publishToStore);
// Exercise
m_messagesStore.dropMessagesInSession("TestClient");
// Verify the message store for session is empty.
IMessagesStore.StoredMessage storedPublish = m_messagesStore.getMessageByGuid(guid);
assertNotNull("The stored retained message must be present after client's session drop", storedPublish);
}
}