From 039422246f93b21662ab306ec2fd8beee578a3fa Mon Sep 17 00:00:00 2001 From: Graham Pople Date: Fri, 27 May 2022 14:25:19 +0100 Subject: [PATCH 1/4] Move CouchbaseTransactionalOperator to use SLF4J, same as rest of the code. --- .../transaction/CouchbaseTransactionalOperator.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java index 10002be95..5f4ea4e98 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java @@ -6,6 +6,8 @@ import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; import com.couchbase.client.java.transactions.TransactionGetResult; import com.couchbase.client.java.transactions.TransactionResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.transaction.ReactiveTransaction; import org.springframework.transaction.TransactionException; @@ -38,10 +40,11 @@ * what it finds in the currentContext()? * */ +// todo gpx ongoing discussions on whether this can support retries & error handling natively public class CouchbaseTransactionalOperator implements TransactionalOperator { // package org.springframework.transaction.reactive; - private static final Log logger = LogFactory.getLog(CouchbaseTransactionalOperator.class); + private static final Logger logger = LoggerFactory.getLogger(CouchbaseTransactionalOperator.class); private final ReactiveTransactionManager transactionManager; private final TransactionDefinition transactionDefinition; From 131112523b21772748cc002fabcd287a2c2acc9d Mon Sep 17 00:00:00 2001 From: Graham Pople Date: Fri, 27 May 2022 14:25:55 +0100 Subject: [PATCH 2/4] Handle all propagation levels --- ...hbaseSimpleCallbackTransactionManager.java | 85 +++- ...nsactionalPropagationIntegrationTests.java | 399 ++++++++++++++++++ 2 files changed, 468 insertions(+), 16 deletions(-) create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java index 772ed2853..7d5eed74e 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java @@ -15,39 +15,29 @@ */ package org.springframework.data.couchbase.transaction; -import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; -import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; import com.couchbase.client.java.transactions.TransactionAttemptContext; import com.couchbase.client.java.transactions.TransactionResult; import com.couchbase.client.java.transactions.config.TransactionOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.lang.Nullable; import org.springframework.transaction.IllegalTransactionStateException; -import org.springframework.transaction.InvalidTimeoutException; -import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.reactive.TransactionContextManager; -import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionSynchronizationManager; -import org.springframework.util.Assert; -import reactor.core.publisher.Mono; -import java.lang.reflect.Field; import java.time.Duration; import java.util.concurrent.atomic.AtomicReference; public class CouchbaseSimpleCallbackTransactionManager implements CallbackPreferringPlatformTransactionManager { - private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionManager.class); + private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseSimpleCallbackTransactionManager.class); private final ReactiveCouchbaseClientFactory couchbaseClientFactory; private TransactionOptions options; @@ -59,10 +49,21 @@ public CouchbaseSimpleCallbackTransactionManager(ReactiveCouchbaseClientFactory @Override public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { - final AtomicReference execResult = new AtomicReference<>(); + boolean createNewTransaction = handlePropagation(definition); setOptionsFromDefinition(definition); + if (createNewTransaction) { + return executeNewTransaction(callback); + } + else { + return callback.doInTransaction(null); + } + } + + private T executeNewTransaction(TransactionCallback callback) { + final AtomicReference execResult = new AtomicReference<>(); + TransactionResult result = couchbaseClientFactory.getCluster().block().transactions().run(ctx -> { CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(null, true, false, false, true, null, null); @@ -70,8 +71,7 @@ public T execute(TransactionDefinition definition, TransactionCallback ca try { execResult.set(callback.doInTransaction(status)); - } - finally { + } finally { TransactionSynchronizationManager.clear(); } }, this.options); @@ -81,6 +81,61 @@ public T execute(TransactionDefinition definition, TransactionCallback ca return execResult.get(); } + // Propagation defines what happens when a @Transactional method is called from another @Transactional method. + private boolean handlePropagation(TransactionDefinition definition) { + boolean isExistingTransaction = TransactionSynchronizationManager.isActualTransactionActive(); + + LOGGER.trace("Deciding propagation behaviour from {} and {}", definition.getPropagationBehavior(), isExistingTransaction); + + switch (definition.getPropagationBehavior()) { + case TransactionDefinition.PROPAGATION_REQUIRED: + // Make a new transaction if required, else just execute the new method in the current transaction. + return !isExistingTransaction; + + case TransactionDefinition.PROPAGATION_SUPPORTS: + // Don't appear to have the ability to execute the callback non-transactionally in this layer. + throw new IllegalTransactionStateException( + "Propagation level 'support' has been specified which is not supported"); + + case TransactionDefinition.PROPAGATION_MANDATORY: + if (!isExistingTransaction) { + throw new IllegalTransactionStateException( + "Propagation level 'mandatory' is specified but not in an active transaction"); + } + return false; + + case TransactionDefinition.PROPAGATION_REQUIRES_NEW: + // This requires suspension of the active transaction. This will be possible to support in a future + // release, if required. + throw new IllegalTransactionStateException( + "Propagation level 'requires_new' has been specified which is not currently supported"); + + case TransactionDefinition.PROPAGATION_NOT_SUPPORTED: + // Don't appear to have the ability to execute the callback non-transactionally in this layer. + throw new IllegalTransactionStateException( + "Propagation level 'not_supported' has been specified which is not supported"); + + case TransactionDefinition.PROPAGATION_NEVER: + if (isExistingTransaction) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); + } + return true; + + case TransactionDefinition.PROPAGATION_NESTED: + if (isExistingTransaction) { + // Couchbase transactions cannot be nested. + throw new IllegalTransactionStateException( + "Propagation level 'nested' has been specified which is not supported"); + } + return true; + + default: + throw new IllegalTransactionStateException( + "Unknown propagation level " + definition.getPropagationBehavior() + " has been specified"); + } + } + /** * @param definition reflects the @Transactional options */ @@ -96,8 +151,6 @@ private void setOptionsFromDefinition(TransactionDefinition definition) { } // readonly is ignored as it is documented as being a hint that won't necessarily cause writes to fail - - // todo gpx what about propagation? } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java new file mode 100644 index 000000000..63cdc42a3 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java @@ -0,0 +1,399 @@ +/* + * Copyright 2012-2022 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.couchbase.transactions; + +import com.couchbase.client.java.transactions.error.TransactionFailedException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +// todo gpx test repository methods in @Transactional +// todo gpx test queries in @Transactional +// todo gpx chekc what happens when try to do reactive @Transcational (unsupported by CallbackPreferring) +// todo gpx handle synchronization + +/** + * Tests for the various propagation values allowed on @Transactional methods. + */ +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) +public class CouchbaseTransactionalPropagationIntegrationTests extends JavaIntegrationTests { + private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionalPropagationIntegrationTests.class); + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + PersonService personService; + @Autowired + CouchbaseTemplate operations; + static GenericApplicationContext context; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + context = new AnnotationConfigApplicationContext(Config.class, PersonService.class); + } + + @BeforeEach + public void beforeEachTest() { + personService = context.getBean(PersonService.class); + + Person walterWhite = new Person(1, "Walter", "White"); + try { + couchbaseClientFactory.getBucket().defaultCollection().remove(walterWhite.getId().toString()); + } catch (Exception ex) { + // System.err.println(ex); + } + } + + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + + @DisplayName("Call @Transactional(propagation = DEFAULT) - succeeds, creates a transaction") + @Test + public void callDefault() { + personService.propagationDefault(ops -> { + assertInTransaction(); + }); + } + + @DisplayName("Call @Transactional(propagation = SUPPORTS) - fails as unsupported") + @Test + public void callSupports() { + try { + personService.propagationSupports(ops -> { + }); + fail(); + } + catch (IllegalTransactionStateException ignored) { + } + } + + @DisplayName("Call @Transactional(propagation = MANDATORY) - fails as not in an active transaction") + @Test + public void callMandatory() { + try { + personService.propagationMandatory(ops -> { + }); + fail(); + } + catch (IllegalTransactionStateException ignored) { + } + } + + @DisplayName("Call @Transactional(propagation = REQUIRES_NEW) - fails as unsupported") + @Test + public void callRequiresNew() { + try { + personService.propagationRequiresNew(ops -> { + }); + fail(); + } + catch (IllegalTransactionStateException ignored) { + } + } + + @DisplayName("Call @Transactional(propagation = NOT_SUPPORTED) - fails as unsupported") + @Test + public void callNotSupported() { + try { + personService.propagationNotSupported(ops -> { + }); + fail(); + } + catch (IllegalTransactionStateException ignored) { + } + } + + @DisplayName("Call @Transactional(propagation = NEVER) - succeeds as not in a transaction, starts one") + @Test + public void callNever() { + personService.propagationNever(ops -> { + assertInTransaction(); + }); + } + + @DisplayName("Call @Transactional(propagation = NESTED) - succeeds as not in an existing transaction, starts one") + @Test + public void callNested() { + personService.propagationNested(ops -> { + assertInTransaction(); + }); + } + + // todo gp check retries + + @DisplayName("Call @Transactional that calls @Transactional(propagation = DEFAULT) - succeeds, continues existing") + @Test + public void callDefaultThatCallsDefault() { + UUID id1 = UUID.randomUUID(); + UUID id2 = UUID.randomUUID(); + + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + + personService.propagationDefault(ops2 -> { + ops2.insertById(Person.class).one(new Person(id2, "Grace", "Hopper")); + + assertInTransaction(); + }); + }); + + // Validate everyting committed + assertNotNull(operations.findById(Person.class).one(id1.toString())); + assertNotNull(operations.findById(Person.class).one(id2.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = REQUIRED) - succeeds, continues existing") + @Test + public void callDefaultThatCallsRequired() { + UUID id1 = UUID.randomUUID(); + UUID id2 = UUID.randomUUID(); + + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + + personService.propagationRequired(ops2 -> { + ops2.insertById(Person.class).one(new Person(id2, "Grace", "Hopper")); + + assertInTransaction(); + }); + }); + + // Validate everyting committed + assertNotNull(operations.findById(Person.class).one(id1.toString())); + assertNotNull(operations.findById(Person.class).one(id2.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = MANDATORY) - succeeds, continues existing") + @Test + public void callDefaultThatCallsMandatory() { + UUID id1 = UUID.randomUUID(); + UUID id2 = UUID.randomUUID(); + + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + + personService.propagationMandatory(ops2 -> { + ops2.insertById(Person.class).one(new Person(id2, "Grace", "Hopper")); + + assertInTransaction(); + }); + }); + + // Validate everyting committed + assertNotNull(operations.findById(Person.class).one(id1.toString())); + assertNotNull(operations.findById(Person.class).one(id2.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = REQUIRES_NEW) - fails as unsupported") + @Test + public void callDefaultThatCallsRequiresNew() { + UUID id1 = UUID.randomUUID(); + + try { + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + + personService.propagationRequiresNew(ops2 -> { + fail(); + }); + }); + fail(); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof IllegalTransactionStateException); + } + + // Validate everything rolled back + assertNull(operations.findById(Person.class).one(id1.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = NOT_SUPPORTED) - fails as unsupported") + @Test + public void callDefaultThatCallsNotSupported() { + UUID id1 = UUID.randomUUID(); + + try { + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + + personService.propagationNotSupported(ops2 -> { + fail(); + }); + }); + fail(); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof IllegalTransactionStateException); + } + + // Validate everything rolled back + assertNull(operations.findById(Person.class).one(id1.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = NEVER) - fails as in a transaction") + @Test + public void callDefaultThatCallsNever() { + UUID id1 = UUID.randomUUID(); + + try { + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + + personService.propagationNever(ops2 -> { + fail(); + }); + }); + fail(); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof IllegalTransactionStateException); + } + + // Validate everything rolled back + assertNull(operations.findById(Person.class).one(id1.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = NESTED) - fails as unsupported") + @Test + public void callDefaultThatCallsNested() { + UUID id1 = UUID.randomUUID(); + + try { + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + + personService.propagationNested(ops2 -> { + fail(); + }); + }); + fail(); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof IllegalTransactionStateException); + } + + // Validate everything rolled back + assertNull(operations.findById(Person.class).one(id1.toString())); + } + + void assertInTransaction() { + assertTrue(org.springframework.transaction.support.TransactionSynchronizationManager.isActualTransactionActive()); + } + + void assertNotInTransaction() { + try { + assertFalse(org.springframework.transaction.support.TransactionSynchronizationManager.isActualTransactionActive()); + } + catch (NoTransactionException ignored) { + } + } + + @Service + @Component + @EnableTransactionManagement + static + class PersonService { + final CouchbaseOperations ops; + + public PersonService(CouchbaseOperations ops) { + this.ops = ops; + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public void propagationDefault(@Nullable Consumer callback) { + LOGGER.info("propagationDefault"); + if (callback != null) callback.accept(ops); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER, propagation = Propagation.REQUIRED) + public void propagationRequired(@Nullable Consumer callback) { + LOGGER.info("propagationRequired"); + if (callback != null) callback.accept(ops); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER, propagation = Propagation.MANDATORY) + public void propagationMandatory(@Nullable Consumer callback) { + LOGGER.info("propagationMandatory"); + if (callback != null) callback.accept(ops); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER, propagation = Propagation.NESTED) + public void propagationNested(@Nullable Consumer callback) { + LOGGER.info("propagationNever"); + if (callback != null) callback.accept(ops); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER, propagation = Propagation.SUPPORTS) + public void propagationSupports(@Nullable Consumer callback) { + LOGGER.info("propagationSupports"); + if (callback != null) callback.accept(ops); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER, propagation = Propagation.NOT_SUPPORTED) + public void propagationNotSupported(@Nullable Consumer callback) { + LOGGER.info("propagationNotSupported"); + if (callback != null) callback.accept(ops); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER, propagation = Propagation.REQUIRES_NEW) + public void propagationRequiresNew(@Nullable Consumer callback) { + LOGGER.info("propagationRequiresNew"); + if (callback != null) callback.accept(ops); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER, propagation = Propagation.NEVER) + public void propagationNever(@Nullable Consumer callback) { + LOGGER.info("propagationNever"); + if (callback != null) callback.accept(ops); + } + } +} From 56201d15a0518eabd4bddbac99e0670e7258cec5 Mon Sep 17 00:00:00 2001 From: Graham Pople Date: Fri, 27 May 2022 17:44:01 +0100 Subject: [PATCH 3/4] Adding new tests for repository calls inside @Transactional One test is failure due to what looks like a bug elsewhere. --- .../data/couchbase/domain/UserRepository.java | 3 + ...ansactionalRepositoryIntegrationTests.java | 156 ++++++++++++++++++ .../util/TransactionTestUtil.java | 41 +++++ 3 files changed, 200 insertions(+) create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java index 31b5eab3c..09398efaa 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java @@ -67,4 +67,7 @@ default List getByFirstname(String firstname) { } catch (InterruptedException ie) {} return findByFirstname(firstname); } + + @Override + User save(User user); } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java new file mode 100644 index 000000000..8de0f821f --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2012-2022 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.couchbase.transactions; + +import com.couchbase.client.java.transactions.error.TransactionFailedException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserRepository; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertInTransaction; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; + +/** + * Tests @Transactional with repository methods. + */ +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) +public class CouchbaseTransactionalRepositoryIntegrationTests extends JavaIntegrationTests { + @Autowired UserRepository userRepo; + @Autowired CouchbaseClientFactory couchbaseClientFactory; + PersonService personService; + @Autowired + CouchbaseTemplate operations; + static GenericApplicationContext context; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + context = new AnnotationConfigApplicationContext(Config.class, PersonService.class); + } + + @BeforeEach + public void beforeEachTest() { + personService = context.getBean(PersonService.class); + } + + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + + + @Test + public void findByFirstname() { + operations.insertById(User.class).one(new User(UUID.randomUUID().toString(), "Ada", "Lovelace")); + + List users = personService.findByFirstname("Ada"); + + assertNotEquals(0, users.size()); + } + + @Test + public void save() { + String id = UUID.randomUUID().toString(); + + personService.run(repo -> { + assertInTransaction(); + + repo.save(new User(id, "Ada", "Lovelace")); + + assertInTransaction(); + + // read your own write + // todo gpx this is failing because it's being executed non-transactionally, due to a bug somewhere + User user = operations.findById(User.class).one(id); + assertNotNull(user); + + assertInTransaction(); + + }); + + User user = operations.findById(User.class).one(id); + assertNotNull(user); + } + + @DisplayName("Test that repo.save() is actually performed transactionally, by forcing a rollback") + @Test + public void saveRolledBack() { + String id = UUID.randomUUID().toString(); + + try { + personService.run(repo -> { + repo.save(new User(id, "Ada", "Lovelace")); + SimulateFailureException.throwEx("fail"); + }); + fail(); + } + catch (TransactionFailedException ignored) { + } + + User user = operations.findById(User.class).one(id); + assertNull(user); + } + + + @Service + @Component + @EnableTransactionManagement + static + class PersonService { + @Autowired UserRepository userRepo; + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public void run(Consumer callback) { + callback.accept(userRepo); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public List findByFirstname(String name) { + return userRepo.findByFirstname(name); + } + + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java b/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java new file mode 100644 index 000000000..e3bfb4c18 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2022 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.couchbase.transactions.util; + +import org.springframework.transaction.NoTransactionException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Utility methods for transaction tests. + */ +public class TransactionTestUtil { + private TransactionTestUtil() {} + + public static void assertInTransaction() { + assertTrue(org.springframework.transaction.support.TransactionSynchronizationManager.isActualTransactionActive()); + } + + public static void assertNotInTransaction() { + try { + assertFalse(org.springframework.transaction.support.TransactionSynchronizationManager.isActualTransactionActive()); + } + catch (NoTransactionException ignored) { + } + } + +} From 673a00d943a6b0e6d4e4b86b104a7f8b490a9790 Mon Sep 17 00:00:00 2001 From: Graham Pople Date: Fri, 27 May 2022 17:45:06 +0100 Subject: [PATCH 4/4] Rename CouchbaseTransactionalIntegrationTests, and check after each test that we're not in a transaction. --- ...chbaseTransactionalTemplateIntegrationTests.java} | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) rename src/test/java/org/springframework/data/couchbase/transactions/{CouchbaseTransactionalIntegrationTests.java => CouchbaseTransactionalTemplateIntegrationTests.java} (98%) diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java similarity index 98% rename from src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalIntegrationTests.java rename to src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java index cee3a1fb0..62e1272e3 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java @@ -18,6 +18,7 @@ import com.couchbase.client.java.transactions.error.TransactionFailedException; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -57,13 +58,14 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; /** - * Tests for @Transactional. + * Tests for @Transactional, using template methods (findById etc.) */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(Config.class) -public class CouchbaseTransactionalIntegrationTests extends JavaIntegrationTests { +public class CouchbaseTransactionalTemplateIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; /* DO NOT @Autowired - it will result in no @Transactional annotation behavior */ PersonService personService; @@ -97,6 +99,11 @@ public void beforeEachTest() { } } + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + @DisplayName("A basic golden path insert should succeed") @Test public void committedInsert() { @@ -268,7 +275,6 @@ public void concurrentTxns() { // todo gpx investigate how @Transactional @Rollback/@Commit interacts with us // todo gpx how to provide per-transaction options? - // todo gpx verify we aren't in a transactional context after the transaction ends (success or failure) @Disabled("taking too long - must fix") @DisplayName("Create a Person outside a @Transactional block, modify it, and then replace that person in the @Transactional. The transaction will retry until timeout.")