rapture.exchange.rabbitmq.RabbitExchangeHandler.java Source code

Java tutorial

Introduction

Here is the source code for rapture.exchange.rabbitmq.RabbitExchangeHandler.java

Source

/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2011-2016 Incapture Technologies LLC
 *
 * 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 rapture.exchange.rabbitmq;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.log4j.Logger;

import rapture.common.RapturePipelineTask;
import rapture.common.exception.ExceptionToString;
import rapture.common.exception.RaptureException;
import rapture.common.exception.RaptureExceptionFactory;
import rapture.common.impl.jackson.JacksonUtil;
import rapture.common.model.RaptureExchange;
import rapture.common.model.RaptureExchangeQueue;
import rapture.common.pipeline.PipelineConstants;
import rapture.config.MultiValueConfigLoader;
import rapture.exchange.ExchangeHandler;
import rapture.exchange.QueueHandler;
import rapture.exchange.RPCMessage;
import rapture.exchange.TopicMessageHandler;
import rapture.util.IDGenerator;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.ConsumerCancelledException;
import com.rabbitmq.client.FlowListener;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.ReturnListener;
import com.rabbitmq.client.ShutdownListener;
import com.rabbitmq.client.ShutdownSignalException;

public class RabbitExchangeHandler implements ExchangeHandler {
    private Connection connection;
    private Channel channel;
    private ExecutorService service = Executors.newCachedThreadPool();
    private static Logger logger = Logger.getLogger(RabbitExchangeHandler.class);
    private String instanceName = "default"; //$NON-NLS-1$
    private int messageCounter = 1;
    private String replyQueueName;
    private Map<String, String> queueNameRegistry = new ConcurrentHashMap<String, String>();
    private QueueingConsumer consumer;

    @Override
    public void setInstanceName(String instanceName) {
        this.instanceName = instanceName;
    }

