Skip to content

Commit 694f0c0

Browse files
committed
fix: re-resolve DNS on each reconnect for hostname-based contact points
Introduce HostNameEndPoint, an EndPoint implementation that calls InetAddress.getAllByName() on every resolve() invocation. ContactPoints now creates HostNameEndPoint for hostname-based contact points (vs DefaultEndPoint for IP literals), allowing the driver to automatically pick up a new IP after a node replacement updates the DNS entry without requiring a driver restart. Fixes CUSTOMER-258.
1 parent 3c1267a commit 694f0c0

4 files changed

Lines changed: 260 additions & 21 deletions

File tree

core/src/main/java/com/datastax/oss/driver/internal/core/ContactPoints.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919

2020
import com.datastax.oss.driver.api.core.metadata.EndPoint;
2121
import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint;
22+
import com.datastax.oss.driver.internal.core.metadata.HostNameEndPoint;
2223
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet;
2324
import com.datastax.oss.driver.shaded.guava.common.collect.Sets;
25+
import com.datastax.oss.driver.shaded.guava.common.net.InetAddresses;
2426
import java.net.InetAddress;
2527
import java.net.InetSocketAddress;
2628
import java.net.UnknownHostException;
@@ -41,18 +43,17 @@ public static Set<EndPoint> merge(
4143

4244
Set<EndPoint> result = Sets.newHashSet(programmaticContactPoints);
4345
for (String spec : configContactPoints) {
44-
for (InetSocketAddress address : extract(spec, resolve)) {
45-
DefaultEndPoint endPoint = new DefaultEndPoint(address);
46+
for (EndPoint endPoint : extract(spec, resolve)) {
4647
boolean wasNew = result.add(endPoint);
4748
if (!wasNew) {
48-
LOG.warn("Duplicate contact point {}", address);
49+
LOG.warn("Duplicate contact point {}", endPoint);
4950
}
5051
}
5152
}
5253
return ImmutableSet.copyOf(result);
5354
}
5455

55-
private static Set<InetSocketAddress> extract(String spec, boolean resolve) {
56+
private static Set<EndPoint> extract(String spec, boolean resolve) {
5657
int separator = spec.lastIndexOf(':');
5758
if (separator < 0) {
5859
LOG.warn("Ignoring invalid contact point {} (expecting host:port)", spec);
@@ -69,8 +70,9 @@ private static Set<InetSocketAddress> extract(String spec, boolean resolve) {
6970
return Collections.emptySet();
7071
}
7172
if (!resolve) {
72-
return ImmutableSet.of(InetSocketAddress.createUnresolved(host, port));
73-
} else {
73+
return ImmutableSet.of(new DefaultEndPoint(InetSocketAddress.createUnresolved(host, port)));
74+
} else if (InetAddresses.isInetAddress(host)) {
75+
// IP literal: resolve once at startup and cache in DefaultEndPoint
7476
try {
7577
InetAddress[] inetAddresses = InetAddress.getAllByName(host);
7678
if (inetAddresses.length > 1) {
@@ -79,15 +81,26 @@ private static Set<InetSocketAddress> extract(String spec, boolean resolve) {
7981
spec,
8082
Arrays.deepToString(inetAddresses));
8183
}
82-
Set<InetSocketAddress> result = new HashSet<>();
84+
Set<EndPoint> result = new HashSet<>();
8385
for (InetAddress inetAddress : inetAddresses) {
84-
result.add(new InetSocketAddress(inetAddress, port));
86+
result.add(new DefaultEndPoint(new InetSocketAddress(inetAddress, port)));
8587
}
8688
return result;
8789
} catch (UnknownHostException e) {
8890
LOG.warn("Ignoring invalid contact point {} (unknown host {})", spec, host);
8991
return Collections.emptySet();
9092
}
93+
} else {
94+
// Hostname: validate it resolves now, but use HostNameEndPoint so DNS is re-queried on each
95+
// reconnect attempt. This allows the driver to pick up a new IP after a node replacement
96+
// updates the DNS entry (e.g. in cloud deployments).
97+
try {
98+
InetAddress.getAllByName(host);
99+
return ImmutableSet.of(new HostNameEndPoint(host, port));
100+
} catch (UnknownHostException e) {
101+
LOG.warn("Ignoring invalid contact point {} (unknown host {})", spec, host);
102+
return Collections.emptySet();
103+
}
91104
}
92105
}
93106
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package com.datastax.oss.driver.internal.core.metadata;
19+
20+
import com.datastax.oss.driver.api.core.metadata.EndPoint;
21+
import com.datastax.oss.driver.shaded.guava.common.primitives.UnsignedBytes;
22+
import edu.umd.cs.findbugs.annotations.NonNull;
23+
import java.net.InetAddress;
24+
import java.net.InetSocketAddress;
25+
import java.net.UnknownHostException;
26+
import java.util.Arrays;
27+
import java.util.Comparator;
28+
import java.util.Locale;
29+
import java.util.Objects;
30+
import java.util.concurrent.atomic.AtomicLong;
31+
32+
/**
33+
* An {@link EndPoint} implementation for hostname-based contact points that re-resolves DNS on
34+
* every {@link #resolve()} call. This allows the driver to automatically pick up a new IP address
35+
* after a node replacement updates the DNS entry, without requiring a driver restart.
36+
*
37+
* <p>Contrast with {@link DefaultEndPoint}, which caches the resolved IP at construction time.
38+
*/
39+
public class HostNameEndPoint implements EndPoint {
40+
41+
private static final AtomicLong OFFSET = new AtomicLong();
42+
43+
private final String hostName;
44+
private final int port;
45+
46+
public HostNameEndPoint(String hostName, int port) {
47+
this.hostName =
48+
Objects.requireNonNull(hostName, "hostName can't be null").toLowerCase(Locale.ROOT);
49+
this.port = port;
50+
}
51+
52+
@NonNull
53+
@Override
54+
public InetSocketAddress resolve() {
55+
try {
56+
InetAddress[] addresses = InetAddress.getAllByName(hostName);
57+
if (addresses.length == 0) {
58+
// Probably never happens, but the JDK docs don't explicitly say so
59+
throw new IllegalArgumentException("Could not resolve contact point hostname " + hostName);
60+
}
61+
// Sort by IP for a true round-robin (order of getAllByName results is unspecified)
62+
Arrays.sort(addresses, IP_COMPARATOR);
63+
int index =
64+
(addresses.length == 1)
65+
? 0
66+
: (int) Math.floorMod(OFFSET.getAndIncrement(), (long) addresses.length);
67+
return new InetSocketAddress(addresses[index], port);
68+
} catch (UnknownHostException e) {
69+
throw new IllegalArgumentException("Could not resolve contact point hostname " + hostName, e);
70+
}
71+
}
72+
73+
@Override
74+
public boolean equals(Object other) {
75+
if (other == this) {
76+
return true;
77+
} else if (other instanceof HostNameEndPoint) {
78+
HostNameEndPoint that = (HostNameEndPoint) other;
79+
return this.hostName.equals(that.hostName) && this.port == that.port;
80+
} else {
81+
return false;
82+
}
83+
}
84+
85+
@Override
86+
public int hashCode() {
87+
return Objects.hash(hostName, port);
88+
}
89+
90+
@Override
91+
public String toString() {
92+
return hostName + ":" + port;
93+
}
94+
95+
@NonNull
96+
@Override
97+
public String asMetricPrefix() {
98+
return hostName.replace('.', '_') + ':' + port;
99+
}
100+
101+
@SuppressWarnings("UnnecessaryLambda")
102+
private static final Comparator<InetAddress> IP_COMPARATOR =
103+
(InetAddress address1, InetAddress address2) ->
104+
UnsignedBytes.lexicographicalComparator()
105+
.compare(address1.getAddress(), address2.getAddress());
106+
}

core/src/test/java/com/datastax/oss/driver/internal/core/ContactPointsTest.java

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
import static org.assertj.core.api.Assertions.assertThat;
2121
import static org.assertj.core.api.Assertions.filter;
22-
import static org.junit.Assume.assumeTrue;
2322
import static org.mockito.Mockito.atLeast;
2423
import static org.mockito.Mockito.verify;
2524

@@ -29,11 +28,10 @@
2928
import ch.qos.logback.core.Appender;
3029
import com.datastax.oss.driver.api.core.metadata.EndPoint;
3130
import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint;
31+
import com.datastax.oss.driver.internal.core.metadata.HostNameEndPoint;
3232
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
3333
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet;
34-
import java.net.InetAddress;
3534
import java.net.InetSocketAddress;
36-
import java.net.UnknownHostException;
3735
import java.util.Collections;
3836
import java.util.Set;
3937
import org.junit.After;
@@ -100,19 +98,22 @@ public void should_parse_host_and_port_in_configuration_and_create_unresolved()
10098
}
10199

102100
@Test
103-
public void should_parse_host_and_port_and_resolve_all_a_records() throws UnknownHostException {
104-
int localhostARecordsCount = InetAddress.getAllByName("localhost").length;
105-
assumeTrue(
106-
"This test assumes that localhost resolves to multiple A-records",
107-
localhostARecordsCount >= 2);
108-
101+
public void should_create_hostname_endpoint_for_hostname_contact_point() {
109102
Set<EndPoint> endPoints =
110103
ContactPoints.merge(Collections.emptySet(), ImmutableList.of("localhost:9042"), true);
111104

112-
assertThat(endPoints).hasSize(localhostARecordsCount);
113-
assertLog(
114-
Level.INFO,
115-
"Contact point localhost:9042 resolves to multiple addresses, will use them all");
105+
assertThat(endPoints).hasSize(1);
106+
assertThat(endPoints.iterator().next()).isInstanceOf(HostNameEndPoint.class);
107+
assertThat(endPoints).containsExactly(new HostNameEndPoint("localhost", 9042));
108+
}
109+
110+
@Test
111+
public void should_create_default_endpoint_for_ip_contact_point() {
112+
Set<EndPoint> endPoints =
113+
ContactPoints.merge(Collections.emptySet(), ImmutableList.of("127.0.0.1:9042"), true);
114+
115+
assertThat(endPoints).hasSize(1);
116+
assertThat(endPoints.iterator().next()).isInstanceOf(DefaultEndPoint.class);
116117
}
117118

118119
@Test
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package com.datastax.oss.driver.internal.core.metadata;
19+
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
22+
23+
import java.net.InetSocketAddress;
24+
import org.junit.Test;
25+
26+
public class HostNameEndPointTest {
27+
28+
@Test
29+
public void should_return_resolved_address() {
30+
HostNameEndPoint endPoint = new HostNameEndPoint("localhost", 9042);
31+
32+
InetSocketAddress address = endPoint.resolve();
33+
34+
assertThat(address).isNotNull();
35+
assertThat(address.getPort()).isEqualTo(9042);
36+
assertThat(address.isUnresolved()).isFalse();
37+
}
38+
39+
@Test
40+
public void should_perform_dns_lookup_on_each_resolve_call() throws Exception {
41+
// Re-resolution is structural: resolve() calls InetAddress.getAllByName() every time
42+
// with no cached field. We verify this by calling resolve() twice and confirming both
43+
// calls succeed and return consistent results (same host, same port), which would fail
44+
// if DNS were broken after the first call.
45+
HostNameEndPoint endPoint = new HostNameEndPoint("localhost", 9042);
46+
47+
InetSocketAddress address1 = endPoint.resolve();
48+
InetSocketAddress address2 = endPoint.resolve();
49+
50+
assertThat(address1.getPort()).isEqualTo(9042);
51+
assertThat(address2.getPort()).isEqualTo(9042);
52+
assertThat(address1.isUnresolved()).isFalse();
53+
assertThat(address2.isUnresolved()).isFalse();
54+
// Both resolved addresses must belong to localhost
55+
assertThat(address1.getAddress().isLoopbackAddress()).isTrue();
56+
assertThat(address2.getAddress().isLoopbackAddress()).isTrue();
57+
}
58+
59+
@Test
60+
public void should_throw_on_resolve_if_hostname_unknown() {
61+
HostNameEndPoint endPoint = new HostNameEndPoint("this-host-does-not-exist.invalid", 9042);
62+
63+
assertThatThrownBy(endPoint::resolve)
64+
.isInstanceOf(IllegalArgumentException.class)
65+
.hasMessageContaining("this-host-does-not-exist.invalid");
66+
}
67+
68+
@Test
69+
public void should_normalize_hostname_to_lowercase() {
70+
HostNameEndPoint endPoint = new HostNameEndPoint("MyHost.Example.COM", 9042);
71+
72+
assertThat(endPoint.toString()).isEqualTo("myhost.example.com:9042");
73+
assertThat(endPoint.asMetricPrefix()).isEqualTo("myhost_example_com:9042");
74+
}
75+
76+
@Test
77+
public void should_implement_equals_based_on_hostname_and_port() {
78+
HostNameEndPoint ep1 = new HostNameEndPoint("localhost", 9042);
79+
HostNameEndPoint ep2 = new HostNameEndPoint("localhost", 9042);
80+
HostNameEndPoint ep3 = new HostNameEndPoint("localhost", 9043);
81+
HostNameEndPoint ep4 = new HostNameEndPoint("otherhost", 9042);
82+
83+
assertThat(ep1).isEqualTo(ep2);
84+
assertThat(ep1).isNotEqualTo(ep3);
85+
assertThat(ep1).isNotEqualTo(ep4);
86+
assertThat(ep1).isNotEqualTo(new DefaultEndPoint(new InetSocketAddress("localhost", 9042)));
87+
}
88+
89+
@Test
90+
public void should_implement_hashcode_consistently_with_equals() {
91+
HostNameEndPoint ep1 = new HostNameEndPoint("localhost", 9042);
92+
HostNameEndPoint ep2 = new HostNameEndPoint("localhost", 9042);
93+
94+
assertThat(ep1.hashCode()).isEqualTo(ep2.hashCode());
95+
}
96+
97+
@Test
98+
public void should_be_equal_regardless_of_input_case() {
99+
HostNameEndPoint ep1 = new HostNameEndPoint("MyHost", 9042);
100+
HostNameEndPoint ep2 = new HostNameEndPoint("myhost", 9042);
101+
102+
assertThat(ep1).isEqualTo(ep2);
103+
assertThat(ep1.hashCode()).isEqualTo(ep2.hashCode());
104+
}
105+
106+
@Test
107+
public void should_format_toString() {
108+
HostNameEndPoint endPoint = new HostNameEndPoint("node-0.example.com", 9042);
109+
110+
assertThat(endPoint.toString()).isEqualTo("node-0.example.com:9042");
111+
}
112+
113+
@Test
114+
public void should_format_metric_prefix() {
115+
HostNameEndPoint endPoint = new HostNameEndPoint("node-0.example.com", 9042);
116+
117+
assertThat(endPoint.asMetricPrefix()).isEqualTo("node-0_example_com:9042");
118+
}
119+
}

0 commit comments

Comments
 (0)