Skip to content

Commit eccdb47

Browse files
LeonardoFerreiraagaryrussell
authored andcommitted
GH-1434: Mixed CFs With/Without Confirms/Returns
GH-1434 allowing to rabbit template have multiple connection factories with not same confirms and returns flags. GH-1434 avoiding call obtainTargetConnectionFactory twice GH-1434 test GH-1434 javadoc + removing else GH-1434 fixing checkstyle GH-1434 using publisherConfirms from PooledChannelConnectionFactory GH-1434 adapting AbstractRoutingConnectionFactory GH-1434 javadoc & checkstyle & BeforeEach > BeforeAll GH-1434 javadoc GH-1434 doc GH-1434 doc
1 parent 6eca017 commit eccdb47

File tree

9 files changed

+224
-20
lines changed

9 files changed

+224
-20
lines changed

CONTRIBUTING.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ _you should see branches on origin as well as upstream, including 'main' and 'ma
5151

5252
== A Day in the Life of a Contributor
5353

54-
* _Always_ work on topic branches (Typically use the HitHub (or JIRA) issue ID as the branch name).
54+
* _Always_ work on topic branches (Typically use the GitHub (or JIRA) issue ID as the branch name).
5555
- For example, to create and switch to a new branch for issue #123: `git checkout -b GH-123`
5656
* You might be working on several different topic branches at any given time, but when at a stopping point for one of those branches, commit (a local operation).
5757
* Please follow the "Commit Guidelines" described in https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project[this chapter of Pro Git].

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java

+28-4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public abstract class AbstractRoutingConnectionFactory implements ConnectionFact
5454

5555
private Boolean returns;
5656

