All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.astyanax.recipes.queue.MessageQueueDispatcher Maven / Gradle / Ivy

There is a newer version: 3.10.2
Show newest version
/**
 * Copyright 2013 Netflix, Inc.
 *
 * 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.
 */
package com.netflix.astyanax.recipes.queue;

import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Queues;
import com.netflix.astyanax.recipes.locks.BusyLockException;

/**
 * The message queue dispatcher reads message from the message queue
 * and dispatches to worker threads.
 * 
 * @author elandau
 *
 */
public class MessageQueueDispatcher {
    private static final Logger LOG = LoggerFactory.getLogger(MessageQueueDispatcher.class);
    
    public final static int   DEFAULT_BATCH_SIZE            = 5;
    public final static int   DEFAULT_POLLING_INTERVAL     = 1000;
    public final static int   DEFAULT_THREAD_COUNT          = 1;
    public final static int   DEFAULT_CONSUMER_COUNT        = 1;
    public final static int   DEFAULT_ACK_SIZE              = 100;
    public final static int   DEFAULT_ACK_INTERVAL          = 100;
    public final static int   DEFAULT_BACKLOG_SIZE          = 1000;
    
    public static class Builder {
        private final MessageQueueDispatcher dispatcher = new MessageQueueDispatcher();
        
        /**
         * Specify the message queue to use for this dispatcher
         * @param messageQueue
         * @return
         */
        public Builder withMessageQueue(MessageQueue messageQueue) {
            dispatcher.messageQueue = messageQueue;
            return this;
        }
        
        /**
         * Change the number of threads reading from the queue
         * 
         * @param threadCount
         * @deprecated Use withProcessorThreadCount
         */
        public Builder withThreadCount(int threadCount) {
            return withProcessorThreadCount(threadCount);
        }
        
        /**
         * Specify number of threads that are processing events popped from the queue
         * @param threadCount
         * @return
         */
        public Builder withProcessorThreadCount(int threadCount) {
            dispatcher.processorThreadCount = threadCount;
            return this;
        }
        
        /**
         * Number of pending events to process in the backlog
         * @param size
         * @return
         */
        public Builder withBacklogSize(int size) {
            dispatcher.backlogSize = size;
            return this;
        }
        
        /**
         * Set the number of consumers that will be removing items from the 
         * queue.  This value must be less than or equal to the thread count.
         * @param consumerCount
         * @return
         */
        public Builder withConsumerCount(int consumerCount) {
            dispatcher.consumerCount = consumerCount;
            return this;
        }
        
        /**
         * Number of 'triggers' to read from the queue in each call.  
         * Default is 1
         * @param batchSize
         */
        public Builder withBatchSize(int batchSize) {
            dispatcher.batchSize = batchSize;
            return this;
        }
        
        /**
         * Flush the ack queue on this interval.
         * @param interval
         * @param units
         */
        public Builder withAckInterval(long interval, TimeUnit units) {
            dispatcher.ackInterval = TimeUnit.MILLISECONDS.convert(interval, units);
            return this;
        }
        
        /**
         * Interval for polling from the queue.  
         * @param interval
         * @param units
         */
        public Builder withPollingInterval(long interval, TimeUnit units) {
            dispatcher.pollingInterval = TimeUnit.MILLISECONDS.convert(interval, units);
            return this;
        }
        
        /**
         * Callback to process messages.  The callback is called from any of the internal processing
         * threads and is therefore not thread safe.
         * @param callback
         * @return true to ack the message, false to not ack and cause the message to timeout
         *          Throw an exception to force the message to be added to the poison queue
         */
        public Builder withCallback(Function callback) {
            dispatcher.callback = callback;
            return this;
        }
        
        /**
         * Provide a message handler factory to use when creating tasks.
         * @param factory
         * @return
         */
        public Builder withMessageHandlerFactory(MessageHandlerFactory factory) {
            dispatcher.handlerFactory = factory;
            return this;
        }
        
        public MessageQueueDispatcher build() {
            Preconditions.checkArgument(dispatcher.consumerCount <= dispatcher.processorThreadCount, "consumerCounter must be <= threadCount");
            dispatcher.initialize();
            return dispatcher;
        }
    }
    
    private int             processorThreadCount   = DEFAULT_THREAD_COUNT;
    private int             batchSize     = DEFAULT_BATCH_SIZE;
    private int             consumerCount = DEFAULT_CONSUMER_COUNT;
    private int             ackSize       = DEFAULT_ACK_SIZE;
    private long            ackInterval   = DEFAULT_ACK_INTERVAL;
    private int             backlogSize   = DEFAULT_BACKLOG_SIZE;
    private long            pollingInterval = DEFAULT_POLLING_INTERVAL;
    private boolean         terminate     = false;
    private MessageQueue    messageQueue;
    private ExecutorService executor;
    private MessageConsumer ackConsumer;
    private Function   callback;
    private MessageHandlerFactory handlerFactory;
    private LinkedBlockingQueue toAck = Queues.newLinkedBlockingQueue();
    private LinkedBlockingQueue toProcess = Queues.newLinkedBlockingQueue(500);
    
