Skip to content

Commit 5c52abb

Browse files
committed
GH-923: Fix start delay for @lazy @RabbitListener
Fixes #923 While checking for missing or mis-matched queues, a lazily-loaded listener container can deadlock for 60 seconds. This occurs if the `allBeanNamesByType` cache does not currently have an entry for `Queue` (e.g. cleared by registering a singleton). When lazy beans are referenced, the `RabbitListenerEndpointRegistry` starts the container and `start()` waits for the consumers to start. Getting a reference to the lazy bean holds the `singletonObjects` lock, which is required by the consumer(s) to get the `Queue` beans to check. Add a test case to demonstrate the issue. Disable the redeclaration logic during the initial start of such a container. **cherry-pick to 2.1.x**
1 parent 0ce50eb commit 5c52abb

File tree

5 files changed

+177
-3
lines changed

5 files changed

+177
-3
lines changed

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor
214214

215215
private String errorHandlerLoggerName = getClass().getName();
216216

217+
private volatile boolean lazyLoad;
218+
217219
/**
218220
* {@inheritDoc}
219221
* @since 1.5
@@ -1190,6 +1192,9 @@ public void start() {
11901192
catch (Exception ex) {
11911193
throw convertRabbitAccessException(ex);
11921194
}
1195+
finally {
1196+
this.lazyLoad = false;
1197+
}
11931198
}
11941199

11951200
/**
@@ -1602,6 +1607,25 @@ protected void checkMismatchedQueues() {
16021607
}
16031608
}
16041609

1610+
@Override
1611+
public void lazyLoad() {
1612+
if (this.mismatchedQueuesFatal) {
1613+
if (this.missingQueuesFatal) {
1614+
logger.warn("'mismatchedQueuesFatal' and 'missingQueuesFatal' are ignored during the initial start(), "
1615+
+ "for lazily loaded containers");
1616+
}
1617+
else {
1618+
logger.warn("'mismatchedQueuesFatal' is ignored during the initial start(), "
1619+
+ "for lazily loaded containers");
1620+
}
1621+
}
1622+
else if (this.missingQueuesFatal) {
1623+
logger.warn("'missingQueuesFatal' is ignored during the initial start(), "
1624+
+ "for lazily loaded containers");
1625+
}
1626+
this.lazyLoad = true;
1627+
}
1628+
16051629
/**
16061630
* Use {@link RabbitAdmin#initialize()} to redeclare everything if necessary.
16071631
* Since auto deletion of a queue can cause upstream elements
@@ -1621,7 +1645,7 @@ protected void checkMismatchedQueues() {
16211645
*/
16221646
protected synchronized void redeclareElementsIfNecessary() {
16231647
RabbitAdmin rabbitAdmin = getRabbitAdmin();
1624-
if (rabbitAdmin == null || !isAutoDeclare()) {
1648+
if (this.lazyLoad || rabbitAdmin == null || !isAutoDeclare()) {
16251649
return;
16261650
}
16271651
try {

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageListenerContainer.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014 the original author or authors.
2+
* Copyright 2014-2019 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.
@@ -48,4 +48,15 @@ public interface MessageListenerContainer extends SmartLifecycle {
4848
@Deprecated
4949
MessageConverter getMessageConverter();
5050

51+
/**
52+
* Do not check for missing or mismatched queues during startup. Used for lazily
53+
* loaded message listener containers to avoid a deadlock when starting such
54+
* containers. Applications lazily loading containers should verify the queue
55+
* configuration before loading the container bean.
56+
* @since 2.1.5
57+
*/
58+
default void lazyLoad() {
59+
// no-op
60+
}
61+
5162
}

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2018 the original author or authors.
2+
* Copyright 2014-2019 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.
@@ -164,6 +164,9 @@ public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitLis
164164
}
165165
containerGroup.add(container);
166166
}
167+
if (this.contextRefreshed) {
168+
container.lazyLoad();
169+
}
167170
if (startImmediately) {
168171
startIfNecessary(container);
169172
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.amqp.rabbit.annotation;
18+
19+
import static org.hamcrest.Matchers.equalTo;
20+
import static org.hamcrest.Matchers.lessThan;
21+
import static org.junit.Assert.assertNotNull;
22+
import static org.junit.Assert.assertThat;
23+
24+
import org.junit.ClassRule;
25+
import org.junit.Test;
26+
import org.junit.runner.RunWith;
27+
28+
import org.springframework.amqp.core.Queue;
29+
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
30+
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
31+
import org.springframework.amqp.rabbit.core.RabbitAdmin;
32+
import org.springframework.amqp.rabbit.core.RabbitTemplate;
33+
import org.springframework.amqp.rabbit.junit.BrokerRunning;
34+
import org.springframework.beans.factory.ObjectProvider;
35+
import org.springframework.beans.factory.annotation.Autowired;
36+
import org.springframework.context.ConfigurableApplicationContext;
37+
import org.springframework.context.annotation.Bean;
38+
import org.springframework.context.annotation.Configuration;
39+
import org.springframework.context.annotation.Lazy;
40+
import org.springframework.test.annotation.DirtiesContext;
41+
import org.springframework.test.context.junit4.SpringRunner;
42+
43+
/**
44+
* @author Gary Russell
45+
* @since 2.1.5
46+
*
47+
*/
48+
@RunWith(SpringRunner.class)
49+
@DirtiesContext
50+
public class LazyContainerTests {
51+
52+
@ClassRule
53+
public static BrokerRunning brokerRunning = BrokerRunning.isRunningWithEmptyQueues("test.lazy");
54+
55+
@Autowired
56+
private ConfigurableApplicationContext context;
57+
58+
@Autowired
59+
private ObjectProvider<LazyListener> lazyListenerProvider;
60+
61+
@Autowired
62+
private RabbitTemplate rabbitTemplate;
63+
64+
@Test
65+
public void lazy() {
66+
this.context.getBeanFactory().registerSingleton("clearTheByTypeCache", "foo");
67+
long t1 = System.currentTimeMillis();
68+
this.lazyListenerProvider.getIfAvailable();
69+
assertThat(System.currentTimeMillis() - t1, lessThan(30_000L));
70+
Object reply = this.rabbitTemplate.convertSendAndReceive("test.lazy", "lazy");
71+
assertNotNull(reply);
72+
assertThat(reply, equalTo("LAZY"));
73+
}
74+
75+
@Configuration
76+
@EnableRabbit
77+
public static class Config {
78+
79+
@Bean
80+
public CachingConnectionFactory cf() {
81+
return new CachingConnectionFactory(brokerRunning.getConnectionFactory());
82+
}
83+
84+
@Bean
85+
public RabbitTemplate template() {
86+
return new RabbitTemplate(cf());
87+
}
88+
89+
@Bean
90+
public RabbitAdmin admin() {
91+
return new RabbitAdmin(template());
92+
}
93+
94+
@Bean
95+
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
96+
SimpleRabbitListenerContainerFactory cf = new SimpleRabbitListenerContainerFactory();
97+
cf.setConnectionFactory(cf());
98+
return cf;
99+
}
100+
101+
@Bean
102+
public Queue queue() {
103+
return new Queue("test.lazy");
104+
}
105+
106+
@Bean
107+
@Lazy
108+
public LazyListener listener() {
109+
return new LazyListener();
110+
}
111+
112+
@RabbitListener(queues = "test.lazy")
113+
public String listen(String in) {
114+
return in.toUpperCase();
115+
}
116+
117+
}
118+
119+
public static class LazyListener {
120+
121+
@RabbitListener(queues = "test.lazy", concurrency = "2")
122+
public String listenLazily(String in) {
123+
return in.toUpperCase();
124+
}
125+
126+
}
127+
128+
}

src/reference/asciidoc/amqp.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4611,6 +4611,10 @@ This global property will not be applied to any containers that have an explicit
46114611

46124612
The default retry properties (3 retries at 5 second intervals) can be overridden using the properties below.
46134613

4614+
IMPORTANT: Missing queue detection is disabled while starting a container for a `@RabbitListener` in a bean that is marked `@Lazy`.
4615+
This is to avoid a potential deadlock which can delay the start of such containers for up to 60 seconds.
4616+
Applications using lazy listener beans should check the queue(s) before getting a reference to the lazy bean.
4617+
46144618
a| image::images/tickmark.png[]
46154619
a| image::images/tickmark.png[]
46164620

@@ -4669,6 +4673,10 @@ If you wish to limit the checks to just those queues used by a container, you sh
46694673
`RabbitAdmin` for the container, and provide a reference to it using the `rabbitAdmin` property.
46704674
See <<conditional-declaration>> for more information.
46714675

4676+
IMPORTANT: Mismatched queue argument detection is disabled while starting a container for a `@RabbitListener` in a bean that is marked `@Lazy`.
4677+
This is to avoid a potential deadlock which can delay the start of such containers for up to 60 seconds.
4678+
Applications using lazy listener beans should check the queue arguments before getting a reference to the lazy bean.
4679+
46724680
a| image::images/tickmark.png[]
46734681
a| image::images/tickmark.png[]
46744682

0 commit comments

Comments
 (0)