Skip to content

Commit b5adde7

Browse files
author
Thomas Darimont
committed
DATAREDIS-242 - Listener Container start() Returns Before Container is Really Started.
We now wait for the subscription to be present in the connection in case of async connections by periodically checking whether the subscription is available in 100ms intervals. The max time to wait for the async subscription can be configured and is set to 2 seconds by default. Moved SpinBarrier and TestCondition from test packages as static inner classes into RedisMessageListenerContainer. Included ConnectionUtils from tests into official API.
1 parent c6d2d7f commit b5adde7

File tree

3 files changed

+184
-27
lines changed

3 files changed

+184
-27
lines changed

src/test/java/org/springframework/data/redis/connection/ConnectionUtils.java renamed to src/main/java/org/springframework/data/redis/connection/ConnectionUtils.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2011-2013 the original author or authors.
2+
* Copyright 2011-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
2424
* Utilities for examining a {@link RedisConnection}
2525
*
2626
* @author Jennifer Hickey
27+
* @author Thomas Darimont
2728
*
2829
*/
2930
public abstract class ConnectionUtils {

src/main/java/org/springframework/data/redis/listener/RedisMessageListenerContainer.java

Lines changed: 159 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2011-2013 the original author or authors.
2+
* Copyright 2011-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,12 +15,7 @@
1515
*/
1616
package org.springframework.data.redis.listener;
1717

18-
import java.util.ArrayList;
19-
import java.util.Collection;
20-
import java.util.Collections;
21-
import java.util.List;
22-
import java.util.Map;
23-
import java.util.Set;
18+
import java.util.*;
2419
import java.util.concurrent.ConcurrentHashMap;
2520
import java.util.concurrent.CopyOnWriteArraySet;
2621
import java.util.concurrent.Executor;
@@ -35,11 +30,7 @@
3530
import org.springframework.core.task.SimpleAsyncTaskExecutor;
3631
import org.springframework.core.task.TaskExecutor;
3732
import org.springframework.data.redis.RedisConnectionFailureException;
38-
import org.springframework.data.redis.connection.Message;
39-
import org.springframework.data.redis.connection.MessageListener;
40-
import org.springframework.data.redis.connection.RedisConnection;
41-
import org.springframework.data.redis.connection.RedisConnectionFactory;
42-
import org.springframework.data.redis.connection.Subscription;
33+
import org.springframework.data.redis.connection.*;
4334
import org.springframework.data.redis.connection.util.ByteArrayWrapper;
4435
import org.springframework.data.redis.serializer.RedisSerializer;
4536
import org.springframework.data.redis.serializer.StringRedisSerializer;
@@ -68,6 +59,7 @@
6859
* @author Costin Leau
6960
* @author Jennifer Hickey
7061
* @author Way Joke
62+
* @author Thomas Darimont
7163
*/
7264
public class RedisMessageListenerContainer implements InitializingBean, DisposableBean, BeanNameAware, SmartLifecycle {
7365

@@ -86,6 +78,11 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
8678
*/
8779
public static final long DEFAULT_RECOVERY_INTERVAL = 5000;
8880

81+
/**
82+
* The default subscription wait time: 2000 ms = 2 seconds.
83+
*/
84+
public static final long DEFAULT_SUBSCRIPTION_REGISTRATION_WAIT_TIME = 2000L;
85+
8986
private long initWait = TimeUnit.SECONDS.toMillis(5);
9087

9188
private Executor subscriptionExecutor;
@@ -127,6 +124,8 @@ public class RedisMessageListenerContainer implements InitializingBean, Disposab
127124

128125
private long recoveryInterval = DEFAULT_RECOVERY_INTERVAL;
129126

127+
private long maxSubscriptionRegistrationWaitingTime = DEFAULT_SUBSCRIPTION_REGISTRATION_WAIT_TIME;
128+
130129
public void afterPropertiesSet() {
131130
if (taskExecutor == null) {
132131
manageExecutor = true;
@@ -746,24 +745,26 @@ public void run() {
746745
throw new IllegalStateException("Retrieved connection is already subscribed; aborting listening");
747746
}
748747

749-
// NB: some drivers' Xsubscribe calls block
750-
synchronized (monitor) {
751-
monitor.notify();
752-
}
748+
boolean asyncConnection = ConnectionUtils.isAsync(connectionFactory);
753749

754-
// subscribe one way or the other
755-
// and schedule the rest
756-
if (!channelMapping.isEmpty()) {
757-
// schedule the rest of the subscription
758-
if (!patternMapping.isEmpty()) {
759-
subscriptionExecutor.execute(new PatternSubscriptionTask());
750+
// NB: async drivers' Xsubscribe calls block, so we notify the RDMLC before performing the actual subscription.
751+
if(!asyncConnection){
752+
synchronized (monitor) {
753+
monitor.notify();
760754
}
761-
connection.subscribe(new DispatchMessageListener(), unwrap(channelMapping.keySet()));
762755
}
763-
else {
764-
connection.pSubscribe(new DispatchMessageListener(), unwrap(patternMapping.keySet()));
756+
757+
SubscriptionPresentCondition subscriptionPresent = eventuallyPerformSubscription();
758+
759+
760+
if(asyncConnection){
761+
SpinBarrier.waitFor(subscriptionPresent, getMaxSubscriptionRegistrationWaitingTime());
762+
763+
synchronized (monitor){
764+
monitor.notify();
765+
}
765766
}
766-
} catch (Throwable t) {
767+
} catch (Throwable t) {
767768
handleSubscriptionException(t);
768769
} finally {
769770
// this block is executed once the subscription thread has ended, this may or may not mean
@@ -775,6 +776,63 @@ public void run() {
775776
}
776777
}
777778

779+
/**
780+
* Performs a potentially asynchronous registration of a subscription.
781+
*
782+
* @return #SubscriptionPresentCondition that can serve as a handle to check whether the subscription is ready.
783+
*/
784+
private SubscriptionPresentCondition eventuallyPerformSubscription() {
785+
786+
SubscriptionPresentCondition condition = null;
787+
788+
if (channelMapping.isEmpty()) {
789+
790+
condition = new PatternSubscriptionPresentCondition();
791+
connection.pSubscribe(new DispatchMessageListener(), unwrap(patternMapping.keySet()));
792+
} else {
793+
794+
if (patternMapping.isEmpty()) {
795+
condition = new SubscriptionPresentCondition();
796+
} else {
797+
// schedule the rest of the subscription
798+
subscriptionExecutor.execute(new PatternSubscriptionTask());
799+
condition = new PatternSubscriptionPresentCondition();
800+
}
801+
802+
connection.subscribe(new DispatchMessageListener(), unwrap(channelMapping.keySet()));
803+
}
804+
805+
return condition;
806+
}
807+
808+
/**
809+
*
810+
* Checks whether the current connection has an associated subscription.
811+
*
812+
* @author Thomas Darimont
813+
*/
814+
private class SubscriptionPresentCondition implements Condition {
815+
816+
public boolean passes() {
817+
return connection.isSubscribed();
818+
}
819+
}
820+
821+
/**
822+
* Checks whether the current connection has an associated pattern subscription.
823+
*
824+
* @author Thomas Darimont
825+
*
826+
* @see org.springframework.data.redis.listener.RedisMessageListenerContainer.SubscriptionTask.SubscriptionPresentTestCondition
827+
*/
828+
private class PatternSubscriptionPresentCondition extends SubscriptionPresentCondition {
829+
830+
@Override
831+
public boolean passes() {
832+
return super.passes() && !CollectionUtils.isEmpty(connection.getSubscription().getPatterns());
833+
}
834+
}
835+
778836
private byte[][] unwrap(Collection<ByteArrayWrapper> holders) {
779837
if (CollectionUtils.isEmpty(holders)) {
780838
return new byte[0][];
@@ -934,4 +992,79 @@ public void run() {
934992
public void setRecoveryInterval(long recoveryInterval) {
935993
this.recoveryInterval = recoveryInterval;
936994
}
995+
996+
public long getMaxSubscriptionRegistrationWaitingTime() {
997+
return maxSubscriptionRegistrationWaitingTime;
998+
}
999+
1000+
/**
1001+
* Specify the max time to wait for subscription registrations, in <b>milliseconds</b>.
1002+
* The default is 2000ms, that is, 2 second.
1003+
*
1004+
* @param maxSubscriptionRegistrationWaitingTime
1005+
*
1006+
* @see #DEFAULT_SUBSCRIPTION_REGISTRATION_WAIT_TIME
1007+
*/
1008+
public void setMaxSubscriptionRegistrationWaitingTime(long maxSubscriptionRegistrationWaitingTime) {
1009+
this.maxSubscriptionRegistrationWaitingTime = maxSubscriptionRegistrationWaitingTime;
1010+
}
1011+
1012+
/**
1013+
* @author Jennifer Hickey
1014+
* @author Thomas Darimont
1015+
*
1016+
* Note: Placed here to avoid API exposure.
1017+
*/
1018+
private static abstract class SpinBarrier {
1019+
1020+
/**
1021+
* Periodically tests, in 100ms intervals, for a condition until it is met or a timeout occurs.
1022+
*
1023+
* @param condition
1024+
* The condition to periodically test
1025+
* @param timeout
1026+
* The timeout
1027+
* @return true if condition passes, false if condition does not pass within
1028+
* timeout
1029+
*/
1030+
static boolean waitFor(Condition condition, long timeout) {
1031+
1032+
long startTime=System.currentTimeMillis();
1033+
1034+
while(!timedOut(startTime, timeout)) {
1035+
if(condition.passes()) {
1036+
return true;
1037+
}
1038+
try {
1039+
Thread.sleep(100);
1040+
} catch (InterruptedException e) {
1041+
Thread.currentThread().interrupt();
1042+
}
1043+
}
1044+
return false;
1045+
}
1046+
1047+
private static boolean timedOut(long startTime, long timeout) {
1048+
return (startTime + timeout) < System.currentTimeMillis();
1049+
}
1050+
}
1051+
1052+
/**
1053+
*
1054+
* A condition to test periodically, used in conjunction with
1055+
* {@link org.springframework.data.redis.listener.RedisMessageListenerContainer.SpinBarrier}
1056+
*
1057+
* @author Jennifer Hickey
1058+
* @author Thomas Darimont
1059+
*
1060+
* Note: Placed here to avoid API exposure.
1061+
*/
1062+
private static interface Condition {
1063+
1064+
/**
1065+
*
1066+
* @return true if condition passes
1067+
*/
1068+
boolean passes();
1069+
}
9371070
}

src/test/java/org/springframework/data/redis/listener/PubSubTests.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,27 @@ public void testStartNoListeners() {
172172
// DATREDIS-207 This test previously took 5 seconds on start due to monitor wait
173173
container.start();
174174
}
175+
176+
177+
/**
178+
* @see DATAREDIS-251
179+
*/
180+
@SuppressWarnings("unchecked")
181+
@Test
182+
public void testStartListenersToNoSpecificChannelTest() throws InterruptedException {
183+
container.removeMessageListener(adapter, new ChannelTopic(CHANNEL));
184+
container.addMessageListener(adapter, Arrays.asList(new PatternTopic("*")));
185+
container.start();
186+
187+
Thread.sleep(1000); //give the container a little time to recover
188+
189+
T payload = getT();
190+
191+
template.convertAndSend(CHANNEL, payload);
192+
193+
Set<T> set = new LinkedHashSet<T>();
194+
set.add((T) bag.poll(3, TimeUnit.SECONDS));
195+
196+
assertThat(set, hasItems(payload));
197+
}
175198
}

0 commit comments

Comments
 (0)