package org.gameoss.gridcast.client; /* * #%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.message.AddTopicSubscription; import org.gameoss.gridcast.message.RemoveTopicSubscription; import org.gameoss.gridcast.message.TopicEnvelop; import org.gameoss.gridcast.p2p.ClusterClient; import org.gameoss.gridcast.p2p.discovery.NodeDiscovery; import org.gameoss.gridcast.p2p.listener.ClusterListener; import org.gameoss.gridcast.p2p.message.MessageRegistry; import org.gameoss.gridcast.p2p.node.NodeId; import org.gameoss.gridcast.subscriptions.CollectListener; import org.gameoss.gridcast.subscriptions.SubscriptionListener; import org.gameoss.gridcast.subscriptions.Subscriptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.concurrent.*; public class GridcastClient implements ClusterListener, SubscriptionListener { private static final Logger logger = LoggerFactory.getLogger(GridcastClient.class); private static final int USER_MESSAGE_ID_START = MessageRegistry.CUSTOM_START_ID + 128; private final ClusterClient cluster; private final NodeId localNodeId; private final ScheduledExecutorService scheduledExecutorService; private final Subscriptions subscriptions; private final ConcurrentHashMap<String,RefCntRunnable> subscriptionRefCnt; /** * Construct a Gridcast client. Recommended approach is to use {@link GridcastClientBuilder}. * * @param nodeDiscovery Node discovery provider. * @param nodePollingTimeInMs Time in milliseconds to poll for new nodes. * @param initialTopicCapacity The initial number of topics. * @param listenerExecutor Executor for running listener callbacks. */ public GridcastClient(String hostAddress, int hostPort, NodeDiscovery nodeDiscovery, long nodePollingTimeInMs, int initialTopicCapacity, Executor listenerExecutor) { // topic subscription table subscriptions = new Subscriptions(initialTopicCapacity, listenerExecutor); subscriptions.addGlobalListener(this); subscriptionRefCnt = new ConcurrentHashMap<>(initialTopicCapacity,0.75f,Runtime.getRuntime().availableProcessors()); // p2p cluster scheduledExecutorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors()); cluster = new ClusterClient(this, hostAddress, hostPort, nodeDiscovery, nodePollingTimeInMs, scheduledExecutorService); cluster.getMessageRegistry().addCustomMessage( 1, TopicEnvelop.class); cluster.getMessageRegistry().addCustomMessage( 2, AddTopicSubscription.class); cluster.getMessageRegistry().addCustomMessage( 3, RemoveTopicSubscription.class); localNodeId = cluster.getLocalNodeId(); } /** * Terminate client and release its resources. */ public void shutdown() { cluster.shutdown(); subscriptions.shutdown(); try { scheduledExecutorService.shutdownNow(); scheduledExecutorService.awaitTermination(1,TimeUnit.SECONDS); } catch (Exception ex) { logger.error("Unexpected exception shutting down scheduledExecutorService", ex); } } /** * Register a message for serialization. */ public void registerUserMessage(int id, Class<?> clazz) { cluster.getMessageRegistry().addCustomMessage(USER_MESSAGE_ID_START + id, clazz); } /** * Send message to all nodes subscribed to the topic. * * @param topic * @param message */ public void sendMessage(String topic, Object message) { List<NodeId> nodes = subscriptions.getSubscribers(topic); if (nodes == null || nodes.isEmpty()) { return; } // wrap message in a topic envelop and send to node list cluster.sendMessage(nodes, new TopicEnvelop(topic, localNodeId, message)); } /** * Send message to ONE randomly selected subscriber to the topic. * * @param topic * @param message */ public void sendMessageToRandom(String topic, Object message) { List<NodeId> nodes = subscriptions.getSubscribers(topic); if (nodes == null || nodes.isEmpty()) { return; } // send message to randomly selected subscribed node NodeId nid = nodes.get( ThreadLocalRandom.current().nextInt(nodes.size()) ); TopicEnvelop topicEnvelop = new TopicEnvelop(topic, localNodeId, message); cluster.sendMessage(nid, topicEnvelop); } /** * Subscribe to messages for a topic. Callback will occur on the specified executor * @param topic * @param executor */ public void subscribeToTopic(final String topic, TopicMessageListener listener, Executor executor) { // add local node as a subscriber for the topic if this is the first listener RefCntRunnable refCnt = subscriptionRefCnt.get(topic); if (refCnt == null) { RefCntRunnable tmp = new RefCntRunnable( new Runnable() { @Override public void run() { if (subscriptions != null) { subscriptions.addSubscription(topic, localNodeId); } } }, new Runnable() { @Override public void run() { if (subscriptions != null) { subscriptions.removeSubscription(topic, localNodeId); } } }); refCnt = subscriptionRefCnt.putIfAbsent(topic,tmp); if (refCnt == null) { subscriptions.addSubscription(topic, localNodeId); refCnt = tmp; } } refCnt.increment(); // add user data with the message listener subscriptions.addUserData(topic, new MessageSubscriberEntry(listener, executor)); } /** * Remove message listener for the topic. * @param topic * @param listener * @param executor */ public void unsubscribeToTopic(String topic, TopicMessageListener listener, Executor executor) { // remove the user data subscriptions.removeUserData(topic, new MessageSubscriberEntry(listener, executor)); // remove local node subscription for topic if this was the last listener subscriptionRefCnt.get(topic).decrement(); } /** * Add a fence to subscription table to know when previous operations * have finished. * @return */ public Future<Boolean> addFence() { return subscriptions.addFence(); } /////////////////////////////////////////////////////////////////////////// // Internals /////////////////////////////////////////////////////////////////////////// @Override public void onSubscribe(String topic, NodeId id) { if (localNodeId.equals(id)) { cluster.sendMessageToAll(new AddTopicSubscription(localNodeId,topic), false); } } @Override public void onUnsubscribe(String topic, NodeId id) { if (localNodeId.equals(id)) { cluster.sendMessageToAll(new RemoveTopicSubscription(localNodeId,topic), false); } } @Override public void onNodeJoin(ClusterClient cluster, NodeId nodeId) { // send local subscriptions to joining node if (nodeId != localNodeId) { final NodeId targetNodeId = nodeId; subscriptions.collectSubscriptionsForNode(localNodeId, new CollectListener() { @Override public void onCollectDone(NodeId nodeId, List<String> topics) { if (GridcastClient.this.cluster != null ) { GridcastClient.this.cluster.sendMessage(targetNodeId, new AddTopicSubscription(localNodeId, topics)); } } }); } } @Override public void onNodeLeft(ClusterClient cluster, NodeId nodeId) { subscriptions.removeAllSubscriptionsForNode(nodeId); } @Override public void onMessage(ClusterClient cluster, NodeId senderId, Object message) { if (message instanceof TopicEnvelop) { // external message to topic TopicEnvelop envelop = (TopicEnvelop) message; onExternalMessage(envelop.getTopic(), envelop.getSenderId(), envelop.getMessage()); } else if (message instanceof AddTopicSubscription) { // add subscription from remote node AddTopicSubscription msg = (AddTopicSubscription) message; for (String topic : msg.getTopics()) { subscriptions.addSubscription( topic, msg.getSenderId() ); } } else if (message instanceof RemoveTopicSubscription) { // remove subscription from remote node RemoveTopicSubscription msg = (RemoveTopicSubscription) message; for (String topic : msg.getTopics()) { subscriptions.removeSubscription(topic, msg.getSenderId()); } } } private void onExternalMessage(final String topic, final NodeId senderId, final Object message) { List<Object> msgSubs = subscriptions.getUserData(topic); if (msgSubs != null) { for (Object tmp : msgSubs) { final MessageSubscriberEntry mse = (MessageSubscriberEntry) tmp; mse.getExecutor().execute(new Runnable() { @Override public void run() { mse.getListener().onMessage(topic, senderId, message); } }); } } } private class MessageSubscriberEntry { private final TopicMessageListener listener; private final Executor executor; private MessageSubscriberEntry(TopicMessageListener listener, Executor executor) { this.listener = listener; this.executor = executor; } public TopicMessageListener getListener() { return listener; } public Executor getExecutor() { return executor; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MessageSubscriberEntry that = (MessageSubscriberEntry) o; if (!executor.equals(that.executor)) return false; if (!listener.equals(that.listener)) return false; return true; } @Override public int hashCode() { int result = listener.hashCode(); result = 31 * result + executor.hashCode(); return result; } } }