remove listeners programatically in RMLContainer
DATAREDIS-107 listeners can be removed not just added programatically fix bug causing double message dispatch for pattern subscriptions introduce more pubsub tests
This commit is contained in:
@@ -20,6 +20,7 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
@@ -42,6 +43,7 @@ import org.springframework.data.redis.connection.util.ByteArrayWrapper;
|
|||||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
import org.springframework.scheduling.SchedulingAwareRunnable;
|
import org.springframework.scheduling.SchedulingAwareRunnable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.ErrorHandler;
|
import org.springframework.util.ErrorHandler;
|
||||||
@@ -55,7 +57,11 @@ import org.springframework.util.ErrorHandler;
|
|||||||
* the message dispatch being done through the task executor.
|
* the message dispatch being done through the task executor.
|
||||||
*
|
*
|
||||||
* <p/>
|
* <p/>
|
||||||
* Note the container uses the connection in a lazy fashion (the connection is used only if at least one listener is configured).
|
* 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 Costin Leau
|
||||||
*/
|
*/
|
||||||
@@ -65,7 +71,6 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
protected final Log logger = LogFactory.getLog(getClass());
|
protected final Log logger = LogFactory.getLog(getClass());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default thread name prefix: "RedisListeningContainer-".
|
* Default thread name prefix: "RedisListeningContainer-".
|
||||||
*/
|
*/
|
||||||
@@ -104,13 +109,15 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
private final Map<ByteArrayWrapper, Collection<MessageListener>> patternMapping = new ConcurrentHashMap<ByteArrayWrapper, Collection<MessageListener>>();
|
private final Map<ByteArrayWrapper, Collection<MessageListener>> patternMapping = new ConcurrentHashMap<ByteArrayWrapper, Collection<MessageListener>>();
|
||||||
// lookup map between channels and listeners
|
// lookup map between channels and listeners
|
||||||
private final Map<ByteArrayWrapper, Collection<MessageListener>> channelMapping = new ConcurrentHashMap<ByteArrayWrapper, Collection<MessageListener>>();
|
private final Map<ByteArrayWrapper, Collection<MessageListener>> channelMapping = new ConcurrentHashMap<ByteArrayWrapper, Collection<MessageListener>>();
|
||||||
|
// lookup map between listeners and channels
|
||||||
|
private final Map<MessageListener, Set<Topic>> listenerTopics = new ConcurrentHashMap<MessageListener, Set<Topic>>();
|
||||||
|
|
||||||
private final SubscriptionTask subscriptionTask = new SubscriptionTask();
|
private final SubscriptionTask subscriptionTask = new SubscriptionTask();
|
||||||
|
|
||||||
private volatile RedisSerializer<String> serializer = new StringRedisSerializer();
|
private volatile RedisSerializer<String> serializer = new StringRedisSerializer();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void afterPropertiesSet() {
|
public void afterPropertiesSet() {
|
||||||
if (taskExecutor == null) {
|
if (taskExecutor == null) {
|
||||||
manageExecutor = true;
|
manageExecutor = true;
|
||||||
@@ -135,7 +142,7 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
return new SimpleAsyncTaskExecutor(threadNamePrefix);
|
return new SimpleAsyncTaskExecutor(threadNamePrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void destroy() throws Exception {
|
public void destroy() throws Exception {
|
||||||
initialized = false;
|
initialized = false;
|
||||||
|
|
||||||
@@ -152,29 +159,29 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public boolean isAutoStartup() {
|
public boolean isAutoStartup() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void stop(Runnable callback) {
|
public void stop(Runnable callback) {
|
||||||
stop();
|
stop();
|
||||||
callback.run();
|
callback.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public int getPhase() {
|
public int getPhase() {
|
||||||
// start the latest
|
// start the latest
|
||||||
return Integer.MAX_VALUE;
|
return Integer.MAX_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public boolean isRunning() {
|
public boolean isRunning() {
|
||||||
return running;
|
return running;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
if (!running) {
|
if (!running) {
|
||||||
running = true;
|
running = true;
|
||||||
@@ -196,13 +203,14 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
if (isRunning()) {
|
if (isRunning()) {
|
||||||
running = false;
|
running = false;
|
||||||
synchronized (monitor) {
|
synchronized (monitor) {
|
||||||
boolean shouldWait = listening;
|
boolean shouldWait = listening;
|
||||||
subscriptionTask.cancel();
|
subscriptionTask.cancel();
|
||||||
|
listening = false;
|
||||||
if (shouldWait) {
|
if (shouldWait) {
|
||||||
try {
|
try {
|
||||||
monitor.wait(initWait);
|
monitor.wait(initWait);
|
||||||
@@ -300,7 +308,7 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
this.connectionFactory = connectionFactory;
|
this.connectionFactory = connectionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void setBeanName(String name) {
|
public void setBeanName(String name) {
|
||||||
this.beanName = name;
|
this.beanName = name;
|
||||||
}
|
}
|
||||||
@@ -388,14 +396,56 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
addMessageListener(listener, Collections.singleton(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) {
|
private void initMapping(Map<? extends MessageListener, Collection<? extends Topic>> listeners) {
|
||||||
// stop the listener if currently running
|
// stop the listener if currently running
|
||||||
if (isRunning()) {
|
if (isRunning()) {
|
||||||
stop();
|
subscriptionTask.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
patternMapping.clear();
|
patternMapping.clear();
|
||||||
channelMapping.clear();
|
channelMapping.clear();
|
||||||
|
listenerTopics.clear();
|
||||||
|
|
||||||
if (!CollectionUtils.isEmpty(listeners)) {
|
if (!CollectionUtils.isEmpty(listeners)) {
|
||||||
for (Map.Entry<? extends MessageListener, Collection<? extends Topic>> entry : listeners.entrySet()) {
|
for (Map.Entry<? extends MessageListener, Collection<? extends Topic>> entry : listeners.entrySet()) {
|
||||||
@@ -440,11 +490,22 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void addListener(MessageListener listener, Collection<? extends Topic> topics) {
|
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<byte[]>(topics.size());
|
List<byte[]> channels = new ArrayList<byte[]>(topics.size());
|
||||||
List<byte[]> patterns = new ArrayList<byte[]>(topics.size());
|
List<byte[]> patterns = new ArrayList<byte[]>(topics.size());
|
||||||
|
|
||||||
boolean trace = logger.isTraceEnabled();
|
boolean trace = logger.isTraceEnabled();
|
||||||
|
|
||||||
|
// add listener mapping
|
||||||
|
Set<Topic> set = listenerTopics.get(listener);
|
||||||
|
if (set == null) {
|
||||||
|
set = new CopyOnWriteArraySet<Topic>();
|
||||||
|
listenerTopics.put(listener, set);
|
||||||
|
}
|
||||||
|
set.addAll(topics);
|
||||||
|
|
||||||
for (Topic topic : topics) {
|
for (Topic topic : topics) {
|
||||||
|
|
||||||
ByteArrayWrapper holder = new ByteArrayWrapper(serializer.serialize(topic.getTopic()));
|
ByteArrayWrapper holder = new ByteArrayWrapper(serializer.serialize(topic.getTopic()));
|
||||||
@@ -487,6 +548,85 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
logger.debug("Stopped listening for Redis messages");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<byte[]> channelsToRemove = new ArrayList<byte[]>();
|
||||||
|
List<byte[]> patternsToRemove = new ArrayList<byte[]>();
|
||||||
|
|
||||||
|
// 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() + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the current listening state
|
||||||
|
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);
|
||||||
|
if (listeners != null) {
|
||||||
|
if (listener != null) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
// remove all listeners for the given topic
|
||||||
|
else {
|
||||||
|
for (MessageListener messageListener : listeners) {
|
||||||
|
Set<Topic> topics = listenerTopics.get(messageListener);
|
||||||
|
if (topics != null) {
|
||||||
|
topics.remove(topic);
|
||||||
|
}
|
||||||
|
if (topics.isEmpty()) {
|
||||||
|
listenerTopics.remove(messageListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listener == null || listeners.isEmpty()) {
|
||||||
|
mapping.remove(holder);
|
||||||
|
topicToRemove.add(holder.getArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runnable used for Redis subscription. Implemented as a dedicated class to provide as many hints
|
* Runnable used for Redis subscription. Implemented as a dedicated class to provide as many hints
|
||||||
@@ -509,12 +649,12 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
private long WAIT = 500;
|
private long WAIT = 500;
|
||||||
private long ROUNDS = 3;
|
private long ROUNDS = 3;
|
||||||
|
|
||||||
|
|
||||||
public boolean isLongLived() {
|
public boolean isLongLived() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void run() {
|
public void run() {
|
||||||
// wait for subscription to be initialized
|
// wait for subscription to be initialized
|
||||||
boolean done = false;
|
boolean done = false;
|
||||||
@@ -542,12 +682,12 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
private volatile RedisConnection connection;
|
private volatile RedisConnection connection;
|
||||||
private final Object localMonitor = new Object();
|
private final Object localMonitor = new Object();
|
||||||
|
|
||||||
|
|
||||||
public boolean isLongLived() {
|
public boolean isLongLived() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void run() {
|
public void run() {
|
||||||
connection = connectionFactory.getConnection();
|
connection = connectionFactory.getConnection();
|
||||||
try {
|
try {
|
||||||
@@ -556,7 +696,6 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NB: each Xsubscribe call blocks
|
// NB: each Xsubscribe call blocks
|
||||||
|
|
||||||
synchronized (monitor) {
|
synchronized (monitor) {
|
||||||
monitor.notify();
|
monitor.notify();
|
||||||
}
|
}
|
||||||
@@ -613,6 +752,9 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
}
|
}
|
||||||
|
|
||||||
void cancel() {
|
void cancel() {
|
||||||
|
if (!listening) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
synchronized (localMonitor) {
|
synchronized (localMonitor) {
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
@@ -694,48 +836,35 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
|
|||||||
*/
|
*/
|
||||||
private class DispatchMessageListener implements MessageListener {
|
private class DispatchMessageListener implements MessageListener {
|
||||||
|
|
||||||
|
|
||||||
public void onMessage(Message message, byte[] pattern) {
|
public void onMessage(Message message, byte[] pattern) {
|
||||||
// do channel matching first
|
Collection<MessageListener> listeners = null;
|
||||||
byte[] channel = message.getChannel();
|
|
||||||
|
|
||||||
Collection<MessageListener> ch = channelMapping.get(new ByteArrayWrapper(channel));
|
// if it's a pattern, disregard channel
|
||||||
Collection<MessageListener> pt = null;
|
|
||||||
|
|
||||||
// followed by pattern matching
|
|
||||||
if (pattern != null && pattern.length > 0) {
|
if (pattern != null && pattern.length > 0) {
|
||||||
pt = patternMapping.get(new ByteArrayWrapper(pattern));
|
listeners = patternMapping.get(new ByteArrayWrapper(pattern));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pattern = null;
|
||||||
|
// do channel matching first
|
||||||
|
listeners = channelMapping.get(new ByteArrayWrapper(message.getChannel()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!CollectionUtils.isEmpty(ch)) {
|
if (!CollectionUtils.isEmpty(listeners)) {
|
||||||
dispatchChannels(ch, message);
|
dispatchMessage(listeners, message, pattern);
|
||||||
}
|
|
||||||
|
|
||||||
if (!CollectionUtils.isEmpty(pt)) {
|
|
||||||
dispatchPatterns(pt, message, pattern);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dispatchChannels(Collection<MessageListener> ch, final Message message) {
|
|
||||||
for (final MessageListener messageListener : ch) {
|
|
||||||
taskExecutor.execute(new Runnable() {
|
|
||||||
|
|
||||||
public void run() {
|
|
||||||
processMessage(messageListener, message, message.getChannel());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dispatchPatterns(Collection<MessageListener> pt, final Message message, final byte[] pattern) {
|
|
||||||
for (final MessageListener messageListener : pt) {
|
|
||||||
taskExecutor.execute(new Runnable() {
|
|
||||||
|
|
||||||
public void run() {
|
|
||||||
processMessage(messageListener, message, pattern.clone());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
processMessage(messageListener, message, source);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2011-2013 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 static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.BlockingDeque;
|
||||||
|
import java.util.concurrent.LinkedBlockingDeque;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||||
|
import org.springframework.core.task.SyncTaskExecutor;
|
||||||
|
import org.springframework.data.redis.ConnectionFactoryTracker;
|
||||||
|
import org.springframework.data.redis.SettingsUtils;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Costin Leau
|
||||||
|
*/
|
||||||
|
public class PubSubResubscribeTests {
|
||||||
|
|
||||||
|
private static final String CHANNEL = "pubsub::test";
|
||||||
|
private final Long ZERO = Long.valueOf(0);
|
||||||
|
private final Long ONE = Long.valueOf(1);
|
||||||
|
private final Long TWO = Long.valueOf(2);
|
||||||
|
|
||||||
|
protected RedisMessageListenerContainer container;
|
||||||
|
protected RedisConnectionFactory factory;
|
||||||
|
protected RedisTemplate template;
|
||||||
|
|
||||||
|
private final BlockingDeque<String> bag = new LinkedBlockingDeque<String>(99);
|
||||||
|
|
||||||
|
private final Object handler = new Object() {
|
||||||
|
public void handleMessage(String message) {
|
||||||
|
System.out.println(message);
|
||||||
|
bag.add(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final MessageListenerAdapter adapter = new MessageListenerAdapter(handler);
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void cleanUp() {
|
||||||
|
ConnectionFactoryTracker.cleanUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
JedisConnectionFactory jedisConnFactory = new JedisConnectionFactory();
|
||||||
|
jedisConnFactory.setUsePool(false);
|
||||||
|
jedisConnFactory.setPort(SettingsUtils.getPort());
|
||||||
|
jedisConnFactory.setHostName(SettingsUtils.getHost());
|
||||||
|
jedisConnFactory.setDatabase(2);
|
||||||
|
jedisConnFactory.afterPropertiesSet();
|
||||||
|
|
||||||
|
factory = jedisConnFactory;
|
||||||
|
|
||||||
|
template = new StringRedisTemplate(jedisConnFactory);
|
||||||
|
ConnectionFactoryTracker.add(template.getConnectionFactory());
|
||||||
|
|
||||||
|
bag.clear();
|
||||||
|
|
||||||
|
adapter.setSerializer(template.getValueSerializer());
|
||||||
|
adapter.afterPropertiesSet();
|
||||||
|
|
||||||
|
container = new RedisMessageListenerContainer();
|
||||||
|
container.setConnectionFactory(template.getConnectionFactory());
|
||||||
|
container.setBeanName("container");
|
||||||
|
container.addMessageListener(adapter, new ChannelTopic(CHANNEL));
|
||||||
|
container.setTaskExecutor(new SyncTaskExecutor());
|
||||||
|
container.setSubscriptionExecutor(new SimpleAsyncTaskExecutor());
|
||||||
|
container.afterPropertiesSet();
|
||||||
|
container.start();
|
||||||
|
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() throws Exception {
|
||||||
|
container.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testContainerPatternResubscribe() throws Exception {
|
||||||
|
String payload1 = "do";
|
||||||
|
String payload2 = "re mi";
|
||||||
|
|
||||||
|
final String PATTERN = "p*";
|
||||||
|
final String ANOTHER_CHANNEL = "pubsub::test::extra";
|
||||||
|
|
||||||
|
MessageListenerAdapter anotherListener = new MessageListenerAdapter(handler);
|
||||||
|
anotherListener.setSerializer(template.getValueSerializer());
|
||||||
|
anotherListener.afterPropertiesSet();
|
||||||
|
|
||||||
|
// remove adapter from all channels
|
||||||
|
container.removeMessageListener(adapter);
|
||||||
|
container.addMessageListener(anotherListener, new PatternTopic(PATTERN));
|
||||||
|
|
||||||
|
// test no messages are sent just to patterns
|
||||||
|
assertEquals(ONE, template.convertAndSend(CHANNEL, payload1));
|
||||||
|
assertEquals(ONE, template.convertAndSend(ANOTHER_CHANNEL, payload2));
|
||||||
|
|
||||||
|
List<String> msgs = new ArrayList<String>();
|
||||||
|
msgs.add(bag.poll(1, TimeUnit.SECONDS));
|
||||||
|
msgs.add(bag.poll(1, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
assertEquals(2, msgs.size());
|
||||||
|
// bind original listener on another channel
|
||||||
|
container.addMessageListener(adapter, new ChannelTopic(ANOTHER_CHANNEL));
|
||||||
|
|
||||||
|
assertEquals(ONE, template.convertAndSend(CHANNEL, payload1));
|
||||||
|
assertEquals(TWO, template.convertAndSend(ANOTHER_CHANNEL, payload2));
|
||||||
|
|
||||||
|
msgs.add(bag.poll(1, TimeUnit.SECONDS));
|
||||||
|
msgs.add(bag.poll(1, TimeUnit.SECONDS));
|
||||||
|
msgs.add(bag.poll(1, TimeUnit.SECONDS));
|
||||||
|
// this message will not arrive on time
|
||||||
|
assertNull(bag.poll(1, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
// same message received first per channel subscription, second based on the pattern
|
||||||
|
assertEquals(5, msgs.size());
|
||||||
|
|
||||||
|
assertTrue(msgs.contains(payload1));
|
||||||
|
assertTrue(msgs.contains(payload2));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
package org.springframework.data.redis.listener;
|
package org.springframework.data.redis.listener;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -33,6 +34,8 @@ import org.junit.Test;
|
|||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.junit.runners.Parameterized;
|
import org.junit.runners.Parameterized;
|
||||||
import org.junit.runners.Parameterized.Parameters;
|
import org.junit.runners.Parameterized.Parameters;
|
||||||
|
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||||
|
import org.springframework.core.task.SyncTaskExecutor;
|
||||||
import org.springframework.data.redis.ConnectionFactoryTracker;
|
import org.springframework.data.redis.ConnectionFactoryTracker;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
|
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
|
||||||
@@ -47,6 +50,9 @@ import org.springframework.data.redis.support.collections.ObjectFactory;
|
|||||||
public class PubSubTests<T> {
|
public class PubSubTests<T> {
|
||||||
|
|
||||||
private static final String CHANNEL = "pubsub::test";
|
private static final String CHANNEL = "pubsub::test";
|
||||||
|
private final Long ZERO = Long.valueOf(0);
|
||||||
|
private final Long ONE = Long.valueOf(1);
|
||||||
|
private final Long TWO = Long.valueOf(2);
|
||||||
|
|
||||||
protected RedisMessageListenerContainer container;
|
protected RedisMessageListenerContainer container;
|
||||||
protected ObjectFactory<T> factory;
|
protected ObjectFactory<T> factory;
|
||||||
@@ -64,6 +70,8 @@ public class PubSubTests<T> {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
|
bag.clear();
|
||||||
|
|
||||||
adapter.setSerializer(template.getValueSerializer());
|
adapter.setSerializer(template.getValueSerializer());
|
||||||
adapter.afterPropertiesSet();
|
adapter.afterPropertiesSet();
|
||||||
|
|
||||||
@@ -71,6 +79,8 @@ public class PubSubTests<T> {
|
|||||||
container.setConnectionFactory(template.getConnectionFactory());
|
container.setConnectionFactory(template.getConnectionFactory());
|
||||||
container.setBeanName("container");
|
container.setBeanName("container");
|
||||||
container.addMessageListener(adapter, Arrays.asList(new ChannelTopic(CHANNEL)));
|
container.addMessageListener(adapter, Arrays.asList(new ChannelTopic(CHANNEL)));
|
||||||
|
container.setTaskExecutor(new SyncTaskExecutor());
|
||||||
|
container.setSubscriptionExecutor(new SimpleAsyncTaskExecutor());
|
||||||
container.afterPropertiesSet();
|
container.afterPropertiesSet();
|
||||||
container.start();
|
container.start();
|
||||||
|
|
||||||
@@ -111,15 +121,13 @@ public class PubSubTests<T> {
|
|||||||
String payload1 = "do";
|
String payload1 = "do";
|
||||||
String payload2 = "re mi";
|
String payload2 = "re mi";
|
||||||
|
|
||||||
template.convertAndSend(CHANNEL, payload1);
|
assertEquals(ONE, template.convertAndSend(CHANNEL, payload1));
|
||||||
template.convertAndSend(CHANNEL, payload2);
|
assertEquals(ONE, template.convertAndSend(CHANNEL, payload2));
|
||||||
|
|
||||||
Set<String> set = new LinkedHashSet<String>();
|
Set<String> set = new LinkedHashSet<String>();
|
||||||
set.add(bag.poll(1, TimeUnit.SECONDS));
|
set.add(bag.poll(1, TimeUnit.SECONDS));
|
||||||
set.add(bag.poll(1, TimeUnit.SECONDS));
|
set.add(bag.poll(1, TimeUnit.SECONDS));
|
||||||
|
|
||||||
System.out.println(set);
|
|
||||||
|
|
||||||
assertTrue(set.contains(payload1));
|
assertTrue(set.contains(payload1));
|
||||||
assertTrue(set.contains(payload2));
|
assertTrue(set.contains(payload2));
|
||||||
}
|
}
|
||||||
@@ -134,4 +142,55 @@ public class PubSubTests<T> {
|
|||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
assertEquals(COUNT, bag.size());
|
assertEquals(COUNT, bag.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testContainerUnsubscribe() throws Exception {
|
||||||
|
String payload1 = "do";
|
||||||
|
String payload2 = "re mi";
|
||||||
|
|
||||||
|
container.removeMessageListener(adapter, new ChannelTopic(CHANNEL));
|
||||||
|
assertEquals(ZERO, template.convertAndSend(CHANNEL, payload1));
|
||||||
|
assertEquals(ZERO, template.convertAndSend(CHANNEL, payload2));
|
||||||
|
|
||||||
|
Set<String> set = new LinkedHashSet<String>();
|
||||||
|
set.add(bag.poll(1, TimeUnit.SECONDS));
|
||||||
|
set.add(bag.poll(1, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
assertFalse(set.contains(payload1));
|
||||||
|
assertFalse(set.contains(payload2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testContainerChannelResubscribe() throws Exception {
|
||||||
|
String payload1 = "do";
|
||||||
|
String payload2 = "re mi";
|
||||||
|
|
||||||
|
String anotherPayload1 = "od";
|
||||||
|
String anotherPayload2 = "mi er";
|
||||||
|
|
||||||
|
String ANOTHER_CHANNEL = "pubsub::test::extra";
|
||||||
|
|
||||||
|
// bind listener on another channel
|
||||||
|
container.addMessageListener(adapter, new ChannelTopic(ANOTHER_CHANNEL));
|
||||||
|
container.removeMessageListener(null, new ChannelTopic(CHANNEL));
|
||||||
|
|
||||||
|
assertEquals(ZERO, template.convertAndSend(CHANNEL, payload1));
|
||||||
|
assertEquals(ZERO, template.convertAndSend(CHANNEL, payload2));
|
||||||
|
|
||||||
|
assertEquals(ONE, template.convertAndSend(ANOTHER_CHANNEL, anotherPayload1));
|
||||||
|
assertEquals(ONE, template.convertAndSend(ANOTHER_CHANNEL, anotherPayload2));
|
||||||
|
|
||||||
|
Set<String> set = new LinkedHashSet<String>();
|
||||||
|
set.add(bag.poll(1, TimeUnit.SECONDS));
|
||||||
|
set.add(bag.poll(1, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
System.out.println(set);
|
||||||
|
|
||||||
|
assertFalse(set.contains(payload1));
|
||||||
|
assertFalse(set.contains(payload2));
|
||||||
|
|
||||||
|
assertTrue(set.contains(anotherPayload1));
|
||||||
|
assertTrue(set.contains(anotherPayload2));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ log4j.appender.stdout=org.apache.log4j.ConsoleAppender
|
|||||||
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
|
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
|
||||||
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n
|
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n
|
||||||
|
|
||||||
log4j.category.org.springframework.data.keyvalue.redis.listener=TRACE
|
log4j.category.org.springframework.data.redis.listener=TRACE
|
||||||
|
|
||||||
# for debugging datasource initialization
|
# for debugging datasource initialization
|
||||||
# log4j.category.test.jdbc=DEBUG
|
# log4j.category.test.jdbc=DEBUG
|
||||||
|
|||||||
Reference in New Issue
Block a user