package eu.hgross.blaubot.messaging; import java.io.IOException; import java.util.Comparator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import eu.hgross.blaubot.core.BlaubotConstants; import eu.hgross.blaubot.core.IActionListener; import eu.hgross.blaubot.core.IBlaubotConnection; import eu.hgross.blaubot.util.Log; /** * The message sender simply queues messages that are going to be sent over the IBlaubotConnection * for which this message sender was created for. * * The sender can be activated/deactivated, meaning stopping and starting a queue consuming thread * that serializes and sends the queued messages (if any) over the given IBlaubotConnection. * * TODO: handle failing connections */ public class BlaubotMessageSender { private static final String LOG_TAG = "BlaubotMessageSender"; /** * Generator for chunk ids */ private final AtomicShort chunkIdGenerator; /** * If a sender is asked to send an already chunked message, the chunkId of the message is mapped * to one of its own ids to avoid clashes on the receiver side. */ private final ConcurrentHashMap<Short, Short> chunkIdMapping; /** * The message queue, prioritized by the priorityComparator */ private final PriorityBlockingQueue<BlaubotMessage> queuedMessages; /** * The connection over which the messages are send */ private final IBlaubotConnection blaubotConnection; /** * Thread which empties the queue and send the messages in it if possible. */ private volatile MessageSendingThread messageSendingThread; /** * Generates sequence numbers for messages added to the queue to ensure that messages which are * sent with the same priority arrive in the sending order. */ private AtomicInteger sequenceNumberGenerator; private long sentMessages = 0; private long sentPayloadBytes = 0; private volatile AtomicLong queuedBytes = new AtomicLong(0); /** * Synchronizing monitor for activation and deactivation. */ private Object activationLock = new Object(); /** * Comparator for the priority queue. * Comparing by the priority first and then the sequence number, if same priority */ private static final Comparator<BlaubotMessage> priorityComparator = new Comparator<BlaubotMessage>() { @Override public int compare(BlaubotMessage o1, BlaubotMessage o2) { final byte o1Val = o1.getPriority().value; final byte o2Val = o2.getPriority().value; if (o1Val < o2Val) { return -1; } else if (o1Val > o2Val) { return 1; } else { // equal priority, the smaller sequence number wins if (o1.sequenceNumber < o2.sequenceNumber) { return -1; } else if (o1.sequenceNumber > o2.sequenceNumber) { return 1; } else { return 0; } } } }; /** * Monitor to avoid two MessageSendingThreads to execute at the same time on this instance. * (could happen on fast activate/deactivate calls) */ private final Object senderMonitor = new Object(); public BlaubotMessageSender(IBlaubotConnection blaubotConnection) { this.sequenceNumberGenerator = new AtomicInteger(0); this.chunkIdGenerator = new AtomicShort((short) 0); this.blaubotConnection = blaubotConnection; this.queuedMessages = new PriorityBlockingQueue<>(50, priorityComparator); this.chunkIdMapping = new ConcurrentHashMap<>(); } /** * Queues the given message to be sent over the IBlaubotConnection this object was * created with. * * @param message the message to be send */ public void sendMessage(BlaubotMessage message) { // check if we need to chunk this message final boolean needsToBeChunked = message.getMessageType().containsPayload() && message.getPayload().length > BlaubotConstants.MAX_PAYLOAD_SIZE; if (needsToBeChunked) { if (message.getMessageType().isChunk()) { throw new IllegalStateException("Already chunked messages should never be chunked again!"); } final short chunkId = chunkIdGenerator.getAndIncrement(); List<BlaubotMessage> chunkMessages = message.createChunks(chunkId); for (BlaubotMessage chunkMessage : chunkMessages) { sendMessage(chunkMessage); } return; } if (message.getMessageType().isChunk()) { // if the mesage is already chunked (probably received by a relay connection's mediator and resend) // we want to map the chunk id to a safe number and set it on the message final short newChunkId = chunkIdGenerator.getAndIncrement(); Short ourChunkId = chunkIdMapping.putIfAbsent(message.getChunkId(), newChunkId); if (ourChunkId == null) { ourChunkId = newChunkId; } message.setChunkId(ourChunkId); } // apply a sequence number and add to queue message.sequenceNumber = sequenceNumberGenerator.incrementAndGet(); queuedMessages.add(message); queuedBytes.addAndGet(message.getPayload().length); } /** * Activates the message receiver (reading from the connection) */ public void activate() { MessageSendingThread mrt = new MessageSendingThread(); mrt.setName("msg-sender-" + blaubotConnection.getRemoteDevice().getUniqueDeviceID() + ", " + mrt.getId()); synchronized (activationLock) { messageSendingThread = mrt; } mrt.start(); } /** * Deactivates the message sender (completes current message readings, if any and then shuts down). * * @param actionListener callback to be informed when the sender was closed (thread finished), can be null */ public void deactivate(IActionListener actionListener) { final MessageSendingThread mst; synchronized (activationLock) { mst = messageSendingThread; messageSendingThread = null; if (mst != null) { mst.attachFinishListener(actionListener); if (!mst.isInterrupted()) { mst.interrupt(); } // the thread will call the listener on finish } else { if (actionListener != null) { actionListener.onFinished(); } } } } /** * @return sent bytes so far */ public long getSentPayloadBytes() { return sentPayloadBytes; } /** * sent messages * * @return sent messages so far */ public long getSentMessages() { return sentMessages; } class MessageSendingThread extends Thread { private static final long POLL_TIMEOUT = 1000; private static final long WAIT_TIME_ON_FAILED_SEND = 500; private static final String LOG_TAG = "MessageSendingThread"; private CopyOnWriteArrayList<IActionListener> finishedListeners; private boolean finished = false; private Object finishedMonitor = new Object(); public MessageSendingThread() { finishedListeners = new CopyOnWriteArrayList<>(); } /** * Attaches a listener that gets called, if the thread finished. * Meaning the run() method finished once. * If attached after it already finished, the listener is called * immediately. * * @param listener the listener */ public void attachFinishListener(IActionListener listener) { synchronized (finishedMonitor) { if (listener == null) { return; } this.finishedListeners.add(listener); if (finished) { listener.onFinished(); } } } @Override public void run() { synchronized (senderMonitor) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Started sender for connection " + blaubotConnection); } while (messageSendingThread == this && !isInterrupted()) { BlaubotMessage messageToSend; try { messageToSend = queuedMessages.poll(POLL_TIMEOUT, TimeUnit.MILLISECONDS); } catch (InterruptedException interruptedException) { break; } if (messageToSend == null) { continue; } try { if (Log.logDebugMessages()) { //Log.d(LOG_TAG, "Sending message: " + messageToSend); } final byte[] bytes = messageToSend.toBytes(); blaubotConnection.write(bytes); // maintain stats sentMessages += 1; sentPayloadBytes += bytes.length; queuedBytes.addAndGet(-messageToSend.getPayload().length); } catch (IOException e) { // back to queue on fail queuedMessages.add(messageToSend); try { // wait an amount of time to mitigate busy waits on failed connections Thread.sleep(WAIT_TIME_ON_FAILED_SEND); } catch (InterruptedException interruptedException) { break; } } // send message } synchronized (finishedMonitor) { finished = true; if (!finishedListeners.isEmpty()) { for (IActionListener finishedListener : finishedListeners) { finishedListener.onFinished(); } } } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Stopped sender for connection " + blaubotConnection); } } } } /** * The connection that is managed by this message sender. * * @return the managed connection */ protected IBlaubotConnection getBlaubotConnection() { return blaubotConnection; } /** * The current amount of messages in the queue * * @return current amount of messages in the queue */ protected int getQueueSize() { return queuedMessages.size(); } /** * The current number of bytes in this MessageSender's queue. * * @return number of bytes */ protected long getQueuedBytes() { return queuedBytes.get(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; BlaubotMessageSender that = (BlaubotMessageSender) o; if (blaubotConnection != null ? !blaubotConnection.equals(that.blaubotConnection) : that.blaubotConnection != null) return false; return true; } @Override public int hashCode() { return blaubotConnection != null ? blaubotConnection.hashCode() : 0; } private static class AtomicShort { short val; AtomicShort(short val) { this.val = val; } synchronized short getAndIncrement() { short prev = val; val += 1; return prev; } } @Override public String toString() { return "BlaubotMessageSender{" + "blaubotConnection=" + blaubotConnection + '}'; } }