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..24bf038a7fd 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
@@ -1173,7 +1173,22 @@ public enum DefaultDriverOption implements DriverOption {
*
*
Value-Type: boolean
*/
- ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES("advanced.address-translator.resolve-addresses");
+ ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES("advanced.address-translator.resolve-addresses"),
+ /**
+ * Whether the driver reports its effective client configuration to ScyllaDB at connection time.
+ *
+ *
When {@code true}, the driver adds two entries to the CQL {@code STARTUP} options, which
+ * ScyllaDB stores in {@code system.clients.client_options} so operators can inspect driver
+ * settings while investigating incidents: a {@code SESSION_ID} on every connection (so the server
+ * can group a session's connections) and a compact JSON payload under the {@code DRIVER_CONFIG}
+ * key on the control connection only, describing the effective configuration of the driver's
+ * default execution profile (connection/socket settings, timeouts, retry/reconnection/
+ * speculative-execution/load-balancing policies, connection pooling, query defaults, and TLS).
+ * When {@code false}, neither entry is sent and there is no change on the wire.
+ *
+ *
Value type: boolean
+ */
+ CLIENT_CONFIG_REPORTING_ENABLED("advanced.client-config-reporting.enabled");
private final String path;
diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java
index 60190fc1cce..86b985437cc 100644
--- a/core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java
+++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java
@@ -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_CONFIG_REPORTING_ENABLED, false);
}
@Immutable
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..e7865d449ef 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
@@ -976,6 +976,11 @@ public String toString() {
new TypedDriverOption<>(
DefaultDriverOption.CLIENT_ROUTES_SHARD_AWARENESS_ENABLED, GenericType.BOOLEAN);
+ /** Whether the driver reports its client configuration to ScyllaDB at connection time. */
+ public static final TypedDriverOption CLIENT_CONFIG_REPORTING_ENABLED =
+ new TypedDriverOption<>(
+ DefaultDriverOption.CLIENT_CONFIG_REPORTING_ENABLED, GenericType.BOOLEAN);
+
private static Iterable> introspectBuiltInValues() {
try {
ImmutableList.Builder> result = ImmutableList.builder();
diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannelOptions.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannelOptions.java
index 208cf52ac22..378fd2dc0b8 100644
--- a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannelOptions.java
+++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannelOptions.java
@@ -47,15 +47,24 @@ public static Builder builder() {
public final String ownerLogPrefix;
+ /**
+ * Whether the channel should report the driver configuration ({@code DRIVER_CONFIG}) in its
+ * {@code STARTUP} options. Set only for the control connection, so the (potentially large) config
+ * blob is sent once per session rather than on every pooled connection.
+ */
+ public final boolean reportConfig;
+
private DriverChannelOptions(
CqlIdentifier keyspace,
List eventTypes,
EventCallback eventCallback,
- String ownerLogPrefix) {
+ String ownerLogPrefix,
+ boolean reportConfig) {
this.keyspace = keyspace;
this.eventTypes = eventTypes;
this.eventCallback = eventCallback;
this.ownerLogPrefix = ownerLogPrefix;
+ this.reportConfig = reportConfig;
}
public static class Builder {
@@ -63,6 +72,7 @@ public static class Builder {
private List eventTypes = Collections.emptyList();
private EventCallback eventCallback = null;
private String ownerLogPrefix = null;
+ private boolean reportConfig = false;
public Builder withKeyspace(CqlIdentifier keyspace) {
this.keyspace = keyspace;
@@ -82,8 +92,17 @@ public Builder withOwnerLogPrefix(String ownerLogPrefix) {
return this;
}
+ /**
+ * Marks this channel as the one that reports {@code DRIVER_CONFIG} (the control connection).
+ */
+ public Builder reportConfig(boolean reportConfig) {
+ this.reportConfig = reportConfig;
+ return this;
+ }
+
public DriverChannelOptions build() {
- return new DriverChannelOptions(keyspace, eventTypes, eventCallback, ownerLogPrefix);
+ return new DriverChannelOptions(
+ keyspace, eventTypes, eventCallback, ownerLogPrefix, reportConfig);
}
}
}
diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java
index 85bffdb016e..46959cd9746 100644
--- a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java
+++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java
@@ -194,6 +194,11 @@ Message getRequest() {
if (featureStore != null) {
featureStore.populateStartupOptions(startupOptions);
}
+ // Adds SESSION_ID on every connection and DRIVER_CONFIG on the control connection
+ // (options.reportConfig); no-op when client config reporting is disabled.
+ context
+ .getDriverConfigReporter()
+ .populateStartupOptions(startupOptions, options.reportConfig);
return request = new Startup(startupOptions);
case GET_CLUSTER_NAME:
return request = CLUSTER_NAME_QUERY;
diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java
new file mode 100644
index 00000000000..9e66601123d
--- /dev/null
+++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java
@@ -0,0 +1,431 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.datastax.oss.driver.internal.core.context;
+
+import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
+import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
+import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy;
+import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy;
+import com.datastax.oss.driver.api.core.retry.RetryPolicy;
+import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy;
+import com.datastax.oss.driver.api.core.uuid.Uuids;
+import com.datastax.oss.driver.internal.core.connection.ConstantReconnectionPolicy;
+import com.datastax.oss.driver.internal.core.connection.ExponentialReconnectionPolicy;
+import com.datastax.oss.driver.internal.core.loadbalancing.DcInferringLoadBalancingPolicy;
+import com.datastax.oss.driver.internal.core.loadbalancing.DefaultLoadBalancingPolicy;
+import com.datastax.oss.driver.internal.core.retry.ConsistencyDowngradingRetryPolicy;
+import com.datastax.oss.driver.internal.core.retry.DefaultRetryPolicy;
+import com.datastax.oss.driver.internal.core.specex.ConstantSpeculativeExecutionPolicy;
+import com.datastax.oss.driver.internal.core.specex.NoSpeculativeExecutionPolicy;
+import com.datastax.oss.driver.internal.core.time.ServerSideTimestampGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.util.Map;
+import java.util.UUID;
+import net.jcip.annotations.ThreadSafe;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default {@link DriverConfigReporter}: serializes the driver configuration to the cross-driver
+ * {@code DRIVER_CONFIG} JSON shape and adds it to the control connection's {@code STARTUP} options.
+ *
+ * The blob is (re)built on demand every time the control connection initializes, so it always
+ * reflects the current (possibly reloaded) configuration without any caching.
+ *
+ *
Follows the schema's omission principle throughout: a key the Java driver has no equivalent
+ * for is left out of the JSON entirely rather than reported as {@code null}.
+ *
+ *
Known limitation: the report always describes {@link
+ * com.datastax.oss.driver.api.core.config.DriverExecutionProfile#DEFAULT_NAME the default execution
+ * profile}, not whichever profile a given request actually runs with. A session that relies on
+ * named execution profiles for some of its traffic will have that traffic's real settings
+ * (consistency level, timeouts, retry policy, ...) differ from what {@code DRIVER_CONFIG} reports.
+ * Reporting per-profile configuration would need a schema shape for multiple profiles, which the
+ * cross-driver schema doesn't define; this is a known gap, not an oversight.
+ *
+ *
Thread safety: this class is safe to use as shipped, but {@link #buildJson()} and
+ * {@link #populateConfig(ObjectNode, DriverExecutionProfile)} are {@code protected} purely to let
+ * tests override them (see {@code DefaultDriverConfigReporterTest}); a production subclass that
+ * overrides either to add caching or other mutable state is responsible for its own synchronization
+ * — {@code buildJson()} runs on every control-connection (re)initialization and may be called
+ * concurrently with a reconnect racing a fresh session start.
+ */
+@ThreadSafe
+public class DefaultDriverConfigReporter implements DriverConfigReporter {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultDriverConfigReporter.class);
+
+ /** STARTUP option key under which the config JSON is sent. */
+ public static final String DRIVER_CONFIG_KEY = "DRIVER_CONFIG";
+
+ /** STARTUP option key under which the per-session identifier is sent. */
+ public static final String SESSION_ID_KEY = "SESSION_ID";
+
+ /**
+ * Major schema version. Adding keys is backward-compatible and does not bump this; only
+ * changing/removing the meaning of an existing key does.
+ */
+ static final int SCHEMA_VERSION = 1;
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ protected final InternalDriverContext context;
+
+ // Dedicated, driver-generated identifier for this session. Not derived from the (user-settable,
+ // Insights-oriented) CLIENT_ID, so that it is guaranteed unique per session as the grouping key
+ // requires. The reporter is a per-session singleton (built once via LazyReference), so this value
+ // is stable and shared across all of the session's connections.
+ private final UUID sessionId = Uuids.random();
+
+ public DefaultDriverConfigReporter(InternalDriverContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public void populateStartupOptions(
+ Map startupOptions, boolean reportDriverConfig) {
+ // Configuration reporting is a best-effort diagnostic aid: it runs on the connection
+ // initialization path, so any failure here (a bad config read, a misbehaving policy while
+ // introspecting, a serialization error) must be swallowed rather than allowed to break the
+ // connection — which would prevent the session from establishing or reconnecting.
+ try {
+ if (!isEnabled()) {
+ return;
+ }
+ // SESSION_ID on every connection so the server can group a session's connections.
+ startupOptions.put(SESSION_ID_KEY, sessionId.toString());
+ // DRIVER_CONFIG blob only on the control connection.
+ if (reportDriverConfig) {
+ String json = buildJson();
+ if (json != null) {
+ startupOptions.put(DRIVER_CONFIG_KEY, json);
+ }
+ }
+ } catch (RuntimeException e) {
+ LOG.warn(
+ "Error while building the driver configuration report; skipping client config reporting",
+ e);
+ }
+ }
+
+ private boolean isEnabled() {
+ return context
+ .getConfig()
+ .getDefaultProfile()
+ .getBoolean(DefaultDriverOption.CLIENT_CONFIG_REPORTING_ENABLED, false);
+ }
+
+ /**
+ * Builds the compact, single-line JSON configuration report.
+ *
+ * Relies on the policy/generator {@code LazyReference}s (reconnection, retry, speculative
+ * execution, load balancing, timestamp generator, SSL engine factory) already being resolved by
+ * the time this runs, which holds today because session bootstrap eagerly forces them before
+ * {@code ProtocolInitHandler} sends the first {@code STARTUP}. That ordering isn't enforced by
+ * this class; a future change to session bootstrap that defers one of those references could make
+ * this the first caller to resolve it, from a Netty event-loop thread mid-{@code STARTUP} build.
+ */
+ protected String buildJson() {
+ ObjectNode root = OBJECT_MAPPER.createObjectNode();
+ root.put("version", SCHEMA_VERSION);
+ populateConfig(root, context.getConfig().getDefaultProfile());
+ try {
+ return OBJECT_MAPPER.writeValueAsString(root);
+ } catch (JsonProcessingException e) {
+ // An in-memory node tree should never fail to serialize; never let it break connection setup.
+ LOG.warn("Failed to serialize driver configuration report; skipping DRIVER_CONFIG", e);
+ return null;
+ }
+ }
+
+ /**
+ * Populates the configuration groups onto the report root, from the default execution profile
+ * plus the context's policies. Each group follows the cross-driver schema; a key the Java driver
+ * has no equivalent for is omitted rather than reported as {@code null}.
+ */
+ protected void populateConfig(ObjectNode root, DriverExecutionProfile config) {
+ root.set("connection", connection(config));
+ root.set("socket", socket(config));
+ root.set("control-plane", controlPlane(config));
+ root.set("reconnection-policy", reconnectionPolicy(config));
+ root.set("retry-policy", retryPolicy());
+ // No null variant in the schema for this group: omitted entirely when there is none.
+ ObjectNode specExec = speculativeExecutionPolicy(config);
+ if (specExec != null) {
+ root.set("speculative-execution-policy", specExec);
+ }
+ root.set("load-balancing-policy", loadBalancingPolicy(config));
+ root.set("node-location-preference", nodeLocationPreference(config));
+ root.set("connection-pool", connectionPool(config));
+ root.set("query-defaults", queryDefaults(config));
+ root.set("tls", tls(config));
+ }
+
+ private ObjectNode connection(DriverExecutionProfile config) {
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ ObjectNode connect = OBJECT_MAPPER.createObjectNode();
+ connect.put(
+ "timeout-ms",
+ config.getDuration(DefaultDriverOption.CONNECTION_CONNECT_TIMEOUT).toMillis());
+ n.set("connect", connect);
+ // The Java driver has no socket-level read/write timeouts, and connection.heartbeat is a
+ // reserved empty placeholder in this schema version (no slot for HEARTBEAT_INTERVAL/TIMEOUT
+ // yet) — all three are omitted entirely rather than reported as empty/null.
+ return n;
+ }
+
+ private ObjectNode socket(DriverExecutionProfile config) {
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ n.put("tcp-no-delay", config.getBoolean(DefaultDriverOption.SOCKET_TCP_NODELAY, true));
+ // keep-alive / reuse-address are not set by default; report the effective driver value (the OS
+ // default, which the driver leaves untouched, is approximated as false when unset).
+ n.put("keep-alive", config.getBoolean(DefaultDriverOption.SOCKET_KEEP_ALIVE, false));
+ n.put("reuse-address", config.getBoolean(DefaultDriverOption.SOCKET_REUSE_ADDRESS, false));
+ if (config.isDefined(DefaultDriverOption.SOCKET_LINGER_INTERVAL)) {
+ ObjectNode linger = OBJECT_MAPPER.createObjectNode();
+ linger.put("interval-s", config.getInt(DefaultDriverOption.SOCKET_LINGER_INTERVAL));
+ n.set("linger", linger);
+ }
+ if (config.isDefined(DefaultDriverOption.SOCKET_RECEIVE_BUFFER_SIZE)) {
+ ObjectNode receiveBuffer = OBJECT_MAPPER.createObjectNode();
+ receiveBuffer.put(
+ "size-bytes", config.getInt(DefaultDriverOption.SOCKET_RECEIVE_BUFFER_SIZE));
+ n.set("receive-buffer", receiveBuffer);
+ }
+ if (config.isDefined(DefaultDriverOption.SOCKET_SEND_BUFFER_SIZE)) {
+ ObjectNode sendBuffer = OBJECT_MAPPER.createObjectNode();
+ sendBuffer.put("size-bytes", config.getInt(DefaultDriverOption.SOCKET_SEND_BUFFER_SIZE));
+ n.set("send-buffer", sendBuffer);
+ }
+ return n;
+ }
+
+ private ObjectNode controlPlane(DriverExecutionProfile config) {
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+
+ ObjectNode timeout = OBJECT_MAPPER.createObjectNode();
+ timeout.put(
+ "client-side-ms",
+ config.getDuration(DefaultDriverOption.CONTROL_CONNECTION_TIMEOUT).toMillis());
+ // The Java metadata/control timeout is client-side; the driver does not add USING TIMEOUT, so
+ // the server-side field is omitted (not applicable).
+ ObjectNode systemQueries = OBJECT_MAPPER.createObjectNode();
+ systemQueries.set("timeout", timeout);
+ n.set("system-queries", systemQueries);
+
+ ObjectNode schemaAgreement = OBJECT_MAPPER.createObjectNode();
+ schemaAgreement.put(
+ "timeout-ms",
+ config.getDuration(DefaultDriverOption.CONTROL_CONNECTION_AGREEMENT_TIMEOUT).toMillis());
+ n.set("schema-agreement", schemaAgreement);
+
+ return n;
+ }
+
+ private ObjectNode reconnectionPolicy(DriverExecutionProfile config) {
+ ReconnectionPolicy policy = context.getReconnectionPolicy();
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ // Exact-class checks, not instanceof: none of these built-ins are final, so a user subclass
+ // (e.g. to tweak one method) must fall through to the "custom" branch below rather than be
+ // misreported as the unmodified built-in.
+ if (policy.getClass() == ExponentialReconnectionPolicy.class) {
+ n.put("type", "exponential");
+ n.put("base-ms", config.getDuration(DefaultDriverOption.RECONNECTION_BASE_DELAY).toMillis());
+ n.put("max-ms", config.getDuration(DefaultDriverOption.RECONNECTION_MAX_DELAY).toMillis());
+ // Java's built-in reconnection policies are unbounded: max-attempts is omitted.
+ } else if (policy.getClass() == ConstantReconnectionPolicy.class) {
+ n.put("type", "constant");
+ n.put("delay-ms", config.getDuration(DefaultDriverOption.RECONNECTION_BASE_DELAY).toMillis());
+ } else {
+ customPolicy(n, policy);
+ }
+ return n;
+ }
+
+ private ObjectNode retryPolicy() {
+ RetryPolicy policy = context.getRetryPolicy(DriverExecutionProfile.DEFAULT_NAME);
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ // Exact-class check, not instanceof: DefaultRetryPolicy/ConsistencyDowngradingRetryPolicy are
+ // not final, so a user subclass must fall through to "custom" rather than be misreported.
+ if (policy.getClass() == DefaultRetryPolicy.class) {
+ n.put("type", "standard-error-aware");
+ // No configurable backoff: omitted.
+ } else if (policy.getClass() == ConsistencyDowngradingRetryPolicy.class) {
+ n.put("type", "downgrading-consistency");
+ // No configurable backoff: omitted.
+ } else {
+ customPolicy(n, policy);
+ }
+ return n;
+ }
+
+ /**
+ * Returns {@code null} when there is no speculative execution policy to report, in which case the
+ * whole group is omitted from the report (the schema has no null variant for it).
+ */
+ private ObjectNode speculativeExecutionPolicy(DriverExecutionProfile config) {
+ SpeculativeExecutionPolicy policy =
+ context.getSpeculativeExecutionPolicy(DriverExecutionProfile.DEFAULT_NAME);
+ // Exact-class checks, not instanceof: neither built-in is final.
+ if (policy.getClass() == NoSpeculativeExecutionPolicy.class) {
+ return null;
+ }
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ if (policy.getClass() == ConstantSpeculativeExecutionPolicy.class) {
+ n.put("type", "constant");
+ n.put("max-executions", config.getInt(DefaultDriverOption.SPECULATIVE_EXECUTION_MAX));
+ n.put(
+ "delay-ms",
+ config.getDuration(DefaultDriverOption.SPECULATIVE_EXECUTION_DELAY).toMillis());
+ } else {
+ customPolicy(n, policy);
+ }
+ return n;
+ }
+
+ private ObjectNode loadBalancingPolicy(DriverExecutionProfile config) {
+ LoadBalancingPolicy policy =
+ context.getLoadBalancingPolicy(DriverExecutionProfile.DEFAULT_NAME);
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ // DcInferringLoadBalancingPolicy extends DefaultLoadBalancingPolicy, overriding only how the
+ // local DC is discovered; every flag below is unaffected, so the two share a branch and differ
+ // only in the reported "type". Exact-class checks (not instanceof) so an actual user subclass
+ // of either still falls through to "custom" below.
+ if (policy.getClass() == DefaultLoadBalancingPolicy.class
+ || policy.getClass() == DcInferringLoadBalancingPolicy.class) {
+ n.put(
+ "type",
+ policy.getClass() == DcInferringLoadBalancingPolicy.class ? "dc-inferring" : "default");
+ // The Java default policy is always token-aware and has no replica-shuffle option.
+ n.put("token-aware", true);
+ n.put("shuffle", false);
+ n.put(
+ "dc-failover",
+ config.getInt(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC, 0)
+ > 0);
+ n.put("latency-awareness", false);
+ } else {
+ customPolicy(n, policy);
+ }
+ return n;
+ }
+
+ /**
+ * Session-level datacenter/rack preference. Java has no session-level locality API separate from
+ * the load balancing policy, so this is sourced from the same places the (default) load balancing
+ * policy itself reads locality from: the local DC can be set either programmatically via {@link
+ * com.datastax.oss.driver.api.core.session.SessionBuilder#withLocalDatacenter} (which takes
+ * precedence, mirroring {@code OptionalLocalDcHelper}) or via config; the local rack has no
+ * programmatic override and is config-only.
+ */
+ private ObjectNode nodeLocationPreference(DriverExecutionProfile config) {
+ String localDc = context.getLocalDatacenter(DriverExecutionProfile.DEFAULT_NAME);
+ boolean dcExplicit = localDc != null;
+ if (!dcExplicit && config.isDefined(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER)) {
+ localDc = config.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER);
+ dcExplicit = true;
+ }
+ boolean rackExplicit = config.isDefined(DefaultDriverOption.LOAD_BALANCING_LOCAL_RACK);
+ String localRack =
+ rackExplicit ? config.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_RACK) : null;
+
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ if (dcExplicit && rackExplicit) {
+ n.put("type", "rack");
+ n.put("local-dc", localDc);
+ n.put("local-rack", localRack);
+ } else if (dcExplicit) {
+ n.put("type", "dc");
+ n.put("local-dc", localDc);
+ } else if (rackExplicit) {
+ // DC isn't explicit (DefaultLoadBalancingPolicy will infer one from the first contacted
+ // node), but rack was configured explicitly on its own: report rack-auto so the known rack
+ // isn't silently dropped. local-dc is omitted since it isn't known yet at report time.
+ n.put("type", "rack-auto");
+ n.put("local-rack", localRack);
+ } else {
+ // DC is inferred from the first contacted node; not known yet at control-connection-init
+ // report time, so local-dc is omitted.
+ n.put("type", "dc-auto");
+ }
+ return n;
+ }
+
+ private ObjectNode connectionPool(DriverExecutionProfile config) {
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ n.put("type", "host");
+ n.put(
+ "desired-connections-count", config.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE));
+ if (config.isDefined(DefaultDriverOption.CONNECTION_MAX_REQUESTS)) {
+ ObjectNode connection = OBJECT_MAPPER.createObjectNode();
+ connection.put("max-requests", config.getInt(DefaultDriverOption.CONNECTION_MAX_REQUESTS));
+ n.set("connection", connection);
+ }
+ ObjectNode shardAware = OBJECT_MAPPER.createObjectNode();
+ shardAware.put(
+ "enabled",
+ config.getBoolean(DefaultDriverOption.CONNECTION_ADVANCED_SHARD_AWARENESS_ENABLED, true));
+ n.set("shard-aware", shardAware);
+ return n;
+ }
+
+ private ObjectNode queryDefaults(DriverExecutionProfile config) {
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ int pageSize = config.getInt(DefaultDriverOption.REQUEST_PAGE_SIZE);
+ if (pageSize > 0) {
+ ObjectNode page = OBJECT_MAPPER.createObjectNode();
+ page.put("size", pageSize);
+ n.set("page", page);
+ }
+ // pageSize <= 0 means paging is unbounded: the "page" group is omitted entirely (the schema
+ // has no "unbounded" sentinel).
+ n.put("consistency", config.getString(DefaultDriverOption.REQUEST_CONSISTENCY));
+ if (config.isDefined(DefaultDriverOption.REQUEST_SERIAL_CONSISTENCY)) {
+ n.put("serial-consistency", config.getString(DefaultDriverOption.REQUEST_SERIAL_CONSISTENCY));
+ }
+ n.put("idempotence", config.getBoolean(DefaultDriverOption.REQUEST_DEFAULT_IDEMPOTENCE));
+ // Client-side timestamps are assigned unless the server-side generator is configured.
+ n.put(
+ "client-timestamps",
+ !(context.getTimestampGenerator() instanceof ServerSideTimestampGenerator));
+ ObjectNode request = OBJECT_MAPPER.createObjectNode();
+ request.put("timeout-ms", config.getDuration(DefaultDriverOption.REQUEST_TIMEOUT).toMillis());
+ n.set("request", request);
+ return n;
+ }
+
+ private ObjectNode tls(DriverExecutionProfile config) {
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ boolean enabled = context.getSslEngineFactory().isPresent();
+ n.put("enabled", enabled);
+ n.put(
+ "hostname-verification",
+ enabled && config.getBoolean(DefaultDriverOption.SSL_HOSTNAME_VALIDATION, true));
+ return n;
+ }
+
+ private void customPolicy(ObjectNode node, Object policy) {
+ node.put("type", "custom");
+ // getSimpleName() is empty for an anonymous class (a common way to supply a one-off policy);
+ // fall back to the full (binary) name so the policy is still identifiable.
+ String name = policy.getClass().getSimpleName();
+ node.put("name", name.isEmpty() ? policy.getClass().getName() : name);
+ }
+}
diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java
index 52d9cdb264c..3d1d5b82b87 100644
--- a/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java
+++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java
@@ -232,6 +232,8 @@ public class DefaultDriverContext implements InternalDriverContext {
new LazyReference<>("requestThrottler", this::buildRequestThrottler, cycleDetector);
private final LazyReference