Merge pull request #76 from wagnerluis1982/master

Accepted but ha to be reworked on the master
This commit is contained in:
Andrea Selva
2015-08-08 09:56:36 +02:00
14 changed files with 488 additions and 10 deletions
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2012-2014 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 org.eclipse.moquette.interception;
import org.eclipse.moquette.proto.messages.*;
import org.eclipse.moquette.spi.impl.subscriptions.Subscription;
/**
* Default empty implementation that can be used to create custom handlers
* without defining every method.
*
* @author Wagner Macedo
*/
public class DefaultInterceptHandler implements InterceptHandler {
@Override
public void onConnect(ConnectMessage msg) {
}
@Override
public void onDisconnect(String clientID) {
}
@Override
public void onPublish(PublishMessage msg) {
}
@Override
public void onSubscribe(Subscription sub) {
}
@Override
public void onUnsubscribe(Subscription sub) {
}
}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2012-2014 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 org.eclipse.moquette.interception;
import org.eclipse.moquette.proto.messages.*;
import org.eclipse.moquette.spi.impl.subscriptions.Subscription;
/**
* This interface is used to inject code for intercepting broker events.
* <p>
* The events can act only as observers.
* <p>
* Almost every method receives a subclass of {@link AbstractMessage}, except
* <code>onDisconnect</code> that receives the client id string and
* <code>onSubscribe</code> and <code>onUnsubscribe</code> that receive a
* {@link Subscription} object.
*
* @author Wagner Macedo
*/
public interface InterceptHandler {
void onConnect(ConnectMessage msg);
void onDisconnect(String clientID);
void onPublish(PublishMessage msg);
void onSubscribe(Subscription sub);
void onUnsubscribe(Subscription sub);
}
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2012-2014 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 org.eclipse.moquette.interception;
import org.eclipse.moquette.proto.messages.ConnectMessage;
import org.eclipse.moquette.proto.messages.PublishMessage;
import org.eclipse.moquette.spi.impl.subscriptions.Subscription;
/**
* This interface is to be used internally by the broker components.
* <p>
* An interface is used instead of a class to allow more flexibility in changing
* an implementation. For instance, this interface provide a lightweight
* no-handler interceptor.
* <p>
* Interceptor implementations forward notifications to a <code>InterceptHandler</code>,
* that is normally a field. So, the implementations should act as a proxy to a custom
* intercept handler.
*
* @see InterceptHandler
* @author Wagner Macedo
*/
public interface Interceptor {
/**
* When no custom handler is provided, this interceptor should be used.
*/
Interceptor NO_HANDLER_INTERCEPTOR = new NoHandlerInterceptor();
void notifyClientConnected(ConnectMessage msg);
void notifyClientDisconnected(String clientID);
void notifyTopicPublished(PublishMessage msg);
void notifyTopicSubscribed(Subscription sub);
void notifyTopicUnsubscribed(Subscription sub);
}
@@ -0,0 +1,33 @@
package org.eclipse.moquette.interception;
import org.eclipse.moquette.proto.messages.ConnectMessage;
import org.eclipse.moquette.proto.messages.PublishMessage;
import org.eclipse.moquette.spi.impl.subscriptions.Subscription;
/**
* @author Wagner Macedo
*/
public class NoHandlerInterceptor implements Interceptor {
NoHandlerInterceptor() {
}
@Override
public void notifyClientConnected(ConnectMessage msg) {
}
@Override
public void notifyClientDisconnected(String clientID) {
}
@Override
public void notifyTopicPublished(PublishMessage msg) {
}
@Override
public void notifyTopicSubscribed(Subscription sub) {
}
@Override
public void notifyTopicUnsubscribed(Subscription sub) {
}
}
@@ -15,6 +15,7 @@
*/
package org.eclipse.moquette.server;
import org.eclipse.moquette.interception.InterceptHandler;
import org.eclipse.moquette.server.netty.NettyAcceptor;
import org.eclipse.moquette.spi.impl.SimpleMessaging;
import org.slf4j.Logger;
@@ -86,8 +87,12 @@ public class Server {
* </ul>
*/
public void startServer(Properties configProps) throws IOException {
ConfigurationParser confParser = new ConfigurationParser(configProps);
ConfigurationParser confParser = new ConfigurationParser(configProps);
m_properties = confParser.getProperties();
final String handlerProp = System.getProperty("intercept.handler");
if (handlerProp != null) {
m_properties.setProperty("intercept.handler", handlerProp);
}
LOG.info("Persistent store file: " + m_properties.get(PERSISTENT_STORE_PROPERTY_NAME));
messaging = SimpleMessaging.getInstance();
messaging.init(m_properties);
@@ -0,0 +1,97 @@
/*
* Copyright (c) 2012-2014 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 org.eclipse.moquette.spi.impl;
import org.eclipse.moquette.interception.InterceptHandler;
import org.eclipse.moquette.interception.Interceptor;
import org.eclipse.moquette.proto.messages.ConnectMessage;
import org.eclipse.moquette.proto.messages.PublishMessage;
import org.eclipse.moquette.spi.impl.subscriptions.Subscription;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* An interceptor that execute the interception tasks asynchronously.
*
* @author Wagner Macedo
*/
final class BrokerInterceptor implements Interceptor {
private final InterceptHandler handler;
private final ExecutorService executor;
BrokerInterceptor(InterceptHandler handler) {
this.handler = handler;
executor = Executors.newFixedThreadPool(1);
}
/**
* Shutdown graciously the executor service
*/
void stop() {
executor.shutdown();
}
@Override
public void notifyClientConnected(final ConnectMessage msg) {
executor.execute(new Runnable() {
@Override
public void run() {
handler.onConnect(msg.readOnlyClone());
}
});
}
@Override
public void notifyClientDisconnected(final String clientID) {
executor.execute(new Runnable() {
@Override
public void run() {
handler.onDisconnect(clientID);
}
});
}
@Override
public void notifyTopicPublished(final PublishMessage msg) {
executor.execute(new Runnable() {
@Override
public void run() {
handler.onPublish(msg.readOnlyClone());
}
});
}
@Override
public void notifyTopicSubscribed(final Subscription sub) {
executor.execute(new Runnable() {
@Override
public void run() {
handler.onSubscribe(sub.clone());
}
});
}
@Override
public void notifyTopicUnsubscribed(final Subscription sub) {
executor.execute(new Runnable() {
@Override
public void run() {
handler.onUnsubscribe(sub.clone());
}
});
}
}
@@ -25,6 +25,7 @@ import java.util.concurrent.Executors;
import io.netty.util.internal.StringUtil;
import org.apache.commons.codec.binary.StringUtils;
import org.eclipse.moquette.interception.Interceptor;
import org.eclipse.moquette.server.netty.NettyChannel;
import org.eclipse.moquette.spi.IMatchingCondition;
import org.eclipse.moquette.spi.IMessagesStore;
@@ -105,6 +106,8 @@ class ProtocolProcessor implements EventHandler<ValueEvent> {
private IMessagesStore m_messagesStore;
private ISessionsStore m_sessionsStore;
private IAuthenticator m_authenticator;
private Interceptor interceptor = Interceptor.NO_HANDLER_INTERCEPTOR;
//maps clientID to Will testament, if specified on CONNECT
private Map<String, WillMessage> m_willStore = new HashMap<>();
@@ -113,6 +116,13 @@ class ProtocolProcessor implements EventHandler<ValueEvent> {
ProtocolProcessor() {}
void init(SubscriptionsStore subscriptions, IMessagesStore storageService,
ISessionsStore sessionsStore,
IAuthenticator authenticator,
boolean allowAnonymous, IAuthorizator authorizator) {
init(subscriptions, storageService, sessionsStore, authenticator, allowAnonymous, authorizator, null);
}
/**
* @param subscriptions the subscription store where are stored all the existing
* clients subscriptions.
@@ -122,12 +132,16 @@ class ProtocolProcessor implements EventHandler<ValueEvent> {
* @param authenticator the authenticator used in connect messages.
* @param allowAnonymous true connection to clients without credentials.
* @param authorizator used to apply ACL policies to publishes and subscriptions.
* @param interceptor to notify events to an intercept handler
*/
void init(SubscriptionsStore subscriptions, IMessagesStore storageService,
ISessionsStore sessionsStore,
IAuthenticator authenticator,
boolean allowAnonymous, IAuthorizator authorizator) {
boolean allowAnonymous, IAuthorizator authorizator, Interceptor interceptor) {
//m_clientIDs = clientIDs;
if (interceptor != null) {
this.interceptor = interceptor;
}
this.subscriptions = subscriptions;
this.allowAnonymous = allowAnonymous;
m_authorizator = authorizator;
@@ -163,6 +177,7 @@ class ProtocolProcessor implements EventHandler<ValueEvent> {
ConnAckMessage okResp = new ConnAckMessage();
okResp.setReturnCode(ConnAckMessage.IDENTIFIER_REJECTED);
session.write(okResp);
interceptor.notifyClientConnected(msg);
return;
}
@@ -238,6 +253,7 @@ class ProtocolProcessor implements EventHandler<ValueEvent> {
okResp.setSessionPresent(true);
}
session.write(okResp);
interceptor.notifyClientConnected(msg);
LOG.info("Create persistent session for clientID <{}>", msg.getClientID());
m_sessionsStore.addNewSubscription(Subscription.createEmptySubscription(msg.getClientID(), true)); //null means EmptySubscription
@@ -301,6 +317,7 @@ class ProtocolProcessor implements EventHandler<ValueEvent> {
String user = (String) session.getAttribute(NettyChannel.ATTR_KEY_USERNAME);
if (m_authorizator.canWrite(topic, user, clientID)) {
executePublish(clientID, msg);
interceptor.notifyTopicPublished(msg);
} else {
LOG.debug("topic {} doesn't have write credentials", topic);
}
@@ -559,6 +576,7 @@ class ProtocolProcessor implements EventHandler<ValueEvent> {
m_willStore.remove(clientID);
LOG.info("DISCONNECT client <{}> with clean session {}", clientID, cleanSession);
interceptor.notifyClientDisconnected(clientID);
}
void processConnectionLost(LostConnectionEvent evt) {
@@ -19,6 +19,8 @@ import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import org.HdrHistogram.Histogram;
import org.eclipse.moquette.interception.InterceptHandler;
import org.eclipse.moquette.interception.Interceptor;
import org.eclipse.moquette.proto.messages.AbstractMessage;
import org.eclipse.moquette.spi.impl.security.*;
import org.eclipse.moquette.server.ServerChannel;
@@ -55,7 +57,7 @@ import static org.eclipse.moquette.commons.Constants.ACL_FILE_PROPERTY_NAME;
* Singleton class that orchestrate the execution of the protocol.
*
* Uses the LMAX Disruptor to serialize the incoming, requests, because it work in a evented fashion;
* the requests income from front Netty connectors and are dispatched to the
* the requests income from front Netty connectors and are dispatched to the
* ProtocolProcessor.
*
* @author andrea
@@ -63,9 +65,9 @@ import static org.eclipse.moquette.commons.Constants.ACL_FILE_PROPERTY_NAME;
public class SimpleMessaging implements IMessaging, EventHandler<ValueEvent> {
private static final Logger LOG = LoggerFactory.getLogger(SimpleMessaging.class);
private SubscriptionsStore subscriptions;
private RingBuffer<ValueEvent> m_ringBuffer;
private IMessagesStore m_storageService;
@@ -73,6 +75,7 @@ public class SimpleMessaging implements IMessaging, EventHandler<ValueEvent> {
private ExecutorService m_executor;
private Disruptor<ValueEvent> m_disruptor;
private Interceptor interceptor;
private static SimpleMessaging INSTANCE;
@@ -192,9 +195,16 @@ public class SimpleMessaging implements IMessaging, EventHandler<ValueEvent> {
m_storageService.initStore();
try {
final InterceptHandler handler = Class.forName(props.getProperty("intercept.handler"))
.asSubclass(InterceptHandler.class).newInstance();
interceptor = new BrokerInterceptor(handler);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NullPointerException | ClassCastException ignored) {
}
//List<Subscription> storedSubscriptions = m_sessionsStore.listAllSubscriptions();
//subscriptions.init(storedSubscriptions);
subscriptions.init(m_sessionsStore);
subscriptions.init(m_sessionsStore, interceptor);
String configPath = System.getProperty("moquette.path", null);
String authenticatorClassName = props.getProperty(Constants.AUTHENTICATOR_CLASS_NAME, "");
@@ -240,7 +250,7 @@ public class SimpleMessaging implements IMessaging, EventHandler<ValueEvent> {
}
boolean allowAnonymous = Boolean.parseBoolean(props.getProperty(ALLOW_ANONYMOUS_PROPERTY_NAME, "true"));
m_processor.init(subscriptions, m_storageService, m_sessionsStore, authenticator, allowAnonymous, authorizator);
m_processor.init(subscriptions, m_storageService, m_sessionsStore, authenticator, allowAnonymous, authorizator, interceptor);
}
private Object loadClass(String className, Class<?> cls) {
@@ -280,6 +290,9 @@ public class SimpleMessaging implements IMessaging, EventHandler<ValueEvent> {
private void processStop() {
LOG.debug("processStop invoked");
if (interceptor != null) {
((BrokerInterceptor) interceptor).stop();
}
m_storageService.close();
LOG.debug("subscription tree {}", subscriptions.dumpTree());
// m_eventProcessor.halt();
@@ -106,4 +106,13 @@ public class Subscription implements Serializable {
public String toString() {
return String.format("[filter:%s, cliID: %s, qos: %s, active: %s]", this.topicFilter, this.clientId, this.requestedQos, this.active);
}
@Override
public Subscription clone() {
try {
return (Subscription) super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
@@ -19,6 +19,7 @@ import java.text.ParseException;
import java.util.*;
import java.util.concurrent.LinkedBlockingDeque;
import org.eclipse.moquette.interception.Interceptor;
import org.eclipse.moquette.spi.ISessionsStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -30,6 +31,8 @@ import org.slf4j.LoggerFactory;
*/
public class SubscriptionsStore {
private Interceptor interceptor = Interceptor.NO_HANDLER_INTERCEPTOR;
/**
* Check if the topic filter of the subscription is well formed
* */
@@ -96,9 +99,20 @@ public class SubscriptionsStore {
/**
* Initialize the subscription tree with the list of subscriptions.
* Maintained for compatibility reasons.
*/
public void init(ISessionsStore sessionsStore) {
init(sessionsStore, null);
}
/**
* Initialize the subscription tree with the list of subscriptions and an interceptor.
*/
public void init(ISessionsStore sessionsStore, Interceptor interceptor) {
LOG.debug("init invoked");
if (interceptor != null) {
this.interceptor = interceptor;
}
m_sessionsStore = sessionsStore;
List<Subscription> subscriptions = sessionsStore.listAllSubscriptions();
//reload any subscriptions persisted
@@ -118,6 +132,7 @@ public class SubscriptionsStore {
protected void addDirect(Subscription newSubscription) {
TreeNode current = findMatchingNode(newSubscription.topicFilter);
current.addSubscription(newSubscription);
interceptor.notifyTopicSubscribed(newSubscription);
}
private TreeNode findMatchingNode(String topic) {
@@ -171,6 +186,7 @@ public class SubscriptionsStore {
if (toBeRemoved != null) {
matchNode.subscriptions().remove(toBeRemoved);
interceptor.notifyTopicUnsubscribed(toBeRemoved);
}
}
@@ -184,6 +200,7 @@ public class SubscriptionsStore {
List<Subscription> allSubscriptions = subsCollector.getResult();
for (Subscription subscription : allSubscriptions) {
removeSubscription(subscription.getTopicFilter(), subscription.getClientId());
interceptor.notifyTopicUnsubscribed(subscription);
}
}
@@ -191,9 +208,16 @@ public class SubscriptionsStore {
* Visit the topics tree to remove matching subscriptions with clientID
*/
public void removeForClient(String clientID) {
subscriptions.removeClientSubscriptions(clientID);
final List<Subscription> clientSubscriptions = new ArrayList<>();
subscriptions.removeClientSubscriptions(clientID, clientSubscriptions);
//persist the update
m_sessionsStore.wipeSubscriptions(clientID);
// Notify the unsubscribe event
for (Subscription sub : clientSubscriptions) {
interceptor.notifyTopicUnsubscribed(sub);
}
}
public void deactivate(String clientID) {
@@ -130,7 +130,7 @@ class TreeNode {
return res;
}
void removeClientSubscriptions(String clientID) {
void removeClientSubscriptions(String clientID, List<Subscription> collect) {
//collect what to delete and then delete to avoid ConcurrentModification
List<Subscription> subsToRemove = new ArrayList<>();
for (Subscription s : m_subscriptions) {
@@ -143,9 +143,11 @@ class TreeNode {
m_subscriptions.remove(s);
}
collect.addAll(subsToRemove);
//go deep
for (TreeNode child : m_children) {
child.removeClientSubscriptions(clientID);
child.removeClientSubscriptions(clientID, collect);
}
}
@@ -0,0 +1,116 @@
/*
* Copyright (c) 2012-2014 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 org.eclipse.moquette.spi.impl;
import org.eclipse.moquette.interception.InterceptHandler;
import org.eclipse.moquette.proto.messages.AbstractMessage;
import org.eclipse.moquette.proto.messages.ConnectMessage;
import org.eclipse.moquette.proto.messages.PublishMessage;
import org.eclipse.moquette.spi.impl.subscriptions.Subscription;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.*;
/**
* @author Wagner Macedo
*/
public class BrokerInterceptorTest {
// value to check for changes after every notification
private static final AtomicInteger n = new AtomicInteger(0);
// Interceptor loaded with a custom InterceptHandler special for the tests
private static final BrokerInterceptor interceptor = new BrokerInterceptor(new InterceptHandler() {
@Override
public void onConnect(ConnectMessage msg) {
n.set(40);
}
@Override
public void onDisconnect(String clientID) {
n.set(50);
}
@Override
public void onPublish(PublishMessage msg) {
n.set(60);
}
@Override
public void onSubscribe(Subscription sub) {
n.set(70);
}
@Override
public void onUnsubscribe(Subscription sub) {
n.set(80);
}
});
@BeforeClass
public static void beforeAllTests() {
// check if any of the handler methods was called before notifications
assertEquals(0, n.get());
}
@AfterClass
public static void afterAllTests() {
interceptor.stop();
}
/* Used to wait handler notification by the interceptor internal thread */
private static void interval() throws InterruptedException {
Thread.sleep(100);
}
@Test
public void testNotifyClientConnected() throws Exception {
interceptor.notifyClientConnected(new ConnectMessage());
interval();
assertEquals(40, n.get());
}
@Test
public void testNotifyClientDisconnected() throws Exception {
interceptor.notifyClientDisconnected("cli1234");
interval();
assertEquals(50, n.get());
}
@Test
public void testNotifyTopicPublished() throws Exception {
interceptor.notifyTopicPublished(new PublishMessage());
interval();
assertEquals(60, n.get());
}
@Test
public void testNotifyTopicSubscribed() throws Exception {
interceptor.notifyTopicSubscribed(new Subscription("cli1", "o2", AbstractMessage.QOSType.MOST_ONE, true));
interval();
assertEquals(70, n.get());
}
@Test
public void testNotifyTopicUnsubscribed() throws Exception {
interceptor.notifyTopicUnsubscribed(new Subscription("cli1", "o2", AbstractMessage.QOSType.MOST_ONE, true));
interval();
assertEquals(80, n.get());
}
}
@@ -164,4 +164,12 @@ public class ConnectMessage extends AbstractMessage {
}
return base;
}
public ConnectMessage readOnlyClone() {
try {
return (ConnectMessage) super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
@@ -54,4 +54,14 @@ public class PublishMessage extends MessageIDMessage {
public void setPayload(ByteBuffer payload) {
this.m_payload = payload;
}
public PublishMessage readOnlyClone() {
try {
final PublishMessage clone = (PublishMessage) super.clone();
clone.m_payload = this.m_payload.asReadOnlyBuffer();
return clone;
} catch (CloneNotSupportedException e) {
return null;
}
}
}