57+
private boolean consistentConfirmsReturns = true;
58+
5759
/**
5860
* Specify the map of target ConnectionFactories, with the lookup key as key.
5961
* <p>The key can be of arbitrary type; this class implements the
@@ -125,10 +127,13 @@ private void checkConfirmsAndReturns(ConnectionFactory cf) {
125127
if (this.returns == null) {
126128
this.returns = cf.isPublisherReturns();
127129
}
128-
Assert.isTrue(this.confirms.booleanValue() == cf.isPublisherConfirms(),
129-
"Target connection factories must have the same setting for publisher confirms");
130-
Assert.isTrue(this.returns.booleanValue() == cf.isPublisherReturns(),
131-
"Target connection factories must have the same setting for publisher returns");
130+
131+
if (this.consistentConfirmsReturns) {
132+
Assert.isTrue(this.confirms.booleanValue() == cf.isPublisherConfirms(),
133+
"Target connection factories must have the same setting for publisher confirms");
134+
Assert.isTrue(this.returns.booleanValue() == cf.isPublisherReturns(),
135+
"Target connection factories must have the same setting for publisher returns");
136+
}
132137
}
133138

134139
@Override
@@ -230,6 +235,25 @@ public ConnectionFactory getTargetConnectionFactory(Object key) {
230235
return this.targetConnectionFactories.get(key);
231236
}
232237

238+
/**
239+
* Specify whether to apply a validation enforcing all {@link ConnectionFactory#isPublisherConfirms()} and
240+
* {@link ConnectionFactory#isPublisherReturns()} have a consistent value.
241+
* <p>
242+
* A consistent value means that all ConnectionFactories must have the same value between all
243+
* {@link ConnectionFactory#isPublisherConfirms()} and the same value between all
244+
* {@link ConnectionFactory#isPublisherReturns()}.
245+
* </p>
246+
* <p>
247+
* Note that in any case the values between {@link ConnectionFactory#isPublisherConfirms()} and
248+
* {@link ConnectionFactory#isPublisherReturns()} don't need to be equals between each other.
249+
* </p>
250+
* @param consistentConfirmsReturns true to validate, false to not validate.
251+
* @since 2.4.4
252+
*/
253+
public void setConsistentConfirmsReturns(boolean consistentConfirmsReturns) {
254+
this.consistentConfirmsReturns = consistentConfirmsReturns;
255+
}
256+
233257
/**
234258
* Adds the given {@link ConnectionFactory} and associates it with the given lookup key.
235259
* @param key the lookup key.

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java

+4
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
* @author Artem Bilan
9595
* @author Steve Powell
9696
* @author Will Droste
97+
* @author Leonardo Ferreira
9798
*/
9899
@ManagedResource
99100
public class CachingConnectionFactory extends AbstractConnectionFactory
@@ -1133,6 +1134,9 @@ else if (methodName.equals("isTransactional")) {
11331134
else if (methodName.equals("isConfirmSelected")) {
11341135
return this.confirmSelected;
11351136
}
1137+
else if (methodName.equals("isPublisherConfirms")) {
1138+
return this.publisherConfirms;
1139+
}
11361140
try {
11371141
if (this.target == null || !this.target.isOpen()) {
11381142
if (this.target instanceof PublisherCallbackChannel) {

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java

+8
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,12 @@ default boolean isConfirmSelected() {
5454
return false;
5555
}
5656

57+
/**
58+
* Return true if publisher confirms are enabled.
59+
* @return true if publisherConfirms.
60+
*/
61+
default boolean isPublisherConfirms() {
62+
return false;
63+
}
64+
5765
}

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java

+3
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,8 @@ private Channel createProxy(Channel channel, boolean transacted) {
222222
return channel.confirmSelect();
223223
case "isConfirmSelected":
224224
return confirmSelected.get();
225+
case "isPublisherConfirms":
226+
return false;
225227
}
226228
return null;
227229
};
@@ -231,6 +233,7 @@ private Channel createProxy(Channel channel, boolean transacted) {
231233
advisor.addMethodName("isTransactional");
232234
advisor.addMethodName("confirmSelect");
233235
advisor.addMethodName("isConfirmSelected");
236+
advisor.addMethodName("isPublisherConfirms");
234237
pf.addAdvisor(advisor);
235238
pf.addInterface(ChannelProxy.class);
236239
proxy.set((Channel) pf.getProxy());

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
* {@link #closeThreadChannel()}.
4545
*
4646
* @author Gary Russell
47+
* @author Leonardo Ferreira
4748
* @since 2.3
4849
*
4950
*/
@@ -288,6 +289,8 @@ private Channel createProxy(Channel channel, boolean transactional) {
288289
return channel.confirmSelect();
289290
case "isConfirmSelected":
290291
return confirmSelected.get();
292+
case "isPublisherConfirms":
293+
return false;
291294
}
292295
return null;
293296
};
@@ -297,6 +300,7 @@ private Channel createProxy(Channel channel, boolean transactional) {
297300
advisor.addMethodName("isTransactional");
298301
advisor.addMethodName("confirmSelect");
299302
advisor.addMethodName("isConfirmSelected");
303+
advisor.addMethodName("isPublisherConfirms");
300304
pf.addAdvisor(advisor);
301305
pf.addInterface(ChannelProxy.class);
302306
return (Channel) pf.getProxy();

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java

+11-15
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
* @author Mark Norkin
148148
* @author Mohammad Hewedy
149149
* @author Alexey Platonov
150+
* @author Leonardo Ferreira
150151
*
151152
* @since 1.0
152153
*/
@@ -257,10 +258,6 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count
257258

258259
private ErrorHandler replyErrorHandler;
259260

260-
private volatile Boolean confirmsOrReturnsCapable;
261-
262-
private volatile boolean publisherConfirms;
263-
264261
private volatile boolean usingFastReplyTo;
265262

266263
private volatile boolean evaluatedFastReplyTo;
@@ -1263,7 +1260,7 @@ else if (isChannelTransacted()) {
12631260
}
12641261
return buildMessageFromDelivery(delivery);
12651262
}
1266-
});
1263+
}, obtainTargetConnectionFactory(this.receiveConnectionFactorySelectorExpression, null));
12671264
logReceived(message);
12681265
return message;
12691266
}
@@ -1960,7 +1957,7 @@ private Message doSendAndReceiveWithDirect(String exchange, String routingKey, M
19601957
boolean cancelConsumer = false;
19611958
try {
19621959
Channel channel = channelHolder.getChannel();
1963-
if (this.confirmsOrReturnsCapable) {
1960+
if (isPublisherConfirmsOrReturns(connectionFactory)) {
19641961
addListener(channel);
19651962
}
19661963
Message reply = doSendAndReceiveAsListener(exchange, routingKey, message, correlationData, channel,
@@ -2224,12 +2221,10 @@ private void cleanUpAfterAction(@Nullable Channel channel, boolean invokeScope,
22242221
private <T> T invokeAction(ChannelCallback<T> action, ConnectionFactory connectionFactory, Channel channel)
22252222
throws Exception { // NOSONAR see the callback
22262223

2227-
if (this.confirmsOrReturnsCapable == null) {
2228-
determineConfirmsReturnsCapability(connectionFactory);
2229-
}
2230-
if (this.confirmsOrReturnsCapable) {
2224+
if (isPublisherConfirmsOrReturns(connectionFactory)) {
22312225
addListener(channel);
22322226
}
2227+
22332228
if (logger.isDebugEnabled()) {
22342229
logger.debug(
22352230
"Executing callback " + action.getClass().getSimpleName() + " on RabbitMQ Channel: " + channel);
@@ -2351,10 +2346,8 @@ public void waitForConfirmsOrDie(long timeout) {
23512346
}
23522347
}
23532348

2354-
public void determineConfirmsReturnsCapability(ConnectionFactory connectionFactory) {
2355-
this.publisherConfirms = connectionFactory.isPublisherConfirms();
2356-
this.confirmsOrReturnsCapable =
2357-
this.publisherConfirms || connectionFactory.isPublisherReturns();
2349+
private boolean isPublisherConfirmsOrReturns(ConnectionFactory connectionFactory) {
2350+
return connectionFactory.isPublisherConfirms() || connectionFactory.isPublisherReturns();
23582351
}
23592352

23602353
/**
@@ -2435,8 +2428,11 @@ protected void sendToRabbit(Channel channel, String exchange, String routingKey,
24352428
}
24362429

24372430
private void setupConfirm(Channel channel, Message message, @Nullable CorrelationData correlationDataArg) {
2438-
if ((this.publisherConfirms || this.confirmCallback != null) && channel instanceof PublisherCallbackChannel) {
2431+
final boolean publisherConfirms = channel instanceof ChannelProxy
2432+
&& ((ChannelProxy) channel).isPublisherConfirms();
24392433

2434+
if ((publisherConfirms || this.confirmCallback != null)
2435+
&& channel instanceof PublisherCallbackChannel) {
24402436
long nextPublishSeqNo = channel.getNextPublishSeqNo();
24412437
if (nextPublishSeqNo > 0) {
24422438
PublisherCallbackChannel publisherCallbackChannel = (PublisherCallbackChannel) channel;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2002-2022 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.core;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import java.nio.charset.StandardCharsets;
22+
import java.time.Duration;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
import java.util.UUID;
26+
import java.util.concurrent.TimeUnit;
27+
28+
import org.junit.jupiter.api.AfterAll;
29+
import org.junit.jupiter.api.BeforeAll;
30+
import org.junit.jupiter.api.Test;
31+
32+
import org.springframework.amqp.core.Message;
33+
import org.springframework.amqp.core.MessageBuilder;
34+
import org.springframework.amqp.rabbit.connection.AbstractRoutingConnectionFactory;
35+
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
36+
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
37+
import org.springframework.amqp.rabbit.connection.CorrelationData;
38+
import org.springframework.amqp.rabbit.connection.PooledChannelConnectionFactory;
39+
import org.springframework.amqp.rabbit.connection.SimpleRoutingConnectionFactory;
40+
import org.springframework.amqp.rabbit.junit.BrokerTestUtils;
41+
import org.springframework.amqp.rabbit.junit.RabbitAvailable;
42+
import org.springframework.expression.Expression;
43+
import org.springframework.expression.spel.standard.SpelExpressionParser;
44+
45+
/**
46+
* @author Leonardo Ferreira
47+
* @since 2.4.4
48+
*/
49+
@RabbitAvailable(queues = RabbitTemplateRoutingConnectionFactoryIntegrationTests.ROUTE)
50+
class RabbitTemplateRoutingConnectionFactoryIntegrationTests {
51+
52+
public static final String ROUTE = "test.queue.RabbitTemplateRoutingConnectionFactoryIntegrationTests";
53+
54+
private static RabbitTemplate rabbitTemplate;
55+
56+
@BeforeAll
57+
static void create() {
58+
final com.rabbitmq.client.ConnectionFactory cf = new com.rabbitmq.client.ConnectionFactory();
59+
cf.setHost("localhost");
60+
cf.setPort(BrokerTestUtils.getPort());
61+
62+
CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(cf);
63+
64+
cachingConnectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);
65+
66+
PooledChannelConnectionFactory pooledChannelConnectionFactory = new PooledChannelConnectionFactory(cf);
67+
68+
Map<Object, ConnectionFactory> connectionFactoryMap = new HashMap<>(2);
69+
connectionFactoryMap.put("true", cachingConnectionFactory);
70+
connectionFactoryMap.put("false", pooledChannelConnectionFactory);
71+
72+
final AbstractRoutingConnectionFactory routingConnectionFactory = new SimpleRoutingConnectionFactory();
73+
routingConnectionFactory.setConsistentConfirmsReturns(false);
74+
routingConnectionFactory.setDefaultTargetConnectionFactory(pooledChannelConnectionFactory);
75+
routingConnectionFactory.setTargetConnectionFactories(connectionFactoryMap);
76+
77+
rabbitTemplate = new RabbitTemplate(routingConnectionFactory);
78+
79+
final Expression sendExpression = new SpelExpressionParser().parseExpression(
80+
"messageProperties.headers['x-use-publisher-confirms'] ?: false");
81+
rabbitTemplate.setSendConnectionFactorySelectorExpression(sendExpression);
82+
}
83+
84+
@AfterAll
85+
static void cleanUp() {
86+
rabbitTemplate.destroy();
87+
}
88+
89+
@Test
90+
void sendWithoutConfirmsTest() {
91+
final String payload = UUID.randomUUID().toString();
92+
rabbitTemplate.convertAndSend(ROUTE, (Object) payload, new CorrelationData());
93+
assertThat(rabbitTemplate.getUnconfirmedCount()).isZero();
94+
95+
final Message received = rabbitTemplate.receive(ROUTE, Duration.ofSeconds(3).toMillis());
96+
assertThat(received).isNotNull();
97+
final String receivedPayload = new String(received.getBody());
98+
99+
assertThat(receivedPayload).isEqualTo(payload);
100+
}
101+
102+
@Test
103+
void sendWithConfirmsTest() throws Exception {
104+
final String payload = UUID.randomUUID().toString();
105+
final Message message = MessageBuilder.withBody(payload.getBytes(StandardCharsets.UTF_8))
106+
.setHeader("x-use-publisher-confirms", "true").build();
107+
108+
final CorrelationData correlationData = new CorrelationData();
109+
rabbitTemplate.send(ROUTE, message, correlationData);
110+
assertThat(rabbitTemplate.getUnconfirmedCount()).isEqualTo(1);
111+
112+
final CorrelationData.Confirm confirm = correlationData.getFuture().get(10, TimeUnit.SECONDS);
113+
114+
assertThat(confirm.isAck()).isTrue();
115+
116+
final Message received = rabbitTemplate.receive(ROUTE, Duration.ofSeconds(10).toMillis());
117+
assertThat(received).isNotNull();
118+
final String receivedPayload = new String(received.getBody());
119+
120+
assertThat(receivedPayload).isEqualTo(payload);
121+
}
122+
123+
}

src/reference/asciidoc/amqp.adoc

+42
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,48 @@ For example, with lookup key qualifier `thing1` and a container listening to que
696696
IMPORTANT: The target (and default, if provided) connection factories must have the same settings for publisher confirms and returns.
697697
See <<cf-pub-conf-ret>>.
698698

699+
Starting with version 2.4.4, this validation can be disabled.
700+
If you have a case that the values between confirms and returns need to be unequal, you can use `AbstractRoutingConnectionFactory#setConsistentConfirmsReturns` to turn of the validation.
701+
Note that the first connection factory added to `AbstractRoutingConnectionFactory` will determine the general values of `confirms` and `returns`.
702+
703+
It may be useful if you have a case that certain messages you would to check confirms/returns and others you don't.
704+
For example:
705+
706+
====
707+
[source, java]
708+
----
709+
@Bean
710+
public RabbitTemplate rabbitTemplate() {
711+
final com.rabbitmq.client.ConnectionFactory cf = new com.rabbitmq.client.ConnectionFactory();
712+
cf.setHost("localhost");
713+
cf.setPort(5672);
714+
715+
CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(cf);
716+
cachingConnectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);
717+
718+
PooledChannelConnectionFactory pooledChannelConnectionFactory = new PooledChannelConnectionFactory(cf);
719+
720+
final Map<Object, ConnectionFactory> connectionFactoryMap = new HashMap<>(2);
721+
connectionFactoryMap.put("true", cachingConnectionFactory);
722+
connectionFactoryMap.put("false", pooledChannelConnectionFactory);
723+
724+
final AbstractRoutingConnectionFactory routingConnectionFactory = new SimpleRoutingConnectionFactory();
725+
routingConnectionFactory.setConsistentConfirmsReturns(false);
726+
routingConnectionFactory.setDefaultTargetConnectionFactory(pooledChannelConnectionFactory);
727+
routingConnectionFactory.setTargetConnectionFactories(connectionFactoryMap);
728+
729+
final RabbitTemplate rabbitTemplate = new RabbitTemplate(routingConnectionFactory);
730+
731+
final Expression sendExpression = new SpelExpressionParser().parseExpression(
732+
"messageProperties.headers['x-use-publisher-confirms'] ?: false");
733+
rabbitTemplate.setSendConnectionFactorySelectorExpression(sendExpression);
734+
}
735+
----
736+
====
737+
738+
This way messages with the header `x-use-publisher-confirms: true` will be sent through the caching connection and you can ensure the message delivery.
739+
See <<cf-pub-conf-ret>> for more information about ensuring message delivery.
740+
699741
[[queue-affinity]]
700742
===== Queue Affinity and the `LocalizedQueueConnectionFactory`
701743

0 commit comments

Comments
 (0)