Skip to content

Commit b404738

Browse files
committed
Dispatch connection shutdown in appropriate threads
For Netty. Make sure that connection shutdown sequence is not executed in the IO event loop if recovery follows. Recovery kicks in the shutdown sequence, so we could end up with a deadlock if the new connection is allocated to the same event loop. References #1663 (cherry picked from commit cef9a48) Conflicts: src/main/java/com/rabbitmq/client/impl/Environment.java src/test/java/com/rabbitmq/client/test/TestUtils.java src/test/resources/logback-test.xml
1 parent e379c74 commit b404738

File tree

9 files changed

+207
-38
lines changed

9 files changed

+207
-38
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,7 @@
776776
<includes>
777777
<include>src/main/java/com/rabbitmq/client/ConnectionFactory.java</include>
778778
<include>src/main/java/com/rabbitmq/client/impl/NettyFrameHandlerFactory.java</include>
779+
<include>src/main/java/com/rabbitmq/client/impl/Environment.java</include>
779780
<include>src/main/java/com/rabbitmq/client/observation/**/*.java</include>
780781
<include>src/test/java/com/rabbitmq/client/test/functional/MicrometerObservationCollectorMetrics.java</include>
781782
<include>src/test/java/com/rabbitmq/client/test/NettyTest.java</include>

src/main/java/com/rabbitmq/client/ConnectionFactory.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,10 @@ protected synchronized FrameHandlerFactory createFrameHandlerFactory() throws IO
10701070
return this.frameHandlerFactory;
10711071
} else if (netty) {
10721072
if (this.frameHandlerFactory == null) {
1073+
Predicate<ShutdownSignalException> recoveryCondition =
1074+
this.connectionRecoveryTriggeringCondition == null
1075+
? AutorecoveringConnection.DEFAULT_CONNECTION_RECOVERY_TRIGGERING_CONDITION
1076+
: this.connectionRecoveryTriggeringCondition;
10731077
this.frameHandlerFactory =
10741078
new NettyFrameHandlerFactory(
10751079
this.nettyConf.eventLoopGroup,
@@ -1079,7 +1083,9 @@ protected synchronized FrameHandlerFactory createFrameHandlerFactory() throws IO
10791083
this.nettyConf.enqueuingTimeout,
10801084
connectionTimeout,
10811085
socketConf,
1082-
maxInboundMessageBodySize);
1086+
maxInboundMessageBodySize,
1087+
this.automaticRecovery,
1088+
recoveryCondition);
10831089
}
10841090
return this.frameHandlerFactory;
10851091
} else {

src/main/java/com/rabbitmq/client/impl/AbstractMetricsCollector.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ private ChannelState(Channel channel) {
426426
*
427427
* @deprecated Use {@link #markRejectedMessage(boolean)} instead
428428
*/
429+
@Deprecated
429430
protected abstract void markRejectedMessage();
430431

431432
/**
Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
// Copyright (c) 2007-2023 Broadcom. All Rights Reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
1+
// Copyright (c) 2007-2025 Broadcom. All Rights Reserved.
2+
// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
23
//
34
// This software, the RabbitMQ Java client library, is triple-licensed under the
45
// Mozilla Public License 2.0 ("MPL"), the GNU General Public License version 2
@@ -12,40 +13,45 @@
1213
//
1314
// If you have any questions regarding licensing, please contact us at
1415
15-
1616
package com.rabbitmq.client.impl;
1717

18+
import java.util.concurrent.Executors;
1819
import java.util.concurrent.ThreadFactory;
1920

2021
/**
21-
* Infers information about the execution environment, e.g.
22-
* security permissions.
23-
* Package-protected API.
22+
* Infers information about the execution environment, e.g. security permissions. Package-protected
23+
* API.
2424
*/
2525
public class Environment {
2626

27-
/**
28-
* This method is deprecated and subject to removal in the next major release.
29-
*
30-
* There is no replacement for this method, as it used to use the
31-
* {@link SecurityManager}, which is itself deprecated and subject to removal.
32-
* @deprecated
33-
* @return always returns true
34-
*/
35-
@Deprecated
36-
public static boolean isAllowedToModifyThreads() {
37-
return true;
38-
}
27+
/**
28+
* This method is deprecated and subject to removal in the next major release.
29+
*
30+
* <p>There is no replacement for this method, as it used to use the {@link SecurityManager},
31+
* which is itself deprecated and subject to removal.
32+
*
33+
* @deprecated
34+
* @return always returns true
35+
*/
36+
@Deprecated
37+
public static boolean isAllowedToModifyThreads() {
38+
return true;
39+
}
40+
41+
static Thread newThread(Runnable runnable, String name) {
42+
return newThread(Executors.defaultThreadFactory(), runnable, name);
43+
}
3944

40-
public static Thread newThread(ThreadFactory factory, Runnable runnable, String name) {
41-
Thread t = factory.newThread(runnable);
42-
t.setName(name);
43-
return t;
44-
}
45+
public static Thread newThread(ThreadFactory factory, Runnable runnable, String name) {
46+
Thread t = factory.newThread(runnable);
47+
t.setName(name);
48+
return t;
49+
}
4550

46-
public static Thread newThread(ThreadFactory factory, Runnable runnable, String name, boolean isDaemon) {
47-
Thread t = newThread(factory, runnable, name);
48-
t.setDaemon(isDaemon);
49-
return t;
50-
}
51+
public static Thread newThread(
52+
ThreadFactory factory, Runnable runnable, String name, boolean isDaemon) {
53+
Thread t = newThread(factory, runnable, name);
54+
t.setDaemon(isDaemon);
55+
return t;
56+
}
5157
}

src/main/java/com/rabbitmq/client/impl/NettyFrameHandlerFactory.java

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.rabbitmq.client.Address;
2424
import com.rabbitmq.client.ConnectionFactory;
2525
import com.rabbitmq.client.MalformedFrameException;
26+
import com.rabbitmq.client.ShutdownSignalException;
2627
import com.rabbitmq.client.SocketConfigurator;
2728
import io.netty.bootstrap.Bootstrap;
2829
import io.netty.buffer.ByteBuf;
@@ -59,6 +60,7 @@
5960
import java.util.concurrent.atomic.AtomicReference;
6061
import java.util.function.Consumer;
6162
import java.util.function.Function;
63+
import java.util.function.Predicate;
6264
import javax.net.ssl.SSLHandshakeException;
6365
import org.slf4j.Logger;
6466
import org.slf4j.LoggerFactory;
@@ -71,6 +73,7 @@ public final class NettyFrameHandlerFactory extends AbstractFrameHandlerFactory
7173
private final Consumer<Channel> channelCustomizer;
7274
private final Consumer<Bootstrap> bootstrapCustomizer;
7375
private final Duration enqueuingTimeout;
76+
private final Predicate<ShutdownSignalException> willRecover;
7477

7578
public NettyFrameHandlerFactory(
7679
EventLoopGroup eventLoopGroup,
@@ -80,14 +83,30 @@ public NettyFrameHandlerFactory(
8083
Duration enqueuingTimeout,
8184
int connectionTimeout,
8285
SocketConfigurator configurator,
83-
int maxInboundMessageBodySize) {
86+
int maxInboundMessageBodySize,
87+
boolean automaticRecovery,
88+
Predicate<ShutdownSignalException> recoveryCondition) {
8489
super(connectionTimeout, configurator, sslContextFactory != null, maxInboundMessageBodySize);
8590
this.eventLoopGroup = eventLoopGroup;
8691
this.sslContextFactory = sslContextFactory == null ? connName -> null : sslContextFactory;
8792
this.channelCustomizer = channelCustomizer == null ? Utils.noOpConsumer() : channelCustomizer;
8893
this.bootstrapCustomizer =
8994
bootstrapCustomizer == null ? Utils.noOpConsumer() : bootstrapCustomizer;
9095
this.enqueuingTimeout = enqueuingTimeout;
96+
this.willRecover =
97+
sse -> {
98+
if (!automaticRecovery) {
99+
return false;
100+
} else {
101+
try {
102+
return recoveryCondition.test(sse);
103+
} catch (Exception e) {
104+
// we assume it will recover, so we take the safe path to dispatch the closing
105+
// it avoids the risk of deadlock
106+
return true;
107+
}
108+
}
109+
};
91110
}
92111

93112
private static void closeNettyState(Channel channel, EventLoopGroup eventLoopGroup) {
@@ -131,6 +150,7 @@ public FrameHandler create(Address addr, String connectionName) throws IOExcepti
131150
sslContext,
132151
this.eventLoopGroup,
133152
this.enqueuingTimeout,
153+
this.willRecover,
134154
this.channelCustomizer,
135155
this.bootstrapCustomizer);
136156
}
@@ -161,6 +181,7 @@ private NettyFrameHandler(
161181
SslContext sslContext,
162182
EventLoopGroup elg,
163183
Duration enqueuingTimeout,
184+
Predicate<ShutdownSignalException> willRecover,
164185
Consumer<Channel> channelCustomizer,
165186
Consumer<Bootstrap> bootstrapCustomizer)
166187
throws IOException {
@@ -193,7 +214,8 @@ private NettyFrameHandler(
193214
int lengthFieldOffset = 3;
194215
int lengthFieldLength = 4;
195216
int lengthAdjustement = 1;
196-
AmqpHandler amqpHandler = new AmqpHandler(maxInboundMessageBodySize, this::close);
217+
AmqpHandler amqpHandler =
218+
new AmqpHandler(maxInboundMessageBodySize, this::close, willRecover);
197219
int port = ConnectionFactory.portOrDefault(addr.getPort(), sslContext != null);
198220
b.handler(
199221
new ChannelInitializer<SocketChannel>() {
@@ -401,14 +423,26 @@ private static class AmqpHandler extends ChannelInboundHandlerAdapter {
401423

402424
private final int maxPayloadSize;
403425
private final Runnable closeSequence;
426+
private final Predicate<ShutdownSignalException> willRecover;
404427
private volatile AMQConnection connection;
428+
private volatile Channel ch;
405429
private final AtomicBoolean writable = new AtomicBoolean(true);
406430
private final AtomicReference<CountDownLatch> writableLatch =
407431
new AtomicReference<>(new CountDownLatch(1));
408432

409-
private AmqpHandler(int maxPayloadSize, Runnable closeSequence) {
433+
private AmqpHandler(
434+
int maxPayloadSize,
435+
Runnable closeSequence,
436+
Predicate<ShutdownSignalException> willRecover) {
410437
this.maxPayloadSize = maxPayloadSize;
411438
this.closeSequence = closeSequence;
439+
this.willRecover = willRecover;
440+
}
441+
442+
@Override
443+
public void channelActive(ChannelHandlerContext ctx) throws Exception {
444+
this.ch = ctx.channel();
445+
super.channelActive(ctx);
412446
}
413447

414448
@Override
@@ -441,7 +475,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
441475
if (noProblem
442476
&& (!this.connection.isRunning() || this.connection.hasBrokerInitiatedShutdown())) {
443477
// looks like the frame was Close-Ok or Close
444-
ctx.executor().submit(() -> this.connection.doFinalShutdown());
478+
this.dispatchShutdownToConnection(() -> this.connection.doFinalShutdown());
445479
}
446480
} finally {
447481
m.release();
@@ -468,10 +502,10 @@ public void channelInactive(ChannelHandlerContext ctx) {
468502
AMQConnection c = this.connection;
469503
if (c.isOpen()) {
470504
// it is likely to be an IO exception
471-
c.handleIoError(null);
505+
this.dispatchShutdownToConnection(() -> c.handleIoError(null));
472506
} else {
473507
// just in case, the call is idempotent anyway
474-
c.doFinalShutdown();
508+
this.dispatchShutdownToConnection(c::doFinalShutdown);
475509
}
476510
}
477511
}
@@ -497,7 +531,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
497531
this.connection.getAddress().getHostName(),
498532
this.connection.getPort());
499533
if (needToDispatchIoError()) {
500-
this.connection.handleHeartbeatFailure();
534+
this.dispatchShutdownToConnection(() -> this.connection.handleHeartbeatFailure());
501535
}
502536
} else if (e.state() == IdleState.WRITER_IDLE) {
503537
this.connection.writeFrame(new Frame(AMQP.FRAME_HEARTBEAT, 0));
@@ -509,7 +543,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
509543

510544
private void handleIoError(Throwable cause) {
511545
if (needToDispatchIoError()) {
512-
this.connection.handleIoError(cause);
546+
this.dispatchShutdownToConnection(() -> this.connection.handleIoError(cause));
513547
} else {
514548
this.closeSequence.run();
515549
}
@@ -527,6 +561,32 @@ private boolean isWritable() {
527561
private CountDownLatch writableLatch() {
528562
return this.writableLatch.get();
529563
}
564+
565+
protected void dispatchShutdownToConnection(Runnable connectionShutdownRunnable) {
566+
String name = "rabbitmq-connection-shutdown";
567+
AMQConnection c = this.connection;
568+
if (c == null || ch == null) {
569+
// not enough information, we dispatch in separate thread
570+
Environment.newThread(connectionShutdownRunnable, name).start();
571+
} else {
572+
if (ch.eventLoop().inEventLoop()) {
573+
if (this.willRecover.test(c.getCloseReason())) {
574+
// the connection will recover, we don't want this to happen in the event loop,
575+
// it could cause a deadlock, so using a separate thread
576+
name = name + "-" + c;
577+
System.out.println("in separate thread");
578+
Environment.newThread(connectionShutdownRunnable, name).start();
579+
} else {
580+
// no recovery, it is safe to dispatch in the event loop
581+
System.out.println("in event loop");
582+
ch.eventLoop().submit(connectionShutdownRunnable);
583+
}
584+
} else {
585+
// not in the event loop, we can run it in the same thread
586+
connectionShutdownRunnable.run();
587+
}
588+
}
589+
}
530590
}
531591

532592
private static final class ProtocolVersionMismatchHandler extends ChannelInboundHandlerAdapter {

src/test/java/com/rabbitmq/client/AmqpClientTestExtension.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ public void afterAll(ExtensionContext context) {
144144
try {
145145
eventLoopGroup.shutdownGracefully(0, 0, SECONDS).get(10, SECONDS);
146146
} catch (InterruptedException e) {
147-
LOGGER.debug("Error while asynchronously closing Netty event loop group", e);
148147
Thread.currentThread().interrupt();
149148
} catch (Exception e) {
150149
LOGGER.warn("Error while asynchronously closing Netty event loop group", e);

0 commit comments

Comments
 (0)