diff --git a/core/src/main/java/com/datastax/dse/driver/internal/core/insights/InsightsClient.java b/core/src/main/java/com/datastax/dse/driver/internal/core/insights/InsightsClient.java index 168477894ed..a592336a0dc 100644 --- a/core/src/main/java/com/datastax/dse/driver/internal/core/insights/InsightsClient.java +++ b/core/src/main/java/com/datastax/dse/driver/internal/core/insights/InsightsClient.java @@ -313,7 +313,7 @@ private InsightsStartupData createStartupData() { .withDriverVersion(getDriverVersion(startupOptions)) .withContactPoints( getResolvedContactPoints( - driverContext.getMetadataManager().getContactPoints().stream() + driverContext.getMetadataManager().getResolvedContactPoints().stream() .map(n -> n.getEndPoint().resolve()) .filter(InetSocketAddress.class::isInstance) .map(InetSocketAddress.class::cast) 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 56a6fe9c2be..6efcbcb35ca 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 e7c606cade8..ccc9c5d81b2 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 @@ -664,7 +664,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 8375f0ef30b..e1a6cc46e20 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 @@ -166,11 +166,11 @@ protected DriverConfigLoader defaultConfigLoader(@Nullable ClassLoader classLoad *

Contact points can also be provided statically in the configuration. If both are specified, * they will be merged. If both are absent, the driver will default to 127.0.0.1:9042. * - *

Contrary to the configuration, DNS names with multiple A-records will not be handled here. - * If you need that, extract them manually with {@link java.net.InetAddress#getAllByName(String)} - * before calling this method. Similarly, if you need connect addresses to stay unresolved, make - * sure you pass unresolved instances here (see {@code advanced.resolve-contact-points} in the - * configuration for more explanations). + *

Addresses passed here are used as-is. For config-based contact points specified as + * hostnames, the driver automatically expands each hostname to all its DNS-mapped IPs at query + * plan time (via {@code MetadataManager.getResolvedContactPoints()}), so passing a single + * hostname in the configuration is sufficient to try all its IPs on initial connect. The {@code + * advanced.resolve-contact-points} option is deprecated and has no effect. */ @NonNull public SelfT addContactPoints(@NonNull Collection contactPoints) { @@ -957,11 +957,11 @@ 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 at query plan time (before the first + // connection attempt) via MetadataManager.getResolvedContactPoints(). 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/loadbalancing/helper/OptionalLocalDcHelper.java b/core/src/main/java/com/datastax/oss/driver/internal/core/loadbalancing/helper/OptionalLocalDcHelper.java index b93a16a6525..97aab92fff7 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/loadbalancing/helper/OptionalLocalDcHelper.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/loadbalancing/helper/OptionalLocalDcHelper.java @@ -26,7 +26,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -68,73 +67,34 @@ public OptionalLocalDcHelper( @Override @NonNull public Optional discoverLocalDc(@NonNull Map nodes) { - String localDcStr = context.getLocalDatacenter(profile.getName()); - Optional localDc; - if (localDcStr != null) { - LOG.debug("[{}] Local DC set programmatically: {}", logPrefix, localDcStr); - localDc = Optional.of(localDcStr); + String localDc = context.getLocalDatacenter(profile.getName()); + if (localDc != null) { + LOG.debug("[{}] Local DC set programmatically: {}", logPrefix, localDc); } else if (profile.isDefined(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER)) { - localDcStr = profile.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER); - LOG.debug("[{}] Local DC set from configuration: {}", logPrefix, localDcStr); - localDc = Optional.of(localDcStr); - } else { - localDc = Optional.empty(); - } - if (localDc.isPresent()) { - checkLocalDatacenterCompatibility( - localDc.get(), context.getMetadataManager().getContactPoints()); - // Also warn if the configured DC doesn't match any node in the cluster - if (!nodes.isEmpty()) { - boolean found = false; - for (Node node : nodes.values()) { - if (localDc.get().equals(node.getDatacenter())) { - found = true; - break; - } - } - if (!found) { - LOG.warn( - "[{}] Configured local DC '{}' does not match any node's datacenter" - + " (available DCs: {}); please verify your configuration", - logPrefix, - localDc.get(), - formatDcs(nodes.values())); - } - } + localDc = profile.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER); + LOG.debug("[{}] Local DC set from configuration: {}", logPrefix, localDc); } else { LOG.debug("[{}] Local DC not set, DC awareness will be disabled", logPrefix); + return Optional.empty(); } - return localDc; - } - - /** - * Checks if the contact points are compatible with the local datacenter specified either through - * configuration, or programmatically. - * - *

The default implementation logs a warning when a contact point reports a datacenter - * different from the local one, and only for the default profile. - * - * @param localDc The local datacenter, as specified in the config, or programmatically. - * @param contactPoints The contact points provided when creating the session. - */ - protected void checkLocalDatacenterCompatibility( - @NonNull String localDc, Set contactPoints) { - if (profile.getName().equals(DriverExecutionProfile.DEFAULT_NAME)) { - Set badContactPoints = new LinkedHashSet<>(); - for (Node node : contactPoints) { - if (!Objects.equals(localDc, node.getDatacenter())) { - badContactPoints.add(node); + if (!nodes.isEmpty()) { + boolean found = false; + for (Node node : nodes.values()) { + if (localDc.equals(node.getDatacenter())) { + found = true; + break; } } - if (!badContactPoints.isEmpty()) { + if (!found) { LOG.warn( - "[{}] You specified {} as the local DC, but some contact points are from a different DC: {}; " - + "please provide the correct local DC, or check your contact points", + "[{}] Configured local DC '{}' does not match any node's datacenter" + + " (available DCs: {}); please verify your configuration", logPrefix, localDc, - formatNodesAndDcs(badContactPoints)); + formatDcs(nodes.values())); } } + return Optional.of(localDc); } /** 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..05683ce0bc5 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,8 +147,7 @@ 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()); + List nodes = new ArrayList<>(context.getMetadataManager().getResolvedContactPoints()); Collections.shuffle(nodes); return new ConcurrentLinkedQueue<>(nodes); case RUNNING: @@ -170,9 +169,11 @@ public Queue newControlReconnectionQueryPlan() { .getConfig() .getDefaultProfile() .getBoolean(DefaultDriverOption.CONTROL_CONNECTION_RECONNECT_CONTACT_POINTS)) { - Set originalNodes = context.getMetadataManager().getContactPoints(); + // Use DNS-expanded contact points so every resolved IP is tried as a fallback, not just + // the first IP the JVM happens to return for the hostname. + List resolvedNodes = context.getMetadataManager().getResolvedContactPoints(); List contactNodes = new ArrayList<>(); - for (DefaultNode node : originalNodes) { + for (Node node : resolvedNodes) { contactNodes.add(DefaultNode.newContactPoint(node.getEndPoint(), context)); } Collections.shuffle(contactNodes); 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..c3800f3c98b 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,60 @@ public Set getContactPoints() { return contactPoints; } + /** + * Returns the contact points expanded to all their DNS-resolved IPs. + * + *

Contact points are stored with unresolved hostnames (the driver always uses deferred DNS + * resolution). For each contact point whose underlying address is an unresolved {@link + * InetSocketAddress}, 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/dse/driver/internal/core/insights/InsightsClientTest.java b/core/src/test/java/com/datastax/dse/driver/internal/core/insights/InsightsClientTest.java index 5085432dbec..5f31090bf3f 100644 --- a/core/src/test/java/com/datastax/dse/driver/internal/core/insights/InsightsClientTest.java +++ b/core/src/test/java/com/datastax/dse/driver/internal/core/insights/InsightsClientTest.java @@ -487,7 +487,7 @@ private DefaultDriverContext mockDefaultDriverContext() throws UnknownHostExcept EndPoint contactEndPoint = mock(EndPoint.class); when(contactEndPoint.resolve()).thenReturn(new InetSocketAddress("127.0.0.1", 9999)); when(contactPoint.getEndPoint()).thenReturn(contactEndPoint); - when(manager.getContactPoints()).thenReturn(ImmutableSet.of(contactPoint)); + when(manager.getResolvedContactPoints()).thenReturn(ImmutableList.of(contactPoint)); DriverConfig driverConfig = mock(DriverConfig.class); when(context.getConfig()).thenReturn(driverConfig); 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..a7c83f3cd22 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 @@ -44,6 +44,7 @@ import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.util.ArrayList; import java.util.Map; import java.util.Objects; import java.util.Queue; @@ -100,7 +101,7 @@ public void setup() { Objects.requireNonNull(node3.getHostId()), node3); when(metadataManager.getMetadata()).thenReturn(metadata); when(metadata.getNodes()).thenReturn(allNodes); - when(metadataManager.getContactPoints()).thenReturn(contactPoints); + when(metadataManager.getResolvedContactPoints()).thenReturn(new ArrayList<>(contactPoints)); when(context.getMetadataManager()).thenReturn(metadataManager); when(context.getConfig()).thenReturn(config); @@ -130,26 +131,36 @@ public void setup() { @Test public void should_build_control_connection_query_plan_from_contact_points_before_init() { + // Given — simulate DNS expansion: node1's hostname resolves to two IPs (node1 + node4) + DefaultNode node4 = TestNodeFactory.newNode(4, context); + when(metadataManager.getResolvedContactPoints()) + .thenReturn(ImmutableList.of(node1, node2, node4)); + // When Queue queryPlan = wrapper.newControlReconnectionQueryPlan(); - // Then + // Then — query plan contains all DNS-expanded nodes, not just the original 2 contact points for (LoadBalancingPolicy policy : ImmutableList.of(policy1, policy2, policy3)) { verify(policy, never()).newQueryPlan(null, null); } - assertThat(queryPlan).hasSameElementsAs(contactPoints); + assertThat(queryPlan).containsExactlyInAnyOrder(node1, node2, node4); } @Test public void should_build_query_plan_from_contact_points_before_init() { + // Given — simulate DNS expansion: node1's hostname resolves to two IPs (node1 + node4) + DefaultNode node4 = TestNodeFactory.newNode(4, context); + when(metadataManager.getResolvedContactPoints()) + .thenReturn(ImmutableList.of(node1, node2, node4)); + // When Queue queryPlan = wrapper.newQueryPlan(null, DriverExecutionProfile.DEFAULT_NAME, null); - // Then + // Then — query plan contains all DNS-expanded nodes, not just the original 2 contact points for (LoadBalancingPolicy policy : ImmutableList.of(policy1, policy2, policy3)) { verify(policy, never()).newQueryPlan(null, null); } - assertThat(queryPlan).hasSameElementsAs(contactPoints); + assertThat(queryPlan).containsExactlyInAnyOrder(node1, node2, node4); } @Test @@ -204,8 +215,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 DNS-expanded (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 6fb1384d889..642cf06e9bf 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,80 @@ 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(); + } + } + + @Test + public void should_skip_contact_point_when_hostname_cannot_be_resolved() { + // Given — a hostname that is guaranteed to fail DNS resolution + EndPoint badEndPoint = + new DefaultEndPoint(InetSocketAddress.createUnresolved("nonexistent.invalid", 9042)); + metadataManager.addContactPoints(ImmutableSet.of(badEndPoint)); + + // When + List resolved = metadataManager.getResolvedContactPoints(); + + // Then — the unresolvable hostname is silently skipped (logged as WARN), result is empty + assertThat(resolved).isEmpty(); + } + private static class TestMetadataManager extends MetadataManager { private List refreshes = new CopyOnWriteArrayList<>(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/resolver/MockResolverIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/resolver/MockResolverIT.java index 4e9eefebf63..1d5ee3ec343 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/resolver/MockResolverIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/resolver/MockResolverIT.java @@ -25,7 +25,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.datastax.oss.driver.api.core.CqlSession; @@ -109,7 +108,7 @@ public void should_connect_with_mocked_hostname() { assertThat(filteredNodes).hasSize(1); InetSocketAddress address = (InetSocketAddress) filteredNodes.iterator().next().getEndPoint().resolve(); - assertTrue(address.isUnresolved()); + assertFalse(address.isUnresolved()); } } }