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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ Message getRequest() {
// SESSION_ID that every connection already carries from context.getStartupOptions().
// No-op when driver config reporting is disabled.
if (options.reportConfig) {
context.getDriverConfigReporter().populateControlConnectionOptions(startupOptions);
context
.getDriverConfigReporter()
.populateControlConnectionOptions(startupOptions, ctx.channel());
}
return request = new Startup(startupOptions);
case GET_CLUSTER_NAME:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import io.netty.channel.Channel;
import io.netty.handler.ssl.SslHandler;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;
Expand Down Expand Up @@ -189,8 +192,8 @@
* cross-driver schema doesn't define; this is a known gap, not an oversight.
*
* <p><b>Thread safety:</b> this class is safe to use as shipped, and holds no mutable state. Note
* that {@code buildJson()} runs on every control-connection (re)initialization, and may be called
* concurrently with a reconnect racing a fresh session start.
* that {@code buildJson(Channel)} runs on every control-connection (re)initialization, and may be
* called concurrently with a reconnect racing a fresh session start.
*/
@ThreadSafe
public class DefaultDriverConfigReporter implements DriverConfigReporter {
Expand Down Expand Up @@ -231,7 +234,8 @@ public DefaultDriverConfigReporter(InternalDriverContext context) {
}

@Override
public void populateControlConnectionOptions(Map<String, String> startupOptions) {
public void populateControlConnectionOptions(
@NonNull Map<String, String> startupOptions, @NonNull Channel channel) {
// Configuration reporting is a best-effort diagnostic aid: it runs on the connection
// initialization path, so any failure here (a bad config read, a misbehaving policy while
// introspecting, a serialization error) must be swallowed rather than allowed to break the
Expand All @@ -247,7 +251,7 @@ public void populateControlConnectionOptions(Map<String, String> startupOptions)
if (!isEnabled()) {
return;
}
String json = buildJson();
String json = buildJson(channel);
if (json == null) {
return;
}
Expand Down Expand Up @@ -288,22 +292,23 @@ private boolean isEnabled() {
* class's to enforce: a future change to session bootstrap that dropped one of those from the
* eager list would quietly reintroduce that.
*
* <p>The configured SSL <em>engine</em> factory is deliberately not among them: {@link #tls()}
* reads the engine factory held by the {@code JdkSslHandlerFactory} in force rather than the one
* behind {@code getSslEngineFactory()}. Those can differ — a context that overrides {@code
* buildSslHandlerFactory()} may wrap an engine factory of its own — and going through the context
* would both describe an engine nothing on the connection path uses and risk being the first
* caller to resolve it, which for the built-in factory means reading keystore/truststore files on
* a Netty event-loop thread (and failing the whole report if that throws).
* <p>The configured SSL <em>engine</em> factory is deliberately not among them: {@link
* #tls(Channel)} reads the engine factory held by the {@code JdkSslHandlerFactory} in force
* rather than the one behind {@code getSslEngineFactory()}. Those can differ — a context that
* overrides {@code buildSslHandlerFactory()} may wrap an engine factory of its own — and going
* through the context would both describe an engine nothing on the connection path uses and risk
* being the first caller to resolve it, which for the built-in factory means reading
* keystore/truststore files on a Netty event-loop thread (and failing the whole report if that
* throws).
*
* @return the report, or {@code null} if it could not be serialized — in which case {@code
* DRIVER_CONFIG} is skipped rather than the connection failed.
*/
@Nullable
String buildJson() {
String buildJson(Channel channel) {
ObjectNode root = OBJECT_MAPPER.createObjectNode();
root.put("version", SCHEMA_VERSION);
populateConfig(root, context.getConfig().getDefaultProfile());
populateConfig(root, context.getConfig().getDefaultProfile(), channel);
try {
return OBJECT_MAPPER.writeValueAsString(root);
} catch (JsonProcessingException e) {
Expand All @@ -318,14 +323,14 @@ String buildJson() {
* plus the context's policies. Each group follows the cross-driver schema; a key the Java driver
* has no equivalent for is omitted rather than reported as {@code null}.
*/
private void populateConfig(ObjectNode root, DriverExecutionProfile config) {
private void populateConfig(ObjectNode root, DriverExecutionProfile config, Channel channel) {
// Resolved once and shared: the load balancing policy decides both its own group and the
// node-location preferences reported under two different parents, and resolving it twice would
// mean a second SPI lookup on the Netty event-loop thread that is building STARTUP.
LoadBalancingPolicy loadBalancingPolicy =
context.getLoadBalancingPolicy(DriverExecutionProfile.DEFAULT_NAME);
NodeLocation nodeLocation = nodeLocation(config, loadBalancingPolicy);
root.set("connection", connection(config, nodeLocation));
root.set("connection", connection(config, nodeLocation, channel));
root.set("control-plane", controlPlane(config));
root.set("query", query(config, loadBalancingPolicy, nodeLocation));
}
Expand All @@ -335,15 +340,15 @@ private void populateConfig(ObjectNode root, DriverExecutionProfile config) {
* top of it, how it is re-established, and which part of the cluster gets one at all.
*/
private ObjectNode connection(
DriverExecutionProfile config, @Nullable NodeLocation nodeLocation) {
DriverExecutionProfile config, @Nullable NodeLocation nodeLocation, Channel channel) {
ObjectNode n = connectionTimeouts(config);
n.set("socket", socket(config));
ObjectNode reconnection = OBJECT_MAPPER.createObjectNode();
reconnection.set("policy", reconnectionPolicy());
n.set("reconnection", reconnection);
// Optional, and absent rather than false when off: presence of the group is what says TLS is
// enabled, since the schema dropped the boolean that used to carry it.
ObjectNode tls = tls();
ObjectNode tls = tls(channel);
if (tls != null) {
n.set("tls", tls);
}
Expand Down Expand Up @@ -1078,15 +1083,16 @@ private static Optional<Boolean> clientTimestamps(TimestampGenerator generator)
* so presence of the group is what reports that it is on.
*/
@Nullable
private ObjectNode tls() {
// TLS is on exactly when the channel pipeline gets an SSL handler, which ChannelFactory decides
// from the low-level SslHandlerFactory. Deliberately not getSslEngineFactory(): that is only
// the public JDK-based path that DefaultDriverContext.buildSslHandlerFactory() wraps, and an
private ObjectNode tls(Channel channel) {
// TLS is on exactly when the channel pipeline has an SSL handler. ChannelFactory installs one
// from the low-level SslHandlerFactory before NettyOptions.afterChannelInitialized(), but that
// hook may add, replace, or remove handlers; report the pipeline left in force rather than the
// factory's configuration intent. Deliberately not getSslEngineFactory(): that is only the
// public JDK-based path that DefaultDriverContext.buildSslHandlerFactory() wraps, and an
// override of that method (the documented expert extension point, e.g. Netty's native OpenSSL)
// supplies a handler factory with no engine factory at all — a session that is encrypted all
// the same.
Optional<SslHandlerFactory> handlerFactory = context.getSslHandlerFactory();
if (!handlerFactory.isPresent()) {
if (channel.pipeline().get(SslHandler.class) == null) {
return null;
}
ObjectNode n = OBJECT_MAPPER.createObjectNode();
Expand All @@ -1105,8 +1111,10 @@ private ObjectNode tls() {
// SessionBuilder.withSslContext(...) (ProgrammaticSslEngineFactory) validates only if
// explicitly asked to (default off) regardless of that option, so reading the option here would
// falsely report validation as on when it isn't.
SslHandlerFactory factory = handlerFactory.get();
if (factory.getClass() == JdkSslHandlerFactory.class) {
Optional<SslHandlerFactory> handlerFactory = context.getSslHandlerFactory();
if (handlerFactory.isPresent()
&& handlerFactory.get().getClass() == JdkSslHandlerFactory.class) {
SslHandlerFactory factory = handlerFactory.get();
SslEngineFactory engineFactory = ((JdkSslHandlerFactory) factory).getSslEngineFactory();
hostnameValidation(engineFactory).ifPresent(v -> n.put("hostname-verification", v));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*/
package com.datastax.oss.driver.internal.core.context;

import edu.umd.cs.findbugs.annotations.NonNull;
import io.netty.channel.Channel;
import java.util.Map;

/**
Expand All @@ -43,8 +45,13 @@ public interface DriverConfigReporter {
* failure to build the report must be swallowed (and logged) rather than propagated, otherwise it
* would prevent the session from establishing or reconnecting.
*
* <p>The report describes the driver's own configuration only, so nothing here depends on which
* backend answered: it can be built before the connection learns anything about its peer.
* <p>The report describes the driver's own configuration and the effective SSL state of the
* control connection. It does not depend on which backend answered, but the SSL handler must
* already be installed on {@code channel}.
*
* @param startupOptions startup options to add the report to
* @param channel control connection whose effective SSL state is reported
*/
void populateControlConnectionOptions(Map<String, String> startupOptions);
void populateControlConnectionOptions(
@NonNull Map<String, String> startupOptions, @NonNull Channel channel);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*/
package com.datastax.oss.driver.internal.core.context;

import edu.umd.cs.findbugs.annotations.NonNull;
import io.netty.channel.Channel;
import java.util.Map;
import net.jcip.annotations.ThreadSafe;

Expand All @@ -41,7 +43,8 @@
public class NoopDriverConfigReporter implements DriverConfigReporter {

@Override
public void populateControlConnectionOptions(Map<String, String> startupOptions) {
public void populateControlConnectionOptions(
@NonNull Map<String, String> startupOptions, @NonNull Channel channel) {
// nothing to do
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public void setup() throws InterruptedException {
when(context.getWriteCoalescer()).thenReturn(new PassThroughWriteCoalescer(null));
when(context.getCompressor()).thenReturn(compressor);
// The init handler consults the config reporter for the control connection; default to a no-op.
when(context.getDriverConfigReporter()).thenReturn(startupOptions -> {});
when(context.getDriverConfigReporter()).thenReturn((startupOptions, controlChannel) -> {});

// Start local server
ServerBootstrap serverBootstrap =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ public void setup() {
.thenReturn(Duration.ofSeconds(30));
when(internalDriverContext.getProtocolVersionRegistry()).thenReturn(protocolVersionRegistry);
// The init handler consults the config reporter for the control connection; default to a no-op.
when(internalDriverContext.getDriverConfigReporter()).thenReturn(startupOptions -> {});
when(internalDriverContext.getDriverConfigReporter())
.thenReturn((startupOptions, controlChannel) -> {});

channel
.pipeline()
Expand Down Expand Up @@ -163,7 +164,7 @@ public void should_initialize() {
private void stubConfigReporter() {
when(internalDriverContext.getDriverConfigReporter())
.thenReturn(
startupOptions ->
(startupOptions, controlChannel) ->
startupOptions.put(
DefaultDriverConfigReporter.DRIVER_CONFIG_KEY, "{\"version\":1}"));
}
Expand Down Expand Up @@ -216,7 +217,7 @@ public void should_not_consult_the_config_reporter_on_pool_connection() {
assertThat(requestFrame.message).isInstanceOf(Startup.class);
Startup startup = (Startup) requestFrame.message;
assertThat(startup.options).doesNotContainKey(DefaultDriverConfigReporter.DRIVER_CONFIG_KEY);
verify(reporter, never()).populateControlConnectionOptions(any());
verify(reporter, never()).populateControlConnectionOptions(any(), any());
}

@Test
Expand Down
Loading
Loading