diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/RedisListeningContainer.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/RedisListeningContainer.java
index 683d3d914..109862ac3 100644
--- a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/RedisListeningContainer.java
+++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/RedisListeningContainer.java
@@ -15,8 +15,10 @@
*/
package org.springframework.data.keyvalue.redis.listener;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
@@ -34,6 +36,7 @@ import org.springframework.data.keyvalue.redis.connection.Message;
import org.springframework.data.keyvalue.redis.connection.MessageListener;
import org.springframework.data.keyvalue.redis.connection.RedisConnection;
import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory;
+import org.springframework.data.keyvalue.redis.connection.Subscription;
import org.springframework.data.keyvalue.redis.serializer.RedisSerializer;
import org.springframework.data.keyvalue.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.SchedulingAwareRunnable;
@@ -171,7 +174,7 @@ public class RedisListeningContainer implements InitializingBean, DisposableBean
@Override
public void stop() {
running = false;
- throw new UnsupportedOperationException();
+ subscriptionTask.cancel();
}
/**
@@ -213,7 +216,8 @@ public class RedisListeningContainer implements InitializingBean, DisposableBean
* 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 thread (depending on whether there are any listeners registered or not).
+ * 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.
*/
@@ -279,29 +283,41 @@ public class RedisListeningContainer implements InitializingBean, DisposableBean
}
/**
- * Method inspecting whether listening for messages (and thus using a thread) is actually needed.
+ * Method inspecting whether listening for messages (and thus using a thread) is actually needed and triggering it.
*/
private void lazyListen() {
boolean debug = log.isDebugEnabled();
+ boolean started = false;
- if (channelMapping.size() > 0 || patternMapping.size() > 0) {
- subscriptionExecutor.execute(subscriptionTask);
- listening = true;
+ if (!listening) {
+ synchronized (monitor) {
+ if (!listening) {
+ if (channelMapping.size() > 0 || patternMapping.size() > 0) {
+ subscriptionExecutor.execute(subscriptionTask);
+ listening = true;
+ started = true;
+ }
+ }
+ else {
+ listening = false;
+ }
- if (debug) {
- log.debug("Started listening for Redis messages");
}
- }
- else {
- listening = false;
if (debug) {
- log.debug("Postpone listening for Redis messages until actual listeners are added");
+ if (started) {
+ log.debug("Started listening for Redis messages");
+ }
+ else {
+ log.debug("Postpone listening for Redis messages until actual listeners are added");
+ }
}
}
}
-
private void addListener(MessageListener listener, Collection topics) {
+ List channels = new ArrayList(topics.size());
+ List patterns = new ArrayList(topics.size());
+
for (Topic topic : topics) {
ArrayHolder holder = new ArrayHolder(serializer.serialize(topic.getTopic()));
@@ -313,6 +329,7 @@ public class RedisListeningContainer implements InitializingBean, DisposableBean
channelMapping.put(holder, collection);
}
collection.add(listener);
+ channels.add(holder.array);
}
else if (topic instanceof PatternTopic) {
@@ -322,12 +339,22 @@ public class RedisListeningContainer implements InitializingBean, DisposableBean
patternMapping.put(holder, collection);
}
collection.add(listener);
+ patterns.add(holder.array);
}
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()][]));
+ }
+ else {
+ lazyListen();
+ }
}
/**
@@ -338,6 +365,52 @@ public class RedisListeningContainer implements InitializingBean, DisposableBean
*/
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 = 1000;
+ private long ROUNDS = 3;
+
+ @Override
+ public boolean isLongLived() {
+ return false;
+ }
+
+ @Override
+ 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 != null && connection.isSubscribed()) {
+ done = true;
+ connection.getSubscription().pSubscribe(unwrap(patternMapping.keySet()));
+ }
+ else {
+ try {
+ Thread.sleep(WAIT);
+ } catch (InterruptedException ex) {
+ done = true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private volatile RedisConnection connection;
+ private final Object localMonitor = new Object();
+
@Override
public boolean isLongLived() {
return true;
@@ -345,10 +418,9 @@ public class RedisListeningContainer implements InitializingBean, DisposableBean
@Override
public void run() {
- RedisConnection connection = connectionFactory.getConnection();
+ connection = connectionFactory.getConnection();
try {
if (connection.isSubscribed()) {
- listening = false;
throw new IllegalStateException("Retrieved connection is already subscribed; aborting listening");
}
@@ -357,14 +429,26 @@ public class RedisListeningContainer implements InitializingBean, DisposableBean
// subscribe one way or the other
// and schedule the rest
if (!channelMapping.isEmpty()) {
+ // schedule the rest of the subscription
+ subscriptionExecutor.execute(new PatternSubscriptionTask());
connection.subscribe(new DispatchMessageListener(), unwrap(channelMapping.keySet()));
}
else {
connection.pSubscribe(new DispatchMessageListener(), unwrap(patternMapping.keySet()));
}
+
} finally {
+ // this block is executed once the subscription has ended
+ // meaning cleanup is required
+ listening = false;
+
if (connection != null) {
- connection.close();
+ synchronized (localMonitor) {
+ if (connection != null) {
+ connection.close();
+ connection = null;
+ }
+ }
}
}
}
@@ -383,6 +467,80 @@ public class RedisListeningContainer implements InitializingBean, DisposableBean
return unwrapped;
}
+
+ void cancel() {
+ if (connection != null) {
+ synchronized (localMonitor) {
+ if (connection != null) {
+ Subscription sub = connection.getSubscription();
+ if (sub != null) {
+ sub.pUnsubscribe();
+ sub.unsubscribe();
+ }
+ }
+ }
+ }
+ }
+
+ void subscribeChannel(byte[]... channels) {
+ if (channels != null && channels.length > 0) {
+ if (connection != null) {
+ synchronized (localMonitor) {
+ if (connection != null) {
+ 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) {
+ if (connection != null) {
+ 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) {
+ if (connection != null) {
+ 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) {
+ if (connection != null) {
+ Subscription sub = connection.getSubscription();
+ if (sub != null) {
+ sub.pUnsubscribe(patterns);
+ }
+ }
+ }
+ }
+ }
+ }
}
/**