    @Override
    public synchronized void setConfig(Map<String, String> config) {
        // Attempt to bind to RabbitMQ
        ConnectionFactory factory = new ConnectionFactory();
        logger.info(Messages.getString("RabbitExchangeHandler.config")); //$NON-NLS-1$
        try {
            String uri = MultiValueConfigLoader.getConfig("RABBITMQ-" //$NON-NLS-1$
                    + instanceName);
            if (uri == null || uri.isEmpty()) {
                uri = "amqp://guest:guest@localhost:5672/%2f"; //$NON-NLS-1$
            }
            factory.setUri(uri);
            factory.setAutomaticRecoveryEnabled(true);
            logger.debug(Messages.getString("RabbitExchangeHandler.creatingChannel")); //$NON-NLS-1$
            connection = factory.newConnection();
            connection.addShutdownListener(new ShutdownListener() {
                @Override
                public void shutdownCompleted(ShutdownSignalException cause) {
                    // This should theoretically be called when we disconnect
                    // from RabbitMQ, but due to a bug in the client library it
                    // instead gets invoked
                    // when reconnecting. This may change in future versions of
                    // amqp-client so may need to revisit
                    logger.info("Reconnected to RabbitMQ");

                }
            });
            logger.debug(Messages.getString("RabbitExchangeHandler.connectionMade")); //$NON-NLS-1$
            channel = connection.createChannel();
            channel.addShutdownListener(new ShutdownListener() {
                @Override
                public void shutdownCompleted(ShutdownSignalException cause) {
                    logger.info("Disconnected from RabbitMQ. Cause :" + cause.getMessage());
                    logger.debug(ExceptionToString.format(cause));
                }
            });
            logger.debug(Messages.getString("RabbitExchangeHandler.channelCreated")); //$NON-NLS-1$
            channel.basicQos(100);
            channel.addReturnListener(new ReturnListener() {

                @Override
                public void handleReturn(int replyCode, String replyText, String exchange, String routingKey,
                        AMQP.BasicProperties properties, byte[] body) throws IOException {
                    logger.debug(
                            String.format(Messages.getString("RabbitExchangeHandler.returnListener"), replyCode, //$NON-NLS-1$
                                    replyText));
                }

            });
            channel.addFlowListener(new FlowListener() {

                @Override
                public void handleFlow(boolean active) throws IOException {
                    logger.debug(String.format(Messages.getString("RabbitExchangeHandler.Flow"), active)); //$NON-NLS-1$
                }

            });

            replyQueueName = channel.queueDeclare().getQueue();
            logger.info("RPC reply queue is " + replyQueueName);
            consumer = new QueueingConsumer(channel);
            channel.basicConsume(replyQueueName, true, consumer);

        } catch (Exception e) {
            String message = Messages.getString("RabbitExchangeHandler.noConnect");
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR, message, e);
        }
    }

    private Set<String> exchangesTested = Collections.synchronizedSet(new HashSet<String>());

    @Override
    public void setupExchange(RaptureExchange exchange) {
        if (exchangesTested.contains(exchange.getName())) {
            if (exchange.getQueueBindings() != null) {
                for (RaptureExchangeQueue queue : exchange.getQueueBindings()) {
                    try {
                        bindQueue(exchange, queue);
                    } catch (IOException e) {
                        logger.error("Unable to bind " + queue.getName() + " to exchange " + exchange.getName());
                        logger.info(ExceptionToString.format(e));
                    }
                }
            }
            return;
        }

        logger.debug(String.format(Messages.getString("RabbitExchangeHandler.ensureAvail"), exchange.getName())); //$NON-NLS-1$
        try {
            logger.debug(
                    String.format(Messages.getString("RabbitExchangeHandler.ExchangeDeclare"), exchange.getName())); //$NON-NLS-1$
            logger.debug(String.format(Messages.getString("RabbitExchangeHandler.exchangeType"), //$NON-NLS-1$
                    exchange.getExchangeType()));
            channel.exchangeDeclare(exchange.getName(), exchange.getExchangeType().name().toLowerCase(), true);
            // logger.info(ok.protocolClassId());
            // Now ensure that any declared queues are bound
            logger.debug(Messages.getString("RabbitExchangeHandler.bindingQueues")); //$NON-NLS-1$
            if (exchange.getQueueBindings() != null) {
                for (RaptureExchangeQueue queue : exchange.getQueueBindings()) {
                    bindQueue(exchange, queue);
                }
            }
            exchangesTested.add(exchange.getName());
        } catch (IOException e) {
            String message = Messages.getString("RabbitExchangeHandler.noExchange");
            try {
                if (channel.isOpen()) {
                    channel.close();
                }
            } catch (IOException e1) {
                logger.error(ExceptionToString.format(e1));
            }
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR, message, e);
        } catch (Throwable t) {
            try {
                if (channel.isOpen()) {
                    channel.close();
                }
            } catch (IOException e1) {
                logger.error(ExceptionToString.format(e1));
            }
            String message = String.format("Caught throwable during exchangeDeclare for exchange %s:\n%s",
                    exchange.getName(), ExceptionToString.format(t));
            logger.info(message);
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR, message, t);
        }
    }

    private void bindQueue(RaptureExchange exchange, RaptureExchangeQueue queue) throws IOException {
        String underlying;
        if (isAnonymousQueue(queue.getName())) {
            if (queueNameRegistry.containsKey(queue.getName())) {
                underlying = queueNameRegistry.get(queue.getName());
            } else {
                underlying = channel.queueDeclare().getQueue();
                queueNameRegistry.put(queue.getName(), underlying);
            }
        } else {
            underlying = getUnderlyingQueueName(exchange.getName(), queue.getName());
            logger.debug(String.format(Messages.getString("RabbitExchangeHandler.underlyingQueue"), underlying)); //$NON-NLS-1$
            channel.queueDeclare(underlying, true, false, false, null);
        }

        if (queue.getRouteBindings().isEmpty()) {
            logger.debug(String.format(Messages.getString("RabbitExchangeHandler.noRoute"), exchange.getName())); //$NON-NLS-1$
            channel.queueBind(underlying, exchange.getName(), ""); //$NON-NLS-1$
        } else {
            for (String bindingKey : queue.getRouteBindings()) {
                logger.debug(String.format(Messages.getString("RabbitExchangeHandler.binding"), bindingKey, //$NON-NLS-1$
                        exchange.getName()));
                channel.queueBind(underlying, exchange.getName(), bindingKey);
            }
        }
    }

    private boolean isAnonymousQueue(String queue) {
        return queue != null && queue.startsWith(PipelineConstants.ANONYMOUS_PREFIX);
    }

    /**
     * This method doens't really "bind" the queue to an exchange. The queue must be already bound beforehand. What happens here is we register a
     * {@link QueueHandler} for messages received by this queue
     */
    @Override
    public String startConsuming(String exchange, String queue, final QueueHandler handler) {
        boolean autoAck = Boolean.valueOf(MultiValueConfigLoader.getConfig("RABBITMQ-autoAck", "false"));
        final String tag = IDGenerator.getUUID();
        try {
            String underlyingQueue;
            if (isAnonymousQueue(queue)) {
                // If anonymous queue, don't use the silly autogenerated
                // anonymous name. Instead, ask RabbitMQ to create one
                if (queueNameRegistry.containsKey(queue)) {
                    underlyingQueue = queueNameRegistry.get(queue);
                } else {
                    throw RaptureExceptionFactory.create(String.format(
                            "Error! Cannot start consuming on undefined anonymous queue %s, on exchange %s", queue,
                            exchange));
                }
            } else {
                underlyingQueue = getUnderlyingQueueName(exchange, queue);
            }
            logger.debug(
                    String.format(Messages.getString("RabbitExchangeHandler.startConsuming"), underlyingQueue)); //$NON-NLS-1$
            logger.debug(String.format(Messages.getString("RabbitExchangeHandler.underly"), underlyingQueue)); //$NON-NLS-1$
            channel.basicConsume(underlyingQueue, autoAck, tag,
                    new MessageConsumer(channel, tag, handler, underlyingQueue));
        } catch (IOException e) {
            String message = String.format(Messages.getString("RabbitExchangeHandler.noStartConsuming"), //$NON-NLS-1$
                    String.format("%s (%s)", queue, exchange));
            try {
                channel.close();
            } catch (IOException e1) {
                logger.error(ExceptionToString.format(e1));
            }
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR, message, e);
        }
        return tag;
    }

    private String getUnderlyingQueueName(String exchange, String queue) {
        return exchange + "-" + queue;
    }

    @Override
    public void tearDownExchange(RaptureExchange exchange) {
        try {
            for (RaptureExchangeQueue queue : exchange.getQueueBindings()) {
                unbindQueue(exchange, queue);
            }
            channel.exchangeDelete(exchange.getName());
            // This will ensure it gets recreated later
            exchangesTested.remove(exchange.getName());
        } catch (IOException e) {
            String message = String.format(Messages.getString("RabbitExchangeHandler.noDelete"));
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR, message, e);
        }
    }

    private void unbindQueue(RaptureExchange exchange, RaptureExchangeQueue queue) throws IOException {
        String underlying = getUnderlyingQueueName(exchange.getName(), queue.getName());
        for (String routingKey : queue.getRouteBindings()) {
            channel.queueUnbind(underlying, exchange.getName(), routingKey);
        }
        channel.queueDelete(underlying);
    }

    @Override
    public void putTaskOnExchange(final String exchange, final RapturePipelineTask task, final String routingKey) {
        logger.debug(Messages.getString("RabbitExchangeHandler.puttingTask")); //$NON-NLS-1$
        Submitter s = new Submitter(exchange, task, routingKey);
        service.submit(s);
        RaptureException ex = s.getError();
        if (ex != null)
            throw ex;
    }

    private class Submitter implements Runnable {
        final String exchange;
        final RapturePipelineTask task;
        final String routingKey;
        private RaptureException error = null;

        public Submitter(final String exchange, final RapturePipelineTask task, final String routingKey) {
            this.exchange = exchange;
            this.task = task;
            this.routingKey = routingKey;
        }

        @Override
        public void run() {
            try {
                byte[] messageBody;
                messageBody = JacksonUtil.bytesJsonFromObject(task);

                // For now, set this to delivery mode 1 (non-persistent),
                // but
                // have this configurable later.

                messageCounter++;
                Integer deliveryMode = Integer
                        .parseInt(MultiValueConfigLoader.getConfig("RABBITMQ-deliveryMode", "1"));
                AMQP.BasicProperties props = new AMQP.BasicProperties().builder().contentType(task.getContentType())
                        .deliveryMode(deliveryMode).priority(task.getPriority()).messageId("" + messageCounter) //$NON-NLS-1$
                        .build();

                logger.debug(String.format(Messages.getString("RabbitExchangeHandler.publishMessage"), //$NON-NLS-1$
                        exchange, routingKey, task.getContentType()));
                try {
                    channel.basicPublish(exchange, routingKey, props, messageBody);
                    logger.debug(String.format(Messages.getString("RabbitExchangeHandler.sendMessage"),
                            props.getMessageId(), task.getContent(), exchange, routingKey));
                } catch (IOException e) {
                    String message = Messages.getString("RabbitExchangeHandler.noPublish");
                    throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR, message, e);
                }
            } catch (RaptureException e) {
                error = e;
            }
        }

        public RaptureException getError() {
            return error;
        }
    }

    @Override
    public String subscribeToExchange(String exchange, List<String> routingKeys, QueueHandler handler) {
        throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR,
                Messages.getString("RabbitExchangeHandler.notYetSupported")); //$NON-NLS-1$
    }

    @Override
    public void ensureExchangeUnAvailable(RaptureExchange exchangeConfig) {
        // This will ensure it gets recreated later
        exchangesTested.remove(exchangeConfig.getName());
    }

    @Override
    public Map<String, Object> makeRPC(String queueName, String fnName, Map<String, Object> params,
            long timeoutInSeconds) {
        // Send a message on a defined queue and wait for a response
        try {
            String corrId = java.util.UUID.randomUUID().toString();
            BasicProperties props = new BasicProperties.Builder().correlationId(corrId).replyTo(replyQueueName)
                    .build();
            // Construct message
            // A message contains the fnName and the map, as JSON
            RPCMessage messageObj = new RPCMessage();
            messageObj.setFnName(fnName);
            messageObj.setParams(params);
            String message = JacksonUtil.jsonFromObject(messageObj);
            logger.debug("Will make call on queue name " + queueName);
            logger.debug("Message is " + message);
            channel.basicPublish("", queueName, props, message.getBytes("UTF-8"));

            // Now wait for a response, max timeout
            String response = null;
            while (true) {
                QueueingConsumer.Delivery delivery = consumer.nextDelivery(timeoutInSeconds * 1000);
                if (delivery == null) {
                    throw RaptureExceptionFactory.create("Timed out while waiting for response");
                }
                if (delivery.getProperties().getCorrelationId().equals(corrId)) {
                    response = new String(delivery.getBody());
                    break;
                }
            }
            Map<String, Object> ret = null;
            if (response != null) {
                ret = JacksonUtil.getMapFromJson(response);
            }
            return ret;
        } catch (IOException e) {
            throw RaptureExceptionFactory.create("Could not create reply rpc queue", e);
        } catch (ShutdownSignalException e) {
            throw RaptureExceptionFactory.create("Shutdown", e);
        } catch (ConsumerCancelledException e) {
            throw RaptureExceptionFactory.create("Cancelled", e);
        } catch (InterruptedException e) {
            throw RaptureExceptionFactory.create("Interrupted", e);

        }
    }

    @Override
    public void publishTopicMessage(String exchange, String topic, String message) {
        Publisher p = new Publisher(exchange, topic, message);
        service.submit(p);
    }

    private class Publisher implements Runnable {
        final String exchange;
        final String topic;
        final String message;

        public Publisher(final String exchange, final String topic, final String message) {
            this.exchange = exchange;
            this.topic = topic;
            this.message = message;
        }

        @Override
        public void run() {
            try {
                channel.basicPublish(exchange, topic, null, message.getBytes());
            } catch (IOException e) {
                throw RaptureExceptionFactory.create("Could not publish on topic queue", e);
            }
        }
    }

    private AtomicLong subscriptionHandler = new AtomicLong(0);

    private Map<Long, SubscriptionThread> subscriberMap = new HashMap<Long, SubscriptionThread>();

    @Override
    public long subscribeTopic(String exchange, String topic, TopicMessageHandler messageHandler) {

        SubscriptionThread t = new SubscriptionThread(exchange, topic, channel, messageHandler);
        long handleId = subscriptionHandler.incrementAndGet();
        subscriberMap.put(handleId, t);
        t.start();
        return handleId;
    }

    @Override
    public void unsubscribeTopic(long handle) {
        SubscriptionThread t = subscriberMap.remove(handle);
        if (t != null) {
            t.closeSubscription();
        }
    }

}