diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java index c5a482c5d50..f949e5004c2 100644 --- a/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java +++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java @@ -837,7 +837,11 @@ public enum DefaultDriverOption implements DriverOption { * Whether to resolve the addresses passed to `basic.contact-points`. * *

Value-type: boolean + * + * @deprecated Contact points are now always kept as unresolved hostnames and expanded to all + * their DNS-mapped IPs lazily at connection time. Setting this option has no effect. */ + @Deprecated RESOLVE_CONTACT_POINTS("advanced.resolve-contact-points"), /** diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java index db5edb5b947..4131408655e 100644 --- a/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java +++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java @@ -656,7 +656,13 @@ public String toString() { /** The coalescer reschedule interval. */ public static final TypedDriverOption COALESCER_INTERVAL = new TypedDriverOption<>(DefaultDriverOption.COALESCER_INTERVAL, GenericType.DURATION); - /** Whether to resolve the addresses passed to `basic.contact-points`. */ + /** + * Whether to resolve the addresses passed to `basic.contact-points`. + * + * @deprecated Contact points are now always kept as unresolved hostnames and expanded to all + * their DNS-mapped IPs lazily at connection time. Setting this option has no effect. + */ + @Deprecated public static final TypedDriverOption RESOLVE_CONTACT_POINTS = new TypedDriverOption<>(DefaultDriverOption.RESOLVE_CONTACT_POINTS, GenericType.BOOLEAN); /** diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/session/SessionBuilder.java b/core/src/main/java/com/datastax/oss/driver/api/core/session/SessionBuilder.java index a2c0f933efc..a161718a1eb 100644 --- a/core/src/main/java/com/datastax/oss/driver/api/core/session/SessionBuilder.java +++ b/core/src/main/java/com/datastax/oss/driver/api/core/session/SessionBuilder.java @@ -935,11 +935,10 @@ protected final CompletionStage buildDefaultSessionAsync() { programmaticArguments = programmaticArgumentsBuilder.build(); } - boolean resolveAddresses = - defaultConfig.getBoolean(DefaultDriverOption.RESOLVE_CONTACT_POINTS, false); - + // RESOLVE_CONTACT_POINTS is deprecated: contact points are always kept as unresolved + // hostnames and expanded to all their DNS IPs lazily at connection time. Set contactPoints = - ContactPoints.merge(programmaticContactPoints, configContactPoints, resolveAddresses); + ContactPoints.merge(programmaticContactPoints, configContactPoints, false); if (keyspace == null && defaultConfig.isDefined(DefaultDriverOption.SESSION_KEYSPACE)) { keyspace = diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapper.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapper.java index f3f3e4fe346..f0697d95ae4 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapper.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapper.java @@ -147,7 +147,6 @@ public Queue newQueryPlan( switch (stateRef.get()) { case BEFORE_INIT: case DURING_INIT: - // The contact points are not stored in the metadata yet: List nodes = new ArrayList<>(context.getMetadataManager().getContactPoints()); Collections.shuffle(nodes); return new ConcurrentLinkedQueue<>(nodes); @@ -170,6 +169,10 @@ public Queue newControlReconnectionQueryPlan() { .getConfig() .getDefaultProfile() .getBoolean(DefaultDriverOption.CONTROL_CONNECTION_RECONNECT_CONTACT_POINTS)) { + // Use the original (potentially unresolved) contact-point endpoints so that the control + // connection channel retains the hostname, preserving hostname-based node identity in + // metadata. DNS expansion to all IPs for each hostname is handled by ChannelFactory at + // actual connection time. Set originalNodes = context.getMetadataManager().getContactPoints(); List contactNodes = new ArrayList<>(); for (DefaultNode node : originalNodes) { diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/MetadataManager.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/MetadataManager.java index cd765c818e6..7e8a5c7b709 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/MetadataManager.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/MetadataManager.java @@ -49,8 +49,11 @@ import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; import edu.umd.cs.findbugs.annotations.NonNull; import io.netty.util.concurrent.EventExecutor; +import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.UnknownHostException; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -173,6 +176,59 @@ public Set getContactPoints() { return contactPoints; } + /** + * Returns the contact points expanded to all their DNS-resolved IPs. + * + *

For each contact point whose underlying address is an unresolved hostname (i.e. stored as + * {@code InetSocketAddress.createUnresolved(...)} when {@code RESOLVE_CONTACT_POINTS=false}), + * this method calls {@link InetAddress#getAllByName(String)} to obtain every IP the hostname maps + * to and creates a synthetic contact-point {@link DefaultNode} for each IP. This lets the load + * balancing policy iterate over all candidate IPs rather than only the first one, so that a + * non-responsive IP does not block initial connection or control-connection reconnection. + * + *

Already-resolved addresses and non-{@link InetSocketAddress} endpoints are returned as-is. + */ + public List getResolvedContactPoints() { + Set nodes = contactPoints; + if (nodes == null) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (DefaultNode node : nodes) { + EndPoint endPoint = node.getEndPoint(); + if (endPoint instanceof DefaultEndPoint) { + InetSocketAddress address = ((DefaultEndPoint) endPoint).resolve(); + if (address.isUnresolved()) { + // Expand hostname to all IPs so callers can try each one in turn. + try { + InetAddress[] all = InetAddress.getAllByName(address.getHostString()); + if (all.length > 1) { + LOG.debug( + "[{}] Contact point {} expands to {} addresses", + logPrefix, + address.getHostString(), + all.length); + } + for (InetAddress ip : all) { + InetSocketAddress resolved = new InetSocketAddress(ip, address.getPort()); + result.add(DefaultNode.newContactPoint(new DefaultEndPoint(resolved), context)); + } + } catch (UnknownHostException e) { + LOG.warn( + "[{}] Could not resolve contact point hostname {}, skipping", + logPrefix, + address.getHostString(), + e); + } + continue; + } + } + // Already resolved or non-InetSocketAddress endpoint — use as-is. + result.add(node); + } + return result; + } + /** Whether the default contact point was used (because none were provided explicitly). */ public boolean wasImplicitContactPoint() { return wasImplicitContactPoint; diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapperTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapperTest.java index 89b36b9ee09..21f1a7a196a 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapperTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapperTest.java @@ -204,8 +204,7 @@ public void should_fetch_control_connection_query_plan_from_policy_after_init() assertThat(queryPlan.poll()).isEqualTo(node3); assertThat(queryPlan.poll()).isEqualTo(node2); assertThat(queryPlan.poll()).isEqualTo(node1); - // Remaining nodes are contact points appended at the end. - // They are new DefaultNode instances created via newContactPoint, so compare by endpoint. + // Remaining nodes are the resolved contact points appended at the end. Set remainingEndpoints = new java.util.HashSet<>(); for (Node n : queryPlan) { remainingEndpoints.add(n.getEndPoint()); diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/MetadataManagerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/MetadataManagerTest.java index 9c5cbdba8ee..7bf9f96febf 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/MetadataManagerTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/MetadataManagerTest.java @@ -490,6 +490,66 @@ public void should_throw_on_registerNode_with_null_hostId() { .hasMessageContaining("Cannot register node without hostId"); } + @Test + public void should_return_empty_list_when_contact_points_not_yet_set() { + // contactPoints field is null until addContactPoints is called + assertThat(metadataManager.getResolvedContactPoints()).isEmpty(); + } + + @Test + public void should_return_already_resolved_contact_points_unchanged() { + // Given — a contact point with an already-resolved InetSocketAddress + metadataManager.addContactPoints(ImmutableSet.of(END_POINT2)); + + // When + List resolved = metadataManager.getResolvedContactPoints(); + + // Then — the single node is returned as-is (no expansion needed) + assertThat(resolved).hasSize(1); + assertThat(resolved.get(0).getEndPoint()).isEqualTo(END_POINT2); + } + + @Test + public void should_expand_unresolved_hostname_to_all_ips() { + // Given — a contact point with an unresolved hostname (localhost → 127.0.0.1) + EndPoint unresolvedEndPoint = + new DefaultEndPoint(InetSocketAddress.createUnresolved("localhost", 9042)); + metadataManager.addContactPoints(ImmutableSet.of(unresolvedEndPoint)); + + // When + List resolved = metadataManager.getResolvedContactPoints(); + + // Then — at least one node is returned, each with a resolved address + assertThat(resolved).isNotEmpty(); + for (Node node : resolved) { + InetSocketAddress addr = (InetSocketAddress) node.getEndPoint().resolve(); + assertThat(addr.isUnresolved()).isFalse(); + assertThat(addr.getPort()).isEqualTo(9042); + } + } + + @Test + public void should_expand_multiple_contact_points_independently() { + // Given — two contact points: one already resolved, one unresolved + EndPoint resolvedEndPoint = END_POINT3; + EndPoint unresolvedEndPoint = + new DefaultEndPoint(InetSocketAddress.createUnresolved("localhost", 9042)); + metadataManager.addContactPoints(ImmutableSet.of(resolvedEndPoint, unresolvedEndPoint)); + + // When + List resolved = metadataManager.getResolvedContactPoints(); + + // Then — at least 2 nodes: 1 for the resolved + at least 1 for localhost expansion + assertThat(resolved.size()).isGreaterThanOrEqualTo(2); + // The resolved endpoint must appear + assertThat(resolved).anySatisfy(n -> assertThat(n.getEndPoint()).isEqualTo(resolvedEndPoint)); + // All returned addresses must be resolved + for (Node node : resolved) { + InetSocketAddress addr = (InetSocketAddress) node.getEndPoint().resolve(); + assertThat(addr.isUnresolved()).isFalse(); + } + } + private static class TestMetadataManager extends MetadataManager { private List refreshes = new CopyOnWriteArrayList<>();