Add tests to assert and verify that Spring's Transaction Management Infrastructure combined with SDG's GemfireTransactionManager properly commits/rollsback in the context of concurrent client transactions.
Resolves gh-481.
This commit is contained in:
65
spring-data-geode/src/test/java/example/app/model/User.java
Normal file
65
spring-data-geode/src/test/java/example/app/model/User.java
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2020 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 example.app.model;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.gemfire.mapping.annotation.Region;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Abstract Data Type (ADT) modeling a user.
|
||||
*
|
||||
* @author John Blum
|
||||
* @see org.springframework.data.gemfire.mapping.annotation.Region
|
||||
* @since 2.5.0
|
||||
*/
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@Setter(AccessLevel.PROTECTED)
|
||||
@Region("Users")
|
||||
@NoArgsConstructor
|
||||
@RequiredArgsConstructor(staticName = "as")
|
||||
@SuppressWarnings("unused")
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
private Integer id;
|
||||
|
||||
@lombok.NonNull
|
||||
private String name;
|
||||
|
||||
public @NonNull User identifiedBy(@Nullable Integer id) {
|
||||
setId(id);
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull User withName(@NonNull String name) {
|
||||
Assert.hasText(name, String.format("Name [%s] is required", name));
|
||||
setName(name);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2020 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 example.app.repo;
|
||||
|
||||
import org.springframework.data.gemfire.repository.GemfireRepository;
|
||||
|
||||
import example.app.model.User;
|
||||
|
||||
/**
|
||||
* Spring Data [Geode] {@link GemfireRepository} used to perform basic CRUD and simple OQL query data access operations
|
||||
* on {@link User} objects to and from an Apache Geode {@link org.apache.geode.cache.GemFireCache}
|
||||
* {@link org.apache.geode.cache.Region}.
|
||||
*
|
||||
* @author John Blum
|
||||
* @see org.apache.geode.cache.GemFireCache
|
||||
* @see org.apache.geode.cache.Region
|
||||
* @see org.springframework.data.gemfire.repository.GemfireRepository
|
||||
* @see example.app.model.User
|
||||
* @since 2.5.0
|
||||
*/
|
||||
public interface UserRepository extends GemfireRepository<User, Integer> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* Copyright 2020 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.data.gemfire.transaction;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import edu.umd.cs.mtc.MultithreadedTestCase;
|
||||
import edu.umd.cs.mtc.TestFramework;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.apache.geode.cache.GemFireCache;
|
||||
import org.apache.geode.cache.RegionShortcut;
|
||||
import org.apache.geode.cache.client.ClientCache;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.gemfire.config.annotation.CacheServerApplication;
|
||||
import org.springframework.data.gemfire.config.annotation.ClientCacheApplication;
|
||||
import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions;
|
||||
import org.springframework.data.gemfire.config.annotation.EnablePdx;
|
||||
import org.springframework.data.gemfire.mapping.GemfireMappingContext;
|
||||
import org.springframework.data.gemfire.process.ProcessWrapper;
|
||||
import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories;
|
||||
import org.springframework.data.gemfire.repository.support.GemfireRepositoryFactoryBean;
|
||||
import org.springframework.data.gemfire.test.support.ClientServerIntegrationTestsSupport;
|
||||
import org.springframework.data.gemfire.transaction.config.EnableGemfireCacheTransactions;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.transaction.NoTransactionException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import example.app.model.User;
|
||||
import example.app.repo.UserRepository;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Integration Tests asserting the concurrent interaction of multiple {@link ClientCache} (client/server) transactions
|
||||
* where 1 transaction commits and the other rolls back.
|
||||
*
|
||||
* @author John Blum
|
||||
* @see org.junit.Test
|
||||
* @see edu.umd.cs.mtc.MultithreadedTestCase
|
||||
* @see edu.umd.cs.mtc.TestFramework
|
||||
* @see org.apache.geode.cache.client.ClientCache
|
||||
* @see org.springframework.data.gemfire.config.annotation.CacheServerApplication
|
||||
* @see org.springframework.data.gemfire.config.annotation.ClientCacheApplication
|
||||
* @see org.springframework.data.gemfire.mapping.annotation.Region
|
||||
* @see org.springframework.data.gemfire.repository.GemfireRepository
|
||||
* @see org.springframework.data.gemfire.repository.config.EnableGemfireRepositories
|
||||
* @see org.springframework.data.gemfire.test.support.ClientServerIntegrationTestsSupport
|
||||
* @see org.springframework.data.gemfire.transaction.config.EnableGemfireCacheTransactions
|
||||
* @see org.springframework.test.context.ContextConfiguration
|
||||
* @see org.springframework.test.context.junit4.SpringRunner
|
||||
* @see org.springframework.transaction.annotation.Transactional
|
||||
* @since 2.5.0
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration(classes = ConcurrentClientDataAccessTransactionsWithRollbackIntegrationTests.TestGeodeClientConfiguration.class)
|
||||
@SuppressWarnings("unused")
|
||||
public class ConcurrentClientDataAccessTransactionsWithRollbackIntegrationTests
|
||||
extends ClientServerIntegrationTestsSupport {
|
||||
|
||||
private static ProcessWrapper geodeServer;
|
||||
|
||||
@BeforeClass
|
||||
public static void startGeodeServer() throws IOException {
|
||||
|
||||
int availablePort = findAvailablePort();
|
||||
|
||||
geodeServer = run(TestGeodeServerConfiguration.class,
|
||||
String.format("-D%s=%d", GEMFIRE_CACHE_SERVER_PORT_PROPERTY, availablePort));
|
||||
|
||||
waitForServerToStart("localhost", availablePort);
|
||||
|
||||
System.setProperty(GEMFIRE_POOL_SERVERS_PROPERTY, String.format("localhost[%d]", availablePort));
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void stopGeodeServer() {
|
||||
stop(geodeServer);
|
||||
System.clearProperty(GEMFIRE_POOL_SERVERS_PROPERTY);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Test
|
||||
public void concurrentTransactionalDataAccessOperations() throws Throwable {
|
||||
TestFramework.runOnce(new TwoConcurrentThreadsTransactionalDataAccessOperationsMultithreadedTestCase(this.userService));
|
||||
}
|
||||
|
||||
@ClientCacheApplication(name = "ConcurrentClientDataAccessTransactionsWithRollbackIntegrationTests")
|
||||
@EnableEntityDefinedRegions(basePackageClasses = User.class)
|
||||
@EnableGemfireRepositories(basePackageClasses = UserRepository.class)
|
||||
@EnableGemfireCacheTransactions
|
||||
@EnablePdx
|
||||
static class TestGeodeClientConfiguration {
|
||||
|
||||
@Bean
|
||||
GemfireRepositoryFactoryBean<UserRepository, User, Integer> userRepository(GemFireCache gemfireCache) {
|
||||
|
||||
GemfireRepositoryFactoryBean<UserRepository, User, Integer> userRepositoryFactoryBean =
|
||||
new GemfireRepositoryFactoryBean<>(UserRepository.class);
|
||||
|
||||
userRepositoryFactoryBean.setCache(gemfireCache);
|
||||
userRepositoryFactoryBean.setGemfireMappingContext(new GemfireMappingContext());
|
||||
|
||||
return userRepositoryFactoryBean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserService userService(UserRepository userRepository) {
|
||||
return new UserService(userRepository);
|
||||
}
|
||||
}
|
||||
|
||||
@CacheServerApplication(name = "ConcurrentClientTransactionsWithRollbackIntegrationTestsServer")
|
||||
@EnableEntityDefinedRegions(basePackageClasses = User.class, serverRegionShortcut = RegionShortcut.REPLICATE)
|
||||
static class TestGeodeServerConfiguration {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
AnnotationConfigApplicationContext applicationContext =
|
||||
new AnnotationConfigApplicationContext(TestGeodeServerConfiguration.class);
|
||||
|
||||
applicationContext.registerShutdownHook();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class TwoConcurrentThreadsTransactionalDataAccessOperationsMultithreadedTestCase
|
||||
extends MultithreadedTestCase {
|
||||
|
||||
@Getter
|
||||
private final UserService userService;
|
||||
|
||||
public TwoConcurrentThreadsTransactionalDataAccessOperationsMultithreadedTestCase(
|
||||
@NonNull UserService userService) {
|
||||
|
||||
Assert.notNull(userService, "UserService must not be null");
|
||||
|
||||
this.userService = userService;
|
||||
this.userService.setTestCase(this);
|
||||
}
|
||||
|
||||
private void assertUser(@NonNull User user, Integer expectedId, String expectedName) {
|
||||
|
||||
assertThat(user).isNotNull();
|
||||
assertThat(user.getId()).isEqualTo(expectedId);
|
||||
assertThat(user.getName()).isEqualTo(expectedName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
|
||||
super.initialize();
|
||||
|
||||
User jonDoe = User.as("jonDoe").identifiedBy(1);
|
||||
|
||||
getUserService().saveAndCommit(jonDoe);
|
||||
|
||||
assertThat(getUserService().exists(jonDoe)).isTrue();
|
||||
}
|
||||
|
||||
public void thread1() {
|
||||
|
||||
Thread.currentThread().setName("Data Access Thread One Running Rollback");
|
||||
|
||||
assertTick(0);
|
||||
|
||||
User jonDoe = getUserService().findById(1);
|
||||
|
||||
assertUser(jonDoe, 1, "jonDoe");
|
||||
|
||||
jonDoe.withName("sourDoe");
|
||||
|
||||
assertUser(jonDoe, 1, "sourDoe");
|
||||
|
||||
AtomicBoolean optimisticLockingFailureExceptionIsPresent = new AtomicBoolean(false);
|
||||
|
||||
try {
|
||||
getUserService().saveThrowRuntimeExceptionAndRollback(jonDoe);
|
||||
}
|
||||
catch (Throwable expected) {
|
||||
|
||||
Throwable cause = expected;
|
||||
|
||||
while (cause != null) {
|
||||
if (cause instanceof OptimisticLockingFailureException) {
|
||||
assertThat(cause).hasMessage("TEST");
|
||||
optimisticLockingFailureExceptionIsPresent.set(true);
|
||||
}
|
||||
else if (cause instanceof NoTransactionException) {
|
||||
fail(String.format("%s was incorrectly thrown", cause.getClass().getName()));
|
||||
}
|
||||
|
||||
cause = cause.getCause();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
assertThat(optimisticLockingFailureExceptionIsPresent).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
public void thread2() {
|
||||
|
||||
Thread.currentThread().setName("Data Access Thread Two Running Commit");
|
||||
|
||||
waitForTick(1);
|
||||
assertTick(1);
|
||||
|
||||
User jonDoe = getUserService().findById(1);
|
||||
|
||||
assertUser(jonDoe, 1, "jonDoe");
|
||||
|
||||
jonDoe.withName("pieDoe");
|
||||
|
||||
assertUser(jonDoe, 1, "pieDoe");
|
||||
|
||||
getUserService().saveAndCommit(jonDoe);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
|
||||
super.finish();
|
||||
|
||||
User pieDoe = getUserService().findById(1);
|
||||
|
||||
assertUser(pieDoe, 1, "pieDoe");
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
static class UserService {
|
||||
|
||||
@Setter
|
||||
private MultithreadedTestCase testCase;
|
||||
|
||||
@Getter
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserService(@NonNull UserRepository userRepository) {
|
||||
Assert.notNull(userRepository, "UserRepository must not be null");
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
protected MultithreadedTestCase getTestCase() {
|
||||
|
||||
Assert.state(this.testCase != null,
|
||||
"A reference to the TestCase was not configured correctly");
|
||||
|
||||
return this.testCase;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public boolean exists(@NonNull User user) {
|
||||
|
||||
return user != null
|
||||
&& user.getId() != null
|
||||
&& getUserRepository().existsById(user.getId());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public @NonNull User findById(@NonNull Integer id) {
|
||||
|
||||
return Optional.ofNullable(id)
|
||||
.flatMap(getUserRepository()::findById)
|
||||
.orElseThrow(() -> new EmptyResultDataAccessException(1));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public @NonNull User saveAndCommit(@NonNull User user) {
|
||||
return save(user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public @NonNull User saveThrowRuntimeExceptionAndRollback(@NonNull User user) {
|
||||
save(user);
|
||||
getTestCase().waitForTick(2);
|
||||
throw new OptimisticLockingFailureException("TEST");
|
||||
}
|
||||
|
||||
private @NonNull User save(@NonNull User user) {
|
||||
return getUserRepository().save(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user