Separated persistence layer, created submodule for MapDB persistence letting the broker use the in-memory message store
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
+196
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user