Skip to content

Commit c8c06ea

Browse files
committed
GH-2607: Add SMLC.enforceImmediateAckForManual option
Fixes: #2607 There are use-cases when `ImmediateAcknowledgeAmqpException` can be thrown outside the listener method, therefore there is no way to reach `Channel.basicAck()`. For example, for `AbstractMessageListenerContainer.afterReceivePostProcessors` * Make force ack for `ImmediateAcknowledgeAmqpException` even if `AcknowledgeMode.MANUAL`. This is controlled with newly introduced `enforceImmediateAckForManual` flag on the `SimpleMessageListenerContainer`. Such an option might be as tentative solution to not break behavior for existing applications using the current point release. We may consider to make this unconditional in future versions
1 parent 5c3e739 commit c8c06ea

File tree

4 files changed

+108
-17
lines changed

4 files changed

+108
-17
lines changed

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.amqp.rabbit.config;
1818

19+
import com.rabbitmq.client.Channel;
20+
21+
import org.springframework.amqp.ImmediateAcknowledgeAmqpException;
1922
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint;
2023
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
2124
import org.springframework.amqp.utils.JavaUtils;
@@ -59,6 +62,8 @@ public class SimpleRabbitListenerContainerFactory
5962

6063
private Boolean consumerBatchEnabled;
6164

