You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by mm...@apache.org on 2026/06/17 19:01:03 UTC

(pulsar) branch master updated: [fix][client] Prevent client shutdown from leaking event loop threads when DNS resolver close fails (#26045)

This is an automated email from the ASF dual-hosted git repository.

merlimat pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar.git


The following commit(s) were added to refs/heads/master by this push:
     new d58727f3bf2 [fix][client] Prevent client shutdown from leaking event loop threads when DNS resolver close fails (#26045)
d58727f3bf2 is described below

commit d58727f3bf2777170ccc19afccf0c4364b94e4da
Author: David Kjerrumgaard <35...@users.noreply.github.com>
AuthorDate: Wed Jun 17 12:00:56 2026 -0700

    [fix][client] Prevent client shutdown from leaking event loop threads when DNS resolver close fails (#26045)
---
 .../pulsar/client/impl/PulsarClientImpl.java       | 14 +++++++--
 .../pulsar/client/impl/PulsarClientImplTest.java   | 34 ++++++++++++++++++++++
 2 files changed, 46 insertions(+), 2 deletions(-)

diff --git a/pulsar-client/src/main/java/org/apache/pulsar/client/impl/PulsarClientImpl.java b/pulsar-client/src/main/java/org/apache/pulsar/client/impl/PulsarClientImpl.java
index 23e6b94dcea..9d0a814c5eb 100644
--- a/pulsar-client/src/main/java/org/apache/pulsar/client/impl/PulsarClientImpl.java
+++ b/pulsar-client/src/main/java/org/apache/pulsar/client/impl/PulsarClientImpl.java
@@ -1071,11 +1071,21 @@ public class PulsarClientImpl implements PulsarClient {
             }
 
             if (addressResolver != null) {
-                addressResolver.close();
+                try {
+                    addressResolver.close();
+                } catch (Throwable t) {
+                    log.warn().exception(t).log("Failed to close addressResolver");
+                    throwable = t;
+                }
             }
 
             if (dnsResolverGroupLocalInstance != null) {
-                dnsResolverGroupLocalInstance.close();
+                try {
+                    dnsResolverGroupLocalInstance.close();
+                } catch (Throwable t) {
+                    log.warn().exception(t).log("Failed to close dnsResolverGroup");
+                    throwable = t;
+                }
             }
 
             try {
diff --git a/pulsar-client/src/test/java/org/apache/pulsar/client/impl/PulsarClientImplTest.java b/pulsar-client/src/test/java/org/apache/pulsar/client/impl/PulsarClientImplTest.java
index b742af7336d..702410f9612 100644
--- a/pulsar-client/src/test/java/org/apache/pulsar/client/impl/PulsarClientImplTest.java
+++ b/pulsar-client/src/test/java/org/apache/pulsar/client/impl/PulsarClientImplTest.java
@@ -24,6 +24,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -39,6 +40,7 @@ import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelPromise;
 import io.netty.channel.EventLoopGroup;
+import io.netty.resolver.AddressResolver;
 import io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider;
 import io.netty.util.HashedWheelTimer;
 import io.netty.util.concurrent.DefaultThreadFactory;
@@ -183,6 +185,38 @@ public class PulsarClientImplTest {
         verify(timer).stop();
     }
 
+    @Test
+    public void testShutdownContinuesWhenAddressResolverCloseFails() throws Exception {
+        ClientConfigurationData conf = new ClientConfigurationData();
+        conf.setServiceUrl("pulsar://localhost:6650");
+        PulsarClientImpl client = new PulsarClientImpl(conf);
+
+        // Simulate the Netty DNS resolver shutdown race where AddressResolver.close()
+        // throws "channel not registered to an event loop".
+        @SuppressWarnings("unchecked")
+        AddressResolver<InetSocketAddress> failingResolver = mock(AddressResolver.class);
+        doThrow(new IllegalStateException("channel not registered to an event loop"))
+                .when(failingResolver).close();
+        Field addressResolverField = PulsarClientImpl.class.getDeclaredField("addressResolver");
+        addressResolverField.setAccessible(true);
+        addressResolverField.set(client, failingResolver);
+
+        // Replace the timer with a mock so we can assert that shutdown still reaches the
+        // cleanup steps that run after the address resolver is closed.
+        HashedWheelTimer timer = mock(HashedWheelTimer.class);
+        Field timerField = PulsarClientImpl.class.getDeclaredField("timer");
+        timerField.setAccessible(true);
+        timerField.set(client, timer);
+
+        // shutdown() still surfaces the original failure, but it must not abort early:
+        // the remaining resources have to be released regardless. The key assertion is
+        // that timer.stop() (a step that runs after the resolver is closed) is reached.
+        assertThrows(IllegalStateException.class, client::shutdown);
+
+        verify(failingResolver).close();
+        verify(timer).stop();
+    }
+
     @Test
     public void testInitializeWithTimer() throws PulsarClientException {
         ClientConfigurationData conf = new ClientConfigurationData();