package eu.hgross.blaubot.messaging; import java.util.Observable; import java.util.Observer; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import eu.hgross.blaubot.admin.AddSubscriptionAdminMessage; import eu.hgross.blaubot.admin.RemoveSubscriptionAdminMessage; import eu.hgross.blaubot.util.Log; /** * A channel managed by the BlaubotChannelManager. * * Subscriptions to a channel can be made via subscribe() and removed by unsubscribe(). * To listen to messages on this channel, attach a listener via {BlaubotChannel#addMessageListener}. * Listeners can be removed via {BlaubotChannel#removeMessageListener}. * * Subscriptions are sent immediately to the network, meaning, that if there is no network, * no subscription is made. * The recommended way to subscribe to channels is to do this by a {ILifecycleListener#onConnected}. * * Messages send via {BlaubotChannel#publish} are added to a bounded queue, which is processed due to * a defined message picking strategy (@see {IBlaubotMessagePickerStrategy}). * The processing is activated/deactivated by the activate/deactivate methods. * If activated, a processing thread uses the specified picker strategy to get messages from the * queue and hands this messages to the BlaubotChannelManager. * * To influence the MessagePicking and message rates, @see {BlaubotChannel#getChannelConfig}. * The picking and rates can be changed at runtime. */ public class BlaubotChannel implements IBlaubotChannel { private static final String LOG_TAG = "BlaubotChannel"; /** * Some configurations like "do not transmit reflexive messages", "do not transmit if no subscribers", * internal pause modes, ... leads to skipping the message processing queue. * To avoid infinite loop like load in these cases, we throw in some sleep time in between. * This specific sleep time and threshold is defined by this constant. */ private static final int LOAD_AVOIDANCE_SLEEPTIME = 50; /** * The channel config used for this channel. * Defines the picking strategy and channel id. */ private final BlaubotChannelConfig channelConfig; /** * The channel manager that created this instance (BlaubotChannelManager#createOrGetChannel} */ private final BlaubotChannelManager channelManager; /** * Set of UniqueDeviceIds that subscribed to this channel */ private final ConcurrentSkipListSet<String> subscriptions; /** * Attached listeners to this channel. */ private final CopyOnWriteArrayList<IBlaubotMessageListener> messageListeners; /** * Listeners which get called, if subscriptions are modified. */ private CopyOnWriteArrayList<IBlaubotSubscriptionChangeListener> subscriptionChangeListeners; /** * The bounded queue where all messages go to on {BlaubotChannel#publish} calls. * See the queueProcessor doc. */ private BlockingQueue<BlaubotMessage> messageQueue; /** * A boolean that is maintained through creation and removal of subscription and indicates, if * our own device is subscribed to this very channel. * Reason: we don't have to look up if that is the case for each message we send, if the transmit * reflexive messages option is set to false */ private volatile boolean ownDeviceIsSubscribed = false; /** * ExecutorService used to notify listeners about messages if the transmitReflexiveMessages option * is set to false to not use the same thread for notifications and to send messages. */ private ExecutorService notificationExecutorService; private long sentMessages = 0; private long sentBytes = 0; private long receivedMessages = 0; private long receivedBytes = 0; /** * If set, no transmission is made whatsoever. Meaning regardless if activated or deactivated, * the picking will be skipped as long as this boolean is false. */ private AtomicBoolean doNotTransmit = new AtomicBoolean(false); /** * The queueProcessor is a Runnable, that uses the channel's config to retrieve * the message picker strategy to empty the channel's message queue. * It picks messages and hands them to th channel manager. */ private final Runnable queueProcessor = new Runnable() { @Override public void run() { try { // suicide if no connections if (!channelManager.hasConnections()) { if (Log.logWarningMessages()) { Log.w(LOG_TAG, "The ChannelManager has no connections but the channel is activated. Not picking and will deactivate the channel. "); } new Thread(new Runnable() { @Override public void run() { deactivate(); } }).start(); return; } // check if we are allowed to pick if (doNotTransmit.get()) { // we are not allowed to send, sleep a while if we have a low message rate and come back later if (channelConfig.getMinMessageRateDelay() < LOAD_AVOIDANCE_SLEEPTIME) { try { Thread.sleep(LOAD_AVOIDANCE_SLEEPTIME); } catch (InterruptedException e) { return; } } return; } /** * True, iff we are the only subscriber */ boolean weAreOnlySubscriber = false; // check if there are subscribers and do nothing, if not told otherwise if (!channelConfig.isTransmitIfNoSubscribers()) { final int subscribers = subscriptions.size(); if (subscribers == 0) { // we don't send anything, no subscribers at all if (channelConfig.getMinMessageRateDelay() < LOAD_AVOIDANCE_SLEEPTIME) { try { // sleep some time to avoid useless load generation. Thread.sleep(LOAD_AVOIDANCE_SLEEPTIME); return; } catch (InterruptedException e) { return; } } } else if (subscribers == 1 && ownDeviceIsSubscribed) { weAreOnlySubscriber = true; } } final IBlaubotMessagePickerStrategy picker = channelConfig.getMessagePicker(); final BlaubotMessage blaubotMessage = picker.pickNextMessage(messageQueue); if (blaubotMessage != null) { final boolean transmitReflexiveMessages = channelConfig.isTransmitReflexiveMessages(); boolean excludeSenderFlagWasSet = blaubotMessage.getMessageType().isSenderExcluded(); if (!transmitReflexiveMessages) { // we don't want to get this mesage from the master // we have to make sure to set the exclude flag on the message blaubotMessage.getMessageType().setExcludeSender(true); } final boolean publishToConnections = !(weAreOnlySubscriber && !transmitReflexiveMessages); boolean wasNotSendToAnyConnection = true; // only publish to master, if needed (respect transmitReflexiveMssages option) if (publishToConnections) { final int connectionCount = channelManager.publishChannelMessage(blaubotMessage); wasNotSendToAnyConnection = connectionCount <= 0; if (wasNotSendToAnyConnection) { if (Log.logWarningMessages()) { Log.w(LOG_TAG, "A picked message was not committed to any MessageSender."); } } } // messages to our own device shall not be received through the master device but // have to be dispatched by the channel directly to save network traffic (1 hop, back from the mater to us) final boolean notifyLocalListeners = !transmitReflexiveMessages && ownDeviceIsSubscribed; if (notifyLocalListeners) { // -- notify in new thread (to not mix up send and notification threads) // we will not receive it again from the master device because the excludeSender flag will be set on the message, // if isTransmitReflexiveMessages is false (see above). // we finally check the prior state of the flag to know, if we have to dispatch it locally if (!excludeSenderFlagWasSet) { notificationExecutorService.execute(new Runnable() { @Override public void run() { BlaubotChannel.this.notify(blaubotMessage); } }); } } if (!wasNotSendToAnyConnection || notifyLocalListeners) { sentBytes += blaubotMessage.getPayload().length; sentMessages += 1; } } } catch (Exception e) { e.printStackTrace(); throw e; } } }; /** * A runnable that just loops over the queue looper over and over again by adding it to the executor * after one run again */ private final Runnable queueLooperTask = new Runnable() { @Override public void run() { queueProcessor.run(); final ExecutorService service = BlaubotChannel.this.queueProcessorExecutorService; if (service != null) { try { service.execute(queueLooperTask); } catch (RejectedExecutionException e) { // executor is shutting dow } } // else: we are done (probably deactivated) } }; /** * The ExecutorService that is used to run the queueProcessor. * It is created/shut down by the activate/deactivate methods. */ private volatile ExecutorService queueProcessorExecutorService; /** * The max time for the queueProcessorExecutorService to shut down on deactivate() */ private static final long TERMINATION_TIMEOUT = 5000; /** * Locks access to the queueProcessorExecutorService variable. */ private final Object activateDeactivateMonitor = new Object(); /** * @param channelId the channel id * @param channelManager the channelManager instance, that created this channel */ protected BlaubotChannel(short channelId, BlaubotChannelManager channelManager) { this.subscriptions = new ConcurrentSkipListSet<>(); this.subscriptionChangeListeners = new CopyOnWriteArrayList<>(); this.messageListeners = new CopyOnWriteArrayList<>(); this.channelManager = channelManager; this.channelConfig = new BlaubotChannelConfig(channelId); this.channelConfig.addObserver(channelConfigObserver); this.notificationExecutorService = Executors.newCachedThreadPool(); this.setUpMessageQueue(); } /** * Creates the message queue. * If the message queue is not null, a new one is created and the messages of the old queue * are appended to the new one. */ private void setUpMessageQueue() { final ArrayBlockingQueue<BlaubotMessage> newMessageQueue = new ArrayBlockingQueue<>(channelConfig.getQueueCapacity()); int sizeBefore = 0; boolean allTransferred = true; if (this.messageQueue != null) { sizeBefore = messageQueue.size(); try { // drain the old queue to the new one this.messageQueue.drainTo(newMessageQueue); } catch (IllegalStateException e) { // -- the new queue size is to small for the messages allTransferred = false; } } this.messageQueue = newMessageQueue; final int sizeAfter = messageQueue.size(); if (sizeAfter != sizeBefore) { allTransferred = false; } if (!allTransferred) { // log error and move on if (Log.logErrorMessages()) { Log.e(LOG_TAG, "Could not add all of the previous messages to the queue (new queue size was smaller than the amount of messages in the queue). Dropped all messages exceeding the capacity."); } } } /** * Listens to changes of the channel config at runtime and restarts * the channel if needed. */ private Observer channelConfigObserver = new Observer() { @Override public void update(Observable o, Object arg) { if (o == channelConfig) { if (arg instanceof Boolean && ((Boolean) arg).booleanValue()) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "BlaubotChannelConfig changed and restart of channel needed. Restarting BlaubotChannel ..."); } // restart channel, if the config notified that this is necessary, which is // told us by the second arg - yeah we could introduce a new listener, but why. restart(); } } } }; /** * Deactivates, then activates the channel to reflect changes made to the channel config. * (Executor restart) */ private synchronized void restart() { boolean wasActiveBefore = deactivate(); if (wasActiveBefore) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "BlaubotChannel #" + channelConfig.getChannelId() + " was activated before the restart, re-activating ..."); } activate(); } else { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "BlaubotChannel #" + channelConfig.getChannelId() + " was not activated before the restart. Not activating the channel."); } } } @Override public boolean publish(BlaubotMessage blaubotMessage) { return publish(blaubotMessage, false); } @Override public boolean publish(BlaubotMessage blaubotMessage, boolean excludeSender) { setUpChannelMessage(blaubotMessage, excludeSender); final boolean addedToQueue = messageQueue.offer(blaubotMessage); return addedToQueue; } @Override public boolean publish(BlaubotMessage blaubotMessage, long timeout) { return publish(blaubotMessage, timeout, false); } @Override public boolean publish(BlaubotMessage blaubotMessage, long timeout, boolean excludeSender) { setUpChannelMessage(blaubotMessage, excludeSender); try { return messageQueue.offer(blaubotMessage, timeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { if (Log.logWarningMessages()) { Log.w(LOG_TAG, "Got interrupted trying to offer a message to the queue. Message was not added: " + blaubotMessage); } return false; } } @Override public boolean publish(byte[] payload) { return publish(payload, false); } @Override public boolean publish(byte[] payload, boolean excludeSender) { BlaubotMessage msg = new BlaubotMessage(); msg.setPayload(payload); return publish(msg, excludeSender); } @Override public boolean publish(byte[] payload, long timeout) { return publish(payload, timeout, false); } @Override public boolean publish(byte[] payload, long timeout, boolean excludeSender) { BlaubotMessage msg = new BlaubotMessage(); msg.setPayload(payload); return publish(msg, timeout, excludeSender); } /** * Takes a blaubot message and modifies the header according to this channel * * @param blaubotMessage the message to be published through this channel * @param excludeSender iff true, the message is not dispatched to the sender's connection */ private void setUpChannelMessage(BlaubotMessage blaubotMessage, boolean excludeSender) { blaubotMessage.setChannelId(this.channelConfig.getChannelId()); blaubotMessage.getMessageType().setIsFirstHop(true); blaubotMessage.setPriority(channelConfig.getPriority()); blaubotMessage.getMessageType().setExcludeSender(excludeSender); } @Override public void subscribe() { final String ownUniqueDeviceId = channelManager.getOwnUniqueDeviceId(); final int involvedSenders = sendAddSubscription(ownUniqueDeviceId); // always add it locally addSubscription(ownUniqueDeviceId); } @Override public void subscribe(IBlaubotMessageListener blaubotMessageListener) { messageListeners.add(blaubotMessageListener); subscribe(); } @Override public void unsubscribe() { final String ownUniqueDeviceId = channelManager.getOwnUniqueDeviceId(); final int involvedSenders = sendRemoveSubscription(ownUniqueDeviceId); if (involvedSenders <= 0) { // we are not connected, just remove removeSubscription(ownUniqueDeviceId); } } @Override public void addMessageListener(IBlaubotMessageListener messageListener) { messageListeners.add(messageListener); } @Override public void removeMessageListener(IBlaubotMessageListener messageListener) { final String ownUniqueDeviceId = channelManager.getOwnUniqueDeviceId(); messageListeners.remove(messageListener); if (messageListeners.isEmpty()) { unsubscribe(); } } /** * Sends the AddSubscriptionMessage to the master. * The Subscription itself is added, when the master sends the message back and addSubscription is called * by the ChannelManager * * @param uniqueDeviceID the unique * @return number of MessageManagers to which the message was committed */ protected int sendAddSubscription(String uniqueDeviceID) { AddSubscriptionAdminMessage msg = new AddSubscriptionAdminMessage(uniqueDeviceID, channelConfig.getChannelId()); return channelManager.broadcastAdminMessage(msg.toBlaubotMessage()); } /** * Sends the RemoveSubscriptionMessage to the master. * The subscription itsef is removed, when the master sends the message back and removeSubscription gets * called by the ChannelManager. * * @param uniqueDeviceId the unique device id * @return number of MessageManagers to which the message was committed */ protected int sendRemoveSubscription(String uniqueDeviceId) { RemoveSubscriptionAdminMessage msg = new RemoveSubscriptionAdminMessage(uniqueDeviceId, channelConfig.getChannelId()); return channelManager.broadcastAdminMessage(msg.toBlaubotMessage()); } /** * Adds a subscription to uniqueDeviceId to this channel. * The operation is idempotent. * * @param uniqueDeviceID the unique device id of the device that will be a new subscriber */ protected void addSubscription(String uniqueDeviceID) { synchronized (channelManager.subscriptionLock) { subscriptions.add(uniqueDeviceID); if (uniqueDeviceID.equals(channelManager.getOwnUniqueDeviceId())) { ownDeviceIsSubscribed = true; } } notifySubscriptionAdded(uniqueDeviceID, channelConfig.getChannelId()); } /** * Removes the subscription of uniqueDeviceId to this channel * The operation is idempotent. * * @param uniqueDeviceId the unique device id of the device that will be unsubscribed */ protected void removeSubscription(String uniqueDeviceId) { synchronized (channelManager.subscriptionLock) { subscriptions.remove(uniqueDeviceId); if (uniqueDeviceId.equals(channelManager.getOwnUniqueDeviceId())) { ownDeviceIsSubscribed = false; } } notifySubscriptionRemoved(uniqueDeviceId, channelConfig.getChannelId()); } protected ConcurrentSkipListSet<String> getSubscriptions() { return subscriptions; } /** * Notifies this channel about a new message. * Gets called from the outside (BlaubotChannelManager.messageDispatcher). * * @param message the message posted to this channel */ protected void notify(BlaubotMessage message) { receivedBytes += message.getPayload().length; receivedMessages += 1; for (IBlaubotMessageListener listener : messageListeners) { listener.onMessage(message); } } /** * The channel config specifying the message picking strategy and message rates as well * as the id. * The config's values can be changed at runtime. * Changes are only reflected, if the channel is activated, meaning the net ChannelManager is activated * or respectively we are connected to a blaubot network. * * @return the channel config */ public BlaubotChannelConfig getChannelConfig() { return channelConfig; } @Override public void clearMessageQueue() { messageQueue.clear(); } /** * Adds a subscription listener to the manager * * @param subscriptionChangeListener the listener to add */ public void addSubscriptionListener(IBlaubotSubscriptionChangeListener subscriptionChangeListener) { subscriptionChangeListeners.add(subscriptionChangeListener); } /** * Removes a subscription listener from the manager * * @param subscriptionChangeListener the listener to remove */ public void removeSubscriptionListener(IBlaubotSubscriptionChangeListener subscriptionChangeListener) { subscriptionChangeListeners.remove(subscriptionChangeListener); } /** * Notifies the attached listeners that a subscription was added. * * @param uniqueDeviceId the subscribing uniquedeviceid * @param channelId the channel id */ private void notifySubscriptionAdded(String uniqueDeviceId, short channelId) { for (IBlaubotSubscriptionChangeListener listener : subscriptionChangeListeners) { listener.onSubscriptionAdded(uniqueDeviceId, channelId); } } /** * Notifies the attached listeners that a subscription was removed. * * @param uniqueDeviceId the formerly subscribing uniquedeviceid * @param channelId the channel id */ private void notifySubscriptionRemoved(String uniqueDeviceId, short channelId) { for (IBlaubotSubscriptionChangeListener listener : subscriptionChangeListeners) { listener.onSubscriptionRemoved(uniqueDeviceId, channelId); } } /** * Activates the channel and therefore the message picking */ protected void activate() { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Activating BlaubotChannel #" + channelConfig.getChannelId() + " ..."); } synchronized (activateDeactivateMonitor) { if (queueProcessorExecutorService != null) { if (Log.logWarningMessages()) { // TODO actually not a warning and might happen -> debug when evaluated Log.w(LOG_TAG, "activate() called but channel was already activated. Doing nothing!"); } return; } // check if we have to adjust the queue size if (messageQueue.size() != channelConfig.getQueueCapacity()) { setUpMessageQueue(); } // TODO: there is a 1 ms delay for the pick all strategy but we want minMessageRateDelay = 0 to be possible. We just have to use a while loop instead of the fixed delay scheduler here final int minMessageRateDelay = channelConfig.getMinMessageRateDelay(); if (minMessageRateDelay <= 0) { queueProcessorExecutorService = Executors.newSingleThreadExecutor(); queueProcessorExecutorService.submit(queueLooperTask); } else { // -- minMessageRateDelay > 0 queueProcessorExecutorService = Executors.newSingleThreadScheduledExecutor(); ((ScheduledExecutorService) queueProcessorExecutorService).scheduleWithFixedDelay(queueProcessor, 0, minMessageRateDelay, TimeUnit.MILLISECONDS); } } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "BlaubotChannel #" + channelConfig.getChannelId() + " activated."); } } /** * Deactivates the channel and therefore the message picking. * Blocks until the channel has shut down! * * @return true, iff the channel was activated before */ protected boolean deactivate() { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Deactivating BlaubotChannel #" + channelConfig.getChannelId() + " ..."); } boolean wasActivated = false; synchronized (activateDeactivateMonitor) { if (queueProcessorExecutorService != null) { queueProcessorExecutorService.shutdownNow(); try { final boolean timedOut = !queueProcessorExecutorService.awaitTermination(TERMINATION_TIMEOUT, TimeUnit.MILLISECONDS); if (timedOut) { throw new RuntimeException("Could not stop channel"); } } catch (InterruptedException e) { // ignore } wasActivated = true; } queueProcessorExecutorService = null; } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "BlaubotChannel #" + channelConfig.getChannelId() + " deactivated."); } return wasActivated; } /** * @return true, iff active (= executor started) */ protected boolean isActive() { return queueProcessorExecutorService != null; } /** * The queue capacity * * @return capacity of the queue */ protected int getQueueCapacity() { return channelConfig.getQueueCapacity(); } /** * The current amount of messages in the queue * * @return current amount of messages in the queue */ protected int getQueueSize() { return messageQueue.size(); } /** * The amount of bytes sent through this channel so far. * * @return number of bytes */ public long getSentBytes() { return sentBytes; } /** * The message count sent through this channel so far. * * @return number of messages */ public long getSentMessages() { return sentMessages; } /** * The message count received by this channel so far. * * @return number of messages */ public long getReceivedMessages() { return receivedMessages; } /** * The amount of bytes received by this channel so far. * * @return number of bytes */ public long getReceivedBytes() { return receivedBytes; } /** * Allows or disallows transmission of messages by this channel. * Is used to block picking as long as an initial subscription handshake is pending. * * @param doNotTransmit if true, no messages will be picked as long as this state is set */ protected void setDoNotTransmit(boolean doNotTransmit) { this.doNotTransmit.set(doNotTransmit); } }