org.geppetto.frontend.messaging.DefaultMessageSender.java Source code

Java tutorial

Introduction

Here is the source code for org.geppetto.frontend.messaging.DefaultMessageSender.java

Source

/*******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2011 - 2015 OpenWorm.
 * http://openworm.org
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the MIT License
 * which accompanies this distribution, and is available at
 * http://opensource.org/licenses/MIT
 *
 * Contributors:
 *        OpenWorm - http://openworm.org/people.html
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
 * USE OR OTHER DEALINGS IN THE SOFTWARE.
 *******************************************************************************/
package org.geppetto.frontend.messaging;

import com.google.gson.Gson;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.catalina.websocket.WsOutbound;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geppetto.frontend.messages.GeppettoTransportMessage;
import org.geppetto.frontend.messages.OutboundMessages;
import org.geppetto.frontend.messages.TransportMessageFactory;

/**
 * <code>DefaultMessageSender</code> handles transmission of messages to a client via WebSockets.
 *
 * Messages are first processed and compressed in a separate worker thread. Processed messages are then handed off to another (separate) worker thread that delivers the messages via the WebSocket.
 *
 * Each worker thread is backed by a blocking queue. One queue and thread provide messaging processing. The other queue and thread provide message transmission via the WebSocket.
 *
 * Queuing (and worker threads) and compression can be disabled. Without queuing DefaultMessageSender does not use worker threads and instead executes all tasks in the calling thread.
 *
 * Only message types specified in the queuedMessageTypes configuration are placed in the queues. All other message types are processed and transmitted in the calling thread.
 *
 * Queued message processing and transmission can be paused and resumed. When paused, the worker threads simply remove tasks from the queue and throw them away.
 *
 * Compression is done with gzip. The configuration parameter, <code>minMessageLengthForCompression</code> specifies the minimum message size for compression. Messages smaller than this size are not
 * compressed.
 *
 * {@link org.geppetto.frontend.controllers.WebsocketConnection} loads the configuration via Spring from <code>app-config.xml</code>.
 */
public class DefaultMessageSender implements MessageSender {

    /**
     * If true then use worker threads for processing and transmission. If false then do everything on calling thread.
     */
    private boolean queuingEnabled = false;

    /**
     * The maximum size of a processing or transmission queue. If the queue is full and <code>discardMessagesIfQueueFull</code> is true then the oldest item is removed from the queue to make space for
     * the new item. If <code>discardMessagesIfQueueFull</code> is false then the calling thread runs the task itself.
     */
    private int maxQueueSize = 5;

    /**
     * If true and a queue is full then discard the oldest task to make room for the new task. If false then run the task in the calling thread.
     */
    private boolean discardMessagesIfQueueFull = true;

    /**
     * If true then compress messages.
     */
    private boolean compressionEnabled = false;

    /**
     * The minimum message size for compression. Messages smaller than this size are not compressed.
     */
    private int minMessageLengthForCompression = 20000;

    /**
     * Message types that should be queued - and thus handled across multiple threads. All other message types are handled on the calling thread.
     */
    private Set<OutboundMessages> queuedMessageTypes;

    private ArrayBlockingQueue<Runnable> preprocessorQueue;
    private PausableThreadPoolExecutor preprocessorExecutor;
    private ArrayBlockingQueue<Runnable> senderQueue;
    private PausableThreadPoolExecutor senderExecutor;
    private WsOutbound wsOutbound;
    private Set<MessageSenderListener> listeners = new HashSet<>();

    private static final Log logger = LogFactory.getLog(DefaultMessageSender.class);

    public DefaultMessageSender() {
    }

    public void initialize(WsOutbound wsOutbound) {

        logger.info(String.format(
                "Initializing message sender - queuing: %b, compression: %b, "
                        + "discard messages if queues full: %b",
                queuingEnabled, compressionEnabled, discardMessagesIfQueueFull));

        this.wsOutbound = wsOutbound;

        if (queuingEnabled) {

            RejectedExecutionHandler rejectedExecutionHandler;

            if (discardMessagesIfQueueFull) {
                rejectedExecutionHandler = new ThreadPoolExecutor.DiscardOldestPolicy();
            } else {
                rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();
            }

            preprocessorQueue = new ArrayBlockingQueue<>(maxQueueSize);

            preprocessorExecutor = new PausableThreadPoolExecutor(1, 1, 30, TimeUnit.SECONDS, preprocessorQueue,
                    rejectedExecutionHandler);

            preprocessorExecutor.prestartAllCoreThreads();

            senderQueue = new ArrayBlockingQueue<>(maxQueueSize);

            senderExecutor = new PausableThreadPoolExecutor(1, 1, 30, TimeUnit.SECONDS, senderQueue,
                    rejectedExecutionHandler);
            senderExecutor.prestartAllCoreThreads();
        }
    }

    @Override
    public void shutdown() {
        logger.debug("Shutting down message sender");
        listeners = new HashSet<>();
        if (preprocessorExecutor != null) {
            preprocessorExecutor.shutdownNow();
        }
        if (senderExecutor != null) {
            senderExecutor.shutdownNow();
        }
    }

