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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions driver-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@
</exclusion>
</exclusions>
</dependency>
<!-- Used by DefaultDriverConfigReporter to build the DRIVER_CONFIG JSON blob -->
Comment thread
nikagra marked this conversation as resolved.
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
Expand All @@ -172,10 +173,6 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>

<!-- added for easier DNS hostname resolution mocking -->
<dependency>
Expand Down
21 changes: 21 additions & 0 deletions driver-core/src/main/java/com/datastax/driver/core/Cluster.java
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,26 @@ public Builder withApplicationInfo(ApplicationInfo applicationInfo) {
return this;
}

/**
* Enables or disables driver configuration reporting, i.e. whether the control connection sends
* a {@code DRIVER_CONFIG} JSON blob describing the effective driver configuration in its
* startup options. <b>Enabled by default.</b>
*
* <p>The server stores it in {@code system.clients.client_options} — a per-node table, so only
* the node holding the control connection stores {@code DRIVER_CONFIG}; consumers must query
* and aggregate across all nodes.
*
* <p>This does not govern the {@code SESSION_ID} startup option, which every connection always
* sends (so the server can group every connection opened from this {@link Cluster}, across all
* of its {@link Session}s), regardless of this setting.
*
* @param enabled whether driver configuration reporting is enabled.
*/
public Builder withDriverConfigReporting(boolean enabled) {
configurationBuilder.withDriverConfigReporting(enabled);
return this;
}

Comment on lines +1446 to +1465

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be enabled by default

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 40b507f3ab: Configuration.Builder now defaults it to true, and the CCM test builds its cluster with no withDriverConfigReporting call so default-on is covered end-to-end. One flag to raise: merged 4.x #967 ships this default-off and gates SESSION_ID behind it, so 3.x and 4.x now differ — want me to open a follow-up aligning 4.x with this?

/**
* The configuration that will be used for the new cluster.
*
Expand Down Expand Up @@ -1598,6 +1618,7 @@ private Manager(
.withNettyOptions(configuration.getNettyOptions())
.withCodecRegistry(configuration.getCodecRegistry())
.withApplicationInfo(configuration.getApplicationInfo())
.withDriverConfigReporting(configuration.isDriverConfigReportingEnabled())
.build();
} else {
this.configuration = configuration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public static Builder builder() {
private final CodecRegistry codecRegistry;
private final String defaultKeyspace;
private final ApplicationInfo applicationInfo;
private final boolean driverConfigReportingEnabled;

private Configuration(
Policies policies,
Expand All @@ -72,7 +73,8 @@ private Configuration(
NettyOptions nettyOptions,
CodecRegistry codecRegistry,
String defaultKeyspace,
ApplicationInfo applicationInfo) {
ApplicationInfo applicationInfo,
boolean driverConfigReportingEnabled) {
this.policies = policies;
this.protocolOptions = protocolOptions;
this.poolingOptions = poolingOptions;
Expand All @@ -84,6 +86,7 @@ private Configuration(
this.codecRegistry = codecRegistry;
this.defaultKeyspace = defaultKeyspace;
this.applicationInfo = applicationInfo;
this.driverConfigReportingEnabled = driverConfigReportingEnabled;
}

/**
Expand All @@ -103,7 +106,8 @@ protected Configuration(Configuration toCopy) {
toCopy.getNettyOptions(),
toCopy.getCodecRegistry(),
toCopy.getDefaultKeyspace(),
toCopy.getApplicationInfo());
toCopy.getApplicationInfo(),
toCopy.isDriverConfigReportingEnabled());
}

void register(Cluster.Manager manager) {
Expand Down Expand Up @@ -222,6 +226,20 @@ public ApplicationInfo getApplicationInfo() {
return applicationInfo;
}

/**
* Whether driver configuration reporting is enabled, i.e. whether the control connection sends a
* {@code DRIVER_CONFIG} JSON blob describing the effective driver configuration in its startup
* options. Enabled by default.
*
* <p>This does not govern the {@code SESSION_ID} startup option, which every connection always
* sends regardless of this setting.
*
* @return {@code true} if driver configuration reporting is enabled.
*/
public boolean isDriverConfigReportingEnabled() {
return driverConfigReportingEnabled;
}

