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<>();