Fix failing Inline Caching with Apache Cassandra Integration Tests.

This commit is contained in:
John Blum
2022-07-08 19:23:23 -07:00
parent a42efb4236
commit d040b23601
12 changed files with 304 additions and 92 deletions

View File

@@ -27,6 +27,7 @@ dependencies {
testCompile "org.mockito:mockito-core"
testCompile "org.projectlombok:lombok"
testCompile "org.testcontainers:testcontainers"
testCompile "org.testcontainers:cassandra"
testCompile "edu.umd.cs.mtc:multithreadedtc"
testCompile("org.springframework.boot:spring-boot-starter-test") {

View File

@@ -15,96 +15,198 @@
*/
package example.app.crm.config;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newRuntimeException;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Optional;
import java.util.function.Consumer;
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.session.Session;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.data.cassandra.config.AbstractCassandraConfiguration;
import org.springframework.data.cassandra.SessionFactory;
import org.springframework.data.cassandra.config.CqlSessionFactoryBean;
import org.springframework.data.gemfire.tests.util.IOUtils;
import org.springframework.data.cassandra.core.CassandraTemplate;
import org.springframework.data.cassandra.core.cql.CqlTemplate;
import org.springframework.data.cassandra.core.cql.RowMapper;
import org.springframework.data.cassandra.core.cql.session.init.KeyspacePopulator;
import org.springframework.data.cassandra.core.cql.session.init.ResourceKeyspacePopulator;
import org.springframework.data.cassandra.core.cql.session.init.SessionFactoryInitializer;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import example.app.crm.model.Customer;
import example.app.crm.repo.CustomerRepository;
/**
* Base test configuration used to configure and bootstrap an Apache Cassandra database with a schema and data.
*
* @author John Blum
* @see com.datastax.oss.driver.api.core.session.Session
* @see org.springframework.beans.factory.config.BeanPostProcessor
* @see org.springframework.context.annotation.Bean
* @see org.springframework.core.io.Resource
* @see org.springframework.data.cassandra.config.AbstractCassandraConfiguration
* @see org.springframework.data.cassandra.SessionFactory
* @see org.springframework.data.cassandra.config.CqlSessionFactoryBean
* @see org.springframework.data.cassandra.core.CassandraTemplate
* @see org.springframework.data.cassandra.core.cql.CqlTemplate
* @see org.springframework.data.cassandra.core.cql.session.init.KeyspacePopulator
* @see org.springframework.data.cassandra.core.cql.session.init.SessionFactoryInitializer
* @since 1.1.0
*/
public abstract class TestCassandraConfiguration extends AbstractCassandraConfiguration {
@SuppressWarnings("unused")
public abstract class TestCassandraConfiguration {
private static final boolean CONTINUE_ON_ERROR = false;
private static final boolean IGNORE_FAILED_DROPS = true;
private static final Customer pieDoe = Customer.newCustomer(16L, "Pie Doe");
private static final String CQL_SCRIPT_ENCODING = null;
protected static final int CASSANDRA_DEFAULT_PORT = CqlSessionFactoryBean.DEFAULT_PORT;
private static final String CASSANDRA_DATA_CQL = "cassandra-data.cql";
private static final String CASSANDRA_SCHEMA_CQL = "cassandra-schema.cql";
private static final String LOCAL_DATA_CENTER = "datacenter1";
private static final String KEYSPACE_NAME = "CustomerService";
private static final String SESSION_NAME = "CustomerServiceCluster";
protected static final String CASSANDRA_DATA_CQL = "cassandra-data.cql";
protected static final String CASSANDRA_INIT_CQL = "cassandra-init.cql";
protected static final String CASSANDRA_SCHEMA_CQL = "cassandra-schema.cql";
protected static final String DEBUGGING_PROFILE = "debugging";
protected static final String KEYSPACE_NAME = "CustomerService";
protected static final String TABLE_NAME = "Customers";
@NonNull
@Override
protected String getKeyspaceName() {
return KEYSPACE_NAME;
protected @NonNull Resource newCassandraDataCqlScriptResource() {
return new ClassPathResource(CASSANDRA_DATA_CQL);
}
@Override
protected String getLocalDataCenter() {
return LOCAL_DATA_CENTER;
protected @NonNull Resource newCassandraInitCqlScriptResource() {
return new ClassPathResource(CASSANDRA_INIT_CQL);
}
@Nullable
@Override
protected String getSessionName() {
return SESSION_NAME;
protected @NonNull Resource newCassandraSchemaCqlScriptResource() {
return new ClassPathResource(CASSANDRA_SCHEMA_CQL);
}
/*
@Nullable @Override
protected KeyspacePopulator keyspacePopulator() {
return cqlSession -> loadCassandraCqlScripts().forEach(cqlSession::execute);
}
*/
@Bean
SessionFactoryInitializer sessionFactoryInitializer(SessionFactory sessionFactory) {
// TODO: Remove use of deprecation after Spring Data for Apache Cassandra issues are resolved!
@Override
protected List<String> getStartupScripts() {
SessionFactoryInitializer sessionFactoryInitializer = new SessionFactoryInitializer();
List<String> startupScripts = new ArrayList<>(super.getStartupScripts());
KeyspacePopulator keyspacePopulator = newKeyspacePopulator(newCassandraDataCqlScriptResource());
startupScripts.addAll(readLines(new ClassPathResource(CASSANDRA_SCHEMA_CQL)));
startupScripts.addAll(readLines(new ClassPathResource(CASSANDRA_DATA_CQL)));
sessionFactoryInitializer.setKeyspacePopulator(keyspacePopulator);
sessionFactoryInitializer.setSessionFactory(sessionFactory);
return startupScripts;
return sessionFactoryInitializer;
}
private @NonNull List<String> readLines(@NonNull Resource resource) {
protected @NonNull KeyspacePopulator newKeyspacePopulator(Resource... cqlScripts) {
return new ResourceKeyspacePopulator(CONTINUE_ON_ERROR, IGNORE_FAILED_DROPS, CQL_SCRIPT_ENCODING, cqlScripts);
}
BufferedReader resourceReader = null;
@Bean
@Profile(DEBUGGING_PROFILE)
BeanPostProcessor cassandraTemplatePostProcessor() {
try {
return new BeanPostProcessor() {
resourceReader = new BufferedReader(new InputStreamReader(resource.getInputStream()));
@Override
public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException {
return resourceReader.lines()
.filter(StringUtils::hasText)
.collect(Collectors.toList());
}
catch (IOException cause) {
throw newRuntimeException(cause, "Failed to read from Resource [%s]", resource);
}
finally {
IOUtils.close(resourceReader);
}
if (bean instanceof CassandraTemplate) {
CassandraTemplate cassandraTemplate = (CassandraTemplate) bean;
Consumer<CassandraTemplate> cassandraTemplateConsumer = noopCassandraTemplateConsumer()
.andThen(insertEntityObjectCassandraTemplateConsumer())
.andThen(assertEntityCountCassandraTemplateConsumer())
.andThen(assertEntityObjectCassandraTemplateConsumer())
.andThen(assertKeyspaceNameCassandraTemplateConsumer())
.andThen(assertTableNameCassandraTemplateConsumer());
cassandraTemplateConsumer.accept(cassandraTemplate);
}
return bean;
}
};
}
private Consumer<CassandraTemplate> noopCassandraTemplateConsumer() {
return cassandraTemplate -> {};
}
private Consumer<CassandraTemplate> assertEntityCountCassandraTemplateConsumer() {
return cassandraTemplate -> assertThat(cassandraTemplate.count(Customer.class)).isOne();
}
private Consumer<CassandraTemplate> assertEntityObjectCassandraTemplateConsumer() {
return cassandraTemplate -> {
String cql = "SELECT id, name FROM \"Customers\"";
RowMapper<Customer> customerRowMapper = (row, rowNumber) ->
Customer.newCustomer(row.getLong("id"), row.getString("name"));
Customer actualCustomer = cassandraTemplate.getCqlOperations().queryForObject(cql, customerRowMapper);
assertThat(actualCustomer).isEqualTo(pieDoe);
assertThat(cassandraTemplate.selectOneById(16L, Customer.class)).isEqualTo(pieDoe);
assertThat(cassandraTemplate.query(Customer.class).stream().findFirst().orElse(null)).isEqualTo(pieDoe);
};
}
private Consumer<CassandraTemplate> insertEntityObjectCassandraTemplateConsumer() {
return cassandraTemplate -> cassandraTemplate.insert(pieDoe);
}
private Consumer<CassandraTemplate> assertKeyspaceNameCassandraTemplateConsumer() {
return cassandraTemplate -> {
String resolvedKeyspaceName = Optional.of(cassandraTemplate.getCqlOperations())
.filter(CqlTemplate.class::isInstance)
.map(CqlTemplate.class::cast)
.map(CqlTemplate::getSessionFactory)
.map(SessionFactory::getSession)
.flatMap(Session::getKeyspace)
.map(CqlIdentifier::toString)
.orElse(null);
assertThat(resolvedKeyspaceName).isEqualToIgnoringCase(KEYSPACE_NAME);
};
}
private Consumer<CassandraTemplate> assertTableNameCassandraTemplateConsumer() {
return cassandraTemplate -> {
String entityTableName = cassandraTemplate.getTableName(Customer.class).toString();
assertThat(entityTableName).endsWith(TABLE_NAME);
Optional.of(cassandraTemplate.getCqlOperations())
.filter(CqlTemplate.class::isInstance)
.map(CqlTemplate.class::cast)
.map(CqlTemplate::getSessionFactory)
.map(SessionFactory::getSession)
.map(Session::getMetadata)
.flatMap(metadata -> metadata.getKeyspace(KEYSPACE_NAME))
.map(keyspaceMetadata -> keyspaceMetadata.getTable(entityTableName))
.orElseThrow(() -> new IllegalStateException(String.format("Table [%s] not found", entityTableName)));
};
}
@Bean
@Profile(DEBUGGING_PROFILE)
ApplicationListener<ContextRefreshedEvent> populateCassandraDatabaseUsingRepository(
CustomerRepository customerRepository) {
return event -> customerRepository.save(pieDoe);
}
}

View File

@@ -15,63 +15,163 @@
*/
package example.app.crm.config;
import static org.assertj.core.api.Assertions.assertThat;
import java.net.InetSocketAddress;
import java.util.Arrays;
import com.datastax.oss.driver.api.core.CqlSession;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties;
import org.springframework.boot.autoconfigure.cassandra.CqlSessionBuilderCustomizer;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.data.cassandra.core.CassandraTemplate;
import org.springframework.lang.NonNull;
import org.testcontainers.containers.CassandraContainer;
import org.testcontainers.containers.GenericContainer;
import example.app.crm.model.Customer;
/**
* Spring {@link @Configuration} for Apache Cassandra using Testcontainers.
*
* @author John Blum
* @see java.net.InetSocketAddress
* @see com.datastax.oss.driver.api.core.CqlSession
* @see org.springframework.boot.autoconfigure.cassandra.CqlSessionBuilderCustomizer
* @see org.springframework.boot.autoconfigure.domain.EntityScan
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.Profile
* @see org.testcontainers.containers.CassandraContainer
* @see org.testcontainers.containers.GenericContainer
* @since 1.1.0
*/
@Configuration
@Profile("inline-caching-cassandra")
@EntityScan(basePackageClasses = Customer.class)
@SuppressWarnings("unused")
public class TestcontainersCassandraConfiguration extends TestCassandraConfiguration {
private static final String CASSANDRA_DOCKER_IMAGE_NAME = "cassandra:latest";
private static final String LOCAL_DATACENTER_NAME = "datacenter1";
@Bean
@SuppressWarnings("rawtypes")
GenericContainer cassandraContainer() {
@Bean("CassandraContainer")
GenericContainer<?> cassandraContainer(Environment environment) {
GenericContainer cassandraContainer = newCustomCassandraContainer();
GenericContainer<?> cassandraContainer = newEnvironmentOptimizedCassandraContainer();
cassandraContainer.start();
return withCassandraServer(cassandraContainer, environment);
}
private @NonNull GenericContainer<?> newCassandraContainer() {
return new CassandraContainer<>(CASSANDRA_DOCKER_IMAGE_NAME)
.withInitScript(CASSANDRA_SCHEMA_CQL)
//.withInitScript(CASSANDRA_INIT_CQL)
.withExposedPorts(CASSANDRA_DEFAULT_PORT)
.withReuse(true);
}
// Information (feedback) received from Sergei Egorov.
private @NonNull GenericContainer<?> newEnvironmentOptimizedCassandraContainer() {
return newCassandraContainer()
.withEnv("CASSANDRA_SNITCH", "GossipingPropertyFileSnitch")
.withEnv("HEAP_NEWSIZE", "128M")
.withEnv("MAX_HEAP_SIZE", "1024M")
.withEnv("JVM_OPTS", "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0");
}
private @NonNull CassandraTemplate newCassandraTemplate(@NonNull CqlSession session) {
return new CassandraTemplate(session);
}
private @NonNull CqlSession newCqlSession(@NonNull GenericContainer<?> cassandraContainer) {
return CqlSession.builder()
.addContactPoint(resolveContactPoint(cassandraContainer))
.withLocalDatacenter(LOCAL_DATACENTER_NAME)
.build();
}
private @NonNull GenericContainer<?> withCassandraServer(@NonNull GenericContainer<?> cassandraContainer,
@NonNull Environment environment) {
if (Arrays.asList(environment.getActiveProfiles()).contains(DEBUGGING_PROFILE)) {
cassandraContainer = initializeCassandraServer(cassandraContainer);
cassandraContainer = assertCassandraServerSetup(cassandraContainer);
}
return cassandraContainer;
}
@SuppressWarnings("rawtypes")
private GenericContainer newCassandraContainer() {
return new GenericContainer(CASSANDRA_DOCKER_IMAGE_NAME)
.withExposedPorts(CASSANDRA_DEFAULT_PORT);
private @NonNull GenericContainer<?> initializeCassandraServer(@NonNull GenericContainer<?> cassandraContainer) {
try (CqlSession session = newCqlSession(cassandraContainer)) {
newKeyspacePopulator(newCassandraSchemaCqlScriptResource()).populate(session);
}
return cassandraContainer;
}
@SuppressWarnings("rawtypes")
private GenericContainer newCustomCassandraContainer() {
private @NonNull GenericContainer<?> assertCassandraServerSetup(@NonNull GenericContainer<?> cassandraContainer) {
return newCassandraContainer()
.withEnv("HEAP_NEWSIZE", "128M")
.withEnv("MAX_HEAP_SIZE", "1024M")
.withEnv("JVM_OPTS", "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0")
.withEnv("CASSANDRA_SNITCH", "GossipingPropertyFileSnitch");
try (CqlSession session = newCqlSession(cassandraContainer)) {
session.getMetadata().getKeyspace(KEYSPACE_NAME)
.map(keyspaceMetadata -> {
assertThat(keyspaceMetadata.getName().toString()).isEqualToIgnoringCase(KEYSPACE_NAME);
keyspaceMetadata.getTable(TABLE_NAME)
.map(tableMetadata -> {
assertThat(tableMetadata.getName().toString()).isEqualTo(TABLE_NAME);
assertThat(tableMetadata.getKeyspace().toString()).isEqualToIgnoringCase(KEYSPACE_NAME);
//assertCustomersTableHasSizeOne(session);
return tableMetadata;
})
.orElseThrow(() -> new IllegalStateException(String.format("Table [%s] not found", TABLE_NAME)));
return keyspaceMetadata;
})
.orElseThrow(() -> new IllegalStateException(String.format("Keyspace [%s] not found", KEYSPACE_NAME)));
}
return cassandraContainer;
}
@Override
protected String getContactPoints() {
return cassandraContainer().getContainerIpAddress();
private void assertCustomersTableHasSizeOne(@NonNull CqlSession session) {
CassandraTemplate template = newCassandraTemplate(session);
assertThat(template.getCqlOperations().execute(String.format("USE %s;", KEYSPACE_NAME))).isTrue();
assertThat(template.getCqlOperations().queryForObject("SELECT count(*) FROM \"Customers\"", Long.class)).isOne();
//assertThat(template.count(Customer.class)).isOne(); // Table Customers not found; needs to use the Keyspace
}
@Override
protected int getPort() {
return cassandraContainer().getFirstMappedPort();
@Bean
CqlSessionBuilderCustomizer cqlSessionBuilderCustomizer(CassandraProperties properties,
@Qualifier("CassandraContainer") GenericContainer<?> cassandraContainer) {
return cqlSessionBuilder -> cqlSessionBuilder
.addContactPoint(resolveContactPoint(cassandraContainer))
.withLocalDatacenter(properties.getLocalDatacenter())
.withKeyspace(properties.getKeyspaceName());
}
private @NonNull InetSocketAddress resolveContactPoint(@NonNull GenericContainer<?> cassandraContainer) {
return new InetSocketAddress(cassandraContainer.getHost(),
cassandraContainer.getMappedPort(CASSANDRA_DEFAULT_PORT));
}
}

View File

@@ -18,7 +18,6 @@ package example.app.crm.model;
import javax.persistence.Entity;
import javax.persistence.Table;
import org.springframework.data.annotation.Id;
import org.springframework.data.cassandra.core.mapping.Indexed;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.gemfire.mapping.annotation.Region;
@@ -32,8 +31,11 @@ import lombok.NoArgsConstructor;
* The {@link Customer} class is an Abstract Data Type (ADT) modeling a customer.
*
* @author John Blum
* @see lombok
* @see org.springframework.data.annotation.Id
* @see javax.persistence.Entity
* @see javax.persistence.Table
* @see org.springframework.data.cassandra.core.mapping.Indexed
* @see org.springframework.data.cassandra.core.mapping.PrimaryKey
* @see org.springframework.data.cassandra.core.mapping.Table
* @see org.springframework.data.gemfire.mapping.annotation.Region
* @since 1.1.0
*/
@@ -46,9 +48,8 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor(staticName = "newCustomer")
public class Customer {
@Id
@javax.persistence.Id
@PrimaryKey
@javax.persistence.Id
private Long id;
@Indexed

View File

@@ -41,8 +41,6 @@ import example.app.crm.repo.CustomerRepository;
@SuppressWarnings("unused")
public abstract class AbstractInlineCachingWithExternalDataSourceIntegrationTests extends IntegrationTestsSupport {
public static final String GEMFIRE_LOG_LEVEL = "off";
@Autowired
private CustomerRepository customerRepository;

View File

@@ -23,7 +23,6 @@ import org.apache.geode.cache.GemFireCache;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
@@ -60,6 +59,7 @@ import example.app.crm.repo.CustomerRepository;
* @see org.springframework.geode.cache.inline.AbstractInlineCachingWithExternalDataSourceIntegrationTests
* @see org.springframework.test.context.ActiveProfiles
* @see org.springframework.test.context.junit4.SpringRunner
* @see example.app.crm.config.TestcontainersCassandraConfiguration
* @since 1.1.0
*/
@SpringBootTest
@@ -69,8 +69,8 @@ import example.app.crm.repo.CustomerRepository;
public class InlineCachingWithCassandraIntegrationTests
extends AbstractInlineCachingWithExternalDataSourceIntegrationTests {
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, CassandraDataAutoConfiguration.class })
@ClientCacheApplication(logLevel = GEMFIRE_LOG_LEVEL)
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@ClientCacheApplication
@EnableCassandraRepositories(basePackageClasses = CustomerRepository.class)
@EnableEntityDefinedRegions(basePackageClasses = Customer.class, clientRegionShortcut = ClientRegionShortcut.LOCAL)
@Import(TestcontainersCassandraConfiguration.class)

View File

@@ -68,7 +68,7 @@ public class InlineCachingWithDatabaseIntegrationTests
extends AbstractInlineCachingWithExternalDataSourceIntegrationTests {
@SpringBootApplication(exclude = { CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class })
@ClientCacheApplication(logLevel = GEMFIRE_LOG_LEVEL)
@ClientCacheApplication
@EntityScan(basePackageClasses = Customer.class)
@EnableEntityDefinedRegions(basePackageClasses = Customer.class, clientRegionShortcut = ClientRegionShortcut.LOCAL)
@EnableJpaRepositories(basePackageClasses = CustomerRepository.class)

View File

@@ -0,0 +1,5 @@
# Spring Boot application.properties for Apache Cassandra configuration of Inline Caching
spring.data.cassandra.keyspace-name=CustomerService
spring.data.cassandra.schema-action=recreate
spring.data.cassandra.local-datacenter=datacenter1

View File

@@ -1,4 +1,4 @@
# RBMS (Database) configuration properties
# Spring Boot application.properties for RDBMS (Database) configuration of Inline Caching
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

View File

@@ -1 +1 @@
INSERT INTO customers (id, name) VALUES (16, 'Pie Doe');
INSERT INTO "Customers" (id, name) VALUES (16, 'Pie Doe');

View File

@@ -0,0 +1,5 @@
CREATE KEYSPACE IF NOT EXISTS CustomerService WITH replication = { 'class':'SimpleStrategy', 'replication_factor':1 };
USE CustomerService;
CREATE TABLE IF NOT EXISTS "Customers" (id BIGINT PRIMARY KEY, name TEXT);
CREATE INDEX IF NOT EXISTS CustomerNameIdx ON "Customers"(name);
INSERT INTO "Customers" (id, name) VALUES (16, 'Pie Doe');

View File

@@ -1,4 +1,4 @@
CREATE KEYSPACE IF NOT EXISTS CustomerService WITH replication = { 'class':'SimpleStrategy', 'replication_factor':1 };
USE CustomerService;
CREATE TABLE IF NOT EXISTS customers (id BIGINT PRIMARY KEY, name TEXT);
CREATE INDEX IF NOT EXISTS CustomerNameIdx ON customers(name);
CREATE TABLE IF NOT EXISTS "Customers" (id BIGINT PRIMARY KEY, name TEXT);
CREATE INDEX IF NOT EXISTS CustomerNameIdx ON "Customers"(name);