package org.gameoss.gridcast.subscriptions; /* * #%L * Gridcast * %% * Copyright (C) 2014 Charles Barry * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import org.gameoss.gridcast.p2p.node.NodeId; import reactor.core.Environment; import reactor.core.Reactor; import reactor.core.spec.Reactors; import reactor.event.Event; import reactor.event.dispatch.RingBufferDispatcher; import reactor.function.Consumer; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.*; import static reactor.event.selector.Selectors.T; /** * Multi-reader, single writer map of topic to node subscriptions. It assumes * a high number of readers and enforces a async single writer using Reactor's * RingBuffer. */ public class Subscriptions { private final Environment environment; private final Reactor reactor; private final ConcurrentHashMap<String,TopicInfo> subs; private final Executor listenerExecutor; private final List<SubscriptionListener> globalListeners = new CopyOnWriteArrayList<>(); public Subscriptions(int initialCapacity, Executor listenerExecutor) { this.listenerExecutor = listenerExecutor; // hash table holding subscriptions subs = new ConcurrentHashMap<>(initialCapacity,0.75f,Runtime.getRuntime().availableProcessors()); // ring buffer reactor for write operations environment = new Environment(); reactor = Reactors.reactor() .env( environment ) .dispatcher( new RingBufferDispatcher("subscriptions") ) .get(); // register handlers for table operations reactor.on( T(AddSubscriber.class), new Consumer<Event<AddSubscriber>>() { @Override public void accept(Event<AddSubscriber> event) { onAddSubscriber(event.getData()); } }); reactor.on( T(RemoveSubscriber.class), new Consumer<Event<RemoveSubscriber>>() { @Override public void accept(Event<RemoveSubscriber> event) { onRemoveSubscriber(event.getData()); } }); reactor.on( T(RemoveAllSubscriptions.class), new Consumer<Event<RemoveAllSubscriptions>>() { @Override public void accept(Event<RemoveAllSubscriptions> event) { onRemoveAllSubscriptions(event.getData()); } }); reactor.on( T(CollectAllSubscriptions.class), new Consumer<Event<CollectAllSubscriptions>>() { @Override public void accept(Event<CollectAllSubscriptions> event) { onCollectAllSubscriptions(event.getData()); } }); reactor.on( T(AddSubscriptionListener.class), new Consumer<Event<AddSubscriptionListener>>() { @Override public void accept(Event<AddSubscriptionListener> event) { onAddListener(event.getData()); } }); reactor.on( T(RemoveSubscriptionListener.class), new Consumer<Event<RemoveSubscriptionListener>>() { @Override public void accept(Event<RemoveSubscriptionListener> event) { onRemoveListener(event.getData()); } }); reactor.on( T(AddUserData.class), new Consumer<Event<AddUserData>>() { @Override public void accept(Event<AddUserData> event) { onAddUserData(event.getData()); } }); reactor.on( T(RemoveUserData.class), new Consumer<Event<RemoveUserData>>() { @Override public void accept(Event<RemoveUserData> event) { onRemoveUserData(event.getData()); } }); reactor.on( T(FutureTask.class), new Consumer<Event<FutureTask<?>>>() { @Override public void accept(Event<FutureTask<?>> event) { event.getData().run(); } }); } /** * Shutdown the reactor servicing the subscriptions table. */ public void shutdown() { environment.shutdown(); } /** * Add a global listener that gets called for all subscription changes. * The callback occurs on the ring buffer worker thread so the handler * MUST BE FAST AND NON-BLOCKING to prevent stalling writes to the * subscription table. * * @param listener * @return same listener that was passed into the function. */ public SubscriptionListener addGlobalListener(SubscriptionListener listener) { globalListeners.add(listener); return listener; } /** * Remove a global listener. * @param listener */ public void removeGlobalListener(SubscriptionListener listener) { globalListeners.remove(listener); } /** * Add a new subscriber node to a topic. * * @param topic * @param listenerId */ public void addSubscription(String topic, NodeId listenerId) { reactor.notify( AddSubscriber.class, Event.wrap(new AddSubscriber(topic, listenerId)) ); } /** * Remove a subscriber node from a topic. * * @param topic * @param listenerId */ public void removeSubscription(String topic, NodeId listenerId) { reactor.notify( RemoveSubscriber.class, Event.wrap(new RemoveSubscriber(topic,listenerId)) ); } /** * Remove all subscriptions for node. * * @param nodeId */ public void removeAllSubscriptionsForNode(NodeId nodeId) { reactor.notify( RemoveAllSubscriptions.class, Event.wrap(new RemoveAllSubscriptions(nodeId)) ); } /** * Get all topic subscriptions for node. * * @param nodeId * @param doneListener */ public void collectSubscriptionsForNode(NodeId nodeId, CollectListener doneListener) { reactor.notify( CollectAllSubscriptions.class, Event.wrap(new CollectAllSubscriptions(nodeId,doneListener))); } /** * Get the array of subscribers for a topic. * * @param topic * @return list of subscribers or null if none. */ public List<NodeId> getSubscribers(String topic) { TopicInfo info = subs.get(topic); if (info != null && !info.subscribers.isEmpty()) { return info.subscribers; } else { return null; } } /** * Add a new listener to topic subscriptions. * * @param topic * @param listener */ public SubscriptionListener addSubscriptionListener(String topic, SubscriptionListener listener) { reactor.notify( AddSubscriptionListener.class, Event.wrap(new AddSubscriptionListener(topic,listener)) ); return listener; } /** * Remove a listener to topic subscriptions. * * @param topic * @param listener */ public void removeSubscriptionListener(String topic, SubscriptionListener listener) { reactor.notify( RemoveSubscriptionListener.class, Event.wrap(new RemoveSubscriptionListener(topic,listener)) ); } /** * Associate a userData object with the topic. * * @param topic * @param obj * @return */ public Object addUserData(String topic, Object obj) { reactor.notify( AddUserData.class, Event.wrap(new AddUserData(topic,obj)) ); return obj; } /** * Remove association between the object and the topic. * * @param topic * @param obj */ public void removeUserData(String topic, Object obj) { reactor.notify( RemoveUserData.class, Event.wrap(new RemoveUserData(topic,obj))); } /** * Get list of user data associated with a topic. * @param topic * @return list of userData objects or null if none. */ public List<Object> getUserData(String topic) { TopicInfo info = subs.get(topic); if (info != null && !info.userData.isEmpty()) { return info.userData; } else { return null; } } /** * Inserts a fence into the worker queue and returns a future * the caller that is completed when then fence has been * processed by the writer thread. * * @return */ public Future<Boolean> addFence() { FutureTask<Boolean> futureTask = new FutureTask<>( new Callable<Boolean>() { @Override public Boolean call() throws Exception { return true; } }); reactor.notify( FutureTask.class, Event.wrap(futureTask) ); return futureTask; } /////////////////////////////////////////////////////////////////////////// // Internals /////////////////////////////////////////////////////////////////////////// /** * Message handler for adding a new subscriber * * @param msg */ private void onAddSubscriber(final AddSubscriber msg) { TopicInfo info = subs.get(msg.topic); // add subscriber boolean added; if (info == null) { info = new TopicInfo(msg.listenerId); subs.put(msg.topic, info); added = true; } else { added = info.subscribers.addIfAbsent(msg.listenerId); } // notify listeners if new serverId if (added) { info.notifyOnSubscribe(msg); } } /** * Message handler for removing a subscriber * @param msg */ private void onRemoveSubscriber(final RemoveSubscriber msg) { TopicInfo info = subs.get(msg.topic); // topic not found, early exit if (info == null) { return; } // remove server from subscribers and notify listeners if (info.subscribers.remove(msg.listenerId)) { info.notifyOnUnsubscribe(msg.topic,msg.listenerId); } // remove topic entry if empty if (info.isEmpty()) { subs.remove(msg.topic); } } /** * Message handler for removing all subscriptions from a node. * @param msg */ private void onRemoveAllSubscriptions(final RemoveAllSubscriptions msg) { for (Map.Entry<String,TopicInfo> kvp : subs.entrySet()) { TopicInfo info = kvp.getValue(); // remove server from subscribers and notify listeners if (info.subscribers.remove(msg.nodeId)) { info.notifyOnUnsubscribe(kvp.getKey(),msg.nodeId); } // remove topic entry if empty if (info.isEmpty()) { subs.remove(kvp.getKey()); } } } /** * Gather all the topics the node is subscribed. * @param msg */ private void onCollectAllSubscriptions(final CollectAllSubscriptions msg) { final List<String> topics = new LinkedList<>(); for (Map.Entry<String,TopicInfo> kvp : subs.entrySet()) { TopicInfo info = kvp.getValue(); if (info.subscribers.contains(msg.nodeId)) { topics.add(kvp.getKey()); } } listenerExecutor.execute(new Runnable() { @Override public void run() { msg.onDone.onCollectDone(msg.nodeId, topics); } }); } /** * Message handler for adding a subscriber * @param msg */ private void onAddListener(final AddSubscriptionListener msg) { TopicInfo info = subs.get(msg.topic); // add listener if (info == null) { subs.put(msg.topic, new TopicInfo(msg.listener)); } else { if (info.listeners.addIfAbsent(msg.listener)) { // call listener with existing subscribers for (NodeId id : info.subscribers) { final NodeId subId = id; listenerExecutor.execute(new Runnable() { @Override public void run() { msg.listener.onSubscribe(msg.topic,subId); } }); } } } } /** * Message handler for removing a subscriber * @param msg */ private void onRemoveListener(final RemoveSubscriptionListener msg) { TopicInfo info = subs.get(msg.topic); // topic not found, early exit if (info == null) { return; } // remove listener from topic info.listeners.remove(msg.listener); // remove topic if empty if (info.isEmpty()) { subs.remove(msg.topic); } } /** * Message handler for adding a user object to a topic * @param msg */ private void onAddUserData(final AddUserData msg) { TopicInfo info = subs.get(msg.topic); // add listener if (info == null) { subs.put(msg.topic, new TopicInfo(msg.userData)); } else { info.userData.addIfAbsent(msg.userData); } } /** * Message handler to remove a user object from a topic * @param msg */ private void onRemoveUserData(final RemoveUserData msg) { TopicInfo info = subs.get(msg.topic); // topic not found, early exit if (info == null) { return; } // remove listener from topic info.userData.remove(msg.userData); // remove topic if empty if (info.isEmpty()) { subs.remove(msg.topic); } } /** * Base message representing subscriber events */ private static class SubscriberEvent { public final String topic; public final NodeId listenerId; public SubscriberEvent(String topic, NodeId listenerId) { this.topic = topic; this.listenerId = listenerId; } } /** * Message to add a subscriber */ private static class AddSubscriber extends SubscriberEvent { public AddSubscriber(String topic, NodeId listenerId) { super(topic, listenerId); } } /** * Message to remove a subscriber */ private static class RemoveSubscriber extends SubscriberEvent { public RemoveSubscriber(String topic, NodeId listenerId) { super(topic,listenerId); } } /** * Message to remove all subscriptions from node. */ private static class RemoveAllSubscriptions { private final NodeId nodeId; public RemoveAllSubscriptions(NodeId listenerId) { nodeId = listenerId; } } /** * Message to collect all subscriptions for a node. */ private static class CollectAllSubscriptions { private final NodeId nodeId; private final CollectListener onDone; public CollectAllSubscriptions(NodeId nodeId, CollectListener onDone) { this.nodeId = nodeId; this.onDone = onDone; } } /** * Internal message to remove a listener */ private static class AddSubscriptionListener { public final String topic; public final SubscriptionListener listener; public AddSubscriptionListener(String topic, SubscriptionListener listener) { this.topic = topic; this.listener = listener; } } /** * Internal message to remove a listener */ private static class RemoveSubscriptionListener { public final String topic; public final SubscriptionListener listener; public RemoveSubscriptionListener(String topic, SubscriptionListener listener) { this.topic = topic; this.listener = listener; } } /** * Internal message to add a user data object */ private static class AddUserData { public final String topic; public final Object userData; private AddUserData(String topic, Object userData) { this.topic = topic; this.userData = userData; } } /** * Internal message to remove a user data object */ private static class RemoveUserData { public final String topic; public final Object userData; private RemoveUserData(String topic, Object userData) { this.topic = topic; this.userData = userData; } } /** * Internal wrapper for topic subscriptions */ private class TopicInfo { public final CopyOnWriteArrayList<NodeId> subscribers = new CopyOnWriteArrayList<>(); public final CopyOnWriteArrayList<SubscriptionListener> listeners = new CopyOnWriteArrayList<>(); public final CopyOnWriteArrayList<Object> userData = new CopyOnWriteArrayList<>(); public TopicInfo(NodeId listenerId) { subscribers.add(listenerId); } public TopicInfo(SubscriptionListener listener) { listeners.add(listener); } public TopicInfo(Object data) { userData.add(data); } public boolean isEmpty() { return subscribers.isEmpty() && listeners.isEmpty() && userData.isEmpty(); } public void notifyOnSubscribe(final AddSubscriber msg) { // Global listeners are called on the ring buffer thread to reduce // overhead. It does mean that the callback needs to be quick. for (SubscriptionListener l : globalListeners) { l.onSubscribe(msg.topic, msg.listenerId); } // Topic listeners are called on the provided executor for (SubscriptionListener l : listeners) { final SubscriptionListener listener = l; listenerExecutor.execute(new Runnable() { @Override public void run() { listener.onSubscribe(msg.topic, msg.listenerId); } }); } } public void notifyOnUnsubscribe(final String topic, final NodeId listenerId) { // Global listeners are called on the ring buffer thread to reduce // overhead. It does mean that the callback needs to be quick. for (SubscriptionListener l : globalListeners) { l.onUnsubscribe(topic, listenerId); } // Topic listeners are called on the provided executor for (SubscriptionListener l : listeners) { final SubscriptionListener listener = l; listenerExecutor.execute(new Runnable() { @Override public void run() { listener.onUnsubscribe(topic, listenerId); } }); } } } }