    /**
     * Pause queued message transmission. This is a simple/crude way to maintain the responsiveness of the user interface. This method sets a flag that causes the worker threads to simply dequeue
     * tasks and throw them away.
     *
     * Note that message types that don't utilize queueing are processed and transmitted normally regardless of whether the message sender is paused or not.
     */
    @Override
    public void pause() {
        if (queuingEnabled) {
            senderExecutor.setPaused(true);
            preprocessorExecutor.setPaused(true);

            preprocessorQueue.clear();
            senderQueue.clear();
        }
    }

    @Override
    public void resume() {
        if (queuingEnabled) {
            preprocessorExecutor.setPaused(false);
            senderExecutor.setPaused(false);
        }
    }

    @Override
    public void reset() {
        if (queuingEnabled) {
            pause();
            preprocessorQueue.clear();
            senderQueue.clear();
            logger.debug("Purged queues");
            resume();
        }
    }

    @Override
    public void addListener(MessageSenderListener listener) {
        listeners.add(listener);
    }

    @Override
    public void removeListener(MessageSenderListener listener) {
        listeners.remove(listener);
    }

    private void notifyListeners(MessageSenderEvent.Type eventType) {
        for (MessageSenderListener listener : listeners) {
            listener.handleMessageSenderEvent(new MessageSenderEvent(this, eventType));
        }
    }

    @Override
    public void sendMessage(String requestID, OutboundMessages messageType, String update) {
        long start = System.currentTimeMillis();
        try {

            if (queuingEnabled && isQueuedMessageType(messageType)) {
                submitTask(preprocessorExecutor, new Preprocessor(requestID, messageType, update));

            } else {
                preprocessAndSendMessage(requestID, messageType, update);
            }

        } catch (Exception e) {
            logger.warn("Failed to send binary message", e);
            notifyListeners(MessageSenderEvent.Type.MESSAGE_SEND_FAILED);
        }
        long length = (System.currentTimeMillis() - start);
        if (length > 5) {
            logger.info("Sending message to the client took " + length + "ms");
        }
    }

    @Override
    public void sendFile(Path path) {
        // TODO: We are sending file name and data but it can be improved to send a type and message
        try {
            long startTime = System.currentTimeMillis();

            // get filename and file content
            byte[] name = path.getFileName().toString().getBytes("UTF-8");
            byte[] data = Files.readAllBytes(path);

            // add to the buffer:
            // - type of message
            // - filename length (filename length is needed client side to parse the message)
            // - filename
            // - file content
            int bufferSize = 1 + 1 + name.length + data.length;
            ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
            buffer.put(BigInteger.valueOf(1).toByteArray());
            buffer.put(BigInteger.valueOf(name.length).toByteArray());
            buffer.put(name);
            buffer.put(data);

            // write binary message in the socket
            wsOutbound.writeBinaryMessage(buffer);

            String debug = ((long) System.currentTimeMillis() - startTime) + "ms were spent sending a file of "
                    + bufferSize / 1024 + "KB to the client";
            logger.info(debug);
        } catch (IOException e) {
            logger.warn("Failed to send file, " + path, e);
            notifyListeners(MessageSenderEvent.Type.MESSAGE_SEND_FAILED);
        }
    }

    private void preprocessAndSendMessage(String requestID, OutboundMessages messageType, String update)
            throws IOException {

        int uncompressedMessageSize = 0;

        if (update != null) {
            uncompressedMessageSize = update.length();
        }

        String message = preprocessMessage(requestID, messageType, update);

        if (!compressionEnabled || message.length() < minMessageLengthForCompression) {
            sendTextMessage(message, messageType);

        } else {
            byte[] compressedMessage = CompressionUtils.gzipCompress(message);
            sendBinaryMessage(compressedMessage, messageType, uncompressedMessageSize, false);
        }
    }

    private void preprocessMessageAndEnqueue(String requestId, OutboundMessages messageType, String update) {

        try {

            int uncompressedMessageSize = update.length();
            String message = preprocessMessage(requestId, messageType, update);

            if (!compressionEnabled || message.length() < minMessageLengthForCompression) {
                submitTask(senderExecutor, new TextMessageSender(message, messageType));

            } else {
                byte[] compressedMessage = CompressionUtils.gzipCompress(message);
                submitTask(senderExecutor,
                        new BinaryMessageSender(compressedMessage, messageType, uncompressedMessageSize));
            }

        } catch (Exception e) {
            logger.warn("Failed to process message before transmission", e);
            notifyListeners(MessageSenderEvent.Type.MESSAGE_SEND_FAILED);
        }
    }

    private String preprocessMessage(String requestId, OutboundMessages type, String update) {

        long startTime = System.currentTimeMillis();

        GeppettoTransportMessage transportMessage = TransportMessageFactory.getTransportMessage(requestId, type,
                update);

        logger.debug(String.format("Created transport message in %dms", System.currentTimeMillis() - startTime));

        startTime = System.currentTimeMillis();
        String message = new Gson().toJson(transportMessage);
        logger.debug(String.format("Created json in %dms", System.currentTimeMillis() - startTime));

        return message;
    }