65+
private Boolean enforceImmediateAckForManual;
66+
6267
/**
6368
* @param batchSize the batch size.
6469
* @since 2.2
@@ -153,6 +158,17 @@ public void setConsumerBatchEnabled(boolean consumerBatchEnabled) {
153158
}
154159
}
155160

161+
/**
162+
* Set to {@code true} to enforce {@link Channel#basicAck(long, boolean)}
163+
* for {@link org.springframework.amqp.core.AcknowledgeMode#MANUAL}
164+
* when {@link ImmediateAcknowledgeAmqpException} is thrown.
165+
* This might be a tentative solution to not break behavior for current minor version.
166+
* @param enforceImmediateAckForManual the flag to ack message for MANUAL mode on ImmediateAcknowledgeAmqpException
167+
* @since 3.1.2
168+
*/
169+
public void setEnforceImmediateAckForManual(Boolean enforceImmediateAckForManual) {
170+
this.enforceImmediateAckForManual = enforceImmediateAckForManual;
171+
}
156172
@Override
157173
protected SimpleMessageListenerContainer createContainerInstance() {
158174
return new SimpleMessageListenerContainer();
@@ -180,7 +196,8 @@ protected void initializeContainer(SimpleMessageListenerContainer instance, Rabb
180196
.acceptIfNotNull(this.consecutiveActiveTrigger, instance::setConsecutiveActiveTrigger)
181197
.acceptIfNotNull(this.consecutiveIdleTrigger, instance::setConsecutiveIdleTrigger)
182198
.acceptIfNotNull(this.receiveTimeout, instance::setReceiveTimeout)
183-
.acceptIfNotNull(this.batchReceiveTimeout, instance::setBatchReceiveTimeout);
199+
.acceptIfNotNull(this.batchReceiveTimeout, instance::setBatchReceiveTimeout)
200+
.acceptIfNotNull(this.enforceImmediateAckForManual, instance::setEnforceImmediateAckForManual);
184201
if (Boolean.TRUE.equals(this.consumerBatchEnabled)) {
185202
instance.setConsumerBatchEnabled(true);
186203
/*

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

+18-6
Original file line numberDiff line numberDiff line change
@@ -875,10 +875,25 @@ public void rollbackOnExceptionIfNecessary(Throwable ex, long tag) {
875875

876876
/**
877877
* Perform a commit or message acknowledgement, as appropriate.
878+
* NOTE: This method was never been intended tobe public.
878879
* @param localTx Whether the channel is locally transacted.
879880
* @return true if at least one delivery tag exists.
881+
* @deprecated in favor of {@link #commitIfNecessary(boolean, boolean)}
880882
*/
883+
@Deprecated(forRemoval = true, since = "3.1.2")
881884
public boolean commitIfNecessary(boolean localTx) {
885+
return commitIfNecessary(localTx, false);
886+
}
887+
888+
/**
889+
* Perform a commit or message acknowledgement, as appropriate.
890+
* NOTE: This method was never been intended tobe public.
891+
* @param localTx Whether the channel is locally transacted.
892+
* @param forceAck perform {@link Channel#basicAck(long, boolean)} independently of {@link #acknowledgeMode}.
893+
* @return true if at least one delivery tag exists.
894+
* @since 3.1.2
895+
*/
896+
boolean commitIfNecessary(boolean localTx, boolean forceAck) {
882897
if (this.deliveryTags.isEmpty()) {
883898
return false;
884899
}
@@ -890,11 +905,10 @@ public boolean commitIfNecessary(boolean localTx) {
890905
|| (this.transactional
891906
&& TransactionSynchronizationManager.getResource(this.connectionFactory) == null);
892907
try {
893-
894-
boolean ackRequired = !this.acknowledgeMode.isAutoAck() && !this.acknowledgeMode.isManual();
908+
boolean ackRequired = forceAck || (!this.acknowledgeMode.isAutoAck() && !this.acknowledgeMode.isManual());
895909

896910
if (ackRequired && (!this.transactional || isLocallyTransacted)) {
897-
long deliveryTag = new ArrayList<Long>(this.deliveryTags).get(this.deliveryTags.size() - 1);
911+
long deliveryTag = new ArrayList<>(this.deliveryTags).get(this.deliveryTags.size() - 1);
898912
try {
899913
this.channel.basicAck(deliveryTag, true);
900914
notifyMessageAckListener(true, deliveryTag, null);
@@ -909,14 +923,12 @@ public boolean commitIfNecessary(boolean localTx) {
909923
// For manual acks we still need to commit
910924
RabbitUtils.commitIfNecessary(this.channel);
911925
}
912-
913926
}
914927
finally {
915928
this.deliveryTags.clear();
916929
}
917930

918931
return true;
919-
920932
}
921933

922934
/**
@@ -931,7 +943,7 @@ private void notifyMessageAckListener(boolean success, long deliveryTag, @Nullab
931943
this.messageAckListener.onComplete(success, deliveryTag, cause);
932944
}
933945
catch (Exception e) {
934-
logger.error("An exception occured in MessageAckListener.", e);
946+
logger.error("An exception occurred in MessageAckListener.", e);
935947
}
936948
}
937949

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

+27-8
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.amqp.core.Message;
4242
import org.springframework.amqp.core.MessagePostProcessor;
4343
import org.springframework.amqp.core.Queue;
44+
import org.springframework.amqp.rabbit.batch.BatchingStrategy;
4445
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
4546
import org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils;
4647
import org.springframework.amqp.rabbit.connection.ConsumerChannelRegistry;
@@ -134,6 +135,8 @@ public class SimpleMessageListenerContainer extends AbstractMessageListenerConta
134135

135136
private long consumerStartTimeout = DEFAULT_CONSUMER_START_TIMEOUT;
136137

138+
private boolean enforceImmediateAckForManual;
139+
137140
private volatile int concurrentConsumers = 1;
138141

139142
private volatile Integer maxConcurrentConsumers;
@@ -504,6 +507,18 @@ public void setConsumerStartTimeout(long consumerStartTimeout) {
504507
this.consumerStartTimeout = consumerStartTimeout;
505508
}
506509

510+
/**
511+
* Set to {@code true} to enforce {@link Channel#basicAck(long, boolean)}
512+
* for {@link org.springframework.amqp.core.AcknowledgeMode#MANUAL}
513+
* when {@link ImmediateAcknowledgeAmqpException} is thrown.
514+
* This might be a tentative solution to not break behavior for current minor version.
515+
* @param enforceImmediateAckForManual the flag to ack message for MANUAL mode on ImmediateAcknowledgeAmqpException
516+
* @since 3.1.2
517+
*/
518+
public void setEnforceImmediateAckForManual(boolean enforceImmediateAckForManual) {
519+
this.enforceImmediateAckForManual = enforceImmediateAckForManual;
520+
}
521+
507522
/**
508523
* Avoid the possibility of not configuring the CachingConnectionFactory in sync with the number of concurrent
509524
* consumers.
@@ -1012,6 +1027,7 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep
10121027

10131028
List<Message> messages = null;
10141029
long deliveryTag = 0;
1030+
boolean immediateAck = false;
10151031
boolean isBatchReceiveTimeoutEnabled = this.batchReceiveTimeout > 0;
10161032
long startTime = isBatchReceiveTimeoutEnabled ? System.currentTimeMillis() : 0;
10171033
for (int i = 0; i < this.batchSize; i++) {
@@ -1050,9 +1066,9 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep
10501066
if (messages == null) {
10511067
messages = new ArrayList<>(this.batchSize);
10521068
}
1053-
if (isDeBatchingEnabled() && getBatchingStrategy().canDebatch(message.getMessageProperties())) {
1054-
final List<Message> messageList = messages;
1055-
getBatchingStrategy().deBatch(message, fragment -> messageList.add(fragment));
1069+
BatchingStrategy batchingStrategy = getBatchingStrategy();
1070+
if (isDeBatchingEnabled() && batchingStrategy.canDebatch(message.getMessageProperties())) {
1071+
batchingStrategy.deBatch(message, messages::add);
10561072
}
10571073
else {
10581074
messages.add(message);
@@ -1073,6 +1089,7 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep
10731089
+ e.getMessage() + "': "
10741090
+ message.getMessageProperties().getDeliveryTag());
10751091
}
1092+
immediateAck = this.enforceImmediateAckForManual;
10761093
break;
10771094
}
10781095
catch (Exception ex) {
@@ -1081,6 +1098,7 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep
10811098
this.logger.debug("User requested ack for failed delivery: "
10821099
+ message.getMessageProperties().getDeliveryTag());
10831100
}
1101+
immediateAck = this.enforceImmediateAckForManual;
10841102
break;
10851103
}
10861104
long tagToRollback = isAsyncReplies()
@@ -1117,14 +1135,14 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep
11171135
}
11181136
}
11191137
if (messages != null) {
1120-
executeWithList(channel, messages, deliveryTag, consumer);
1138+
immediateAck = executeWithList(channel, messages, deliveryTag, consumer);
11211139
}
11221140

1123-
return consumer.commitIfNecessary(isChannelLocallyTransacted());
1141+
return consumer.commitIfNecessary(isChannelLocallyTransacted(), immediateAck);
11241142

11251143
}
11261144

1127-
private void executeWithList(Channel channel, List<Message> messages, long deliveryTag,
1145+
private boolean executeWithList(Channel channel, List<Message> messages, long deliveryTag,
11281146
BlockingQueueConsumer consumer) {
11291147

11301148
try {
@@ -1136,15 +1154,15 @@ private void executeWithList(Channel channel, List<Message> messages, long deliv
11361154
+ e.getMessage() + "' (last in batch): "
11371155
+ deliveryTag);
11381156
}
1139-
return;
1157+
return this.enforceImmediateAckForManual;
11401158
}
11411159
catch (Exception ex) {
11421160
if (causeChainHasImmediateAcknowledgeAmqpException(ex)) {
11431161
if (this.logger.isDebugEnabled()) {
11441162
this.logger.debug("User requested ack for failed delivery (last in batch): "
11451163
+ deliveryTag);
11461164
}
1147-
return;
1165+
return this.enforceImmediateAckForManual;
11481166
}
11491167
if (getTransactionManager() != null) {
11501168
if (getTransactionAttribute().rollbackOn(ex)) {
@@ -1173,6 +1191,7 @@ private void executeWithList(Channel channel, List<Message> messages, long deliv
11731191
throw ex;
11741192
}
11751193
}
1194+
return false;
11761195
}
11771196

11781197
protected void handleStartupFailure(BackOffExecution backOffExecution) {

spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java

+45-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -17,6 +17,7 @@
1717
package org.springframework.amqp.rabbit.listener;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.awaitility.Awaitility.await;
2021

2122
import java.util.concurrent.CountDownLatch;
2223
import java.util.concurrent.TimeUnit;
@@ -27,8 +28,10 @@
2728
import org.junit.jupiter.api.BeforeEach;
2829
import org.junit.jupiter.api.Test;
2930

31+
import org.springframework.amqp.ImmediateAcknowledgeAmqpException;
3032
import org.springframework.amqp.core.AcknowledgeMode;
3133
import org.springframework.amqp.core.Message;
34+
import org.springframework.amqp.core.MessageListener;
3235
import org.springframework.amqp.core.Queue;
3336
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
3437
import org.springframework.amqp.rabbit.core.RabbitTemplate;
@@ -45,6 +48,7 @@
4548
* @author Dave Syer
4649
* @author Gunnar Hillert
4750
* @author Gary Russell
51+
* @author Artem Bilan
4852
*
4953
* @since 1.0
5054
*
@@ -56,7 +60,7 @@ public class MessageListenerManualAckIntegrationTests {
5660

5761
public static final String TEST_QUEUE = "test.queue.MessageListenerManualAckIntegrationTests";
5862

59-
private static Log logger = LogFactory.getLog(MessageListenerManualAckIntegrationTests.class);
63+
private static final Log logger = LogFactory.getLog(MessageListenerManualAckIntegrationTests.class);
6064

6165
private final Queue queue = new Queue(TEST_QUEUE);
6266

@@ -121,6 +125,26 @@ public void testListenerWithManualAckTransactional() throws Exception {
121125
assertThat(template.receiveAndConvert(queue.getName())).isNull();
122126
}
123127

128+
@Test
129+
public void immediateIsAckedForManual() throws Exception {
130+
CountDownLatch latch = new CountDownLatch(1);
131+
container = createContainer(new ImmediateTestListener(latch));
132+
container.setEnforceImmediateAckForManual(true);
133+
134+
template.convertAndSend(queue.getName(), "test data");
135+
136+
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
137+
138+
container.stop();
139+
140+
Channel channel = template.getConnectionFactory().createConnection().createChannel(false);
141+
142+
await().untilAsserted(() -> assertThat(channel.consumerCount(queue.getName())).isEqualTo(0));
143+
assertThat(channel.messageCount(queue.getName())).isEqualTo(0);
144+
145+
channel.close();
146+
}
147+
124148
private SimpleMessageListenerContainer createContainer(Object listener) {
125149
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(template.getConnectionFactory());
126150
container.setMessageListener(new MessageListenerAdapter(listener));
@@ -159,4 +183,23 @@ public void onMessage(Message message, Channel channel) throws Exception {
159183
}
160184
}
161185

186+
static class ImmediateTestListener implements MessageListener {
187+
188+
private final CountDownLatch latch;
189+
190+
ImmediateTestListener(CountDownLatch latch) {
191+
this.latch = latch;
192+
}
193+
194+
@Override
195+
public void onMessage(Message message) {
196+
try {
197+
throw new ImmediateAcknowledgeAmqpException("intentional");
198+
}
199+
finally {
200+
this.latch.countDown();
201+
}
202+
}
203+
}
204+
162205
}

0 commit comments

Comments
 (0)