diff --git a/driver-core/pom.xml b/driver-core/pom.xml
index 8af93c038de..998f0a05b5f 100644
--- a/driver-core/pom.xml
+++ b/driver-core/pom.xml
@@ -187,6 +187,11 @@
The report follows the approved v2 cross-driver schema: kebab-case keys, nested objects, and + * omission of any key or group that has no value (nothing is ever emitted as {@code null}). + * The same applies where a configured value falls outside what the schema can express but the key + * is optional: a disabled read timeout, a disabled {@code SO_LINGER} and an unbounded page + * size are omitted rather than emitted as a number the schema rejects. + * + *
Known limitation: that omission is not always available. A few schema fields are + * required and constrained to a positive integer, yet the 3.x option behind them accepts a + * value outside that range — and none of those setters validate their argument. {@link + * SocketOptions#setConnectTimeoutMillis(int)} and {@link SocketOptions#setReadTimeoutMillis(int)} + * both document a non-positive value as "no timeout", which lands in {@code + * connection.connect.timeout-ms} and {@code query-defaults.request.timeout-ms}; and {@code + * query-defaults.consistency} is an enum without the serial levels, which {@link + * QueryOptions#setConsistencyLevel(ConsistencyLevel)} nevertheless accepts. Such a value is + * reported as-is: the reporter deliberately neither fabricates an in-range value — which + * would misreport a setting an operator may have chosen on purpose — nor drops the whole report + * over one field, so the document is accurate but fails schema validation. Tracked as a + * cross-driver schema gap: the fix is to let those fields express the value, the way {@code + * control-plane.schema-agreement.timeout-ms} already admits 0. + * + *
One consequence of the two rules together: the read timeout feeds three fields, two of them + * optional, so disabling it omits {@code connection.read} and {@code + * control-plane.system-queries.timeout.client-side-ms} while {@code + * query-defaults.request.timeout-ms} still reports 0. */ public class DefaultDriverConfigReporter implements DriverConfigReporter { @@ -44,6 +92,38 @@ public class DefaultDriverConfigReporter implements DriverConfigReporter { */ static final int SCHEMA_VERSION = 1; + /** + * Upper bound on the UTF-8 size of the {@code DRIVER_CONFIG} value; a longer report is dropped + * rather than sent. + * + *
{@code STARTUP} options are serialized with {@code CBUtil.writeStringMap}, which writes each + * value with a 16-bit length prefix and no bounds check: a value longer than 65535 bytes would + * silently truncate that prefix modulo 65536 while still appending the whole body, corrupting the + * frame and failing the handshake. Note that nothing throws on that path, so it is not a failure + * the {@code try/catch} in {@link #buildReport()} could contain. + * + *
Most of this report is fixed-shape, but some of it is user-supplied and unbounded — + * datacenter and rack names, consistency levels, and the class names of custom policy objects — + * so enforcing a limit here keeps "reporting must never prevent a connection from being + * established" a property of this class rather than of the user's configuration. 32KiB is + * generous for a configuration report, and is the same limit the other ScyllaDB drivers apply. + */ + static final int MAX_DRIVER_CONFIG_LENGTH = 32 * 1024; + + /** + * Upper bound on the number of policies visited while walking a load balancing policy chain. + * + *
The walk follows {@code ChainableLoadBalancingPolicy.getChildPolicy()} on arbitrary + * user-supplied policy objects, so a policy that returns itself — or any cycle — would otherwise + * spin forever on the {@link Cluster} initialization path. That is the one failure mode the + * {@code try/catch} in {@link #buildReport()} cannot contain, because it hangs rather than + * throws. + * + *
The built-in chains are a handful of policies deep at most, so hitting this bound means a + * malformed chain rather than a legitimately deep one. + */ + private static final int MAX_POLICY_CHAIN_LENGTH = 16; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); protected final Configuration configuration; @@ -57,10 +137,28 @@ public String buildReport() { // Configuration reporting is a best-effort diagnostic aid, so any failure here (a bad config // read, a misbehaving policy while introspecting, a serialization error) must be swallowed // rather than allowed to propagate: it is built on the Cluster-initialization path, which must - // not fail because of a diagnostic. + // not fail because of a diagnostic. Also catches InternalError specifically: customPolicy() + // calls getClass().getSimpleName() on arbitrary user-supplied policy objects, which has a + // documented JDK edge case throwing InternalError for certain synthetic classes. Deliberately + // not a bare `Error` — that would also swallow OutOfMemoryError/StackOverflowError, masking a + // real JVM-level failure instead of this one narrow, documented case. try { - return buildJson(); - } catch (RuntimeException e) { + String json = buildJson(); + if (json == null) { + return null; + } + // Measured on the encoded bytes, since that is what the length prefix on the wire counts. + int length = json.getBytes(StandardCharsets.UTF_8).length; + if (length > MAX_DRIVER_CONFIG_LENGTH) { + LOGGER.warn( + "The driver configuration report is {} bytes long, which exceeds the {} byte limit; " + + "skipping DRIVER_CONFIG", + length, + MAX_DRIVER_CONFIG_LENGTH); + return null; + } + return json; + } catch (InternalError | RuntimeException e) { LOGGER.warn( "Error while building the driver configuration report; skipping driver config reporting", e); @@ -68,12 +166,7 @@ public String buildReport() { } } - /** - * Builds the compact, single-line JSON configuration report. - * - *
Stage 1 emits only the schema {@code version}; the individual configuration groups are
- * populated in {@link #populateConfig(ObjectNode)} in a later stage.
- */
+ /** Builds the compact, single-line JSON configuration report. */
protected String buildJson() {
ObjectNode root = OBJECT_MAPPER.createObjectNode();
root.put("version", SCHEMA_VERSION);
@@ -88,11 +181,402 @@ protected String buildJson() {
}
/**
- * Populates the configuration groups onto the report root. Placeholder in Stage 1; Stage 2 fills
- * in {@code connection}, {@code socket}, the policy groups, {@code query-defaults}, {@code tls},
- * etc. from {@link #configuration}.
+ * Populates the configuration groups onto the report root from {@link #configuration} and its
+ * policies, following the v2 schema. Keys the driver has no equivalent for (or cannot introspect
+ * in 3.x) are omitted rather than emitted as {@code null}.
*/
protected void populateConfig(ObjectNode root) {
- // Stage 2: populate configuration groups from `configuration`.
+ Policies policies = configuration.getPolicies();
+ // The load balancing policy chain feeds both load-balancing-policy and
+ // node-location-preference, so it is walked once here and handed to both.
+ LoadBalancingPolicy lbPolicy = policies.getLoadBalancingPolicy();
+ List The outermost is preferred over the innermost because it is what the user handed to the
+ * builder; a custom policy chaining to further custom policies is described by its outer one.
+ */
+ private static LoadBalancingPolicy configuredPolicy(
+ List Looked up here rather than in a static field so that a failure stays inside {@link
+ * #buildReport()}'s fail-safe handling instead of breaking class initialization on the {@link
+ * Connection.Factory} path.
+ */
+ private static int v3PoolDefault(String key) {
+ return PoolingOptions.DEFAULTS.get(ProtocolVersion.V3).get(key);
+ }
+
+ /** {@code value} unless it is unset ({@link PoolingOptions#UNSET} is negative) or nonsensical. */
+ private static int effective(int value, int defaultValue) {
+ return value <= 0 ? defaultValue : value;
}
}
diff --git a/driver-core/src/main/java/com/datastax/driver/core/DriverConfigReporter.java b/driver-core/src/main/java/com/datastax/driver/core/DriverConfigReporter.java
index 7ada1c34a2b..c50ba315484 100644
--- a/driver-core/src/main/java/com/datastax/driver/core/DriverConfigReporter.java
+++ b/driver-core/src/main/java/com/datastax/driver/core/DriverConfigReporter.java
@@ -39,7 +39,10 @@ public interface DriverConfigReporter {
*
* Implementations must not throw: a failure to build the report must be swallowed (and
* logged) rather than propagated, so that a diagnostic aid can never break cluster
- * initialization.
+ * initialization. For the same reason they must return {@code null} rather than a report that
+ * exceeds {@code DefaultDriverConfigReporter.MAX_DRIVER_CONFIG_LENGTH}: {@code STARTUP} option
+ * values carry an unchecked 16-bit length prefix, so an oversized one corrupts the frame instead
+ * of merely being useless.
*
* @return the report to send under the {@code DRIVER_CONFIG} startup option, or {@code null} to
* send nothing.
diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/DCAwareRoundRobinPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/DCAwareRoundRobinPolicy.java
index a1274f6b458..5882bf587e5 100644
--- a/driver-core/src/main/java/com/datastax/driver/core/policies/DCAwareRoundRobinPolicy.java
+++ b/driver-core/src/main/java/com/datastax/driver/core/policies/DCAwareRoundRobinPolicy.java
@@ -77,6 +77,7 @@ public static Builder builder() {
private final int usedHostsPerRemoteDc;
private final boolean dontHopForLocalCL;
+ private final boolean localDcExplicit;
private volatile Configuration configuration;
@@ -90,6 +91,40 @@ private DCAwareRoundRobinPolicy(
this.localDc = localDc == null ? UNSET : localDc;
this.usedHostsPerRemoteDc = usedHostsPerRemoteDc;
this.dontHopForLocalCL = !allowRemoteDCsForLocalConsistencyLevel;
+ this.localDcExplicit = !Strings.isNullOrEmpty(localDc);
+ }
+
+ /**
+ * The datacenter this policy considers local, or {@code null} if it has neither been configured
+ * explicitly nor inferred yet. When {@link #isLocalDcExplicit()} is {@code false}, this is the
+ * datacenter inferred from the first contacted node, which is only available once the policy has
+ * been initialized.
+ *
+ * @return the local datacenter name, or {@code null}.
+ */
+ public String getLocalDc() {
+ String dc = localDc;
+ return Strings.isNullOrEmpty(dc) ? null : dc;
+ }
+
+ /**
+ * Whether the local datacenter was configured explicitly (as opposed to being inferred from the
+ * first contacted node).
+ *
+ * @return {@code true} if the local datacenter was set explicitly.
+ */
+ public boolean isLocalDcExplicit() {
+ return localDcExplicit;
+ }
+
+ /**
+ * The number of hosts per remote datacenter that this policy considers for failover (0 means no
+ * remote failover).
+ *
+ * @return the number of used hosts per remote datacenter.
+ */
+ public int getUsedHostsPerRemoteDc() {
+ return usedHostsPerRemoteDc;
}
@Override
diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/PagingOptimizingLoadBalancingPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/PagingOptimizingLoadBalancingPolicy.java
index edfe690900e..2096e3ad914 100644
--- a/driver-core/src/main/java/com/datastax/driver/core/policies/PagingOptimizingLoadBalancingPolicy.java
+++ b/driver-core/src/main/java/com/datastax/driver/core/policies/PagingOptimizingLoadBalancingPolicy.java
@@ -12,7 +12,7 @@
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
-public class PagingOptimizingLoadBalancingPolicy implements LoadBalancingPolicy {
+public class PagingOptimizingLoadBalancingPolicy implements ChainableLoadBalancingPolicy {
private final LoadBalancingPolicy wrapped;
private volatile CopyOnWriteArrayList The cluster under test uses the default configuration — no {@code withDriverConfigReporting}
* call — so these also assert that reporting is enabled by default.
*
- * Stage 1 emits only the schema version, so {@code DRIVER_CONFIG} is asserted to be {@code
- * {"version":1}}. ScyllaDB-only: {@code system.clients.client_options} is a Scylla feature (added
- * in ScyllaDB 2026.1).
+ * ScyllaDB-only: {@code system.clients.client_options} is a Scylla feature (added in ScyllaDB
+ * 2026.1).
*/
@ScyllaVersion(
minOSS = "2026.1",
@@ -77,10 +80,23 @@ public void should_store_session_id_on_all_connections_and_driver_config_on_cont
}
}
- // DRIVER_CONFIG is stored for exactly one connection (the control connection). Stage 1 reports
- // only the schema version.
+ // DRIVER_CONFIG is stored for exactly one connection (the control connection), as the versioned
+ // JSON report the driver builds.
assertThat(driverConfigs).hasSize(1);
- assertThat(driverConfigs.get(0)).isEqualTo("{\"version\":1}");
+ JsonNode report = parse(driverConfigs.get(0));
+ assertThat(report.path("version").asInt()).isEqualTo(1);
+ // Spot-check a few required v2 groups made it through intact (no truncation).
+ assertThat(report.has("connection")).isTrue();
+ assertThat(report.has("load-balancing-policy")).isTrue();
+ assertThat(report.path("connection-pool").path("shard-aware").has("enabled")).isTrue();
+ }
+
+ private JsonNode parse(String json) {
+ try {
+ return new ObjectMapper().readTree(json);
+ } catch (IOException e) {
+ throw new AssertionError("DRIVER_CONFIG is not valid JSON: " + json, e);
+ }
}
@Test(groups = "short")
diff --git a/driver-core/src/test/resources/config/driver-config-report-v1.schema.json b/driver-core/src/test/resources/config/driver-config-report-v1.schema.json
new file mode 100644
index 00000000000..b2b5886bb7c
--- /dev/null
+++ b/driver-core/src/test/resources/config/driver-config-report-v1.schema.json
@@ -0,0 +1,846 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://scylladb.com/schemas/driver-client-options/v1.json",
+ "title": "ScyllaDB driver DRIVER_CONFIG configuration",
+ "description": "Schema for the JSON value sent under the STARTUP option key DRIVER_CONFIG, describing the effective client configuration. The top-level object must include `version` and the required configuration groups listed by this schema. Unknown top-level keys are rejected. Built-in groups reject unknown keys and require the keys listed in each group; custom policy objects may include additional implementation-specific public attributes where explicitly allowed.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "version",
+ "connection",
+ "socket",
+ "control-plane",
+ "reconnection-policy",
+ "retry-policy",
+ "load-balancing-policy",
+ "connection-pool",
+ "query-defaults",
+ "tls"
+ ],
+ "properties": {
+ "version": {
+ "description": "Major schema version. Adding keys is backward-compatible and does not bump this; only changing/removing the meaning of an existing key does.",
+ "type": "integer",
+ "const": 1
+ },
+ "connection": {
+ "$ref": "#/$defs/connection"
+ },
+ "socket": {
+ "$ref": "#/$defs/socket"
+ },
+ "control-plane": {
+ "$ref": "#/$defs/control-plane"
+ },
+ "reconnection-policy": {
+ "$ref": "#/$defs/reconnection-policy"
+ },
+ "retry-policy": {
+ "$ref": "#/$defs/retry-policy"
+ },
+ "speculative-execution-policy": {
+ "$ref": "#/$defs/speculative-execution-policy"
+ },
+ "load-balancing-policy": {
+ "$ref": "#/$defs/load-balancing-policy"
+ },
+ "node-location-preference": {
+ "$ref": "#/$defs/node-location-preference"
+ },
+ "connection-pool": {
+ "$ref": "#/$defs/connection-pool"
+ },
+ "query-defaults": {
+ "$ref": "#/$defs/query-defaults"
+ },
+ "tls": {
+ "$ref": "#/$defs/tls"
+ }
+ },
+ "$defs": {
+ "positiveInteger": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "nonNegativeInteger": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "retryPolicyBackoff": {
+ "description": "Delay inserted between retry attempts of a retry policy. Discriminated union: when present, `type` selects the backoff algorithm and each algorithm carries only its own parameters. Absent when there is no delay between attempts.",
+ "oneOf": [
+ {
+ "type": "object",
+ "description": "Exponential backoff: the delay starts at base-ms and doubles after each attempt (capped at max-ms), with a small random jitter to de-synchronize concurrent retries.",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "base-ms"
+ ],
+ "properties": {
+ "type": {
+ "const": "exponential",
+ "description": "Exponential backoff algorithm."
+ },
+ "base-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Initial delay between retries in milliseconds; the starting delay that doubles each attempt."
+ },
+ "max-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Maximum delay between retries in milliseconds; the exponentially growing delay is capped here. Absent when no maximum delay is configured."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "Constant backoff: a fixed delay is inserted between every retry attempt.",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "delay-ms"
+ ],
+ "properties": {
+ "type": {
+ "const": "constant",
+ "description": "Constant (fixed-delay) backoff algorithm."
+ },
+ "delay-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Fixed delay between retries in milliseconds."
+ }
+ }
+ }
+ ]
+ },
+ "connection": {
+ "description": "Connection-level settings: socket read/write/connect timeouts plus the CQL-level idle heartbeat. Durations are in milliseconds. Optional duration fields are absent when unset or not applicable.",
+ "type": "object",
+ "required": [
+ "connect"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "connect": {
+ "type": "object",
+ "description": "Settings for establishing a TCP/CQL connection to a node.",
+ "required": [
+ "timeout-ms"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "timeout-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Timeout for establishing a TCP/CQL connection to a node."
+ }
+ }
+ },
+ "read": {
+ "type": "object",
+ "description": "Settings for reading from a connection.",
+ "additionalProperties": false,
+ "properties": {
+ "timeout-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Read operation timeout."
+ }
+ }
+ },
+ "write": {
+ "type": "object",
+ "description": "Settings for writing to a connection. Direction-specific options such as write coalescing are expected to be added here in a future schema version.",
+ "additionalProperties": false,
+ "properties": {
+ "coalescing": {
+ "type": "object",
+ "description": "Settings for write coalescing. It is a placeholder for v2",
+ "additionalProperties": false,
+ "properties": {}
+ },
+ "timeout-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Write operation timeout."
+ }
+ }
+ },
+ "heartbeat": {
+ "type": "object",
+ "description": "Reserved for CQL-level idle heartbeat settings. Optional and intentionally empty in this schema version. It is a placeholder for v2",
+ "additionalProperties": false,
+ "properties": {}
+ }
+ }
+ },
+ "control-plane": {
+ "description": "Control-plane timeout settings for internal/system queries run over the control connection and for schema agreement. Each value is in milliseconds. Optional values are absent when unset or not applicable.",
+ "type": "object",
+ "required": [
+ "system-queries",
+ "schema-agreement"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "system-queries": {
+ "type": "object",
+ "description": "Settings for internal/system queries run over the control connection.",
+ "additionalProperties": false,
+ "required": [
+ "timeout"
+ ],
+ "properties": {
+ "timeout": {
+ "type": "object",
+ "description": "Timeouts applied to internal/system queries. Each value is in milliseconds. Optional values are absent when unset or not applicable.",
+ "additionalProperties": false,
+ "properties": {
+ "client-side-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "A client-side timeout for internal queries."
+ },
+ "server-side-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "A server-side timeout for internal queries."
+ }
+ }
+ }
+ }
+ },
+ "schema-agreement": {
+ "type": "object",
+ "description": "Settings for schema agreement across nodes.",
+ "additionalProperties": false,
+ "required": [
+ "timeout-ms"
+ ],
+ "properties": {
+ "timeout-ms": {
+ "$ref": "#/$defs/nonNegativeInteger",
+ "description": "Maximum time to wait for schema agreement across nodes. Always a concrete value; 0 means do not wait for agreement."
+ }
+ }
+ }
+ }
+ },
+ "socket": {
+ "description": "Low-level TCP socket options applied to client connections. Boolean options (tcp-no-delay, keep-alive, reuse-address) report the effective on/off state: when no explicit value is configured, the OS/platform default is reported. Buffer sizes are in bytes and linger is in seconds; these fields are absent when unset (kernel auto-tuned buffer / linger disabled).",
+ "type": "object",
+ "required": [
+ "tcp-no-delay",
+ "keep-alive",
+ "reuse-address"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "tcp-no-delay": {
+ "type": "boolean",
+ "description": "TCP_NODELAY: disable Nagle's algorithm. Reports the effective value; when no explicit value is configured, the OS/platform default is reported."
+ },
+ "keep-alive": {
+ "type": "boolean",
+ "description": "SO_KEEPALIVE: OS-level TCP keep-alive probes on idle connections. Reports the effective on/off state; when no explicit value is configured, the OS/platform default is reported."
+ },
+ "reuse-address": {
+ "type": "boolean",
+ "description": "SO_REUSEADDR: allow reuse of a local address. Reports the effective on/off state; when no explicit value is configured, the OS/platform default is reported."
+ },
+ "linger": {
+ "type": "object",
+ "required": [
+ "interval-s"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "interval-s": {
+ "$ref": "#/$defs/nonNegativeInteger",
+ "description": "SO_LINGER lingering-close interval in seconds."
+ }
+ }
+ },
+ "receive-buffer": {
+ "type": "object",
+ "required": [
+ "size-bytes"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "size-bytes": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "SO_RCVBUF socket receive buffer size hint in bytes."
+ }
+ }
+ },
+ "send-buffer": {
+ "type": "object",
+ "required": [
+ "size-bytes"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "size-bytes": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "SO_SNDBUF socket send buffer size hint in bytes."
+ }
+ }
+ }
+ }
+ },
+ "reconnection-policy": {
+ "description": "Defines how connection attempts to a node are retried after a connection failure.",
+ "oneOf": [
+ {
+ "type": "object",
+ "description": "Exponential backoff reconnection policy.",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "base-ms",
+ "max-ms"
+ ],
+ "properties": {
+ "type": {
+ "const": "exponential",
+ "description": "Reconnection policy type."
+ },
+ "base-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Initial delay before the first reconnection attempt in milliseconds. Always a concrete value when this policy is reported."
+ },
+ "max-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Maximum delay between reconnection attempts (in milliseconds). Always a concrete value when this policy is reported."
+ },
+ "max-attempts": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Maximum number of reconnection attempts before giving up. Absent when attempts are unlimited."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "Constant delay reconnection policy.",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "delay-ms"
+ ],
+ "properties": {
+ "type": {
+ "const": "constant",
+ "description": "Reconnection policy type."
+ },
+ "delay-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Fixed delay between reconnection attempts (in milliseconds). Always a concrete value when this policy is reported."
+ },
+ "max-attempts": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Maximum number of reconnection attempts before giving up. Absent when attempts are unlimited."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "A user-supplied reconnection policy that is not one of the built-ins. Identified by `name` only. Implementations that can introspect a policy instance MAY also serialize its public attributes as additional properties on this object.",
+ "additionalProperties": true,
+ "required": [
+ "type",
+ "name"
+ ],
+ "properties": {
+ "type": {
+ "const": "custom",
+ "description": "Reconnection policy type: a user-supplied policy."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the custom policy (e.g. the public type name of the user-provided policy)."
+ }
+ }
+ },
+ {
+ "type": "null",
+ "description": "No reconnection attempts will be made."
+ }
+ ]
+ },
+ "retry-policy": {
+ "description": "Controls whether and how a failed query is retried. Discriminated on `type`; each policy only permits its own parameters.",
+ "oneOf": [
+ {
+ "type": "object",
+ "description": "Error-type-aware retry policy with fixed, non-configurable rules: retry at most once on Unavailable (on the next node), retry on a read timeout only when enough replicas responded but the data was not retrieved, retry on a write timeout only for batch-log writes, retry the next node on Overloaded/ServerError/Bootstrapping/broken-connection when the request is idempotent, and never retry serial (LWT) reads. It can have backoff enabled.",
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "standard-error-aware",
+ "description": "Retry policy type."
+ },
+ "backoff": {
+ "$ref": "#/$defs/retryPolicyBackoff",
+ "description": "Delay inserted between retries."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "Simple retry policy with a fixed number of retries.",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "max-retries"
+ ],
+ "properties": {
+ "type": {
+ "const": "simple",
+ "description": "Retry policy type."
+ },
+ "backoff": {
+ "$ref": "#/$defs/retryPolicyBackoff",
+ "description": "Delay inserted between retries."
+ },
+ "max-retries": {
+ "$ref": "#/$defs/nonNegativeInteger",
+ "description": "Maximum number of retries before giving up. Always a concrete value when this policy is reported; 0 means no retries."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "Fall-through retry policy: never retries anything and always rethrows the original error to the caller. Every error type — read timeout, write timeout, unavailable, and unexpected request errors (connection errors, Overloaded, ServerError, Bootstrapping) — is propagated unchanged. This is a true no-op and is stricter than the 'never' policy, which still retries the next host on connection/server errors.",
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "fallthrough",
+ "description": "Retry policy type."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "Downgrading-consistency retry policy: retries at a lower consistency level on failure.",
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "downgrading-consistency",
+ "description": "Retry policy type."
+ },
+ "backoff": {
+ "$ref": "#/$defs/retryPolicyBackoff",
+ "description": "Delay inserted between retries."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "A user-supplied retry policy that is not one of the built-ins. Identified by `name` only. Implementations that can introspect a policy instance MAY also serialize its public attributes as additional properties on this object.",
+ "additionalProperties": true,
+ "required": [
+ "type",
+ "name"
+ ],
+ "properties": {
+ "type": {
+ "const": "custom",
+ "description": "Retry policy type: a user-supplied policy."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the custom policy (e.g. the public type name of the user-provided policy)."
+ },
+ "description": {
+ "type": "string",
+ "description": "Textual description of what this policy does."
+ }
+ }
+ }
+ ]
+ },
+ "speculative-execution-policy": {
+ "description": "Controls pre-emptive duplicate requests to other replicas. Discriminated on `type`; each policy only permits its own parameters.",
+ "oneOf": [
+ {
+ "type": "object",
+ "description": "Constant-delay speculative execution: launch extra executions after a fixed delay.",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "max-executions",
+ "delay-ms"
+ ],
+ "properties": {
+ "type": {
+ "const": "constant",
+ "description": "Speculative execution policy type."
+ },
+ "max-executions": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Maximum number of speculative executions per request."
+ },
+ "delay-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Delay before launching each additional execution (in milliseconds)."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "Percentile-based speculative execution: launch extra executions once latency exceeds a percentile threshold.",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "max-executions",
+ "percentile"
+ ],
+ "properties": {
+ "type": {
+ "const": "percentile",
+ "description": "Speculative execution policy type."
+ },
+ "max-executions": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Maximum number of speculative executions per request."
+ },
+ "percentile": {
+ "type": "number",
+ "exclusiveMinimum": 0,
+ "exclusiveMaximum": 100,
+ "description": "Latency percentile (0–100, exclusive; e.g. 99.0) that triggers an additional execution."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "A user-supplied speculative execution policy that is not one of the built-ins. Identified by `name` only. Implementations that can introspect a policy instance MAY also serialize its public attributes as additional properties on this object.",
+ "additionalProperties": true,
+ "required": [
+ "type",
+ "name"
+ ],
+ "properties": {
+ "type": {
+ "const": "custom",
+ "description": "Speculative execution policy type: a user-supplied policy."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the custom policy (e.g. the public type name of the user-provided policy)."
+ },
+ "description": {
+ "type": "string",
+ "description": "Textual description of what this policy does."
+ }
+ }
+ }
+ ]
+ },
+ "load-balancing-policy": {
+ "description": "Load balancing / host selection policy. Discriminated on `type`: a built-in policy reports the normalized flags below, while a user-supplied policy is reported as type 'custom' with a `name` and, optionally, serialized public attributes.",
+ "oneOf": [
+ {
+ "type": "object",
+ "description": "A built-in load balancing policy, reported with normalized location/awareness flags.",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "shuffle",
+ "token-aware",
+ "dc-failover",
+ "latency-awareness"
+ ],
+ "properties": {
+ "type": {
+ "enum": [
+ "token-aware",
+ "round-robin",
+ "dc-aware",
+ "rack-aware",
+ "dc-inferring",
+ "basic",
+ "white-list",
+ "host-filter",
+ "default"
+ ],
+ "description": "Policy type one of e.g. token-aware, round-robin, dc-aware, rack-aware."
+ },
+ "token-aware": {
+ "type": "boolean",
+ "description": "Whether queries are routed to token replicas."
+ },
+ "shuffle": {
+ "type": "boolean",
+ "description": "Whether candidate order in a query plan is shuffled for regular queries. LWT and strongly consistent queries are not shuffled."
+ },
+ "dc-failover": {
+ "type": "boolean",
+ "description": "Whether requests may fail over to remote datacenters."
+ },
+ "latency-awareness": {
+ "type": "boolean",
+ "description": "Whether latency-aware host ordering is enabled."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "A user-supplied load balancing policy that is not one of the built-ins. Identified by `name` only. Implementations that can introspect a policy instance MAY also serialize its public attributes as additional properties on this object.",
+ "additionalProperties": true,
+ "required": [
+ "type",
+ "name"
+ ],
+ "properties": {
+ "type": {
+ "const": "custom",
+ "description": "Load balancing policy type: a user-supplied policy."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the custom policy (e.g. the public type name of the user-provided policy)."
+ },
+ "description": {
+ "type": "string",
+ "description": "Textual description of what this policy does."
+ }
+ }
+ }
+ ]
+ },
+ "node-location-preference": {
+ "description": "Session-level datacenter/rack preference, set independently of the load balancing policy. Some implementations let users set a preferred DC/rack directly on the session configuration; the load balancing policy and other components read this preference unless a policy overrides it. May be sourced from different places; if DC/rack preferences are specified in the load balancing policy, they should be reported here.",
+ "oneOf": [
+ {
+ "type": "object",
+ "description": "Explicitly configured datacenter preference.",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "local-dc"
+ ],
+ "properties": {
+ "type": {
+ "const": "dc",
+ "description": "Session-level location preference: explicit datacenter."
+ },
+ "local-dc": {
+ "type": "string",
+ "description": "Explicitly configured preferred datacenter."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "Explicitly configured datacenter and rack preference.",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "local-dc",
+ "local-rack"
+ ],
+ "properties": {
+ "type": {
+ "const": "rack",
+ "description": "Session-level location preference: explicit datacenter and rack."
+ },
+ "local-dc": {
+ "type": "string",
+ "description": "Explicitly configured preferred datacenter."
+ },
+ "local-rack": {
+ "type": "string",
+ "description": "Explicitly configured preferred rack."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "Datacenter preference inferred from the first node the client connects to.",
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "dc-auto",
+ "description": "Session-level location preference: inferred datacenter."
+ },
+ "local-dc": {
+ "type": "string",
+ "description": "Inferred preferred datacenter. Absent when not yet known at report time."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "Datacenter and rack preference inferred from the first node the client connects to.",
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "rack-auto",
+ "description": "Session-level location preference: inferred datacenter and rack."
+ },
+ "local-dc": {
+ "type": "string",
+ "description": "Inferred preferred datacenter. Absent when not yet known at report time."
+ },
+ "local-rack": {
+ "type": "string",
+ "description": "Inferred preferred rack. Absent when not yet known at report time."
+ }
+ }
+ }
+ ]
+ },
+ "connection-pool": {
+ "description": "Connection pooling configuration.",
+ "type": "object",
+ "required": [
+ "type",
+ "desired-connections-count",
+ "shard-aware"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "enum": [
+ "host",
+ "shard"
+ ],
+ "description": "What each pool is keyed by: per host or per shard."
+ },
+ "desired-connections-count": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Number of connections to open per host or per shard."
+ },
+ "connection": {
+ "type": "object",
+ "description": "Per-connection pool settings.",
+ "required": [
+ "max-requests"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "max-requests": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Maximum number of in-flight requests per connection."
+ }
+ }
+ },
+ "shard-aware": {
+ "type": "object",
+ "required": [
+ "enabled"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Whether the client is configured to use ScyllaDB's dedicated shard-aware port (default 19042, TLS 19043) to reach a chosen shard in a single connect, versus the fallback of opening connections on the normal port and reading the server-assigned shard. Reports configuration intent; at runtime the port must also be advertised by the server and reachable, otherwise the client falls back transparently."
+ }
+ }
+ }
+ }
+ },
+ "query-defaults": {
+ "description": "Default per-request settings applied to statements that do not override them.",
+ "type": "object",
+ "required": [
+ "consistency",
+ "idempotence",
+ "client-timestamps",
+ "request"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "page": {
+ "type": "object",
+ "required": [
+ "size"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "size": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Default page (fetch) size for result sets. Absent when page is not limited."
+ }
+ }
+ },
+ "consistency": {
+ "description": "Default consistency level applied to requests that do not override it. Always present when this group is reported.",
+ "type": "string",
+ "enum": [
+ "ANY",
+ "ONE",
+ "TWO",
+ "THREE",
+ "QUORUM",
+ "ALL",
+ "LOCAL_QUORUM",
+ "EACH_QUORUM",
+ "LOCAL_ONE"
+ ]
+ },
+ "serial-consistency": {
+ "description": "Default serial consistency for LWT/conditional statements. Absent when unset; the server default applies.",
+ "type": "string",
+ "enum": [
+ "SERIAL",
+ "LOCAL_SERIAL"
+ ]
+ },
+ "idempotence": {
+ "description": "Default idempotence flag applied to statements that do not set their own.",
+ "type": "boolean"
+ },
+ "client-timestamps": {
+ "description": "True when the client assigns the write timestamp client-side (protocol-level/USING TIMESTAMP) instead of letting the coordinator assign it.",
+ "type": "boolean"
+ },
+ "request": {
+ "type": "object",
+ "description": "Default request-level settings.",
+ "required": [
+ "timeout-ms"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "timeout-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Client-side timeout for a single request/query in milliseconds. Always a concrete value when query defaults are reported."
+ }
+ }
+ }
+ }
+ },
+ "tls": {
+ "description": "TLS/SSL transport settings. Reports only booleans; never credentials, keys, or host lists.",
+ "type": "object",
+ "required": [
+ "enabled",
+ "hostname-verification"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Whether TLS is enabled for connections."
+ },
+ "hostname-verification": {
+ "type": "boolean",
+ "description": "Whether the server hostname is verified against its certificate."
+ }
+ }
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index d6c97121022..fbc9ab2909b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -88,6 +88,7 @@