    private void submitTask(ThreadPoolExecutor executor, Runnable task) throws InterruptedException {

        if (discardMessagesIfQueueFull) {
            executor.execute(task);
        } else {
            executor.getQueue().put(task);
        }
    }

    private void sendTextMessage(String message, OutboundMessages messageType) {

        try {

            long startTime = System.currentTimeMillis();
            CharBuffer buffer = CharBuffer.wrap(message);
            wsOutbound.writeTextMessage(buffer);
            if (messageType.equals("experiment_status")) {
                logger.info(String.format("Sent text message - %s, length: %d bytes, took: %d ms", messageType,
                        message.length(), System.currentTimeMillis() - startTime));
            }

        } catch (IOException e) {
            logger.warn("Failed to send message", e);
            notifyListeners(MessageSenderEvent.Type.MESSAGE_SEND_FAILED);
        }
    }

    private void sendBinaryMessage(byte[] message, OutboundMessages messageType, int uncompressedMessageSize,
            boolean fromQueue) {
        try {
            long startTime = System.currentTimeMillis();

            // add to the buffer:
            // - type of message
            // - message
            int bufferSize = 1 + message.length;
            ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
            buffer.put(BigInteger.valueOf(0).toByteArray());
            buffer.put(message);

            // ByteBuffer buffer = ByteBuffer.wrap(message);
            wsOutbound.writeBinaryMessage(buffer);

            String logMessage = "Sent binary/compressed message - %s, length: %d (%d) bytes, duration: %d ms";
            if (fromQueue) {
                logMessage = "Sent binary/compressed message from queue - %s, length: %d (%d) bytes, duration: %d ms";
            }

            logger.info(String.format(logMessage, messageType, message.length, uncompressedMessageSize,
                    System.currentTimeMillis() - startTime));

        } catch (IOException e) {
            logger.warn("Failed to send binary message", e);
            notifyListeners(MessageSenderEvent.Type.MESSAGE_SEND_FAILED);
        }
    }

    private boolean isQueuedMessageType(OutboundMessages messageType) {
        return queuedMessageTypes != null && queuedMessageTypes.contains(messageType);
    }

    public boolean isCompressionEnabled() {
        return compressionEnabled;
    }

    public void setCompressionEnabled(boolean compressionEnabled) {
        this.compressionEnabled = compressionEnabled;
    }

    public boolean isQueuingEnabled() {
        return queuingEnabled;
    }

    public void setQueuingEnabled(boolean queuingEnabled) {

        this.queuingEnabled = queuingEnabled;

        if (!queuingEnabled) {

            if (preprocessorQueue != null) {
                preprocessorQueue.clear();
            }
            if (senderQueue != null) {
                senderQueue.clear();
            }
        }
    }

    public int getMaxQueueSize() {
        return maxQueueSize;
    }

    public void setMaxQueueSize(int maxQueueSize) {
        this.maxQueueSize = maxQueueSize;
    }

    public boolean getDiscardMessagesIfQueueFull() {
        return discardMessagesIfQueueFull;
    }

    public void setDiscardMessagesIfQueueFull(boolean discardMessagesIfQueueFull) {
        this.discardMessagesIfQueueFull = discardMessagesIfQueueFull;
    }

    public int getMinMessageLengthForCompression() {
        return minMessageLengthForCompression;
    }

    public void setMinMessageLengthForCompression(int minMessageLengthForCompression) {
        this.minMessageLengthForCompression = minMessageLengthForCompression;
    }

    public Set<OutboundMessages> getQueuedMessageTypes() {
        return queuedMessageTypes;
    }

    public void setQueuedMessageTypes(Set<OutboundMessages> queuedMessageTypes) {
        this.queuedMessageTypes = queuedMessageTypes;
    }

    private class TextMessageSender implements Runnable {

        private String message;
        private OutboundMessages messageType;

        public TextMessageSender(String message, OutboundMessages messageType) {
            this.message = message;
            this.messageType = messageType;
        }

        public void run() {
            sendTextMessage(message, messageType);
        }
    }

    private class BinaryMessageSender implements Runnable {

        private byte[] message;
        private OutboundMessages messageType;
        private int uncompressedMessageSize;

        public BinaryMessageSender(byte[] message, OutboundMessages messageType, int uncompressedMessageSize) {
            this.message = message;
            this.messageType = messageType;
            this.uncompressedMessageSize = uncompressedMessageSize;
        }

        public void run() {
            sendBinaryMessage(message, messageType, uncompressedMessageSize, true);
        }
    }

    private class Preprocessor implements Runnable {

        private String requestId;
        private OutboundMessages type;
        private String update;

        public Preprocessor(String requestId, OutboundMessages type, String update) {
            this.requestId = requestId;
            this.type = type;
            this.update = update;
        }

        public void run() {
            preprocessMessageAndEnqueue(requestId, type, update);
        }
    }
}