-
Notifications
You must be signed in to change notification settings - Fork 646
GH-3031: Defer SMLC shutdown for pending replies #3147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
368fe54
1a48e51
7a732db
cc2e48c
1dd2d72
00487ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,7 @@ | |
| import org.springframework.amqp.rabbit.connection.RoutingConnectionFactory; | ||
| import org.springframework.amqp.rabbit.connection.SimpleResourceHolder; | ||
| import org.springframework.amqp.rabbit.listener.api.ChannelAwareBatchMessageListener; | ||
| import org.springframework.amqp.rabbit.listener.api.PendingReplyProvider; | ||
| import org.springframework.amqp.rabbit.listener.exception.FatalListenerExecutionException; | ||
| import org.springframework.amqp.rabbit.listener.exception.FatalListenerStartupException; | ||
| import org.springframework.amqp.rabbit.support.ActiveObjectCounter; | ||
|
|
@@ -130,6 +131,10 @@ public class SimpleMessageListenerContainer extends AbstractMessageListenerConta | |
|
|
||
| private long batchReceiveTimeout; | ||
|
|
||
| private long pendingReplyCheckInterval = 200L; | ||
|
|
||
| private @Nullable PendingReplyProvider pendingReplyProvider; | ||
|
|
||
| private @Nullable Set<BlockingQueueConsumer> consumers; | ||
|
|
||
| private @Nullable Integer declarationRetries; | ||
|
|
@@ -357,6 +362,28 @@ public void setBatchReceiveTimeout(long batchReceiveTimeout) { | |
| this.batchReceiveTimeout = batchReceiveTimeout; | ||
| } | ||
|
|
||
| /** | ||
| * Set the interval for checking for pending replies during shutdown. | ||
| * Default is 200ms. | ||
| * @param pendingReplyCheckInterval the interval in milliseconds. | ||
| * @since 4.0 | ||
| */ | ||
| public void setPendingReplyCheckInterval(long pendingReplyCheckInterval) { | ||
| this.pendingReplyCheckInterval = pendingReplyCheckInterval; | ||
| } | ||
|
|
||
| /** | ||
| * Set a provider for the number of pending replies. | ||
| * When set, the container will wait for pending replies during shutdown, | ||
| * up to the {@link #setShutdownTimeout(long) shutdownTimeout}. | ||
| * @param pendingReplyProvider the pending reply provider. | ||
| * @since 4.0 | ||
| * @see org.springframework.amqp.rabbit.core.RabbitTemplate#getPendingReplyCount() | ||
| */ | ||
| public void setPendingReplyProvider(PendingReplyProvider pendingReplyProvider) { | ||
| this.pendingReplyProvider = pendingReplyProvider; | ||
| } | ||
|
|
||
| /** | ||
| * This property has several functions. | ||
| * <p> | ||
|
|
@@ -652,6 +679,8 @@ private void waitForConsumersToStart(Set<AsyncMessageProcessingConsumer> process | |
|
|
||
| @Override | ||
| protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { | ||
| waitForPendingReplies(); | ||
|
||
|
|
||
| Thread thread = this.containerStoppingForAbort.get(); | ||
| if (thread != null && !thread.equals(Thread.currentThread())) { | ||
| logger.info("Shutdown ignored - container is stopping due to an aborted consumer"); | ||
|
|
@@ -732,6 +761,39 @@ protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Wait for pending replies if a pending reply provider is configured. | ||
| */ | ||
| private void waitForPendingReplies() { | ||
| PendingReplyProvider provider = this.pendingReplyProvider; | ||
| if (provider != null && getShutdownTimeout() > 0) { | ||
| long deadline = System.currentTimeMillis() + getShutdownTimeout(); | ||
| try { | ||
| while (isRunning() && System.currentTimeMillis() < deadline) { | ||
| int pendingCount = provider.getPendingReplyCount(); | ||
| if (pendingCount <= 0) { | ||
| if (logger.isInfoEnabled()) { | ||
| logger.info("No pending replies detected, proceeding with shutdown."); | ||
| } | ||
| return; | ||
| } | ||
| if (logger.isInfoEnabled()) { | ||
| logger.info("Waiting for " + pendingCount + " pending replies before final shutdown..."); | ||
| } | ||
| Thread.sleep(this.pendingReplyCheckInterval); | ||
| } | ||
| int remaining = provider.getPendingReplyCount(); | ||
| if (remaining > 0) { | ||
| logger.warn("Shutdown timeout expired, but " + remaining + " pending replies still remain."); | ||
| } | ||
| } | ||
| catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| logger.warn("Interrupted while waiting for pending replies."); | ||
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| private void runCallbackIfNotNull(@Nullable Runnable callback) { | ||
| if (callback != null) { | ||
| callback.run(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| /* | ||
| * Copyright 2025-present 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 | ||
| * | ||
| * https://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.amqp.rabbit.listener.api; | ||
|
|
||
| /** | ||
| * A functional interface to provide the number of pending replies, | ||
| * used to delay listener container shutdown. | ||
| * | ||
| * @author Jeongjun Min | ||
| * @since 4.0 | ||
| * @see org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer#setPendingReplyProvider(PendingReplyProvider) | ||
| */ | ||
| @FunctionalInterface | ||
| public interface PendingReplyProvider { | ||
|
|
||
| /** | ||
| * Return the number of pending replies. | ||
| * @return the number of pending replies. | ||
| */ | ||
| int getPendingReplyCount(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ | |
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Set; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.CountDownLatch; | ||
| import java.util.concurrent.Executor; | ||
| import java.util.concurrent.ExecutorService; | ||
|
|
@@ -62,6 +63,7 @@ | |
| import org.springframework.amqp.rabbit.connection.Connection; | ||
| import org.springframework.amqp.rabbit.connection.ConnectionFactory; | ||
| import org.springframework.amqp.rabbit.connection.SingleConnectionFactory; | ||
| import org.springframework.amqp.rabbit.core.RabbitTemplate; | ||
| import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; | ||
| import org.springframework.amqp.utils.test.TestUtils; | ||
| import org.springframework.aop.support.AopUtils; | ||
|
|
@@ -716,6 +718,43 @@ void testWithConsumerStartWhenNotActive() { | |
| assertThat(start.getCount()).isEqualTo(0L); | ||
| } | ||
|
|
||
| @Test | ||
| @SuppressWarnings("unchecked") | ||
| void testShutdownWithPendingReplies() { | ||
| ConnectionFactory connectionFactory = mock(ConnectionFactory.class); | ||
| Connection connection = mock(Connection.class); | ||
| Channel channel = mock(Channel.class); | ||
| given(connectionFactory.createConnection()).willReturn(connection); | ||
| given(connection.createChannel(false)).willReturn(channel); | ||
| given(channel.isOpen()).willReturn(true); | ||
|
|
||
| RabbitTemplate template = new RabbitTemplate(connectionFactory); | ||
| SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); | ||
| container.setQueueNames("foo"); | ||
|
||
| container.setMessageListener(mock(MessageListener.class)); | ||
|
|
||
| long shutdownTimeout = 2000L; | ||
|
||
| long checkInterval = 500L; | ||
| container.setShutdownTimeout(shutdownTimeout); | ||
| container.setPendingReplyCheckInterval(checkInterval); | ||
| container.setPendingReplyProvider(template::getPendingReplyCount); | ||
|
|
||
| Map<String, Object> replyHolder = (Map<String, Object>) ReflectionTestUtils.getField(template, "replyHolder"); | ||
| assertThat(replyHolder).isNotNull(); | ||
| replyHolder.put("foo", new CompletableFuture<Message>()); | ||
|
|
||
| assertThat(template.getPendingReplyCount()).isEqualTo(1); | ||
|
|
||
| container.start(); | ||
|
|
||
| long startTime = System.currentTimeMillis(); | ||
| container.stop(); | ||
| long stopDuration = System.currentTimeMillis() - startTime; | ||
|
||
|
|
||
| assertThat(stopDuration).isGreaterThanOrEqualTo(shutdownTimeout - 500); | ||
| assertThat(template.getPendingReplyCount()).isEqualTo(1); | ||
| } | ||
|
|
||
| private Answer<Object> messageToConsumer(final Channel mockChannel, final SimpleMessageListenerContainer container, | ||
| final boolean cancel, final CountDownLatch latch) { | ||
| return invocation -> { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need this extra abstraction.
We already have a
ListenerContainerAwareimplemented on theRabbitTemplate.So, that
getPendingReplyCount()could be moved into that contract.