Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,22 @@ public enum DefaultDriverOption implements DriverOption {
*
* <p>Value type: boolean
*/
CLIENT_ROUTES_SHARD_AWARENESS_ENABLED("advanced.client-routes.shard-awarness-enabled");
CLIENT_ROUTES_SHARD_AWARENESS_ENABLED("advanced.client-routes.shard-awarness-enabled"),

/**
* Whether the driver connects exclusively through proxies when client routes are configured.
*
* <p>When {@code true}, any node whose {@code host_id} does not appear in the {@code
* system.client_routes} table is treated as unreachable; the driver will never fall back to the
* node's broadcast address. The node stays DOWN and the reconnection loop retries until a {@code
* CLIENT_ROUTES_CHANGE} event populates the route.
*
* <p>When {@code false} (the default), nodes that have no route entry are contacted directly
* using their broadcast address, preserving backward-compatible mixed proxy/direct topologies.
*
* <p>Value type: boolean
*/
CLIENT_ROUTES_EXCLUSIVE_PROXY("advanced.client-routes.exclusive-proxy");

private final String path;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ protected static void fillWithDriverDefaults(OptionsMap map) {
// values) with no sensible scalar default, analogous to how CONFIG_RELOAD_INTERVAL is omitted.
map.put(TypedDriverOption.CLIENT_ROUTES_NATIVE_TRANSPORT_PORT, 9042);
map.put(TypedDriverOption.CLIENT_ROUTES_SHARD_AWARENESS_ENABLED, false);
map.put(TypedDriverOption.CLIENT_ROUTES_EXCLUSIVE_PROXY, false);
}

@Immutable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,15 @@ public String toString() {
new TypedDriverOption<>(
DefaultDriverOption.CLIENT_ROUTES_SHARD_AWARENESS_ENABLED, GenericType.BOOLEAN);

/**
* Whether the driver connects exclusively through proxies when client routes are configured. When
* {@code false} (default), nodes without a route entry are contacted directly via their broadcast
* address.
*/
public static final TypedDriverOption<Boolean> CLIENT_ROUTES_EXCLUSIVE_PROXY =
new TypedDriverOption<>(
DefaultDriverOption.CLIENT_ROUTES_EXCLUSIVE_PROXY, GenericType.BOOLEAN);

private static Iterable<TypedDriverOption<?>> introspectBuiltInValues() {
try {
ImmutableList.Builder<TypedDriverOption<?>> result = ImmutableList.builder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@
import java.net.SocketAddress;
import java.util.Objects;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ClientRoutesEndPoint implements EndPoint {
private static final Logger LOG = LoggerFactory.getLogger(ClientRoutesEndPoint.class);

private final UUID hostId;
private final ClientRoutesTopologyMonitor topologyMonitor;
private final String metricPrefix;
@NonNull private final EndPoint fallbackEndPoint;
@Nullable private final EndPoint fallbackEndPoint;
private final boolean exclusiveProxy;

/**
* @param topologyMonitor the topology monitor used to resolve the endpoint address on demand.
Expand All @@ -42,18 +47,26 @@ public class ClientRoutesEndPoint implements EndPoint {
* determined, in which case the hostId is used as the metric prefix instead.
* @param fallbackEndPoint the default endpoint to fall back to when {@code
* topologyMonitor.resolve()} returns {@code null}, i.e. when this node is not accessed via a
* cloud private endpoint. Must not be {@code null}.
* cloud private endpoint. Must be non-null when {@code exclusiveProxy} is {@code false};
* ignored (and may be {@code null}) when {@code exclusiveProxy} is {@code true}.
* @param exclusiveProxy when {@code true}, {@link #resolve()} throws instead of falling back to
* {@code fallbackEndPoint} if no route is found.
*/
public ClientRoutesEndPoint(
@NonNull ClientRoutesTopologyMonitor topologyMonitor,
@NonNull UUID hostId,
@Nullable InetAddress broadcastInetAddress,
@NonNull EndPoint fallbackEndPoint) {
@Nullable EndPoint fallbackEndPoint,
boolean exclusiveProxy) {
this.topologyMonitor =
Objects.requireNonNull(topologyMonitor, "Topology monitor cannot be null");
this.hostId = Objects.requireNonNull(hostId, "HOST uuid cannot be null");
this.fallbackEndPoint =
Objects.requireNonNull(fallbackEndPoint, "Fallback endpoint cannot be null");
if (!exclusiveProxy && fallbackEndPoint == null) {
throw new IllegalArgumentException(
"fallbackEndPoint must be non-null when exclusiveProxy is false");
}
this.fallbackEndPoint = fallbackEndPoint;
this.exclusiveProxy = exclusiveProxy;
Comment thread
nikagra marked this conversation as resolved.
this.metricPrefix = buildMetricPrefix(broadcastInetAddress, hostId);
}

Expand All @@ -73,7 +86,26 @@ public SocketAddress resolve() {
} catch (IOException e) {
throw new UncheckedIOException("DNS resolution failed for host_id=" + hostId, e);
}
return fallbackEndPoint.resolve();
if (!exclusiveProxy && fallbackEndPoint != null) {
// Default (backward-compatible) mode: fall back to the node's broadcast address.
// This supports mixed proxy/direct topologies where some nodes are behind the private
// endpoint and others are reached directly.
return fallbackEndPoint.resolve();
}
// Exclusive-proxy mode: the driver must connect only through proxies. Falling back to the
// node's broadcast address would bypass the proxy infrastructure and cause silent
// misbehaviour (e.g. during the window between adding a new node and posting its client
// route entry). The node will remain DOWN and the reconnection loop will retry until a
// CLIENT_ROUTES_CHANGE event populates the route.
LOG.warn(
"No client route entry found for host_id={}. "
+ "The node will remain DOWN until a route is published via CLIENT_ROUTES_CHANGE.",
hostId);
throw new IllegalStateException(
"No client route entry found for host_id="
+ hostId
+ ". Will not connect to the node's broadcast address because client routes "
+ "are configured; the driver must connect exclusively through proxies.");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public class ClientRoutesTopologyMonitor extends DefaultTopologyMonitor {
private final String logPrefix;
private final AtomicReference<Map<UUID, ClientRouteRecord>> resolvedRoutesCache;
private final boolean useSSL;
private final boolean exclusiveProxy;
private volatile boolean closed = false;
private final AtomicInteger consecutiveEmptyResults = new AtomicInteger(0);

Expand Down Expand Up @@ -147,6 +148,11 @@ public ClientRoutesTopologyMonitor(
this.logPrefix = context.getSessionName();
this.resolvedRoutesCache = new AtomicReference<>(Collections.emptyMap());
this.useSSL = context.getSslEngineFactory().isPresent();
this.exclusiveProxy =
context
.getConfig()
.getDefaultProfile()
.getBoolean(DefaultDriverOption.CLIENT_ROUTES_EXCLUSIVE_PROXY);
}

@Override
Expand Down Expand Up @@ -459,14 +465,15 @@ protected EndPoint buildNodeEndPoint(
UUID hostId = row.getUuid("host_id");
if (hostId == null) {
LOG.warn(
"[{}] host_id is null in system row for address {} — cannot assign a client route. "
+ "This may indicate corrupted system tables. "
+ "Falling back to default endpoint resolution.",
"[{}] host_id is null in system row for address {} — cannot build a client-routes"
+ " endpoint. This may indicate corrupted system tables. The node will be ignored.",
logPrefix,
broadcastRpcAddress);
return super.buildNodeEndPoint(row, broadcastRpcAddress, localEndPoint);
throw new IllegalStateException(
"host_id is null in system row for address "
+ broadcastRpcAddress
+ "; cannot build a ClientRoutesEndPoint without a host_id");
}
EndPoint fallback = super.buildNodeEndPoint(row, broadcastRpcAddress, localEndPoint);
InetAddress broadcastInetAddress = null;
if (broadcastRpcAddress != null) {
broadcastInetAddress = broadcastRpcAddress.getAddress();
Expand All @@ -477,7 +484,9 @@ protected EndPoint buildNodeEndPoint(
if (broadcastInetAddress == null) {
broadcastInetAddress = row.getInetAddress("peer");
}
return new ClientRoutesEndPoint(this, hostId, broadcastInetAddress, fallback);
EndPoint fallback =
exclusiveProxy ? null : super.buildNodeEndPoint(row, broadcastRpcAddress, localEndPoint);
return new ClientRoutesEndPoint(this, hostId, broadcastInetAddress, fallback, exclusiveProxy);
}

/**
Expand Down
13 changes: 13 additions & 0 deletions core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,19 @@ datastax-java-driver {
# Default: false
shard-awarness-enabled = false

# When true, the driver connects exclusively through proxies and will never fall back to a
# node's broadcast address when no route entry exists in system.client_routes. Any node
# without a route is treated as unreachable; it goes DOWN and the reconnection loop retries
# until a CLIENT_ROUTES_CHANGE event populates the route.
#
# When false (default), nodes that have no route entry are reached directly using their
# broadcast address, preserving backward-compatible mixed proxy/direct topologies where some
# nodes are behind the private endpoint and others are not.
#
# Required: no
# Default: false
exclusive-proxy = false

}

# Whether to resolve the addresses passed to `basic.contact-points`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,32 +46,46 @@ public void should_resolve_via_topology_monitor() throws UnknownHostException {
InetSocketAddress expected = new InetSocketAddress("127.0.0.1", 9042);
when(topologyMonitor.resolve(hostId)).thenReturn(expected);

ClientRoutesEndPoint ep =
new ClientRoutesEndPoint(topologyMonitor, hostId, null, fallbackEndPoint);
ClientRoutesEndPoint ep = new ClientRoutesEndPoint(topologyMonitor, hostId, null, null, true);

assertThat(ep.resolve()).isEqualTo(expected);
}

@Test
public void should_fallback_when_resolve_returns_null() throws UnknownHostException {
public void should_throw_when_exclusive_proxy_and_resolve_returns_null()
throws UnknownHostException {
UUID hostId = UUID.randomUUID();
when(topologyMonitor.resolve(hostId)).thenReturn(null);

ClientRoutesEndPoint ep = new ClientRoutesEndPoint(topologyMonitor, hostId, null, null, true);

assertThatThrownBy(ep::resolve)
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("No client route entry found")
.hasMessageContaining(hostId.toString())
.hasMessageContaining("exclusively through proxies");
}

@Test
public void should_fall_back_to_broadcast_when_exclusive_proxy_disabled()
throws UnknownHostException {
UUID hostId = UUID.randomUUID();
InetSocketAddress fallbackAddr = new InetSocketAddress("10.0.0.1", 9042);
InetSocketAddress fallbackAddress = new InetSocketAddress("10.0.0.1", 9042);
when(topologyMonitor.resolve(hostId)).thenReturn(null);
when(fallbackEndPoint.resolve()).thenReturn(fallbackAddr);
when(fallbackEndPoint.resolve()).thenReturn(fallbackAddress);

ClientRoutesEndPoint ep =
new ClientRoutesEndPoint(topologyMonitor, hostId, null, fallbackEndPoint);
new ClientRoutesEndPoint(topologyMonitor, hostId, null, fallbackEndPoint, false);

assertThat(ep.resolve()).isEqualTo(fallbackAddr);
assertThat(ep.resolve()).isEqualTo(fallbackAddress);
}

@Test
public void should_wrap_io_exceptions_in_unchecked_io_exception() throws UnknownHostException {
UUID hostId = UUID.randomUUID();
when(topologyMonitor.resolve(hostId)).thenThrow(new UnknownHostException("no-such-host"));

ClientRoutesEndPoint ep =
new ClientRoutesEndPoint(topologyMonitor, hostId, null, fallbackEndPoint);
ClientRoutesEndPoint ep = new ClientRoutesEndPoint(topologyMonitor, hostId, null, null, true);

assertThatThrownBy(ep::resolve)
.isInstanceOf(UncheckedIOException.class)
Expand All @@ -85,8 +99,7 @@ public void should_reflect_route_changes_on_subsequent_resolve() throws UnknownH
InetSocketAddress addr2 = new InetSocketAddress("10.0.0.1", 9043);
when(topologyMonitor.resolve(hostId)).thenReturn(addr1);

ClientRoutesEndPoint ep =
new ClientRoutesEndPoint(topologyMonitor, hostId, null, fallbackEndPoint);
ClientRoutesEndPoint ep = new ClientRoutesEndPoint(topologyMonitor, hostId, null, null, true);

assertThat(ep.resolve()).isEqualTo(addr1);

Expand All @@ -101,10 +114,8 @@ public void should_reflect_route_changes_on_subsequent_resolve() throws UnknownH
@Test
public void should_be_equal_when_same_host_id() {
UUID hostId = UUID.randomUUID();
ClientRoutesEndPoint ep1 =
new ClientRoutesEndPoint(topologyMonitor, hostId, null, fallbackEndPoint);
ClientRoutesEndPoint ep2 =
new ClientRoutesEndPoint(topologyMonitor, hostId, null, fallbackEndPoint);
ClientRoutesEndPoint ep1 = new ClientRoutesEndPoint(topologyMonitor, hostId, null, null, true);
ClientRoutesEndPoint ep2 = new ClientRoutesEndPoint(topologyMonitor, hostId, null, null, true);

assertThat(ep1).isEqualTo(ep2);
assertThat(ep1.hashCode()).isEqualTo(ep2.hashCode());
Expand All @@ -113,18 +124,17 @@ public void should_be_equal_when_same_host_id() {
@Test
public void should_not_be_equal_when_different_host_id() {
ClientRoutesEndPoint ep1 =
new ClientRoutesEndPoint(topologyMonitor, UUID.randomUUID(), null, fallbackEndPoint);
new ClientRoutesEndPoint(topologyMonitor, UUID.randomUUID(), null, null, true);
ClientRoutesEndPoint ep2 =
new ClientRoutesEndPoint(topologyMonitor, UUID.randomUUID(), null, fallbackEndPoint);
new ClientRoutesEndPoint(topologyMonitor, UUID.randomUUID(), null, null, true);

assertThat(ep1).isNotEqualTo(ep2);
}

@Test
public void should_not_be_equal_to_non_client_routes_endpoint() {
UUID hostId = UUID.randomUUID();
ClientRoutesEndPoint ep =
new ClientRoutesEndPoint(topologyMonitor, hostId, null, fallbackEndPoint);
ClientRoutesEndPoint ep = new ClientRoutesEndPoint(topologyMonitor, hostId, null, null, true);

assertThat(ep).isNotEqualTo("not an endpoint");
assertThat(ep).isNotEqualTo(null);
Expand All @@ -135,8 +145,7 @@ public void should_not_be_equal_to_non_client_routes_endpoint() {
@Test
public void should_use_host_id_as_metric_prefix_when_address_is_null() {
UUID hostId = UUID.randomUUID();
ClientRoutesEndPoint ep =
new ClientRoutesEndPoint(topologyMonitor, hostId, null, fallbackEndPoint);
ClientRoutesEndPoint ep = new ClientRoutesEndPoint(topologyMonitor, hostId, null, null, true);

assertThat(ep.asMetricPrefix()).isEqualTo(hostId.toString());
}
Expand All @@ -145,8 +154,7 @@ public void should_use_host_id_as_metric_prefix_when_address_is_null() {
public void should_format_ipv4_metric_prefix() throws Exception {
UUID hostId = UUID.randomUUID();
InetAddress ipv4 = InetAddress.getByAddress(new byte[] {10, 0, 0, 1});
ClientRoutesEndPoint ep =
new ClientRoutesEndPoint(topologyMonitor, hostId, ipv4, fallbackEndPoint);
ClientRoutesEndPoint ep = new ClientRoutesEndPoint(topologyMonitor, hostId, ipv4, null, true);

assertThat(ep.asMetricPrefix()).isEqualTo("10_0_0_1_" + hostId);
}
Expand All @@ -156,8 +164,7 @@ public void should_format_ipv6_metric_prefix() throws Exception {
UUID hostId = UUID.randomUUID();
InetAddress ipv6 =
InetAddress.getByAddress(new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1});
ClientRoutesEndPoint ep =
new ClientRoutesEndPoint(topologyMonitor, hostId, ipv6, fallbackEndPoint);
ClientRoutesEndPoint ep = new ClientRoutesEndPoint(topologyMonitor, hostId, ipv6, null, true);

// IPv6 keeps colons (consistent with DefaultEndPoint), dots replaced by underscores
assertThat(ep.asMetricPrefix()).isEqualTo("0:0:0:0:0:0:0:1_" + hostId);
Expand All @@ -168,8 +175,7 @@ public void should_format_ipv6_metric_prefix() throws Exception {
@Test
public void should_return_host_id_as_string() {
UUID hostId = UUID.randomUUID();
ClientRoutesEndPoint ep =
new ClientRoutesEndPoint(topologyMonitor, hostId, null, fallbackEndPoint);
ClientRoutesEndPoint ep = new ClientRoutesEndPoint(topologyMonitor, hostId, null, null, true);

assertThat(ep.toString()).isEqualTo("ClientRoutesEndPoint(" + hostId + ")");
}
Expand Down
Loading
Loading