org.springframework.data.redis.listener.RedisMessageListenerContainer.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.data.redis.listener.RedisMessageListenerContainer.java

Source

/*
 * Copyright 2011-2019 the original author or authors.
 *
 * 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
 *
 *      https://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 org.springframework.data.redis.listener;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.connection.ConnectionUtils;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.Subscription;
import org.springframework.data.redis.connection.util.ByteArrayWrapper;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.SchedulingAwareRunnable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ErrorHandler;

/**
 * Container providing asynchronous behaviour for Redis message listeners. Handles the low level details of listening,
 * converting and message dispatching.
 * <p>
 * As oppose to the low level Redis (one connection per subscription), the container uses only one connection that is
 * 'multiplexed' for all registered listeners, the message dispatch being done through the task executor.
 * <p>
 * Note the container uses the connection in a lazy fashion (the connection is used only if at least one listener is
 * configured).
 * <p>
 * Adding and removing listeners at the same time has undefined results. It is strongly recommended to synchronize/order
 * these methods accordingly.
 *
 * @author Costin Leau
 * @author Jennifer Hickey
 * @author Way Joke
 * @author Thomas Darimont
 * @author Mark Paluch
 */
public class RedisMessageListenerContainer
        implements InitializingBean, DisposableBean, BeanNameAware, SmartLifecycle {

    /** Logger available to subclasses */
    protected final Log logger = LogFactory.getLog(getClass());

    /**
     * Default thread name prefix: "RedisListeningContainer-".
     */
    public static final String DEFAULT_THREAD_NAME_PREFIX = ClassUtils
            .getShortName(RedisMessageListenerContainer.class) + "-";

    /**
     * The default recovery interval: 5000 ms = 5 seconds.
     */
    public static final long DEFAULT_RECOVERY_INTERVAL = 5000;

    /**
     * The default subscription wait time: 2000 ms = 2 seconds.
     */
    public static final long DEFAULT_SUBSCRIPTION_REGISTRATION_WAIT_TIME = 2000L;

    private long initWait = TimeUnit.SECONDS.toMillis(5);

    private @Nullable Executor subscriptionExecutor;

    private @Nullable Executor taskExecutor;

    private @Nullable RedisConnectionFactory connectionFactory;

    private @Nullable String beanName;

    private @Nullable ErrorHandler errorHandler;

    private final Object monitor = new Object();
    // whether the container is running (or not)
    private volatile boolean running = false;
    // whether the container has been initialized
    private volatile boolean initialized = false;
    // whether the container uses a connection or not
    // (as the container might be running but w/o listeners, it won't use any resources)
    private volatile boolean listening = false;

    private volatile boolean manageExecutor = false;

    // lookup maps
    // to avoid creation of hashes for each message, the maps use raw byte arrays (wrapped to respect the equals/hashcode
    // contract)

    // lookup map between patterns and listeners
    private final Map<ByteArrayWrapper, Collection<MessageListener>> patternMapping = new ConcurrentHashMap<>();
    // lookup map between channels and listeners
    private final Map<ByteArrayWrapper, Collection<MessageListener>> channelMapping = new ConcurrentHashMap<>();
    // lookup map between listeners and channels
    private final Map<MessageListener, Set<Topic>> listenerTopics = new ConcurrentHashMap<>();

    private final SubscriptionTask subscriptionTask = new SubscriptionTask();

    private volatile RedisSerializer<String> serializer = RedisSerializer.string();

    private long recoveryInterval = DEFAULT_RECOVERY_INTERVAL;

    private long maxSubscriptionRegistrationWaitingTime = DEFAULT_SUBSCRIPTION_REGISTRATION_WAIT_TIME;

    public void afterPropertiesSet() {
        if (taskExecutor == null) {
            manageExecutor = true;
            taskExecutor = createDefaultTaskExecutor();
        }

        if (subscriptionExecutor == null) {
            subscriptionExecutor = taskExecutor;
        }

        initialized = true;
    }

    /**
     * Creates a default TaskExecutor. Called if no explicit TaskExecutor has been specified.
     * <p>
     * The default implementation builds a {@link org.springframework.core.task.SimpleAsyncTaskExecutor} with the
     * specified bean name (or the class name, if no bean name specified) as thread name prefix.
     *
     * @see org.springframework.core.task.SimpleAsyncTaskExecutor#SimpleAsyncTaskExecutor(String)
     */
    protected TaskExecutor createDefaultTaskExecutor() {
        String threadNamePrefix = (beanName != null ? beanName + "-" : DEFAULT_THREAD_NAME_PREFIX);
        return new SimpleAsyncTaskExecutor(threadNamePrefix);
    }

    public void destroy() throws Exception {
        initialized = false;

        stop();

        if (manageExecutor) {
            if (taskExecutor instanceof DisposableBean) {
                ((DisposableBean) taskExecutor).destroy();

                if (logger.isDebugEnabled()) {
                    logger.debug("Stopped internally-managed task executor");
                }
            }
        }
    }

    public boolean isAutoStartup() {
        return true;
    }

    public void stop(Runnable callback) {
        stop();
        callback.run();
    }

    public int getPhase() {
        // start the latest
        return Integer.MAX_VALUE;
    }

    public boolean isRunning() {
        return running;
    }

    public void start() {
        if (!running) {
            running = true;
            // wait for the subscription to start before returning
            // technically speaking we can only be notified right before the subscription starts
            synchronized (monitor) {
                lazyListen();
                if (listening) {
                    try {
                        // wait up to 5 seconds for Subscription thread
                        monitor.wait(initWait);
                    } catch (InterruptedException e) {
                        // stop waiting
                        Thread.currentThread().interrupt();
                        running = false;
                        return;
                    }
                }
            }

            if (logger.isDebugEnabled()) {
                logger.debug("Started RedisMessageListenerContainer");
            }
        }
    }

    public void stop() {
        if (isRunning()) {
            running = false;
            subscriptionTask.cancel();
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Stopped RedisMessageListenerContainer");
        }
    }

    /**
     * Process a message received from the provider.
     *
     * @param message
     * @param pattern
     */
    protected void processMessage(MessageListener listener, Message message, byte[] pattern) {
        executeListener(listener, message, pattern);
    }

    /**
     * Execute the specified listener.
     *
     * @see #handleListenerException
     */
    protected void executeListener(MessageListener listener, Message message, byte[] pattern) {
        try {
            listener.onMessage(message, pattern);
        } catch (Throwable ex) {
            handleListenerException(ex);
        }
    }

    /**
     * Return whether this container is currently active, that is, whether it has been set up but not shut down yet.
     */
    public final boolean isActive() {
        return initialized;
    }

    /**
     * Handle the given exception that arose during listener execution.
     * <p>
     * The default implementation logs the exception at error level. This can be overridden in subclasses.
     *
     * @param ex the exception to handle
     */
    protected void handleListenerException(Throwable ex) {
        if (isActive()) {
            // Regular case: failed while active.
            // Invoke ErrorHandler if available.
            invokeErrorHandler(ex);
        } else {
            // Rare case: listener thread failed after container shutdown.
            // Log at debug level, to avoid spamming the shutdown logger.
            logger.debug("Listener exception after container shutdown", ex);
        }
    }

    /**
     * Invoke the registered ErrorHandler, if any. Log at error level otherwise.
     *
     * @param ex the uncaught error that arose during message processing.
     * @see #setErrorHandler
     */
    protected void invokeErrorHandler(Throwable ex) {
        if (this.errorHandler != null) {
            this.errorHandler.handleError(ex);
        } else if (logger.isWarnEnabled()) {
            logger.warn("Execution of message listener failed, and no ErrorHandler has been set.", ex);
        }
    }

    /**
     * Returns the connectionFactory.
     *
     * @return Returns the connectionFactory
     */
    @Nullable
    public RedisConnectionFactory getConnectionFactory() {
        return connectionFactory;
    }

    /**
     * @param connectionFactory The connectionFactory to set.
     */
    public void setConnectionFactory(RedisConnectionFactory connectionFactory) {

        Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
        this.connectionFactory = connectionFactory;
    }

    public void setBeanName(String name) {
        this.beanName = name;
    }

    /**
     * Sets the task executor used for running the message listeners when messages are received. If no task executor is
     * set, an instance of {@link SimpleAsyncTaskExecutor} will be used by default. The task executor can be adjusted
     * depending on the work done by the listeners and the number of messages coming in.
     *
     * @param taskExecutor The taskExecutor to set.
     */
    public void setTaskExecutor(Executor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    /**
     * Sets the task execution used for subscribing to Redis channels. By default, if no executor is set, the
     * {@link #setTaskExecutor(Executor)} will be used. In some cases, this might be undersired as the listening to the
     * connection is a long running task.
     * <p>
     * Note: This implementation uses at most one long running thread (depending on whether there are any listeners
     * registered or not) and up to two threads during the initial registration.
     *
     * @param subscriptionExecutor The subscriptionExecutor to set.
     */
    public void setSubscriptionExecutor(Executor subscriptionExecutor) {
        this.subscriptionExecutor = subscriptionExecutor;
    }

    /**
     * Sets the serializer for converting the {@link Topic}s into low-level channels and patterns. By default,
     * {@link StringRedisSerializer} is used.
     *
     * @param serializer The serializer to set.
     */
    public void setTopicSerializer(RedisSerializer<String> serializer) {
        this.serializer = serializer;
    }

    /**
     * Set an ErrorHandler to be invoked in case of any uncaught exceptions thrown while processing a Message. By default
     * there will be <b>no</b> ErrorHandler so that error-level logging is the only result.
     */
    public void setErrorHandler(ErrorHandler errorHandler) {
        this.errorHandler = errorHandler;
    }

    /**
     * Attaches the given listeners (and their topics) to the container.
     * <p>
     * Note: it's possible to call this method while the container is running forcing a reinitialization of the container.
     * Note however that this might cause some messages to be lost (while the container reinitializes) - hence calling
     * this method at runtime is considered advanced usage.
     *
     * @param listeners map of message listeners and their associated topics
     */
    public void setMessageListeners(Map<? extends MessageListener, Collection<? extends Topic>> listeners) {
        initMapping(listeners);
    }

    /**
     * Adds a message listener to the (potentially running) container. If the container is running, the listener starts
     * receiving (matching) messages as soon as possible.
     *
     * @param listener message listener
     * @param topics message listener topic
     */
    public void addMessageListener(MessageListener listener, Collection<? extends Topic> topics) {
        addListener(listener, topics);
        lazyListen();
    }

    /**
     * Adds a message listener to the (potentially running) container. If the container is running, the listener starts
     * receiving (matching) messages as soon as possible.
     *
     * @param listener message listener
     * @param topic message topic
     */
    public void addMessageListener(MessageListener listener, Topic topic) {
        addMessageListener(listener, Collections.singleton(topic));
    }

    /**
     * Removes a message listener from the given topics. If the container is running, the listener stops receiving
     * (matching) messages as soon as possible.
     * <p>
     * Note that this method obeys the Redis (p)unsubscribe semantics - meaning an empty/null collection will remove
     * listener from all channels. Similarly a null listener will unsubscribe all listeners from the given topic.
     *
     * @param listener message listener
     * @param topics message listener topics
     */
    public void removeMessageListener(MessageListener listener, Collection<? extends Topic> topics) {
        removeListener(listener, topics);
    }

    /**
     * Removes a message listener from the from the given topic. If the container is running, the listener stops receiving
     * (matching) messages as soon as possible.
     * <p>
     * Note that this method obeys the Redis (p)unsubscribe semantics - meaning an empty/null collection will remove
     * listener from all channels. Similarly a null listener will unsubscribe all listeners from the given topic.
     *
     * @param listener message listener
     * @param topic message topic
     */
    public void removeMessageListener(MessageListener listener, Topic topic) {
        removeMessageListener(listener, Collections.singleton(topic));
    }

    /**
     * Removes the given message listener completely (from all topics). If the container is running, the listener stops
     * receiving (matching) messages as soon as possible. Similarly a null listener will unsubscribe all listeners from
     * the given topic.
     *
     * @param listener message listener
     */
    public void removeMessageListener(MessageListener listener) {
        removeMessageListener(listener, Collections.<Topic>emptySet());
    }

    private void initMapping(Map<? extends MessageListener, Collection<? extends Topic>> listeners) {
        // stop the listener if currently running
        if (isRunning()) {
            subscriptionTask.cancel();
        }

        patternMapping.clear();
        channelMapping.clear();
        listenerTopics.clear();

        if (!CollectionUtils.isEmpty(listeners)) {
            for (Map.Entry<? extends MessageListener, Collection<? extends Topic>> entry : listeners.entrySet()) {
                addListener(entry.getKey(), entry.getValue());
            }
        }

        // resume activity
        if (initialized) {
            start();
        }
    }

    /**
     * Method inspecting whether listening for messages (and thus using a thread) is actually needed and triggering it.
     */
    private void lazyListen() {
        boolean debug = logger.isDebugEnabled();
        boolean started = false;

        if (isRunning()) {
            if (!listening) {
                synchronized (monitor) {
                    if (!listening) {
                        if (channelMapping.size() > 0 || patternMapping.size() > 0) {
                            subscriptionExecutor.execute(subscriptionTask);
                            listening = true;
                            started = true;
                        }
                    }
                }
                if (debug) {
                    if (started) {
                        logger.debug("Started listening for Redis messages");
                    } else {
                        logger.debug("Postpone listening for Redis messages until actual listeners are added");
                    }
                }
            }
        }
    }

    private void addListener(MessageListener listener, Collection<? extends Topic> topics) {
        Assert.notNull(listener, "a valid listener is required");
        Assert.notEmpty(topics, "at least one topic is required");

        List<byte[]> channels = new ArrayList<>(topics.size());
        List<byte[]> patterns = new ArrayList<>(topics.size());

        boolean trace = logger.isTraceEnabled();

        // add listener mapping
        Set<Topic> set = listenerTopics.get(listener);
        if (set == null) {
            set = new CopyOnWriteArraySet<>();
            listenerTopics.put(listener, set);
        }
        set.addAll(topics);

        for (Topic topic : topics) {

            ByteArrayWrapper holder = new ByteArrayWrapper(serializer.serialize(topic.getTopic()));

            if (topic instanceof ChannelTopic) {
                Collection<MessageListener> collection = channelMapping.get(holder);
                if (collection == null) {
                    collection = new CopyOnWriteArraySet<>();
                    channelMapping.put(holder, collection);
                }
                collection.add(listener);
                channels.add(holder.getArray());

                if (trace)
                    logger.trace("Adding listener '" + listener + "' on channel '" + topic.getTopic() + "'");
            }

            else if (topic instanceof PatternTopic) {
                Collection<MessageListener> collection = patternMapping.get(holder);
                if (collection == null) {
                    collection = new CopyOnWriteArraySet<>();
                    patternMapping.put(holder, collection);
                }
                collection.add(listener);
                patterns.add(holder.getArray());

                if (trace)
                    logger.trace("Adding listener '" + listener + "' for pattern '" + topic.getTopic() + "'");
            }

            else {
                throw new IllegalArgumentException("Unknown topic type '" + topic.getClass() + "'");
            }
        }

        // check the current listening state
        if (listening) {
            subscriptionTask.subscribeChannel(channels.toArray(new byte[channels.size()][]));
            subscriptionTask.subscribePattern(patterns.toArray(new byte[patterns.size()][]));
        }
    }

    private void removeListener(MessageListener listener, Collection<? extends Topic> topics) {
        boolean trace = logger.isTraceEnabled();

        // check stop listening case
        if (listener == null && CollectionUtils.isEmpty(topics)) {
            subscriptionTask.cancel();
            return;
        }

        List<byte[]> channelsToRemove = new ArrayList<>();
        List<byte[]> patternsToRemove = new ArrayList<>();

        // check unsubscribe all topics case
        if (CollectionUtils.isEmpty(topics)) {
            Set<Topic> set = listenerTopics.remove(listener);
            // listener not found, bail out
            if (set == null) {
                return;
            }
            topics = set;
        }

        for (Topic topic : topics) {
            ByteArrayWrapper holder = new ByteArrayWrapper(serializer.serialize(topic.getTopic()));

            if (topic instanceof ChannelTopic) {
                remove(listener, topic, holder, channelMapping, channelsToRemove);

                if (trace) {
                    String msg = (listener != null ? "listener '" + listener + "'" : "all listeners");
                    logger.trace("Removing " + msg + " from channel '" + topic.getTopic() + "'");
                }
            }

            else if (topic instanceof PatternTopic) {
                remove(listener, topic, holder, patternMapping, patternsToRemove);

                if (trace) {
                    String msg = (listener != null ? "listener '" + listener + "'" : "all listeners");
                    logger.trace("Removing " + msg + " from pattern '" + topic.getTopic() + "'");
                }
            }
        }

        // double check whether there are still subscriptions available otherwise cancel the connection
        // as most drivers forfeit the connection on unsubscribe
        if (listenerTopics.isEmpty()) {
            subscriptionTask.cancel();
        }

        // check the current listening state
        else if (listening) {
            subscriptionTask.unsubscribeChannel(channelsToRemove.toArray(new byte[channelsToRemove.size()][]));
            subscriptionTask.unsubscribePattern(patternsToRemove.toArray(new byte[patternsToRemove.size()][]));
        }
    }

    private void remove(MessageListener listener, Topic topic, ByteArrayWrapper holder,
            Map<ByteArrayWrapper, Collection<MessageListener>> mapping, List<byte[]> topicToRemove) {

        Collection<MessageListener> listeners = mapping.get(holder);
        Collection<MessageListener> listenersToRemove = null;

        if (listeners != null) {
            // remove only one listener
            if (listener != null) {
                listeners.remove(listener);
                listenersToRemove = Collections.singletonList(listener);
            }

            // no listener given - remove all of them
            else {
                listenersToRemove = listeners;
            }

            // start removing listeners
            for (MessageListener messageListener : listenersToRemove) {
                Set<Topic> topics = listenerTopics.get(messageListener);
                if (topics != null) {
                    topics.remove(topic);
                }
                if (CollectionUtils.isEmpty(topics)) {
                    listenerTopics.remove(messageListener);
                }
            }
            // if we removed everything, remove the empty holder collection
            if (listener == null || listeners.isEmpty()) {
                mapping.remove(holder);
                topicToRemove.add(holder.getArray());
            }
        }
    }

    /**
     * Handle subscription task exception. Will attempt to restart the subscription if the Exception is a connection
     * failure (for example, Redis was restarted).
     *
     * @param ex Throwable exception
     */
    protected void handleSubscriptionException(Throwable ex) {
        listening = false;
        subscriptionTask.closeConnection();
        if (ex instanceof RedisConnectionFailureException) {
            if (isRunning()) {
                logger.error("Connection failure occurred. Restarting subscription task after " + recoveryInterval
                        + " ms");
                sleepBeforeRecoveryAttempt();
                lazyListen();
            }
        } else {
            logger.error("SubscriptionTask aborted with exception:", ex);
        }
    }

    /**
     * Sleep according to the specified recovery interval. Called between recovery attempts.
     */
    protected void sleepBeforeRecoveryAttempt() {
        if (this.recoveryInterval > 0) {
            try {
                Thread.sleep(this.recoveryInterval);
            } catch (InterruptedException interEx) {
                logger.debug("Thread interrupted while sleeping the recovery interval");
                Thread.currentThread().interrupt();
            }
        }
    }

    /**
     * Runnable used for Redis subscription. Implemented as a dedicated class to provide as many hints as possible to the
     * underlying thread pool.
     *
     * @author Costin Leau
     */
    private class SubscriptionTask implements SchedulingAwareRunnable {

        /**
         * Runnable used, on a parallel thread, to do the initial pSubscribe. This is required since, during initialization,
         * both subscribe and pSubscribe might be needed but since the first call is blocking, the second call needs to
         * executed in parallel.
         *
         * @author Costin Leau
         */
        private class PatternSubscriptionTask implements SchedulingAwareRunnable {

            private long WAIT = 500;
            private long ROUNDS = 3;

            public boolean isLongLived() {
                return false;
            }

            public void run() {
                // wait for subscription to be initialized
                boolean done = false;
                // wait 3 rounds for subscription to be initialized
                for (int i = 0; i < ROUNDS && !done; i++) {
                    if (connection != null) {
                        synchronized (localMonitor) {
                            if (connection.isSubscribed()) {
                                done = true;
                                connection.getSubscription().pSubscribe(unwrap(patternMapping.keySet()));
                            } else {
                                try {
                                    Thread.sleep(WAIT);
                                } catch (InterruptedException ex) {
                                    Thread.currentThread().interrupt();
                                    return;
                                }
                            }
                        }
                    }
                }
            }
        }

        private volatile @Nullable RedisConnection connection;
        private boolean subscriptionTaskRunning = false;
        private final Object localMonitor = new Object();
        private long subscriptionWait = TimeUnit.SECONDS.toMillis(5);

        public boolean isLongLived() {
            return true;
        }

        public void run() {

            synchronized (localMonitor) {
                subscriptionTaskRunning = true;
            }

            try {
                connection = connectionFactory.getConnection();
                if (connection.isSubscribed()) {
                    throw new IllegalStateException(
                            "Retrieved connection is already subscribed; aborting listening");
                }

                boolean asyncConnection = ConnectionUtils.isAsync(connectionFactory);

                // NB: sync drivers' Xsubscribe calls block, so we notify the RDMLC before performing the actual subscription.
                if (!asyncConnection) {
                    synchronized (monitor) {
                        monitor.notify();
                    }
                }

                SubscriptionPresentCondition subscriptionPresent = eventuallyPerformSubscription();

                if (asyncConnection) {
                    SpinBarrier.waitFor(subscriptionPresent, getMaxSubscriptionRegistrationWaitingTime());

                    synchronized (monitor) {
                        monitor.notify();
                    }
                }
            } catch (Throwable t) {
                handleSubscriptionException(t);
            } finally {
                // this block is executed once the subscription thread has ended, this may or may not mean
                // the connection has been unsubscribed, depending on driver
                synchronized (localMonitor) {
                    subscriptionTaskRunning = false;
                    localMonitor.notify();
                }
            }
        }

        /**
         * Performs a potentially asynchronous registration of a subscription.
         *
         * @return #SubscriptionPresentCondition that can serve as a handle to check whether the subscription is ready.
         */
        private SubscriptionPresentCondition eventuallyPerformSubscription() {

            SubscriptionPresentCondition condition = null;

            if (channelMapping.isEmpty()) {

                condition = new PatternSubscriptionPresentCondition();
                connection.pSubscribe(new DispatchMessageListener(), unwrap(patternMapping.keySet()));
            } else {

                if (patternMapping.isEmpty()) {
                    condition = new SubscriptionPresentCondition();
                } else {
                    // schedule the rest of the subscription
                    subscriptionExecutor.execute(new PatternSubscriptionTask());
                    condition = new PatternSubscriptionPresentCondition();
                }

                connection.subscribe(new DispatchMessageListener(), unwrap(channelMapping.keySet()));
            }

            return condition;
        }

        /**
         * Checks whether the current connection has an associated subscription.
         *
         * @author Thomas Darimont
         */
        private class SubscriptionPresentCondition implements Condition {

            public boolean passes() {
                return connection.isSubscribed();
            }
        }

        /**
         * Checks whether the current connection has an associated pattern subscription.
         *
         * @author Thomas Darimont
         * @see org.springframework.data.redis.listener.RedisMessageListenerContainer.SubscriptionTask.SubscriptionPresentTestCondition
         */
        private class PatternSubscriptionPresentCondition extends SubscriptionPresentCondition {

            @Override
            public boolean passes() {
                return super.passes() && !CollectionUtils.isEmpty(connection.getSubscription().getPatterns());
            }
        }

        private byte[][] unwrap(Collection<ByteArrayWrapper> holders) {
            if (CollectionUtils.isEmpty(holders)) {
                return new byte[0][];
            }

            byte[][] unwrapped = new byte[holders.size()][];

            int index = 0;
            for (ByteArrayWrapper arrayHolder : holders) {
                unwrapped[index++] = arrayHolder.getArray();
            }

            return unwrapped;
        }

        void cancel() {

            if (!listening || connection == null) {
                return;
            }

            listening = false;

            if (logger.isTraceEnabled()) {
                logger.trace("Cancelling Redis subscription...");
            }

            Subscription sub = connection.getSubscription();

            if (sub != null) {

                synchronized (localMonitor) {

                    if (logger.isTraceEnabled()) {
                        logger.trace("Unsubscribing from all channels");
                    }

                    try {
                        sub.close();
                    } catch (Exception e) {
                        logger.warn("Unable to unsubscribe from subscriptions", e);
                    }

                    if (subscriptionTaskRunning) {
                        try {
                            localMonitor.wait(subscriptionWait);
                        } catch (InterruptedException e) {
                            // Stop waiting
                            Thread.currentThread().interrupt();
                        }
                    }

                    if (!subscriptionTaskRunning) {
                        closeConnection();
                    } else {
                        logger.warn("Unable to close connection. Subscription task still running");
                    }
                }
            }
        }

        void closeConnection() {

            if (connection != null) {
                logger.trace("Closing connection");
                try {
                    connection.close();
                } catch (Exception e) {
                    logger.warn("Error closing subscription connection", e);
                }
                connection = null;
            }
        }

        void subscribeChannel(byte[]... channels) {

            if (channels != null && channels.length > 0) {
                if (connection != null) {
                    synchronized (localMonitor) {
                        Subscription sub = connection.getSubscription();
                        if (sub != null) {
                            sub.subscribe(channels);
                        }
                    }
                }
            }
        }

        void subscribePattern(byte[]... patterns) {
            if (patterns != null && patterns.length > 0) {
                if (connection != null) {
                    synchronized (localMonitor) {
                        Subscription sub = connection.getSubscription();
                        if (sub != null) {
                            sub.pSubscribe(patterns);
                        }
                    }
                }
            }
        }

        void unsubscribeChannel(byte[]... channels) {
            if (channels != null && channels.length > 0) {
                if (connection != null) {
                    synchronized (localMonitor) {
                        Subscription sub = connection.getSubscription();
                        if (sub != null) {
                            sub.unsubscribe(channels);
                        }
                    }
                }
            }
        }

        void unsubscribePattern(byte[]... patterns) {
            if (patterns != null && patterns.length > 0) {
                if (connection != null) {
                    synchronized (localMonitor) {
                        Subscription sub = connection.getSubscription();
                        if (sub != null) {
                            sub.pUnsubscribe(patterns);
                        }
                    }
                }
            }
        }
    }

    /**
     * Actual message dispatcher/multiplexer.
     *
     * @author Costin Leau
     */
    private class DispatchMessageListener implements MessageListener {

        @Override
        public void onMessage(Message message, @Nullable byte[] pattern) {
            Collection<MessageListener> listeners = null;

            // if it's a pattern, disregard channel
            if (pattern != null && pattern.length > 0) {
                listeners = patternMapping.get(new ByteArrayWrapper(pattern));
            } else {
                pattern = null;
                // do channel matching first
                listeners = channelMapping.get(new ByteArrayWrapper(message.getChannel()));
            }

            if (!CollectionUtils.isEmpty(listeners)) {
                dispatchMessage(listeners, message, pattern);
            }
        }
    }

    private void dispatchMessage(Collection<MessageListener> listeners, final Message message,
            final byte[] pattern) {
        final byte[] source = (pattern != null ? pattern.clone() : message.getChannel());

        for (final MessageListener messageListener : listeners) {
            taskExecutor.execute(() -> processMessage(messageListener, message, source));
        }
    }

    /**
     * Specify the interval between recovery attempts, in <b>milliseconds</b>. The default is 5000 ms, that is, 5 seconds.
     *
     * @see #handleSubscriptionException
     */
    public void setRecoveryInterval(long recoveryInterval) {
        this.recoveryInterval = recoveryInterval;
    }

    public long getMaxSubscriptionRegistrationWaitingTime() {
        return maxSubscriptionRegistrationWaitingTime;
    }

    /**
     * Specify the max time to wait for subscription registrations, in <b>milliseconds</b>. The default is 2000ms, that
     * is, 2 second.
     *
     * @param maxSubscriptionRegistrationWaitingTime
     * @see #DEFAULT_SUBSCRIPTION_REGISTRATION_WAIT_TIME
     */
    public void setMaxSubscriptionRegistrationWaitingTime(long maxSubscriptionRegistrationWaitingTime) {
        this.maxSubscriptionRegistrationWaitingTime = maxSubscriptionRegistrationWaitingTime;
    }

    /**
     * @author Jennifer Hickey
     * @author Thomas Darimont Note: Placed here to avoid API exposure.
     */
    private static abstract class SpinBarrier {

        /**
         * Periodically tests, in 100ms intervals, for a condition until it is met or a timeout occurs.
         *
         * @param condition The condition to periodically test
         * @param timeout The timeout
         * @return true if condition passes, false if condition does not pass within timeout
         */
        static boolean waitFor(Condition condition, long timeout) {

            long startTime = System.currentTimeMillis();

            try {
                while (!timedOut(startTime, timeout)) {
                    if (condition.passes()) {
                        return true;
                    }

                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

            return false;
        }

        private static boolean timedOut(long startTime, long timeout) {
            return (startTime + timeout) < System.currentTimeMillis();
        }
    }

    /**
     * A condition to test periodically, used in conjunction with
     * {@link org.springframework.data.redis.listener.RedisMessageListenerContainer.SpinBarrier}
     *
     * @author Jennifer Hickey
     * @author Thomas Darimont Note: Placed here to avoid API exposure.
     */
    private static interface Condition {

        /**
         * @return true if condition passes
         */
        boolean passes();
    }
}