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

frankvicky pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new d1c0bd82c0e KAFKA-14648: Do not fail clients if bootstrap servers is 
not immediately resolvable (2/2) (#22897)
d1c0bd82c0e is described below

commit d1c0bd82c0e6591ec999fb23e5feca05360e7256
Author: TengYao Chi <[email protected]>
AuthorDate: Wed Jul 29 21:00:39 2026 +0100

    KAFKA-14648: Do not fail clients if bootstrap servers is not immediately 
resolvable (2/2) (#22897)
    
    This PR is a part of KIP-909.  It aims to apply the KIP-909 to Share
    Consumer.
    
    Reviewers: Sushant Mahajan <[email protected]>, Lianet Magrans
     <[email protected]>
---
 .../consumer/internals/ShareConsumerImpl.java      | 15 +++++--
 .../clients/consumer/KafkaShareConsumerTest.java   | 48 ++++++++++++++++++++++
 2 files changed, 59 insertions(+), 4 deletions(-)

diff --git 
a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/ShareConsumerImpl.java
 
b/clients/src/main/java/org/apache/kafka/clients/consumer/internals/ShareConsumerImpl.java
index 04d3955ec02..a0c980a6cd2 100644
--- 
a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/ShareConsumerImpl.java
+++ 
b/clients/src/main/java/org/apache/kafka/clients/consumer/internals/ShareConsumerImpl.java
@@ -55,6 +55,7 @@ import org.apache.kafka.common.Metric;
 import org.apache.kafka.common.MetricName;
 import org.apache.kafka.common.TopicIdPartition;
 import org.apache.kafka.common.Uuid;
+import org.apache.kafka.common.errors.BootstrapResolutionException;
 import org.apache.kafka.common.errors.GroupAuthorizationException;
 import org.apache.kafka.common.errors.InterruptException;
 import org.apache.kafka.common.errors.InvalidGroupIdException;
@@ -77,7 +78,6 @@ import org.apache.kafka.common.utils.internals.LogContext;
 import org.slf4j.Logger;
 import org.slf4j.event.Level;
 
-import java.net.InetSocketAddress;
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.Collection;
@@ -272,8 +272,6 @@ public class ShareConsumerImpl<K, V> implements 
ShareConsumerDelegate<K, V> {
                     metrics.reporters(),
                     Arrays.asList(deserializers.keyDeserializer(), 
deserializers.valueDeserializer()));
             this.metadata = new ShareConsumerMetadata(config, subscriptions, 
logContext, clusterResourceListeners);
-            final List<InetSocketAddress> addresses = 
ClientUtils.parseAndValidateAddresses(config);
-            metadata.bootstrap(addresses);
 
             this.shareFetchMetricsManager = 
createShareFetchMetricsManager(metrics);
             ApiVersions apiVersions = new ApiVersions();
@@ -1095,8 +1093,11 @@ public class ShareConsumerImpl<K, V> implements 
ShareConsumerDelegate<K, V> {
         try {
             // If users have fatal error, they will get some exceptions in the 
background queue.
             // When running unsubscribe, these exceptions should be ignored, 
or users can't unsubscribe successfully.
+            // BootstrapResolutionException is also ignored here: a permanent 
DNS failure prevents the
+            // unsubscribe from completing normally, but it is not actionable 
during close.
             processBackgroundEvents(unsubscribeEvent.future(), timer, e -> (e 
instanceof GroupAuthorizationException
-                || e instanceof TopicAuthorizationException || e instanceof 
InvalidTopicException));
+                || e instanceof TopicAuthorizationException || e instanceof 
InvalidTopicException
+                || e instanceof BootstrapResolutionException));
             log.info("Completed releasing assignment and leaving group to 
close consumer.");
         } catch (TimeoutException e) {
             log.warn("Consumer triggered an unsubscribe event to leave the 
group but couldn't " +
@@ -1125,6 +1126,12 @@ public class ShareConsumerImpl<K, V> implements 
ShareConsumerDelegate<K, V> {
             release();
             throw new IllegalStateException("This consumer has already been 
closed.");
         }
+        try {
+            metadata.maybeThrowBootstrapFatalException();
+        } catch (RuntimeException e) {
+            release();
+            throw e;
+        }
     }
 
     /**
diff --git 
a/clients/src/test/java/org/apache/kafka/clients/consumer/KafkaShareConsumerTest.java
 
b/clients/src/test/java/org/apache/kafka/clients/consumer/KafkaShareConsumerTest.java
index 45242f6d0b2..51d7d61f2b5 100644
--- 
a/clients/src/test/java/org/apache/kafka/clients/consumer/KafkaShareConsumerTest.java
+++ 
b/clients/src/test/java/org/apache/kafka/clients/consumer/KafkaShareConsumerTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.kafka.clients.consumer;
 
+import org.apache.kafka.clients.CommonClientConfigs;
 import org.apache.kafka.clients.KafkaClient;
 import org.apache.kafka.clients.MockClient;
 import org.apache.kafka.clients.consumer.internals.AutoOffsetResetStrategy;
@@ -27,6 +28,7 @@ import org.apache.kafka.common.TopicIdPartition;
 import org.apache.kafka.common.TopicPartition;
 import org.apache.kafka.common.Uuid;
 import org.apache.kafka.common.compress.Compression;
+import org.apache.kafka.common.errors.BootstrapResolutionException;
 import org.apache.kafka.common.internals.ClusterResourceListeners;
 import org.apache.kafka.common.message.ShareAcknowledgeResponseData;
 import org.apache.kafka.common.message.ShareFetchResponseData;
@@ -66,7 +68,9 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
 
 // This test exercises the KafkaShareConsumer with the MockClient to validate 
the Kafka protocol RPCs
 @Timeout(value = 120)
@@ -409,4 +413,48 @@ public class KafkaShareConsumerTest {
                 .setResponses(new 
ShareAcknowledgeResponseData.ShareAcknowledgeTopicResponseCollection(List.of(topicResponse)))
         );
     }
+
+    @Test
+    public void 
testShareConsumerBootstrapResolutionExceptionPropagatedToPoll() {
+        // Use an invalid hostname that will fail DNS resolution (using RFC 
6761 reserved .invalid TLD)
+        String invalidHost = "unresolvable.invalid:9092";
+
+        Map<String, Object> configs = Map.of(
+            ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, 
StringDeserializer.class.getName(),
+            ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, 
StringDeserializer.class.getName(),
+            CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, invalidHost,
+            // Set a short bootstrap timeout so the test doesn't take too long
+            CommonClientConfigs.BOOTSTRAP_RESOLVE_TIMEOUT_MS_CONFIG, "3000",
+            ConsumerConfig.GROUP_ID_CONFIG, "test-share-group"
+        );
+
+        KafkaShareConsumer<String, String> consumer = new 
KafkaShareConsumer<>(configs);
+        try {
+            consumer.subscribe(Set.of("test-topic"));
+
+            // Poll continuously until we get the BootstrapResolutionException
+            // The exception should be thrown after 
bootstrap.resolve.timeout.ms expires
+            BootstrapResolutionException exception =
+                assertThrows(BootstrapResolutionException.class, () -> {
+                    long startTime = System.currentTimeMillis();
+                    long maxWaitTime = 15000; // 15 seconds max to prevent 
test hanging
+
+                    while (System.currentTimeMillis() - startTime < 
maxWaitTime) {
+                        consumer.poll(Duration.ofMillis(100));
+                    }
+                    fail("Expected BootstrapResolutionException to be thrown 
within " + maxWaitTime + "ms");
+                });
+
+            // Verify the exception message contains information about DNS 
resolution failure
+            assertTrue(exception.getMessage().contains("Failed to resolve 
bootstrap servers") ||
+                       exception.getMessage().contains("DNS resolution"),
+                       "Exception message should mention DNS resolution 
failure: " + exception.getMessage());
+
+            // After the first failure, any further API call must also throw. 
This guards against
+            // accidentally clearing the bootstrap error from the metadata 
layer.
+            assertThrows(BootstrapResolutionException.class, () -> 
consumer.poll(Duration.ofMillis(100)));
+        } finally {
+            consumer.close();
+        }
+    }
 }

Reply via email to