Create spring-boot-cassandra module

This commit is contained in:
Andy Wilkinson
2025-04-23 10:02:17 +01:00
committed by Phillip Webb
parent 42f12380da
commit d75e5bef41
57 changed files with 188 additions and 143 deletions

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://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 org.springframework.boot.cassandra.autoconfigure;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
import org.junit.jupiter.api.Test;
import org.testcontainers.cassandra.CassandraContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.spy;
/**
* Integration tests for {@link CassandraAutoConfiguration}.
*
* @author Andy Wilkinson
*/
@Testcontainers(disabledWithoutDocker = true)
class CassandraAutoConfigurationIntegrationTests {
@Container
static final CassandraContainer cassandra = TestImage.container(CassandraContainer.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CassandraAutoConfiguration.class))
.withPropertyValues(
"spring.cassandra.contact-points:" + cassandra.getHost() + ":" + cassandra.getFirstMappedPort(),
"spring.cassandra.local-datacenter=datacenter1", "spring.cassandra.connection.connect-timeout=60s",
"spring.cassandra.connection.init-query-timeout=60s", "spring.cassandra.request.timeout=60s");
@Test
void whenTheContextIsClosedThenTheDriverConfigLoaderIsClosed() {
this.contextRunner.withUserConfiguration(DriverConfigLoaderSpyConfiguration.class).run((context) -> {
assertThat(((BeanDefinitionRegistry) context.getSourceApplicationContext())
.getBeanDefinition("cassandraDriverConfigLoader")
.getDestroyMethodName()).isEmpty();
// Initialize lazy bean
context.getBean(CqlSession.class);
DriverConfigLoader driverConfigLoader = context.getBean(DriverConfigLoader.class);
context.close();
then(driverConfigLoader).should().close();
});
}
@Configuration(proxyBeanMethods = false)
static class DriverConfigLoaderSpyConfiguration {
@Bean
static BeanPostProcessor driverConfigLoaderSpy() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof DriverConfigLoader) {
return spy(bean);
}
return bean;
}
};
}
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://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 org.springframework.boot.cassandra.autoconfigure;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import com.datastax.oss.driver.api.core.ConsistencyLevel;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.CqlSessionBuilder;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
import org.junit.jupiter.api.Test;
import org.rnorth.ducttape.TimeoutException;
import org.rnorth.ducttape.unreliables.Unreliables;
import org.testcontainers.cassandra.CassandraContainer;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;
import org.testcontainers.images.builder.Transferable;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link CassandraAutoConfiguration} that only uses password authentication.
*
* @author Stephane Nicoll
*/
@Testcontainers(disabledWithoutDocker = true)
class CassandraAutoConfigurationWithPasswordAuthenticationIntegrationTests {
@Container
static final CassandraContainer cassandra = TestImage.container(PasswordAuthenticatorCassandraContainer.class)
.withStartupAttempts(5)
.waitingFor(new CassandraWaitStrategy());
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CassandraAutoConfiguration.class))
.withPropertyValues(
"spring.cassandra.contact-points:" + cassandra.getHost() + ":" + cassandra.getFirstMappedPort(),
"spring.cassandra.local-datacenter=datacenter1", "spring.cassandra.connection.connect-timeout=60s",
"spring.cassandra.connection.init-query-timeout=60s", "spring.cassandra.request.timeout=60s");
@Test
void authenticationWithValidUsernameAndPassword() {
this.contextRunner
.withPropertyValues("spring.cassandra.username=cassandra", "spring.cassandra.password=cassandra")
.run((context) -> {
SimpleStatement select = SimpleStatement.newInstance("SELECT release_version FROM system.local")
.setConsistencyLevel(ConsistencyLevel.LOCAL_ONE);
assertThat(context.getBean(CqlSession.class).execute(select).one()).isNotNull();
});
}
@Test
void authenticationWithInvalidCredentials() {
this.contextRunner
.withPropertyValues("spring.cassandra.username=not-a-user", "spring.cassandra.password=invalid-password")
.run((context) -> assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> context.getBean(CqlSession.class))
.withMessageContaining("Authentication error"));
}
static final class PasswordAuthenticatorCassandraContainer extends CassandraContainer {
PasswordAuthenticatorCassandraContainer(DockerImageName dockerImageName) {
super(dockerImageName);
}
@Override
protected void containerIsCreated(String containerId) {
String config = copyFileFromContainer("/etc/cassandra/cassandra.yaml",
(stream) -> StreamUtils.copyToString(stream, StandardCharsets.UTF_8));
String updatedConfig = config.replace("authenticator: AllowAllAuthenticator",
"authenticator: PasswordAuthenticator");
copyFileToContainer(Transferable.of(updatedConfig.getBytes(StandardCharsets.UTF_8)),
"/etc/cassandra/cassandra.yaml");
}
}
static final class CassandraWaitStrategy extends AbstractWaitStrategy {
@Override
protected void waitUntilReady() {
try {
Unreliables.retryUntilSuccess((int) this.startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> {
getRateLimiter().doWhenReady(() -> cqlSessionBuilder().build());
return true;
});
}
catch (TimeoutException ex) {
throw new ContainerLaunchException(
"Timed out waiting for Cassandra to be accessible for query execution");
}
}
private CqlSessionBuilder cqlSessionBuilder() {
return CqlSession.builder()
.addContactPoint(new InetSocketAddress(this.waitStrategyTarget.getHost(),
this.waitStrategyTarget.getFirstMappedPort()))
.withLocalDatacenter("datacenter1")
.withAuthCredentials("cassandra", "cassandra");
}
}
}

View File