/**
* Returns the {@link CodecRegistry} instance for this configuration.
*
Expand All @@ -247,6 +265,7 @@ public static class Builder {
private ThreadingOptions threadingOptions;
private NettyOptions nettyOptions;
private ApplicationInfo applicationInfo;
private boolean driverConfigReportingEnabled = true;
private CodecRegistry codecRegistry;
private String defaultKeyspace;

Expand All @@ -261,6 +280,19 @@ public Builder withApplicationInfo(ApplicationInfo applicationInfo) {
return this;
}

/**
* Enables or disables driver configuration reporting (the {@code DRIVER_CONFIG} startup option
* sent by the control connection). Enabled by default; see {@link
* Configuration#isDriverConfigReportingEnabled()}.
*
* @param driverConfigReportingEnabled whether driver configuration reporting is enabled.
* @return this builder.
*/
public Builder withDriverConfigReporting(boolean driverConfigReportingEnabled) {
this.driverConfigReportingEnabled = driverConfigReportingEnabled;
return this;
}

/**
* Sets the policies for this cluster.
*
Expand Down Expand Up @@ -392,7 +424,8 @@ public Configuration build() {
nettyOptions != null ? nettyOptions : NettyOptions.DEFAULT_INSTANCE,
codecRegistry != null ? codecRegistry : CodecRegistry.DEFAULT_INSTANCE,
defaultKeyspace,
applicationInfo);
applicationInfo,
driverConfigReportingEnabled);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
Expand Down Expand Up @@ -118,6 +119,12 @@ class Connection {

private static final long ADV_SHARD_AWARENESS_BLOCK_ON_ERROR = 5 * 60 * 1000;

/**
* STARTUP option key under which the Cluster-scoped connection-grouping identifier is sent, on
* every connection (see {@link Factory#sessionId}).
*/
static final String SESSION_ID_KEY = "SESSION_ID";

