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

org.springframework.data.redis.listener.RedisMessageListenerContainer Maven / Gradle / Ivy

There is a newer version: 3.2.5
Show newest version
/*
 * Copyright 2011-2018 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
 *
 *      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 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.
 * 

* 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. *

* Note the container uses the connection in a lazy fashion (the connection is used only if at least one listener is * configured). *

* 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> patternMapping = new ConcurrentHashMap<>(); // lookup map between channels and listeners private final Map> channelMapping = new ConcurrentHashMap<>(); // lookup map between listeners and channels private final Map> listenerTopics = new ConcurrentHashMap<>(); private final SubscriptionTask subscriptionTask = new SubscriptionTask(); private volatile RedisSerializer 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. *

* 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. *

* 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. *

* 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 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 no 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. *

* 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> 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 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. *

* 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 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. *

* 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. emptySet()); } private void initMapping(Map> listeners) { // stop the listener if currently running if (isRunning()) { subscriptionTask.cancel(); } patternMapping.clear(); channelMapping.clear(); listenerTopics.clear(); if (!CollectionUtils.isEmpty(listeners)) { for (Map.Entry> 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 topics) { Assert.notNull(listener, "a valid listener is required"); Assert.notEmpty(topics, "at least one topic is required"); List channels = new ArrayList<>(topics.size()); List patterns = new ArrayList<>(topics.size()); boolean trace = logger.isTraceEnabled(); // add listener mapping Set 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 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 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 topics) { boolean trace = logger.isTraceEnabled(); // check stop listening case if (listener == null && CollectionUtils.isEmpty(topics)) { subscriptionTask.cancel(); return; } List channelsToRemove = new ArrayList<>(); List patternsToRemove = new ArrayList<>(); // check unsubscribe all topics case if (CollectionUtils.isEmpty(topics)) { Set 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> mapping, List topicToRemove) { Collection listeners = mapping.get(holder); Collection 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 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 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 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 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 milliseconds. 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 milliseconds. 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(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy