From 24057c49e30e1555a7a4d338bfee9f80f6052198 Mon Sep 17 00:00:00 2001 From: themechbro Date: Fri, 22 May 2026 20:45:00 +0530 Subject: [PATCH 1/2] netty: Allow network errors to override graceful shutdown status Fixes #12812 --- .../ClientTransportLifecycleManager.java | 5 ++++ .../grpc/netty/NettyClientTransportTest.java | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java b/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java index 01e7bc3ed12..327fcdf65fb 100644 --- a/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java +++ b/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java @@ -69,6 +69,11 @@ public void notifyGracefulShutdown(Status s, DisconnectError disconnectError) { public boolean notifyShutdown(Status s, DisconnectError disconnectError) { notifyGracefulShutdown(s, disconnectError); if (shutdownStatus != null) { + // Status Upgrade: Overwrite graceful shutdown if a hard network error occurs + if (shutdownStatus.getCause() == null && s.getCause() != null) { + shutdownStatus = s; + return true; + } return false; } shutdownStatus = s; diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index db44c8f50fd..a333746ed33 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -299,6 +299,30 @@ public void maxMessageSizeShouldBeEnforced() throws Throwable { } } +@Test + public void networkErrorOverridesGracefulShutdownStatus() throws Exception { + startServer(); + NettyClientTransport transport = newTransport(newNegotiator()); + callMeMaybe(transport.start(clientTransportListener)); + + // 1. Trigger graceful shutdown (Has NO cause) + Status gracefulStatus = Status.UNAVAILABLE.withDescription("Channel shutdown invoked"); + transport.shutdown(gracefulStatus); + + // 2. Simulate hard network error by dropping the channel + // Netty will automatically generate a Status with a ClosedChannelException + transport.channel().pipeline().fireChannelInactive(); + + // 3. Verify the listener receives the network error (Has Cause), not the graceful status + verify(clientTransportListener, timeout(5000)).transportShutdown( + org.mockito.ArgumentMatchers.argThat(status -> + status != null && status.getCause() instanceof java.nio.channels.ClosedChannelException + ), + org.mockito.ArgumentMatchers.any() + ); + } + + /** * Verifies that we can create multiple TLS client transports from the same builder. */ From 706eb40bcfc725f8a9de1750181ba3d81487c3be Mon Sep 17 00:00:00 2001 From: themechbro Date: Fri, 22 May 2026 22:56:30 +0530 Subject: [PATCH 2/2] netty: Fix test indentation and ignore ClosedChannelException in status override --- .../grpc/netty/ClientTransportLifecycleManager.java | 5 ++++- .../io/grpc/netty/NettyClientTransportTest.java | 13 +++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java b/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java index 327fcdf65fb..2c456aebf54 100644 --- a/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java +++ b/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java @@ -69,8 +69,11 @@ public void notifyGracefulShutdown(Status s, DisconnectError disconnectError) { public boolean notifyShutdown(Status s, DisconnectError disconnectError) { notifyGracefulShutdown(s, disconnectError); if (shutdownStatus != null) { + // Check if the incoming error is just the routine channel closure exception + boolean isClosedChannel = s.getCause() instanceof java.nio.channels.ClosedChannelException; + // Status Upgrade: Overwrite graceful shutdown if a hard network error occurs - if (shutdownStatus.getCause() == null && s.getCause() != null) { + if (shutdownStatus.getCause() == null && s.getCause() != null && !isClosedChannel) { shutdownStatus = s; return true; } diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index a333746ed33..7d1ead1face 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -299,24 +299,25 @@ public void maxMessageSizeShouldBeEnforced() throws Throwable { } } -@Test + @Test public void networkErrorOverridesGracefulShutdownStatus() throws Exception { startServer(); NettyClientTransport transport = newTransport(newNegotiator()); callMeMaybe(transport.start(clientTransportListener)); - // 1. Trigger graceful shutdown (Has NO cause) + // 1. Trigger graceful shutdown Status gracefulStatus = Status.UNAVAILABLE.withDescription("Channel shutdown invoked"); transport.shutdown(gracefulStatus); - // 2. Simulate hard network error by dropping the channel - // Netty will automatically generate a Status with a ClosedChannelException + // 2. Simulate a real network drop (e.g., Connection Reset) + java.io.IOException networkCause = new java.io.IOException("Connection reset by peer"); + transport.channel().pipeline().fireExceptionCaught(networkCause); transport.channel().pipeline().fireChannelInactive(); - // 3. Verify the listener receives the network error (Has Cause), not the graceful status + // 3. Verify the listener receives the IO error, NOT the graceful status verify(clientTransportListener, timeout(5000)).transportShutdown( org.mockito.ArgumentMatchers.argThat(status -> - status != null && status.getCause() instanceof java.nio.channels.ClosedChannelException + status != null && status.getCause() instanceof java.io.IOException ), org.mockito.ArgumentMatchers.any() );