    private MessageQueueDispatcher() {
    }
    
    private void initialize() {
        Preconditions.checkNotNull(messageQueue, "Must specify message queue");
        
        if (this.handlerFactory == null)
            this.handlerFactory = new SimpleMessageHandlerFactory();
        toProcess = Queues.newLinkedBlockingQueue(backlogSize);
    }
    
    public void start() {
        executor = Executors.newScheduledThreadPool(processorThreadCount + consumerCount + 1);
        
        startAckThread();
        
        for (int i = 0; i < consumerCount; i++) {
            startConsumer(i);
        }
        
        for (int i = 0; i < processorThreadCount; i++) {
            startProcessor(i);
        }
    }
    
    public void stop() {
        terminate = true;
        executor.shutdownNow();
    }
    
    private void startAckThread() {
        ackConsumer = messageQueue.createConsumer();
        
        executor.submit(new Runnable() {
            @Override
            public void run() {
                String name = StringUtils.join(Lists.newArrayList(messageQueue.getName(), "Ack"), ":");
                Thread.currentThread().setName(name);
                
                while (!terminate) {
                    try {
                        List messages = Lists.newArrayList();
                        toAck.drainTo(messages);
                        if (!messages.isEmpty()) {
                            try {
                                ackConsumer.ackMessages(messages);
                            } catch (MessageQueueException e) {
                                toAck.addAll(messages);
                                LOG.warn("Failed to ack consumer", e);
                            }
                        }
                    }
                    catch (Throwable t) {
                        LOG.info("Error acking messages", t);
                    }
                    
                    try {
                        Thread.sleep(ackInterval);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        return;
                    }
                }
            }
        });
    }
    
    private void startConsumer(final int id) {
        executor.submit(new Runnable() {
            @Override
            public void run() {
                String name = StringUtils.join(Lists.newArrayList(messageQueue.getName(), "Consumer", Integer.toString(id)), ":");
                Thread.currentThread().setName(name);
                
                // Create the consumer context
                final MessageConsumer consumer = messageQueue.createConsumer();
                
                while (!terminate) {
                    // Process events in a tight loop, until asked to terminate
                    Collection messages = null;
                    try {
                        messages = consumer.readMessages(batchSize);
                        if (messages.isEmpty()) {
                            Thread.sleep(pollingInterval);
                        }
                        else {
                            for (MessageContext context : messages) {
                                toProcess.put(context);
                            }
                        }
                    } 
                    catch (BusyLockException e) {
                        try {
                            Thread.sleep(pollingInterval);
                        } catch (InterruptedException e1) {
                            Thread.interrupted();
                            return;
                        }
                    }
                    catch (Throwable t) {
                        LOG.warn("Error consuming messages ", t);
                    }
                }
            }
        });
    }
    
    private void startProcessor(final int id) {
        executor.submit(new Runnable() {
            @Override
            public void run() {
                String name = StringUtils.join(Lists.newArrayList(messageQueue.getName(), "Processor", Integer.toString(id)), ":");
                Thread.currentThread().setName(name);
                LOG.info("Starting message processor : " + name);
                try {
                    while (!terminate) {
                        // Pop a message off the queue, blocking if empty
                        final MessageContext context;
                        try {
                            context = toProcess.take();
                            if (context == null)
                                continue;
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            return;
                        }
                        
                        // Process the message
                        Message message = context.getMessage();
                        try {
                            // Message has a specific handler class
                            if (message.getTaskClass() != null) {
                                @SuppressWarnings("unchecked")
                                Function task = handlerFactory.createInstance(message.getTaskClass());
                                if (task.apply(context)) {
                                    toAck.add(context);
                                }
                                continue;
                            }
                            
                            // Use default callback
                            if (callback.apply(context)) {
                                context.setStatus(MessageStatus.DONE);
                                toAck.add(context);
                                continue;
                            }
                        }
                        catch (Throwable t) {
                            context.setException(t);
                            toAck.add(context);
                            LOG.error("Error processing message " + message.getKey(), t);
//                            try {
//                                ackConsumer.ackPoisonMessage(context);
//                            } catch (MessageQueueException e) {
//                                LOG.warn("Failed to ack poison message " + message.getKey(), e);
//                            }
                        }
                    }
                }
                catch (Throwable t) {
                    LOG.error("Error running producer : " + name, t);
                }
            }
        });
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy