apply(Void input) throws Exception {
// server can group all the connections opened from this Cluster.
extraOptions.put(SESSION_ID_KEY, factory.sessionId.toString());
- // Built once when the Cluster initialized; non-null only on the control connection, and
- // only when driver config reporting is enabled.
- if (driverConfig != null) {
- extraOptions.put(DefaultDriverConfigReporter.DRIVER_CONFIG_KEY, driverConfig);
+ // Built here, on the control connection only, and rebuilt for every one of them: the report
+ // must describe the objects in force at this handshake, not the ones that existed when the
+ // Cluster was constructed. A load balancing policy is initialized after the first control
+ // connection's STARTUP, so a datacenter or rack it infers can only reach the server on a
+ // later one.
+ if (reportConfig) {
+ String driverConfig = factory.buildDriverConfigReport();
+ if (driverConfig != null) {
+ extraOptions.put(DefaultDriverConfigReporter.DRIVER_CONFIG_KEY, driverConfig);
+ }
}
if (protocolFeatureStore != null) {
@@ -1318,21 +1324,19 @@ static class Factory {
// affiliation, so Cluster-wide is the finest granularity available here).
final UUID sessionId = UUID.randomUUID();
- // The DRIVER_CONFIG blob sent on the control connection, or null when driver config reporting
- // is disabled (or the report could not be built). Built once here, as the Cluster initializes,
- // and reused for every control connection this factory opens: it is never rebuilt while the
- // session is in flight.
- final String driverConfig;
+ // Whether this factory reports DRIVER_CONFIG at all: false when reporting is disabled, or when
+ // the reporter cannot be loaded on this classpath (see canBuildDriverConfigReport). The report
+ // itself is not cached — it is rebuilt for every control connection.
+ final boolean driverConfigReportable;
Factory(Cluster.Manager manager, Configuration configuration) {
this.defaultHandler = manager;
this.manager = manager;
this.reaper = manager.reaper;
this.configuration = configuration;
- this.driverConfig =
+ this.driverConfigReportable =
configuration.isDriverConfigReportingEnabled()
- ? new DefaultDriverConfigReporter(configuration).buildReport()
- : null;
+ && canBuildDriverConfigReport(configuration);
this.authProvider = configuration.getProtocolOptions().getAuthProvider();
this.protocolVersion = configuration.getProtocolOptions().initialProtocolVersion;
this.nettyOptions = configuration.getNettyOptions();
@@ -1351,6 +1355,63 @@ static class Factory {
.createThreadFactory(manager.clusterName, "timeouter"));
}
+ /**
+ * The {@code DRIVER_CONFIG} report to send in this handshake's {@code STARTUP} options, or
+ * {@code null} when there is none to send.
+ *
+ * Called once per control connection, from the {@code STARTUP} frame assembly, rather than
+ * cached: the report describes the objects that are in force, and one of them changes after the
+ * first handshake. {@code Cluster.Manager.init()} builds this factory before it initializes the
+ * load balancing policy, so a datacenter or rack the policy infers from the node it reaches is
+ * unknown while the first control connection is being opened and known on every later one.
+ * Rebuilding costs a few hundred microseconds on a connection that opens once per Cluster and
+ * then only on reconnect.
+ */
+ String buildDriverConfigReport() {
+ return driverConfigReportable
+ ? new DefaultDriverConfigReporter(configuration).buildReport()
+ : null;
+ }
+
+ /**
+ * Whether {@link DefaultDriverConfigReporter} can be loaded and run at all on this classpath.
+ * Also serves to load it here, on the {@link Cluster} initialization thread, rather than
+ * leaving a Netty event loop to be the first to touch Jackson; the report it builds is
+ * deliberately discarded, since every control connection builds its own.
+ *
+ *
Guarded because {@link DefaultDriverConfigReporter} serializes the report with Jackson,
+ * and holds an {@code ObjectMapper} in a static field: without Jackson, merely
+ * initializing that class raises {@link NoClassDefFoundError}. That is an {@code
+ * Error} raised while initializing the class rather than from any method it declares, so the
+ * reporter's own fail-safe cannot contain it and neither can anything it calls — and this runs
+ * on the {@link Cluster} initialization path, so it would take a classpath that merely lacks an
+ * optional serializer from "the report is skipped" to "no connection can be established at
+ * all". Choosing whether to touch the class is therefore the only place the check can live.
+ *
+ *
{@link LinkageError} rather than {@code NoClassDefFoundError} alone, so that a partially
+ * present or version-mismatched Jackson — which surfaces as {@code ExceptionInInitializerError}
+ * out of the static initializer — is covered too; probing for one class name would pass and
+ * then still fail here. This is the same fallback {@code SnappyCompressor} applies for its own
+ * optional library, and it does not contradict {@link
+ * DefaultDriverConfigReporter#buildReport()} deliberately not catching bare {@code Error}: that
+ * is about report building never masking a real JVM-level failure, while this is a call site
+ * tolerating a missing optional dependency.
+ */
+ static boolean canBuildDriverConfigReport(Configuration configuration) {
+ try {
+ new DefaultDriverConfigReporter(configuration).buildReport();
+ return true;
+ } catch (LinkageError e) {
+ // Logged unconditionally, and at WARN: reporting ships enabled, so nobody opted into it and
+ // nobody would think to look for a message saying it is off.
+ logger.warn(
+ "Cannot build the driver configuration report, so no DRIVER_CONFIG will be reported; "
+ + "this is expected if Jackson was excluded from the classpath ({})",
+ e.toString());
+ return false;
+ }
+ }
+
int getPort() {
return configuration.getProtocolOptions().getPort();
}
@@ -1368,9 +1429,9 @@ Connection open(Host host)
}
/**
- * Same as {@link #open(Host)}, but when {@code reportConfig} is true, hands the connection the
- * {@code DRIVER_CONFIG} blob to report, marking it as the control connection (a no-op when
- * driver config reporting is disabled, since there is then no blob to report).
+ * Same as {@link #open(Host)}, but when {@code reportConfig} is true, marks the connection as
+ * the control connection, which builds and reports a {@code DRIVER_CONFIG} blob of its own in
+ * its {@code STARTUP} options (a no-op when driver config reporting is disabled).
*/
Connection open(Host host, boolean reportConfig)
throws ConnectionException, InterruptedException, UnsupportedProtocolVersionException,
@@ -1381,8 +1442,7 @@ Connection open(Host host, boolean reportConfig)
host.convictionPolicy.signalConnectionsOpening(1);
Connection connection =
- new Connection(
- buildConnectionName(host), endPoint, this, null, reportConfig ? driverConfig : null);
+ new Connection(buildConnectionName(host), endPoint, this, null, reportConfig);
// This method opens the connection synchronously, so wait until it's initialized
try {
connection.initAsync().get();
diff --git a/driver-core/src/main/java/com/datastax/driver/core/DefaultDriverConfigReporter.java b/driver-core/src/main/java/com/datastax/driver/core/DefaultDriverConfigReporter.java
index 30c01848608..a985bd3db6e 100644
--- a/driver-core/src/main/java/com/datastax/driver/core/DefaultDriverConfigReporter.java
+++ b/driver-core/src/main/java/com/datastax/driver/core/DefaultDriverConfigReporter.java
@@ -15,9 +15,39 @@
*/
package com.datastax.driver.core;
+import com.datastax.driver.core.policies.ChainableLoadBalancingPolicy;
+import com.datastax.driver.core.policies.ConstantReconnectionPolicy;
+import com.datastax.driver.core.policies.ConstantSpeculativeExecutionPolicy;
+import com.datastax.driver.core.policies.DCAwareRoundRobinPolicy;
+import com.datastax.driver.core.policies.DefaultRetryPolicy;
+import com.datastax.driver.core.policies.DowngradingConsistencyRetryPolicy;
+import com.datastax.driver.core.policies.ErrorAwarePolicy;
+import com.datastax.driver.core.policies.ExponentialReconnectionPolicy;
+import com.datastax.driver.core.policies.FallthroughRetryPolicy;
+import com.datastax.driver.core.policies.HostFilterPolicy;
+import com.datastax.driver.core.policies.LatencyAwarePolicy;
+import com.datastax.driver.core.policies.LoadBalancingPolicy;
+import com.datastax.driver.core.policies.NoSpeculativeExecutionPolicy;
+import com.datastax.driver.core.policies.PagingOptimizingLoadBalancingPolicy;
+import com.datastax.driver.core.policies.PercentileSpeculativeExecutionPolicy;
+import com.datastax.driver.core.policies.Policies;
+import com.datastax.driver.core.policies.RackAwareRoundRobinPolicy;
+import com.datastax.driver.core.policies.ReconnectionPolicy;
+import com.datastax.driver.core.policies.RetryPolicy;
+import com.datastax.driver.core.policies.RoundRobinPolicy;
+import com.datastax.driver.core.policies.SpeculativeExecutionPolicy;
+import com.datastax.driver.core.policies.TokenAwarePolicy;
+import com.datastax.driver.core.policies.WhiteListPolicy;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.annotations.Beta;
+import com.google.common.base.Strings;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,11 +56,110 @@
* {@code DRIVER_CONFIG} JSON shape, which {@link Connection.Factory} then sends in the control
* connection's {@code STARTUP} options.
*
- *
The report is built once, when the {@link Cluster} initializes, and the resulting string is
- * reused for the lifetime of that {@code Cluster} — it is never rebuilt while the session is in
- * flight, so a control-connection reconnect costs nothing and always reports the same
- * configuration.
+ *
The report is built afresh for every control connection, from inside its {@code STARTUP} frame
+ * assembly, and never cached: it describes the objects that are in force at that handshake rather
+ * than the configuration the {@link Cluster} was constructed from. That distinction is not
+ * theoretical. {@code Cluster.Manager.init()} builds the {@link Connection.Factory} before it calls
+ * {@link LoadBalancingPolicy#init}, and the first control connection's {@code STARTUP} goes out in
+ * between — so a datacenter or rack the policy infers from the node it reaches cannot be known on
+ * the first report and is known on every later one. A reconnect therefore costs one report build
+ * and may legitimately report more than the connection before it did.
+ *
+ *
The report follows the normative cross-driver schema (report {@code version} 1): 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 connect timeout, a disabled read
+ * timeout, a disabled {@code SO_LINGER}, an unbounded page size and a non-serial default serial
+ * consistency level are omitted rather than emitted as a value the schema rejects. Two keys are
+ * omitted for a third reason — the schema admits only a boolean, and 3.x cannot observe which one
+ * applies: {@code query.defaults.client-timestamps} for a timestamp generator that is none of the
+ * driver's own, and {@code connection.tls.hostname-verification} for any {@link SSLOptions} other
+ * than {@link SniSSLOptions}. Both keys are documented as absent when the behavior is unknown,
+ * which is exactly the case here: a custom generator decides per call whether to assign a timestamp
+ * at all, and every other {@code SSLOptions} builds its engine from a user-supplied {@code
+ * SSLContext} (or hands the whole handler over to Netty), so nothing the reporter can read says
+ * whether the hostname is checked. Two more optional bounds are omitted for the opposite reason —
+ * 3.x simply has no such bound to report: {@code connection.reconnection.policy.max-attempts},
+ * since its reconnection policies retry forever, and {@code query.retry.policy.max-retries}, since
+ * no single number describes a 3.x retry policy: {@link RetryPolicy#onRequestError} is the one path
+ * that does not stop after a single retry, and it is only reached for an idempotent statement, so
+ * the same policy bounds a non-idempotent request at one retry and an idempotent one at the length
+ * of the query plan. Which of the two a statement gets is not configuration — {@link
+ * Statement#setIdempotent} overrides the reported {@code query.defaults.idempotence} per statement.
+ *
+ *
Only a token-aware chain has a built-in shape in the schema, whose {@code
+ * query.load-balancing.policy.type} admits {@code token-aware} and {@code custom} and nothing else.
+ * That shape carries no name, so it is claimed only for a chain every element of which the group
+ * can describe; any other chain is {@code custom}, named after every policy in it — {@code
+ * WhiteListPolicy(TokenAwarePolicy(DCAwareRoundRobinPolicy))} — and keeps the capability keys it
+ * can still state, which the {@code custom} branch admits as additional properties. See {@link
+ * #loadBalancingPolicy}. A datacenter and rack are reported in {@code
+ * query.load-balancing.node-preference} whenever the chain prefers one: from a DC- or rack-aware
+ * policy, or from a filter restricting the session to a single datacenter. A bare {@link
+ * RoundRobinPolicy}, or a {@link WhiteListPolicy} over one, prefers neither and both preference
+ * keys are omitted.
+ *
+ *
The schema reports that preference in a second, optional place, {@code
+ * connection.node-preference}, which describes the part of the cluster the driver holds connections
+ * to rather than how a query is routed. One {@link LoadBalancingPolicy} decides both in 3.x, since
+ * {@link LoadBalancingPolicy#distance(Host)} is what governs whether a host is pooled at all, so
+ * that key is derived from the same chain — but from its datacenter half alone. A rack-aware
+ * policy's {@code distance()} returns {@code REMOTE}, never {@code IGNORED}, for a local-datacenter
+ * host in another rack, so those hosts are still pooled and the rack does not scope what the driver
+ * connects to. The datacenter does: a host outside the preferred one is {@code IGNORED} unless the
+ * policy is configured to use hosts there, and an ignored host gets no pool at all.
+ *
+ *
That last claim is exact only for the policy the preference was read from. A chainable policy
+ * above it can narrow pooling further: {@link HostFilterPolicy#distance(Host)} — and
+ * therefore {@link WhiteListPolicy}'s — returns {@code IGNORED} for any host failing its predicate,
+ * including one inside the reported datacenter, and a custom policy computes {@code distance()}
+ * itself and need honor nothing the chain below it prefers. The reported datacenter is thus
+ * necessary but not sufficient: it is what the located policy prefers, not the whole of what gets a
+ * pool. Reported all the same, deliberately, on the grounds that hiding a datacenter the operator
+ * really did configure is the worse failure mode — note the asymmetry with the inferred values
+ * above, in that nothing is inferred on a third party's behalf while what was explicitly configured
+ * is passed through even where a wrapper may override it.
+ *
+ *
A filter that restricts the session to exactly one datacenter is read the other way round:
+ * {@link HostFilterPolicy#fromDCWhiteList(LoadBalancingPolicy, Iterable)} retains the datacenters
+ * it was given, so when nothing in the chain prefers a datacenter of its own — {@code
+ * fromDCWhiteList(new RoundRobinPolicy(), ["dc1"])} — the restriction is what decides the
+ * preference and is reported as one. Anything a filter can express that names no single datacenter
+ * keeps the group omitted: a blacklist, several allowed datacenters, a blank name, a {@link
+ * WhiteListPolicy} (which filters on addresses), or a caller-supplied {@code Predicate},
+ * which stays opaque. A filter the preference was not read from — because a location-aware
+ * policy below it won — narrows the chain without being stated anywhere, so it costs the chain its
+ * built-in shape and is named instead, exactly like any other restricting wrapper.
+ *
+ * Known limitations: omission is not always available, because these fields are
+ * required by the schema.
+ *
+ *
+ * - {@code connection.requests.in-flight.max} must be positive, while {@link
+ * PoolingOptions#setMaxRequestsPerConnection(HostDistance, int)} also accepts 0.
+ *
- {@code query.speculative-execution.policy.percentile} is bounded to 0..100 exclusive by the
+ * schema, while {@link PercentileSpeculativeExecutionPolicy} accepts a percentile of 0.
+ *
+ *
+ * {@code connection.requests.orphaned.max} used to belong to that list and no longer does: 3.x
+ * has no orphaned-request setting to report it from — a request the driver stopped waiting for
+ * keeps its stream identifier until the response arrives, with no configurable bound and no
+ * connection replacement (only 4.x has {@code advanced.connection.max-orphan-requests}) — and the
+ * schema now makes the group optional for exactly that case, so omission is the schema-valid
+ * answer.
+ *
+ *
Such a value is reported as-is, and a key with no equivalent stays omitted: the
+ * reporter deliberately neither fabricates an in-range value — which would misreport a setting an
+ * operator may have chosen on purpose, or a policy 3.x does not implement — 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.
+ *
+ *
The read timeout feeds three places, all of them optional, so disabling it omits all three:
+ * {@code connection.read}, {@code control-plane.queries.system.timeout.client-side-ms} and {@code
+ * query.defaults.request}.
*/
+@Beta
public class DefaultDriverConfigReporter implements DriverConfigReporter {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDriverConfigReporter.class);
@@ -44,6 +173,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 +218,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 +247,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 +262,730 @@ 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 three top-level configuration groups onto the report root from {@link
+ * #configuration} and its policies. 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 three keys across two groups, so it is walked once here
+ // and handed to both: the policy object and the full node preference under "query", and the
+ // datacenter half of that preference under "connection".
+ List lbChain = policyChain(policies.getLoadBalancingPolicy());
+ NodeLocation nodeLocation = nodeLocation(lbChain);
+ root.set("connection", connection(policies, nodeLocation));
+ root.set("control-plane", controlPlane());
+ root.set("query", query(policies, lbChain, nodeLocation));
+ }
+
+ private ObjectNode connection(Policies policies, NodeLocation nodeLocation) {
+ SocketOptions socketOptions = configuration.getSocketOptions();
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ // The "connect" group is required but its timeout is not, and a non-positive connect timeout
+ // disables it: omit the timeout rather than report a number the schema rejects, leaving the
+ // group empty.
+ ObjectNode connect = OBJECT_MAPPER.createObjectNode();
+ int connectTimeoutMillis = socketOptions.getConnectTimeoutMillis();
+ if (connectTimeoutMillis > 0) {
+ connect.put("timeout-ms", connectTimeoutMillis);
+ }
+ n.set("connect", connect);
+ // Optional group, positive-only: a non-positive read timeout disables read timeouts, so omit
+ // the group rather than report a number the schema rejects.
+ int readTimeoutMillis = socketOptions.getReadTimeoutMillis();
+ if (readTimeoutMillis > 0) {
+ n.set("read", OBJECT_MAPPER.createObjectNode().put("timeout-ms", readTimeoutMillis));
+ }
+ // No socket-level write timeout in 3.x -> omit "write". "heartbeat" is a reserved-empty
+ // placeholder in this schema version (the heartbeat interval has no home yet) -> omit.
+ n.set("requests", requests());
+ n.set("pool", pool());
+ n.set("socket", socket());
+ n.set("reconnection", wrap("policy", reconnectionPolicy(policies)));
+ // Optional, and absent when TLS is disabled rather than reported as off.
+ ObjectNode tls = tls();
+ if (tls != null) {
+ n.set("tls", tls);
+ }
+ // Optional, and the datacenter half of the preference only -- the rack does not scope which
+ // hosts are pooled; see the class javadoc.
+ if (nodeLocation != null) {
+ n.set("node-preference", nodeLocation.toDatacenterPreference());
+ }
+ return n;
+ }
+
+ /**
+ * The {@code query} group: the per-request defaults plus the three policies that act on a query,
+ * each nested under the group the schema gives it.
+ */
+ private ObjectNode query(
+ Policies policies, List lbChain, NodeLocation nodeLocation) {
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ n.set("defaults", queryDefaults(policies));
+ // 3.x retries immediately: no built-in retry policy inserts a delay between attempts, and a
+ // custom one exposes no schedule to introspect -> omit the optional "backoff".
+ n.set("retry", wrap("policy", retryPolicy(policies)));
+
+ ObjectNode loadBalancing =
+ wrap(
+ "policy",
+ loadBalancingPolicy(lbChain, policies.getLoadBalancingPolicy(), nodeLocation));
+ // Optional: omitted when the LB policy carries no DC/rack notion.
+ if (nodeLocation != null) {
+ loadBalancing.set("node-preference", nodeLocation.toFullPreference());
+ }
+ n.set("load-balancing", loadBalancing);
+
+ // Optional: omitted when there is no speculative execution.
+ ObjectNode specEx = speculativeExecutionPolicy(policies);
+ if (specEx != null) {
+ n.set("speculative-execution", wrap("policy", specEx));
+ }
+ return n;
+ }
+
+ private ObjectNode requests() {
+ PoolingOptions pooling = configuration.getPoolingOptions();
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ n.set(
+ "in-flight",
+ OBJECT_MAPPER
+ .createObjectNode()
+ .put(
+ "max",
+ effective(
+ pooling.getMaxRequestsPerConnection(HostDistance.LOCAL),
+ poolDefault(PoolingOptions.MAX_REQUESTS_PER_CONNECTION_LOCAL_KEY))));
+ // "orphaned" has no 3.x equivalent, so it is omitted; the schema makes the group optional for
+ // exactly that case — see the class javadoc.
+ return n;
+ }
+
+ private ObjectNode pool() {
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ // The schema carries no pool size or keying: 3.x has a single per-host pool type, and a
+ // per-shard count could not be reported anyway, since this report is built before the control
+ // connection is up and no node's shard count is known yet.
+ n.set(
+ "shard-aware",
+ OBJECT_MAPPER
+ .createObjectNode()
+ .put("enabled", configuration.getProtocolOptions().isUseAdvancedShardAwareness()));
+ return n;
+ }
+
+ private ObjectNode socket() {
+ SocketOptions o = configuration.getSocketOptions();
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ // Boolean options report the effective on/off state; when the driver leaves them unset the
+ // OS/platform default applies, approximated here (tcp-no-delay defaults on, the others off).
+ // Best-effort: Connection.Factory applies each option only when SocketOptions has a value for
+ // it, then hands the bootstrap to NettyOptions.afterBootstrapInitialized, which can set any
+ // ChannelOption afterwards without this being able to see it.
+ n.put("tcp-no-delay", boolOrDefault(o.getTcpNoDelay(), true));
+ n.put("keep-alive", boolOrDefault(o.getKeepAlive(), false));
+ n.put("reuse-address", boolOrDefault(o.getReuseAddress(), false));
+ // All three groups below are optional, so a value the schema cannot express is omitted rather
+ // than emitted: a negative SO_LINGER means lingering close is disabled (the schema takes a
+ // non-negative interval, so 0 is still reported), and a non-positive buffer size leaves the
+ // JDK/OS default in place (the schema takes a positive size).
+ Integer soLinger = o.getSoLinger();
+ if (soLinger != null && soLinger >= 0) {
+ n.set("linger", OBJECT_MAPPER.createObjectNode().put("interval-s", soLinger));
+ }
+ Integer receiveBufferSize = o.getReceiveBufferSize();
+ if (receiveBufferSize != null && receiveBufferSize > 0) {
+ n.set(
+ "receive-buffer", OBJECT_MAPPER.createObjectNode().put("size-bytes", receiveBufferSize));
+ }
+ Integer sendBufferSize = o.getSendBufferSize();
+ if (sendBufferSize != null && sendBufferSize > 0) {
+ n.set("send-buffer", OBJECT_MAPPER.createObjectNode().put("size-bytes", sendBufferSize));
+ }
+ return n;
+ }
+
+ private ObjectNode controlPlane() {
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ // 3.x has no dedicated control-connection timeout; internal/system queries use the read
+ // timeout.
+ // There is no client-configurable server-side ("USING TIMEOUT") timeout -> omit server-side-ms.
+ ObjectNode timeout = OBJECT_MAPPER.createObjectNode();
+ // Optional and positive-only, and a non-positive read timeout disables read timeouts: omit
+ // rather than report a number the schema rejects. The enclosing "timeout" object is required,
+ // so it stays (empty).
+ int clientSideMs = configuration.getSocketOptions().getReadTimeoutMillis();
+ if (clientSideMs > 0) {
+ timeout.put("client-side-ms", clientSideMs);
+ }
+ n.set("queries", wrap("system", wrap("timeout", timeout)));
+ // Required and non-negative in the schema, and 0 is meaningful (do not wait for agreement).
+ // Cluster.Builder rejects a non-positive wait, but ProtocolOptions can be constructed with one
+ // directly, and a negative wait behaves exactly like 0 — so normalizing it is exact rather than
+ // invented, and keeps the required field in range.
+ long schemaAgreementMs =
+ Math.max(0L, configuration.getProtocolOptions().getMaxSchemaAgreementWaitSeconds() * 1000L);
+ n.set(
+ "schema",
+ wrap("agreement", OBJECT_MAPPER.createObjectNode().put("timeout-ms", schemaAgreementMs)));
+ return n;
+ }
+
+ private ObjectNode reconnectionPolicy(Policies policies) {
+ ReconnectionPolicy policy = policies.getReconnectionPolicy();
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ if (policy instanceof ExponentialReconnectionPolicy) {
+ ExponentialReconnectionPolicy p = (ExponentialReconnectionPolicy) policy;
+ n.put("type", "exponential");
+ n.put("base-ms", p.getBaseDelayMs());
+ n.put("max-ms", p.getMaxDelayMs());
+ // Optional, and omitted because 3.x built-in reconnection policies never give up. The
+ // maxAttempts field ExponentialReconnectionPolicy carries is not that bound: it is an
+ // overflow
+ // guard on the doubling, and past it nextDelayMs() keeps returning maxDelayMs forever.
+ } else if (policy instanceof ConstantReconnectionPolicy) {
+ n.put("type", "constant");
+ n.put("delay-ms", ((ConstantReconnectionPolicy) policy).getConstantDelayMs());
+ } else {
+ customPolicy(n, policy);
+ }
+ return n;
+ }
+
+ private ObjectNode retryPolicy(Policies policies) {
+ RetryPolicy policy = policies.getRetryPolicy();
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ // No 3.x policy has a retry count to report, so the optional "max-retries" is always omitted;
+ // both built-ins below are parameterless singletons, and the schema's own wording for the key
+ // is "absent when no explicit retry limit is configured". Nor could one be derived: they stop
+ // after one attempt on a read timeout, a write timeout or an unavailable error -- all three
+ // sharing one counter, so one retry between them, not one each -- while onRequestError leaves
+ // nbRetry unread and keeps trying the next host until the query plan runs out. Which of those
+ // applies is decided per statement rather than by configuration: RequestHandler only consults
+ // onRequestError and onWriteTimeout for an idempotent statement, so the same policy bounds a
+ // non-idempotent request at one retry and an idempotent one at the length of the query plan.
+ // FallthroughRetryPolicy retries nothing, and the schema's "fallthrough" admits no such key
+ // anyway; a wrapper reported as custom cannot be introspected at all.
+ if (policy instanceof DefaultRetryPolicy) {
+ n.put("type", "standard-error-aware");
+ } else if (policy instanceof DowngradingConsistencyRetryPolicy) {
+ n.put("type", "downgrading-consistency");
+ } else if (policy instanceof FallthroughRetryPolicy) {
+ n.put("type", "fallthrough");
+ } else {
+ // LoggingRetryPolicy / IdempotenceAwareRetryPolicy wrap a child but expose no getter, so only
+ // the outer type can be reported.
+ customPolicy(n, policy);
+ }
+ return n;
+ }
+
+ private ObjectNode speculativeExecutionPolicy(Policies policies) {
+ SpeculativeExecutionPolicy policy = policies.getSpeculativeExecutionPolicy();
+ if (policy instanceof NoSpeculativeExecutionPolicy) {
+ return null;
+ }
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ if (policy instanceof ConstantSpeculativeExecutionPolicy) {
+ ConstantSpeculativeExecutionPolicy p = (ConstantSpeculativeExecutionPolicy) policy;
+ n.put("type", "constant");
+ n.put("max-executions", p.getMaxSpeculativeExecutions());
+ n.put("delay-ms", p.getConstantDelayMillis());
+ } else if (policy instanceof PercentileSpeculativeExecutionPolicy) {
+ PercentileSpeculativeExecutionPolicy p = (PercentileSpeculativeExecutionPolicy) policy;
+ n.put("type", "percentile");
+ n.put("max-executions", p.getMaxSpeculativeExecutions());
+ // Required, so a percentile of 0 — which the schema's range does not admit but the policy
+ // accepts — is reported as-is; see the class javadoc.
+ n.put("percentile", p.getPercentile());
+ } else {
+ customPolicy(n, policy);
+ }
+ return n;
+ }
+
+ /**
+ * The {@code query.load-balancing.policy} group for a chain, discriminated on whether the schema
+ * can describe every element of it.
+ *
+ * The built-in {@code token-aware} shape has no room for a name, so claiming it means claiming
+ * the chain is exactly that shape. Each element is therefore classified: {@link
+ * TokenAwarePolicy} and {@link LatencyAwarePolicy} are capabilities the shape carries as {@code
+ * load-distribution} and {@code adaptive-ordering}; {@link DCAwareRoundRobinPolicy}, {@link
+ * RackAwareRoundRobinPolicy} and {@link RoundRobinPolicy} are described by {@code
+ * node-preference} and {@code fallback-to-non-preferred-nodes} beside it; {@link
+ * PagingOptimizingLoadBalancingPolicy} is an internal wrapper that describes nothing and is
+ * dropped; the {@link HostFilterPolicy} the reported {@code node-preference} was read from is
+ * described by that preference (see {@link #nodeLocation}). Anything else — {@link
+ * WhiteListPolicy}, {@link ErrorAwarePolicy}, an opaque filter, a user policy — restricts or
+ * reorders candidates in a way nothing in the group states.
+ *
+ *
A filter is only described by the preference when the preference is its
+ * restriction, which is why this is tested against {@code nodeLocation} rather than against the
+ * filter alone. A location-aware policy below it wins that slot, and the filter then narrows the
+ * chain further without appearing anywhere: {@code fromDCWhiteList(DCAware("dc1"), ["dc2"])}
+ * reports {@code dc1} while nothing outside {@code dc2} is reachable. Treating such a filter as
+ * described would leave it invisible behind an inner {@link TokenAwarePolicy} and visible without
+ * one — the nesting-dependent blind spot this whole classification exists to remove.
+ *
+ *
One such element and the whole chain is reported as {@code custom}, whose {@code name} names
+ * every policy involved, outermost first: {@code
+ * WhiteListPolicy(TokenAwarePolicy(DCAwareRoundRobinPolicy))}. The capability keys are kept
+ * alongside it — the {@code custom} branch admits additional properties precisely so a driver
+ * that can introspect a policy may serialize what it knows — so naming the wrapper costs nothing
+ * that was previously reported. It was previously invisible: an outer {@code WhiteListPolicy}
+ * over a token-aware chain reported plain {@code token-aware}, so whether an operator could see
+ * that the client is pinned to a host list depended on an unrelated nesting choice.
+ */
+ private ObjectNode loadBalancingPolicy(
+ List chain, LoadBalancingPolicy policy, NodeLocation nodeLocation) {
+ TokenAwarePolicy tokenAware = null;
+ boolean latencyAware = false;
+ DCAwareRoundRobinPolicy dcAware = null;
+ RackAwareRoundRobinPolicy rackAware = null;
+ boolean describedByTheSchema = true;
+ for (LoadBalancingPolicy current : chain) {
+ if (current instanceof TokenAwarePolicy) {
+ tokenAware = (TokenAwarePolicy) current;
+ } else if (current instanceof LatencyAwarePolicy) {
+ latencyAware = true;
+ } else if (current instanceof DCAwareRoundRobinPolicy) {
+ dcAware = (DCAwareRoundRobinPolicy) current;
+ } else if (current instanceof RackAwareRoundRobinPolicy) {
+ rackAware = (RackAwareRoundRobinPolicy) current;
+ } else if (!(current instanceof PagingOptimizingLoadBalancingPolicy
+ || current instanceof RoundRobinPolicy
+ || (nodeLocation != null && current == nodeLocation.source))) {
+ describedByTheSchema = false;
+ }
+ }
+
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ if (describedByTheSchema && tokenAware != null) {
+ n.put("type", "token-aware");
+ } else {
+ n.put("type", "custom");
+ n.put("name", chainName(chain, policy));
+ }
+ if (tokenAware != null) {
+ n.put("load-distribution", loadDistribution(tokenAware.getReplicaOrdering()));
+ // A rack-aware policy always reaches the other racks of its local datacenter -- the second
+ // tier of a normal query plan, once the local rack is exhausted, and folded into the first
+ // tier for an LWT or serial-consistency statement, which skips rack prioritization altogether
+ // -- and its distance() returns REMOTE, not IGNORED, for them, so its preference is always
+ // fallen back on either way. A DC-aware policy only leaves the preferred datacenter when
+ // configured to use hosts there.
+ boolean fallback =
+ rackAware != null || (dcAware != null && dcAware.getUsedHostsPerRemoteDc() > 0);
+ n.put("fallback-to-non-preferred-nodes", fallback);
+ }
+ // Optional, and its "signals" cannot be empty: the group is omitted altogether when nothing
+ // reorders candidates at runtime. Latency, via LatencyAwarePolicy, is the only observation a
+ // 3.x policy can reorder them on, and it is reported wherever that policy appears -- adaptive
+ // ordering is a property of the chain, not of token awareness, so LatencyAwarePolicy over a
+ // bare RoundRobinPolicy carries it just as it does over a token-aware one.
+ if (latencyAware) {
+ ObjectNode adaptiveOrdering = OBJECT_MAPPER.createObjectNode();
+ adaptiveOrdering.putArray("signals").add("latency");
+ n.set("adaptive-ordering", adaptiveOrdering);
+ }
+ return n;
+ }
+
+ /**
+ * Every policy in {@code chain} rendered outermost-first as {@code Outer(Inner(Innermost))},
+ * skipping the internal {@code PagingOptimizingLoadBalancingPolicy} wrapper {@code
+ * Cluster.Manager} puts around every session's policy — reporting that would tell an operator
+ * nothing about what the client runs.
+ */
+ private static String chainName(List chain, LoadBalancingPolicy fallback) {
+ StringBuilder name = new StringBuilder();
+ int open = 0;
+ for (LoadBalancingPolicy current : chain) {
+ if (current instanceof PagingOptimizingLoadBalancingPolicy) {
+ continue;
+ }
+ if (open > 0) {
+ name.append('(');
+ }
+ name.append(policyName(current));
+ open++;
+ }
+ if (open == 0) {
+ return policyName(fallback);
+ }
+ for (int i = 1; i < open; i++) {
+ name.append(')');
+ }
+ return name.toString();
+ }
+
+ /**
+ * The schema's normalized name for how a token-aware policy distributes requests over the
+ * replicas it considers equally preferred.
+ */
+ private static String loadDistribution(TokenAwarePolicy.ReplicaOrdering replicaOrdering) {
+ switch (replicaOrdering) {
+ case RANDOM:
+ // A different random order for each query plan.
+ return "shuffle";
+ case TOPOLOGICAL:
+ // The replica set's own order around the token ring, never reordered.
+ return "replica-set";
+ case NEUTRAL:
+ default:
+ // Whatever order the child policy's query plan has, i.e. round-robin for every built-in
+ // child, each of which rotates its starting host across successive plans.
+ return "round-robin";
+ }
+ }
+
+ /**
+ * The node location {@code chain} prefers, or {@code null} when no policy in it carries one — a
+ * bare {@link RoundRobinPolicy}, or a custom policy with no introspectable location — in which
+ * case both of the schema's node preference keys are omitted.
+ *
+ * A location-aware policy wins over a filter above it: {@code
+ * fromDCWhiteList(DCAwareRoundRobinPolicy(...))} reports the policy's own datacenter, which is
+ * what routes its query plans. The filter is consulted only when nothing in the chain prefers a
+ * datacenter of its own, and only when it restricts the session to exactly one — see {@link
+ * #singleWhiteListedDatacenter}.
+ */
+ private static NodeLocation nodeLocation(List chain) {
+ DCAwareRoundRobinPolicy dcAware = null;
+ RackAwareRoundRobinPolicy rackAware = null;
+ HostFilterPolicy dcFilter = null;
+ String filteredDc = null;
+ for (LoadBalancingPolicy current : chain) {
+ if (current instanceof DCAwareRoundRobinPolicy) {
+ dcAware = (DCAwareRoundRobinPolicy) current;
+ } else if (current instanceof RackAwareRoundRobinPolicy) {
+ rackAware = (RackAwareRoundRobinPolicy) current;
+ } else if (current instanceof HostFilterPolicy) {
+ String dc = singleWhiteListedDatacenter((HostFilterPolicy) current);
+ if (dc != null) {
+ dcFilter = (HostFilterPolicy) current;
+ filteredDc = dc;
+ }
+ }
+ }
+ if (rackAware != null) {
+ return NodeLocation.ofRack(
+ rackAware,
+ rackAware.getLocalDc(),
+ rackAware.isLocalDcExplicit(),
+ rackAware.getLocalRack(),
+ rackAware.isLocalRackExplicit());
+ }
+ if (dcAware != null) {
+ return NodeLocation.ofDatacenter(dcAware, dcAware.getLocalDc(), dcAware.isLocalDcExplicit());
+ }
+ if (dcFilter != null) {
+ // Explicit: the datacenter was named by the caller, not inferred from the node the driver
+ // happened to reach first.
+ return NodeLocation.ofDatacenter(dcFilter, filteredDc, true);
+ }
+ return null;
+ }
+
+ /**
+ * The one datacenter {@code policy} restricts the session to, or {@code null} when it names none,
+ * several, or something that is not a datacenter at all: a denied datacenter names no preferred
+ * one, several allowed datacenters name no single one, and a {@link WhiteListPolicy} (or a
+ * caller-supplied predicate) filters on something else entirely — {@link
+ * HostFilterPolicy#getWhiteListedDatacenters()} is empty for all of those.
+ *
+ * A blank name is no name either. Nothing validates the strings handed to {@link
+ * HostFilterPolicy#fromDCWhiteList(LoadBalancingPolicy, Iterable)}, and the schema requires a
+ * non-empty {@code local-dc} beside {@code type: "dc"} — so the whole preference is omitted
+ * rather than reported as a value the schema rejects, which is also how {@link
+ * DCAwareRoundRobinPolicy#getLocalDc()} and {@link RackAwareRoundRobinPolicy#getLocalRack()}
+ * treat one. A merely padded name is not blank and is reported verbatim: hiding the whitespace
+ * would hide the typo the report exists to expose.
+ */
+ private static String singleWhiteListedDatacenter(HostFilterPolicy policy) {
+ Set dcs = policy.getWhiteListedDatacenters();
+ if (dcs.size() != 1) {
+ return null;
+ }
+ String dc = dcs.iterator().next();
+ return Strings.isNullOrEmpty(dc) ? null : dc;
+ }
+
+ /**
+ * The datacenter and rack a load balancing policy chain prefers, and whether each of them was
+ * configured or is left for the policy to infer.
+ *
+ * Extracted once and rendered twice, because the schema's two node preference keys make
+ * different claims about the same policy: {@link #toFullPreference()} for the preference the
+ * policy routes queries by, {@link #toDatacenterPreference()} for the part of it that decides
+ * which hosts are pooled at all.
+ */
+ private static final class NodeLocation {
+
+ /**
+ * The policy in the chain this location was read from. {@link #loadBalancingPolicy} needs it to
+ * tell a filter whose restriction the report states from one that narrows the chain further
+ * without being stated anywhere; only the former can be left out of the policy's name.
+ */
+ private final LoadBalancingPolicy source;
+
+ private final String dc;
+ private final boolean dcExplicit;
+ private final String rack;
+ private final boolean rackExplicit;
+ private final boolean rackAware;
+
+ static NodeLocation ofDatacenter(LoadBalancingPolicy source, String dc, boolean dcExplicit) {
+ return new NodeLocation(source, dc, dcExplicit, null, false, false);
+ }
+
+ static NodeLocation ofRack(
+ LoadBalancingPolicy source,
+ String dc,
+ boolean dcExplicit,
+ String rack,
+ boolean rackExplicit) {
+ return new NodeLocation(source, dc, dcExplicit, rack, rackExplicit, true);
+ }
+
+ private NodeLocation(
+ LoadBalancingPolicy source,
+ String dc,
+ boolean dcExplicit,
+ String rack,
+ boolean rackExplicit,
+ boolean rackAware) {
+ this.source = source;
+ this.dc = dc;
+ this.dcExplicit = dcExplicit;
+ this.rack = rack;
+ this.rackExplicit = rackExplicit;
+ this.rackAware = rackAware;
+ }
+
+ /** The datacenter half alone, for {@code connection.node-preference}. */
+ ObjectNode toDatacenterPreference() {
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ // "dc" requires the name and always has one: both policies derive the explicitness flag from
+ // the configured string, so an explicit datacenter is never blank. "dc-auto" carries an
+ // inferred name under plain "local-dc" rather than an "inferred-" prefixed key, unlike
+ // "rack-auto" below; that asymmetry is the schema's.
+ n.put("type", dcExplicit ? "dc" : "dc-auto");
+ putIfNotNull(n, "local-dc", dc);
+ return n;
+ }
+
+ /** The whole preference, rack included, for {@code query.load-balancing.node-preference}. */
+ ObjectNode toFullPreference() {
+ if (!rackAware) {
+ // A DC-aware policy has no rack notion, so its whole preference is the datacenter.
+ return toDatacenterPreference();
+ }
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ if (dcExplicit && rackExplicit) {
+ n.put("type", "rack");
+ putIfNotNull(n, "local-dc", dc);
+ putIfNotNull(n, "local-rack", rack);
+ return n;
+ }
+ // At least one part is inferred, and the schema reports configured and inferred values under
+ // separate keys — of which it admits only one per part. An inferred value is only known once
+ // the policy has been initialized, i.e. never at report time, so in practice its key is
+ // absent.
+ n.put("type", "rack-auto");
+ putIfNotNull(n, dcExplicit ? "local-dc" : "inferred-local-dc", dc);
+ putIfNotNull(n, rackExplicit ? "local-rack" : "inferred-local-rack", rack);
+ return n;
+ }
+ }
+
+ private ObjectNode queryDefaults(Policies policies) {
+ QueryOptions q = configuration.getQueryOptions();
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ // Paging is unbounded either way the driver can express it, and the schema has no sentinel for
+ // that ("absent when page is not limited"), so this optional group is omitted entirely.
+ // Integer.MAX_VALUE is the documented way to disable paging (QueryOptions#setFetchSize), and is
+ // the value that actually reaches a session; a non-positive size is rejected by that setter and
+ // only a QueryOptions subclass overriding the getter can still produce one.
+ int fetchSize = q.getFetchSize();
+ if (fetchSize > 0 && fetchSize != Integer.MAX_VALUE) {
+ n.set("page", OBJECT_MAPPER.createObjectNode().put("size", fetchSize));
+ }
+ // Required, and the schema's enum admits every level QueryOptions accepts, serial ones
+ // included. The setter rejects null, so only a QueryOptions subclass overriding the getter can
+ // still return one; guarded because letting it through would throw here and cost the whole
+ // report, not just this key.
+ ConsistencyLevel consistency = q.getConsistencyLevel();
+ if (consistency != null) {
+ n.put("consistency", consistency.name());
+ }
+ // Optional, and unlike Statement.setSerialConsistencyLevel, QueryOptions does not check that
+ // the level is serial: omit one the schema's enum cannot express rather than emit it.
+ ConsistencyLevel serialConsistency = q.getSerialConsistencyLevel();
+ if (serialConsistency != null && serialConsistency.isSerial()) {
+ n.put("serial-consistency", serialConsistency.name());
+ }
+ n.put("idempotence", q.getDefaultIdempotence());
+ // Optional, and reported only for the driver's own generators, whose answer is fixed:
+ // ServerSideTimestampGenerator always returns Long.MIN_VALUE and so always leaves the timestamp
+ // to the coordinator, while an AbstractMonotonicTimestampGenerator always computes one
+ // client-side (its documented extension point is onDrift, not next). Any other generator
+ // decides per call, so nothing here can say which happens -- the "absent when this behavior is
+ // unknown" case the schema's optional key is for.
+ TimestampGenerator timestampGenerator = policies.getTimestampGenerator();
+ if (timestampGenerator instanceof ServerSideTimestampGenerator) {
+ n.put("client-timestamps", false);
+ } else if (timestampGenerator instanceof AbstractMonotonicTimestampGenerator) {
+ n.put("client-timestamps", true);
+ }
+ // 3.x has no per-request timeout of its own: the read timeout bounds every request, so it is
+ // also what connection.read and control-plane.queries.system report. Both the timeout and its
+ // enclosing group are optional and the timeout is positive-only, so a disabled read timeout
+ // omits the group entirely rather than reporting it empty.
+ int requestTimeoutMs = configuration.getSocketOptions().getReadTimeoutMillis();
+ if (requestTimeoutMs > 0) {
+ n.set("request", OBJECT_MAPPER.createObjectNode().put("timeout-ms", requestTimeoutMs));
+ }
+ return n;
+ }
+
+ /** The {@code tls} group, or {@code null} when TLS is disabled and the group is omitted. */
+ private ObjectNode tls() {
+ SSLOptions sslOptions = configuration.getProtocolOptions().getSSLOptions();
+ if (sslOptions == null) {
+ return null;
+ }
+ ObjectNode n = OBJECT_MAPPER.createObjectNode();
+ // 3.x exposes no hostname-verification flag. SniSSLOptions is the one implementation that
+ // verifies the hostname and it hard-codes it on, so it is the only case that can be reported.
+ // Every other SSLOptions builds its engine from a user-supplied SSLContext, or hands the whole
+ // handler to Netty, and neither says whether endpoint identification is enabled -- which is the
+ // "absent when this behavior is unknown" case the schema's optional key is for. The group can
+ // therefore be empty: its presence is what reports that TLS is on.
+ if (sslOptions instanceof SniSSLOptions) {
+ n.put("hostname-verification", true);
+ }
+ return n;
+ }
+
+ /**
+ * Returns the load balancing policy chain, outermost policy first, by following {@code
+ * ChainableLoadBalancingPolicy.getChildPolicy()} for at most {@link #MAX_POLICY_CHAIN_LENGTH}
+ * policies. The chain is walked rather than the configured policy read directly because {@code
+ * Cluster.Manager} wraps that policy at runtime; {@code query.load-balancing.policy}, {@code
+ * query.load-balancing.node-preference} and {@code connection.node-preference} are all derived
+ * from it.
+ */
+ private static List policyChain(LoadBalancingPolicy policy) {
+ List chain = new ArrayList();
+ LoadBalancingPolicy current = policy;
+ while (current != null && chain.size() < MAX_POLICY_CHAIN_LENGTH) {
+ chain.add(current);
+ current =
+ current instanceof ChainableLoadBalancingPolicy
+ ? ((ChainableLoadBalancingPolicy) current).getChildPolicy()
+ : null;
+ }
+ if (current != null) {
+ // Only reachable from a user policy whose getChildPolicy() chain is cyclic or absurdly deep;
+ // report what was seen rather than walking forever or dropping the whole report.
+ LOGGER.warn(
+ "Stopped walking the load balancing policy chain after {} policies; reporting only those. "
+ + "Does a ChainableLoadBalancingPolicy in the chain return a cyclic child policy?",
+ MAX_POLICY_CHAIN_LENGTH);
+ }
+ return chain;
+ }
+
+ private static void customPolicy(ObjectNode node, Object policy) {
+ node.put("type", "custom");
+ node.put("name", policyName(policy));
+ }
+
+ /**
+ * The name to report a policy object under: its simple class name, or its binary name when the
+ * simple name is empty. An anonymous class has no simple name, and the schema requires a
+ * non-empty one.
+ */
+ private static String policyName(Object policy) {
+ Class> policyClass = policy.getClass();
+ String simpleName = policyClass.getSimpleName();
+ return simpleName.isEmpty() ? policyClass.getName() : simpleName;
+ }
+
+ /**
+ * A new object node holding {@code value} under {@code key}. The schema nests most values one or
+ * two single-key objects deep — {@code connection.reconnection.policy}, {@code
+ * control-plane.queries.system.timeout} — which this keeps readable.
+ */
+ private static ObjectNode wrap(String key, JsonNode value) {
+ ObjectNode node = OBJECT_MAPPER.createObjectNode();
+ node.set(key, value);
+ return node;
+ }
+
+ private static void putIfNotNull(ObjectNode node, String key, String value) {
+ if (value != null) {
+ node.put(key, value);
+ }
+ }
+
+ private static boolean boolOrDefault(Boolean value, boolean defaultValue) {
+ return value == null ? defaultValue : value;
+ }
+
+ /**
+ * The effective pooling default for {@code key}. Needed as a fallback because {@link
+ * PoolingOptions} returns {@link PoolingOptions#UNSET} until the protocol version is known, which
+ * only happens once the control connection is up — after this report is built. A value the user
+ * configured explicitly takes precedence.
+ *
+ * Resolved with the same walk {@code PoolingOptions.setProtocolVersion} applies once the
+ * version is negotiated: the highest {@link PoolingOptions#DEFAULTS} key that does not exceed it.
+ * The version is the one the user pinned with {@link
+ * Cluster.Builder#withProtocolVersion(ProtocolVersion)}, which is known here, and otherwise
+ * {@link ProtocolVersion#V3} — the lowest version ScyllaDB negotiates, and the reference row for
+ * every version above it. Without the pinned version this would report the v3 limit for a cluster
+ * deliberately pinned to v2, whose pools are sized from the v1 row instead.
+ *
+ *
That fallback is an assumption rather than an observation, and it is the one way this field
+ * can be wrong: an unpinned cluster negotiates downward from the highest version the
+ * driver supports, so one that settles on v2 — a Cassandra 2.0 cluster, no ScyllaDB being that
+ * old — is sized from the v1 row (128) while this has already reported 1024. Pinning is the only
+ * part of negotiation knowable before the control connection is up, and the report is built once,
+ * so there is nothing later to correct it with.
+ *
+ *
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 int poolDefault(String key) {
+ ProtocolVersion pinned = configuration.getProtocolOptions().initialProtocolVersion;
+ ProtocolVersion version = pinned == null ? ProtocolVersion.V3 : pinned;
+ ProtocolVersion reference = null;
+ for (ProtocolVersion candidate : PoolingOptions.DEFAULTS.keySet()) {
+ if (candidate.compareTo(version) > 0) {
+ break;
+ }
+ reference = candidate;
+ }
+ // V1 is a key, so it is always at or below any version; a null would only mean DEFAULTS lost
+ // it,
+ // and the NPE is then contained by buildReport()'s fail-safe.
+ return PoolingOptions.DEFAULTS.get(reference).get(key);
+ }
+
+ /**
+ * {@code value} unless it is {@link PoolingOptions#UNSET}, in which case {@code defaultValue}.
+ *
+ *
Only {@code UNSET} falls back: {@link
+ * PoolingOptions#setMaxRequestsPerConnection(HostDistance, int)} accepts 0, and reporting the
+ * protocol default for it would misreport a limit the operator set deliberately. 0 is below the
+ * schema's minimum and reported as-is — see the class javadoc.
+ */
+ private static int effective(int value, int defaultValue) {
+ return value == PoolingOptions.UNSET ? 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..a0aa2b02dad 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
@@ -15,6 +15,8 @@
*/
package com.datastax.driver.core;
+import com.google.common.annotations.Beta;
+
/**
* Builds the {@code DRIVER_CONFIG} payload that the control connection sends in its CQL {@code
* STARTUP} options, so ScyllaDB can store it in {@code system.clients.client_options} and operators
@@ -28,18 +30,31 @@
* connection stores {@code DRIVER_CONFIG}; other nodes only see {@code SESSION_ID}-bearing pooled-
* connection rows. Consumers must query and aggregate across all nodes to see the full picture.
*/
+@Beta
public interface DriverConfigReporter {
/**
* Builds the configuration report, or returns {@code null} if it could not be built (in which
* case no {@code DRIVER_CONFIG} option is sent).
*
- *
Called once per {@link Cluster}, while it initializes; the returned string is then reused
- * for every control connection that cluster opens.
+ *
Called once per control connection, from its {@code STARTUP} frame assembly, and not cached:
+ * the report describes the objects in force at the handshake that sends it, rather than the
+ * configuration the {@link Cluster} was constructed from. Implementations must therefore tolerate
+ * being called on a Netty event loop, and repeatedly over a cluster's lifetime.
*
*
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 bound the report's size and return {@code null}
+ * rather than an oversized one: {@code STARTUP} option values carry an unchecked 16-bit length
+ * prefix, so a value over 65535 encoded bytes corrupts the frame instead of merely being useless.
+ * The built-in reporter caps the report at 32KiB of UTF-8, the limit the other ScyllaDB drivers
+ * apply.
+ *
+ *
An implementation that cannot even be loaded is handled one level up rather than
+ * here: {@link DefaultDriverConfigReporter} needs Jackson, and on a classpath without it the
+ * failure is a {@link LinkageError} raised while initializing the class, which no method of this
+ * interface could catch. {@code Connection.Factory.buildDriverConfigReport} contains that and
+ * reports nothing, so the connection is still established.
*
* @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/QueryOptions.java b/driver-core/src/main/java/com/datastax/driver/core/QueryOptions.java
index ee55dfcf381..a93d8976491 100644
--- a/driver-core/src/main/java/com/datastax/driver/core/QueryOptions.java
+++ b/driver-core/src/main/java/com/datastax/driver/core/QueryOptions.java
@@ -18,6 +18,7 @@
import com.datastax.driver.core.exceptions.UnsupportedFeatureException;
import com.datastax.driver.core.utils.MoreFutures;
import com.datastax.driver.core.utils.MoreObjects;
+import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
@@ -101,10 +102,14 @@ void register(Cluster.Manager manager) {
*
The consistency level set through this method will be use for queries that don't explicitly
* have a consistency level, i.e. when {@link Statement#getConsistencyLevel} returns {@code null}.
*
- * @param consistencyLevel the new consistency level to set as default.
+ * @param consistencyLevel the new consistency level to set as default. It must not be {@code
+ * null}: every query needs a consistency level, so a null default would fail any statement
+ * that does not set one of its own.
* @return this {@code QueryOptions} instance.
+ * @throws NullPointerException if {@code consistencyLevel} is {@code null}.
*/
public QueryOptions setConsistencyLevel(ConsistencyLevel consistencyLevel) {
+ Preconditions.checkNotNull(consistencyLevel, "consistencyLevel cannot be null");
this.consistencySet = true;
this.consistency = consistencyLevel;
return this;
diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/ConstantSpeculativeExecutionPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/ConstantSpeculativeExecutionPolicy.java
index 93e70a197fc..ee61a9d7ad9 100644
--- a/driver-core/src/main/java/com/datastax/driver/core/policies/ConstantSpeculativeExecutionPolicy.java
+++ b/driver-core/src/main/java/com/datastax/driver/core/policies/ConstantSpeculativeExecutionPolicy.java
@@ -18,6 +18,7 @@
import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.Host;
import com.datastax.driver.core.Statement;
+import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import java.util.concurrent.atomic.AtomicInteger;
@@ -52,6 +53,28 @@ public ConstantSpeculativeExecutionPolicy(
this.maxSpeculativeExecutions = maxSpeculativeExecutions;
}
+ /**
+ * The number of speculative executions this policy schedules for a request, as specified at
+ * instantiation. This does not include the initial, normal request.
+ *
+ * @return the number of speculative executions, always strictly positive.
+ */
+ @Beta
+ public int getMaxSpeculativeExecutions() {
+ return maxSpeculativeExecutions;
+ }
+
+ /**
+ * The delay between each speculative execution, as specified at instantiation. Zero means all
+ * executions are sent immediately, along with the original request.
+ *
+ * @return the delay in milliseconds, never negative.
+ */
+ @Beta
+ public long getConstantDelayMillis() {
+ return constantDelayMillis;
+ }
+
@Override
public SpeculativeExecutionPlan newPlan(String loggedKeyspace, Statement statement) {
return new SpeculativeExecutionPlan() {
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..dd24a56b47d 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
@@ -21,6 +21,7 @@
import com.datastax.driver.core.Host;
import com.datastax.driver.core.HostDistance;
import com.datastax.driver.core.Statement;
+import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
@@ -77,6 +78,7 @@ public static Builder builder() {
private final int usedHostsPerRemoteDc;
private final boolean dontHopForLocalCL;
+ private final boolean localDcExplicit;
private volatile Configuration configuration;
@@ -90,6 +92,43 @@ 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}.
+ */
+ @Beta
+ 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.
+ */
+ @Beta
+ 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.
+ */
+ @Beta
+ public int getUsedHostsPerRemoteDc() {
+ return usedHostsPerRemoteDc;
}
@Override
diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/HostFilterPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/HostFilterPolicy.java
index 8bb1a5e6573..d2546157202 100644
--- a/driver-core/src/main/java/com/datastax/driver/core/policies/HostFilterPolicy.java
+++ b/driver-core/src/main/java/com/datastax/driver/core/policies/HostFilterPolicy.java
@@ -19,6 +19,7 @@
import com.datastax.driver.core.Host;
import com.datastax.driver.core.HostDistance;
import com.datastax.driver.core.Statement;
+import com.google.common.annotations.Beta;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
@@ -26,6 +27,7 @@
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
+import java.util.Set;
/**
* A load balancing policy wrapper that ensures that only hosts matching the predicate will ever be
@@ -39,6 +41,7 @@
public class HostFilterPolicy implements ChainableLoadBalancingPolicy {
private final LoadBalancingPolicy childPolicy;
private final Predicate predicate;
+ private final ImmutableSet whiteListedDcs;
/**
* Create a new policy that wraps the provided child policy but only "allows" hosts matching the
@@ -49,8 +52,16 @@ public class HostFilterPolicy implements ChainableLoadBalancingPolicy {
* (whether they will get connected to or not depends on the child policy).
*/
public HostFilterPolicy(LoadBalancingPolicy childPolicy, Predicate predicate) {
+ this(childPolicy, predicate, ImmutableSet.of());
+ }
+
+ private HostFilterPolicy(
+ LoadBalancingPolicy childPolicy,
+ Predicate predicate,
+ ImmutableSet whiteListedDcs) {
this.childPolicy = childPolicy;
this.predicate = predicate;
+ this.whiteListedDcs = whiteListedDcs;
}
@Override
@@ -58,6 +69,24 @@ public LoadBalancingPolicy getChildPolicy() {
return childPolicy;
}
+ /**
+ * The datacenters this policy restricts the session to, or an empty set if that is not something
+ * it can say.
+ *
+ * A {@link Predicate} is opaque once built, so only {@link
+ * #fromDCWhiteList(LoadBalancingPolicy, Iterable)} populates this: it is the one factory whose
+ * argument is the set of allowed datacenters. An instance built from {@link
+ * #fromDCBlackList(LoadBalancingPolicy, Iterable)} or from a caller-supplied predicate returns an
+ * empty set — a denied datacenter names no preferred one, and an arbitrary predicate cannot be
+ * read back at all.
+ *
+ * @return an immutable set of allowed datacenter names, empty when unknown.
+ */
+ @Beta
+ public Set getWhiteListedDatacenters() {
+ return whiteListedDcs;
+ }
+
/**
* {@inheritDoc}
*
@@ -137,7 +166,8 @@ public void close() {
*/
public static HostFilterPolicy fromDCWhiteList(
LoadBalancingPolicy childPolicy, Iterable dcs) {
- return new HostFilterPolicy(childPolicy, hostDCPredicate(dcs, true));
+ ImmutableSet whiteListedDcs = ImmutableSet.copyOf(dcs);
+ return new HostFilterPolicy(childPolicy, hostDCPredicate(whiteListedDcs, true), whiteListedDcs);
}
/**
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..961b56b1290 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
@@ -8,11 +8,12 @@
import com.datastax.driver.core.Host;
import com.datastax.driver.core.HostDistance;
import com.datastax.driver.core.Statement;
+import com.google.common.annotations.Beta;
import java.util.Collection;
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 hosts;
@@ -20,6 +21,12 @@ public PagingOptimizingLoadBalancingPolicy(LoadBalancingPolicy loadBalancingPoli
wrapped = loadBalancingPolicy;
}
+ @Override
+ @Beta
+ public LoadBalancingPolicy getChildPolicy() {
+ return wrapped;
+ }
+
@Override
public void init(Cluster cluster, Collection hosts) {
this.hosts = new CopyOnWriteArrayList(hosts);
diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/PercentileSpeculativeExecutionPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/PercentileSpeculativeExecutionPolicy.java
index ecad11826c9..a7d7068b50e 100644
--- a/driver-core/src/main/java/com/datastax/driver/core/policies/PercentileSpeculativeExecutionPolicy.java
+++ b/driver-core/src/main/java/com/datastax/driver/core/policies/PercentileSpeculativeExecutionPolicy.java
@@ -22,6 +22,7 @@
import com.datastax.driver.core.LatencyTracker;
import com.datastax.driver.core.PercentileTracker;
import com.datastax.driver.core.Statement;
+import com.google.common.annotations.Beta;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -58,6 +59,28 @@ public PercentileSpeculativeExecutionPolicy(
this.maxSpeculativeExecutions = maxSpeculativeExecutions;
}
+ /**
+ * The maximum number of speculative executions this policy triggers for a request, as specified
+ * at instantiation. This does not include the initial, normal request.
+ *
+ * @return the maximum number of speculative executions, always strictly positive.
+ */
+ @Beta
+ public int getMaxSpeculativeExecutions() {
+ return maxSpeculativeExecutions;
+ }
+
+ /**
+ * The latency percentile a request must fall into to be considered slow, as specified at
+ * instantiation.
+ *
+ * @return the percentile, in the range 0 (inclusive) to 100 (exclusive).
+ */
+ @Beta
+ public double getPercentile() {
+ return percentile;
+ }
+
@Override
public SpeculativeExecutionPlan newPlan(String loggedKeyspace, Statement statement) {
return new SpeculativeExecutionPlan() {
diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/RackAwareRoundRobinPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/RackAwareRoundRobinPolicy.java
index ab3d019cbb8..04f4f4bf858 100644
--- a/driver-core/src/main/java/com/datastax/driver/core/policies/RackAwareRoundRobinPolicy.java
+++ b/driver-core/src/main/java/com/datastax/driver/core/policies/RackAwareRoundRobinPolicy.java
@@ -27,6 +27,7 @@
import com.datastax.driver.core.Host;
import com.datastax.driver.core.HostDistance;
import com.datastax.driver.core.Statement;
+import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
@@ -90,6 +91,8 @@ public static Builder builder() {
private final int usedHostsPerRemoteDc;
private final boolean dontHopForLocalCL;
+ private final boolean localDcExplicit;
+ private final boolean localRackExplicit;
private volatile Configuration configuration;
@@ -109,6 +112,56 @@ public RackAwareRoundRobinPolicy(
this.localRack = localRack == null ? UNSET : localRack;
this.usedHostsPerRemoteDc = usedHostsPerRemoteDc;
this.dontHopForLocalCL = !allowRemoteDCsForLocalConsistencyLevel;
+ this.localDcExplicit = !Strings.isNullOrEmpty(localDc);
+ this.localRackExplicit = !Strings.isNullOrEmpty(localRack);
+ }
+
+ /**
+ * 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}.
+ */
+ @Beta
+ public String getLocalDc() {
+ String dc = localDc;
+ return Strings.isNullOrEmpty(dc) ? null : dc;
+ }
+
+ /**
+ * The rack this policy considers local, or {@code null} if it has neither been configured
+ * explicitly nor inferred yet.
+ *
+ * @return the local rack name, or {@code null}.
+ */
+ @Beta
+ public String getLocalRack() {
+ String rack = localRack;
+ return Strings.isNullOrEmpty(rack) ? null : rack;
+ }
+
+ /**
+ * 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.
+ */
+ @Beta
+ public boolean isLocalDcExplicit() {
+ return localDcExplicit;
+ }
+
+ /**
+ * Whether the local rack was configured explicitly (as opposed to being inferred from the first
+ * contacted node).
+ *
+ * @return {@code true} if the local rack was set explicitly.
+ */
+ @Beta
+ public boolean isLocalRackExplicit() {
+ return localRackExplicit;
}
@Override
diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/TokenAwarePolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/TokenAwarePolicy.java
index afb7a526c75..ae138f9d8f1 100644
--- a/driver-core/src/main/java/com/datastax/driver/core/policies/TokenAwarePolicy.java
+++ b/driver-core/src/main/java/com/datastax/driver/core/policies/TokenAwarePolicy.java
@@ -35,6 +35,7 @@
import com.datastax.driver.core.QueryOptions;
import com.datastax.driver.core.Statement;
import com.datastax.driver.core.Token;
+import com.google.common.annotations.Beta;
import com.google.common.collect.AbstractIterator;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -392,6 +393,17 @@ public LoadBalancingPolicy getChildPolicy() {
return childPolicy;
}
+ /**
+ * The strategy this policy uses to order the replicas of a query's partition, as specified at
+ * instantiation.
+ *
+ * @return the replica ordering strategy.
+ */
+ @Beta
+ public ReplicaOrdering getReplicaOrdering() {
+ return replicaOrdering;
+ }
+
@Override
public void init(Cluster cluster, Collection hosts) {
clusterMetadata = cluster.getMetadata();
diff --git a/driver-core/src/test/java/com/datastax/driver/core/DefaultDriverConfigReporterTest.java b/driver-core/src/test/java/com/datastax/driver/core/DefaultDriverConfigReporterTest.java
index cabc6a591a5..f70dc56e886 100644
--- a/driver-core/src/test/java/com/datastax/driver/core/DefaultDriverConfigReporterTest.java
+++ b/driver-core/src/test/java/com/datastax/driver/core/DefaultDriverConfigReporterTest.java
@@ -16,11 +16,85 @@
package com.datastax.driver.core;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import com.datastax.driver.core.policies.ConstantReconnectionPolicy;
+import com.datastax.driver.core.policies.ConstantSpeculativeExecutionPolicy;
+import com.datastax.driver.core.policies.DCAwareRoundRobinPolicy;
+import com.datastax.driver.core.policies.DelegatingLoadBalancingPolicy;
+import com.datastax.driver.core.policies.DowngradingConsistencyRetryPolicy;
+import com.datastax.driver.core.policies.ErrorAwarePolicy;
+import com.datastax.driver.core.policies.FallthroughRetryPolicy;
+import com.datastax.driver.core.policies.HostFilterPolicy;
+import com.datastax.driver.core.policies.LatencyAwarePolicy;
+import com.datastax.driver.core.policies.LoadBalancingPolicy;
+import com.datastax.driver.core.policies.LoggingRetryPolicy;
+import com.datastax.driver.core.policies.PagingOptimizingLoadBalancingPolicy;
+import com.datastax.driver.core.policies.PercentileSpeculativeExecutionPolicy;
+import com.datastax.driver.core.policies.RackAwareRoundRobinPolicy;
+import com.datastax.driver.core.policies.ReconnectionPolicy;
+import com.datastax.driver.core.policies.RoundRobinPolicy;
+import com.datastax.driver.core.policies.SpeculativeExecutionPolicy;
+import com.datastax.driver.core.policies.TokenAwarePolicy;
+import com.datastax.driver.core.policies.WhiteListPolicy;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Predicate;
+import com.networknt.schema.JsonSchema;
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.SpecVersion;
+import com.networknt.schema.ValidationMessage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
import org.testng.annotations.Test;
public class DefaultDriverConfigReporterTest {
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ // The normative JSON Schema from the design doc, shipped verbatim as a test resource. Loaded once
+ // and pinned to draft 2020-12 (its declared $schema); its internal "#/$defs/..." refs resolve
+ // locally, so validation needs no network access.
+ //
+ // "v1" throughout is the report's "version" field, which the schema pins to 1. The design doc has
+ // been revised several times without bumping it — only a key whose meaning changes does that, and
+ // nothing has shipped yet — so the doc's revision is deliberately not a label here.
+ private static final JsonSchema SCHEMA = loadSchema();
+
+ // The one gap left between the 3.x configuration and the schema, carried only by a report of a
+ // PercentileSpeculativeExecutionPolicy configured with a percentile of 0: the policy accepts it
+ // and the schema's exclusiveMinimum does not. See should_report_a_zero_percentile_as_is and the
+ // reporter's class javadoc.
+ private static final String PERCENTILE_ZERO_GAP =
+ "$.query.speculative-execution.policy.percentile: must have an exclusive minimum value of 0";
+
+ private static final String SPECULATIVE_EXECUTION_POLICY_PATH =
+ "$.query.speculative-execution.policy";
+
+ private static JsonSchema loadSchema() {
+ try (InputStream in =
+ DefaultDriverConfigReporterTest.class.getResourceAsStream(
+ "/config/driver-config-report-v1.schema.json")) {
+ return JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)
+ .getSchema(MAPPER.readTree(in));
+ } catch (Exception e) {
+ throw new AssertionError("Cannot load the DRIVER_CONFIG v1 JSON Schema resource", e);
+ }
+ }
+
+ // ---- Stage 1: default / fail-safe -----------------------------------------------------------
+
private static Configuration config() {
return Configuration.builder().build();
}
@@ -37,10 +111,11 @@ public void should_enable_driver_config_reporting_by_default() {
}
@Test(groups = "unit")
- public void should_report_schema_version() {
- // Stage 1 emits only the schema version.
- assertThat(new DefaultDriverConfigReporter(config()).buildReport())
- .isEqualTo("{\"version\":1}");
+ public void should_report_schema_version_and_config_groups() throws Exception {
+ JsonNode report = MAPPER.readTree(new DefaultDriverConfigReporter(config()).buildReport());
+
+ assertThat(report.path("version").asInt()).isEqualTo(1);
+ assertThat(report.has("connection")).isTrue();
}
@Test(groups = "unit")
@@ -57,4 +132,1578 @@ protected String buildJson() {
// no DRIVER_CONFIG option is sent, and nothing else about the connection is affected.
assertThat(reporter.buildReport()).isNull();
}
+
+ @Test(groups = "unit")
+ public void should_be_fail_safe_when_report_build_throws_internal_error() {
+ DefaultDriverConfigReporter reporter =
+ new DefaultDriverConfigReporter(config()) {
+ @Override
+ protected String buildJson() {
+ // customPolicy() calls getClass().getSimpleName() on arbitrary user-supplied policy
+ // objects, which has a documented JDK edge case throwing InternalError for certain
+ // synthetic classes.
+ throw new InternalError("simulated getSimpleName() JDK edge case");
+ }
+ };
+
+ assertThat(reporter.buildReport()).isNull();
+ }
+
+ @Test(groups = "unit")
+ public void should_build_the_report_through_the_connection_factory_guard() throws Exception {
+ // Connection.Factory never touches DefaultDriverConfigReporter on the connection path: on a
+ // classpath without Jackson, initializing that class raises a LinkageError from its static
+ // ObjectMapper field -- an Error raised while initializing the class, so no fail-safe inside it
+ // could catch it, and the Cluster would fail to initialize at all rather than merely skip the
+ // report. The guard runs once, as the Cluster initializes, and decides whether any control
+ // connection may build a report at all; on a normal classpath it is transparent, which is what
+ // this pins.
+ assertThat(Connection.Factory.canBuildDriverConfigReport(Cluster.builder().getConfiguration()))
+ .isTrue();
+ }
+
+ @Test(groups = "unit")
+ public void should_skip_driver_config_when_it_exceeds_the_size_limit() {
+ // STARTUP option values are written with an unchecked 16-bit length prefix, so an oversized
+ // report would corrupt the frame and fail the handshake rather than merely be useless. Parts of
+ // the report come from unbounded user-supplied values (DC/rack names, consistency levels,
+ // custom policy class names), so the limit has to be enforced here.
+ assertThat(reporting(oversizedReport()).buildReport()).isNull();
+ }
+
+ @Test(groups = "unit")
+ public void should_return_driver_config_that_is_just_within_the_size_limit() {
+ String atLimit = padTo(DefaultDriverConfigReporter.MAX_DRIVER_CONFIG_LENGTH);
+
+ assertThat(reporting(atLimit).buildReport()).isEqualTo(atLimit);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_default_configuration_within_the_size_limit() {
+ // Tripwire: the real report is nowhere near the limit today. If it ever grows past it, this
+ // fails loudly instead of DRIVER_CONFIG silently disappearing from the wire.
+ String report =
+ new DefaultDriverConfigReporter(Cluster.builder().getConfiguration()).buildReport();
+
+ assertThat(report).isNotNull();
+ assertThat(report.getBytes(StandardCharsets.UTF_8).length)
+ .isLessThanOrEqualTo(DefaultDriverConfigReporter.MAX_DRIVER_CONFIG_LENGTH);
+ }
+
+ @Test(groups = "unit", timeOut = 30000)
+ public void should_not_follow_a_cyclic_load_balancing_policy_chain_forever() throws Exception {
+ // getChildPolicy() is walked on arbitrary user policies, so a cyclic chain would spin forever
+ // on the cluster-initialization path. That is the one failure mode the reporter's try/catch
+ // cannot contain, since it hangs rather than throws. The walk is bounded, so the report is
+ // still produced, describing the outermost policy.
+ JsonNode report =
+ report(Cluster.builder().withLoadBalancingPolicy(new CyclicLoadBalancingPolicy()));
+
+ assertThat(lbPolicy(report).path("type").asText()).isEqualTo("custom");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_name_the_configured_policy_not_the_internal_wrapper() throws Exception {
+ // Cluster.Manager wraps every configured policy in PagingOptimizingLoadBalancingPolicy, so
+ // naming the outermost policy of the chain would report that internal wrapper for every custom
+ // policy, telling an operator nothing about what the client actually runs.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new PagingOptimizingLoadBalancingPolicy(new CustomLoadBalancingPolicy())));
+
+ JsonNode policy = lbPolicy(report);
+ assertThat(policy.path("type").asText()).isEqualTo("custom");
+ assertThat(policy.path("name").asText()).isEqualTo("CustomLoadBalancingPolicy");
+ assertConformsToSchema(report);
+ }
+
+ // ---- Stage 2: the full report ----------------------------------------------------------------
+
+ @Test(groups = "unit")
+ public void should_report_default_configuration_shape() throws Exception {
+ JsonNode report = report(Cluster.builder());
+
+ assertThat(report.path("version").asInt()).isEqualTo(1);
+ // Everything hangs off exactly three groups: the schema keeps connection-scoped settings under
+ // "connection", control-connection ones under "control-plane", and everything acting on a query
+ // under "query".
+ assertThat(fieldNames(report)).containsOnly("version", "connection", "control-plane", "query");
+
+ // connection: connect + read only (no write timeout, no heartbeat in this schema version), plus
+ // the per-connection request capacity, the pool, the socket and the reconnection policy.
+ JsonNode connection = report.path("connection");
+ assertThat(connection.path("connect").path("timeout-ms").asInt()).isEqualTo(5000);
+ assertThat(connection.path("read").path("timeout-ms").asInt()).isEqualTo(12000);
+ assertThat(connection.has("write")).isFalse();
+ assertThat(connection.has("heartbeat")).isFalse();
+ // Effective v3+ default; "orphaned" has no 3.x equivalent and is omitted.
+ assertThat(connection.path("requests").path("in-flight").path("max").asInt()).isEqualTo(1024);
+ assertThat(connection.path("requests").has("orphaned")).isFalse();
+ // shard-aware is on by default via Cluster.Builder; the pool carries nothing else.
+ assertThat(connection.path("pool").path("shard-aware").path("enabled").asBoolean()).isTrue();
+ // The datacenter half of the node preference, which is what scopes pooling. Nothing is
+ // configured by default, and nothing is inferred before the policy is initialized, so the
+ // name is absent — but dc-auto is the accurate claim, the default policy being DC-aware.
+ assertThat(connection.path("node-preference").path("type").asText()).isEqualTo("dc-auto");
+ assertThat(connection.path("node-preference").has("local-dc")).isFalse();
+ // TLS is off by default, and the group is absent rather than reporting "off".
+ assertThat(connection.has("tls")).isFalse();
+
+ // connection.socket: booleans present; buffers/linger omitted when unset.
+ JsonNode socket = connection.path("socket");
+ assertThat(socket.path("tcp-no-delay").asBoolean()).isTrue();
+ assertThat(socket.path("keep-alive").asBoolean()).isFalse();
+ assertThat(socket.path("reuse-address").asBoolean()).isFalse();
+ assertThat(socket.has("linger")).isFalse();
+ assertThat(socket.has("receive-buffer")).isFalse();
+ assertThat(socket.has("send-buffer")).isFalse();
+
+ // connection.reconnection: exponential, unbounded (no max-attempts).
+ JsonNode reconnection = connection.path("reconnection").path("policy");
+ assertThat(reconnection.path("type").asText()).isEqualTo("exponential");
+ assertThat(reconnection.path("base-ms").asInt()).isEqualTo(1000);
+ assertThat(reconnection.path("max-ms").asInt()).isEqualTo(600000);
+ assertThat(reconnection.has("max-attempts")).isFalse();
+
+ // control-plane.
+ JsonNode systemTimeout =
+ report.path("control-plane").path("queries").path("system").path("timeout");
+ assertThat(systemTimeout.path("client-side-ms").asInt()).isEqualTo(12000);
+ assertThat(systemTimeout.has("server-side-ms")).isFalse();
+ assertThat(
+ report
+ .path("control-plane")
+ .path("schema")
+ .path("agreement")
+ .path("timeout-ms")
+ .asInt())
+ .isEqualTo(10000);
+
+ JsonNode query = report.path("query");
+
+ // query.defaults.
+ JsonNode defaults = query.path("defaults");
+ assertThat(defaults.path("page").path("size").asInt()).isEqualTo(5000);
+ assertThat(defaults.path("consistency").asText()).isEqualTo("LOCAL_ONE");
+ assertThat(defaults.path("serial-consistency").asText()).isEqualTo("SERIAL");
+ assertThat(defaults.path("idempotence").asBoolean()).isFalse();
+ assertThat(defaults.path("client-timestamps").asBoolean()).isTrue();
+ assertThat(defaults.path("request").path("timeout-ms").asInt()).isEqualTo(12000);
+
+ // query.retry: no built-in 3.x retry policy delays its attempts, so there is no backoff, and
+ // none bounds every one of its error paths, so there is no retry count either.
+ assertThat(query.path("retry").path("policy").path("type").asText())
+ .isEqualTo("standard-error-aware");
+ assertThat(query.path("retry").has("backoff")).isFalse();
+ assertThat(query.path("retry").path("policy").has("max-retries")).isFalse();
+
+ // query.speculative-execution: absent, since there is none by default.
+ assertThat(query.has("speculative-execution")).isFalse();
+
+ // query.load-balancing: the default is token-aware over DC-aware (auto DC), with RANDOM replica
+ // ordering.
+ JsonNode lb = query.path("load-balancing").path("policy");
+ assertThat(lb.path("type").asText()).isEqualTo("token-aware");
+ assertThat(lb.path("load-distribution").asText()).isEqualTo("shuffle");
+ assertThat(lb.path("fallback-to-non-preferred-nodes").asBoolean()).isFalse();
+ // Nothing reorders candidates at runtime, and the group's "signals" cannot be empty, so it is
+ // omitted rather than reported as disabled.
+ assertThat(lb.has("adaptive-ordering")).isFalse();
+
+ // query.load-balancing.node-preference: inferred DC, not yet resolved.
+ JsonNode nodePreference = query.path("load-balancing").path("node-preference");
+ assertThat(nodePreference.path("type").asText()).isEqualTo("dc-auto");
+ assertThat(nodePreference.has("local-dc")).isFalse();
+ }
+
+ @Test(groups = "unit")
+ public void should_report_constant_reconnection_policy() throws Exception {
+ JsonNode report =
+ report(Cluster.builder().withReconnectionPolicy(new ConstantReconnectionPolicy(2500)));
+
+ JsonNode reconnection = report.path("connection").path("reconnection").path("policy");
+ assertThat(reconnection.path("type").asText()).isEqualTo("constant");
+ assertThat(reconnection.path("delay-ms").asInt()).isEqualTo(2500);
+ assertThat(reconnection.has("max-attempts")).isFalse();
+ }
+
+ @Test(groups = "unit")
+ public void should_discriminate_retry_policies() throws Exception {
+ assertThat(retryPolicyType(Cluster.builder().withRetryPolicy(FallthroughRetryPolicy.INSTANCE)))
+ .isEqualTo("fallthrough");
+
+ JsonNode downgrading =
+ retryPolicy(
+ report(Cluster.builder().withRetryPolicy(DowngradingConsistencyRetryPolicy.INSTANCE)));
+ assertThat(downgrading.path("type").asText()).isEqualTo("downgrading-consistency");
+ // This policy stops after one attempt on the errors it downgrades, but tries the next host
+ // without a bound on request errors, so there is no single retry count to report.
+ assertThat(downgrading.has("max-retries")).isFalse();
+ }
+
+ @Test(groups = "unit")
+ public void should_report_constant_speculative_execution() throws Exception {
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withSpeculativeExecutionPolicy(new ConstantSpeculativeExecutionPolicy(100L, 2)));
+
+ JsonNode specEx = speculativeExecution(report);
+ assertThat(specEx.path("type").asText()).isEqualTo("constant");
+ assertThat(specEx.path("max-executions").asInt()).isEqualTo(2);
+ assertThat(specEx.path("delay-ms").asLong()).isEqualTo(100L);
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_percentile_speculative_execution() throws Exception {
+ JsonNode report = report(Cluster.builder().withSpeculativeExecutionPolicy(percentile(99.0)));
+
+ JsonNode specEx = speculativeExecution(report);
+ assertThat(specEx.path("type").asText()).isEqualTo("percentile");
+ assertThat(specEx.path("max-executions").asInt()).isEqualTo(3);
+ assertThat(specEx.path("percentile").asDouble()).isEqualTo(99.0);
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_a_custom_speculative_execution_policy_by_name() throws Exception {
+ JsonNode report =
+ report(Cluster.builder().withSpeculativeExecutionPolicy(new CustomSpeculativeExecution()));
+
+ JsonNode specEx = speculativeExecution(report);
+ assertThat(specEx.path("type").asText()).isEqualTo("custom");
+ assertThat(specEx.path("name").asText()).isEqualTo("CustomSpeculativeExecution");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_explicit_datacenter_node_location_preference() throws Exception {
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new TokenAwarePolicy(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build())));
+
+ JsonNode nodePreference = nodePreference(report);
+ assertThat(nodePreference.path("type").asText()).isEqualTo("dc");
+ assertThat(nodePreference.path("local-dc").asText()).isEqualTo("dc1");
+ assertThat(nodePreference.has("local-rack")).isFalse();
+ // The same preference scopes pooling, so it is reported under connection as well. A DC-aware
+ // policy has nothing beyond the datacenter, so the two slots agree exactly here.
+ assertThat(connectionNodePreference(report)).isEqualTo(nodePreference);
+ // token-aware wrapper is still reflected in the policy next to it.
+ assertThat(lbPolicy(report).path("type").asText()).isEqualTo("token-aware");
+ }
+
+ @Test(groups = "unit")
+ public void should_unwrap_paging_optimizing_load_balancing_policy() throws Exception {
+ // At runtime Cluster.Manager wraps the configured LB policy in a
+ // PagingOptimizingLoadBalancingPolicy, so the reporter must unwrap it to recover the real
+ // policy's flags and location preference (rather than reporting it as a custom policy).
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new PagingOptimizingLoadBalancingPolicy(
+ new TokenAwarePolicy(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build()))));
+
+ assertThat(lbPolicy(report).path("type").asText()).isEqualTo("token-aware");
+ assertThat(lbPolicy(report).path("load-distribution").asText()).isEqualTo("shuffle");
+ JsonNode nodePreference = nodePreference(report);
+ assertThat(nodePreference.path("type").asText()).isEqualTo("dc");
+ assertThat(nodePreference.path("local-dc").asText()).isEqualTo("dc1");
+ }
+
+ @Test(groups = "unit")
+ public void should_report_rack_node_location_preference() throws Exception {
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new RackAwareRoundRobinPolicy("dc1", "rack1", 0, false, false, false)));
+
+ JsonNode nodePreference = nodePreference(report);
+ assertThat(nodePreference.path("type").asText()).isEqualTo("rack");
+ assertThat(nodePreference.path("local-dc").asText()).isEqualTo("dc1");
+ assertThat(nodePreference.path("local-rack").asText()).isEqualTo("rack1");
+ // The rack never reaches the connection slot: a local-datacenter host in another rack
+ // is REMOTE, not IGNORED, so it is still pooled and the rack scopes no pooling at all.
+ // Reporting "rack" here would claim a restriction the driver does not apply.
+ JsonNode connectionPreference = connectionNodePreference(report);
+ assertThat(connectionPreference.path("type").asText()).isEqualTo("dc");
+ assertThat(connectionPreference.path("local-dc").asText()).isEqualTo("dc1");
+ assertThat(connectionPreference.has("local-rack")).isFalse();
+ // Not token-aware, so the policy itself can only be reported by name.
+ assertThat(lbPolicy(report).path("name").asText()).isEqualTo("RackAwareRoundRobinPolicy");
+ }
+
+ @Test(groups = "unit")
+ public void should_report_configured_and_inferred_rack_location_under_separate_keys()
+ throws Exception {
+ // The datacenter is configured and the rack is left to be inferred, which the schema reports as
+ // rack-auto with the configured part under local-dc — it admits only one key per part, and
+ // rejects a configured DC and rack together (that is the "rack" type).
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new RackAwareRoundRobinPolicy("dc1", null, 0, false, false, true)));
+
+ JsonNode nodePreference = nodePreference(report);
+ assertThat(nodePreference.path("type").asText()).isEqualTo("rack-auto");
+ assertThat(nodePreference.path("local-dc").asText()).isEqualTo("dc1");
+ assertThat(nodePreference.has("inferred-local-dc")).isFalse();
+ // Nothing has been inferred yet: the policy is only initialized once the cluster connects.
+ assertThat(nodePreference.has("local-rack")).isFalse();
+ assertThat(nodePreference.has("inferred-local-rack")).isFalse();
+ // The connection slot takes the configured datacenter, and the pending rack leaves no trace in
+ // it — rack-auto is a claim about routing only.
+ JsonNode connectionPreference = connectionNodePreference(report);
+ assertThat(connectionPreference.path("type").asText()).isEqualTo("dc");
+ assertThat(connectionPreference.path("local-dc").asText()).isEqualTo("dc1");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_the_datacenter_the_policy_has_already_inferred() throws Exception {
+ DCAwareRoundRobinPolicy policy = DCAwareRoundRobinPolicy.builder().build();
+ Cluster.Builder builder = Cluster.builder().withLoadBalancingPolicy(policy);
+
+ // Cluster.Manager.init() builds the Connection.Factory, and the first control connection sends
+ // its STARTUP, before it calls LoadBalancingPolicy#init -- so the report that first handshake
+ // carries can only say that a datacenter will be inferred, not which one.
+ JsonNode first = report(builder);
+ assertThat(nodePreference(first).path("type").asText()).isEqualTo("dc-auto");
+ assertThat(nodePreference(first).has("local-dc")).isFalse();
+ assertThat(connectionNodePreference(first).has("local-dc")).isFalse();
+ assertConformsToSchema(first);
+
+ initWithNode(policy, "dc-inferred", "rack1");
+
+ // Every later control connection builds its own report, so the datacenter the policy has since
+ // inferred reaches the server on the first reconnect. Nothing was cached in between.
+ JsonNode second = report(builder);
+ assertThat(nodePreference(second).path("type").asText()).isEqualTo("dc-auto");
+ assertThat(nodePreference(second).path("local-dc").asText()).isEqualTo("dc-inferred");
+ assertThat(connectionNodePreference(second).path("local-dc").asText()).isEqualTo("dc-inferred");
+ assertConformsToSchema(second);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_the_rack_the_policy_has_already_inferred() throws Exception {
+ // Same for a rack-aware policy, which infers both halves -- and this is the case that puts the
+ // schema's inferred-* keys on the wire at all: rack-auto names the inferred datacenter and rack
+ // under their own keys, so that a consumer can tell them from configured ones.
+ RackAwareRoundRobinPolicy policy =
+ new RackAwareRoundRobinPolicy(null, null, 0, false, true, true);
+ Cluster.Builder builder = Cluster.builder().withLoadBalancingPolicy(policy);
+
+ JsonNode first = report(builder);
+ assertThat(nodePreference(first).path("type").asText()).isEqualTo("rack-auto");
+ assertThat(nodePreference(first).has("inferred-local-dc")).isFalse();
+ assertThat(nodePreference(first).has("inferred-local-rack")).isFalse();
+ assertConformsToSchema(first);
+
+ initWithNode(policy, "dc-inferred", "rack-inferred");
+
+ JsonNode second = report(builder);
+ assertThat(nodePreference(second).path("type").asText()).isEqualTo("rack-auto");
+ assertThat(nodePreference(second).path("inferred-local-dc").asText()).isEqualTo("dc-inferred");
+ assertThat(nodePreference(second).path("inferred-local-rack").asText())
+ .isEqualTo("rack-inferred");
+ assertThat(nodePreference(second).has("local-dc")).isFalse();
+ assertThat(nodePreference(second).has("local-rack")).isFalse();
+ // The connection slot takes the datacenter half only, and reports an inferred one under the
+ // plain local-dc key -- dc-auto has no inferred- prefix, which is the schema's own asymmetry.
+ assertThat(connectionNodePreference(second).path("type").asText()).isEqualTo("dc-auto");
+ assertThat(connectionNodePreference(second).path("local-dc").asText()).isEqualTo("dc-inferred");
+ assertConformsToSchema(second);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_server_side_timestamps_as_disabled_client_timestamps()
+ throws Exception {
+ JsonNode report =
+ report(Cluster.builder().withTimestampGenerator(ServerSideTimestampGenerator.INSTANCE));
+
+ assertThat(queryDefaults(report).path("client-timestamps").asBoolean()).isFalse();
+ }
+
+ @Test(groups = "unit")
+ public void should_omit_client_timestamps_for_a_custom_timestamp_generator() throws Exception {
+ // next() returning Long.MIN_VALUE is documented as "let Cassandra generate the timestamp", and
+ // a custom generator decides that per call — so whether timestamps are assigned client-side is
+ // not a property of the configuration at all. Optional, and documented as absent exactly when
+ // the behavior is unknown, so it is omitted rather than guessed from the class.
+ JsonNode report = report(Cluster.builder().withTimestampGenerator(new CustomTimestamps()));
+
+ assertThat(queryDefaults(report).has("client-timestamps")).isFalse();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_client_timestamps_for_both_built_in_monotonic_generators()
+ throws Exception {
+ // Neither of the two can return Long.MIN_VALUE, so both always assign the timestamp
+ // client-side. The default is the atomic one, pinned by
+ // should_report_default_configuration_shape.
+ for (TimestampGenerator generator :
+ new TimestampGenerator[] {
+ new AtomicMonotonicTimestampGenerator(), new ThreadLocalMonotonicTimestampGenerator()
+ }) {
+ JsonNode report = report(Cluster.builder().withTimestampGenerator(generator));
+
+ assertThat(queryDefaults(report).path("client-timestamps").asBoolean()).isTrue();
+ assertConformsToSchema(report);
+ }
+ }
+
+ @Test(groups = "unit")
+ public void should_report_the_configured_page_size() throws Exception {
+ JsonNode report =
+ report(Cluster.builder().withQueryOptions(new QueryOptions().setFetchSize(1234)));
+
+ assertThat(queryDefaults(report).path("page").path("size").asInt()).isEqualTo(1234);
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_omit_the_page_size_when_paging_is_disabled() throws Exception {
+ // Integer.MAX_VALUE is how QueryOptions#setFetchSize documents "disable paging", and it is the
+ // only way a session can end up unpaged: the setter rejects anything <= 0. The schema says page
+ // is absent when paging is not limited, so the group goes rather than carrying the sentinel as
+ // if it were a page size somebody chose.
+ JsonNode report =
+ report(
+ Cluster.builder().withQueryOptions(new QueryOptions().setFetchSize(Integer.MAX_VALUE)));
+
+ assertThat(queryDefaults(report).has("page")).isFalse();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_a_serial_default_consistency() throws Exception {
+ // QueryOptions accepts a serial level as the default *request* consistency, which is legal for
+ // a serial read, and the schema's enum admits both of them — so this needs no special handling
+ // and the document stays valid. It used to be one of the gaps the reporter knowingly violated.
+ for (ConsistencyLevel level :
+ new ConsistencyLevel[] {ConsistencyLevel.SERIAL, ConsistencyLevel.LOCAL_SERIAL}) {
+ JsonNode report =
+ report(Cluster.builder().withQueryOptions(new QueryOptions().setConsistencyLevel(level)));
+
+ assertThat(queryDefaults(report).path("consistency").asText()).isEqualTo(level.name());
+ assertConformsToSchema(report);
+ }
+ }
+
+ @Test(groups = "unit")
+ public void should_omit_a_non_serial_default_serial_consistency() throws Exception {
+ // QueryOptions, unlike Statement, does not check that the level is serial, so a non-serial one
+ // reaches the reporter. serial-consistency is optional, so it is omitted rather than emitted as
+ // a value the schema's SERIAL/LOCAL_SERIAL enum rejects.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withQueryOptions(
+ new QueryOptions().setSerialConsistencyLevel(ConsistencyLevel.QUORUM)));
+
+ assertThat(queryDefaults(report).has("serial-consistency")).isFalse();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_reject_a_null_default_consistency() {
+ // Every query needs a level, so a null default fails any statement that does not set one of
+ // its own: RoundRobinPolicy and DCAwareRoundRobinPolicy call isDCLocal() on it while building
+ // a query plan. Rejecting it here turns a driver that breaks on its first query into one that
+ // breaks while being configured.
+ try {
+ new QueryOptions().setConsistencyLevel(null);
+ fail("Expected a NullPointerException");
+ } catch (NullPointerException e) {
+ assertThat(e).hasMessage("consistencyLevel cannot be null");
+ }
+ }
+
+ @Test(groups = "unit")
+ public void should_omit_a_default_consistency_that_a_subclass_nulls_out() throws Exception {
+ // With the setter rejecting null, only a QueryOptions subclass overriding the getter can still
+ // hand the reporter one. The key stays omitted even though the schema requires it — that beats
+ // losing every other group to an NPE caught by the fail-safe.
+ QueryOptions queryOptions =
+ new QueryOptions() {
+ @Override
+ public ConsistencyLevel getConsistencyLevel() {
+ return null;
+ }
+ };
+ JsonNode report = report(Cluster.builder().withQueryOptions(queryOptions));
+
+ assertThat(queryDefaults(report).has("consistency")).isFalse();
+ assertConformsToSchema(report, "$.query.defaults: required property 'consistency' not found");
+ }
+
+ @Test(groups = "unit")
+ public void should_report_tls_only_when_it_is_enabled() throws Exception {
+ // The group carries no "enabled" flag: its presence is what says TLS is on, so with TLS off it
+ // is omitted rather than reported as disabled.
+ assertThat(report(Cluster.builder()).path("connection").has("tls")).isFalse();
+
+ JsonNode tls = report(Cluster.builder().withSSL()).path("connection").path("tls");
+ assertThat(tls.isObject()).isTrue();
+ }
+
+ @Test(groups = "unit")
+ public void should_report_hostname_verification_for_sni_ssl_options() throws Exception {
+ // SniSSLOptions is the one 3.x SSLOptions that verifies the hostname, and it hard-codes it on,
+ // so it is the only case the reporter can state.
+ JsonNode report = report(Cluster.builder().withSSL(SniSSLOptions.builder().build()));
+
+ assertThat(report.path("connection").path("tls").path("hostname-verification").asBoolean())
+ .isTrue();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_omit_hostname_verification_it_cannot_observe() throws Exception {
+ // withSSL() installs RemoteEndpointAwareJdkSSLOptions, which builds its engine from a user
+ // SSLContext without touching endpoint identification; NettySSLOptions hands the whole handler
+ // to Netty. Neither can be read for it, so the key is omitted rather than asserted false — the
+ // schema documents it as absent exactly when the behavior is unknown. The group itself stays,
+ // since its presence is what reports that TLS is on.
+ JsonNode tls = report(Cluster.builder().withSSL()).path("connection").path("tls");
+
+ assertThat(tls.isObject()).isTrue();
+ assertThat(tls.has("hostname-verification")).isFalse();
+ assertThat(fieldNames(tls)).isEmpty();
+ }
+
+ @Test(groups = "unit")
+ public void should_report_socket_overrides() throws Exception {
+ SocketOptions socketOptions =
+ new SocketOptions()
+ .setKeepAlive(true)
+ .setReuseAddress(true)
+ .setSoLinger(15)
+ .setReceiveBufferSize(4096)
+ .setSendBufferSize(8192);
+
+ JsonNode socket = socket(report(Cluster.builder().withSocketOptions(socketOptions)));
+
+ assertThat(socket.path("keep-alive").asBoolean()).isTrue();
+ assertThat(socket.path("reuse-address").asBoolean()).isTrue();
+ assertThat(socket.path("linger").path("interval-s").asInt()).isEqualTo(15);
+ assertThat(socket.path("receive-buffer").path("size-bytes").asInt()).isEqualTo(4096);
+ assertThat(socket.path("send-buffer").path("size-bytes").asInt()).isEqualTo(8192);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_fallback_to_non_preferred_nodes_when_remote_hosts_are_used()
+ throws Exception {
+ // Leaving the preferred datacenter is the only way a DC-aware policy reaches a node outside
+ // the reported node-preference, so the flag follows usedHostsPerRemoteDc. It needs a
+ // token-aware wrapper to be reported at all: that is the only built-in shape the schema
+ // defines, and the only one carrying the flag. The false case is pinned by
+ // should_report_default_configuration_shape.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new TokenAwarePolicy(
+ DCAwareRoundRobinPolicy.builder()
+ .withLocalDc("dc1")
+ .withUsedHostsPerRemoteDc(2)
+ .build())));
+
+ assertThat(lbPolicy(report).path("fallback-to-non-preferred-nodes").asBoolean()).isTrue();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_always_report_fallback_to_non_preferred_nodes_for_a_rack_aware_policy()
+ throws Exception {
+ // A rack-aware policy reports a rack preference, and the other racks of its local datacenter
+ // are outside it yet are the second tier of every query plan — distance() returns REMOTE, not
+ // IGNORED, for them. So the flag is true even with no remote datacenter host configured, which
+ // is the default.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new TokenAwarePolicy(
+ new RackAwareRoundRobinPolicy("dc1", "rack1", 0, false, false, false))));
+
+ assertThat(nodePreference(report).path("type").asText()).isEqualTo("rack");
+ assertThat(lbPolicy(report).path("fallback-to-non-preferred-nodes").asBoolean()).isTrue();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_fallback_to_non_preferred_nodes_as_false_without_a_preference()
+ throws Exception {
+ // A token-aware policy over a child with no locality notion has no preference to report, so the
+ // flag has nothing to be relative to: the schema defines it against
+ // query.load-balancing.node-preference, and that key is absent here. The flag is required on
+ // the
+ // token-aware branch, so it has to carry some value; false is reported because RoundRobinPolicy
+ // has no tiering to fall back *from*, not because anything is restricted. Pinned so the pairing
+ // cannot change silently while it is an open question for the schema owner.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(new TokenAwarePolicy(new RoundRobinPolicy())));
+
+ assertThat(lbPolicy(report).path("type").asText()).isEqualTo("token-aware");
+ assertThat(lbPolicy(report).path("fallback-to-non-preferred-nodes").asBoolean()).isFalse();
+ assertThat(loadBalancing(report).has("node-preference")).isFalse();
+ assertThat(report.path("connection").has("node-preference")).isFalse();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_the_configured_in_flight_request_limit() throws Exception {
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withPoolingOptions(
+ new PoolingOptions().setMaxRequestsPerConnection(HostDistance.LOCAL, 512)));
+
+ assertThat(report.path("connection").path("requests").path("in-flight").path("max").asInt())
+ .isEqualTo(512);
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_a_configured_zero_in_flight_request_limit_as_is() throws Exception {
+ // PoolingOptions rejects only negatives, so 0 is a limit an operator can set deliberately.
+ // Falling back to the protocol default for it would misreport the pool; the key is required and
+ // positive-only, so it is reported as-is and shows up as a violation instead. The other end
+ // needs no such treatment — see should_report_the_maximal_in_flight_request_limit.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withPoolingOptions(
+ new PoolingOptions().setMaxRequestsPerConnection(HostDistance.LOCAL, 0)));
+
+ assertThat(report.path("connection").path("requests").path("in-flight").path("max").asInt())
+ .isEqualTo(0);
+ assertConformsToSchema(
+ report, "$.connection.requests.in-flight.max: must have a minimum value of 1");
+ }
+
+ @Test(groups = "unit")
+ public void should_report_the_maximal_in_flight_request_limit() throws Exception {
+ // The largest limit PoolingOptions accepts is the 32768 stream identifiers protocol v3
+ // provides, and the schema bounds the key only from below, so the largest legal configuration
+ // is still a valid document.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withPoolingOptions(
+ new PoolingOptions().setMaxRequestsPerConnection(HostDistance.LOCAL, 32768)));
+
+ assertThat(report.path("connection").path("requests").path("in-flight").path("max").asInt())
+ .isEqualTo(32768);
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_the_in_flight_request_limit_of_the_pinned_protocol_version()
+ throws Exception {
+ // PoolingOptions is still UNSET when the report is built, so the limit comes from the DEFAULTS
+ // row the pools will eventually be sized from. That row is not always the v3 one: a user who
+ // pinned the protocol version gets the highest row not above it, and DEFAULTS only has v1 and
+ // v3
+ // entries, so a v2 cluster is sized from v1's 128. Pinning the version is the one part of
+ // negotiation that is knowable at report time.
+ JsonNode report = report(Cluster.builder().withProtocolVersion(ProtocolVersion.V2));
+
+ assertThat(report.path("connection").path("requests").path("in-flight").path("max").asInt())
+ .isEqualTo(128);
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_omit_the_orphaned_request_limit_it_has_no_equivalent_for() throws Exception {
+ // A request the 3.x driver stopped waiting for keeps its stream identifier until the response
+ // arrives; there is no configurable bound on those and no connection replacement, so there is
+ // nothing to report. The group is optional as of the schema revision that added "absent only
+ // when this bound is unknown", so omitting it is now the schema-valid answer rather than the
+ // one violation every report carried — pinned here so the key cannot silently come back.
+ JsonNode report = report(Cluster.builder());
+
+ JsonNode requests = report.path("connection").path("requests");
+ assertThat(requests.has("in-flight")).isTrue();
+ assertThat(requests.has("orphaned")).isFalse();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_load_distribution_from_the_replica_ordering() throws Exception {
+ assertThat(loadDistribution(TokenAwarePolicy.ReplicaOrdering.RANDOM)).isEqualTo("shuffle");
+ assertThat(loadDistribution(TokenAwarePolicy.ReplicaOrdering.TOPOLOGICAL))
+ .isEqualTo("replica-set");
+ // NEUTRAL keeps the child policy's plan order, which for every built-in child rotates the first
+ // host across successive query plans.
+ assertThat(loadDistribution(TokenAwarePolicy.ReplicaOrdering.NEUTRAL)).isEqualTo("round-robin");
+ }
+
+ @Test(groups = "unit")
+ public void should_report_latency_awareness_as_adaptive_ordering() throws Exception {
+ // The wrapper contributes a capability, not a type: the type still comes from the token-aware
+ // policy it wraps.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ LatencyAwarePolicy.builder(
+ new TokenAwarePolicy(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build()))
+ .build()));
+
+ JsonNode lb = lbPolicy(report);
+ assertThat(lb.path("type").asText()).isEqualTo("token-aware");
+ JsonNode adaptiveOrdering = lb.path("adaptive-ordering");
+ // The group has no "enabled" flag; its presence is what says candidates get reordered, and its
+ // signals cannot be empty.
+ assertThat(fieldNames(adaptiveOrdering)).containsOnly("signals");
+ assertThat(adaptiveOrdering.path("signals").size()).isEqualTo(1);
+ assertThat(adaptiveOrdering.path("signals").get(0).asText()).isEqualTo("latency");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_a_built_in_policy_that_is_not_token_aware_as_custom() throws Exception {
+ // "token-aware" is the only built-in shape the schema defines, so any other policy can only be
+ // reported by name — its datacenter still shows up in the node preference beside it.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build()));
+
+ JsonNode lb = lbPolicy(report);
+ assertThat(lb.path("type").asText()).isEqualTo("custom");
+ assertThat(lb.path("name").asText()).isEqualTo("DCAwareRoundRobinPolicy");
+ assertThat(nodePreference(report).path("local-dc").asText()).isEqualTo("dc1");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_keep_reporting_token_aware_for_a_fully_representable_chain() throws Exception {
+ // The default chain, and every chain built only from policies the group can describe: token
+ // awareness is claimed, and no name appears -- the built-in shape has no room for one.
+ for (LoadBalancingPolicy policy :
+ new LoadBalancingPolicy[] {
+ new TokenAwarePolicy(DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build()),
+ new TokenAwarePolicy(new RoundRobinPolicy()),
+ LatencyAwarePolicy.builder(
+ new TokenAwarePolicy(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build()))
+ .build(),
+ // The internal wrapper Cluster.Manager adds describes nothing, so it does not cost the
+ // chain its built-in shape any more than it earns a mention in a name.
+ new PagingOptimizingLoadBalancingPolicy(
+ new TokenAwarePolicy(DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build())),
+ // A filter naming one datacenter is described by the node preference it produces.
+ HostFilterPolicy.fromDCWhiteList(
+ new TokenAwarePolicy(new RoundRobinPolicy()), Collections.singletonList("dc1"))
+ }) {
+ JsonNode report = report(Cluster.builder().withLoadBalancingPolicy(policy));
+
+ assertThat(lbPolicy(report).path("type").asText()).as("%s", policy).isEqualTo("token-aware");
+ assertThat(lbPolicy(report).has("name")).as("%s", policy).isFalse();
+ assertConformsToSchema(report);
+ }
+ }
+
+ @Test(groups = "unit")
+ public void should_name_a_restricting_wrapper_over_a_token_aware_chain() throws Exception {
+ // The bug this closes: an outer restriction used to vanish behind an inner TokenAwarePolicy, so
+ // whether an operator could see that the client is pinned to a host list depended on an
+ // unrelated nesting choice -- WhiteListPolicy(RoundRobinPolicy) reported the wrapper by name
+ // while WhiteListPolicy(TokenAwarePolicy(...)) reported plain token-aware. Now the chain that
+ // cannot be described as a built-in says so, and names every policy in it.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new WhiteListPolicy(
+ new TokenAwarePolicy(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build()),
+ Collections.singletonList(new InetSocketAddress("127.0.0.1", 9042)))));
+
+ JsonNode lb = lbPolicy(report);
+ assertThat(lb.path("type").asText()).isEqualTo("custom");
+ assertThat(lb.path("name").asText())
+ .isEqualTo("WhiteListPolicy(TokenAwarePolicy(DCAwareRoundRobinPolicy))");
+ // Naming the wrapper costs nothing that was reported before it: what the chain can still state
+ // about itself stays, as additional properties the custom branch admits.
+ assertThat(lb.path("load-distribution").asText()).isEqualTo("shuffle");
+ assertThat(lb.path("fallback-to-non-preferred-nodes").asBoolean()).isFalse();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_name_an_error_aware_wrapper_that_has_no_other_representation()
+ throws Exception {
+ // ErrorAwarePolicy excludes hosts over an error-rate threshold -- a restriction, not a
+ // reordering -- so adaptive-ordering would be the wrong home for it despite its inviting
+ // response-rate signal. The name is the only place it can appear, and now it does.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ ErrorAwarePolicy.builder(
+ new TokenAwarePolicy(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build()))
+ .build()));
+
+ JsonNode lb = lbPolicy(report);
+ assertThat(lb.path("type").asText()).isEqualTo("custom");
+ assertThat(lb.path("name").asText())
+ .isEqualTo("ErrorAwarePolicy(TokenAwarePolicy(DCAwareRoundRobinPolicy))");
+ assertThat(lb.has("adaptive-ordering")).isFalse();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_an_anonymous_custom_policy_under_its_binary_name() throws Exception {
+ // An anonymous class has no simple name, and the schema requires a non-empty one.
+ JsonNode report =
+ report(Cluster.builder().withLoadBalancingPolicy(new CustomLoadBalancingPolicy() {}));
+
+ assertThat(lbPolicy(report).path("name").asText())
+ .startsWith(DefaultDriverConfigReporterTest.class.getName() + "$");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_omit_every_field_a_disabled_read_timeout_feeds() throws Exception {
+ // A non-positive read timeout disables read timeouts. It feeds three places, all optional and
+ // positive-only, so all three are omitted rather than emitted as a value the schema rejects.
+ JsonNode report =
+ report(Cluster.builder().withSocketOptions(new SocketOptions().setReadTimeoutMillis(0)));
+
+ assertThat(report.path("connection").has("read")).isFalse();
+ // The enclosing "timeout" object here is required, so it stays behind, empty.
+ JsonNode systemTimeout =
+ report.path("control-plane").path("queries").path("system").path("timeout");
+ assertThat(systemTimeout.isObject()).isTrue();
+ assertThat(systemTimeout.has("client-side-ms")).isFalse();
+ // "request" is optional, so the whole group goes rather than being reported empty.
+ assertThat(queryDefaults(report).has("request")).isFalse();
+
+ // Nothing beyond the gap every report carries: making the request timeout and its group
+ // optional
+ // removed the one violation this configuration used to add.
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_omit_a_disabled_connect_timeout() throws Exception {
+ // Non-positive means "no timeout". The timeout is optional now, so it is omitted rather than
+ // reported as a value the schema rejects; its enclosing group is required, so it stays (empty).
+ JsonNode connect =
+ report(Cluster.builder().withSocketOptions(new SocketOptions().setConnectTimeoutMillis(0)))
+ .path("connection")
+ .path("connect");
+
+ assertThat(connect.isObject()).isTrue();
+ assertThat(connect.has("timeout-ms")).isFalse();
+ }
+
+ @Test(groups = "unit")
+ public void should_omit_socket_values_the_schema_cannot_express() throws Exception {
+ // A negative SO_LINGER disables lingering close and a non-positive buffer size leaves the
+ // JDK/OS default in place; all three groups are optional, so they are omitted.
+ JsonNode socket =
+ socket(
+ report(
+ Cluster.builder()
+ .withSocketOptions(
+ new SocketOptions()
+ .setSoLinger(-1)
+ .setReceiveBufferSize(0)
+ .setSendBufferSize(-1))));
+
+ assertThat(socket.has("linger")).isFalse();
+ assertThat(socket.has("receive-buffer")).isFalse();
+ assertThat(socket.has("send-buffer")).isFalse();
+ }
+
+ @Test(groups = "unit")
+ public void should_report_a_zero_linger_interval() throws Exception {
+ // 0 means "close immediately, discarding unsent data", which the schema does admit.
+ JsonNode report =
+ report(Cluster.builder().withSocketOptions(new SocketOptions().setSoLinger(0)));
+
+ assertThat(socket(report).path("linger").path("interval-s").asInt()).isEqualTo(0);
+ assertConformsToSchema(report);
+ }
+
+ // ==================== Schema conformance ====================
+ //
+ // These build a config, serialize it via the reporter, and validate the produced JSON against the
+ // normative JSON Schema (the same document ScyllaDB uses to interpret DRIVER_CONFIG). They cover
+ // every discriminated-union branch and optional-group case the 3.x reporter can emit, turning the
+ // "no emitted document violates the schema beyond the documented gaps" invariant into an enforced
+ // test.
+ //
+ // There is one documented gap left, PERCENTILE_ZERO_GAP, which only a percentile of 0 provokes;
+ // see the reporter's class javadoc and should_report_a_zero_percentile_as_is, which pins it.
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_default_report() throws Exception {
+ assertConformsToSchema(report(Cluster.builder()));
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_constant_reconnection_policy() throws Exception {
+ assertConformsToSchema(
+ report(Cluster.builder().withReconnectionPolicy(new ConstantReconnectionPolicy(2500))));
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_zero_delay_constant_reconnection_policy()
+ throws Exception {
+ // A zero delay means "reconnect immediately", which the schema admits.
+ JsonNode report =
+ report(Cluster.builder().withReconnectionPolicy(new ConstantReconnectionPolicy(0)));
+
+ assertThat(
+ report.path("connection").path("reconnection").path("policy").path("delay-ms").asInt())
+ .isEqualTo(0);
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_a_disabled_connect_timeout() throws Exception {
+ assertConformsToSchema(
+ report(
+ Cluster.builder().withSocketOptions(new SocketOptions().setConnectTimeoutMillis(0))));
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_custom_reconnection_policy() throws Exception {
+ assertConformsToSchema(
+ report(Cluster.builder().withReconnectionPolicy(new CustomReconnectionPolicy())));
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_custom_retry_policy() throws Exception {
+ assertConformsToSchema(
+ report(
+ Cluster.builder()
+ .withRetryPolicy(new LoggingRetryPolicy(FallthroughRetryPolicy.INSTANCE))));
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_downgrading_consistency_retry_policy() throws Exception {
+ assertConformsToSchema(
+ report(Cluster.builder().withRetryPolicy(DowngradingConsistencyRetryPolicy.INSTANCE)));
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_fallthrough_retry_policy() throws Exception {
+ assertConformsToSchema(
+ report(Cluster.builder().withRetryPolicy(FallthroughRetryPolicy.INSTANCE)));
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_a_zero_delay_constant_speculative_execution()
+ throws Exception {
+ // A zero delay means every extra execution is sent immediately, which the schema's
+ // nonNegativeInteger admits.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withSpeculativeExecutionPolicy(new ConstantSpeculativeExecutionPolicy(0L, 1)));
+
+ assertThat(speculativeExecution(report).path("delay-ms").asLong()).isEqualTo(0L);
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_a_zero_percentile_as_is() throws Exception {
+ // PercentileSpeculativeExecutionPolicy accepts a percentile of 0, which the schema's
+ // exclusiveMinimum rejects, and the key is required by the percentile branch. Reported as-is;
+ // see the reporter's class javadoc.
+ JsonNode report = report(Cluster.builder().withSpeculativeExecutionPolicy(percentile(0.0)));
+
+ assertThat(speculativeExecution(report).path("percentile").asDouble()).isEqualTo(0.0);
+
+ Set violations = violations(report);
+ assertThat(violations).contains(PERCENTILE_ZERO_GAP);
+ // Failing the percentile branch drops the object out of the discriminated union, so the
+ // validator goes on to explain why it matches neither of the other two branches either. Those
+ // messages all follow from this one gap, so rather than pin the cascade, assert that it stays
+ // inside this one object — nothing else in the report is affected.
+ for (String violation : violations) {
+ assertThat(violation).startsWith(SPECULATIVE_EXECUTION_POLICY_PATH);
+ }
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_explicit_datacenter_node_location() throws Exception {
+ assertConformsToSchema(
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new TokenAwarePolicy(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build()))));
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_rack_node_location() throws Exception {
+ assertConformsToSchema(
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new RackAwareRoundRobinPolicy("dc1", "rack1", 0, false, false, false))));
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_inferred_rack_node_location() throws Exception {
+ // Neither DC nor rack configured: reported as rack-auto, with both names absent because the
+ // policy has not been initialized (and so has inferred nothing) at report time.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new RackAwareRoundRobinPolicy(null, null, 0, false, true, true)));
+
+ assertThat(nodePreference(report).path("type").asText()).isEqualTo("rack-auto");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_configured_rack_and_inferred_datacenter()
+ throws Exception {
+ // The mirror of should_report_configured_and_inferred_rack_location_under_separate_keys: the
+ // configured part is the rack this time, which exercises the schema's "not local-dc together
+ // with local-rack" guard from the other side.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new RackAwareRoundRobinPolicy(null, "rack1", 0, false, true, false)));
+
+ JsonNode nodePreference = nodePreference(report);
+ assertThat(nodePreference.path("type").asText()).isEqualTo("rack-auto");
+ assertThat(nodePreference.path("local-rack").asText()).isEqualTo("rack1");
+ assertThat(nodePreference.has("local-dc")).isFalse();
+ // The connection slot degrades to dc-auto with no name: the configured rack says nothing about
+ // which datacenter is pooled, and the datacenter itself is still to be inferred.
+ JsonNode connectionPreference = connectionNodePreference(report);
+ assertThat(connectionPreference.path("type").asText()).isEqualTo("dc-auto");
+ assertThat(connectionPreference.has("local-dc")).isFalse();
+ assertThat(connectionPreference.has("local-rack")).isFalse();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_round_robin_load_balancing_policy() throws Exception {
+ JsonNode report = report(Cluster.builder().withLoadBalancingPolicy(new RoundRobinPolicy()));
+
+ assertThat(lbPolicy(report).path("name").asText()).isEqualTo("RoundRobinPolicy");
+ // No DC/rack notion at all, so neither of the schema's two node preference keys is reported.
+ assertThat(loadBalancing(report).has("node-preference")).isFalse();
+ assertThat(report.path("connection").has("node-preference")).isFalse();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_white_list_load_balancing_policy() throws Exception {
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new WhiteListPolicy(
+ new RoundRobinPolicy(),
+ Collections.singletonList(new InetSocketAddress("127.0.0.1", 9042)))));
+
+ assertThat(lbPolicy(report).path("name").asText())
+ .isEqualTo("WhiteListPolicy(RoundRobinPolicy)");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_the_preferred_datacenter_under_a_filtering_wrapper() throws Exception {
+ // A filtering wrapper narrows pooling below the datacenter reported beside it: WhiteListPolicy
+ // extends HostFilterPolicy, whose distance() returns IGNORED for any host failing the
+ // predicate, including one inside dc1. The configured datacenter is reported anyway -- hiding a
+ // setting the operator really did make is the worse failure mode -- so this pins the
+ // approximation the class javadoc documents rather than leaving the shape to change silently.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new WhiteListPolicy(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build(),
+ Collections.singletonList(new InetSocketAddress("127.0.0.1", 9042)))));
+
+ // The whitelist has no home in the schema's built-in shape, so the chain cannot claim it; the
+ // name says the whole of what the client runs rather than only its outermost policy.
+ assertThat(lbPolicy(report).path("type").asText()).isEqualTo("custom");
+ assertThat(lbPolicy(report).path("name").asText())
+ .isEqualTo("WhiteListPolicy(DCAwareRoundRobinPolicy)");
+ // Both preference slots still carry the datacenter of the DC-aware child inside the chain.
+ assertThat(nodePreference(report).path("type").asText()).isEqualTo("dc");
+ assertThat(nodePreference(report).path("local-dc").asText()).isEqualTo("dc1");
+ assertThat(connectionNodePreference(report).path("type").asText()).isEqualTo("dc");
+ assertThat(connectionNodePreference(report).path("local-dc").asText()).isEqualTo("dc1");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_host_filter_load_balancing_policy() throws Exception {
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ HostFilterPolicy.fromDCWhiteList(
+ new RoundRobinPolicy(), Collections.singletonList("dc1"))));
+
+ assertThat(lbPolicy(report).path("name").asText())
+ .isEqualTo("HostFilterPolicy(RoundRobinPolicy)");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_report_the_datacenter_a_filter_restricts_the_session_to() throws Exception {
+ // fromDCWhiteList over a RoundRobinPolicy is the one restriction the driver ships that names a
+ // datacenter without any policy in the chain preferring one. Its distance() returns IGNORED for
+ // every host outside dc1, so the restriction really is what decides which hosts are pooled and
+ // which appear in a query plan -- both preference slots, in schema terms.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ HostFilterPolicy.fromDCWhiteList(
+ new RoundRobinPolicy(), Collections.singletonList("dc1"))));
+
+ assertThat(nodePreference(report).path("type").asText()).isEqualTo("dc");
+ assertThat(nodePreference(report).path("local-dc").asText()).isEqualTo("dc1");
+ assertThat(connectionNodePreference(report).path("type").asText()).isEqualTo("dc");
+ assertThat(connectionNodePreference(report).path("local-dc").asText()).isEqualTo("dc1");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_prefer_a_location_aware_policy_over_the_filter_above_it() throws Exception {
+ // The filter and the policy disagree here, which is a misconfiguration -- but the policy is
+ // what builds the query plan, so its datacenter is the one reported. The filter is only ever
+ // consulted when nothing in the chain prefers a datacenter of its own.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ HostFilterPolicy.fromDCWhiteList(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build(),
+ Collections.singletonList("dc2"))));
+
+ assertThat(nodePreference(report).path("local-dc").asText()).isEqualTo("dc1");
+ assertThat(connectionNodePreference(report).path("local-dc").asText()).isEqualTo("dc1");
+ // The filter names a datacenter, but not the one reported -- so it restricts the session
+ // without being stated anywhere, and the chain cannot claim to be fully described. Pinned
+ // beside the token-aware case below, which is the same chain with one more wrapper.
+ assertThat(lbPolicy(report).path("type").asText()).isEqualTo("custom");
+ assertThat(lbPolicy(report).path("name").asText())
+ .isEqualTo("HostFilterPolicy(DCAwareRoundRobinPolicy)");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_name_a_datacenter_filter_that_disagrees_with_the_policy_below_it()
+ throws Exception {
+ // The chain of should_prefer_a_location_aware_policy_over_the_filter_above_it plus a
+ // token-aware wrapper, which must not make the filter disappear. A filter is only described by
+ // the node preference when that preference is *its* restriction; here the DC-aware policy owns
+ // it, so the whitelist narrows the session on top of what is reported -- to a datacenter
+ // disjoint from it, in fact, leaving nothing reachable at all. Claiming the built-in
+ // token-aware shape would say none of that, and would say it only because of the wrapper: the
+ // same nesting-dependent blind spot should_name_a_restricting_wrapper_over_a_token_aware_chain
+ // closes for WhiteListPolicy.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new TokenAwarePolicy(
+ HostFilterPolicy.fromDCWhiteList(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build(),
+ Collections.singletonList("dc2")))));
+
+ JsonNode lb = lbPolicy(report);
+ assertThat(lb.path("type").asText()).isEqualTo("custom");
+ assertThat(lb.path("name").asText())
+ .isEqualTo("TokenAwarePolicy(HostFilterPolicy(DCAwareRoundRobinPolicy))");
+ // What the chain can still state about itself stays, as additional properties.
+ assertThat(lb.path("load-distribution").asText()).isEqualTo("shuffle");
+ assertThat(lb.path("fallback-to-non-preferred-nodes").asBoolean()).isFalse();
+ // The preference is still the policy's, which is what builds the query plan.
+ assertThat(nodePreference(report).path("local-dc").asText()).isEqualTo("dc1");
+ assertThat(connectionNodePreference(report).path("local-dc").asText()).isEqualTo("dc1");
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_omit_the_node_preference_for_restrictions_that_name_no_single_datacenter()
+ throws Exception {
+ // Everything a filter can express other than "exactly these hosts, in exactly this one
+ // datacenter". A blacklist denies a datacenter without naming a preferred one; two allowed
+ // datacenters name no single one; a WhiteListPolicy filters on addresses, and a caller-supplied
+ // predicate on whatever it likes. None of those has a schema-valid form, so the group goes.
+ // Nor does a blank name: nothing validates the strings fromDCWhiteList is handed, and local-dc
+ // is a non-empty string the "dc" type requires -- so the whole group goes rather than one key,
+ // the same way the DC- and rack-aware policies treat a blank name of their own.
+ LoadBalancingPolicy[] policies = {
+ HostFilterPolicy.fromDCWhiteList(new RoundRobinPolicy(), Arrays.asList("dc1", "dc2")),
+ HostFilterPolicy.fromDCBlackList(new RoundRobinPolicy(), Collections.singletonList("dc2")),
+ HostFilterPolicy.fromDCWhiteList(new RoundRobinPolicy(), Collections.singletonList("")),
+ new HostFilterPolicy(
+ new RoundRobinPolicy(),
+ new Predicate() {
+ @Override
+ public boolean apply(Host host) {
+ return true;
+ }
+ }),
+ new WhiteListPolicy(
+ new RoundRobinPolicy(),
+ Collections.singletonList(new InetSocketAddress("127.0.0.1", 9042)))
+ };
+
+ for (LoadBalancingPolicy policy : policies) {
+ JsonNode report = report(Cluster.builder().withLoadBalancingPolicy(policy));
+
+ assertThat(loadBalancing(report).has("node-preference")).as("%s", policy).isFalse();
+ assertThat(report.path("connection").has("node-preference")).as("%s", policy).isFalse();
+ assertConformsToSchema(report);
+ }
+ }
+
+ @Test(groups = "unit")
+ public void should_report_adaptive_ordering_without_token_awareness() throws Exception {
+ // Adaptive ordering is a property of the chain, not of token awareness: LatencyAwarePolicy
+ // reorders the candidates its child produces whatever that child is, so it is reported over a
+ // bare RoundRobinPolicy exactly as it is over a token-aware policy. The custom branch admits
+ // additional properties, which is what lets the capability be stated next to the name.
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ LatencyAwarePolicy.builder(new RoundRobinPolicy()).build()));
+
+ JsonNode lb = lbPolicy(report);
+ assertThat(lb.path("type").asText()).isEqualTo("custom");
+ assertThat(lb.path("name").asText()).isEqualTo("LatencyAwarePolicy(RoundRobinPolicy)");
+ assertThat(lb.path("adaptive-ordering").path("signals").get(0).asText()).isEqualTo("latency");
+ // No token-aware policy in the chain, so neither key that describes one is claimed.
+ assertThat(lb.has("load-distribution")).isFalse();
+ assertThat(lb.has("fallback-to-non-preferred-nodes")).isFalse();
+ assertConformsToSchema(report);
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_unwrapped_paging_optimizing_load_balancing_policy()
+ throws Exception {
+ assertConformsToSchema(
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new PagingOptimizingLoadBalancingPolicy(
+ new TokenAwarePolicy(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build())))));
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_tls_enabled() throws Exception {
+ assertConformsToSchema(report(Cluster.builder().withSSL()));
+ }
+
+ @Test(groups = "unit")
+ public void should_conform_to_schema_for_socket_overrides() throws Exception {
+ SocketOptions socketOptions =
+ new SocketOptions()
+ .setKeepAlive(true)
+ .setReuseAddress(true)
+ .setSoLinger(15)
+ .setReceiveBufferSize(4096)
+ .setSendBufferSize(8192);
+ assertConformsToSchema(report(Cluster.builder().withSocketOptions(socketOptions)));
+ }
+
+ @Test(groups = "unit")
+ public void should_reject_a_report_that_violates_the_schema() throws Exception {
+ // Sanity check that the validator actually enforces the schema (rather than accepting
+ // anything): an unknown top-level key must be rejected, since the schema sets
+ // additionalProperties=false.
+ ObjectNode report = (ObjectNode) report(Cluster.builder());
+ report.put("bogus-unknown-key", "x");
+ assertThat(violations(report))
+ .as("unknown top-level key must be rejected")
+ .contains(
+ "$: property 'bogus-unknown-key' is not defined in the schema and the schema does not "
+ + "allow additional properties");
+ }
+
+ // ---- Helpers --------------------------------------------------------------------------------
+
+ /**
+ * Drives {@code policy} through the initialization {@code Cluster.Manager.init()} performs after
+ * the first control connection's handshake, with a single node in the given datacenter and rack,
+ * so that the policy infers what it would infer against a live cluster. Stubbing the getters
+ * would pin the reporter against a fiction; this exercises the policy's own inference.
+ */
+ private static void initWithNode(LoadBalancingPolicy policy, String datacenter, String rack) {
+ Cluster cluster = mock(Cluster.class);
+ when(cluster.getConfiguration()).thenReturn(Cluster.builder().getConfiguration());
+ Host host = mock(Host.class);
+ when(host.getDatacenter()).thenReturn(datacenter);
+ when(host.getRack()).thenReturn(rack);
+ policy.init(cluster, Collections.singletonList(host));
+ }
+
+ /** Builds the full report from a cluster builder and parses it. */
+ private static JsonNode report(Cluster.Builder builder) throws IOException {
+ String json = new DefaultDriverConfigReporter(builder.getConfiguration()).buildJson();
+ return MAPPER.readTree(json);
+ }
+
+ // Accessors for the groups the schema nests, so that each test reads as what it asserts rather
+ // than as a path walk. The full paths are pinned once, explicitly, in
+ // should_report_default_configuration_shape.
+
+ /** The {@code query.load-balancing} group. */
+ private static JsonNode loadBalancing(JsonNode report) {
+ return report.path("query").path("load-balancing");
+ }
+
+ /** The reported load balancing policy. */
+ private static JsonNode lbPolicy(JsonNode report) {
+ return loadBalancing(report).path("policy");
+ }
+
+ /** The reported node preference, which the schema nests next to the policy it derives from. */
+ private static JsonNode nodePreference(JsonNode report) {
+ return loadBalancing(report).path("node-preference");
+ }
+
+ /**
+ * The schema's second node preference slot, under {@code connection}: the part of the same
+ * preference that decides which hosts are pooled, i.e. the datacenter alone.
+ */
+ private static JsonNode connectionNodePreference(JsonNode report) {
+ return report.path("connection").path("node-preference");
+ }
+
+ /** The {@code query.defaults} group. */
+ private static JsonNode queryDefaults(JsonNode report) {
+ return report.path("query").path("defaults");
+ }
+
+ /** The reported retry policy. */
+ private static JsonNode retryPolicy(JsonNode report) {
+ return report.path("query").path("retry").path("policy");
+ }
+
+ /** The reported speculative execution policy. */
+ private static JsonNode speculativeExecution(JsonNode report) {
+ return report.path("query").path("speculative-execution").path("policy");
+ }
+
+ /** A percentile speculative execution policy triggering at {@code percentile}. */
+ private static PercentileSpeculativeExecutionPolicy percentile(double percentile) {
+ return new PercentileSpeculativeExecutionPolicy(
+ PerHostPercentileTracker.builder(1000).build(), percentile, 3);
+ }
+
+ /** The discriminator of the retry policy {@code builder} is configured with. */
+ private static String retryPolicyType(Cluster.Builder builder) throws IOException {
+ return retryPolicy(report(builder)).path("type").asText();
+ }
+
+ /** The {@code connection.socket} group. */
+ private static JsonNode socket(JsonNode report) {
+ return report.path("connection").path("socket");
+ }
+
+ private static Set fieldNames(JsonNode node) {
+ Set names = new HashSet();
+ Iterator iterator = node.fieldNames();
+ while (iterator.hasNext()) {
+ names.add(iterator.next());
+ }
+ return names;
+ }
+
+ /**
+ * Asserts that {@code report} validates against the schema, save for any {@code extraGaps} the
+ * caller's configuration is expected to add. Passing none therefore also pins that no
+ * configuration silently grows a violation.
+ */
+ private static void assertConformsToSchema(JsonNode report, String... extraGaps) {
+ Set expected = new HashSet(Arrays.asList(extraGaps));
+ assertThat(violations(report)).as("schema violations in %s", report).isEqualTo(expected);
+ }
+
+ /**
+ * The {@code load-distribution} reported for a token-aware policy ordering its replicas with
+ * {@code replicaOrdering}.
+ */
+ private static String loadDistribution(TokenAwarePolicy.ReplicaOrdering replicaOrdering)
+ throws IOException {
+ JsonNode report =
+ report(
+ Cluster.builder()
+ .withLoadBalancingPolicy(
+ new TokenAwarePolicy(
+ DCAwareRoundRobinPolicy.builder().withLocalDc("dc1").build(),
+ replicaOrdering)));
+ assertConformsToSchema(report);
+ return lbPolicy(report).path("load-distribution").asText();
+ }
+
+ private static Set violations(JsonNode report) {
+ Set messages = new HashSet();
+ for (ValidationMessage message : SCHEMA.validate(report)) {
+ messages.add(message.getMessage());
+ }
+ return messages;
+ }
+
+ /** A reporter that reports {@code json} verbatim, bypassing the configuration read. */
+ private static DefaultDriverConfigReporter reporting(final String json) {
+ return new DefaultDriverConfigReporter(config()) {
+ @Override
+ protected String buildJson() {
+ return json;
+ }
+ };
+ }
+
+ /**
+ * A report that is within the limit by {@link String#length()} but over it once encoded, so that
+ * the check is pinned to UTF-8 bytes rather than characters.
+ */
+ private static String oversizedReport() {
+ StringBuilder sb = new StringBuilder("{\"version\":1,\"pad\":\"");
+ // 3 bytes each in UTF-8, so two thirds of the limit in characters is over it in bytes.
+ for (int i = 0; i < (DefaultDriverConfigReporter.MAX_DRIVER_CONFIG_LENGTH * 2) / 3; i++) {
+ sb.append('€');
+ }
+ String report = sb.append("\"}").toString();
+ assertThat(report.length()).isLessThan(DefaultDriverConfigReporter.MAX_DRIVER_CONFIG_LENGTH);
+ assertThat(report.getBytes(StandardCharsets.UTF_8).length)
+ .isGreaterThan(DefaultDriverConfigReporter.MAX_DRIVER_CONFIG_LENGTH);
+ return report;
+ }
+
+ /** A single-byte-per-character report of exactly {@code length} bytes. */
+ private static String padTo(int length) {
+ String prefix = "{\"version\":1,\"pad\":\"";
+ String suffix = "\"}";
+ StringBuilder sb = new StringBuilder(prefix);
+ for (int i = prefix.length() + suffix.length(); i < length; i++) {
+ sb.append('x');
+ }
+ String report = sb.append(suffix).toString();
+ assertThat(report.getBytes(StandardCharsets.UTF_8).length).isEqualTo(length);
+ return report;
+ }
+
+ /**
+ * A user policy that matches none of the built-in types and is not chainable, so it is reported
+ * as {@code custom}. Only its class name is ever read by the reporter.
+ */
+ private static class CustomLoadBalancingPolicy implements LoadBalancingPolicy {
+ @Override
+ public void init(Cluster cluster, Collection hosts) {}
+
+ @Override
+ public HostDistance distance(Host host) {
+ return HostDistance.LOCAL;
+ }
+
+ @Override
+ public Iterator newQueryPlan(String loggedKeyspace, Statement statement) {
+ return Collections.emptyList().iterator();
+ }
+
+ @Override
+ public void onAdd(Host host) {}
+
+ @Override
+ public void onUp(Host host) {}
+
+ @Override
+ public void onDown(Host host) {}
+
+ @Override
+ public void onRemove(Host host) {}
+
+ @Override
+ public void close() {}
+ }
+
+ /** A speculative execution policy that is none of the built-ins, so reported by name only. */
+ private static class CustomSpeculativeExecution implements SpeculativeExecutionPolicy {
+ @Override
+ public SpeculativeExecutionPlan newPlan(String loggedKeyspace, Statement statement) {
+ throw new UnsupportedOperationException("never planned in this test");
+ }
+
+ @Override
+ public void init(Cluster cluster) {}
+
+ @Override
+ public void close() {}
+ }
+
+ /**
+ * A user timestamp generator that is neither of the driver's own, so whether it assigns a
+ * timestamp client-side cannot be told from its class.
+ */
+ private static class CustomTimestamps implements TimestampGenerator {
+ @Override
+ public long next() {
+ throw new UnsupportedOperationException("never called in this test");
+ }
+ }
+
+ /**
+ * A user reconnection policy that is neither of the built-ins, so it is reported as {@code
+ * custom}. Only its class name is ever read by the reporter.
+ */
+ private static class CustomReconnectionPolicy implements ReconnectionPolicy {
+ @Override
+ public ReconnectionSchedule newSchedule() {
+ throw new UnsupportedOperationException("never scheduled in this test");
+ }
+
+ @Override
+ public void init(Cluster cluster) {}
+
+ @Override
+ public void close() {}
+ }
+
+ /** A chainable policy whose child is itself, i.e. a chain the reporter must not walk forever. */
+ private static class CyclicLoadBalancingPolicy extends DelegatingLoadBalancingPolicy {
+ CyclicLoadBalancingPolicy() {
+ super(new RoundRobinPolicy());
+ }
+
+ @Override
+ public LoadBalancingPolicy getChildPolicy() {
+ return this;
+ }
+ }
}
diff --git a/driver-core/src/test/java/com/datastax/driver/core/DriverConfigReportingCcmTest.java b/driver-core/src/test/java/com/datastax/driver/core/DriverConfigReportingCcmTest.java
index a51ed8568d3..a24a84c0ede 100644
--- a/driver-core/src/test/java/com/datastax/driver/core/DriverConfigReportingCcmTest.java
+++ b/driver-core/src/test/java/com/datastax/driver/core/DriverConfigReportingCcmTest.java
@@ -18,6 +18,9 @@
import static org.assertj.core.api.Assertions.assertThat;
import com.datastax.driver.core.utils.ScyllaVersion;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -35,7 +38,10 @@
* shared value;
* {@code SESSION_ID} is shared across every {@code Session} obtained from the same {@code
* Cluster} (it is Cluster-scoped, not Session-scoped — see {@link Connection.Factory});
- * {@code DRIVER_CONFIG} is stored for exactly one connection (the control connection);
+ * {@code DRIVER_CONFIG} is stored for exactly one connection (the control connection), as the
+ * full versioned JSON report;
+ * a later control connection reports the datacenter the load balancing policy has inferred in
+ * the meantime, which the first one could not yet know;
* with reporting disabled, {@code SESSION_ID} is still stored but {@code DRIVER_CONFIG} is
* not.
*
@@ -43,9 +49,8 @@
* 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",
@@ -69,6 +74,90 @@ public void should_store_session_id_on_all_connections_and_driver_config_on_cont
.as("connections carrying this cluster's SESSION_ID")
.isGreaterThanOrEqualTo(2);
+ // DRIVER_CONFIG is stored for exactly one connection (the control connection, which
+ // soleDriverConfig asserts), and what arrived is the versioned report shape once parsed rather
+ // than merely a plausible string. The byte-for-byte round trip lives in the reconnect test
+ // below, where a rebuild is comparable with what was sent; there is nothing to compare this
+ // first report against, since the driver keeps no copy and rebuilding now would produce a
+ // different, later report.
+ //
+ // Asserted by JSON type and not by value alone: asInt() coerces a string "1", and has() is
+ // satisfied by an explicit null, so either would pass on a report of the wrong shape.
+ JsonNode report = parse(soleDriverConfig(rows));
+ JsonNode version = report.path("version");
+ assertThat(version.isIntegralNumber()).as("version is a JSON integer").isTrue();
+ assertThat(version.intValue()).isEqualTo(1);
+ assertThat(
+ report.path("connection").path("pool").path("shard-aware").path("enabled").isBoolean())
+ .as("connection.pool.shard-aware.enabled is a JSON boolean")
+ .isTrue();
+ }
+
+ @Test(groups = "short")
+ public void should_report_the_inferred_datacenter_after_a_control_connection_reconnect() {
+ // Its own Cluster rather than the class-level one, because this test is the only one here that
+ // mutates the cluster it observes: forcing a reconnect leaves the superseded control connection
+ // in system.clients until the server reaps its row, so two rows carry a DRIVER_CONFIG for a
+ // while. should_store_session_id_on_all_connections_and_driver_config_on_control asserts there
+ // is exactly one, and TestNG orders methods within a class alphabetically, which runs it after
+ // this one -- against the very cluster this would have perturbed.
+ try (Cluster cluster = register(createClusterBuilder().build())) {
+ try (Session ignored = cluster.connect()) {
+ String sessionId = sessionId(cluster);
+ List rows = awaitClusterConnections(sessionId, 2);
+ assertThat(rows).as("connections carrying this cluster's SESSION_ID").isNotEmpty();
+
+ // The default load balancing policy is TokenAwarePolicy(DCAwareRoundRobinPolicy) with no
+ // configured datacenter, so it infers one -- but only in Cluster.Manager.init(), which runs
+ // after the first control connection's STARTUP. That first report can therefore say no more
+ // than "a datacenter will be inferred".
+ JsonNode first = parse(soleDriverConfig(rows));
+ JsonNode firstPreference = first.path("connection").path("node-preference");
+ assertThat(firstPreference.path("type").asText()).isEqualTo("dc-auto");
+ assertThat(firstPreference.has("local-dc")).isFalse();
+
+ // Force a new control connection. Every one of them builds its own report, so this is where
+ // the datacenter the policy has since inferred reaches the server.
+ Set before = connectionKeys(rows);
+ cluster.manager.controlConnection.triggerReconnect();
+
+ String reconnected = awaitNewDriverConfig(sessionId, before);
+ assertThat(reconnected)
+ .as("DRIVER_CONFIG on the reconnected control connection")
+ .isNotNull();
+
+ String localDc = cluster.manager.controlConnection.connectedHost().getDatacenter();
+ assertThat(localDc).as("datacenter of the node the control connection is on").isNotEmpty();
+ JsonNode second = parse(reconnected);
+ // dc-auto keeps an inferred datacenter under the plain local-dc key; only rack-auto
+ // prefixes.
+ assertThat(second.path("connection").path("node-preference").path("local-dc").asText())
+ .isEqualTo(localDc);
+ assertThat(
+ second
+ .path("query")
+ .path("load-balancing")
+ .path("node-preference")
+ .path("local-dc")
+ .asText())
+ .isEqualTo(localDc);
+
+ // Byte-for-byte against a report built right now, rather than a spot-check of a few keys:
+ // an oversized STARTUP value is silently truncated by an unchecked 16-bit length prefix
+ // instead of being rejected (which is why MAX_DRIVER_CONFIG_LENGTH exists), and truncation,
+ // reordering or encoding damage is exactly what a key-by-key check would miss. A rebuild is
+ // a fair comparison only here: by now the protocol version is negotiated and PoolingOptions
+ // settled, and the policy has inferred everything it is going to.
+ assertThat(reconnected)
+ .isEqualTo(cluster.manager.connectionFactory.buildDriverConfigReport());
+ }
+ }
+ }
+
+ /**
+ * The single {@code DRIVER_CONFIG} among the given connections, which the control one carries.
+ */
+ private String soleDriverConfig(List rows) {
List driverConfigs = new ArrayList();
for (Row row : rows) {
String driverConfig = clientOptions(row).get("DRIVER_CONFIG");
@@ -76,11 +165,46 @@ public void should_store_session_id_on_all_connections_and_driver_config_on_cont
driverConfigs.add(driverConfig);
}
}
-
- // DRIVER_CONFIG is stored for exactly one connection (the control connection). Stage 1 reports
- // only the schema version.
assertThat(driverConfigs).hasSize(1);
- assertThat(driverConfigs.get(0)).isEqualTo("{\"version\":1}");
+ return driverConfigs.get(0);
+ }
+
+ /**
+ * Polls {@code system.clients} until a connection of the given {@code SESSION_ID}'s cluster, and
+ * outside {@code excludeKeys}, carries a {@code DRIVER_CONFIG}; returns it, or {@code null} if
+ * none appears in time. Scoped to one cluster because every other driver connection to this CCM
+ * node is equally "new" to a key snapshot — including the class-level session's control
+ * connection, which carries a {@code DRIVER_CONFIG} of its own.
+ */
+ private String awaitNewDriverConfig(String sessionId, Set excludeKeys) {
+ long deadline = System.currentTimeMillis() + 60_000L;
+ while (System.currentTimeMillis() < deadline) {
+ for (Row row : newDriverConnections(excludeKeys)) {
+ Map options = clientOptions(row);
+ if (!sessionId.equals(options.get("SESSION_ID"))) {
+ continue;
+ }
+ String driverConfig = options.get("DRIVER_CONFIG");
+ if (driverConfig != null) {
+ return driverConfig;
+ }
+ }
+ try {
+ Thread.sleep(500L);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ return null;
+ }
+
+ 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..64f8ad6ce8a
--- /dev/null
+++ b/driver-core/src/test/resources/config/driver-config-report-v1.schema.json
@@ -0,0 +1,1026 @@
+{
+ "$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",
+ "control-plane",
+ "query"
+ ],
+ "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"
+ },
+ "control-plane": {
+ "$ref": "#/$defs/control-plane"
+ },
+ "query": {
+ "$ref": "#/$defs/query"
+ }
+ },
+ "$defs": {
+ "positiveInteger": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "nonNegativeInteger": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "nonEmptyString": {
+ "type": "string",
+ "minLength": 1
+ },
+ "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. When max-ms is present, it MUST be greater than or equal to base-ms; this cross-property invariant must be checked by the producer or consumer because JSON Schema Draft 2020-12 cannot compare sibling numeric values.",
+ "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. MUST be greater than or equal to base-ms. Absent when no maximum delay is configured."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "Constant backoff: wait a fixed, strictly positive delay 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. Must be greater than 0; omit backoff when no delay is configured."
+ }
+ }
+ }
+ ]
+ },
+ "requests": {
+ "type": "object",
+ "description": "Per-connection CQL request and protocol stream capacity. `orphaned.max` is expected to be lower than `in-flight.max`.",
+ "additionalProperties": false,
+ "required": [
+ "in-flight"
+ ],
+ "properties": {
+ "in-flight": {
+ "type": "object",
+ "description": "Requests currently awaiting a response on the connection.",
+ "additionalProperties": false,
+ "required": [
+ "max"
+ ],
+ "properties": {
+ "max": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Maximum number of concurrent in-flight requests allowed on one connection."
+ }
+ }
+ },
+ "orphaned": {
+ "type": "object",
+ "description": "Requests that the client stopped waiting for but whose stream identifiers cannot yet be safely reused.",
+ "additionalProperties": false,
+ "required": [
+ "max"
+ ],
+ "properties": {
+ "max": {
+ "$ref": "#/$defs/nonNegativeInteger",
+ "description": "Maximum number of orphaned requests allowed on one connection before the driver closes and replaces it. Absent only when this bound is unknown, for example when the client never replaces a connection over accumulated orphans and so has no limit to report."
+ }
+ }
+ }
+ }
+ },
+ "connection-pool": {
+ "description": "Connection pooling configuration.",
+ "type": "object",
+ "required": [
+ "shard-aware"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "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."
+ }
+ }
+ }
+ }
+ },
+ "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",
+ "requests",
+ "pool",
+ "socket",
+ "reconnection"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "requests": {
+ "$ref": "#/$defs/requests"
+ },
+ "node-preference": {
+ "$ref": "#/$defs/node-location-preference",
+ "description": "Defines part of the cluster driver holds connections to."
+ },
+ "connect": {
+ "type": "object",
+ "description": "Settings for establishing a TCP/CQL connection to a node.",
+ "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": {}
+ },
+ "pool": {
+ "$ref": "#/$defs/connection-pool",
+ "description": "A connection pooling configuration."
+ },
+ "socket": {
+ "$ref": "#/$defs/socket"
+ },
+ "reconnection": {
+ "description": "Connection reconnection configuration.",
+ "type": "object",
+ "required": [
+ "policy"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "policy": {
+ "$ref": "#/$defs/reconnection-policy"
+ }
+ }
+ },
+ "tls": {
+ "$ref": "#/$defs/tls"
+ }
+ }
+ },
+ "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": [
+ "queries",
+ "schema"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "queries": {
+ "type": "object",
+ "description": "Control-plane query settings.",
+ "additionalProperties": false,
+ "required": [
+ "system"
+ ],
+ "properties": {
+ "system": {
+ "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": {
+ "type": "object",
+ "description": "Control-plane schema settings.",
+ "additionalProperties": false,
+ "required": [
+ "agreement"
+ ],
+ "properties": {
+ "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. max-ms MUST be greater than or equal to base-ms; this cross-property invariant must be checked by the producer or consumer because JSON Schema Draft 2020-12 cannot compare sibling numeric values.",
+ "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. MUST be greater than or equal to base-ms. 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. A delay of 0 means reconnect immediately.",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "delay-ms"
+ ],
+ "properties": {
+ "type": {
+ "const": "constant",
+ "description": "Reconnection policy type."
+ },
+ "delay-ms": {
+ "$ref": "#/$defs/nonNegativeInteger",
+ "description": "Fixed delay between reconnection attempts in milliseconds; 0 means reconnect immediately. 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": {
+ "$ref": "#/$defs/nonEmptyString",
+ "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": "Standard error-aware retry policy.",
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "standard-error-aware",
+ "description": "Retry policy type."
+ },
+ "max-retries": {
+ "$ref": "#/$defs/nonNegativeInteger",
+ "description": "Maximum number of retries before giving up; 0 means no retries. Absent when no explicit retry limit is configured."
+ }
+ }
+ },
+ {
+ "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."
+ },
+ "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": "Never-retry policy: does not retry read timeouts, write timeouts, or unavailable errors, but may try the next host for connection and server errors.",
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "never",
+ "description": "Retry policy type."
+ },
+ "max-retries": {
+ "$ref": "#/$defs/nonNegativeInteger",
+ "description": "Maximum number of retries before giving up; 0 means no retries. Absent when no explicit retry limit is configured."
+ }
+ }
+ },
+ {
+ "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."
+ },
+ "max-retries": {
+ "$ref": "#/$defs/nonNegativeInteger",
+ "description": "Maximum number of retries before giving up; 0 means no retries. Absent when no explicit retry limit is configured."
+ }
+ }
+ },
+ {
+ "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": {
+ "$ref": "#/$defs/nonEmptyString",
+ "description": "Name of the custom policy (e.g. the public type name of the user-provided policy)."
+ },
+ "description": {
+ "$ref": "#/$defs/nonEmptyString",
+ "description": "Textual description of what this policy does."
+ },
+ "max-retries": {
+ "$ref": "#/$defs/nonNegativeInteger",
+ "description": "Maximum number of retries before giving up; 0 means no retries. Absent when no explicit retry limit is configured."
+ }
+ }
+ }
+ ]
+ },
+ "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. A delay of 0 means launch them immediately.",
+ "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/nonNegativeInteger",
+ "description": "Delay before launching each additional execution in milliseconds; 0 means launch immediately."
+ }
+ }
+ },
+ {
+ "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": {
+ "$ref": "#/$defs/nonEmptyString",
+ "description": "Name of the custom policy (e.g. the public type name of the user-provided policy)."
+ },
+ "description": {
+ "$ref": "#/$defs/nonEmptyString",
+ "description": "Textual description of what this policy does."
+ }
+ }
+ }
+ ]
+ },
+ "adaptive-ordering": {
+ "type": "object",
+ "description": "Dynamic reordering of otherwise eligible candidate nodes using runtime responsiveness, load, or health observations. Absent when adaptive ordering is disabled. This capability does not imply a particular algorithm.",
+ "additionalProperties": false,
+ "required": [
+ "signals"
+ ],
+ "properties": {
+ "signals": {
+ "type": "array",
+ "description": "Runtime observations used to influence ordering.",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "enum": [
+ "latency",
+ "response-rate",
+ "in-flight-requests",
+ "recovery-state"
+ ]
+ }
+ }
+ }
+ },
+ "load-balancing-policy": {
+ "description": "Load balancing / host selection policy, discriminated by `type`. A built-in token-aware policy is reported with `type` set to `token-aware` and the normalized capability flags below. A user-supplied policy is reported with `type` set to `custom`, 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",
+ "load-distribution",
+ "fallback-to-non-preferred-nodes"
+ ],
+ "properties": {
+ "type": {
+ "const": "token-aware",
+ "description": "Load balancing policy type: the built-in token-aware policy."
+ },
+ "load-distribution": {
+ "type": "string",
+ "enum": [
+ "shuffle",
+ "round-robin",
+ "replica-set"
+ ],
+ "description": "Strategy used to distribute requests across otherwise equally preferred nodes. `shuffle` randomizes node selection across query plans; `round-robin` rotates the first selected node across successive query plans; `replica-set` preserves the replica set's existing order without reordering it."
+ },
+ "fallback-to-non-preferred-nodes": {
+ "type": "boolean",
+ "description": "Whether requests may fail over to nodes outside of the preference configured by `query.load-balancing.node-preference`."
+ },
+ "adaptive-ordering": {
+ "$ref": "#/$defs/adaptive-ordering"
+ }
+ }
+ },
+ {
+ "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": {
+ "$ref": "#/$defs/nonEmptyString",
+ "description": "Name of the custom policy (e.g. the public type name of the user-provided policy)."
+ },
+ "description": {
+ "$ref": "#/$defs/nonEmptyString",
+ "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": {
+ "$ref": "#/$defs/nonEmptyString",
+ "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": {
+ "$ref": "#/$defs/nonEmptyString",
+ "description": "Explicitly configured preferred datacenter."
+ },
+ "local-rack": {
+ "$ref": "#/$defs/nonEmptyString",
+ "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": {
+ "$ref": "#/$defs/nonEmptyString",
+ "description": "Inferred preferred datacenter. Absent when not yet known at report time."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "description": "Datacenter and/or rack preference inferred from the connected node. Configured and inferred values are reported separately.",
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "rack-auto",
+ "description": "At least one part of the location preference is inferred."
+ },
+ "local-dc": {
+ "$ref": "#/$defs/nonEmptyString",
+ "description": "Explicitly configured preferred datacenter."
+ },
+ "local-rack": {
+ "$ref": "#/$defs/nonEmptyString",
+ "description": "Explicitly configured preferred rack."
+ },
+ "inferred-local-dc": {
+ "$ref": "#/$defs/nonEmptyString",
+ "description": "Inferred preferred datacenter. Absent when not yet known."
+ },
+ "inferred-local-rack": {
+ "$ref": "#/$defs/nonEmptyString",
+ "description": "Inferred preferred rack. Absent when not yet known."
+ }
+ },
+ "allOf": [
+ {
+ "not": {
+ "required": [
+ "local-dc",
+ "inferred-local-dc"
+ ]
+ }
+ },
+ {
+ "not": {
+ "required": [
+ "local-rack",
+ "inferred-local-rack"
+ ]
+ }
+ },
+ {
+ "not": {
+ "required": [
+ "local-dc",
+ "local-rack"
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "query": {
+ "description": "Query execution configuration.",
+ "type": "object",
+ "required": [
+ "defaults",
+ "retry",
+ "load-balancing"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "defaults": {
+ "$ref": "#/$defs/query-defaults"
+ },
+ "retry": {
+ "description": "Query retry configuration. Backoff is optional and is omitted when no retry delay is configured.",
+ "type": "object",
+ "required": [
+ "policy"
+ ],
+ "additionalProperties": false,
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "policy": {
+ "properties": {
+ "type": {
+ "const": "fallthrough"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "required": [
+ "policy"
+ ]
+ },
+ "then": {
+ "not": {
+ "required": [
+ "backoff"
+ ]
+ }
+ }
+ }
+ ],
+ "properties": {
+ "policy": {
+ "$ref": "#/$defs/retry-policy"
+ },
+ "backoff": {
+ "$ref": "#/$defs/retryPolicyBackoff",
+ "description": "Delay inserted between retries. Omitted when no retry backoff is configured. Every configured delay must be greater than 0."
+ }
+ }
+ },
+ "load-balancing": {
+ "description": "Load-balancing configuration applied to queries.",
+ "type": "object",
+ "required": [
+ "policy"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "policy": {
+ "$ref": "#/$defs/load-balancing-policy"
+ },
+ "node-preference": {
+ "$ref": "#/$defs/node-location-preference",
+ "description": "Defines part of the cluster queries will be scheduled on"
+ }
+ }
+ },
+ "speculative-execution": {
+ "description": "Speculative-execution configuration applied to queries. Absent when speculative execution is disabled.",
+ "type": "object",
+ "required": [
+ "policy"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "policy": {
+ "$ref": "#/$defs/speculative-execution-policy"
+ }
+ }
+ }
+ }
+ },
+ "query-defaults": {
+ "description": "Default per-request settings applied to statements that do not override them.",
+ "type": "object",
+ "required": [
+ "consistency",
+ "idempotence"
+ ],
+ "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",
+ "LOCAL_SERIAL"
+ ]
+ },
+ "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. Absent only when this behavior is unknown, for example when a custom timestamp generator may or may not enforce a timestamp.",
+ "type": "boolean"
+ },
+ "request": {
+ "type": "object",
+ "description": "Default request-level settings.",
+ "additionalProperties": false,
+ "properties": {
+ "timeout-ms": {
+ "$ref": "#/$defs/positiveInteger",
+ "description": "Client-side timeout for a single request/query in milliseconds. Absent when the timeout is disabled or unset."
+ }
+ }
+ }
+ }
+ },
+ "tls": {
+ "description": "TLS/SSL transport settings. Absent when TLS is disabled. Reports only booleans; never credentials, keys, or host lists.",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "hostname-verification": {
+ "type": "boolean",
+ "description": "Whether the server hostname is verified against its certificate. Absent only when this behavior is unknown, for example when a custom certificate validator may or may not enforce hostname verification."
+ }
+ }
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 88a6c1a6e9f..253727c9962 100644
--- a/pom.xml
+++ b/pom.xml
@@ -88,6 +88,10 @@
1.2.13
3.0.8
0.27.2
+
+ 1.5.9
3.5.4
127.0.1.
@@ -402,6 +406,12 @@
4.3.0
+
+ com.networknt
+ json-schema-validator
+ ${json-schema-validator.version}
+
+