Add support for external configuration for Cassandra

Closes gh-24065
pull/25690/head
Stephane Nicoll 4 years ago
parent 7eccf4a92d
commit 9b0cdac97a

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.boot.autoconfigure.cassandra;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.LinkedHashMap;
@ -52,6 +53,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.core.io.Resource;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Cassandra.
@ -116,6 +118,28 @@ public class CassandraAutoConfiguration {
}
private Config cassandraConfiguration(CassandraProperties properties) {
Config config = mapConfig(properties);
Resource configFile = properties.getConfig();
return (configFile != null) ? applyDefaultFallback(config.withFallback(loadConfig(configFile)))
: applyDefaultFallback(config);
}
private Config applyDefaultFallback(Config config) {
ConfigFactory.invalidateCaches();
return ConfigFactory.defaultOverrides().withFallback(config).withFallback(ConfigFactory.defaultReference())
.resolve();
}
private Config loadConfig(Resource config) {
try {
return ConfigFactory.parseURL(config.getURL());
}
catch (IOException ex) {
throw new IllegalStateException("Failed to load cassandra configuration from " + config, ex);
}
}
private Config mapConfig(CassandraProperties properties) {
CassandraDriverOptions options = new CassandraDriverOptions();
PropertyMapper map = PropertyMapper.get();
map.from(properties.getSessionName()).whenHasText()
@ -133,9 +157,7 @@ public class CassandraAutoConfiguration {
.to((contactPoints) -> options.add(DefaultDriverOption.CONTACT_POINTS, contactPoints));
map.from(properties.getLocalDatacenter()).to(
(localDatacenter) -> options.add(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, localDatacenter));
ConfigFactory.invalidateCaches();
return ConfigFactory.defaultOverrides().withFallback(options.build())
.withFallback(ConfigFactory.defaultReference()).resolve();
return options.build();
}
private void mapConnectionOptions(CassandraProperties properties, CassandraDriverOptions options) {

@ -24,6 +24,7 @@ import java.util.List;
import com.datastax.oss.driver.api.core.DefaultConsistencyLevel;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
/**
* Configuration properties for Cassandra.
@ -37,6 +38,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.data.cassandra")
public class CassandraProperties {
/**
* Location of the configuration file to use.
*/
private Resource config;
/**
* Keyspace name to use.
*/
@ -109,6 +115,14 @@ public class CassandraProperties {
*/
private final Controlconnection controlconnection = new Controlconnection();
public Resource getConfig() {
return this.config;
}
public void setConfig(Resource config) {
this.config = config;
}
public String getKeyspaceName() {
return this.keyspaceName;
}
@ -206,13 +220,13 @@ public class CassandraProperties {
/**
* Timeout to use when establishing driver connections.
*/
private Duration connectTimeout = Duration.ofSeconds(5);
private Duration connectTimeout;
/**
* Timeout to use for internal queries that run as part of the initialization
* process, just after a connection is opened.
*/
private Duration initQueryTimeout = Duration.ofSeconds(5);
private Duration initQueryTimeout;
public Duration getConnectTimeout() {
return this.connectTimeout;
@ -237,7 +251,7 @@ public class CassandraProperties {
/**
* How long the driver waits for a request to complete.
*/
private Duration timeout = Duration.ofSeconds(2);
private Duration timeout;
/**
* Queries consistency level.
@ -252,7 +266,7 @@ public class CassandraProperties {
/**
* How many rows will be retrieved simultaneously in a single network roundtrip.
*/
private int pageSize = 5000;
private int pageSize;
private final Throttler throttler = new Throttler();
@ -302,13 +316,13 @@ public class CassandraProperties {
/**
* Idle timeout before an idle connection is removed.
*/
private Duration idleTimeout = Duration.ofSeconds(5);
private Duration idleTimeout;
/**
* Heartbeat interval after which a message is sent on an idle connection to make
* sure it's still alive.
*/
private Duration heartbeatInterval = Duration.ofSeconds(30);
private Duration heartbeatInterval;
public Duration getIdleTimeout() {
return this.idleTimeout;
@ -350,7 +364,7 @@ public class CassandraProperties {
/**
* Request throttling type.
*/
private ThrottlerType type = ThrottlerType.NONE;
private ThrottlerType type;
/**
* Maximum number of requests that can be enqueued when the throttling threshold

@ -511,6 +511,14 @@
"name": "spring.data.cassandra.compression",
"defaultValue": "none"
},
{
"name": "spring.data.cassandra.connection.connection-timeout",
"defaultValue": "5s"
},
{
"name": "spring.data.cassandra.connection.init-query-timeout",
"defaultValue": "5s"
},
{
"name": "spring.data.cassandra.contact-points",
"defaultValue": [
@ -565,6 +573,14 @@
"description": "Type of Cassandra repositories to enable.",
"defaultValue": "auto"
},
{
"name": "spring.data.cassandra.request.page-size",
"defaultValue": 5000
},
{
"name": "spring.data.cassandra.request.timeout",
"defaultValue": "2s"
},
{
"name": "spring.data.cassandra.request.throttler.type",
"defaultValue": "none"
@ -577,6 +593,14 @@
"level": "error"
}
},
{
"name": "spring.data.cassandra.pool.heartbeat-interval",
"defaultValue": "30s"
},
{
"name": "spring.data.cassandra.pool.idle-timeout",
"defaultValue": "5s"
},
{
"name": "spring.data.couchbase.consistency",
"type": "org.springframework.data.couchbase.core.query.Consistency",

@ -16,10 +16,13 @@
package org.springframework.boot.autoconfigure.cassandra;
import java.time.Duration;
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.CqlSessionBuilder;
import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
import com.datastax.oss.driver.api.core.config.DriverConfig;
import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
import com.datastax.oss.driver.internal.core.session.throttling.ConcurrencyLimitingRequestThrottler;
@ -228,6 +231,31 @@ class CassandraAutoConfigurationTests {
});
}
@Test
void driverConfigLoaderWithConfigComplementSettings() {
String configLocation = "org/springframework/boot/autoconfigure/cassandra/simple.conf";
this.contextRunner.withPropertyValues("spring.data.cassandra.session-name=testcluster",
"spring.data.cassandra.config=" + configLocation).run((context) -> {
assertThat(context).hasSingleBean(DriverConfigLoader.class);
assertThat(context.getBean(DriverConfigLoader.class).getInitialConfig().getDefaultProfile()
.getString(DefaultDriverOption.SESSION_NAME)).isEqualTo("testcluster");
assertThat(context.getBean(DriverConfigLoader.class).getInitialConfig().getDefaultProfile()
.getDuration(DefaultDriverOption.REQUEST_TIMEOUT)).isEqualTo(Duration.ofMillis(500));
});
}
@Test
void driverConfigLoaderWithConfigCreateProfiles() {
String configLocation = "org/springframework/boot/autoconfigure/cassandra/profiles.conf";
this.contextRunner.withPropertyValues("spring.data.cassandra.config=" + configLocation).run((context) -> {
assertThat(context).hasSingleBean(DriverConfigLoader.class);
DriverConfig driverConfig = context.getBean(DriverConfigLoader.class).getInitialConfig();
assertThat(driverConfig.getProfiles()).containsOnlyKeys("default", "first", "second");
assertThat(driverConfig.getProfile("first").getDuration(DefaultDriverOption.REQUEST_TIMEOUT))
.isEqualTo(Duration.ofMillis(100));
});
}
@Configuration(proxyBeanMethods = false)
static class SimpleDriverConfigLoaderBuilderCustomizerConfig {

@ -16,6 +16,8 @@
package org.springframework.boot.autoconfigure.cassandra;
import java.time.Duration;
import com.datastax.oss.driver.api.core.config.OptionsMap;
import com.datastax.oss.driver.api.core.config.TypedDriverOption;
import org.junit.jupiter.api.Test;
@ -26,27 +28,34 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link CassandraProperties}.
*
* @author Chris Bono
* @author Stephane Nicoll
*/
class CassandraPropertiesTests {
/**
* To let a configuration file override values, {@link CassandraProperties} can't have
* any default hardcoded. This test makes sure that the default that we moved to
* manual meta-data are accurate.
*/
@Test
void defaultValuesAreConsistent() {
CassandraProperties properties = new CassandraProperties();
void defaultValuesInManualMetadataAreConsistent() {
OptionsMap driverDefaults = OptionsMap.driverDefaults();
assertThat(properties.getConnection().getConnectTimeout())
.isEqualTo(driverDefaults.get(TypedDriverOption.CONNECTION_CONNECT_TIMEOUT));
assertThat(properties.getConnection().getInitQueryTimeout())
.isEqualTo(driverDefaults.get(TypedDriverOption.CONNECTION_INIT_QUERY_TIMEOUT));
assertThat(properties.getRequest().getTimeout())
.isEqualTo(driverDefaults.get(TypedDriverOption.REQUEST_TIMEOUT));
assertThat(properties.getRequest().getPageSize())
.isEqualTo(driverDefaults.get(TypedDriverOption.REQUEST_PAGE_SIZE));
assertThat(properties.getRequest().getThrottler().getType().type())
.isEqualTo(driverDefaults.get(TypedDriverOption.REQUEST_THROTTLER_CLASS));
assertThat(properties.getPool().getHeartbeatInterval())
.isEqualTo(driverDefaults.get(TypedDriverOption.HEARTBEAT_INTERVAL));
assertThat(properties.getPool().getIdleTimeout())
.isEqualTo(driverDefaults.get(TypedDriverOption.HEARTBEAT_TIMEOUT));
// spring.data.cassandra.connection.connection-timeout
assertThat(driverDefaults.get(TypedDriverOption.CONNECTION_CONNECT_TIMEOUT)).isEqualTo(Duration.ofSeconds(5));
// spring.data.cassandra.connection.init-query-timeout
assertThat(driverDefaults.get(TypedDriverOption.CONNECTION_INIT_QUERY_TIMEOUT))
.isEqualTo(Duration.ofSeconds(5));
// spring.data.cassandra.request.timeout
assertThat(driverDefaults.get(TypedDriverOption.REQUEST_TIMEOUT)).isEqualTo(Duration.ofSeconds(2));
// spring.data.cassandra.request.page-size
assertThat(driverDefaults.get(TypedDriverOption.REQUEST_PAGE_SIZE)).isEqualTo(5000);
// spring.data.cassandra.request.throttler.type
assertThat(driverDefaults.get(TypedDriverOption.REQUEST_THROTTLER_CLASS))
.isEqualTo("PassThroughRequestThrottler"); // "none"
// spring.data.cassandra.pool.heartbeat-interval
assertThat(driverDefaults.get(TypedDriverOption.HEARTBEAT_INTERVAL)).isEqualTo(Duration.ofSeconds(30));
// spring.data.cassandra.pool.idle-timeout
assertThat(driverDefaults.get(TypedDriverOption.HEARTBEAT_TIMEOUT)).isEqualTo(Duration.ofSeconds(5));
}
}

@ -0,0 +1,12 @@
datastax-java-driver {
profiles {
first {
basic.request.timeout = 100 milliseconds
basic.request.consistency = ONE
}
second {
basic.request.timeout = 5 seconds
basic.request.consistency = QUORUM
}
}
}

@ -0,0 +1,6 @@
datastax-java-driver {
basic {
session-name = Test session
request.timeout = 500 milliseconds
}
}

@ -4554,7 +4554,9 @@ If you need to configure the port, use `spring.data.cassandra.port`.
====
The Cassandra driver has its own configuration infrastructure that loads an `application.conf` at the root of the classpath.
Spring Boot does not look for such a file and rather provides a number of configuration properties via the `spring.data.cassandra.*` namespace.
Spring Boot does not look for such a file by default but can load one using `spring.data.cassandra.config`.
If a property is both present in `+spring.data.cassandra.*+` and the configuration file, the value in `+spring.data.cassandra.*+` takes precedence.
For more advanced driver customizations, you can register an arbitrary number of beans that implement `DriverConfigLoaderBuilderCustomizer`.
The `CqlSession` can be customized with a bean of type `CqlSessionBuilderCustomizer`.
====

Loading…
Cancel
Save