@@ -0,0 +1,367 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://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 org.springframework.boot.cassandra.autoconfigure;
import java.io.IOException;
import java.time.Duration;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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.DriverConfigLoader;
import com.datastax.oss.driver.api.core.config.DriverOption;
import com.datastax.oss.driver.api.core.config.ProgrammaticDriverConfigLoaderBuilder;
import com.datastax.oss.driver.api.core.ssl.ProgrammaticSslEngineFactory;
import com.datastax.oss.driver.internal.core.config.typesafe.DefaultDriverConfigLoader;
import com.datastax.oss.driver.internal.core.config.typesafe.DefaultProgrammaticDriverConfigLoaderBuilder;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.cassandra.autoconfigure.CassandraProperties.Connection;
import org.springframework.boot.cassandra.autoconfigure.CassandraProperties.Controlconnection;
import org.springframework.boot.cassandra.autoconfigure.CassandraProperties.Request;
import org.springframework.boot.cassandra.autoconfigure.CassandraProperties.Ssl;
import org.springframework.boot.cassandra.autoconfigure.CassandraProperties.Throttler;
import org.springframework.boot.cassandra.autoconfigure.CassandraProperties.ThrottlerType;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Cassandra.
*
* @author Julien Dubois
* @author Phillip Webb
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Steffen F. Qvistgaard
* @author Ittay Stern
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnClass(CqlSession.class)
@EnableConfigurationProperties(CassandraProperties.class)
public class CassandraAutoConfiguration {
private static final Config SPRING_BOOT_DEFAULTS;
static {
CassandraDriverOptions options = new CassandraDriverOptions();
options.add(DefaultDriverOption.CONTACT_POINTS, Collections.singletonList("127.0.0.1:9042"));
options.add(DefaultDriverOption.PROTOCOL_COMPRESSION, "none");
options.add(DefaultDriverOption.CONTROL_CONNECTION_TIMEOUT, (int) Duration.ofSeconds(5).toMillis());
SPRING_BOOT_DEFAULTS = options.build();
}
private final CassandraProperties properties;
CassandraAutoConfiguration(CassandraProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean(CassandraConnectionDetails.class)
PropertiesCassandraConnectionDetails cassandraConnectionDetails(ObjectProvider<SslBundles> sslBundles) {
return new PropertiesCassandraConnectionDetails(this.properties, sslBundles.getIfAvailable());
}
@Bean
@ConditionalOnMissingBean
@Lazy
public CqlSession cassandraSession(CqlSessionBuilder cqlSessionBuilder) {
return cqlSessionBuilder.build();
}
@Bean
@ConditionalOnMissingBean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public CqlSessionBuilder cassandraSessionBuilder(DriverConfigLoader driverConfigLoader,
CassandraConnectionDetails connectionDetails,
ObjectProvider<CqlSessionBuilderCustomizer> builderCustomizers) {
CqlSessionBuilder builder = CqlSession.builder().withConfigLoader(driverConfigLoader);
configureAuthentication(builder, connectionDetails);
configureSsl(builder, connectionDetails);
builder.withKeyspace(this.properties.getKeyspaceName());
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder;
}
private void configureAuthentication(CqlSessionBuilder builder, CassandraConnectionDetails connectionDetails) {
String username = connectionDetails.getUsername();
if (username != null) {
builder.withAuthCredentials(username, connectionDetails.getPassword());
}
}
private void configureSsl(CqlSessionBuilder builder, CassandraConnectionDetails connectionDetails) {
SslBundle sslBundle = connectionDetails.getSslBundle();
if (sslBundle == null) {
return;
}
SslOptions options = sslBundle.getOptions();
Assert.state(options.getEnabledProtocols() == null, "SSL protocol options cannot be specified with Cassandra");
builder
.withSslEngineFactory(new ProgrammaticSslEngineFactory(sslBundle.createSslContext(), options.getCiphers()));
}
@Bean(destroyMethod = "")
@ConditionalOnMissingBean
public DriverConfigLoader cassandraDriverConfigLoader(CassandraConnectionDetails connectionDetails,
ObjectProvider<DriverConfigLoaderBuilderCustomizer> builderCustomizers) {
ProgrammaticDriverConfigLoaderBuilder builder = new DefaultProgrammaticDriverConfigLoaderBuilder(
() -> cassandraConfiguration(connectionDetails), DefaultDriverConfigLoader.DEFAULT_ROOT_PATH);
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
private Config cassandraConfiguration(CassandraConnectionDetails connectionDetails) {
ConfigFactory.invalidateCaches();
Config config = ConfigFactory.defaultOverrides();
config = config.withFallback(mapConfig(connectionDetails));
if (this.properties.getConfig() != null) {
config = config.withFallback(loadConfig(this.properties.getConfig()));
}
config = config.withFallback(SPRING_BOOT_DEFAULTS);
config = config.withFallback(ConfigFactory.defaultReferenceUnresolved());
return config.resolve();
}
private Config loadConfig(Resource resource) {
try {
return ConfigFactory.parseURL(resource.getURL());
}
catch (IOException ex) {
throw new IllegalStateException("Failed to load cassandra configuration from " + resource, ex);
}
}
private Config mapConfig(CassandraConnectionDetails connectionDetails) {
CassandraDriverOptions options = new CassandraDriverOptions();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.properties.getSessionName())
.whenHasText()
.to((sessionName) -> options.add(DefaultDriverOption.SESSION_NAME, sessionName));
map.from(connectionDetails.getUsername())
.to((value) -> options.add(DefaultDriverOption.AUTH_PROVIDER_USER_NAME, value)
.add(DefaultDriverOption.AUTH_PROVIDER_PASSWORD, connectionDetails.getPassword()));
map.from(this.properties::getCompression)
.to((compression) -> options.add(DefaultDriverOption.PROTOCOL_COMPRESSION, compression));
mapConnectionOptions(options);
mapPoolingOptions(options);
mapRequestOptions(options);
mapControlConnectionOptions(options);
map.from(mapContactPoints(connectionDetails))
.to((contactPoints) -> options.add(DefaultDriverOption.CONTACT_POINTS, contactPoints));
map.from(connectionDetails.getLocalDatacenter())
.whenHasText()
.to((localDatacenter) -> options.add(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, localDatacenter));
return options.build();
}
private void mapConnectionOptions(CassandraDriverOptions options) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
Connection connectionProperties = this.properties.getConnection();
map.from(connectionProperties::getConnectTimeout)
.asInt(Duration::toMillis)
.to((connectTimeout) -> options.add(DefaultDriverOption.CONNECTION_CONNECT_TIMEOUT, connectTimeout));
map.from(connectionProperties::getInitQueryTimeout)
.asInt(Duration::toMillis)
.to((initQueryTimeout) -> options.add(DefaultDriverOption.CONNECTION_INIT_QUERY_TIMEOUT, initQueryTimeout));
}
private void mapPoolingOptions(CassandraDriverOptions options) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
CassandraProperties.Pool poolProperties = this.properties.getPool();
map.from(poolProperties::getIdleTimeout)
.asInt(Duration::toMillis)
.to((idleTimeout) -> options.add(DefaultDriverOption.HEARTBEAT_TIMEOUT, idleTimeout));
map.from(poolProperties::getHeartbeatInterval)
.asInt(Duration::toMillis)
.to((heartBeatInterval) -> options.add(DefaultDriverOption.HEARTBEAT_INTERVAL, heartBeatInterval));
}
private void mapRequestOptions(CassandraDriverOptions options) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
Request requestProperties = this.properties.getRequest();
map.from(requestProperties::getTimeout)
.asInt(Duration::toMillis)
.to(((timeout) -> options.add(DefaultDriverOption.REQUEST_TIMEOUT, timeout)));
map.from(requestProperties::getConsistency)
.to(((consistency) -> options.add(DefaultDriverOption.REQUEST_CONSISTENCY, consistency)));
map.from(requestProperties::getSerialConsistency)
.to((serialConsistency) -> options.add(DefaultDriverOption.REQUEST_SERIAL_CONSISTENCY, serialConsistency));
map.from(requestProperties::getPageSize)
.to((pageSize) -> options.add(DefaultDriverOption.REQUEST_PAGE_SIZE, pageSize));
Throttler throttlerProperties = requestProperties.getThrottler();
map.from(throttlerProperties::getType)
.as(ThrottlerType::type)
.to((type) -> options.add(DefaultDriverOption.REQUEST_THROTTLER_CLASS, type));
map.from(throttlerProperties::getMaxQueueSize)
.to((maxQueueSize) -> options.add(DefaultDriverOption.REQUEST_THROTTLER_MAX_QUEUE_SIZE, maxQueueSize));
map.from(throttlerProperties::getMaxConcurrentRequests)
.to((maxConcurrentRequests) -> options.add(DefaultDriverOption.REQUEST_THROTTLER_MAX_CONCURRENT_REQUESTS,
maxConcurrentRequests));
map.from(throttlerProperties::getMaxRequestsPerSecond)
.to((maxRequestsPerSecond) -> options.add(DefaultDriverOption.REQUEST_THROTTLER_MAX_REQUESTS_PER_SECOND,
maxRequestsPerSecond));
map.from(throttlerProperties::getDrainInterval)
.asInt(Duration::toMillis)
.to((drainInterval) -> options.add(DefaultDriverOption.REQUEST_THROTTLER_DRAIN_INTERVAL, drainInterval));
}
private void mapControlConnectionOptions(CassandraDriverOptions options) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
Controlconnection controlProperties = this.properties.getControlconnection();
map.from(controlProperties::getTimeout)
.asInt(Duration::toMillis)
.to((timeout) -> options.add(DefaultDriverOption.CONTROL_CONNECTION_TIMEOUT, timeout));
}
private List<String> mapContactPoints(CassandraConnectionDetails connectionDetails) {
return connectionDetails.getContactPoints().stream().map((node) -> node.host() + ":" + node.port()).toList();
}
private static final class CassandraDriverOptions {
private final Map<String, String> options = new LinkedHashMap<>();
private CassandraDriverOptions add(DriverOption option, String value) {
String key = createKeyFor(option);
this.options.put(key, value);
return this;
}
private CassandraDriverOptions add(DriverOption option, int value) {
return add(option, String.valueOf(value));
}
private CassandraDriverOptions add(DriverOption option, Enum<?> value) {
return add(option, value.name());
}
private CassandraDriverOptions add(DriverOption option, List<String> values) {
for (int i = 0; i < values.size(); i++) {
this.options.put(String.format("%s.%s", createKeyFor(option), i), values.get(i));
}
return this;
}
private Config build() {
return ConfigFactory.parseMap(this.options, "Environment");
}
private static String createKeyFor(DriverOption option) {
return String.format("%s.%s", DefaultDriverConfigLoader.DEFAULT_ROOT_PATH, option.getPath());
}
}
/**
* Adapts {@link CassandraProperties} to {@link CassandraConnectionDetails}.
*/
static final class PropertiesCassandraConnectionDetails implements CassandraConnectionDetails {
private final CassandraProperties properties;
private final SslBundles sslBundles;
private PropertiesCassandraConnectionDetails(CassandraProperties properties, SslBundles sslBundles) {
this.properties = properties;
this.sslBundles = sslBundles;
}
@Override
public List<Node> getContactPoints() {
List<String> contactPoints = this.properties.getContactPoints();
return (contactPoints != null) ? contactPoints.stream().map(this::asNode).toList()
: Collections.emptyList();
}
@Override
public String getUsername() {
return this.properties.getUsername();
}
@Override
public String getPassword() {
return this.properties.getPassword();
}
@Override
public String getLocalDatacenter() {
return this.properties.getLocalDatacenter();
}
@Override
public SslBundle getSslBundle() {
Ssl ssl = this.properties.getSsl();
if (ssl == null || !ssl.isEnabled()) {
return null;
}
if (StringUtils.hasLength(ssl.getBundle())) {
Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
return this.sslBundles.getBundle(ssl.getBundle());
}
return SslBundle.systemDefault();
}
private Node asNode(String contactPoint) {
int i = contactPoint.lastIndexOf(':');
if (i >= 0) {
String portCandidate = contactPoint.substring(i + 1);
Integer port = asPort(portCandidate);
if (port != null) {
return new Node(contactPoint.substring(0, i), port);
}
}
return new Node(contactPoint, this.properties.getPort());
}
private Integer asPort(String value) {
try {
int i = Integer.parseInt(value);
return (i > 0 && i < 65535) ? i : null;
}
catch (Exception ex) {
return null;
}
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://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 org.springframework.boot.cassandra.autoconfigure;
import java.util.List;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.ssl.SslBundle;
/**
* Details required to establish a connection to a Cassandra service.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 4.0.0
*/
public interface CassandraConnectionDetails extends ConnectionDetails {
/**
* Cluster node addresses.
* @return the cluster node addresses
*/
List<Node> getContactPoints();
/**
* Login user of the server.
* @return the login user of the server or {@code null}
*/
default String getUsername() {
return null;
}
/**
* Login password of the server.
* @return the login password of the server or {@code null}
*/
default String getPassword() {
return null;
}
/**
* Datacenter that is considered "local". Contact points should be from this
* datacenter.
* @return the datacenter that is considered "local"
*/
String getLocalDatacenter();
/**
* SSL bundle to use.
* @return the SSL bundle to use
* @since 3.5.0
*/
default SslBundle getSslBundle() {
return null;
}
/**
* A Cassandra node.
*
* @param host the hostname
* @param port the port
*/
record Node(String host, int port) {
}
}

View File

@@ -0,0 +1,518 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://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 org.springframework.boot.cassandra.autoconfigure;
import java.time.Duration;
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.
*
* @author Julien Dubois
* @author Phillip Webb
* @author Mark Paluch
* @author Stephane Nicoll
* @author Scott Frederick
* @since 4.0.0
*/
@ConfigurationProperties("spring.cassandra")
public class CassandraProperties {
/**
* Location of the configuration file to use.
*/
private Resource config;
/**
* Keyspace name to use.
*/
private String keyspaceName;
/**
* Name of the Cassandra session.
*/
private String sessionName;
/**
* Cluster node addresses in the form 'host:port', or a simple 'host' to use the
* configured port.
*/
private List<String> contactPoints;
/**
* Port to use if a contact point does not specify one.
*/
private int port = 9042;
/**
* Datacenter that is considered "local". Contact points should be from this
* datacenter.
*/
private String localDatacenter;
/**
* Login user of the server.
*/
private String username;
/**
* Login password of the server.
*/
private String password;
/**
* Compression supported by the Cassandra binary protocol.
*/
private Compression compression;
/**
* Schema action to take at startup.
*/
private String schemaAction = "none";
/**
* SSL configuration.
*/
private Ssl ssl = new Ssl();
/**
* Connection configuration.
*/
private final Connection connection = new Connection();
/**
* Pool configuration.
*/
private final Pool pool = new Pool();
/**
* Request configuration.
*/
private final Request request = new Request();
/**
* Control connection configuration.
*/
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;
}
public void setKeyspaceName(String keyspaceName) {
this.keyspaceName = keyspaceName;
}
public String getSessionName() {
return this.sessionName;
}
public void setSessionName(String sessionName) {
this.sessionName = sessionName;
}
public List<String> getContactPoints() {
return this.contactPoints;
}
public void setContactPoints(List<String> contactPoints) {
this.contactPoints = contactPoints;
}
public int getPort() {
return this.port;
}
public void setPort(int port) {
this.port = port;
}
public String getLocalDatacenter() {
return this.localDatacenter;
}
public void setLocalDatacenter(String localDatacenter) {
this.localDatacenter = localDatacenter;
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public Compression getCompression() {
return this.compression;
}
public void setCompression(Compression compression) {
this.compression = compression;
}
public Ssl getSsl() {
return this.ssl;
}
public void setSsl(Ssl ssl) {
this.ssl = ssl;
}
public String getSchemaAction() {
return this.schemaAction;
}
public void setSchemaAction(String schemaAction) {
this.schemaAction = schemaAction;
}
public Connection getConnection() {
return this.connection;
}
public Pool getPool() {
return this.pool;
}
public Request getRequest() {
return this.request;
}
public Controlconnection getControlconnection() {
return this.controlconnection;
}
public static class Ssl {
/**
* Whether to enable SSL support.
*/
private Boolean enabled;
/**
* SSL bundle name.
*/
private String bundle;
public boolean isEnabled() {
return (this.enabled != null) ? this.enabled : this.bundle != null;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getBundle() {
return this.bundle;
}
public void setBundle(String bundle) {
this.bundle = bundle;
}
}
public static class Connection {
/**
* Timeout to use when establishing driver connections.
*/
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;
public Duration getConnectTimeout() {
return this.connectTimeout;
}
public void setConnectTimeout(Duration connectTimeout) {
this.connectTimeout = connectTimeout;
}
public Duration getInitQueryTimeout() {
return this.initQueryTimeout;
}
public void setInitQueryTimeout(Duration initQueryTimeout) {
this.initQueryTimeout = initQueryTimeout;
}
}
public static class Request {
/**
* How long the driver waits for a request to complete.
*/
private Duration timeout;
/**
* Queries consistency level.
*/
private DefaultConsistencyLevel consistency;
/**
* Queries serial consistency level.
*/
private DefaultConsistencyLevel serialConsistency;
/**
* How many rows will be retrieved simultaneously in a single network round-trip.
*/
private Integer pageSize;
private final Throttler throttler = new Throttler();
public Duration getTimeout() {
return this.timeout;
}
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
public DefaultConsistencyLevel getConsistency() {
return this.consistency;
}
public void setConsistency(DefaultConsistencyLevel consistency) {
this.consistency = consistency;
}
public DefaultConsistencyLevel getSerialConsistency() {
return this.serialConsistency;
}
public void setSerialConsistency(DefaultConsistencyLevel serialConsistency) {
this.serialConsistency = serialConsistency;
}
public Integer getPageSize() {
return this.pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public Throttler getThrottler() {
return this.throttler;
}
}
/**
* Pool properties.
*/
public static class Pool {
/**
* Idle timeout before an idle connection is removed.
*/
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;
public Duration getIdleTimeout() {
return this.idleTimeout;
}
public void setIdleTimeout(Duration idleTimeout) {
this.idleTimeout = idleTimeout;
}
public Duration getHeartbeatInterval() {
return this.heartbeatInterval;
}
public void setHeartbeatInterval(Duration heartbeatInterval) {
this.heartbeatInterval = heartbeatInterval;
}
}
public static class Controlconnection {
/**
* Timeout to use for control queries.
*/
private Duration timeout;
public Duration getTimeout() {
return this.timeout;
}
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
}
public static class Throttler {
/**
* Request throttling type.
*/
private ThrottlerType type;
/**
* Maximum number of requests that can be enqueued when the throttling threshold
* is exceeded.
*/
private Integer maxQueueSize;
/**
* Maximum number of requests that are allowed to execute in parallel.
*/
private Integer maxConcurrentRequests;
/**
* Maximum allowed request rate.
*/
private Integer maxRequestsPerSecond;
/**
* How often the throttler attempts to dequeue requests. Set this high enough that
* each attempt will process multiple entries in the queue, but not delay requests
* too much.
*/
private Duration drainInterval;
public ThrottlerType getType() {
return this.type;
}
public void setType(ThrottlerType type) {
this.type = type;
}
public Integer getMaxQueueSize() {
return this.maxQueueSize;
}
public void setMaxQueueSize(int maxQueueSize) {
this.maxQueueSize = maxQueueSize;
}
public Integer getMaxConcurrentRequests() {
return this.maxConcurrentRequests;
}
public void setMaxConcurrentRequests(int maxConcurrentRequests) {
this.maxConcurrentRequests = maxConcurrentRequests;
}
public Integer getMaxRequestsPerSecond() {
return this.maxRequestsPerSecond;
}
public void setMaxRequestsPerSecond(int maxRequestsPerSecond) {
this.maxRequestsPerSecond = maxRequestsPerSecond;
}
public Duration getDrainInterval() {
return this.drainInterval;
}
public void setDrainInterval(Duration drainInterval) {
this.drainInterval = drainInterval;
}
}
/**
* Name of the algorithm used to compress protocol frames.
*/
public enum Compression {
/**
* Requires 'net.jpountz.lz4:lz4'.
*/
LZ4,
/**
* Requires org.xerial.snappy:snappy-java.
*/
SNAPPY,
/**
* No compression.
*/
NONE
}
public enum ThrottlerType {
/**
* Limit the number of requests that can be executed in parallel.
*/
CONCURRENCY_LIMITING("ConcurrencyLimitingRequestThrottler"),
/**
* Limits the request rate per second.
*/
RATE_LIMITING("RateLimitingRequestThrottler"),
/**
* No request throttling.
*/
NONE("PassThroughRequestThrottler");
private final String type;
ThrottlerType(String type) {
this.type = type;
}
public String type() {
return this.type;
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://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 org.springframework.boot.cassandra.autoconfigure;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.CqlSessionBuilder;
/**
* Callback interface that can be implemented by beans wishing to customize the
* {@link CqlSession} through a {@link CqlSessionBuilder} whilst retaining default
* auto-configuration.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
@FunctionalInterface
public interface CqlSessionBuilderCustomizer {
/**
* Customize the {@link CqlSessionBuilder}.
* @param cqlSessionBuilder the builder to customize
*/
void customize(CqlSessionBuilder cqlSessionBuilder);
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://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 org.springframework.boot.cassandra.autoconfigure;
import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
import com.datastax.oss.driver.api.core.config.ProgrammaticDriverConfigLoaderBuilder;
/**
* Callback interface that can be implemented by beans wishing to customize the
* {@link DriverConfigLoader} through a {@link DriverConfigLoaderBuilderCustomizer} whilst
* retaining default auto-configuration.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
public interface DriverConfigLoaderBuilderCustomizer {
/**
* Customize the {@linkplain ProgrammaticDriverConfigLoaderBuilder DriverConfigLoader
* builder}.
* @param builder the builder to customize
*/
void customize(ProgrammaticDriverConfigLoaderBuilder builder);
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://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.
*/
/**
* Auto-configuration for Cassandra.
*/
package org.springframework.boot.cassandra.autoconfigure;

View File

@@ -0,0 +1,68 @@
{
"groups": [],
"properties": [
{
"name": "spring.cassandra.compression",
"defaultValue": "none"
},
{
"name": "spring.cassandra.connection.connect-timeout",
"defaultValue": "5s"
},
{
"name": "spring.cassandra.connection.init-query-timeout",
"defaultValue": "5s"
},
{
"name": "spring.cassandra.contact-points",
"defaultValue": [
"127.0.0.1:9042"
]
},
{
"name": "spring.cassandra.controlconnection.timeout",
"defaultValue": "5s"
},
{
"name": "spring.cassandra.pool.heartbeat-interval",
"defaultValue": "30s"
},
{
"name": "spring.cassandra.pool.idle-timeout",
"defaultValue": "5s"
},
{
"name": "spring.cassandra.request.page-size",
"defaultValue": 5000
},
{
"name": "spring.cassandra.request.throttler.type",
"defaultValue": "none"
},
{
"name": "spring.cassandra.request.timeout",
"defaultValue": "2s"
},
{
"name": "spring.cassandra.ssl",
"type": "java.lang.Boolean",
"deprecation": {
"replacement": "spring.cassandra.ssl.enabled",
"level": "error"
}
}
],
"hints": [
{
"name": "spring.cassandra.schema-action",
"providers": [
{
"name": "handle-as",
"parameters": {
"target": "org.springframework.data.cassandra.config.SchemaAction"
}
}
]
}
]
}

View File

@@ -0,0 +1 @@
org.springframework.boot.cassandra.autoconfigure.CassandraAutoConfiguration

View File

@@ -0,0 +1,449 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://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 org.springframework.boot.cassandra.autoconfigure;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
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;
import com.datastax.oss.driver.internal.core.session.throttling.PassThroughRequestThrottler;
import com.datastax.oss.driver.internal.core.session.throttling.RateLimitingRequestThrottler;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.cassandra.autoconfigure.CassandraAutoConfiguration.PropertiesCassandraConnectionDetails;
import org.springframework.boot.ssl.NoSuchSslBundleException;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link CassandraAutoConfiguration}
*
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Ittay Stern
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
*/
class CassandraAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CassandraAutoConfiguration.class, SslAutoConfiguration.class));
@Test
void cqlSessionBuildHasScopePrototype() {
this.contextRunner.run((context) -> {
CqlIdentifier keyspace = CqlIdentifier.fromCql("test");
CqlSessionBuilder firstBuilder = context.getBean(CqlSessionBuilder.class);
assertThat(firstBuilder.withKeyspace(keyspace)).hasFieldOrPropertyWithValue("keyspace", keyspace);
CqlSessionBuilder secondBuilder = context.getBean(CqlSessionBuilder.class);
assertThat(secondBuilder).hasFieldOrPropertyWithValue("keyspace", null);
});
}
@Test
void cqlSessionBuilderWithNoSslConfiguration() {
this.contextRunner.run((context) -> {
CqlSessionBuilder builder = context.getBean(CqlSessionBuilder.class);
assertThat(builder).hasFieldOrPropertyWithValue("programmaticSslFactory", false);
});
}
@Test
void cqlSessionBuilderWithSslEnabled() {
this.contextRunner.withPropertyValues("spring.cassandra.ssl.enabled=true").run((context) -> {
CqlSessionBuilder builder = context.getBean(CqlSessionBuilder.class);
assertThat(builder).hasFieldOrPropertyWithValue("programmaticSslFactory", true);
});
}
@Test
@WithPackageResources("test.jks")
void cqlSessionBuilderWithSslBundle() {
this.contextRunner
.withPropertyValues("spring.cassandra.ssl.bundle=test-bundle",
"spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:test.jks",
"spring.ssl.bundle.jks.test-bundle.keystore.password=secret",
"spring.ssl.bundle.jks.test-bundle.key.password=password")
.run((context) -> {
CqlSessionBuilder builder = context.getBean(CqlSessionBuilder.class);
assertThat(builder).hasFieldOrPropertyWithValue("programmaticSslFactory", true);
});
}
@Test
void cqlSessionBuilderWithSslBundleAndSslDisabled() {
this.contextRunner
.withPropertyValues("spring.cassandra.ssl.enabled=false", "spring.cassandra.ssl.bundle=test-bundle")
.run((context) -> {
CqlSessionBuilder builder = context.getBean(CqlSessionBuilder.class);
assertThat(builder).hasFieldOrPropertyWithValue("programmaticSslFactory", false);
});
}
@Test
void cqlSessionBuilderWithInvalidSslBundle() {
this.contextRunner.withPropertyValues("spring.cassandra.ssl.bundle=test-bundle")
.run((context) -> assertThatException().isThrownBy(() -> context.getBean(CqlSessionBuilder.class))
.withRootCauseInstanceOf(NoSuchSslBundleException.class)
.withMessageContaining("test-bundle"));
}
@Test
void driverConfigLoaderWithDefaultConfiguration() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(DriverConfigLoader.class);
assertThat(context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile()
.isDefined(DefaultDriverOption.SESSION_NAME)).isFalse();
});
}
@Test
void driverConfigLoaderWithContactPoints() {
this.contextRunner
.withPropertyValues("spring.cassandra.contact-points=cluster.example.com:9042",
"spring.cassandra.local-datacenter=cassandra-eu1")
.run((context) -> {
assertThat(context).hasSingleBean(DriverConfigLoader.class);
DriverExecutionProfile configuration = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(configuration.getStringList(DefaultDriverOption.CONTACT_POINTS))
.containsOnly("cluster.example.com:9042");
assertThat(configuration.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER))
.isEqualTo("cassandra-eu1");
});
}
@Test
void definesPropertiesBasedConnectionDetailsByDefault() {
this.contextRunner
.run((context) -> assertThat(context).hasSingleBean(PropertiesCassandraConnectionDetails.class));
}
@Test
void shouldUseCustomConnectionDetailsWhenDefined() {
this.contextRunner
.withPropertyValues("spring.cassandra.contact-points=localhost:9042", "spring.cassandra.username=a-user",
"spring.cassandra.password=a-password", "spring.cassandra.local-datacenter=some-datacenter")
.withBean(CassandraConnectionDetails.class, this::cassandraConnectionDetails)
.run((context) -> {
assertThat(context).hasSingleBean(DriverConfigLoader.class)
.hasSingleBean(CassandraConnectionDetails.class)
.doesNotHaveBean(PropertiesCassandraConnectionDetails.class);
DriverExecutionProfile configuration = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(configuration.getStringList(DefaultDriverOption.CONTACT_POINTS))
.containsOnly("cassandra.example.com:9042");
assertThat(configuration.getString(DefaultDriverOption.AUTH_PROVIDER_USER_NAME)).isEqualTo("user-1");
assertThat(configuration.getString(DefaultDriverOption.AUTH_PROVIDER_PASSWORD)).isEqualTo("secret-1");
assertThat(configuration.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER))
.isEqualTo("datacenter-1");
});
}
@Test
void driverConfigLoaderWithContactPointAndNoPort() {
this.contextRunner
.withPropertyValues("spring.cassandra.contact-points=cluster.example.com,another.example.com:9041",
"spring.cassandra.local-datacenter=cassandra-eu1")
.run((context) -> {
assertThat(context).hasSingleBean(DriverConfigLoader.class);
DriverExecutionProfile configuration = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(configuration.getStringList(DefaultDriverOption.CONTACT_POINTS))
.containsOnly("cluster.example.com:9042", "another.example.com:9041");
assertThat(configuration.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER))
.isEqualTo("cassandra-eu1");
});
}
@Test
void driverConfigLoaderWithContactPointAndNoPortAndCustomPort() {
this.contextRunner
.withPropertyValues("spring.cassandra.contact-points=cluster.example.com:9041,another.example.com",
"spring.cassandra.port=9043", "spring.cassandra.local-datacenter=cassandra-eu1")
.run((context) -> {
assertThat(context).hasSingleBean(DriverConfigLoader.class);
DriverExecutionProfile configuration = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(configuration.getStringList(DefaultDriverOption.CONTACT_POINTS))
.containsOnly("cluster.example.com:9041", "another.example.com:9043");
assertThat(configuration.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER))
.isEqualTo("cassandra-eu1");
});
}
@Test
void driverConfigLoaderWithCustomSessionName() {
this.contextRunner.withPropertyValues("spring.cassandra.session-name=testcluster").run((context) -> {
assertThat(context).hasSingleBean(DriverConfigLoader.class);
assertThat(context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile()
.getString(DefaultDriverOption.SESSION_NAME)).isEqualTo("testcluster");
});
}
@Test
void driverConfigLoaderWithCustomSessionNameAndCustomizer() {
this.contextRunner.withUserConfiguration(SimpleDriverConfigLoaderBuilderCustomizerConfig.class)
.withPropertyValues("spring.cassandra.session-name=testcluster")
.run((context) -> {
assertThat(context).hasSingleBean(DriverConfigLoader.class);
assertThat(context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile()
.getString(DefaultDriverOption.SESSION_NAME)).isEqualTo("overridden-name");
});
}
@Test
void driverConfigLoaderCustomizeConnectionOptions() {
this.contextRunner
.withPropertyValues("spring.cassandra.connection.connect-timeout=200ms",
"spring.cassandra.connection.init-query-timeout=10")
.run((context) -> {
DriverExecutionProfile config = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(config.getInt(DefaultDriverOption.CONNECTION_CONNECT_TIMEOUT)).isEqualTo(200);
assertThat(config.getInt(DefaultDriverOption.CONNECTION_INIT_QUERY_TIMEOUT)).isEqualTo(10);
});
}
@Test
void driverConfigLoaderCustomizePoolOptions() {
this.contextRunner
.withPropertyValues("spring.cassandra.pool.idle-timeout=42", "spring.cassandra.pool.heartbeat-interval=62")
.run((context) -> {
DriverExecutionProfile config = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(config.getInt(DefaultDriverOption.HEARTBEAT_TIMEOUT)).isEqualTo(42);
assertThat(config.getInt(DefaultDriverOption.HEARTBEAT_INTERVAL)).isEqualTo(62);
});
}
@Test
void driverConfigLoaderCustomizeRequestOptions() {
this.contextRunner
.withPropertyValues("spring.cassandra.request.timeout=5s", "spring.cassandra.request.consistency=two",
"spring.cassandra.request.serial-consistency=quorum", "spring.cassandra.request.page-size=42")
.run((context) -> {
DriverExecutionProfile config = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(config.getInt(DefaultDriverOption.REQUEST_TIMEOUT)).isEqualTo(5000);
assertThat(config.getString(DefaultDriverOption.REQUEST_CONSISTENCY)).isEqualTo("TWO");
assertThat(config.getString(DefaultDriverOption.REQUEST_SERIAL_CONSISTENCY)).isEqualTo("QUORUM");
assertThat(config.getInt(DefaultDriverOption.REQUEST_PAGE_SIZE)).isEqualTo(42);
});
}
@Test
void driverConfigLoaderCustomizeControlConnectionOptions() {
this.contextRunner.withPropertyValues("spring.cassandra.controlconnection.timeout=200ms").run((context) -> {
DriverExecutionProfile config = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(config.getInt(DefaultDriverOption.CONTROL_CONNECTION_TIMEOUT)).isEqualTo(200);
});
}
@Test
void driverConfigLoaderUsePassThroughLimitingRequestThrottlerByDefault() {
this.contextRunner.withPropertyValues().run((context) -> {
DriverExecutionProfile config = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(config.getString(DefaultDriverOption.REQUEST_THROTTLER_CLASS))
.isEqualTo(PassThroughRequestThrottler.class.getSimpleName());
});
}
@Test
void driverConfigLoaderWithRateLimitingRequiresExtraConfiguration() {
this.contextRunner.withPropertyValues("spring.cassandra.request.throttler.type=rate-limiting")
.run((context) -> assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> context.getBean(CqlSession.class))
.withMessageContaining("Error instantiating class RateLimitingRequestThrottler")
.withMessageContaining("No configuration setting found for key"));
}
@Test
void driverConfigLoaderCustomizeConcurrencyLimitingRequestThrottler() {
this.contextRunner
.withPropertyValues("spring.cassandra.request.throttler.type=concurrency-limiting",
"spring.cassandra.request.throttler.max-concurrent-requests=62",
"spring.cassandra.request.throttler.max-queue-size=72")
.run((context) -> {
DriverExecutionProfile config = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(config.getString(DefaultDriverOption.REQUEST_THROTTLER_CLASS))
.isEqualTo(ConcurrencyLimitingRequestThrottler.class.getSimpleName());
assertThat(config.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_CONCURRENT_REQUESTS)).isEqualTo(62);
assertThat(config.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_QUEUE_SIZE)).isEqualTo(72);
});
}
@Test
void driverConfigLoaderCustomizeRateLimitingRequestThrottler() {
this.contextRunner
.withPropertyValues("spring.cassandra.request.throttler.type=rate-limiting",
"spring.cassandra.request.throttler.max-requests-per-second=62",
"spring.cassandra.request.throttler.max-queue-size=72",
"spring.cassandra.request.throttler.drain-interval=16ms")
.run((context) -> {
DriverExecutionProfile config = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(config.getString(DefaultDriverOption.REQUEST_THROTTLER_CLASS))
.isEqualTo(RateLimitingRequestThrottler.class.getSimpleName());
assertThat(config.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_REQUESTS_PER_SECOND)).isEqualTo(62);
assertThat(config.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_QUEUE_SIZE)).isEqualTo(72);
assertThat(config.getInt(DefaultDriverOption.REQUEST_THROTTLER_DRAIN_INTERVAL)).isEqualTo(16);
});
}
@Test
void driverConfigLoaderWithConfigComplementSettings() {
String configLocation = "org/springframework/boot/cassandra/autoconfigure/simple.conf";
this.contextRunner
.withPropertyValues("spring.cassandra.session-name=testcluster",
"spring.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 // gh-31238
void driverConfigLoaderWithConfigOverridesDefaults() {
String configLocation = "org/springframework/boot/cassandra/autoconfigure/override-defaults.conf";
this.contextRunner.withPropertyValues("spring.cassandra.config=" + configLocation).run((context) -> {
DriverExecutionProfile actual = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(actual.getString(DefaultDriverOption.SESSION_NAME)).isEqualTo("advanced session");
assertThat(actual.getDuration(DefaultDriverOption.REQUEST_TIMEOUT)).isEqualTo(Duration.ofSeconds(2));
assertThat(actual.getStringList(DefaultDriverOption.CONTACT_POINTS))
.isEqualTo(Collections.singletonList("1.2.3.4:5678"));
assertThat(actual.getBoolean(DefaultDriverOption.RESOLVE_CONTACT_POINTS)).isFalse();
assertThat(actual.getInt(DefaultDriverOption.REQUEST_PAGE_SIZE)).isEqualTo(11);
assertThat(actual.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER)).isEqualTo("datacenter1");
assertThat(actual.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_CONCURRENT_REQUESTS)).isEqualTo(22);
assertThat(actual.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_REQUESTS_PER_SECOND)).isEqualTo(33);
assertThat(actual.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_QUEUE_SIZE)).isEqualTo(44);
assertThat(actual.getDuration(DefaultDriverOption.CONTROL_CONNECTION_TIMEOUT))
.isEqualTo(Duration.ofMillis(5555));
assertThat(actual.getString(DefaultDriverOption.PROTOCOL_COMPRESSION)).isEqualTo("SNAPPY");
});
}
@Test
void placeholdersInReferenceConfAreResolvedAgainstConfigDerivedFromSpringCassandraProperties() {
this.contextRunner.withPropertyValues("spring.cassandra.request.timeout=60s").run((context) -> {
DriverExecutionProfile actual = context.getBean(DriverConfigLoader.class)
.getInitialConfig()
.getDefaultProfile();
assertThat(actual.getDuration(DefaultDriverOption.REQUEST_TIMEOUT)).isEqualTo(Duration.ofSeconds(60));
assertThat(actual.getDuration(DefaultDriverOption.METADATA_SCHEMA_REQUEST_TIMEOUT))
.isEqualTo(Duration.ofSeconds(60));
});
}
@Test
void driverConfigLoaderWithConfigCreateProfiles() {
String configLocation = "org/springframework/boot/cassandra/autoconfigure/profiles.conf";
this.contextRunner.withPropertyValues("spring.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));
});
}
private CassandraConnectionDetails cassandraConnectionDetails() {
return new CassandraConnectionDetails() {
@Override
public List<Node> getContactPoints() {
return List.of(new Node("cassandra.example.com", 9042));
}
@Override
public String getUsername() {
return "user-1";
}
@Override
public String getPassword() {
return "secret-1";
}
@Override
public String getLocalDatacenter() {
return "datacenter-1";
}
};
}
@Configuration(proxyBeanMethods = false)
static class SimpleDriverConfigLoaderBuilderCustomizerConfig {
@Bean
DriverConfigLoaderBuilderCustomizer customizer() {
return (builder) -> builder.withString(DefaultDriverOption.SESSION_NAME, "overridden-name");
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://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 org.springframework.boot.cassandra.autoconfigure;
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;
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 defaultValuesInManualMetadataAreConsistent() {
OptionsMap driverDefaults = OptionsMap.driverDefaults();
// spring.cassandra.connection.connect-timeout
assertThat(driverDefaults.get(TypedDriverOption.CONNECTION_CONNECT_TIMEOUT)).isEqualTo(Duration.ofSeconds(5));
// spring.cassandra.connection.init-query-timeout
assertThat(driverDefaults.get(TypedDriverOption.CONNECTION_INIT_QUERY_TIMEOUT))
.isEqualTo(Duration.ofSeconds(5));
// spring.cassandra.request.timeout
assertThat(driverDefaults.get(TypedDriverOption.REQUEST_TIMEOUT)).isEqualTo(Duration.ofSeconds(2));
// spring.cassandra.request.page-size
assertThat(driverDefaults.get(TypedDriverOption.REQUEST_PAGE_SIZE)).isEqualTo(5000);
// spring.cassandra.request.throttler.type
assertThat(driverDefaults.get(TypedDriverOption.REQUEST_THROTTLER_CLASS))
.isEqualTo("PassThroughRequestThrottler"); // "none"
// spring.cassandra.pool.heartbeat-interval
assertThat(driverDefaults.get(TypedDriverOption.HEARTBEAT_INTERVAL)).isEqualTo(Duration.ofSeconds(30));
// spring.cassandra.pool.idle-timeout
assertThat(driverDefaults.get(TypedDriverOption.HEARTBEAT_TIMEOUT)).isEqualTo(Duration.ofSeconds(5));
}
}

View File

@@ -0,0 +1,20 @@
datastax-java-driver {
basic {
session-name = advanced session
load-balancing-policy {
local-datacenter = datacenter1
}
request.page-size = 11
contact-points = [ "1.2.3.4:5678" ]
}
advanced {
throttler {
max-concurrent-requests = 22
max-requests-per-second = 33
max-queue-size = 44
}
control-connection.timeout = 5555
protocol.compression = SNAPPY
resolve-contact-points = false
}
}

View File

@@ -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
}
}
}

View File

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