Create spring-boot-cassandra module
This commit is contained in:
committed by
Phillip Webb
parent
42f12380da
commit
d75e5bef41
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.boot.cassandra.autoconfigure.CassandraAutoConfiguration
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user