enum State {
OPEN,
TRASHED,
Expand Down Expand Up @@ -159,6 +166,11 @@ enum State {
private final ApplicationInfo applicationInfo;
private ProtocolFeatureStore protocolFeatureStore;

// The DRIVER_CONFIG blob this connection reports in its STARTUP options, or null to report none.
// Set only for the control connection, so the (potentially large) config blob is sent once per
// Cluster rather than on every connection. SESSION_ID is still sent on every connection.
private final String driverConfig;

/**
* Create a new connection to a Cassandra node and associate it with the given pool.
*
Expand All @@ -169,6 +181,11 @@ enum State {
* connection can also be associated to an owner later with {@link #setOwner(Owner)}.
*/
protected Connection(String name, EndPoint endPoint, Factory factory, Owner owner) {
this(name, endPoint, factory, owner, null);
}

private Connection(
String name, EndPoint endPoint, Factory factory, Owner owner, String driverConfig) {
this.endPoint = endPoint;
this.factory = factory;
this.dispatcher = new Dispatcher();
Expand All @@ -178,6 +195,7 @@ protected Connection(String name, EndPoint endPoint, Factory factory, Owner owne
this.defaultKeyspaceAttempt = new SetKeyspaceAttempt(null, thisFuture);
this.targetKeyspace = new AtomicReference<SetKeyspaceAttempt>(defaultKeyspaceAttempt);
this.applicationInfo = factory.configuration.getApplicationInfo();
this.driverConfig = driverConfig;
}

/** Create a new connection to a Cassandra node. */
Expand Down Expand Up @@ -512,6 +530,16 @@ public ListenableFuture<Void> apply(Void input) throws Exception {
applicationInfo.addOption(extraOptions);
}

// Sent on every connection, unconditionally (like DRIVER_NAME / DRIVER_VERSION), so the
// 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);
}

if (protocolFeatureStore != null) {
protocolFeatureStore.populateStartupOptions(protocolVersion, extraOptions);
}
Expand Down Expand Up @@ -1283,11 +1311,28 @@ static class Factory {
volatile ProtocolVersion protocolVersion;
private final NettyOptions nettyOptions;

// Dedicated, driver-generated identifier sent as SESSION_ID on every connection, so the server
// can group them. Not derived from the (user-settable) CLIENT_ID, so that it is guaranteed
// unique as the grouping key requires. There is one Factory per Cluster, so this is stable and
// shared by every Session obtained from that Cluster (the control connection has no Session
// 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;

Factory(Cluster.Manager manager, Configuration configuration) {
this.defaultHandler = manager;
this.manager = manager;
this.reaper = manager.reaper;
this.configuration = configuration;
this.driverConfig =
configuration.isDriverConfigReportingEnabled()
? new DefaultDriverConfigReporter(configuration).buildReport()
: null;
this.authProvider = configuration.getProtocolOptions().getAuthProvider();
this.protocolVersion = configuration.getProtocolOptions().initialProtocolVersion;
this.nettyOptions = configuration.getNettyOptions();
Expand Down Expand Up @@ -1319,12 +1364,25 @@ int getPort() {
Connection open(Host host)
throws ConnectionException, InterruptedException, UnsupportedProtocolVersionException,
ClusterNameMismatchException {
return open(host, false);
}

/**
* 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).
*/
Connection open(Host host, boolean reportConfig)
throws ConnectionException, InterruptedException, UnsupportedProtocolVersionException,
ClusterNameMismatchException {
EndPoint endPoint = host.getEndPoint();

if (isShutdown) throw new ConnectionException(endPoint, "Connection factory is shut down");

host.convictionPolicy.signalConnectionsOpening(1);
Connection connection = new Connection(buildConnectionName(host), endPoint, this);
Connection connection =
new Connection(
buildConnectionName(host), endPoint, this, null, reportConfig ? driverConfig : null);
// This method opens the connection synchronously, so wait until it's initialized
try {
connection.initAsync().get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,9 @@ private static Map<EndPoint, Throwable> logError(
private Connection tryConnect(Host host, boolean isInitialConnection)
throws ConnectionException, ExecutionException, InterruptedException,
UnsupportedProtocolVersionException, ClusterNameMismatchException {
Connection connection = cluster.connectionFactory.open(host);
// Mark the control connection so it reports the full DRIVER_CONFIG blob (other connections send
// only SESSION_ID). No-op unless driver config reporting is enabled.
Connection connection = cluster.connectionFactory.open(host, true);
Comment thread
nikagra marked this conversation as resolved.
String productType = connection.optionsQuery().get();
// If no protocol version was specified, set the default as soon as a connection succeeds (it's
// needed to parse UDTs in refreshSchema)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright ScyllaDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.datastax.driver.core;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Default {@link DriverConfigReporter}: serializes the driver configuration to the cross-driver
* {@code DRIVER_CONFIG} JSON shape, which {@link Connection.Factory} then sends in the control
* connection's {@code STARTUP} options.
*
* <p>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.
*/
public class DefaultDriverConfigReporter implements DriverConfigReporter {

private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDriverConfigReporter.class);

/** STARTUP option key under which the config JSON is sent. */
public static final String DRIVER_CONFIG_KEY = "DRIVER_CONFIG";

/**
* Major schema version. Adding keys is backward-compatible and does not bump this; only
* changing/removing the meaning of an existing key does.
*/
static final int SCHEMA_VERSION = 1;

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
Comment thread
nikagra marked this conversation as resolved.

protected final Configuration configuration;

public DefaultDriverConfigReporter(Configuration configuration) {
this.configuration = configuration;
}

@Override
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.
try {
return buildJson();
} catch (RuntimeException e) {
LOGGER.warn(
"Error while building the driver configuration report; skipping driver config reporting",
e);
return null;
}
}

/**
* Builds the compact, single-line JSON configuration report.
*
* <p>Stage 1 emits only the schema {@code version}; the individual configuration groups are
* populated in {@link #populateConfig(ObjectNode)} in a later stage.
*/
protected String buildJson() {
ObjectNode root = OBJECT_MAPPER.createObjectNode();
root.put("version", SCHEMA_VERSION);
populateConfig(root);
try {
return OBJECT_MAPPER.writeValueAsString(root);
} catch (JsonProcessingException e) {
// An in-memory node tree should never fail to serialize; never let it break connection setup.
LOGGER.warn("Failed to serialize driver configuration report; skipping DRIVER_CONFIG", e);
return null;
}
}
Comment thread
nikagra marked this conversation as resolved.

/**
* 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}.
*/
protected void populateConfig(ObjectNode root) {
// Stage 2: populate configuration groups from `configuration`.
}
}
Loading
Loading