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

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


The following commit(s) were added to refs/heads/master by this push:
     new 229721e98 ZOOKEEPER-3100: ZooKeeper client times out due to random 
choice of resolved addresses
229721e98 is described below

commit 229721e98d562f1d3a74e9f8212155483dce269f
Author: Andor Molnár <[email protected]>
AuthorDate: Wed Dec 3 16:07:09 2025 -0600

    ZOOKEEPER-3100: ZooKeeper client times out due to random choice of resolved 
addresses
    
    Reviewers: kezhuw
    Author: anmolnar
    Closes #2338 from anmolnar/ZOOKEEPER-3100
---
 .../resources/markdown/zookeeperProgrammers.md     | 16 +++++++++
 .../main/java/org/apache/zookeeper/ZooKeeper.java  |  2 +-
 .../zookeeper/client/StaticHostProvider.java       | 32 ++++++++++++++++-
 .../apache/zookeeper/client/ZKClientConfig.java    | 15 ++++++++
 .../zookeeper/test/StaticHostProviderTest.java     | 40 ++++++++++++++++++++++
 5 files changed, 103 insertions(+), 2 deletions(-)

diff --git a/zookeeper-docs/src/main/resources/markdown/zookeeperProgrammers.md 
b/zookeeper-docs/src/main/resources/markdown/zookeeperProgrammers.md
index 620f0f508..59008215c 100644
--- a/zookeeper-docs/src/main/resources/markdown/zookeeperProgrammers.md
+++ b/zookeeper-docs/src/main/resources/markdown/zookeeperProgrammers.md
@@ -1381,6 +1381,22 @@ and [SASL authentication for 
ZooKeeper](https://cwiki.apache.org/confluence/disp
 * *zookeeper.kinit* :
     Specifies path to kinit binary. Default is "/usr/bin/kinit".
 
+* *zookeeper.shuffleDnsResponse* :
+  **New in 3.10.0:**
+  Specifies whether ZooKeeper client should randomly pick an IP address from 
the DNS lookup query result when resolving 
+  server addresses or not. This is a feature flag in order to keep the old 
behavior of the default DNS resolver in 
+  `StaticHostProvider`, because we disabled it by default. The reason behind 
that is shuffling the response of DNS queries 
+  shadows JVM network property `java.net.preferIPv6Addresses` (default: 
false). This property controls whether JVM's built-in 
+  resolver should prioritize v4 (false value) or v6 (true value) addresses 
when putting together the list of IP addresses 
+  in the result. In other words the above Java system property was completely 
ineffective in the case of ZooKeeper server host 
+  resolution protocol and this must have been fixed. In a dual stack 
environment one might want to prefer one stack over 
+  the other which, in case of Java processes, should be controlled by JVM 
network properties and ZooKeeper must honor 
+  these settings. Since the old behavior has been with us since day zero, we 
introduced this feature flag in case you 
+  need it. Such case could be when you have a buggy DNS server which responds 
IP addresses always in the same order and 
+  you want to randomize that.
+  Default: false
+
+
 <a name="C+Binding"></a>
 
 ### C Binding
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java 
b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java
index 8831c0622..b3b5af454 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java
@@ -1140,7 +1140,7 @@ public ZooKeeper(ZooKeeperOptions options) throws 
IOException {
         if (options.getHostProvider() != null) {
             hostProvider = 
options.getHostProvider().apply(connectStringParser.getServerAddresses());
         } else {
-            hostProvider = new 
StaticHostProvider(connectStringParser.getServerAddresses());
+            hostProvider = new 
StaticHostProvider(connectStringParser.getServerAddresses(), clientConfig);
         }
         this.hostProvider = hostProvider;
 
diff --git 
a/zookeeper-server/src/main/java/org/apache/zookeeper/client/StaticHostProvider.java
 
b/zookeeper-server/src/main/java/org/apache/zookeeper/client/StaticHostProvider.java
index bfd771943..e07754c35 100644
--- 
a/zookeeper-server/src/main/java/org/apache/zookeeper/client/StaticHostProvider.java
+++ 
b/zookeeper-server/src/main/java/org/apache/zookeeper/client/StaticHostProvider.java
@@ -50,6 +50,8 @@ public interface Resolver {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(StaticHostProvider.class);
 
+    private ZKClientConfig clientConfig = null;
+
     private List<InetSocketAddress> serverAddresses = new ArrayList<>(5);
 
     private Random sourceOfRandomness;
@@ -90,6 +92,26 @@ public InetAddress[] getAllByName(String name) throws 
UnknownHostException {
         });
     }
 
+    /**
+     * Constructs a SimpleHostSet with ZKClientConfig.
+     *
+     * Introduced this new overload in 3.10.0 in order to take advantage of 
some newly introduced feature flags. Like
+     * the shuffle (old) / not to shuffle (new) behavior of DNS resolution.
+     *
+     * @param serverAddresses
+     *            possibly unresolved ZooKeeper server addresses
+     * @param clientConfig
+     *            ZooKeeper client configuration
+     */
+    public StaticHostProvider(Collection<InetSocketAddress> serverAddresses, 
ZKClientConfig clientConfig) {
+        init(serverAddresses, System.currentTimeMillis() ^ this.hashCode(), 
new Resolver() {
+            @Override
+            public InetAddress[] getAllByName(String name) throws 
UnknownHostException {
+                return InetAddress.getAllByName(name);
+            }
+        }, clientConfig);
+    }
+
     /**
      * Constructs a SimpleHostSet.
      *
@@ -125,6 +147,12 @@ public InetAddress[] getAllByName(String name) throws 
UnknownHostException {
     }
 
     private void init(Collection<InetSocketAddress> serverAddresses, long 
randomnessSeed, Resolver resolver) {
+        init(serverAddresses, randomnessSeed, resolver, null);
+    }
+
+    private void init(Collection<InetSocketAddress> serverAddresses, long 
randomnessSeed, Resolver resolver,
+                      ZKClientConfig clientConfig) {
+        this.clientConfig = clientConfig == null ? new ZKClientConfig() : 
clientConfig;
         this.sourceOfRandomness = new Random(randomnessSeed);
         this.resolver = resolver;
         if (serverAddresses.isEmpty()) {
@@ -142,7 +170,9 @@ private InetSocketAddress resolve(InetSocketAddress 
address) {
             if (resolvedAddresses.isEmpty()) {
                 return address;
             }
-            Collections.shuffle(resolvedAddresses);
+            if (clientConfig.isShuffleDnsResponseEnabled()) {
+                Collections.shuffle(resolvedAddresses);
+            }
             return new InetSocketAddress(resolvedAddresses.get(0), 
address.getPort());
         } catch (UnknownHostException e) {
             LOG.error("Unable to resolve address: {}", address.toString(), e);
diff --git 
a/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZKClientConfig.java
 
b/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZKClientConfig.java
index 8ef00ac43..5472838bd 100644
--- 
a/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZKClientConfig.java
+++ 
b/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZKClientConfig.java
@@ -62,6 +62,13 @@ public class ZKClientConfig extends ZKConfig {
     public static final long ZOOKEEPER_REQUEST_TIMEOUT_DEFAULT = 0;
     public static final String ZK_SASL_CLIENT_ALLOW_REVERSE_DNS = 
"zookeeper.sasl.client.allowReverseDnsLookup";
     public static final boolean ZK_SASL_CLIENT_ALLOW_REVERSE_DNS_DEFAULT = 
false;
+    /**
+     * True value preserves the old behavior is to shuffle the addresses in 
DNS response regardless of address type.
+     * This could help with buggy DNS servers which always return addresses in 
the same order, but at the same time
+     * ignores the JVM option java.net.preferIPv6Addresses.
+     */
+    public static final String ZOOKEEPER_SHUFFLE_DNS_RESPONSE = 
"zookeeper.shuffleDnsResponse";
+    public static final boolean ZOOKEEPER_SHUFFLE_DNS_RESPONSE_DEFAULT = false;
 
     public ZKClientConfig() {
         super();
@@ -137,6 +144,14 @@ public boolean isSaslClientEnabled() {
         return Boolean.valueOf(getProperty(ENABLE_CLIENT_SASL_KEY, 
ENABLE_CLIENT_SASL_DEFAULT));
     }
 
+    /**
+     * Return true if we need to use the old behavior of default DNS resolver 
and always shuffle the IP addresses
+     * in the DNS lookup response in order to pick a completely random IP 
address.
+     */
+    public boolean isShuffleDnsResponseEnabled() {
+        return getBoolean(ZOOKEEPER_SHUFFLE_DNS_RESPONSE, 
ZOOKEEPER_SHUFFLE_DNS_RESPONSE_DEFAULT);
+    }
+
     /**
      * Get the value of the <code>key</code> property as an <code>long</code>.
      * If property is not set, the provided <code>defaultValue</code> is
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/test/StaticHostProviderTest.java
 
b/zookeeper-server/src/test/java/org/apache/zookeeper/test/StaticHostProviderTest.java
index 2289847d4..7ab207519 100644
--- 
a/zookeeper-server/src/test/java/org/apache/zookeeper/test/StaticHostProviderTest.java
+++ 
b/zookeeper-server/src/test/java/org/apache/zookeeper/test/StaticHostProviderTest.java
@@ -30,9 +30,12 @@
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
@@ -115,6 +118,7 @@ public void testNextDoesNotSleepForZero() {
         assertTrue(5 > stop - start);
     }
 
+
     @Test
     public void testEmptyServerAddressesList() {
         assertThrows(IllegalArgumentException.class, () -> {
@@ -888,6 +892,42 @@ public void testReResolvingLocalhost() {
                         + hostProvider.size() + " (after), " + sizeBefore + " 
(before)");
     }
 
+    @Test
+    public void testResolverKeepsResolutionOrderPreferV6() {
+        // Arrange
+        Collection<InetSocketAddress> servers = new ArrayList<>();
+        servers.add(InetSocketAddress.createUnresolved("test-server-name", 
1111));
+        InetAddress[] resolvedAddresses = new InetAddress[] {
+            mock(Inet6Address.class),
+            mock(Inet4Address.class)
+        };
+        StaticHostProvider staticHostProvider = new 
StaticHostProvider(servers, name -> resolvedAddresses);
+
+        // Act & Assert
+        for (int i = 0; i < 100; i++) {
+           InetSocketAddress next = staticHostProvider.next(0);
+           assertSame(next.getAddress(), resolvedAddresses[0], "Default 
resolver should always return the first address");
+        }
+    }
+
+    @Test
+    public void testResolverKeepsResolutionOrderPreferV4() {
+        // Arrange
+        Collection<InetSocketAddress> servers = new ArrayList<>();
+        servers.add(InetSocketAddress.createUnresolved("test-server-name", 
1111));
+        InetAddress[] resolvedAddresses = new InetAddress[] {
+            mock(Inet4Address.class),
+            mock(Inet6Address.class)
+        };
+        StaticHostProvider staticHostProvider = new 
StaticHostProvider(servers, name -> resolvedAddresses);
+
+        // Act & Assert
+        for (int i = 0; i < 100; i++) {
+            InetSocketAddress next = staticHostProvider.next(0);
+            assertSame(next.getAddress(), resolvedAddresses[0], "Default 
resolver should always return the first address");
+        }
+    }
+
     private StaticHostProvider getHostProviderUnresolved(byte size) {
         return new StaticHostProvider(getUnresolvedServerAddresses(size), 
r.nextLong());
     }

Reply via email to